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:
@@ -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:
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user