From 89c6a151ae988ef3b3a76570b9da97dd45eb7d69 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:53:05 +0200 Subject: [PATCH] fix: one page grid, shorter stacked rows Search box, column and table of contents are placed from one grid width on every page. Long code in table cells breaks to fit, stacked rows drop to one line per cell, and the button, search marker, choice list and crumb spacing are aligned with the rest. --- tests/test_site_r2_css.py | 9 ++- tests/test_site_r4_css.py | 9 +-- tests/test_site_r5_css.py | 113 +++++++++++++++++++++++++++++++ website/src/styles/retrobios.css | 84 +++++++++++++++++++---- 4 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 tests/test_site_r5_css.py 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;