diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd8fd4..af019bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ at boot. Entries start at 0.3.0. ## Unreleased +### The whole nav tree shows on engines without `:has()` + +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. + +### 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 +light-mode success, warning and info badge text is darker. A plugin that overrides `--accent` or +`--l-accent` should set `--l-on-accent` and `--d-on-accent` to match. + +### Unused auth stylesheet rules are gone + +`public/css/auth.css` no longer holds the `.auth-view` screen switch, `.tpl-*` preview controls, +`.forgot-alert`, `.register-alert`, `.remember` and `.auth-foot`; no core view used them. A plugin +view that did must carry its own rules. + ### Colours render on engines without `oklch()` The theme palette in `public/css/styles.css` is hex, not `oklch()`. On an engine older than diff --git a/docs/comprehension-history.md b/docs/comprehension-history.md index 67ce870..62dec9c 100644 --- a/docs/comprehension-history.md +++ b/docs/comprehension-history.md @@ -19,3 +19,27 @@ Inherited architect, decided by: - Self-sufficiency, Locality: `public/css/styles.css:24` `:root`: the comment above the hex palette says why a colour must not be oklch(), where a colour is edited - Locality: `src/ui/stylesheet-floor.test.ts:11` `declares no oklch() colour`: a red gate explains the symptom in the file it lands on - Self-sufficiency: `README.md:1027` "Two browser floors": the colour rule now sits beside the floor rule + +## 2026-10-31T00:43:19Z, PR #182 at 19987ba, against 3e70768 + +Ruling: better + +| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | +|---|---|---|---|---|---| +| Mid A | better | better | better | better | better | +| Inherited architect | better | better | better | better | better | + +Mid A, decided by: + +- Navigation, Shape, Self-sufficiency, Overall: `public/css/auth.css:1` `auth.css`: about 120 lines of rules for markup no view renders are gone, so a cold reader no longer rules them out first +- Locality, Self-sufficiency, Overall: `public/css/styles.css:31` `--on-accent`: the text colour on the accent is one token where four hard-coded `#fff` stood +- Self-sufficiency, Navigation, Overall: `public/css/styles.css:287` `@supports not selector(:has(a))`: the nav comment, the partial's header and the rule all say the tree folds only where `:has()` works +- Locality: `public/css/styles.css:241` `@supports selector(:has(a))`: the nav-fold behaviour now sits in two blocks about 50 lines apart, the second order-dependent; the comment names it + +Inherited architect, decided by: + +- Navigation, Shape, Overall: `public/css/auth.css:1` `auth.css`: the false `:target` banner and the phantom state machine are gone, so every selector left names something a view emits +- Self-sufficiency, Overall: `views/partials/nav-tree.ejs:4` `nav-tree.ejs`: the header and `public/css/styles.css:194` now say the branch folds only where `:has()` works; the old text was untrue on old engines +- Locality, Overall: `public/css/styles.css:31` `--l-on-accent`: one token feeds `.btn-primary`, `.page-btn[aria-current]` and `.brand-mark` +- Locality: `public/css/styles.css:287` `@supports not selector(:has(a))`: its `.nav-disc` rule sits about 85 lines from the nav rules it overrides, and `stylesheet-floor.test.ts` guards the order for `.theme-switch` only +- Self-sufficiency: `src/ui/stylesheet-contrast.test.ts:29` `stylesheet-contrast.test.ts`: the darker badge colours now have a stated 4.5:1 reason someone can find diff --git a/docs/reading-cost.md b/docs/reading-cost.md index d281e8b..6a5a028 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -16,3 +16,17 @@ Read most (opens and searches): - `ci.sh` 1 - `examples/plugins/scheduling/views/shifts.ejs` 1 - `src/auth/csrf.ts` 1 + +## Reading cost, chunk df5e30a4db5f, finished 2026-10-31T00:22:00Z + +80251 new input tokens before the first commit. + +Read most (opens and searches): + +- `todo.md` 5 +- `README.md` 3 +- `CHANGELOG.md` 2 +- `public/css/styles.css` 2 +- `AGENTS.md` 1 +- `src/ui/stylesheet-floor.test.ts` 1 +- `src/ui/theme-switch.test.ts` 1 diff --git a/e2e-tests/visual.spec.ts b/e2e-tests/visual.spec.ts index ce75af7..4f6c164 100644 --- a/e2e-tests/visual.spec.ts +++ b/e2e-tests/visual.spec.ts @@ -174,6 +174,50 @@ 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 elements, so a theme block that drops --on-accent fails here. +const contrastOf = (el: Element): number => { + 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); +}; +for (const [name, colorScheme, forceDark] of [["light", "light", false], ["dark", "dark", false], ["forced dark", "light", true]] as const) { + test(`text on the accent reaches 4.5:1 in ${name} mode`, async ({ page, context }) => { + await context.clearCookies(); + await page.emulateMedia({ colorScheme }); + await page.goto("/"); + if (forceDark) await page.locator("#theme-dark").check({ force: true }); + // No page of this stack has a second page of results, so add the current-page button. + await page.locator("#main-content").evaluate((el) => el.insertAdjacentHTML("beforeend", '2')); + for (const selector of [".landing .btn-primary", ".brand-mark", ".page-btn"]) { + await expect.poll(() => page.locator(selector).first().evaluate(contrastOf), { message: selector }).toBeGreaterThanOrEqual(4.5); // polls past the colour transition + } + }); +} + +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 d284249..46a2499 100644 --- a/examples/plugins/scheduling/public/scheduling.css +++ b/examples/plugins/scheduling/public/scheduling.css @@ -4,3 +4,9 @@ .scheduling-page .table-wrap { margin-top: var(--space-3, 0.75rem); } +/* 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 { + margin-inline: var(--pad-x); +} diff --git a/public/css/auth.css b/public/css/auth.css index dbee49f..b8fbe20 100644 --- a/public/css/auth.css +++ b/public/css/auth.css @@ -1,11 +1,3 @@ -/* ============================================================= - Auth flow — login / register / forgot password - Built on the shared tokens in styles.css. Pure HTML/CSS: - - screens switch via :target (no JS) - - SSO is a config section toggled by a class/checkbox; providers - are a