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.
59 lines
2.5 KiB
TypeScript
59 lines
2.5 KiB
TypeScript
/**
|
|
* Shell base sheet contract, asserted against the CSS text on disk: base.css is
|
|
* where the ui-theme token sheets enter the bundle, every sheet it names exists,
|
|
* and scrollbar.css follows design-platform.css because it reads that sheet's
|
|
* tokens.
|
|
*/
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const THEME_PACKAGE = '@deepseek-ai/dsh-client-ui-theme'
|
|
const baseCss = readFileSync(fileURLToPath(new URL('../src/base.css', import.meta.url)), 'utf8')
|
|
|
|
/**
|
|
* Import specifiers of the sheet, in source order. Quote style and surrounding
|
|
* whitespace are normalized away.
|
|
* @param css - stylesheet text.
|
|
* @returns each `@import` target in the order the sheet lists it.
|
|
*/
|
|
function importOrder(css: string): string[] {
|
|
// The destructuring default only satisfies noUncheckedIndexedAccess; the
|
|
// group is unconditional in the pattern.
|
|
return [...css.matchAll(/@import\s+['"]([^'"]+)['"]/g)].map(([, specifier = '']) => specifier)
|
|
}
|
|
|
|
/**
|
|
* Resolve a `<package>/styles/<file>` specifier to its path in the workspace.
|
|
* The theme package maps `./styles/*` to `./src/styles/*`, so the sheets stay
|
|
* on the source plane rather than needing a build.
|
|
* @param specifier - import specifier from base.css.
|
|
* @returns absolute path of the file the specifier names.
|
|
*/
|
|
function resolveThemeSheet(specifier: string): string {
|
|
const name = specifier.slice(`${THEME_PACKAGE}/styles/`.length)
|
|
return fileURLToPath(new URL(`../../ui-theme/src/styles/${name}`, import.meta.url))
|
|
}
|
|
|
|
const imports = importOrder(baseCss)
|
|
|
|
describe('web shell base.css', () => {
|
|
it('imports every sheet from the theme package and each one exists', () => {
|
|
expect(imports.length).toBeGreaterThan(0)
|
|
for (const specifier of imports) {
|
|
expect(specifier.startsWith(`${THEME_PACKAGE}/styles/`), specifier).toBe(true)
|
|
expect(existsSync(resolveThemeSheet(specifier)), specifier).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('imports the scrollbar sheet after the token sheet it reads', () => {
|
|
// Both sheets bind on `body`, so with scrollbar.css first the alias tokens
|
|
// would still resolve; the order encodes the dependency direction so a
|
|
// later specificity or selector change cannot silently invert it.
|
|
const platform = imports.indexOf(`${THEME_PACKAGE}/styles/design-platform.css`)
|
|
const scrollbar = imports.indexOf(`${THEME_PACKAGE}/styles/scrollbar.css`)
|
|
expect(platform).toBeGreaterThanOrEqual(0)
|
|
expect(scrollbar).toBeGreaterThan(platform)
|
|
})
|
|
})
|