diff --git a/context/code/clients.md b/context/code/clients.md index 96080d7c..ac7c8ee5 100644 --- a/context/code/clients.md +++ b/context/code/clients.md @@ -1726,6 +1726,18 @@ stylesheet rather than the page working out which row is selected. And `.notice-toggle`'s glyph is chosen by `[open]`, which is the one piece of state on this page that genuinely is the browser's and that nothing else knows. +**One rule in it is load-bearing and is the stylesheet's own failure mode.** +`td.error` is the only place on this page where colour carries *meaning* +rather than emphasis — a circular reference or a `mixed` aggregate reading as +an ordinary number is worse than it reading as nothing. It used to coexist with +the dimming of unsettable cells because they set **different** properties +(`color` against `opacity`); once both are `color` at the same specificity, +only source order separates them, and nothing in any language fails when the +order is wrong. So the dimming sits **above** the semantic colours, and a +selection is a ring and a tint and never a colour. That is the class of defect +a stylesheet has and a program does not, and it is why the one assertion this +file makes about colour is about which rule wins rather than about a value. + **The harness cannot see any of this**, and that is the honest scope. Playwright asserts what a browser can answer: that the notice starts collapsed and remembers, that it opens from the keyboard, and that the minibuffer is inside diff --git a/context/test-evidence/clients.md b/context/test-evidence/clients.md index 2337db63..83a74129 100644 --- a/context/test-evidence/clients.md +++ b/context/test-evidence/clients.md @@ -3257,3 +3257,37 @@ body's before and is now strictly larger. The precondition matters: `#mode` must read `INSERT` before anything is measured, because `getBoundingClientRect` on a hidden element returns zeroes and every comparison below would then be about the origin. + +### "an error cell stays an error cell when it is also dimmed or selected" + +Found reviewing the styling change that introduced it, before it was pushed. +`td.error` and `td:not(.editable)` used to set **different** properties — +`color: var(--error)` and `opacity: 0.7` — so both applied and a dimmed error +cell was a dimmed red. The restyling made the dimming a `color` too, at the +same specificity, and put it **after** `td.error` in the file, so an error in +an aggregate cell rendered plain grey: measured `rgb(107, 103, 95)` where the +error colour is `rgb(168, 50, 45)`. `td.selected` and `td.typing` had the same +effect by setting `color: var(--fg)`, which is how a *selected* error lost its +colour as well. + +That is the whole class of defect a stylesheet has and a program does not: +two rules that were independent become one property, and nothing in any +language fails. `#grid td.error` is the only place in this page where colour +carries meaning rather than emphasis, and it is the one a reader takes at face +value — a circular reference or a `mixed` aggregate reading as an ordinary +number is worse than it reading as nothing. + +The fix is ordering plus a rule: the dimming goes **above** the semantic +colours, and a selection is a ring and a tint and never a colour. The test is +aimed at the stylesheet rather than at a document, deliberately — the question +is which rule wins, and a fixture per combination (a dimmed error, a selected +error, an error being typed over) would be three documents testing three +documents. It appends a `` carrying each combination to the real page's +real table, reads `getComputedStyle`, and compares against two controls taken +the same way: a plain unsettable cell (what it must not be) and an error the +dimming cannot reach (what it must be). Reading absolute colours instead would +pin the palette, which is not the property. + +Inject the fault by moving the `td:not(.editable)` block back below +`td.error`, or by restoring either `color: var(--fg)`; each makes exactly the +corresponding assertion fail. diff --git a/harness/tests/contract.spec.js b/harness/tests/contract.spec.js index 95f3aa6e..b679cfac 100644 --- a/harness/tests/contract.spec.js +++ b/harness/tests/contract.spec.js @@ -505,3 +505,48 @@ test.describe("the shell puts the application first", () => { await expect(page.locator("#minibuffer")).toBeHidden(); }); }); + +// The stylesheet's own cascade, which no other test here can reach: an +// error is a colour now, where it used to be a colour AND an opacity, so +// the order of two equally-specific rules decides it. See +// context/test-evidence/clients.md. +test.describe("the grid's semantic colours survive the cascade", () => { + test("an error cell stays an error cell when it is also dimmed or selected", async ({ + page, + }) => { + await page.goto("/app/index.html?doc=error-colour.improv"); + await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 }); + + // The question is about the STYLESHEET, so the combinations are put to it + // directly: the document that produces each of them is a separate matter, + // and a fixture per combination would test the fixture. + const colours = await page.evaluate(() => { + const row = document.querySelector("#grid tbody tr"); + const read = (classes) => { + const td = document.createElement("td"); + td.className = classes; + td.textContent = "#err"; + row.appendChild(td); + const colour = getComputedStyle(td).color; + td.remove(); + return colour; + }; + return { + // An aggregate error, which is the common case: `editable` is false + // for every cell that is not a stored record. + dimmed: read("error"), + selected: read("error selected"), + typing: read("error typing"), + // What "dimmed" must NOT be, read off a plain unsettable cell. + dim: read(""), + // …and what it must be, read off an error the dimming cannot reach. + error: read("error editable"), + }; + }); + + expect(colours.error).not.toBe(colours.dim); + expect(colours.dimmed).toBe(colours.error); + expect(colours.selected).toBe(colours.error); + expect(colours.typing).toBe(colours.error); + }); +}); diff --git a/web/style.css b/web/style.css index 98f55683..ce0e8b9d 100644 --- a/web/style.css +++ b/web/style.css @@ -203,6 +203,10 @@ header h1 { } .notice-body { + /* Expanded, this must not take the whole window: `main` is free to shrink + to nothing, so a short viewport would lose the grid entirely. */ + max-height: 40vh; + overflow: auto; padding: 0 0.9rem 0.6rem; max-width: 100ch; color: var(--dim); @@ -391,6 +395,14 @@ td.numeric { text-align: right; } +/* Everything else on the grid is an aggregate, and dimming it is what makes + the settable cells read as the surface you can act on. */ +td:not(.editable) { + color: var(--dim); +} + +/* …and the semantic colours come AFTER it, because they are now the same + property at the same specificity and only the order separates them. */ td.error { color: var(--error); font-weight: 600; @@ -405,12 +417,6 @@ td.editable:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); } -/* Everything else on the grid is an aggregate, and dimming it is what makes - the settable cells read as the surface you can act on. */ -td:not(.editable) { - color: var(--dim); -} - /* The selected cell, and the row header that names it. `:has()` is what lets the header light up without the page deriving which row is selected — the selection is already on the cell, decided in Rust. */ @@ -418,7 +424,6 @@ td.selected { background: var(--accent-soft); outline: 2px solid var(--accent); outline-offset: -2px; - color: var(--fg); font-weight: 700; } @@ -434,7 +439,6 @@ td.typing { outline: 2px solid var(--accent); outline-offset: -2px; background: var(--accent-soft); - color: var(--fg); font-weight: 700; }