--- 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: "#69B3E7" blue-04: "#CBEDFD" blue-05: "#EBF5FF" red-01: "#3E0014" red-02: "#D7153A" red-03: "#F5C5D0" red-04: "#FDDDE5" red-05: "#FFF5F8" orange-01: "#7A3300" orange-02: "#EC6608" orange-03: "#F5B98A" orange-04: "#FEF0E4" green-01: "#005C35" green-02: "#00A651" green-03: "#89E5B3" green-04: "#E0F8EA" grey-01: "#22272B" grey-02: "#6D7278" grey-03: "#C0C0C0" grey-04: "#E0E0E0" off-white: "#F4F4F4" white: "#FFFFFF" # Semantic (use these in components) primary: "{colors.blue-02}" primary-dark: "{colors.blue-01}" 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.off-white}" surface: "{colors.white}" 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: 6px lg: 10px xl: 16px full: 9999px spacing: unit: 4px scale: "Tailwind default (4px base: 1=4px, 2=8px, 3=12px, 4=16px, 6=24px, 8=32px)" components: button-primary-default: backgroundColor: "{colors.primary-dark}" textColor: "{colors.white}" typography: "{typography.body}" rounded: "{rounded.DEFAULT}" height: 48px button-primary-danger: backgroundColor: "{colors.red-02}" textColor: "{colors.white}" button-secondary-default: backgroundColor: transparent textColor: "{colors.primary-dark}" input: backgroundColor: "{colors.white}" textColor: "{colors.text}" typography: "{typography.body}" rounded: "{rounded.DEFAULT}" height: 48px card-surface: backgroundColor: "{colors.surface}" rounded: "{rounded.xl}" badge-info: backgroundColor: "{colors.blue-02}" textColor: "{colors.white}" rounded: "{rounded.full}" alert-info: backgroundColor: "{colors.blue-05}" rounded: "{rounded.DEFAULT}" --- ## 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 blue/grey palette with semantic colour 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 three layers: 1. **Palette** — Raw values (`blue-01` through `blue-05`, `grey-01` through `grey-04`, etc.). Never use these directly in component code. 2. **Semantic** — Purpose-based aliases (`primary`, `error`, `success`, `warning`, `text`, `surface`, `bg`, `border`). Use these for general UI. 3. **Domain** — Component-specific tokens (`button-default`, `control-border`, `badge-info`, `alert-error-bg`, `tag-navy`, etc.). Use these within their respective components. The primary brand colour is `blue-01` (#002664, dark navy) for interactive elements like buttons and links. `blue-02` (#146CFD) is the brighter accent used 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. This may change in future. ## 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` (6px) — buttons, inputs, alerts - `rounded-lg` (10px) — cards, dialogs - `rounded-xl` (16px) — large containers - `rounded-full` (9999px) — badges, circular buttons ## Components ### 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' Save }>Delete Cancel ``` ### 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' } aria-label="Close" variant="tertiary" /> } 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' } /> } /> ``` ### 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' ``` ### 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' console.log(val)} /> ``` ### 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' ``` ### 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' ``` ### 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' ``` ### 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' Active Overdue }>Pending ``` ### Chip Selectable/dismissible filter chip. | Prop | Type | Default | Description | |------|------|---------|-------------| | `children` | `ReactNode` | — | **Required.** Chip label | | `selected` | `boolean` | `false` | Selected state | | `onDismiss` | `() => void` | — | Shows dismiss icon, makes removable | | `rightIcon` | `ReactNode` | — | Custom right icon | ```tsx import { Chip } from '@/components/atoms/Chip' Qualitative removeFilter('date')}>2024 ``` ### Tag Coloured label for categorisation. | Prop | Type | Default | Description | |------|------|---------|-------------| | `variant` | `'outline' \| 'filled' \| 'light'` | `'outline'` | Visual style | | `color` | `'navy' \| 'blue' \| 'green' \| 'red' \| 'orange' \| 'grey'` | `'navy'` | Colour | | `size` | `'default' \| 'sm'` | `'default'` | Tag size | | `icon` | `ReactNode` | — | Leading icon | | `onRemove` | `() => void` | — | Shows remove button | ```tsx import { Tag } from '@/components/atoms/Tag' Research Complete ``` ### 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' Save ``` ### Card Container with variant styles. Composed of `Card`, `CardHeader`, `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, CardContent, CardFooter } from '@/components/molecules/Card' } aria-label="Options" variant="tertiary" />}> Card Title Card body content goes here. View details ``` ### Accordion Collapsible content sections. Composed of `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' What is this? Explanation here. How does it work? Details here. ``` ### 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' Your changes have been saved successfully. Something went wrong. ``` ### Dialog Modal dialog with backdrop, size options, and composed sections. | 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 | Composed with `DialogHeader`, `DialogContent`, `DialogFooter`. ```tsx import { Dialog, DialogHeader, DialogContent, DialogFooter } from '@/components/molecules/Dialog' setIsOpen(false)} size="sm"> setIsOpen(false)}>Confirm Are you sure you want to delete this? setIsOpen(false)}>Cancel Delete ``` ### Popover Floating content panel triggered by a child element. Composed of `Popover`, `PopoverTrigger`, `PopoverContent`. | 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 | ```tsx import { Popover, PopoverTrigger, PopoverContent } from '@/components/molecules/Popover' Options Popover content here. ``` ## 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 (``, ``, ``) — not `` - Include `aria-label` on icon-only buttons - Use `lucide-react` for icons (already a dev dependency) **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)
Card body content goes here.
Popover content here.