mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-02 23:56:24 +08:00
feat(webui): restyle the design-system primitives onto the Gemini shape scale
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<Icon name="close" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -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 — <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<ComponentPropsWithoutRef<"input">, "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
|
||||
|
||||
@@ -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)]"
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
|
||||
@@ -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 && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-[calc(100%+0.35rem)] z-30 min-w-full overflow-hidden rounded-[10px]",
|
||||
"absolute top-[calc(100%+0.35rem)] z-30 min-w-full overflow-hidden rounded-surface",
|
||||
"border border-[color-mix(in_srgb,var(--v2-text-strong)_16%,var(--v2-panel-border))]",
|
||||
"bg-[color-mix(in_srgb,var(--v2-canvas-strong)_92%,var(--v2-surface))] p-1",
|
||||
"shadow-[0_30px_72px_-18px_rgba(0,0,0,0.86),0_10px_24px_-18px_rgba(0,0,0,0.68)]",
|
||||
@@ -477,7 +477,7 @@ export function SelectMenu({
|
||||
onChange={(event) => 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)]",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user