feat(tasks-ui): migrate the Tasks page to shadcn/ui (Tailwind v4 + radix, lazy chunk)
Visual base replacement for the Tasks page after the 'hard shadows make a dense graph unreadable' review. Behaviour, hooks, aria labels and i18n are unchanged; the base layer is not. - FOUNDATION: Tailwind v4 wired into tsdown's css-inline plugin (theme + utilities ONLY — preflight is excluded because the plugin's stylesheet is a global <style data-plugin> tag and would repaint the host page), shadcn tokens bridged to --dsw-* so the DSH theme keeps flipping light/dark with no dark: overrides, cn() util, components.json for the CLI, src/client/ui/. - COMPONENTS: shadcn's component source vendored into src/client/ui (button, card, badge, separator, input, textarea, tooltip, popover, dropdown-menu, scroll-area, collapsible, toggle-group, skeleton, spinner, empty) with a documented local diff: lucide-react replaced by host IconXxx (no new icon dep), shadow-* dropped from non-floating variants, unused variants pruned, and React.forwardRef restored on Button — the host runs React 18, where radix's Slot cannot hand a ref to a plain function component (dev warns, production silently mis-anchors the floating layer). - SHADOWS: static panels are now hairline + surface tiers with hover:bg-muted; shadows exist only on floating layers. Settled nodes recede by ink (text-muted-foreground/foreground-3) instead of opacity. - DENSITY: node cards widened 132 -> 150px with a two-line clamped title (13px text in a 132px card left ~7 CJK characters), reserved height 46 -> 60. - CHUNK: the whole page (graph, tree, task window, board, drawer, shadcn/radix layer and its stylesheet) moved into the lazy 'tasks' chunk. Core bundle 988.5 -> 835.6 KiB (-153 KiB); the 709.8 KiB UI payload loads only when the Tasks tab opens. - MODEL: workflow members whose childId is already a real node elsewhere are no longer synthesized into a duplicate id (React keys/edges), with a last-resort per-id dedupe. - GUARDS: tests/ui-foundation.spec.ts, tests/ui-bundle.spec.ts (core/chunk split + size budget + no preflight/oklch/lucide), tests/ui-shadows.spec.ts, extended tests/theme.spec.ts (no colour literal / palette class in src/client/ui/** and the migrated files). - REVIEW FIXES: portal popovers re-scoped under .dsw-tasks, graph loading state, 11px floor, error nodes no longer tint a whole border, TeamBoard's delete item is neutral (the window owns the two-step confirm). Verified: typecheck / lint / 136 files 1502 tests / build bundle assertions / mount e2e 7-7; browser harness 65-65 checks with 0 console errors.
This commit is contained in:
@@ -191,3 +191,44 @@ describe('tasksEdges', () => {
|
||||
expect(kindOf('m1')).toBe('workflow')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildTasksModel: duplicate workflow members', () => {
|
||||
it('never synthesizes a second node for a member another parent already lists', () => {
|
||||
// `m1` is a real child of `a`, but a run started by `root` names it as a
|
||||
// member — the node must stay single (React keys / edge map depend on it)
|
||||
// and the run simply does not duplicate it on the canvas.
|
||||
const audit = run({
|
||||
originSessionId: 'root',
|
||||
phases: [{ title: '侦察', members: [{ seq: 1, label: 'm1', childId: 'm1', outcome: 'completed' }] }],
|
||||
})
|
||||
const model = buildTasksModel(input({
|
||||
catalogs: {
|
||||
root: catalog([child('a')]),
|
||||
a: catalog([child('m1', { activity: 'inactive' })]),
|
||||
},
|
||||
byId: { root: summary('root'), a: summary('a'), m1: summary('m1') },
|
||||
runs: [audit],
|
||||
folded: false,
|
||||
}))
|
||||
const ids = model.map(node => node.id)
|
||||
expect(ids.filter(id => id === 'm1')).toHaveLength(1)
|
||||
expect(new Set(ids).size).toBe(ids.length)
|
||||
// The real node keeps its catalog parent.
|
||||
expect(model.find(node => node.id === 'm1')?.parentId).toBe('a')
|
||||
})
|
||||
|
||||
it('still synthesizes a member no catalog knows', () => {
|
||||
const audit = run({
|
||||
originSessionId: 'root',
|
||||
phases: [{ title: '侦察', members: [{ seq: 1, label: 'ghost member', childId: 'ghost' }] }],
|
||||
})
|
||||
const model = buildTasksModel(input({
|
||||
catalogs: { root: catalog([]) },
|
||||
byId: { root: summary('root') },
|
||||
runs: [audit],
|
||||
folded: false,
|
||||
}))
|
||||
const ghost = model.find(node => node.id === 'ghost')
|
||||
expect(ghost).toMatchObject({ parentId: 'run:run-1', synthesized: true })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user