mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
feat: quiet the site theme
# Conflicts: # wiki/testing-guide.md
This commit is contained in:
commit
4edebc56bc
4 files changed
+380
-24
No files matched your search
@@ -0,0 +1,215 @@
|
||||
"""Round 2 theme: the stylesheet fixes of package 1, read from its source.
|
||||
|
||||
Each test fails on the stylesheet as it was before the fix: a red focus ring
|
||||
and hover, Starlight's 35 px h2, tinted disclosure blocks, striped asides,
|
||||
the search result file dump, lifting shadows, a sidebar without hierarchy,
|
||||
two indigos, permalinks hidden on touch screens, OS tabs cut at 320 px, a
|
||||
column that moves on pages without a TOC.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CSS_PATH = ROOT / "website" / "src" / "styles" / "retrobios.css"
|
||||
|
||||
# Page backgrounds Starlight 0.42.6 draws under our tokens, read with
|
||||
# getComputedStyle on a built page (light: white, dark: gray-7 hsl(224 10% 10%)).
|
||||
PAGE_BG = {"light": "#ffffff", "dark": "#17181c"}
|
||||
# Text Starlight draws on a filled button (--sl-color-black in each theme).
|
||||
ON_FILL = {"light": "#ffffff", "dark": "#17181c"}
|
||||
|
||||
|
||||
def _strip_comments(css: str) -> str:
|
||||
return re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
||||
|
||||
|
||||
def _blocks(css: str) -> list[tuple[str, str]]:
|
||||
"""Innermost (selector, body) pairs, media queries flattened."""
|
||||
return [(sel.strip(), body) for sel, body in re.findall(r"([^{}]+)\{([^{}]*)\}", css)]
|
||||
|
||||
|
||||
def _theme_tokens(css: str, selector: str) -> dict[str, str]:
|
||||
for sel, body in _blocks(css):
|
||||
if sel == selector:
|
||||
return dict(re.findall(r"(--[\w-]+)\s*:\s*([^;]+);", body))
|
||||
raise AssertionError(f"no block {selector}")
|
||||
|
||||
|
||||
def _rules(css: str, selector_part: str) -> list[str]:
|
||||
return [body for sel, body in _blocks(css) if selector_part in sel]
|
||||
|
||||
|
||||
def _luminance(hex_colour: str) -> float:
|
||||
value = hex_colour.lstrip("#")
|
||||
channels = [int(value[i : i + 2], 16) / 255 for i in (0, 2, 4)]
|
||||
linear = [c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 for c in channels]
|
||||
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
|
||||
|
||||
|
||||
def _ratio(a: str, b: str) -> float:
|
||||
la, lb = sorted((_luminance(a), _luminance(b)), reverse=True)
|
||||
return (la + 0.05) / (lb + 0.05)
|
||||
|
||||
|
||||
class ThemeStylesheet(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.css = _strip_comments(CSS_PATH.read_text(encoding="utf-8"))
|
||||
dark = _theme_tokens(cls.css, ":root")
|
||||
light = {**dark, **_theme_tokens(cls.css, ":root[data-theme='light']")}
|
||||
cls.tokens = {"dark": dark, "light": light}
|
||||
|
||||
def resolve(self, theme: str, token: str) -> str:
|
||||
value = self.tokens[theme][token].strip()
|
||||
while value.startswith("var("):
|
||||
value = self.tokens[theme][value[4:-1].strip()].strip()
|
||||
return value
|
||||
|
||||
# 1.1
|
||||
def test_focus_ring_is_the_link_colour_not_red(self):
|
||||
focus = _rules(self.css, ":focus-visible")
|
||||
self.assertTrue(focus)
|
||||
for body in focus:
|
||||
self.assertIn("2px solid var(--rb-link)", body)
|
||||
self.assertNotIn("accent", body.replace("--rb-link", ""))
|
||||
ring = next(b for s, b in _blocks(self.css) if s.startswith(":where(a, button"))
|
||||
self.assertIn("outline-offset: 2px", ring)
|
||||
self.assertNotIn("#b42318", self.css)
|
||||
self.assertNotIn("--rb-accent", self.css)
|
||||
|
||||
def test_button_hover_stays_in_its_own_colour(self):
|
||||
self.assertNotRegex(self.css, r"\.rb-button:is\(:hover,\s*:focus\)")
|
||||
primary = _rules(self.css, ".rb-button-primary:hover")
|
||||
self.assertTrue(primary)
|
||||
self.assertIn("var(--rb-link-strong)", primary[0])
|
||||
self.assertEqual(self.resolve("light", "--rb-link-strong"), "#1d2f73")
|
||||
self.assertEqual(self.resolve("dark", "--rb-link-strong"), "#c9d2ff")
|
||||
outline = [b for s, b in _blocks(self.css) if s.endswith(".rb-button:hover")]
|
||||
self.assertIn("var(--sl-color-accent-low)", outline[0])
|
||||
|
||||
# 1.3
|
||||
def test_heading_scale(self):
|
||||
root = self.tokens["dark"]
|
||||
self.assertEqual(root["--sl-text-h1"], "2.25rem")
|
||||
self.assertEqual(root["--sl-text-h2"], "1.5rem")
|
||||
self.assertEqual(root["--sl-text-h3"], "1.2rem")
|
||||
self.assertEqual(root["--sl-text-h4"], "1rem")
|
||||
self.assertRegex(self.css, r"@media \(max-width: 50rem\) \{\s*:root \{ --sl-text-h1: 1\.875rem; \}")
|
||||
self.assertRegex(self.css, r"level-h2, h2\) \{ margin-top: 2\.75rem; \}")
|
||||
self.assertRegex(self.css, r"level-h3, h3:not\(\.rb-file-title\)\) \{ margin-top: 1\.75rem; \}")
|
||||
|
||||
# 1.2
|
||||
def test_asides_are_neutral_boxes(self):
|
||||
neutral = _rules(self.css, ".starlight-aside:is(.starlight-aside--note, .starlight-aside--tip)")
|
||||
joined = "".join(neutral)
|
||||
self.assertIn("--sl-color-asides-border: var(--rb-border)", joined)
|
||||
self.assertIn("background: var(--rb-surface)", joined)
|
||||
self.assertIn("display: none", joined)
|
||||
box = _rules(self.css, ".sl-markdown-content .starlight-aside")[0]
|
||||
self.assertIn("border: 1px solid", box)
|
||||
|
||||
# 1.4
|
||||
def test_disclosure_blocks_are_neutral(self):
|
||||
self.assertNotIn("rb-details-tip", self.css)
|
||||
self.assertNotIn("--sl-color-blue", self.css)
|
||||
base = next(b for s, b in _blocks(self.css) if s == ".sl-markdown-content details.rb-details")
|
||||
self.assertIn("background: transparent", base)
|
||||
self.assertIn("var(--rb-border)", base)
|
||||
self.assertIn("0.95rem", _rules(self.css, "details.rb-details > summary")[0])
|
||||
|
||||
# 1.5
|
||||
def test_search_hides_the_file_list(self):
|
||||
self.assertIn("display: none", _rules(self.css, "site-search .pagefind-ui__result-tags")[0])
|
||||
|
||||
# 1.6
|
||||
def test_no_lifting_shadow_on_pagination_or_menu(self):
|
||||
self.assertIn("box-shadow: none", _rules(self.css, ".pagination-links a")[0])
|
||||
self.assertIn("font-size: 1.05rem", _rules(self.css, ".pagination-links .link-title")[0])
|
||||
menu = next(b for s, b in _blocks(self.css) if s == ".sl-menu-button")
|
||||
self.assertIn("box-shadow: none", menu)
|
||||
self.assertIn("background: transparent", menu)
|
||||
self.assertIn("1px solid var(--rb-border)", menu)
|
||||
|
||||
# 1.7
|
||||
def test_sidebar_hierarchy(self):
|
||||
label = _rules(self.css, ".top-level summary .large")[0]
|
||||
self.assertIn("var(--sl-text-xs)", label)
|
||||
self.assertIn("var(--rb-text-secondary)", label)
|
||||
current = _rules(self.css, "a[aria-current='page']")[0]
|
||||
self.assertIn("background: var(--sl-color-accent-low)", current)
|
||||
self.assertIn("color: var(--sl-color-accent-high)", current)
|
||||
self.assertNotIn("border", current)
|
||||
|
||||
# 1.8
|
||||
def test_one_indigo(self):
|
||||
for theme in ("light", "dark"):
|
||||
with self.subTest(theme=theme):
|
||||
self.assertEqual(self.resolve(theme, "--sl-color-accent"), self.resolve(theme, "--rb-link"))
|
||||
self.assertEqual(self.resolve(theme, "--sl-color-text-accent"), self.resolve(theme, "--rb-link"))
|
||||
|
||||
# 1.9
|
||||
def test_permalinks_visible_without_hover(self):
|
||||
self.assertRegex(self.css, r"@media \(hover: none\) \{\s*\.rb-permalink \{ opacity: 1; \}")
|
||||
permalink = next(b for s, b in _blocks(self.css) if s == ".rb-permalink")
|
||||
self.assertIn("min-height: 24px", permalink)
|
||||
|
||||
# 1.10
|
||||
def test_os_tabs_wrap(self):
|
||||
self.assertIn("flex-wrap: wrap", _rules(self.css, "starlight-tabs [role='tablist']")[0])
|
||||
self.assertIn("white-space: nowrap", _rules(self.css, "starlight-tabs [role='tab']")[0])
|
||||
|
||||
# 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)
|
||||
|
||||
# 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])
|
||||
facts = _rules(self.css, "dl.rb-facts")[0]
|
||||
self.assertIn("grid-template-columns: 9rem 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
|
||||
def test_long_values_wrap_between_words(self):
|
||||
table = _rules(self.css, ".rb-meta-card table")[0]
|
||||
self.assertIn("table-layout: fixed", table)
|
||||
self.assertIn("display: table", table)
|
||||
self.assertNotRegex(self.css, r"\.rb-meta-card td \{ overflow-wrap: anywhere")
|
||||
self.assertIn("overflow-wrap: anywhere", _rules(self.css, ".rb-meta-card td code")[0])
|
||||
self.assertIn("width: 40%", "".join(_rules(self.css, ".rb-meta-card td:first-child")))
|
||||
upstream = _rules(self.css, "#by-upstream-emulator")[0]
|
||||
self.assertIn("max-width: 22rem", upstream)
|
||||
|
||||
# 1.15
|
||||
def test_two_column_tables_stack_label_over_value(self):
|
||||
self.assertIn("content: none", _rules(self.css, "table[data-rb-stack='kv'] td::before")[0])
|
||||
self.assertTrue(_rules(self.css, "table[data-rb-stack='kv'] td:first-child"))
|
||||
|
||||
def test_contrast_meets_wcag_aa_in_both_themes(self):
|
||||
for theme in ("light", "dark"):
|
||||
bg = PAGE_BG[theme]
|
||||
pairs = [
|
||||
("link", self.resolve(theme, "--rb-link"), bg, 4.5),
|
||||
("focus ring", self.resolve(theme, "--rb-link"), bg, 3.0),
|
||||
("primary button", ON_FILL[theme], self.resolve(theme, "--rb-link"), 4.5),
|
||||
("primary hover", ON_FILL[theme], self.resolve(theme, "--rb-link-strong"), 4.5),
|
||||
("outline hover", self.resolve(theme, "--rb-link"), self.resolve(theme, "--sl-color-accent-low"), 4.5),
|
||||
("sidebar current", self.resolve(theme, "--sl-color-accent-high"), self.resolve(theme, "--sl-color-accent-low"), 4.5),
|
||||
("secondary text", self.resolve(theme, "--rb-text-secondary"), bg, 4.5),
|
||||
("link on surface", self.resolve(theme, "--rb-link"), self.resolve(theme, "--rb-surface"), 4.5),
|
||||
]
|
||||
for label, fg, back, need in pairs:
|
||||
with self.subTest(theme=theme, pair=label):
|
||||
self.assertGreaterEqual(_ratio(fg, back), need, f"{fg} on {back}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -369,7 +369,8 @@ class PlainHouseStyle(unittest.TestCase):
|
||||
"gradient": re.compile(r"gradient\("),
|
||||
"side stripe": re.compile(r"border-(?:left|inline-start)\s*:\s*\d+px"),
|
||||
"lifting card": re.compile(r"translateY\("),
|
||||
"drop shadow": re.compile(r"box-shadow"),
|
||||
# "box-shadow: none" removes one of Starlight's shadows.
|
||||
"drop shadow": re.compile(r"box-shadow\s*:(?!\s*none\s*[;}])"),
|
||||
"small caps header": re.compile(r"text-transform\s*:\s*uppercase"),
|
||||
"pill": re.compile(r"border-radius\s*:\s*(?:[5-9]|\d{2,})px|50%"),
|
||||
"box-drawing rule": re.compile("[\u2500-\u257f]"),
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
code.rb-path a folder path shown as a chip
|
||||
.rb-figure a generated diagram (scripts/sitediagrams.py)
|
||||
.rb-bar inline coverage bar, width from --v
|
||||
.rb-details-* collapsible block, in the callout colour of its role
|
||||
.rb-details-* collapsible block, neutral unless caution or danger
|
||||
dl.rb-facts the facts of a file card, label and value side by side
|
||||
abbr.rb-term a glossary term, its definition on hover or tap
|
||||
*/
|
||||
|
||||
@@ -23,11 +24,16 @@
|
||||
--sl-font: system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
--sl-font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
--sl-color-accent-low: #1f2147;
|
||||
--sl-color-accent: #8c9cf0;
|
||||
--sl-color-accent: #aebcff;
|
||||
--sl-color-accent-high: #c9d2ff;
|
||||
--sl-color-text-accent: var(--sl-color-accent);
|
||||
--sl-text-h1: 2.25rem;
|
||||
--sl-text-h2: 1.5rem;
|
||||
--sl-text-h3: 1.2rem;
|
||||
--sl-text-h4: 1rem;
|
||||
|
||||
--rb-link: #aebcff;
|
||||
--rb-accent: #ff9a8f;
|
||||
--rb-link-strong: #c9d2ff;
|
||||
--rb-surface: #252838;
|
||||
--rb-border: #3a3d52;
|
||||
--rb-border-strong: #5a5e7a;
|
||||
@@ -57,11 +63,12 @@
|
||||
|
||||
:root[data-theme='light'] {
|
||||
--sl-color-accent-low: #e3e5f6;
|
||||
--sl-color-accent: #3b3f7a;
|
||||
--sl-color-accent-high: #2b2e5c;
|
||||
--sl-color-accent: #273c8f;
|
||||
--sl-color-accent-high: #1d2f73;
|
||||
--sl-color-text-accent: var(--sl-color-accent);
|
||||
|
||||
--rb-link: #273c8f;
|
||||
--rb-accent: #b42318;
|
||||
--rb-link-strong: #1d2f73;
|
||||
--rb-surface: #f5f6fa;
|
||||
--rb-border: #e0e3eb;
|
||||
--rb-border-strong: #b8bdcc;
|
||||
@@ -95,14 +102,21 @@
|
||||
}
|
||||
.sl-markdown-content a:not(.rb-button):hover { text-decoration-thickness: 0.14em; }
|
||||
.sl-markdown-content a code { color: var(--rb-link); }
|
||||
/* The ring is the link colour: red is kept for what is wrong. The offset
|
||||
puts it on the page background, never on the fill it surrounds. */
|
||||
:where(a, button, input, select, textarea, summary, abbr, [tabindex]):focus-visible {
|
||||
outline: 3px solid var(--rb-accent);
|
||||
outline-offset: 3px;
|
||||
outline: 2px solid var(--rb-link);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Type scale */
|
||||
.sl-markdown-content h2 { font-weight: 600; letter-spacing: -0.005em; }
|
||||
/* Type scale: h2 two thirds of the h1, so a page of short blocks keeps
|
||||
one title and its sections, not a column of headlines. */
|
||||
.sl-markdown-content h2 { font-weight: 600; letter-spacing: -0.01em; }
|
||||
.sl-markdown-content h3 { font-weight: 600; }
|
||||
.sl-markdown-content :not(h1, h2, h3, h4, h5, h6, .sl-heading-wrapper) + :is(.sl-heading-wrapper.level-h2, h2) { margin-top: 2.75rem; }
|
||||
.sl-markdown-content :not(h1, h2, h3, h4, h5, h6, .sl-heading-wrapper) + :is(.sl-heading-wrapper.level-h3, h3:not(.rb-file-title)) { margin-top: 1.75rem; }
|
||||
/* A row of buttons right under a heading does not touch it. */
|
||||
.sl-markdown-content .sl-heading-wrapper:has(+ .rb-button) { margin-bottom: 0.5rem; }
|
||||
.sl-markdown-content .rb-lead { font-size: 1.05em; line-height: 1.55; max-width: 44em; }
|
||||
|
||||
/* Breadcrumbs above the title */
|
||||
@@ -159,10 +173,10 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
.sl-markdown-content .rb-button-primary { background: var(--rb-link); color: var(--sl-color-black); }
|
||||
.sl-markdown-content .rb-button:is(:hover, :focus) {
|
||||
background: var(--rb-accent);
|
||||
border-color: var(--rb-accent);
|
||||
color: var(--sl-color-black);
|
||||
.sl-markdown-content .rb-button:hover { background: var(--sl-color-accent-low); }
|
||||
.sl-markdown-content .rb-button-primary:hover {
|
||||
background: var(--rb-link-strong);
|
||||
border-color: var(--rb-link-strong);
|
||||
}
|
||||
.sl-markdown-content table .rb-button { font-size: 0.8rem; padding: 0.4rem 0.8rem; }
|
||||
|
||||
@@ -310,13 +324,25 @@
|
||||
/* Tables */
|
||||
.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: 3px solid var(--rb-accent); outline-offset: 2px; }
|
||||
.sl-markdown-content table[role='region']:focus-visible { outline: 2px solid var(--rb-link); outline-offset: 2px; }
|
||||
|
||||
/* Key and value cards: the empty header row is not drawn */
|
||||
.rb-meta-card thead,
|
||||
.sl-markdown-content table thead:not(:has(th:not(:empty))) { display: none; }
|
||||
.rb-meta-card td { overflow-wrap: anywhere; }
|
||||
.rb-meta-card td:first-child { font-weight: 600; color: var(--rb-text-secondary); white-space: nowrap; width: 140px; }
|
||||
/* Fixed layout: the label column keeps its share and a value wraps between
|
||||
words. Only code and long addresses break inside a word. */
|
||||
.sl-markdown-content .rb-meta-card table { display: table; table-layout: fixed; width: 100%; }
|
||||
.rb-meta-card td { overflow-wrap: break-word; }
|
||||
.rb-meta-card td code { overflow-wrap: anywhere; }
|
||||
.rb-meta-card td a { word-break: normal; }
|
||||
.rb-meta-card td:first-child { font-weight: 600; color: var(--rb-text-secondary); width: 11rem; }
|
||||
|
||||
/* Cross-reference by upstream: the address column wraps instead of pushing
|
||||
the cores and platforms off screen. */
|
||||
.sl-markdown-content .sl-heading-wrapper:has(> #by-upstream-emulator) + section td:has(a[href^='http']) {
|
||||
max-width: 22rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.rb-timestamp {
|
||||
font-size: 0.8rem;
|
||||
@@ -326,10 +352,15 @@
|
||||
border-top: 1px solid var(--rb-border);
|
||||
}
|
||||
|
||||
/* File cards on system and emulator pages */
|
||||
/* 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; }
|
||||
.sl-markdown-content :is(.rb-sys-file, .rb-file-entry) {
|
||||
border-top: 1px solid var(--rb-border);
|
||||
padding: 0.75rem 0 0.25rem;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 320px;
|
||||
}
|
||||
.sl-markdown-content :is(.rb-sys-file, .rb-file-entry) > :first-child { margin-top: 0; }
|
||||
.sl-markdown-content .rb-file-title {
|
||||
@@ -355,6 +386,9 @@
|
||||
}
|
||||
.rb-hash code { background: none; padding: 0; overflow-wrap: anywhere; }
|
||||
.rb-permalink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
margin-left: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--rb-text-secondary);
|
||||
@@ -363,25 +397,63 @@
|
||||
}
|
||||
:is(.rb-sys-file, .rb-file-entry):hover .rb-permalink,
|
||||
.rb-permalink:focus { opacity: 1; }
|
||||
/* No hover on a touch screen: the link is always shown there. */
|
||||
@media (hover: none) {
|
||||
.rb-permalink { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 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);
|
||||
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; }
|
||||
.sl-markdown-content .rb-facts code { overflow-wrap: anywhere; }
|
||||
@media (max-width: 30rem) {
|
||||
.sl-markdown-content dl.rb-facts { grid-template-columns: minmax(0, 1fr); gap: 0; }
|
||||
.sl-markdown-content .rb-facts dd + dt { margin-top: 0.4rem; }
|
||||
}
|
||||
:is(.rb-sys-file, .rb-file-entry):has(> .rb-file-title:target) { background: var(--rb-surface); }
|
||||
|
||||
/* Collapsible blocks keep the callout colour of their role */
|
||||
/* Collapsible blocks are quiet: a closed block must not outweigh the
|
||||
command it explains. Colour is kept for caution and danger. */
|
||||
.sl-markdown-content details.rb-details {
|
||||
border: 1px solid var(--sl-color-blue);
|
||||
border: 1px solid var(--rb-border);
|
||||
border-radius: 4px;
|
||||
background: var(--sl-color-blue-low);
|
||||
background: transparent;
|
||||
padding: 0.6rem 1rem;
|
||||
}
|
||||
.sl-markdown-content details.rb-details-tip { border-color: var(--sl-color-purple); background: var(--sl-color-purple-low); }
|
||||
.sl-markdown-content details.rb-details:not(.rb-details-caution, .rb-details-danger):has(> summary:hover) { border-color: var(--rb-border-strong); }
|
||||
.sl-markdown-content details.rb-details-caution { border-color: var(--sl-color-orange); background: var(--sl-color-orange-low); }
|
||||
.sl-markdown-content details.rb-details-danger { border-color: var(--sl-color-red); background: var(--sl-color-red-low); }
|
||||
.sl-markdown-content details.rb-details > summary {
|
||||
min-height: 24px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Starlight's chevron, which turns when the block opens, in the muted colour. */
|
||||
.sl-markdown-content details.rb-details > summary::before { background-color: var(--rb-text-secondary); }
|
||||
.sl-markdown-content details.rb-details[open] > summary { margin-bottom: 0.6rem; }
|
||||
|
||||
/* Asides: a neutral box for notes and tips, no side stripe, no icon.
|
||||
Caution and danger keep their colour, as a 1 px border all round. */
|
||||
.sl-markdown-content .starlight-aside {
|
||||
border: 1px solid var(--sl-color-asides-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.sl-markdown-content .starlight-aside:is(.starlight-aside--note, .starlight-aside--tip) {
|
||||
--sl-color-asides-border: var(--rb-border);
|
||||
--sl-color-asides-text-accent: var(--sl-color-white);
|
||||
background: var(--rb-surface);
|
||||
}
|
||||
.sl-markdown-content .starlight-aside:is(.starlight-aside--note, .starlight-aside--tip) .starlight-aside__icon { display: none; }
|
||||
|
||||
/* Terminal output: what the screen shows, not something to type */
|
||||
.sl-markdown-content .rb-terminal .expressive-code pre { background: var(--rb-terminal-bg); color: var(--rb-terminal-fg); }
|
||||
|
||||
@@ -506,6 +578,59 @@ abbr.rb-term[data-open]::after {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Search: a system page's file names rank it (its `files` meta field) but
|
||||
are not printed under the result. */
|
||||
site-search .pagefind-ui__result-tags { display: none; }
|
||||
|
||||
/* The site name keeps its size: Starlight sizes it from the h4 step. */
|
||||
.site-title { font-size: var(--sl-text-xl); }
|
||||
@media (min-width: 50rem) {
|
||||
.site-title { font-size: var(--sl-text-2xl); }
|
||||
}
|
||||
|
||||
/* Pagination and the phone menu button sit flat on the page. */
|
||||
.pagination-links a { box-shadow: none; border-color: var(--rb-border); }
|
||||
.pagination-links a:hover { border-color: var(--rb-border-strong); }
|
||||
.pagination-links .link-title { font-size: 1.05rem; font-weight: 600; }
|
||||
.sl-menu-button {
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--rb-border);
|
||||
background: transparent;
|
||||
color: var(--sl-color-white);
|
||||
}
|
||||
.sl-menu-button:has(~ :popover-open) { box-shadow: none; background: var(--sl-color-accent-low); }
|
||||
|
||||
/* Sidebar: small grey group labels, regular links, the current page on a
|
||||
tinted background with no bar or rule beside it. */
|
||||
#starlight__sidebar .top-level summary .large {
|
||||
font-size: var(--sl-text-xs);
|
||||
font-weight: 600;
|
||||
color: var(--rb-text-secondary);
|
||||
}
|
||||
#starlight__sidebar .top-level a { font-weight: 400; }
|
||||
#starlight__sidebar .top-level a[aria-current='page'] {
|
||||
background: var(--sl-color-accent-low);
|
||||
color: var(--sl-color-accent-high);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* OS tabs wrap on a narrow phone instead of hiding the last one. */
|
||||
starlight-tabs [role='tablist'] { flex-wrap: wrap; }
|
||||
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. */
|
||||
:root[data-has-sidebar]:not([data-has-toc]) { --__toc-width: initial; }
|
||||
@media (min-width: 72rem) {
|
||||
:root[data-has-sidebar]:not([data-has-toc]) .main-pane {
|
||||
--sl-content-margin-inline: auto 0;
|
||||
width: min(
|
||||
calc(100% - var(--sl-sidebar-width)),
|
||||
calc(var(--sl-content-width) + (100% - var(--sl-content-width) - var(--sl-sidebar-width)) / 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Licence line under every page */
|
||||
.rb-copyright { margin: 1.5rem 0 0; font-size: var(--sl-text-xs); color: var(--sl-color-gray-3); }
|
||||
|
||||
@@ -541,6 +666,16 @@ abbr.rb-term[data-open]::after {
|
||||
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::before { content: none; }
|
||||
.sl-markdown-content table[data-rb-stack='kv'] td:first-child {
|
||||
padding-bottom: 0.15rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--rb-text-secondary);
|
||||
}
|
||||
|
||||
/* On a phone every command wraps: the end of a one-liner is never off-screen. */
|
||||
.sl-markdown-content .expressive-code pre > code { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
@@ -548,6 +683,11 @@ abbr.rb-term[data-open]::after {
|
||||
.rb-hash { font-size: 0.75rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
:root { --sl-text-h1: 1.875rem; }
|
||||
.rb-meta-card td:first-child { width: 40%; }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.rb-table-filter { grid-template-columns: 1fr; }
|
||||
.rb-table-filter label { grid-column: auto; }
|
||||
|
||||
@@ -56,7 +56,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Handheld port | stubbed PortMaster dialogs, fake installer | `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_check_freshness`, `test_dedup`, `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_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
|
||||
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `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_emulators`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_schema_semantic_gate`, `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_css`, `test_site_r2_emulators`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
|
||||
## Test architecture
|
||||
|
||||
|
||||
Reference in new issue
Block a user