diff --git a/tests/test_site_r2_css.py b/tests/test_site_r2_css.py index 928fca1d..7f54b927 100644 --- a/tests/test_site_r2_css.py +++ b/tests/test_site_r2_css.py @@ -166,15 +166,18 @@ class ThemeStylesheet(unittest.TestCase): # 1.11 def test_page_without_toc_keeps_the_column(self): self.assertIn("--__toc-width: initial", _rules(self.css, ":root[data-has-sidebar]:not([data-has-toc])")[0]) - pane = _rules(self.css, ":not([data-has-toc]) .main-pane")[0] - self.assertIn("--sl-content-margin-inline: auto 0", pane) + # Round 5: one pane rule for every page with a sidebar, "On this page" + # or not, placed from the grid width. + pane = _rules(self.css, ":root[data-has-sidebar] .main-pane")[0] + self.assertIn("--sl-content-margin-inline: max(0rem, 100% - var(--rb-grid-width)) auto", pane) # 1.12, 1.14 def test_card_pages(self): cards = next(b for s, b in _blocks(self.css) if s == ".sl-markdown-content :is(.rb-sys-file, .rb-file-entry)") self.assertIn("content-visibility: auto", cards) self.assertIn("contain-intrinsic-size: auto 320px", cards) - self.assertIn("--sl-content-width: 54rem", _rules(self.css, "body:has(.rb-sys-file, .rb-file-entry)")[0]) + self.assertIn("--sl-content-width: var(--rb-grid-width)", _rules(self.css, "body:has(.rb-sys-file, .rb-file-entry)")[0]) + self.assertEqual(_theme_tokens(self.css, ":root")["--rb-grid-width"], "54rem") facts = _rules(self.css, "dl.rb-facts")[0] self.assertIn("grid-template-columns: 11rem minmax(0, 1fr)", facts) self.assertRegex(self.css, r"@media \(max-width: 30rem\) \{\s*\.sl-markdown-content dl\.rb-facts \{ grid-template-columns: minmax\(0, 1fr\)") diff --git a/tests/test_site_r4_css.py b/tests/test_site_r4_css.py index 9c7b7527..4b7a44a2 100644 --- a/tests/test_site_r4_css.py +++ b/tests/test_site_r4_css.py @@ -39,16 +39,17 @@ class ThemeStylesheetRound4(unittest.TestCase): # 1.2 def test_stacked_cells_close_up_and_labels_push_rows(self): cell = _block(self.css, STACK) - for declaration in ("margin-top: 0", "display: flow-root", "line-height: 1.5", - "padding: 0.125rem 0.6rem 0.125rem 8.4rem"): + # Round 5 tightened the values: 7.1rem column, 1.35 lines (test_site_r5_css). + for declaration in ("margin-top: 0", "display: flow-root", "line-height: 1.35", + "padding: 0 0.6rem 0 7.1rem"): with self.subTest(declaration=declaration): self.assertIn(declaration, cell) label = _block(self.css, STACK + "::before") self.assertIn("position: static", label) self.assertIn("float: left", label) self.assertNotIn("position: absolute", label) - # The label sits in the cell's left padding: 8.4 - 0.6 rem. - self.assertIn("margin-left: -7.8rem", label) + # The label sits in the cell's left padding. + self.assertIn("margin: 0 0.6rem 0 -7.1rem", label) # 1.3 def test_short_inline_code_never_splits(self): diff --git a/tests/test_site_r5_css.py b/tests/test_site_r5_css.py new file mode 100644 index 00000000..12f8feb0 --- /dev/null +++ b/tests/test_site_r5_css.py @@ -0,0 +1,113 @@ +"""Round 5 theme: the stylesheet fixes of package 1, read from its source. + +Each test fails on the stylesheet as it was before the fix: detection tables +whose last column sat past the page at 1440 px, a header search box, title +and "On this page" that moved 12 px between a guide and a console page, +stacked rows 136 px tall for four short cells, a line 4 px under a button, +a heading touching its first line, the browser's own choice list, and a +crumb separator free to end a line alone. +""" +from __future__ import annotations + +import re +import unittest + +from tests.test_site_r2_css import CSS_PATH, _blocks, _strip_comments, _theme_tokens + +STACK = ".sl-markdown-content table[data-rb-stack] td" + + +def _block(css: str, selector: str) -> str: + bodies = [b for s, b in _blocks(css) if s == selector] + if not bodies: + raise AssertionError(f"no block {selector}") + return "".join(bodies) + + +def _declarations(body: str) -> dict[str, str]: + return {k.strip(): v.strip() for k, v in re.findall(r"([\w-]+)\s*:\s*([^;]+);", body)} + + +class ThemeStylesheetRound5(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.css = _strip_comments(CSS_PATH.read_text(encoding="utf-8")) + + # 1.1 + def test_long_code_in_a_cell_breaks_to_fit_its_column(self): + self.assertIn("overflow-wrap: anywhere", + _block(self.css, ".sl-markdown-content table td code:not(.rb-short)")) + last = _block(self.css, ".sl-markdown-content table:not([data-rb-stack='kv']) " + "td:last-child:not(:first-child):has(code:not(.rb-short))") + self.assertIn("min-width: 9rem", last) + # A flag or a short name in a cell still never splits. + self.assertIn("white-space: pre", _block(self.css, ".sl-markdown-content :not(pre) > code.rb-short")) + + # 1.2 + def test_one_grid_for_every_page(self): + self.assertEqual(_theme_tokens(self.css, ":root").get("--rb-grid-width"), "54rem") + self.assertIn("--sl-content-width: var(--rb-grid-width)", + _block(self.css, "body:has(.rb-sys-file, .rb-file-entry)")) + # The header places its search box from the grid, not the page's column. + self.assertIn("--sl-content-width: var(--rb-grid-width)", + _block(self.css, ":root[data-has-sidebar] header.header")) + toc = _block(self.css, ":root[data-has-sidebar] .right-sidebar-container") + pane = _block(self.css, ":root[data-has-sidebar] .main-pane") + for body in (toc, pane): + self.assertIn("var(--rb-grid-width)", body) + self.assertNotIn("var(--sl-content-width)", body) + # Every column starts on the grid's edge, the narrow one included. + self.assertIn("--sl-content-margin-inline: max(0rem, 100% - var(--rb-grid-width)) auto", pane) + # The pane rule covers pages with and without "On this page" alike. + self.assertFalse([s for s, _ in _blocks(self.css) if s.endswith(":not([data-has-toc]) .main-pane")]) + + # 1.3 + def test_stacked_rows_are_short(self): + cell = _declarations(_block(self.css, STACK)) + self.assertEqual(cell["font-size"], "0.875rem") + self.assertEqual(cell["line-height"], "1.35") + label = _declarations(_block(self.css, STACK + "::before")) + self.assertEqual(label["width"], "max-content") + self.assertEqual(label["min-width"], "6.5rem") + self.assertEqual(label["max-width"], "9rem") + self.assertEqual(label["font-size"], "0.8125rem") + # Values start in the column of the shortest label: the label's + # negative margin is the cell's left padding. + indent = cell["padding"].split()[3] + self.assertEqual(label["margin"].split()[3], f"-{indent}") + self.assertEqual(indent, "7.1rem") + self.assertIn("padding: 0.3rem 0", _block(self.css, ".sl-markdown-content table[data-rb-stack] tr")) + # A two-column list keeps body text. + self.assertIn("font-size: 1rem", _block(self.css, ".sl-markdown-content table[data-rb-stack='kv'] td")) + + # 1.5 + def test_spacing_after_a_button_and_a_search_marker(self): + self.assertIn("margin-top: 1rem", + _block(self.css, ".sl-markdown-content :is(.rb-button, p:has(> .rb-button)) + p")) + self.assertIn("margin-top: var(--sl-content-gap-y)", + _block(self.css, ".sl-markdown-content span[data-pagefind-meta]:empty + :is(p, ul, ol, table, div)")) + + def test_choice_list_matches_the_filter_box(self): + select = _declarations(_block(self.css, ".sl-markdown-content :is(.rb-file-lookup, .rb-picker) select,\n" + ".sl-markdown-content select")) + box = _declarations(_block(self.css, ".rb-table-filter input")) + for prop in ("border", "border-radius", "min-height"): + with self.subTest(prop=prop): + self.assertEqual(select[prop], box[prop]) + self.assertEqual(select["appearance"], "none") + self.assertIn("var(--rb-select-arrow)", select["background"]) + dark = _theme_tokens(self.css, ":root")["--rb-select-arrow"] + light = _theme_tokens(self.css, ":root[data-theme='light']")["--rb-select-arrow"] + # The arrow is drawn in each theme's secondary text colour. + for arrow, tokens in ((dark, _theme_tokens(self.css, ":root")), + (light, _theme_tokens(self.css, ":root[data-theme='light']"))): + colour = tokens["--rb-text-secondary"].lstrip("#") + self.assertIn(f"%23{colour}", arrow) + + def test_crumb_separator_stays_with_its_label(self): + self.assertIn("white-space: nowrap", _block(self.css, ".rb-breadcrumbs li")) + self.assertIn("white-space: normal", _block(self.css, ".rb-breadcrumbs li > :is(a, span)")) + + +if __name__ == "__main__": + unittest.main() diff --git a/website/src/styles/retrobios.css b/website/src/styles/retrobios.css index bbbf8eb2..763427cf 100644 --- a/website/src/styles/retrobios.css +++ b/website/src/styles/retrobios.css @@ -31,6 +31,8 @@ --sl-text-h2: 1.5rem; --sl-text-h3: 1.2rem; --sl-text-h4: 1rem; + /* The width the page grid is placed from: the file card column. */ + --rb-grid-width: 54rem; --rb-link: #aebcff; --rb-link-strong: #c9d2ff; @@ -59,6 +61,7 @@ --rb-diagram-fill: #252838; --rb-diagram-stroke: #7c82a3; --rb-diagram-accent: #aebcff; + --rb-select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%23a6adc8' stroke-width='1.6'/%3E%3C/svg%3E"); } :root[data-theme='light'] { @@ -92,6 +95,7 @@ --rb-diagram-fill: #f5f6fa; --rb-diagram-stroke: #8c92ac; --rb-diagram-accent: #3b3f7a; + --rb-select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%23546e7a' stroke-width='1.6'/%3E%3C/svg%3E"); } /* Links and focus */ @@ -130,6 +134,10 @@ font-size: var(--sl-text-sm); color: var(--rb-text-secondary); } +/* The separator stays with the crumb after it: a long label wraps inside + its own box, never leaving the "/" alone at a line end. */ +.rb-breadcrumbs li { white-space: nowrap; } +.rb-breadcrumbs li > :is(a, span) { white-space: normal; } .rb-breadcrumbs li + li::before { content: '/'; padding: 0 0.45rem; color: var(--rb-border-strong); } .rb-breadcrumbs a { display: inline-block; @@ -179,6 +187,12 @@ border-color: var(--rb-link-strong); } .sl-markdown-content table .rb-button { font-size: 0.8rem; padding: 0.4rem 0.8rem; } +/* Starlight spaces blocks only after a block: the line under a button sat + 4 px below it. */ +.sl-markdown-content :is(.rb-button, p:has(> .rb-button)) + p { margin-top: 1rem; } +/* A search keyword marker between two blocks is inline and empty: the block + after it kept no gap ("iPhone and iPad" touched its first line). */ +.sl-markdown-content span[data-pagefind-meta]:empty + :is(p, ul, ol, table, div) { margin-top: var(--sl-content-gap-y); } /* Badges */ .rb-badge { @@ -333,6 +347,14 @@ .sl-markdown-content table { font-variant-numeric: tabular-nums; } .sl-markdown-content table th { font-weight: 600; } .sl-markdown-content table[role='region']:focus-visible { outline: 2px solid var(--rb-link); outline-offset: 2px; } +/* A long path in a cell breaks to fit its column, at a separator where it + can: the installer's detection tables and Cemu's file list pushed their + last column past the page at 1440 px. A last column of such paths keeps + room to read. */ +.sl-markdown-content table td code:not(.rb-short) { overflow-wrap: anywhere; } +@media (min-width: 50rem) { + .sl-markdown-content table:not([data-rb-stack='kv']) td:last-child:not(:first-child):has(code:not(.rb-short)) { min-width: 9rem; } +} /* A hash in a table cell is its first 12 characters, the full value in the title: one line, dotted to say there is more on hover. */ .sl-markdown-content td code[title] { @@ -417,7 +439,7 @@ /* File cards on system and emulator pages. A page holds hundreds: those off screen are laid out when they come near, and the column is wide enough for a sha256 on one line. */ -body:has(.rb-sys-file, .rb-file-entry) { --sl-content-width: 54rem; } +body:has(.rb-sys-file, .rb-file-entry) { --sl-content-width: var(--rb-grid-width); } .sl-markdown-content :is(.rb-sys-file, .rb-file-entry) { border-top: 1px solid var(--rb-border); padding: 0.75rem 0 0.25rem; @@ -603,6 +625,21 @@ abbr.rb-term[data-open]::after { font: inherit; } .sl-markdown-content .rb-file-lookup input { flex: 1 1 20rem; font-family: var(--sl-font-mono); } +/* A choice list reads as the filter boxes do: their border, radius and + padding, and an arrow in the quiet text colour instead of the browser's. */ +.sl-markdown-content :is(.rb-file-lookup, .rb-picker) select, +.sl-markdown-content select { + appearance: none; + min-width: min(100%, 16rem); + min-height: 44px; + padding: 0.55rem 2.25rem 0.55rem 0.75rem; + border: 1px solid var(--rb-text-secondary); + border-radius: 2px; + background: var(--sl-color-bg) var(--rb-select-arrow) no-repeat right 0.75rem center / 0.75rem; + color: var(--sl-color-text); + font: inherit; + cursor: pointer; +} /* Both lookup labels, the hash and the file chooser, share one weight. */ .sl-markdown-content .rb-file-lookup label { font-weight: 600; } .sl-markdown-content :is(.rb-file-lookup, .rb-picker) button { @@ -699,15 +736,29 @@ site-search .pagefind-ui__result-tags { display: none; } starlight-tabs [role='tablist'] { flex-wrap: wrap; overflow-x: visible; row-gap: 0.25rem; } starlight-tabs [role='tab'] { white-space: nowrap; } -/* A page without "On this page" keeps the column, and the search box above - it, where a page with one has them: they no longer jump between pages. */ +/* One grid on every page. Starlight places the search box, the column and + "On this page" from --sl-content-width, which the file card pages widen: + crossing from a guide to a console moved all three by 12 px. They are + placed from one width instead, the widest column's. Every column starts + on the same edge, and a narrower one leaves the room on its right. + A page without "On this page" keeps the same places. */ :root[data-has-sidebar]:not([data-has-toc]) { --__toc-width: initial; } +@media (min-width: 50rem) { + :root[data-has-sidebar] header.header { --sl-content-width: var(--rb-grid-width); } +} @media (min-width: 72rem) { - :root[data-has-sidebar]:not([data-has-toc]) .main-pane { - --sl-content-margin-inline: auto 0; + :root[data-has-sidebar] .right-sidebar-container { + width: max( + var(--sl-sidebar-width), + calc(var(--sl-sidebar-width) + (100% - var(--rb-grid-width) - var(--sl-sidebar-width)) / 2) + ); + } + :root[data-has-sidebar] .main-pane { + --sl-content-margin-inline: max(0rem, 100% - var(--rb-grid-width)) auto; + order: 1; width: min( calc(100% - var(--sl-sidebar-width)), - calc(var(--sl-content-width) + (100% - var(--sl-content-width) - var(--sl-sidebar-width)) / 2) + calc(var(--rb-grid-width) + (100% - var(--rb-grid-width) - var(--sl-sidebar-width)) / 2) ); } } @@ -727,16 +778,20 @@ starlight-tabs [role='tab'] { white-space: nowrap; } clip: rect(0, 0, 0, 0); } .sl-markdown-content table[data-rb-stack] :is(tbody, tr) { display: block; } - .sl-markdown-content table[data-rb-stack] tr { padding: 0.5rem 0; border-top: 1px solid var(--rb-border); } + .sl-markdown-content table[data-rb-stack] tr { padding: 0.3rem 0; border-top: 1px solid var(--rb-border); } /* A cell holds its label as a float in its left padding: a label on two lines pushes the row down instead of running into the next one. No - sibling margin between cells. */ + sibling margin between cells. Values start 7.1rem in, the column of + the shortest labels. A longer label takes what it needs up to 9rem and + its value starts after it on that line only. One line per cell at + 0.875rem: a row of four short cells is 87 px, not 136. */ .sl-markdown-content table[data-rb-stack] td { display: flow-root; margin-top: 0; - padding: 0.125rem 0.6rem 0.125rem 8.4rem; + padding: 0 0.6rem 0 7.1rem; border: 0; - line-height: 1.5; + font-size: 0.875rem; + line-height: 1.35; text-align: left !important; overflow-wrap: anywhere; } @@ -744,15 +799,18 @@ starlight-tabs [role='tab'] { white-space: nowrap; } content: attr(data-label); position: static; float: left; - width: 7.4rem; - margin-left: -7.8rem; + width: max-content; + min-width: 6.5rem; + max-width: 9rem; + margin: 0 0.6rem 0 -7.1rem; + font-size: 0.8125rem; font-weight: 600; color: var(--rb-text-secondary); } .sl-markdown-content table[data-rb-stack] td:empty { display: none; } /* A two-column table of labels and values: the label above, the value on the full width under it. */ - .sl-markdown-content table[data-rb-stack='kv'] td { padding: 0 0.6rem; } + .sl-markdown-content table[data-rb-stack='kv'] td { padding: 0 0.6rem; font-size: 1rem; line-height: 1.5; } .sl-markdown-content table[data-rb-stack='kv'] td::before { content: none; } .sl-markdown-content table[data-rb-stack='kv'] td:first-child { padding-bottom: 0.15rem;