From 07be9d73140b897587086cac7f8f7e530daa349d Mon Sep 17 00:00:00 2001 From: Richie Date: Thu, 21 May 2026 14:00:56 +1000 Subject: [PATCH] Add Input, Checkbox, Radio, and Switch form components with semantic token layer Build four form primitives from Figma references with brand-aligned creative decisions: restrained press states (scale-95 instead of highlight splashes), clean iconless Switch, and consistent error states with inline warning icons. Introduce form-control semantic tokens (--color-control-*) in tokens.css so all form components share a single source for borders, checked states, focus rings, labels, and errors. Retrofit Input to use these tokens instead of direct palette references. Update CLAUDE.md and ARCHITECTURE.md with token layer documentation, token discipline rule (no palette references in components), and component tier decision criteria. Co-Authored-By: Claude Opus 4.6 --- ARCHITECTURE.md | 26 +- CLAUDE.md | 19 +- plans/input.md | 55 +++ plans/toggle-controls.md | 90 +++++ plans/workflow.md | 73 +--- .../ui/Checkbox/Checkbox.stories.tsx | 120 +++++++ src/components/ui/Checkbox/Checkbox.tsx | 143 ++++++++ src/components/ui/Checkbox/index.ts | 2 + src/components/ui/Input/Input.stories.tsx | 324 ++++++++++++++++++ src/components/ui/Input/Input.tsx | 193 +++++++++++ src/components/ui/Input/index.ts | 2 + src/components/ui/Radio/Radio.stories.tsx | 138 ++++++++ src/components/ui/Radio/Radio.tsx | 205 +++++++++++ src/components/ui/Radio/index.ts | 2 + src/components/ui/Switch/Switch.stories.tsx | 85 +++++ src/components/ui/Switch/Switch.tsx | 89 +++++ src/components/ui/Switch/index.ts | 2 + src/tokens/tokens.css | 12 + 18 files changed, 1523 insertions(+), 57 deletions(-) create mode 100644 plans/input.md create mode 100644 plans/toggle-controls.md create mode 100644 src/components/ui/Checkbox/Checkbox.stories.tsx create mode 100644 src/components/ui/Checkbox/Checkbox.tsx create mode 100644 src/components/ui/Checkbox/index.ts create mode 100644 src/components/ui/Input/Input.stories.tsx create mode 100644 src/components/ui/Input/Input.tsx create mode 100644 src/components/ui/Input/index.ts create mode 100644 src/components/ui/Radio/Radio.stories.tsx create mode 100644 src/components/ui/Radio/Radio.tsx create mode 100644 src/components/ui/Radio/index.ts create mode 100644 src/components/ui/Switch/Switch.stories.tsx create mode 100644 src/components/ui/Switch/Switch.tsx create mode 100644 src/components/ui/Switch/index.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c7bc437..246f683 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -26,8 +26,17 @@ Components (use Tailwind utilities or var() references) Storybook (visual verification) ``` +### Token Layers +Tokens are structured in three layers: + +1. **Palette** — raw colour values (`--color-blue-01`, `--color-grey-03`). Not used directly in components. +2. **Semantic** — purpose-based aliases (`--color-primary`, `--color-error`, `--color-text`). General UI usage. +3. **Form control** — shared interactive-state tokens for all form components (`--color-control-border`, `--color-control-checked`, `--color-control-label`, etc.). Ensures consistent styling across Input, Checkbox, Radio, Switch, Select, and future form primitives. + ### Token Categories -- **Colours**: `--color-*` (bg, surface, border, text, primary, success, warning, error) +- **Palette colours**: `--color-{palette}-{shade}` (e.g., `--color-blue-01`, `--color-grey-03`) +- **Semantic colours**: `--color-{purpose}` (e.g., `--color-primary`, `--color-error`, `--color-text`) +- **Form control colours**: `--color-control-{role}` (e.g., `--color-control-border`, `--color-control-checked`) - **Radii**: `--radius-*` (sm, default, lg, full) - **Shadows**: `--shadow-*` (default, md) @@ -40,7 +49,8 @@ Declaring `--color-primary: #2563eb` inside `@theme` in `tokens.css` automatical ### `src/components/ui/` — Primitives Atomic, reusable building blocks. Each is self-contained with no domain logic. -- Button, Input, Textarea, Select +- Button, Input, Checkbox, Radio/RadioGroup, Switch +- Textarea, Select - Card, Badge, Tag - Dialog, Tooltip, Popover @@ -55,6 +65,17 @@ Page-level structural components. - AppShell (header + sidebar + content area) - PageHeader +### Which Tier Does a Component Belong To? + +| Question | If yes → | +|---|---| +| Does it wrap a single native element or a single interaction pattern (button, input, toggle)? | **ui/** (primitive) | +| Does it compose 2+ primitives into a reusable unit (e.g., a search bar = Input + Button)? | **composite/** | +| Does it carry domain-specific naming or logic (e.g., ThemeCard, ParticipantRow)? | **composite/** | +| Does it define a page-level region or shell (header, sidebar, content area)? | **layout/** | + +When in doubt: start in `ui/`. Promote to `composite/` when a component begins importing other `ui/` components. If the `composite/` directory grows beyond ~15 components, consider splitting it into `molecules/` (generic compositions) and `organisms/` (domain-aware compositions). + --- ## 4. Styling Approach @@ -62,6 +83,7 @@ Page-level structural components. - **Primary**: Tailwind utility classes - **Conditional classes**: `cn()` from `@/lib/utils` (clsx + tailwind-merge) - **Token values**: Always from `src/tokens/tokens.css`, never hardcoded +- **Token discipline**: Components reference semantic or form-control tokens, not palette tokens. If the needed semantic token doesn't exist, add it to `tokens.css` before using a raw palette value. - **No CSS modules, no styled-components, no inline styles** (except truly dynamic values) - **Class ordering**: Enforced by `prettier-plugin-tailwindcss` diff --git a/CLAUDE.md b/CLAUDE.md index b370de9..8b6f2d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,8 +45,24 @@ All design tokens live in `src/tokens/tokens.css` as a `@theme` block. This is t Use Tailwind utilities (`bg-primary`, `text-error`, `rounded-default`, etc.) or CSS variables (`var(--color-primary)`) when utilities don't cover the case. -Token naming convention: +### Token Layers + +Tokens are organised in three layers, from raw to consumable: + +1. **Palette** — raw colour values: `--color-blue-01`, `--color-grey-03`, etc. Never reference these directly in components. +2. **Semantic** — purpose-based aliases: `--color-primary`, `--color-error`, `--color-text`, etc. Use these for general UI. +3. **Form control** — shared interactive-state tokens for all form components (Input, Checkbox, Radio, Switch, Select, etc.): + - `--color-control-border` / `--color-control-border-hover` + - `--color-control-checked` / `--color-control-checked-hover` + - `--color-control-focus-ring` + - `--color-control-label` / `--color-control-description` / `--color-control-error` + - `--color-control-bg` / `--color-control-bg-readonly` + +**Rule**: Components must reference semantic or form-control tokens, not palette tokens. If you need a colour that has no semantic token, add one — don't reach for the palette directly. + +### Token Naming Convention - Colours: `--color-{name}` (e.g., `--color-primary`, `--color-text-secondary`) +- Form controls: `--color-control-{role}` (e.g., `--color-control-border`, `--color-control-checked`) - Radii: `--radius-{size}` (e.g., `--radius-default`, `--radius-lg`) - Shadows: `--shadow-{size}` (e.g., `--shadow-default`, `--shadow-md`) @@ -78,6 +94,7 @@ src/components/ui/Button/ - Use the `cn()` utility from `@/lib/utils` for conditional classes. - Never use inline styles except for truly dynamic values. - Never use CSS modules or styled-components. +- **Token discipline**: Reference semantic or form-control tokens (`text-control-label`, `border-control-border`), never palette tokens (`text-blue-01`, `border-grey-03`) in component code. If the right semantic token doesn't exist, add one to `tokens.css` first. ### Stories - Every component MUST have a Storybook story file. diff --git a/plans/input.md b/plans/input.md new file mode 100644 index 0000000..4683bd1 --- /dev/null +++ b/plans/input.md @@ -0,0 +1,55 @@ +# Input Component Plan + +## Figma Reference +https://www.figma.com/design/mrabO6AtxN3ektGiTk0I9c/ResearchInsights?node-id=22-3845 + +## Design Style (from Figma) +- Outlined text field with label overlapping top border (Material-style notch) +- Label: 14px bold blue-01, sits on top of border with white background +- Input: 16px regular grey-01, placeholder at 50% opacity +- Border radius: 4px (rounded-sm) +- Optional left/right icon slots (24px) +- Supportive text below: hint (left) + character count (right), 14px grey-02 + +## Sizes +| Size | Input height | Text | Maps to | +|------|-------------|------|---------| +| default | 48px (h-12) | text-body (16px) | Figma "Comfortable" | +| compact | 40px (h-10) | text-small (14px) | Figma "Compact" | + +## States (best practice, not 1:1 Figma) +| State | Border | Label | Hint | +|-------|--------|-------|------| +| Default | grey-03 (1px) | blue-01 | grey-02 | +| Hover | grey-01 (1px) | blue-01 | grey-02 | +| Focus | blue-01 (2px) | blue-01 | grey-02 | +| Error | red-02 (1px) | red-02 | red-02 (shows error message) | +| Error+Focus | red-02 (2px) | red-02 | red-02 | +| Disabled | grey-03 at 50% opacity | grey-02 | grey-02 at 50% opacity | + +## Props +```ts +interface InputProps extends InputHTMLAttributes { + label: string + hint?: string + error?: string + size?: 'default' | 'compact' + leftIcon?: React.ReactNode + rightIcon?: React.ReactNode +} +``` +- `error` takes precedence over `hint` — when set, replaces hint text and turns red +- `maxLength` (native attribute) triggers character counter display +- `disabled` and `readOnly` use native attributes, styled accordingly + +## Accessibility +- `