Files
ADS3-Design-System/DESIGN.md
Richie d36330084a Add CenteredPage template and fix SideNav collapse animation
Add CenteredPage template for no-sidebar, centered-content layouts
(login, error, onboarding). Fix SideNav collapse animation where items
slid right — removed nav-level items-center (each item already handles
its own centering) and added overflow-hidden to clip text during the
width transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-03 15:38:03 +10:00

1136 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 600700; 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>
```
#### CenteredPage
Full-page layout with no sidebar and centered content. Use for login, sign-up, error pages, onboarding, or any focused single-task flow.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `topBar` | `ReactNode` | — | Optional TopBar |
| `maxWidth` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Content max width |
| `children` | `ReactNode` | — | **Required.** Centered content |
```tsx
import { CenteredPage } from '@/components/templates/CenteredPage'
<CenteredPage topBar={<TopBar title="" logo={<NswLogo />} />} maxWidth="md">
<Card variant="elevated">
<CardContent>Login form here</CardContent>
</Card>
</CenteredPage>
```
---
## 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