design-platform.css declared four --dsw-alias-scrollbar-* tokens in both palettes that no rule read, so every scrolling region rendered the user agent's own scrollbar and the dark theme showed a light native bar against dark surfaces. The symptom that surfaced the gap was in the sidebar: the workspace browser's session list is its only scrolling region, and each row's trailing content (the relative timestamp, and the hover action buttons that replace it) is `flex: none` flush against the row's 8px right padding, so an overlaid scrollbar painted on top of the timestamp. ui-theme/styles/scrollbar.css becomes the sole consumer of the four tokens, imported by the web shell's base.css after design-platform.css because it reads that sheet's tokens. The rules sit on `body`, not `html`: the alias tokens are declared on `body`, custom properties inherit only downward, and from `html` they resolve to the guaranteed-invalid value with scrollbar-color computing to `auto`. scrollbar-width and scrollbar-color are declared on `body, body *` rather than inherited, because inheritance would carry the color already substituted at `body` and an elevated surface could not retint its own thumb; scrollbar-width does not inherit at all. Both the standard properties and the ::-webkit-scrollbar pseudo-elements read one indirection pair bound to the l1 tokens, so an elevated surface rebinds that pair to the l2 tokens once and retints both renderings. The command popup, slash menu, model-select panel, and settings panel do so, which gives the l2 tokens their first consumers. WorkspaceBrowser's `.list` declares scrollbar-gutter: stable, keeping the bar beside the rows. `stable` rather than `auto` so the reservation holds when the list is short enough not to scroll: expanding a workspace group would otherwise shift every row sideways at the moment it starts scrolling.
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
/**
|
|
* WorkspaceBrowser scroll-region style contract, asserted against the CSS text
|
|
* on disk: the session list reserves its scrollbar gutter so the scrollbar
|
|
* cannot overlay row trailing content, and reserves it whether or not the list
|
|
* currently overflows so expanding a group does not shift rows sideways.
|
|
*/
|
|
import { readFileSync } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const css = readFileSync(fileURLToPath(new URL('../src/client/WorkspaceBrowser.module.css', import.meta.url)), 'utf8')
|
|
|
|
/**
|
|
* Declarations of one class rule, keyed by property with whitespace collapsed.
|
|
* Declaration order and trailing semicolons are normalized away.
|
|
* @param className - local class name, without the leading dot.
|
|
* @returns the rule's declarations, or undefined when no such rule exists.
|
|
*/
|
|
function declarations(className: string): Map<string, string> | undefined {
|
|
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
const match = new RegExp(String.raw`(^|[\s,}])\.${className}\s*\{([^{}]*)\}`).exec(withoutComments)
|
|
if (match === null) return undefined
|
|
const found = new Map<string, string>()
|
|
// The body group is unconditional in the pattern; the fallback only satisfies
|
|
// noUncheckedIndexedAccess.
|
|
for (const part of (match[2] ?? '').split(';')) {
|
|
const colon = part.indexOf(':')
|
|
if (colon === -1) continue
|
|
found.set(part.slice(0, colon).trim(), part.slice(colon + 1).trim().replace(/\s+/g, ' '))
|
|
}
|
|
return found
|
|
}
|
|
|
|
describe('WorkspaceBrowser.module.css list', () => {
|
|
const list = declarations('list')
|
|
|
|
it('is the scrolling region', () => {
|
|
expect(list).toBeDefined()
|
|
expect(list!.get('overflow-y')).toBe('auto')
|
|
})
|
|
|
|
it('reserves the scrollbar gutter unconditionally', () => {
|
|
// Row trailing content sits flush against the row's right padding, so an
|
|
// overlay scrollbar covers it. `stable` keeps the reservation when the list
|
|
// is short enough not to scroll, so expanding a group does not shift rows.
|
|
expect(list!.get('scrollbar-gutter')).toBe('stable')
|
|
})
|
|
})
|