Initial commit: FA Design System source files

Copy of the Funeral Arranger design system components, theme, tokens,
and Storybook config from the original Parsons project. Pre-upgrade
baseline with React 18, MUI v5, Storybook 8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 13:12:45 +10:00
commit 4cafd84142
2555 changed files with 40558 additions and 0 deletions

View File

@@ -0,0 +1,243 @@
# Component conventions
These conventions MUST be followed by any agent creating or modifying components.
## File structure
Every component lives in its own folder:
```
src/components/atoms/Button/
├── Button.tsx # The component
├── Button.stories.tsx # Storybook stories
├── Button.test.tsx # Unit tests (optional, added later)
└── index.ts # Re-export
```
The `index.ts` always looks like:
```typescript
export { default } from './Button';
export * from './Button';
```
## Component template
```typescript
import React from 'react';
import { ButtonBase, ButtonBaseProps } from '@mui/material';
import { styled, useTheme } from '@mui/material/styles';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the Button component */
export interface ButtonProps extends Omit<ButtonBaseProps, 'color'> {
/** Visual style variant */
variant?: 'contained' | 'outlined' | 'text';
/** Size preset */
size?: 'small' | 'medium' | 'large';
/** Colour intent */
color?: 'primary' | 'secondary' | 'neutral';
/** Show loading spinner and disable interaction */
loading?: boolean;
/** Full width of parent container */
fullWidth?: boolean;
}
// ─── Styled wrapper (if needed) ──────────────────────────────────────────────
const StyledButton = styled(ButtonBase, {
shouldForwardProp: (prop) =>
!['variant', 'size', 'color', 'loading', 'fullWidth'].includes(prop as string),
})<ButtonProps>(({ theme, variant, size, color }) => ({
// ALL values come from theme — never hardcode
borderRadius: theme.shape.borderRadius,
fontFamily: theme.typography.fontFamily,
fontWeight: theme.typography.fontWeightMedium,
transition: theme.transitions.create(
['background-color', 'box-shadow', 'border-color'],
{ duration: theme.transitions.duration.short }
),
// ... variant-specific styles using theme values
}));
// ─── Component ───────────────────────────────────────────────────────────────
/** Primary action button for the FA design system */
export const Button: React.FC<ButtonProps> = ({
variant = 'contained',
size = 'medium',
color = 'primary',
loading = false,
fullWidth = false,
children,
disabled,
...props
}) => {
return (
<StyledButton
variant={variant}
size={size}
color={color}
loading={loading}
fullWidth={fullWidth}
disabled={loading || disabled}
{...props}
>
{loading ? 'Loading…' : children}
</StyledButton>
);
};
export default Button;
```
## Rules
### Theming
- **NEVER hardcode** colour values, spacing, font sizes, shadows, or border radii
- **Semantic tokens** (text, surface, border, interactive, feedback, typography, spacing): use MUI theme accessors — `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*` — when inside a theme callback. CSS variables (`var(--fa-color-*)`, `var(--fa-spacing-*)`) are also acceptable for semantic tokens when more ergonomic (e.g., static colour maps, non-callback contexts).
- **Component tokens** (badge sizes, card shadows, input dimensions, etc.): use CSS variables (`var(--fa-badge-*)`, `var(--fa-card-*)`) — these are NOT mapped into the MUI theme.
- See decision D031 in `docs/memory/decisions-log.md` for full rationale.
- For one-off theme overrides, use the `sx` prop pattern
- Every component MUST accept and forward the `sx` prop for consumer overrides
### TypeScript
- Every prop must have a JSDoc description
- Use interface (not type) for props — interfaces produce better autodocs
- Export both the component and the props interface
- Use `Omit<>` to remove conflicting MUI base props
### Composition
- Prefer composition over configuration
- Small, focused components that compose well
- Avoid god-components with 20+ props — split into variants or sub-components
- Use React.forwardRef for all interactive elements (buttons, inputs, links)
### Accessibility
- All interactive elements must have a minimum 44×44px touch target
- Always include `aria-label` or visible label text
- Buttons must have `type="button"` unless they're form submit buttons
- Focus indicators must be visible — never remove outline without replacement
- Disabled elements should use `aria-disabled` alongside visual treatment
### MUI integration patterns
**When wrapping MUI components:**
```typescript
// Extend MUI's own props type
interface ButtonProps extends Omit<MuiButtonProps, 'color'> {
// Add your custom props, omitting any MUI props you're overriding
}
// Forward all unknown props to MUI
const Button: React.FC<ButtonProps> = ({ customProp, ...muiProps }) => {
return <MuiButton {...muiProps} />;
};
```
**When building from scratch with styled():**
```typescript
// Use shouldForwardProp to prevent custom props leaking to DOM
const StyledDiv = styled('div', {
shouldForwardProp: (prop) => prop !== 'isActive',
})<{ isActive?: boolean }>(({ theme, isActive }) => ({
backgroundColor: isActive
? theme.palette.primary.main
: theme.palette.background.paper,
}));
```
**Theme-aware responsive styles:**
```typescript
const StyledCard = styled(Card)(({ theme }) => ({
padding: theme.spacing(3),
[theme.breakpoints.up('md')]: {
padding: theme.spacing(4),
},
}));
```
## Storybook story template
```typescript
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button', // Tier/ComponentName
component: Button,
tags: ['autodocs'], // Always include for auto-documentation
parameters: {
layout: 'centered', // Use 'centered' for atoms, 'fullscreen' for layouts
},
argTypes: {
variant: {
control: 'select',
options: ['contained', 'outlined', 'text'],
description: 'Visual style variant',
table: { defaultValue: { summary: 'contained' } },
},
// ... one entry per prop
onClick: { action: 'clicked' }, // Log actions for event props
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// ─── Individual stories ──────────────────────────────────────────────────────
/** Default button appearance */
export const Default: Story = {
args: { children: 'Get started' },
};
/** All visual variants side by side */
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button variant="contained">Contained</Button>
<Button variant="outlined">Outlined</Button>
<Button variant="text">Text</Button>
</div>
),
};
/** All sizes side by side */
export const AllSizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
<Button size="large">Large</Button>
</div>
),
};
/** Interactive states */
export const Disabled: Story = {
args: { children: 'Disabled', disabled: true },
};
export const Loading: Story = {
args: { children: 'Submitting', loading: true },
};
```
### Story naming conventions
- `title`: Use atomic tier as prefix: `Atoms/Button`, `Molecules/PriceCard`, `Organisms/PricingTable`
- Individual stories: PascalCase, descriptive of the state or variant shown
- Always include: `Default`, `AllVariants` (if applicable), `AllSizes` (if applicable), `Disabled`
- For composed components, include a story showing the component with realistic content
### Story coverage checklist
For every component, stories must cover:
- [ ] Default state with typical content
- [ ] All visual variants side by side
- [ ] All sizes side by side (if applicable)
- [ ] Disabled state
- [ ] Loading state (if applicable)
- [ ] Error state (if applicable)
- [ ] Long content / content overflow
- [ ] Empty/minimal content
- [ ] With and without optional elements (icons, badges, etc.)

View File

@@ -0,0 +1,144 @@
# Token conventions
These conventions MUST be followed by any agent creating or modifying tokens.
## W3C DTCG format
All tokens use the DTCG JSON format. Every token has `$value`, `$type`, and `$description`.
```json
{
"color": {
"brand": {
"primary": {
"$value": "#1B4965",
"$type": "color",
"$description": "Primary brand colour — deep navy. Used for primary actions, key headings, and trust-building UI elements."
}
}
}
}
```
## Naming rules
### General
- Use dot notation for hierarchy in documentation: `color.brand.primary`
- In JSON, dots become nested objects
- Names are lowercase, no spaces, no special characters except hyphens for compound words
- Names must be descriptive of purpose (semantic) or scale position (primitive)
### Primitive naming
Primitives describe WHAT the value is, not WHERE it's used.
```
color.{hue}.{scale} → color.blue.500, color.neutral.100
spacing.{scale} → spacing.1, spacing.2, spacing.4, spacing.8
fontSize.{scale} → fontSize.xs, fontSize.sm, fontSize.base, fontSize.lg
fontWeight.{name} → fontWeight.regular, fontWeight.medium, fontWeight.bold
fontFamily.{purpose} → fontFamily.heading, fontFamily.body, fontFamily.mono
borderRadius.{scale} → borderRadius.sm, borderRadius.md, borderRadius.lg, borderRadius.full
shadow.{scale} → shadow.sm, shadow.md, shadow.lg
lineHeight.{scale} → lineHeight.tight, lineHeight.normal, lineHeight.relaxed
letterSpacing.{scale} → letterSpacing.tight, letterSpacing.normal, letterSpacing.wide
opacity.{scale} → opacity.disabled, opacity.hover, opacity.overlay
```
### Colour scale convention
Use a 50-950 scale (matching Tailwind/MUI convention):
- 50: Lightest tint (backgrounds, subtle fills)
- 100-200: Light tints (hover states, borders)
- 300-400: Mid tones (secondary text, icons)
- 500: Base/reference value
- 600-700: Strong tones (primary text on light bg, active states)
- 800-900: Darkest shades (headings, high-contrast text)
- 950: Near-black (used sparingly)
### Semantic naming
Semantic tokens describe WHERE and WHY a value is used.
```
color.text.{variant} → color.text.primary, color.text.secondary, color.text.disabled, color.text.inverse
color.surface.{variant} → color.surface.default, color.surface.raised, color.surface.overlay
color.border.{variant} → color.border.default, color.border.strong, color.border.subtle
color.interactive.{state} → color.interactive.default, color.interactive.hover, color.interactive.active, color.interactive.disabled
color.feedback.{type} → color.feedback.success, color.feedback.warning, color.feedback.error, color.feedback.info
spacing.component.{size} → spacing.component.xs, spacing.component.sm, spacing.component.md, spacing.component.lg
spacing.layout.{size} → spacing.layout.section, spacing.layout.page, spacing.layout.gutter
typography.{role} → typography.display, typography.heading, typography.body, typography.caption, typography.label
```
### Component token naming
Component tokens are scoped to a specific component.
```
{component}.{property}.{state}
button.background.default
button.background.hover
button.background.active
button.background.disabled
button.text.default
button.text.disabled
button.border.default
button.border.focus
button.padding.horizontal
button.padding.vertical
button.borderRadius
card.background.default
card.border.default
card.padding
card.borderRadius
card.shadow
input.background.default
input.background.focus
input.border.default
input.border.error
input.border.focus
input.text.default
input.text.placeholder
```
## Alias rules
- Semantic tokens MUST reference primitives (never hardcode values)
- Component tokens MUST reference semantic tokens (never reference primitives directly)
- This creates the chain: component → semantic → primitive
- Exception: spacing and borderRadius component tokens may reference primitives directly when the semantic layer adds no value
```json
// CORRECT: component → semantic → primitive
"button.background.default": { "$value": "{color.interactive.default}" }
"color.interactive.default": { "$value": "{color.brand.primary}" }
"color.brand.primary": { "$value": "{color.blue.700}" }
"color.blue.700": { "$value": "#1B4965" }
// WRONG: component referencing a primitive directly
"button.background.default": { "$value": "{color.blue.700}" }
```
## Accessibility requirements
- All colour combinations used for text must meet WCAG 2.1 AA contrast ratio (4.5:1 for normal text, 3:1 for large text)
- Interactive elements must have a visible focus indicator
- Disabled states must still be distinguishable from enabled states
- When creating colour tokens, note the contrast ratio with common background colours in the `$description`
## File organisation
```
tokens/
├── primitives/
│ ├── colours.json # All colour primitives (brand, neutral, feedback hues)
│ ├── typography.json # Font families, sizes, weights, line heights
│ ├── spacing.json # Spacing scale, border radius, sizing
│ └── effects.json # Shadows, opacity values
├── semantic/
│ ├── colours.json # Semantic colour mappings
│ ├── typography.json # Typography role mappings
│ └── spacing.json # Layout and component spacing
└── component/
├── button.json # Button-specific tokens
├── input.json # Input-specific tokens
├── card.json # Card-specific tokens
└── ... # One file per component that needs specific tokens
```

174
docs/design-system.md Normal file
View File

@@ -0,0 +1,174 @@
# FA Design System
This is the living design system specification. It is the primary reference for
all agents when creating tokens, components, or compositions.
**This file will be updated progressively as the system is built.**
## Brand context
Funeral Arranger is an Australian online funeral planning platform. It connects
families with funeral directors and provides transparent pricing and service
comparison. The design must:
- **Feel warm and trustworthy** — families are often in grief or distress
- **Prioritise clarity** — reduce cognitive load, no visual noise
- **Be transparent** — pricing, options, and processes should feel open
- **Respect cultural sensitivity** — serve diverse Australian communities
- **Be accessible** — WCAG 2.1 AA minimum across all components
## Brand colours
### Primary palette — Brand (warm gold/copper)
Derived from Parsons brand swatches. The warm gold family conveys trust and warmth.
| Step | Token | Value | Usage |
|------|-------|-------|-------|
| 50 | color.brand.50 | #FEF9F5 | Warm section backgrounds |
| 100 | color.brand.100 | #F7ECDF | Hover backgrounds, subtle fills |
| 200 | color.brand.200 | #EBDAC8 | Secondary backgrounds |
| 300 | color.brand.300 | #D8C3B5 | Surface warmth, card tints |
| 400 | color.brand.400 | #D0A070 | Secondary interactive, step indicators |
| **500** | **color.brand.500** | **#BA834E** | **Primary CTA, main interactive** |
| 600 | color.brand.600 | #B0610F | Hover/emphasis, brand links (4.8:1 on white) |
| 700 | color.brand.700 | #8B4E0D | Active states, brand text (6.7:1 on white) |
| 800 | color.brand.800 | #6B3C13 | Bold brand accents |
| 900 | color.brand.900 | #51301B | Deep emphasis, dark brand surfaces |
| 950 | color.brand.950 | #251913 | Darkest brand tone |
### Secondary palette — Sage (cool grey-green)
Calming, professional secondary palette for the funeral services context.
| Step | Token | Value | Usage |
|------|-------|-------|-------|
| 50 | color.sage.50 | #F2F5F6 | Cool section backgrounds |
| 200 | color.sage.200 | #D7E1E2 | Light cool surfaces |
| 400 | color.sage.400 | #B9C7C9 | Mid sage accents |
| 700 | color.sage.700 | #4C5B6B | Secondary buttons, dark accents (6.1:1 on white) |
| 800 | color.sage.800 | #4C5459 | Supplementary text (6.7:1 on white) |
### Neutral palette
True grey for text, borders, and UI chrome. Cool-tinted charcoal (#2C2E35) for primary text.
| Step | Token | Value | Usage |
|------|-------|-------|-------|
| 50 | color.neutral.50 | #FAFAFA | Page background alternative |
| 100 | color.neutral.100 | #F5F5F5 | Subtle backgrounds |
| 200 | color.neutral.200 | #E8E8E8 | Borders, dividers |
| 300 | color.neutral.300 | #D4D4D4 | Disabled borders |
| 400 | color.neutral.400 | #A3A3A3 | Placeholder text, disabled content |
| 500 | color.neutral.500 | #737373 | Tertiary text, icons |
| 600 | color.neutral.600 | #525252 | Secondary text (7.1:1 on white) |
| 700 | color.neutral.700 | #404040 | Strong text (9.7:1 on white) |
| **800** | **color.neutral.800** | **#2C2E35** | **Primary text colour (13.2:1 on white)** |
| 900 | color.neutral.900 | #1A1A1C | Maximum contrast |
### Feedback colours
| Type | Token | Value | Background token | Background value |
|------|-------|-------|-----------------|------------------|
| Success | color.feedback.success | #3B7A3B (green.600) | color.feedback.success-subtle | #F0F7F0 |
| Warning | color.feedback.warning | #CC8500 (amber.600) | color.feedback.warning-subtle | #FFF9EB |
| Error | color.feedback.error | #BC2F2F (red.600) | color.feedback.error-subtle | #FEF2F2 |
| Info | color.feedback.info | #2563EB (blue.600) | color.feedback.info-subtle | #EFF6FF |
## Typography
### Font stack
| Role | Family | Fallback | Weight range |
|------|--------|----------|-------------|
| Display/Headings (H1-H2) | Noto Serif SC | Georgia, Times New Roman, serif | 600-700 |
| Body/Headings (H3+) | Montserrat | Helvetica Neue, Arial, sans-serif | 400-700 |
| Mono | JetBrains Mono | Fira Code, Consolas, monospace | 400 |
### Type scale
| Role | Size | Line height | Weight | Letter spacing | Token |
|------|------|-------------|--------|----------------|-------|
| Display | 36px | 44px | 700 | -0.02em | typography.display |
| H1 | 30px | 38px | 700 | -0.01em | typography.h1 |
| H2 | 24px | 32px | 600 | 0 | typography.h2 |
| H3 | 20px | 28px | 600 | 0 | typography.h3 |
| H4 | 18px | 24px | 600 | 0 | typography.h4 |
| Body Large | 18px | 28px | 400 | 0 | typography.bodyLarge |
| Body | 16px | 24px | 400 | 0 | typography.body |
| Body Small | 14px | 20px | 400 | 0 | typography.bodySmall |
| Caption | 12px | 16px | 400 | 0.02em | typography.caption |
| Label | 14px | 20px | 500 | 0.01em | typography.label |
| Overline | 12px | 16px | 600 | 0.08em | typography.overline |
## Spacing system
Base unit: 4px. All spacing values are multiples of 4.
| Token | Value | Typical usage |
|-------|-------|---------------|
| spacing.0.5 | 2px | Hairline gaps (icon-to-text tight) |
| spacing.1 | 4px | Tight inline spacing |
| spacing.2 | 8px | Related element gap, small padding |
| spacing.3 | 12px | Component internal padding (small) |
| spacing.4 | 16px | Component internal padding (default), form field gap |
| spacing.5 | 20px | Medium component spacing |
| spacing.6 | 24px | Card padding, section gap (small) |
| spacing.8 | 32px | Section gap (medium) |
| spacing.10 | 40px | Section gap (large) |
| spacing.12 | 48px | Page section separation |
| spacing.16 | 64px | Hero/banner vertical spacing |
| spacing.20 | 80px | Major page sections |
## Border radius
| Token | Value | Usage |
|-------|-------|-------|
| borderRadius.none | 0px | Square corners (tables, dividers) |
| borderRadius.sm | 4px | Inputs, small interactive elements |
| borderRadius.md | 8px | Cards, buttons, dropdowns |
| borderRadius.lg | 12px | Modals, large cards |
| borderRadius.xl | 16px | Feature cards, hero elements |
| borderRadius.full | 9999px | Pills, avatars, circular elements |
## Shadows
| Token | Value | Usage |
|-------|-------|-------|
| shadow.sm | 0 1px 2px rgba(0,0,0,0.05) | Subtle lift (buttons on hover) |
| shadow.md | 0 4px 6px rgba(0,0,0,0.07) | Cards, dropdowns |
| shadow.lg | 0 10px 15px rgba(0,0,0,0.1) | Modals, popovers |
| shadow.xl | 0 20px 25px rgba(0,0,0,0.1) | Elevated panels |
## Responsive breakpoints
| Name | Value | Target |
|------|-------|--------|
| xs | 0px | Mobile portrait |
| sm | 600px | Mobile landscape / small tablet |
| md | 900px | Tablet |
| lg | 1200px | Desktop |
| xl | 1536px | Large desktop |
### Layout conventions
- Max content width: 1200px (`Container maxWidth="lg"`)
- Page horizontal padding: `spacing.4` (mobile), `spacing.8` (desktop)
- Section vertical spacing: `spacing.12`
- Card grid gutter: `spacing.4` (mobile), `spacing.6` (desktop)
## Component conventions
### Interactive elements
- Minimum touch target: 44px height on mobile
- Focus-visible outline: 2px solid `color.interactive.default`, 2px offset
- Hover transition: 150ms ease-in-out
- Active state: slightly darkened background (5-10%)
- Disabled: 40% opacity, no pointer events
### Cards
- Border radius: `borderRadius.md` (8px)
- Internal padding: `spacing.4` (mobile), `spacing.6` (desktop)
- Shadow: `shadow.md` by default, `shadow.lg` on hover
- Price displays: use Display or H2 typography with brand primary colour
### Forms
- Labels above inputs, using Label typography
- Helper text below inputs, using Caption typography in `color.text.secondary`
- Error text replaces helper text in `color.feedback.error`
- Input height: 44px (matching button medium height)
- Field gap: `spacing.4` (16px)
- Section gap within forms: `spacing.8` (32px)