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
+53
View File
@@ -0,0 +1,53 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "./utils"
import { Slot } from "radix-ui"
/**
* Vendored shadcn/ui badge, trimmed to the task page's real inventory.
*
* Removed from upstream (`new-york`): the `ghost` and `link` variants (dead
* class strings in a runtime object, so they would ship). `text-white` became
* the `--destructive-foreground` token, which is what the theme bridge defines
* that role for — no color literal lives in this directory.
*/
const badgeVariants = cva(
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&>svg]:pointer-events-none [&>svg]:size-3",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 [a&]:hover:bg-destructive/90",
outline:
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+76
View File
@@ -0,0 +1,76 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "./utils"
import { Slot } from "radix-ui"
/**
* Vendored shadcn/ui button, trimmed to the task page's real inventory.
*
* Removed from upstream (`new-york`): the `link` variant and the `xs` / `lg` /
* `icon-xs` / `icon-sm` / `icon-lg` sizes. The page's control heights are 28 px
* (`sm`), 36 px (`default`) and the square 36 px icon button; every removed
* class still lived in this runtime object and would have shipped in the bundle.
*/
const buttonVariants = cva(
// LOCAL ADAPTATION: `transition-all` narrowed to the same property list the
// page's other vendored controls use (see ui/toggle.tsx) — `all` also
// animates layout/size changes, which makes resizes and mount transitions
// laggy on a dense task page.
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20",
outline:
"border bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
/**
* LOCAL ADAPTATION (dsh-better-sidebar): `React.forwardRef`, because this
* plugin runs on the DSH host's React 18 (react@^18.2.0 — ref-as-prop is a
* React 19 feature). Without it, `<TooltipTrigger asChild><Button/></…>`
* cannot measure its anchor: radix's `Slot` hands the child a ref and a plain
* function component drops it (dev builds warn, production silently
* mis-positions the floating layer). Upstream ships no forwardRef because the
* registry targets React 19; the shadcn CLI `--diff` flow will try to remove
* it — re-apply this adaptation when updating the file.
*/
const Button = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}
>(function Button({ className, variant = "default", size = "default", asChild = false, ...props }, ref) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
ref={ref}
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
})
export { Button, buttonVariants }
+91
View File
@@ -0,0 +1,91 @@
import * as React from "react"
import { cn } from "./utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+31
View File
@@ -0,0 +1,31 @@
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+184
View File
@@ -0,0 +1,184 @@
/**
* Vendored shadcn/ui dropdown menu, trimmed to the task page's real inventory.
*
* Removed from upstream (`new-york`): `DropdownMenuCheckboxItem`,
* `DropdownMenuRadioGroup`, `DropdownMenuRadioItem` and `DropdownMenuShortcut`
* (no checkbox/radio/shortcut rows exist on the page). `DropdownMenuSub` and its
* trigger/content stay — the node menu's "reassign owner" is a submenu.
*
* The submenu arrow is the host's `IconChevronRightOutline14`, so the file
* imports no icon library of its own.
*/
import * as React from "react"
import { IconChevronRightOutline14 } from "@deepseek-ai/dsh-client-ui-primitives"
import { cn } from "./utils"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
className
)}
{...props}
/>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...props}
>
{children}
<IconChevronRightOutline14 size={16} className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
+103
View File
@@ -0,0 +1,103 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "./utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className
)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
}
+33
View File
@@ -0,0 +1,33 @@
import * as React from "react"
import { cn } from "./utils"
/**
* Vendored shadcn/ui input, minus upstream's `file:` variant utilities
* (`file:inline-flex` `file:h-7` `file:border-0` `file:bg-transparent`
* `file:text-sm` `file:font-medium` `file:text-foreground`).
*
* Two reasons, and both are load-bearing: the plugin has no `<input
* type="file">` surface, so they are dead class strings in a runtime object;
* and they compile to `::file-selector-button` selectors, which is also
* Tailwind preflight's signature — `tests/ui-foundation.spec.ts` uses that
* string to prove the reset never ships (plugin CSS is a global `<style>`, so a
* reset would repaint the whole DSH page), and the file-input variant would
* trip that guard forever. Nothing else on the element changes.
*/
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
className
)}
{...props}
/>
)
}
export { Input }
+45
View File
@@ -0,0 +1,45 @@
import * as React from "react"
import { cn } from "./utils"
import { Popover as PopoverPrimitive } from "radix-ui"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
+55
View File
@@ -0,0 +1,55 @@
import * as React from "react"
import { cn } from "./utils"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }
+25
View File
@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "./utils"
import { Separator as SeparatorPrimitive } from "radix-ui"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }
+13
View File
@@ -0,0 +1,13 @@
import { cn } from "./utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-accent", className)}
{...props}
/>
)
}
export { Skeleton }
+29
View File
@@ -0,0 +1,29 @@
import { IconLoadingOutline16 } from "@deepseek-ai/dsh-client-ui-primitives"
import { cn } from "./utils"
/**
* Loading indicator. The host supplies the glyph (`IconLoadingOutline16`), and
* the rotation rides `animate-spin` on it — the host icons render an `svg`, so
* the utility applies to the glyph box the same way it did upstream.
*
* Upstream took `React.ComponentProps<"svg">`; host icons expose exactly
* `size` / `className`, so the accessible status wrapper moved out one level
* instead of being spread onto the glyph.
*/
function Spinner({
className,
size = 16,
label = "Loading",
}: {
className?: string
size?: number
label?: string
}) {
return (
<span role="status" aria-label={label} className="inline-flex">
<IconLoadingOutline16 size={size} className={cn("animate-spin", className)} />
</span>
)
}
export { Spinner }
+17
View File
@@ -0,0 +1,17 @@
import * as React from "react"
import { cn } from "./utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }
+181
View File
@@ -0,0 +1,181 @@
/*
* Tailwind v4 entry for the plugin's vendored shadcn/ui components.
*
* Two deliberate omissions:
*
* 1. PREFLIGHT IS NEVER IMPORTED. The plugin's CSS ships as a GLOBAL
* `<style data-plugin>` tag injected by the client bundle (no shadow DOM),
* so Tailwind's reset would repaint the whole DSH host page (headings,
* lists, buttons, borders — every element in the conversation and the
* native sidebar). Only `theme.css` (design tokens) and `utilities.css`
* (the classes we actually use) are pulled in.
* 2. NO COLOR LITERALS AND NO DARK PALETTE. Every value in `:root` below
* resolves to a DSH `--dsw-*` token, which already flips with the host
* light/dark theme (`body[data-ds-dark-theme]`). That is the skin contract
* (docs/external-plugin-guide.md §12, guarded by tests/theme.spec.ts).
*
* `@source` is EXPLICIT so the scanner covers only the client TSX tree —
* without it Tailwind would glob the project root and pick class-like strings
* out of `lib/`, `tests/`, `node_modules/` and the docs.
*/
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@source "../**/*.tsx";
/* The DSH theme flips by toggling `data-ds-dark-theme` on <body> (see
* @deepseek-ai/dsh-client-ui-theme), not by adding a `.dark` class. */
@custom-variant dark (&:where([data-ds-dark-theme], [data-ds-dark-theme] *));
/*
* Bridge: shadcn/ui's semantic names -> Tailwind color/radius/font utilities.
*
* `inline` keeps the generated utilities as `var(--background)` etc. instead of
* copying the (indirect) value, so exactly one indirection layer exists and a
* host skin that re-paints a token is picked up live.
*/
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
/* Extra rungs the task page needs beyond stock shadcn: success/warning for
* task state and a third text weight (design language: three ink levels by
* weight, not by hue). */
--color-success: var(--success);
--color-warning: var(--warning);
--color-foreground-3: var(--foreground-3);
--color-border-strong: var(--border-strong);
--radius-sm: var(--radius-sm);
--radius-md: var(--radius-md);
--radius-lg: var(--radius-lg);
--radius-xl: var(--radius-xl);
/* Text roles come from the host's own font tokens so the plugin cannot
* drift from the DSH type scale. */
--font-sans: var(--dsw-font-family-default, system-ui, sans-serif);
--font-mono: var(--dsw-font-family-code, ui-monospace, monospace);
}
/*
* shadcn tokens -> DSH alias tokens. Every value is a `var(--dsw-…)`
* reference; nothing here is a color literal, and no dark override is needed
* because the DSH tokens themselves flip with the host theme.
*/
:root {
--radius: 6px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--background: var(--dsw-alias-bg-base);
--foreground: var(--dsw-alias-label-primary);
--card: var(--dsw-alias-bg-layer-1);
--card-foreground: var(--dsw-alias-label-primary);
--popover: var(--dsw-alias-bg-layer-2);
--popover-foreground: var(--dsw-alias-label-primary);
--primary: var(--dsw-alias-state-business-primary);
--primary-foreground: var(--dsw-alias-label-primary-foreground);
--secondary: var(--dsw-alias-interactive-bg-hover);
--secondary-foreground: var(--dsw-alias-label-primary);
--muted: var(--dsw-alias-interactive-bg-hover);
--muted-foreground: var(--dsw-alias-label-secondary);
--accent: var(--dsw-alias-interactive-bg-hover);
--accent-foreground: var(--dsw-alias-label-primary);
--destructive: var(--dsw-alias-state-error-primary);
--destructive-foreground: var(--dsw-alias-label-primary-foreground);
--border: var(--dsw-alias-border-l4);
--input: var(--dsw-alias-border-l4);
--ring: var(--dsw-alias-state-business-primary);
--success: var(--dsw-alias-state-success-primary);
--warning: var(--dsw-alias-state-warn-primary);
--foreground-3: var(--dsw-alias-label-tertiary);
--border-strong: var(--dsw-alias-label-tertiary);
}
/*
* Scoped resets.
*
* The host's own stylesheets are UNLAYERED, so an unlayered host rule outranks
* anything Tailwind puts in `layer(utilities)` regardless of specificity — a
* layered `.rounded-md` cannot beat a bare host rule. The compensation is
* `.dsw-tasks` (the task page root class): its own rules live in the layer but
* carry class-level specificity, and `:where()` keeps the element resets at
* class specificity too, so `color`/`font`/`box-sizing` reach the page's
* elements without a preflight and without leaking to the rest of DSH.
*/
@layer base {
.dsw-tasks {
color: var(--dsw-alias-label-primary);
}
.dsw-tasks :where(button, input, textarea, select) {
font: inherit;
}
.dsw-tasks :where(*, *::before, *::after) {
box-sizing: border-box;
}
/* The host strips/overrides the UA focus ring in places; the task page owns
* a visible, token-colored one (design language: focus-visible must show). */
.dsw-tasks :where(a, button, input, textarea, select, [tabindex]):focus-visible {
outline: 2px solid var(--dsw-alias-state-business-primary);
outline-offset: 1px;
}
}
/*
* Thin scrollbars for the shadcn ScrollArea / scrollable task surfaces, riding
* the host's scrollbar tokens (they already flip with the theme).
*/
@layer utilities {
.dsw-tasks {
scrollbar-width: thin;
scrollbar-color: var(--dsw-alias-scrollbar-hover-l2) transparent;
}
.dsw-tasks :where(*)::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dsw-tasks :where(*)::-webkit-scrollbar-track {
background: transparent;
}
.dsw-tasks :where(*)::-webkit-scrollbar-thumb {
background: var(--dsw-alias-scrollbar-bg-l2);
border-radius: 4px;
}
.dsw-tasks :where(*)::-webkit-scrollbar-thumb:hover {
background: var(--dsw-alias-scrollbar-hover-l2);
}
}
/* Motion is decorative only (opacity / background / transform); users who ask
* for less get none of it. */
@media (prefers-reduced-motion: reduce) {
.dsw-tasks :where(*, *::before, *::after) {
animation: none !important;
transition: none !important;
}
}
+81
View File
@@ -0,0 +1,81 @@
import * as React from "react"
import { type VariantProps } from "class-variance-authority"
import { cn } from "./utils"
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
import { toggleVariants } from "./toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & {
spacing?: number
}
>({
size: "default",
variant: "default",
spacing: 0,
})
function ToggleGroup({
className,
variant,
size,
spacing = 0,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & {
spacing?: number
}) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
"data-[spacing=0]:rounded-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
}
export { ToggleGroup, ToggleGroupItem }
+50
View File
@@ -0,0 +1,50 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "./utils"
import { Toggle as TogglePrimitive } from "radix-ui"
/**
* Vendored shadcn/ui toggle, trimmed to the task page's real inventory.
*
* Removed from upstream (`new-york`): the `lg` size (the page's toggle heights
* top out at 36 px). `Toggle` itself stays — the view switcher's segmented
* control is built from it via `ToggleGroup`.
*/
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 min-w-9 px-2",
sm: "h-8 min-w-8 px-1.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }
+54
View File
@@ -0,0 +1,54 @@
import * as React from "react"
import { cn } from "./utils"
import { Tooltip as TooltipPrimitive } from "radix-ui"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+16
View File
@@ -0,0 +1,16 @@
/**
* The shadcn/ui class-name helper: `clsx` for conditional composition,
* `tailwind-merge` so a caller's later utility wins over a component's default
* of the same kind (`cn('px-2', 'px-4')` -> `px-4`) instead of both landing in
* the class list and letting stylesheet order decide.
*
* Vendored shadcn components import this file directly (relative path); the
* CLI's `@/lib/utils` alias is never used in this repo.
*/
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
/** Merge conditional class values, resolving conflicting Tailwind utilities. */
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs))
}