format: Apply Prettier to existing codebase

Formatting-only changes across all component and story files.
No logic or behaviour changes — only whitespace, line breaks, and trailing commas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 16:42:16 +11:00
parent aa7cdeecf0
commit 047d913960
46 changed files with 1510 additions and 886 deletions

View File

@@ -78,12 +78,24 @@ export const AllColoursFilled: Story = {
name: 'All Colours — Filled',
render: () => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Badge variant="filled" color="default">Default</Badge>
<Badge variant="filled" color="brand">Brand</Badge>
<Badge variant="filled" color="success">Success</Badge>
<Badge variant="filled" color="warning">Warning</Badge>
<Badge variant="filled" color="error">Error</Badge>
<Badge variant="filled" color="info">Info</Badge>
<Badge variant="filled" color="default">
Default
</Badge>
<Badge variant="filled" color="brand">
Brand
</Badge>
<Badge variant="filled" color="success">
Success
</Badge>
<Badge variant="filled" color="warning">
Warning
</Badge>
<Badge variant="filled" color="error">
Error
</Badge>
<Badge variant="filled" color="info">
Info
</Badge>
</div>
),
};
@@ -95,11 +107,21 @@ export const WithIcons: Story = {
name: 'With Icons',
render: () => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Badge color="brand" icon={<StarIcon />}>Popular</Badge>
<Badge color="success" icon={<CheckCircleIcon />}>Verified</Badge>
<Badge color="warning" icon={<WarningAmberIcon />}>Limited</Badge>
<Badge color="error" icon={<ErrorOutlineIcon />}>Sold out</Badge>
<Badge color="info" icon={<InfoOutlinedIcon />}>New</Badge>
<Badge color="brand" icon={<StarIcon />}>
Popular
</Badge>
<Badge color="success" icon={<CheckCircleIcon />}>
Verified
</Badge>
<Badge color="warning" icon={<WarningAmberIcon />}>
Limited
</Badge>
<Badge color="error" icon={<ErrorOutlineIcon />}>
Sold out
</Badge>
<Badge color="info" icon={<InfoOutlinedIcon />}>
New
</Badge>
</div>
),
};
@@ -109,11 +131,21 @@ export const WithIconsFilled: Story = {
name: 'With Icons — Filled',
render: () => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Badge variant="filled" color="brand" icon={<StarIcon />}>Popular</Badge>
<Badge variant="filled" color="success" icon={<CheckCircleIcon />}>Included</Badge>
<Badge variant="filled" color="warning" icon={<WarningAmberIcon />}>Attention</Badge>
<Badge variant="filled" color="error" icon={<ErrorOutlineIcon />}>Unavailable</Badge>
<Badge variant="filled" color="info" icon={<InfoOutlinedIcon />}>Updated</Badge>
<Badge variant="filled" color="brand" icon={<StarIcon />}>
Popular
</Badge>
<Badge variant="filled" color="success" icon={<CheckCircleIcon />}>
Included
</Badge>
<Badge variant="filled" color="warning" icon={<WarningAmberIcon />}>
Attention
</Badge>
<Badge variant="filled" color="error" icon={<ErrorOutlineIcon />}>
Unavailable
</Badge>
<Badge variant="filled" color="info" icon={<InfoOutlinedIcon />}>
Updated
</Badge>
</div>
),
};
@@ -124,9 +156,15 @@ export const WithIconsFilled: Story = {
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Badge size="small" color="brand" icon={<StarIcon />}>Small</Badge>
<Badge size="medium" color="brand" icon={<StarIcon />}>Medium</Badge>
<Badge size="large" color="brand" icon={<StarIcon />}>Large</Badge>
<Badge size="small" color="brand" icon={<StarIcon />}>
Small
</Badge>
<Badge size="medium" color="brand" icon={<StarIcon />}>
Medium
</Badge>
<Badge size="large" color="brand" icon={<StarIcon />}>
Large
</Badge>
</div>
),
};
@@ -146,7 +184,9 @@ export const InPriceCard: Story = {
<Typography variant="overline" color="text.secondary">
Essential
</Typography>
<Badge size="small" color="default">Standard</Badge>
<Badge size="small" color="default">
Standard
</Badge>
</Box>
<Typography variant="display3" color="primary" sx={{ mb: 1 }}>
$3,200
@@ -162,7 +202,9 @@ export const InPriceCard: Story = {
<Typography variant="overline" color="text.secondary">
Premium
</Typography>
<Badge color="brand" icon={<StarIcon />}>Most popular</Badge>
<Badge color="brand" icon={<StarIcon />}>
Most popular
</Badge>
</Box>
<Typography variant="display3" color="primary" sx={{ mb: 1 }}>
$5,800
@@ -178,7 +220,9 @@ export const InPriceCard: Story = {
<Typography variant="overline" color="text.secondary">
Bespoke
</Typography>
<Badge color="info" icon={<LocalOfferIcon />}>Best value</Badge>
<Badge color="info" icon={<LocalOfferIcon />}>
Best value
</Badge>
</Box>
<Typography variant="display3" color="primary" sx={{ mb: 1 }}>
$8,500
@@ -202,11 +246,46 @@ export const ServiceStatus: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 500 }}>
{[
{ service: 'Chapel ceremony', badge: <Badge color="success" icon={<CheckCircleIcon />}>Confirmed</Badge> },
{ service: 'Floral arrangements', badge: <Badge color="warning" icon={<WarningAmberIcon />}>Pending</Badge> },
{ service: 'Catering', badge: <Badge color="error" icon={<ErrorOutlineIcon />}>Unavailable</Badge> },
{ service: 'Memorial printing', badge: <Badge color="info" icon={<NewReleasesIcon />}>New option</Badge> },
{ service: 'Premium casket', badge: <Badge variant="filled" color="brand" icon={<VerifiedIcon />}>Included</Badge> },
{
service: 'Chapel ceremony',
badge: (
<Badge color="success" icon={<CheckCircleIcon />}>
Confirmed
</Badge>
),
},
{
service: 'Floral arrangements',
badge: (
<Badge color="warning" icon={<WarningAmberIcon />}>
Pending
</Badge>
),
},
{
service: 'Catering',
badge: (
<Badge color="error" icon={<ErrorOutlineIcon />}>
Unavailable
</Badge>
),
},
{
service: 'Memorial printing',
badge: (
<Badge color="info" icon={<NewReleasesIcon />}>
New option
</Badge>
),
},
{
service: 'Premium casket',
badge: (
<Badge variant="filled" color="brand" icon={<VerifiedIcon />}>
Included
</Badge>
),
},
].map((item) => (
<Card key={item.service} variant="outlined" padding="compact">
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@@ -231,7 +310,14 @@ export const CompleteMatrix: Story = {
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{(['soft', 'filled'] as const).map((variant) => (
<div key={variant}>
<div style={{ marginBottom: 8, fontWeight: 600, fontSize: 14, textTransform: 'capitalize' }}>
<div
style={{
marginBottom: 8,
fontWeight: 600,
fontSize: 14,
textTransform: 'capitalize',
}}
>
{variant}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
@@ -239,7 +325,13 @@ export const CompleteMatrix: Story = {
<div key={size} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ width: 60, fontSize: 12, color: '#737373' }}>{size}</span>
{colors.map((color) => (
<Badge key={color} variant={variant} color={color} size={size} icon={<StarIcon />}>
<Badge
key={color}
variant={variant}
color={color}
size={size}
icon={<StarIcon />}
>
{color}
</Badge>
))}

View File

@@ -87,7 +87,9 @@ export const FigmaMapping: Story = {
<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="soft" color="secondary">
Sec / Grey
</Button>
<Button variant="text">Ghost</Button>
</div>
),
@@ -113,10 +115,18 @@ 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>
<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>
),
};
@@ -140,10 +150,18 @@ 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>
<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>
),
};
@@ -180,10 +198,18 @@ 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>
<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>
),
};
@@ -204,9 +230,15 @@ export const DisabledAllVariants: Story = {
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>
<Button disabled variant="soft">
Soft
</Button>
<Button disabled variant="outlined">
Outlined
</Button>
<Button disabled variant="text">
Text
</Button>
</div>
),
};
@@ -225,9 +257,15 @@ export const LoadingAllVariants: Story = {
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>
<Button loading variant="soft">
Processing...
</Button>
<Button loading variant="outlined">
Processing...
</Button>
<Button loading variant="text">
Loading...
</Button>
</div>
),
};
@@ -293,9 +331,15 @@ export const TextButtonComparison: Story = {
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>
<Button variant="text" underline>
With underline
</Button>
<Button variant="text" color="secondary">
Secondary
</Button>
<Button variant="text" color="secondary" underline>
Secondary underlined
</Button>
</div>
),
};
@@ -305,10 +349,18 @@ 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>
<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>
),
};
@@ -348,15 +400,27 @@ export const CompleteMatrix: Story = {
<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' }}>
<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>
<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>
))}

View File

@@ -69,13 +69,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{children}
{loading && (
<>
<CircularProgress
size={16}
color="inherit"
thickness={3}
aria-hidden
sx={{ ml: 1 }}
/>
<CircularProgress size={16} color="inherit" thickness={3} aria-hidden sx={{ ml: 1 }} />
<span
style={{
position: 'absolute',

View File

@@ -52,8 +52,8 @@ export const Default: Story = {
Funeral package
</Typography>
<Typography variant="body1" color="text.secondary">
A comprehensive service including chapel ceremony, transport, and
preparation. Suitable for families seeking a traditional farewell.
A comprehensive service including chapel ceremony, transport, and preparation. Suitable
for families seeking a traditional farewell.
</Typography>
</>
),
@@ -99,12 +99,7 @@ export const Variants: Story = {
export const Interactive: Story = {
render: () => (
<div style={{ display: 'flex', gap: 24, maxWidth: 800 }}>
<Card
interactive
sx={{ flex: 1 }}
tabIndex={0}
onClick={() => alert('Card clicked')}
>
<Card interactive sx={{ flex: 1 }} tabIndex={0} onClick={() => alert('Card clicked')}>
<Typography variant="h5" gutterBottom>
Elevated + Interactive
</Typography>
@@ -305,11 +300,15 @@ export const OnDifferentBackgrounds: Story = {
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card variant="elevated">
<Typography variant="labelLg">Elevated</Typography>
<Typography variant="body2" color="text.secondary">Shadow defines edges</Typography>
<Typography variant="body2" color="text.secondary">
Shadow defines edges
</Typography>
</Card>
<Card variant="outlined">
<Typography variant="labelLg">Outlined</Typography>
<Typography variant="body2" color="text.secondary">Border defines edges</Typography>
<Typography variant="body2" color="text.secondary">
Border defines edges
</Typography>
</Card>
</div>
</div>
@@ -328,11 +327,15 @@ export const OnDifferentBackgrounds: Story = {
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card variant="elevated">
<Typography variant="labelLg">Elevated</Typography>
<Typography variant="body2" color="text.secondary">White card + shadow on grey</Typography>
<Typography variant="body2" color="text.secondary">
White card + shadow on grey
</Typography>
</Card>
<Card variant="outlined">
<Typography variant="labelLg">Outlined</Typography>
<Typography variant="body2" color="text.secondary">Contrast + border on grey</Typography>
<Typography variant="body2" color="text.secondary">
Contrast + border on grey
</Typography>
</Card>
</div>
</div>
@@ -395,8 +398,8 @@ export const PriceCardPreview: Story = {
$3,200
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
A respectful and simple service with chapel ceremony, transport, and
professional preparation.
A respectful and simple service with chapel ceremony, transport, and professional
preparation.
</Typography>
<Button fullWidth size="large">
Select this package
@@ -432,8 +435,8 @@ export const WithImage: Story = {
Parsons Chapel
</Typography>
<Typography variant="body2" color="text.secondary">
Our heritage-listed chapel seats up to 120 guests and features
modern audio-visual facilities.
Our heritage-listed chapel seats up to 120 guests and features modern audio-visual
facilities.
</Typography>
</Box>
</Card>

View File

@@ -58,9 +58,7 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(
const muiVariant = variant === 'outlined' ? 'outlined' : undefined;
// Interactive cards need keyboard operability
const interactiveProps = interactive
? { tabIndex: 0 as const, role: 'button' as const }
: {};
const interactiveProps = interactive ? { tabIndex: 0 as const, role: 'button' as const } : {};
return (
<MuiCard
@@ -88,8 +86,7 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(
// Focus-visible for keyboard accessibility on interactive cards
interactive && {
'&:focus-visible': {
outline: (theme: Theme) =>
`2px solid ${theme.palette.primary.main}`,
outline: (theme: Theme) => `2px solid ${theme.palette.primary.main}`,
outlineOffset: '2px',
},
},

View File

@@ -117,7 +117,12 @@ export const Clickable: Story = {
<Chip label="Clickable default" onClick={() => {}} />
<Chip label="Clickable primary" color="primary" onClick={() => {}} />
<Chip label="Clickable outlined" variant="outlined" onClick={() => {}} />
<Chip label="Clickable outlined primary" variant="outlined" color="primary" onClick={() => {}} />
<Chip
label="Clickable outlined primary"
variant="outlined"
color="primary"
onClick={() => {}}
/>
</Box>
),
};
@@ -143,14 +148,18 @@ export const Selected: Story = {
render: () => (
<Box sx={{ display: 'flex', gap: 2, flexDirection: 'column' }}>
<Box>
<Typography variant="label" sx={{ mb: 1 }}>Filled</Typography>
<Typography variant="label" sx={{ mb: 1 }}>
Filled
</Typography>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<Chip label="Not selected" onClick={() => {}} />
<Chip label="Selected" selected onClick={() => {}} />
</Box>
</Box>
<Box>
<Typography variant="label" sx={{ mb: 1 }}>Outlined</Typography>
<Typography variant="label" sx={{ mb: 1 }}>
Outlined
</Typography>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<Chip variant="outlined" label="Not selected" onClick={() => {}} />
<Chip variant="outlined" label="Selected" selected onClick={() => {}} />
@@ -253,12 +262,7 @@ export const RemovableTags: Story = {
</Typography>
) : (
tags.map((tag) => (
<Chip
key={tag}
label={tag}
color="primary"
onDelete={() => remove(tag)}
/>
<Chip key={tag} label={tag} color="primary" onDelete={() => remove(tag)} />
))
)}
</Box>
@@ -283,7 +287,9 @@ export const InServiceOption: Story = {
<Card interactive>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', mb: 1 }}>
<Typography variant="h5">Chapel Ceremony</Typography>
<Typography variant="display3" color="primary">$1,200</Typography>
<Typography variant="display3" color="primary">
$1,200
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Traditional chapel service with celebrant and music of your choosing.
@@ -298,7 +304,9 @@ export const InServiceOption: Story = {
<Card interactive>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', mb: 1 }}>
<Typography variant="h5">Graveside Service</Typography>
<Typography variant="display3" color="primary">$900</Typography>
<Typography variant="display3" color="primary">
$900
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Intimate outdoor farewell at the burial site.
@@ -337,8 +345,20 @@ export const CompleteMatrix: Story = {
{colors.map((color) => (
<React.Fragment key={color}>
<Chip variant={variant} color={color} size={size} label={color} />
<Chip variant={variant} color={color} size={size} label={`${color} + icon`} icon={<LocalOfferIcon />} />
<Chip variant={variant} color={color} size={size} label={`${color} delete`} onDelete={() => {}} />
<Chip
variant={variant}
color={color}
size={size}
label={`${color} + icon`}
icon={<LocalOfferIcon />}
/>
<Chip
variant={variant}
color={color}
size={size}
label={`${color} delete`}
onDelete={() => {}}
/>
</React.Fragment>
))}
</Box>
@@ -348,7 +368,9 @@ export const CompleteMatrix: Story = {
))}
<Box>
<Typography variant="label" sx={{ mb: 1 }}>Selected state</Typography>
<Typography variant="label" sx={{ mb: 1 }}>
Selected state
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Chip selected label="Filled selected" onClick={() => {}} />
<Chip selected variant="outlined" label="Outlined selected" onClick={() => {}} />

View File

@@ -44,16 +44,7 @@ export interface ChipProps extends MuiChipProps {
* is being removed.
*/
export const Chip = React.forwardRef<HTMLDivElement, ChipProps>(
(
{
selected = false,
variant = 'filled',
color,
sx,
...props
},
ref,
) => {
({ selected = false, variant = 'filled', color, sx, ...props }, ref) => {
// When selected, promote to primary colour unless explicitly set
const resolvedColor = color ?? (selected ? 'primary' : 'default');
@@ -63,11 +54,12 @@ export const Chip = React.forwardRef<HTMLDivElement, ChipProps>(
variant={variant}
color={resolvedColor}
sx={[
selected && variant === 'outlined' && {
borderWidth: 2,
borderColor: 'var(--fa-color-brand-500)',
backgroundColor: 'var(--fa-color-brand-50)',
},
selected &&
variant === 'outlined' && {
borderWidth: 2,
borderColor: 'var(--fa-color-brand-500)',
backgroundColor: 'var(--fa-color-brand-50)',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...props}

View File

@@ -19,7 +19,13 @@ type Story = StoryObj<typeof Divider>;
// ─── Default ────────────────────────────────────────────────────────────────
export const Default: Story = {
decorators: [(Story) => <Box sx={{ width: 400 }}><Story /></Box>],
decorators: [
(Story) => (
<Box sx={{ width: 400 }}>
<Story />
</Box>
),
],
};
// ─── Variants ───────────────────────────────────────────────────────────────
@@ -84,9 +90,7 @@ export const InContent: Story = {
</Box>
<Divider />
<Box sx={{ fontWeight: 600, mt: 2, mb: 1 }}>Venue</Box>
<Box sx={{ fontSize: 14, color: 'text.secondary', mb: 2 }}>
West Chapel, Strathfield
</Box>
<Box sx={{ fontSize: 14, color: 'text.secondary', mb: 2 }}>West Chapel, Strathfield</Box>
<Divider />
<Box sx={{ fontWeight: 600, mt: 2, mb: 1 }}>Total</Box>
<Box sx={{ fontSize: 14, color: 'text.primary' }}>$2,400</Box>

View File

@@ -116,7 +116,17 @@ export const CommonUseCases: Story = {
{/* Card actions toolbar */}
<Box>
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Card action toolbar</Box>
<Box sx={{ display: 'flex', gap: 1, p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, width: 'fit-content' }}>
<Box
sx={{
display: 'flex',
gap: 1,
p: 1,
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
width: 'fit-content',
}}
>
<IconButton size="small" color="primary" aria-label="Favourite">
<FavoriteBorderIcon />
</IconButton>
@@ -132,7 +142,18 @@ export const CommonUseCases: Story = {
{/* Dialog close */}
<Box>
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Dialog close button</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1, width: 300 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
width: 300,
}}
>
<Box sx={{ fontWeight: 600 }}>Confirm Selection</Box>
<IconButton size="small" aria-label="Close dialog">
<CloseIcon />
@@ -143,7 +164,17 @@ export const CommonUseCases: Story = {
{/* Navigation header */}
<Box>
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Mobile navigation toggle</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1, backgroundColor: 'var(--fa-color-brand-50)', borderRadius: 1, width: 'fit-content' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
p: 1,
backgroundColor: 'var(--fa-color-brand-50)',
borderRadius: 1,
width: 'fit-content',
}}
>
<IconButton size="large" aria-label="Open menu">
<MenuIcon />
</IconButton>

View File

@@ -122,10 +122,7 @@ export const FigmaMapping: Story = {
helperText="Input Label - Description"
endIcon={<SearchIcon />}
/>
<Input
placeholder="Select an option"
endIcon={<SearchIcon />}
/>
<Input placeholder="Select an option" endIcon={<SearchIcon />} />
<Input placeholder="Select an option" />
</div>
),
@@ -222,20 +219,16 @@ export const SizeAlignment: Story = {
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>
<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>
<Input placeholder="Quick search..." endIcon={<SearchIcon />} size="small" />
<Button size="medium" sx={{ minWidth: 100, minHeight: 40 }}>
Search
</Button>
</div>
</div>
),
@@ -248,11 +241,7 @@ 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="Search" placeholder="Search services..." endIcon={<SearchIcon />} />
<Input
label="Email"
placeholder="you@example.com"
@@ -265,12 +254,7 @@ export const WithIcons: Story = {
startIcon={<PhoneOutlinedIcon />}
type="tel"
/>
<Input
label="Amount"
placeholder="0.00"
startIcon={<AttachMoneyIcon />}
type="number"
/>
<Input label="Amount" placeholder="0.00" startIcon={<AttachMoneyIcon />} type="number" />
<Input
label="Email verified"
defaultValue="john@example.com"
@@ -355,18 +339,22 @@ export const ValidationFlow: Story = {
required
startIcon={<EmailOutlinedIcon />}
endIcon={
showSuccess ? <CheckCircleOutlineIcon sx={{ color: 'success.main' }} /> :
showError ? <ErrorOutlineIcon sx={{ color: 'error.main' }} /> :
undefined
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'
showError
? 'Please enter a valid email address'
: showSuccess
? 'Looks good!'
: 'Required for arrangement confirmation'
}
/>
);
@@ -387,9 +375,7 @@ export const ArrangementForm: Story = {
],
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ fontWeight: 700, fontSize: 20, marginBottom: 4 }}>
Contact details
</div>
<div style={{ fontWeight: 700, fontSize: 20, marginBottom: 4 }}>Contact details</div>
<Input
label="Full name"
placeholder="Enter your full name"
@@ -443,7 +429,16 @@ export const CompleteMatrix: Story = {
<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' }}>
<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 }}>

View File

@@ -77,11 +77,15 @@ export const Input = React.forwardRef<HTMLDivElement, InputProps>(
// Prefer convenience icon props; fall back to raw adornment props
const resolvedStart = startIcon ? (
<InputAdornment position="start">{startIcon}</InputAdornment>
) : startAdornment;
) : (
startAdornment
);
const resolvedEnd = endIcon ? (
<InputAdornment position="end">{endIcon}</InputAdornment>
) : endAdornment;
) : (
endAdornment
);
return (
<FormControl
@@ -133,18 +137,19 @@ export const Input = React.forwardRef<HTMLDivElement, InputProps>(
aria-describedby={helperId}
sx={[
// Success border + focus ring (not a native MUI state)
success && !error && {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'success.main',
success &&
!error && {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'success.main',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: 'success.main',
},
'&.Mui-focused': {
boxShadow: (theme: Theme) =>
`0 0 0 3px ${theme.palette.common.white}, 0 0 0 5px ${theme.palette.success.main}`,
},
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: 'success.main',
},
'&.Mui-focused': {
boxShadow: (theme: Theme) =>
`0 0 0 3px ${theme.palette.common.white}, 0 0 0 5px ${theme.palette.success.main}`,
},
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...props}

View File

@@ -32,15 +32,21 @@ export const UnderlineVariants: Story = {
render: () => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Link href="#" underline="hover">Hover (default)</Link>
<Link href="#" underline="hover">
Hover (default)
</Link>
<Box sx={{ fontSize: 11, color: 'text.secondary' }}>underline="hover"</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Link href="#" underline="always">Always underlined</Link>
<Link href="#" underline="always">
Always underlined
</Link>
<Box sx={{ fontSize: 11, color: 'text.secondary' }}>underline="always"</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Link href="#" underline="none">No underline</Link>
<Link href="#" underline="none">
No underline
</Link>
<Box sx={{ fontSize: 11, color: 'text.secondary' }}>underline="none"</Box>
</Box>
</Box>
@@ -58,13 +64,19 @@ export const ColourVariants: Story = {
<Link href="#">Brand link (default copper, 4.8:1 contrast)</Link>
</Box>
<Box>
<Link href="#" color="text.secondary">Secondary link (neutral grey)</Link>
<Link href="#" color="text.secondary">
Secondary link (neutral grey)
</Link>
</Box>
<Box>
<Link href="#" color="text.primary">Primary text link (charcoal)</Link>
<Link href="#" color="text.primary">
Primary text link (charcoal)
</Link>
</Box>
<Box>
<Link href="#" color="error.main">Error link (red for destructive actions)</Link>
<Link href="#" color="error.main">
Error link (red for destructive actions)
</Link>
</Box>
</Box>
),
@@ -76,12 +88,10 @@ export const ColourVariants: Story = {
export const Inline: Story = {
render: () => (
<Box sx={{ maxWidth: 500, lineHeight: 1.7 }}>
If you need help planning a funeral, our{' '}
<Link href="#">arrangement guide</Link> walks you through each
step. You can also browse our{' '}
<Link href="#">provider directory</Link> to find local funeral
directors, or <Link href="#">contact us</Link> directly for
personalised assistance.
If you need help planning a funeral, our <Link href="#">arrangement guide</Link> walks you
through each step. You can also browse our <Link href="#">provider directory</Link> to find
local funeral directors, or <Link href="#">contact us</Link> directly for personalised
assistance.
</Box>
),
};
@@ -92,9 +102,15 @@ export const Inline: Story = {
export const Navigation: Story = {
render: () => (
<Box sx={{ display: 'flex', gap: 3 }}>
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>FAQ</Link>
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>Contact Us</Link>
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>Log In</Link>
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>
FAQ
</Link>
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>
Contact Us
</Link>
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>
Log In
</Link>
</Box>
),
};
@@ -106,10 +122,18 @@ export const FooterLinks: Story = {
name: 'Footer Links',
render: () => (
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Link href="#" color="text.secondary" variant="body2">Privacy Policy</Link>
<Link href="#" color="text.secondary" variant="body2">Terms of Service</Link>
<Link href="#" color="text.secondary" variant="body2">Accessibility</Link>
<Link href="#" color="text.secondary" variant="body2">Cookie Settings</Link>
<Link href="#" color="text.secondary" variant="body2">
Privacy Policy
</Link>
<Link href="#" color="text.secondary" variant="body2">
Terms of Service
</Link>
<Link href="#" color="text.secondary" variant="body2">
Accessibility
</Link>
<Link href="#" color="text.secondary" variant="body2">
Cookie Settings
</Link>
</Box>
),
};
@@ -121,7 +145,15 @@ export const OnDifferentBackgrounds: Story = {
name: 'On Different Backgrounds',
render: () => (
<Box sx={{ display: 'flex', gap: 3 }}>
<Box sx={{ p: 3, backgroundColor: 'background.default', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
<Box
sx={{
p: 3,
backgroundColor: 'background.default',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ fontSize: 11, color: 'text.secondary', mb: 1 }}>White</Box>
<Link href="#">Learn more</Link>
</Box>

View File

@@ -63,7 +63,9 @@ export const Group: Story = {
name: 'Radio Group',
render: () => (
<FormControl>
<Typography variant="label" sx={{ mb: 1 }}>Service type</Typography>
<Typography variant="label" sx={{ mb: 1 }}>
Service type
</Typography>
<RadioGroup defaultValue="chapel">
<FormControlLabel value="chapel" control={<Radio />} label="Chapel ceremony" />
<FormControlLabel value="graveside" control={<Radio />} label="Graveside service" />
@@ -88,14 +90,31 @@ export const CardSelection: Story = {
const [selected, setSelected] = useState('standard');
const options = [
{ value: 'direct', label: 'Direct cremation', desc: 'Simple, dignified cremation with no service.', price: '$1,800' },
{ value: 'standard', label: 'Standard service', desc: 'Traditional chapel ceremony with viewing.', price: '$4,200' },
{ value: 'premium', label: 'Premium service', desc: 'Full service with personalised memorial.', price: '$7,500' },
{
value: 'direct',
label: 'Direct cremation',
desc: 'Simple, dignified cremation with no service.',
price: '$1,800',
},
{
value: 'standard',
label: 'Standard service',
desc: 'Traditional chapel ceremony with viewing.',
price: '$4,200',
},
{
value: 'premium',
label: 'Premium service',
desc: 'Full service with personalised memorial.',
price: '$7,500',
},
];
return (
<Box sx={{ maxWidth: 420 }}>
<Typography variant="h4" sx={{ mb: 2 }}>Choose a package</Typography>
<Typography variant="h4" sx={{ mb: 2 }}>
Choose a package
</Typography>
<RadioGroup value={selected} onChange={(e) => setSelected(e.target.value)}>
{options.map((opt) => (
<Card
@@ -110,11 +129,21 @@ export const CardSelection: Story = {
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<Radio value={opt.value} sx={{ mt: -0.5 }} />
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Typography variant="label">{opt.label}</Typography>
<Typography variant="labelLg" color="primary">{opt.price}</Typography>
<Typography variant="labelLg" color="primary">
{opt.price}
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">{opt.desc}</Typography>
<Typography variant="body2" color="text.secondary">
{opt.desc}
</Typography>
</Box>
</Box>
</Card>
@@ -135,7 +164,9 @@ export const PaymentMethod: Story = {
name: 'Interactive — Payment Method',
render: () => (
<FormControl>
<Typography variant="label" sx={{ mb: 1 }}>Payment method</Typography>
<Typography variant="label" sx={{ mb: 1 }}>
Payment method
</Typography>
<RadioGroup defaultValue="card" row>
<FormControlLabel value="card" control={<Radio />} label="Credit card" />
<FormControlLabel value="bank" control={<Radio />} label="Bank transfer" />

View File

@@ -77,39 +77,84 @@ export const ServiceAddOns: Story = {
};
const items = [
{ key: 'catering' as const, label: 'Catering', desc: 'Light refreshments after the service', price: '$450' },
{ key: 'flowers' as const, label: 'Floral arrangements', desc: 'Seasonal flowers for the chapel', price: '$280' },
{ key: 'music' as const, label: 'Live music', desc: 'Organist or solo musician', price: '$350' },
{ key: 'memorial' as const, label: 'Memorial video', desc: 'Photo slideshow with music', price: '$200' },
{ key: 'guestBook' as const, label: 'Guest book', desc: 'Leather-bound memorial guest book', price: '$85' },
{
key: 'catering' as const,
label: 'Catering',
desc: 'Light refreshments after the service',
price: '$450',
},
{
key: 'flowers' as const,
label: 'Floral arrangements',
desc: 'Seasonal flowers for the chapel',
price: '$280',
},
{
key: 'music' as const,
label: 'Live music',
desc: 'Organist or solo musician',
price: '$350',
},
{
key: 'memorial' as const,
label: 'Memorial video',
desc: 'Photo slideshow with music',
price: '$200',
},
{
key: 'guestBook' as const,
label: 'Guest book',
desc: 'Leather-bound memorial guest book',
price: '$85',
},
];
const total = items.reduce((sum, item) =>
addOns[item.key] ? sum + parseInt(item.price.replace('$', ''), 10) : sum, 0,
const total = items.reduce(
(sum, item) => (addOns[item.key] ? sum + parseInt(item.price.replace('$', ''), 10) : sum),
0,
);
return (
<Box sx={{ maxWidth: 420 }}>
<Typography variant="h4" sx={{ mb: 2 }}>Service add-ons</Typography>
<Typography variant="h4" sx={{ mb: 2 }}>
Service add-ons
</Typography>
<FormGroup>
{items.map((item) => (
<Card key={item.key} variant="outlined" padding="compact" sx={{ mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
>
<Box sx={{ flex: 1 }}>
<Typography variant="label">{item.label}</Typography>
<Typography variant="body2" color="text.secondary">{item.desc}</Typography>
<Typography variant="body2" color="text.secondary">
{item.desc}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="label" color="text.secondary">{item.price}</Typography>
<Typography variant="label" color="text.secondary">
{item.price}
</Typography>
<Switch checked={addOns[item.key]} onChange={() => toggle(item.key)} />
</Box>
</Box>
</Card>
))}
</FormGroup>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
mt: 2,
pt: 2,
borderTop: 1,
borderColor: 'divider',
}}
>
<Typography variant="labelLg">Total add-ons</Typography>
<Typography variant="labelLg" color="primary">${total}</Typography>
<Typography variant="labelLg" color="primary">
${total}
</Typography>
</Box>
</Box>
);

View File

@@ -16,22 +16,35 @@ const meta: Meta<typeof Typography> = {
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',
'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',
],
options: ['textPrimary', 'textSecondary', 'textDisabled', 'primary', 'secondary', 'error'],
},
maxLines: { control: 'number' },
gutterBottom: { control: 'boolean' },
@@ -47,7 +60,8 @@ const SAMPLE = 'Discover, Explore, and Plan Funerals in Minutes, Not Hours';
export const Default: Story = {
args: {
children: 'Funeral Arranger helps families find transparent, affordable funeral services across Australia.',
children:
'Funeral Arranger helps families find transparent, affordable funeral services across Australia.',
},
};
@@ -59,23 +73,33 @@ export const Display: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<Typography variant="captionSm" color="textSecondary">displayHero 80px</Typography>
<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="captionSm" color="textSecondary">
display1 64px
</Typography>
<Typography variant="display1">{SAMPLE}</Typography>
</div>
<div>
<Typography variant="captionSm" color="textSecondary">display2 52px</Typography>
<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="captionSm" color="textSecondary">
display3 40px
</Typography>
<Typography variant="display3">{SAMPLE}</Typography>
</div>
<div>
<Typography variant="captionSm" color="textSecondary">displaySm 32px</Typography>
<Typography variant="captionSm" color="textSecondary">
displaySm 32px
</Typography>
<Typography variant="displaySm">{SAMPLE}</Typography>
</div>
</div>
@@ -90,27 +114,39 @@ export const Headings: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div>
<Typography variant="captionSm" color="textSecondary">h1 36px</Typography>
<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="captionSm" color="textSecondary">
h2 30px
</Typography>
<Typography variant="h2">{SAMPLE}</Typography>
</div>
<div>
<Typography variant="captionSm" color="textSecondary">h3 24px</Typography>
<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="captionSm" color="textSecondary">
h4 20px
</Typography>
<Typography variant="h4">{SAMPLE}</Typography>
</div>
<div>
<Typography variant="captionSm" color="textSecondary">h5 18px</Typography>
<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="captionSm" color="textSecondary">
h6 16px
</Typography>
<Typography variant="h6">{SAMPLE}</Typography>
</div>
</div>
@@ -125,31 +161,39 @@ export const Body: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 640 }}>
<div>
<Typography variant="overline" gutterBottom>bodyLg 18px</Typography>
<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.
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="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="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.
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="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.
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>
@@ -164,32 +208,60 @@ export const UIText: Story = {
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>
<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>
<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>
<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>
<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>
<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>
<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>
<Typography variant="captionSm" color="textSecondary">
overlineSm 11px semibold uppercase
</Typography>
<Typography variant="overlineSm" display="block">
Compact overline
</Typography>
</div>
</div>
),
@@ -222,20 +294,25 @@ export const FontFamilies: Story = {
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 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.
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 variant="overline" gutterBottom>
Body font Montserrat
</Typography>
<Typography variant="h3" gutterBottom>
Clean, modern, and highly readable
</Typography>
<Typography>
Used for all headings (h1h6), body text, labels, captions, and UI elements.
Headings use Bold (700), body uses Medium (500), captions use Regular (400).
Used for all headings (h1h6), body text, labels, captions, and UI elements. Headings use
Bold (700), body uses Medium (500), captions use Regular (400).
</Typography>
</div>
</div>
@@ -249,18 +326,21 @@ export const MaxLines: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 400 }}>
<div>
<Typography variant="label" gutterBottom>maxLines=1</Typography>
<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.
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 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.
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>
@@ -276,23 +356,27 @@ export const RealisticContent: Story = {
<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.
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 variant="h2" sx={{ mt: 2 }}>
How it works
</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.
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.
Prices are indicative and current as of March 2026. Contact the funeral director for a
binding quote.
</Typography>
</div>
),
@@ -333,7 +417,11 @@ export const CompleteScale: Story = {
<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' }}>
<Typography
variant="captionSm"
color="textSecondary"
sx={{ width: 160, flexShrink: 0, textAlign: 'right' }}
>
{label}
</Typography>
<Typography variant={variant}>{SAMPLE}</Typography>

View File

@@ -150,7 +150,8 @@ export const ServiceAddOns: Story = {
export const WithoutPrice: Story = {
args: {
name: 'Order of service booklet',
description: 'Complimentary printed booklet with the service programme and a photo of your loved one.',
description:
'Complimentary printed booklet with the service programme and a photo of your loved one.',
},
};
@@ -160,13 +161,7 @@ export const WithoutPrice: Story = {
export const WithoutDescription: Story = {
render: function Render() {
const [checked, setChecked] = React.useState(false);
return (
<AddOnOption
name="Include GST in pricing"
checked={checked}
onChange={setChecked}
/>
);
return <AddOnOption name="Include GST in pricing" checked={checked} onChange={setChecked} />;
},
};
@@ -176,7 +171,8 @@ export const WithoutDescription: Story = {
export const Disabled: Story = {
args: {
name: 'Catering',
description: 'Not available at this venue. Please contact the venue directly for catering options.',
description:
'Not available at this venue. Please contact the venue directly for catering options.',
price: 1200,
disabled: true,
},

View File

@@ -54,7 +54,19 @@ export interface AddOnOptionProps {
* ```
*/
export const AddOnOption = React.forwardRef<HTMLDivElement, AddOnOptionProps>(
({ name, description, price, checked = false, onChange, disabled = false, maxDescriptionLines, sx }, ref) => {
(
{
name,
description,
price,
checked = false,
onChange,
disabled = false,
maxDescriptionLines,
sx,
},
ref,
) => {
const switchId = React.useId();
const [expanded, setExpanded] = React.useState(false);
const [isClamped, setIsClamped] = React.useState(false);
@@ -129,10 +141,7 @@ export const AddOnOption = React.forwardRef<HTMLDivElement, AddOnOptionProps>(
{/* Price — tucks directly under heading */}
{price != null && (
<Typography
variant="body2"
color="text.secondary"
>
<Typography variant="body2" color="text.secondary">
${price.toLocaleString('en-AU')}
</Typography>
)}
@@ -146,12 +155,13 @@ export const AddOnOption = React.forwardRef<HTMLDivElement, AddOnOptionProps>(
color="text.secondary"
sx={{
mt: 0.5,
...(maxDescriptionLines && !expanded && {
display: '-webkit-box',
WebkitLineClamp: maxDescriptionLines,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}),
...(maxDescriptionLines &&
!expanded && {
display: '-webkit-box',
WebkitLineClamp: maxDescriptionLines,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}),
}}
>
{description}

View File

@@ -88,15 +88,54 @@ export const PackageContents: Story = {
Essentials
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<LineItem name="Accommodation" price={1500} info="Refrigerated holding of the deceased prior to the funeral service." />
<LineItem name="Death Registration Certificate" price={1500} info="Lodgement of death registration with NSW Registry of Births, Deaths & Marriages." />
<LineItem name="Doctor Fee for Cremation" price={1500} info="Statutory medical referee fee required for all cremations in NSW." />
<LineItem name="NSW Government Levy — Cremation" price={1500} info="NSW Government cremation levy as set by the Department of Health." />
<LineItem name="Professional Mortuary Care" price={1500} info="Preparation and care of the deceased." />
<LineItem name="Professional Service Fee" price={1500} info="Coordination of all funeral arrangements and services." />
<LineItem name="Allowance for Coffin" price={1500} isAllowance info="Allowance amount — upgrade options available during arrangement." />
<LineItem name="Allowance for Crematorium" price={1500} isAllowance info="Allowance for crematorium fees — varies by location." />
<LineItem name="Allowance for Hearse" price={1500} isAllowance info="Allowance for hearse transfer — distance surcharges may apply." />
<LineItem
name="Accommodation"
price={1500}
info="Refrigerated holding of the deceased prior to the funeral service."
/>
<LineItem
name="Death Registration Certificate"
price={1500}
info="Lodgement of death registration with NSW Registry of Births, Deaths & Marriages."
/>
<LineItem
name="Doctor Fee for Cremation"
price={1500}
info="Statutory medical referee fee required for all cremations in NSW."
/>
<LineItem
name="NSW Government Levy — Cremation"
price={1500}
info="NSW Government cremation levy as set by the Department of Health."
/>
<LineItem
name="Professional Mortuary Care"
price={1500}
info="Preparation and care of the deceased."
/>
<LineItem
name="Professional Service Fee"
price={1500}
info="Coordination of all funeral arrangements and services."
/>
<LineItem
name="Allowance for Coffin"
price={1500}
isAllowance
info="Allowance amount — upgrade options available during arrangement."
/>
<LineItem
name="Allowance for Crematorium"
price={1500}
isAllowance
info="Allowance for crematorium fees — varies by location."
/>
<LineItem
name="Allowance for Hearse"
price={1500}
isAllowance
info="Allowance for hearse transfer — distance surcharges may apply."
/>
</Box>
<Divider sx={{ my: 3 }} />
@@ -105,7 +144,10 @@ export const PackageContents: Story = {
Complimentary Items
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<LineItem name="Dressing Fee" info="Dressing and preparation of the deceased — included at no charge." />
<LineItem
name="Dressing Fee"
info="Dressing and preparation of the deceased — included at no charge."
/>
<LineItem name="Viewing Fee" info="One private family viewing — included at no charge." />
</Box>
@@ -117,12 +159,38 @@ export const PackageContents: Story = {
Extras
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<LineItem name="Allowance for Flowers" price={1500} isAllowance info="Seasonal floral arrangements for the service." />
<LineItem name="Allowance for Master of Ceremonies" price={1500} isAllowance info="Professional celebrant or MC for the funeral service." />
<LineItem name="After Business Hours Service Surcharge" price={1500} info="Additional fee for services held outside standard business hours." />
<LineItem name="After Hours Prayers" price={1500} info="Evening prayer service at the funeral home." />
<LineItem name="Coffin Bearing by Funeral Directors" price={1500} info="Professional pallbearing by funeral directors." />
<LineItem name="Digital Recording" price={1500} info="Professional video recording of the funeral service." />
<LineItem
name="Allowance for Flowers"
price={1500}
isAllowance
info="Seasonal floral arrangements for the service."
/>
<LineItem
name="Allowance for Master of Ceremonies"
price={1500}
isAllowance
info="Professional celebrant or MC for the funeral service."
/>
<LineItem
name="After Business Hours Service Surcharge"
price={1500}
info="Additional fee for services held outside standard business hours."
/>
<LineItem
name="After Hours Prayers"
price={1500}
info="Evening prayer service at the funeral home."
/>
<LineItem
name="Coffin Bearing by Funeral Directors"
price={1500}
info="Professional pallbearing by funeral directors."
/>
<LineItem
name="Digital Recording"
price={1500}
info="Professional video recording of the funeral service."
/>
</Box>
</Box>
),

View File

@@ -48,8 +48,9 @@ export const LineItem = React.forwardRef<HTMLDivElement, LineItemProps>(
({ name, info, price, isAllowance = false, priceLabel, variant = 'default', sx }, ref) => {
const isTotal = variant === 'total';
const formattedPrice = priceLabel
?? (price != null ? `$${price.toLocaleString('en-AU')}${isAllowance ? '*' : ''}` : undefined);
const formattedPrice =
priceLabel ??
(price != null ? `$${price.toLocaleString('en-AU')}${isAllowance ? '*' : ''}` : undefined);
return (
<Box

View File

@@ -68,7 +68,8 @@ export const Default: Story = {
reviewCount: 127,
capabilityLabel: 'Online Arrangement',
capabilityColor: 'success',
capabilityDescription: 'Complete your arrangement entirely online — no in-person visit required.',
capabilityDescription:
'Complete your arrangement entirely online — no in-person visit required.',
startingPrice: 900,
},
};
@@ -311,11 +312,7 @@ export const EdgeCases: Story = {
onClick={() => {}}
/>
{/* Minimal — just name and location */}
<ProviderCard
name="Minimal Card"
location="Hobart"
onClick={() => {}}
/>
<ProviderCard name="Minimal Card" location="Hobart" onClick={() => {}} />
</Box>
),
};

View File

@@ -144,12 +144,7 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
>
{/* Verified badge */}
<Box sx={{ position: 'absolute', top: 12, right: 12 }}>
<Badge
variant="filled"
color="brand"
size="medium"
icon={<VerifiedOutlinedIcon />}
>
<Badge variant="filled" color="brand" size="medium" icon={<VerifiedOutlinedIcon />}>
Verified
</Badge>
</Box>
@@ -219,9 +214,7 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
>
{/* Location */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<LocationOnOutlinedIcon
sx={{ fontSize: 14, color: 'text.secondary' }}
/>
<LocationOnOutlinedIcon sx={{ fontSize: 14, color: 'text.secondary' }} />
<Typography variant="caption" color="text.secondary">
{location}
</Typography>
@@ -233,10 +226,7 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
aria-label={`Rated ${rating} out of 5${reviewCount != null ? `, ${reviewCount} reviews` : ''}`}
>
<StarRoundedIcon
sx={{ fontSize: 14, color: 'warning.main' }}
aria-hidden
/>
<StarRoundedIcon sx={{ fontSize: 14, color: 'warning.main' }} aria-hidden />
<Typography variant="caption" color="text.secondary">
{rating}
{reviewCount != null && ` (${reviewCount.toLocaleString('en-AU')})`}
@@ -249,17 +239,8 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
{capabilityLabel && (
<Box>
{capabilityDescription ? (
<Tooltip
title={capabilityDescription}
arrow
placement="top"
enterTouchDelay={0}
>
<Badge
color={capabilityColor}
size="medium"
sx={{ cursor: 'help' }}
>
<Tooltip title={capabilityDescription} arrow placement="top" enterTouchDelay={0}>
<Badge color={capabilityColor} size="medium" sx={{ cursor: 'help' }}>
{capabilityLabel}
<InfoOutlinedIcon />
</Badge>

View File

@@ -2,7 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react';
import Box from '@mui/material/Box';
import { ProviderCardCompact } from './ProviderCardCompact';
const DEMO_IMAGE = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
const DEMO_IMAGE =
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
const meta: Meta<typeof ProviderCardCompact> = {
title: 'Molecules/ProviderCardCompact',

View File

@@ -101,10 +101,7 @@ export const ProviderCardCompact = React.forwardRef<HTMLDivElement, ProviderCard
{/* Location */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<LocationOnOutlinedIcon
sx={{ fontSize: 16, color: 'text.secondary' }}
aria-hidden
/>
<LocationOnOutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} aria-hidden />
<Typography variant="body2" color="text.secondary">
{location}
</Typography>
@@ -118,7 +115,10 @@ export const ProviderCardCompact = React.forwardRef<HTMLDivElement, ProviderCard
aria-hidden
/>
<Typography variant="caption" color="text.secondary">
{rating} Rating{reviewCount != null ? ` (${reviewCount} ${reviewCount === 1 ? 'Review' : 'Reviews'})` : ''}
{rating} Rating
{reviewCount != null
? ` (${reviewCount} ${reviewCount === 1 ? 'Review' : 'Reviews'})`
: ''}
</Typography>
</Box>
)}

View File

@@ -149,13 +149,7 @@ export const Loading: Story = {
const [value, setValue] = React.useState('Parsons funeral');
return (
<SearchBar
value={value}
onChange={setValue}
placeholder="Search..."
showButton
loading
/>
<SearchBar value={value} onChange={setValue} placeholder="Search..." showButton loading />
);
},
};
@@ -200,11 +194,7 @@ export const ProviderSearch: Story = {
setResults([]);
return;
}
setResults(
providers.filter((p) =>
p.toLowerCase().includes(query.toLowerCase()),
),
);
setResults(providers.filter((p) => p.toLowerCase().includes(query.toLowerCase())));
};
return (

View File

@@ -252,17 +252,9 @@ export const EdgeCases: Story = {
onClick={() => {}}
/>
{/* No description */}
<ServiceOption
name="Flowers"
price={250}
selected
onClick={() => {}}
/>
<ServiceOption name="Flowers" price={250} selected onClick={() => {}} />
{/* No price, no description */}
<ServiceOption
name="Contact us for pricing"
onClick={() => {}}
/>
<ServiceOption name="Contact us for pricing" onClick={() => {}} />
</Box>
),
};

View File

@@ -55,7 +55,19 @@ export interface ServiceOptionProps {
* ```
*/
export const ServiceOption = React.forwardRef<HTMLDivElement, ServiceOptionProps>(
({ name, description, price, selected = false, disabled = false, onClick, maxDescriptionLines, sx }, ref) => {
(
{
name,
description,
price,
selected = false,
disabled = false,
onClick,
maxDescriptionLines,
sx,
},
ref,
) => {
const [expanded, setExpanded] = React.useState(false);
const [isClamped, setIsClamped] = React.useState(false);
const descRef = React.useRef<HTMLElement>(null);
@@ -123,12 +135,13 @@ export const ServiceOption = React.forwardRef<HTMLDivElement, ServiceOptionProps
color="text.secondary"
sx={{
mt: 0.5,
...(maxDescriptionLines && !expanded && {
display: '-webkit-box',
WebkitLineClamp: maxDescriptionLines,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}),
...(maxDescriptionLines &&
!expanded && {
display: '-webkit-box',
WebkitLineClamp: maxDescriptionLines,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}),
}}
>
{description}

View File

@@ -106,7 +106,15 @@ export const Interactive: Story = {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<StepIndicator steps={arrangementSteps} currentStep={step} />
<Box sx={{ p: 3, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
<Box
sx={{
p: 3,
bgcolor: 'background.paper',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="h5" sx={{ mb: 1 }}>
{arrangementSteps[step].label}
</Typography>

View File

@@ -161,12 +161,7 @@ export const EdgeCases: Story = {
onClick={() => {}}
/>
{/* Minimal — just name, image, location */}
<VenueCard
name="Minimal Venue"
imageUrl={VENUE_BEACH}
location="Kiama"
onClick={() => {}}
/>
<VenueCard name="Minimal Venue" imageUrl={VENUE_BEACH} location="Kiama" onClick={() => {}} />
</Box>
),
};
@@ -186,9 +181,7 @@ export const Responsive: Story = {
<Box sx={{ display: 'flex', gap: 3, alignItems: 'start', flexWrap: 'wrap' }}>
{[280, 340, 420].map((width) => (
<Box key={width} sx={{ width }}>
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>
{width}px
</Box>
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>{width}px</Box>
<VenueCard
name="West Chapel"
imageUrl={VENUE_CHAPEL}
@@ -218,9 +211,7 @@ export const OnDifferentBackgrounds: Story = {
render: () => (
<Box sx={{ display: 'flex', gap: 3 }}>
<Box sx={{ width: 360, p: 3, backgroundColor: 'background.default' }}>
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>
White surface
</Box>
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>White surface</Box>
<VenueCard
name="West Chapel"
imageUrl={VENUE_CHAPEL}
@@ -237,9 +228,7 @@ export const OnDifferentBackgrounds: Story = {
backgroundColor: 'var(--fa-color-surface-subtle)',
}}
>
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>
Grey surface (neutral.50)
</Box>
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>Grey surface (neutral.50)</Box>
<VenueCard
name="Macquarie Park Gardens"
imageUrl={VENUE_GARDEN}

View File

@@ -112,9 +112,7 @@ export const VenueCard = React.forwardRef<HTMLDivElement, VenueCardProps>(
>
{/* Location */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<LocationOnOutlinedIcon
sx={{ fontSize: 14, color: 'text.secondary' }}
/>
<LocationOnOutlinedIcon sx={{ fontSize: 14, color: 'text.secondary' }} />
<Typography variant="caption" color="text.secondary">
{location}
</Typography>
@@ -126,10 +124,7 @@ export const VenueCard = React.forwardRef<HTMLDivElement, VenueCardProps>(
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
aria-label={`Capacity: ${capacity} guests`}
>
<PeopleOutlinedIcon
sx={{ fontSize: 14, color: 'text.secondary' }}
aria-hidden
/>
<PeopleOutlinedIcon sx={{ fontSize: 14, color: 'text.secondary' }} aria-hidden />
<Typography variant="caption" color="text.secondary">
{capacity} guests
</Typography>
@@ -143,12 +138,7 @@ export const VenueCard = React.forwardRef<HTMLDivElement, VenueCardProps>(
<Typography variant="body2" color="text.secondary">
From
</Typography>
<Typography
variant="h6"
component="span"
color="primary"
sx={{ fontWeight: 600 }}
>
<Typography variant="h6" component="span" color="primary" sx={{ fontWeight: 600 }}>
${price.toLocaleString('en-AU')}
</Typography>
</Box>

View File

@@ -15,12 +15,7 @@ const FALogoInverse = () => (
);
const FALogoNav = () => (
<Box
component="img"
src="/brandlogo/logo-full.svg"
alt="Funeral Arranger"
sx={{ height: 28 }}
/>
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 28 }} />
);
const defaultLinkGroups = [
@@ -169,8 +164,8 @@ export const FullPage: Story = {
Find a funeral director
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4, maxWidth: 600 }}>
Compare trusted funeral directors in your area. View services,
pricing, and reviews to find the right support for your family.
Compare trusted funeral directors in your area. View services, pricing, and reviews to
find the right support for your family.
</Typography>
{Array.from({ length: 4 }).map((_, i) => (
<Box

View File

@@ -64,19 +64,7 @@ export interface FooterProps {
* ```
*/
export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
(
{
logo,
tagline,
linkGroups = [],
phone,
email,
copyright,
legalLinks = [],
sx,
},
ref,
) => {
({ logo, tagline, linkGroups = [], phone, email, copyright, legalLinks = [], sx }, ref) => {
const year = new Date().getFullYear();
const copyrightText = copyright || `\u00A9 ${year} Funeral Arranger. All rights reserved.`;
@@ -143,10 +131,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
<Typography variant="overlineSm" sx={overlineSx}>
Email
</Typography>
<Link
href={`mailto:${email}`}
sx={contactLinkSx}
>
<Link href={`mailto:${email}`} sx={contactLinkSx}>
{email}
</Link>
</Box>
@@ -157,7 +142,15 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
{/* Link group columns */}
{linkGroups.map((group) => (
<Grid item xs={6} sm={4} md key={group.heading} component="nav" aria-label={group.heading}>
<Grid
item
xs={6}
sm={4}
md
key={group.heading}
component="nav"
aria-label={group.heading}
>
<Typography
variant="label"
sx={{
@@ -170,7 +163,14 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
</Typography>
<Box
component="ul"
sx={{ listStyle: 'none', p: 0, m: 0, display: 'flex', flexDirection: 'column', gap: 1.5 }}
sx={{
listStyle: 'none',
p: 0,
m: 0,
display: 'flex',
flexDirection: 'column',
gap: 1.5,
}}
>
{group.links.map((link) => (
<li key={link.label}>
@@ -206,10 +206,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
py: 3,
}}
>
<Typography
variant="captionSm"
sx={{ color: 'var(--fa-color-brand-400)' }}
>
<Typography variant="captionSm" sx={{ color: 'var(--fa-color-brand-400)' }}>
{copyrightText}
</Typography>

View File

@@ -9,7 +9,12 @@ import { Typography } from '../../atoms/Typography';
const funeralTypes = [
{ id: 'cremation', label: 'Cremation', hasServiceOption: true },
{ id: 'burial', label: 'Burial', hasServiceOption: true },
{ id: 'water-burial', label: 'Water Burial', note: 'Available in QLD only', hasServiceOption: false },
{
id: 'water-burial',
label: 'Water Burial',
note: 'Available in QLD only',
hasServiceOption: false,
},
];
const themeOptions = [
@@ -152,8 +157,8 @@ export const InHeroDesktop: Story = {
color="text.secondary"
sx={{ textAlign: 'center', mb: 4, maxWidth: 440, mx: 'auto' }}
>
Whether you're thinking ahead or arranging for a loved one, find
trusted local providers with transparent pricing.
Whether you're thinking ahead or arranging for a loved one, find trusted local
providers with transparent pricing.
</Typography>
<FuneralFinder
funeralTypes={funeralTypes}
@@ -199,7 +204,11 @@ export const InHeroMobile: Story = {
]}
/>
<Box sx={{ bgcolor: 'var(--fa-color-brand-100)', px: 3, py: 4, textAlign: 'center' }}>
<Typography variant="h3" component="h1" sx={{ mb: 1.5, color: 'var(--fa-color-brand-950)' }}>
<Typography
variant="h3"
component="h1"
sx={{ mb: 1.5, color: 'var(--fa-color-brand-950)' }}
>
Discover, Explore, and Plan Funerals in Minutes
</Typography>
<Typography variant="body2" color="text.secondary">
@@ -210,7 +219,8 @@ export const InHeroMobile: Story = {
sx={{
height: 180,
bgcolor: 'var(--fa-color-brand-200)',
backgroundImage: 'url(https://images.unsplash.com/photo-1516733968668-dbdce39c0571?w=800&h=400&fit=crop)',
backgroundImage:
'url(https://images.unsplash.com/photo-1516733968668-dbdce39c0571?w=800&h=400&fit=crop)',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}

View File

@@ -146,7 +146,11 @@ function ChoiceCard({
</Typography>
</Box>
{description && (
<Typography variant="caption" component="span" sx={{ display: 'block', mt: 0.5, color: 'text.secondary' }}>
<Typography
variant="caption"
component="span"
sx={{ display: 'block', mt: 0.5, color: 'text.secondary' }}
>
{description}
</Typography>
)}
@@ -214,12 +218,20 @@ function TypeCard({
</Typography>
</Box>
{description && (
<Typography variant="caption" component="span" sx={{ display: 'block', mt: 0.25, color: 'text.secondary' }}>
<Typography
variant="caption"
component="span"
sx={{ display: 'block', mt: 0.25, color: 'text.secondary' }}
>
{description}
</Typography>
)}
{note && (
<Typography variant="captionSm" component="span" sx={{ display: 'block', mt: 0.5, color: 'text.secondary', fontWeight: 500 }}>
<Typography
variant="captionSm"
component="span"
sx={{ display: 'block', mt: 0.5, color: 'text.secondary', fontWeight: 500 }}
>
{note}
</Typography>
)}
@@ -261,7 +273,13 @@ function CompletedRow({
onClick={onChangeClick}
underline="hover"
aria-label={`Change ${question.toLowerCase()}`}
sx={{ color: 'text.secondary', ml: 'auto', minHeight: 44, display: 'inline-flex', alignItems: 'center' }}
sx={{
color: 'text.secondary',
ml: 'auto',
minHeight: 44,
display: 'inline-flex',
alignItems: 'center',
}}
>
Change
</Link>
@@ -348,9 +366,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
const typeSummary = [typeLabel, themeSuffix].filter(Boolean).join(', ');
const serviceLabel =
servicePref === 'with-service' ? 'With a service'
: servicePref === 'without-service' ? 'No service'
: 'Flexible';
servicePref === 'with-service'
? 'With a service'
: servicePref === 'without-service'
? 'No service'
: 'Flexible';
// ─── Handlers ───────────────────────────────────────────────────
const selectIntent = (value: Intent) => {
@@ -409,7 +429,7 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
intent,
planningFor: needsPlanningFor ? (planningFor ?? undefined) : undefined,
funeralTypeId: isExploreAll ? null : (typeSelection ?? null),
servicePreference: (showServiceStep && serviceAnswered) ? servicePref : 'either',
servicePreference: showServiceStep && serviceAnswered ? servicePref : 'either',
themes: selectedThemes,
location: location.trim(),
});
@@ -448,16 +468,32 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
{/* ── Completed rows ─────────────────────────────────────── */}
<Collapse in={intent !== null && activeStep !== 1} timeout={250}>
<CompletedRow question="I'm here to" answer={intentLabel} onChangeClick={() => revertTo(1)} />
<CompletedRow
question="I'm here to"
answer={intentLabel}
onChangeClick={() => revertTo(1)}
/>
</Collapse>
<Collapse in={needsPlanningFor && planningFor !== null && activeStep !== 2} timeout={250}>
<CompletedRow question="Planning for" answer={planningForLabel} onChangeClick={() => revertTo(2)} />
<CompletedRow
question="Planning for"
answer={planningForLabel}
onChangeClick={() => revertTo(2)}
/>
</Collapse>
<Collapse in={typeSelected && activeStep !== 3} timeout={250}>
<CompletedRow question="Looking for" answer={typeSummary} onChangeClick={() => revertTo(3)} />
<CompletedRow
question="Looking for"
answer={typeSummary}
onChangeClick={() => revertTo(3)}
/>
</Collapse>
<Collapse in={showServiceStep && serviceAnswered && activeStep !== 4} timeout={250}>
<CompletedRow question="Service" answer={serviceLabel} onChangeClick={() => revertTo(4)} />
<CompletedRow
question="Service"
answer={serviceLabel}
onChangeClick={() => revertTo(4)}
/>
</Collapse>
{/* ── Step 1: Intent ─────────────────────────────────────── */}
@@ -467,13 +503,22 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Typography
variant="caption"
role="alert"
sx={{ color: 'var(--fa-color-brand-600)', textAlign: 'center', display: 'block', mb: 1.5 }}
sx={{
color: 'var(--fa-color-brand-600)',
textAlign: 'center',
display: 'block',
mb: 1.5,
}}
>
Please let us know how we can help
</Typography>
)}
<StepHeading>How can we help you today?</StepHeading>
<Box role="radiogroup" aria-label="How can we help" sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box
role="radiogroup"
aria-label="How can we help"
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
>
<ChoiceCard
label="Arrange a funeral now"
description="Someone has passed and I need to make arrangements"
@@ -494,7 +539,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Collapse in={activeStep === 2 && needsPlanningFor} timeout={250}>
<Box sx={{ mt: 3 }}>
<StepHeading>Who are you planning for?</StepHeading>
<Box role="radiogroup" aria-label="Who are you planning for" sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box
role="radiogroup"
aria-label="Who are you planning for"
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
>
<ChoiceCard
label="Myself"
description="I want to plan my own funeral in advance"
@@ -515,7 +564,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Collapse in={activeStep === 3} timeout={250}>
<Box sx={{ mt: 3 }}>
<StepHeading>What type of funeral are you considering?</StepHeading>
<Box role="radiogroup" aria-label="Type of funeral" sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box
role="radiogroup"
aria-label="Type of funeral"
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
>
{funeralTypes.map((ft) => (
<TypeCard
key={ft.id}
@@ -543,11 +596,20 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Typography variant="body2" component="span" sx={{ fontWeight: 600 }}>
Any preferences?
</Typography>
<Typography variant="caption" component="span" color="text.secondary" sx={{ ml: 0.75 }}>
<Typography
variant="caption"
component="span"
color="text.secondary"
sx={{ ml: 0.75 }}
>
(optional)
</Typography>
</Box>
<Box role="group" aria-label="Preferences" sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Box
role="group"
aria-label="Preferences"
sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}
>
{themeOptions.map((theme) => {
const isSelected = selectedThemes.includes(theme.id);
return (
@@ -573,7 +635,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Collapse in={activeStep === 4 && showServiceStep} timeout={250}>
<Box sx={{ mt: 3 }}>
<StepHeading>Would you like a service?</StepHeading>
<Box role="group" aria-label="Service preference" sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box
role="group"
aria-label="Service preference"
sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}
>
{SERVICE_OPTIONS.map((opt) => (
<Chip
key={opt.value}
@@ -583,7 +649,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
onClick={() => selectService(opt.value)}
clickable
aria-pressed={serviceAnswered && servicePref === opt.value}
sx={{ justifyContent: 'flex-start', height: 44, borderRadius: 'var(--fa-border-radius-md)' }}
sx={{
justifyContent: 'flex-start',
height: 44,
borderRadius: 'var(--fa-border-radius-md)',
}}
/>
))}
</Box>

View File

@@ -41,12 +41,8 @@ export const BelowMasthead: Story = {
textAlign: 'center',
}}
>
<Box sx={{ fontSize: '2rem', fontWeight: 700, mb: 1 }}>
Funeral Arranger
</Box>
<Box sx={{ opacity: 0.8 }}>
Find trusted funeral directors near you
</Box>
<Box sx={{ fontSize: '2rem', fontWeight: 700, mb: 1 }}>Funeral Arranger</Box>
<Box sx={{ opacity: 0.8 }}>Find trusted funeral directors near you</Box>
</Box>
{/* Widget below masthead */}
<Box sx={{ maxWidth: 560, mx: 'auto', mt: -4, px: 2, position: 'relative', zIndex: 1 }}>

View File

@@ -103,7 +103,10 @@ function StepCircle({
transition: 'background-color 200ms ease, color 200ms ease',
...(usePrimary
? { bgcolor: 'var(--fa-color-brand-500)', color: 'common.white' }
: { bgcolor: 'var(--fa-color-brand-200, #EBDAC8)', color: 'var(--fa-color-brand-700, #8B4E0D)' }),
: {
bgcolor: 'var(--fa-color-brand-200, #EBDAC8)',
color: 'var(--fa-color-brand-700, #8B4E0D)',
}),
// Connector line from bottom of this circle toward the next
...(showConnector && {
'&::after': {
@@ -245,7 +248,8 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
}
: { lookingTo: false, planningFor: false, funeralType: false, location: false };
const hasErrors = submitted && (errs.lookingTo || errs.planningFor || errs.funeralType || errs.location);
const hasErrors =
submitted && (errs.lookingTo || errs.planningFor || errs.funeralType || errs.location);
// ─── Handlers ────────────────────────────────────────────────
const handleLookingTo = (e: SelectChangeEvent<string>) => {
@@ -311,11 +315,7 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
>
{heading}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ textAlign: 'center', mb: 0 }}
>
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mb: 0 }}>
{subheading}
</Typography>
<Divider sx={{ my: 3.5 }} />
@@ -326,7 +326,10 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={1} completed={!!lookingTo} active showConnector />
<Box sx={{ flex: 1 }}>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: 'var(--fa-color-brand-700)' }}>
<Typography
variant="body1"
sx={{ fontWeight: 600, mb: 1, color: 'var(--fa-color-brand-700)' }}
>
I&rsquo;m looking to&hellip;
</Typography>
<Select
@@ -352,7 +355,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={2} completed={!!planningFor} showConnector />
<Box sx={{ flex: 1 }}>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: lookingTo ? 'var(--fa-color-brand-700)' : 'text.disabled' }}>
<Typography
variant="body1"
sx={{
fontWeight: 600,
mb: 1,
color: lookingTo ? 'var(--fa-color-brand-700)' : 'text.disabled',
}}
>
I&rsquo;m planning for
</Typography>
<Select
@@ -383,7 +393,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={3} completed={!!funeralType} showConnector />
<Box sx={{ flex: 1 }}>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: step3Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)' }}>
<Typography
variant="body1"
sx={{
fontWeight: 600,
mb: 1,
color: step3Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)',
}}
>
Type of funeral
</Typography>
<Select
@@ -410,7 +427,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={4} completed={location.trim().length >= 3} />
<Box sx={{ flex: 1 }}>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: step4Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)' }}>
<Typography
variant="body1"
sx={{
fontWeight: 600,
mb: 1,
color: step4Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)',
}}
>
Looking for providers in
</Typography>
<Input

View File

@@ -33,8 +33,7 @@ export const BelowMasthead: Story = {
<Box>
<Box
sx={{
background:
'linear-gradient(160deg, #2C2E35 0%, #4C5B6B 60%, #6B3C13 100%)',
background: 'linear-gradient(160deg, #2C2E35 0%, #4C5B6B 60%, #6B3C13 100%)',
color: '#fff',
py: 8,
px: 4,

View File

@@ -75,28 +75,15 @@ const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [
/** Hoisted outside component to avoid re-creation on render */
const selectPlaceholder = (
<span style={{ color: 'var(--fa-color-text-disabled)' }}>
Select funeral type
</span>
<span style={{ color: 'var(--fa-color-text-disabled)' }}>Select funeral type</span>
);
// ─── Sub-components ──────────────────────────────────────────────────────────
/** Uppercase section label — overline style */
function SectionLabel({
children,
id,
}: {
children: React.ReactNode;
id?: string;
}) {
function SectionLabel({ children, id }: { children: React.ReactNode; id?: string }) {
return (
<Typography
variant="overline"
component="div"
id={id}
sx={{ color: 'text.secondary' }}
>
<Typography variant="overline" component="div" id={id} sx={{ color: 'text.secondary' }}>
{children}
</Typography>
);
@@ -138,8 +125,7 @@ const StatusCard = React.forwardRef<
cursor: 'pointer',
fontFamily: 'inherit',
textAlign: 'center',
transition:
'border-color 200ms ease, background-color 200ms ease, transform 100ms ease',
transition: 'border-color 200ms ease, background-color 200ms ease, transform 100ms ease',
'&:hover': {
borderColor: selected
? 'var(--fa-color-border-brand, #BA834E)'
@@ -164,9 +150,7 @@ const StatusCard = React.forwardRef<
fontWeight: 600,
display: 'block',
mb: 0.75,
color: selected
? 'var(--fa-color-text-brand, #B0610F)'
: 'text.primary',
color: selected ? 'var(--fa-color-text-brand, #B0610F)' : 'text.primary',
}}
>
{title}
@@ -253,317 +237,309 @@ const selectMenuProps = {
* Required fields: status + location (min 3 chars).
* Funeral type defaults to "show all" if not selected.
*/
export const FuneralFinderV3 = React.forwardRef<
HTMLDivElement,
FuneralFinderV3Props
>((props, ref) => {
const {
onSearch,
loading = false,
heading = 'Find funeral directors near you',
subheading =
"Tell us what you need and we\u2019ll show options in your area.",
sx,
} = props;
export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3Props>(
(props, ref) => {
const {
onSearch,
loading = false,
heading = 'Find funeral directors near you',
subheading = 'Tell us what you need and we\u2019ll show options in your area.',
sx,
} = props;
// ─── IDs for aria-labelledby ──────────────────────────────
const id = React.useId();
const statusLabelId = `${id}-status`;
const funeralTypeLabelId = `${id}-funeral-type`;
const locationLabelId = `${id}-location`;
// ─── IDs for aria-labelledby ──────────────────────────────
const id = React.useId();
const statusLabelId = `${id}-status`;
const funeralTypeLabelId = `${id}-funeral-type`;
const locationLabelId = `${id}-location`;
// ─── State ───────────────────────────────────────────────
const [status, setStatus] = React.useState<Status | ''>('immediate');
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
const [location, setLocation] = React.useState('');
const [errors, setErrors] = React.useState<{
status?: boolean;
location?: boolean;
}>({});
// ─── State ───────────────────────────────────────────────
const [status, setStatus] = React.useState<Status | ''>('immediate');
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
const [location, setLocation] = React.useState('');
const [errors, setErrors] = React.useState<{
status?: boolean;
location?: boolean;
}>({});
// ─── Refs ────────────────────────────────────────────────
const statusSectionRef = React.useRef<HTMLDivElement>(null);
const locationSectionRef = React.useRef<HTMLDivElement>(null);
const locationInputRef = React.useRef<HTMLInputElement>(null);
const cardRefs = React.useRef<(HTMLButtonElement | null)[]>([null, null]);
// ─── Refs ────────────────────────────────────────────────
const statusSectionRef = React.useRef<HTMLDivElement>(null);
const locationSectionRef = React.useRef<HTMLDivElement>(null);
const locationInputRef = React.useRef<HTMLInputElement>(null);
const cardRefs = React.useRef<(HTMLButtonElement | null)[]>([null, null]);
// ─── Clear errors as fields are filled ───────────────────
const prevStatus = React.useRef(status);
React.useEffect(() => {
if (status !== prevStatus.current) {
prevStatus.current = status;
if (status && errors.status) {
setErrors((prev) => ({ ...prev, status: false }));
// ─── Clear errors as fields are filled ───────────────────
const prevStatus = React.useRef(status);
React.useEffect(() => {
if (status !== prevStatus.current) {
prevStatus.current = status;
if (status && errors.status) {
setErrors((prev) => ({ ...prev, status: false }));
}
}
}
}, [status, errors.status]);
}, [status, errors.status]);
const prevLocation = React.useRef(location);
React.useEffect(() => {
if (location !== prevLocation.current) {
prevLocation.current = location;
if (location.trim().length >= 3 && errors.location) {
setErrors((prev) => ({ ...prev, location: false }));
const prevLocation = React.useRef(location);
React.useEffect(() => {
if (location !== prevLocation.current) {
prevLocation.current = location;
if (location.trim().length >= 3 && errors.location) {
setErrors((prev) => ({ ...prev, location: false }));
}
}
}
}, [location, errors.location]);
}, [location, errors.location]);
// ─── Radiogroup keyboard nav (WAI-ARIA pattern) ──────────
const activeStatusIndex = status
? STATUS_OPTIONS.findIndex((o) => o.key === status)
: 0;
// ─── Radiogroup keyboard nav (WAI-ARIA pattern) ──────────
const activeStatusIndex = status ? STATUS_OPTIONS.findIndex((o) => o.key === status) : 0;
const handleStatusKeyDown = (e: React.KeyboardEvent) => {
const isNext = e.key === 'ArrowRight' || e.key === 'ArrowDown';
const isPrev = e.key === 'ArrowLeft' || e.key === 'ArrowUp';
if (!isNext && !isPrev) return;
e.preventDefault();
const current = cardRefs.current.indexOf(
e.target as HTMLButtonElement,
);
if (current === -1) return;
const next = isNext
? Math.min(current + 1, STATUS_OPTIONS.length - 1)
: Math.max(current - 1, 0);
if (next !== current) {
cardRefs.current[next]?.focus();
setStatus(STATUS_OPTIONS[next].key);
}
};
const handleStatusKeyDown = (e: React.KeyboardEvent) => {
const isNext = e.key === 'ArrowRight' || e.key === 'ArrowDown';
const isPrev = e.key === 'ArrowLeft' || e.key === 'ArrowUp';
if (!isNext && !isPrev) return;
e.preventDefault();
const current = cardRefs.current.indexOf(e.target as HTMLButtonElement);
if (current === -1) return;
const next = isNext
? Math.min(current + 1, STATUS_OPTIONS.length - 1)
: Math.max(current - 1, 0);
if (next !== current) {
cardRefs.current[next]?.focus();
setStatus(STATUS_OPTIONS[next].key);
}
};
// ─── Handlers ────────────────────────────────────────────
const handleFuneralType = (e: SelectChangeEvent<string>) => {
setFuneralType(e.target.value as FuneralType);
};
// ─── Handlers ────────────────────────────────────────────
const handleFuneralType = (e: SelectChangeEvent<string>) => {
setFuneralType(e.target.value as FuneralType);
};
const handleSubmit = () => {
if (!status) {
setErrors({ status: true });
statusSectionRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
const handleSubmit = () => {
if (!status) {
setErrors({ status: true });
statusSectionRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
return;
}
if (location.trim().length < 3) {
setErrors({ location: true });
locationSectionRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
locationInputRef.current?.focus();
return;
}
setErrors({});
onSearch?.({
status,
funeralType: funeralType || 'show-all',
location: location.trim(),
});
return;
}
if (location.trim().length < 3) {
setErrors({ location: true });
locationSectionRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
locationInputRef.current?.focus();
return;
}
setErrors({});
onSearch?.({
status,
funeralType: funeralType || 'show-all',
location: location.trim(),
});
};
};
// ─── Render ──────────────────────────────────────────────
return (
<Box
ref={ref}
role="search"
aria-label="Find funeral directors"
sx={[
{
bgcolor: 'var(--fa-color-surface-raised, #fff)',
borderRadius: 'var(--fa-card-border-radius-default, 8px)',
boxShadow: 'var(--fa-card-shadow-default)',
px: { xs: 3.5, sm: 5 },
py: { xs: 4, sm: 5 },
display: 'flex',
flexDirection: 'column',
gap: 4,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{/* ── Header ──────────────────────────────────────────── */}
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h3"
component="h2"
sx={{
fontFamily: 'var(--fa-font-family-display)',
fontWeight: 400,
mb: 1,
}}
>
{heading}
</Typography>
<Typography variant="body2" color="text.secondary">
{subheading}
</Typography>
</Box>
<Divider />
{/* ── How can we help ─────────────────────────────────── */}
<Box ref={statusSectionRef}>
<SectionLabel id={statusLabelId}>How Can We Help</SectionLabel>
<Box
role="radiogroup"
aria-labelledby={statusLabelId}
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
gap: 2,
mt: 2,
}}
>
{STATUS_OPTIONS.map((opt, i) => (
<StatusCard
key={opt.key}
ref={(el) => {
cardRefs.current[i] = el;
}}
title={opt.title}
description={opt.description}
selected={status === opt.key}
onClick={() => setStatus(opt.key)}
tabIndex={i === activeStatusIndex ? 0 : -1}
onKeyDown={handleStatusKeyDown}
/>
))}
</Box>
<Box aria-live="polite" sx={{ textAlign: 'center' }}>
{errors.status && (
<Typography
variant="caption"
role="alert"
sx={{
color: 'var(--fa-color-text-brand, #B0610F)',
display: 'block',
mt: 1,
}}
>
Please select how we can help
</Typography>
)}
</Box>
</Box>
{/* ── Funeral Type ────────────────────────────────────── */}
<Box>
<SectionLabel id={funeralTypeLabelId}>Funeral Type</SectionLabel>
<Box sx={{ mt: 2 }}>
<Select
value={funeralType}
onChange={handleFuneralType}
displayEmpty
renderValue={(v) =>
v
? FUNERAL_TYPE_OPTIONS.find((o) => o.value === v)?.label
: selectPlaceholder
}
MenuProps={selectMenuProps}
// ─── Render ──────────────────────────────────────────────
return (
<Box
ref={ref}
role="search"
aria-label="Find funeral directors"
sx={[
{
bgcolor: 'var(--fa-color-surface-raised, #fff)',
borderRadius: 'var(--fa-card-border-radius-default, 8px)',
boxShadow: 'var(--fa-card-shadow-default)',
px: { xs: 3.5, sm: 5 },
py: { xs: 4, sm: 5 },
display: 'flex',
flexDirection: 'column',
gap: 4,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{/* ── Header ──────────────────────────────────────────── */}
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h3"
component="h2"
sx={{
...fieldBaseSx,
'& .MuiSelect-select': {
...fieldInputStyles,
minHeight: 'unset !important',
},
'& .MuiSelect-icon': {
color: 'var(--fa-color-text-disabled)',
right: 12,
},
fontFamily: 'var(--fa-font-family-display)',
fontWeight: 400,
mb: 1,
}}
inputProps={{ 'aria-labelledby': funeralTypeLabelId }}
>
{FUNERAL_TYPE_OPTIONS.map((o) => (
<MenuItem key={o.value} value={o.value}>
{o.label}
</MenuItem>
))}
</Select>
{heading}
</Typography>
<Typography variant="body2" color="text.secondary">
{subheading}
</Typography>
</Box>
</Box>
{/* ── Location ────────────────────────────────────────── */}
<Box ref={locationSectionRef}>
<SectionLabel id={locationLabelId}>Location</SectionLabel>
<Box sx={{ mt: 2 }}>
<OutlinedInput
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="Enter suburb or postcode"
inputRef={locationInputRef}
startAdornment={
<InputAdornment position="start" sx={{ ml: 0.5 }}>
<LocationOnOutlinedIcon
sx={{
fontSize: 20,
color: 'var(--fa-color-text-disabled)',
}}
/>
</InputAdornment>
}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSubmit();
}}
<Divider />
{/* ── How can we help ─────────────────────────────────── */}
<Box ref={statusSectionRef}>
<SectionLabel id={statusLabelId}>How Can We Help</SectionLabel>
<Box
role="radiogroup"
aria-labelledby={statusLabelId}
sx={{
...fieldBaseSx,
'& .MuiOutlinedInput-input': {
...fieldInputStyles,
'&::placeholder': {
color: 'var(--fa-color-text-disabled)',
opacity: 1,
},
},
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
gap: 2,
mt: 2,
}}
inputProps={{
'aria-labelledby': locationLabelId,
'aria-required': true,
}}
/>
>
{STATUS_OPTIONS.map((opt, i) => (
<StatusCard
key={opt.key}
ref={(el) => {
cardRefs.current[i] = el;
}}
title={opt.title}
description={opt.description}
selected={status === opt.key}
onClick={() => setStatus(opt.key)}
tabIndex={i === activeStatusIndex ? 0 : -1}
onKeyDown={handleStatusKeyDown}
/>
))}
</Box>
<Box aria-live="polite" sx={{ textAlign: 'center' }}>
{errors.status && (
<Typography
variant="caption"
role="alert"
sx={{
color: 'var(--fa-color-text-brand, #B0610F)',
display: 'block',
mt: 1,
}}
>
Please select how we can help
</Typography>
)}
</Box>
</Box>
<Box aria-live="polite">
{errors.location && (
<Typography
variant="caption"
role="alert"
{/* ── Funeral Type ────────────────────────────────────── */}
<Box>
<SectionLabel id={funeralTypeLabelId}>Funeral Type</SectionLabel>
<Box sx={{ mt: 2 }}>
<Select
value={funeralType}
onChange={handleFuneralType}
displayEmpty
renderValue={(v) =>
v ? FUNERAL_TYPE_OPTIONS.find((o) => o.value === v)?.label : selectPlaceholder
}
MenuProps={selectMenuProps}
sx={{
color: 'var(--fa-color-text-brand, #B0610F)',
display: 'block',
mt: 1,
...fieldBaseSx,
'& .MuiSelect-select': {
...fieldInputStyles,
minHeight: 'unset !important',
},
'& .MuiSelect-icon': {
color: 'var(--fa-color-text-disabled)',
right: 12,
},
}}
inputProps={{ 'aria-labelledby': funeralTypeLabelId }}
>
Please enter a suburb or postcode
</Typography>
)}
{FUNERAL_TYPE_OPTIONS.map((o) => (
<MenuItem key={o.value} value={o.value}>
{o.label}
</MenuItem>
))}
</Select>
</Box>
</Box>
{/* ── Location ────────────────────────────────────────── */}
<Box ref={locationSectionRef}>
<SectionLabel id={locationLabelId}>Location</SectionLabel>
<Box sx={{ mt: 2 }}>
<OutlinedInput
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="Enter suburb or postcode"
inputRef={locationInputRef}
startAdornment={
<InputAdornment position="start" sx={{ ml: 0.5 }}>
<LocationOnOutlinedIcon
sx={{
fontSize: 20,
color: 'var(--fa-color-text-disabled)',
}}
/>
</InputAdornment>
}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSubmit();
}}
sx={{
...fieldBaseSx,
'& .MuiOutlinedInput-input': {
...fieldInputStyles,
'&::placeholder': {
color: 'var(--fa-color-text-disabled)',
opacity: 1,
},
},
}}
inputProps={{
'aria-labelledby': locationLabelId,
'aria-required': true,
}}
/>
</Box>
<Box aria-live="polite">
{errors.location && (
<Typography
variant="caption"
role="alert"
sx={{
color: 'var(--fa-color-text-brand, #B0610F)',
display: 'block',
mt: 1,
}}
>
Please enter a suburb or postcode
</Typography>
)}
</Box>
</Box>
<Divider />
{/* ── CTA ─────────────────────────────────────────────── */}
<Box>
<Button
variant="contained"
size="large"
fullWidth
loading={loading}
endIcon={!loading ? <ArrowForwardIcon /> : undefined}
onClick={handleSubmit}
sx={{ minHeight: 52 }}
>
Find Funeral Directors
</Button>
<Typography
variant="captionSm"
color="text.secondary"
sx={{ textAlign: 'center', display: 'block', mt: 1.5 }}
>
Free to use &middot; No obligation
</Typography>
</Box>
</Box>
<Divider />
{/* ── CTA ─────────────────────────────────────────────── */}
<Box>
<Button
variant="contained"
size="large"
fullWidth
loading={loading}
endIcon={!loading ? <ArrowForwardIcon /> : undefined}
onClick={handleSubmit}
sx={{ minHeight: 52 }}
>
Find Funeral Directors
</Button>
<Typography
variant="captionSm"
color="text.secondary"
sx={{ textAlign: 'center', display: 'block', mt: 1.5 }}
>
Free to use &middot; No obligation
</Typography>
</Box>
</Box>
);
});
);
},
);
FuneralFinderV3.displayName = 'FuneralFinderV3';
export default FuneralFinderV3;

View File

@@ -77,11 +77,7 @@ export const WithCTA: Story = {
export const WithPageContent: Story = {
render: () => (
<Box>
<Navigation
logo={<FALogo />}
items={defaultItems}
ctaLabel="Start planning"
/>
<Navigation logo={<FALogo />} items={defaultItems} ctaLabel="Start planning" />
<Box
sx={{
maxWidth: 'lg',
@@ -94,8 +90,8 @@ export const WithPageContent: Story = {
Find a funeral director
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4, maxWidth: 600 }}>
Compare trusted funeral directors in your area. View services,
pricing, and reviews to find the right support for your family.
Compare trusted funeral directors in your area. View services, pricing, and reviews to
find the right support for your family.
</Typography>
{Array.from({ length: 8 }).map((_, i) => (
<Box

View File

@@ -156,11 +156,7 @@ export const Navigation = React.forwardRef<HTMLDivElement, NavigationProps>(
))}
{ctaLabel && (
<Button
variant="contained"
size="medium"
onClick={onCtaClick}
>
<Button variant="contained" size="medium" onClick={onCtaClick}>
{ctaLabel}
</Button>
)}
@@ -193,14 +189,8 @@ export const Navigation = React.forwardRef<HTMLDivElement, NavigationProps>(
bgcolor: 'var(--fa-color-surface-subtle)',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{logo}
</Box>
<IconButton
aria-label="Close menu"
onClick={handleDrawerToggle}
size="small"
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>{logo}</Box>
<IconButton aria-label="Close menu" onClick={handleDrawerToggle} size="small">
<CloseIcon />
</IconButton>
</Box>

View File

@@ -10,44 +10,136 @@ import { Button } from '../../atoms/Button';
import { Navigation } from '../Navigation';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
const DEMO_IMAGE = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
const DEMO_IMAGE =
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
const essentials = [
{ name: 'Accommodation', price: 1500, info: 'Refrigerated holding of the deceased prior to the funeral service.' },
{ name: 'Death Registration Certificate', price: 1500, info: 'Lodgement of death registration with NSW Registry of Births, Deaths & Marriages.' },
{ name: 'Doctor Fee for Cremation', price: 1500, info: 'Statutory medical referee fee required for all cremations in NSW.' },
{ name: 'NSW Government Levy — Cremation', price: 1500, info: 'NSW Government cremation levy as set by the Department of Health.' },
{ name: 'Professional Mortuary Care', price: 1500, info: 'Preparation and care of the deceased.' },
{ name: 'Professional Service Fee', price: 1500, info: 'Coordination of all funeral arrangements and services.' },
{ name: 'Allowance for Coffin', price: 1500, isAllowance: true, info: 'Allowance amount — upgrade options available during arrangement.' },
{ name: 'Allowance for Crematorium', price: 1500, isAllowance: true, info: 'Allowance for crematorium fees — varies by location.' },
{ name: 'Allowance for Hearse', price: 1500, isAllowance: true, info: 'Allowance for hearse transfer — distance surcharges may apply.' },
{
name: 'Accommodation',
price: 1500,
info: 'Refrigerated holding of the deceased prior to the funeral service.',
},
{
name: 'Death Registration Certificate',
price: 1500,
info: 'Lodgement of death registration with NSW Registry of Births, Deaths & Marriages.',
},
{
name: 'Doctor Fee for Cremation',
price: 1500,
info: 'Statutory medical referee fee required for all cremations in NSW.',
},
{
name: 'NSW Government Levy — Cremation',
price: 1500,
info: 'NSW Government cremation levy as set by the Department of Health.',
},
{
name: 'Professional Mortuary Care',
price: 1500,
info: 'Preparation and care of the deceased.',
},
{
name: 'Professional Service Fee',
price: 1500,
info: 'Coordination of all funeral arrangements and services.',
},
{
name: 'Allowance for Coffin',
price: 1500,
isAllowance: true,
info: 'Allowance amount — upgrade options available during arrangement.',
},
{
name: 'Allowance for Crematorium',
price: 1500,
isAllowance: true,
info: 'Allowance for crematorium fees — varies by location.',
},
{
name: 'Allowance for Hearse',
price: 1500,
isAllowance: true,
info: 'Allowance for hearse transfer — distance surcharges may apply.',
},
];
const complimentary = [
{ name: 'Dressing Fee', info: 'Dressing and preparation of the deceased — included at no charge.' },
{
name: 'Dressing Fee',
info: 'Dressing and preparation of the deceased — included at no charge.',
},
{ name: 'Viewing Fee', info: 'One private family viewing — included at no charge.' },
];
const extras = {
heading: 'Extras',
items: [
{ name: 'Allowance for Flowers', price: 1500, isAllowance: true, info: 'Seasonal floral arrangements for the service.' },
{ name: 'Allowance for Master of Ceremonies', price: 1500, isAllowance: true, info: 'Professional celebrant or MC for the funeral service.' },
{ name: 'After Business Hours Service Surcharge', price: 1500, info: 'Additional fee for services held outside standard business hours.' },
{ name: 'After Hours Prayers', price: 1500, info: 'Evening prayer service at the funeral home.' },
{ name: 'Coffin Bearing by Funeral Directors', price: 1500, info: 'Professional pallbearing by funeral directors.' },
{ name: 'Digital Recording', price: 1500, info: 'Professional video recording of the funeral service.' },
{
name: 'Allowance for Flowers',
price: 1500,
isAllowance: true,
info: 'Seasonal floral arrangements for the service.',
},
{
name: 'Allowance for Master of Ceremonies',
price: 1500,
isAllowance: true,
info: 'Professional celebrant or MC for the funeral service.',
},
{
name: 'After Business Hours Service Surcharge',
price: 1500,
info: 'Additional fee for services held outside standard business hours.',
},
{
name: 'After Hours Prayers',
price: 1500,
info: 'Evening prayer service at the funeral home.',
},
{
name: 'Coffin Bearing by Funeral Directors',
price: 1500,
info: 'Professional pallbearing by funeral directors.',
},
{
name: 'Digital Recording',
price: 1500,
info: 'Professional video recording of the funeral service.',
},
],
};
const termsText = '* This package includes a funeral service at a chapel or a church with a funeral procession following to the crematorium. It includes many of the most commonly selected funeral options preselected for you. Many people choose this package for the extended funeral rituals — of course, you can tailor the funeral service to meet your needs and budget as you go through the selections.';
const termsText =
'* This package includes a funeral service at a chapel or a church with a funeral procession following to the crematorium. It includes many of the most commonly selected funeral options preselected for you. Many people choose this package for the extended funeral rituals — of course, you can tailor the funeral service to meet your needs and budget as you go through the selections.';
const packages = [
{ id: 'everyday', name: 'Everyday Funeral Package', price: 900, description: 'Our most popular package with all essential services included. Suitable for a traditional chapel or church service.' },
{ id: 'deluxe', name: 'Deluxe Funeral Package', price: 1200, description: 'An enhanced package with premium coffin and additional floral arrangements.' },
{ id: 'essential', name: 'Essential Funeral Package', price: 600, description: 'A simple, dignified service covering all necessary arrangements.' },
{ id: 'catholic', name: 'Catholic Service', price: 950, description: 'A service tailored for Catholic traditions including prayers and church ceremony.' },
{
id: 'everyday',
name: 'Everyday Funeral Package',
price: 900,
description:
'Our most popular package with all essential services included. Suitable for a traditional chapel or church service.',
},
{
id: 'deluxe',
name: 'Deluxe Funeral Package',
price: 1200,
description: 'An enhanced package with premium coffin and additional floral arrangements.',
},
{
id: 'essential',
name: 'Essential Funeral Package',
price: 600,
description: 'A simple, dignified service covering all necessary arrangements.',
},
{
id: 'catholic',
name: 'Catholic Service',
price: 950,
description:
'A service tailored for Catholic traditions including prayers and church ceremony.',
},
];
const funeralTypes = ['All', 'Cremation', 'Burial', 'Memorial', 'Catholic', 'Direct Cremation'];
@@ -101,9 +193,7 @@ export const CompareLoading: Story = {
args: {
name: 'Everyday Funeral Package',
price: 900,
sections: [
{ heading: 'Essentials', items: essentials.slice(0, 4) },
],
sections: [{ heading: 'Essentials', items: essentials.slice(0, 4) }],
total: 6000,
onArrange: () => alert('Make Arrangement'),
onCompare: () => {},

View File

@@ -155,15 +155,14 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
<Typography variant="h3" component="h2">
{name}
</Typography>
<Typography
variant="h5"
sx={{ mt: 0.5, color: 'primary.main', fontWeight: 600 }}
>
<Typography variant="h5" sx={{ mt: 0.5, color: 'primary.main', fontWeight: 600 }}>
${price.toLocaleString('en-AU')}
</Typography>
{/* CTA buttons */}
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, mt: 2.5 }}>
<Box
sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, mt: 2.5 }}
>
<Button
variant="contained"
size="large"
@@ -198,9 +197,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
))}
{/* Total — separates included content from extras */}
{total != null && (
<LineItem name="Total" price={total} variant="total" />
)}
{total != null && <LineItem name="Total" price={total} variant="total" />}
{/* Extras — additional cost items after the total */}
{extras && extras.items.length > 0 && (

View File

@@ -1 +1,6 @@
export { PackageDetail, type PackageDetailProps, type PackageSection, type PackageLineItem } from './PackageDetail';
export {
PackageDetail,
type PackageDetailProps,
type PackageSection,
type PackageLineItem,
} from './PackageDetail';

View File

@@ -37,10 +37,30 @@ const serviceTypes = [
];
const coffinOptions = [
{ id: 'eco', name: 'Eco Willow', price: 850, description: 'Handwoven natural willow. Biodegradable and sustainable.' },
{ id: 'classic', name: 'Classic Maple', price: 1400, description: 'Solid maple with satin finish and brass handles.' },
{ id: 'premium', name: 'Premium Oak', price: 2200, description: 'Quarter-sawn oak with high-gloss lacquer and gold-plated handles.' },
{ id: 'simple', name: 'Simple Pine', price: 600, description: 'Unfinished pine. Can be personalised with paint, photos, or messages.' },
{
id: 'eco',
name: 'Eco Willow',
price: 850,
description: 'Handwoven natural willow. Biodegradable and sustainable.',
},
{
id: 'classic',
name: 'Classic Maple',
price: 1400,
description: 'Solid maple with satin finish and brass handles.',
},
{
id: 'premium',
name: 'Premium Oak',
price: 2200,
description: 'Quarter-sawn oak with high-gloss lacquer and gold-plated handles.',
},
{
id: 'simple',
name: 'Simple Pine',
price: 600,
description: 'Unfinished pine. Can be personalised with paint, photos, or messages.',
},
];
const meta: Meta<typeof ServiceSelector> = {
@@ -72,7 +92,7 @@ type Story = StoryObj<typeof ServiceSelector>;
export const Default: Story = {
args: {
heading: 'Choose a service type',
subheading: 'Select the type of service you\'d like to arrange. Prices are starting estimates.',
subheading: "Select the type of service you'd like to arrange. Prices are starting estimates.",
items: serviceTypes,
continueLabel: 'Continue',
},
@@ -180,7 +200,13 @@ export const WithDisabledOptions: Story = {
const [selected, setSelected] = useState<string | undefined>();
const items = serviceTypes.map((item) =>
item.id === 'memorial' ? { ...item, disabled: true, description: item.description + ' (Currently unavailable at this location.)' } : item,
item.id === 'memorial'
? {
...item,
disabled: true,
description: item.description + ' (Currently unavailable at this location.)',
}
: item,
);
return (
@@ -227,7 +253,11 @@ export const InArrangementFlow: Story = {
maxDescriptionLines={2}
/>
<Typography variant="captionSm" color="text.secondary" sx={{ mt: 3, textAlign: 'center', display: 'block' }}>
<Typography
variant="captionSm"
color="text.secondary"
sx={{ mt: 3, textAlign: 'center', display: 'block' }}
>
All prices are estimates and may vary based on your specific requirements.
</Typography>
</Box>

View File

@@ -92,13 +92,7 @@ export const ServiceSelector = React.forwardRef<HTMLDivElement, ServiceSelectorP
const isContinueDisabled = continueDisabled ?? nothingSelected;
return (
<Box
ref={ref}
sx={[
{ width: '100%' },
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<Box ref={ref} sx={[{ width: '100%' }, ...(Array.isArray(sx) ? sx : [sx])]}>
{/* Header */}
<Box sx={{ mb: 3 }}>
<Typography variant="h4" component="h2" sx={{ mb: subheading ? 1 : 0 }}>

View File

@@ -8,7 +8,9 @@ const App = () => (
<CssBaseline />
<div style={{ padding: 32 }}>
<h1>FA Design System</h1>
<p>Run <code>npm run storybook</code> to view components.</p>
<p>
Run <code>npm run storybook</code> to view components.
</p>
</div>
</ThemeProvider>
);
@@ -16,5 +18,5 @@ const App = () => (
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
</React.StrictMode>,
);

View File

@@ -356,7 +356,8 @@ export const theme = createTheme({
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',
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',
@@ -482,7 +483,8 @@ export const theme = createTheme({
// Reserve 2px border on ALL cards (transparent for elevated, coloured for outlined).
// Prevents layout shift when toggling selected state.
border: '2px solid transparent',
transition: 'box-shadow 150ms ease-in-out, border-color 150ms ease-in-out, background-color 150ms ease-in-out',
transition:
'box-shadow 150ms ease-in-out, border-color 150ms ease-in-out, background-color 150ms ease-in-out',
},
},
variants: [
@@ -512,9 +514,10 @@ export const theme = createTheme({
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,
},
'&: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,
@@ -586,7 +589,8 @@ export const theme = createTheme({
borderRadius: parseInt(t.ChipBorderRadiusDefault, 10),
fontWeight: 500,
letterSpacing: '0.01em',
transition: 'background-color 150ms ease-in-out, border-color 150ms ease-in-out, box-shadow 150ms ease-in-out',
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',
@@ -597,27 +601,39 @@ export const theme = createTheme({
fontSize: t.ChipFontSizeMd,
'& .MuiChip-label': { paddingLeft: t.ChipPaddingXMd, paddingRight: t.ChipPaddingXMd },
'& .MuiChip-icon': { fontSize: t.ChipIconSizeMd, marginLeft: t.ChipPaddingXMd },
'& .MuiChip-deleteIcon': { fontSize: t.ChipDeleteIconSizeMd, marginRight: t.ChipPaddingXMd },
'& .MuiChip-deleteIcon': {
fontSize: t.ChipDeleteIconSizeMd,
marginRight: t.ChipPaddingXMd,
},
},
sizeSmall: {
height: parseInt(t.ChipHeightSm, 10),
fontSize: t.ChipFontSizeSm,
'& .MuiChip-label': { paddingLeft: t.ChipPaddingXMd, paddingRight: t.ChipPaddingXMd },
'& .MuiChip-icon': { fontSize: t.ChipIconSizeSm, marginLeft: t.ChipPaddingXSm },
'& .MuiChip-deleteIcon': { fontSize: t.ChipDeleteIconSizeSm, marginRight: t.ChipPaddingXSm },
'& .MuiChip-deleteIcon': {
fontSize: t.ChipDeleteIconSizeSm,
marginRight: t.ChipPaddingXSm,
},
},
filled: {
'&.MuiChip-colorDefault': {
backgroundColor: t.ColorNeutral200,
color: t.ColorNeutral700,
'&:hover': { backgroundColor: t.ColorNeutral300 },
'& .MuiChip-deleteIcon': { color: t.ColorNeutral500, '&:hover': { color: t.ColorNeutral700 } },
'& .MuiChip-deleteIcon': {
color: t.ColorNeutral500,
'&:hover': { color: t.ColorNeutral700 },
},
},
'&.MuiChip-colorPrimary': {
backgroundColor: t.ColorBrand200,
color: t.ColorBrand700,
'&:hover': { backgroundColor: t.ColorBrand300 },
'& .MuiChip-deleteIcon': { color: t.ColorBrand400, '&:hover': { color: t.ColorBrand700 } },
'& .MuiChip-deleteIcon': {
color: t.ColorBrand400,
'&:hover': { color: t.ColorBrand700 },
},
},
},
outlined: {
@@ -625,13 +641,19 @@ export const theme = createTheme({
borderColor: t.ColorNeutral300,
color: t.ColorNeutral700,
'&:hover': { backgroundColor: t.ColorNeutral100, borderColor: t.ColorNeutral400 },
'& .MuiChip-deleteIcon': { color: t.ColorNeutral400, '&:hover': { color: t.ColorNeutral700 } },
'& .MuiChip-deleteIcon': {
color: t.ColorNeutral400,
'&:hover': { color: t.ColorNeutral700 },
},
},
'&.MuiChip-colorPrimary': {
borderColor: t.ColorBrand400,
color: t.ColorBrand700,
'&:hover': { backgroundColor: t.ColorBrand100, borderColor: t.ColorBrand500 },
'& .MuiChip-deleteIcon': { color: t.ColorBrand400, '&:hover': { color: t.ColorBrand700 } },
'& .MuiChip-deleteIcon': {
color: t.ColorBrand400,
'&:hover': { color: t.ColorBrand700 },
},
},
},
},