From e500a3d2b9105e326dcfabefbf99b4d29242d416 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Mon, 21 Sep 2026 10:15:54 +0800 Subject: [PATCH] style(tasks-ui): retune the page to the plugin's own visual language Stock shadcn defaults are designed for a standalone app; next to this plugin's other tabs (files, changes, side chat, settings) they read as foreign: 14px body against 12px, 36px controls against 28px, #00000029 borders against the plugin's 10%/4% hairlines, solid primary badges against flat state tints, and elevation where every other panel is flat. Measured the plugin's own vocabulary from its CSS modules (12px body / 11px meta / 10px micro; 16-20px line heights; 28px controls; 6x8 and 6x10 paddings; 8/6/4 gaps; 6/8px radii; `border-l2` for 1px borders and `border-l1` for hairlines; `--dsw-shadow-lv2/3` only on floating layers) and mapped the vendored set onto it instead of editing every page: - theme.css bridge: `--border`/`--input` -> `border-l2` (l4 was visibly darker than every other tab), new `--border-subtle` -> `border-l1`, the type scale remapped (text-xs 11px, text-sm 12px, text-base 13px, text-lg 14px), elevation mapped to the plugin's tokens (static shadows none, md/lg -> --dsw-shadow-lv2 /lv3), and the state tints bridged. - component defaults retuned to the same scale: button sizes 28/24/20px with text-xs, card padding 12px with gap-3 and no shadow, input 28px, badge 18px with 11px ink, empty at p-4, separators on the hairline token. - badges: the default variant is the plugin's flat state tint (bg-state-business-tertiary + state ink, the same recipe sidebar.module.css uses for revealed rows) rather than a solid primary fill. Guards updated with the reasoning inline: the border bridge expectations, a `none` value being the plugin's way of saying "flat", and the skin-contract check now derives local token names from the file instead of a hand-kept list. Verified: typecheck/lint clean; 136 files / 1515 tests; build with the bundle guards; mount e2e 7/7; mirror-page screenshots (real host stylesheet + real components) re-shot at 360/720 for graph, tree, board and the 77-node fixture. --- src/client/ui/badge.tsx | 7 +++++-- src/client/ui/button.tsx | 18 ++++++++-------- src/client/ui/card.tsx | 6 +++--- src/client/ui/empty.tsx | 6 +++--- src/client/ui/input.tsx | 2 +- src/client/ui/separator.tsx | 2 +- src/client/ui/theme.css | 41 +++++++++++++++++++++++++++++++++++-- tests/ui-foundation.spec.ts | 12 +++++++++-- 8 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/client/ui/badge.tsx b/src/client/ui/badge.tsx index dcac11b..07c703f 100644 --- a/src/client/ui/badge.tsx +++ b/src/client/ui/badge.tsx @@ -4,11 +4,14 @@ import { cn } from "./utils" import { Slot } from "radix-ui" 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", + "inline-flex h-[18px] w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-sm border border-transparent px-1.5 py-0 text-[11px] leading-none 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", + // LOCAL ADAPTATION: the plugin's own status chips are a FLAT TINT with the + // state ink (see sidebar.module.css `.explorerRowRevealed`), not a solid + // primary fill — a solid badge is the loudest thing on every row here. + default: "bg-state-business-tertiary text-state-business-primary [a&]:hover:bg-state-business-tertiary", secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: diff --git a/src/client/ui/button.tsx b/src/client/ui/button.tsx index 69d1162..13bc08d 100644 --- a/src/client/ui/button.tsx +++ b/src/client/ui/button.tsx @@ -12,7 +12,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 ", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground ", + "border bg-background hover:bg-accent hover:text-accent-foreground ", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: @@ -20,14 +20,14 @@ const buttonVariants = cva( link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", + default: "h-7 gap-1.5 px-2.5 text-xs has-[>svg]:px-2", + xs: "h-5 gap-1 rounded-md px-1.5 text-[11px] has-[>svg]:px-1 [&_svg:not([class*='size-'])]:size-3", + sm: "h-6 gap-1.5 rounded-md px-2 text-[11px] has-[>svg]:px-1.5", + lg: "h-8 rounded-md px-3 text-xs has-[>svg]:px-2.5", + icon: "size-7", + "icon-xs": "size-5 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-6", + "icon-lg": "size-8", }, }, defaultVariants: { diff --git a/src/client/ui/card.tsx b/src/client/ui/card.tsx index 86683cf..f32bc74 100644 --- a/src/client/ui/card.tsx +++ b/src/client/ui/card.tsx @@ -6,7 +6,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
) {
) { return (
) diff --git a/src/client/ui/empty.tsx b/src/client/ui/empty.tsx index 3a13339..2fdffde 100644 --- a/src/client/ui/empty.tsx +++ b/src/client/ui/empty.tsx @@ -6,7 +6,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
) { return (
) @@ -72,7 +72,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", + "text-[11px]/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className )} {...props} diff --git a/src/client/ui/input.tsx b/src/client/ui/input.tsx index 9bbcc5c..a9c9b91 100644 --- a/src/client/ui/input.tsx +++ b/src/client/ui/input.tsx @@ -7,7 +7,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"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 shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm ", + "h-7 w-full min-w-0 rounded-md border border-input bg-transparent px-2 py-1 text-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-5 file:border-0 file:bg-transparent file:text-[11px] file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 ", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 ", className diff --git a/src/client/ui/separator.tsx b/src/client/ui/separator.tsx index 94803b8..d8772bc 100644 --- a/src/client/ui/separator.tsx +++ b/src/client/ui/separator.tsx @@ -14,7 +14,7 @@ function 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", + "shrink-0 bg-border-subtle data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", className )} {...props} diff --git a/src/client/ui/theme.css b/src/client/ui/theme.css index 7c90a87..d46063f 100644 --- a/src/client/ui/theme.css +++ b/src/client/ui/theme.css @@ -44,6 +44,40 @@ * host skin that re-paints a token is picked up live. */ @theme inline { + /* + * TYPE SCALE = THE PLUGIN'S OWN, not Tailwind's default (14/12px). Every + * other surface of this plugin (files, changes, side chat, settings) is set + * at 12px body / 11px meta / 10px micro with 16-20px line heights, so the + * vendored components inherit that scale through the ordinary `text-sm` / + * `text-xs` classes instead of every page re-overriding them. + */ + --text-xs: 0.6875rem; + --text-xs--line-height: 1rem; + --text-sm: 0.75rem; + --text-sm--line-height: 1.125rem; + --text-base: 0.8125rem; + --text-base--line-height: 1.25rem; + --text-lg: 0.875rem; + --text-lg--line-height: 1.375rem; + + /* + * ELEVATION = TOKENS, AND ONLY WHERE THE PLUGIN USES IT. Static panels in + * this plugin are flat with a hairline border and a hover fill; only floating + * layers take the host's own elevation tokens (`--dsw-shadow-lv2/3`, the same + * ones the plugin's settings modal and popovers already wear). + */ + --shadow-2xs: none; + --shadow-xs: none; + --shadow-sm: none; + --shadow-md: var(--dsw-shadow-lv2); + --shadow-lg: var(--dsw-shadow-lv3); + --shadow-xl: var(--dsw-shadow-lv3); + + --color-border-subtle: var(--border-subtle); + --color-state-business-tertiary: var(--dsw-alias-state-business-tertiary); + --color-state-success-tertiary: var(--dsw-alias-state-success-tertiary); + --color-state-warn-tertiary: var(--dsw-alias-state-warn-tertiary); + --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -110,8 +144,11 @@ --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); + /* The plugin's own vocabulary: 1px borders are `border-l2` (10%), hairlines + * `border-l1` (4%). `l4` (16%) is noticeably darker than every other tab. */ + --border: var(--dsw-alias-border-l2); + --border-subtle: var(--dsw-alias-border-l1); + --input: var(--dsw-alias-border-l2); --ring: var(--dsw-alias-state-business-primary); --success: var(--dsw-alias-state-success-primary); --warning: var(--dsw-alias-state-warn-primary); diff --git a/tests/ui-foundation.spec.ts b/tests/ui-foundation.spec.ts index b7bb9d4..d2d08b8 100644 --- a/tests/ui-foundation.spec.ts +++ b/tests/ui-foundation.spec.ts @@ -47,8 +47,10 @@ const TOKEN_BRIDGE: Array<[shadcn: string, dsw: string]> = [ ['--accent-foreground', '--dsw-alias-label-primary'], ['--destructive', '--dsw-alias-state-error-primary'], ['--destructive-foreground', '--dsw-alias-label-primary-foreground'], - ['--border', '--dsw-alias-border-l4'], - ['--input', '--dsw-alias-border-l4'], + // The plugin's own vocabulary: 1px borders are border-l2 (10%), hairlines + // border-l1 (4%). l4 is noticeably darker than every other tab's chrome. + ['--border', '--dsw-alias-border-l2'], + ['--input', '--dsw-alias-border-l2'], ['--ring', '--dsw-alias-state-business-primary'], ['--success', '--dsw-alias-state-success-primary'], ['--warning', '--dsw-alias-state-warn-primary'], @@ -154,6 +156,9 @@ describe('tailwind entry: theme + utilities only', () => { // scale — never a paint value of its own. const definedElsewhere = new Set([ ...TOKEN_BRIDGE.map(([name]) => name.slice(2)), + // Non-bridged locals the bridge forwards to (e.g. `--border-subtle`), + // read straight out of the file so they cannot drift out of the set. + ...[...withoutComments.matchAll(/^\s*--([a-z0-9-]+)\s*:/gm)].map(m => m[1]), 'radius', 'radius-sm', 'radius-md', 'radius-lg', 'radius-xl', ]) for (const declaration of withoutComments.matchAll(/^\s*(--[a-z0-9-]+):\s*([^;]+);/gm)) { @@ -163,6 +168,9 @@ describe('tailwind entry: theme + utilities only', () => { const refName = reference?.[1] const ok = (refName !== undefined && (refName.startsWith('dsw-') || definedElsewhere.has(refName))) || /^[0-9.]+(?:px|rem|em|%)$/.test(value) + // `none` is how the plugin says "flat": static panels take no elevation + // (the floating layers map to the host's --dsw-shadow-lv* tokens). + || value === 'none' expect(ok, `${name}: ${value}`).toBe(true) } })