From 732c872576d3baa4378a831765c18cebea1741a0 Mon Sep 17 00:00:00 2001 From: Richie Date: Wed, 25 Mar 2026 15:08:15 +1100 Subject: [PATCH] Initial commit: FA 2.0 Design System foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/agents/component-builder.md | 82 + .claude/agents/story-writer.md | 70 + .claude/agents/token-architect.md | 57 + .claude/skills/build-atom/SKILL.md | 22 + .claude/skills/build-molecule/SKILL.md | 23 + .claude/skills/build-organism/SKILL.md | 24 + .claude/skills/create-tokens/SKILL.md | 20 + .claude/skills/review-component/SKILL.md | 49 + .claude/skills/status/SKILL.md | 35 + .claude/skills/sync-tokens/SKILL.md | 22 + .claude/skills/write-stories/SKILL.md | 29 + .gitignore | 9 + .mcp.json | 8 + .storybook/main.ts | 22 + .storybook/preview-head.html | 6 + .storybook/preview.tsx | 27 + CLAUDE.md | 80 + QUICKSTART.md | 106 + bootstrap.sh | 138 + docs/conventions/component-conventions.md | 241 + docs/conventions/token-conventions.md | 144 + docs/design-system.md | 174 + docs/memory/component-registry.md | 62 + docs/memory/decisions-log.md | 221 + docs/memory/session-log.md | 290 + docs/memory/token-registry.md | 193 + docs/reference/mcp-setup.md | 127 + index.html | 18 + package-lock.json | 6655 +++++++++++++++++ package.json | 41 + .../atoms/Button/Button.stories.tsx | 365 + src/components/atoms/Button/Button.tsx | 79 + src/components/atoms/Button/index.ts | 3 + src/components/atoms/Input/Input.stories.tsx | 506 ++ src/components/atoms/Input/Input.tsx | 184 + src/components/atoms/Input/index.ts | 2 + .../atoms/Typography/Typography.stories.tsx | 345 + .../atoms/Typography/Typography.tsx | 63 + src/components/atoms/Typography/index.ts | 3 + src/main.tsx | 20 + src/theme/generated/tokens.css | 354 + src/theme/generated/tokens.js | 377 + src/theme/index.ts | 568 ++ src/vite-env.d.ts | 1 + style-dictionary/config.js | 53 + tokens/component/button.json | 58 + tokens/component/input.json | 42 + tokens/primitives/colours.json | 111 + tokens/primitives/effects.json | 28 + tokens/primitives/spacing.json | 28 + tokens/primitives/typography.json | 78 + tokens/semantic/colours.json | 164 + tokens/semantic/spacing.json | 22 + tokens/semantic/typography.json | 198 + tsconfig.json | 28 + vite.config.ts | 15 + 56 files changed, 12690 insertions(+) create mode 100644 .claude/agents/component-builder.md create mode 100644 .claude/agents/story-writer.md create mode 100644 .claude/agents/token-architect.md create mode 100644 .claude/skills/build-atom/SKILL.md create mode 100644 .claude/skills/build-molecule/SKILL.md create mode 100644 .claude/skills/build-organism/SKILL.md create mode 100644 .claude/skills/create-tokens/SKILL.md create mode 100644 .claude/skills/review-component/SKILL.md create mode 100644 .claude/skills/status/SKILL.md create mode 100644 .claude/skills/sync-tokens/SKILL.md create mode 100644 .claude/skills/write-stories/SKILL.md create mode 100644 .gitignore create mode 100644 .mcp.json create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview-head.html create mode 100644 .storybook/preview.tsx create mode 100644 CLAUDE.md create mode 100644 QUICKSTART.md create mode 100755 bootstrap.sh create mode 100644 docs/conventions/component-conventions.md create mode 100644 docs/conventions/token-conventions.md create mode 100644 docs/design-system.md create mode 100644 docs/memory/component-registry.md create mode 100644 docs/memory/decisions-log.md create mode 100644 docs/memory/session-log.md create mode 100644 docs/memory/token-registry.md create mode 100644 docs/reference/mcp-setup.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/components/atoms/Button/Button.stories.tsx create mode 100644 src/components/atoms/Button/Button.tsx create mode 100644 src/components/atoms/Button/index.ts create mode 100644 src/components/atoms/Input/Input.stories.tsx create mode 100644 src/components/atoms/Input/Input.tsx create mode 100644 src/components/atoms/Input/index.ts create mode 100644 src/components/atoms/Typography/Typography.stories.tsx create mode 100644 src/components/atoms/Typography/Typography.tsx create mode 100644 src/components/atoms/Typography/index.ts create mode 100644 src/main.tsx create mode 100644 src/theme/generated/tokens.css create mode 100644 src/theme/generated/tokens.js create mode 100644 src/theme/index.ts create mode 100644 src/vite-env.d.ts create mode 100644 style-dictionary/config.js create mode 100644 tokens/component/button.json create mode 100644 tokens/component/input.json create mode 100644 tokens/primitives/colours.json create mode 100644 tokens/primitives/effects.json create mode 100644 tokens/primitives/spacing.json create mode 100644 tokens/primitives/typography.json create mode 100644 tokens/semantic/colours.json create mode 100644 tokens/semantic/spacing.json create mode 100644 tokens/semantic/typography.json create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.claude/agents/component-builder.md b/.claude/agents/component-builder.md new file mode 100644 index 0000000..2da0ce3 --- /dev/null +++ b/.claude/agents/component-builder.md @@ -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 diff --git a/.claude/agents/story-writer.md b/.claude/agents/story-writer.md new file mode 100644 index 0000000..ef68ad6 --- /dev/null +++ b/.claude/agents/story-writer.md @@ -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 = { + 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; +``` + +### 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 diff --git a/.claude/agents/token-architect.md b/.claude/agents/token-architect.md new file mode 100644 index 0000000..e1b3990 --- /dev/null +++ b/.claude/agents/token-architect.md @@ -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 diff --git a/.claude/skills/build-atom/SKILL.md b/.claude/skills/build-atom/SKILL.md new file mode 100644 index 0000000..9b0c15d --- /dev/null +++ b/.claude/skills/build-atom/SKILL.md @@ -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 diff --git a/.claude/skills/build-molecule/SKILL.md b/.claude/skills/build-molecule/SKILL.md new file mode 100644 index 0000000..6aff9f0 --- /dev/null +++ b/.claude/skills/build-molecule/SKILL.md @@ -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 diff --git a/.claude/skills/build-organism/SKILL.md b/.claude/skills/build-organism/SKILL.md new file mode 100644 index 0000000..fe94bcc --- /dev/null +++ b/.claude/skills/build-organism/SKILL.md @@ -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 diff --git a/.claude/skills/create-tokens/SKILL.md b/.claude/skills/create-tokens/SKILL.md new file mode 100644 index 0000000..ed82838 --- /dev/null +++ b/.claude/skills/create-tokens/SKILL.md @@ -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 diff --git a/.claude/skills/review-component/SKILL.md b/.claude/skills/review-component/SKILL.md new file mode 100644 index 0000000..178d5ff --- /dev/null +++ b/.claude/skills/review-component/SKILL.md @@ -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. diff --git a/.claude/skills/status/SKILL.md b/.claude/skills/status/SKILL.md new file mode 100644 index 0000000..4c59324 --- /dev/null +++ b/.claude/skills/status/SKILL.md @@ -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] +``` diff --git a/.claude/skills/sync-tokens/SKILL.md b/.claude/skills/sync-tokens/SKILL.md new file mode 100644 index 0000000..1852937 --- /dev/null +++ b/.claude/skills/sync-tokens/SKILL.md @@ -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 diff --git a/.claude/skills/write-stories/SKILL.md b/.claude/skills/write-stories/SKILL.md new file mode 100644 index 0000000..1b66f63 --- /dev/null +++ b/.claude/skills/write-stories/SKILL.md @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bc046e --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +storybook-static/ +tokens/export/ +*.local +.env +.env.* +.DS_Store +*.tgz diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..891556b --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "figma-remote-mcp": { + "type": "http", + "url": "https://mcp.figma.com/mcp" + } + } +} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..a1af62e --- /dev/null +++ b/.storybook/main.ts @@ -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; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..f2662d7 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,6 @@ + + + diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..86a33c6 --- /dev/null +++ b/.storybook/preview.tsx @@ -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) => ( + + + + + ), + ], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d4546dd --- /dev/null +++ b/CLAUDE.md @@ -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` diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..4efe4bc --- /dev/null +++ b/QUICKSTART.md @@ -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 +``` diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..a70da11 --- /dev/null +++ b/bootstrap.sh @@ -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) => ( + + + + + ), + ], + 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 "" diff --git a/docs/conventions/component-conventions.md b/docs/conventions/component-conventions.md new file mode 100644 index 0000000..2432b2d --- /dev/null +++ b/docs/conventions/component-conventions.md @@ -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 { + /** 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), +})(({ 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 = ({ + variant = 'contained', + size = 'medium', + color = 'primary', + loading = false, + fullWidth = false, + children, + disabled, + ...props +}) => { + return ( + + {loading ? 'Loading…' : children} + + ); +}; + +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 { + // Add your custom props, omitting any MUI props you're overriding +} + +// Forward all unknown props to MUI +const Button: React.FC = ({ customProp, ...muiProps }) => { + return ; +}; +``` + +**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 = { + 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; + +// ─── Individual stories ────────────────────────────────────────────────────── + +/** Default button appearance */ +export const Default: Story = { + args: { children: 'Get started' }, +}; + +/** All visual variants side by side */ +export const AllVariants: Story = { + render: () => ( +
+ + + +
+ ), +}; + +/** All sizes side by side */ +export const AllSizes: Story = { + render: () => ( +
+ + + +
+ ), +}; + +/** 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.) diff --git a/docs/conventions/token-conventions.md b/docs/conventions/token-conventions.md new file mode 100644 index 0000000..e4cf2d6 --- /dev/null +++ b/docs/conventions/token-conventions.md @@ -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 +``` diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..29f52b4 --- /dev/null +++ b/docs/design-system.md @@ -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) diff --git a/docs/memory/component-registry.md b/docs/memory/component-registry.md new file mode 100644 index 0000000..7466293 --- /dev/null +++ b/docs/memory/component-registry.md @@ -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~~ | diff --git a/docs/memory/decisions-log.md b/docs/memory/decisions-log.md new file mode 100644 index 0000000..ac5ade7 --- /dev/null +++ b/docs/memory/decisions-log.md @@ -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 ` + + + + + ), +}; + +// ─── Variants ─────────────────────────────────────────────────────────────── + +/** All visual variants for primary (brand) colour */ +export const VariantsPrimary: Story = { + name: 'Variants — Primary', + render: () => ( +
+ + + + +
+ ), +}; + +/** All visual variants for secondary (neutral grey) colour */ +export const VariantsSecondary: Story = { + name: 'Variants — Secondary', + render: () => ( +
+ + + + +
+ ), +}; + +// ─── Sizes ────────────────────────────────────────────────────────────────── + +/** All four sizes side by side */ +export const AllSizes: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +/** All sizes in soft variant */ +export const AllSizesSoft: Story = { + name: 'All Sizes — Soft', + render: () => ( +
+ + + + +
+ ), +}; + +// ─── With Icons ───────────────────────────────────────────────────────────── + +/** Button with a leading (start) icon */ +export const WithStartIcon: Story = { + args: { + children: 'Add to package', + startIcon: , + }, +}; + +/** Button with a trailing (end) icon */ +export const WithEndIcon: Story = { + args: { + children: 'Continue', + endIcon: , + }, +}; + +/** Button with both leading and trailing icons */ +export const WithBothIcons: Story = { + args: { + children: 'Search', + startIcon: , + endIcon: , + }, +}; + +/** Icons across all sizes */ +export const IconsAllSizes: Story = { + name: 'Icons — All Sizes', + render: () => ( +
+ + + + +
+ ), +}; + +// ─── 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: () => ( +
+ + + + +
+ ), +}; + +/** 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: () => ( +
+ + + + +
+ ), +}; + +// ─── 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 ( +
+ +

+ Click to see: idle → loading → success → idle +

+
+ ); + }, +}; + +// ─── 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: () => ( +
+ + + + +
+ ), +}; + +/** Text button sizes (from the merged Text Button Figma component) */ +export const TextButtonSizes: Story = { + name: 'Text Buttons — All Sizes', + render: () => ( +
+ + + + +
+ ), +}; + +// ─── 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) => ( +
+ +
+ ), + ], +}; + +// ─── 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: () => ( +
+ {(['contained', 'soft', 'outlined', 'text'] as const).map((variant) => ( +
+
+ {variant} +
+
+ + + + + +
+
+ ))} +
+ ), +}; diff --git a/src/components/atoms/Button/Button.tsx b/src/components/atoms/Button/Button.tsx new file mode 100644 index 0000000..5fd6bd9 --- /dev/null +++ b/src/components/atoms/Button/Button.tsx @@ -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( + ( + { + loading = false, + underline = false, + disabled, + children, + variant = 'contained', + size = 'medium', + sx, + ...props + }, + ref, + ) => { + return ( + + {children} + {loading && ( + + )} + + ); + }, +); + +Button.displayName = 'Button'; +export default Button; diff --git a/src/components/atoms/Button/index.ts b/src/components/atoms/Button/index.ts new file mode 100644 index 0000000..ef8465c --- /dev/null +++ b/src/components/atoms/Button/index.ts @@ -0,0 +1,3 @@ +export { default } from './Button'; +export { Button } from './Button'; +export type { ButtonProps } from './Button'; diff --git a/src/components/atoms/Input/Input.stories.tsx b/src/components/atoms/Input/Input.stories.tsx new file mode 100644 index 0000000..bcbdcba --- /dev/null +++ b/src/components/atoms/Input/Input.stories.tsx @@ -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 = { + 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) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +// ─── 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: () => ( +
+ } + /> + } + /> + } + /> + +
+ ), +}; + +// ─── States ───────────────────────────────────────────────────────────────── + +/** All visual states matching the Figma design */ +export const AllStates: Story = { + name: 'All States', + render: () => ( +
+ + + + + + + +
+ ), +}; + +// ─── 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: () => ( +
+ + +
+ ), +}; + +/** Size comparison with Buttons (for search bar alignment) */ +export const SizeAlignment: Story = { + name: 'Size Alignment with Button', + render: () => ( +
+
+ } + size="medium" + /> + +
+
+ } + size="small" + /> + +
+
+ ), +}; + +// ─── With Icons ───────────────────────────────────────────────────────────── + +/** Leading and trailing icon examples */ +export const WithIcons: Story = { + name: 'With Icons', + render: () => ( +
+ } + /> + } + type="email" + /> + } + type="tel" + /> + } + type="number" + /> + } + endIcon={} + success + helperText="Email address confirmed" + /> + } + endIcon={} + error + helperText="Please enter a valid email address" + /> +
+ ), +}; + +// ─── 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 ( + } + endAdornment={ + + setShow(!show)} + edge="end" + size="small" + > + {show ? : } + + + } + 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 ( + } + endIcon={ + showSuccess ? : + showError ? : + 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) => ( +
+ +
+ ), + ], + render: () => ( +
+
+ Contact details +
+ +
+ } + type="email" + /> + } + type="tel" + /> +
+ + +
+ ), +}; + +// ─── Complete Matrix ──────────────────────────────────────────────────────── + +/** Full state matrix for visual QA — all states across both sizes */ +export const CompleteMatrix: Story = { + name: 'Complete Matrix', + decorators: [ + (Story) => ( +
+ +
+ ), + ], + render: () => ( +
+ {(['medium', 'small'] as const).map((size) => ( +
+
+ Size: {size} ({size === 'medium' ? '48px' : '40px'}) +
+
+ } + /> + } + /> + + } + /> + } + /> + + +
+
+ ))} +
+ ), +}; diff --git a/src/components/atoms/Input/Input.tsx b/src/components/atoms/Input/Input.tsx new file mode 100644 index 0000000..b956685 --- /dev/null +++ b/src/components/atoms/Input/Input.tsx @@ -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 { + /** 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( + ( + { + 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 ? ( + {startIcon} + ) : startAdornment; + + const resolvedEnd = endIcon ? ( + {endIcon} + ) : endAdornment; + + return ( + + {label && ( + 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} + + )} + + ) => + `0 0 0 3px ${theme.palette.common.white}, 0 0 0 5px ${theme.palette.success.main}`, + }, + }, + ...(Array.isArray(sx) ? sx : [sx]), + ]} + {...props} + /> + + {helperText && ( + 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} + + )} + + ); + }, +); + +Input.displayName = 'Input'; +export default Input; diff --git a/src/components/atoms/Input/index.ts b/src/components/atoms/Input/index.ts new file mode 100644 index 0000000..a28f2c5 --- /dev/null +++ b/src/components/atoms/Input/index.ts @@ -0,0 +1,2 @@ +export { Input, default } from './Input'; +export type { InputProps } from './Input'; diff --git a/src/components/atoms/Typography/Typography.stories.tsx b/src/components/atoms/Typography/Typography.stories.tsx new file mode 100644 index 0000000..8d4cd99 --- /dev/null +++ b/src/components/atoms/Typography/Typography.stories.tsx @@ -0,0 +1,345 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography } from './Typography'; + +const meta: Meta = { + 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; + +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: () => ( +
+
+ displayHero — 80px + {SAMPLE} +
+
+ display1 — 64px + {SAMPLE} +
+
+ display2 — 52px + {SAMPLE} +
+
+ display3 — 40px + {SAMPLE} +
+
+ displaySm — 32px + {SAMPLE} +
+
+ ), +}; + +// ─── 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: () => ( +
+
+ h1 — 36px + {SAMPLE} +
+
+ h2 — 30px + {SAMPLE} +
+
+ h3 — 24px + {SAMPLE} +
+
+ h4 — 20px + {SAMPLE} +
+
+ h5 — 18px + {SAMPLE} +
+
+ h6 — 16px + {SAMPLE} +
+
+ ), +}; + +// ─── Body (Montserrat, Medium) ────────────────────────────────────────────── + +/** 4 body sizes — Montserrat Medium (500). For content text. */ +export const Body: Story = { + name: 'Body Text', + render: () => ( +
+
+ bodyLg — 18px + + 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. + +
+
+ body1 (default) — 16px + + Compare funeral directors in your area, view transparent pricing, and make informed + decisions at your own pace. Every family deserves clarity during this time. + +
+
+ body2 (small) — 14px + + Prices shown are indicative and may vary based on your specific requirements. + Contact the funeral director directly for a detailed quote. + +
+
+ bodyXs — 12px + + Terms and conditions apply. Funeral Arranger is a comparison service and does not + directly provide funeral services. ABN 12 345 678 901. + +
+
+ ), +}; + +// ─── 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: () => ( +
+
+ labelLg — 16px medium + Form label or section label +
+
+ label — 14px medium + Default form label +
+
+ labelSm — 12px medium + Compact label or tag text +
+
+ caption — 12px regular + Fine print, timestamps, metadata +
+
+ captionSm — 11px regular + Compact metadata, footnotes +
+
+ overline — 12px semibold uppercase + Section overline +
+
+ overlineSm — 11px semibold uppercase + Compact overline +
+
+ ), +}; + +// ─── Colours ──────────────────────────────────────────────────────────────── + +export const Colours: Story = { + name: 'Colours', + render: () => ( +
+ Text Primary — main body text (neutral.800) + Text Secondary — helper text (neutral.600) + Text Disabled — inactive (neutral.400) + Primary — brand emphasis (brand.600) + Secondary — neutral emphasis (neutral.600) + Error — validation errors (red.600) + Warning — cautionary (amber.600) + Success — confirmations (green.600) + Info — helpful tips (blue.600) +
+ ), +}; + +// ─── Font Families ────────────────────────────────────────────────────────── + +/** The two font families: serif for display, sans-serif for everything else */ +export const FontFamilies: Story = { + name: 'Font Families', + render: () => ( +
+
+ Display font — Noto Serif SC (Regular 400) + + Warm, trustworthy, and professional + + + Used exclusively for display variants (hero through sm). Regular weight — serif carries inherent visual weight at large sizes. + +
+
+ Body font — Montserrat + Clean, modern, and highly readable + + 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). + +
+
+ ), +}; + +// ─── Max Lines ────────────────────────────────────────────────────────────── + +export const MaxLines: Story = { + name: 'Max Lines (Truncation)', + render: () => ( +
+
+ maxLines=1 + + H. Parsons Funeral Directors — trusted by Australian families for over 30 years, + providing compassionate and transparent funeral services. + +
+
+ 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. + +
+
+ ), +}; + +// ─── Realistic Content ────────────────────────────────────────────────────── + +export const RealisticContent: Story = { + name: 'Realistic Content', + render: () => ( +
+ Funeral planning + Compare funeral services in your area + + Transparent pricing and service comparison to help you make informed + decisions during a difficult time. + + How it works + + 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. + + Step 1: Browse packages + + Compare packages side by side. Each package clearly shows what is and + isn't included, so there are no surprises. + + + Prices are indicative and current as of March 2026. Contact the funeral + director for a binding quote. + +
+ ), +}; + +// ─── 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 ( +
+ {variants.map(({ variant, label }) => ( +
+ + {label} + + {SAMPLE} +
+ ))} +
+ ); + }, +}; diff --git a/src/components/atoms/Typography/Typography.tsx b/src/components/atoms/Typography/Typography.tsx new file mode 100644 index 0000000..0d21915 --- /dev/null +++ b/src/components/atoms/Typography/Typography.tsx @@ -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( + ({ maxLines, sx, ...props }, ref) => { + return ( + + ); + }, +); + +Typography.displayName = 'Typography'; +export default Typography; diff --git a/src/components/atoms/Typography/index.ts b/src/components/atoms/Typography/index.ts new file mode 100644 index 0000000..fd6b120 --- /dev/null +++ b/src/components/atoms/Typography/index.ts @@ -0,0 +1,3 @@ +export { default } from './Typography'; +export { Typography } from './Typography'; +export type { TypographyProps } from './Typography'; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..b9c9d62 --- /dev/null +++ b/src/main.tsx @@ -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 = () => ( + + +
+

FA Design System

+

Run npm run storybook to view components.

+
+
+); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/theme/generated/tokens.css b/src/theme/generated/tokens.css new file mode 100644 index 0000000..981775d --- /dev/null +++ b/src/theme/generated/tokens.css @@ -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); +} diff --git a/src/theme/generated/tokens.js b/src/theme/generated/tokens.js new file mode 100644 index 0000000..5c1d97e --- /dev/null +++ b/src/theme/generated/tokens.js @@ -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"; diff --git a/src/theme/index.ts b/src/theme/index.ts new file mode 100644 index 0000000..83a59db --- /dev/null +++ b/src/theme/index.ts @@ -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; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/style-dictionary/config.js b/style-dictionary/config.js new file mode 100644 index 0000000..355eb02 --- /dev/null +++ b/style-dictionary/config.js @@ -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(); diff --git a/tokens/component/button.json b/tokens/component/button.json new file mode 100644 index 0000000..61a312d --- /dev/null +++ b/tokens/component/button.json @@ -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" } + } + } +} diff --git a/tokens/component/input.json b/tokens/component/input.json new file mode 100644 index 0000000..613dd1b --- /dev/null +++ b/tokens/component/input.json @@ -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" } + } + } +} diff --git a/tokens/primitives/colours.json b/tokens/primitives/colours.json new file mode 100644 index 0000000..c8238b9 --- /dev/null +++ b/tokens/primitives/colours.json @@ -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" + } + } +} diff --git a/tokens/primitives/effects.json b/tokens/primitives/effects.json new file mode 100644 index 0000000..e7cd4b8 --- /dev/null +++ b/tokens/primitives/effects.json @@ -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" } + } +} diff --git a/tokens/primitives/spacing.json b/tokens/primitives/spacing.json new file mode 100644 index 0000000..9870052 --- /dev/null +++ b/tokens/primitives/spacing.json @@ -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" } + } +} diff --git a/tokens/primitives/typography.json b/tokens/primitives/typography.json new file mode 100644 index 0000000..1830fad --- /dev/null +++ b/tokens/primitives/typography.json @@ -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" } + } +} diff --git a/tokens/semantic/colours.json b/tokens/semantic/colours.json new file mode 100644 index 0000000..af72acf --- /dev/null +++ b/tokens/semantic/colours.json @@ -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" + } + } + } +} diff --git a/tokens/semantic/spacing.json b/tokens/semantic/spacing.json new file mode 100644 index 0000000..59af518 --- /dev/null +++ b/tokens/semantic/spacing.json @@ -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" } + } + } +} diff --git a/tokens/semantic/typography.json b/tokens/semantic/typography.json new file mode 100644 index 0000000..90a3629 --- /dev/null +++ b/tokens/semantic/typography.json @@ -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" } + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3abcd62 --- /dev/null +++ b/tsconfig.json @@ -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"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..1543b9e --- /dev/null +++ b/vite.config.ts @@ -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'), + }, + }, +});