fix(client): rebind the scrollbar indirection on three missed elevated surfaces
Review found three scroll containers sitting on surfaces the rebinding contract covers, none of which rebound: ui-primitives' shared Menu card on --dsw-specific-menu (the surface PopupSelectView already rebinds for), and the composer input and question composer cards, both on --dsw-specific-input-major. Each rendered the l1 thumb, which differs from l2 only in the dark palette and only on that surface, so a light-palette screenshot and a code read both look correct. Adds the mechanical check that would have caught them instead of leaving it to inspection: a sheet that scrolls somewhere and paints a known elevated surface somewhere must rebind. The elevated set is derived from the sheets that already rebind, since a rebinding rule paints the surface whose elevation it declares, so a new elevated surface joins the set by rebinding rather than by anyone updating a list. Surface-level rather than element-level because the card and the descendant that scrolls are separate rules and CSS text does not say which contains which. Verified by reverting each of the three fixes in turn: the check names the sheet and the surface every time. Also commits snapshots/sidebar-scrollbar/geometry.expected.md, the resolved scrollbar style and geometry in both palettes. The aria goldens the other web scenarios commit cannot carry a CSS-only change, since it alters no DOM and no accessible name and leaves their trees byte-identical. Absolute coordinates stay out: they track font metrics and the laid-out sidebar width, so committing them would document the platform and force a per-platform re-record.
This commit is contained in:
+5
-1
@@ -20,7 +20,9 @@ The rules sit on `body`, not `html`. `design-platform.css` declares the `--dsw-a
|
||||
|
||||
The two renderings are mutually exclusive, and the exclusion is enforced rather than assumed. A non-`auto` `scrollbar-width` or `scrollbar-color` makes Chromium and Safari discard every `::-webkit-scrollbar*` rule for that element, `::-webkit-scrollbar-thumb:hover` included. Declaring both unconditionally therefore leaves the hover token rendering nowhere at all: the engines that implement the hover pseudo-element are exactly the ones the standard properties silence, and Firefox has no hover pseudo-element to fall back on. The standard properties consequently sit inside `@supports not selector(::-webkit-scrollbar)`, which is true only where the pseudo-element is unimplemented, so Firefox takes the standard path and WebKit-based engines take the pseudo-element path. The WebKit rules are not gated in turn: an engine without those pseudo-elements drops them as unknown selectors, so a gate would only restate what selector matching already does. An engine too old for the `selector()` function makes the condition invalid, which evaluates false and selects the pseudo-element path — the correct side for the pre-16.4 Safari that is the realistic case for that reading.
|
||||
|
||||
Both paths read one indirection pair, `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover`, bound on `body` to the l1 (base-surface) tokens. **This is the rebinding contract, and it is the part the CSS alone does not state**: an elevated surface sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container, and that one rebind reaches the standard properties and the WebKit pseudo-elements together. The pair is rebound as a pair; rebinding the resting thumb alone leaves the hover state on the base-surface token. Four surfaces rebind today: the command popup, the slash menu, the model-select panel, and the settings panel. The last two declare it on the elevated panel rather than on the scrolling descendant, because the elevation is a property of the surface and custom properties inherit down to whichever child actually scrolls.
|
||||
Both paths read one indirection pair, `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover`, bound on `body` to the l1 (base-surface) tokens. **This is the rebinding contract, and it is the part the CSS alone does not state**: an elevated surface sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container, and that one rebind reaches the standard properties and the WebKit pseudo-elements together. The pair is rebound as a pair; rebinding the resting thumb alone leaves the hover state on the base-surface token. Seven surfaces rebind today: the command popup, the slash menu, the model-select panel, the settings panel, the shared `ui-primitives` menu card, the composer input card, and the question composer card. Most declare it on the elevated card rather than on the scrolling descendant, because the elevation is a property of the surface and custom properties inherit down to whichever child actually scrolls.
|
||||
|
||||
The last three were missed in the first implementation and found in review, which is why the rebinding contract is now checked mechanically rather than by inspection: a sheet that scrolls somewhere and paints a known elevated surface somewhere must rebind. The elevated set is derived from the sheets that already rebind — a rebinding rule paints the surface whose elevation it declares — so it is self-maintaining rather than a list to update. The check is surface-level rather than element-level because the card and the descendant that scrolls are separate rules, and CSS text does not express which contains which.
|
||||
|
||||
The track and the corner stay transparent, so the thumb reads against whatever surface scrolls under it; only the thumb and its hover state carry a token color.
|
||||
|
||||
@@ -61,6 +63,8 @@ Three unit specs read the CSS text on disk. `ui-theme/tests/scrollbar-styles.spe
|
||||
|
||||
`apps/web/tests/sidebar-scrollbar.e2e.ts` covers the facts only a real engine reports: the reserved band width, and which rendering path the engine took. It needs no model calls — the list only has to overflow — so it seeds cold sessions from an existing committed fixture read-only.
|
||||
|
||||
That scenario also commits a golden, `snapshots/sidebar-scrollbar/geometry.expected.md`, holding the resolved scrollbar style and geometry in both palettes. The aria goldens the other web scenarios commit cannot carry a CSS-only change: it alters no DOM and no accessible name, so their normalized trees are byte-identical with and without it. Recording the resolved values instead makes an unintended shift in thumb colour, band width, or rendering path a reviewable diff rather than a threshold someone has to reason about. Absolute coordinates are deliberately excluded — `timeRight` and the two edges depend on the sidebar's laid-out width and on font metrics, so committing them would produce a fixture that has to be re-recorded per platform and would document the platform rather than the change. What is recorded is the band, the overlap, and two orderings, each a difference or a comparison that survives any layout preserving the reservation.
|
||||
|
||||
Confirmed in headless chromium on the built client by reading computed values, which is what distinguishes a working token chain from a syntactically valid one: a scroll container computes the l1 thumb color in each palette, and a container that rebinds the indirection computes the l2 color, proving the rebind reaches the computed value rather than only the custom property. Firefox was verified the same way for the standard path, including the l1-to-l2 rebind on `scrollbar-color`; headless Firefox reports `scrollbar-width: none` on every element, styled or not, which is a headless artifact rather than an effect of the sheet.
|
||||
|
||||
Two chromium measurement limits shape what the e2e can assert. The gate makes chromium report `scrollbar-width` and `scrollbar-color` as `auto`, so the substituted `scrollbar-color` is no longer the observable — the e2e asserts the `auto` reading deliberately, since a concrete value there would mean the gate leaked and silenced the pseudo-elements. And `getComputedStyle(el, '::-webkit-scrollbar-thumb')` folds in the `::-webkit-scrollbar-thumb:hover` rule, so it reports the hover color at rest and pins neither state; proven by deleting the hover rule through `CSSStyleSheet.deleteRule` in the live page, which flipped that same query from the hover color to the resting one. The e2e therefore reads the resting and hover colors as the indirection variables resolve on the list — one throwaway probe element per variable, because `getComputedStyle` returns a live declaration and a reused probe reports only the last value read — and reads the hover declaration out of the cascade as rule text.
|
||||
|
||||
Reference in New Issue
Block a user