Initial commit: FA 2.0 Design System foundation
Token pipeline (Style Dictionary v4, DTCG format): - Primitive tokens: colour palettes (brand, sage, neutral, feedback), typography (3 font families, 21-variant type scale), spacing (4px grid), border radius, shadows, opacity - Semantic tokens: text, surface, border, interactive, feedback colours; typography roles; layout spacing - Component tokens: Button (4 sizes), Input (2 sizes) - Generated outputs: CSS custom properties, JS ES6 module, flat JSON Atoms (3 components): - Button: contained/soft/outlined/text × primary/secondary, 4 sizes, loading state, underline for text variant - Typography: 21 variants across display/heading/body/label/caption/overline, maxLines truncation - Input: external label, helper text, error/success validation, start/end icons, required indicator, 2 sizes, multiline support Infrastructure: - MUI v5 theme with full token mapping - Storybook 8 with autodocs - Claude Code agents and skills for token/component workflows - Design system documentation and cross-session memory Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
365
src/components/atoms/Button/Button.stories.tsx
Normal file
365
src/components/atoms/Button/Button.stories.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Button } from './Button';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Atoms/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/design/3t6fpT5inh7zzjxQdW8U5p/Design-System---Template?node-id=28-50',
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['contained', 'soft', 'outlined', 'text'],
|
||||
description: 'Visual style variant',
|
||||
table: { defaultValue: { summary: 'contained' } },
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary'],
|
||||
description: 'Colour intent',
|
||||
table: { defaultValue: { summary: 'primary' } },
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['xs', 'small', 'medium', 'large'],
|
||||
description: 'Size preset',
|
||||
table: { defaultValue: { summary: 'medium' } },
|
||||
},
|
||||
loading: {
|
||||
control: 'boolean',
|
||||
description: 'Show loading spinner and disable interaction',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
underline: {
|
||||
control: 'boolean',
|
||||
description: 'Underline decoration for text variant buttons',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disable the button',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
fullWidth: {
|
||||
control: 'boolean',
|
||||
description: 'Stretch to full width of parent container',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
onClick: { action: 'clicked' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
// ─── Default ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Default button appearance — primary contained, medium size */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Get started',
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Figma Mapping ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Maps directly to the Figma button component columns:
|
||||
* - **Primary** → `contained` + `primary` (strong copper fill)
|
||||
* - **Secondary/Brand** → `soft` + `primary` (warm tonal fill)
|
||||
* - **Secondary/Grey** → `soft` + `secondary` (neutral tonal fill)
|
||||
* - **Ghost** → `text` + `primary` (no fill, copper text)
|
||||
*/
|
||||
export const FigmaMapping: Story = {
|
||||
name: 'Figma Mapping',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button variant="contained">Primary</Button>
|
||||
<Button variant="soft">Sec / Brand</Button>
|
||||
<Button variant="soft" color="secondary">Sec / Grey</Button>
|
||||
<Button variant="text">Ghost</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Variants ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** All visual variants for primary (brand) colour */
|
||||
export const VariantsPrimary: Story = {
|
||||
name: 'Variants — Primary',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button variant="contained">Contained</Button>
|
||||
<Button variant="soft">Soft</Button>
|
||||
<Button variant="outlined">Outlined</Button>
|
||||
<Button variant="text">Text</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** All visual variants for secondary (neutral grey) colour */
|
||||
export const VariantsSecondary: Story = {
|
||||
name: 'Variants — Secondary',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button variant="contained" color="secondary">Contained</Button>
|
||||
<Button variant="soft" color="secondary">Soft</Button>
|
||||
<Button variant="outlined" color="secondary">Outlined</Button>
|
||||
<Button variant="text" color="secondary">Text</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Sizes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** All four sizes side by side */
|
||||
export const AllSizes: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button size="xs">Extra small</Button>
|
||||
<Button size="small">Small</Button>
|
||||
<Button size="medium">Medium</Button>
|
||||
<Button size="large">Large</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** All sizes in soft variant */
|
||||
export const AllSizesSoft: Story = {
|
||||
name: 'All Sizes — Soft',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button variant="soft" size="xs">Extra small</Button>
|
||||
<Button variant="soft" size="small">Small</Button>
|
||||
<Button variant="soft" size="medium">Medium</Button>
|
||||
<Button variant="soft" size="large">Large</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── With Icons ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Button with a leading (start) icon */
|
||||
export const WithStartIcon: Story = {
|
||||
args: {
|
||||
children: 'Add to package',
|
||||
startIcon: <AddIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
/** Button with a trailing (end) icon */
|
||||
export const WithEndIcon: Story = {
|
||||
args: {
|
||||
children: 'Continue',
|
||||
endIcon: <ArrowForwardIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
/** Button with both leading and trailing icons */
|
||||
export const WithBothIcons: Story = {
|
||||
args: {
|
||||
children: 'Search',
|
||||
startIcon: <SearchIcon />,
|
||||
endIcon: <ArrowForwardIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
/** Icons across all sizes */
|
||||
export const IconsAllSizes: Story = {
|
||||
name: 'Icons — All Sizes',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button size="xs" startIcon={<AddIcon />}>Add</Button>
|
||||
<Button size="small" startIcon={<AddIcon />}>Add</Button>
|
||||
<Button size="medium" startIcon={<AddIcon />}>Add</Button>
|
||||
<Button size="large" startIcon={<AddIcon />}>Add</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── States ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Disabled button */
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: 'Unavailable',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** Disabled across all variants */
|
||||
export const DisabledAllVariants: Story = {
|
||||
name: 'Disabled — All Variants',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button disabled>Contained</Button>
|
||||
<Button disabled variant="soft">Soft</Button>
|
||||
<Button disabled variant="outlined">Outlined</Button>
|
||||
<Button disabled variant="text">Text</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** Loading state with spinner (spinner appears on the right) */
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: 'Submitting...',
|
||||
loading: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** Loading across variants */
|
||||
export const LoadingAllVariants: Story = {
|
||||
name: 'Loading — All Variants',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button loading>Submitting...</Button>
|
||||
<Button loading variant="soft">Processing...</Button>
|
||||
<Button loading variant="outlined">Processing...</Button>
|
||||
<Button loading variant="text">Loading...</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Loading → Success Pattern ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Demonstrates the recommended loading → success flow for async actions.
|
||||
*
|
||||
* The Button itself stays simple — the consumer controls the state
|
||||
* by toggling `loading`, `children`, and `endIcon`. Click to see the flow.
|
||||
*/
|
||||
export const LoadingToSuccess: Story = {
|
||||
name: 'Loading → Success Pattern',
|
||||
render: function LoadingSuccessDemo() {
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle');
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'loading') {
|
||||
const timer = setTimeout(() => setStatus('success'), 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
if (status === 'success') {
|
||||
const timer = setTimeout(() => setStatus('idle'), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'center' }}>
|
||||
<Button
|
||||
loading={status === 'loading'}
|
||||
endIcon={status === 'success' ? <CheckIcon /> : undefined}
|
||||
color={status === 'success' ? 'success' : 'primary'}
|
||||
onClick={() => setStatus('loading')}
|
||||
>
|
||||
{status === 'idle' && 'Add to package'}
|
||||
{status === 'loading' && 'Adding...'}
|
||||
{status === 'success' && 'Added'}
|
||||
</Button>
|
||||
<p style={{ fontSize: 12, color: '#737373', margin: 0 }}>
|
||||
Click to see: idle → loading → success → idle
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Text / Underline ───────────────────────────────────────────────────────
|
||||
|
||||
/** Text button with underline decoration (link-style) */
|
||||
export const TextWithUnderline: Story = {
|
||||
args: {
|
||||
children: 'Go back',
|
||||
variant: 'text',
|
||||
underline: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** Text buttons with and without underline */
|
||||
export const TextButtonComparison: Story = {
|
||||
name: 'Text Buttons — With & Without Underline',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
|
||||
<Button variant="text">No underline</Button>
|
||||
<Button variant="text" underline>With underline</Button>
|
||||
<Button variant="text" color="secondary">Secondary</Button>
|
||||
<Button variant="text" color="secondary" underline>Secondary underlined</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** Text button sizes (from the merged Text Button Figma component) */
|
||||
export const TextButtonSizes: Story = {
|
||||
name: 'Text Buttons — All Sizes',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<Button variant="text" size="xs">Extra small</Button>
|
||||
<Button variant="text" size="small">Small</Button>
|
||||
<Button variant="text" size="medium">Medium</Button>
|
||||
<Button variant="text" size="large">Large</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Full Width ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Full width button (useful in mobile layouts and forms) */
|
||||
export const FullWidth: Story = {
|
||||
args: {
|
||||
children: 'Complete arrangement',
|
||||
fullWidth: true,
|
||||
size: 'large',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: 360 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
// ─── Edge Cases ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Long content to test text wrapping and overflow */
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
children: 'Add funeral arrangement to your saved packages for comparison',
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Complete Matrix ────────────────────────────────────────────────────────
|
||||
|
||||
/** Full variant x colour matrix for visual QA */
|
||||
export const CompleteMatrix: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
{(['contained', 'soft', 'outlined', 'text'] as const).map((variant) => (
|
||||
<div key={variant}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600, fontSize: 14, textTransform: 'capitalize' }}>
|
||||
{variant}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Button variant={variant} color="primary">Primary</Button>
|
||||
<Button variant={variant} color="secondary">Secondary</Button>
|
||||
<Button variant={variant} color="primary" startIcon={<AddIcon />}>With icon</Button>
|
||||
<Button variant={variant} color="primary" disabled>Disabled</Button>
|
||||
<Button variant={variant} color="primary" loading>Loading...</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
79
src/components/atoms/Button/Button.tsx
Normal file
79
src/components/atoms/Button/Button.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import MuiButton from '@mui/material/Button';
|
||||
import type { ButtonProps as MuiButtonProps } from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Props for the FA Button component */
|
||||
export interface ButtonProps extends MuiButtonProps {
|
||||
/** Show a loading spinner and disable interaction */
|
||||
loading?: boolean;
|
||||
/** Add underline decoration (useful for text variant link-style buttons) */
|
||||
underline?: boolean;
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Primary interactive element for the FA design system.
|
||||
*
|
||||
* Wraps MUI Button with FA brand tokens, custom sizes (xs/sm/md/lg),
|
||||
* loading state, and underline support for text-variant buttons.
|
||||
*
|
||||
* Variant mapping from design:
|
||||
* - `contained` + `primary` — Primary CTA (copper fill)
|
||||
* - `soft` + `primary` — Secondary/Brand (warm tonal fill)
|
||||
* - `soft` + `secondary` — Secondary/Grey (neutral tonal fill)
|
||||
* - `outlined` + `primary` — Outlined brand (copper border)
|
||||
* - `outlined` + `secondary` — Outlined grey (neutral border)
|
||||
* - `text` + `primary` — Ghost / text button (copper text)
|
||||
* - `text` + `secondary` — Ghost secondary (grey text)
|
||||
*/
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
loading = false,
|
||||
underline = false,
|
||||
disabled,
|
||||
children,
|
||||
variant = 'contained',
|
||||
size = 'medium',
|
||||
sx,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<MuiButton
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
disabled={loading || disabled}
|
||||
sx={[
|
||||
underline &&
|
||||
variant === 'text' && {
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '3px',
|
||||
'&:hover': { textDecoration: 'underline' },
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
size={16}
|
||||
color="inherit"
|
||||
thickness={3}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
</MuiButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
export default Button;
|
||||
3
src/components/atoms/Button/index.ts
Normal file
3
src/components/atoms/Button/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default } from './Button';
|
||||
export { Button } from './Button';
|
||||
export type { ButtonProps } from './Button';
|
||||
506
src/components/atoms/Input/Input.stories.tsx
Normal file
506
src/components/atoms/Input/Input.stories.tsx
Normal file
@@ -0,0 +1,506 @@
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Input } from './Input';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined';
|
||||
import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import AttachMoneyIcon from '@mui/icons-material/AttachMoney';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import { Button } from '../Button';
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: 'Atoms/Input',
|
||||
component: Input,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/design/3t6fpT5inh7zzjxQdW8U5p/Design-System---Template?node-id=39-713',
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
label: {
|
||||
control: 'text',
|
||||
description: 'Label text displayed above the input',
|
||||
},
|
||||
helperText: {
|
||||
control: 'text',
|
||||
description: 'Helper/description text displayed below the input',
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
description: 'Placeholder text',
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['small', 'medium'],
|
||||
description: 'Size preset',
|
||||
table: { defaultValue: { summary: 'medium' } },
|
||||
},
|
||||
error: {
|
||||
control: 'boolean',
|
||||
description: 'Show error validation state',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
success: {
|
||||
control: 'boolean',
|
||||
description: 'Show success validation state',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disable the input',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
required: {
|
||||
control: 'boolean',
|
||||
description: 'Mark as required (adds asterisk to label)',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
fullWidth: {
|
||||
control: 'boolean',
|
||||
description: 'Stretch to full width of parent container',
|
||||
table: { defaultValue: { summary: 'true' } },
|
||||
},
|
||||
multiline: {
|
||||
control: 'boolean',
|
||||
description: 'Render as a textarea',
|
||||
table: { defaultValue: { summary: 'false' } },
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: 400 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Input>;
|
||||
|
||||
// ─── Default ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Default input appearance — medium size, full width */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Full name',
|
||||
placeholder: 'Enter your full name',
|
||||
helperText: 'As it appears on official documents',
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Figma Mapping ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Maps directly to the Figma input component properties:
|
||||
* - **label=true** → `label` prop
|
||||
* - **description=true** → `helperText` prop
|
||||
* - **trailing.icon=true** → `endIcon` prop
|
||||
* - **placeholder=true** → `placeholder` prop
|
||||
*/
|
||||
export const FigmaMapping: Story = {
|
||||
name: 'Figma Mapping',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<Input
|
||||
label="Label Header"
|
||||
placeholder="Select an option"
|
||||
helperText="Input Label - Description"
|
||||
endIcon={<SearchIcon />}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Select an option"
|
||||
helperText="Input Label - Description"
|
||||
endIcon={<SearchIcon />}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Select an option"
|
||||
endIcon={<SearchIcon />}
|
||||
/>
|
||||
<Input placeholder="Select an option" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── States ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** All visual states matching the Figma design */
|
||||
export const AllStates: Story = {
|
||||
name: 'All States',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<Input
|
||||
label="Default"
|
||||
placeholder="Enter text..."
|
||||
helperText="Resting state — neutral border"
|
||||
/>
|
||||
<Input
|
||||
label="Filled"
|
||||
defaultValue="John Smith"
|
||||
helperText="Has a value — text colour changes from placeholder to primary"
|
||||
/>
|
||||
<Input
|
||||
label="Error (empty)"
|
||||
placeholder="Enter text..."
|
||||
error
|
||||
helperText="This field is required"
|
||||
/>
|
||||
<Input
|
||||
label="Error (filled)"
|
||||
defaultValue="invalid@"
|
||||
error
|
||||
helperText="Please enter a valid email address"
|
||||
/>
|
||||
<Input
|
||||
label="Success"
|
||||
defaultValue="john.smith@example.com"
|
||||
success
|
||||
helperText="Email address verified"
|
||||
/>
|
||||
<Input
|
||||
label="Disabled (empty)"
|
||||
placeholder="Enter text..."
|
||||
disabled
|
||||
helperText="This field is currently unavailable"
|
||||
/>
|
||||
<Input
|
||||
label="Disabled (filled)"
|
||||
defaultValue="Pre-filled value"
|
||||
disabled
|
||||
helperText="This value cannot be changed"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Required ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Required field with asterisk indicator */
|
||||
export const Required: Story = {
|
||||
args: {
|
||||
label: 'Email address',
|
||||
placeholder: 'you@example.com',
|
||||
helperText: 'We will use this to send the arrangement confirmation',
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Sizes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Both sizes side by side */
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<Input
|
||||
label="Medium (48px) — default"
|
||||
placeholder="Standard form input"
|
||||
size="medium"
|
||||
helperText="Matches Button large height for alignment"
|
||||
/>
|
||||
<Input
|
||||
label="Small (40px) — compact"
|
||||
placeholder="Compact form input"
|
||||
size="small"
|
||||
helperText="Matches Button medium height for dense layouts"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** Size comparison with Buttons (for search bar alignment) */
|
||||
export const SizeAlignment: Story = {
|
||||
name: 'Size Alignment with Button',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
|
||||
<Input
|
||||
placeholder="Search arrangements..."
|
||||
endIcon={<SearchIcon />}
|
||||
size="medium"
|
||||
/>
|
||||
<Button size="large" sx={{ minWidth: 100, minHeight: 48 }}>Search</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
|
||||
<Input
|
||||
placeholder="Quick search..."
|
||||
endIcon={<SearchIcon />}
|
||||
size="small"
|
||||
/>
|
||||
<Button size="medium" sx={{ minWidth: 100, minHeight: 40 }}>Search</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── With Icons ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Leading and trailing icon examples */
|
||||
export const WithIcons: Story = {
|
||||
name: 'With Icons',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<Input
|
||||
label="Search"
|
||||
placeholder="Search services..."
|
||||
endIcon={<SearchIcon />}
|
||||
/>
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="you@example.com"
|
||||
startIcon={<EmailOutlinedIcon />}
|
||||
type="email"
|
||||
/>
|
||||
<Input
|
||||
label="Phone"
|
||||
placeholder="+61 400 000 000"
|
||||
startIcon={<PhoneOutlinedIcon />}
|
||||
type="tel"
|
||||
/>
|
||||
<Input
|
||||
label="Amount"
|
||||
placeholder="0.00"
|
||||
startIcon={<AttachMoneyIcon />}
|
||||
type="number"
|
||||
/>
|
||||
<Input
|
||||
label="Email verified"
|
||||
defaultValue="john@example.com"
|
||||
startIcon={<EmailOutlinedIcon />}
|
||||
endIcon={<CheckCircleOutlineIcon sx={{ color: 'success.main' }} />}
|
||||
success
|
||||
helperText="Email address confirmed"
|
||||
/>
|
||||
<Input
|
||||
label="Email invalid"
|
||||
defaultValue="john@"
|
||||
startIcon={<EmailOutlinedIcon />}
|
||||
endIcon={<ErrorOutlineIcon sx={{ color: 'error.main' }} />}
|
||||
error
|
||||
helperText="Please enter a valid email address"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Password ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Password field with show/hide toggle using raw endAdornment */
|
||||
export const PasswordToggle: Story = {
|
||||
name: 'Password Toggle',
|
||||
render: function PasswordDemo() {
|
||||
const [show, setShow] = useState(false);
|
||||
return (
|
||||
<Input
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
type={show ? 'text' : 'password'}
|
||||
required
|
||||
startIcon={<LockOutlinedIcon />}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label={show ? 'Hide password' : 'Show password'}
|
||||
onClick={() => setShow(!show)}
|
||||
edge="end"
|
||||
size="small"
|
||||
>
|
||||
{show ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlinedIcon />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
helperText="Must be at least 8 characters"
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Multiline ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Multiline textarea for longer text */
|
||||
export const Multiline: Story = {
|
||||
args: {
|
||||
label: 'Special instructions',
|
||||
placeholder: 'Any specific requests or notes for the arrangement...',
|
||||
helperText: 'Optional — include any details that may help us prepare',
|
||||
multiline: true,
|
||||
minRows: 3,
|
||||
maxRows: 6,
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Validation Example ─────────────────────────────────────────────────────
|
||||
|
||||
/** Interactive validation flow */
|
||||
export const ValidationFlow: Story = {
|
||||
name: 'Validation Flow',
|
||||
render: function ValidationDemo() {
|
||||
const [value, setValue] = useState('');
|
||||
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||||
const showError = value.length > 0 && !isValid;
|
||||
const showSuccess = value.length > 0 && isValid;
|
||||
|
||||
return (
|
||||
<Input
|
||||
label="Email address"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
startIcon={<EmailOutlinedIcon />}
|
||||
endIcon={
|
||||
showSuccess ? <CheckCircleOutlineIcon sx={{ color: 'success.main' }} /> :
|
||||
showError ? <ErrorOutlineIcon sx={{ color: 'error.main' }} /> :
|
||||
undefined
|
||||
}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
error={showError}
|
||||
success={showSuccess}
|
||||
helperText={
|
||||
showError ? 'Please enter a valid email address' :
|
||||
showSuccess ? 'Looks good!' :
|
||||
'Required for arrangement confirmation'
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Realistic Form ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Realistic arrangement form layout */
|
||||
export const ArrangementForm: Story = {
|
||||
name: 'Arrangement Form',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: 480 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 20, marginBottom: 4 }}>
|
||||
Contact details
|
||||
</div>
|
||||
<Input
|
||||
label="Full name"
|
||||
placeholder="Enter your full name"
|
||||
required
|
||||
helperText="As it appears on official documents"
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
startIcon={<EmailOutlinedIcon />}
|
||||
type="email"
|
||||
/>
|
||||
<Input
|
||||
label="Phone"
|
||||
placeholder="+61 400 000 000"
|
||||
startIcon={<PhoneOutlinedIcon />}
|
||||
type="tel"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Relationship to the deceased"
|
||||
placeholder="e.g. Son, Daughter, Partner, Friend"
|
||||
helperText="This helps us personalise the arrangement"
|
||||
/>
|
||||
<Input
|
||||
label="Special instructions"
|
||||
placeholder="Any specific requests or notes..."
|
||||
multiline
|
||||
minRows={3}
|
||||
helperText="Optional"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Complete Matrix ────────────────────────────────────────────────────────
|
||||
|
||||
/** Full state matrix for visual QA — all states across both sizes */
|
||||
export const CompleteMatrix: Story = {
|
||||
name: 'Complete Matrix',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: 600 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||||
{(['medium', 'small'] as const).map((size) => (
|
||||
<div key={size}>
|
||||
<div style={{ marginBottom: 12, fontWeight: 600, fontSize: 14, textTransform: 'uppercase', letterSpacing: 1, color: '#737373' }}>
|
||||
Size: {size} ({size === 'medium' ? '48px' : '40px'})
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<Input
|
||||
size={size}
|
||||
label="Default"
|
||||
placeholder="Enter text..."
|
||||
helperText="Helper text"
|
||||
endIcon={<SearchIcon />}
|
||||
/>
|
||||
<Input
|
||||
size={size}
|
||||
label="Filled"
|
||||
defaultValue="Entered value"
|
||||
helperText="Helper text"
|
||||
endIcon={<SearchIcon />}
|
||||
/>
|
||||
<Input
|
||||
size={size}
|
||||
label="Required"
|
||||
placeholder="Required field..."
|
||||
helperText="This field is required"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
size={size}
|
||||
label="Error"
|
||||
defaultValue="Invalid input"
|
||||
error
|
||||
helperText="Validation error message"
|
||||
endIcon={<ErrorOutlineIcon sx={{ color: 'error.main' }} />}
|
||||
/>
|
||||
<Input
|
||||
size={size}
|
||||
label="Success"
|
||||
defaultValue="Valid input"
|
||||
success
|
||||
helperText="Validation success message"
|
||||
endIcon={<CheckCircleOutlineIcon sx={{ color: 'success.main' }} />}
|
||||
/>
|
||||
<Input
|
||||
size={size}
|
||||
label="Disabled"
|
||||
placeholder="Unavailable"
|
||||
disabled
|
||||
helperText="This field is disabled"
|
||||
/>
|
||||
<Input
|
||||
size={size}
|
||||
label="Disabled filled"
|
||||
defaultValue="Pre-filled"
|
||||
disabled
|
||||
helperText="This value is locked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
184
src/components/atoms/Input/Input.tsx
Normal file
184
src/components/atoms/Input/Input.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React from 'react';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import type { OutlinedInputProps } from '@mui/material/OutlinedInput';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Props for the FA Input component */
|
||||
export interface InputProps extends Omit<OutlinedInputProps, 'notched' | 'label'> {
|
||||
/** Label text displayed above the input */
|
||||
label?: string;
|
||||
/** Helper/description text displayed below the input */
|
||||
helperText?: React.ReactNode;
|
||||
/** Show success validation state (green border and helper text) */
|
||||
success?: boolean;
|
||||
/** Icon element to show at the start (left) of the input */
|
||||
startIcon?: React.ReactNode;
|
||||
/** Icon element to show at the end (right) of the input */
|
||||
endIcon?: React.ReactNode;
|
||||
/** Whether the input takes full width of its container */
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Text input component for the FA design system.
|
||||
*
|
||||
* Wraps MUI OutlinedInput with an external label pattern, FA brand tokens,
|
||||
* two sizes (small/medium), and success/error validation states.
|
||||
*
|
||||
* Features:
|
||||
* - External label with required asterisk indicator
|
||||
* - Helper text that contextually colours for error/success
|
||||
* - Leading and trailing icon slots (via `startIcon`/`endIcon`)
|
||||
* - Branded focus ring (warm gold double-ring from Figma)
|
||||
* - Two sizes: `medium` (48px, default) and `small` (40px)
|
||||
* - Multiline/textarea support via `multiline` + `rows`/`minRows`
|
||||
*
|
||||
* State mapping from Figma design:
|
||||
* - Default → resting state, neutral border
|
||||
* - Hover → darker border (CSS :hover)
|
||||
* - Focus → brand.500 border + double focus ring
|
||||
* - Error → `error` prop — red border + red helper text
|
||||
* - Success → `success` prop — green border + green helper text
|
||||
* - Disabled → `disabled` prop — grey background, muted text
|
||||
*/
|
||||
export const Input = React.forwardRef<HTMLDivElement, InputProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
success = false,
|
||||
error = false,
|
||||
required = false,
|
||||
disabled = false,
|
||||
fullWidth = true,
|
||||
startIcon,
|
||||
endIcon,
|
||||
startAdornment,
|
||||
endAdornment,
|
||||
id,
|
||||
size = 'medium',
|
||||
sx,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const autoId = React.useId();
|
||||
const inputId = id || autoId;
|
||||
const helperId = helperText ? `${inputId}-helper` : undefined;
|
||||
|
||||
// Prefer convenience icon props; fall back to raw adornment props
|
||||
const resolvedStart = startIcon ? (
|
||||
<InputAdornment position="start">{startIcon}</InputAdornment>
|
||||
) : startAdornment;
|
||||
|
||||
const resolvedEnd = endIcon ? (
|
||||
<InputAdornment position="end">{endIcon}</InputAdornment>
|
||||
) : endAdornment;
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
ref={ref}
|
||||
fullWidth={fullWidth}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
>
|
||||
{label && (
|
||||
<InputLabel
|
||||
htmlFor={inputId}
|
||||
shrink
|
||||
sx={{
|
||||
position: 'static',
|
||||
transform: 'none',
|
||||
maxWidth: 'none',
|
||||
pointerEvents: 'auto',
|
||||
mb: '10px',
|
||||
// labelLg typography
|
||||
fontFamily: (theme) => theme.typography.labelLg.fontFamily,
|
||||
fontSize: (theme) => theme.typography.labelLg.fontSize,
|
||||
fontWeight: (theme) => theme.typography.labelLg.fontWeight,
|
||||
lineHeight: (theme) => theme.typography.labelLg.lineHeight,
|
||||
letterSpacing: (theme) =>
|
||||
(theme.typography.labelLg as { letterSpacing?: string }).letterSpacing ?? 'normal',
|
||||
color: 'text.secondary',
|
||||
// Label stays neutral on error/focus/success (per Figma design)
|
||||
'&.Mui-focused': { color: 'text.secondary' },
|
||||
'&.Mui-error': { color: 'text.secondary' },
|
||||
'&.Mui-disabled': { color: 'text.disabled' },
|
||||
// Required asterisk in error red
|
||||
'& .MuiInputLabel-asterisk': { color: 'error.main' },
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
)}
|
||||
|
||||
<OutlinedInput
|
||||
id={inputId}
|
||||
size={size}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
notched={false}
|
||||
startAdornment={resolvedStart}
|
||||
endAdornment={resolvedEnd}
|
||||
aria-describedby={helperId}
|
||||
sx={[
|
||||
// Success border + focus ring (not a native MUI state)
|
||||
success && !error && {
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'success.main',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'success.main',
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
boxShadow: (theme: Record<string, any>) =>
|
||||
`0 0 0 3px ${theme.palette.common.white}, 0 0 0 5px ${theme.palette.success.main}`,
|
||||
},
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{helperText && (
|
||||
<FormHelperText
|
||||
id={helperId}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
role={error ? 'alert' : undefined}
|
||||
sx={{
|
||||
mx: 0,
|
||||
mt: '6px',
|
||||
// caption typography
|
||||
fontFamily: (theme) => theme.typography.caption.fontFamily,
|
||||
fontSize: (theme) => theme.typography.caption.fontSize,
|
||||
fontWeight: (theme) => theme.typography.caption.fontWeight,
|
||||
lineHeight: (theme) => theme.typography.caption.lineHeight,
|
||||
letterSpacing: (theme) => theme.typography.caption.letterSpacing,
|
||||
// Contextual colour: error > success > secondary
|
||||
...(error
|
||||
? { color: 'error.main' }
|
||||
: success
|
||||
? { color: 'success.main' }
|
||||
: { color: 'text.secondary' }),
|
||||
}}
|
||||
>
|
||||
{helperText}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
export default Input;
|
||||
2
src/components/atoms/Input/index.ts
Normal file
2
src/components/atoms/Input/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Input, default } from './Input';
|
||||
export type { InputProps } from './Input';
|
||||
345
src/components/atoms/Typography/Typography.stories.tsx
Normal file
345
src/components/atoms/Typography/Typography.stories.tsx
Normal file
@@ -0,0 +1,345 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Typography } from './Typography';
|
||||
|
||||
const meta: Meta<typeof Typography> = {
|
||||
title: 'Atoms/Typography',
|
||||
component: Typography,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/design/3t6fpT5inh7zzjxQdW8U5p/Design-System---Template?node-id=23-30',
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'displayHero', 'display1', 'display2', 'display3', 'displaySm',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'bodyLg', 'body1', 'body2', 'bodyXs',
|
||||
'labelLg', 'label', 'labelSm',
|
||||
'caption', 'captionSm',
|
||||
'overline', 'overlineSm',
|
||||
],
|
||||
description: 'Typography variant — 21 variants across 6 categories',
|
||||
table: { defaultValue: { summary: 'body1' } },
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'textPrimary', 'textSecondary', 'textDisabled',
|
||||
'primary', 'secondary', 'error',
|
||||
],
|
||||
},
|
||||
maxLines: { control: 'number' },
|
||||
gutterBottom: { control: 'boolean' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Typography>;
|
||||
|
||||
const SAMPLE = 'Discover, Explore, and Plan Funerals in Minutes, Not Hours';
|
||||
|
||||
// ─── Default ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Funeral Arranger helps families find transparent, affordable funeral services across Australia.',
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Display (Noto Serif SC, Regular) ───────────────────────────────────────
|
||||
|
||||
/** 5 display levels — Noto Serif SC Regular. For hero/marketing text. All scale down on mobile. */
|
||||
export const Display: Story = {
|
||||
name: 'Display (Serif)',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">displayHero — 80px</Typography>
|
||||
<Typography variant="displayHero">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">display1 — 64px</Typography>
|
||||
<Typography variant="display1">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">display2 — 52px</Typography>
|
||||
<Typography variant="display2">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">display3 — 40px</Typography>
|
||||
<Typography variant="display3">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">displaySm — 32px</Typography>
|
||||
<Typography variant="displaySm">{SAMPLE}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Headings (Montserrat, Bold) ────────────────────────────────────────────
|
||||
|
||||
/** 6 heading levels — Montserrat Bold. For content structure. All scale down on mobile. */
|
||||
export const Headings: Story = {
|
||||
name: 'Headings (Sans-serif)',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">h1 — 36px</Typography>
|
||||
<Typography variant="h1">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">h2 — 30px</Typography>
|
||||
<Typography variant="h2">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">h3 — 24px</Typography>
|
||||
<Typography variant="h3">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">h4 — 20px</Typography>
|
||||
<Typography variant="h4">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">h5 — 18px</Typography>
|
||||
<Typography variant="h5">{SAMPLE}</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">h6 — 16px</Typography>
|
||||
<Typography variant="h6">{SAMPLE}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Body (Montserrat, Medium) ──────────────────────────────────────────────
|
||||
|
||||
/** 4 body sizes — Montserrat Medium (500). For content text. */
|
||||
export const Body: Story = {
|
||||
name: 'Body Text',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 640 }}>
|
||||
<div>
|
||||
<Typography variant="overline" gutterBottom>bodyLg — 18px</Typography>
|
||||
<Typography variant="bodyLg">
|
||||
Planning a funeral is one of the most difficult tasks a family faces. Funeral Arranger
|
||||
is here to help you navigate this process with care and transparency.
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="overline" gutterBottom>body1 (default) — 16px</Typography>
|
||||
<Typography variant="body1">
|
||||
Compare funeral directors in your area, view transparent pricing, and make informed
|
||||
decisions at your own pace. Every family deserves clarity during this time.
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="overline" gutterBottom>body2 (small) — 14px</Typography>
|
||||
<Typography variant="body2">
|
||||
Prices shown are indicative and may vary based on your specific requirements.
|
||||
Contact the funeral director directly for a detailed quote.
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="overline" gutterBottom>bodyXs — 12px</Typography>
|
||||
<Typography variant="bodyXs">
|
||||
Terms and conditions apply. Funeral Arranger is a comparison service and does not
|
||||
directly provide funeral services. ABN 12 345 678 901.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Label, Caption, Overline ───────────────────────────────────────────────
|
||||
|
||||
/** UI text variants — labels (medium 500), captions (regular 400), overlines (semibold 600 uppercase) */
|
||||
export const UIText: Story = {
|
||||
name: 'UI Text (Label / Caption / Overline)',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">labelLg — 16px medium</Typography>
|
||||
<Typography variant="labelLg" display="block">Form label or section label</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">label — 14px medium</Typography>
|
||||
<Typography variant="label" display="block">Default form label</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">labelSm — 12px medium</Typography>
|
||||
<Typography variant="labelSm" display="block">Compact label or tag text</Typography>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Typography variant="captionSm" color="textSecondary">caption — 12px regular</Typography>
|
||||
<Typography variant="caption" display="block">Fine print, timestamps, metadata</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">captionSm — 11px regular</Typography>
|
||||
<Typography variant="captionSm" display="block">Compact metadata, footnotes</Typography>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Typography variant="captionSm" color="textSecondary">overline — 12px semibold uppercase</Typography>
|
||||
<Typography variant="overline" display="block">Section overline</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="captionSm" color="textSecondary">overlineSm — 11px semibold uppercase</Typography>
|
||||
<Typography variant="overlineSm" display="block">Compact overline</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Colours ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Colours: Story = {
|
||||
name: 'Colours',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Typography color="textPrimary">Text Primary — main body text (neutral.800)</Typography>
|
||||
<Typography color="textSecondary">Text Secondary — helper text (neutral.600)</Typography>
|
||||
<Typography color="textDisabled">Text Disabled — inactive (neutral.400)</Typography>
|
||||
<Typography color="primary">Primary — brand emphasis (brand.600)</Typography>
|
||||
<Typography color="secondary">Secondary — neutral emphasis (neutral.600)</Typography>
|
||||
<Typography color="error">Error — validation errors (red.600)</Typography>
|
||||
<Typography color="warning.main">Warning — cautionary (amber.600)</Typography>
|
||||
<Typography color="success.main">Success — confirmations (green.600)</Typography>
|
||||
<Typography color="info.main">Info — helpful tips (blue.600)</Typography>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Font Families ──────────────────────────────────────────────────────────
|
||||
|
||||
/** The two font families: serif for display, sans-serif for everything else */
|
||||
export const FontFamilies: Story = {
|
||||
name: 'Font Families',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<div>
|
||||
<Typography variant="overline" gutterBottom>Display font — Noto Serif SC (Regular 400)</Typography>
|
||||
<Typography variant="display3">
|
||||
Warm, trustworthy, and professional
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary" sx={{ mt: 1 }}>
|
||||
Used exclusively for display variants (hero through sm). Regular weight — serif carries inherent visual weight at large sizes.
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="overline" gutterBottom>Body font — Montserrat</Typography>
|
||||
<Typography variant="h3" gutterBottom>Clean, modern, and highly readable</Typography>
|
||||
<Typography>
|
||||
Used for all headings (h1–h6), body text, labels, captions, and UI elements.
|
||||
Headings use Bold (700), body uses Medium (500), captions use Regular (400).
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Max Lines ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const MaxLines: Story = {
|
||||
name: 'Max Lines (Truncation)',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 400 }}>
|
||||
<div>
|
||||
<Typography variant="label" gutterBottom>maxLines=1</Typography>
|
||||
<Typography maxLines={1}>
|
||||
H. Parsons Funeral Directors — trusted by Australian families for over 30 years,
|
||||
providing compassionate and transparent funeral services.
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="label" gutterBottom>maxLines=2</Typography>
|
||||
<Typography maxLines={2}>
|
||||
H. Parsons Funeral Directors — trusted by Australian families for over 30 years,
|
||||
providing compassionate and transparent funeral services across metropolitan
|
||||
and regional areas.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Realistic Content ──────────────────────────────────────────────────────
|
||||
|
||||
export const RealisticContent: Story = {
|
||||
name: 'Realistic Content',
|
||||
render: () => (
|
||||
<div style={{ maxWidth: 640, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<Typography variant="overline">Funeral planning</Typography>
|
||||
<Typography variant="display3">Compare funeral services in your area</Typography>
|
||||
<Typography variant="bodyLg" color="textSecondary">
|
||||
Transparent pricing and service comparison to help you make informed
|
||||
decisions during a difficult time.
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 2 }}>How it works</Typography>
|
||||
<Typography>
|
||||
Enter your suburb or postcode to find funeral directors near you. Each
|
||||
listing includes a full price breakdown, service inclusions, and reviews
|
||||
from families who have used their services.
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ mt: 1 }}>Step 1: Browse packages</Typography>
|
||||
<Typography>
|
||||
Compare packages side by side. Each package clearly shows what is and
|
||||
isn't included, so there are no surprises.
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary" sx={{ mt: 2 }}>
|
||||
Prices are indicative and current as of March 2026. Contact the funeral
|
||||
director for a binding quote.
|
||||
</Typography>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Complete Scale ─────────────────────────────────────────────────────────
|
||||
|
||||
/** All 21 variants in a single view — matches the Figma "Fonts - Desktop" layout */
|
||||
export const CompleteScale: Story = {
|
||||
name: 'Complete Scale (All 21 Variants)',
|
||||
render: () => {
|
||||
const variants = [
|
||||
{ variant: 'displayHero', label: 'display/hero — 80px' },
|
||||
{ variant: 'display1', label: 'display/1 — 64px' },
|
||||
{ variant: 'display2', label: 'display/2 — 52px' },
|
||||
{ variant: 'display3', label: 'display/3 — 40px' },
|
||||
{ variant: 'displaySm', label: 'display/sm — 32px' },
|
||||
{ variant: 'h1', label: 'heading/1 — 36px' },
|
||||
{ variant: 'h2', label: 'heading/2 — 30px' },
|
||||
{ variant: 'h3', label: 'heading/3 — 24px' },
|
||||
{ variant: 'h4', label: 'heading/4 — 20px' },
|
||||
{ variant: 'h5', label: 'heading/5 — 18px' },
|
||||
{ variant: 'h6', label: 'heading/6 — 16px' },
|
||||
{ variant: 'bodyLg', label: 'body/lg — 18px' },
|
||||
{ variant: 'body1', label: 'body/md — 16px' },
|
||||
{ variant: 'body2', label: 'body/sm — 14px' },
|
||||
{ variant: 'bodyXs', label: 'body/xs — 12px' },
|
||||
{ variant: 'labelLg', label: 'label/lg — 16px' },
|
||||
{ variant: 'label', label: 'label/md — 14px' },
|
||||
{ variant: 'labelSm', label: 'label/sm — 12px' },
|
||||
{ variant: 'caption', label: 'caption/md — 12px' },
|
||||
{ variant: 'captionSm', label: 'caption/sm — 11px' },
|
||||
{ variant: 'overline', label: 'overline/md — 12px' },
|
||||
{ variant: 'overlineSm', label: 'overline/sm — 11px' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{variants.map(({ variant, label }) => (
|
||||
<div key={variant} style={{ display: 'flex', alignItems: 'baseline', gap: 16 }}>
|
||||
<Typography variant="captionSm" color="textSecondary" sx={{ width: 160, flexShrink: 0, textAlign: 'right' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant={variant}>{SAMPLE}</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
63
src/components/atoms/Typography/Typography.tsx
Normal file
63
src/components/atoms/Typography/Typography.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import MuiTypography from '@mui/material/Typography';
|
||||
import type { TypographyProps as MuiTypographyProps } from '@mui/material/Typography';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Props for the FA Typography component */
|
||||
export interface TypographyProps extends MuiTypographyProps {
|
||||
/** Truncate text with ellipsis after this many lines (CSS line-clamp) */
|
||||
maxLines?: number;
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Text display component for the FA design system.
|
||||
*
|
||||
* Wraps MUI Typography with FA brand fonts and type scale. All variant
|
||||
* styles (sizes, weights, line heights) come from the MUI theme which
|
||||
* maps to our design tokens.
|
||||
*
|
||||
* Variant guide (21 variants across 6 categories):
|
||||
*
|
||||
* Display (Noto Serif SC, Regular 400):
|
||||
* - `displayHero` 80px, `display1` 64px, `display2` 52px, `display3` 40px, `displaySm` 32px
|
||||
*
|
||||
* Headings (Montserrat, Bold 700):
|
||||
* - `h1` 36px, `h2` 30px, `h3` 24px, `h4` 20px, `h5` 18px, `h6` 16px
|
||||
*
|
||||
* Body (Montserrat, Medium 500):
|
||||
* - `bodyLg` 18px, `body1` 16px, `body2` 14px, `bodyXs` 12px
|
||||
*
|
||||
* Label (Montserrat, Medium 500):
|
||||
* - `labelLg` 16px, `label` 14px, `labelSm` 12px
|
||||
*
|
||||
* Caption (Montserrat, Regular 400):
|
||||
* - `caption` 12px, `captionSm` 11px
|
||||
*
|
||||
* Overline (Montserrat, SemiBold 600, uppercase):
|
||||
* - `overline` 12px, `overlineSm` 11px
|
||||
*/
|
||||
export const Typography = React.forwardRef<HTMLElement, TypographyProps>(
|
||||
({ maxLines, sx, ...props }, ref) => {
|
||||
return (
|
||||
<MuiTypography
|
||||
ref={ref}
|
||||
sx={[
|
||||
maxLines != null && {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: maxLines,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Typography.displayName = 'Typography';
|
||||
export default Typography;
|
||||
3
src/components/atoms/Typography/index.ts
Normal file
3
src/components/atoms/Typography/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default } from './Typography';
|
||||
export { Typography } from './Typography';
|
||||
export type { TypographyProps } from './Typography';
|
||||
20
src/main.tsx
Normal file
20
src/main.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ThemeProvider, CssBaseline } from '@mui/material';
|
||||
import { theme } from './theme';
|
||||
|
||||
const App = () => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<div style={{ padding: 32 }}>
|
||||
<h1>FA Design System</h1>
|
||||
<p>Run <code>npm run storybook</code> to view components.</p>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
354
src/theme/generated/tokens.css
Normal file
354
src/theme/generated/tokens.css
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Do not edit directly, this file was auto-generated.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--fa-button-height-xs: 28px; /** Extra-small — compact text buttons, inline actions */
|
||||
--fa-button-height-sm: 32px; /** Small — secondary actions, toolbar buttons */
|
||||
--fa-button-height-md: 40px; /** Medium — default size, form submissions */
|
||||
--fa-button-height-lg: 48px; /** Large — primary CTAs, mobile touch targets (meets 44px minimum) */
|
||||
--fa-button-icon-size-xs: 14px; /** 14px icons in extra-small buttons */
|
||||
--fa-button-icon-size-sm: 16px; /** 16px icons in small buttons */
|
||||
--fa-button-icon-size-md: 18px; /** 18px icons in medium buttons */
|
||||
--fa-button-icon-size-lg: 20px; /** 20px icons in large buttons */
|
||||
--fa-input-height-sm: 40px; /** Small — compact forms, admin layouts, matches Button medium height */
|
||||
--fa-input-height-md: 48px; /** Medium (default) — standard forms, matches Button large for alignment */
|
||||
--fa-input-icon-size-default: 20px; /** 20px — icon size inside input field, matches Figma trailing icon */
|
||||
--fa-color-brand-50: #fef9f5; /** Lightest warm tint — warm section backgrounds */
|
||||
--fa-color-brand-100: #f7ecdf; /** Light warm — hover backgrounds, subtle fills */
|
||||
--fa-color-brand-200: #ebdac8; /** Warm light — secondary backgrounds, divider tones */
|
||||
--fa-color-brand-300: #d8c3b5; /** Warm beige — from brand swatch. Surface warmth, card tints */
|
||||
--fa-color-brand-400: #d0a070; /** Mid gold — from brand swatch. Secondary interactive, step indicators */
|
||||
--fa-color-brand-500: #ba834e; /** Base brand gold — from brand swatch. Primary CTA colour, main interactive. 3.7:1 contrast on white */
|
||||
--fa-color-brand-600: #b0610f; /** Rich copper — from brand swatch. Hover/emphasis on brand elements. 4.8:1 contrast on white */
|
||||
--fa-color-brand-700: #8b4e0d; /** Deep copper — active states, strong brand text on light backgrounds. 6.7:1 contrast on white */
|
||||
--fa-color-brand-800: #6b3c13; /** Dark brown — bold brand accents, high-contrast brand text */
|
||||
--fa-color-brand-900: #51301b; /** Chocolate — from brand swatch. Deep emphasis, dark brand surfaces */
|
||||
--fa-color-brand-950: #251913; /** Espresso — from brand swatch. Darkest brand tone, near-black warm */
|
||||
--fa-color-sage-50: #f2f5f6; /** Lightest sage — subtle cool backgrounds */
|
||||
--fa-color-sage-100: #e3eaeb; /** Light sage — hover states on cool surfaces */
|
||||
--fa-color-sage-200: #d7e1e2; /** From brand swatch — light cool surface, soft borders */
|
||||
--fa-color-sage-300: #c8d4d6; /** Mid-light sage — dividers, secondary borders */
|
||||
--fa-color-sage-400: #b9c7c9; /** From brand swatch — mid sage, secondary text on dark backgrounds */
|
||||
--fa-color-sage-500: #8ea2a7; /** Base sage — secondary content, muted icons */
|
||||
--fa-color-sage-600: #687d84; /** Dark sage — secondary text, subtle icons */
|
||||
--fa-color-sage-700: #4c5b6b; /** From brand swatch — strong secondary, dark accents. 6.1:1 contrast on white */
|
||||
--fa-color-sage-800: #4c5459; /** From brand swatch — near-dark grey, supplementary text. 6.7:1 contrast on white */
|
||||
--fa-color-sage-900: #343c40; /** Very dark sage — high-contrast secondary elements */
|
||||
--fa-color-sage-950: #1e2528; /** Near-black cool — darkest secondary tone */
|
||||
--fa-color-neutral-50: #fafafa; /** Lightest neutral — default page background alternative */
|
||||
--fa-color-neutral-100: #f5f5f5; /** Light neutral — subtle background differentiation */
|
||||
--fa-color-neutral-200: #e8e8e8; /** Light grey — standard borders, dividers */
|
||||
--fa-color-neutral-300: #d4d4d4; /** Mid-light grey — disabled borders, subtle separators */
|
||||
--fa-color-neutral-400: #a3a3a3; /** Mid grey — placeholder text, disabled content */
|
||||
--fa-color-neutral-500: #737373; /** Base grey — secondary body text, icons */
|
||||
--fa-color-neutral-600: #525252; /** Dark grey — body text, labels. 7.1:1 contrast on white */
|
||||
--fa-color-neutral-700: #404040; /** Strong grey — headings, emphasis text. 9.7:1 contrast on white */
|
||||
--fa-color-neutral-800: #2c2e35; /** From brand swatch — charcoal with cool tint. Primary text colour. 13.2:1 contrast on white */
|
||||
--fa-color-neutral-900: #1a1a1c; /** Near-black — maximum contrast text */
|
||||
--fa-color-neutral-950: #0a0a0b; /** Deepest neutral — use sparingly */
|
||||
--fa-color-red-50: #fef2f2; /** Error tint — error message backgrounds */
|
||||
--fa-color-red-100: #fde8e8; /** Light error — hover on error surfaces */
|
||||
--fa-color-red-200: #f9bfbf; /** Light red — error borders, subtle indicators */
|
||||
--fa-color-red-300: #f09898; /** Mid-light red — error icon backgrounds */
|
||||
--fa-color-red-400: #e56b6b; /** Mid red — error indicators, badges */
|
||||
--fa-color-red-500: #d64545; /** Base red — form validation errors, alert accents */
|
||||
--fa-color-red-600: #bc2f2f; /** Strong red — error text on light backgrounds. 5.7:1 contrast on white */
|
||||
--fa-color-red-700: #9b2424; /** Dark red — error headings, strong alerts */
|
||||
--fa-color-red-800: #7a1d1d; /** Deep red — high-contrast error emphasis */
|
||||
--fa-color-red-900: #5c1616; /** Very dark red — use sparingly */
|
||||
--fa-color-red-950: #3d0e0e; /** Darkest red */
|
||||
--fa-color-amber-50: #fff9eb; /** Warning tint — warning message backgrounds */
|
||||
--fa-color-amber-100: #fff0cc; /** Light amber — hover on warning surfaces */
|
||||
--fa-color-amber-200: #ffe099; /** Light amber — warning borders */
|
||||
--fa-color-amber-300: #ffcc66; /** Mid-light amber — warning icon backgrounds */
|
||||
--fa-color-amber-400: #ffb833; /** Mid amber — warning badges, price alerts */
|
||||
--fa-color-amber-500: #f5a000; /** Base amber — warning accents */
|
||||
--fa-color-amber-600: #cc8500; /** Strong amber — warning text. 3.6:1 contrast on white (large text AA) */
|
||||
--fa-color-amber-700: #a36b00; /** Dark amber — warning headings. 5.1:1 contrast on white */
|
||||
--fa-color-amber-800: #7a5000; /** Deep amber — high-contrast warning emphasis */
|
||||
--fa-color-amber-900: #523600; /** Very dark amber — use sparingly */
|
||||
--fa-color-amber-950: #331f00; /** Darkest amber */
|
||||
--fa-color-green-50: #f0f7f0; /** Success tint — success message backgrounds */
|
||||
--fa-color-green-100: #d8ecd8; /** Light green — hover on success surfaces */
|
||||
--fa-color-green-200: #b8d8b8; /** Light green — success borders */
|
||||
--fa-color-green-300: #8dc08d; /** Mid-light green — success icon backgrounds */
|
||||
--fa-color-green-400: #66a866; /** Mid green — success badges, completion indicators */
|
||||
--fa-color-green-500: #4a8f4a; /** Base green — success accents, completed steps */
|
||||
--fa-color-green-600: #3b7a3b; /** Strong green — success text on light backgrounds. 4.8:1 contrast on white */
|
||||
--fa-color-green-700: #2e6b2e; /** Dark green — success headings */
|
||||
--fa-color-green-800: #235523; /** Deep green — high-contrast success emphasis */
|
||||
--fa-color-green-900: #1a3f1a; /** Very dark green — use sparingly */
|
||||
--fa-color-green-950: #0f2a0f; /** Darkest green */
|
||||
--fa-color-blue-50: #eff6ff; /** Info tint — info message backgrounds */
|
||||
--fa-color-blue-100: #dbeafe; /** Light blue — hover on info surfaces */
|
||||
--fa-color-blue-200: #bfdbfe; /** Light blue — info borders */
|
||||
--fa-color-blue-300: #93c5fd; /** Mid-light blue — info icon backgrounds */
|
||||
--fa-color-blue-400: #60a5fa; /** Mid blue — info badges */
|
||||
--fa-color-blue-500: #3b82f6; /** Base blue — info accents, supplementary links */
|
||||
--fa-color-blue-600: #2563eb; /** Strong blue — info text on light backgrounds. 4.6:1 contrast on white */
|
||||
--fa-color-blue-700: #1d4ed8; /** Dark blue — info headings */
|
||||
--fa-color-blue-800: #1e40af; /** Deep blue — high-contrast info emphasis */
|
||||
--fa-color-blue-900: #1e3a8a; /** Very dark blue — use sparingly */
|
||||
--fa-color-blue-950: #172554; /** Darkest blue */
|
||||
--fa-color-white: #ffffff; /** Pure white — card backgrounds, inverse text, primary surface */
|
||||
--fa-color-black: #000000; /** Pure black — from brand swatch. Use sparingly; prefer neutral.800-900 for text */
|
||||
--fa-color-surface-overlay: rgba(0, 0, 0, 0.5); /** Overlay surface — modal/dialog backdrop at 50% black */
|
||||
--fa-shadow-sm: 0 1px 2px rgba(0,0,0,0.05); /** Subtle lift — resting buttons, input focus subtle elevation */
|
||||
--fa-shadow-md: 0 4px 6px rgba(0,0,0,0.07); /** Medium elevation — cards at rest, dropdowns, popovers */
|
||||
--fa-shadow-lg: 0 10px 15px rgba(0,0,0,0.1); /** High elevation — modals, popovers, card hover states */
|
||||
--fa-shadow-xl: 0 20px 25px rgba(0,0,0,0.1); /** Maximum elevation — elevated panels, dialog boxes */
|
||||
--fa-opacity-disabled: 0.4; /** Disabled state — 40% opacity. Clearly diminished but still distinguishable */
|
||||
--fa-opacity-hover: 0.08; /** Hover overlay — subtle 8% tint applied over backgrounds on hover */
|
||||
--fa-opacity-overlay: 0.5; /** Modal/dialog backdrop — 50% black overlay behind modals */
|
||||
--fa-spacing-1: 4px; /** Tight — inline spacing, minimal gaps between related elements */
|
||||
--fa-spacing-2: 8px; /** Small — related element gaps, compact padding, icon margins */
|
||||
--fa-spacing-3: 12px; /** Component internal padding (small), chip padding */
|
||||
--fa-spacing-4: 16px; /** Default component padding, form field gap, card grid gutter (mobile) */
|
||||
--fa-spacing-5: 20px; /** Medium component spacing */
|
||||
--fa-spacing-6: 24px; /** Card padding, section gap (small), card grid gutter (desktop) */
|
||||
--fa-spacing-8: 32px; /** Section gap (medium), form section separation */
|
||||
--fa-spacing-10: 40px; /** Section gap (large) */
|
||||
--fa-spacing-12: 48px; /** Page section separation, vertical rhythm break */
|
||||
--fa-spacing-16: 64px; /** Hero/banner vertical spacing */
|
||||
--fa-spacing-20: 80px; /** Major page sections, large vertical spacing */
|
||||
--fa-spacing-0-5: 2px; /** Hairline — icon-to-text tight spacing, fine adjustments */
|
||||
--fa-border-radius-none: 0px; /** Square corners — tables, dividers, sharp elements */
|
||||
--fa-border-radius-sm: 4px; /** Small radius — inputs, small interactive elements, chips */
|
||||
--fa-border-radius-md: 8px; /** Medium radius — cards, buttons, dropdowns (default) */
|
||||
--fa-border-radius-lg: 12px; /** Large radius — modals, large cards */
|
||||
--fa-border-radius-xl: 16px; /** Extra large — feature cards, hero elements */
|
||||
--fa-border-radius-full: 9999px; /** Full/pill — avatars, pill buttons, circular elements */
|
||||
--fa-font-family-body: 'Montserrat', 'Helvetica Neue', Arial, sans-serif; /** Primary font — Montserrat. Used for headings (h1-h6), body text, labels, and all UI elements */
|
||||
--fa-font-family-display: 'Noto Serif SC', Georgia, 'Times New Roman', serif; /** Display font — Noto Serif SC. Elegant serif for hero/display text only. Adds warmth and gravitas at large sizes */
|
||||
--fa-font-family-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace; /** Monospace font — for reference numbers (FA-2026-00847), tabular pricing data, and code */
|
||||
--fa-font-size-2xs: 0.6875rem; /** 11px — smallest UI text: compact captions, compact overlines */
|
||||
--fa-font-size-xs: 0.75rem; /** 12px — small text: captions, labels, overlines, body/xs */
|
||||
--fa-font-size-sm: 0.875rem; /** 14px — body small, labels, helper text */
|
||||
--fa-font-size-base: 1rem; /** 16px — default body text, heading/6, label/lg */
|
||||
--fa-font-size-md: 1.125rem; /** 18px — body large, heading/5 */
|
||||
--fa-font-size-lg: 1.25rem; /** 20px — heading/4 */
|
||||
--fa-font-size-xl: 1.5rem; /** 24px — heading/3 */
|
||||
--fa-font-size-2xl: 1.875rem; /** 30px — heading/2 */
|
||||
--fa-font-size-3xl: 2.25rem; /** 36px — heading/1 */
|
||||
--fa-font-size-4xl: 3rem; /** 48px — reserved (legacy) */
|
||||
--fa-font-size-display-1: 4rem; /** 64px — display/1 */
|
||||
--fa-font-size-display-2: 3.25rem; /** 52px — display/2 */
|
||||
--fa-font-size-display-3: 2.5rem; /** 40px — display/3 */
|
||||
--fa-font-size-display-sm: 2rem; /** 32px — display/sm, smallest display text */
|
||||
--fa-font-size-display-hero: 5rem; /** 80px — display/hero, largest display text */
|
||||
--fa-font-size-mobile-display-hero: 2rem; /** 32px — mobile display/hero (from 80px desktop) */
|
||||
--fa-font-size-mobile-display1: 1.75rem; /** 28px — mobile display/1 (from 64px desktop) */
|
||||
--fa-font-size-mobile-display2: 1.5rem; /** 24px — mobile display/2 (from 52px desktop) */
|
||||
--fa-font-size-mobile-display3: 1.375rem; /** 22px — mobile display/3 (from 40px desktop) */
|
||||
--fa-font-size-mobile-display-sm: 1.25rem; /** 20px — mobile display/sm (from 32px desktop) */
|
||||
--fa-font-size-mobile-h1: 1.625rem; /** 26px — mobile heading/1 (from 36px desktop) */
|
||||
--fa-font-size-mobile-h2: 1.375rem; /** 22px — mobile heading/2 (from 30px desktop) */
|
||||
--fa-font-size-mobile-h3: 1.25rem; /** 20px — mobile heading/3 (from 24px desktop) */
|
||||
--fa-font-size-mobile-h4: 1.125rem; /** 18px — mobile heading/4 (from 20px desktop) */
|
||||
--fa-font-size-mobile-h5: 1rem; /** 16px — mobile heading/5 (from 18px desktop) */
|
||||
--fa-font-size-mobile-h6: 0.875rem; /** 14px — mobile heading/6 (from 16px desktop) */
|
||||
--fa-font-weight-regular: 400; /** Regular weight — captions, display text (serif carries inherent weight) */
|
||||
--fa-font-weight-medium: 500; /** Medium weight — body text, labels. Slightly bolder than regular for improved readability */
|
||||
--fa-font-weight-semibold: 600; /** Semibold — overlines, button text, navigation */
|
||||
--fa-font-weight-bold: 700; /** Bold — all headings (h1-h6) */
|
||||
--fa-line-height-tight: 1.25; /** Tight leading — large headings, display text */
|
||||
--fa-line-height-snug: 1.375; /** Snug leading — sub-headings, labels, small text */
|
||||
--fa-line-height-normal: 1.5; /** Normal leading — default body text, optimal readability */
|
||||
--fa-line-height-relaxed: 1.75; /** Relaxed leading — large body text, long-form content */
|
||||
--fa-letter-spacing-tighter: -0.02em; /** Tighter tracking — large display text */
|
||||
--fa-letter-spacing-tight: -0.01em; /** Tight tracking — headings */
|
||||
--fa-letter-spacing-normal: 0em; /** Normal tracking — body text, most content */
|
||||
--fa-letter-spacing-wide: 0.02em; /** Wide tracking — captions, small text */
|
||||
--fa-letter-spacing-wider: 0.05em; /** Wider tracking — labels, UI text */
|
||||
--fa-letter-spacing-widest: 0.08em; /** Widest tracking — overlines, uppercase text */
|
||||
--fa-typography-display-hero-line-height: 1.05;
|
||||
--fa-typography-display-hero-letter-spacing: -1.5px;
|
||||
--fa-typography-display1-line-height: 1.078;
|
||||
--fa-typography-display1-letter-spacing: -1px;
|
||||
--fa-typography-display2-line-height: 1.096;
|
||||
--fa-typography-display2-letter-spacing: -0.5px;
|
||||
--fa-typography-display3-line-height: 1.15;
|
||||
--fa-typography-display3-letter-spacing: -0.25px;
|
||||
--fa-typography-display-sm-line-height: 1.1875;
|
||||
--fa-typography-display-sm-letter-spacing: 0px;
|
||||
--fa-typography-h1-line-height: 1.194;
|
||||
--fa-typography-h1-letter-spacing: -0.5px;
|
||||
--fa-typography-h2-line-height: 1.267;
|
||||
--fa-typography-h2-letter-spacing: -0.25px;
|
||||
--fa-typography-h3-line-height: 1.292;
|
||||
--fa-typography-h3-letter-spacing: 0px;
|
||||
--fa-typography-h4-line-height: 1.35;
|
||||
--fa-typography-h4-letter-spacing: 0px;
|
||||
--fa-typography-h5-line-height: 1.389;
|
||||
--fa-typography-h5-letter-spacing: 0px;
|
||||
--fa-typography-h6-line-height: 1.375;
|
||||
--fa-typography-h6-letter-spacing: 0px;
|
||||
--fa-typography-body-lg-line-height: 1.611;
|
||||
--fa-typography-body-lg-letter-spacing: 0px;
|
||||
--fa-typography-body-line-height: 1.625;
|
||||
--fa-typography-body-letter-spacing: 0px;
|
||||
--fa-typography-body-sm-line-height: 1.571;
|
||||
--fa-typography-body-sm-letter-spacing: 0px;
|
||||
--fa-typography-body-xs-line-height: 1.5;
|
||||
--fa-typography-body-xs-letter-spacing: 0.1px;
|
||||
--fa-typography-label-lg-line-height: 1.3125;
|
||||
--fa-typography-label-lg-letter-spacing: 0.1px;
|
||||
--fa-typography-label-line-height: 1.286;
|
||||
--fa-typography-label-letter-spacing: 0.15px;
|
||||
--fa-typography-label-sm-line-height: 1.333;
|
||||
--fa-typography-label-sm-letter-spacing: 0.2px;
|
||||
--fa-typography-caption-line-height: 1.417;
|
||||
--fa-typography-caption-letter-spacing: 0.2px;
|
||||
--fa-typography-caption-sm-line-height: 1.364;
|
||||
--fa-typography-caption-sm-letter-spacing: 0.2px;
|
||||
--fa-typography-overline-line-height: 1.333;
|
||||
--fa-typography-overline-letter-spacing: 1.5px;
|
||||
--fa-typography-overline-sm-line-height: 1.273;
|
||||
--fa-typography-overline-sm-letter-spacing: 1.5px;
|
||||
--fa-button-padding-x-xs: var(--fa-spacing-2); /** 8px — compact horizontal padding */
|
||||
--fa-button-padding-x-sm: var(--fa-spacing-3); /** 12px — small horizontal padding */
|
||||
--fa-button-padding-x-md: var(--fa-spacing-4); /** 16px — default horizontal padding */
|
||||
--fa-button-padding-x-lg: var(--fa-spacing-6); /** 24px — generous CTA horizontal padding */
|
||||
--fa-button-padding-y-xs: var(--fa-spacing-1); /** 4px — compact vertical padding */
|
||||
--fa-button-padding-y-sm: var(--fa-spacing-1); /** 4px — small vertical padding */
|
||||
--fa-button-padding-y-md: var(--fa-spacing-2); /** 8px — default vertical padding */
|
||||
--fa-button-padding-y-lg: var(--fa-spacing-3); /** 12px — generous CTA vertical padding */
|
||||
--fa-button-font-size-xs: var(--fa-font-size-xs); /** 12px — extra-small button text */
|
||||
--fa-button-font-size-sm: var(--fa-font-size-sm); /** 14px — small button text */
|
||||
--fa-button-font-size-md: var(--fa-font-size-sm); /** 14px — default button text */
|
||||
--fa-button-font-size-lg: var(--fa-font-size-base); /** 16px — large button text */
|
||||
--fa-button-icon-gap-xs: var(--fa-spacing-1); /** 4px icon-text gap */
|
||||
--fa-button-icon-gap-sm: var(--fa-spacing-1); /** 4px icon-text gap */
|
||||
--fa-button-icon-gap-md: var(--fa-spacing-2); /** 8px icon-text gap */
|
||||
--fa-button-icon-gap-lg: var(--fa-spacing-2); /** 8px icon-text gap */
|
||||
--fa-button-border-radius-default: var(--fa-border-radius-md); /** 8px — standard button rounding */
|
||||
--fa-input-padding-x-default: var(--fa-spacing-3); /** 12px — inner horizontal padding matching Figma design */
|
||||
--fa-input-padding-y-sm: var(--fa-spacing-2); /** 8px — compact vertical padding for small size */
|
||||
--fa-input-padding-y-md: var(--fa-spacing-3); /** 12px — standard vertical padding for medium size */
|
||||
--fa-input-font-size-default: var(--fa-font-size-base); /** 16px — prevents iOS auto-zoom on focus, matches Figma */
|
||||
--fa-input-border-radius-default: var(--fa-border-radius-sm); /** 4px — subtle rounding, consistent with Figma design */
|
||||
--fa-input-gap-default: var(--fa-spacing-2); /** 8px — vertical rhythm between label/input/helper, slightly more generous than Figma's 6px for readability */
|
||||
--fa-color-text-primary: var(--fa-color-neutral-800); /** Primary text — body content, headings. Cool charcoal (#2C2E35) for comfortable extended reading */
|
||||
--fa-color-text-secondary: var(--fa-color-neutral-600); /** Secondary text — helper text, descriptions, metadata, less prominent content */
|
||||
--fa-color-text-tertiary: var(--fa-color-neutral-500); /** Tertiary text — placeholders, timestamps, attribution, meta information */
|
||||
--fa-color-text-disabled: var(--fa-color-neutral-400); /** Disabled text — clearly diminished but still readable for accessibility */
|
||||
--fa-color-text-inverse: var(--fa-color-white); /** Inverse text — white text on dark or coloured backgrounds (buttons, banners) */
|
||||
--fa-color-text-brand: var(--fa-color-brand-600); /** Brand-coloured text — links, inline brand emphasis. Copper tone meets AA on white (4.8:1) */
|
||||
--fa-color-text-error: var(--fa-color-red-600); /** Error text — form validation messages, error descriptions */
|
||||
--fa-color-text-success: var(--fa-color-green-600); /** Success text — confirmation messages, positive feedback */
|
||||
--fa-color-text-warning: var(--fa-color-amber-700); /** Warning text — cautionary messages. Uses amber.700 for WCAG AA compliance on white (5.1:1) */
|
||||
--fa-color-surface-default: var(--fa-color-white); /** Default surface — main page background, card faces */
|
||||
--fa-color-surface-subtle: var(--fa-color-neutral-50); /** Subtle surface — slight differentiation from default, alternate row backgrounds */
|
||||
--fa-color-surface-raised: var(--fa-color-white); /** Raised surface — cards, elevated containers (distinguished by shadow rather than colour) */
|
||||
--fa-color-surface-warm: var(--fa-color-brand-50); /** Warm surface — brand-tinted sections, promotional areas, upsell cards like 'Protect your plan' */
|
||||
--fa-color-surface-cool: var(--fa-color-sage-50); /** Cool surface — calming sections, information panels, FAQ backgrounds */
|
||||
--fa-color-border-default: var(--fa-color-neutral-200); /** Default border — cards, containers, resting input borders */
|
||||
--fa-color-border-strong: var(--fa-color-neutral-400); /** Strong border — emphasis borders, active input borders */
|
||||
--fa-color-border-subtle: var(--fa-color-neutral-100); /** Subtle border — section dividers, soft separators */
|
||||
--fa-color-border-brand: var(--fa-color-brand-500); /** Brand border — focused inputs, selected cards, brand-accented containers */
|
||||
--fa-color-border-error: var(--fa-color-red-500); /** Error border — form fields with validation errors */
|
||||
--fa-color-border-success: var(--fa-color-green-500); /** Success border — validated fields, confirmed selections */
|
||||
--fa-color-interactive-default: var(--fa-color-brand-600); /** Default interactive — primary button fill, link colour, checkbox accent. Uses brand.600 (copper) for WCAG AA compliance (4.6:1 on white) */
|
||||
--fa-color-interactive-hover: var(--fa-color-brand-700); /** Hover state — deepened copper on hover for clear visual feedback */
|
||||
--fa-color-interactive-active: var(--fa-color-brand-800); /** Active/pressed state — dark brown during click/tap */
|
||||
--fa-color-interactive-disabled: var(--fa-color-neutral-300); /** Disabled interactive — muted grey, no pointer events */
|
||||
--fa-color-interactive-focus: var(--fa-color-brand-600); /** Focus ring colour — keyboard navigation indicator, 2px outline with 2px offset */
|
||||
--fa-color-interactive-secondary: var(--fa-color-sage-700); /** Secondary interactive — grey/sage buttons, less prominent actions */
|
||||
--fa-color-interactive-secondary-hover: var(--fa-color-sage-800); /** Secondary interactive hover — darkened sage on hover */
|
||||
--fa-color-feedback-success: var(--fa-color-green-600); /** Success — confirmations, completed arrangement steps, booking confirmed */
|
||||
--fa-color-feedback-success-subtle: var(--fa-color-green-50); /** Success background — success message container fill, completion banners */
|
||||
--fa-color-feedback-warning: var(--fa-color-amber-600); /** Warning — price change alerts, important notices, bond/insurance prompts */
|
||||
--fa-color-feedback-warning-subtle: var(--fa-color-amber-50); /** Warning background — warning message container fill, notice banners */
|
||||
--fa-color-feedback-error: var(--fa-color-red-600); /** Error — form validation failures, system errors, payment issues */
|
||||
--fa-color-feedback-error-subtle: var(--fa-color-red-50); /** Error background — error message container fill, alert banners */
|
||||
--fa-color-feedback-info: var(--fa-color-blue-600); /** Info — helpful tips, supplementary information, guidance callouts */
|
||||
--fa-color-feedback-info-subtle: var(--fa-color-blue-50); /** Info background — info message container fill, tip banners */
|
||||
--fa-spacing-component-xs: var(--fa-spacing-1); /** 4px — tight gaps: icon margins, chip internal padding */
|
||||
--fa-spacing-component-sm: var(--fa-spacing-2); /** 8px — small padding: badge padding, inline element gaps */
|
||||
--fa-spacing-component-md: var(--fa-spacing-4); /** 16px — default padding: button padding, input padding, form field gap */
|
||||
--fa-spacing-component-lg: var(--fa-spacing-6); /** 24px — large padding: card padding (desktop), modal padding */
|
||||
--fa-spacing-layout-gutter: var(--fa-spacing-4); /** 16px — grid gutter on mobile, card grid gap */
|
||||
--fa-spacing-layout-gutter-desktop: var(--fa-spacing-6); /** 24px — grid gutter on desktop */
|
||||
--fa-spacing-layout-section: var(--fa-spacing-12); /** 48px — vertical gap between page sections */
|
||||
--fa-spacing-layout-page: var(--fa-spacing-16); /** 64px — major page section separation, hero spacing */
|
||||
--fa-spacing-layout-page-padding: var(--fa-spacing-4); /** 16px — horizontal page padding on mobile */
|
||||
--fa-spacing-layout-page-padding-desktop: var(--fa-spacing-8); /** 32px — horizontal page padding on desktop */
|
||||
--fa-typography-display-hero-font-family: var(--fa-font-family-display);
|
||||
--fa-typography-display-hero-font-size: var(--fa-font-size-display-hero); /** 80px desktop */
|
||||
--fa-typography-display-hero-font-size-mobile: var(--fa-font-size-mobile-display-hero); /** 32px mobile */
|
||||
--fa-typography-display-hero-font-weight: var(--fa-font-weight-regular); /** 400 — serif carries inherent weight at large sizes */
|
||||
--fa-typography-display1-font-family: var(--fa-font-family-display);
|
||||
--fa-typography-display1-font-size: var(--fa-font-size-display-1); /** 64px desktop */
|
||||
--fa-typography-display1-font-size-mobile: var(--fa-font-size-mobile-display1); /** 28px mobile */
|
||||
--fa-typography-display1-font-weight: var(--fa-font-weight-regular);
|
||||
--fa-typography-display2-font-family: var(--fa-font-family-display);
|
||||
--fa-typography-display2-font-size: var(--fa-font-size-display-2); /** 52px desktop */
|
||||
--fa-typography-display2-font-size-mobile: var(--fa-font-size-mobile-display2); /** 24px mobile */
|
||||
--fa-typography-display2-font-weight: var(--fa-font-weight-regular);
|
||||
--fa-typography-display3-font-family: var(--fa-font-family-display);
|
||||
--fa-typography-display3-font-size: var(--fa-font-size-display-3); /** 40px desktop */
|
||||
--fa-typography-display3-font-size-mobile: var(--fa-font-size-mobile-display3); /** 22px mobile */
|
||||
--fa-typography-display3-font-weight: var(--fa-font-weight-regular);
|
||||
--fa-typography-display-sm-font-family: var(--fa-font-family-display);
|
||||
--fa-typography-display-sm-font-size: var(--fa-font-size-display-sm); /** 32px desktop */
|
||||
--fa-typography-display-sm-font-size-mobile: var(--fa-font-size-mobile-display-sm); /** 20px mobile */
|
||||
--fa-typography-display-sm-font-weight: var(--fa-font-weight-regular);
|
||||
--fa-typography-h1-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-h1-font-size: var(--fa-font-size-3xl); /** 36px desktop */
|
||||
--fa-typography-h1-font-size-mobile: var(--fa-font-size-mobile-h1); /** 26px mobile */
|
||||
--fa-typography-h1-font-weight: var(--fa-font-weight-bold);
|
||||
--fa-typography-h2-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-h2-font-size: var(--fa-font-size-2xl); /** 30px desktop */
|
||||
--fa-typography-h2-font-size-mobile: var(--fa-font-size-mobile-h2); /** 22px mobile */
|
||||
--fa-typography-h2-font-weight: var(--fa-font-weight-bold);
|
||||
--fa-typography-h3-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-h3-font-size: var(--fa-font-size-xl); /** 24px desktop */
|
||||
--fa-typography-h3-font-size-mobile: var(--fa-font-size-mobile-h3); /** 20px mobile */
|
||||
--fa-typography-h3-font-weight: var(--fa-font-weight-bold);
|
||||
--fa-typography-h4-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-h4-font-size: var(--fa-font-size-lg); /** 20px desktop */
|
||||
--fa-typography-h4-font-size-mobile: var(--fa-font-size-mobile-h4); /** 18px mobile */
|
||||
--fa-typography-h4-font-weight: var(--fa-font-weight-bold);
|
||||
--fa-typography-h5-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-h5-font-size: var(--fa-font-size-md); /** 18px desktop */
|
||||
--fa-typography-h5-font-size-mobile: var(--fa-font-size-mobile-h5); /** 16px mobile */
|
||||
--fa-typography-h5-font-weight: var(--fa-font-weight-bold);
|
||||
--fa-typography-h6-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-h6-font-size: var(--fa-font-size-base); /** 16px desktop */
|
||||
--fa-typography-h6-font-size-mobile: var(--fa-font-size-mobile-h6); /** 14px mobile */
|
||||
--fa-typography-h6-font-weight: var(--fa-font-weight-bold);
|
||||
--fa-typography-body-lg-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-body-lg-font-size: var(--fa-font-size-md); /** 18px */
|
||||
--fa-typography-body-lg-font-weight: var(--fa-font-weight-medium);
|
||||
--fa-typography-body-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-body-font-size: var(--fa-font-size-base); /** 16px */
|
||||
--fa-typography-body-font-weight: var(--fa-font-weight-medium);
|
||||
--fa-typography-body-sm-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-body-sm-font-size: var(--fa-font-size-sm); /** 14px */
|
||||
--fa-typography-body-sm-font-weight: var(--fa-font-weight-medium);
|
||||
--fa-typography-body-xs-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-body-xs-font-size: var(--fa-font-size-xs); /** 12px */
|
||||
--fa-typography-body-xs-font-weight: var(--fa-font-weight-medium);
|
||||
--fa-typography-label-lg-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-label-lg-font-size: var(--fa-font-size-base); /** 16px */
|
||||
--fa-typography-label-lg-font-weight: var(--fa-font-weight-medium);
|
||||
--fa-typography-label-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-label-font-size: var(--fa-font-size-sm); /** 14px */
|
||||
--fa-typography-label-font-weight: var(--fa-font-weight-medium);
|
||||
--fa-typography-label-sm-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-label-sm-font-size: var(--fa-font-size-xs); /** 12px */
|
||||
--fa-typography-label-sm-font-weight: var(--fa-font-weight-medium);
|
||||
--fa-typography-caption-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-caption-font-size: var(--fa-font-size-xs); /** 12px */
|
||||
--fa-typography-caption-font-weight: var(--fa-font-weight-regular);
|
||||
--fa-typography-caption-sm-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-caption-sm-font-size: var(--fa-font-size-2xs); /** 11px — accessibility floor */
|
||||
--fa-typography-caption-sm-font-weight: var(--fa-font-weight-regular);
|
||||
--fa-typography-overline-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-overline-font-size: var(--fa-font-size-xs); /** 12px */
|
||||
--fa-typography-overline-font-weight: var(--fa-font-weight-semibold);
|
||||
--fa-typography-overline-sm-font-family: var(--fa-font-family-body);
|
||||
--fa-typography-overline-sm-font-size: var(--fa-font-size-2xs); /** 11px — accessibility floor */
|
||||
--fa-typography-overline-sm-font-weight: var(--fa-font-weight-semibold);
|
||||
}
|
||||
377
src/theme/generated/tokens.js
Normal file
377
src/theme/generated/tokens.js
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* Do not edit directly, this file was auto-generated.
|
||||
*/
|
||||
|
||||
export const ButtonHeightXs = "28px"; // Extra-small — compact text buttons, inline actions
|
||||
export const ButtonHeightSm = "32px"; // Small — secondary actions, toolbar buttons
|
||||
export const ButtonHeightMd = "40px"; // Medium — default size, form submissions
|
||||
export const ButtonHeightLg = "48px"; // Large — primary CTAs, mobile touch targets (meets 44px minimum)
|
||||
export const ButtonPaddingXXs = "8px"; // 8px — compact horizontal padding
|
||||
export const ButtonPaddingXSm = "12px"; // 12px — small horizontal padding
|
||||
export const ButtonPaddingXMd = "16px"; // 16px — default horizontal padding
|
||||
export const ButtonPaddingXLg = "24px"; // 24px — generous CTA horizontal padding
|
||||
export const ButtonPaddingYXs = "4px"; // 4px — compact vertical padding
|
||||
export const ButtonPaddingYSm = "4px"; // 4px — small vertical padding
|
||||
export const ButtonPaddingYMd = "8px"; // 8px — default vertical padding
|
||||
export const ButtonPaddingYLg = "12px"; // 12px — generous CTA vertical padding
|
||||
export const ButtonFontSizeXs = "0.75rem"; // 12px — extra-small button text
|
||||
export const ButtonFontSizeSm = "0.875rem"; // 14px — small button text
|
||||
export const ButtonFontSizeMd = "0.875rem"; // 14px — default button text
|
||||
export const ButtonFontSizeLg = "1rem"; // 16px — large button text
|
||||
export const ButtonIconSizeXs = "14px"; // 14px icons in extra-small buttons
|
||||
export const ButtonIconSizeSm = "16px"; // 16px icons in small buttons
|
||||
export const ButtonIconSizeMd = "18px"; // 18px icons in medium buttons
|
||||
export const ButtonIconSizeLg = "20px"; // 20px icons in large buttons
|
||||
export const ButtonIconGapXs = "4px"; // 4px icon-text gap
|
||||
export const ButtonIconGapSm = "4px"; // 4px icon-text gap
|
||||
export const ButtonIconGapMd = "8px"; // 8px icon-text gap
|
||||
export const ButtonIconGapLg = "8px"; // 8px icon-text gap
|
||||
export const ButtonBorderRadiusDefault = "8px"; // 8px — standard button rounding
|
||||
export const InputHeightSm = "40px"; // Small — compact forms, admin layouts, matches Button medium height
|
||||
export const InputHeightMd = "48px"; // Medium (default) — standard forms, matches Button large for alignment
|
||||
export const InputPaddingXDefault = "12px"; // 12px — inner horizontal padding matching Figma design
|
||||
export const InputPaddingYSm = "8px"; // 8px — compact vertical padding for small size
|
||||
export const InputPaddingYMd = "12px"; // 12px — standard vertical padding for medium size
|
||||
export const InputFontSizeDefault = "1rem"; // 16px — prevents iOS auto-zoom on focus, matches Figma
|
||||
export const InputBorderRadiusDefault = "4px"; // 4px — subtle rounding, consistent with Figma design
|
||||
export const InputGapDefault = "8px"; // 8px — vertical rhythm between label/input/helper, slightly more generous than Figma's 6px for readability
|
||||
export const InputIconSizeDefault = "20px"; // 20px — icon size inside input field, matches Figma trailing icon
|
||||
export const ColorBrand50 = "#fef9f5"; // Lightest warm tint — warm section backgrounds
|
||||
export const ColorBrand100 = "#f7ecdf"; // Light warm — hover backgrounds, subtle fills
|
||||
export const ColorBrand200 = "#ebdac8"; // Warm light — secondary backgrounds, divider tones
|
||||
export const ColorBrand300 = "#d8c3b5"; // Warm beige — from brand swatch. Surface warmth, card tints
|
||||
export const ColorBrand400 = "#d0a070"; // Mid gold — from brand swatch. Secondary interactive, step indicators
|
||||
export const ColorBrand500 = "#ba834e"; // Base brand gold — from brand swatch. Primary CTA colour, main interactive. 3.7:1 contrast on white
|
||||
export const ColorBrand600 = "#b0610f"; // Rich copper — from brand swatch. Hover/emphasis on brand elements. 4.8:1 contrast on white
|
||||
export const ColorBrand700 = "#8b4e0d"; // Deep copper — active states, strong brand text on light backgrounds. 6.7:1 contrast on white
|
||||
export const ColorBrand800 = "#6b3c13"; // Dark brown — bold brand accents, high-contrast brand text
|
||||
export const ColorBrand900 = "#51301b"; // Chocolate — from brand swatch. Deep emphasis, dark brand surfaces
|
||||
export const ColorBrand950 = "#251913"; // Espresso — from brand swatch. Darkest brand tone, near-black warm
|
||||
export const ColorSage50 = "#f2f5f6"; // Lightest sage — subtle cool backgrounds
|
||||
export const ColorSage100 = "#e3eaeb"; // Light sage — hover states on cool surfaces
|
||||
export const ColorSage200 = "#d7e1e2"; // From brand swatch — light cool surface, soft borders
|
||||
export const ColorSage300 = "#c8d4d6"; // Mid-light sage — dividers, secondary borders
|
||||
export const ColorSage400 = "#b9c7c9"; // From brand swatch — mid sage, secondary text on dark backgrounds
|
||||
export const ColorSage500 = "#8ea2a7"; // Base sage — secondary content, muted icons
|
||||
export const ColorSage600 = "#687d84"; // Dark sage — secondary text, subtle icons
|
||||
export const ColorSage700 = "#4c5b6b"; // From brand swatch — strong secondary, dark accents. 6.1:1 contrast on white
|
||||
export const ColorSage800 = "#4c5459"; // From brand swatch — near-dark grey, supplementary text. 6.7:1 contrast on white
|
||||
export const ColorSage900 = "#343c40"; // Very dark sage — high-contrast secondary elements
|
||||
export const ColorSage950 = "#1e2528"; // Near-black cool — darkest secondary tone
|
||||
export const ColorNeutral50 = "#fafafa"; // Lightest neutral — default page background alternative
|
||||
export const ColorNeutral100 = "#f5f5f5"; // Light neutral — subtle background differentiation
|
||||
export const ColorNeutral200 = "#e8e8e8"; // Light grey — standard borders, dividers
|
||||
export const ColorNeutral300 = "#d4d4d4"; // Mid-light grey — disabled borders, subtle separators
|
||||
export const ColorNeutral400 = "#a3a3a3"; // Mid grey — placeholder text, disabled content
|
||||
export const ColorNeutral500 = "#737373"; // Base grey — secondary body text, icons
|
||||
export const ColorNeutral600 = "#525252"; // Dark grey — body text, labels. 7.1:1 contrast on white
|
||||
export const ColorNeutral700 = "#404040"; // Strong grey — headings, emphasis text. 9.7:1 contrast on white
|
||||
export const ColorNeutral800 = "#2c2e35"; // From brand swatch — charcoal with cool tint. Primary text colour. 13.2:1 contrast on white
|
||||
export const ColorNeutral900 = "#1a1a1c"; // Near-black — maximum contrast text
|
||||
export const ColorNeutral950 = "#0a0a0b"; // Deepest neutral — use sparingly
|
||||
export const ColorRed50 = "#fef2f2"; // Error tint — error message backgrounds
|
||||
export const ColorRed100 = "#fde8e8"; // Light error — hover on error surfaces
|
||||
export const ColorRed200 = "#f9bfbf"; // Light red — error borders, subtle indicators
|
||||
export const ColorRed300 = "#f09898"; // Mid-light red — error icon backgrounds
|
||||
export const ColorRed400 = "#e56b6b"; // Mid red — error indicators, badges
|
||||
export const ColorRed500 = "#d64545"; // Base red — form validation errors, alert accents
|
||||
export const ColorRed600 = "#bc2f2f"; // Strong red — error text on light backgrounds. 5.7:1 contrast on white
|
||||
export const ColorRed700 = "#9b2424"; // Dark red — error headings, strong alerts
|
||||
export const ColorRed800 = "#7a1d1d"; // Deep red — high-contrast error emphasis
|
||||
export const ColorRed900 = "#5c1616"; // Very dark red — use sparingly
|
||||
export const ColorRed950 = "#3d0e0e"; // Darkest red
|
||||
export const ColorAmber50 = "#fff9eb"; // Warning tint — warning message backgrounds
|
||||
export const ColorAmber100 = "#fff0cc"; // Light amber — hover on warning surfaces
|
||||
export const ColorAmber200 = "#ffe099"; // Light amber — warning borders
|
||||
export const ColorAmber300 = "#ffcc66"; // Mid-light amber — warning icon backgrounds
|
||||
export const ColorAmber400 = "#ffb833"; // Mid amber — warning badges, price alerts
|
||||
export const ColorAmber500 = "#f5a000"; // Base amber — warning accents
|
||||
export const ColorAmber600 = "#cc8500"; // Strong amber — warning text. 3.6:1 contrast on white (large text AA)
|
||||
export const ColorAmber700 = "#a36b00"; // Dark amber — warning headings. 5.1:1 contrast on white
|
||||
export const ColorAmber800 = "#7a5000"; // Deep amber — high-contrast warning emphasis
|
||||
export const ColorAmber900 = "#523600"; // Very dark amber — use sparingly
|
||||
export const ColorAmber950 = "#331f00"; // Darkest amber
|
||||
export const ColorGreen50 = "#f0f7f0"; // Success tint — success message backgrounds
|
||||
export const ColorGreen100 = "#d8ecd8"; // Light green — hover on success surfaces
|
||||
export const ColorGreen200 = "#b8d8b8"; // Light green — success borders
|
||||
export const ColorGreen300 = "#8dc08d"; // Mid-light green — success icon backgrounds
|
||||
export const ColorGreen400 = "#66a866"; // Mid green — success badges, completion indicators
|
||||
export const ColorGreen500 = "#4a8f4a"; // Base green — success accents, completed steps
|
||||
export const ColorGreen600 = "#3b7a3b"; // Strong green — success text on light backgrounds. 4.8:1 contrast on white
|
||||
export const ColorGreen700 = "#2e6b2e"; // Dark green — success headings
|
||||
export const ColorGreen800 = "#235523"; // Deep green — high-contrast success emphasis
|
||||
export const ColorGreen900 = "#1a3f1a"; // Very dark green — use sparingly
|
||||
export const ColorGreen950 = "#0f2a0f"; // Darkest green
|
||||
export const ColorBlue50 = "#eff6ff"; // Info tint — info message backgrounds
|
||||
export const ColorBlue100 = "#dbeafe"; // Light blue — hover on info surfaces
|
||||
export const ColorBlue200 = "#bfdbfe"; // Light blue — info borders
|
||||
export const ColorBlue300 = "#93c5fd"; // Mid-light blue — info icon backgrounds
|
||||
export const ColorBlue400 = "#60a5fa"; // Mid blue — info badges
|
||||
export const ColorBlue500 = "#3b82f6"; // Base blue — info accents, supplementary links
|
||||
export const ColorBlue600 = "#2563eb"; // Strong blue — info text on light backgrounds. 4.6:1 contrast on white
|
||||
export const ColorBlue700 = "#1d4ed8"; // Dark blue — info headings
|
||||
export const ColorBlue800 = "#1e40af"; // Deep blue — high-contrast info emphasis
|
||||
export const ColorBlue900 = "#1e3a8a"; // Very dark blue — use sparingly
|
||||
export const ColorBlue950 = "#172554"; // Darkest blue
|
||||
export const ColorWhite = "#ffffff"; // Pure white — card backgrounds, inverse text, primary surface
|
||||
export const ColorBlack = "#000000"; // Pure black — from brand swatch. Use sparingly; prefer neutral.800-900 for text
|
||||
export const ColorTextPrimary = "#2c2e35"; // Primary text — body content, headings. Cool charcoal (#2C2E35) for comfortable extended reading
|
||||
export const ColorTextSecondary = "#525252"; // Secondary text — helper text, descriptions, metadata, less prominent content
|
||||
export const ColorTextTertiary = "#737373"; // Tertiary text — placeholders, timestamps, attribution, meta information
|
||||
export const ColorTextDisabled = "#a3a3a3"; // Disabled text — clearly diminished but still readable for accessibility
|
||||
export const ColorTextInverse = "#ffffff"; // Inverse text — white text on dark or coloured backgrounds (buttons, banners)
|
||||
export const ColorTextBrand = "#b0610f"; // Brand-coloured text — links, inline brand emphasis. Copper tone meets AA on white (4.8:1)
|
||||
export const ColorTextError = "#bc2f2f"; // Error text — form validation messages, error descriptions
|
||||
export const ColorTextSuccess = "#3b7a3b"; // Success text — confirmation messages, positive feedback
|
||||
export const ColorTextWarning = "#a36b00"; // Warning text — cautionary messages. Uses amber.700 for WCAG AA compliance on white (5.1:1)
|
||||
export const ColorSurfaceDefault = "#ffffff"; // Default surface — main page background, card faces
|
||||
export const ColorSurfaceSubtle = "#fafafa"; // Subtle surface — slight differentiation from default, alternate row backgrounds
|
||||
export const ColorSurfaceRaised = "#ffffff"; // Raised surface — cards, elevated containers (distinguished by shadow rather than colour)
|
||||
export const ColorSurfaceWarm = "#fef9f5"; // Warm surface — brand-tinted sections, promotional areas, upsell cards like 'Protect your plan'
|
||||
export const ColorSurfaceCool = "#f2f5f6"; // Cool surface — calming sections, information panels, FAQ backgrounds
|
||||
export const ColorSurfaceOverlay = "#00000080"; // Overlay surface — modal/dialog backdrop at 50% black
|
||||
export const ColorBorderDefault = "#e8e8e8"; // Default border — cards, containers, resting input borders
|
||||
export const ColorBorderStrong = "#a3a3a3"; // Strong border — emphasis borders, active input borders
|
||||
export const ColorBorderSubtle = "#f5f5f5"; // Subtle border — section dividers, soft separators
|
||||
export const ColorBorderBrand = "#ba834e"; // Brand border — focused inputs, selected cards, brand-accented containers
|
||||
export const ColorBorderError = "#d64545"; // Error border — form fields with validation errors
|
||||
export const ColorBorderSuccess = "#4a8f4a"; // Success border — validated fields, confirmed selections
|
||||
export const ColorInteractiveDefault = "#b0610f"; // Default interactive — primary button fill, link colour, checkbox accent. Uses brand.600 (copper) for WCAG AA compliance (4.6:1 on white)
|
||||
export const ColorInteractiveHover = "#8b4e0d"; // Hover state — deepened copper on hover for clear visual feedback
|
||||
export const ColorInteractiveActive = "#6b3c13"; // Active/pressed state — dark brown during click/tap
|
||||
export const ColorInteractiveDisabled = "#d4d4d4"; // Disabled interactive — muted grey, no pointer events
|
||||
export const ColorInteractiveFocus = "#b0610f"; // Focus ring colour — keyboard navigation indicator, 2px outline with 2px offset
|
||||
export const ColorInteractiveSecondary = "#4c5b6b"; // Secondary interactive — grey/sage buttons, less prominent actions
|
||||
export const ColorInteractiveSecondaryHover = "#4c5459"; // Secondary interactive hover — darkened sage on hover
|
||||
export const ColorFeedbackSuccess = "#3b7a3b"; // Success — confirmations, completed arrangement steps, booking confirmed
|
||||
export const ColorFeedbackSuccessSubtle = "#f0f7f0"; // Success background — success message container fill, completion banners
|
||||
export const ColorFeedbackWarning = "#cc8500"; // Warning — price change alerts, important notices, bond/insurance prompts
|
||||
export const ColorFeedbackWarningSubtle = "#fff9eb"; // Warning background — warning message container fill, notice banners
|
||||
export const ColorFeedbackError = "#bc2f2f"; // Error — form validation failures, system errors, payment issues
|
||||
export const ColorFeedbackErrorSubtle = "#fef2f2"; // Error background — error message container fill, alert banners
|
||||
export const ColorFeedbackInfo = "#2563eb"; // Info — helpful tips, supplementary information, guidance callouts
|
||||
export const ColorFeedbackInfoSubtle = "#eff6ff"; // Info background — info message container fill, tip banners
|
||||
export const ShadowSm = "0 1px 2px rgba(0,0,0,0.05)"; // Subtle lift — resting buttons, input focus subtle elevation
|
||||
export const ShadowMd = "0 4px 6px rgba(0,0,0,0.07)"; // Medium elevation — cards at rest, dropdowns, popovers
|
||||
export const ShadowLg = "0 10px 15px rgba(0,0,0,0.1)"; // High elevation — modals, popovers, card hover states
|
||||
export const ShadowXl = "0 20px 25px rgba(0,0,0,0.1)"; // Maximum elevation — elevated panels, dialog boxes
|
||||
export const OpacityDisabled = 0.4; // Disabled state — 40% opacity. Clearly diminished but still distinguishable
|
||||
export const OpacityHover = 0.08; // Hover overlay — subtle 8% tint applied over backgrounds on hover
|
||||
export const OpacityOverlay = 0.5; // Modal/dialog backdrop — 50% black overlay behind modals
|
||||
export const Spacing1 = "4px"; // Tight — inline spacing, minimal gaps between related elements
|
||||
export const Spacing2 = "8px"; // Small — related element gaps, compact padding, icon margins
|
||||
export const Spacing3 = "12px"; // Component internal padding (small), chip padding
|
||||
export const Spacing4 = "16px"; // Default component padding, form field gap, card grid gutter (mobile)
|
||||
export const Spacing5 = "20px"; // Medium component spacing
|
||||
export const Spacing6 = "24px"; // Card padding, section gap (small), card grid gutter (desktop)
|
||||
export const Spacing8 = "32px"; // Section gap (medium), form section separation
|
||||
export const Spacing10 = "40px"; // Section gap (large)
|
||||
export const Spacing12 = "48px"; // Page section separation, vertical rhythm break
|
||||
export const Spacing16 = "64px"; // Hero/banner vertical spacing
|
||||
export const Spacing20 = "80px"; // Major page sections, large vertical spacing
|
||||
export const Spacing05 = "2px"; // Hairline — icon-to-text tight spacing, fine adjustments
|
||||
export const SpacingComponentXs = "4px"; // 4px — tight gaps: icon margins, chip internal padding
|
||||
export const SpacingComponentSm = "8px"; // 8px — small padding: badge padding, inline element gaps
|
||||
export const SpacingComponentMd = "16px"; // 16px — default padding: button padding, input padding, form field gap
|
||||
export const SpacingComponentLg = "24px"; // 24px — large padding: card padding (desktop), modal padding
|
||||
export const SpacingLayoutGutter = "16px"; // 16px — grid gutter on mobile, card grid gap
|
||||
export const SpacingLayoutGutterDesktop = "24px"; // 24px — grid gutter on desktop
|
||||
export const SpacingLayoutSection = "48px"; // 48px — vertical gap between page sections
|
||||
export const SpacingLayoutPage = "64px"; // 64px — major page section separation, hero spacing
|
||||
export const SpacingLayoutPagePadding = "16px"; // 16px — horizontal page padding on mobile
|
||||
export const SpacingLayoutPagePaddingDesktop = "32px"; // 32px — horizontal page padding on desktop
|
||||
export const BorderRadiusNone = "0px"; // Square corners — tables, dividers, sharp elements
|
||||
export const BorderRadiusSm = "4px"; // Small radius — inputs, small interactive elements, chips
|
||||
export const BorderRadiusMd = "8px"; // Medium radius — cards, buttons, dropdowns (default)
|
||||
export const BorderRadiusLg = "12px"; // Large radius — modals, large cards
|
||||
export const BorderRadiusXl = "16px"; // Extra large — feature cards, hero elements
|
||||
export const BorderRadiusFull = "9999px"; // Full/pill — avatars, pill buttons, circular elements
|
||||
export const FontFamilyBody =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif"; // Primary font — Montserrat. Used for headings (h1-h6), body text, labels, and all UI elements
|
||||
export const FontFamilyDisplay =
|
||||
"'Noto Serif SC', Georgia, 'Times New Roman', serif"; // Display font — Noto Serif SC. Elegant serif for hero/display text only. Adds warmth and gravitas at large sizes
|
||||
export const FontFamilyMono =
|
||||
"'JetBrains Mono', 'Fira Code', Consolas, monospace"; // Monospace font — for reference numbers (FA-2026-00847), tabular pricing data, and code
|
||||
export const FontSize2xs = "0.6875rem"; // 11px — smallest UI text: compact captions, compact overlines
|
||||
export const FontSizeXs = "0.75rem"; // 12px — small text: captions, labels, overlines, body/xs
|
||||
export const FontSizeSm = "0.875rem"; // 14px — body small, labels, helper text
|
||||
export const FontSizeBase = "1rem"; // 16px — default body text, heading/6, label/lg
|
||||
export const FontSizeMd = "1.125rem"; // 18px — body large, heading/5
|
||||
export const FontSizeLg = "1.25rem"; // 20px — heading/4
|
||||
export const FontSizeXl = "1.5rem"; // 24px — heading/3
|
||||
export const FontSize2xl = "1.875rem"; // 30px — heading/2
|
||||
export const FontSize3xl = "2.25rem"; // 36px — heading/1
|
||||
export const FontSize4xl = "3rem"; // 48px — reserved (legacy)
|
||||
export const FontSizeDisplay1 = "4rem"; // 64px — display/1
|
||||
export const FontSizeDisplay2 = "3.25rem"; // 52px — display/2
|
||||
export const FontSizeDisplay3 = "2.5rem"; // 40px — display/3
|
||||
export const FontSizeDisplaySm = "2rem"; // 32px — display/sm, smallest display text
|
||||
export const FontSizeDisplayHero = "5rem"; // 80px — display/hero, largest display text
|
||||
export const FontSizeMobileDisplayHero = "2rem"; // 32px — mobile display/hero (from 80px desktop)
|
||||
export const FontSizeMobileDisplay1 = "1.75rem"; // 28px — mobile display/1 (from 64px desktop)
|
||||
export const FontSizeMobileDisplay2 = "1.5rem"; // 24px — mobile display/2 (from 52px desktop)
|
||||
export const FontSizeMobileDisplay3 = "1.375rem"; // 22px — mobile display/3 (from 40px desktop)
|
||||
export const FontSizeMobileDisplaySm = "1.25rem"; // 20px — mobile display/sm (from 32px desktop)
|
||||
export const FontSizeMobileH1 = "1.625rem"; // 26px — mobile heading/1 (from 36px desktop)
|
||||
export const FontSizeMobileH2 = "1.375rem"; // 22px — mobile heading/2 (from 30px desktop)
|
||||
export const FontSizeMobileH3 = "1.25rem"; // 20px — mobile heading/3 (from 24px desktop)
|
||||
export const FontSizeMobileH4 = "1.125rem"; // 18px — mobile heading/4 (from 20px desktop)
|
||||
export const FontSizeMobileH5 = "1rem"; // 16px — mobile heading/5 (from 18px desktop)
|
||||
export const FontSizeMobileH6 = "0.875rem"; // 14px — mobile heading/6 (from 16px desktop)
|
||||
export const FontWeightRegular = 400; // Regular weight — captions, display text (serif carries inherent weight)
|
||||
export const FontWeightMedium = 500; // Medium weight — body text, labels. Slightly bolder than regular for improved readability
|
||||
export const FontWeightSemibold = 600; // Semibold — overlines, button text, navigation
|
||||
export const FontWeightBold = 700; // Bold — all headings (h1-h6)
|
||||
export const LineHeightTight = 1.25; // Tight leading — large headings, display text
|
||||
export const LineHeightSnug = 1.375; // Snug leading — sub-headings, labels, small text
|
||||
export const LineHeightNormal = 1.5; // Normal leading — default body text, optimal readability
|
||||
export const LineHeightRelaxed = 1.75; // Relaxed leading — large body text, long-form content
|
||||
export const LetterSpacingTighter = "-0.02em"; // Tighter tracking — large display text
|
||||
export const LetterSpacingTight = "-0.01em"; // Tight tracking — headings
|
||||
export const LetterSpacingNormal = "0em"; // Normal tracking — body text, most content
|
||||
export const LetterSpacingWide = "0.02em"; // Wide tracking — captions, small text
|
||||
export const LetterSpacingWider = "0.05em"; // Wider tracking — labels, UI text
|
||||
export const LetterSpacingWidest = "0.08em"; // Widest tracking — overlines, uppercase text
|
||||
export const TypographyDisplayHeroFontFamily =
|
||||
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||
export const TypographyDisplayHeroFontSize = "5rem"; // 80px desktop
|
||||
export const TypographyDisplayHeroFontSizeMobile = "2rem"; // 32px mobile
|
||||
export const TypographyDisplayHeroFontWeight = 400; // 400 — serif carries inherent weight at large sizes
|
||||
export const TypographyDisplayHeroLineHeight = 1.05;
|
||||
export const TypographyDisplayHeroLetterSpacing = "-1.5px";
|
||||
export const TypographyDisplay1FontFamily =
|
||||
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||
export const TypographyDisplay1FontSize = "4rem"; // 64px desktop
|
||||
export const TypographyDisplay1FontSizeMobile = "1.75rem"; // 28px mobile
|
||||
export const TypographyDisplay1FontWeight = 400;
|
||||
export const TypographyDisplay1LineHeight = 1.078;
|
||||
export const TypographyDisplay1LetterSpacing = "-1px";
|
||||
export const TypographyDisplay2FontFamily =
|
||||
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||
export const TypographyDisplay2FontSize = "3.25rem"; // 52px desktop
|
||||
export const TypographyDisplay2FontSizeMobile = "1.5rem"; // 24px mobile
|
||||
export const TypographyDisplay2FontWeight = 400;
|
||||
export const TypographyDisplay2LineHeight = 1.096;
|
||||
export const TypographyDisplay2LetterSpacing = "-0.5px";
|
||||
export const TypographyDisplay3FontFamily =
|
||||
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||
export const TypographyDisplay3FontSize = "2.5rem"; // 40px desktop
|
||||
export const TypographyDisplay3FontSizeMobile = "1.375rem"; // 22px mobile
|
||||
export const TypographyDisplay3FontWeight = 400;
|
||||
export const TypographyDisplay3LineHeight = 1.15;
|
||||
export const TypographyDisplay3LetterSpacing = "-0.25px";
|
||||
export const TypographyDisplaySmFontFamily =
|
||||
"'Noto Serif SC', Georgia, 'Times New Roman', serif";
|
||||
export const TypographyDisplaySmFontSize = "2rem"; // 32px desktop
|
||||
export const TypographyDisplaySmFontSizeMobile = "1.25rem"; // 20px mobile
|
||||
export const TypographyDisplaySmFontWeight = 400;
|
||||
export const TypographyDisplaySmLineHeight = 1.1875;
|
||||
export const TypographyDisplaySmLetterSpacing = "0px";
|
||||
export const TypographyH1FontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyH1FontSize = "2.25rem"; // 36px desktop
|
||||
export const TypographyH1FontSizeMobile = "1.625rem"; // 26px mobile
|
||||
export const TypographyH1FontWeight = 700;
|
||||
export const TypographyH1LineHeight = 1.194;
|
||||
export const TypographyH1LetterSpacing = "-0.5px";
|
||||
export const TypographyH2FontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyH2FontSize = "1.875rem"; // 30px desktop
|
||||
export const TypographyH2FontSizeMobile = "1.375rem"; // 22px mobile
|
||||
export const TypographyH2FontWeight = 700;
|
||||
export const TypographyH2LineHeight = 1.267;
|
||||
export const TypographyH2LetterSpacing = "-0.25px";
|
||||
export const TypographyH3FontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyH3FontSize = "1.5rem"; // 24px desktop
|
||||
export const TypographyH3FontSizeMobile = "1.25rem"; // 20px mobile
|
||||
export const TypographyH3FontWeight = 700;
|
||||
export const TypographyH3LineHeight = 1.292;
|
||||
export const TypographyH3LetterSpacing = "0px";
|
||||
export const TypographyH4FontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyH4FontSize = "1.25rem"; // 20px desktop
|
||||
export const TypographyH4FontSizeMobile = "1.125rem"; // 18px mobile
|
||||
export const TypographyH4FontWeight = 700;
|
||||
export const TypographyH4LineHeight = 1.35;
|
||||
export const TypographyH4LetterSpacing = "0px";
|
||||
export const TypographyH5FontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyH5FontSize = "1.125rem"; // 18px desktop
|
||||
export const TypographyH5FontSizeMobile = "1rem"; // 16px mobile
|
||||
export const TypographyH5FontWeight = 700;
|
||||
export const TypographyH5LineHeight = 1.389;
|
||||
export const TypographyH5LetterSpacing = "0px";
|
||||
export const TypographyH6FontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyH6FontSize = "1rem"; // 16px desktop
|
||||
export const TypographyH6FontSizeMobile = "0.875rem"; // 14px mobile
|
||||
export const TypographyH6FontWeight = 700;
|
||||
export const TypographyH6LineHeight = 1.375;
|
||||
export const TypographyH6LetterSpacing = "0px";
|
||||
export const TypographyBodyLgFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyBodyLgFontSize = "1.125rem"; // 18px
|
||||
export const TypographyBodyLgFontWeight = 500;
|
||||
export const TypographyBodyLgLineHeight = 1.611;
|
||||
export const TypographyBodyLgLetterSpacing = "0px";
|
||||
export const TypographyBodyFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyBodyFontSize = "1rem"; // 16px
|
||||
export const TypographyBodyFontWeight = 500;
|
||||
export const TypographyBodyLineHeight = 1.625;
|
||||
export const TypographyBodyLetterSpacing = "0px";
|
||||
export const TypographyBodySmFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyBodySmFontSize = "0.875rem"; // 14px
|
||||
export const TypographyBodySmFontWeight = 500;
|
||||
export const TypographyBodySmLineHeight = 1.571;
|
||||
export const TypographyBodySmLetterSpacing = "0px";
|
||||
export const TypographyBodyXsFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyBodyXsFontSize = "0.75rem"; // 12px
|
||||
export const TypographyBodyXsFontWeight = 500;
|
||||
export const TypographyBodyXsLineHeight = 1.5;
|
||||
export const TypographyBodyXsLetterSpacing = "0.1px";
|
||||
export const TypographyLabelLgFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyLabelLgFontSize = "1rem"; // 16px
|
||||
export const TypographyLabelLgFontWeight = 500;
|
||||
export const TypographyLabelLgLineHeight = 1.3125;
|
||||
export const TypographyLabelLgLetterSpacing = "0.1px";
|
||||
export const TypographyLabelFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyLabelFontSize = "0.875rem"; // 14px
|
||||
export const TypographyLabelFontWeight = 500;
|
||||
export const TypographyLabelLineHeight = 1.286;
|
||||
export const TypographyLabelLetterSpacing = "0.15px";
|
||||
export const TypographyLabelSmFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyLabelSmFontSize = "0.75rem"; // 12px
|
||||
export const TypographyLabelSmFontWeight = 500;
|
||||
export const TypographyLabelSmLineHeight = 1.333;
|
||||
export const TypographyLabelSmLetterSpacing = "0.2px";
|
||||
export const TypographyCaptionFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyCaptionFontSize = "0.75rem"; // 12px
|
||||
export const TypographyCaptionFontWeight = 400;
|
||||
export const TypographyCaptionLineHeight = 1.417;
|
||||
export const TypographyCaptionLetterSpacing = "0.2px";
|
||||
export const TypographyCaptionSmFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyCaptionSmFontSize = "0.6875rem"; // 11px — accessibility floor
|
||||
export const TypographyCaptionSmFontWeight = 400;
|
||||
export const TypographyCaptionSmLineHeight = 1.364;
|
||||
export const TypographyCaptionSmLetterSpacing = "0.2px";
|
||||
export const TypographyOverlineFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyOverlineFontSize = "0.75rem"; // 12px
|
||||
export const TypographyOverlineFontWeight = 600;
|
||||
export const TypographyOverlineLineHeight = 1.333;
|
||||
export const TypographyOverlineLetterSpacing = "1.5px";
|
||||
export const TypographyOverlineSmFontFamily =
|
||||
"'Montserrat', 'Helvetica Neue', Arial, sans-serif";
|
||||
export const TypographyOverlineSmFontSize = "0.6875rem"; // 11px — accessibility floor
|
||||
export const TypographyOverlineSmFontWeight = 600;
|
||||
export const TypographyOverlineSmLineHeight = 1.273;
|
||||
export const TypographyOverlineSmLetterSpacing = "1.5px";
|
||||
568
src/theme/index.ts
Normal file
568
src/theme/index.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
/**
|
||||
* FA Design System — MUI Theme
|
||||
*
|
||||
* Maps design tokens to MUI's theme structure. All values come from the
|
||||
* token pipeline: Token JSON → Style Dictionary → generated/tokens.js → here.
|
||||
*
|
||||
* To update colours/typography/spacing: edit tokens/ JSON, run `npm run build:tokens`,
|
||||
* then update references here if token names changed.
|
||||
*/
|
||||
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import type { CSSProperties } from 'react';
|
||||
import * as t from './generated/tokens.js';
|
||||
|
||||
/* ---------- Types ---------- */
|
||||
|
||||
type StyleWithMedia = CSSProperties & {
|
||||
[key: `@media ${string}`]: CSSProperties;
|
||||
};
|
||||
|
||||
/* ---------- Custom typography variant declarations ---------- */
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface TypographyVariants {
|
||||
displayHero: StyleWithMedia;
|
||||
display1: StyleWithMedia;
|
||||
display2: StyleWithMedia;
|
||||
display3: StyleWithMedia;
|
||||
displaySm: StyleWithMedia;
|
||||
bodyLg: CSSProperties;
|
||||
bodyXs: CSSProperties;
|
||||
labelLg: CSSProperties;
|
||||
label: CSSProperties;
|
||||
labelSm: CSSProperties;
|
||||
captionSm: CSSProperties;
|
||||
overlineSm: CSSProperties;
|
||||
}
|
||||
interface TypographyVariantsOptions {
|
||||
displayHero?: StyleWithMedia;
|
||||
display1?: StyleWithMedia;
|
||||
display2?: StyleWithMedia;
|
||||
display3?: StyleWithMedia;
|
||||
displaySm?: StyleWithMedia;
|
||||
bodyLg?: CSSProperties;
|
||||
bodyXs?: CSSProperties;
|
||||
labelLg?: CSSProperties;
|
||||
label?: CSSProperties;
|
||||
labelSm?: CSSProperties;
|
||||
captionSm?: CSSProperties;
|
||||
overlineSm?: CSSProperties;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Typography' {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
displayHero: true;
|
||||
display1: true;
|
||||
display2: true;
|
||||
display3: true;
|
||||
displaySm: true;
|
||||
bodyLg: true;
|
||||
bodyXs: true;
|
||||
labelLg: true;
|
||||
label: true;
|
||||
labelSm: true;
|
||||
captionSm: true;
|
||||
overlineSm: true;
|
||||
// Disable old aliases
|
||||
display: false;
|
||||
bodyLarge: false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Custom Button declarations ---------- */
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsSizeOverrides {
|
||||
xs: true;
|
||||
}
|
||||
interface ButtonPropsVariantOverrides {
|
||||
soft: true;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Theme ---------- */
|
||||
|
||||
const MOBILE = '@media (max-width:600px)';
|
||||
|
||||
export const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: t.ColorInteractiveDefault,
|
||||
dark: t.ColorInteractiveActive,
|
||||
light: t.ColorBrand400,
|
||||
contrastText: t.ColorTextInverse,
|
||||
},
|
||||
secondary: {
|
||||
main: t.ColorNeutral600,
|
||||
dark: t.ColorNeutral700,
|
||||
light: t.ColorNeutral300,
|
||||
contrastText: t.ColorTextInverse,
|
||||
},
|
||||
error: {
|
||||
main: t.ColorFeedbackError,
|
||||
light: t.ColorRed400,
|
||||
dark: t.ColorRed800,
|
||||
contrastText: t.ColorTextInverse,
|
||||
},
|
||||
warning: {
|
||||
main: t.ColorFeedbackWarning,
|
||||
light: t.ColorAmber400,
|
||||
dark: t.ColorAmber800,
|
||||
contrastText: t.ColorTextInverse,
|
||||
},
|
||||
success: {
|
||||
main: t.ColorFeedbackSuccess,
|
||||
light: t.ColorGreen400,
|
||||
dark: t.ColorGreen800,
|
||||
contrastText: t.ColorTextInverse,
|
||||
},
|
||||
info: {
|
||||
main: t.ColorFeedbackInfo,
|
||||
light: t.ColorBlue400,
|
||||
dark: t.ColorBlue800,
|
||||
contrastText: t.ColorTextInverse,
|
||||
},
|
||||
text: {
|
||||
primary: t.ColorTextPrimary,
|
||||
secondary: t.ColorTextSecondary,
|
||||
disabled: t.ColorTextDisabled,
|
||||
},
|
||||
background: {
|
||||
default: t.ColorSurfaceDefault,
|
||||
paper: t.ColorSurfaceRaised,
|
||||
},
|
||||
divider: t.ColorBorderDefault,
|
||||
action: {
|
||||
disabled: t.ColorTextDisabled,
|
||||
disabledBackground: t.ColorInteractiveDisabled,
|
||||
},
|
||||
},
|
||||
|
||||
typography: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
|
||||
/* ── Display (Noto Serif SC, Regular 400) ── */
|
||||
displayHero: {
|
||||
fontFamily: t.FontFamilyDisplay,
|
||||
fontSize: t.TypographyDisplayHeroFontSize,
|
||||
fontWeight: t.TypographyDisplayHeroFontWeight,
|
||||
lineHeight: t.TypographyDisplayHeroLineHeight,
|
||||
letterSpacing: t.TypographyDisplayHeroLetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyDisplayHeroFontSizeMobile },
|
||||
},
|
||||
display1: {
|
||||
fontFamily: t.FontFamilyDisplay,
|
||||
fontSize: t.TypographyDisplay1FontSize,
|
||||
fontWeight: t.TypographyDisplay1FontWeight,
|
||||
lineHeight: t.TypographyDisplay1LineHeight,
|
||||
letterSpacing: t.TypographyDisplay1LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyDisplay1FontSizeMobile },
|
||||
},
|
||||
display2: {
|
||||
fontFamily: t.FontFamilyDisplay,
|
||||
fontSize: t.TypographyDisplay2FontSize,
|
||||
fontWeight: t.TypographyDisplay2FontWeight,
|
||||
lineHeight: t.TypographyDisplay2LineHeight,
|
||||
letterSpacing: t.TypographyDisplay2LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyDisplay2FontSizeMobile },
|
||||
},
|
||||
display3: {
|
||||
fontFamily: t.FontFamilyDisplay,
|
||||
fontSize: t.TypographyDisplay3FontSize,
|
||||
fontWeight: t.TypographyDisplay3FontWeight,
|
||||
lineHeight: t.TypographyDisplay3LineHeight,
|
||||
letterSpacing: t.TypographyDisplay3LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyDisplay3FontSizeMobile },
|
||||
},
|
||||
displaySm: {
|
||||
fontFamily: t.FontFamilyDisplay,
|
||||
fontSize: t.TypographyDisplaySmFontSize,
|
||||
fontWeight: t.TypographyDisplaySmFontWeight,
|
||||
lineHeight: t.TypographyDisplaySmLineHeight,
|
||||
letterSpacing: t.TypographyDisplaySmLetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyDisplaySmFontSizeMobile },
|
||||
},
|
||||
|
||||
/* ── Headings (Montserrat, Bold 700) ── */
|
||||
h1: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyH1FontSize,
|
||||
fontWeight: t.TypographyH1FontWeight,
|
||||
lineHeight: t.TypographyH1LineHeight,
|
||||
letterSpacing: t.TypographyH1LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyH1FontSizeMobile },
|
||||
},
|
||||
h2: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyH2FontSize,
|
||||
fontWeight: t.TypographyH2FontWeight,
|
||||
lineHeight: t.TypographyH2LineHeight,
|
||||
letterSpacing: t.TypographyH2LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyH2FontSizeMobile },
|
||||
},
|
||||
h3: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyH3FontSize,
|
||||
fontWeight: t.TypographyH3FontWeight,
|
||||
lineHeight: t.TypographyH3LineHeight,
|
||||
letterSpacing: t.TypographyH3LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyH3FontSizeMobile },
|
||||
},
|
||||
h4: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyH4FontSize,
|
||||
fontWeight: t.TypographyH4FontWeight,
|
||||
lineHeight: t.TypographyH4LineHeight,
|
||||
letterSpacing: t.TypographyH4LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyH4FontSizeMobile },
|
||||
},
|
||||
h5: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyH5FontSize,
|
||||
fontWeight: t.TypographyH5FontWeight,
|
||||
lineHeight: t.TypographyH5LineHeight,
|
||||
letterSpacing: t.TypographyH5LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyH5FontSizeMobile },
|
||||
},
|
||||
h6: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyH6FontSize,
|
||||
fontWeight: t.TypographyH6FontWeight,
|
||||
lineHeight: t.TypographyH6LineHeight,
|
||||
letterSpacing: t.TypographyH6LetterSpacing,
|
||||
[MOBILE]: { fontSize: t.TypographyH6FontSizeMobile },
|
||||
},
|
||||
|
||||
/* ── Body (Montserrat, Medium 500) ── */
|
||||
bodyLg: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyBodyLgFontSize,
|
||||
fontWeight: t.TypographyBodyLgFontWeight,
|
||||
lineHeight: t.TypographyBodyLgLineHeight,
|
||||
letterSpacing: t.TypographyBodyLgLetterSpacing,
|
||||
},
|
||||
body1: {
|
||||
fontSize: t.TypographyBodyFontSize,
|
||||
fontWeight: t.TypographyBodyFontWeight,
|
||||
lineHeight: t.TypographyBodyLineHeight,
|
||||
letterSpacing: t.TypographyBodyLetterSpacing,
|
||||
},
|
||||
body2: {
|
||||
fontSize: t.TypographyBodySmFontSize,
|
||||
fontWeight: t.TypographyBodySmFontWeight,
|
||||
lineHeight: t.TypographyBodySmLineHeight,
|
||||
letterSpacing: t.TypographyBodySmLetterSpacing,
|
||||
},
|
||||
bodyXs: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyBodyXsFontSize,
|
||||
fontWeight: t.TypographyBodyXsFontWeight,
|
||||
lineHeight: t.TypographyBodyXsLineHeight,
|
||||
letterSpacing: t.TypographyBodyXsLetterSpacing,
|
||||
},
|
||||
|
||||
/* ── Subtitle (maps to body variants for MUI compat) ── */
|
||||
subtitle1: {
|
||||
fontSize: t.TypographyBodyLgFontSize,
|
||||
fontWeight: t.TypographyBodyLgFontWeight,
|
||||
lineHeight: t.TypographyBodyLgLineHeight,
|
||||
},
|
||||
subtitle2: {
|
||||
fontSize: t.TypographyLabelFontSize,
|
||||
fontWeight: t.TypographyLabelFontWeight,
|
||||
lineHeight: t.TypographyLabelLineHeight,
|
||||
letterSpacing: t.TypographyLabelLetterSpacing,
|
||||
},
|
||||
|
||||
/* ── Label (Montserrat, Medium 500) ── */
|
||||
labelLg: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyLabelLgFontSize,
|
||||
fontWeight: t.TypographyLabelLgFontWeight,
|
||||
lineHeight: t.TypographyLabelLgLineHeight,
|
||||
letterSpacing: t.TypographyLabelLgLetterSpacing,
|
||||
},
|
||||
label: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyLabelFontSize,
|
||||
fontWeight: t.TypographyLabelFontWeight,
|
||||
lineHeight: t.TypographyLabelLineHeight,
|
||||
letterSpacing: t.TypographyLabelLetterSpacing,
|
||||
},
|
||||
labelSm: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyLabelSmFontSize,
|
||||
fontWeight: t.TypographyLabelSmFontWeight,
|
||||
lineHeight: t.TypographyLabelSmLineHeight,
|
||||
letterSpacing: t.TypographyLabelSmLetterSpacing,
|
||||
},
|
||||
|
||||
/* ── Caption (Montserrat, Regular 400) ── */
|
||||
caption: {
|
||||
fontSize: t.TypographyCaptionFontSize,
|
||||
fontWeight: t.TypographyCaptionFontWeight,
|
||||
lineHeight: t.TypographyCaptionLineHeight,
|
||||
letterSpacing: t.TypographyCaptionLetterSpacing,
|
||||
},
|
||||
captionSm: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyCaptionSmFontSize,
|
||||
fontWeight: t.TypographyCaptionSmFontWeight,
|
||||
lineHeight: t.TypographyCaptionSmLineHeight,
|
||||
letterSpacing: t.TypographyCaptionSmLetterSpacing,
|
||||
},
|
||||
|
||||
/* ── Overline (Montserrat, SemiBold 600, uppercase) ── */
|
||||
overline: {
|
||||
fontSize: t.TypographyOverlineFontSize,
|
||||
fontWeight: t.TypographyOverlineFontWeight,
|
||||
lineHeight: t.TypographyOverlineLineHeight,
|
||||
letterSpacing: t.TypographyOverlineLetterSpacing,
|
||||
textTransform: 'uppercase' as const,
|
||||
},
|
||||
overlineSm: {
|
||||
fontFamily: t.FontFamilyBody,
|
||||
fontSize: t.TypographyOverlineSmFontSize,
|
||||
fontWeight: t.TypographyOverlineSmFontWeight,
|
||||
lineHeight: t.TypographyOverlineSmLineHeight,
|
||||
letterSpacing: t.TypographyOverlineSmLetterSpacing,
|
||||
textTransform: 'uppercase' as const,
|
||||
},
|
||||
|
||||
/* ── Button text ── */
|
||||
button: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.02em',
|
||||
textTransform: 'none' as const,
|
||||
},
|
||||
},
|
||||
|
||||
spacing: 4,
|
||||
|
||||
shape: {
|
||||
borderRadius: parseInt(t.BorderRadiusMd, 10),
|
||||
},
|
||||
|
||||
components: {
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: parseInt(t.ButtonBorderRadiusDefault, 10),
|
||||
textTransform: 'none' as const,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.02em',
|
||||
transition: 'background-color 150ms ease-in-out, border-color 150ms ease-in-out, box-shadow 150ms ease-in-out',
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${t.ColorInteractiveFocus}`,
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
},
|
||||
sizeSmall: {
|
||||
minHeight: parseInt(t.ButtonHeightSm, 10),
|
||||
padding: `${t.ButtonPaddingYSm} ${t.ButtonPaddingXSm}`,
|
||||
fontSize: t.ButtonFontSizeSm,
|
||||
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapSm },
|
||||
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapSm },
|
||||
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||
fontSize: t.ButtonIconSizeSm,
|
||||
},
|
||||
},
|
||||
sizeMedium: {
|
||||
minHeight: parseInt(t.ButtonHeightMd, 10),
|
||||
padding: `${t.ButtonPaddingYMd} ${t.ButtonPaddingXMd}`,
|
||||
fontSize: t.ButtonFontSizeMd,
|
||||
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapMd },
|
||||
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapMd },
|
||||
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||
fontSize: t.ButtonIconSizeMd,
|
||||
},
|
||||
},
|
||||
sizeLarge: {
|
||||
minHeight: parseInt(t.ButtonHeightLg, 10),
|
||||
padding: `${t.ButtonPaddingYLg} ${t.ButtonPaddingXLg}`,
|
||||
fontSize: t.ButtonFontSizeLg,
|
||||
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapLg },
|
||||
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapLg },
|
||||
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||
fontSize: t.ButtonIconSizeLg,
|
||||
},
|
||||
},
|
||||
containedPrimary: {
|
||||
'&:hover': { backgroundColor: t.ColorInteractiveHover },
|
||||
'&:active': { backgroundColor: t.ColorInteractiveActive },
|
||||
},
|
||||
containedSecondary: {
|
||||
'&:hover': { backgroundColor: t.ColorNeutral700 },
|
||||
},
|
||||
outlinedPrimary: {
|
||||
borderColor: t.ColorInteractiveDefault,
|
||||
color: t.ColorInteractiveDefault,
|
||||
'&:hover': {
|
||||
borderColor: t.ColorInteractiveHover,
|
||||
color: t.ColorInteractiveHover,
|
||||
backgroundColor: t.ColorBrand100,
|
||||
},
|
||||
'&:active': {
|
||||
borderColor: t.ColorInteractiveActive,
|
||||
color: t.ColorInteractiveActive,
|
||||
},
|
||||
},
|
||||
outlinedSecondary: {
|
||||
borderColor: t.ColorNeutral400,
|
||||
color: t.ColorNeutral600,
|
||||
'&:hover': {
|
||||
borderColor: t.ColorNeutral600,
|
||||
color: t.ColorNeutral700,
|
||||
backgroundColor: t.ColorNeutral200,
|
||||
},
|
||||
},
|
||||
textPrimary: {
|
||||
color: t.ColorInteractiveDefault,
|
||||
'&:hover': {
|
||||
color: t.ColorInteractiveHover,
|
||||
backgroundColor: t.ColorBrand100,
|
||||
},
|
||||
'&:active': {
|
||||
color: t.ColorInteractiveActive,
|
||||
},
|
||||
},
|
||||
textSecondary: {
|
||||
color: t.ColorNeutral600,
|
||||
'&:hover': {
|
||||
color: t.ColorNeutral700,
|
||||
backgroundColor: t.ColorNeutral200,
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: { size: 'xs' as const },
|
||||
style: {
|
||||
minHeight: parseInt(t.ButtonHeightXs, 10),
|
||||
padding: `${t.ButtonPaddingYXs} ${t.ButtonPaddingXXs}`,
|
||||
fontSize: t.ButtonFontSizeXs,
|
||||
'& .MuiButton-startIcon': { marginRight: t.ButtonIconGapXs },
|
||||
'& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapXs },
|
||||
'& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': {
|
||||
fontSize: t.ButtonIconSizeXs,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: { variant: 'soft' as const, color: 'primary' as const },
|
||||
style: {
|
||||
backgroundColor: t.ColorBrand200,
|
||||
color: t.ColorInteractiveHover,
|
||||
'&:hover': { backgroundColor: t.ColorBrand300 },
|
||||
'&:active': { backgroundColor: t.ColorBrand400 },
|
||||
},
|
||||
},
|
||||
{
|
||||
props: { variant: 'soft' as const, color: 'secondary' as const },
|
||||
style: {
|
||||
backgroundColor: t.ColorNeutral200,
|
||||
color: t.ColorNeutral700,
|
||||
'&:hover': { backgroundColor: t.ColorNeutral300 },
|
||||
'&:active': { backgroundColor: t.ColorNeutral400, color: t.ColorNeutral700 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: parseInt(t.BorderRadiusMd, 10),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: parseInt(t.InputBorderRadiusDefault, 10),
|
||||
transition: 'box-shadow 150ms ease-in-out',
|
||||
// Default (medium) height
|
||||
minHeight: parseInt(t.InputHeightMd, 10),
|
||||
// Small size
|
||||
'&.MuiInputBase-sizeSmall': {
|
||||
minHeight: parseInt(t.InputHeightSm, 10),
|
||||
},
|
||||
// Default border
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: t.ColorNeutral300,
|
||||
transition: 'border-color 150ms ease-in-out',
|
||||
},
|
||||
// Hover — darker border (skip when focused, error, or disabled)
|
||||
'&:hover:not(.Mui-focused):not(.Mui-error):not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: t.ColorNeutral400,
|
||||
},
|
||||
// Focus — brand gold border + double ring (white gap + brand ring)
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: t.ColorBrand500,
|
||||
borderWidth: '1px',
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
boxShadow: `0 0 0 3px ${t.ColorWhite}, 0 0 0 5px ${t.ColorBrand500}`,
|
||||
},
|
||||
// Error border
|
||||
'&.Mui-error .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: t.ColorFeedbackError,
|
||||
borderWidth: '1px',
|
||||
},
|
||||
// Error + focused — error-coloured ring
|
||||
'&.Mui-error.Mui-focused': {
|
||||
boxShadow: `0 0 0 3px ${t.ColorWhite}, 0 0 0 5px ${t.ColorFeedbackError}`,
|
||||
},
|
||||
// Disabled — muted background
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: t.ColorNeutral200,
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: t.ColorNeutral300,
|
||||
},
|
||||
},
|
||||
// Adornment icon sizing
|
||||
'& .MuiInputAdornment-root': {
|
||||
color: t.ColorNeutral400,
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: t.InputIconSizeDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: {
|
||||
padding: `${t.InputPaddingYMd} ${t.InputPaddingXDefault}`,
|
||||
fontSize: t.InputFontSizeDefault,
|
||||
color: t.ColorTextPrimary,
|
||||
'&::placeholder': {
|
||||
color: t.ColorNeutral400,
|
||||
opacity: 1,
|
||||
},
|
||||
// Small size padding
|
||||
'.MuiInputBase-sizeSmall &': {
|
||||
padding: `${t.InputPaddingYSm} ${t.InputPaddingXDefault}`,
|
||||
},
|
||||
},
|
||||
notchedOutline: {
|
||||
// Reset top offset — MUI defaults to -5px for floating label space.
|
||||
// We use external labels, so the border should be flush with the input.
|
||||
top: 0,
|
||||
// Hide the notch legend — we use external labels
|
||||
'& legend': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
multiline: {
|
||||
padding: 0,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: `${t.InputPaddingYMd} ${t.InputPaddingXDefault}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user