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:
@@ -185,6 +185,24 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
|
||||
const out: TasksNode[] = []
|
||||
|
||||
/**
|
||||
* Every session id the catalogs know about, anywhere in the tree. A
|
||||
* workflow member whose `childId` is listed under a DIFFERENT parent (or
|
||||
* whose catalog row is missing from the run's origin) must NOT be
|
||||
* synthesized into a second node with the same id: duplicate ids collide in
|
||||
* React keys and in the edge map. Known ids keep their single real node;
|
||||
* unknown ones are synthesized below.
|
||||
*/
|
||||
const knownAgentIds = new Set<string>()
|
||||
for (const catalog of Object.values(catalogs)) {
|
||||
if (catalog?.state !== 'ready') continue
|
||||
for (const entry of catalog.entries) {
|
||||
if (entry.kind === 'child') knownAgentIds.add(entry.id)
|
||||
}
|
||||
}
|
||||
// The local children lists of `appendChildren` are built lazily per parent,
|
||||
// so this global set is what makes the guard below order-independent.
|
||||
|
||||
/** The agent children of one parent, in catalog order (side threads and
|
||||
* diagnostics excluded), with workflow runs appended in start order. */
|
||||
const appendChildren = (parentId: string): void => {
|
||||
@@ -236,6 +254,9 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
existing.parentId = runNode.id
|
||||
if (tasksByNode.get(existing.id) !== undefined) existing.tasks = tasksByNode.get(existing.id)
|
||||
members.push(existing)
|
||||
} else if (member.childId !== '' && knownAgentIds.has(member.childId)) {
|
||||
// Its real node exists elsewhere in the tree: the run's popover
|
||||
// already lists the member, so no card is duplicated here.
|
||||
} else {
|
||||
const childKnown = member.childId !== ''
|
||||
members.push({
|
||||
@@ -335,7 +356,16 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
|
||||
...(tasksByNode.get(rootId) !== undefined ? { tasks: tasksByNode.get(rootId) } : {}),
|
||||
})
|
||||
appendChildren(rootId)
|
||||
return out
|
||||
|
||||
// Last-resort guard: one node per session id, first occurrence wins. Real
|
||||
// catalogs list a child under exactly one parent, so this only fires on
|
||||
// pathological trees (and keeps React keys unique when it does).
|
||||
const seen = new Set<string>()
|
||||
return out.filter((node) => {
|
||||
if (seen.has(node.id)) return false
|
||||
seen.add(node.id)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/** The parent→child edges of a model (derived, kept out of the build). */
|
||||
|
||||
Reference in New Issue
Block a user