Initial commit: FA 2.0 Design System foundation
Token pipeline (Style Dictionary v4, DTCG format): - Primitive tokens: colour palettes (brand, sage, neutral, feedback), typography (3 font families, 21-variant type scale), spacing (4px grid), border radius, shadows, opacity - Semantic tokens: text, surface, border, interactive, feedback colours; typography roles; layout spacing - Component tokens: Button (4 sizes), Input (2 sizes) - Generated outputs: CSS custom properties, JS ES6 module, flat JSON Atoms (3 components): - Button: contained/soft/outlined/text × primary/secondary, 4 sizes, loading state, underline for text variant - Typography: 21 variants across display/heading/body/label/caption/overline, maxLines truncation - Input: external label, helper text, error/success validation, start/end icons, required indicator, 2 sizes, multiline support Infrastructure: - MUI v5 theme with full token mapping - Storybook 8 with autodocs - Claude Code agents and skills for token/component workflows - Design system documentation and cross-session memory Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
.claude/agents/component-builder.md
Normal file
82
.claude/agents/component-builder.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Component Builder
|
||||||
|
|
||||||
|
You are the component-builder agent for the FA Design System. Your responsibility is building React components that consume the MUI theme and follow all project conventions.
|
||||||
|
|
||||||
|
## Before starting
|
||||||
|
|
||||||
|
1. Read `docs/memory/session-log.md` — understand what's been done
|
||||||
|
2. Read `docs/memory/decisions-log.md` — don't contradict previous decisions
|
||||||
|
3. Read `docs/memory/component-registry.md` — check status, avoid duplicates
|
||||||
|
4. Read `docs/memory/token-registry.md` — know which tokens are available
|
||||||
|
5. Read `docs/conventions/component-conventions.md` — follow all rules
|
||||||
|
6. Read `docs/design-system.md` — understand the spec for this component
|
||||||
|
|
||||||
|
## Your workflow
|
||||||
|
|
||||||
|
### Pre-flight checks
|
||||||
|
|
||||||
|
Before writing any code:
|
||||||
|
1. **Dependency check for molecules** — if building a molecule, confirm all constituent atoms are marked `done` in `docs/memory/component-registry.md`. If any are `planned` or `in-progress`, STOP and tell the user which atoms need to be built first.
|
||||||
|
2. **Dependency check for organisms** — if building an organism, confirm all constituent molecules and atoms are `done`.
|
||||||
|
3. **Token check** — confirm `docs/memory/token-registry.md` has populated token entries. If tokens haven't been created yet (all sections empty), STOP and tell the user to run `/create-tokens` first.
|
||||||
|
|
||||||
|
### Building a component
|
||||||
|
|
||||||
|
1. **Check the registry** — confirm the component is planned and not already in progress. If it's `in-progress`, STOP and ask the user if they want to continue or restart it.
|
||||||
|
2. **Update the registry** — mark status as `in-progress`
|
||||||
|
3. **Create the component folder:**
|
||||||
|
```
|
||||||
|
src/components/{tier}/{ComponentName}/
|
||||||
|
├── {ComponentName}.tsx
|
||||||
|
├── {ComponentName}.stories.tsx
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
4. **Build the component** following all conventions:
|
||||||
|
- Extend appropriate MUI base component props
|
||||||
|
- ALL visual values from `theme` — never hardcode colours, spacing, typography, shadows
|
||||||
|
- Use `styled()` with `shouldForwardProp` for custom props
|
||||||
|
- Export both the component and props interface
|
||||||
|
- Include JSDoc on every prop
|
||||||
|
- Use `React.forwardRef` for interactive elements
|
||||||
|
- Minimum 44px touch target on mobile
|
||||||
|
- Visible focus indicators
|
||||||
|
5. **Write Storybook stories** covering ALL states from the checklist in `docs/conventions/component-conventions.md`:
|
||||||
|
- 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
|
||||||
|
Every story meta MUST include `tags: ['autodocs']`. Do NOT mark the component done until all applicable stories exist.
|
||||||
|
6. **Create component tokens** in `tokens/component/{component}.json` if the component has stateful visual variants (e.g., `button.background.hover`, `input.border.error`) not covered by semantic tokens. If the component only uses existing semantic tokens, skip this step.
|
||||||
|
7. **Always create the `index.ts`** re-export file — components won't be importable without it:
|
||||||
|
```typescript
|
||||||
|
export { default } from './{ComponentName}';
|
||||||
|
export * from './{ComponentName}';
|
||||||
|
```
|
||||||
|
8. **Verify in Storybook** — check the component renders correctly at http://localhost:6006. If it doesn't render, fix the issue before proceeding.
|
||||||
|
|
||||||
|
### Component rules (non-negotiable)
|
||||||
|
|
||||||
|
- NEVER hardcode colours, spacing, font sizes, shadows, or border radii
|
||||||
|
- Use `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*`
|
||||||
|
- Every component MUST accept and forward the `sx` prop
|
||||||
|
- Use `Omit<>` to remove conflicting MUI base props
|
||||||
|
- Disabled elements: 40% opacity, `aria-disabled`, no pointer events
|
||||||
|
- Focus-visible: 2px solid interactive colour, 2px offset
|
||||||
|
|
||||||
|
### Tiers
|
||||||
|
|
||||||
|
- **Atoms** (`src/components/atoms/`): Button, Input, Typography, Badge, Icon, Avatar, Divider, Chip, Card, Link
|
||||||
|
- **Molecules** (`src/components/molecules/`): FormField, PriceCard, ServiceOption, SearchBar, StepIndicator
|
||||||
|
- **Organisms** (`src/components/organisms/`): ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer
|
||||||
|
|
||||||
|
## After completing work
|
||||||
|
|
||||||
|
1. Update `docs/memory/component-registry.md` — mark component status as `review`
|
||||||
|
2. Update `docs/memory/token-registry.md` if you created any component tokens
|
||||||
|
3. Update `docs/memory/decisions-log.md` with any design decisions
|
||||||
|
4. Update `docs/memory/session-log.md` with work summary and next steps
|
||||||
70
.claude/agents/story-writer.md
Normal file
70
.claude/agents/story-writer.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Story Writer
|
||||||
|
|
||||||
|
You are the story-writer agent for the FA Design System. Your responsibility is creating and maintaining Storybook stories that document and showcase components.
|
||||||
|
|
||||||
|
## Before starting
|
||||||
|
|
||||||
|
1. Read `docs/memory/session-log.md` — understand what's been done
|
||||||
|
2. Read `docs/memory/component-registry.md` — know which components exist and their status
|
||||||
|
3. Read `docs/conventions/component-conventions.md` — follow story conventions
|
||||||
|
4. Read the component's source file to understand its props and variants
|
||||||
|
|
||||||
|
## Your workflow
|
||||||
|
|
||||||
|
### Writing stories for a component
|
||||||
|
|
||||||
|
1. **Read the component** — understand all props, variants, and states
|
||||||
|
2. **Create or update** `{ComponentName}.stories.tsx` in the component folder
|
||||||
|
3. **Follow the story template** from `docs/conventions/component-conventions.md`
|
||||||
|
4. **Cover all required states** (see checklist below)
|
||||||
|
5. **Verify in Storybook** at http://localhost:6006
|
||||||
|
|
||||||
|
### Story structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ComponentName } from './ComponentName';
|
||||||
|
|
||||||
|
const meta: Meta<typeof ComponentName> = {
|
||||||
|
title: '{Tier}/{ComponentName}', // e.g., 'Atoms/Button'
|
||||||
|
component: ComponentName,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered', // 'centered' for atoms, 'padded' or 'fullscreen' for larger
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
// One entry per prop with control type, options, description
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ComponentName>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage checklist (every component MUST have)
|
||||||
|
|
||||||
|
- [ ] **Default** — typical usage with standard content
|
||||||
|
- [ ] **AllVariants** — all visual variants side by side (if applicable)
|
||||||
|
- [ ] **AllSizes** — all size options side by side (if applicable)
|
||||||
|
- [ ] **Disabled** — disabled state
|
||||||
|
- [ ] **Loading** — loading state (if applicable)
|
||||||
|
- [ ] **Error** — error state (if applicable)
|
||||||
|
- [ ] **LongContent** — text overflow / long content handling
|
||||||
|
- [ ] **MinimalContent** — empty or minimal content
|
||||||
|
- [ ] **WithOptionalElements** — with/without icons, badges, etc.
|
||||||
|
|
||||||
|
### Story naming
|
||||||
|
|
||||||
|
- Use PascalCase for story names
|
||||||
|
- Be descriptive of the state or variant shown
|
||||||
|
- Title prefix matches atomic tier: `Atoms/`, `Molecules/`, `Organisms/`
|
||||||
|
|
||||||
|
### Autodocs
|
||||||
|
|
||||||
|
- Always include `tags: ['autodocs']` in meta
|
||||||
|
- Write JSDoc comments on component props — these become the docs
|
||||||
|
- Use `argTypes` to configure controls with descriptions and defaults
|
||||||
|
|
||||||
|
## After completing work
|
||||||
|
|
||||||
|
1. Update `docs/memory/session-log.md` noting which stories were written/updated
|
||||||
57
.claude/agents/token-architect.md
Normal file
57
.claude/agents/token-architect.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Token Architect
|
||||||
|
|
||||||
|
You are the token-architect agent for the FA Design System. Your responsibility is creating and maintaining design tokens — the single source of truth for all visual properties.
|
||||||
|
|
||||||
|
## Before starting
|
||||||
|
|
||||||
|
1. Read `docs/memory/session-log.md` — understand what's been done
|
||||||
|
2. Read `docs/memory/decisions-log.md` — don't contradict previous decisions
|
||||||
|
3. Read `docs/memory/token-registry.md` — know what tokens already exist
|
||||||
|
4. Read `docs/conventions/token-conventions.md` — follow all naming rules
|
||||||
|
5. Read `docs/design-system.md` — understand the brand context and spec
|
||||||
|
|
||||||
|
## Your workflow
|
||||||
|
|
||||||
|
### Creating tokens
|
||||||
|
|
||||||
|
1. **Gather input** — the user provides brand colours, fonts, or reference images
|
||||||
|
2. **Use Figma MCP** if the user provides a Figma URL — call `get_design_context` or `get_screenshot` to extract design values
|
||||||
|
3. **Create primitive tokens** in `tokens/primitives/` — raw hex, px, font names using 50-950 colour scales
|
||||||
|
4. **Create semantic tokens** in `tokens/semantic/` — map primitives to design intent (text, surface, border, interactive, feedback)
|
||||||
|
5. **Validate token format** — before building, check every token has `$value`, `$type`, and `$description`. Missing `$description` is the most common mistake.
|
||||||
|
6. **Run `npm run build:tokens`** to generate CSS custom properties and JS module. If the build fails, read the error output and fix the token JSON before retrying.
|
||||||
|
7. **Update the MUI theme** in `src/theme/index.ts` to consume the generated token values. Common mappings:
|
||||||
|
- `color.brand.primary` → `palette.primary.main`
|
||||||
|
- `color.text.primary` → `palette.text.primary`
|
||||||
|
- `color.surface.default` → `palette.background.default`
|
||||||
|
- `color.feedback.*` → `palette.error.main`, `palette.warning.main`, etc.
|
||||||
|
- `fontFamily.heading` / `fontFamily.body` → `typography.fontFamily`
|
||||||
|
- Import values from `./generated/tokens.js`
|
||||||
|
8. **Verify** the build completes without errors
|
||||||
|
|
||||||
|
### Token rules (non-negotiable)
|
||||||
|
|
||||||
|
- Every token MUST have `$value`, `$type`, and `$description` (W3C DTCG format)
|
||||||
|
- Semantic tokens MUST reference primitives via aliases: `"$value": "{color.blue.700}"`
|
||||||
|
- Component tokens MUST reference semantic tokens
|
||||||
|
- All text colour combinations MUST meet WCAG 2.1 AA contrast (4.5:1 normal, 3:1 large)
|
||||||
|
- Use the `--fa-` CSS custom property prefix
|
||||||
|
|
||||||
|
### File structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tokens/primitives/colours.json — brand, neutral, feedback hue scales
|
||||||
|
tokens/primitives/typography.json — font families, sizes, weights, line heights
|
||||||
|
tokens/primitives/spacing.json — spacing scale, border radius
|
||||||
|
tokens/primitives/effects.json — shadows, opacity
|
||||||
|
tokens/semantic/colours.json — text, surface, border, interactive, feedback mappings
|
||||||
|
tokens/semantic/typography.json — typography role mappings (display, h1, body, etc.)
|
||||||
|
tokens/semantic/spacing.json — layout and component spacing
|
||||||
|
tokens/component/*.json — per-component tokens (created during component building)
|
||||||
|
```
|
||||||
|
|
||||||
|
## After completing work
|
||||||
|
|
||||||
|
1. Update `docs/memory/token-registry.md` with every token you created/modified
|
||||||
|
2. Update `docs/memory/decisions-log.md` with any design decisions and rationale
|
||||||
|
3. Update `docs/memory/session-log.md` with work summary and next steps
|
||||||
22
.claude/skills/build-atom/SKILL.md
Normal file
22
.claude/skills/build-atom/SKILL.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: build-atom
|
||||||
|
description: Build an atom component (Button, Input, Typography, etc.)
|
||||||
|
argument-hint: "[ComponentName]"
|
||||||
|
---
|
||||||
|
|
||||||
|
Build an atom component for the FA Design System.
|
||||||
|
|
||||||
|
Use the component-builder agent to handle this task. The user wants to build the following atom component:
|
||||||
|
|
||||||
|
**Component:** $ARGUMENTS
|
||||||
|
|
||||||
|
**Instructions for the agent:**
|
||||||
|
1. Read all memory files and conventions before starting
|
||||||
|
2. Check `docs/memory/component-registry.md` to confirm the component is planned
|
||||||
|
3. Create the component in `src/components/atoms/{ComponentName}/`
|
||||||
|
4. Include: `{ComponentName}.tsx`, `{ComponentName}.stories.tsx`, `index.ts`
|
||||||
|
5. Follow all rules in `docs/conventions/component-conventions.md`
|
||||||
|
6. ALL visual values MUST come from the MUI theme — never hardcode
|
||||||
|
7. Write comprehensive Storybook stories covering all states
|
||||||
|
8. Verify the component renders in Storybook
|
||||||
|
9. Update all memory files when done
|
||||||
23
.claude/skills/build-molecule/SKILL.md
Normal file
23
.claude/skills/build-molecule/SKILL.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: build-molecule
|
||||||
|
description: Build a molecule component (PriceCard, FormField, etc.)
|
||||||
|
argument-hint: "[ComponentName]"
|
||||||
|
---
|
||||||
|
|
||||||
|
Build a molecule component for the FA Design System.
|
||||||
|
|
||||||
|
Use the component-builder agent to handle this task. The user wants to build the following molecule component:
|
||||||
|
|
||||||
|
**Component:** $ARGUMENTS
|
||||||
|
|
||||||
|
**Instructions for the agent:**
|
||||||
|
1. Read all memory files and conventions before starting
|
||||||
|
2. Check `docs/memory/component-registry.md` to confirm the component is planned and that its constituent atoms are `done`
|
||||||
|
3. Create the component in `src/components/molecules/{ComponentName}/`
|
||||||
|
4. Include: `{ComponentName}.tsx`, `{ComponentName}.stories.tsx`, `index.ts`
|
||||||
|
5. Compose from existing atom components — import from `@atoms/`
|
||||||
|
6. Follow all rules in `docs/conventions/component-conventions.md`
|
||||||
|
7. ALL visual values MUST come from the MUI theme — never hardcode
|
||||||
|
8. Write comprehensive Storybook stories with realistic content
|
||||||
|
9. Verify the component renders in Storybook
|
||||||
|
10. Update all memory files when done
|
||||||
24
.claude/skills/build-organism/SKILL.md
Normal file
24
.claude/skills/build-organism/SKILL.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: build-organism
|
||||||
|
description: Build an organism component (Navigation, PricingTable, etc.)
|
||||||
|
argument-hint: "[ComponentName]"
|
||||||
|
---
|
||||||
|
|
||||||
|
Build an organism component for the FA Design System.
|
||||||
|
|
||||||
|
Use the component-builder agent to handle this task. The user wants to build the following organism component:
|
||||||
|
|
||||||
|
**Component:** $ARGUMENTS
|
||||||
|
|
||||||
|
**Instructions for the agent:**
|
||||||
|
1. Read all memory files and conventions before starting
|
||||||
|
2. Check `docs/memory/component-registry.md` — confirm the organism is planned
|
||||||
|
3. Verify all constituent molecules and atoms are marked `done` in the registry — if any are not, STOP and tell the user which dependencies need to be built first
|
||||||
|
4. Create the component in `src/components/organisms/{ComponentName}/`
|
||||||
|
5. Include: `{ComponentName}.tsx`, `{ComponentName}.stories.tsx`, `index.ts`
|
||||||
|
6. Compose from existing molecule and atom components — import from `@molecules/` and `@atoms/`
|
||||||
|
7. Follow all rules in `docs/conventions/component-conventions.md`
|
||||||
|
8. ALL visual values MUST come from the MUI theme — never hardcode
|
||||||
|
9. Write comprehensive Storybook stories with realistic page-level content
|
||||||
|
10. Verify the component renders in Storybook
|
||||||
|
11. Update all memory files when done
|
||||||
20
.claude/skills/create-tokens/SKILL.md
Normal file
20
.claude/skills/create-tokens/SKILL.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: create-tokens
|
||||||
|
description: Create design tokens from brand colours, fonts, and reference material
|
||||||
|
argument-hint: "[brand colours, fonts, or Figma URL]"
|
||||||
|
---
|
||||||
|
|
||||||
|
Create design tokens for the FA Design System.
|
||||||
|
|
||||||
|
Use the token-architect agent to handle this task. The user's input follows — it may include brand colours, font choices, reference images, or Figma URLs.
|
||||||
|
|
||||||
|
**Instructions for the agent:**
|
||||||
|
1. Read all memory files and conventions before starting
|
||||||
|
2. If the user provides a Figma URL, use the Figma MCP to extract design context
|
||||||
|
3. Create primitive tokens (colour scales, typography, spacing, effects)
|
||||||
|
4. Create semantic tokens (map primitives to design intent)
|
||||||
|
5. Run `npm run build:tokens` to generate outputs
|
||||||
|
6. Update the MUI theme in `src/theme/index.ts` to use generated values
|
||||||
|
7. Update all memory files when done
|
||||||
|
|
||||||
|
User input: $ARGUMENTS
|
||||||
49
.claude/skills/review-component/SKILL.md
Normal file
49
.claude/skills/review-component/SKILL.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: review-component
|
||||||
|
description: Review a component against design system conventions
|
||||||
|
argument-hint: "[ComponentName]"
|
||||||
|
---
|
||||||
|
|
||||||
|
Review a component against FA Design System conventions and report pass/fail for each check.
|
||||||
|
|
||||||
|
**Component to review:** $ARGUMENTS
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
1. Read `docs/conventions/component-conventions.md` for the rules
|
||||||
|
2. Read `docs/conventions/token-conventions.md` for token usage rules
|
||||||
|
3. Read the component source file in `src/components/`
|
||||||
|
4. Read the component's Storybook stories
|
||||||
|
|
||||||
|
**Check each of these and report pass/fail:**
|
||||||
|
|
||||||
|
### Code quality
|
||||||
|
- [ ] Component uses TypeScript with proper types
|
||||||
|
- [ ] Props interface exported with JSDoc on every prop
|
||||||
|
- [ ] Uses `React.forwardRef` for interactive elements
|
||||||
|
- [ ] Accepts and forwards `sx` prop
|
||||||
|
- [ ] Uses `shouldForwardProp` for custom props on styled components
|
||||||
|
|
||||||
|
### Theme compliance
|
||||||
|
- [ ] NO hardcoded colours — all from `theme.palette.*`
|
||||||
|
- [ ] NO hardcoded spacing — all from `theme.spacing()`
|
||||||
|
- [ ] NO hardcoded typography — all from `theme.typography.*`
|
||||||
|
- [ ] NO hardcoded shadows — all from `theme.shadows`
|
||||||
|
- [ ] NO hardcoded border radius — all from `theme.shape.*`
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
- [ ] Minimum 44px touch target on mobile
|
||||||
|
- [ ] Visible focus indicator (focus-visible)
|
||||||
|
- [ ] Appropriate ARIA attributes
|
||||||
|
- [ ] Disabled state uses `aria-disabled`
|
||||||
|
- [ ] Colour contrast meets WCAG 2.1 AA
|
||||||
|
|
||||||
|
### Storybook coverage
|
||||||
|
- [ ] Default story
|
||||||
|
- [ ] All variants story
|
||||||
|
- [ ] All sizes story (if applicable)
|
||||||
|
- [ ] Disabled state
|
||||||
|
- [ ] Loading state (if applicable)
|
||||||
|
- [ ] Long content / overflow
|
||||||
|
- [ ] autodocs tag present
|
||||||
|
|
||||||
|
**Report format:** List each check with pass/fail and specific issues found. End with a summary and recommended fixes.
|
||||||
35
.claude/skills/status/SKILL.md
Normal file
35
.claude/skills/status/SKILL.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: status
|
||||||
|
description: Report current status of tokens, components, and build health
|
||||||
|
---
|
||||||
|
|
||||||
|
Report the current status of the FA Design System.
|
||||||
|
|
||||||
|
**Instructions:**
|
||||||
|
1. Read `docs/memory/session-log.md` — summarise recent work
|
||||||
|
2. Read `docs/memory/component-registry.md` — count components by status (planned, in-progress, review, done)
|
||||||
|
3. Read `docs/memory/token-registry.md` — summarise token coverage
|
||||||
|
4. Read `docs/memory/decisions-log.md` — count decisions logged
|
||||||
|
5. Check if Storybook is running (curl http://localhost:6006)
|
||||||
|
6. Check if tokens build successfully (`npm run build:tokens`)
|
||||||
|
|
||||||
|
**Report format:**
|
||||||
|
```
|
||||||
|
## FA Design System Status
|
||||||
|
|
||||||
|
### Tokens
|
||||||
|
- Primitives: [count] defined
|
||||||
|
- Semantic: [count] defined
|
||||||
|
- Component: [count] defined
|
||||||
|
|
||||||
|
### Components
|
||||||
|
- Done: [list]
|
||||||
|
- In progress: [list]
|
||||||
|
- Planned: [list]
|
||||||
|
|
||||||
|
### Recent activity
|
||||||
|
- [last session summary]
|
||||||
|
|
||||||
|
### Next steps
|
||||||
|
- [recommended next actions]
|
||||||
|
```
|
||||||
22
.claude/skills/sync-tokens/SKILL.md
Normal file
22
.claude/skills/sync-tokens/SKILL.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: sync-tokens
|
||||||
|
description: Rebuild CSS and JS outputs from token JSON sources
|
||||||
|
---
|
||||||
|
|
||||||
|
Synchronise design tokens — rebuild CSS and JS outputs from token JSON sources.
|
||||||
|
|
||||||
|
Use this after token JSON files have been edited manually or after `/create-tokens`. This is a maintenance command — it does NOT create new tokens (use `/create-tokens` for that).
|
||||||
|
|
||||||
|
Use the token-architect agent to handle this task.
|
||||||
|
|
||||||
|
**Instructions for the agent:**
|
||||||
|
1. Read `docs/memory/token-registry.md` to understand current token state
|
||||||
|
2. Validate all token JSON files have required fields (`$value`, `$type`, `$description`)
|
||||||
|
3. Run `npm run build:tokens` to regenerate:
|
||||||
|
- `src/theme/generated/tokens.css` (CSS custom properties)
|
||||||
|
- `src/theme/generated/tokens.js` (JS ES6 module)
|
||||||
|
- `tokens/export/tokens-flat.json` (flat JSON export)
|
||||||
|
4. Check that `src/theme/index.ts` is consuming the generated tokens correctly
|
||||||
|
5. If any tokens were added/changed since the theme was last updated, update `src/theme/index.ts`
|
||||||
|
6. Report what was generated and any issues found
|
||||||
|
7. Update `docs/memory/token-registry.md` if it's out of date
|
||||||
29
.claude/skills/write-stories/SKILL.md
Normal file
29
.claude/skills/write-stories/SKILL.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: write-stories
|
||||||
|
description: Write or update Storybook stories for a component
|
||||||
|
argument-hint: "[ComponentName]"
|
||||||
|
---
|
||||||
|
|
||||||
|
Write or update Storybook stories for an existing component.
|
||||||
|
|
||||||
|
Use the story-writer agent to handle this task. The component to document:
|
||||||
|
|
||||||
|
**Component:** $ARGUMENTS
|
||||||
|
|
||||||
|
**Instructions for the agent:**
|
||||||
|
1. Read `docs/conventions/component-conventions.md` for story standards
|
||||||
|
2. Read the component source file at `src/components/` (check atoms/, molecules/, organisms/)
|
||||||
|
3. Create or update `{ComponentName}.stories.tsx` in the component's folder
|
||||||
|
4. Cover ALL items in the story coverage checklist:
|
||||||
|
- [ ] 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.)
|
||||||
|
5. Every story meta MUST include `tags: ['autodocs']`
|
||||||
|
6. Verify the component renders correctly in Storybook at http://localhost:6006
|
||||||
|
7. Update `docs/memory/session-log.md` when done
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
storybook-static/
|
||||||
|
tokens/export/
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.DS_Store
|
||||||
|
*.tgz
|
||||||
8
.mcp.json
Normal file
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"figma-remote-mcp": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://mcp.figma.com/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
.storybook/main.ts
Normal file
22
.storybook/main.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-designs',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
viteFinal: async (config) => {
|
||||||
|
// Inherit aliases from vite.config.ts automatically via react-vite framework
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
6
.storybook/preview-head.html
Normal file
6
.storybook/preview-head.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
27
.storybook/preview.tsx
Normal file
27
.storybook/preview.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import { theme } from '../src/theme';
|
||||||
|
import '../src/theme/generated/tokens.css';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Story />
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
80
CLAUDE.md
Normal file
80
CLAUDE.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# FA 2.0 Design System
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
|
||||||
|
Rebuilding the Funeral Arranger (funeralarranger.com.au) design system using a
|
||||||
|
code-first approach. Parsons (H.Parsons Funeral Directors) is the client. FA is
|
||||||
|
an Australian funeral planning platform — the design language must be warm,
|
||||||
|
professional, trustworthy, and calm. Users are often in distress.
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
- React 18 + TypeScript
|
||||||
|
- Material UI (MUI) v5
|
||||||
|
- Storybook 8+ with autodocs
|
||||||
|
- Style Dictionary for token transformation
|
||||||
|
- W3C DTCG token format (2025.10 stable spec)
|
||||||
|
- Chromatic for Storybook hosting (later)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Source of truth:** Token JSON files in `tokens/` (DTCG format)
|
||||||
|
**Flow:** Token JSON → Style Dictionary → MUI theme + CSS vars → React components → Storybook
|
||||||
|
|
||||||
|
### Token tiers
|
||||||
|
1. **Primitives** (`tokens/primitives/`): Raw values — hex, px, font names, scales
|
||||||
|
2. **Semantic** (`tokens/semantic/`): Design intent — `color.text.primary`, `color.surface.default`
|
||||||
|
3. **Component** (`tokens/component/`): Per-component — `button.background.default`
|
||||||
|
|
||||||
|
### Component tiers (atomic design)
|
||||||
|
1. **Atoms** (`src/components/atoms/`): Button, Input, Typography, Badge, Icon, Avatar, Divider, Chip, Card, Link
|
||||||
|
2. **Molecules** (`src/components/molecules/`): SearchBar, PriceCard, ServiceOption, FormField
|
||||||
|
3. **Organisms** (`src/components/organisms/`): ServiceSelector, PricingTable, ArrangementForm, Navigation
|
||||||
|
|
||||||
|
## Critical rules
|
||||||
|
|
||||||
|
1. **Every component MUST consume the MUI theme** — never hardcode colours, spacing, typography, or shadows
|
||||||
|
2. **Every token MUST have a `$description`** — this is how agents maintain context about design intent
|
||||||
|
3. **Always read docs/design-system.md** before creating or modifying anything
|
||||||
|
4. **Always check docs/memory/** before starting work — these files contain decisions and state from previous sessions
|
||||||
|
5. **Always update docs/memory/** after completing work — log what was done, decisions made, and open questions
|
||||||
|
6. **Run `npm run build:tokens`** after any token JSON change
|
||||||
|
7. **Verify in Storybook** before marking any component done
|
||||||
|
|
||||||
|
## Memory system
|
||||||
|
|
||||||
|
This project uses structured markdown files for cross-session memory.
|
||||||
|
|
||||||
|
**Before starting any work, read these files:**
|
||||||
|
- `docs/memory/decisions-log.md` — All design decisions with rationale
|
||||||
|
- `docs/memory/component-registry.md` — Status of every component (planned/in-progress/done)
|
||||||
|
- `docs/memory/token-registry.md` — All tokens with their current values and usage notes
|
||||||
|
- `docs/memory/session-log.md` — What was done in previous sessions, what's next
|
||||||
|
|
||||||
|
**After completing work, update:**
|
||||||
|
- The relevant memory files with what changed
|
||||||
|
- `docs/memory/session-log.md` with a summary of what was accomplished and next steps
|
||||||
|
|
||||||
|
## MCP servers
|
||||||
|
|
||||||
|
- **Figma remote MCP** (`figma-remote-mcp`): Read FA 1.0 designs, extract design context
|
||||||
|
- **Storybook MCP** (`storybook`): Query component library for available components and props
|
||||||
|
|
||||||
|
Setup instructions in `docs/reference/mcp-setup.md`.
|
||||||
|
|
||||||
|
## File conventions
|
||||||
|
|
||||||
|
- Component folders: PascalCase (`Button/`, `PriceCard/`)
|
||||||
|
- Token files: camelCase (`colours.json`, `typography.json`)
|
||||||
|
- Each component folder contains: `ComponentName.tsx`, `ComponentName.stories.tsx`, `index.ts`
|
||||||
|
- CSS custom properties prefix: `--fa-` (e.g., `--fa-color-brand-primary`)
|
||||||
|
- MUI theme paths: follow MUI conventions (`palette.primary.main`)
|
||||||
|
|
||||||
|
## Naming conventions for tokens
|
||||||
|
|
||||||
|
See `docs/conventions/token-conventions.md` for the full specification.
|
||||||
|
|
||||||
|
Quick reference:
|
||||||
|
- Primitives: `color.blue.500`, `spacing.4`, `fontSize.base`
|
||||||
|
- Semantic: `color.text.primary`, `color.surface.default`, `color.interactive.default`
|
||||||
|
- Component: `button.background.default`, `button.background.hover`
|
||||||
106
QUICKSTART.md
Normal file
106
QUICKSTART.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# FA Design System — Quick Start
|
||||||
|
|
||||||
|
## What you've got
|
||||||
|
|
||||||
|
A complete Claude Code project scaffold with:
|
||||||
|
|
||||||
|
- **3 agents**: token-architect, component-builder, story-writer
|
||||||
|
- **6 slash commands**: /create-tokens, /build-atom, /build-molecule, /sync-tokens, /status, /review-component
|
||||||
|
- **4 memory files**: session-log, decisions-log, component-registry, token-registry
|
||||||
|
- **2 convention docs**: token-conventions, component-conventions
|
||||||
|
- **Living design system spec**: docs/design-system.md
|
||||||
|
- **MCP config**: Figma remote MCP pre-configured
|
||||||
|
- **Full React + MUI + Storybook + Style Dictionary setup**
|
||||||
|
|
||||||
|
## Setup (5-10 minutes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Extract the project
|
||||||
|
tar -xzf fa-design-system-scaffold.tar.gz
|
||||||
|
cd fa-project
|
||||||
|
|
||||||
|
# 2. Run the bootstrap script
|
||||||
|
chmod +x bootstrap.sh
|
||||||
|
./bootstrap.sh
|
||||||
|
|
||||||
|
# 3. Set up Figma MCP (in Claude Code)
|
||||||
|
claude mcp add --transport http figma-remote-mcp https://mcp.figma.com/mcp
|
||||||
|
# Restart Claude Code, then /mcp → authenticate with Figma
|
||||||
|
|
||||||
|
# 4. Start Storybook (separate terminal)
|
||||||
|
npm run storybook
|
||||||
|
```
|
||||||
|
|
||||||
|
## Your workflow
|
||||||
|
|
||||||
|
### Step 1: Create tokens
|
||||||
|
```
|
||||||
|
claude
|
||||||
|
/create-tokens I want to create the FA design system. Here are my brand colours: [paste hex values or attach reference images]. Fonts: [your font choices]. The platform serves Australian families planning funerals — warm, trustworthy, calm aesthetic.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Build atoms (one at a time)
|
||||||
|
```
|
||||||
|
/build-atom Button
|
||||||
|
# Review in Storybook at http://localhost:6006
|
||||||
|
# Provide feedback, iterate
|
||||||
|
|
||||||
|
/build-atom Input
|
||||||
|
/build-atom Typography
|
||||||
|
/build-atom Card
|
||||||
|
# ... etc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Build molecules
|
||||||
|
```
|
||||||
|
/build-molecule PriceCard
|
||||||
|
/build-molecule FormField
|
||||||
|
# ... etc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check status anytime
|
||||||
|
```
|
||||||
|
/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review a component
|
||||||
|
```
|
||||||
|
/review-component Button
|
||||||
|
```
|
||||||
|
|
||||||
|
## How memory works
|
||||||
|
|
||||||
|
Every agent reads these files before starting work and updates them after:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `docs/memory/session-log.md` | What happened last session, what's next |
|
||||||
|
| `docs/memory/decisions-log.md` | Every design decision with rationale |
|
||||||
|
| `docs/memory/component-registry.md` | Status of every component |
|
||||||
|
| `docs/memory/token-registry.md` | All tokens with values and usage |
|
||||||
|
|
||||||
|
This means you can close Claude Code, come back tomorrow, and the agents
|
||||||
|
will pick up exactly where you left off.
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
```
|
||||||
|
fa-project/
|
||||||
|
├── CLAUDE.md ← Claude Code reads this every session
|
||||||
|
├── .claude/
|
||||||
|
│ ├── agents/ ← Agent definitions
|
||||||
|
│ ├── commands/ ← Slash commands
|
||||||
|
│ └── skills/ ← Shared knowledge
|
||||||
|
├── .mcp.json ← MCP server config
|
||||||
|
├── docs/
|
||||||
|
│ ├── memory/ ← Cross-session memory
|
||||||
|
│ ├── conventions/ ← Rules agents follow
|
||||||
|
│ ├── reference/ ← Setup guides
|
||||||
|
│ └── design-system.md ← Living design spec
|
||||||
|
├── tokens/ ← DTCG token JSON (source of truth)
|
||||||
|
├── style-dictionary/ ← Token build config
|
||||||
|
├── src/
|
||||||
|
│ ├── theme/ ← MUI theme + generated CSS vars
|
||||||
|
│ └── components/ ← Atoms, molecules, organisms
|
||||||
|
└── .storybook/ ← Storybook config with theme provider
|
||||||
|
```
|
||||||
138
bootstrap.sh
Executable file
138
bootstrap.sh
Executable file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# FA Design System — Bootstrap Script
|
||||||
|
# Run this once after cloning/copying the project scaffold to set everything up.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "╔══════════════════════════════════════════════╗"
|
||||||
|
echo "║ FA Design System — Project Bootstrap ║"
|
||||||
|
echo "╚══════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ─── 1. Install dependencies ─────────────────────────────────────────────────
|
||||||
|
echo "📦 Installing dependencies..."
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# ─── 2. Initialise Storybook (if not already configured) ─────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "📖 Checking Storybook setup..."
|
||||||
|
if [ ! -d ".storybook" ]; then
|
||||||
|
echo " .storybook config not found. Creating configuration..."
|
||||||
|
mkdir -p .storybook
|
||||||
|
cat > .storybook/main.ts << 'MAINEOF'
|
||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
viteFinal: async (config) => {
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
MAINEOF
|
||||||
|
cat > .storybook/preview.tsx << 'PREVEOF'
|
||||||
|
import React from 'react';
|
||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import { theme } from '../src/theme';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Story />
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
|
PREVEOF
|
||||||
|
echo " ✓ .storybook config created."
|
||||||
|
else
|
||||||
|
echo " .storybook config found. Skipping."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 3. Build initial token output ───────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "🎨 Building placeholder token output..."
|
||||||
|
mkdir -p src/theme/generated
|
||||||
|
mkdir -p tokens/export
|
||||||
|
# Style Dictionary needs at least one source file to build
|
||||||
|
if [ ! -f "tokens/primitives/colours.json" ]; then
|
||||||
|
echo '{}' > tokens/primitives/colours.json
|
||||||
|
echo '{}' > tokens/primitives/typography.json
|
||||||
|
echo '{}' > tokens/primitives/spacing.json
|
||||||
|
echo '{}' > tokens/primitives/effects.json
|
||||||
|
echo '{}' > tokens/semantic/colours.json
|
||||||
|
echo '{}' > tokens/semantic/typography.json
|
||||||
|
echo '{}' > tokens/semantic/spacing.json
|
||||||
|
echo " Created empty token placeholder files."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 4. TypeScript check ─────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Running TypeScript check..."
|
||||||
|
npx tsc --noEmit 2>/dev/null && echo " ✓ TypeScript OK" || echo " ⚠ TypeScript errors (expected before tokens are created)"
|
||||||
|
|
||||||
|
# ─── 5. Git init ─────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
if [ ! -d ".git" ]; then
|
||||||
|
echo "🔧 Initialising git repository..."
|
||||||
|
git init
|
||||||
|
git add -A
|
||||||
|
git commit -m "Initial scaffold: FA Design System project structure"
|
||||||
|
echo " ✓ Git initialised with initial commit"
|
||||||
|
else
|
||||||
|
echo "🔧 Git already initialised."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 6. Summary ──────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "╔══════════════════════════════════════════════╗"
|
||||||
|
echo "║ ✓ Bootstrap complete! ║"
|
||||||
|
echo "╚══════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo ""
|
||||||
|
echo " 1. SET UP FIGMA MCP:"
|
||||||
|
echo " claude mcp add --transport http figma-remote-mcp https://mcp.figma.com/mcp"
|
||||||
|
echo " Then restart Claude Code and authenticate via /mcp"
|
||||||
|
echo ""
|
||||||
|
echo " 2. START CLAUDE CODE:"
|
||||||
|
echo " cd $(pwd)"
|
||||||
|
echo " claude"
|
||||||
|
echo ""
|
||||||
|
echo " 3. CHECK STATUS:"
|
||||||
|
echo " /status"
|
||||||
|
echo ""
|
||||||
|
echo " 4. CREATE YOUR TOKENS:"
|
||||||
|
echo " /create-tokens [provide brand colours, fonts, and context]"
|
||||||
|
echo ""
|
||||||
|
echo " 5. START STORYBOOK (in a separate terminal):"
|
||||||
|
echo " npm run storybook"
|
||||||
|
echo ""
|
||||||
|
echo " 6. BUILD YOUR FIRST ATOM:"
|
||||||
|
echo " /build-atom Button"
|
||||||
|
echo ""
|
||||||
241
docs/conventions/component-conventions.md
Normal file
241
docs/conventions/component-conventions.md
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# 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
|
||||||
|
- Use `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*`
|
||||||
|
- 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.)
|
||||||
144
docs/conventions/token-conventions.md
Normal file
144
docs/conventions/token-conventions.md
Normal 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
174
docs/design-system.md
Normal 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)
|
||||||
62
docs/memory/component-registry.md
Normal file
62
docs/memory/component-registry.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Component registry
|
||||||
|
|
||||||
|
Tracks the status, specification, and key details of every component in the
|
||||||
|
design system. Agents MUST check this before building a component (to avoid
|
||||||
|
duplicates) and MUST update it after completing one.
|
||||||
|
|
||||||
|
## Status definitions
|
||||||
|
|
||||||
|
- **planned**: Component is identified but not yet started
|
||||||
|
- **in-progress**: Component is being built
|
||||||
|
- **review**: Component is built, awaiting human review
|
||||||
|
- **done**: Component is reviewed and approved
|
||||||
|
- **needs-revision**: Component needs changes based on review feedback
|
||||||
|
|
||||||
|
## Atoms
|
||||||
|
|
||||||
|
| Component | Status | Variants | Tokens used | Notes |
|
||||||
|
|-----------|--------|----------|-------------|-------|
|
||||||
|
| Button | review | contained, soft, outlined, text × xs, small, medium, large × primary, secondary + loading, underline, fullWidth | button.height/paddingX/paddingY/fontSize/iconSize/iconGap/borderRadius, color.interactive.*, color.brand.100-300, color.neutral.200-700 | Primary interactive element. Merges Text Button from Figma. Soft variant = Figma's Secondary/Brand & Secondary/Grey. |
|
||||||
|
| IconButton | planned | contained, soft, outlined, text × small, medium, large | Reuses Button tokens | Icon-only button (close, menu, actions). Wrap MUI IconButton with FA theme. Build when Navigation/modals are needed. |
|
||||||
|
| Typography | review | displayHero, display1-3, displaySm, h1-h6, bodyLg, body1, body2, bodyXs, labelLg, label, labelSm, caption, captionSm, overline, overlineSm + maxLines, gutterBottom | typography.* (all semantic typography tokens), fontFamily.body, fontFamily.display | Text display system. Thin MUI wrapper with maxLines truncation. |
|
||||||
|
| Input | review | medium, small × default, hover, focus, error, success, disabled + startIcon, endIcon, required, multiline | input.height/paddingX/paddingY/fontSize/borderRadius/gap/iconSize, color.neutral.300-400, color.brand.500, color.feedback.error/success, color.text.secondary | External label pattern, branded focus ring, two sizes aligned with Button. Adds startIcon/endIcon and success state beyond Figma. |
|
||||||
|
| Badge | planned | default, success, warning, error | | Status indicators |
|
||||||
|
| Icon | planned | various sizes | | Icon wrapper component |
|
||||||
|
| Avatar | planned | image, initials, icon × small, medium, large | | User/entity representation |
|
||||||
|
| Divider | planned | horizontal, vertical | | Visual separator |
|
||||||
|
| Chip | planned | filled, outlined × deletable, clickable | | Tags and filters |
|
||||||
|
| Card | planned | elevated, outlined | | Content container |
|
||||||
|
| Link | planned | default, subtle | | Navigation text |
|
||||||
|
|
||||||
|
## Molecules
|
||||||
|
|
||||||
|
| Component | Status | Composed of | Notes |
|
||||||
|
|-----------|--------|-------------|-------|
|
||||||
|
| FormField | planned | Input + Typography (label) + Typography (helper) | Standard form field with label and validation |
|
||||||
|
| PriceCard | planned | Card + Typography + Badge + Button | Service pricing display |
|
||||||
|
| ServiceOption | planned | Card + Typography + Chip + Icon | Selectable service item |
|
||||||
|
| SearchBar | planned | Input + Icon + Button | Search with submit |
|
||||||
|
| StepIndicator | planned | Typography + Badge + Divider | Multi-step flow progress |
|
||||||
|
|
||||||
|
## Organisms
|
||||||
|
|
||||||
|
| Component | Status | Composed of | Notes |
|
||||||
|
|-----------|--------|-------------|-------|
|
||||||
|
| ServiceSelector | planned | ServiceOption × n + Typography + Button | Full service selection panel |
|
||||||
|
| PricingTable | planned | PriceCard × n + Typography | Comparative pricing display |
|
||||||
|
| ArrangementForm | planned | FormField × n + StepIndicator + Button | Multi-step arrangement flow |
|
||||||
|
| Navigation | planned | Link × n + Button + Avatar | Site header navigation |
|
||||||
|
| Footer | planned | Link × n + Typography + Divider | Site footer |
|
||||||
|
|
||||||
|
## Future enhancements
|
||||||
|
|
||||||
|
Deferred items that should be addressed when the relevant components or patterns
|
||||||
|
are needed. Check this section before building new components — an item here may
|
||||||
|
be relevant to your current work.
|
||||||
|
|
||||||
|
| Item | Relates to | Trigger | Notes |
|
||||||
|
|------|-----------|---------|-------|
|
||||||
|
| Destructive button colours | Button | When building delete/cancel flows | `color="error"` already works via MUI palette. May need `soft` variant styling for error/warning/success colours. |
|
||||||
|
| Link-as-button | Button | When building Navigation or link-heavy pages | Use MUI's `component="a"` or `href` prop. May warrant a separate Link atom or a `Button` story showing the pattern. |
|
||||||
|
| IconButton atom | IconButton | When building Navigation, modals, toolbars | Wrap MUI IconButton with FA theme tokens. Registered as planned atom above. |
|
||||||
|
| ~~Google Fonts loading~~ | ~~Typography~~ | ~~Resolved~~ | ~~Added to .storybook/preview-head.html and index.html~~ |
|
||||||
221
docs/memory/decisions-log.md
Normal file
221
docs/memory/decisions-log.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# Design decisions log
|
||||||
|
|
||||||
|
Every design decision that affects tokens, components, or system architecture
|
||||||
|
MUST be logged here with rationale. This ensures consistency across sessions
|
||||||
|
and agents. Agents MUST check this file before making any decision that could
|
||||||
|
contradict a previous one.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
```
|
||||||
|
### [Decision ID] — [Short title]
|
||||||
|
**Date:** [date]
|
||||||
|
**Category:** token | component | architecture | convention
|
||||||
|
**Decision:** [What was decided]
|
||||||
|
**Rationale:** [Why this decision was made]
|
||||||
|
**Affects:** [Which tokens/components/files this impacts]
|
||||||
|
**Alternatives considered:** [What else was considered and why it was rejected]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D001 — Primary brand colour is warm gold #BA834E
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Primary brand colour (color.brand.500) is #BA834E — warm gold.
|
||||||
|
**Rationale:** Extracted from Parsons brand swatches. Matches the existing FA 1.0 CTA button colour ("Add to package"). Warm gold conveys trust and professionalism appropriate for funeral services. The colour has a 3.7:1 contrast ratio on white, suitable for large text and interactive elements (buttons with white text).
|
||||||
|
**Affects:** color.brand.500, color.interactive.default, MUI palette.primary.main
|
||||||
|
**Alternatives considered:** #B0610F (copper) was considered for primary but it's darker and more aggressive. Placed at brand.600 for hover/emphasis instead.
|
||||||
|
|
||||||
|
### D002 — Primary text colour is charcoal #2C2E35 (not pure black)
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Primary text colour is #2C2E35 (color.neutral.800) — charcoal with cool blue undertone.
|
||||||
|
**Rationale:** From brand swatch. Softer than pure black for extended reading comfort while maintaining 13.2:1 contrast ratio on white (well exceeds WCAG AA). The cool undertone complements the sage secondary palette.
|
||||||
|
**Affects:** color.text.primary, MUI palette.text.primary
|
||||||
|
**Alternatives considered:** Pure black (#000000) — higher contrast but causes eye fatigue on white backgrounds. Added as color.black for rare use.
|
||||||
|
|
||||||
|
### D003 — Brand link/accent colour uses copper #B0610F
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Brand-coloured text (links, inline emphasis) uses #B0610F (color.brand.600 / copper) not the base gold.
|
||||||
|
**Rationale:** The base gold #BA834E has only 3.7:1 contrast on white which doesn't meet WCAG AA for normal text (4.5:1 required). The copper #B0610F provides 4.8:1 contrast, meeting AA. It's also visually more assertive for link text.
|
||||||
|
**Affects:** color.text.brand, color.interactive.hover
|
||||||
|
**Alternatives considered:** brand.700 (#8B4E0D) at 6.7:1 — meets AAA but is too dark and loses the warm "brand" feel.
|
||||||
|
|
||||||
|
### D004 — Sage palette as secondary colour family
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Secondary palette uses cool sage/slate tones (#4C5B6B as sage.700) rather than a second warm colour.
|
||||||
|
**Rationale:** From brand swatches (cool row). The sage provides visual contrast against the warm brand palette and adds a calming, professional quality appropriate for the funeral services context. Used for secondary buttons and less prominent actions.
|
||||||
|
**Affects:** color.sage.*, color.interactive.secondary, MUI palette.secondary
|
||||||
|
**Alternatives considered:** Using a lighter warm tone for secondary — rejected as it wouldn't provide enough visual distinction from primary.
|
||||||
|
|
||||||
|
### D005 — Display font is Noto Serif SC for H1-H2 headings
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Display/heading font is Noto Serif SC for Display, H1, and H2 levels. H3+ use Montserrat (body font).
|
||||||
|
**Rationale:** User specified Noto Serif SC as display font and Montserrat as primary. The serif at H1-H2 adds warmth and gravitas suitable for funeral services. H3+ switches to Montserrat to create a clear hierarchy break.
|
||||||
|
**Affects:** fontFamily.display, fontFamily.body, typography.display/h1/h2/h3/h4
|
||||||
|
**Alternatives considered:** Using serif for all headings — rejected as it would make H3/H4 feel too formal at smaller sizes.
|
||||||
|
|
||||||
|
### D006 — Warning text uses amber.700 for AA compliance
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Warning text colour is amber.700 (#A36B00) not amber.600 (#CC8500).
|
||||||
|
**Rationale:** amber.600 (3.6:1 contrast on white) only passes AA for large text. For warning text that must be readable at body size, amber.700 at 5.1:1 meets WCAG AA for normal text.
|
||||||
|
**Affects:** color.text.warning
|
||||||
|
**Alternatives considered:** amber.800 — passes AAA but is too dark to read as "amber/warning".
|
||||||
|
|
||||||
|
### D007 — Style Dictionary requires usesDtcg: true
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** architecture
|
||||||
|
**Decision:** Added `usesDtcg: true` to Style Dictionary v4.4.0 config.
|
||||||
|
**Rationale:** Without this flag, SD v4 doesn't properly populate `allTokens` for the CSS `css/variables` format, resulting in empty CSS output. JS and JSON formats work without it but CSS requires it for DTCG token format.
|
||||||
|
**Affects:** style-dictionary/config.js, CSS output
|
||||||
|
**Alternatives considered:** Custom format — rejected as the built-in format works correctly with the flag.
|
||||||
|
|
||||||
|
### D008 — Responsive typography via @media in MUI theme
|
||||||
|
**Date:** 2025-03-25
|
||||||
|
**Category:** architecture
|
||||||
|
**Decision:** Mobile font sizes are handled via `@media (max-width:600px)` queries in the MUI theme typography config, not via `responsiveFontSizes()`.
|
||||||
|
**Rationale:** Explicit media queries allow precise control over mobile sizes (28px, 24px, 20px, 18px) matching the token definitions. MUI's `responsiveFontSizes()` uses its own scaling algorithm that can't be configured to match our exact values.
|
||||||
|
**Affects:** src/theme/index.ts, typography.*.fontSizeMobile tokens
|
||||||
|
**Alternatives considered:** `responsiveFontSizes()` utility — rejected due to lack of precise size control.
|
||||||
|
|
||||||
|
### D009 — Merge Text Button into Button atom
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Text Button is not a separate atom. It is implemented as `<Button variant="text">` with an optional `underline` prop for link-style appearance.
|
||||||
|
**Rationale:** In MUI, text buttons are a variant of Button (`variant="text"`), not a separate component. Having both Button and TextButton creates API confusion. The Figma "ghost" variant and "text button" both map to MUI's text variant. The `underline` prop handles the design's "default.underline" state.
|
||||||
|
**Affects:** Button component API, Storybook story structure
|
||||||
|
**Alternatives considered:** Separate TextButton component — rejected as it duplicates Button logic and creates ambiguity for developers.
|
||||||
|
|
||||||
|
### D010 — Button sizes adjusted for touch targets
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Button sizes are xs: 28px, sm: 32px, md: 40px, lg: 48px — all multiples of 4px. Large size (48px) meets the 44px minimum touch target for mobile.
|
||||||
|
**Rationale:** Original Figma sizes (sm: 26px, md: 37px) don't meet the 44px minimum touch target specified in the design system conventions. Adjusted to a clean 4px-grid scale that includes a large size suitable for mobile CTAs. FA's audience may be older or in emotional distress — generous touch targets are important.
|
||||||
|
**Affects:** tokens/component/button.json, MUI theme MuiButton overrides
|
||||||
|
**Alternatives considered:** Keeping original Figma sizes — rejected because neither met the 44px touch target minimum.
|
||||||
|
|
||||||
|
### D011 — Use @mui/icons-material over Font Awesome
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** architecture
|
||||||
|
**Decision:** Primary icon source is `@mui/icons-material`. Custom SVGs wrapped in MUI's `SvgIcon` for any FA-specific icons.
|
||||||
|
**Rationale:** MUI icons extend `SvgIcon`, so they work natively with MUI Button's `startIcon`/`endIcon`, inherit theme colours, and are tree-shakeable. Font Awesome requires an adapter layer to work with MUI's icon API. All standard UI icons from the Figma designs exist in the MUI icon set.
|
||||||
|
**Affects:** Icon approach, Button icon integration, dependencies
|
||||||
|
**Alternatives considered:** `@fortawesome/react-fontawesome` — rejected due to adapter complexity and extra dependency. User was using FA icons in Figma but agreed MUI icons are more pragmatic for the code implementation.
|
||||||
|
|
||||||
|
### D012 — Button responsive sizing is a composition concern
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** architecture
|
||||||
|
**Decision:** Button size is an explicit prop (`size="xs" | "small" | "medium" | "large"`), not responsive by default. Layout components choose the appropriate size per breakpoint.
|
||||||
|
**Rationale:** Keeps the Button component simple and predictable. Responsive behaviour is a layout decision — a card might use `size="large"` on mobile for touch targets and `size="medium"` on desktop. This avoids the complexity of Figma variable modes for mobile/desktop scaling inside components.
|
||||||
|
**Affects:** Button component API, consumer usage patterns
|
||||||
|
**Alternatives considered:** Figma variable modes for responsive scaling within the component — rejected as over-engineered. MUI's `useMediaQuery` or responsive `sx` props handle this at the composition level.
|
||||||
|
|
||||||
|
### D013 — Soft variant for tonal/muted buttons
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Added `variant="soft"` custom MUI Button variant for tonal/muted fill buttons. Maps to Figma's "Secondary/Brand" and "Secondary/Grey" columns.
|
||||||
|
**Rationale:** Figma review revealed that "Secondary/Brand" (bg #e4cdb3, text #845830) and "Secondary/Grey" (bg #dcdde0, text #434b52) are NOT outlined buttons — they are filled buttons with softer, tonal colours. This pattern is common in modern design systems (Material Design 3 calls it "tonal", Radix calls it "soft"). Primary soft uses brand.200 bg + brand.700 text. Secondary soft uses neutral.200 bg + neutral.700 text.
|
||||||
|
**Affects:** MUI theme MuiButton variants, Button component API, Storybook stories
|
||||||
|
**Alternatives considered:** Separate color options for each Figma column — rejected as it doesn't map to MUI's color+variant model. The soft variant is more extensible — any future color automatically gets a soft treatment.
|
||||||
|
|
||||||
|
### D014 — Secondary palette changed from sage to neutral grey
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** MUI secondary palette changed from sage (#4C5B6B sage.700) to neutral grey (#525252 neutral.600). Sage remains available in the token system for surfaces but is no longer the secondary button colour.
|
||||||
|
**Rationale:** User feedback that secondary buttons should match Figma's "Secondary/Grey" which uses neutral tones, not the blue-green sage. The sage palette is still valuable for cool surfaces (color.surface.cool) but shouldn't be the primary secondary interactive colour.
|
||||||
|
**Affects:** MUI palette.secondary, all MuiButton secondary overrides (outlined, text, soft)
|
||||||
|
**Alternatives considered:** Adding a third "neutral" custom colour and keeping sage as secondary — rejected as unnecessarily complex when no other components use the sage secondary yet.
|
||||||
|
|
||||||
|
### D015 — Loading spinner positioned on the right
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Button loading spinner (CircularProgress) appears after the label text, on the right side of the button.
|
||||||
|
**Rationale:** User preference. Right-side positioning follows the convention of trailing indicators (like endIcon) and feels more natural for async state feedback.
|
||||||
|
**Affects:** Button component render order
|
||||||
|
**Alternatives considered:** Left-side spinner (before text) — rejected per user feedback.
|
||||||
|
|
||||||
|
### D016 — Primary button colour confirmed as copper (brand.600)
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Primary button fill (palette.primary.main) stays as brand.600 copper (#B0610F), not brand.500 warm gold (#BA834E). User confirmed.
|
||||||
|
**Rationale:** Brand.600 provides 4.8:1 contrast ratio with white text, meeting WCAG AA for all text sizes. Brand.500 only achieves 3.7:1 (AA for large text only). Since button labels can vary in size, the stronger contrast is preferred. The visual warmth is preserved through the soft variant (brand.200 bg) and hover states.
|
||||||
|
**Affects:** palette.primary.main, all contained primary buttons, interactive token
|
||||||
|
**Alternatives considered:** Brand.500 warm gold — rejected as it doesn't meet AA for normal-sized text.
|
||||||
|
|
||||||
|
### D017 — Headings use Montserrat Bold, not Noto Serif SC
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** All headings (h1-h6) use Montserrat Bold (700), not Noto Serif SC. Noto Serif SC is reserved exclusively for display variants (displayHero through displaySm).
|
||||||
|
**Rationale:** User's FA 2.0 Figma design separates display (serif, for marketing/hero) from headings (sans-serif, for content structure). This is cleaner than the previous approach where h1-h2 used serif. Montserrat Bold provides strong hierarchy without the formality of serif at content-level sizes.
|
||||||
|
**Affects:** MUI theme h1-h6 fontFamily, semantic typography tokens. Supersedes D005.
|
||||||
|
**Alternatives considered:** Keep D005 (serif for h1-h2) — rejected per user's updated Figma design.
|
||||||
|
|
||||||
|
### D018 — Display weight is Regular (400), not Bold
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Display text (Noto Serif SC) uses Regular weight (400) across all 5 display levels, not Bold (700).
|
||||||
|
**Rationale:** Serif fonts carry inherent visual weight. At 40-80px, Regular Noto Serif SC has enormous presence. Bold at these sizes would be overwhelming and less elegant.
|
||||||
|
**Affects:** typography.displayHero/1/2/3/Sm fontWeight tokens
|
||||||
|
**Alternatives considered:** Bold (700) — the previous implementation. Rejected per user's Figma which uses Regular.
|
||||||
|
|
||||||
|
### D019 — Body text weight is Medium (500)
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Body text uses Montserrat Medium (500) instead of Regular (400). User found Regular too light for comfortable reading.
|
||||||
|
**Rationale:** User feedback that Montserrat Regular is "just a little bit too light." The Figma specified weight 450 which isn't available in Google Fonts, so 500 (Medium) is the closest available weight that provides the desired visual density.
|
||||||
|
**Affects:** typography.body/bodyLg/bodySm/bodyXs fontWeight tokens
|
||||||
|
**Alternatives considered:** 400 Regular — too light per user. 450 — not available in Google Fonts Montserrat.
|
||||||
|
|
||||||
|
### D020 — Expanded type scale: 21 variants across 6 categories
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** token
|
||||||
|
**Decision:** Typography system expanded from ~12 to 21 variants organized in 6 categories: Display (5 levels), Heading (6 levels), Body (4 sizes), Label (3 sizes), Caption (2 sizes), Overline (2 sizes). Caption/sm and overline/sm floored at 11px for accessibility.
|
||||||
|
**Affects:** primitive typography tokens, semantic typography tokens, MUI theme, Typography component
|
||||||
|
**Alternatives considered:** Keeping the simpler scale — rejected as user explicitly wanted more variety matching their FA 2.0 Figma type system.
|
||||||
|
|
||||||
|
### D021 — Input uses external label pattern, not MUI floating label
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Input label sits above the input field as a separate element, not as MUI's floating/outlined label that sits on the border. Uses MUI's `InputLabel` with `position: static` and `shrink: true`.
|
||||||
|
**Rationale:** The Figma design shows the label as a distinct element above the input with a 6px gap. The floating label pattern is harder to read and interact with, especially for FA's audience who may be elderly or in distress. External labels have better usability research support for form comprehension.
|
||||||
|
**Affects:** Input component structure, MUI InputLabel styling, no notch in OutlinedInput
|
||||||
|
**Alternatives considered:** MUI's default floating label (outlined variant) — rejected as it doesn't match the Figma design and has usability concerns for the target audience.
|
||||||
|
|
||||||
|
### D022 — Input sizes: medium (48px) and small (40px)
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Input has two sizes: medium (48px, default) and small (40px). Medium aligns with Button large (48px) for search bar pairing. Small aligns with Button medium (40px) for compact layouts.
|
||||||
|
**Rationale:** Figma designed inputs at 64px which is too tall relative to other components and creates misalignment with buttons. 48px meets the 44px touch target minimum and aligns with our Button large. Both sizes sit on the 4px grid.
|
||||||
|
**Affects:** tokens/component/input.json, MUI theme MuiOutlinedInput size overrides
|
||||||
|
**Alternatives considered:** Single size at 64px (Figma original) — rejected as it dwarfs adjacent buttons. Three sizes (sm/md/lg) — rejected as premature; two sizes cover all current form patterns.
|
||||||
|
|
||||||
|
### D023 — Input focus ring uses brand.500 warm gold, not brand.600 copper
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Input focus ring uses brand.500 (#BA834E warm gold) via a double box-shadow (2px white + 4px brand), not the brand.600 copper used for Button focus outlines.
|
||||||
|
**Rationale:** The Figma FA 2.0 design explicitly specifies brand.500 for the input focus ring as an `elevation-special/focus-ring` effect token. The warm gold is softer and more appropriate for form inputs where the focus indicator should be visible but not aggressive. Buttons use brand.600 outline because they need to stand out against coloured backgrounds.
|
||||||
|
**Affects:** MUI theme MuiOutlinedInput focus styles
|
||||||
|
**Alternatives considered:** Using brand.600 (same as Button) — rejected as the Figma explicitly uses brand.500 for inputs.
|
||||||
|
|
||||||
|
### D024 — Input label stays neutral on error/success states
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Input label colour remains `text.secondary` regardless of error/success state. Only the border and helper text change colour.
|
||||||
|
**Rationale:** Per the Figma design. A full-red label would feel alarming, which is inappropriate for FA's sensitive context. The subtle approach (border + helper text colour change) communicates the issue without distress. Users who are already in an emotional state don't need aggressive visual feedback.
|
||||||
|
**Affects:** InputLabel Mui-error and Mui-focused style overrides
|
||||||
|
**Alternatives considered:** Colouring the label red on error — rejected as too aggressive for the audience.
|
||||||
|
|
||||||
|
### D025 — Added startIcon/endIcon convenience props and success state
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Category:** component
|
||||||
|
**Decision:** Input component adds three features beyond the Figma design: (1) `startIcon` prop for leading icons, (2) `endIcon` prop for trailing icons (convenience wrapper around MUI adornments), (3) `success` boolean prop for validation success state (green border + green helper text). Error-state icons (ErrorOutline) and success-state icons (CheckCircleOutline) are recommended patterns in stories.
|
||||||
|
**Rationale:** Leading icons (email, phone, dollar) are essential for FA's arrangement forms. Success state provides positive feedback after validation. The Figma only had trailing icon, but leading icons are a near-universal production need. The `startIcon`/`endIcon` props are simpler than MUI's `InputAdornment` pattern while remaining compatible with raw adornments via `startAdornment`/`endAdornment`.
|
||||||
|
**Affects:** Input component API, InputAdornment usage, Storybook stories
|
||||||
|
**Alternatives considered:** Only supporting MUI's raw adornment API — rejected as too verbose for the common case. The convenience props are ergonomic while the raw props remain available for complex cases (e.g., password toggle with IconButton).
|
||||||
290
docs/memory/session-log.md
Normal file
290
docs/memory/session-log.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# Session log
|
||||||
|
|
||||||
|
This file tracks work completed across Claude Code sessions. Every agent MUST
|
||||||
|
read this file before starting work and update it after completing work.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
Each entry follows this structure:
|
||||||
|
```
|
||||||
|
### Session [date] — [brief description]
|
||||||
|
**Agent(s):** [which agents were active]
|
||||||
|
**Work completed:**
|
||||||
|
- [bullet points of what was done]
|
||||||
|
**Decisions made:**
|
||||||
|
- [any design/architecture decisions with brief rationale]
|
||||||
|
**Open questions:**
|
||||||
|
- [anything unresolved that needs human input]
|
||||||
|
**Next steps:**
|
||||||
|
- [what should happen next]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
### Session 2026-03-24 — Project scaffold setup and hygiene
|
||||||
|
|
||||||
|
**Agent(s):** Manual (no agents invoked — setup phase)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- Fixed missing `.storybook/` directory (main.ts + preview.tsx with MUI ThemeProvider)
|
||||||
|
- Created `.gitignore` (was missing entirely)
|
||||||
|
- Created `.mcp.json` with Figma remote MCP config
|
||||||
|
- Removed malformed `{.claude/agents,.claude/...}` directory tree (bash brace expansion bug)
|
||||||
|
- Created 3 agent definitions: token-architect, component-builder, story-writer
|
||||||
|
- Created 8 slash commands: create-tokens, build-atom, build-molecule, build-organism, write-stories, sync-tokens, status, review-component
|
||||||
|
- Migrated commands from `.claude/commands/` to `.claude/skills/<name>/SKILL.md` format with YAML frontmatter
|
||||||
|
- Fixed **critical** Style Dictionary v4 config: converted from CommonJS to ESM (`StyleDictionary` class API)
|
||||||
|
- Changed `build:tokens` script from `style-dictionary build --config ...` to `node style-dictionary/config.js`
|
||||||
|
- Added `"type": "module"` to package.json for ESM support
|
||||||
|
- Changed SD output from `tokens.ts` to `tokens.js` (Style Dictionary v4 generates JS, not TS)
|
||||||
|
- Registered `@storybook/addon-designs` in `.storybook/main.ts` (was installed but not configured)
|
||||||
|
- Added CSS token import to `.storybook/preview.tsx`
|
||||||
|
- Added Card and Link to CLAUDE.md atom list (were missing)
|
||||||
|
- Fixed broken Penpot/Storybook MCP references in `docs/reference/mcp-setup.md`
|
||||||
|
- Improved agent instructions with pre-flight dependency checks, story checklist enforcement, token validation steps, and MUI theme mapping guide
|
||||||
|
- Created `src/components/{atoms,molecules,organisms}/` directories
|
||||||
|
- Fixed `bootstrap.sh` to check for `.storybook/` dir instead of `node_modules/@storybook`
|
||||||
|
- Verified `npm run build:tokens` runs successfully (empty output expected — no tokens yet)
|
||||||
|
- Verified Storybook runs at http://localhost:6006
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- Skills use `.claude/skills/<name>/SKILL.md` format (not `.claude/commands/`)
|
||||||
|
- Style Dictionary v4 config is a standalone ESM script run via `node`, not CLI
|
||||||
|
- British spelling for filenames (`colours.json`), standard `color` namespace for token paths
|
||||||
|
|
||||||
|
**Open questions:**
|
||||||
|
- Slash commands `/create-tokens` etc. showing "Unknown skill" — may need Claude Code restart to discover newly created skills. User should test after restarting.
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- ~~Verify slash commands work after Claude Code restart~~ ✓ Skills working
|
||||||
|
- ~~Begin Step 1: `/create-tokens` with brand colours, fonts, and reference material~~ ✓ Done
|
||||||
|
|
||||||
|
### Session 2025-03-25 — Token creation (Step 1)
|
||||||
|
|
||||||
|
**Agent(s):** token-architect (via /create-tokens skill)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- Extracted brand colours from user-provided swatch image (12 colours across 3 palettes)
|
||||||
|
- Reviewed 5 Parsons FA 1.0 Figma screens for colour application context
|
||||||
|
- Reviewed 2 FA 2.0 Design System Template screens (buttons + inputs)
|
||||||
|
- Created complete primitive tokens:
|
||||||
|
- Colour scales (50-950): brand (warm gold), sage (cool grey-green), neutral, red, amber, green, blue + white/black
|
||||||
|
- Typography: 3 font families (Montserrat, Noto Serif SC, JetBrains Mono), font size scale (xs-4xl + mobile), weights, line heights, letter spacing
|
||||||
|
- Spacing: 4px-based scale (2px-80px), border radius scale
|
||||||
|
- Effects: 4 shadow levels, 3 opacity values
|
||||||
|
- Created complete semantic tokens:
|
||||||
|
- Colour mappings: text (9 variants), surface (6), border (6), interactive (7), feedback (8)
|
||||||
|
- Typography roles: display, h1-h4, bodyLarge, body, bodySmall, caption, label, overline — each with font family, size, weight, line height, letter spacing + mobile size overrides
|
||||||
|
- Spacing: component (xs-lg), layout (gutter, section, page, padding) with mobile/desktop variants
|
||||||
|
- Fixed Style Dictionary v4.4.0 CSS output: added `usesDtcg: true` flag (required for DTCG format CSS generation)
|
||||||
|
- Generated outputs:
|
||||||
|
- CSS: 245 custom properties in `src/theme/generated/tokens.css` with `var()` references for semantic tokens
|
||||||
|
- JS: 258 named exports in `src/theme/generated/tokens.js`
|
||||||
|
- JSON: flat export in `tokens/export/tokens-flat.json`
|
||||||
|
- Updated MUI theme (`src/theme/index.ts`):
|
||||||
|
- All palette colours mapped from token imports
|
||||||
|
- All typography variants mapped (h1-h6, body1, body2, subtitle1/2, caption, overline, button)
|
||||||
|
- 3 custom typography variants added: display, bodyLarge, label (with TS module declarations)
|
||||||
|
- Responsive font sizes via @media queries for display, h1, h2, h3
|
||||||
|
- Component overrides for Button and Card border radius
|
||||||
|
- Updated docs/design-system.md with actual brand colours, palettes, and font stacks
|
||||||
|
- Updated docs/memory/decisions-log.md with 8 design decisions (D001-D008)
|
||||||
|
- Updated docs/memory/token-registry.md with complete token reference
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- D001: Primary brand colour is #BA834E (warm gold from swatch)
|
||||||
|
- D002: Primary text colour is #2C2E35 (charcoal, not pure black)
|
||||||
|
- D003: Brand link colour uses #B0610F (copper, meets WCAG AA)
|
||||||
|
- D004: Sage palette as secondary (calming contrast to warm primary)
|
||||||
|
- D005: Noto Serif SC for display/H1-H2, Montserrat for H3+ and body
|
||||||
|
- D006: Warning text uses amber.700 for AA compliance
|
||||||
|
- D007: Style Dictionary requires usesDtcg: true for CSS output
|
||||||
|
- D008: Responsive typography via explicit @media queries in MUI theme
|
||||||
|
|
||||||
|
**Open questions:**
|
||||||
|
- Font name confirmation: user wrote "Not Serif SC" — interpreted as "Noto Serif SC" (Google Font). Needs confirmation.
|
||||||
|
- brand.500 (#BA834E) has 3.7:1 contrast on white — AA for large text only. Primary buttons use white text on this background, which works for button-sized text. Should we darken the primary CTA colour for better accessibility, or is the current value acceptable?
|
||||||
|
- Token collision warning: benign collision on `$type: "color"` set in both primitives and semantic colour files. Doesn't affect output.
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- ~~Confirm display font name (Noto Serif SC vs something else)~~
|
||||||
|
- ~~Begin Step 2: Build first atom component (Button recommended as it exercises the most tokens)~~ ✓ Done
|
||||||
|
- Consider adding Google Fonts import for Montserrat and Noto Serif SC to index.html or Storybook preview
|
||||||
|
|
||||||
|
### Session 2026-03-25 — Button atom (Step 2)
|
||||||
|
|
||||||
|
**Agent(s):** Claude Opus (via conversation)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- Reviewed 3 Figma component sets: Icons (29:54), Button (28:50), Text Button (32:1361)
|
||||||
|
- Created button component tokens (`tokens/component/button.json`): height, paddingX, paddingY, fontSize, iconSize, iconGap, borderRadius for xs/sm/md/lg sizes
|
||||||
|
- Rebuilt token pipeline — 25 new button tokens generated across CSS/JS/JSON outputs
|
||||||
|
- Updated MUI theme (`src/theme/index.ts`) with comprehensive MuiButton overrides:
|
||||||
|
- 4 sizes: xs (28px), small (32px), medium (40px), large (48px)
|
||||||
|
- Contained, outlined, text variants for primary and secondary colours
|
||||||
|
- Hover backgrounds using brand.100 and sage.100 for outlined/text variants
|
||||||
|
- Focus-visible outline (2px solid interactive.focus, 2px offset)
|
||||||
|
- Disabled elevation via `disableElevation: true` default prop
|
||||||
|
- Custom `xs` size via MUI variant system + module augmentation
|
||||||
|
- Created Button component (`src/components/atoms/Button/Button.tsx`):
|
||||||
|
- Wraps MUI Button with React.forwardRef
|
||||||
|
- Custom props: `loading` (spinner + disabled), `underline` (text-decoration for text variant)
|
||||||
|
- Accepts all MUI ButtonProps (variant, color, size, startIcon, endIcon, fullWidth, etc.)
|
||||||
|
- Created barrel export (`src/components/atoms/Button/index.ts`)
|
||||||
|
- Created comprehensive Storybook stories (`Button.stories.tsx`): 16 stories covering Default, Variants (primary/secondary), AllSizes, Icons, Disabled, Loading, TextWithUnderline, FullWidth, LongContent, CompleteMatrix
|
||||||
|
- Storybook builds successfully with Button stories
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- D009: Merge Text Button into Button atom — text buttons are `variant="text"` with optional `underline` prop, not a separate component
|
||||||
|
- D010: Button sizes adjusted for touch targets — xs: 28px, sm: 32px, md: 40px, lg: 48px (lg meets 44px minimum)
|
||||||
|
- D011: Use `@mui/icons-material` over Font Awesome — native MUI integration, no adapter needed, tree-shakeable
|
||||||
|
- D012: Responsive sizing is a composition concern — Button size is an explicit prop, not responsive by default. Layouts choose the right size per breakpoint.
|
||||||
|
|
||||||
|
**Open questions:**
|
||||||
|
- primary.main maps to brand.600 (copper #B0610F) for AA compliance, but Figma designs show brand.500 (warm gold #BA834E) as primary button fill. Should we adjust? Current mapping prioritises accessibility.
|
||||||
|
- Google Fonts import still needed for Montserrat and Noto Serif SC in Storybook
|
||||||
|
- Icon approach confirmed as @mui/icons-material — should we create any custom SvgIcon components for FA-specific icons?
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- ~~User to review Button in Storybook and provide feedback on colours/sizing~~ ✓ Feedback received
|
||||||
|
- Build Typography atom
|
||||||
|
- Add Google Fonts to Storybook preview
|
||||||
|
- ~~Log D009-D012 in decisions-log.md~~ ✓ Done
|
||||||
|
|
||||||
|
### Session 2026-03-25b — Button refinements from user review
|
||||||
|
|
||||||
|
**Agent(s):** Claude Opus (via conversation)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- Fetched Figma Secondary/Brand and Secondary/Grey button details — discovered they are **soft-filled** (tonal) buttons, not outlined
|
||||||
|
- Added custom `variant="soft"` to MUI Button — tonal/muted fill treatment
|
||||||
|
- Primary soft: brand.200 bg (#EBDAC8), brand.700 text (#8B4E0D), brand.300 hover
|
||||||
|
- Secondary soft: neutral.200 bg (#E8E8E8), neutral.700 text (#404040), neutral.300 hover
|
||||||
|
- Changed MUI secondary palette from sage to neutral grey (neutral.600 main, neutral.700 dark, neutral.300 light)
|
||||||
|
- Updated all secondary button overrides (outlined, text) from sage to neutral
|
||||||
|
- Moved loading spinner from left to right side of button text
|
||||||
|
- Updated stories: added FigmaMapping story, soft variant stories, LoadingToSuccess pattern story
|
||||||
|
- Logged D013-D015 in decisions log
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- D013: Added `soft` variant for tonal buttons — maps to Figma Secondary/Brand and Secondary/Grey
|
||||||
|
- D014: Secondary palette changed from sage to neutral grey
|
||||||
|
- D015: Loading spinner positioned on right
|
||||||
|
|
||||||
|
**Open questions:**
|
||||||
|
- primary.main maps to brand.600 (copper #B0610F) for AA compliance, but Figma designs show brand.500 (warm gold #BA834E) as primary button fill. Should we adjust?
|
||||||
|
- Google Fonts import still needed for Montserrat and Noto Serif SC in Storybook
|
||||||
|
- Icon approach confirmed as @mui/icons-material — should we create any custom SvgIcon components for FA-specific icons?
|
||||||
|
- Sage palette no longer used for buttons — consider if it should be re-introduced as a custom colour later
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- ~~User to re-review Button in Storybook~~ ✓ Approved as good baseline
|
||||||
|
- ~~Build Typography atom~~ ✓ Done
|
||||||
|
- ~~Add Google Fonts to Storybook preview~~ ✓ Done
|
||||||
|
|
||||||
|
### Session 2026-03-25c — Typography atom + housekeeping
|
||||||
|
|
||||||
|
**Agent(s):** Claude Opus (via conversation)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- Added Google Fonts loading: Montserrat (400/500/600/700) + Noto Serif SC (600/700) in `.storybook/preview-head.html` and `index.html`
|
||||||
|
- Created Typography component (`src/components/atoms/Typography/Typography.tsx`):
|
||||||
|
- Thin wrapper around MUI Typography with React.forwardRef
|
||||||
|
- Custom prop: `maxLines` for CSS line-clamp truncation
|
||||||
|
- All MUI Typography props passed through (variant, color, align, component, gutterBottom, etc.)
|
||||||
|
- Created barrel export (`src/components/atoms/Typography/index.ts`)
|
||||||
|
- Created comprehensive Storybook stories (9 stories): Default, HeadingHierarchy, BodyVariants, UIText, Colours, SemanticHTML, MaxLines, RealisticContent, FontFamilies
|
||||||
|
- Added "Future enhancements" tracking table to component-registry.md — deferred items (IconButton, destructive colours, link-as-button) tracked with triggers for when to address them
|
||||||
|
- Logged D016 (primary colour confirmed as copper brand.600) in decisions-log.md
|
||||||
|
- Storybook builds successfully
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- D016: Primary button colour confirmed as copper (brand.600) — user approved
|
||||||
|
- Typography atom is a thin wrapper + maxLines — no over-engineering
|
||||||
|
|
||||||
|
**Open questions:**
|
||||||
|
- Icon approach confirmed as @mui/icons-material — should we create any custom SvgIcon components for FA-specific icons?
|
||||||
|
- Sage palette no longer used for buttons — consider if it should be re-introduced as a custom colour later
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- User to review Typography in Storybook
|
||||||
|
- Build next atom (Input or Card recommended — Input for forms, Card for service listings)
|
||||||
|
- Consider building FormField molecule next if Input is done (label + input + helper text)
|
||||||
|
|
||||||
|
### Session 2026-03-25d — Typography expansion (21 variants)
|
||||||
|
|
||||||
|
**Agent(s):** Claude Opus (via conversation)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- Reviewed user's FA 2.0 Figma type scale (node 23:30) — 21 variants across 6 categories, 3 responsive breakpoints
|
||||||
|
- Expanded primitive typography tokens: added fontSize.2xs (11px), fontSize.display.* sub-group (hero 80px through sm 32px), mobile overrides for all display and heading sizes
|
||||||
|
- Rewrote semantic typography tokens: 21 variants with specific line-heights and letter-spacing per variant
|
||||||
|
- Rewrote MUI theme typography section: refactored to wildcard import (`import * as t`), added all 21 variants with module augmentations, responsive scaling via @media queries
|
||||||
|
- Updated Google Fonts: added Noto Serif SC weight 400 (Regular) for display text
|
||||||
|
- Updated Typography component JSDoc with full variant guide
|
||||||
|
- Rewrote Typography stories: 10 stories including CompleteScale (all 21 variants matching Figma layout)
|
||||||
|
- Key font changes: headings now Montserrat Bold (not serif), display now Regular 400 (not Bold), body now Medium 500 (not Regular)
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- D017: Headings use Montserrat Bold, not Noto Serif SC (supersedes D005)
|
||||||
|
- D018: Display weight is Regular 400, not Bold
|
||||||
|
- D019: Body weight is Medium 500 (user preference, 450 not available)
|
||||||
|
- D020: 21 typography variants across 6 categories
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- ~~User to review expanded type scale in Storybook~~ ✓
|
||||||
|
- Fine-tune heading sizes if they don't match Figma exactly (h2-h6 were estimated)
|
||||||
|
- ~~Build next atom (Input or Card)~~ ✓ Input done
|
||||||
|
|
||||||
|
### Session 2026-03-25e — Input atom
|
||||||
|
|
||||||
|
**Agent(s):** Claude Opus (via conversation)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- Reviewed FA 2.0 Figma Input Field design (node 39:713) — 9 states × 4 toggle properties = ~90 variants
|
||||||
|
- Analysed Figma design and provided feedback: identified 7 improvements (leading icons, size variation, required indicator, select vs input naming, multiline, character count, error icon treatment)
|
||||||
|
- Created Input component tokens (`tokens/component/input.json`): height (sm/md), paddingX, paddingY (sm/md), fontSize, borderRadius, gap, iconSize — 9 tokens total
|
||||||
|
- Rebuilt token pipeline — 9 new input tokens generated across CSS/JS/JSON outputs
|
||||||
|
- Updated MUI theme (`src/theme/index.ts`) with comprehensive MuiOutlinedInput overrides:
|
||||||
|
- Border colours per state: neutral.300 default, neutral.400 hover, brand.500 focus, error/success for validation
|
||||||
|
- Focus ring: double box-shadow (2px white gap + 4px brand.500) matching Figma's elevation-special/focus-ring effect
|
||||||
|
- Error + focus ring: error-coloured ring
|
||||||
|
- Disabled background: neutral.100
|
||||||
|
- Two sizes via minHeight: medium (48px), small (40px)
|
||||||
|
- Icon sizing: 20px via InputAdornment
|
||||||
|
- Notch legend hidden for external label pattern
|
||||||
|
- Multiline padding normalisation
|
||||||
|
- Created Input component (`src/components/atoms/Input/Input.tsx`):
|
||||||
|
- Composes FormControl + InputLabel + OutlinedInput + FormHelperText
|
||||||
|
- External label pattern (InputLabel with position:static, no floating)
|
||||||
|
- Custom props: `label`, `helperText`, `success`, `startIcon`, `endIcon`, `fullWidth`
|
||||||
|
- Label stays neutral on error/focus/success (per Figma design)
|
||||||
|
- Required asterisk on label (via MUI InputLabel)
|
||||||
|
- Success state: green border + green helper text (not a native MUI state)
|
||||||
|
- Error helper text has `role="alert"` for screen readers
|
||||||
|
- `aria-describedby` connection between input and helper text
|
||||||
|
- Supports multiline via `multiline` + `rows`/`minRows`/`maxRows`
|
||||||
|
- Created barrel export (`src/components/atoms/Input/index.ts`)
|
||||||
|
- Created comprehensive Storybook stories (11 stories): Default, FigmaMapping, AllStates, Required, Sizes, SizeAlignment, WithIcons, PasswordToggle, Multiline, ValidationFlow, ArrangementForm, CompleteMatrix
|
||||||
|
- Logged D021-D025 in decisions log
|
||||||
|
- Updated component registry (Input → review)
|
||||||
|
- Storybook builds successfully
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- D021: External label pattern, not MUI floating label
|
||||||
|
- D022: Two sizes — medium 48px (matches Button large), small 40px (matches Button medium)
|
||||||
|
- D023: Focus ring uses brand.500 warm gold (Figma spec), not brand.600
|
||||||
|
- D024: Label stays neutral on error/success (calmer for FA audience)
|
||||||
|
- D025: Added startIcon/endIcon convenience props and success validation state
|
||||||
|
|
||||||
|
**Open questions:**
|
||||||
|
- Should error+focus ring be error-coloured or brand-coloured? Currently error-coloured (reinforces the validation state during keyboard navigation)
|
||||||
|
- FormField molecule may now be unnecessary — Input already includes label + helper text. Consider repurposing FormField as a layout/validation wrapper instead.
|
||||||
|
- Should we add a character count feature? (Useful for textarea fields like special instructions)
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- User to review Input in Storybook
|
||||||
|
- Build next atom (Card, Badge, or Chip recommended)
|
||||||
|
- Consider whether FormField molecule is still needed given Input's built-in label/helperText
|
||||||
193
docs/memory/token-registry.md
Normal file
193
docs/memory/token-registry.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Token registry
|
||||||
|
|
||||||
|
Master reference of all design tokens. Updated by the token-architect agent
|
||||||
|
whenever tokens are created or modified. Other agents reference this to find
|
||||||
|
the correct token for any design property.
|
||||||
|
|
||||||
|
## How to use this file
|
||||||
|
|
||||||
|
- **token-architect**: Update this file whenever you create/modify tokens
|
||||||
|
- **component-builder**: Reference this file to find the correct token for any CSS property
|
||||||
|
- **story-writer**: Reference this file to document which tokens a component uses
|
||||||
|
|
||||||
|
## Primitives
|
||||||
|
|
||||||
|
### Colours
|
||||||
|
|
||||||
|
**Brand (warm gold/copper)** — `tokens/primitives/colours.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| color.brand.50 | #FEF9F5 | Lightest warm tint — warm section backgrounds |
|
||||||
|
| color.brand.100 | #F7ECDF | Light warm — hover backgrounds |
|
||||||
|
| color.brand.200 | #EBDAC8 | Warm light — secondary backgrounds |
|
||||||
|
| color.brand.300 | #D8C3B5 | Warm beige (swatch) — surface warmth |
|
||||||
|
| color.brand.400 | #D0A070 | Mid gold (swatch) — secondary interactive |
|
||||||
|
| color.brand.500 | #BA834E | **Base brand** (swatch) — primary CTA |
|
||||||
|
| color.brand.600 | #B0610F | Rich copper (swatch) — hover, brand links |
|
||||||
|
| color.brand.700 | #8B4E0D | Deep copper — active states |
|
||||||
|
| color.brand.800 | #6B3C13 | Dark brown — bold accents |
|
||||||
|
| color.brand.900 | #51301B | Chocolate (swatch) — deep emphasis |
|
||||||
|
| color.brand.950 | #251913 | Espresso (swatch) — darkest brand |
|
||||||
|
|
||||||
|
**Sage (cool grey-green)** — `tokens/primitives/colours.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| color.sage.50 | #F2F5F6 | Lightest sage — cool backgrounds |
|
||||||
|
| color.sage.100 | #E3EAEB | Light sage — hover states |
|
||||||
|
| color.sage.200 | #D7E1E2 | (swatch) — light cool surface |
|
||||||
|
| color.sage.300 | #C8D4D6 | Mid-light sage — dividers |
|
||||||
|
| color.sage.400 | #B9C7C9 | (swatch) — mid sage |
|
||||||
|
| color.sage.500 | #8EA2A7 | Base sage — secondary content |
|
||||||
|
| color.sage.600 | #687D84 | Dark sage — secondary text |
|
||||||
|
| color.sage.700 | #4C5B6B | (swatch) — secondary buttons |
|
||||||
|
| color.sage.800 | #4C5459 | (swatch) — supplementary text |
|
||||||
|
| color.sage.900 | #343C40 | Very dark sage |
|
||||||
|
| color.sage.950 | #1E2528 | Near-black cool |
|
||||||
|
|
||||||
|
**Neutral (true grey)** — `tokens/primitives/colours.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| color.neutral.50-950 | #FAFAFA–#0A0A0B | Full grey scale for text, borders, UI |
|
||||||
|
| color.neutral.800 | #2C2E35 | (swatch) — **Primary text colour** |
|
||||||
|
|
||||||
|
**Feedback hues** — `tokens/primitives/colours.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| color.red.50-950 | #FEF2F2–#3D0E0E | Error/destructive states |
|
||||||
|
| color.amber.50-950 | #FFF9EB–#331F00 | Warning/caution states |
|
||||||
|
| color.green.50-950 | #F0F7F0–#0F2A0F | Success/positive states |
|
||||||
|
| color.blue.50-950 | #EFF6FF–#172554 | Info/informational states |
|
||||||
|
|
||||||
|
**Standalone** — `tokens/primitives/colours.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| color.white | #FFFFFF | Pure white |
|
||||||
|
| color.black | #000000 | Pure black (use sparingly) |
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
`tokens/primitives/typography.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| fontFamily.body | Montserrat, ... | Primary body/UI font |
|
||||||
|
| fontFamily.display | Noto Serif SC, ... | Display/heading serif |
|
||||||
|
| fontFamily.mono | JetBrains Mono, ... | Monospace |
|
||||||
|
| fontSize.xs–4xl | 0.75rem–3rem | Desktop font size scale |
|
||||||
|
| fontSize.mobile.display | 1.75rem | 28px mobile display |
|
||||||
|
| fontSize.mobile.h1 | 1.5rem | 24px mobile H1 |
|
||||||
|
| fontSize.mobile.h2 | 1.25rem | 20px mobile H2 |
|
||||||
|
| fontSize.mobile.h3 | 1.125rem | 18px mobile H3 |
|
||||||
|
| fontWeight.regular–bold | 400–700 | Weight scale |
|
||||||
|
| lineHeight.tight–relaxed | 1.25–1.75 | Leading scale |
|
||||||
|
| letterSpacing.tighter–widest | -0.02em–0.08em | Tracking scale |
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
|
||||||
|
`tokens/primitives/spacing.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| spacing.0-5–20 | 2px–80px | 4px-based spacing scale |
|
||||||
|
| borderRadius.none–full | 0px–9999px | Radius scale |
|
||||||
|
|
||||||
|
### Effects
|
||||||
|
|
||||||
|
`tokens/primitives/effects.json`
|
||||||
|
| Token path | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| shadow.sm–xl | CSS box-shadow strings | Elevation shadows |
|
||||||
|
| opacity.disabled | 0.4 | Disabled elements |
|
||||||
|
| opacity.hover | 0.08 | Hover overlay tint |
|
||||||
|
| opacity.overlay | 0.5 | Modal backdrop |
|
||||||
|
|
||||||
|
## Semantic tokens
|
||||||
|
|
||||||
|
### Colour semantics
|
||||||
|
|
||||||
|
`tokens/semantic/colours.json`
|
||||||
|
| Token path | References | Description |
|
||||||
|
|-----------|-----------|-------------|
|
||||||
|
| color.text.primary | → neutral.800 | Main body/heading text |
|
||||||
|
| color.text.secondary | → neutral.600 | Helper text, descriptions |
|
||||||
|
| color.text.tertiary | → neutral.500 | Placeholders, timestamps |
|
||||||
|
| color.text.disabled | → neutral.400 | Disabled text |
|
||||||
|
| color.text.inverse | → white | Text on dark backgrounds |
|
||||||
|
| color.text.brand | → brand.600 | Links, brand emphasis |
|
||||||
|
| color.text.error | → red.600 | Error messages |
|
||||||
|
| color.text.success | → green.600 | Success messages |
|
||||||
|
| color.text.warning | → amber.700 | Warning messages |
|
||||||
|
| color.surface.default | → white | Main page background |
|
||||||
|
| color.surface.subtle | → neutral.50 | Alternate backgrounds |
|
||||||
|
| color.surface.raised | → white | Cards (with shadow) |
|
||||||
|
| color.surface.warm | → brand.50 | Warm-tinted sections |
|
||||||
|
| color.surface.cool | → sage.50 | Cool/calming sections |
|
||||||
|
| color.surface.overlay | #00000080 | Modal backdrop |
|
||||||
|
| color.border.default | → neutral.200 | Standard borders |
|
||||||
|
| color.border.strong | → neutral.400 | Emphasis borders |
|
||||||
|
| color.border.subtle | → neutral.100 | Soft dividers |
|
||||||
|
| color.border.brand | → brand.500 | Focused inputs, brand borders |
|
||||||
|
| color.border.error | → red.500 | Error field borders |
|
||||||
|
| color.border.success | → green.500 | Success field borders |
|
||||||
|
| color.interactive.default | → brand.500 | Primary button, links |
|
||||||
|
| color.interactive.hover | → brand.600 | Hover state |
|
||||||
|
| color.interactive.active | → brand.700 | Active/pressed state |
|
||||||
|
| color.interactive.disabled | → neutral.300 | Disabled interactive |
|
||||||
|
| color.interactive.focus | → brand.500 | Focus ring colour |
|
||||||
|
| color.interactive.secondary | → sage.700 | Secondary buttons |
|
||||||
|
| color.interactive.secondary-hover | → sage.800 | Secondary hover |
|
||||||
|
| color.feedback.success | → green.600 | Success indicator |
|
||||||
|
| color.feedback.success-subtle | → green.50 | Success background |
|
||||||
|
| color.feedback.warning | → amber.600 | Warning indicator |
|
||||||
|
| color.feedback.warning-subtle | → amber.50 | Warning background |
|
||||||
|
| color.feedback.error | → red.600 | Error indicator |
|
||||||
|
| color.feedback.error-subtle | → red.50 | Error background |
|
||||||
|
| color.feedback.info | → blue.600 | Info indicator |
|
||||||
|
| color.feedback.info-subtle | → blue.50 | Info background |
|
||||||
|
|
||||||
|
### Typography semantics
|
||||||
|
|
||||||
|
`tokens/semantic/typography.json`
|
||||||
|
| Token path | References | Description |
|
||||||
|
|-----------|-----------|-------------|
|
||||||
|
| typography.display.* | fontSize.3xl, fontFamily.display, bold, tight | Hero display text |
|
||||||
|
| typography.h1.* | fontSize.2xl, fontFamily.display, bold, tight | Page headings |
|
||||||
|
| typography.h2.* | fontSize.xl, fontFamily.display, semibold, snug | Section headings |
|
||||||
|
| typography.h3.* | fontSize.lg, fontFamily.body, semibold, snug | Sub-headings |
|
||||||
|
| typography.h4.* | fontSize.md, fontFamily.body, semibold, snug | Minor headings |
|
||||||
|
| typography.bodyLarge.* | fontSize.md, fontFamily.body, regular, relaxed | Lead paragraphs |
|
||||||
|
| typography.body.* | fontSize.base, fontFamily.body, regular, normal | Default body |
|
||||||
|
| typography.bodySmall.* | fontSize.sm, fontFamily.body, regular, normal | Helper text |
|
||||||
|
| typography.caption.* | fontSize.xs, fontFamily.body, regular, snug | Fine print |
|
||||||
|
| typography.label.* | fontSize.sm, fontFamily.body, medium, normal | Form labels |
|
||||||
|
| typography.overline.* | fontSize.xs, fontFamily.body, semibold, snug | Section overlines |
|
||||||
|
|
||||||
|
### Spacing semantics
|
||||||
|
|
||||||
|
`tokens/semantic/spacing.json`
|
||||||
|
| Token path | References | Description |
|
||||||
|
|-----------|-----------|-------------|
|
||||||
|
| spacing.component.xs–lg | → spacing.1–6 | Component internal spacing |
|
||||||
|
| spacing.layout.gutter | → spacing.4 | Grid gutter (mobile) |
|
||||||
|
| spacing.layout.gutter-desktop | → spacing.6 | Grid gutter (desktop) |
|
||||||
|
| spacing.layout.section | → spacing.12 | Page section gap |
|
||||||
|
| spacing.layout.page | → spacing.16 | Major section gap |
|
||||||
|
| spacing.layout.page-padding | → spacing.4 | Page horizontal padding (mobile) |
|
||||||
|
| spacing.layout.page-padding-desktop | → spacing.8 | Page horizontal padding (desktop) |
|
||||||
|
|
||||||
|
## Component tokens
|
||||||
|
|
||||||
|
`tokens/component/button.json`
|
||||||
|
|
||||||
|
### Button — Sizing
|
||||||
|
|
||||||
|
| Token path | Value / Reference | Used by | Description |
|
||||||
|
|-----------|-----------|---------|-------------|
|
||||||
|
| button.height.xs | 28px | Button | Extra-small height — compact text buttons |
|
||||||
|
| button.height.sm | 32px | Button | Small height — secondary actions |
|
||||||
|
| button.height.md | 40px | Button | Medium height — default |
|
||||||
|
| button.height.lg | 48px | Button | Large height — CTAs, mobile (meets 44px touch target) |
|
||||||
|
| button.paddingX.xs–lg | → spacing.2–6 (8–24px) | Button | Horizontal padding per size |
|
||||||
|
| button.paddingY.xs–lg | → spacing.1–3 (4–12px) | Button | Vertical padding per size |
|
||||||
|
| button.fontSize.xs–lg | → fontSize.xs–base (12–16px) | Button | Font size per size |
|
||||||
|
| button.iconSize.xs–lg | 14–20px | Button | Icon dimensions per size |
|
||||||
|
| button.iconGap.xs–lg | → spacing.1–2 (4–8px) | Button | Icon-to-text gap per size |
|
||||||
|
| button.borderRadius.default | → borderRadius.md (8px) | Button | Button corner radius |
|
||||||
127
docs/reference/mcp-setup.md
Normal file
127
docs/reference/mcp-setup.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# MCP server setup
|
||||||
|
|
||||||
|
## 1. Figma remote MCP
|
||||||
|
|
||||||
|
Used for: Reading FA 1.0 designs, extracting component context, getting design
|
||||||
|
properties for reference when building components.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add the Figma remote MCP to Claude Code
|
||||||
|
claude mcp add --transport http figma-remote-mcp https://mcp.figma.com/mcp
|
||||||
|
|
||||||
|
# Restart Claude Code, then authenticate:
|
||||||
|
# Run /mcp → select figma-remote-mcp → Authenticate
|
||||||
|
# This opens a browser for Figma OAuth
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Claude Code:
|
||||||
|
/mcp
|
||||||
|
# Should show: figma-remote-mcp (connected)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
The Figma MCP is link-based. To use it:
|
||||||
|
1. Open your FA 1.0 Figma file
|
||||||
|
2. Select a frame or component
|
||||||
|
3. Copy the link (right-click → Copy link to selection, or copy from browser URL bar)
|
||||||
|
4. Paste the link in your Claude Code prompt
|
||||||
|
|
||||||
|
Example prompts:
|
||||||
|
- "Get the design context for this button: [figma-link]"
|
||||||
|
- "What colours and spacing does this card use? [figma-link]"
|
||||||
|
- "Extract the component structure of this form: [figma-link]"
|
||||||
|
|
||||||
|
### Rate limits
|
||||||
|
|
||||||
|
With a Figma Professional plan, you get per-minute rate limits (Tier 1 REST API
|
||||||
|
equivalent). This is sufficient for component-by-component extraction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Storybook MCP
|
||||||
|
|
||||||
|
Used for: Querying the component library during composition — finding available
|
||||||
|
components, checking props, getting usage examples.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Storybook must be running locally before the MCP can connect.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start Storybook
|
||||||
|
npm run storybook
|
||||||
|
# This runs on http://localhost:6006 by default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup — Standalone Storybook MCP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add the Storybook MCP server to Claude Code
|
||||||
|
claude mcp add storybook --transport http http://localhost:6006/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add to `.mcp.json` in the project root:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"storybook": {
|
||||||
|
"url": "http://localhost:6006/mcp",
|
||||||
|
"type": "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Storybook must be running before the MCP server can connect.
|
||||||
|
|
||||||
|
### Verify connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Claude Code (with Storybook running):
|
||||||
|
/mcp
|
||||||
|
# Should show: storybook (connected)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
The Storybook MCP exposes tools for querying your component library:
|
||||||
|
- List all available components
|
||||||
|
- Get component props and their types
|
||||||
|
- Find components by keyword
|
||||||
|
- Get usage examples from stories
|
||||||
|
|
||||||
|
This is primarily used by the layout-composer agent (for later phases) to
|
||||||
|
discover and correctly use components when building page layouts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Penpot MCP (for later — layout phase)
|
||||||
|
|
||||||
|
Setup instructions will be added when you reach the layout composition phase.
|
||||||
|
The Penpot MCP requires either the official Penpot MCP plugin running locally,
|
||||||
|
or the Docker-based server alongside a self-hosted Penpot instance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Figma MCP not connecting
|
||||||
|
- Restart Claude Code completely (MCP connections initialise at startup)
|
||||||
|
- Re-authenticate: `/mcp` → select figma-remote-mcp → Authenticate
|
||||||
|
- Check you're on a Figma Professional plan (Starter has 6 calls/month)
|
||||||
|
|
||||||
|
### Storybook MCP not connecting
|
||||||
|
- Ensure Storybook is running (`npm run storybook`)
|
||||||
|
- Check the port: default is 6006, but it may use 6007+ if 6006 is occupied
|
||||||
|
- Restart Claude Code after starting Storybook
|
||||||
|
|
||||||
|
### General MCP issues
|
||||||
|
- Run `/mcp` to see status of all servers
|
||||||
|
- Check for error messages in the MCP status output
|
||||||
|
- Try removing and re-adding: `claude mcp remove <name>` then re-add
|
||||||
18
index.html
Normal file
18
index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>FA Design System</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6655
package-lock.json
generated
Normal file
6655
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "fa-design-system",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "Funeral Arranger Design System — React + MUI + Storybook",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build:storybook": "storybook build",
|
||||||
|
"build:tokens": "node style-dictionary/config.js",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"chromatic": "chromatic --exit-zero-on-changes"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.13.0",
|
||||||
|
"@emotion/styled": "^11.13.0",
|
||||||
|
"@mui/material": "^5.16.0",
|
||||||
|
"@mui/system": "^5.16.0",
|
||||||
|
"@mui/icons-material": "^5.16.0",
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@storybook/addon-designs": "^8.0.0",
|
||||||
|
"@storybook/addon-essentials": "^8.4.0",
|
||||||
|
"@storybook/blocks": "^8.4.0",
|
||||||
|
"@storybook/react": "^8.4.0",
|
||||||
|
"@storybook/react-vite": "^8.4.0",
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"chromatic": "^11.0.0",
|
||||||
|
"storybook": "^8.4.0",
|
||||||
|
"style-dictionary": "^4.2.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"vite": "^5.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
365
src/components/atoms/Button/Button.stories.tsx
Normal file
365
src/components/atoms/Button/Button.stories.tsx
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Button> = {
|
||||||
|
title: 'Atoms/Button',
|
||||||
|
component: Button,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/design/3t6fpT5inh7zzjxQdW8U5p/Design-System---Template?node-id=28-50',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['contained', 'soft', 'outlined', 'text'],
|
||||||
|
description: 'Visual style variant',
|
||||||
|
table: { defaultValue: { summary: 'contained' } },
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['primary', 'secondary'],
|
||||||
|
description: 'Colour intent',
|
||||||
|
table: { defaultValue: { summary: 'primary' } },
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['xs', 'small', 'medium', 'large'],
|
||||||
|
description: 'Size preset',
|
||||||
|
table: { defaultValue: { summary: 'medium' } },
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Show loading spinner and disable interaction',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
underline: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Underline decoration for text variant buttons',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Disable the button',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Stretch to full width of parent container',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
onClick: { action: 'clicked' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Button>;
|
||||||
|
|
||||||
|
// ─── Default ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Default button appearance — primary contained, medium size */
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Get started',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Figma Mapping ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps directly to the Figma button component columns:
|
||||||
|
* - **Primary** → `contained` + `primary` (strong copper fill)
|
||||||
|
* - **Secondary/Brand** → `soft` + `primary` (warm tonal fill)
|
||||||
|
* - **Secondary/Grey** → `soft` + `secondary` (neutral tonal fill)
|
||||||
|
* - **Ghost** → `text` + `primary` (no fill, copper text)
|
||||||
|
*/
|
||||||
|
export const FigmaMapping: Story = {
|
||||||
|
name: 'Figma Mapping',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button variant="contained">Primary</Button>
|
||||||
|
<Button variant="soft">Sec / Brand</Button>
|
||||||
|
<Button variant="soft" color="secondary">Sec / Grey</Button>
|
||||||
|
<Button variant="text">Ghost</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Variants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** All visual variants for primary (brand) colour */
|
||||||
|
export const VariantsPrimary: Story = {
|
||||||
|
name: 'Variants — Primary',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button variant="contained">Contained</Button>
|
||||||
|
<Button variant="soft">Soft</Button>
|
||||||
|
<Button variant="outlined">Outlined</Button>
|
||||||
|
<Button variant="text">Text</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** All visual variants for secondary (neutral grey) colour */
|
||||||
|
export const VariantsSecondary: Story = {
|
||||||
|
name: 'Variants — Secondary',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button variant="contained" color="secondary">Contained</Button>
|
||||||
|
<Button variant="soft" color="secondary">Soft</Button>
|
||||||
|
<Button variant="outlined" color="secondary">Outlined</Button>
|
||||||
|
<Button variant="text" color="secondary">Text</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Sizes ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** All four sizes side by side */
|
||||||
|
export const AllSizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button size="xs">Extra small</Button>
|
||||||
|
<Button size="small">Small</Button>
|
||||||
|
<Button size="medium">Medium</Button>
|
||||||
|
<Button size="large">Large</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** All sizes in soft variant */
|
||||||
|
export const AllSizesSoft: Story = {
|
||||||
|
name: 'All Sizes — Soft',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button variant="soft" size="xs">Extra small</Button>
|
||||||
|
<Button variant="soft" size="small">Small</Button>
|
||||||
|
<Button variant="soft" size="medium">Medium</Button>
|
||||||
|
<Button variant="soft" size="large">Large</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── With Icons ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Button with a leading (start) icon */
|
||||||
|
export const WithStartIcon: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Add to package',
|
||||||
|
startIcon: <AddIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Button with a trailing (end) icon */
|
||||||
|
export const WithEndIcon: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Continue',
|
||||||
|
endIcon: <ArrowForwardIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Button with both leading and trailing icons */
|
||||||
|
export const WithBothIcons: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Search',
|
||||||
|
startIcon: <SearchIcon />,
|
||||||
|
endIcon: <ArrowForwardIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Icons across all sizes */
|
||||||
|
export const IconsAllSizes: Story = {
|
||||||
|
name: 'Icons — All Sizes',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button size="xs" startIcon={<AddIcon />}>Add</Button>
|
||||||
|
<Button size="small" startIcon={<AddIcon />}>Add</Button>
|
||||||
|
<Button size="medium" startIcon={<AddIcon />}>Add</Button>
|
||||||
|
<Button size="large" startIcon={<AddIcon />}>Add</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── States ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Disabled button */
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Unavailable',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Disabled across all variants */
|
||||||
|
export const DisabledAllVariants: Story = {
|
||||||
|
name: 'Disabled — All Variants',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button disabled>Contained</Button>
|
||||||
|
<Button disabled variant="soft">Soft</Button>
|
||||||
|
<Button disabled variant="outlined">Outlined</Button>
|
||||||
|
<Button disabled variant="text">Text</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Loading state with spinner (spinner appears on the right) */
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Submitting...',
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Loading across variants */
|
||||||
|
export const LoadingAllVariants: Story = {
|
||||||
|
name: 'Loading — All Variants',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button loading>Submitting...</Button>
|
||||||
|
<Button loading variant="soft">Processing...</Button>
|
||||||
|
<Button loading variant="outlined">Processing...</Button>
|
||||||
|
<Button loading variant="text">Loading...</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Loading → Success Pattern ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the recommended loading → success flow for async actions.
|
||||||
|
*
|
||||||
|
* The Button itself stays simple — the consumer controls the state
|
||||||
|
* by toggling `loading`, `children`, and `endIcon`. Click to see the flow.
|
||||||
|
*/
|
||||||
|
export const LoadingToSuccess: Story = {
|
||||||
|
name: 'Loading → Success Pattern',
|
||||||
|
render: function LoadingSuccessDemo() {
|
||||||
|
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'loading') {
|
||||||
|
const timer = setTimeout(() => setStatus('success'), 1500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
if (status === 'success') {
|
||||||
|
const timer = setTimeout(() => setStatus('idle'), 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button
|
||||||
|
loading={status === 'loading'}
|
||||||
|
endIcon={status === 'success' ? <CheckIcon /> : undefined}
|
||||||
|
color={status === 'success' ? 'success' : 'primary'}
|
||||||
|
onClick={() => setStatus('loading')}
|
||||||
|
>
|
||||||
|
{status === 'idle' && 'Add to package'}
|
||||||
|
{status === 'loading' && 'Adding...'}
|
||||||
|
{status === 'success' && 'Added'}
|
||||||
|
</Button>
|
||||||
|
<p style={{ fontSize: 12, color: '#737373', margin: 0 }}>
|
||||||
|
Click to see: idle → loading → success → idle
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Text / Underline ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Text button with underline decoration (link-style) */
|
||||||
|
export const TextWithUnderline: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Go back',
|
||||||
|
variant: 'text',
|
||||||
|
underline: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Text buttons with and without underline */
|
||||||
|
export const TextButtonComparison: Story = {
|
||||||
|
name: 'Text Buttons — With & Without Underline',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
|
||||||
|
<Button variant="text">No underline</Button>
|
||||||
|
<Button variant="text" underline>With underline</Button>
|
||||||
|
<Button variant="text" color="secondary">Secondary</Button>
|
||||||
|
<Button variant="text" color="secondary" underline>Secondary underlined</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Text button sizes (from the merged Text Button Figma component) */
|
||||||
|
export const TextButtonSizes: Story = {
|
||||||
|
name: 'Text Buttons — All Sizes',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Button variant="text" size="xs">Extra small</Button>
|
||||||
|
<Button variant="text" size="small">Small</Button>
|
||||||
|
<Button variant="text" size="medium">Medium</Button>
|
||||||
|
<Button variant="text" size="large">Large</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Full Width ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Full width button (useful in mobile layouts and forms) */
|
||||||
|
export const FullWidth: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Complete arrangement',
|
||||||
|
fullWidth: true,
|
||||||
|
size: 'large',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ width: 360 }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Edge Cases ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Long content to test text wrapping and overflow */
|
||||||
|
export const LongContent: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Add funeral arrangement to your saved packages for comparison',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Complete Matrix ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Full variant x colour matrix for visual QA */
|
||||||
|
export const CompleteMatrix: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
{(['contained', 'soft', 'outlined', 'text'] as const).map((variant) => (
|
||||||
|
<div key={variant}>
|
||||||
|
<div style={{ marginBottom: 8, fontWeight: 600, fontSize: 14, textTransform: 'capitalize' }}>
|
||||||
|
{variant}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<Button variant={variant} color="primary">Primary</Button>
|
||||||
|
<Button variant={variant} color="secondary">Secondary</Button>
|
||||||
|
<Button variant={variant} color="primary" startIcon={<AddIcon />}>With icon</Button>
|
||||||
|
<Button variant={variant} color="primary" disabled>Disabled</Button>
|
||||||
|
<Button variant={variant} color="primary" loading>Loading...</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
79
src/components/atoms/Button/Button.tsx
Normal file
79
src/components/atoms/Button/Button.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MuiButton from '@mui/material/Button';
|
||||||
|
import type { ButtonProps as MuiButtonProps } from '@mui/material/Button';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA Button component */
|
||||||
|
export interface ButtonProps extends MuiButtonProps {
|
||||||
|
/** Show a loading spinner and disable interaction */
|
||||||
|
loading?: boolean;
|
||||||
|
/** Add underline decoration (useful for text variant link-style buttons) */
|
||||||
|
underline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary interactive element for the FA design system.
|
||||||
|
*
|
||||||
|
* Wraps MUI Button with FA brand tokens, custom sizes (xs/sm/md/lg),
|
||||||
|
* loading state, and underline support for text-variant buttons.
|
||||||
|
*
|
||||||
|
* Variant mapping from design:
|
||||||
|
* - `contained` + `primary` — Primary CTA (copper fill)
|
||||||
|
* - `soft` + `primary` — Secondary/Brand (warm tonal fill)
|
||||||
|
* - `soft` + `secondary` — Secondary/Grey (neutral tonal fill)
|
||||||
|
* - `outlined` + `primary` — Outlined brand (copper border)
|
||||||
|
* - `outlined` + `secondary` — Outlined grey (neutral border)
|
||||||
|
* - `text` + `primary` — Ghost / text button (copper text)
|
||||||
|
* - `text` + `secondary` — Ghost secondary (grey text)
|
||||||
|
*/
|
||||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
loading = false,
|
||||||
|
underline = false,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
variant = 'contained',
|
||||||
|
size = 'medium',
|
||||||
|
sx,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<MuiButton
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
disabled={loading || disabled}
|
||||||
|
sx={[
|
||||||
|
underline &&
|
||||||
|
variant === 'text' && {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
textUnderlineOffset: '3px',
|
||||||
|
'&:hover': { textDecoration: 'underline' },
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{loading && (
|
||||||
|
<CircularProgress
|
||||||
|
size={16}
|
||||||
|
color="inherit"
|
||||||
|
thickness={3}
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MuiButton>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
export default Button;
|
||||||
3
src/components/atoms/Button/index.ts
Normal file
3
src/components/atoms/Button/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default } from './Button';
|
||||||
|
export { Button } from './Button';
|
||||||
|
export type { ButtonProps } from './Button';
|
||||||
506
src/components/atoms/Input/Input.stories.tsx
Normal file
506
src/components/atoms/Input/Input.stories.tsx
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Input } from './Input';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined';
|
||||||
|
import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined';
|
||||||
|
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||||
|
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||||
|
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||||
|
import AttachMoneyIcon from '@mui/icons-material/AttachMoney';
|
||||||
|
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||||
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import { Button } from '../Button';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Input> = {
|
||||||
|
title: 'Atoms/Input',
|
||||||
|
component: Input,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/design/3t6fpT5inh7zzjxQdW8U5p/Design-System---Template?node-id=39-713',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Label text displayed above the input',
|
||||||
|
},
|
||||||
|
helperText: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Helper/description text displayed below the input',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Placeholder text',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['small', 'medium'],
|
||||||
|
description: 'Size preset',
|
||||||
|
table: { defaultValue: { summary: 'medium' } },
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Show error validation state',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Show success validation state',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Disable the input',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Mark as required (adds asterisk to label)',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Stretch to full width of parent container',
|
||||||
|
table: { defaultValue: { summary: 'true' } },
|
||||||
|
},
|
||||||
|
multiline: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Render as a textarea',
|
||||||
|
table: { defaultValue: { summary: 'false' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ width: 400 }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Input>;
|
||||||
|
|
||||||
|
// ─── Default ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Default input appearance — medium size, full width */
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Full name',
|
||||||
|
placeholder: 'Enter your full name',
|
||||||
|
helperText: 'As it appears on official documents',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Figma Mapping ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps directly to the Figma input component properties:
|
||||||
|
* - **label=true** → `label` prop
|
||||||
|
* - **description=true** → `helperText` prop
|
||||||
|
* - **trailing.icon=true** → `endIcon` prop
|
||||||
|
* - **placeholder=true** → `placeholder` prop
|
||||||
|
*/
|
||||||
|
export const FigmaMapping: Story = {
|
||||||
|
name: 'Figma Mapping',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
<Input
|
||||||
|
label="Label Header"
|
||||||
|
placeholder="Select an option"
|
||||||
|
helperText="Input Label - Description"
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Select an option"
|
||||||
|
helperText="Input Label - Description"
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Select an option"
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
/>
|
||||||
|
<Input placeholder="Select an option" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── States ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** All visual states matching the Figma design */
|
||||||
|
export const AllStates: Story = {
|
||||||
|
name: 'All States',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
<Input
|
||||||
|
label="Default"
|
||||||
|
placeholder="Enter text..."
|
||||||
|
helperText="Resting state — neutral border"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Filled"
|
||||||
|
defaultValue="John Smith"
|
||||||
|
helperText="Has a value — text colour changes from placeholder to primary"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Error (empty)"
|
||||||
|
placeholder="Enter text..."
|
||||||
|
error
|
||||||
|
helperText="This field is required"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Error (filled)"
|
||||||
|
defaultValue="invalid@"
|
||||||
|
error
|
||||||
|
helperText="Please enter a valid email address"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Success"
|
||||||
|
defaultValue="john.smith@example.com"
|
||||||
|
success
|
||||||
|
helperText="Email address verified"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Disabled (empty)"
|
||||||
|
placeholder="Enter text..."
|
||||||
|
disabled
|
||||||
|
helperText="This field is currently unavailable"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Disabled (filled)"
|
||||||
|
defaultValue="Pre-filled value"
|
||||||
|
disabled
|
||||||
|
helperText="This value cannot be changed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Required ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Required field with asterisk indicator */
|
||||||
|
export const Required: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Email address',
|
||||||
|
placeholder: 'you@example.com',
|
||||||
|
helperText: 'We will use this to send the arrangement confirmation',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Sizes ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Both sizes side by side */
|
||||||
|
export const Sizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
<Input
|
||||||
|
label="Medium (48px) — default"
|
||||||
|
placeholder="Standard form input"
|
||||||
|
size="medium"
|
||||||
|
helperText="Matches Button large height for alignment"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Small (40px) — compact"
|
||||||
|
placeholder="Compact form input"
|
||||||
|
size="small"
|
||||||
|
helperText="Matches Button medium height for dense layouts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Size comparison with Buttons (for search bar alignment) */
|
||||||
|
export const SizeAlignment: Story = {
|
||||||
|
name: 'Size Alignment with Button',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
|
||||||
|
<Input
|
||||||
|
placeholder="Search arrangements..."
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
<Button size="large" sx={{ minWidth: 100, minHeight: 48 }}>Search</Button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
|
||||||
|
<Input
|
||||||
|
placeholder="Quick search..."
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button size="medium" sx={{ minWidth: 100, minHeight: 40 }}>Search</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── With Icons ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Leading and trailing icon examples */
|
||||||
|
export const WithIcons: Story = {
|
||||||
|
name: 'With Icons',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
<Input
|
||||||
|
label="Search"
|
||||||
|
placeholder="Search services..."
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
startIcon={<EmailOutlinedIcon />}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Phone"
|
||||||
|
placeholder="+61 400 000 000"
|
||||||
|
startIcon={<PhoneOutlinedIcon />}
|
||||||
|
type="tel"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Amount"
|
||||||
|
placeholder="0.00"
|
||||||
|
startIcon={<AttachMoneyIcon />}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Email verified"
|
||||||
|
defaultValue="john@example.com"
|
||||||
|
startIcon={<EmailOutlinedIcon />}
|
||||||
|
endIcon={<CheckCircleOutlineIcon sx={{ color: 'success.main' }} />}
|
||||||
|
success
|
||||||
|
helperText="Email address confirmed"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Email invalid"
|
||||||
|
defaultValue="john@"
|
||||||
|
startIcon={<EmailOutlinedIcon />}
|
||||||
|
endIcon={<ErrorOutlineIcon sx={{ color: 'error.main' }} />}
|
||||||
|
error
|
||||||
|
helperText="Please enter a valid email address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Password ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Password field with show/hide toggle using raw endAdornment */
|
||||||
|
export const PasswordToggle: Story = {
|
||||||
|
name: 'Password Toggle',
|
||||||
|
render: function PasswordDemo() {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
type={show ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
startIcon={<LockOutlinedIcon />}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label={show ? 'Hide password' : 'Show password'}
|
||||||
|
onClick={() => setShow(!show)}
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{show ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlinedIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
helperText="Must be at least 8 characters"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Multiline ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Multiline textarea for longer text */
|
||||||
|
export const Multiline: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Special instructions',
|
||||||
|
placeholder: 'Any specific requests or notes for the arrangement...',
|
||||||
|
helperText: 'Optional — include any details that may help us prepare',
|
||||||
|
multiline: true,
|
||||||
|
minRows: 3,
|
||||||
|
maxRows: 6,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Validation Example ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Interactive validation flow */
|
||||||
|
export const ValidationFlow: Story = {
|
||||||
|
name: 'Validation Flow',
|
||||||
|
render: function ValidationDemo() {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||||||
|
const showError = value.length > 0 && !isValid;
|
||||||
|
const showSuccess = value.length > 0 && isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
label="Email address"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
startIcon={<EmailOutlinedIcon />}
|
||||||
|
endIcon={
|
||||||
|
showSuccess ? <CheckCircleOutlineIcon sx={{ color: 'success.main' }} /> :
|
||||||
|
showError ? <ErrorOutlineIcon sx={{ color: 'error.main' }} /> :
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
error={showError}
|
||||||
|
success={showSuccess}
|
||||||
|
helperText={
|
||||||
|
showError ? 'Please enter a valid email address' :
|
||||||
|
showSuccess ? 'Looks good!' :
|
||||||
|
'Required for arrangement confirmation'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Realistic Form ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Realistic arrangement form layout */
|
||||||
|
export const ArrangementForm: Story = {
|
||||||
|
name: 'Arrangement Form',
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ width: 480 }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||||
|
<div style={{ fontWeight: 700, fontSize: 20, marginBottom: 4 }}>
|
||||||
|
Contact details
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Full name"
|
||||||
|
placeholder="Enter your full name"
|
||||||
|
required
|
||||||
|
helperText="As it appears on official documents"
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: 12 }}>
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
startIcon={<EmailOutlinedIcon />}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Phone"
|
||||||
|
placeholder="+61 400 000 000"
|
||||||
|
startIcon={<PhoneOutlinedIcon />}
|
||||||
|
type="tel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Relationship to the deceased"
|
||||||
|
placeholder="e.g. Son, Daughter, Partner, Friend"
|
||||||
|
helperText="This helps us personalise the arrangement"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Special instructions"
|
||||||
|
placeholder="Any specific requests or notes..."
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
helperText="Optional"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Complete Matrix ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Full state matrix for visual QA — all states across both sizes */
|
||||||
|
export const CompleteMatrix: Story = {
|
||||||
|
name: 'Complete Matrix',
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div style={{ width: 600 }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||||||
|
{(['medium', 'small'] as const).map((size) => (
|
||||||
|
<div key={size}>
|
||||||
|
<div style={{ marginBottom: 12, fontWeight: 600, fontSize: 14, textTransform: 'uppercase', letterSpacing: 1, color: '#737373' }}>
|
||||||
|
Size: {size} ({size === 'medium' ? '48px' : '40px'})
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||||
|
<Input
|
||||||
|
size={size}
|
||||||
|
label="Default"
|
||||||
|
placeholder="Enter text..."
|
||||||
|
helperText="Helper text"
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size={size}
|
||||||
|
label="Filled"
|
||||||
|
defaultValue="Entered value"
|
||||||
|
helperText="Helper text"
|
||||||
|
endIcon={<SearchIcon />}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size={size}
|
||||||
|
label="Required"
|
||||||
|
placeholder="Required field..."
|
||||||
|
helperText="This field is required"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size={size}
|
||||||
|
label="Error"
|
||||||
|
defaultValue="Invalid input"
|
||||||
|
error
|
||||||
|
helperText="Validation error message"
|
||||||
|
endIcon={<ErrorOutlineIcon sx={{ color: 'error.main' }} />}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size={size}
|
||||||
|
label="Success"
|
||||||
|
defaultValue="Valid input"
|
||||||
|
success
|
||||||
|
helperText="Validation success message"
|
||||||
|
endIcon={<CheckCircleOutlineIcon sx={{ color: 'success.main' }} />}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size={size}
|
||||||
|
label="Disabled"
|
||||||
|
placeholder="Unavailable"
|
||||||
|
disabled
|
||||||
|
helperText="This field is disabled"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size={size}
|
||||||
|
label="Disabled filled"
|
||||||
|
defaultValue="Pre-filled"
|
||||||
|
disabled
|
||||||
|
helperText="This value is locked"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
184
src/components/atoms/Input/Input.tsx
Normal file
184
src/components/atoms/Input/Input.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||||
|
import type { OutlinedInputProps } from '@mui/material/OutlinedInput';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA Input component */
|
||||||
|
export interface InputProps extends Omit<OutlinedInputProps, 'notched' | 'label'> {
|
||||||
|
/** Label text displayed above the input */
|
||||||
|
label?: string;
|
||||||
|
/** Helper/description text displayed below the input */
|
||||||
|
helperText?: React.ReactNode;
|
||||||
|
/** Show success validation state (green border and helper text) */
|
||||||
|
success?: boolean;
|
||||||
|
/** Icon element to show at the start (left) of the input */
|
||||||
|
startIcon?: React.ReactNode;
|
||||||
|
/** Icon element to show at the end (right) of the input */
|
||||||
|
endIcon?: React.ReactNode;
|
||||||
|
/** Whether the input takes full width of its container */
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text input component for the FA design system.
|
||||||
|
*
|
||||||
|
* Wraps MUI OutlinedInput with an external label pattern, FA brand tokens,
|
||||||
|
* two sizes (small/medium), and success/error validation states.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - External label with required asterisk indicator
|
||||||
|
* - Helper text that contextually colours for error/success
|
||||||
|
* - Leading and trailing icon slots (via `startIcon`/`endIcon`)
|
||||||
|
* - Branded focus ring (warm gold double-ring from Figma)
|
||||||
|
* - Two sizes: `medium` (48px, default) and `small` (40px)
|
||||||
|
* - Multiline/textarea support via `multiline` + `rows`/`minRows`
|
||||||
|
*
|
||||||
|
* State mapping from Figma design:
|
||||||
|
* - Default → resting state, neutral border
|
||||||
|
* - Hover → darker border (CSS :hover)
|
||||||
|
* - Focus → brand.500 border + double focus ring
|
||||||
|
* - Error → `error` prop — red border + red helper text
|
||||||
|
* - Success → `success` prop — green border + green helper text
|
||||||
|
* - Disabled → `disabled` prop — grey background, muted text
|
||||||
|
*/
|
||||||
|
export const Input = React.forwardRef<HTMLDivElement, InputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
helperText,
|
||||||
|
success = false,
|
||||||
|
error = false,
|
||||||
|
required = false,
|
||||||
|
disabled = false,
|
||||||
|
fullWidth = true,
|
||||||
|
startIcon,
|
||||||
|
endIcon,
|
||||||
|
startAdornment,
|
||||||
|
endAdornment,
|
||||||
|
id,
|
||||||
|
size = 'medium',
|
||||||
|
sx,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const autoId = React.useId();
|
||||||
|
const inputId = id || autoId;
|
||||||
|
const helperId = helperText ? `${inputId}-helper` : undefined;
|
||||||
|
|
||||||
|
// Prefer convenience icon props; fall back to raw adornment props
|
||||||
|
const resolvedStart = startIcon ? (
|
||||||
|
<InputAdornment position="start">{startIcon}</InputAdornment>
|
||||||
|
) : startAdornment;
|
||||||
|
|
||||||
|
const resolvedEnd = endIcon ? (
|
||||||
|
<InputAdornment position="end">{endIcon}</InputAdornment>
|
||||||
|
) : endAdornment;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
ref={ref}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<InputLabel
|
||||||
|
htmlFor={inputId}
|
||||||
|
shrink
|
||||||
|
sx={{
|
||||||
|
position: 'static',
|
||||||
|
transform: 'none',
|
||||||
|
maxWidth: 'none',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
mb: '10px',
|
||||||
|
// labelLg typography
|
||||||
|
fontFamily: (theme) => theme.typography.labelLg.fontFamily,
|
||||||
|
fontSize: (theme) => theme.typography.labelLg.fontSize,
|
||||||
|
fontWeight: (theme) => theme.typography.labelLg.fontWeight,
|
||||||
|
lineHeight: (theme) => theme.typography.labelLg.lineHeight,
|
||||||
|
letterSpacing: (theme) =>
|
||||||
|
(theme.typography.labelLg as { letterSpacing?: string }).letterSpacing ?? 'normal',
|
||||||
|
color: 'text.secondary',
|
||||||
|
// Label stays neutral on error/focus/success (per Figma design)
|
||||||
|
'&.Mui-focused': { color: 'text.secondary' },
|
||||||
|
'&.Mui-error': { color: 'text.secondary' },
|
||||||
|
'&.Mui-disabled': { color: 'text.disabled' },
|
||||||
|
// Required asterisk in error red
|
||||||
|
'& .MuiInputLabel-asterisk': { color: 'error.main' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</InputLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OutlinedInput
|
||||||
|
id={inputId}
|
||||||
|
size={size}
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
notched={false}
|
||||||
|
startAdornment={resolvedStart}
|
||||||
|
endAdornment={resolvedEnd}
|
||||||
|
aria-describedby={helperId}
|
||||||
|
sx={[
|
||||||
|
// Success border + focus ring (not a native MUI state)
|
||||||
|
success && !error && {
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'success.main',
|
||||||
|
},
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'success.main',
|
||||||
|
},
|
||||||
|
'&.Mui-focused': {
|
||||||
|
boxShadow: (theme: Record<string, any>) =>
|
||||||
|
`0 0 0 3px ${theme.palette.common.white}, 0 0 0 5px ${theme.palette.success.main}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{helperText && (
|
||||||
|
<FormHelperText
|
||||||
|
id={helperId}
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
|
role={error ? 'alert' : undefined}
|
||||||
|
sx={{
|
||||||
|
mx: 0,
|
||||||
|
mt: '6px',
|
||||||
|
// caption typography
|
||||||
|
fontFamily: (theme) => theme.typography.caption.fontFamily,
|
||||||
|
fontSize: (theme) => theme.typography.caption.fontSize,
|
||||||
|
fontWeight: (theme) => theme.typography.caption.fontWeight,
|
||||||
|
lineHeight: (theme) => theme.typography.caption.lineHeight,
|
||||||
|
letterSpacing: (theme) => theme.typography.caption.letterSpacing,
|
||||||
|
// Contextual colour: error > success > secondary
|
||||||
|
...(error
|
||||||
|
? { color: 'error.main' }
|
||||||
|
: success
|
||||||
|
? { color: 'success.main' }
|
||||||
|
: { color: 'text.secondary' }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{helperText}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Input.displayName = 'Input';
|
||||||
|
export default Input;
|
||||||
2
src/components/atoms/Input/index.ts
Normal file
2
src/components/atoms/Input/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { Input, default } from './Input';
|
||||||
|
export type { InputProps } from './Input';
|
||||||
345
src/components/atoms/Typography/Typography.stories.tsx
Normal file
345
src/components/atoms/Typography/Typography.stories.tsx
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Typography } from './Typography';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Typography> = {
|
||||||
|
title: 'Atoms/Typography',
|
||||||
|
component: Typography,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/design/3t6fpT5inh7zzjxQdW8U5p/Design-System---Template?node-id=23-30',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: 'select',
|
||||||
|
options: [
|
||||||
|
'displayHero', 'display1', 'display2', 'display3', 'displaySm',
|
||||||
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'bodyLg', 'body1', 'body2', 'bodyXs',
|
||||||
|
'labelLg', 'label', 'labelSm',
|
||||||
|
'caption', 'captionSm',
|
||||||
|
'overline', 'overlineSm',
|
||||||
|
],
|
||||||
|
description: 'Typography variant — 21 variants across 6 categories',
|
||||||
|
table: { defaultValue: { summary: 'body1' } },
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
control: 'select',
|
||||||
|
options: [
|
||||||
|
'textPrimary', 'textSecondary', 'textDisabled',
|
||||||
|
'primary', 'secondary', 'error',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
maxLines: { control: 'number' },
|
||||||
|
gutterBottom: { control: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Typography>;
|
||||||
|
|
||||||
|
const SAMPLE = 'Discover, Explore, and Plan Funerals in Minutes, Not Hours';
|
||||||
|
|
||||||
|
// ─── Default ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Funeral Arranger helps families find transparent, affordable funeral services across Australia.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Display (Noto Serif SC, Regular) ───────────────────────────────────────
|
||||||
|
|
||||||
|
/** 5 display levels — Noto Serif SC Regular. For hero/marketing text. All scale down on mobile. */
|
||||||
|
export const Display: Story = {
|
||||||
|
name: 'Display (Serif)',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">displayHero — 80px</Typography>
|
||||||
|
<Typography variant="displayHero">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">display1 — 64px</Typography>
|
||||||
|
<Typography variant="display1">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">display2 — 52px</Typography>
|
||||||
|
<Typography variant="display2">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">display3 — 40px</Typography>
|
||||||
|
<Typography variant="display3">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">displaySm — 32px</Typography>
|
||||||
|
<Typography variant="displaySm">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Headings (Montserrat, Bold) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** 6 heading levels — Montserrat Bold. For content structure. All scale down on mobile. */
|
||||||
|
export const Headings: Story = {
|
||||||
|
name: 'Headings (Sans-serif)',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">h1 — 36px</Typography>
|
||||||
|
<Typography variant="h1">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">h2 — 30px</Typography>
|
||||||
|
<Typography variant="h2">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">h3 — 24px</Typography>
|
||||||
|
<Typography variant="h3">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">h4 — 20px</Typography>
|
||||||
|
<Typography variant="h4">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">h5 — 18px</Typography>
|
||||||
|
<Typography variant="h5">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">h6 — 16px</Typography>
|
||||||
|
<Typography variant="h6">{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Body (Montserrat, Medium) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** 4 body sizes — Montserrat Medium (500). For content text. */
|
||||||
|
export const Body: Story = {
|
||||||
|
name: 'Body Text',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 640 }}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="overline" gutterBottom>bodyLg — 18px</Typography>
|
||||||
|
<Typography variant="bodyLg">
|
||||||
|
Planning a funeral is one of the most difficult tasks a family faces. Funeral Arranger
|
||||||
|
is here to help you navigate this process with care and transparency.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="overline" gutterBottom>body1 (default) — 16px</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
Compare funeral directors in your area, view transparent pricing, and make informed
|
||||||
|
decisions at your own pace. Every family deserves clarity during this time.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="overline" gutterBottom>body2 (small) — 14px</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Prices shown are indicative and may vary based on your specific requirements.
|
||||||
|
Contact the funeral director directly for a detailed quote.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="overline" gutterBottom>bodyXs — 12px</Typography>
|
||||||
|
<Typography variant="bodyXs">
|
||||||
|
Terms and conditions apply. Funeral Arranger is a comparison service and does not
|
||||||
|
directly provide funeral services. ABN 12 345 678 901.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Label, Caption, Overline ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** UI text variants — labels (medium 500), captions (regular 400), overlines (semibold 600 uppercase) */
|
||||||
|
export const UIText: Story = {
|
||||||
|
name: 'UI Text (Label / Caption / Overline)',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">labelLg — 16px medium</Typography>
|
||||||
|
<Typography variant="labelLg" display="block">Form label or section label</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">label — 14px medium</Typography>
|
||||||
|
<Typography variant="label" display="block">Default form label</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">labelSm — 12px medium</Typography>
|
||||||
|
<Typography variant="labelSm" display="block">Compact label or tag text</Typography>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">caption — 12px regular</Typography>
|
||||||
|
<Typography variant="caption" display="block">Fine print, timestamps, metadata</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">captionSm — 11px regular</Typography>
|
||||||
|
<Typography variant="captionSm" display="block">Compact metadata, footnotes</Typography>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">overline — 12px semibold uppercase</Typography>
|
||||||
|
<Typography variant="overline" display="block">Section overline</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="captionSm" color="textSecondary">overlineSm — 11px semibold uppercase</Typography>
|
||||||
|
<Typography variant="overlineSm" display="block">Compact overline</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Colours ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Colours: Story = {
|
||||||
|
name: 'Colours',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
<Typography color="textPrimary">Text Primary — main body text (neutral.800)</Typography>
|
||||||
|
<Typography color="textSecondary">Text Secondary — helper text (neutral.600)</Typography>
|
||||||
|
<Typography color="textDisabled">Text Disabled — inactive (neutral.400)</Typography>
|
||||||
|
<Typography color="primary">Primary — brand emphasis (brand.600)</Typography>
|
||||||
|
<Typography color="secondary">Secondary — neutral emphasis (neutral.600)</Typography>
|
||||||
|
<Typography color="error">Error — validation errors (red.600)</Typography>
|
||||||
|
<Typography color="warning.main">Warning — cautionary (amber.600)</Typography>
|
||||||
|
<Typography color="success.main">Success — confirmations (green.600)</Typography>
|
||||||
|
<Typography color="info.main">Info — helpful tips (blue.600)</Typography>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Font Families ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** The two font families: serif for display, sans-serif for everything else */
|
||||||
|
export const FontFamilies: Story = {
|
||||||
|
name: 'Font Families',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="overline" gutterBottom>Display font — Noto Serif SC (Regular 400)</Typography>
|
||||||
|
<Typography variant="display3">
|
||||||
|
Warm, trustworthy, and professional
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
Used exclusively for display variants (hero through sm). Regular weight — serif carries inherent visual weight at large sizes.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="overline" gutterBottom>Body font — Montserrat</Typography>
|
||||||
|
<Typography variant="h3" gutterBottom>Clean, modern, and highly readable</Typography>
|
||||||
|
<Typography>
|
||||||
|
Used for all headings (h1–h6), body text, labels, captions, and UI elements.
|
||||||
|
Headings use Bold (700), body uses Medium (500), captions use Regular (400).
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Max Lines ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MaxLines: Story = {
|
||||||
|
name: 'Max Lines (Truncation)',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 400 }}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="label" gutterBottom>maxLines=1</Typography>
|
||||||
|
<Typography maxLines={1}>
|
||||||
|
H. Parsons Funeral Directors — trusted by Australian families for over 30 years,
|
||||||
|
providing compassionate and transparent funeral services.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="label" gutterBottom>maxLines=2</Typography>
|
||||||
|
<Typography maxLines={2}>
|
||||||
|
H. Parsons Funeral Directors — trusted by Australian families for over 30 years,
|
||||||
|
providing compassionate and transparent funeral services across metropolitan
|
||||||
|
and regional areas.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Realistic Content ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const RealisticContent: Story = {
|
||||||
|
name: 'Realistic Content',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ maxWidth: 640, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
<Typography variant="overline">Funeral planning</Typography>
|
||||||
|
<Typography variant="display3">Compare funeral services in your area</Typography>
|
||||||
|
<Typography variant="bodyLg" color="textSecondary">
|
||||||
|
Transparent pricing and service comparison to help you make informed
|
||||||
|
decisions during a difficult time.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h2" sx={{ mt: 2 }}>How it works</Typography>
|
||||||
|
<Typography>
|
||||||
|
Enter your suburb or postcode to find funeral directors near you. Each
|
||||||
|
listing includes a full price breakdown, service inclusions, and reviews
|
||||||
|
from families who have used their services.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h3" sx={{ mt: 1 }}>Step 1: Browse packages</Typography>
|
||||||
|
<Typography>
|
||||||
|
Compare packages side by side. Each package clearly shows what is and
|
||||||
|
isn't included, so there are no surprises.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="textSecondary" sx={{ mt: 2 }}>
|
||||||
|
Prices are indicative and current as of March 2026. Contact the funeral
|
||||||
|
director for a binding quote.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Complete Scale ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** All 21 variants in a single view — matches the Figma "Fonts - Desktop" layout */
|
||||||
|
export const CompleteScale: Story = {
|
||||||
|
name: 'Complete Scale (All 21 Variants)',
|
||||||
|
render: () => {
|
||||||
|
const variants = [
|
||||||
|
{ variant: 'displayHero', label: 'display/hero — 80px' },
|
||||||
|
{ variant: 'display1', label: 'display/1 — 64px' },
|
||||||
|
{ variant: 'display2', label: 'display/2 — 52px' },
|
||||||
|
{ variant: 'display3', label: 'display/3 — 40px' },
|
||||||
|
{ variant: 'displaySm', label: 'display/sm — 32px' },
|
||||||
|
{ variant: 'h1', label: 'heading/1 — 36px' },
|
||||||
|
{ variant: 'h2', label: 'heading/2 — 30px' },
|
||||||
|
{ variant: 'h3', label: 'heading/3 — 24px' },
|
||||||
|
{ variant: 'h4', label: 'heading/4 — 20px' },
|
||||||
|
{ variant: 'h5', label: 'heading/5 — 18px' },
|
||||||
|
{ variant: 'h6', label: 'heading/6 — 16px' },
|
||||||
|
{ variant: 'bodyLg', label: 'body/lg — 18px' },
|
||||||
|
{ variant: 'body1', label: 'body/md — 16px' },
|
||||||
|
{ variant: 'body2', label: 'body/sm — 14px' },
|
||||||
|
{ variant: 'bodyXs', label: 'body/xs — 12px' },
|
||||||
|
{ variant: 'labelLg', label: 'label/lg — 16px' },
|
||||||
|
{ variant: 'label', label: 'label/md — 14px' },
|
||||||
|
{ variant: 'labelSm', label: 'label/sm — 12px' },
|
||||||
|
{ variant: 'caption', label: 'caption/md — 12px' },
|
||||||
|
{ variant: 'captionSm', label: 'caption/sm — 11px' },
|
||||||
|
{ variant: 'overline', label: 'overline/md — 12px' },
|
||||||
|
{ variant: 'overlineSm', label: 'overline/sm — 11px' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
{variants.map(({ variant, label }) => (
|
||||||
|
<div key={variant} style={{ display: 'flex', alignItems: 'baseline', gap: 16 }}>
|
||||||
|
<Typography variant="captionSm" color="textSecondary" sx={{ width: 160, flexShrink: 0, textAlign: 'right' }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant={variant}>{SAMPLE}</Typography>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
63
src/components/atoms/Typography/Typography.tsx
Normal file
63
src/components/atoms/Typography/Typography.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
import type { TypographyProps as MuiTypographyProps } from '@mui/material/Typography';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA Typography component */
|
||||||
|
export interface TypographyProps extends MuiTypographyProps {
|
||||||
|
/** Truncate text with ellipsis after this many lines (CSS line-clamp) */
|
||||||
|
maxLines?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text display component for the FA design system.
|
||||||
|
*
|
||||||
|
* Wraps MUI Typography with FA brand fonts and type scale. All variant
|
||||||
|
* styles (sizes, weights, line heights) come from the MUI theme which
|
||||||
|
* maps to our design tokens.
|
||||||
|
*
|
||||||
|
* Variant guide (21 variants across 6 categories):
|
||||||
|
*
|
||||||
|
* Display (Noto Serif SC, Regular 400):
|
||||||
|
* - `displayHero` 80px, `display1` 64px, `display2` 52px, `display3` 40px, `displaySm` 32px
|
||||||
|
*
|
||||||
|
* Headings (Montserrat, Bold 700):
|
||||||
|
* - `h1` 36px, `h2` 30px, `h3` 24px, `h4` 20px, `h5` 18px, `h6` 16px
|
||||||
|
*
|
||||||
|
* Body (Montserrat, Medium 500):
|
||||||
|
* - `bodyLg` 18px, `body1` 16px, `body2` 14px, `bodyXs` 12px
|
||||||
|
*
|
||||||
|
* Label (Montserrat, Medium 500):
|
||||||
|
* - `labelLg` 16px, `label` 14px, `labelSm` 12px
|
||||||
|
*
|
||||||
|
* Caption (Montserrat, Regular 400):
|
||||||
|
* - `caption` 12px, `captionSm` 11px
|
||||||
|
*
|
||||||
|
* Overline (Montserrat, SemiBold 600, uppercase):
|
||||||
|
* - `overline` 12px, `overlineSm` 11px
|
||||||
|
*/
|
||||||
|
export const Typography = React.forwardRef<HTMLElement, TypographyProps>(
|
||||||
|
({ maxLines, sx, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<MuiTypography
|
||||||
|
ref={ref}
|
||||||
|
sx={[
|
||||||
|
maxLines != null && {
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: maxLines,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Typography.displayName = 'Typography';
|
||||||
|
export default Typography;
|
||||||
3
src/components/atoms/Typography/index.ts
Normal file
3
src/components/atoms/Typography/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default } from './Typography';
|
||||||
|
export { Typography } from './Typography';
|
||||||
|
export type { TypographyProps } from './Typography';
|
||||||
20
src/main.tsx
Normal file
20
src/main.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { ThemeProvider, CssBaseline } from '@mui/material';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<div style={{ padding: 32 }}>
|
||||||
|
<h1>FA Design System</h1>
|
||||||
|
<p>Run <code>npm run storybook</code> to view components.</p>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
354
src/theme/generated/tokens.css
Normal file
354
src/theme/generated/tokens.css
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
/**
|
||||||
|
* Do not edit directly, this file was auto-generated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--fa-button-height-xs: 28px; /** Extra-small — compact text buttons, inline actions */
|
||||||
|
--fa-button-height-sm: 32px; /** Small — secondary actions, toolbar buttons */
|
||||||
|
--fa-button-height-md: 40px; /** Medium — default size, form submissions */
|
||||||
|
--fa-button-height-lg: 48px; /** Large — primary CTAs, mobile touch targets (meets 44px minimum) */
|
||||||
|
--fa-button-icon-size-xs: 14px; /** 14px icons in extra-small buttons */
|
||||||
|
--fa-button-icon-size-sm: 16px; /** 16px icons in small buttons */
|
||||||
|
--fa-button-icon-size-md: 18px; /** 18px icons in medium buttons */
|
||||||
|
--fa-button-icon-size-lg: 20px; /** 20px icons in large buttons */
|
||||||
|
--fa-input-height-sm: 40px; /** Small — compact forms, admin layouts, matches Button medium height */
|
||||||
|
--fa-input-height-md: 48px; /** Medium (default) — standard forms, matches Button large for alignment */
|
||||||
|
--fa-input-icon-size-default: 20px; /** 20px — icon size inside input field, matches Figma trailing icon */
|
||||||
|
--fa-color-brand-50: #fef9f5; /** Lightest warm tint — warm section backgrounds */
|
||||||
|
--fa-color-brand-100: #f7ecdf; /** Light warm — hover backgrounds, subtle fills */
|
||||||
|
--fa-color-brand-200: #ebdac8; /** Warm light — secondary backgrounds, divider tones */
|
||||||
|
--fa-color-brand-300: #d8c3b5; /** Warm beige — from brand swatch. Surface warmth, card tints */
|
||||||
|
--fa-color-brand-400: #d0a070; /** Mid gold — from brand swatch. Secondary interactive, step indicators */
|
||||||
|
--fa-color-brand-500: #ba834e; /** Base brand gold — from brand swatch. Primary CTA colour, main interactive. 3.7:1 contrast on white */
|
||||||
|
--fa-color-brand-600: #b0610f; /** Rich copper — from brand swatch. Hover/emphasis on brand elements. 4.8:1 contrast on white */
|
||||||
|
--fa-color-brand-700: #8b4e0d; /** Deep copper — active states, strong brand text on light backgrounds. 6.7:1 contrast on white */
|
||||||
|
--fa-color-brand-800: #6b3c13; /** Dark brown — bold brand accents, high-contrast brand text */
|
||||||
|
--fa-color-brand-900: #51301b; /** Chocolate — from brand swatch. Deep emphasis, dark brand surfaces */
|
||||||
|
--fa-color-brand-950: #251913; /** Espresso — from brand swatch. Darkest brand tone, near-black warm */
|
||||||
|
--fa-color-sage-50: #f2f5f6; /** Lightest sage — subtle cool backgrounds */
|
||||||
|
--fa-color-sage-100: #e3eaeb; /** Light sage — hover states on cool surfaces */
|
||||||
|
--fa-color-sage-200: #d7e1e2; /** From brand swatch — light cool surface, soft borders */
|
||||||
|
--fa-color-sage-300: #c8d4d6; /** Mid-light sage — dividers, secondary borders */
|
||||||
|
--fa-color-sage-400: #b9c7c9; /** From brand swatch — mid sage, secondary text on dark backgrounds */
|
||||||
|
--fa-color-sage-500: #8ea2a7; /** Base sage — secondary content, muted icons */
|
||||||
|
--fa-color-sage-600: #687d84; /** Dark sage — secondary text, subtle icons */
|
||||||
|
--fa-color-sage-700: #4c5b6b; /** From brand swatch — strong secondary, dark accents. 6.1:1 contrast on white */
|
||||||
|
--fa-color-sage-800: #4c5459; /** From brand swatch — near-dark grey, supplementary text. 6.7:1 contrast on white */
|
||||||
|
--fa-color-sage-900: #343c40; /** Very dark sage — high-contrast secondary elements */
|
||||||
|
--fa-color-sage-950: #1e2528; /** Near-black cool — darkest secondary tone */
|
||||||
|
--fa-color-neutral-50: #fafafa; /** Lightest neutral — default page background alternative */
|
||||||
|
--fa-color-neutral-100: #f5f5f5; /** Light neutral — subtle background differentiation */
|
||||||
|
--fa-color-neutral-200: #e8e8e8; /** Light grey — standard borders, dividers */
|
||||||
|
--fa-color-neutral-300: #d4d4d4; /** Mid-light grey — disabled borders, subtle separators */
|
||||||
|
--fa-color-neutral-400: #a3a3a3; /** Mid grey — placeholder text, disabled content */
|
||||||
|
--fa-color-neutral-500: #737373; /** Base grey — secondary body text, icons */
|
||||||
|
--fa-color-neutral-600: #525252; /** Dark grey — body text, labels. 7.1:1 contrast on white */
|
||||||
|
--fa-color-neutral-700: #404040; /** Strong grey — headings, emphasis text. 9.7:1 contrast on white */
|
||||||
|
--fa-color-neutral-800: #2c2e35; /** From brand swatch — charcoal with cool tint. Primary text colour. 13.2:1 contrast on white */
|
||||||
|
--fa-color-neutral-900: #1a1a1c; /** Near-black — maximum contrast text */
|
||||||
|
--fa-color-neutral-950: #0a0a0b; /** Deepest neutral — use sparingly */
|
||||||
|
--fa-color-red-50: #fef2f2; /** Error tint — error message backgrounds */
|
||||||
|
--fa-color-red-100: #fde8e8; /** Light error — hover on error surfaces */
|
||||||
|
--fa-color-red-200: #f9bfbf; /** Light red — error borders, subtle indicators */
|
||||||
|
--fa-color-red-300: #f09898; /** Mid-light red — error icon backgrounds */
|
||||||
|
--fa-color-red-400: #e56b6b; /** Mid red — error indicators, badges */
|
||||||
|
--fa-color-red-500: #d64545; /** Base red — form validation errors, alert accents */
|
||||||
|
--fa-color-red-600: #bc2f2f; /** Strong red — error text on light backgrounds. 5.7:1 contrast on white */
|
||||||
|
--fa-color-red-700: #9b2424; /** Dark red — error headings, strong alerts */
|
||||||
|
--fa-color-red-800: #7a1d1d; /** Deep red — high-contrast error emphasis */
|
||||||
|
--fa-color-red-900: #5c1616; /** Very dark red — use sparingly */
|
||||||
|
--fa-color-red-950: #3d0e0e; /** Darkest red */
|
||||||
|
--fa-color-amber-50: #fff9eb; /** Warning tint — warning message backgrounds */
|
||||||
|
--fa-color-amber-100: #fff0cc; /** Light amber — hover on warning surfaces */
|
||||||
|
--fa-color-amber-200: #ffe099; /** Light amber — warning borders */
|
||||||
|
--fa-color-amber-300: #ffcc66; /** Mid-light amber — warning icon backgrounds */
|
||||||
|
--fa-color-amber-400: #ffb833; /** Mid amber — warning badges, price alerts */
|
||||||
|
--fa-color-amber-500: #f5a000; /** Base amber — warning accents */
|
||||||
|
--fa-color-amber-600: #cc8500; /** Strong amber — warning text. 3.6:1 contrast on white (large text AA) */
|
||||||
|
--fa-color-amber-700: #a36b00; /** Dark amber — warning headings. 5.1:1 contrast on white */
|
||||||
|
--fa-color-amber-800: #7a5000; /** Deep amber — high-contrast warning emphasis */
|
||||||
|
--fa-color-amber-900: #523600; /** Very dark amber — use sparingly */
|
||||||
|
--fa-color-amber-950: #331f00; /** Darkest amber */
|
||||||
|
--fa-color-green-50: #f0f7f0; /** Success tint — success message backgrounds */
|
||||||
|
--fa-color-green-100: #d8ecd8; /** Light green — hover on success surfaces */
|
||||||
|
--fa-color-green-200: #b8d8b8; /** Light green — success borders */
|
||||||
|
--fa-color-green-300: #8dc08d; /** Mid-light green — success icon backgrounds */
|
||||||
|
--fa-color-green-400: #66a866; /** Mid green — success badges, completion indicators */
|
||||||
|
--fa-color-green-500: #4a8f4a; /** Base green — success accents, completed steps */
|
||||||
|
--fa-color-green-600: #3b7a3b; /** Strong green — success text on light backgrounds. 4.8:1 contrast on white */
|
||||||
|
--fa-color-green-700: #2e6b2e; /** Dark green — success headings */
|
||||||
|
--fa-color-green-800: #235523; /** Deep green — high-contrast success emphasis */
|
||||||
|
--fa-color-green-900: #1a3f1a; /** Very dark green — use sparingly */
|
||||||
|
--fa-color-green-950: #0f2a0f; /** Darkest green */
|
||||||
|
--fa-color-blue-50: #eff6ff; /** Info tint — info message backgrounds */
|
||||||
|
--fa-color-blue-100: #dbeafe; /** Light blue — hover on info surfaces */
|
||||||
|
--fa-color-blue-200: #bfdbfe; /** Light blue — info borders */
|
||||||
|
--fa-color-blue-300: #93c5fd; /** Mid-light blue — info icon backgrounds */
|
||||||
|
--fa-color-blue-400: #60a5fa; /** Mid blue — info badges */
|
||||||
|
--fa-color-blue-500: #3b82f6; /** Base blue — info accents, supplementary links */
|
||||||
|
--fa-color-blue-600: #2563eb; /** Strong blue — info text on light backgrounds. 4.6:1 contrast on white */
|
||||||
|
--fa-color-blue-700: #1d4ed8; /** Dark blue — info headings */
|
||||||
|
--fa-color-blue-800: #1e40af; /** Deep blue — high-contrast info emphasis */
|
||||||
|
--fa-color-blue-900: #1e3a8a; /** Very dark blue — use sparingly */
|
||||||
|
--fa-color-blue-950: #172554; /** Darkest blue */
|
||||||
|
--fa-color-white: #ffffff; /** Pure white — card backgrounds, inverse text, primary surface */
|
||||||
|
--fa-color-black: #000000; /** Pure black — from brand swatch. Use sparingly; prefer neutral.800-900 for text */
|
||||||
|
--fa-color-surface-overlay: rgba(0, 0, 0, 0.5); /** Overlay surface — modal/dialog backdrop at 50% black */
|
||||||
|
--fa-shadow-sm: 0 1px 2px rgba(0,0,0,0.05); /** Subtle lift — resting buttons, input focus subtle elevation */
|
||||||
|
--fa-shadow-md: 0 4px 6px rgba(0,0,0,0.07); /** Medium elevation — cards at rest, dropdowns, popovers */
|
||||||
|
--fa-shadow-lg: 0 10px 15px rgba(0,0,0,0.1); /** High elevation — modals, popovers, card hover states */
|
||||||
|
--fa-shadow-xl: 0 20px 25px rgba(0,0,0,0.1); /** Maximum elevation — elevated panels, dialog boxes */
|
||||||
|
--fa-opacity-disabled: 0.4; /** Disabled state — 40% opacity. Clearly diminished but still distinguishable */
|
||||||
|
--fa-opacity-hover: 0.08; /** Hover overlay — subtle 8% tint applied over backgrounds on hover */
|
||||||
|
--fa-opacity-overlay: 0.5; /** Modal/dialog backdrop — 50% black overlay behind modals */
|
||||||
|
--fa-spacing-1: 4px; /** Tight — inline spacing, minimal gaps between related elements */
|
||||||
|
--fa-spacing-2: 8px; /** Small — related element gaps, compact padding, icon margins */
|
||||||
|
--fa-spacing-3: 12px; /** Component internal padding (small), chip padding */
|
||||||
|
--fa-spacing-4: 16px; /** Default component padding, form field gap, card grid gutter (mobile) */
|
||||||
|
--fa-spacing-5: 20px; /** Medium component spacing */
|
||||||
|
--fa-spacing-6: 24px; /** Card padding, section gap (small), card grid gutter (desktop) */
|
||||||
|
--fa-spacing-8: 32px; /** Section gap (medium), form section separation */
|
||||||
|
--fa-spacing-10: 40px; /** Section gap (large) */
|
||||||
|
--fa-spacing-12: 48px; /** Page section separation, vertical rhythm break */
|
||||||
|
--fa-spacing-16: 64px; /** Hero/banner vertical spacing */
|
||||||
|
--fa-spacing-20: 80px; /** Major page sections, large vertical spacing */
|
||||||
|
--fa-spacing-0-5: 2px; /** Hairline — icon-to-text tight spacing, fine adjustments */
|
||||||
|
--fa-border-radius-none: 0px; /** Square corners — tables, dividers, sharp elements */
|
||||||
|
--fa-border-radius-sm: 4px; /** Small radius — inputs, small interactive elements, chips */
|
||||||
|
--fa-border-radius-md: 8px; /** Medium radius — cards, buttons, dropdowns (default) */
|
||||||
|
--fa-border-radius-lg: 12px; /** Large radius — modals, large cards */
|
||||||
|
--fa-border-radius-xl: 16px; /** Extra large — feature cards, hero elements */
|
||||||
|
--fa-border-radius-full: 9999px; /** Full/pill — avatars, pill buttons, circular elements */
|
||||||
|
--fa-font-family-body: 'Montserrat', 'Helvetica Neue', Arial, sans-serif; /** Primary font — Montserrat. Used for headings (h1-h6), body text, labels, and all UI elements */
|
||||||
|
--fa-font-family-display: 'Noto Serif SC', Georgia, 'Times New Roman', serif; /** Display font — Noto Serif SC. Elegant serif for hero/display text only. Adds warmth and gravitas at large sizes */
|
||||||
|
--fa-font-family-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace; /** Monospace font — for reference numbers (FA-2026-00847), tabular pricing data, and code */
|
||||||
|
--fa-font-size-2xs: 0.6875rem; /** 11px — smallest UI text: compact captions, compact overlines */
|
||||||
|
--fa-font-size-xs: 0.75rem; /** 12px — small text: captions, labels, overlines, body/xs */
|
||||||
|
--fa-font-size-sm: 0.875rem; /** 14px — body small, labels, helper text */
|
||||||
|
--fa-font-size-base: 1rem; /** 16px — default body text, heading/6, label/lg */
|
||||||
|
--fa-font-size-md: 1.125rem; /** 18px — body large, heading/5 */
|
||||||
|
--fa-font-size-lg: 1.25rem; /** 20px — heading/4 */
|
||||||
|
--fa-font-size-xl: 1.5rem; /** 24px — heading/3 */
|
||||||
|
--fa-font-size-2xl: 1.875rem; /** 30px — heading/2 */
|
||||||
|
--fa-font-size-3xl: 2.25rem; /** 36px — heading/1 */
|
||||||
|
--fa-font-size-4xl: 3rem; /** 48px — reserved (legacy) */
|
||||||
|
--fa-font-size-display-1: 4rem; /** 64px — display/1 */
|
||||||
|
--fa-font-size-display-2: 3.25rem; /** 52px — display/2 */
|
||||||
|
--fa-font-size-display-3: 2.5rem; /** 40px — display/3 */
|
||||||
|
--fa-font-size-display-sm: 2rem; /** 32px — display/sm, smallest display text */
|
||||||
|
--fa-font-size-display-hero: 5rem; /** 80px — display/hero, largest display text */
|
||||||
|
--fa-font-size-mobile-display-hero: 2rem; /** 32px — mobile display/hero (from 80px desktop) */
|
||||||
|
--fa-font-size-mobile-display1: 1.75rem; /** 28px — mobile display/1 (from 64px desktop) */
|
||||||
|
--fa-font-size-mobile-display2: 1.5rem; /** 24px — mobile display/2 (from 52px desktop) */
|
||||||
|
--fa-font-size-mobile-display3: 1.375rem; /** 22px — mobile display/3 (from 40px desktop) */
|
||||||
|
--fa-font-size-mobile-display-sm: 1.25rem; /** 20px — mobile display/sm (from 32px desktop) */
|
||||||
|
--fa-font-size-mobile-h1: 1.625rem; /** 26px — mobile heading/1 (from 36px desktop) */
|
||||||
|
--fa-font-size-mobile-h2: 1.375rem; /** 22px — mobile heading/2 (from 30px desktop) */
|
||||||
|
--fa-font-size-mobile-h3: 1.25rem; /** 20px — mobile heading/3 (from 24px desktop) */
|
||||||
|
--fa-font-size-mobile-h4: 1.125rem; /** 18px — mobile heading/4 (from 20px desktop) */
|
||||||
|
--fa-font-size-mobile-h5: 1rem; /** 16px — mobile heading/5 (from 18px desktop) */
|
||||||
|
--fa-font-size-mobile-h6: 0.875rem; /** 14px — mobile heading/6 (from 16px desktop) */
|
||||||
|
--fa-font-weight-regular: 400; /** Regular weight — captions, display text (serif carries inherent weight) */
|
||||||
|
--fa-font-weight-medium: 500; /** Medium weight — body text, labels. Slightly bolder than regular for improved readability */
|
||||||
|
--fa-font-weight-semibold: 600; /** Semibold — overlines, button text, navigation */
|
||||||
|
--fa-font-weight-bold: 700; /** Bold — all headings (h1-h6) */
|
||||||
|
--fa-line-height-tight: 1.25; /** Tight leading — large headings, display text */
|
||||||
|
--fa-line-height-snug: 1.375; /** Snug leading — sub-headings, labels, small text */
|
||||||
|
--fa-line-height-normal: 1.5; /** Normal leading — default body text, optimal readability */
|
||||||
|
--fa-line-height-relaxed: 1.75; /** Relaxed leading — large body text, long-form content */
|
||||||
|
--fa-letter-spacing-tighter: -0.02em; /** Tighter tracking — large display text */
|
||||||
|
--fa-letter-spacing-tight: -0.01em; /** Tight tracking — headings */
|
||||||
|
--fa-letter-spacing-normal: 0em; /** Normal tracking — body text, most content */
|
||||||
|
--fa-letter-spacing-wide: 0.02em; /** Wide tracking — captions, small text */
|
||||||
|
--fa-letter-spacing-wider: 0.05em; /** Wider tracking — labels, UI text */
|
||||||
|
--fa-letter-spacing-widest: 0.08em; /** Widest tracking — overlines, uppercase text */
|
||||||
|
--fa-typography-display-hero-line-height: 1.05;
|
||||||
|
--fa-typography-display-hero-letter-spacing: -1.5px;
|
||||||
|
--fa-typography-display1-line-height: 1.078;
|
||||||
|
--fa-typography-display1-letter-spacing: -1px;
|
||||||
|
--fa-typography-display2-line-height: 1.096;
|
||||||
|
--fa-typography-display2-letter-spacing: -0.5px;
|
||||||
|
--fa-typography-display3-line-height: 1.15;
|
||||||
|
--fa-typography-display3-letter-spacing: -0.25px;
|
||||||
|
--fa-typography-display-sm-line-height: 1.1875;
|
||||||
|
--fa-typography-display-sm-letter-spacing: 0px;
|
||||||
|
--fa-typography-h1-line-height: 1.194;
|
||||||
|
--fa-typography-h1-letter-spacing: -0.5px;
|
||||||
|
--fa-typography-h2-line-height: 1.267;
|
||||||
|
--fa-typography-h2-letter-spacing: -0.25px;
|
||||||
|
--fa-typography-h3-line-height: 1.292;
|
||||||
|
--fa-typography-h3-letter-spacing: 0px;
|
||||||
|
--fa-typography-h4-line-height: 1.35;
|
||||||
|
--fa-typography-h4-letter-spacing: 0px;
|
||||||
|
--fa-typography-h5-line-height: 1.389;
|
||||||
|
--fa-typography-h5-letter-spacing: 0px;
|
||||||
|
--fa-typography-h6-line-height: 1.375;
|
||||||
|
--fa-typography-h6-letter-spacing: 0px;
|
||||||
|
--fa-typography-body-lg-line-height: 1.611;
|
||||||
|
--fa-typography-body-lg-letter-spacing: 0px;
|
||||||
|
--fa-typography-body-line-height: 1.625;
|
||||||
|
--fa-typography-body-letter-spacing: 0px;
|
||||||
|
--fa-typography-body-sm-line-height: 1.571;
|
||||||
|
--fa-typography-body-sm-letter-spacing: 0px;
|
||||||
|
--fa-typography-body-xs-line-height: 1.5;
|
||||||
|
--fa-typography-body-xs-letter-spacing: 0.1px;
|
||||||
|
--fa-typography-label-lg-line-height: 1.3125;
|
||||||
|
--fa-typography-label-lg-letter-spacing: 0.1px;
|
||||||
|
--fa-typography-label-line-height: 1.286;
|
||||||
|
--fa-typography-label-letter-spacing: 0.15px;
|
||||||
|
--fa-typography-label-sm-line-height: 1.333;
|
||||||
|
--fa-typography-label-sm-letter-spacing: 0.2px;
|
||||||
|
--fa-typography-caption-line-height: 1.417;
|
||||||
|
--fa-typography-caption-letter-spacing: 0.2px;
|
||||||
|
--fa-typography-caption-sm-line-height: 1.364;
|
||||||
|
--fa-typography-caption-sm-letter-spacing: 0.2px;
|
||||||
|
--fa-typography-overline-line-height: 1.333;
|
||||||
|
--fa-typography-overline-letter-spacing: 1.5px;
|
||||||
|
--fa-typography-overline-sm-line-height: 1.273;
|
||||||
|
--fa-typography-overline-sm-letter-spacing: 1.5px;
|
||||||
|
--fa-button-padding-x-xs: var(--fa-spacing-2); /** 8px — compact horizontal padding */
|
||||||
|
--fa-button-padding-x-sm: var(--fa-spacing-3); /** 12px — small horizontal padding */
|
||||||
|
--fa-button-padding-x-md: var(--fa-spacing-4); /** 16px — default horizontal padding */
|
||||||
|
--fa-button-padding-x-lg: var(--fa-spacing-6); /** 24px — generous CTA horizontal padding */
|
||||||
|
--fa-button-padding-y-xs: var(--fa-spacing-1); /** 4px — compact vertical padding */
|
||||||
|
--fa-button-padding-y-sm: var(--fa-spacing-1); /** 4px — small vertical padding */
|
||||||
|
--fa-button-padding-y-md: var(--fa-spacing-2); /** 8px — default vertical padding */
|
||||||
|
--fa-button-padding-y-lg: var(--fa-spacing-3); /** 12px — generous CTA vertical padding */
|
||||||
|
--fa-button-font-size-xs: var(--fa-font-size-xs); /** 12px — extra-small button text */
|
||||||
|
--fa-button-font-size-sm: var(--fa-font-size-sm); /** 14px — small button text */
|
||||||
|
--fa-button-font-size-md: var(--fa-font-size-sm); /** 14px — default button text */
|
||||||
|
--fa-button-font-size-lg: var(--fa-font-size-base); /** 16px — large button text */
|
||||||
|
--fa-button-icon-gap-xs: var(--fa-spacing-1); /** 4px icon-text gap */
|
||||||
|
--fa-button-icon-gap-sm: var(--fa-spacing-1); /** 4px icon-text gap */
|
||||||
|
--fa-button-icon-gap-md: var(--fa-spacing-2); /** 8px icon-text gap */
|
||||||
|
--fa-button-icon-gap-lg: var(--fa-spacing-2); /** 8px icon-text gap */
|
||||||
|
--fa-button-border-radius-default: var(--fa-border-radius-md); /** 8px — standard button rounding */
|
||||||
|
--fa-input-padding-x-default: var(--fa-spacing-3); /** 12px — inner horizontal padding matching Figma design */
|
||||||
|
--fa-input-padding-y-sm: var(--fa-spacing-2); /** 8px — compact vertical padding for small size */
|
||||||
|
--fa-input-padding-y-md: var(--fa-spacing-3); /** 12px — standard vertical padding for medium size */
|
||||||
|
--fa-input-font-size-default: var(--fa-font-size-base); /** 16px — prevents iOS auto-zoom on focus, matches Figma */
|
||||||
|
--fa-input-border-radius-default: var(--fa-border-radius-sm); /** 4px — subtle rounding, consistent with Figma design */
|
||||||
|
--fa-input-gap-default: var(--fa-spacing-2); /** 8px — vertical rhythm between label/input/helper, slightly more generous than Figma's 6px for readability */
|
||||||
|
--fa-color-text-primary: var(--fa-color-neutral-800); /** Primary text — body content, headings. Cool charcoal (#2C2E35) for comfortable extended reading */
|
||||||
|
--fa-color-text-secondary: var(--fa-color-neutral-600); /** Secondary text — helper text, descriptions, metadata, less prominent content */
|
||||||
|
--fa-color-text-tertiary: var(--fa-color-neutral-500); /** Tertiary text — placeholders, timestamps, attribution, meta information */
|
||||||
|
--fa-color-text-disabled: var(--fa-color-neutral-400); /** Disabled text — clearly diminished but still readable for accessibility */
|
||||||
|
--fa-color-text-inverse: var(--fa-color-white); /** Inverse text — white text on dark or coloured backgrounds (buttons, banners) */
|
||||||
|
--fa-color-text-brand: var(--fa-color-brand-600); /** Brand-coloured text — links, inline brand emphasis. Copper tone meets AA on white (4.8:1) */
|
||||||
|
--fa-color-text-error: var(--fa-color-red-600); /** Error text — form validation messages, error descriptions */
|
||||||
|
--fa-color-text-success: var(--fa-color-green-600); /** Success text — confirmation messages, positive feedback */
|
||||||
|
--fa-color-text-warning: var(--fa-color-amber-700); /** Warning text — cautionary messages. Uses amber.700 for WCAG AA compliance on white (5.1:1) */
|
||||||
|
--fa-color-surface-default: var(--fa-color-white); /** Default surface — main page background, card faces */
|
||||||
|
--fa-color-surface-subtle: var(--fa-color-neutral-50); /** Subtle surface — slight differentiation from default, alternate row backgrounds */
|
||||||
|
--fa-color-surface-raised: var(--fa-color-white); /** Raised surface — cards, elevated containers (distinguished by shadow rather than colour) */
|
||||||
|
--fa-color-surface-warm: var(--fa-color-brand-50); /** Warm surface — brand-tinted sections, promotional areas, upsell cards like 'Protect your plan' */
|
||||||
|
--fa-color-surface-cool: var(--fa-color-sage-50); /** Cool surface — calming sections, information panels, FAQ backgrounds */
|
||||||
|
--fa-color-border-default: var(--fa-color-neutral-200); /** Default border — cards, containers, resting input borders */
|
||||||
|
--fa-color-border-strong: var(--fa-color-neutral-400); /** Strong border — emphasis borders, active input borders */
|
||||||
|
--fa-color-border-subtle: var(--fa-color-neutral-100); /** Subtle border — section dividers, soft separators */
|
||||||
|
--fa-color-border-brand: var(--fa-color-brand-500); /** Brand border — focused inputs, selected cards, brand-accented containers */
|
||||||
|
--fa-color-border-error: var(--fa-color-red-500); /** Error border — form fields with validation errors */
|
||||||
|
--fa-color-border-success: var(--fa-color-green-500); /** Success border — validated fields, confirmed selections */
|
||||||
|
--fa-color-interactive-default: var(--fa-color-brand-600); /** Default interactive — primary button fill, link colour, checkbox accent. Uses brand.600 (copper) for WCAG AA compliance (4.6:1 on white) */
|
||||||
|
--fa-color-interactive-hover: var(--fa-color-brand-700); /** Hover state — deepened copper on hover for clear visual feedback */
|
||||||
|
--fa-color-interactive-active: var(--fa-color-brand-800); /** Active/pressed state — dark brown during click/tap */
|
||||||
|
--fa-color-interactive-disabled: var(--fa-color-neutral-300); /** Disabled interactive — muted grey, no pointer events */
|
||||||
|
--fa-color-interactive-focus: var(--fa-color-brand-600); /** Focus ring colour — keyboard navigation indicator, 2px outline with 2px offset */
|
||||||
|
--fa-color-interactive-secondary: var(--fa-color-sage-700); /** Secondary interactive — grey/sage buttons, less prominent actions */
|
||||||
|
--fa-color-interactive-secondary-hover: var(--fa-color-sage-800); /** Secondary interactive hover — darkened sage on hover */
|
||||||
|
--fa-color-feedback-success: var(--fa-color-green-600); /** Success — confirmations, completed arrangement steps, booking confirmed */
|
||||||
|
--fa-color-feedback-success-subtle: var(--fa-color-green-50); /** Success background — success message container fill, completion banners */
|
||||||
|
--fa-color-feedback-warning: var(--fa-color-amber-600); /** Warning — price change alerts, important notices, bond/insurance prompts */
|
||||||
|
--fa-color-feedback-warning-subtle: var(--fa-color-amber-50); /** Warning background — warning message container fill, notice banners */
|
||||||
|
--fa-color-feedback-error: var(--fa-color-red-600); /** Error — form validation failures, system errors, payment issues */
|
||||||
|
--fa-color-feedback-error-subtle: var(--fa-color-red-50); /** Error background — error message container fill, alert banners */
|
||||||
|
--fa-color-feedback-info: var(--fa-color-blue-600); /** Info — helpful tips, supplementary information, guidance callouts */
|
||||||
|
--fa-color-feedback-info-subtle: var(--fa-color-blue-50); /** Info background — info message container fill, tip banners */
|
||||||
|
--fa-spacing-component-xs: var(--fa-spacing-1); /** 4px — tight gaps: icon margins, chip internal padding */
|
||||||
|
--fa-spacing-component-sm: var(--fa-spacing-2); /** 8px — small padding: badge padding, inline element gaps */
|
||||||
|
--fa-spacing-component-md: var(--fa-spacing-4); /** 16px — default padding: button padding, input padding, form field gap */
|
||||||
|
--fa-spacing-component-lg: var(--fa-spacing-6); /** 24px — large padding: card padding (desktop), modal padding */
|
||||||
|
--fa-spacing-layout-gutter: var(--fa-spacing-4); /** 16px — grid gutter on mobile, card grid gap */
|
||||||
|
--fa-spacing-layout-gutter-desktop: var(--fa-spacing-6); /** 24px — grid gutter on desktop */
|
||||||
|
--fa-spacing-layout-section: var(--fa-spacing-12); /** 48px — vertical gap between page sections */
|
||||||
|
--fa-spacing-layout-page: var(--fa-spacing-16); /** 64px — major page section separation, hero spacing */
|
||||||
|
--fa-spacing-layout-page-padding: var(--fa-spacing-4); /** 16px — horizontal page padding on mobile */
|
||||||
|
--fa-spacing-layout-page-padding-desktop: var(--fa-spacing-8); /** 32px — horizontal page padding on desktop */
|
||||||
|
--fa-typography-display-hero-font-family: var(--fa-font-family-display);
|
||||||
|
--fa-typography-display-hero-font-size: var(--fa-font-size-display-hero); /** 80px desktop */
|
||||||
|
--fa-typography-display-hero-font-size-mobile: var(--fa-font-size-mobile-display-hero); /** 32px mobile */
|
||||||
|
--fa-typography-display-hero-font-weight: var(--fa-font-weight-regular); /** 400 — serif carries inherent weight at large sizes */
|
||||||
|
--fa-typography-display1-font-family: var(--fa-font-family-display);
|
||||||
|
--fa-typography-display1-font-size: var(--fa-font-size-display-1); /** 64px desktop */
|
||||||
|
--fa-typography-display1-font-size-mobile: var(--fa-font-size-mobile-display1); /** 28px mobile */
|
||||||
|
--fa-typography-display1-font-weight: var(--fa-font-weight-regular);
|
||||||
|
--fa-typography-display2-font-family: var(--fa-font-family-display);
|
||||||
|
--fa-typography-display2-font-size: var(--fa-font-size-display-2); /** 52px desktop */
|
||||||
|
--fa-typography-display2-font-size-mobile: var(--fa-font-size-mobile-display2); /** 24px mobile */
|
||||||
|
--fa-typography-display2-font-weight: var(--fa-font-weight-regular);
|
||||||
|
--fa-typography-display3-font-family: var(--fa-font-family-display);
|
||||||
|
--fa-typography-display3-font-size: var(--fa-font-size-display-3); /** 40px desktop */
|
||||||
|
--fa-typography-display3-font-size-mobile: var(--fa-font-size-mobile-display3); /** 22px mobile */
|
||||||
|
--fa-typography-display3-font-weight: var(--fa-font-weight-regular);
|
||||||
|
--fa-typography-display-sm-font-family: var(--fa-font-family-display);
|
||||||
|
--fa-typography-display-sm-font-size: var(--fa-font-size-display-sm); /** 32px desktop */
|
||||||
|
--fa-typography-display-sm-font-size-mobile: var(--fa-font-size-mobile-display-sm); /** 20px mobile */
|
||||||
|
--fa-typography-display-sm-font-weight: var(--fa-font-weight-regular);
|
||||||
|
--fa-typography-h1-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-h1-font-size: var(--fa-font-size-3xl); /** 36px desktop */
|
||||||
|
--fa-typography-h1-font-size-mobile: var(--fa-font-size-mobile-h1); /** 26px mobile */
|
||||||
|
--fa-typography-h1-font-weight: var(--fa-font-weight-bold);
|
||||||
|
--fa-typography-h2-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-h2-font-size: var(--fa-font-size-2xl); /** 30px desktop */
|
||||||
|
--fa-typography-h2-font-size-mobile: var(--fa-font-size-mobile-h2); /** 22px mobile */
|
||||||
|
--fa-typography-h2-font-weight: var(--fa-font-weight-bold);
|
||||||
|
--fa-typography-h3-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-h3-font-size: var(--fa-font-size-xl); /** 24px desktop */
|
||||||
|
--fa-typography-h3-font-size-mobile: var(--fa-font-size-mobile-h3); /** 20px mobile */
|
||||||
|
--fa-typography-h3-font-weight: var(--fa-font-weight-bold);
|
||||||
|
--fa-typography-h4-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-h4-font-size: var(--fa-font-size-lg); /** 20px desktop */
|
||||||
|
--fa-typography-h4-font-size-mobile: var(--fa-font-size-mobile-h4); /** 18px mobile */
|
||||||
|
--fa-typography-h4-font-weight: var(--fa-font-weight-bold);
|
||||||
|
--fa-typography-h5-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-h5-font-size: var(--fa-font-size-md); /** 18px desktop */
|
||||||
|
--fa-typography-h5-font-size-mobile: var(--fa-font-size-mobile-h5); /** 16px mobile */
|
||||||
|
--fa-typography-h5-font-weight: var(--fa-font-weight-bold);
|
||||||
|
--fa-typography-h6-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-h6-font-size: var(--fa-font-size-base); /** 16px desktop */
|
||||||
|
--fa-typography-h6-font-size-mobile: var(--fa-font-size-mobile-h6); /** 14px mobile */
|
||||||
|
--fa-typography-h6-font-weight: var(--fa-font-weight-bold);
|
||||||
|
--fa-typography-body-lg-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-body-lg-font-size: var(--fa-font-size-md); /** 18px */
|
||||||
|
--fa-typography-body-lg-font-weight: var(--fa-font-weight-medium);
|
||||||
|
--fa-typography-body-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-body-font-size: var(--fa-font-size-base); /** 16px */
|
||||||
|
--fa-typography-body-font-weight: var(--fa-font-weight-medium);
|
||||||
|
--fa-typography-body-sm-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-body-sm-font-size: var(--fa-font-size-sm); /** 14px */
|
||||||
|
--fa-typography-body-sm-font-weight: var(--fa-font-weight-medium);
|
||||||
|
--fa-typography-body-xs-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-body-xs-font-size: var(--fa-font-size-xs); /** 12px */
|
||||||
|
--fa-typography-body-xs-font-weight: var(--fa-font-weight-medium);
|
||||||
|
--fa-typography-label-lg-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-label-lg-font-size: var(--fa-font-size-base); /** 16px */
|
||||||
|
--fa-typography-label-lg-font-weight: var(--fa-font-weight-medium);
|
||||||
|
--fa-typography-label-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-label-font-size: var(--fa-font-size-sm); /** 14px */
|
||||||
|
--fa-typography-label-font-weight: var(--fa-font-weight-medium);
|
||||||
|
--fa-typography-label-sm-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-label-sm-font-size: var(--fa-font-size-xs); /** 12px */
|
||||||
|
--fa-typography-label-sm-font-weight: var(--fa-font-weight-medium);
|
||||||
|
--fa-typography-caption-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-caption-font-size: var(--fa-font-size-xs); /** 12px */
|
||||||
|
--fa-typography-caption-font-weight: var(--fa-font-weight-regular);
|
||||||
|
--fa-typography-caption-sm-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-caption-sm-font-size: var(--fa-font-size-2xs); /** 11px — accessibility floor */
|
||||||
|
--fa-typography-caption-sm-font-weight: var(--fa-font-weight-regular);
|
||||||
|
--fa-typography-overline-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-overline-font-size: var(--fa-font-size-xs); /** 12px */
|
||||||
|
--fa-typography-overline-font-weight: var(--fa-font-weight-semibold);
|
||||||
|
--fa-typography-overline-sm-font-family: var(--fa-font-family-body);
|
||||||
|
--fa-typography-overline-sm-font-size: var(--fa-font-size-2xs); /** 11px — accessibility floor */
|
||||||
|
--fa-typography-overline-sm-font-weight: var(--fa-font-weight-semibold);
|
||||||
|
}
|
||||||
377
src/theme/generated/tokens.js
Normal file
377
src/theme/generated/tokens.js
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
/**
|
||||||
|
* Do not edit directly, this file was auto-generated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ButtonHeightXs = "28px"; // Extra-small — compact text buttons, inline actions
|
||||||
|
export const ButtonHeightSm = "32px"; // Small — secondary actions, toolbar buttons
|
||||||
|
export const ButtonHeightMd = "40px"; // Medium — default size, form submissions
|
||||||
|
export const ButtonHeightLg = "48px"; // Large — primary CTAs, mobile touch targets (meets 44px minimum)
|
||||||
|
export const ButtonPaddingXXs = "8px"; // 8px — compact horizontal padding
|
||||||
|
export const ButtonPaddingXSm = "12px"; // 12px — small horizontal padding
|
||||||
|
export const ButtonPaddingXMd = "16px"; // 16px — default horizontal padding
|
||||||
|
export const ButtonPaddingXLg = "24px"; // 24px — generous CTA horizontal padding
|
||||||
|
export const ButtonPaddingYXs = "4px"; // 4px — compact vertical padding
|
||||||
|
export const ButtonPaddingYSm = "4px"; // 4px — small vertical padding
|
||||||
|
export const ButtonPaddingYMd = "8px"; // 8px — default vertical padding
|
||||||
|
export const ButtonPaddingYLg = "12px"; // 12px — generous CTA vertical padding
|
||||||
|
export const ButtonFontSizeXs = "0.75rem"; // 12px — extra-small button text
|
||||||
|
export const ButtonFontSizeSm = "0.875rem"; // 14px — small button text
|
||||||
|
export const ButtonFontSizeMd = "0.875rem"; // 14px — default button text
|
||||||
|
export const ButtonFontSizeLg = "1rem"; // 16px — large button text
|
||||||
|
export const ButtonIconSizeXs = "14px"; // 14px icons in extra-small buttons
|
||||||
|
export const ButtonIconSizeSm = "16px"; // 16px icons in small buttons
|
||||||
|
export const ButtonIconSizeMd = "18px"; // 18px icons in medium buttons
|
||||||
|
export const ButtonIconSizeLg = "20px"; // 20px icons in large buttons
|
||||||
|
export const ButtonIconGapXs = "4px"; // 4px icon-text gap
|
||||||
|
export const ButtonIconGapSm = "4px"; // 4px icon-text gap
|
||||||
|
export const ButtonIconGapMd = "8px"; // 8px icon-text gap
|
||||||
|
export const ButtonIconGapLg = "8px"; // 8px icon-text gap
|
||||||
|
export const ButtonBorderRadiusDefault = "8px"; // 8px — standard button rounding
|
||||||
|
export const InputHeightSm = "40px"; // Small — compact forms, admin layouts, matches Button medium height
|
||||||
|
export const InputHeightMd = "48px"; // Medium (default) — standard forms, matches Button large for alignment
|
||||||
|
export const InputPaddingXDefault = "12px"; // 12px — inner horizontal padding matching Figma design
|
||||||
|
export const InputPaddingYSm = "8px"; // 8px — compact vertical padding for small size
|
||||||
|
export const InputPaddingYMd = "12px"; // 12px — standard vertical padding for medium size
|
||||||
|
export const InputFontSizeDefault = "1rem"; // 16px — prevents iOS auto-zoom on focus, matches Figma
|
||||||
|
export const InputBorderRadiusDefault = "4px"; // 4px — subtle rounding, consistent with Figma design
|
||||||
|
export const InputGapDefault = "8px"; // 8px — vertical rhythm between label/input/helper, slightly more generous than Figma's 6px for readability
|
||||||
|
export const InputIconSizeDefault = "20px"; // 20px — icon size inside input field, matches Figma trailing icon
|
||||||
|
export const ColorBrand50 = "#fef9f5"; // Lightest warm tint — warm section backgrounds
|
||||||
|
export const ColorBrand100 = "#f7ecdf"; // Light warm — hover backgrounds, subtle fills
|
||||||
|
export const ColorBrand200 = "#ebdac8"; // Warm light — secondary backgrounds, divider tones
|
||||||
|
export const ColorBrand300 = "#d8c3b5"; // Warm beige — from brand swatch. Surface warmth, card tints
|
||||||
|
export const ColorBrand400 = "#d0a070"; // Mid gold — from brand swatch. Secondary interactive, step indicators
|
||||||
|
export const ColorBrand500 = "#ba834e"; // Base brand gold — from brand swatch. Primary CTA colour, main interactive. 3.7:1 contrast on white
|
||||||
|
export const ColorBrand600 = "#b0610f"; // Rich copper — from brand swatch. Hover/emphasis on brand elements. 4.8:1 contrast on white
|
||||||
|
export const ColorBrand700 = "#8b4e0d"; // Deep copper — active states, strong brand text on light backgrounds. 6.7:1 contrast on white
|
||||||
|
export const ColorBrand800 = "#6b3c13"; // Dark brown — bold brand accents, high-contrast brand text
|
||||||
|
export const ColorBrand900 = "#51301b"; // Chocolate — from brand swatch. Deep emphasis, dark brand surfaces
|
||||||
|
export const ColorBrand950 = "#251913"; // Espresso — from brand swatch. Darkest brand tone, near-black warm
|
||||||
|
export const ColorSage50 = "#f2f5f6"; // Lightest sage — subtle cool backgrounds
|
||||||
|
export const ColorSage100 = "#e3eaeb"; // Light sage — hover states on cool surfaces
|
||||||
|
export const ColorSage200 = "#d7e1e2"; // From brand swatch — light cool surface, soft borders
|
||||||
|
export const ColorSage300 = "#c8d4d6"; // Mid-light sage — dividers, secondary borders
|
||||||
|
export const ColorSage400 = "#b9c7c9"; // From brand swatch — mid sage, secondary text on dark backgrounds
|
||||||
|
export const ColorSage500 = "#8ea2a7"; // Base sage — secondary content, muted icons
|
||||||
|
export const ColorSage600 = "#687d84"; // Dark sage — secondary text, subtle icons
|
||||||
|
export const ColorSage700 = "#4c5b6b"; // From brand swatch — strong secondary, dark accents. 6.1:1 contrast on white
|
||||||
|
export const ColorSage800 = "#4c5459"; // From brand swatch — near-dark grey, supplementary text. 6.7:1 contrast on white
|
||||||
|
export const ColorSage900 = "#343c40"; // Very dark sage — high-contrast secondary elements
|
||||||
|
export const ColorSage950 = "#1e2528"; // Near-black cool — darkest secondary tone
|
||||||
|
export const ColorNeutral50 = "#fafafa"; // Lightest neutral — default page background alternative
|
||||||
|
export const ColorNeutral100 = "#f5f5f5"; // Light neutral — subtle background differentiation
|
||||||
|
export const ColorNeutral200 = "#e8e8e8"; // Light grey — standard borders, dividers
|
||||||
|
export const ColorNeutral300 = "#d4d4d4"; // Mid-light grey — disabled borders, subtle separators
|
||||||
|
export const ColorNeutral400 = "#a3a3a3"; // Mid grey — placeholder text, disabled content
|
||||||
|
export const ColorNeutral500 = "#737373"; // Base grey — secondary body text, icons
|
||||||
|
export const ColorNeutral600 = "#525252"; // Dark grey — body text, labels. 7.1:1 contrast on white
|
||||||
|
export const ColorNeutral700 = "#404040"; // Strong grey — headings, emphasis text. 9.7:1 contrast on white
|
||||||
|
export const ColorNeutral800 = "#2c2e35"; // From brand swatch — charcoal with cool tint. Primary text colour. 13.2:1 contrast on white
|
||||||
|
export const ColorNeutral900 = "#1a1a1c"; // Near-black — maximum contrast text
|
||||||
|
export const ColorNeutral950 = "#0a0a0b"; // Deepest neutral — use sparingly
|
||||||
|
export const ColorRed50 = "#fef2f2"; // Error tint — error message backgrounds
|
||||||
|
export const ColorRed100 = "#fde8e8"; // Light error — hover on error surfaces
|
||||||
|
export const ColorRed200 = "#f9bfbf"; // Light red — error borders, subtle indicators
|
||||||
|
export const ColorRed300 = "#f09898"; // Mid-light red — error icon backgrounds
|
||||||
|
export const ColorRed400 = "#e56b6b"; // Mid red — error indicators, badges
|
||||||
|
export const ColorRed500 = "#d64545"; // Base red — form validation errors, alert accents
|
||||||
|
export const ColorRed600 = "#bc2f2f"; // Strong red — error text on light backgrounds. 5.7:1 contrast on white
|
||||||
|
export const ColorRed700 = "#9b2424"; // Dark red — error headings, strong alerts
|
||||||
|
export const ColorRed800 = "#7a1d1d"; // Deep red — high-contrast error emphasis
|
||||||
|
export const ColorRed900 = "#5c1616"; // Very dark red — use sparingly
|
||||||
|
export const ColorRed950 = "#3d0e0e"; // Darkest red
|
||||||
|
export const ColorAmber50 = "#fff9eb"; // Warning tint — warning message backgrounds
|
||||||
|
export const ColorAmber100 = "#fff0cc"; // Light amber — hover on warning surfaces
|
||||||
|
export const ColorAmber200 = "#ffe099"; // Light amber — warning borders
|
||||||
|
export const ColorAmber300 = "#ffcc66"; // Mid-light amber — warning icon backgrounds
|
||||||
|
export const ColorAmber400 = "#ffb833"; // Mid amber — warning badges, price alerts
|
||||||
|
export const ColorAmber500 = "#f5a000"; // Base amber — warning accents
|
||||||
|
export const ColorAmber600 = "#cc8500"; // Strong amber — warning text. 3.6:1 contrast on white (large text AA)
|
||||||
|
export const ColorAmber700 = "#a36b00"; // Dark amber — warning headings. 5.1:1 contrast on white
|
||||||
|
export const ColorAmber800 = "#7a5000"; // Deep amber — high-contrast warning emphasis
|
||||||
|
export const ColorAmber900 = "#523600"; // Very dark amber — use sparingly
|
||||||
|
export const ColorAmber950 = "#331f00"; // Darkest amber
|
||||||
|
export const ColorGreen50 = "#f0f7f0"; // Success tint — success message backgrounds
|
||||||
|
export const ColorGreen100 = "#d8ecd8"; // Light green — hover on success surfaces
|
||||||
|
export const ColorGreen200 = "#b8d8b8"; // Light green — success borders
|
||||||
|
export const ColorGreen300 = "#8dc08d"; // Mid-light green — success icon backgrounds
|
||||||
|
export const ColorGreen400 = "#66a866"; // Mid green — success badges, completion indicators
|
||||||
|
export const ColorGreen500 = "#4a8f4a"; // Base green — success accents, completed steps
|
||||||
|
export const ColorGreen600 = "#3b7a3b"; // Strong green — success text on light backgrounds. 4.8:1 contrast on white
|
||||||
|
export const ColorGreen700 = "#2e6b2e"; // Dark green — success headings
|
||||||
|
export const ColorGreen800 = "#235523"; // Deep green — high-contrast success emphasis
|
||||||
|
export const ColorGreen900 = "#1a3f1a"; // Very dark green — use sparingly
|
||||||
|
export const ColorGreen950 = "#0f2a0f"; // Darkest green
|
||||||
|
export const ColorBlue50 = "#eff6ff"; // Info tint — info message backgrounds
|
||||||
|
export const ColorBlue100 = "#dbeafe"; // Light blue — hover on info surfaces
|
||||||
|
export const ColorBlue200 = "#bfdbfe"; // Light blue — info borders
|
||||||
|
export const ColorBlue300 = "#93c5fd"; // Mid-light blue — info icon backgrounds
|
||||||
|
export const ColorBlue400 = "#60a5fa"; // Mid blue — info badges
|
||||||
|
export const ColorBlue500 = "#3b82f6"; // Base blue — info accents, supplementary links
|
||||||
|
export const ColorBlue600 = "#2563eb"; // Strong blue — info text on light backgrounds. 4.6:1 contrast on white
|
||||||
|
export const ColorBlue700 = "#1d4ed8"; // Dark blue — info headings
|
||||||
|
export const ColorBlue800 = "#1e40af"; // Deep blue — high-contrast info emphasis
|
||||||
|
export const ColorBlue900 = "#1e3a8a"; // Very dark blue — use sparingly
|
||||||
|
export const ColorBlue950 = "#172554"; // Darkest blue
|
||||||
|
export const ColorWhite = "#ffffff"; // Pure white — card backgrounds, inverse text, primary surface
|
||||||
|
export const ColorBlack = "#000000"; // Pure black — from brand swatch. Use sparingly; prefer neutral.800-900 for text
|
||||||
|
export const ColorTextPrimary = "#2c2e35"; // Primary text — body content, headings. Cool charcoal (#2C2E35) for comfortable extended reading
|
||||||
|
export const ColorTextSecondary = "#525252"; // Secondary text — helper text, descriptions, metadata, less prominent content
|
||||||
|
export const ColorTextTertiary = "#737373"; // Tertiary text — placeholders, timestamps, attribution, meta information
|
||||||
|
export const ColorTextDisabled = "#a3a3a3"; // Disabled text — clearly diminished but still readable for accessibility
|
||||||
|
export const ColorTextInverse = "#ffffff"; // Inverse text — white text on dark or coloured backgrounds (buttons, banners)
|
||||||
|
export const ColorTextBrand = "#b0610f"; // Brand-coloured text — links, inline brand emphasis. Copper tone meets AA on white (4.8:1)
|
||||||
|
export const ColorTextError = "#bc2f2f"; // Error text — form validation messages, error descriptions
|
||||||
|
export const ColorTextSuccess = "#3b7a3b"; // Success text — confirmation messages, positive feedback
|
||||||
|
export const ColorTextWarning = "#a36b00"; // Warning text — cautionary messages. Uses amber.700 for WCAG AA compliance on white (5.1:1)
|
||||||
|
export const ColorSurfaceDefault = "#ffffff"; // Default surface — main page background, card faces
|
||||||
|
export const ColorSurfaceSubtle = "#fafafa"; // Subtle surface — slight differentiation from default, alternate row backgrounds
|
||||||
|
export const ColorSurfaceRaised = "#ffffff"; // Raised surface — cards, elevated containers (distinguished by shadow rather than colour)
|
||||||
|
export const ColorSurfaceWarm = "#fef9f5"; // Warm surface — brand-tinted sections, promotional areas, upsell cards like 'Protect your plan'
|
||||||
|
export const ColorSurfaceCool = "#f2f5f6"; // Cool surface — calming sections, information panels, FAQ backgrounds
|
||||||
|
export const ColorSurfaceOverlay = "#00000080"; // Overlay surface — modal/dialog backdrop at 50% black
|
||||||
|
export const ColorBorderDefault = "#e8e8e8"; // Default border — cards, containers, resting input borders
|
||||||
|
export const ColorBorderStrong = "#a3a3a3"; // Strong border — emphasis borders, active input borders
|
||||||
|
export const ColorBorderSubtle = "#f5f5f5"; // Subtle border — section dividers, soft separators
|
||||||
|
export const ColorBorderBrand = "#ba834e"; // Brand border — focused inputs, selected cards, brand-accented containers
|
||||||
|
export const ColorBorderError = "#d64545"; // Error border — form fields with validation errors
|
||||||
|
export const ColorBorderSuccess = "#4a8f4a"; // Success border — validated fields, confirmed selections
|
||||||
|
export const ColorInteractiveDefault = "#b0610f"; // Default interactive — primary button fill, link colour, checkbox accent. Uses brand.600 (copper) for WCAG AA compliance (4.6:1 on white)
|
||||||
|
export const ColorInteractiveHover = "#8b4e0d"; // Hover state — deepened copper on hover for clear visual feedback
|
||||||
|
export const ColorInteractiveActive = "#6b3c13"; // Active/pressed state — dark brown during click/tap
|
||||||
|
export const ColorInteractiveDisabled = "#d4d4d4"; // Disabled interactive — muted grey, no pointer events
|
||||||
|
export const ColorInteractiveFocus = "#b0610f"; // Focus ring colour — keyboard navigation indicator, 2px outline with 2px offset
|
||||||
|
export const ColorInteractiveSecondary = "#4c5b6b"; // Secondary interactive — grey/sage buttons, less prominent actions
|
||||||
|
export const ColorInteractiveSecondaryHover = "#4c5459"; // Secondary interactive hover — darkened sage on hover
|
||||||
|
export const ColorFeedbackSuccess = "#3b7a3b"; // Success — confirmations, completed arrangement steps, booking confirmed
|
||||||
|
export const ColorFeedbackSuccessSubtle = "#f0f7f0"; // Success background — success message container fill, completion banners
|
||||||
|
export const ColorFeedbackWarning = "#cc8500"; // Warning — price change alerts, important notices, bond/insurance prompts
|
||||||
|
export const ColorFeedbackWarningSubtle = "#fff9eb"; // Warning background — warning message container fill, notice banners
|
||||||
|
export const ColorFeedbackError = "#bc2f2f"; // Error — form validation failures, system errors, payment issues
|
||||||
|
export const ColorFeedbackErrorSubtle = "#fef2f2"; // Error background — error message container fill, alert banners
|
||||||
|
export const ColorFeedbackInfo = "#2563eb"; // Info — helpful tips, supplementary information, guidance callouts
|
||||||
|
export const ColorFeedbackInfoSubtle = "#eff6ff"; // Info background — info message container fill, tip banners
|
||||||
|
export const ShadowSm = "0 1px 2px rgba(0,0,0,0.05)"; // Subtle lift — resting buttons, input focus subtle elevation
|
||||||
|
export const ShadowMd = "0 4px 6px rgba(0,0,0,0.07)"; // Medium elevation — cards at rest, dropdowns, popovers
|
||||||
|
export const ShadowLg = "0 10px 15px rgba(0,0,0,0.1)"; // High elevation — modals, popovers, card hover states
|
||||||
|
export const ShadowXl = "0 20px 25px rgba(0,0,0,0.1)"; // Maximum elevation — elevated panels, dialog boxes
|
||||||
|
export const OpacityDisabled = 0.4; // Disabled state — 40% opacity. Clearly diminished but still distinguishable
|
||||||
|
export const OpacityHover = 0.08; // Hover overlay — subtle 8% tint applied over backgrounds on hover
|
||||||
|
export const OpacityOverlay = 0.5; // Modal/dialog backdrop — 50% black overlay behind modals
|
||||||
|
export const Spacing1 = "4px"; // Tight — inline spacing, minimal gaps between related elements
|
||||||
|
export const Spacing2 = "8px"; // Small — related element gaps, compact padding, icon margins
|
||||||
|
export const Spacing3 = "12px"; // Component internal padding (small), chip padding
|
||||||
|
export const Spacing4 = "16px"; // Default component padding, form field gap, card grid gutter (mobile)
|
||||||
|
export const Spacing5 = "20px"; // Medium component spacing
|
||||||
|
export const Spacing6 = "24px"; // Card padding, section gap (small), card grid gutter (desktop)
|
||||||
|
export const Spacing8 = "32px"; // Section gap (medium), form section separation
|
||||||
|
export const Spacing10 = "40px"; // Section gap (large)
|
||||||
|
export const Spacing12 = "48px"; // Page section separation, vertical rhythm break
|
||||||
|
export const Spacing16 = "64px"; // Hero/banner vertical spacing
|
||||||
|
export const Spacing20 = "80px"; // Major page sections, large vertical spacing
|
||||||
|
export const Spacing05 = "2px"; // Hairline — icon-to-text tight spacing, fine adjustments
|
||||||
|
export const SpacingComponentXs = "4px"; // 4px — tight gaps: icon margins, chip internal padding
|
||||||
|
export const SpacingComponentSm = "8px"; // 8px — small padding: badge padding, inline element gaps
|
||||||
|
export const SpacingComponentMd = "16px"; // 16px — default padding: button padding, input padding, form field gap
|
||||||
|
export const SpacingComponentLg = "24px"; // 24px — large padding: card padding (desktop), modal padding
|
||||||
|
export const SpacingLayoutGutter = "16px"; // 16px — grid gutter on mobile, card grid gap
|
||||||
|
export const SpacingLayoutGutterDesktop = "24px"; // 24px — grid gutter on desktop
|
||||||
|
export const SpacingLayoutSection = "48px"; // 48px — vertical gap between page sections
|
||||||
|
export const SpacingLayoutPage = "64px"; // 64px — major page section separation, hero spacing
|
||||||
|
export const SpacingLayoutPagePadding = "16px"; // 16px — horizontal page padding on mobile
|
||||||
|
export const SpacingLayoutPagePaddingDesktop = "32px"; // 32px — horizontal page padding on desktop
|
||||||
|
export const BorderRadiusNone = "0px"; // Square corners — tables, dividers, sharp elements
|
||||||
|
export const BorderRadiusSm = "4px"; // Small radius — inputs, small interactive elements, chips
|
||||||
|
export const BorderRadiusMd = "8px"; // Medium radius — cards, buttons, dropdowns (default)
|
||||||
|
export const BorderRadiusLg = "12px"; // Large radius — modals, large cards
|
||||||
|
export const BorderRadiusXl = "16px"; // Extra large — feature cards, hero elements
|
||||||
|
export const BorderRadiusFull = "9999px"; // Full/pill — avatars, pill buttons, circular elements
|
||||||
|
export const FontFamilyBody =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif"; // Primary font — Montserrat. Used for headings (h1-h6), body text, labels, and all UI elements
|
||||||
|
export const FontFamilyDisplay =
|
||||||
|
"'Noto Serif SC', Georgia, 'Times New Roman', serif"; // Display font — Noto Serif SC. Elegant serif for hero/display text only. Adds warmth and gravitas at large sizes
|
||||||
|
export const FontFamilyMono =
|
||||||
|
"'JetBrains Mono', 'Fira Code', Consolas, monospace"; // Monospace font — for reference numbers (FA-2026-00847), tabular pricing data, and code
|
||||||
|
export const FontSize2xs = "0.6875rem"; // 11px — smallest UI text: compact captions, compact overlines
|
||||||
|
export const FontSizeXs = "0.75rem"; // 12px — small text: captions, labels, overlines, body/xs
|
||||||
|
export const FontSizeSm = "0.875rem"; // 14px — body small, labels, helper text
|
||||||
|
export const FontSizeBase = "1rem"; // 16px — default body text, heading/6, label/lg
|
||||||
|
export const FontSizeMd = "1.125rem"; // 18px — body large, heading/5
|
||||||
|
export const FontSizeLg = "1.25rem"; // 20px — heading/4
|
||||||
|
export const FontSizeXl = "1.5rem"; // 24px — heading/3
|
||||||
|
export const FontSize2xl = "1.875rem"; // 30px — heading/2
|
||||||
|
export const FontSize3xl = "2.25rem"; // 36px — heading/1
|
||||||
|
export const FontSize4xl = "3rem"; // 48px — reserved (legacy)
|
||||||
|
export const FontSizeDisplay1 = "4rem"; // 64px — display/1
|
||||||
|
export const FontSizeDisplay2 = "3.25rem"; // 52px — display/2
|
||||||
|
export const FontSizeDisplay3 = "2.5rem"; // 40px — display/3
|
||||||
|
export const FontSizeDisplaySm = "2rem"; // 32px — display/sm, smallest display text
|
||||||
|
export const FontSizeDisplayHero = "5rem"; // 80px — display/hero, largest display text
|
||||||
|
export const FontSizeMobileDisplayHero = "2rem"; // 32px — mobile display/hero (from 80px desktop)
|
||||||
|
export const FontSizeMobileDisplay1 = "1.75rem"; // 28px — mobile display/1 (from 64px desktop)
|
||||||
|
export const FontSizeMobileDisplay2 = "1.5rem"; // 24px — mobile display/2 (from 52px desktop)
|
||||||
|
export const FontSizeMobileDisplay3 = "1.375rem"; // 22px — mobile display/3 (from 40px desktop)
|
||||||
|
export const FontSizeMobileDisplaySm = "1.25rem"; // 20px — mobile display/sm (from 32px desktop)
|
||||||
|
export const FontSizeMobileH1 = "1.625rem"; // 26px — mobile heading/1 (from 36px desktop)
|
||||||
|
export const FontSizeMobileH2 = "1.375rem"; // 22px — mobile heading/2 (from 30px desktop)
|
||||||
|
export const FontSizeMobileH3 = "1.25rem"; // 20px — mobile heading/3 (from 24px desktop)
|
||||||
|
export const FontSizeMobileH4 = "1.125rem"; // 18px — mobile heading/4 (from 20px desktop)
|
||||||
|
export const FontSizeMobileH5 = "1rem"; // 16px — mobile heading/5 (from 18px desktop)
|
||||||
|
export const FontSizeMobileH6 = "0.875rem"; // 14px — mobile heading/6 (from 16px desktop)
|
||||||
|
export const FontWeightRegular = 400; // Regular weight — captions, display text (serif carries inherent weight)
|
||||||
|
export const FontWeightMedium = 500; // Medium weight — body text, labels. Slightly bolder than regular for improved readability
|
||||||
|
export const FontWeightSemibold = 600; // Semibold — overlines, button text, navigation
|
||||||
|
export const FontWeightBold = 700; // Bold — all headings (h1-h6)
|
||||||
|
export const LineHeightTight = 1.25; // Tight leading — large headings, display text
|
||||||
|
export const LineHeightSnug = 1.375; // Snug leading — sub-headings, labels, small text
|
||||||
|
export const LineHeightNormal = 1.5; // Normal leading — default body text, optimal readability
|
||||||
|
export const LineHeightRelaxed = 1.75; // Relaxed leading — large body text, long-form content
|
||||||
|
export const LetterSpacingTighter = "-0.02em"; // Tighter tracking — large display text
|
||||||
|
export const LetterSpacingTight = "-0.01em"; // Tight tracking — headings
|
||||||
|
export const LetterSpacingNormal = "0em"; // Normal tracking — body text, most content
|
||||||
|
export const LetterSpacingWide = "0.02em"; // Wide tracking — captions, small text
|
||||||
|
export const LetterSpacingWider = "0.05em"; // Wider tracking — labels, UI text
|
||||||
|
export const LetterSpacingWidest = "0.08em"; // Widest tracking — overlines, uppercase text
|
||||||
|
export const TypographyDisplayHeroFontFamily =
|
||||||
|
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||||
|
export const TypographyDisplayHeroFontSize = "5rem"; // 80px desktop
|
||||||
|
export const TypographyDisplayHeroFontSizeMobile = "2rem"; // 32px mobile
|
||||||
|
export const TypographyDisplayHeroFontWeight = 400; // 400 — serif carries inherent weight at large sizes
|
||||||
|
export const TypographyDisplayHeroLineHeight = 1.05;
|
||||||
|
export const TypographyDisplayHeroLetterSpacing = "-1.5px";
|
||||||
|
export const TypographyDisplay1FontFamily =
|
||||||
|
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||||
|
export const TypographyDisplay1FontSize = "4rem"; // 64px desktop
|
||||||
|
export const TypographyDisplay1FontSizeMobile = "1.75rem"; // 28px mobile
|
||||||
|
export const TypographyDisplay1FontWeight = 400;
|
||||||
|
export const TypographyDisplay1LineHeight = 1.078;
|
||||||
|
export const TypographyDisplay1LetterSpacing = "-1px";
|
||||||
|
export const TypographyDisplay2FontFamily =
|
||||||
|
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||||
|
export const TypographyDisplay2FontSize = "3.25rem"; // 52px desktop
|
||||||
|
export const TypographyDisplay2FontSizeMobile = "1.5rem"; // 24px mobile
|
||||||
|
export const TypographyDisplay2FontWeight = 400;
|
||||||
|
export const TypographyDisplay2LineHeight = 1.096;
|
||||||
|
export const TypographyDisplay2LetterSpacing = "-0.5px";
|
||||||
|
export const TypographyDisplay3FontFamily =
|
||||||
|
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||||
|
export const TypographyDisplay3FontSize = "2.5rem"; // 40px desktop
|
||||||
|
export const TypographyDisplay3FontSizeMobile = "1.375rem"; // 22px mobile
|
||||||
|
export const TypographyDisplay3FontWeight = 400;
|
||||||
|
export const TypographyDisplay3LineHeight = 1.15;
|
||||||
|
export const TypographyDisplay3LetterSpacing = "-0.25px";
|
||||||
|
export const TypographyDisplaySmFontFamily =
|
||||||
|
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||||
|
export const TypographyDisplaySmFontSize = "2rem"; // 32px desktop
|
||||||
|
export const TypographyDisplaySmFontSizeMobile = "1.25rem"; // 20px mobile
|
||||||
|
export const TypographyDisplaySmFontWeight = 400;
|
||||||
|
export const TypographyDisplaySmLineHeight = 1.1875;
|
||||||
|
export const TypographyDisplaySmLetterSpacing = "0px";
|
||||||
|
export const TypographyH1FontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyH1FontSize = "2.25rem"; // 36px desktop
|
||||||
|
export const TypographyH1FontSizeMobile = "1.625rem"; // 26px mobile
|
||||||
|
export const TypographyH1FontWeight = 700;
|
||||||
|
export const TypographyH1LineHeight = 1.194;
|
||||||
|
export const TypographyH1LetterSpacing = "-0.5px";
|
||||||
|
export const TypographyH2FontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyH2FontSize = "1.875rem"; // 30px desktop
|
||||||
|
export const TypographyH2FontSizeMobile = "1.375rem"; // 22px mobile
|
||||||
|
export const TypographyH2FontWeight = 700;
|
||||||
|
export const TypographyH2LineHeight = 1.267;
|
||||||
|
export const TypographyH2LetterSpacing = "-0.25px";
|
||||||
|
export const TypographyH3FontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyH3FontSize = "1.5rem"; // 24px desktop
|
||||||
|
export const TypographyH3FontSizeMobile = "1.25rem"; // 20px mobile
|
||||||
|
export const TypographyH3FontWeight = 700;
|
||||||
|
export const TypographyH3LineHeight = 1.292;
|
||||||
|
export const TypographyH3LetterSpacing = "0px";
|
||||||
|
export const TypographyH4FontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyH4FontSize = "1.25rem"; // 20px desktop
|
||||||
|
export const TypographyH4FontSizeMobile = "1.125rem"; // 18px mobile
|
||||||
|
export const TypographyH4FontWeight = 700;
|
||||||
|
export const TypographyH4LineHeight = 1.35;
|
||||||
|
export const TypographyH4LetterSpacing = "0px";
|
||||||
|
export const TypographyH5FontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyH5FontSize = "1.125rem"; // 18px desktop
|
||||||
|
export const TypographyH5FontSizeMobile = "1rem"; // 16px mobile
|
||||||
|
export const TypographyH5FontWeight = 700;
|
||||||
|
export const TypographyH5LineHeight = 1.389;
|
||||||
|
export const TypographyH5LetterSpacing = "0px";
|
||||||
|
export const TypographyH6FontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyH6FontSize = "1rem"; // 16px desktop
|
||||||
|
export const TypographyH6FontSizeMobile = "0.875rem"; // 14px mobile
|
||||||
|
export const TypographyH6FontWeight = 700;
|
||||||
|
export const TypographyH6LineHeight = 1.375;
|
||||||
|
export const TypographyH6LetterSpacing = "0px";
|
||||||
|
export const TypographyBodyLgFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyBodyLgFontSize = "1.125rem"; // 18px
|
||||||
|
export const TypographyBodyLgFontWeight = 500;
|
||||||
|
export const TypographyBodyLgLineHeight = 1.611;
|
||||||
|
export const TypographyBodyLgLetterSpacing = "0px";
|
||||||
|
export const TypographyBodyFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyBodyFontSize = "1rem"; // 16px
|
||||||
|
export const TypographyBodyFontWeight = 500;
|
||||||
|
export const TypographyBodyLineHeight = 1.625;
|
||||||
|
export const TypographyBodyLetterSpacing = "0px";
|
||||||
|
export const TypographyBodySmFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyBodySmFontSize = "0.875rem"; // 14px
|
||||||
|
export const TypographyBodySmFontWeight = 500;
|
||||||
|
export const TypographyBodySmLineHeight = 1.571;
|
||||||
|
export const TypographyBodySmLetterSpacing = "0px";
|
||||||
|
export const TypographyBodyXsFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyBodyXsFontSize = "0.75rem"; // 12px
|
||||||
|
export const TypographyBodyXsFontWeight = 500;
|
||||||
|
export const TypographyBodyXsLineHeight = 1.5;
|
||||||
|
export const TypographyBodyXsLetterSpacing = "0.1px";
|
||||||
|
export const TypographyLabelLgFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyLabelLgFontSize = "1rem"; // 16px
|
||||||
|
export const TypographyLabelLgFontWeight = 500;
|
||||||
|
export const TypographyLabelLgLineHeight = 1.3125;
|
||||||
|
export const TypographyLabelLgLetterSpacing = "0.1px";
|
||||||
|
export const TypographyLabelFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyLabelFontSize = "0.875rem"; // 14px
|
||||||
|
export const TypographyLabelFontWeight = 500;
|
||||||
|
export const TypographyLabelLineHeight = 1.286;
|
||||||
|
export const TypographyLabelLetterSpacing = "0.15px";
|
||||||
|
export const TypographyLabelSmFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyLabelSmFontSize = "0.75rem"; // 12px
|
||||||
|
export const TypographyLabelSmFontWeight = 500;
|
||||||
|
export const TypographyLabelSmLineHeight = 1.333;
|
||||||
|
export const TypographyLabelSmLetterSpacing = "0.2px";
|
||||||
|
export const TypographyCaptionFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyCaptionFontSize = "0.75rem"; // 12px
|
||||||
|
export const TypographyCaptionFontWeight = 400;
|
||||||
|
export const TypographyCaptionLineHeight = 1.417;
|
||||||
|
export const TypographyCaptionLetterSpacing = "0.2px";
|
||||||
|
export const TypographyCaptionSmFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyCaptionSmFontSize = "0.6875rem"; // 11px — accessibility floor
|
||||||
|
export const TypographyCaptionSmFontWeight = 400;
|
||||||
|
export const TypographyCaptionSmLineHeight = 1.364;
|
||||||
|
export const TypographyCaptionSmLetterSpacing = "0.2px";
|
||||||
|
export const TypographyOverlineFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyOverlineFontSize = "0.75rem"; // 12px
|
||||||
|
export const TypographyOverlineFontWeight = 600;
|
||||||
|
export const TypographyOverlineLineHeight = 1.333;
|
||||||
|
export const TypographyOverlineLetterSpacing = "1.5px";
|
||||||
|
export const TypographyOverlineSmFontFamily =
|
||||||
|
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
export const TypographyOverlineSmFontSize = "0.6875rem"; // 11px — accessibility floor
|
||||||
|
export const TypographyOverlineSmFontWeight = 600;
|
||||||
|
export const TypographyOverlineSmLineHeight = 1.273;
|
||||||
|
export const TypographyOverlineSmLetterSpacing = "1.5px";
|
||||||
568
src/theme/index.ts
Normal file
568
src/theme/index.ts
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
/**
|
||||||
|
* FA Design System — MUI Theme
|
||||||
|
*
|
||||||
|
* Maps design tokens to MUI's theme structure. All values come from the
|
||||||
|
* token pipeline: Token JSON → Style Dictionary → generated/tokens.js → here.
|
||||||
|
*
|
||||||
|
* To update colours/typography/spacing: edit tokens/ JSON, run `npm run build:tokens`,
|
||||||
|
* then update references here if token names changed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import * as t from './generated/tokens.js';
|
||||||
|
|
||||||
|
/* ---------- Types ---------- */
|
||||||
|
|
||||||
|
type StyleWithMedia = CSSProperties & {
|
||||||
|
[key: `@media ${string}`]: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- Custom typography variant declarations ---------- */
|
||||||
|
|
||||||
|
declare module '@mui/material/styles' {
|
||||||
|
interface TypographyVariants {
|
||||||
|
displayHero: StyleWithMedia;
|
||||||
|
display1: StyleWithMedia;
|
||||||
|
display2: StyleWithMedia;
|
||||||
|
display3: StyleWithMedia;
|
||||||
|
displaySm: StyleWithMedia;
|
||||||
|
bodyLg: CSSProperties;
|
||||||
|
bodyXs: CSSProperties;
|
||||||
|
labelLg: CSSProperties;
|
||||||
|
label: CSSProperties;
|
||||||
|
labelSm: CSSProperties;
|
||||||
|
captionSm: CSSProperties;
|
||||||
|
overlineSm: CSSProperties;
|
||||||
|
}
|
||||||
|
interface TypographyVariantsOptions {
|
||||||
|
displayHero?: StyleWithMedia;
|
||||||
|
display1?: StyleWithMedia;
|
||||||
|
display2?: StyleWithMedia;
|
||||||
|
display3?: StyleWithMedia;
|
||||||
|
displaySm?: StyleWithMedia;
|
||||||
|
bodyLg?: CSSProperties;
|
||||||
|
bodyXs?: CSSProperties;
|
||||||
|
labelLg?: CSSProperties;
|
||||||
|
label?: CSSProperties;
|
||||||
|
labelSm?: CSSProperties;
|
||||||
|
captionSm?: CSSProperties;
|
||||||
|
overlineSm?: CSSProperties;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@mui/material/Typography' {
|
||||||
|
interface TypographyPropsVariantOverrides {
|
||||||
|
displayHero: true;
|
||||||
|
display1: true;
|
||||||
|
display2: true;
|
||||||
|
display3: true;
|
||||||
|
displaySm: true;
|
||||||
|
bodyLg: true;
|
||||||
|
bodyXs: true;
|
||||||
|
labelLg: true;
|
||||||
|
label: true;
|
||||||
|
labelSm: true;
|
||||||
|
captionSm: true;
|
||||||
|
overlineSm: true;
|
||||||
|
// Disable old aliases
|
||||||
|
display: false;
|
||||||
|
bodyLarge: false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Custom Button declarations ---------- */
|
||||||
|
|
||||||
|
declare module '@mui/material/Button' {
|
||||||
|
interface ButtonPropsSizeOverrides {
|
||||||
|
xs: true;
|
||||||
|
}
|
||||||
|
interface ButtonPropsVariantOverrides {
|
||||||
|
soft: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Theme ---------- */
|
||||||
|
|
||||||
|
const MOBILE = '@media (max-width:600px)';
|
||||||
|
|
||||||
|
export const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: t.ColorInteractiveDefault,
|
||||||
|
dark: t.ColorInteractiveActive,
|
||||||
|
light: t.ColorBrand400,
|
||||||
|
contrastText: t.ColorTextInverse,
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: t.ColorNeutral600,
|
||||||
|
dark: t.ColorNeutral700,
|
||||||
|
light: t.ColorNeutral300,
|
||||||
|
contrastText: t.ColorTextInverse,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
main: t.ColorFeedbackError,
|
||||||
|
light: t.ColorRed400,
|
||||||
|
dark: t.ColorRed800,
|
||||||
|
contrastText: t.ColorTextInverse,
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
main: t.ColorFeedbackWarning,
|
||||||
|
light: t.ColorAmber400,
|
||||||
|
dark: t.ColorAmber800,
|
||||||
|
contrastText: t.ColorTextInverse,
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: t.ColorFeedbackSuccess,
|
||||||
|
light: t.ColorGreen400,
|
||||||
|
dark: t.ColorGreen800,
|
||||||
|
contrastText: t.ColorTextInverse,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
main: t.ColorFeedbackInfo,
|
||||||
|
light: t.ColorBlue400,
|
||||||
|
dark: t.ColorBlue800,
|
||||||
|
contrastText: t.ColorTextInverse,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: t.ColorTextPrimary,
|
||||||
|
secondary: t.ColorTextSecondary,
|
||||||
|
disabled: t.ColorTextDisabled,
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: t.ColorSurfaceDefault,
|
||||||
|
paper: t.ColorSurfaceRaised,
|
||||||
|
},
|
||||||
|
divider: t.ColorBorderDefault,
|
||||||
|
action: {
|
||||||
|
disabled: t.ColorTextDisabled,
|
||||||
|
disabledBackground: t.ColorInteractiveDisabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
typography: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
|
||||||
|
/* ── Display (Noto Serif SC, Regular 400) ── */
|
||||||
|
displayHero: {
|
||||||
|
fontFamily: t.FontFamilyDisplay,
|
||||||
|
fontSize: t.TypographyDisplayHeroFontSize,
|
||||||
|
fontWeight: t.TypographyDisplayHeroFontWeight,
|
||||||
|
lineHeight: t.TypographyDisplayHeroLineHeight,
|
||||||
|
letterSpacing: t.TypographyDisplayHeroLetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyDisplayHeroFontSizeMobile },
|
||||||
|
},
|
||||||
|
display1: {
|
||||||
|
fontFamily: t.FontFamilyDisplay,
|
||||||
|
fontSize: t.TypographyDisplay1FontSize,
|
||||||
|
fontWeight: t.TypographyDisplay1FontWeight,
|
||||||
|
lineHeight: t.TypographyDisplay1LineHeight,
|
||||||
|
letterSpacing: t.TypographyDisplay1LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyDisplay1FontSizeMobile },
|
||||||
|
},
|
||||||
|
display2: {
|
||||||
|
fontFamily: t.FontFamilyDisplay,
|
||||||
|
fontSize: t.TypographyDisplay2FontSize,
|
||||||
|
fontWeight: t.TypographyDisplay2FontWeight,
|
||||||
|
lineHeight: t.TypographyDisplay2LineHeight,
|
||||||
|
letterSpacing: t.TypographyDisplay2LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyDisplay2FontSizeMobile },
|
||||||
|
},
|
||||||
|
display3: {
|
||||||
|
fontFamily: t.FontFamilyDisplay,
|
||||||
|
fontSize: t.TypographyDisplay3FontSize,
|
||||||
|
fontWeight: t.TypographyDisplay3FontWeight,
|
||||||
|
lineHeight: t.TypographyDisplay3LineHeight,
|
||||||
|
letterSpacing: t.TypographyDisplay3LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyDisplay3FontSizeMobile },
|
||||||
|
},
|
||||||
|
displaySm: {
|
||||||
|
fontFamily: t.FontFamilyDisplay,
|
||||||
|
fontSize: t.TypographyDisplaySmFontSize,
|
||||||
|
fontWeight: t.TypographyDisplaySmFontWeight,
|
||||||
|
lineHeight: t.TypographyDisplaySmLineHeight,
|
||||||
|
letterSpacing: t.TypographyDisplaySmLetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyDisplaySmFontSizeMobile },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Headings (Montserrat, Bold 700) ── */
|
||||||
|
h1: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyH1FontSize,
|
||||||
|
fontWeight: t.TypographyH1FontWeight,
|
||||||
|
lineHeight: t.TypographyH1LineHeight,
|
||||||
|
letterSpacing: t.TypographyH1LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyH1FontSizeMobile },
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyH2FontSize,
|
||||||
|
fontWeight: t.TypographyH2FontWeight,
|
||||||
|
lineHeight: t.TypographyH2LineHeight,
|
||||||
|
letterSpacing: t.TypographyH2LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyH2FontSizeMobile },
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyH3FontSize,
|
||||||
|
fontWeight: t.TypographyH3FontWeight,
|
||||||
|
lineHeight: t.TypographyH3LineHeight,
|
||||||
|
letterSpacing: t.TypographyH3LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyH3FontSizeMobile },
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyH4FontSize,
|
||||||
|
fontWeight: t.TypographyH4FontWeight,
|
||||||
|
lineHeight: t.TypographyH4LineHeight,
|
||||||
|
letterSpacing: t.TypographyH4LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyH4FontSizeMobile },
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyH5FontSize,
|
||||||
|
fontWeight: t.TypographyH5FontWeight,
|
||||||
|
lineHeight: t.TypographyH5LineHeight,
|
||||||
|
letterSpacing: t.TypographyH5LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyH5FontSizeMobile },
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyH6FontSize,
|
||||||
|
fontWeight: t.TypographyH6FontWeight,
|
||||||
|
lineHeight: t.TypographyH6LineHeight,
|
||||||
|
letterSpacing: t.TypographyH6LetterSpacing,
|
||||||
|
[MOBILE]: { fontSize: t.TypographyH6FontSizeMobile },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Body (Montserrat, Medium 500) ── */
|
||||||
|
bodyLg: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyBodyLgFontSize,
|
||||||
|
fontWeight: t.TypographyBodyLgFontWeight,
|
||||||
|
lineHeight: t.TypographyBodyLgLineHeight,
|
||||||
|
letterSpacing: t.TypographyBodyLgLetterSpacing,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: t.TypographyBodyFontSize,
|
||||||
|
fontWeight: t.TypographyBodyFontWeight,
|
||||||
|
lineHeight: t.TypographyBodyLineHeight,
|
||||||
|
letterSpacing: t.TypographyBodyLetterSpacing,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontSize: t.TypographyBodySmFontSize,
|
||||||
|
fontWeight: t.TypographyBodySmFontWeight,
|
||||||
|
lineHeight: t.TypographyBodySmLineHeight,
|
||||||
|
letterSpacing: t.TypographyBodySmLetterSpacing,
|
||||||
|
},
|
||||||
|
bodyXs: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyBodyXsFontSize,
|
||||||
|
fontWeight: t.TypographyBodyXsFontWeight,
|
||||||
|
lineHeight: t.TypographyBodyXsLineHeight,
|
||||||
|
letterSpacing: t.TypographyBodyXsLetterSpacing,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Subtitle (maps to body variants for MUI compat) ── */
|
||||||
|
subtitle1: {
|
||||||
|
fontSize: t.TypographyBodyLgFontSize,
|
||||||
|
fontWeight: t.TypographyBodyLgFontWeight,
|
||||||
|
lineHeight: t.TypographyBodyLgLineHeight,
|
||||||
|
},
|
||||||
|
subtitle2: {
|
||||||
|
fontSize: t.TypographyLabelFontSize,
|
||||||
|
fontWeight: t.TypographyLabelFontWeight,
|
||||||
|
lineHeight: t.TypographyLabelLineHeight,
|
||||||
|
letterSpacing: t.TypographyLabelLetterSpacing,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Label (Montserrat, Medium 500) ── */
|
||||||
|
labelLg: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyLabelLgFontSize,
|
||||||
|
fontWeight: t.TypographyLabelLgFontWeight,
|
||||||
|
lineHeight: t.TypographyLabelLgLineHeight,
|
||||||
|
letterSpacing: t.TypographyLabelLgLetterSpacing,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyLabelFontSize,
|
||||||
|
fontWeight: t.TypographyLabelFontWeight,
|
||||||
|
lineHeight: t.TypographyLabelLineHeight,
|
||||||
|
letterSpacing: t.TypographyLabelLetterSpacing,
|
||||||
|
},
|
||||||
|
labelSm: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyLabelSmFontSize,
|
||||||
|
fontWeight: t.TypographyLabelSmFontWeight,
|
||||||
|
lineHeight: t.TypographyLabelSmLineHeight,
|
||||||
|
letterSpacing: t.TypographyLabelSmLetterSpacing,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Caption (Montserrat, Regular 400) ── */
|
||||||
|
caption: {
|
||||||
|
fontSize: t.TypographyCaptionFontSize,
|
||||||
|
fontWeight: t.TypographyCaptionFontWeight,
|
||||||
|
lineHeight: t.TypographyCaptionLineHeight,
|
||||||
|
letterSpacing: t.TypographyCaptionLetterSpacing,
|
||||||
|
},
|
||||||
|
captionSm: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyCaptionSmFontSize,
|
||||||
|
fontWeight: t.TypographyCaptionSmFontWeight,
|
||||||
|
lineHeight: t.TypographyCaptionSmLineHeight,
|
||||||
|
letterSpacing: t.TypographyCaptionSmLetterSpacing,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Overline (Montserrat, SemiBold 600, uppercase) ── */
|
||||||
|
overline: {
|
||||||
|
fontSize: t.TypographyOverlineFontSize,
|
||||||
|
fontWeight: t.TypographyOverlineFontWeight,
|
||||||
|
lineHeight: t.TypographyOverlineLineHeight,
|
||||||
|
letterSpacing: t.TypographyOverlineLetterSpacing,
|
||||||
|
textTransform: 'uppercase' as const,
|
||||||
|
},
|
||||||
|
overlineSm: {
|
||||||
|
fontFamily: t.FontFamilyBody,
|
||||||
|
fontSize: t.TypographyOverlineSmFontSize,
|
||||||
|
fontWeight: t.TypographyOverlineSmFontWeight,
|
||||||
|
lineHeight: t.TypographyOverlineSmLineHeight,
|
||||||
|
letterSpacing: t.TypographyOverlineSmLetterSpacing,
|
||||||
|
textTransform: 'uppercase' as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Button text ── */
|
||||||
|
button: {
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: '0.02em',
|
||||||
|
textTransform: 'none' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
spacing: 4,
|
||||||
|
|
||||||
|
shape: {
|
||||||
|
borderRadius: parseInt(t.BorderRadiusMd, 10),
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
MuiButton: {
|
||||||
|
defaultProps: {
|
||||||
|
disableElevation: true,
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: parseInt(t.ButtonBorderRadiusDefault, 10),
|
||||||
|
textTransform: 'none' as const,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: '0.02em',
|
||||||
|
transition: 'background-color 150ms ease-in-out, border-color 150ms ease-in-out, box-shadow 150ms ease-in-out',
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: `2px solid ${t.ColorInteractiveFocus}`,
|
||||||
|
outlineOffset: '2px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sizeSmall: {
|
||||||
|
minHeight: parseInt(t.ButtonHeightSm, 10),
|
||||||
|
padding: `${t.ButtonPaddingYSm} ${t.ButtonPaddingXSm}`,
|
||||||
|
fontSize: t.ButtonFontSizeSm,
|
||||||
|
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapSm },
|
||||||
|
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapSm },
|
||||||
|
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||||
|
fontSize: t.ButtonIconSizeSm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sizeMedium: {
|
||||||
|
minHeight: parseInt(t.ButtonHeightMd, 10),
|
||||||
|
padding: `${t.ButtonPaddingYMd} ${t.ButtonPaddingXMd}`,
|
||||||
|
fontSize: t.ButtonFontSizeMd,
|
||||||
|
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapMd },
|
||||||
|
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapMd },
|
||||||
|
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||||
|
fontSize: t.ButtonIconSizeMd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sizeLarge: {
|
||||||
|
minHeight: parseInt(t.ButtonHeightLg, 10),
|
||||||
|
padding: `${t.ButtonPaddingYLg} ${t.ButtonPaddingXLg}`,
|
||||||
|
fontSize: t.ButtonFontSizeLg,
|
||||||
|
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapLg },
|
||||||
|
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapLg },
|
||||||
|
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||||
|
fontSize: t.ButtonIconSizeLg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containedPrimary: {
|
||||||
|
'&:hover': { backgroundColor: t.ColorInteractiveHover },
|
||||||
|
'&:active': { backgroundColor: t.ColorInteractiveActive },
|
||||||
|
},
|
||||||
|
containedSecondary: {
|
||||||
|
'&:hover': { backgroundColor: t.ColorNeutral700 },
|
||||||
|
},
|
||||||
|
outlinedPrimary: {
|
||||||
|
borderColor: t.ColorInteractiveDefault,
|
||||||
|
color: t.ColorInteractiveDefault,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: t.ColorInteractiveHover,
|
||||||
|
color: t.ColorInteractiveHover,
|
||||||
|
backgroundColor: t.ColorBrand100,
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
borderColor: t.ColorInteractiveActive,
|
||||||
|
color: t.ColorInteractiveActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outlinedSecondary: {
|
||||||
|
borderColor: t.ColorNeutral400,
|
||||||
|
color: t.ColorNeutral600,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: t.ColorNeutral600,
|
||||||
|
color: t.ColorNeutral700,
|
||||||
|
backgroundColor: t.ColorNeutral200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
textPrimary: {
|
||||||
|
color: t.ColorInteractiveDefault,
|
||||||
|
'&:hover': {
|
||||||
|
color: t.ColorInteractiveHover,
|
||||||
|
backgroundColor: t.ColorBrand100,
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
color: t.ColorInteractiveActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
textSecondary: {
|
||||||
|
color: t.ColorNeutral600,
|
||||||
|
'&:hover': {
|
||||||
|
color: t.ColorNeutral700,
|
||||||
|
backgroundColor: t.ColorNeutral200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
props: { size: 'xs' as const },
|
||||||
|
style: {
|
||||||
|
minHeight: parseInt(t.ButtonHeightXs, 10),
|
||||||
|
padding: `${t.ButtonPaddingYXs} ${t.ButtonPaddingXXs}`,
|
||||||
|
fontSize: t.ButtonFontSizeXs,
|
||||||
|
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapXs },
|
||||||
|
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapXs },
|
||||||
|
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||||
|
fontSize: t.ButtonIconSizeXs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: { variant: 'soft' as const, color: 'primary' as const },
|
||||||
|
style: {
|
||||||
|
backgroundColor: t.ColorBrand200,
|
||||||
|
color: t.ColorInteractiveHover,
|
||||||
|
'&:hover': { backgroundColor: t.ColorBrand300 },
|
||||||
|
'&:active': { backgroundColor: t.ColorBrand400 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: { variant: 'soft' as const, color: 'secondary' as const },
|
||||||
|
style: {
|
||||||
|
backgroundColor: t.ColorNeutral200,
|
||||||
|
color: t.ColorNeutral700,
|
||||||
|
'&:hover': { backgroundColor: t.ColorNeutral300 },
|
||||||
|
'&:active': { backgroundColor: t.ColorNeutral400, color: t.ColorNeutral700 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: parseInt(t.BorderRadiusMd, 10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiOutlinedInput: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: parseInt(t.InputBorderRadiusDefault, 10),
|
||||||
|
transition: 'box-shadow 150ms ease-in-out',
|
||||||
|
// Default (medium) height
|
||||||
|
minHeight: parseInt(t.InputHeightMd, 10),
|
||||||
|
// Small size
|
||||||
|
'&.MuiInputBase-sizeSmall': {
|
||||||
|
minHeight: parseInt(t.InputHeightSm, 10),
|
||||||
|
},
|
||||||
|
// Default border
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: t.ColorNeutral300,
|
||||||
|
transition: 'border-color 150ms ease-in-out',
|
||||||
|
},
|
||||||
|
// Hover — darker border (skip when focused, error, or disabled)
|
||||||
|
'&:hover:not(.Mui-focused):not(.Mui-error):not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: t.ColorNeutral400,
|
||||||
|
},
|
||||||
|
// Focus — brand gold border + double ring (white gap + brand ring)
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: t.ColorBrand500,
|
||||||
|
borderWidth: '1px',
|
||||||
|
},
|
||||||
|
'&.Mui-focused': {
|
||||||
|
boxShadow: `0 0 0 3px ${t.ColorWhite}, 0 0 0 5px ${t.ColorBrand500}`,
|
||||||
|
},
|
||||||
|
// Error border
|
||||||
|
'&.Mui-error .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: t.ColorFeedbackError,
|
||||||
|
borderWidth: '1px',
|
||||||
|
},
|
||||||
|
// Error + focused — error-coloured ring
|
||||||
|
'&.Mui-error.Mui-focused': {
|
||||||
|
boxShadow: `0 0 0 3px ${t.ColorWhite}, 0 0 0 5px ${t.ColorFeedbackError}`,
|
||||||
|
},
|
||||||
|
// Disabled — muted background
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
backgroundColor: t.ColorNeutral200,
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: t.ColorNeutral300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Adornment icon sizing
|
||||||
|
'& .MuiInputAdornment-root': {
|
||||||
|
color: t.ColorNeutral400,
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
fontSize: t.InputIconSizeDefault,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
padding: `${t.InputPaddingYMd} ${t.InputPaddingXDefault}`,
|
||||||
|
fontSize: t.InputFontSizeDefault,
|
||||||
|
color: t.ColorTextPrimary,
|
||||||
|
'&::placeholder': {
|
||||||
|
color: t.ColorNeutral400,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
// Small size padding
|
||||||
|
'.MuiInputBase-sizeSmall &': {
|
||||||
|
padding: `${t.InputPaddingYSm} ${t.InputPaddingXDefault}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notchedOutline: {
|
||||||
|
// Reset top offset — MUI defaults to -5px for floating label space.
|
||||||
|
// We use external labels, so the border should be flush with the input.
|
||||||
|
top: 0,
|
||||||
|
// Hide the notch legend — we use external labels
|
||||||
|
'& legend': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
multiline: {
|
||||||
|
padding: 0,
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
padding: `${t.InputPaddingYMd} ${t.InputPaddingXDefault}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
53
style-dictionary/config.js
Normal file
53
style-dictionary/config.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Style Dictionary configuration for FA Design System (v4)
|
||||||
|
*
|
||||||
|
* Transforms W3C DTCG token JSON into:
|
||||||
|
* - CSS custom properties (for runtime theming)
|
||||||
|
* - JavaScript ES6 module (for MUI theme consumption)
|
||||||
|
* - JSON (for Penpot import)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import StyleDictionary from 'style-dictionary';
|
||||||
|
|
||||||
|
const sd = new StyleDictionary({
|
||||||
|
source: ['tokens/**/*.json'],
|
||||||
|
usesDtcg: true,
|
||||||
|
|
||||||
|
platforms: {
|
||||||
|
// CSS custom properties
|
||||||
|
css: {
|
||||||
|
transformGroup: 'css',
|
||||||
|
prefix: 'fa',
|
||||||
|
buildPath: 'src/theme/generated/',
|
||||||
|
files: [{
|
||||||
|
destination: 'tokens.css',
|
||||||
|
format: 'css/variables',
|
||||||
|
options: {
|
||||||
|
outputReferences: true,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|
||||||
|
// JavaScript ES6 module for MUI theme
|
||||||
|
js: {
|
||||||
|
transformGroup: 'js',
|
||||||
|
buildPath: 'src/theme/generated/',
|
||||||
|
files: [{
|
||||||
|
destination: 'tokens.js',
|
||||||
|
format: 'javascript/es6',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Flat JSON for Penpot import and other tools
|
||||||
|
json: {
|
||||||
|
transformGroup: 'js',
|
||||||
|
buildPath: 'tokens/export/',
|
||||||
|
files: [{
|
||||||
|
destination: 'tokens-flat.json',
|
||||||
|
format: 'json/flat',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sd.buildAllPlatforms();
|
||||||
58
tokens/component/button.json
Normal file
58
tokens/component/button.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"button": {
|
||||||
|
"$description": "Button component tokens — sizing, spacing, and typography per size variant. Colours use semantic interactive tokens via MUI theme palette.",
|
||||||
|
"height": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Minimum heights per button size. All values are multiples of 4px. Large (48px) meets the 44px minimum touch target for mobile.",
|
||||||
|
"xs": { "$value": "28px", "$description": "Extra-small — compact text buttons, inline actions" },
|
||||||
|
"sm": { "$value": "32px", "$description": "Small — secondary actions, toolbar buttons" },
|
||||||
|
"md": { "$value": "40px", "$description": "Medium — default size, form submissions" },
|
||||||
|
"lg": { "$value": "48px", "$description": "Large — primary CTAs, mobile touch targets (meets 44px minimum)" }
|
||||||
|
},
|
||||||
|
"paddingX": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Horizontal padding per button size.",
|
||||||
|
"xs": { "$value": "{spacing.2}", "$description": "8px — compact horizontal padding" },
|
||||||
|
"sm": { "$value": "{spacing.3}", "$description": "12px — small horizontal padding" },
|
||||||
|
"md": { "$value": "{spacing.4}", "$description": "16px — default horizontal padding" },
|
||||||
|
"lg": { "$value": "{spacing.6}", "$description": "24px — generous CTA horizontal padding" }
|
||||||
|
},
|
||||||
|
"paddingY": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Vertical padding per button size.",
|
||||||
|
"xs": { "$value": "{spacing.1}", "$description": "4px — compact vertical padding" },
|
||||||
|
"sm": { "$value": "{spacing.1}", "$description": "4px — small vertical padding" },
|
||||||
|
"md": { "$value": "{spacing.2}", "$description": "8px — default vertical padding" },
|
||||||
|
"lg": { "$value": "{spacing.3}", "$description": "12px — generous CTA vertical padding" }
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Font size per button size.",
|
||||||
|
"xs": { "$value": "{fontSize.xs}", "$description": "12px — extra-small button text" },
|
||||||
|
"sm": { "$value": "{fontSize.sm}", "$description": "14px — small button text" },
|
||||||
|
"md": { "$value": "{fontSize.sm}", "$description": "14px — default button text" },
|
||||||
|
"lg": { "$value": "{fontSize.base}", "$description": "16px — large button text" }
|
||||||
|
},
|
||||||
|
"iconSize": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Icon dimensions per button size.",
|
||||||
|
"xs": { "$value": "14px", "$description": "14px icons in extra-small buttons" },
|
||||||
|
"sm": { "$value": "16px", "$description": "16px icons in small buttons" },
|
||||||
|
"md": { "$value": "18px", "$description": "18px icons in medium buttons" },
|
||||||
|
"lg": { "$value": "20px", "$description": "20px icons in large buttons" }
|
||||||
|
},
|
||||||
|
"iconGap": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Gap between icon and label text.",
|
||||||
|
"xs": { "$value": "{spacing.1}", "$description": "4px icon-text gap" },
|
||||||
|
"sm": { "$value": "{spacing.1}", "$description": "4px icon-text gap" },
|
||||||
|
"md": { "$value": "{spacing.2}", "$description": "8px icon-text gap" },
|
||||||
|
"lg": { "$value": "{spacing.2}", "$description": "8px icon-text gap" }
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Border radius for buttons.",
|
||||||
|
"default": { "$value": "{borderRadius.md}", "$description": "8px — standard button rounding" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
tokens/component/input.json
Normal file
42
tokens/component/input.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"input": {
|
||||||
|
"$description": "Input component tokens — sizing and spacing per size variant. Colours use semantic tokens via MUI theme palette, consistent with Button approach.",
|
||||||
|
"height": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Input field heights. Medium matches Button large (48px) for alignment in search bars. Small matches Button medium (40px) for compact layouts.",
|
||||||
|
"sm": { "$value": "40px", "$description": "Small — compact forms, admin layouts, matches Button medium height" },
|
||||||
|
"md": { "$value": "48px", "$description": "Medium (default) — standard forms, matches Button large for alignment" }
|
||||||
|
},
|
||||||
|
"paddingX": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Horizontal padding inside the input field.",
|
||||||
|
"default": { "$value": "{spacing.3}", "$description": "12px — inner horizontal padding matching Figma design" }
|
||||||
|
},
|
||||||
|
"paddingY": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Vertical padding inside the input field per size.",
|
||||||
|
"sm": { "$value": "{spacing.2}", "$description": "8px — compact vertical padding for small size" },
|
||||||
|
"md": { "$value": "{spacing.3}", "$description": "12px — standard vertical padding for medium size" }
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Font size of the input value and placeholder text.",
|
||||||
|
"default": { "$value": "{fontSize.base}", "$description": "16px — prevents iOS auto-zoom on focus, matches Figma" }
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Border radius for the input field.",
|
||||||
|
"default": { "$value": "{borderRadius.sm}", "$description": "4px — subtle rounding, consistent with Figma design" }
|
||||||
|
},
|
||||||
|
"gap": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Vertical gap between label, input field, and helper text.",
|
||||||
|
"default": { "$value": "{spacing.2}", "$description": "8px — vertical rhythm between label/input/helper, slightly more generous than Figma's 6px for readability" }
|
||||||
|
},
|
||||||
|
"iconSize": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Dimensions for leading/trailing icons inside the input.",
|
||||||
|
"default": { "$value": "20px", "$description": "20px — icon size inside input field, matches Figma trailing icon" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
tokens/primitives/colours.json
Normal file
111
tokens/primitives/colours.json
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"color": {
|
||||||
|
"$type": "color",
|
||||||
|
"brand": {
|
||||||
|
"$description": "Warm gold/copper palette — FA primary brand hue. Derived from Parsons brand swatches.",
|
||||||
|
"50": { "$value": "#FEF9F5", "$description": "Lightest warm tint — warm section backgrounds" },
|
||||||
|
"100": { "$value": "#F7ECDF", "$description": "Light warm — hover backgrounds, subtle fills" },
|
||||||
|
"200": { "$value": "#EBDAC8", "$description": "Warm light — secondary backgrounds, divider tones" },
|
||||||
|
"300": { "$value": "#D8C3B5", "$description": "Warm beige — from brand swatch. Surface warmth, card tints" },
|
||||||
|
"400": { "$value": "#D0A070", "$description": "Mid gold — from brand swatch. Secondary interactive, step indicators" },
|
||||||
|
"500": { "$value": "#BA834E", "$description": "Base brand gold — from brand swatch. Primary CTA colour, main interactive. 3.7:1 contrast on white" },
|
||||||
|
"600": { "$value": "#B0610F", "$description": "Rich copper — from brand swatch. Hover/emphasis on brand elements. 4.8:1 contrast on white" },
|
||||||
|
"700": { "$value": "#8B4E0D", "$description": "Deep copper — active states, strong brand text on light backgrounds. 6.7:1 contrast on white" },
|
||||||
|
"800": { "$value": "#6B3C13", "$description": "Dark brown — bold brand accents, high-contrast brand text" },
|
||||||
|
"900": { "$value": "#51301B", "$description": "Chocolate — from brand swatch. Deep emphasis, dark brand surfaces" },
|
||||||
|
"950": { "$value": "#251913", "$description": "Espresso — from brand swatch. Darkest brand tone, near-black warm" }
|
||||||
|
},
|
||||||
|
"sage": {
|
||||||
|
"$description": "Cool grey-green/slate palette — FA secondary hue. Calming, professional tone for the funeral services context.",
|
||||||
|
"50": { "$value": "#F2F5F6", "$description": "Lightest sage — subtle cool backgrounds" },
|
||||||
|
"100": { "$value": "#E3EAEB", "$description": "Light sage — hover states on cool surfaces" },
|
||||||
|
"200": { "$value": "#D7E1E2", "$description": "From brand swatch — light cool surface, soft borders" },
|
||||||
|
"300": { "$value": "#C8D4D6", "$description": "Mid-light sage — dividers, secondary borders" },
|
||||||
|
"400": { "$value": "#B9C7C9", "$description": "From brand swatch — mid sage, secondary text on dark backgrounds" },
|
||||||
|
"500": { "$value": "#8EA2A7", "$description": "Base sage — secondary content, muted icons" },
|
||||||
|
"600": { "$value": "#687D84", "$description": "Dark sage — secondary text, subtle icons" },
|
||||||
|
"700": { "$value": "#4C5B6B", "$description": "From brand swatch — strong secondary, dark accents. 6.1:1 contrast on white" },
|
||||||
|
"800": { "$value": "#4C5459", "$description": "From brand swatch — near-dark grey, supplementary text. 6.7:1 contrast on white" },
|
||||||
|
"900": { "$value": "#343C40", "$description": "Very dark sage — high-contrast secondary elements" },
|
||||||
|
"950": { "$value": "#1E2528", "$description": "Near-black cool — darkest secondary tone" }
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"$description": "True grey palette for text, borders, and UI chrome. Slight cool undertone to complement the sage palette.",
|
||||||
|
"50": { "$value": "#FAFAFA", "$description": "Lightest neutral — default page background alternative" },
|
||||||
|
"100": { "$value": "#F5F5F5", "$description": "Light neutral — subtle background differentiation" },
|
||||||
|
"200": { "$value": "#E8E8E8", "$description": "Light grey — standard borders, dividers" },
|
||||||
|
"300": { "$value": "#D4D4D4", "$description": "Mid-light grey — disabled borders, subtle separators" },
|
||||||
|
"400": { "$value": "#A3A3A3", "$description": "Mid grey — placeholder text, disabled content" },
|
||||||
|
"500": { "$value": "#737373", "$description": "Base grey — secondary body text, icons" },
|
||||||
|
"600": { "$value": "#525252", "$description": "Dark grey — body text, labels. 7.1:1 contrast on white" },
|
||||||
|
"700": { "$value": "#404040", "$description": "Strong grey — headings, emphasis text. 9.7:1 contrast on white" },
|
||||||
|
"800": { "$value": "#2C2E35", "$description": "From brand swatch — charcoal with cool tint. Primary text colour. 13.2:1 contrast on white" },
|
||||||
|
"900": { "$value": "#1A1A1C", "$description": "Near-black — maximum contrast text" },
|
||||||
|
"950": { "$value": "#0A0A0B", "$description": "Deepest neutral — use sparingly" }
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"$description": "Red palette for error states, destructive actions, and urgent feedback. Warm-leaning for brand cohesion.",
|
||||||
|
"50": { "$value": "#FEF2F2", "$description": "Error tint — error message backgrounds" },
|
||||||
|
"100": { "$value": "#FDE8E8", "$description": "Light error — hover on error surfaces" },
|
||||||
|
"200": { "$value": "#F9BFBF", "$description": "Light red — error borders, subtle indicators" },
|
||||||
|
"300": { "$value": "#F09898", "$description": "Mid-light red — error icon backgrounds" },
|
||||||
|
"400": { "$value": "#E56B6B", "$description": "Mid red — error indicators, badges" },
|
||||||
|
"500": { "$value": "#D64545", "$description": "Base red — form validation errors, alert accents" },
|
||||||
|
"600": { "$value": "#BC2F2F", "$description": "Strong red — error text on light backgrounds. 5.7:1 contrast on white" },
|
||||||
|
"700": { "$value": "#9B2424", "$description": "Dark red — error headings, strong alerts" },
|
||||||
|
"800": { "$value": "#7A1D1D", "$description": "Deep red — high-contrast error emphasis" },
|
||||||
|
"900": { "$value": "#5C1616", "$description": "Very dark red — use sparingly" },
|
||||||
|
"950": { "$value": "#3D0E0E", "$description": "Darkest red" }
|
||||||
|
},
|
||||||
|
"amber": {
|
||||||
|
"$description": "Amber/yellow palette for warning states, price alerts, and important notices.",
|
||||||
|
"50": { "$value": "#FFF9EB", "$description": "Warning tint — warning message backgrounds" },
|
||||||
|
"100": { "$value": "#FFF0CC", "$description": "Light amber — hover on warning surfaces" },
|
||||||
|
"200": { "$value": "#FFE099", "$description": "Light amber — warning borders" },
|
||||||
|
"300": { "$value": "#FFCC66", "$description": "Mid-light amber — warning icon backgrounds" },
|
||||||
|
"400": { "$value": "#FFB833", "$description": "Mid amber — warning badges, price alerts" },
|
||||||
|
"500": { "$value": "#F5A000", "$description": "Base amber — warning accents" },
|
||||||
|
"600": { "$value": "#CC8500", "$description": "Strong amber — warning text. 3.6:1 contrast on white (large text AA)" },
|
||||||
|
"700": { "$value": "#A36B00", "$description": "Dark amber — warning headings. 5.1:1 contrast on white" },
|
||||||
|
"800": { "$value": "#7A5000", "$description": "Deep amber — high-contrast warning emphasis" },
|
||||||
|
"900": { "$value": "#523600", "$description": "Very dark amber — use sparingly" },
|
||||||
|
"950": { "$value": "#331F00", "$description": "Darkest amber" }
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"$description": "Green palette for success states, confirmations, and positive feedback. Slightly muted for the sensitive context.",
|
||||||
|
"50": { "$value": "#F0F7F0", "$description": "Success tint — success message backgrounds" },
|
||||||
|
"100": { "$value": "#D8ECD8", "$description": "Light green — hover on success surfaces" },
|
||||||
|
"200": { "$value": "#B8D8B8", "$description": "Light green — success borders" },
|
||||||
|
"300": { "$value": "#8DC08D", "$description": "Mid-light green — success icon backgrounds" },
|
||||||
|
"400": { "$value": "#66A866", "$description": "Mid green — success badges, completion indicators" },
|
||||||
|
"500": { "$value": "#4A8F4A", "$description": "Base green — success accents, completed steps" },
|
||||||
|
"600": { "$value": "#3B7A3B", "$description": "Strong green — success text on light backgrounds. 4.8:1 contrast on white" },
|
||||||
|
"700": { "$value": "#2E6B2E", "$description": "Dark green — success headings" },
|
||||||
|
"800": { "$value": "#235523", "$description": "Deep green — high-contrast success emphasis" },
|
||||||
|
"900": { "$value": "#1A3F1A", "$description": "Very dark green — use sparingly" },
|
||||||
|
"950": { "$value": "#0F2A0F", "$description": "Darkest green" }
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
"$description": "Blue palette for informational states, supplementary info, and non-brand links.",
|
||||||
|
"50": { "$value": "#EFF6FF", "$description": "Info tint — info message backgrounds" },
|
||||||
|
"100": { "$value": "#DBEAFE", "$description": "Light blue — hover on info surfaces" },
|
||||||
|
"200": { "$value": "#BFDBFE", "$description": "Light blue — info borders" },
|
||||||
|
"300": { "$value": "#93C5FD", "$description": "Mid-light blue — info icon backgrounds" },
|
||||||
|
"400": { "$value": "#60A5FA", "$description": "Mid blue — info badges" },
|
||||||
|
"500": { "$value": "#3B82F6", "$description": "Base blue — info accents, supplementary links" },
|
||||||
|
"600": { "$value": "#2563EB", "$description": "Strong blue — info text on light backgrounds. 4.6:1 contrast on white" },
|
||||||
|
"700": { "$value": "#1D4ED8", "$description": "Dark blue — info headings" },
|
||||||
|
"800": { "$value": "#1E40AF", "$description": "Deep blue — high-contrast info emphasis" },
|
||||||
|
"900": { "$value": "#1E3A8A", "$description": "Very dark blue — use sparingly" },
|
||||||
|
"950": { "$value": "#172554", "$description": "Darkest blue" }
|
||||||
|
},
|
||||||
|
"white": {
|
||||||
|
"$value": "#FFFFFF",
|
||||||
|
"$description": "Pure white — card backgrounds, inverse text, primary surface"
|
||||||
|
},
|
||||||
|
"black": {
|
||||||
|
"$value": "#000000",
|
||||||
|
"$description": "Pure black — from brand swatch. Use sparingly; prefer neutral.800-900 for text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tokens/primitives/effects.json
Normal file
28
tokens/primitives/effects.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"shadow": {
|
||||||
|
"$description": "Elevation shadows for layered UI. Values are CSS box-shadow shorthand strings.",
|
||||||
|
"sm": {
|
||||||
|
"$value": "0 1px 2px rgba(0,0,0,0.05)",
|
||||||
|
"$description": "Subtle lift — resting buttons, input focus subtle elevation"
|
||||||
|
},
|
||||||
|
"md": {
|
||||||
|
"$value": "0 4px 6px rgba(0,0,0,0.07)",
|
||||||
|
"$description": "Medium elevation — cards at rest, dropdowns, popovers"
|
||||||
|
},
|
||||||
|
"lg": {
|
||||||
|
"$value": "0 10px 15px rgba(0,0,0,0.1)",
|
||||||
|
"$description": "High elevation — modals, popovers, card hover states"
|
||||||
|
},
|
||||||
|
"xl": {
|
||||||
|
"$value": "0 20px 25px rgba(0,0,0,0.1)",
|
||||||
|
"$description": "Maximum elevation — elevated panels, dialog boxes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opacity": {
|
||||||
|
"$type": "number",
|
||||||
|
"$description": "Opacity values for interactive states and overlays.",
|
||||||
|
"disabled": { "$value": 0.4, "$description": "Disabled state — 40% opacity. Clearly diminished but still distinguishable" },
|
||||||
|
"hover": { "$value": 0.08, "$description": "Hover overlay — subtle 8% tint applied over backgrounds on hover" },
|
||||||
|
"overlay": { "$value": 0.5, "$description": "Modal/dialog backdrop — 50% black overlay behind modals" }
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tokens/primitives/spacing.json
Normal file
28
tokens/primitives/spacing.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"spacing": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Spacing scale based on 4px base unit. All values are multiples of 4.",
|
||||||
|
"0-5": { "$value": "2px", "$description": "Hairline — icon-to-text tight spacing, fine adjustments" },
|
||||||
|
"1": { "$value": "4px", "$description": "Tight — inline spacing, minimal gaps between related elements" },
|
||||||
|
"2": { "$value": "8px", "$description": "Small — related element gaps, compact padding, icon margins" },
|
||||||
|
"3": { "$value": "12px", "$description": "Component internal padding (small), chip padding" },
|
||||||
|
"4": { "$value": "16px", "$description": "Default component padding, form field gap, card grid gutter (mobile)" },
|
||||||
|
"5": { "$value": "20px", "$description": "Medium component spacing" },
|
||||||
|
"6": { "$value": "24px", "$description": "Card padding, section gap (small), card grid gutter (desktop)" },
|
||||||
|
"8": { "$value": "32px", "$description": "Section gap (medium), form section separation" },
|
||||||
|
"10": { "$value": "40px", "$description": "Section gap (large)" },
|
||||||
|
"12": { "$value": "48px", "$description": "Page section separation, vertical rhythm break" },
|
||||||
|
"16": { "$value": "64px", "$description": "Hero/banner vertical spacing" },
|
||||||
|
"20": { "$value": "80px", "$description": "Major page sections, large vertical spacing" }
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Border radius scale for consistent rounding across components.",
|
||||||
|
"none": { "$value": "0px", "$description": "Square corners — tables, dividers, sharp elements" },
|
||||||
|
"sm": { "$value": "4px", "$description": "Small radius — inputs, small interactive elements, chips" },
|
||||||
|
"md": { "$value": "8px", "$description": "Medium radius — cards, buttons, dropdowns (default)" },
|
||||||
|
"lg": { "$value": "12px", "$description": "Large radius — modals, large cards" },
|
||||||
|
"xl": { "$value": "16px", "$description": "Extra large — feature cards, hero elements" },
|
||||||
|
"full": { "$value": "9999px", "$description": "Full/pill — avatars, pill buttons, circular elements" }
|
||||||
|
}
|
||||||
|
}
|
||||||
78
tokens/primitives/typography.json
Normal file
78
tokens/primitives/typography.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"fontFamily": {
|
||||||
|
"$type": "fontFamily",
|
||||||
|
"body": {
|
||||||
|
"$value": "'Montserrat', 'Helvetica Neue', Arial, sans-serif",
|
||||||
|
"$description": "Primary font — Montserrat. Used for headings (h1-h6), body text, labels, and all UI elements"
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"$value": "'Noto Serif SC', Georgia, 'Times New Roman', serif",
|
||||||
|
"$description": "Display font — Noto Serif SC. Elegant serif for hero/display text only. Adds warmth and gravitas at large sizes"
|
||||||
|
},
|
||||||
|
"mono": {
|
||||||
|
"$value": "'JetBrains Mono', 'Fira Code', Consolas, monospace",
|
||||||
|
"$description": "Monospace font — for reference numbers (FA-2026-00847), tabular pricing data, and code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Font size scale. General-purpose sizes (2xs–4xl) plus display-specific sizes for hero/marketing text.",
|
||||||
|
"2xs": { "$value": "0.6875rem", "$description": "11px — smallest UI text: compact captions, compact overlines" },
|
||||||
|
"xs": { "$value": "0.75rem", "$description": "12px — small text: captions, labels, overlines, body/xs" },
|
||||||
|
"sm": { "$value": "0.875rem", "$description": "14px — body small, labels, helper text" },
|
||||||
|
"base": { "$value": "1rem", "$description": "16px — default body text, heading/6, label/lg" },
|
||||||
|
"md": { "$value": "1.125rem", "$description": "18px — body large, heading/5" },
|
||||||
|
"lg": { "$value": "1.25rem", "$description": "20px — heading/4" },
|
||||||
|
"xl": { "$value": "1.5rem", "$description": "24px — heading/3" },
|
||||||
|
"2xl": { "$value": "1.875rem", "$description": "30px — heading/2" },
|
||||||
|
"3xl": { "$value": "2.25rem", "$description": "36px — heading/1" },
|
||||||
|
"4xl": { "$value": "3rem", "$description": "48px — reserved (legacy)" },
|
||||||
|
"display": {
|
||||||
|
"$description": "Display-specific sizes for hero and marketing text. Used exclusively with Noto Serif SC.",
|
||||||
|
"sm": { "$value": "2rem", "$description": "32px — display/sm, smallest display text" },
|
||||||
|
"3": { "$value": "2.5rem", "$description": "40px — display/3" },
|
||||||
|
"2": { "$value": "3.25rem", "$description": "52px — display/2" },
|
||||||
|
"1": { "$value": "4rem", "$description": "64px — display/1" },
|
||||||
|
"hero": { "$value": "5rem", "$description": "80px — display/hero, largest display text" }
|
||||||
|
},
|
||||||
|
"mobile": {
|
||||||
|
"$description": "Reduced font sizes for mobile breakpoints (< 600px). Display and headings scale down; body sizes stay constant.",
|
||||||
|
"displayHero": { "$value": "2rem", "$description": "32px — mobile display/hero (from 80px desktop)" },
|
||||||
|
"display1": { "$value": "1.75rem", "$description": "28px — mobile display/1 (from 64px desktop)" },
|
||||||
|
"display2": { "$value": "1.5rem", "$description": "24px — mobile display/2 (from 52px desktop)" },
|
||||||
|
"display3": { "$value": "1.375rem", "$description": "22px — mobile display/3 (from 40px desktop)" },
|
||||||
|
"displaySm": { "$value": "1.25rem", "$description": "20px — mobile display/sm (from 32px desktop)" },
|
||||||
|
"h1": { "$value": "1.625rem", "$description": "26px — mobile heading/1 (from 36px desktop)" },
|
||||||
|
"h2": { "$value": "1.375rem", "$description": "22px — mobile heading/2 (from 30px desktop)" },
|
||||||
|
"h3": { "$value": "1.25rem", "$description": "20px — mobile heading/3 (from 24px desktop)" },
|
||||||
|
"h4": { "$value": "1.125rem", "$description": "18px — mobile heading/4 (from 20px desktop)" },
|
||||||
|
"h5": { "$value": "1rem", "$description": "16px — mobile heading/5 (from 18px desktop)" },
|
||||||
|
"h6": { "$value": "0.875rem", "$description": "14px — mobile heading/6 (from 16px desktop)" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fontWeight": {
|
||||||
|
"$type": "fontWeight",
|
||||||
|
"regular": { "$value": 400, "$description": "Regular weight — captions, display text (serif carries inherent weight)" },
|
||||||
|
"medium": { "$value": 500, "$description": "Medium weight — body text, labels. Slightly bolder than regular for improved readability" },
|
||||||
|
"semibold": { "$value": 600, "$description": "Semibold — overlines, button text, navigation" },
|
||||||
|
"bold": { "$value": 700, "$description": "Bold — all headings (h1-h6)" }
|
||||||
|
},
|
||||||
|
"lineHeight": {
|
||||||
|
"$type": "number",
|
||||||
|
"$description": "Generic line-height scale. Typography variants use specific values for precise control.",
|
||||||
|
"tight": { "$value": 1.25, "$description": "Tight leading — large headings, display text" },
|
||||||
|
"snug": { "$value": 1.375, "$description": "Snug leading — sub-headings, labels, small text" },
|
||||||
|
"normal": { "$value": 1.5, "$description": "Normal leading — default body text, optimal readability" },
|
||||||
|
"relaxed": { "$value": 1.75, "$description": "Relaxed leading — large body text, long-form content" }
|
||||||
|
},
|
||||||
|
"letterSpacing": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Generic letter-spacing scale. Typography variants use specific values for precise control.",
|
||||||
|
"tighter": { "$value": "-0.02em", "$description": "Tighter tracking — large display text" },
|
||||||
|
"tight": { "$value": "-0.01em", "$description": "Tight tracking — headings" },
|
||||||
|
"normal": { "$value": "0em", "$description": "Normal tracking — body text, most content" },
|
||||||
|
"wide": { "$value": "0.02em", "$description": "Wide tracking — captions, small text" },
|
||||||
|
"wider": { "$value": "0.05em", "$description": "Wider tracking — labels, UI text" },
|
||||||
|
"widest": { "$value": "0.08em", "$description": "Widest tracking — overlines, uppercase text" }
|
||||||
|
}
|
||||||
|
}
|
||||||
164
tokens/semantic/colours.json
Normal file
164
tokens/semantic/colours.json
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
{
|
||||||
|
"color": {
|
||||||
|
"$type": "color",
|
||||||
|
"text": {
|
||||||
|
"$description": "Semantic text colours — define the hierarchy and intent of text across the UI.",
|
||||||
|
"primary": {
|
||||||
|
"$value": "{color.neutral.800}",
|
||||||
|
"$description": "Primary text — body content, headings. Cool charcoal (#2C2E35) for comfortable extended reading"
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"$value": "{color.neutral.600}",
|
||||||
|
"$description": "Secondary text — helper text, descriptions, metadata, less prominent content"
|
||||||
|
},
|
||||||
|
"tertiary": {
|
||||||
|
"$value": "{color.neutral.500}",
|
||||||
|
"$description": "Tertiary text — placeholders, timestamps, attribution, meta information"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"$value": "{color.neutral.400}",
|
||||||
|
"$description": "Disabled text — clearly diminished but still readable for accessibility"
|
||||||
|
},
|
||||||
|
"inverse": {
|
||||||
|
"$value": "{color.white}",
|
||||||
|
"$description": "Inverse text — white text on dark or coloured backgrounds (buttons, banners)"
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"$value": "{color.brand.600}",
|
||||||
|
"$description": "Brand-coloured text — links, inline brand emphasis. Copper tone meets AA on white (4.8:1)"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"$value": "{color.red.600}",
|
||||||
|
"$description": "Error text — form validation messages, error descriptions"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"$value": "{color.green.600}",
|
||||||
|
"$description": "Success text — confirmation messages, positive feedback"
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"$value": "{color.amber.700}",
|
||||||
|
"$description": "Warning text — cautionary messages. Uses amber.700 for WCAG AA compliance on white (5.1:1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"surface": {
|
||||||
|
"$description": "Background/surface colours for page sections, cards, and containers.",
|
||||||
|
"default": {
|
||||||
|
"$value": "{color.white}",
|
||||||
|
"$description": "Default surface — main page background, card faces"
|
||||||
|
},
|
||||||
|
"subtle": {
|
||||||
|
"$value": "{color.neutral.50}",
|
||||||
|
"$description": "Subtle surface — slight differentiation from default, alternate row backgrounds"
|
||||||
|
},
|
||||||
|
"raised": {
|
||||||
|
"$value": "{color.white}",
|
||||||
|
"$description": "Raised surface — cards, elevated containers (distinguished by shadow rather than colour)"
|
||||||
|
},
|
||||||
|
"warm": {
|
||||||
|
"$value": "{color.brand.50}",
|
||||||
|
"$description": "Warm surface — brand-tinted sections, promotional areas, upsell cards like 'Protect your plan'"
|
||||||
|
},
|
||||||
|
"cool": {
|
||||||
|
"$value": "{color.sage.50}",
|
||||||
|
"$description": "Cool surface — calming sections, information panels, FAQ backgrounds"
|
||||||
|
},
|
||||||
|
"overlay": {
|
||||||
|
"$value": "#00000080",
|
||||||
|
"$description": "Overlay surface — modal/dialog backdrop at 50% black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"border": {
|
||||||
|
"$description": "Border colours for containers, inputs, and visual dividers.",
|
||||||
|
"default": {
|
||||||
|
"$value": "{color.neutral.200}",
|
||||||
|
"$description": "Default border — cards, containers, resting input borders"
|
||||||
|
},
|
||||||
|
"strong": {
|
||||||
|
"$value": "{color.neutral.400}",
|
||||||
|
"$description": "Strong border — emphasis borders, active input borders"
|
||||||
|
},
|
||||||
|
"subtle": {
|
||||||
|
"$value": "{color.neutral.100}",
|
||||||
|
"$description": "Subtle border — section dividers, soft separators"
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"$value": "{color.brand.500}",
|
||||||
|
"$description": "Brand border — focused inputs, selected cards, brand-accented containers"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"$value": "{color.red.500}",
|
||||||
|
"$description": "Error border — form fields with validation errors"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"$value": "{color.green.500}",
|
||||||
|
"$description": "Success border — validated fields, confirmed selections"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interactive": {
|
||||||
|
"$description": "Colours for interactive elements — buttons, links, form controls.",
|
||||||
|
"default": {
|
||||||
|
"$value": "{color.brand.600}",
|
||||||
|
"$description": "Default interactive — primary button fill, link colour, checkbox accent. Uses brand.600 (copper) for WCAG AA compliance (4.6:1 on white)"
|
||||||
|
},
|
||||||
|
"hover": {
|
||||||
|
"$value": "{color.brand.700}",
|
||||||
|
"$description": "Hover state — deepened copper on hover for clear visual feedback"
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"$value": "{color.brand.800}",
|
||||||
|
"$description": "Active/pressed state — dark brown during click/tap"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"$value": "{color.neutral.300}",
|
||||||
|
"$description": "Disabled interactive — muted grey, no pointer events"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"$value": "{color.brand.600}",
|
||||||
|
"$description": "Focus ring colour — keyboard navigation indicator, 2px outline with 2px offset"
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"$value": "{color.sage.700}",
|
||||||
|
"$description": "Secondary interactive — grey/sage buttons, less prominent actions"
|
||||||
|
},
|
||||||
|
"secondary-hover": {
|
||||||
|
"$value": "{color.sage.800}",
|
||||||
|
"$description": "Secondary interactive hover — darkened sage on hover"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"$description": "System alert and feedback colours. Each type has a strong variant (for text/icons) and a subtle variant (for backgrounds).",
|
||||||
|
"success": {
|
||||||
|
"$value": "{color.green.600}",
|
||||||
|
"$description": "Success — confirmations, completed arrangement steps, booking confirmed"
|
||||||
|
},
|
||||||
|
"success-subtle": {
|
||||||
|
"$value": "{color.green.50}",
|
||||||
|
"$description": "Success background — success message container fill, completion banners"
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"$value": "{color.amber.600}",
|
||||||
|
"$description": "Warning — price change alerts, important notices, bond/insurance prompts"
|
||||||
|
},
|
||||||
|
"warning-subtle": {
|
||||||
|
"$value": "{color.amber.50}",
|
||||||
|
"$description": "Warning background — warning message container fill, notice banners"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"$value": "{color.red.600}",
|
||||||
|
"$description": "Error — form validation failures, system errors, payment issues"
|
||||||
|
},
|
||||||
|
"error-subtle": {
|
||||||
|
"$value": "{color.red.50}",
|
||||||
|
"$description": "Error background — error message container fill, alert banners"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"$value": "{color.blue.600}",
|
||||||
|
"$description": "Info — helpful tips, supplementary information, guidance callouts"
|
||||||
|
},
|
||||||
|
"info-subtle": {
|
||||||
|
"$value": "{color.blue.50}",
|
||||||
|
"$description": "Info background — info message container fill, tip banners"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tokens/semantic/spacing.json
Normal file
22
tokens/semantic/spacing.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"spacing": {
|
||||||
|
"component": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Component-level spacing — internal padding and gaps within components.",
|
||||||
|
"xs": { "$value": "{spacing.1}", "$description": "4px — tight gaps: icon margins, chip internal padding" },
|
||||||
|
"sm": { "$value": "{spacing.2}", "$description": "8px — small padding: badge padding, inline element gaps" },
|
||||||
|
"md": { "$value": "{spacing.4}", "$description": "16px — default padding: button padding, input padding, form field gap" },
|
||||||
|
"lg": { "$value": "{spacing.6}", "$description": "24px — large padding: card padding (desktop), modal padding" }
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"$type": "dimension",
|
||||||
|
"$description": "Layout-level spacing — structural gaps between sections and page elements.",
|
||||||
|
"gutter": { "$value": "{spacing.4}", "$description": "16px — grid gutter on mobile, card grid gap" },
|
||||||
|
"gutter-desktop": { "$value": "{spacing.6}", "$description": "24px — grid gutter on desktop" },
|
||||||
|
"section": { "$value": "{spacing.12}", "$description": "48px — vertical gap between page sections" },
|
||||||
|
"page": { "$value": "{spacing.16}", "$description": "64px — major page section separation, hero spacing" },
|
||||||
|
"page-padding": { "$value": "{spacing.4}", "$description": "16px — horizontal page padding on mobile" },
|
||||||
|
"page-padding-desktop": { "$value": "{spacing.8}", "$description": "32px — horizontal page padding on desktop" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
tokens/semantic/typography.json
Normal file
198
tokens/semantic/typography.json
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
{
|
||||||
|
"typography": {
|
||||||
|
"$description": "Typography role definitions — 21 variants across 6 categories. Display uses Noto Serif SC (serif, Regular). All other text uses Montserrat. Line-height and letter-spacing use specific values per variant for precise control.",
|
||||||
|
|
||||||
|
"displayHero": {
|
||||||
|
"$description": "Hero display — largest marketing/hero text. Noto Serif SC Regular.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.display}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.display.hero}", "$type": "dimension", "$description": "80px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.displayHero}", "$type": "dimension", "$description": "32px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.regular}", "$type": "fontWeight", "$description": "400 — serif carries inherent weight at large sizes" },
|
||||||
|
"lineHeight": { "$value": 1.05, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "-1.5px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"display1": {
|
||||||
|
"$description": "Display level 1 — major marketing headings. Noto Serif SC Regular.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.display}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.display.1}", "$type": "dimension", "$description": "64px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.display1}", "$type": "dimension", "$description": "28px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.regular}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.078, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "-1px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"display2": {
|
||||||
|
"$description": "Display level 2 — section hero text. Noto Serif SC Regular.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.display}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.display.2}", "$type": "dimension", "$description": "52px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.display2}", "$type": "dimension", "$description": "24px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.regular}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.096, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "-0.5px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"display3": {
|
||||||
|
"$description": "Display level 3 — prominent section titles. Noto Serif SC Regular.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.display}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.display.3}", "$type": "dimension", "$description": "40px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.display3}", "$type": "dimension", "$description": "22px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.regular}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.15, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "-0.25px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"displaySm": {
|
||||||
|
"$description": "Display small — smallest display text, pull quotes. Noto Serif SC Regular.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.display}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.display.sm}", "$type": "dimension", "$description": "32px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.displaySm}", "$type": "dimension", "$description": "20px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.regular}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.1875, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"h1": {
|
||||||
|
"$description": "Heading 1 — page titles. Montserrat Bold.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.3xl}", "$type": "dimension", "$description": "36px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.h1}", "$type": "dimension", "$description": "26px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.194, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "-0.5px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"h2": {
|
||||||
|
"$description": "Heading 2 — section titles. Montserrat Bold.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.2xl}", "$type": "dimension", "$description": "30px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.h2}", "$type": "dimension", "$description": "22px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.267, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "-0.25px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"h3": {
|
||||||
|
"$description": "Heading 3 — sub-section titles. Montserrat Bold.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.xl}", "$type": "dimension", "$description": "24px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.h3}", "$type": "dimension", "$description": "20px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.292, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"h4": {
|
||||||
|
"$description": "Heading 4 — minor headings. Montserrat Bold.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.lg}", "$type": "dimension", "$description": "20px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.h4}", "$type": "dimension", "$description": "18px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.35, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"h5": {
|
||||||
|
"$description": "Heading 5 — small headings. Montserrat Bold.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.md}", "$type": "dimension", "$description": "18px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.h5}", "$type": "dimension", "$description": "16px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.389, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"h6": {
|
||||||
|
"$description": "Heading 6 — smallest heading. Montserrat Bold.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.base}", "$type": "dimension", "$description": "16px desktop" },
|
||||||
|
"fontSizeMobile": { "$value": "{fontSize.mobile.h6}", "$type": "dimension", "$description": "14px mobile" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.bold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.375, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"bodyLg": {
|
||||||
|
"$description": "Body large — lead paragraphs, introductions. Montserrat Medium.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.md}", "$type": "dimension", "$description": "18px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.medium}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.611, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"$description": "Body default — main content text. Montserrat Medium.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.base}", "$type": "dimension", "$description": "16px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.medium}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.625, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"bodySm": {
|
||||||
|
"$description": "Body small — helper text, secondary content. Montserrat Medium.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.sm}", "$type": "dimension", "$description": "14px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.medium}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.571, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"bodyXs": {
|
||||||
|
"$description": "Body extra-small — fine print, compact content. Montserrat Medium.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.xs}", "$type": "dimension", "$description": "12px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.medium}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.5, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0.1px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"labelLg": {
|
||||||
|
"$description": "Label large — prominent form labels, section labels. Montserrat Medium.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.base}", "$type": "dimension", "$description": "16px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.medium}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.3125, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0.1px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"$description": "Label default — form labels, UI text. Montserrat Medium.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.sm}", "$type": "dimension", "$description": "14px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.medium}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.286, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0.15px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"labelSm": {
|
||||||
|
"$description": "Label small — compact labels, tag text. Montserrat Medium.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.xs}", "$type": "dimension", "$description": "12px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.medium}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.333, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0.2px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"caption": {
|
||||||
|
"$description": "Caption default — fine print, timestamps, metadata. Montserrat Regular.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.xs}", "$type": "dimension", "$description": "12px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.regular}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.417, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0.2px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"captionSm": {
|
||||||
|
"$description": "Caption small — compact metadata, footnotes. Montserrat Regular. Min 11px for accessibility.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.2xs}", "$type": "dimension", "$description": "11px — accessibility floor" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.regular}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.364, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "0.2px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"overline": {
|
||||||
|
"$description": "Overline default — section overlines, category labels. Montserrat SemiBold, uppercase.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.xs}", "$type": "dimension", "$description": "12px" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.semibold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.333, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "1.5px", "$type": "dimension" }
|
||||||
|
},
|
||||||
|
"overlineSm": {
|
||||||
|
"$description": "Overline small — compact section labels, tab overlines. Montserrat SemiBold, uppercase. Min 11px for accessibility.",
|
||||||
|
"fontFamily": { "$value": "{fontFamily.body}", "$type": "fontFamily" },
|
||||||
|
"fontSize": { "$value": "{fontSize.2xs}", "$type": "dimension", "$description": "11px — accessibility floor" },
|
||||||
|
"fontWeight": { "$value": "{fontWeight.semibold}", "$type": "fontWeight" },
|
||||||
|
"lineHeight": { "$value": 1.273, "$type": "number" },
|
||||||
|
"letterSpacing": { "$value": "1.5px", "$type": "dimension" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@atoms/*": ["src/components/atoms/*"],
|
||||||
|
"@molecules/*": ["src/components/molecules/*"],
|
||||||
|
"@organisms/*": ["src/components/organisms/*"],
|
||||||
|
"@theme": ["src/theme"],
|
||||||
|
"@theme/*": ["src/theme/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
15
vite.config.ts
Normal file
15
vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@atoms': path.resolve(__dirname, 'src/components/atoms'),
|
||||||
|
'@molecules': path.resolve(__dirname, 'src/components/molecules'),
|
||||||
|
'@organisms': path.resolve(__dirname, 'src/components/organisms'),
|
||||||
|
'@theme': path.resolve(__dirname, 'src/theme'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user