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:
Menghuan1918
2026-09-27 22:18:19 +08:00
parent 7cbfc766a1
commit f6cf73c4cf
43 changed files with 3394 additions and 2048 deletions
+69
View File
@@ -0,0 +1,69 @@
/**
* Canvas-only styles of the workflow graph (TasksGraph.tsx): the dotted grid,
* the transformed node layer, the SVG relation layer and the edge stroke
* recipe. These are the four pieces Tailwind utilities cannot express in the
* markup (a radial-gradient background image, `transform-origin: 0 0` on the
* scaled layer, an overflow-visible SVG stage, and SVG stroke/dash painting) —
* everything else in the graph is a utility class over the shadcn tokens.
*
* Skin contract: every color is a `--dsw-*` token (no literal, no palette
* class), so the canvas follows the host light/dark theme like the rest of the
* page. Relations are 1px hairlines in the border ink — the KIND of a relation
* is its dash pattern and opacity, never a second hue.
*/
.canvas {
position: absolute;
inset: 0;
cursor: grab;
/* Panning must never turn into a text selection or a native scroll. */
touch-action: none;
user-select: none;
/* Graph paper: the dotted grid is part of the language. */
background-image: radial-gradient(circle, var(--dsw-alias-border-l1) 0.6px, transparent 0.6px);
background-size: 16px 16px;
background-position: 0 0;
}
.canvasDragging {
cursor: grabbing;
}
.canvasInner {
position: absolute;
top: 0;
left: 0;
transform-origin: 0 0;
}
.edges {
position: absolute;
top: 0;
left: 0;
overflow: visible;
pointer-events: none;
}
.edge {
fill: none;
stroke: var(--dsw-alias-border-l4);
stroke-width: 1;
opacity: 0.5;
}
/* Team membership: dashed. */
.edgeTeam {
stroke-dasharray: 3 3;
opacity: 0.8;
}
/* Workflow member: solid, one notch stronger than a plain lineage edge. */
.edgeWorkflow {
opacity: 0.75;
}
/* Collapsed aggregate: dotted and faint — it stands for many rows. */
.edgeFold {
stroke-dasharray: 1 4;
opacity: 0.4;
}