From 8e1b49bc0fca78c093f33ce8c5d8f99ce2585323 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:39:42 +0200 Subject: [PATCH 1/2] fix: wrap os tabs, close up stacked table rows Every OS tab stays on screen at 320 px, stacked cells drop the 16 px sibling margin and push their row down when a label wraps, short inline code keeps whole, lone card links get a 24 px target and the card label column fits its longest label. --- tests/test_site_r2_css.py | 8 +-- tests/test_site_r3_css.py | 12 +---- tests/test_site_r4_css.py | 84 ++++++++++++++++++++++++++++++++ website/src/styles/retrobios.css | 44 ++++++++++++----- 4 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 tests/test_site_r4_css.py diff --git a/tests/test_site_r2_css.py b/tests/test_site_r2_css.py index b2a968a7..928fca1d 100644 --- a/tests/test_site_r2_css.py +++ b/tests/test_site_r2_css.py @@ -156,11 +156,11 @@ class ThemeStylesheet(unittest.TestCase): permalink = next(b for s, b in _blocks(self.css) if s == ".rb-permalink") self.assertIn("min-height: 24px", permalink) - # 1.10, then round 3 item 1.3: one row that scrolls instead of two rows + # 1.10, then round 4 item 1.1: the row wraps, every tab on screen def test_os_tabs_stay_reachable(self): tablist = _rules(self.css, "starlight-tabs [role='tablist']")[0] - self.assertIn("flex-wrap: nowrap", tablist) - self.assertIn("overflow-x: auto", tablist) + self.assertIn("flex-wrap: wrap", tablist) + self.assertIn("overflow-x: visible", tablist) self.assertIn("white-space: nowrap", _rules(self.css, "starlight-tabs [role='tab']")[0]) # 1.11 @@ -176,7 +176,7 @@ class ThemeStylesheet(unittest.TestCase): 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]) facts = _rules(self.css, "dl.rb-facts")[0] - self.assertIn("grid-template-columns: 9rem minmax(0, 1fr)", facts) + 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\)") # 1.13 diff --git a/tests/test_site_r3_css.py b/tests/test_site_r3_css.py index b78dde38..6607230b 100644 --- a/tests/test_site_r3_css.py +++ b/tests/test_site_r3_css.py @@ -2,7 +2,7 @@ Each test fails on the stylesheet as it was before the fix: commands allowed to break inside any word on a phone, a sidebar whose top links outsize its -group labels, OS tabs on two rows, badge links cut to a few pixels by the +group labels, badge links cut to a few pixels by the next row, 12 px hashes, Get your files tables each sizing their own columns. """ from __future__ import annotations @@ -44,18 +44,10 @@ class ThemeStylesheetRound3(unittest.TestCase): self.assertIn("font-size: var(--sl-text-sm)", links) self.assertIn("font-weight: 400", links) - # 1.3 - def test_os_tabs_on_one_row(self): - tablist = next(b for s, b in _blocks(self.css) if s == "starlight-tabs [role='tablist']") - self.assertIn("flex-wrap: nowrap", tablist) - self.assertIn("overflow-x: auto", tablist) - self.assertIn("scrollbar-width: none", tablist) - self.assertIn("display: none", _rules(self.css, "starlight-tabs [role='tablist']::-webkit-scrollbar")[0]) - # 1.4 def test_badge_links_keep_a_24px_target(self): dd = next(b for s, b in _blocks(self.css) if s == ".sl-markdown-content .rb-facts dd") - self.assertIn("line-height: 1.8", dd) + self.assertIn("line-height: 24px", dd) badge = _rules(self.css, ".rb-facts :is(.rb-badge:has(> a), a.rb-badge)")[0] for declaration in ("display: inline-flex", "align-items: center", "min-height: 24px", "margin-block: 2px"): with self.subTest(declaration=declaration): diff --git a/tests/test_site_r4_css.py b/tests/test_site_r4_css.py new file mode 100644 index 00000000..9c7b7527 --- /dev/null +++ b/tests/test_site_r4_css.py @@ -0,0 +1,84 @@ +"""Round 4 theme: the stylesheet fixes of package 1, read from its source. + +Each test fails on the stylesheet as it was before the fix: OS tabs on one +hidden scrolling row, stacked table cells 16 px apart with labels drawn over +the next row, short flags split across lines, lone card links 19 px high, a +card label column too narrow for its longest label. +""" +from __future__ import annotations + +import unittest + +from tests.test_site_r2_css import CSS_PATH, _blocks, _rules, _strip_comments + +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) + + +class ThemeStylesheetRound4(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.raw = CSS_PATH.read_text(encoding="utf-8") + cls.css = _strip_comments(cls.raw) + + # 1.1 + def test_os_tabs_wrap_and_never_hide(self): + tablist = _block(self.css, "starlight-tabs [role='tablist']") + for declaration in ("flex-wrap: wrap", "overflow-x: visible", "row-gap: 0.25rem"): + with self.subTest(declaration=declaration): + self.assertIn(declaration, tablist) + self.assertNotIn("scrollbar-width: none", self.css) + self.assertNotIn("::-webkit-scrollbar", self.css) + + # 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"): + 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) + + # 1.3 + def test_short_inline_code_never_splits(self): + self.assertIn("white-space: pre", _block(self.css, ".sl-markdown-content :not(pre) > code.rb-short")) + self.assertIn("white-space: break-spaces", _block(self.css, ".sl-markdown-content :not(pre) > code")) + self.assertIn("white-space: nowrap", _block(self.css, ".sl-markdown-content table.rb-options td:first-child")) + # A stacked value column is narrow: short code there moves down whole + # and splits only when wider than the column, never pushing the page. + stacked = _block(self.css, STACK + " code.rb-short") + for declaration in ("display: inline-block", "max-width: 100%", "overflow-wrap: anywhere"): + with self.subTest(declaration=declaration): + self.assertIn(declaration, stacked) + # Commands in code blocks keep wrapping on a phone. + self.assertIn("white-space: pre-wrap", _block(self.css, ".sl-markdown-content .expressive-code pre > code")) + + # 1.4 + def test_lone_card_links_are_24px_targets(self): + dd = _block(self.css, ".sl-markdown-content .rb-facts dd") + self.assertIn("min-height: 24px", dd) + self.assertIn("line-height: 24px", dd) + link = _block(self.css, ".sl-markdown-content .rb-facts dd > a") + self.assertIn("display: inline-block", link) + self.assertIn("min-height: 24px", link) + + # 1.5 + def test_card_labels_fit_and_lookup_labels_match(self): + facts = _rules(self.css, "dl.rb-facts")[0] + self.assertIn("grid-template-columns: 11rem minmax(0, 1fr)", facts) + self.assertIn("font-weight: 600", _block(self.css, ".sl-markdown-content .rb-file-lookup label")) + + +if __name__ == "__main__": + unittest.main() diff --git a/website/src/styles/retrobios.css b/website/src/styles/retrobios.css index 45c6637e..bbbf8eb2 100644 --- a/website/src/styles/retrobios.css +++ b/website/src/styles/retrobios.css @@ -467,14 +467,16 @@ body:has(.rb-sys-file, .rb-file-entry) { --sl-content-width: 54rem; } /* The facts of a file card: label column, value column, one column on a phone. */ .sl-markdown-content dl.rb-facts { display: grid; - grid-template-columns: 9rem minmax(0, 1fr); + grid-template-columns: 11rem minmax(0, 1fr); gap: 0.2rem 1rem; margin: 0.4rem 0 0; font-size: 0.85rem; } .sl-markdown-content .rb-facts :is(dt, dd) { margin: 0; padding: 0; } .sl-markdown-content .rb-facts dt { font-size: 0.8rem; font-weight: 400; color: var(--rb-text-secondary); } -.sl-markdown-content .rb-facts dd { min-width: 0; overflow-wrap: break-word; line-height: 1.8; } +.sl-markdown-content .rb-facts dd { min-width: 0; min-height: 24px; overflow-wrap: break-word; line-height: 24px; } +/* A lone link in a card value is a 24 px target too (WCAG 2.5.8). */ +.sl-markdown-content .rb-facts dd > a { display: inline-block; min-height: 24px; } /* A badge link in a card keeps a 24 px target, rows 4 px apart (WCAG 2.5.8). */ .sl-markdown-content .rb-facts :is(.rb-badge:has(> a), a.rb-badge) { display: inline-flex; @@ -577,6 +579,12 @@ abbr.rb-term[data-open]::after { white-space: normal; } +/* Inline code: a flag or a name of 30 characters or fewer (marked rb-short + at build time) never splits, longer code wraps at its spaces and keeps them. */ +.sl-markdown-content :not(pre) > code { white-space: break-spaces; } +.sl-markdown-content :not(pre) > code.rb-short { white-space: pre; } +.sl-markdown-content table.rb-options td:first-child { white-space: nowrap; } + /* Forms: lookup, picker, table filters */ .rb-file-lookup, .rb-picker fieldset { @@ -595,6 +603,8 @@ 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); } +/* 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 { background: var(--rb-link); color: var(--sl-color-black); @@ -685,9 +695,8 @@ site-search .pagefind-ui__result-tags { display: none; } font-weight: 600; } -/* OS tabs stay on one row and scroll sideways on a narrow phone. */ -starlight-tabs [role='tablist'] { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; } -starlight-tabs [role='tablist']::-webkit-scrollbar { display: none; } +/* OS tabs wrap to a second row on a narrow phone: every tab stays on screen. */ +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 @@ -719,21 +728,24 @@ starlight-tabs [role='tab'] { white-space: nowrap; } } .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); } + /* 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. */ .sl-markdown-content table[data-rb-stack] td { - display: block; - position: relative; - min-height: 1.6rem; - padding: 0.25rem 0.6rem 0.25rem 8.4rem; + display: flow-root; + margin-top: 0; + padding: 0.125rem 0.6rem 0.125rem 8.4rem; border: 0; + line-height: 1.5; text-align: left !important; overflow-wrap: anywhere; } .sl-markdown-content table[data-rb-stack] td::before { content: attr(data-label); - position: absolute; - top: 0.25rem; - left: 0.6rem; + position: static; + float: left; width: 7.4rem; + margin-left: -7.8rem; font-weight: 600; color: var(--rb-text-secondary); } @@ -748,6 +760,14 @@ starlight-tabs [role='tab'] { white-space: nowrap; } font-weight: 600; color: var(--rb-text-secondary); } + /* A stacked value column is about 14rem wide: short code moves to the + next line whole, and splits only when it is wider than the column. */ + .sl-markdown-content table[data-rb-stack] td code.rb-short { + display: inline-block; + max-width: 100%; + white-space: pre-wrap; + overflow-wrap: anywhere; + } /* On a phone every command wraps: the end of a one-liner is never off-screen. A word is cut only when it cannot fit on a line. */ From 632c7f51e3d6a0fa9e36c3662b5659eefc0e6dbd Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:39:45 +0200 Subject: [PATCH 2/2] docs: name the round 4 theme tests in the guide --- wiki/testing-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 6e833970..423bdd81 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -57,7 +57,7 @@ Every module in `tests/`, by the area it guards. | Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_from`, `test_install_offline`, `test_install_selection`, `test_install_tui` | | Handheld launchers | a pty as the console, fake installer and firmware tools, stubbed PortMaster dialogs | `test_handheld_launchers`, `test_port` | | Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` | -| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r4_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | +| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r4_p0`, `test_site_r4_css`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | ## Test architecture