From 43d226cafb63f81c5072b748838c8c756ecb2e79 Mon Sep 17 00:00:00 2001 From: Ron Date: Mon, 31 Aug 2026 07:22:23 -0700 Subject: [PATCH] feat(webui): restyle the design-system primitives onto the Gemini shape scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second half of Phase 3 (Epic #7781 WS3): the control styling that the token values in the previous commit exist to drive. Paired with those values in one PR deliberately — tokens alone repaint the palette but leave every control the old shape, which cannot be judged visually. Shape values move to the Gemini scale. Controls become pills, fields 12px, surfaces 16px, dialogs 28px: --v2-radius-control{,-sm,-lg,-xl} 0.625–1.125rem -> 9999px --v2-radius-field 0.375rem -> 0.75rem --v2-radius-surface 0.75rem -> 1rem --v2-radius-surface-lg 1rem -> 1.75rem --v2-radius-chip 0.25rem -> 0.5rem The four control slots collapsing to one value is intentional: they carry different heights and paddings, and at every height a pill is a pill. The accent surface stops being a gradient. #7831 parked --v2-accent-gradient/-hover/-edge/-glow as theme-invariant literals and asked WS3 to decide whether the primary wants a per-theme value. It does — but by REFERENCE rather than duplication: Gemini's primary is a flat tonal fill, so each now resolves through --v2-accent, which is already per-theme. One definition, two themes, no literals to keep in sync. The edge and glow go to transparent/none, since depth is tonal now. button.tsx: the primary label was hardcoded `text-white`. That is exactly the 2.98:1 failure the previous commit documented, and it becomes unreadable on the light theme's tonal container. It now reads --v2-accent-on. Six more primitives move off hardcoded radii onto the scale, so their shape is reachable from app.css like Button's already was: card, input, modal, select-menu, search-field, inline-notice. No other visual property is touched in those files — this is shape only. Verified at crates/product/ironclaw_webui/frontend: typecheck, lint:conventions, test:storybook (105 tests / 32 files), pnpm build with bundle budgets and the design-token bundle contract. The six radius utilities the migrated primitives now depend on were confirmed present in dist/assets/*.css (.rounded-surface, -surface-lg, -field, -chip, -control, -control-sm), and the emitted custom properties confirmed to carry the new values — a class-string assertion cannot prove either. pnpm test is 18 failed / 1499 passed, the same two pre-existing files (theme.test.tsx, inspector-panel.test.tsx) that fail identically on main under Node 26. Refs #7781 Co-Authored-By: Claude Opus 5 --- .../frontend/src/design-system/button.tsx | 2 +- .../frontend/src/design-system/card.tsx | 6 +-- .../src/design-system/inline-notice.tsx | 4 +- .../frontend/src/design-system/input.tsx | 12 +++--- .../frontend/src/design-system/modal.tsx | 4 +- .../src/design-system/search-field.tsx | 4 +- .../src/design-system/select-menu.tsx | 8 ++-- .../frontend/src/styles/app.css | 38 +++++++++---------- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/crates/product/ironclaw_webui/frontend/src/design-system/button.tsx b/crates/product/ironclaw_webui/frontend/src/design-system/button.tsx index 49355d12ec..65d767d054 100644 --- a/crates/product/ironclaw_webui/frontend/src/design-system/button.tsx +++ b/crates/product/ironclaw_webui/frontend/src/design-system/button.tsx @@ -159,7 +159,7 @@ export function Button({ sizeClass, fullClass, disabledAnchorClass, - "relative overflow-hidden text-white group", + "relative overflow-hidden text-[var(--v2-accent-on)] group", "hover:shadow-[var(--v2-accent-glow)]", className )} diff --git a/crates/product/ironclaw_webui/frontend/src/design-system/card.tsx b/crates/product/ironclaw_webui/frontend/src/design-system/card.tsx index e34681dc34..f5374939de 100644 --- a/crates/product/ironclaw_webui/frontend/src/design-system/card.tsx +++ b/crates/product/ironclaw_webui/frontend/src/design-system/card.tsx @@ -47,9 +47,9 @@ const VARIANTS = { /* ─── Radius ──────────────────────────────────────────────────────── */ const RADII = { - sm: "rounded-[14px]", - md: "rounded-[1.25rem] md:rounded-[1.5rem]", - lg: "rounded-[1.5rem]", + sm: "rounded-surface", + md: "rounded-surface md:rounded-surface-lg", + lg: "rounded-surface-lg", }; /* ─── Padding ─────────────────────────────────────────────────────── */ diff --git a/crates/product/ironclaw_webui/frontend/src/design-system/inline-notice.tsx b/crates/product/ironclaw_webui/frontend/src/design-system/inline-notice.tsx index 6421b472bb..940e6e24c0 100644 --- a/crates/product/ironclaw_webui/frontend/src/design-system/inline-notice.tsx +++ b/crates/product/ironclaw_webui/frontend/src/design-system/inline-notice.tsx @@ -65,7 +65,7 @@ export function InlineNotice({ role={role} data-tone={tone} className={cn( - "flex items-start gap-3 rounded-xl border px-4 py-3 text-sm", + "flex items-start gap-3 rounded-surface border px-4 py-3 text-sm", TONE_STYLES[tone], className, )} @@ -81,7 +81,7 @@ export function InlineNotice({ type="button" onClick={onDismiss} aria-label={dismissLabel} - className="shrink-0 self-center rounded-md p-1 opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current" + className="shrink-0 self-center rounded-chip p-1 opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current" > diff --git a/crates/product/ironclaw_webui/frontend/src/design-system/input.tsx b/crates/product/ironclaw_webui/frontend/src/design-system/input.tsx index 9a4c4b9de3..d0e67e4db0 100644 --- a/crates/product/ironclaw_webui/frontend/src/design-system/input.tsx +++ b/crates/product/ironclaw_webui/frontend/src/design-system/input.tsx @@ -3,8 +3,8 @@ * * All styling via Tailwind + CSS variables — no app.css classes. * Sizes and focus ring match the reference AppInput exactly: - * mobile h-[44px] rounded-[14px] px-3.5 text-ui - * desktop h-[50px] rounded-[16px] px-4 text-ui + * mobile h-[44px] rounded-field px-3.5 text-ui + * desktop h-[50px] rounded-field px-4 text-ui * * Exports * Input — wrapper @@ -30,9 +30,9 @@ const INPUT_BASE = /* Sizes mirroring reference AppInput */ const INPUT_SIZES = { - sm: "h-9 rounded-[10px] px-3 text-ui-sm", - md: "h-[44px] rounded-[14px] px-3.5 text-ui md:h-[50px] md:rounded-[16px] md:px-4", - lg: "h-[54px] rounded-[18px] px-4 text-ui-lg", + sm: "h-9 rounded-field px-3 text-ui-sm", + md: "h-[44px] rounded-field px-3.5 text-ui md:h-[50px] md:px-4", + lg: "h-[54px] rounded-field px-4 text-ui-lg", }; export type InputProps = Omit, "size"> & { @@ -97,7 +97,7 @@ export function Textarea({ rows={rows} className={cn( INPUT_BASE, - "rounded-[14px] px-3.5 py-3 text-ui md:rounded-[16px] md:px-4", + "rounded-field px-3.5 py-3 text-ui md:px-4", "resize-y min-h-[80px]", error && "border-[var(--v2-danger-text)] focus:ring-[color-mix(in_srgb,var(--v2-danger-text)_28%,transparent)]", className diff --git a/crates/product/ironclaw_webui/frontend/src/design-system/modal.tsx b/crates/product/ironclaw_webui/frontend/src/design-system/modal.tsx index a81f5caa67..f85c57d543 100644 --- a/crates/product/ironclaw_webui/frontend/src/design-system/modal.tsx +++ b/crates/product/ironclaw_webui/frontend/src/design-system/modal.tsx @@ -107,7 +107,7 @@ export function Modal({ "relative z-10 w-full", "bg-[var(--v2-card-bg)] border border-[var(--v2-panel-border)]", "shadow-[0_24px_60px_rgba(0,0,0,0.35)]", - "rounded-[1.5rem]", + "rounded-surface-lg", "flex flex-col max-h-[90dvh] overflow-hidden", SIZES[size] ?? SIZES.md, className @@ -151,7 +151,7 @@ export function ModalHeader({ type="button" onClick={onClose} aria-label={effectiveCloseLabel} - className="grid h-8 w-8 shrink-0 place-items-center rounded-[10px] + className="grid h-8 w-8 shrink-0 place-items-center rounded-control-sm border border-[var(--v2-panel-border)] bg-[var(--v2-surface-soft)] text-[var(--v2-text-muted)] hover:bg-[var(--v2-surface-muted)] hover:text-[var(--v2-text-strong)]" diff --git a/crates/product/ironclaw_webui/frontend/src/design-system/search-field.tsx b/crates/product/ironclaw_webui/frontend/src/design-system/search-field.tsx index ce87412685..39102283c3 100644 --- a/crates/product/ironclaw_webui/frontend/src/design-system/search-field.tsx +++ b/crates/product/ironclaw_webui/frontend/src/design-system/search-field.tsx @@ -44,7 +44,7 @@ export function SearchField({ disabled={disabled} onChange={(event) => onChange(event.currentTarget.value)} className={cn( - "h-9 w-full rounded-[10px] border border-[var(--v2-panel-border)]", + "h-9 w-full rounded-control border border-[var(--v2-panel-border)]", "bg-[var(--v2-input-bg)] pl-9 text-sm text-[var(--v2-text-strong)] outline-none", "appearance-none placeholder:text-[var(--v2-text-faint)] focus:border-[var(--v2-accent)]", "[&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden", @@ -59,7 +59,7 @@ export function SearchField({ disabled={disabled} onClick={onClear} className={cn( - "absolute right-2 top-1/2 grid h-6 w-6 -translate-y-1/2 place-items-center rounded-md", + "absolute right-2 top-1/2 grid h-6 w-6 -translate-y-1/2 place-items-center rounded-chip", "text-[var(--v2-text-faint)] hover:bg-[var(--v2-surface-muted)] hover:text-[var(--v2-text-strong)]", "disabled:cursor-not-allowed disabled:opacity-50", )} diff --git a/crates/product/ironclaw_webui/frontend/src/design-system/select-menu.tsx b/crates/product/ironclaw_webui/frontend/src/design-system/select-menu.tsx index 6f1d8f54b7..1212362d13 100644 --- a/crates/product/ironclaw_webui/frontend/src/design-system/select-menu.tsx +++ b/crates/product/ironclaw_webui/frontend/src/design-system/select-menu.tsx @@ -426,7 +426,7 @@ export function SelectMenu({ })} onKeyDown={handleKeyDown} className={cn( - "inline-flex w-full items-center justify-between gap-2 rounded-[8px] border", + "inline-flex w-full items-center justify-between gap-2 rounded-field border", "border-[var(--v2-panel-border)] bg-[var(--v2-surface-soft)]", "text-[var(--v2-text-strong)] shadow-none transition-colors", "hover:bg-[var(--v2-surface-muted)]", @@ -453,7 +453,7 @@ export function SelectMenu({ {open && (
setSearchQuery(event.currentTarget.value)} onKeyDown={handleSearchKeyDown} className={cn( - "sticky top-0 z-10 mb-1 h-9 w-full rounded-[7px] border px-2.5", + "sticky top-0 z-10 mb-1 h-9 w-full rounded-chip border px-2.5", "border-[var(--v2-panel-border)] bg-[var(--v2-input-bg)]", "text-[var(--v2-text-strong)] placeholder:text-[var(--v2-text-faint)]", "focus-visible:outline-none focus-visible:ring-2", @@ -501,7 +501,7 @@ export function SelectMenu({ onMouseEnter={() => !option.disabled && setActiveIndex(index)} onClick={() => chooseOption(option)} className={cn( - "flex w-full items-center justify-between gap-3 rounded-[7px]", + "flex w-full items-center justify-between gap-3 rounded-chip", "text-left text-[var(--v2-text)] transition-colors", "focus-visible:outline-none", "focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--v2-accent)_30%,transparent)]", diff --git a/crates/product/ironclaw_webui/frontend/src/styles/app.css b/crates/product/ironclaw_webui/frontend/src/styles/app.css index 85f34b7566..9d5591c08d 100644 --- a/crates/product/ironclaw_webui/frontend/src/styles/app.css +++ b/crates/product/ironclaw_webui/frontend/src/styles/app.css @@ -275,14 +275,16 @@ surface rounded-xl ( 56 uses) cards, panels surface-lg rounded-2xl ( 18 uses) modals, sheets pill rounded-full ( 47 uses) avatars, toggles, pills */ - --v2-radius-chip: 0.25rem; - --v2-radius-field: 0.375rem; - --v2-radius-control-sm: 0.625rem; - --v2-radius-control: 0.875rem; - --v2-radius-control-lg: 1rem; - --v2-radius-control-xl: 1.125rem; - --v2-radius-surface: 0.75rem; - --v2-radius-surface-lg: 1rem; + --v2-radius-chip: 0.5rem; + --v2-radius-field: 0.75rem; + /* Controls are pills. The four slots stay distinct because they carry + different heights and paddings; at every height a pill is a pill. */ + --v2-radius-control-sm: 9999px; + --v2-radius-control: 9999px; + --v2-radius-control-lg: 9999px; + --v2-radius-control-xl: 9999px; + --v2-radius-surface: 1rem; + --v2-radius-surface-lg: 1.75rem; --v2-radius-pill: 9999px; /* ── Spacing ────────────────────────────────────────────────────── @@ -332,17 +334,15 @@ as `button.tsx` hardcoded them so retheming the loudest control in the app stops meaning "edit a component". - Theme-INVARIANT on purpose: the literals they replace were shared by - light and dark, so splitting them per theme now would be a visual - change wearing a refactor's clothes. Phase 3a (#7781 WS3) decides - whether the M3X primary wants a per-theme value and moves these into - the `[data-theme]` stanzas if so. */ - --v2-accent-gradient: - radial-gradient(ellipse 100% 100% at 50% 130%, #4ca7e6 0%, #2882c8 65%); - --v2-accent-gradient-hover: - radial-gradient(ellipse 200% 220% at 50% 110%, #5bbaf5 0%, #2882c8 60%); - --v2-accent-edge: rgba(76, 167, 230, 0.72); - --v2-accent-glow: 0 24px 24px -20px rgba(76, 167, 230, 0.55); + #7831 left these theme-invariant and asked WS3 to decide whether the + primary wants a per-theme value. It does — but by REFERENCE rather than + by duplication: Gemini's primary is a flat tonal fill, so each of these + now resolves through `--v2-accent`, which is already per-theme. One + definition, two themes, no literals to keep in sync. */ + --v2-accent-gradient: var(--v2-accent); + --v2-accent-gradient-hover: var(--v2-accent-strong); + --v2-accent-edge: transparent; + --v2-accent-glow: 0 0 #0000; /* ── Z-index ────────────────────────────────────────────────────── A stacking ORDER, not a set of magic numbers. The two four-digit