Rebrand to ADS 3.0 Design System and add DESIGN.md component reference
Convert from Research Synthesiser-specific project to general-purpose ADS 3.0 design system intended to be forked for downstream applications. Add DESIGN.md following Google Labs spec as machine-readable reference for AI coding agents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
586
DESIGN.md
Normal file
586
DESIGN.md
Normal file
@@ -0,0 +1,586 @@
|
||||
---
|
||||
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'
|
||||
|
||||
<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)}
|
||||
/>
|
||||
```
|
||||
|
||||
### 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" />
|
||||
```
|
||||
|
||||
### 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>
|
||||
```
|
||||
|
||||
### 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'
|
||||
|
||||
<Chip selected={isActive} onClick={toggle}>Qualitative</Chip>
|
||||
<Chip onDismiss={() => removeFilter('date')}>2024</Chip>
|
||||
```
|
||||
|
||||
### 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'
|
||||
|
||||
<Tag color="blue" variant="light">Research</Tag>
|
||||
<Tag color="green" variant="filled" onRemove={handleRemove}>Complete</Tag>
|
||||
```
|
||||
|
||||
### 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>
|
||||
```
|
||||
|
||||
### 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'
|
||||
|
||||
<Card variant="elevated">
|
||||
<CardHeader action={<IconButton icon={<MoreHorizontal />} aria-label="Options" variant="tertiary" />}>
|
||||
<h3>Card Title</h3>
|
||||
</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. 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'
|
||||
|
||||
<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.
|
||||
|
||||
| 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'
|
||||
|
||||
<Dialog open={isOpen} onClose={() => setIsOpen(false)} size="sm">
|
||||
<DialogHeader onClose={() => setIsOpen(false)}>Confirm</DialogHeader>
|
||||
<DialogContent>Are you sure you want to delete this?</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. 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'
|
||||
|
||||
<Popover placement="bottom-start">
|
||||
<PopoverTrigger>
|
||||
<Button variant="secondary">Options</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<p>Popover content here.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
```
|
||||
|
||||
## 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)
|
||||
|
||||
**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 in New Issue
Block a user