Replace domain-specific (education/PDP) recipe stories with generic content. Fix TopBar action buttons using properly styled light-on-dark buttons instead of invisible IconButton tertiary. Use the real NSW waratah SVG logo. Add shared _story-helpers for TopBar actions and logo. Add DetailPage template for single-record/profile/document views with constrained width and tab support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1116 lines
41 KiB
Markdown
1116 lines
41 KiB
Markdown
---
|
||
name: ADS 3.0 Design System
|
||
description: React component library implementing the NSW Department of Education's ADS 3.0 design language
|
||
version: alpha
|
||
colors:
|
||
# Palette (do not reference directly in components)
|
||
blue-01: "#002664"
|
||
blue-02: "#146CFD"
|
||
blue-03: "#8CE0FF"
|
||
blue-04: "#CBEDFD"
|
||
red-01: "#630019"
|
||
red-02: "#D7153A"
|
||
red-03: "#FFB8C1"
|
||
red-04: "#FFE6EA"
|
||
orange-01: "#941B00"
|
||
orange-02: "#F3631B"
|
||
orange-03: "#FFCE99"
|
||
orange-04: "#FDEDDF"
|
||
green-01: "#004000"
|
||
green-02: "#00AA45"
|
||
green-03: "#A8EDB3"
|
||
green-04: "#DBFADF"
|
||
teal-01: "#0B3F47"
|
||
teal-02: "#2E808E"
|
||
teal-03: "#8CDBE5"
|
||
teal-04: "#D1EEEA"
|
||
brown-01: "#523719"
|
||
brown-02: "#B68D5D"
|
||
brown-03: "#E8D0B5"
|
||
brown-04: "#EDE3D7"
|
||
purple-01: "#441170"
|
||
purple-02: "#8055F1"
|
||
purple-03: "#CEBFFF"
|
||
purple-04: "#E6E1FD"
|
||
fuchsia-01: "#65004D"
|
||
fuchsia-02: "#D912AE"
|
||
fuchsia-03: "#F4B5E6"
|
||
fuchsia-04: "#FDDEF2"
|
||
yellow-01: "#694800"
|
||
yellow-02: "#FAAF05"
|
||
yellow-03: "#FDE79A"
|
||
yellow-04: "#FFF4CF"
|
||
grey-01: "#22272B"
|
||
grey-02: "#495054"
|
||
grey-03: "#CDD3D6"
|
||
grey-04: "#EBEBEB"
|
||
grey-05: "#F2F2F2"
|
||
white: "#FFFFFF"
|
||
# Semantic (use these in components)
|
||
primary: "{colors.blue-01}"
|
||
info: "{colors.blue-02}"
|
||
secondary: "{colors.blue-04}"
|
||
error: "{colors.red-02}"
|
||
success: "{colors.green-02}"
|
||
warning: "{colors.orange-02}"
|
||
text: "{colors.grey-01}"
|
||
text-secondary: "{colors.grey-02}"
|
||
border: "{colors.grey-04}"
|
||
bg: "{colors.grey-05}"
|
||
surface: "{colors.white}"
|
||
# Form controls
|
||
control-border: "{colors.grey-03}"
|
||
control-border-hover: "{colors.grey-01}"
|
||
control-checked: "{colors.blue-01}"
|
||
control-checked-hover: "{colors.blue-02}"
|
||
control-focus-ring: "{colors.blue-04}"
|
||
control-label: "{colors.blue-01}"
|
||
control-description: "{colors.grey-02}"
|
||
control-error: "{colors.red-02}"
|
||
control-bg: "{colors.white}"
|
||
control-bg-readonly: "{colors.grey-05}"
|
||
# Button
|
||
button-default: "{colors.blue-01}"
|
||
button-danger: "{colors.red-02}"
|
||
button-neutral: "{colors.grey-01}"
|
||
button-subtle-bg: "{colors.blue-04}"
|
||
button-subtle-text: "{colors.blue-01}"
|
||
# Switch
|
||
switch-on: "{colors.green-02}"
|
||
switch-on-hover: "{colors.green-01}"
|
||
typography:
|
||
h1:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 48px
|
||
fontWeight: "700"
|
||
lineHeight: 1.25
|
||
h2:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 32px
|
||
fontWeight: "700"
|
||
lineHeight: 1.25
|
||
h3:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 24px
|
||
fontWeight: "600"
|
||
lineHeight: 1.333
|
||
h4:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 20px
|
||
fontWeight: "600"
|
||
lineHeight: 1.4
|
||
h5:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 16px
|
||
fontWeight: "600"
|
||
lineHeight: 1.5
|
||
h6:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 14px
|
||
fontWeight: "600"
|
||
lineHeight: 1.43
|
||
intro:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 20px
|
||
fontWeight: "400"
|
||
lineHeight: 1.4
|
||
body:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 16px
|
||
fontWeight: "400"
|
||
lineHeight: 1.5
|
||
small:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 14px
|
||
fontWeight: "400"
|
||
lineHeight: 1.357
|
||
caption:
|
||
fontFamily: Public Sans Variable
|
||
fontSize: 12px
|
||
fontWeight: "400"
|
||
lineHeight: 1.5
|
||
rounded:
|
||
sm: 4px
|
||
DEFAULT: 8px
|
||
lg: 16px
|
||
xl: 24px
|
||
full: 9999px
|
||
spacing:
|
||
unit: 4px
|
||
scale: "Tailwind default (4px base: 1=4px, 2=8px, 3=12px, 4=16px, 6=24px, 8=32px)"
|
||
---
|
||
|
||
## Overview
|
||
|
||
ADS 3.0 (Adaptive Design System) is a React component library based on the NSW Department of Education's digital design language. It uses Public Sans as its typeface, a structured colour palette with semantic aliases, and consistent border radii and spacing. Components are built with React 19, TypeScript (strict), and Tailwind CSS v4.
|
||
|
||
All visual values come from design tokens defined in `src/tokens/tokens.css`. Components never use raw colour values — they reference semantic or domain-specific token classes via Tailwind utilities.
|
||
|
||
## Colors
|
||
|
||
The palette is organised in layers, from raw to consumable:
|
||
|
||
1. **Palette** — Raw values across 10 colour families (blue, red, orange, green, teal, brown, purple, fuchsia, yellow, grey) with 4 shades each (`-01` darkest to `-04` lightest), plus grey-05 and white. Never use these directly in component code.
|
||
2. **Semantic** — Purpose-based aliases (`primary`, `info`, `secondary`, `error`, `success`, `warning`, `text`, `text-secondary`, `surface`, `bg`, `border`). Use these for general UI.
|
||
3. **Form control** — Shared interactive-state tokens for all form components: `control-border`, `control-border-hover`, `control-checked`, `control-checked-hover`, `control-focus-ring`, `control-label`, `control-description`, `control-error`, `control-bg`, `control-bg-readonly`.
|
||
4. **Button** — Intent tokens: `button-default` (navy), `button-danger` (red), `button-neutral` (dark grey), `button-subtle-bg`, `button-subtle-text`.
|
||
5. **Switch** — On-state tokens: `switch-on` (success green), `switch-on-hover`.
|
||
6. **Badge** — Status colour tokens: `badge-navy`, `badge-info`, `badge-info-light`, `badge-success`, `badge-success-light`, `badge-error`, `badge-error-light`, `badge-warning`, `badge-warning-light`, `badge-neutral`, plus contrast text tokens (`badge-on-success-light`, `badge-on-error-light`, `badge-on-warning-light`).
|
||
7. **Chip** — Border/fill state tokens: `chip-border`, `chip-text`, `chip-bg`, `chip-selected-bg`, `chip-selected-text`.
|
||
8. **Tag** — 11-colour system, each with a `-light` variant: navy, blue, green, red, orange, grey, teal, brown, purple, fuchsia, yellow.
|
||
9. **Alert** — Background, border, and icon tokens for 5 variants: `alert-{variant}-bg`, `alert-{variant}-border`, `alert-{variant}-icon` (info, warning, error, success, neutral).
|
||
10. **Avatar** — `avatar` (background), `avatar-text`.
|
||
11. **TopBar** — `topbar` (background, navy).
|
||
12. **SideNav** — `nav-bg`, `nav-text`, `nav-icon`, `nav-active`, `nav-divider`.
|
||
|
||
The primary brand colour is `blue-01` (#002664, dark navy) used for buttons, links, and navigation. `blue-02` (#146CFD) is the brighter accent for info states, focus rings, and highlights.
|
||
|
||
## Typography
|
||
|
||
The system uses **Public Sans Variable** (loaded via `@fontsource-variable/public-sans`). The type scale runs from `caption` (12px) to `h1` (48px). Headings use weight 600–700; body text uses 400.
|
||
|
||
Use Tailwind's `text-{scale}` utilities: `text-h1`, `text-h2`, `text-h3`, `text-h4`, `text-h5`, `text-h6`, `text-intro`, `text-body`, `text-small`, `text-caption`.
|
||
|
||
## Layout & Spacing
|
||
|
||
Spacing follows Tailwind's default 4px base scale. Common values:
|
||
- `p-2` (8px), `p-3` (12px), `p-4` (16px), `p-6` (24px) for padding
|
||
- `gap-2` (8px), `gap-3` (12px), `gap-4` (16px) for flex/grid gaps
|
||
- `space-y-4` (16px) for stacked content
|
||
|
||
No custom spacing tokens are defined — the Tailwind defaults are sufficient.
|
||
|
||
## Elevation & Depth
|
||
|
||
Two shadow levels:
|
||
- `shadow-default` — subtle lift for cards and surfaces (`0 1px 3px rgba(0,0,0,0.08)`)
|
||
- `shadow-md` — elevated elements like dropdowns and dialogs (`0 4px 6px -1px rgba(0,0,0,0.1)`)
|
||
|
||
## Shapes
|
||
|
||
Border radii use the custom scale:
|
||
- `rounded-sm` (4px) — small elements, tags
|
||
- `rounded-default` (8px) — buttons, inputs, alerts, chips
|
||
- `rounded-lg` (16px) — cards, dialogs
|
||
- `rounded-xl` (24px) — large containers, page headers
|
||
- `rounded-full` (9999px) — badges, avatars, circular buttons
|
||
|
||
## Page Layout
|
||
|
||
Pages are composed using the **AppShell** template, which provides the fixed TopBar, collapsible SideNav, and scrollable content area. Content within the main area follows these conventions:
|
||
|
||
### Shell structure
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ TopBar (h-16, fixed top, full width) │
|
||
├────────┬────────────────────────────────┤
|
||
│SideNav │ Content area (scrollable) │
|
||
│(w-[360]│ │
|
||
│ or │ PageHeader (optional) │
|
||
│ w-20 │ │
|
||
│collapsed│ Main content │
|
||
│) │ │
|
||
└────────┴────────────────────────────────┘
|
||
```
|
||
|
||
### Content patterns
|
||
|
||
**Dashboard** — Use a responsive grid for content cards:
|
||
- 2-column grid at `lg` breakpoint: `grid grid-cols-1 lg:grid-cols-2 gap-6`
|
||
- Stat cards in a horizontal row: `flex gap-4` or `grid grid-cols-3 gap-4`
|
||
- Content padding: `p-6` or `p-8`
|
||
- Section spacing: `space-y-6`
|
||
|
||
**List page** — Stat summary row above a scrollable list:
|
||
- Stat cards row: `flex gap-4` with Card components
|
||
- List section: Card wrapping a vertical list with `divide-y divide-border`
|
||
- Each list item: `px-6 py-4` with flex layout for metadata
|
||
|
||
**Form page** — Stepped form with vertical stepper:
|
||
- Content max-width: `max-w-3xl` for readable line length
|
||
- Form sections: `space-y-6` between groups
|
||
- Stepper: vertical list on left, form content fills remaining space
|
||
- Submit actions: right-aligned with `flex justify-end gap-3`
|
||
|
||
### Spacing conventions
|
||
- Page content padding: `p-6` (24px) minimum
|
||
- Section gap: `gap-6` (24px) between major sections
|
||
- Card internal padding: `p-5` or `p-6`
|
||
- Form field spacing: `space-y-4` (16px) between fields, `space-y-6` (24px) between groups
|
||
|
||
---
|
||
|
||
## Components
|
||
|
||
### Atoms
|
||
|
||
#### Button
|
||
|
||
Interactive button with variant/intent/size matrix.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `variant` | `'primary' \| 'secondary' \| 'tertiary'` | `'primary'` | Visual weight |
|
||
| `intent` | `'default' \| 'danger' \| 'subtle' \| 'neutral'` | `'default'` | Semantic purpose |
|
||
| `size` | `'default' \| 'comfortable' \| 'compact'` | `'default'` | Height and padding |
|
||
| `loading` | `boolean` | `false` | Shows spinner, disables interaction |
|
||
| `leftIcon` | `ReactNode` | — | Icon before label |
|
||
| `rightIcon` | `ReactNode` | — | Icon after label |
|
||
|
||
```tsx
|
||
import { Button } from '@/components/atoms/Button'
|
||
|
||
<Button variant="primary" intent="default">Save</Button>
|
||
<Button variant="secondary" intent="danger" leftIcon={<Trash2 />}>Delete</Button>
|
||
<Button variant="tertiary" size="compact">Cancel</Button>
|
||
```
|
||
|
||
#### IconButton
|
||
|
||
Icon-only button with required `aria-label`.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `variant` | `'primary' \| 'secondary' \| 'tertiary'` | `'primary'` | Visual weight |
|
||
| `intent` | `'default' \| 'danger' \| 'neutral'` | `'default'` | Semantic purpose |
|
||
| `size` | `'default' \| 'large' \| 'compact' \| 'small' \| 'xsmall'` | `'default'` | Button dimensions |
|
||
| `shape` | `'circle' \| 'square'` | `'circle'` | Border radius |
|
||
| `icon` | `ReactNode` | — | **Required.** The icon element |
|
||
| `aria-label` | `string` | — | **Required.** Accessible label |
|
||
|
||
```tsx
|
||
import { IconButton } from '@/components/atoms/IconButton'
|
||
import { X, Settings } from 'lucide-react'
|
||
|
||
<IconButton icon={<X />} aria-label="Close" variant="tertiary" />
|
||
<IconButton icon={<Settings />} aria-label="Settings" shape="square" size="compact" />
|
||
```
|
||
|
||
#### Input
|
||
|
||
Text input with label, description, hint, error, and icon slots.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | **Required.** Visible label |
|
||
| `description` | `string` | — | Help text below label |
|
||
| `hint` | `string` | — | Inline hint (right-aligned) |
|
||
| `error` | `string` | — | Error message (replaces description) |
|
||
| `variant` | `'outlined' \| 'stacked'` | `'outlined'` | Label position |
|
||
| `size` | `'default' \| 'compact'` | `'default'` | Input height |
|
||
| `leftIcon` | `ReactNode` | — | Icon inside input (left) |
|
||
| `rightIcon` | `ReactNode` | — | Icon inside input (right) |
|
||
|
||
```tsx
|
||
import { Input } from '@/components/atoms/Input'
|
||
import { Search, Mail } from 'lucide-react'
|
||
|
||
<Input label="Email" type="email" placeholder="you@example.com" leftIcon={<Mail />} />
|
||
<Input label="Search" variant="stacked" size="compact" leftIcon={<Search />} />
|
||
<Input label="Name" error="Name is required" />
|
||
```
|
||
|
||
#### Textarea
|
||
|
||
Multi-line text input with optional auto-resize.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | **Required.** Visible label |
|
||
| `description` | `string` | — | Help text below label |
|
||
| `hint` | `string` | — | Inline hint |
|
||
| `error` | `string` | — | Error message |
|
||
| `variant` | `'outlined' \| 'stacked'` | `'outlined'` | Label position |
|
||
| `resize` | `'vertical' \| 'horizontal' \| 'both' \| 'none'` | `'vertical'` | Resize handle |
|
||
| `autoResize` | `boolean` | `false` | Auto-grow with content |
|
||
|
||
```tsx
|
||
import { Textarea } from '@/components/atoms/Textarea'
|
||
|
||
<Textarea label="Notes" placeholder="Add your notes..." rows={4} />
|
||
<Textarea label="Description" autoResize error="Too short" />
|
||
```
|
||
|
||
#### Select
|
||
|
||
Custom dropdown select with keyboard navigation.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | **Required.** Visible label |
|
||
| `description` | `string` | — | Help text |
|
||
| `hint` | `string` | — | Inline hint |
|
||
| `error` | `string` | — | Error message |
|
||
| `variant` | `'outlined' \| 'stacked'` | `'outlined'` | Label position |
|
||
| `placeholder` | `string` | — | Placeholder text |
|
||
| `options` | `SelectOption[]` | — | **Required.** `{ value, label, disabled? }` |
|
||
| `value` | `string` | — | Controlled value |
|
||
| `defaultValue` | `string` | — | Uncontrolled default |
|
||
| `onChange` | `(value: string) => void` | — | Change handler |
|
||
| `disabled` | `boolean` | `false` | Disables the select |
|
||
|
||
```tsx
|
||
import { Select } from '@/components/atoms/Select'
|
||
|
||
<Select
|
||
label="Status"
|
||
options={[
|
||
{ value: 'active', label: 'Active' },
|
||
{ value: 'archived', label: 'Archived' },
|
||
{ value: 'draft', label: 'Draft' },
|
||
]}
|
||
onChange={(val) => console.log(val)}
|
||
/>
|
||
```
|
||
|
||
#### Autocomplete
|
||
|
||
Combobox input with filtering, keyboard navigation, and optional free-text entry.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | **Required.** Visible label |
|
||
| `description` | `string` | — | Help text below label |
|
||
| `hint` | `string` | — | Inline hint |
|
||
| `error` | `string` | — | Error message |
|
||
| `placeholder` | `string` | — | Placeholder text |
|
||
| `options` | `AutocompleteOption[]` | — | **Required.** `{ value, label, disabled? }` |
|
||
| `value` | `string` | — | Controlled value |
|
||
| `onChange` | `(value: string) => void` | — | Change handler |
|
||
| `freeSolo` | `boolean` | `false` | Allow arbitrary text input |
|
||
| `disabled` | `boolean` | `false` | Disables the input |
|
||
| `loading` | `boolean` | `false` | Shows loading state |
|
||
| `noResultsText` | `string` | `'No results found'` | Empty state message |
|
||
|
||
```tsx
|
||
import { Autocomplete } from '@/components/atoms/Autocomplete'
|
||
|
||
<Autocomplete
|
||
label="Country"
|
||
options={[
|
||
{ value: 'au', label: 'Australia' },
|
||
{ value: 'nz', label: 'New Zealand' },
|
||
{ value: 'uk', label: 'United Kingdom' },
|
||
]}
|
||
onChange={(val) => setCountry(val)}
|
||
/>
|
||
<Autocomplete label="Tags" freeSolo options={tagOptions} />
|
||
```
|
||
|
||
#### Checkbox
|
||
|
||
Checkbox with label, description, error, and indeterminate state.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | Visible label |
|
||
| `description` | `string` | — | Help text below label |
|
||
| `error` | `string` | — | Error message |
|
||
| `indeterminate` | `boolean` | `false` | Shows minus icon |
|
||
|
||
```tsx
|
||
import { Checkbox } from '@/components/atoms/Checkbox'
|
||
|
||
<Checkbox label="I agree to the terms" />
|
||
<Checkbox label="Select all" indeterminate description="Some items are selected" />
|
||
<Checkbox label="Required" error="You must agree" />
|
||
```
|
||
|
||
#### Radio / RadioGroup
|
||
|
||
Radio buttons grouped with shared state.
|
||
|
||
| Prop (RadioGroup) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | Group label |
|
||
| `description` | `string` | — | Group help text |
|
||
| `error` | `string` | — | Error message |
|
||
| `value` | `string` | — | Controlled value |
|
||
| `defaultValue` | `string` | — | Uncontrolled default |
|
||
| `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
|
||
| `onChange` | `(value: string) => void` | — | Change handler |
|
||
|
||
| Prop (Radio) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | Radio label |
|
||
| `value` | `string` | — | **Required.** Radio value |
|
||
|
||
```tsx
|
||
import { RadioGroup, Radio } from '@/components/atoms/Radio'
|
||
|
||
<RadioGroup label="Priority" orientation="horizontal" onChange={setPriority}>
|
||
<Radio value="low" label="Low" />
|
||
<Radio value="medium" label="Medium" />
|
||
<Radio value="high" label="High" />
|
||
</RadioGroup>
|
||
```
|
||
|
||
#### Switch
|
||
|
||
Toggle switch with label and description.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | Visible label |
|
||
| `description` | `string` | — | Help text |
|
||
| `checked` | `boolean` | `false` | Controlled state |
|
||
| `onChange` | `(checked: boolean) => void` | — | Change handler |
|
||
| `disabled` | `boolean` | `false` | Disables the switch |
|
||
|
||
```tsx
|
||
import { Switch } from '@/components/atoms/Switch'
|
||
|
||
<Switch label="Dark mode" checked={dark} onChange={setDark} />
|
||
<Switch label="Notifications" description="Receive email alerts" />
|
||
```
|
||
|
||
#### Slider
|
||
|
||
Single-value slider with step support and keyboard navigation.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | Visible label |
|
||
| `value` | `number` | — | **Required.** Current value |
|
||
| `onChange` | `(value: number) => void` | — | **Required.** Change handler |
|
||
| `min` | `number` | `0` | Minimum value |
|
||
| `max` | `number` | `100` | Maximum value |
|
||
| `step` | `number` | `1` | Step increment |
|
||
| `disabled` | `boolean` | `false` | Disables the slider |
|
||
|
||
```tsx
|
||
import { Slider } from '@/components/atoms/Slider'
|
||
|
||
<Slider label="Volume" value={volume} onChange={setVolume} min={0} max={100} />
|
||
<Slider label="Opacity" value={opacity} onChange={setOpacity} min={0} max={1} step={0.1} />
|
||
```
|
||
|
||
#### RangeSlider
|
||
|
||
Dual-thumb range slider for selecting a value range.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | Visible label |
|
||
| `value` | `[number, number]` | — | **Required.** Current range |
|
||
| `onChange` | `(value: [number, number]) => void` | — | **Required.** Change handler |
|
||
| `min` | `number` | `0` | Minimum value |
|
||
| `max` | `number` | `100` | Maximum value |
|
||
| `step` | `number` | `1` | Step increment |
|
||
| `disabled` | `boolean` | `false` | Disables the slider |
|
||
|
||
```tsx
|
||
import { RangeSlider } from '@/components/atoms/Slider'
|
||
|
||
<RangeSlider label="Price range" value={[20, 80]} onChange={setRange} min={0} max={200} />
|
||
```
|
||
|
||
#### FileInput
|
||
|
||
File picker with multi-file support and removable file tags.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `label` | `string` | — | **Required.** Visible label |
|
||
| `description` | `string` | — | Help text |
|
||
| `error` | `string` | — | Error message |
|
||
| `accept` | `string` | — | Accepted file types (e.g., `.pdf,.docx`) |
|
||
| `multiple` | `boolean` | `false` | Allow multiple files |
|
||
| `disabled` | `boolean` | `false` | Disables the input |
|
||
| `onChange` | `(files: File[]) => void` | — | Change handler |
|
||
|
||
```tsx
|
||
import { FileInput } from '@/components/atoms/FileInput'
|
||
|
||
<FileInput label="Upload documents" accept=".pdf,.docx" multiple onChange={setFiles} />
|
||
<FileInput label="Profile photo" accept="image/*" />
|
||
```
|
||
|
||
#### Badge
|
||
|
||
Small status indicator with colour variants.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `variant` | `'navy' \| 'info' \| 'info-light' \| 'success' \| 'success-light' \| 'error' \| 'error-light' \| 'warning' \| 'warning-light' \| 'neutral' \| 'white'` | `'navy'` | Colour variant |
|
||
| `leftIcon` | `ReactNode` | — | Icon before text |
|
||
| `rightIcon` | `ReactNode` | — | Icon after text |
|
||
|
||
```tsx
|
||
import { Badge } from '@/components/atoms/Badge'
|
||
|
||
<Badge variant="success">Active</Badge>
|
||
<Badge variant="error-light">Overdue</Badge>
|
||
<Badge variant="info" leftIcon={<Clock />}>Pending</Badge>
|
||
```
|
||
|
||
#### Tag
|
||
|
||
Coloured label for categorisation.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `variant` | `'outline' \| 'filled' \| 'light'` | `'outline'` | Visual style |
|
||
| `color` | `'navy' \| 'blue' \| 'green' \| 'red' \| 'orange' \| 'grey' \| 'teal' \| 'brown' \| 'purple' \| 'fuchsia' \| 'yellow'` | `'navy'` | Colour |
|
||
| `size` | `'default' \| 'sm'` | `'default'` | Tag size |
|
||
| `icon` | `ReactNode` | — | Leading icon |
|
||
| `onRemove` | `() => void` | — | Shows remove button |
|
||
|
||
```tsx
|
||
import { Tag } from '@/components/atoms/Tag'
|
||
|
||
<Tag color="blue" variant="light">Research</Tag>
|
||
<Tag color="green" variant="filled" onRemove={handleRemove}>Complete</Tag>
|
||
```
|
||
|
||
#### Chip
|
||
|
||
Selectable/dismissible filter chip with optional colour variants.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `children` | `ReactNode` | — | **Required.** Chip label |
|
||
| `selected` | `boolean` | `false` | Selected state |
|
||
| `color` | `'default' \| 'info' \| 'error' \| 'warning' \| 'success'` | — | Colour variant (unselected only) |
|
||
| `onDismiss` | `() => void` | — | Shows dismiss icon, makes removable |
|
||
| `rightIcon` | `ReactNode` | — | Custom right icon |
|
||
|
||
```tsx
|
||
import { Chip } from '@/components/atoms/Chip'
|
||
|
||
<Chip selected={isActive} onClick={toggle}>Qualitative</Chip>
|
||
<Chip onDismiss={() => removeFilter('date')}>2024</Chip>
|
||
<Chip color="success">Approved</Chip>
|
||
```
|
||
|
||
#### Tabs
|
||
|
||
Tab navigation with controlled selection. Compound component: `Tabs`, `TabList`, `Tab`, `TabPanel`.
|
||
|
||
| Prop (Tabs) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `value` | `string` | — | **Required.** Active tab value |
|
||
| `onChange` | `(value: string) => void` | — | **Required.** Tab change handler |
|
||
|
||
| Prop (Tab) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `value` | `string` | — | **Required.** Tab identifier |
|
||
| `icon` | `ReactNode` | — | Icon before label |
|
||
| `disabled` | `boolean` | `false` | Disables the tab |
|
||
|
||
| Prop (TabPanel) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `value` | `string` | — | **Required.** Must match a Tab value |
|
||
|
||
```tsx
|
||
import { Tabs, TabList, Tab, TabPanel } from '@/components/atoms/Tabs'
|
||
|
||
<Tabs value={activeTab} onChange={setActiveTab}>
|
||
<TabList>
|
||
<Tab value="overview">Overview</Tab>
|
||
<Tab value="details" icon={<Info />}>Details</Tab>
|
||
<Tab value="settings">Settings</Tab>
|
||
</TabList>
|
||
<TabPanel value="overview">Overview content</TabPanel>
|
||
<TabPanel value="details">Details content</TabPanel>
|
||
<TabPanel value="settings">Settings content</TabPanel>
|
||
</Tabs>
|
||
```
|
||
|
||
#### List
|
||
|
||
Vertical list with icon support, active state, and optional links. Compound component: `List`, `ListItem`, `ListSubheader`, `ListDivider`.
|
||
|
||
| Prop (ListItem) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `icon` | `ReactNode` | — | Leading icon |
|
||
| `active` | `boolean` | `false` | Highlighted state |
|
||
| `disabled` | `boolean` | `false` | Disables the item |
|
||
| `href` | `string` | — | Renders as `<a>` instead of `<li>` |
|
||
|
||
```tsx
|
||
import { List, ListItem, ListSubheader, ListDivider } from '@/components/atoms/List'
|
||
import { Home, Settings, Users } from 'lucide-react'
|
||
|
||
<List>
|
||
<ListSubheader>Navigation</ListSubheader>
|
||
<ListItem icon={<Home />} active>Dashboard</ListItem>
|
||
<ListItem icon={<Users />}>Team</ListItem>
|
||
<ListDivider />
|
||
<ListItem icon={<Settings />}>Settings</ListItem>
|
||
</List>
|
||
```
|
||
|
||
#### Avatar
|
||
|
||
Circular avatar showing initials or an image.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `initials` | `string` | — | **Required.** Fallback text (first 2 chars, uppercased) |
|
||
| `src` | `string` | — | Image URL |
|
||
| `alt` | `string` | — | Image alt text |
|
||
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Avatar dimensions (32/40/48px) |
|
||
|
||
```tsx
|
||
import { Avatar } from '@/components/atoms/Avatar'
|
||
|
||
<Avatar initials="JD" />
|
||
<Avatar initials="MM" src="/photos/mary.jpg" alt="Mary McKay" size="lg" />
|
||
```
|
||
|
||
#### Tooltip
|
||
|
||
Floating tooltip on hover/focus with arrow.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `content` | `ReactNode` | — | **Required.** Tooltip content |
|
||
| `placement` | Floating UI `Placement` | `'top'` | Position relative to trigger |
|
||
| `delay` | `number \| { open?, close? }` | `{ open: 400, close: 0 }` | Show/hide delay (ms) |
|
||
| `children` | `ReactElement` | — | **Required.** Trigger element |
|
||
|
||
```tsx
|
||
import { Tooltip } from '@/components/atoms/Tooltip'
|
||
|
||
<Tooltip content="Save your changes">
|
||
<Button>Save</Button>
|
||
</Tooltip>
|
||
```
|
||
|
||
### Molecules
|
||
|
||
#### Card
|
||
|
||
Container with variant styles. Compound component: `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`.
|
||
|
||
| Prop (Card) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `variant` | `'surface' \| 'outlined' \| 'elevated' \| 'filled'` | `'surface'` | Visual style |
|
||
|
||
| Prop (CardHeader) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `action` | `ReactNode` | — | Action element (top-right) |
|
||
|
||
```tsx
|
||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/molecules/Card'
|
||
|
||
<Card variant="elevated">
|
||
<CardHeader action={<IconButton icon={<MoreHorizontal />} aria-label="Options" variant="tertiary" />}>
|
||
<CardTitle>Card Title</CardTitle>
|
||
<CardDescription>Supporting description text</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p>Card body content goes here.</p>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button variant="tertiary" size="compact">View details</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
```
|
||
|
||
#### Accordion
|
||
|
||
Collapsible content sections. Compound component: `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`.
|
||
|
||
| Prop (Accordion) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `type` | `'single' \| 'multiple'` | `'single'` | One or many open at once |
|
||
| `collapsible` | `boolean` | `false` | Allow closing all (single mode) |
|
||
| `defaultValue` | `string \| string[]` | — | Initially open item(s) |
|
||
| `value` | `string \| string[]` | — | Controlled open state |
|
||
| `onValueChange` | `(value) => void` | — | Change handler |
|
||
|
||
| Prop (AccordionItem) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `value` | `string` | — | **Required.** Unique item identifier |
|
||
| `disabled` | `boolean` | `false` | Prevents opening |
|
||
|
||
```tsx
|
||
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/molecules/Accordion'
|
||
|
||
<Accordion type="single" collapsible>
|
||
<AccordionItem value="section-1">
|
||
<AccordionTrigger>What is this?</AccordionTrigger>
|
||
<AccordionContent>Explanation here.</AccordionContent>
|
||
</AccordionItem>
|
||
<AccordionItem value="section-2">
|
||
<AccordionTrigger>How does it work?</AccordionTrigger>
|
||
<AccordionContent>Details here.</AccordionContent>
|
||
</AccordionItem>
|
||
</Accordion>
|
||
```
|
||
|
||
#### Alert
|
||
|
||
Contextual message with icon, title, close button, and action slot.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `variant` | `'info' \| 'warning' \| 'error' \| 'success' \| 'neutral'` | `'info'` | Alert type and colour |
|
||
| `title` | `string` | — | Bold heading |
|
||
| `onClose` | `() => void` | — | Shows close button |
|
||
| `action` | `ReactNode` | — | Action element (e.g. link or button) |
|
||
| `icon` | `ReactNode` | — | Custom icon (default based on variant) |
|
||
|
||
```tsx
|
||
import { Alert } from '@/components/molecules/Alert'
|
||
|
||
<Alert variant="success" title="Saved" onClose={dismiss}>
|
||
Your changes have been saved successfully.
|
||
</Alert>
|
||
<Alert variant="error" title="Error">Something went wrong.</Alert>
|
||
```
|
||
|
||
#### Dialog
|
||
|
||
Modal dialog with backdrop, size options, and composed sections. Uses the native `<dialog>` element. Compound component: `Dialog`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogContent`, `DialogFooter`, `DialogClose`.
|
||
|
||
| Prop (Dialog) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `open` | `boolean` | — | **Required.** Controls visibility |
|
||
| `onClose` | `() => void` | — | **Required.** Close handler |
|
||
| `size` | `'sm' \| 'default' \| 'lg' \| 'full'` | `'default'` | Max width |
|
||
| `closeOnBackdrop` | `boolean` | `true` | Close when clicking backdrop |
|
||
|
||
| Prop (DialogHeader) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `onClose` | `() => void` | — | Shows close button in header |
|
||
|
||
| Prop (DialogClose) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `onClose` | `() => void` | — | **Required.** Close handler |
|
||
| `children` | `ReactNode` | — | **Required.** Button content |
|
||
|
||
```tsx
|
||
import { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogContent, DialogFooter } from '@/components/molecules/Dialog'
|
||
|
||
<Dialog open={isOpen} onClose={() => setIsOpen(false)} size="sm">
|
||
<DialogHeader onClose={() => setIsOpen(false)}>
|
||
<DialogTitle>Confirm deletion</DialogTitle>
|
||
<DialogDescription>This action cannot be undone.</DialogDescription>
|
||
</DialogHeader>
|
||
<DialogContent>Are you sure you want to delete this item?</DialogContent>
|
||
<DialogFooter>
|
||
<Button variant="tertiary" onClick={() => setIsOpen(false)}>Cancel</Button>
|
||
<Button intent="danger">Delete</Button>
|
||
</DialogFooter>
|
||
</Dialog>
|
||
```
|
||
|
||
#### Popover
|
||
|
||
Floating content panel triggered by a child element. Compound component: `Popover`, `PopoverTrigger`, `PopoverContent`, `PopoverClose`.
|
||
|
||
| Prop (Popover) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `placement` | Floating UI `Placement` | `'bottom'` | Position relative to trigger |
|
||
| `open` | `boolean` | — | Controlled open state |
|
||
| `onOpenChange` | `(open: boolean) => void` | — | Open state handler |
|
||
|
||
| Prop (PopoverClose) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `onClose` | `() => void` | — | **Required.** Close handler |
|
||
| `children` | `ReactNode` | — | **Required.** Button content |
|
||
|
||
```tsx
|
||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/molecules/Popover'
|
||
|
||
<Popover placement="bottom-start">
|
||
<PopoverTrigger>
|
||
<Button variant="secondary">Options</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent>
|
||
<p>Popover content here.</p>
|
||
</PopoverContent>
|
||
</Popover>
|
||
```
|
||
|
||
#### DataTable
|
||
|
||
Data table with sorting, row selection, and pagination. Generic component supporting typed row data.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `columns` | `DataTableColumn<T>[]` | — | **Required.** Column definitions |
|
||
| `data` | `T[]` | — | **Required.** Row data array |
|
||
| `selectable` | `boolean` | `false` | Enable row selection with checkboxes |
|
||
| `pagination` | `boolean` | `true` | Show pagination controls |
|
||
| `pageSize` | `number` | `5` | Initial rows per page |
|
||
| `pageSizeOptions` | `number[]` | `[5, 10, 25]` | Page size dropdown options |
|
||
| `loading` | `boolean` | `false` | Shows loading state |
|
||
| `emptyMessage` | `string` | `'No data available'` | Empty state text |
|
||
| `onSelectionChange` | `(selected: T[]) => void` | — | Selection change handler |
|
||
|
||
**DataTableColumn:**
|
||
|
||
| Property | Type | Default | Description |
|
||
|----------|------|---------|-------------|
|
||
| `key` | `string` | — | **Required.** Row data key |
|
||
| `header` | `string` | — | **Required.** Column header text |
|
||
| `sortable` | `boolean` | `false` | Enable column sorting |
|
||
| `align` | `'left' \| 'center' \| 'right'` | `'left'` | Text alignment |
|
||
| `render` | `(value, row, index) => ReactNode` | — | Custom cell renderer |
|
||
|
||
```tsx
|
||
import { DataTable } from '@/components/molecules/DataTable'
|
||
|
||
<DataTable
|
||
columns={[
|
||
{ key: 'name', header: 'Name', sortable: true },
|
||
{ key: 'email', header: 'Email' },
|
||
{ key: 'role', header: 'Role', sortable: true },
|
||
{ key: 'status', header: 'Status', render: (val) => <Badge variant={val === 'active' ? 'success' : 'neutral'}>{val}</Badge> },
|
||
]}
|
||
data={users}
|
||
selectable
|
||
pageSize={10}
|
||
onSelectionChange={setSelected}
|
||
/>
|
||
```
|
||
|
||
### Organisms
|
||
|
||
#### TopBar
|
||
|
||
Fixed application header with logo, title, and action slots.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `title` | `string` | — | **Required.** Application title |
|
||
| `leading` | `ReactNode` | — | Leading element (e.g., hamburger menu) |
|
||
| `logo` | `ReactNode` | — | Logo element next to title |
|
||
| `children` | `ReactNode` | — | Trailing actions (e.g., notifications, avatar) |
|
||
|
||
```tsx
|
||
import { TopBar } from '@/components/organisms/TopBar'
|
||
|
||
<TopBar
|
||
title="Performance and development plan"
|
||
leading={<IconButton icon={<Menu />} aria-label="Menu" variant="tertiary" />}
|
||
logo={<img src="/nsw-logo.svg" alt="NSW" className="h-7" />}
|
||
>
|
||
<IconButton icon={<Bell />} aria-label="Notifications" variant="tertiary" />
|
||
<Avatar initials="MM" size="sm" />
|
||
</TopBar>
|
||
```
|
||
|
||
#### SideNav
|
||
|
||
Collapsible sidebar navigation with grouped items, badges, and alert indicators. Compound component: `SideNav`, `SideNavItem`, `SideNavGroup`, `SideNavDivider`.
|
||
|
||
| Prop (SideNav) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `collapsed` | `boolean` | `false` | Collapsed to icon-only mode (w-20) |
|
||
|
||
| Prop (SideNavItem) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `icon` | `ReactNode` | — | Leading icon |
|
||
| `active` | `boolean` | `false` | Active/current state |
|
||
| `badge` | `number` | — | Count badge |
|
||
| `alert` | `boolean \| 'info' \| 'success' \| 'warning' \| 'error'` | — | Alert dot indicator |
|
||
| `href` | `string` | — | Renders as `<a>` instead of `<button>` |
|
||
|
||
| Prop (SideNavGroup) | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `icon` | `ReactNode` | — | **Required.** Group icon |
|
||
| `label` | `string` | — | **Required.** Group label |
|
||
| `defaultOpen` | `boolean` | `false` | Initially expanded |
|
||
| `badge` | `number` | — | Count badge |
|
||
| `alert` | `boolean \| 'info' \| 'success' \| 'warning' \| 'error'` | — | Alert dot indicator |
|
||
| `active` | `boolean` | `false` | Highlight the group trigger |
|
||
|
||
```tsx
|
||
import { SideNav, SideNavItem, SideNavGroup, SideNavDivider } from '@/components/organisms/SideNav'
|
||
import { Home, FileText, Settings, Users } from 'lucide-react'
|
||
|
||
<SideNav collapsed={isCollapsed}>
|
||
<SideNavItem icon={<Home />} active>My status</SideNavItem>
|
||
<SideNavItem icon={<FileText />}>My details</SideNavItem>
|
||
<SideNavDivider />
|
||
<SideNavGroup icon={<Users />} label="PDP" defaultOpen>
|
||
<SideNavItem active>My PDP</SideNavItem>
|
||
<SideNavItem>PDP guide</SideNavItem>
|
||
<SideNavItem>Management</SideNavItem>
|
||
</SideNavGroup>
|
||
<SideNavItem icon={<Settings />}>Settings</SideNavItem>
|
||
</SideNav>
|
||
```
|
||
|
||
#### PageHeader
|
||
|
||
Page title section with themed background, decorative arcs, and optional icon.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `title` | `string` | — | **Required.** Page heading |
|
||
| `subtitle` | `string` | — | Supporting text below title |
|
||
| `icon` | `ReactNode` | — | Leading icon or illustration |
|
||
| `iconSize` | `string` | `'50px'` | Icon width/height |
|
||
| `theme` | `'light' \| 'dark'` | `'light'` | Background theme (light: blue-04 bg, dark: navy bg) |
|
||
| `centered` | `boolean` | `false` | Center-aligned layout |
|
||
| `noBackground` | `boolean` | `false` | No background or decorative arcs |
|
||
| `children` | `ReactNode` | — | Additional content below title |
|
||
|
||
```tsx
|
||
import { PageHeader } from '@/components/organisms/PageHeader'
|
||
|
||
<PageHeader title="My Workspace" subtitle="Accreditation Level: Maintaining Proficient Teacher">
|
||
<div className="flex gap-4 mt-2">
|
||
<Badge variant="info">Maintaining</Badge>
|
||
<Badge variant="navy">5 Years</Badge>
|
||
</div>
|
||
</PageHeader>
|
||
<PageHeader title="Welcome" theme="dark" centered />
|
||
<PageHeader title="Settings" noBackground />
|
||
```
|
||
|
||
### Templates
|
||
|
||
Templates are page-level layout components that define where content goes via typed slot props. They do not own content. Use them inside `AppShell` for full page layouts, or standalone for the content area only.
|
||
|
||
#### AppShell
|
||
|
||
Application shell that composes TopBar + SideNav + scrollable content area. All pages render inside this.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `topBar` | `ReactNode` | — | **Required.** TopBar component |
|
||
| `sideNav` | `ReactNode` | — | **Required.** SideNav component |
|
||
| `sideNavCollapsed` | `boolean` | `false` | Whether SideNav is in icon-only mode |
|
||
| `children` | `ReactNode` | — | Scrollable content area |
|
||
|
||
```tsx
|
||
import { AppShell } from '@/components/templates/AppShell'
|
||
|
||
<AppShell
|
||
topBar={<TopBar title="My App" />}
|
||
sideNav={<SideNav>{/* nav items */}</SideNav>}
|
||
>
|
||
{/* Page content */}
|
||
</AppShell>
|
||
```
|
||
|
||
#### DashboardPage
|
||
|
||
Dashboard layout with stat summary row and responsive 2-column content grid.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `header` | `ReactNode` | — | PageHeader or custom header |
|
||
| `stats` | `ReactNode` | — | Stat cards row (flex wrapped) |
|
||
| `children` | `ReactNode` | — | **Required.** Content grid items (2-col at lg) |
|
||
|
||
```tsx
|
||
import { DashboardPage } from '@/components/templates/DashboardPage'
|
||
|
||
<DashboardPage
|
||
header={<PageHeader title="Dashboard" />}
|
||
stats={<><StatCard /><StatCard /><StatCard /></>}
|
||
>
|
||
<Card>Left column</Card>
|
||
<Card>Right column</Card>
|
||
</DashboardPage>
|
||
```
|
||
|
||
#### ListPage
|
||
|
||
List layout with stat summary, list header with actions, and scrollable item list.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `header` | `ReactNode` | — | PageHeader or custom header |
|
||
| `stats` | `ReactNode` | — | Stat cards row |
|
||
| `listHeader` | `ReactNode` | — | Title + action button above the list |
|
||
| `children` | `ReactNode` | — | **Required.** List items (rendered inside divide-y container) |
|
||
|
||
```tsx
|
||
import { ListPage } from '@/components/templates/ListPage'
|
||
|
||
<ListPage
|
||
header={<PageHeader title="Activity Log" />}
|
||
stats={<><StatCard /><StatCard /></>}
|
||
listHeader={<div className="flex justify-between"><h2>Items</h2><Button>Add</Button></div>}
|
||
>
|
||
<div className="px-6 py-4">Item 1</div>
|
||
<div className="px-6 py-4">Item 2</div>
|
||
</ListPage>
|
||
```
|
||
|
||
#### FormPage
|
||
|
||
Form layout with optional vertical stepper and constrained-width content.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `header` | `ReactNode` | — | PageHeader or custom header |
|
||
| `actions` | `ReactNode` | — | Action bar above form (dropdowns, buttons) |
|
||
| `steps` | `FormPageStep[]` | — | Vertical stepper steps (`{ label, status }`) |
|
||
| `children` | `ReactNode` | — | **Required.** Form content (max-w-3xl centered) |
|
||
|
||
**FormPageStep:**
|
||
|
||
| Property | Type | Description |
|
||
|----------|------|-------------|
|
||
| `label` | `string` | Step label text |
|
||
| `status` | `'complete' \| 'current' \| 'upcoming'` | Step state |
|
||
|
||
```tsx
|
||
import { FormPage } from '@/components/templates/FormPage'
|
||
|
||
<FormPage
|
||
header={<PageHeader title="Setup" />}
|
||
steps={[
|
||
{ label: 'Your details', status: 'current' },
|
||
{ label: 'Review', status: 'upcoming' },
|
||
]}
|
||
>
|
||
<Card><CardContent><Input label="Name" /></CardContent></Card>
|
||
</FormPage>
|
||
```
|
||
|
||
#### DetailPage
|
||
|
||
Single-column detail view for records, profiles, or documents. Constrained width for readability.
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `header` | `ReactNode` | — | PageHeader or custom header |
|
||
| `actions` | `ReactNode` | — | Action bar below header (e.g. tabs, buttons) |
|
||
| `maxWidth` | `'md' \| 'lg' \| 'xl' \| 'full'` | `'xl'` | Content area max width |
|
||
| `children` | `ReactNode` | — | **Required.** Page content |
|
||
|
||
```tsx
|
||
import { DetailPage } from '@/components/templates/DetailPage'
|
||
|
||
<DetailPage
|
||
header={<PageHeader title="Alex Chen" subtitle="Senior Engineer" />}
|
||
actions={<Tabs value={tab} onChange={setTab}><TabList><Tab value="overview">Overview</Tab></TabList></Tabs>}
|
||
maxWidth="lg"
|
||
>
|
||
<Card><CardContent>Detail content</CardContent></Card>
|
||
</DetailPage>
|
||
```
|
||
|
||
---
|
||
|
||
## Do's and Don'ts
|
||
|
||
**Do:**
|
||
- Use semantic token classes (`bg-primary`, `text-error`, `border-control-border`), never palette tokens (`bg-blue-01`)
|
||
- Use the `cn()` utility from `@/lib/utils` for conditional classes
|
||
- Use `forwardRef` for components wrapping native elements
|
||
- Use semantic HTML (`<button>`, `<input>`, `<dialog>`) — not `<div onClick>`
|
||
- Include `aria-label` on icon-only buttons
|
||
- Use `lucide-react` for icons (already a dev dependency)
|
||
- Compose pages using AppShell (TopBar + SideNav + content) as the outer layout
|
||
- Use the spacing conventions (p-6 content padding, gap-6 between sections)
|
||
|
||
**Don't:**
|
||
- Hardcode colour hex values in component code
|
||
- Use inline styles (except for truly dynamic values like calculated positions)
|
||
- Use CSS modules or styled-components
|
||
- Skip the `label` prop on form controls — all inputs must have visible labels
|
||
- Nest interactive elements (button inside button, link inside button)
|
||
- Reference palette tokens (`text-blue-01`, `border-grey-03`) in component code — add a semantic token if one doesn't exist
|