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.
This commit is contained in:
Menghuan1918
2026-09-27 22:18:19 +08:00
parent 5deb5285da
commit e500a3d2b9
8 changed files with 71 additions and 23 deletions
+5 -2
View File
@@ -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:
+9 -9
View File
@@ -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: {
+3 -3
View File
@@ -6,7 +6,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
"flex flex-col gap-3 rounded-lg border bg-card py-3 text-card-foreground",
className
)}
{...props}
@@ -19,7 +19,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<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",
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-center gap-1 px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-3",
className
)}
{...props}
@@ -41,7 +41,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-[11px] text-muted-foreground", className)}
{...props}
/>
)
+3 -3
View File
@@ -6,7 +6,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
<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",
"flex min-w-0 flex-1 flex-col items-center justify-center gap-2 rounded-lg border-dashed p-4 text-center text-balance",
className
)}
{...props}
@@ -61,7 +61,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
className={cn("text-[13px] font-medium tracking-tight", className)}
{...props}
/>
)
@@ -72,7 +72,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
<div
data-slot="empty-description"
className={cn(
"text-sm/relaxed text-muted-foreground [&>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}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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}
+39 -2
View File
@@ -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);
+10 -2
View File
@@ -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)
}
})