revert(tasks-ui): drop the shadcn/Tailwind layer and return to the plugin's own UI system
shadcn's defaults are built for a standalone app: 14px body, 36px controls, solid primary fills and elevation read as a foreign surface next to this plugin's own tabs (files, changes, side chat, settings), which are 12/11/10px type, 28px controls, hairline borders and flat panels. Retuning the layer did not converge, so the layer goes. - Removed: src/client/ui/** (21 vendored components), components.json, scripts/ui-css.mjs, the Tailwind/PostCSS compile step in tsdown, and the deps tailwindcss / @tailwindcss/postcss / postcss / radix-ui / class-variance-authority / tailwind-merge. (diff and simple-icons stay: they patch undeclared bare imports of the host primitives, unrelated to shadcn.) - The seven page surfaces are back on the plugin's own base: *.module.css with --dsw-* tokens plus the host primitives (Button/Menu/Modal/Pill/Tag/Switch/ Input/Tooltip/StateDot/MarkdownText). Behaviour, data-* hooks, aria labels, i18n keys and the AnchoredPopover geometry/drag/dismiss contract are unchanged; the non-styling fixes landed during the shadcn era (model id dedupe, task-window CAS, drawer arming, popover dragging, the chunk packaging glob) were re-applied and are covered by the existing specs. - The tasks page returns to the core bundle: `tasks` left CHUNK_NAMES / CHUNKS / ChunkName, src/client/chunks/tasks.tsx is gone and tabs.tsx imports SubagentView directly. - The reverted stylesheet was retuned to the plugin's measured vocabulary (12/11/10px, 28px controls, border-l2 with border-l1 hairlines, hover fills, no static shadows, --dsw-shadow-lv2/3 on floating layers only) and verified against a 70-assertion real-browser contract; the harness's own two measurement defects were fixed with a falsification test proving the assertions still catch injected regressions (56/70, 14 FAIL). - Tests: ui-foundation / ui-bundle / ui-shadows (the shadcn guards) deleted, theme.spec scoped back to the plugin's own stylesheets, bundle-route and manifest-consistency chunk lists back to four. Docs: AGENTS.md and the plugin guide drop the Tailwind/shadcn guidance; the design doc marks those sections superseded and records why the layer was removed. Verified: typecheck/lint clean; 133 files / 1410 tests; core bundle 987.3 KiB with no radix-ui/--tw-/tailwind strings and no client-tasks.js; mount e2e 7/7; 20 real-browser screenshots at 360/720 for graph, tree, board, task window, drawer (including armed), both popovers and the 77-node fixture.
This commit is contained in:
+43
-3
@@ -14,7 +14,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import { colorAlpha, effectiveTokenValue, tokenValue } from '../src/client/theme.ts'
|
||||
|
||||
afterEach(() => {
|
||||
@@ -91,12 +91,31 @@ describe('effectiveTokenValue', () => {
|
||||
* `FileTypeIcon` artwork from a platform module (the host owns those pixels
|
||||
* and its own palette), and everything the plugin renders around them —
|
||||
* including the colored tab glyphs — rides theme tokens. So the guard is
|
||||
* simply that no plugin module carries a color literal, and that no icon
|
||||
* dataset sneaked back in as a chunk.
|
||||
* simply that no plugin module carries a color literal, that every module
|
||||
* stylesheet paints `color` from a token, and that no icon dataset sneaked
|
||||
* back in as a chunk.
|
||||
*/
|
||||
// jsdom has no file:// import.meta.url; vitest runs from the repo root.
|
||||
const ROOT = process.cwd()
|
||||
|
||||
/** `color` as a property — never `background-color` / `scrollbar-color` / `-webkit-text-fill-color`. */
|
||||
const COLOR_PROPERTY = /(?:^|[;{\s])color\s*:\s*([^;}]+)/g
|
||||
|
||||
/** Values that name no paint of their own and are therefore exempt. */
|
||||
const INERT_COLOR = /^(?:inherit|currentcolor|transparent)$/i
|
||||
|
||||
/** A token allowed by the contract: the plugin's own `--dsw-*` or the host's `--ds-*`. */
|
||||
function isThemeToken(name: string): boolean {
|
||||
return name.startsWith('dsw-') || name.startsWith('ds-')
|
||||
}
|
||||
|
||||
/** Every `*.module.css` sheet under `src/client/`, as repo-relative POSIX paths. */
|
||||
function moduleStylesheets(): string[] {
|
||||
return readdirSync(resolve(ROOT, 'src/client'), { recursive: true, encoding: 'utf8' })
|
||||
.filter(name => name.endsWith('.module.css'))
|
||||
.map(name => `src/client/${name.split(sep).join('/')}`)
|
||||
}
|
||||
|
||||
describe('skin contract: the plugin owns no color of its own', () => {
|
||||
it('the icon modules carry no color literals', () => {
|
||||
for (const file of ['src/client/file-icons.tsx', 'src/client/icons.tsx']) {
|
||||
@@ -121,6 +140,27 @@ describe('skin contract: the plugin owns no color of its own', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('every module stylesheet paints `color` from a theme token', () => {
|
||||
const sheets = moduleStylesheets()
|
||||
// Guard the scan itself: a glob that silently matches nothing would make
|
||||
// this contract vacuous.
|
||||
expect(sheets.length).toBeGreaterThan(5)
|
||||
for (const file of sheets) {
|
||||
const styles = readFileSync(resolve(ROOT, file), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
for (const declaration of styles.matchAll(COLOR_PROPERTY)) {
|
||||
const value = declaration[1]!.trim()
|
||||
// `color: inherit` / `currentcolor` / `transparent` is the plugin
|
||||
// deliberately taking the surrounding color, not painting one.
|
||||
if (INERT_COLOR.test(value)) continue
|
||||
// Dereference every var() — `color-mix()` chains are fine as long as
|
||||
// the whole expression bottoms out in theme tokens.
|
||||
const tokens = [...value.matchAll(/var\(--([a-z0-9-]+)/gi)].map(match => match[1]!)
|
||||
expect(tokens.length, `${file}: color: ${value}`).toBeGreaterThan(0)
|
||||
for (const name of tokens) expect(isThemeToken(name), `${file}: color: ${value}`).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('the empty-pane welcome capsule follows the host guide recipe', () => {
|
||||
// The card is the same surface as DSH's own guide capsule ("pick what
|
||||
// opens here"), so this pins the shared geometry and the token-only
|
||||
|
||||
Reference in New Issue
Block a user