Give every palette colour a hex value an engine without oklch can read #181

Merged
lilleman merged 7 commits from item-30 into main 2026-10-31 01:21:41 +01:00
8 changed files with 98 additions and 23 deletions
+10
View File
@@ -463,3 +463,13 @@ Default to **no comment**. Delete one that restates the adjacent code, repeats a
elsewhere, justifies self-evident code, or records history. Write one only for what a competent elsewhere, justifies self-evident code, or records history. Write one only for what a competent
reader of *this* codebase could not infer: a surprising why, a footgun, an invariant, an external reader of *this* codebase could not infer: a surprising why, a footgun, an invariant, an external
constraint. See [Prose discipline](#prose-discipline). constraint. See [Prose discipline](#prose-discipline).
## Scoring run
The comprehension panel's seats fill their prompts from these fields.
- **Language:** strict TypeScript on Node 24, with EJS views and plain CSS
- **Kind:** a server-rendered web GUI host with a plugin system
- **Domain:** web authentication, authorization and server-rendered admin GUIs
- **Domain docs:** the Ory Kratos, Keto and Hydra documentation
- **3am question:** a visitor on an old TV browser reports that the sign-in button is blank. Which file and rule?
+7
View File
@@ -6,6 +6,13 @@ at boot. Entries start at 0.3.0.
## Unreleased ## Unreleased
### Colours render on engines without `oklch()`
The theme palette in `public/css/styles.css` is hex, not `oklch()`. On an engine older than
Chromium 111 (Tizen 2024, webOS TV 24) `var(--accent)` was invalid, so the primary button was white
text on no background. A plugin that sets a colour custom property, such as `--accent` or `--l-accent`,
should use hex, `rgb()` or `hsl()` too.
### One horizontal inset across the content pane ### One horizontal inset across the content pane
The topbar, filter bar, pager, form page and empty-table message inset their content by `--pad-x` The topbar, filter bar, pager, form page and empty-table message inset their content by `--pad-x`
+4 -2
View File
@@ -1023,8 +1023,10 @@ URL is the only state the UI keeps.
Chromium 87, Safari 15 and Firefox 115, the engines in years-old TVs, kiosks and Qt panels. Only Chromium 87, Safari 15 and Firefox 115, the engines in years-old TVs, kiosks and Qt panels. Only
features [Baseline Widely available](https://web-platform-dx.github.io/baseline/) (in every features [Baseline Widely available](https://web-platform-dx.github.io/baseline/) (in every
major engine for 30 months) may shape how a page looks; anything newer, like popovers or anchor major engine for 30 months) may shape how a page looks; anything newer, like popovers or anchor
positioning, only enhances. A feature the floor engines lack, such as `:has()` or `oklch()`, may positioning, only enhances. A feature the floor engines lack, such as `:has()`, may
decorate a page but never decide whether something shows or can be read. decorate a page but never decide whether something shows or can be read. A colour is hex, `rgb()` or
`hsl()`, never `oklch()`: a custom property holding it passes unchecked, and the `var()` reading it
then renders transparent.
**A field whose options depend on another field is asked for in steps, one GET form each**, so every **A field whose options depend on another field is asked for in steps, one GET form each**, so every
choice is in the URL, and nothing typed is lost to one because nothing is typed until they are made. choice is in the URL, and nothing typed is lost to one because nothing is typed until they are made.
+21
View File
@@ -0,0 +1,21 @@
# Comprehension history
## 2026-10-31T00:16:02Z, PR #181 at 56ff987, against eed1050
Ruling: better
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|---|---|---|---|---|---|
| Mid A | same | better | same | better | better |
| Inherited architect | same | better | same | better | better |
Mid A, 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 colour edit that breaks the browser floor fails a test instead of an old engine
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
+18
View File
@@ -0,0 +1,18 @@
# Reading cost
## Reading cost, chunk 57e864bce02a, finished 2026-10-30T23:59:00Z
100624 new input tokens before the first commit.
Read most (opens and searches):
- `src/http/app.test.ts` 9
- `README.md` 7
- `src/http/app.ts` 4
- `todo.md` 3
- `e2e-tests/playwright.config.ts` 2
- `AGENTS.md` 1
- `CHANGELOG.md` 1
- `ci.sh` 1
- `examples/plugins/scheduling/views/shifts.ejs` 1
- `src/auth/csrf.ts` 1
+14 -12
View File
@@ -21,27 +21,29 @@
widgets (date picker, scrollbars, selects). widgets (date picker, scrollbars, selects).
------------------------------------------------------------- */ ------------------------------------------------------------- */
:root { :root {
/* Hex, never oklch: below Chromium 111 `var(--accent)` is invalid at computed-value time and the
primary button loses its background. A fallback declaration cannot catch that. */
/* ---- raw LIGHT palette ---- */ /* ---- raw LIGHT palette ---- */
--l-bg:#f3f3f4; --l-surface:#ffffff; --l-surface-2:#f6f6f7; --l-surface-3:#fbfbfc; --l-bg:#f3f3f4; --l-surface:#ffffff; --l-surface-2:#f6f6f7; --l-surface-3:#fbfbfc;
--l-border:#e5e5e8; --l-border-2:#d6d6da; --l-border:#e5e5e8; --l-border-2:#d6d6da;
--l-text:#18181b; --l-text-muted:#6b6b73; --l-text-faint:#9a9aa2; --l-text:#18181b; --l-text-muted:#6b6b73; --l-text-faint:#9a9aa2;
--l-accent:oklch(0.52 0.12 256); --l-accent-bg:oklch(0.96 0.03 256); --l-accent:#3669ac; --l-accent-bg:#e5f3ff;
--l-accent-bd:oklch(0.82 0.07 256); --l-focus:oklch(0.52 0.14 256); --l-accent-bd:#a7c7f2; --l-focus:#2a68b7;
--l-pos:oklch(0.54 0.13 150); --l-pos-bg:oklch(0.95 0.03 150); --l-pos-bd:oklch(0.83 0.07 150); --l-pos:#258343; --l-pos-bg:#e1f5e4; --l-pos-bd:#a8d5b0;
--l-neg:oklch(0.55 0.18 25); --l-neg-bg:oklch(0.96 0.03 25); --l-neg-bd:oklch(0.85 0.08 25); --l-neg:#c53637; --l-neg-bg:#ffebe8; --l-neg-bd:#febab4;
--l-warn:oklch(0.55 0.12 75); --l-warn-bg:oklch(0.96 0.04 85); --l-warn-bd:oklch(0.84 0.08 85); --l-warn:#9a6500; --l-warn-bg:#fef0d4; --l-warn-bd:#e3c78e;
--l-info:oklch(0.54 0.12 245);--l-info-bg:oklch(0.96 0.03 245);--l-info-bd:oklch(0.83 0.07 245); --l-info:#2274af;--l-info-bg:#e2f5ff;--l-info-bd:#a2cdf2;
/* ---- raw DARK palette ---- */ /* ---- raw DARK palette ---- */
--d-bg:#0d0d0f; --d-surface:#161619; --d-surface-2:#1d1d22; --d-surface-3:#1a1a1e; --d-bg:#0d0d0f; --d-surface:#161619; --d-surface-2:#1d1d22; --d-surface-3:#1a1a1e;
--d-border:#2a2a31; --d-border-2:#3a3a43; --d-border:#2a2a31; --d-border-2:#3a3a43;
--d-text:#ededf0; --d-text-muted:#9b9ba6; --d-text-faint:#6a6a74; --d-text:#ededf0; --d-text-muted:#9b9ba6; --d-text-faint:#6a6a74;
--d-accent:oklch(0.70 0.12 256); --d-accent-bg:oklch(0.32 0.06 256); --d-accent:#6ba0e8; --d-accent-bg:#1d3351;
--d-accent-bd:oklch(0.48 0.08 256); --d-focus:oklch(0.74 0.13 256); --d-accent-bd:#3f5f8a; --d-focus:#72adfb;
--d-pos:oklch(0.74 0.14 150); --d-pos-bg:oklch(0.30 0.05 150); --d-pos-bd:oklch(0.45 0.07 150); --d-pos:#62c37a; --d-pos-bg:#1a3520; --d-pos-bd:#376040;
--d-neg:oklch(0.74 0.16 25); --d-neg-bg:oklch(0.30 0.07 25); --d-neg-bd:oklch(0.47 0.10 25); --d-neg:#ff7e76; --d-neg-bg:#4b1d1b; --d-neg-bd:#8a423d;
--d-warn:oklch(0.80 0.13 85); --d-warn-bg:oklch(0.31 0.05 80); --d-warn-bd:oklch(0.48 0.07 80); --d-warn:#e4b750; --d-warn-bg:#3e2d10; --d-warn-bd:#72592c;
--d-info:oklch(0.74 0.12 245);--d-info-bg:oklch(0.31 0.06 245);--d-info-bd:oklch(0.47 0.08 245); --d-info:#65b2f1;--d-info-bg:#12334c;--d-info-bd:#305f85;
/* ---- active mapping: default = LIGHT ---- */ /* ---- active mapping: default = LIGHT ---- */
--bg:var(--l-bg); --surface:var(--l-surface); --surface-2:var(--l-surface-2); --surface-3:var(--l-surface-3); --bg:var(--l-bg); --surface:var(--l-surface); --surface-2:var(--l-surface-2); --surface-3:var(--l-surface-3);
+15
View File
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
const cssDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "public", "css");
// A custom property holding oklch() passes unchecked on an engine below Chromium 111, so the
// var() that reads it is invalid at computed-value time: white text on no background.
for (const file of readdirSync(cssDir).filter((f) => f.endsWith(".css"))) {
test(`${file} declares no oklch() colour`, () => {
assert.ok(!/oklch\(/i.test(readFileSync(join(cssDir, file), "utf8")));
});
}
+9 -9
View File
@@ -6,14 +6,14 @@
`Bar = 1` `Bar = 1`
`Next ID = 35` `Next ID = 36`
## Items ## Items
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|---|---|
| 30 | 0.4.21 | | defect | **Give every `oklch()` colour a fallback, so primary buttons stay readable on an engine without it.** | 1 | 2 | 2 | 3 | 2 | 4 by 5 | 29.9 |
| 29 | 0.4.21 | | defect | **Show the whole nav tree on an engine without `:has()`, and fold it only where `:has()` works.** | 2 | 2 | 2 | 4 | 3 | 4 by 6 | 26.9 | | 29 | 0.4.21 | | defect | **Show the whole nav tree on an engine without `:has()`, and fold it only where `:has()` works.** | 2 | 2 | 2 | 4 | 3 | 4 by 6 | 26.9 |
| 35 | 0.4.21 | | defect | **Raise the contrast of the dark-mode primary button's white text and of the light-mode status badges' text to 4.5:1.** | 1 | 2 | 1 | 2 | 2 | 6 by 3 | 18.0 |
| 31 | 0.4.21 | | | **Delete the design-preview rules in `public/css/auth.css` that no view uses.** | 1 | 1 | 1 | 1 | 2 | code health by 2, 8 by 1 | 13.3 | | 31 | 0.4.21 | | | **Delete the design-preview rules in `public/css/auth.css` that no view uses.** | 1 | 1 | 1 | 1 | 2 | code health by 2, 8 by 1 | 13.3 |
| 26 | 0.4.21 | | | **Add a domain glossary and the project's intended horizon to the README.** | 1 | 2 | 1 | 2 | 3 | 9 by 3, code health by 2 | 11.9 | | 26 | 0.4.21 | | | **Add a domain glossary and the project's intended horizon to the README.** | 1 | 2 | 1 | 2 | 3 | 9 by 3, code health by 2 | 11.9 |
| 8 | 0.4.21 | | | **Run the visual E2E specs at viewports from a large desktop down to a small phone.** | 2 | 1 | 1 | 2 | 4 | code health by 3 | 9.0 | | 8 | 0.4.21 | | | **Run the visual E2E specs at viewports from a large desktop down to a small phone.** | 2 | 1 | 1 | 2 | 4 | code health by 3 | 9.0 |
@@ -21,7 +21,7 @@
| 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 | | 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 |
| 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 | | 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 |
| 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 | | 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 |
| 32 | 0.4.21 | 29, 30 | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 | | 32 | 0.4.21 | 29 | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 |
| 27 | 0.4.21 | | | **Inset the scheduling example's lead text, shift count, alerts and call-to-action by `--pad-x`, like the bars around them.** | 1 | 2 | 1 | 2 | 1 | 9 by 2, 2 by 1 | 4.8 | | 27 | 0.4.21 | | | **Inset the scheduling example's lead text, shift count, alerts and call-to-action by `--pad-x`, like the bars around them.** | 1 | 2 | 1 | 2 | 1 | 9 by 2, 2 by 1 | 4.8 |
| 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 | | 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 |
| 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 | | 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
@@ -42,12 +42,6 @@
## Details ## Details
### 30. Give every `oklch()` colour a fallback, so primary buttons stay readable on an engine without it.
The accent, focus and status tokens in `public/css/styles.css` are `oklch()`, which needs Chromium
111. Below it (Tizen 2024, webOS TV 24) `--accent` is invalid, so `.btn-primary` is white text on
no background: the sign-in button reads as blank.
### 29. Show the whole nav tree on an engine without `:has()`, and fold it only where `:has()` works. ### 29. Show the whole nav tree on an engine without `:has()`, and fold it only where `:has()` works.
README → "Two browser floors": Chromium 87 must reach every page. `public/css/styles.css` hides README → "Two browser floors": Chromium 87 must reach every page. `public/css/styles.css` hides
@@ -55,6 +49,12 @@ README → "Two browser floors": Chromium 87 must reach every page. `public/css/
no nested entry can be reached. Hide the children only inside `@supports selector(:has(a))`. The no nested entry can be reached. Hide the children only inside `@supports selector(:has(a))`. The
theme switch is inert there too, since every theme rule is a `:has()`: hide it under the same guard. theme switch is inert there too, since every theme rule is a `:has()`: hide it under the same guard.
### 35. Raise the contrast of the dark-mode primary button's white text and of the light-mode status badges' text to 4.5:1.
Measured in `public/css/styles.css`: white text on the dark-mode primary button (`--d-accent`
`#6ba0e8`) is 2.69:1, and the text of the light-mode pos, warn and info badges on their backgrounds 4.17, 4.40 and 4.48:1. WCAG AA
needs 4.5:1 for normal text.
### 31. Delete the design-preview rules in `public/css/auth.css` that no view uses. ### 31. Delete the design-preview rules in `public/css/auth.css` that no view uses.
`.auth-view`, the `:target` screen switch, `.tpl-*`, the `fstate`/`rstate` radios and the `.auth-view`, the `:target` screen switch, `.tpl-*`, the `fstate`/`rstate` radios and the