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 +- `