fix(web): an error cell must not lose its colour to the dimming or the selection
Found by reviewing the restyling before pushing it, and demonstrated before it was fixed. `td.error` and `td:not(.editable)` used to set DIFFERENT properties — colour and opacity — so both applied and a dimmed error cell was a dimmed red. The restyling made the dimming a colour too, at the same specificity and below `td.error` in the file, so an error in an aggregate cell rendered plain grey: rgb(107, 103, 95) where the error colour is rgb(168, 50, 45). `td.selected` and `td.typing` did the same by setting `color: var(--fg)`. `#grid td.error` is the only place on 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. So the dimming moves 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 — two rules that were independent become one property, and nothing in any language fails. Also: an expanded notice now caps at 40vh and scrolls. `main` is free to shrink to nothing, so on a short viewport opening it could have pushed the grid out entirely. The test is aimed at the STYLESHEET rather than at a document, because the question is which rule wins and a fixture per combination would be three documents testing three documents. It appends a <td> carrying each combination to the real page's real table and compares two controls taken the same way, rather than pinning absolute colours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 `<td>` 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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+12
-8
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user