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
|
`.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.
|
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
|
**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
|
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
|
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
|
The precondition matters: `#mode` must read `INSERT` before anything is
|
||||||
measured, because `getBoundingClientRect` on a hidden element returns zeroes
|
measured, because `getBoundingClientRect` on a hidden element returns zeroes
|
||||||
and every comparison below would then be about the origin.
|
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();
|
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 {
|
.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;
|
padding: 0 0.9rem 0.6rem;
|
||||||
max-width: 100ch;
|
max-width: 100ch;
|
||||||
color: var(--dim);
|
color: var(--dim);
|
||||||
@@ -391,6 +395,14 @@ td.numeric {
|
|||||||
text-align: right;
|
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 {
|
td.error {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -405,12 +417,6 @@ td.editable:hover {
|
|||||||
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
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 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
|
the header light up without the page deriving which row is selected — the
|
||||||
selection is already on the cell, decided in Rust. */
|
selection is already on the cell, decided in Rust. */
|
||||||
@@ -418,7 +424,6 @@ td.selected {
|
|||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
color: var(--fg);
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +439,6 @@ td.typing {
|
|||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--fg);
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user