From a19016642197d98c3cc1aa3963f7e07e324b9dbf Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 01:32:26 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=206=20=F0=9F=9F=A1=20(0=20misread);=20hide=20the?= =?UTF-8?q?=20theme=20switch=20after=20its=20base=20rule,=20hide=20inert?= =?UTF-8?q?=20nav=20toggles,=20test=20the=20rendered=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++-- e2e-tests/visual.spec.ts | 39 +++++++++++++++++++ .../plugins/scheduling/public/scheduling.css | 2 +- public/css/auth.css | 6 --- public/css/styles.css | 16 +++++--- src/ui/stylesheet-contrast.test.ts | 5 --- src/ui/stylesheet-floor.test.ts | 35 +++++++++++------ views/partials/nav-tree.ejs | 4 +- 8 files changed, 82 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a14179..af019bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ at boot. Entries start at 0.3.0. ### The whole nav tree shows on engines without `:has()` -On Chromium below 105 (webOS TV 22 and 23) every nested nav entry was hidden with nothing to open -it. Now the tree folds only where `:has()` works and stays fully open elsewhere. The theme switch, -which also needs `:has()`, is hidden there. +On an engine without `:has()` (Chromium below 105, such as webOS TV 22 and 23, and Firefox below 121) +every nested nav entry was hidden with nothing to open it. Now the tree folds only where `:has()` +works and stays fully open elsewhere, with its toggles hidden. The theme switch, which also needs +`:has()`, is hidden there. -### Text meets 4.5:1 contrast +### Button and badge text meets 4.5:1 contrast The primary button and the current page button take their text colour from the new `--on-accent` token: white in light mode, near-black in dark mode, where white on the accent was 2.7:1. The diff --git a/e2e-tests/visual.spec.ts b/e2e-tests/visual.spec.ts index ce75af7..afa21d9 100644 --- a/e2e-tests/visual.spec.ts +++ b/e2e-tests/visual.spec.ts @@ -174,6 +174,45 @@ test("the public landing at / is ungated and links to sign in + register", async await shot(page, "live-05-public-landing"); }); +// Measured on the rendered button, so a theme block that drops --on-accent fails here. +for (const colorScheme of ["light", "dark"] as const) { + test(`the primary button's text reaches 4.5:1 in ${colorScheme} mode`, async ({ page, context }) => { + await context.clearCookies(); + await page.emulateMedia({ colorScheme }); + await page.goto("/"); + const ratio = await page.locator(".landing .btn-primary").first().evaluate((el) => { + const luminance = (rgb: string): number => { + const [r = 0, g = 0, b = 0] = (rgb.match(/[\d.]+/g) ?? []).map((c) => { + const v = Number(c) / 255; + return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }; + const style = getComputedStyle(el); + const [hi = 0, lo = 0] = [luminance(style.color), luminance(style.backgroundColor)].sort((x, y) => y - x); + return (hi + 0.05) / (lo + 0.05); + }); + expect(ratio).toBeGreaterThanOrEqual(4.5); + }); +} + +test("a nav branch is folded until its toggle opens it — no JavaScript", async ({ page }) => { + await page.goto("/dashboard"); + const child = page.locator('.sidebar a[href="/scheduling"]'); + await expect(child).toBeHidden(); + await page.locator('.sidebar summary[aria-label="Toggle Scheduling"]').click(); + await expect(child).toBeVisible(); +}); + +test("the reference plugin's lead text is inset by --pad-x, like the bars around it", async ({ page }) => { + await page.goto("/scheduling"); + const [inset, padX] = await page.locator(".scheduling-page > p").evaluate((p) => [ + p.getBoundingClientRect().left - (p.parentElement as HTMLElement).getBoundingClientRect().left, + parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--pad-x")), + ] as const); + expect(inset).toBe(padX); +}); + test("unknown routes serve the 404 page (a real user-facing flow, covered end-to-end)", async ({ page }) => { allowConsole(/status of 404 .*\/no-such-page$/); // the navigation under test, which Chromium and WebKit log — not a sub-resource of it const res = await page.goto("/no-such-page"); diff --git a/examples/plugins/scheduling/public/scheduling.css b/examples/plugins/scheduling/public/scheduling.css index a50fe5f..46a2499 100644 --- a/examples/plugins/scheduling/public/scheduling.css +++ b/examples/plugins/scheduling/public/scheduling.css @@ -4,7 +4,7 @@ .scheduling-page .table-wrap { margin-top: var(--space-3, 0.75rem); } -/* Inset the elements that sit directly in the pane like the filter bar, table and pager around them. */ +/* Inset what sits directly in the pane by --pad-x, as the filter bar, table and pager already are. */ .scheduling-page > p, .scheduling-page > .alert, .scheduling-page > .btn { diff --git a/public/css/auth.css b/public/css/auth.css index 777be1e..b8fbe20 100644 --- a/public/css/auth.css +++ b/public/css/auth.css @@ -1,8 +1,3 @@ -/* ============================================================= - Auth flow — login / register / forgot password - Built on the shared tokens in styles.css. Pure HTML/CSS. - ============================================================= */ - .auth-stage { min-height: 100dvh; display: grid; place-items: center; @@ -126,7 +121,6 @@ .auth-aside a { color: var(--accent); } .auth-aside a:hover { text-decoration: underline; } -/* field-level error, shown under a field the server marked .has-error */ .field-error { display: none; align-items: center; gap: 6px; margin: 0; font-size: var(--fz-xs); color: var(--neg); diff --git a/public/css/styles.css b/public/css/styles.css index 9c02c7a..77c4eea 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -191,9 +191,9 @@ summary { list-style: none; cursor: pointer; } • CLICKABLE = label is → navigates on click. STATIC = label is → not a navigation target. So "header" and "clickable" are independent: any combination works. - Children sit in a sibling