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

View File

@@ -87,7 +87,9 @@ export const FigmaMapping: Story = {
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button variant="contained">Primary</Button> <Button variant="contained">Primary</Button>
<Button variant="soft">Sec / Brand</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> <Button variant="text">Ghost</Button>
</div> </div>
), ),
@@ -113,10 +115,18 @@ export const VariantsSecondary: Story = {
name: 'Variants — Secondary', name: 'Variants — Secondary',
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button variant="contained" color="secondary">Contained</Button> <Button variant="contained" color="secondary">
<Button variant="soft" color="secondary">Soft</Button> Contained
<Button variant="outlined" color="secondary">Outlined</Button> </Button>
<Button variant="text" color="secondary">Text</Button> <Button variant="soft" color="secondary">
Soft
</Button>
<Button variant="outlined" color="secondary">
Outlined
</Button>
<Button variant="text" color="secondary">
Text
</Button>
</div> </div>
), ),
}; };
@@ -140,10 +150,18 @@ export const AllSizesSoft: Story = {
name: 'All Sizes — Soft', name: 'All Sizes — Soft',
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button variant="soft" size="xs">Extra small</Button> <Button variant="soft" size="xs">
<Button variant="soft" size="small">Small</Button> Extra small
<Button variant="soft" size="medium">Medium</Button> </Button>
<Button variant="soft" size="large">Large</Button> <Button variant="soft" size="small">
Small
</Button>
<Button variant="soft" size="medium">
Medium
</Button>
<Button variant="soft" size="large">
Large
</Button>
</div> </div>
), ),
}; };
@@ -180,10 +198,18 @@ export const IconsAllSizes: Story = {
name: 'Icons — All Sizes', name: 'Icons — All Sizes',
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button size="xs" startIcon={<AddIcon />}>Add</Button> <Button size="xs" startIcon={<AddIcon />}>
<Button size="small" startIcon={<AddIcon />}>Add</Button> Add
<Button size="medium" startIcon={<AddIcon />}>Add</Button> </Button>
<Button size="large" 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> </div>
), ),
}; };
@@ -204,9 +230,15 @@ export const DisabledAllVariants: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button disabled>Contained</Button> <Button disabled>Contained</Button>
<Button disabled variant="soft">Soft</Button> <Button disabled variant="soft">
<Button disabled variant="outlined">Outlined</Button> Soft
<Button disabled variant="text">Text</Button> </Button>
<Button disabled variant="outlined">
Outlined
</Button>
<Button disabled variant="text">
Text
</Button>
</div> </div>
), ),
}; };
@@ -225,9 +257,15 @@ export const LoadingAllVariants: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button loading>Submitting...</Button> <Button loading>Submitting...</Button>
<Button loading variant="soft">Processing...</Button> <Button loading variant="soft">
<Button loading variant="outlined">Processing...</Button> Processing...
<Button loading variant="text">Loading...</Button> </Button>
<Button loading variant="outlined">
Processing...
</Button>
<Button loading variant="text">
Loading...
</Button>
</div> </div>
), ),
}; };
@@ -293,9 +331,15 @@ export const TextButtonComparison: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 24, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
<Button variant="text">No underline</Button> <Button variant="text">No underline</Button>
<Button variant="text" underline>With underline</Button> <Button variant="text" underline>
<Button variant="text" color="secondary">Secondary</Button> With underline
<Button variant="text" color="secondary" underline>Secondary underlined</Button> </Button>
<Button variant="text" color="secondary">
Secondary
</Button>
<Button variant="text" color="secondary" underline>
Secondary underlined
</Button>
</div> </div>
), ),
}; };
@@ -305,10 +349,18 @@ export const TextButtonSizes: Story = {
name: 'Text Buttons — All Sizes', name: 'Text Buttons — All Sizes',
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button variant="text" size="xs">Extra small</Button> <Button variant="text" size="xs">
<Button variant="text" size="small">Small</Button> Extra small
<Button variant="text" size="medium">Medium</Button> </Button>
<Button variant="text" size="large">Large</Button> <Button variant="text" size="small">
Small
</Button>
<Button variant="text" size="medium">
Medium
</Button>
<Button variant="text" size="large">
Large
</Button>
</div> </div>
), ),
}; };
@@ -348,15 +400,27 @@ export const CompleteMatrix: Story = {
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{(['contained', 'soft', 'outlined', 'text'] as const).map((variant) => ( {(['contained', 'soft', 'outlined', 'text'] as const).map((variant) => (
<div key={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} {variant}
</div> </div>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
<Button variant={variant} color="primary">Primary</Button> <Button variant={variant} color="primary">
<Button variant={variant} color="secondary">Secondary</Button> Primary
<Button variant={variant} color="primary" startIcon={<AddIcon />}>With icon</Button> </Button>
<Button variant={variant} color="primary" disabled>Disabled</Button> <Button variant={variant} color="secondary">
<Button variant={variant} color="primary" loading>Loading...</Button> Secondary
</Button>
<Button variant={variant} color="primary" startIcon={<AddIcon />}>
With icon
</Button>
<Button variant={variant} color="primary" disabled>
Disabled
</Button>
<Button variant={variant} color="primary" loading>
Loading...
</Button>
</div> </div>
</div> </div>
))} ))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -116,7 +116,17 @@ export const CommonUseCases: Story = {
{/* Card actions toolbar */} {/* Card actions toolbar */}
<Box> <Box>
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Card action 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"> <IconButton size="small" color="primary" aria-label="Favourite">
<FavoriteBorderIcon /> <FavoriteBorderIcon />
</IconButton> </IconButton>
@@ -132,7 +142,18 @@ export const CommonUseCases: Story = {
{/* Dialog close */} {/* Dialog close */}
<Box> <Box>
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Dialog close button</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> <Box sx={{ fontWeight: 600 }}>Confirm Selection</Box>
<IconButton size="small" aria-label="Close dialog"> <IconButton size="small" aria-label="Close dialog">
<CloseIcon /> <CloseIcon />
@@ -143,7 +164,17 @@ export const CommonUseCases: Story = {
{/* Navigation header */} {/* Navigation header */}
<Box> <Box>
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Mobile navigation toggle</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"> <IconButton size="large" aria-label="Open menu">
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>

View File

@@ -122,10 +122,7 @@ export const FigmaMapping: Story = {
helperText="Input Label - Description" helperText="Input Label - Description"
endIcon={<SearchIcon />} endIcon={<SearchIcon />}
/> />
<Input <Input placeholder="Select an option" endIcon={<SearchIcon />} />
placeholder="Select an option"
endIcon={<SearchIcon />}
/>
<Input placeholder="Select an option" /> <Input placeholder="Select an option" />
</div> </div>
), ),
@@ -222,20 +219,16 @@ export const SizeAlignment: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}> <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
<Input <Input placeholder="Search arrangements..." endIcon={<SearchIcon />} size="medium" />
placeholder="Search arrangements..." <Button size="large" sx={{ minWidth: 100, minHeight: 48 }}>
endIcon={<SearchIcon />} Search
size="medium" </Button>
/>
<Button size="large" sx={{ minWidth: 100, minHeight: 48 }}>Search</Button>
</div> </div>
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}> <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
<Input <Input placeholder="Quick search..." endIcon={<SearchIcon />} size="small" />
placeholder="Quick search..." <Button size="medium" sx={{ minWidth: 100, minHeight: 40 }}>
endIcon={<SearchIcon />} Search
size="small" </Button>
/>
<Button size="medium" sx={{ minWidth: 100, minHeight: 40 }}>Search</Button>
</div> </div>
</div> </div>
), ),
@@ -248,11 +241,7 @@ export const WithIcons: Story = {
name: 'With Icons', name: 'With Icons',
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<Input <Input label="Search" placeholder="Search services..." endIcon={<SearchIcon />} />
label="Search"
placeholder="Search services..."
endIcon={<SearchIcon />}
/>
<Input <Input
label="Email" label="Email"
placeholder="you@example.com" placeholder="you@example.com"
@@ -265,12 +254,7 @@ export const WithIcons: Story = {
startIcon={<PhoneOutlinedIcon />} startIcon={<PhoneOutlinedIcon />}
type="tel" type="tel"
/> />
<Input <Input label="Amount" placeholder="0.00" startIcon={<AttachMoneyIcon />} type="number" />
label="Amount"
placeholder="0.00"
startIcon={<AttachMoneyIcon />}
type="number"
/>
<Input <Input
label="Email verified" label="Email verified"
defaultValue="john@example.com" defaultValue="john@example.com"
@@ -355,18 +339,22 @@ export const ValidationFlow: Story = {
required required
startIcon={<EmailOutlinedIcon />} startIcon={<EmailOutlinedIcon />}
endIcon={ endIcon={
showSuccess ? <CheckCircleOutlineIcon sx={{ color: 'success.main' }} /> : showSuccess ? (
showError ? <ErrorOutlineIcon sx={{ color: 'error.main' }} /> : <CheckCircleOutlineIcon sx={{ color: 'success.main' }} />
undefined ) : showError ? (
<ErrorOutlineIcon sx={{ color: 'error.main' }} />
) : undefined
} }
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
error={showError} error={showError}
success={showSuccess} success={showSuccess}
helperText={ helperText={
showError ? 'Please enter a valid email address' : showError
showSuccess ? 'Looks good!' : ? 'Please enter a valid email address'
'Required for arrangement confirmation' : showSuccess
? 'Looks good!'
: 'Required for arrangement confirmation'
} }
/> />
); );
@@ -387,9 +375,7 @@ export const ArrangementForm: Story = {
], ],
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ fontWeight: 700, fontSize: 20, marginBottom: 4 }}> <div style={{ fontWeight: 700, fontSize: 20, marginBottom: 4 }}>Contact details</div>
Contact details
</div>
<Input <Input
label="Full name" label="Full name"
placeholder="Enter your full name" placeholder="Enter your full name"
@@ -443,7 +429,16 @@ export const CompleteMatrix: Story = {
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{(['medium', 'small'] as const).map((size) => ( {(['medium', 'small'] as const).map((size) => (
<div key={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'}) Size: {size} ({size === 'medium' ? '48px' : '40px'})
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}> <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 // Prefer convenience icon props; fall back to raw adornment props
const resolvedStart = startIcon ? ( const resolvedStart = startIcon ? (
<InputAdornment position="start">{startIcon}</InputAdornment> <InputAdornment position="start">{startIcon}</InputAdornment>
) : startAdornment; ) : (
startAdornment
);
const resolvedEnd = endIcon ? ( const resolvedEnd = endIcon ? (
<InputAdornment position="end">{endIcon}</InputAdornment> <InputAdornment position="end">{endIcon}</InputAdornment>
) : endAdornment; ) : (
endAdornment
);
return ( return (
<FormControl <FormControl
@@ -133,18 +137,19 @@ export const Input = React.forwardRef<HTMLDivElement, InputProps>(
aria-describedby={helperId} aria-describedby={helperId}
sx={[ sx={[
// Success border + focus ring (not a native MUI state) // Success border + focus ring (not a native MUI state)
success && !error && { success &&
'& .MuiOutlinedInput-notchedOutline': { !error && {
borderColor: 'success.main', '& .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]), ...(Array.isArray(sx) ? sx : [sx]),
]} ]}
{...props} {...props}

View File

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

View File

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

View File

@@ -77,39 +77,84 @@ export const ServiceAddOns: Story = {
}; };
const items = [ 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: 'catering' as const,
{ key: 'music' as const, label: 'Live music', desc: 'Organist or solo musician', price: '$350' }, label: 'Catering',
{ key: 'memorial' as const, label: 'Memorial video', desc: 'Photo slideshow with music', price: '$200' }, desc: 'Light refreshments after the service',
{ key: 'guestBook' as const, label: 'Guest book', desc: 'Leather-bound memorial guest book', price: '$85' }, 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) => const total = items.reduce(
addOns[item.key] ? sum + parseInt(item.price.replace('$', ''), 10) : sum, 0, (sum, item) => (addOns[item.key] ? sum + parseInt(item.price.replace('$', ''), 10) : sum),
0,
); );
return ( return (
<Box sx={{ maxWidth: 420 }}> <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> <FormGroup>
{items.map((item) => ( {items.map((item) => (
<Card key={item.key} variant="outlined" padding="compact" sx={{ mb: 1 }}> <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 }}> <Box sx={{ flex: 1 }}>
<Typography variant="label">{item.label}</Typography> <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>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <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)} /> <Switch checked={addOns[item.key]} onChange={() => toggle(item.key)} />
</Box> </Box>
</Box> </Box>
</Card> </Card>
))} ))}
</FormGroup> </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">Total add-ons</Typography>
<Typography variant="labelLg" color="primary">${total}</Typography> <Typography variant="labelLg" color="primary">
${total}
</Typography>
</Box> </Box>
</Box> </Box>
); );

View File

@@ -16,22 +16,35 @@ const meta: Meta<typeof Typography> = {
variant: { variant: {
control: 'select', control: 'select',
options: [ options: [
'displayHero', 'display1', 'display2', 'display3', 'displaySm', 'displayHero',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'display1',
'bodyLg', 'body1', 'body2', 'bodyXs', 'display2',
'labelLg', 'label', 'labelSm', 'display3',
'caption', 'captionSm', 'displaySm',
'overline', 'overlineSm', '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', description: 'Typography variant — 21 variants across 6 categories',
table: { defaultValue: { summary: 'body1' } }, table: { defaultValue: { summary: 'body1' } },
}, },
color: { color: {
control: 'select', control: 'select',
options: [ options: ['textPrimary', 'textSecondary', 'textDisabled', 'primary', 'secondary', 'error'],
'textPrimary', 'textSecondary', 'textDisabled',
'primary', 'secondary', 'error',
],
}, },
maxLines: { control: 'number' }, maxLines: { control: 'number' },
gutterBottom: { control: 'boolean' }, gutterBottom: { control: 'boolean' },
@@ -47,7 +60,8 @@ const SAMPLE = 'Discover, Explore, and Plan Funerals in Minutes, Not Hours';
export const Default: Story = { export const Default: Story = {
args: { 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: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div> <div>
<Typography variant="captionSm" color="textSecondary">displayHero 80px</Typography> <Typography variant="captionSm" color="textSecondary">
displayHero 80px
</Typography>
<Typography variant="displayHero">{SAMPLE}</Typography> <Typography variant="displayHero">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">display1 64px</Typography> <Typography variant="captionSm" color="textSecondary">
display1 64px
</Typography>
<Typography variant="display1">{SAMPLE}</Typography> <Typography variant="display1">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">display2 52px</Typography> <Typography variant="captionSm" color="textSecondary">
display2 52px
</Typography>
<Typography variant="display2">{SAMPLE}</Typography> <Typography variant="display2">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">display3 40px</Typography> <Typography variant="captionSm" color="textSecondary">
display3 40px
</Typography>
<Typography variant="display3">{SAMPLE}</Typography> <Typography variant="display3">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">displaySm 32px</Typography> <Typography variant="captionSm" color="textSecondary">
displaySm 32px
</Typography>
<Typography variant="displaySm">{SAMPLE}</Typography> <Typography variant="displaySm">{SAMPLE}</Typography>
</div> </div>
</div> </div>
@@ -90,27 +114,39 @@ export const Headings: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div> <div>
<Typography variant="captionSm" color="textSecondary">h1 36px</Typography> <Typography variant="captionSm" color="textSecondary">
h1 36px
</Typography>
<Typography variant="h1">{SAMPLE}</Typography> <Typography variant="h1">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">h2 30px</Typography> <Typography variant="captionSm" color="textSecondary">
h2 30px
</Typography>
<Typography variant="h2">{SAMPLE}</Typography> <Typography variant="h2">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">h3 24px</Typography> <Typography variant="captionSm" color="textSecondary">
h3 24px
</Typography>
<Typography variant="h3">{SAMPLE}</Typography> <Typography variant="h3">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">h4 20px</Typography> <Typography variant="captionSm" color="textSecondary">
h4 20px
</Typography>
<Typography variant="h4">{SAMPLE}</Typography> <Typography variant="h4">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">h5 18px</Typography> <Typography variant="captionSm" color="textSecondary">
h5 18px
</Typography>
<Typography variant="h5">{SAMPLE}</Typography> <Typography variant="h5">{SAMPLE}</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">h6 16px</Typography> <Typography variant="captionSm" color="textSecondary">
h6 16px
</Typography>
<Typography variant="h6">{SAMPLE}</Typography> <Typography variant="h6">{SAMPLE}</Typography>
</div> </div>
</div> </div>
@@ -125,31 +161,39 @@ export const Body: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 640 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 640 }}>
<div> <div>
<Typography variant="overline" gutterBottom>bodyLg 18px</Typography> <Typography variant="overline" gutterBottom>
bodyLg 18px
</Typography>
<Typography variant="bodyLg"> <Typography variant="bodyLg">
Planning a funeral is one of the most difficult tasks a family faces. Funeral Arranger Planning a funeral is one of the most difficult tasks a family faces. Funeral Arranger is
is here to help you navigate this process with care and transparency. here to help you navigate this process with care and transparency.
</Typography> </Typography>
</div> </div>
<div> <div>
<Typography variant="overline" gutterBottom>body1 (default) 16px</Typography> <Typography variant="overline" gutterBottom>
body1 (default) 16px
</Typography>
<Typography variant="body1"> <Typography variant="body1">
Compare funeral directors in your area, view transparent pricing, and make informed Compare funeral directors in your area, view transparent pricing, and make informed
decisions at your own pace. Every family deserves clarity during this time. decisions at your own pace. Every family deserves clarity during this time.
</Typography> </Typography>
</div> </div>
<div> <div>
<Typography variant="overline" gutterBottom>body2 (small) 14px</Typography> <Typography variant="overline" gutterBottom>
body2 (small) 14px
</Typography>
<Typography variant="body2"> <Typography variant="body2">
Prices shown are indicative and may vary based on your specific requirements. Prices shown are indicative and may vary based on your specific requirements. Contact the
Contact the funeral director directly for a detailed quote. funeral director directly for a detailed quote.
</Typography> </Typography>
</div> </div>
<div> <div>
<Typography variant="overline" gutterBottom>bodyXs 12px</Typography> <Typography variant="overline" gutterBottom>
bodyXs 12px
</Typography>
<Typography variant="bodyXs"> <Typography variant="bodyXs">
Terms and conditions apply. Funeral Arranger is a comparison service and does not Terms and conditions apply. Funeral Arranger is a comparison service and does not directly
directly provide funeral services. ABN 12 345 678 901. provide funeral services. ABN 12 345 678 901.
</Typography> </Typography>
</div> </div>
</div> </div>
@@ -164,32 +208,60 @@ export const UIText: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div> <div>
<Typography variant="captionSm" color="textSecondary">labelLg 16px medium</Typography> <Typography variant="captionSm" color="textSecondary">
<Typography variant="labelLg" display="block">Form label or section label</Typography> labelLg 16px medium
</Typography>
<Typography variant="labelLg" display="block">
Form label or section label
</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">label 14px medium</Typography> <Typography variant="captionSm" color="textSecondary">
<Typography variant="label" display="block">Default form label</Typography> label 14px medium
</Typography>
<Typography variant="label" display="block">
Default form label
</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">labelSm 12px medium</Typography> <Typography variant="captionSm" color="textSecondary">
<Typography variant="labelSm" display="block">Compact label or tag text</Typography> labelSm 12px medium
</Typography>
<Typography variant="labelSm" display="block">
Compact label or tag text
</Typography>
</div> </div>
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<Typography variant="captionSm" color="textSecondary">caption 12px regular</Typography> <Typography variant="captionSm" color="textSecondary">
<Typography variant="caption" display="block">Fine print, timestamps, metadata</Typography> caption 12px regular
</Typography>
<Typography variant="caption" display="block">
Fine print, timestamps, metadata
</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">captionSm 11px regular</Typography> <Typography variant="captionSm" color="textSecondary">
<Typography variant="captionSm" display="block">Compact metadata, footnotes</Typography> captionSm 11px regular
</Typography>
<Typography variant="captionSm" display="block">
Compact metadata, footnotes
</Typography>
</div> </div>
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<Typography variant="captionSm" color="textSecondary">overline 12px semibold uppercase</Typography> <Typography variant="captionSm" color="textSecondary">
<Typography variant="overline" display="block">Section overline</Typography> overline 12px semibold uppercase
</Typography>
<Typography variant="overline" display="block">
Section overline
</Typography>
</div> </div>
<div> <div>
<Typography variant="captionSm" color="textSecondary">overlineSm 11px semibold uppercase</Typography> <Typography variant="captionSm" color="textSecondary">
<Typography variant="overlineSm" display="block">Compact overline</Typography> overlineSm 11px semibold uppercase
</Typography>
<Typography variant="overlineSm" display="block">
Compact overline
</Typography>
</div> </div>
</div> </div>
), ),
@@ -222,20 +294,25 @@ export const FontFamilies: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div> <div>
<Typography variant="overline" gutterBottom>Display font Noto Serif SC (Regular 400)</Typography> <Typography variant="overline" gutterBottom>
<Typography variant="display3"> Display font Noto Serif SC (Regular 400)
Warm, trustworthy, and professional
</Typography> </Typography>
<Typography variant="display3">Warm, trustworthy, and professional</Typography>
<Typography variant="caption" color="textSecondary" sx={{ mt: 1 }}> <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> </Typography>
</div> </div>
<div> <div>
<Typography variant="overline" gutterBottom>Body font Montserrat</Typography> <Typography variant="overline" gutterBottom>
<Typography variant="h3" gutterBottom>Clean, modern, and highly readable</Typography> Body font Montserrat
</Typography>
<Typography variant="h3" gutterBottom>
Clean, modern, and highly readable
</Typography>
<Typography> <Typography>
Used for all headings (h1h6), body text, labels, captions, and UI elements. Used for all headings (h1h6), body text, labels, captions, and UI elements. Headings use
Headings use Bold (700), body uses Medium (500), captions use Regular (400). Bold (700), body uses Medium (500), captions use Regular (400).
</Typography> </Typography>
</div> </div>
</div> </div>
@@ -249,18 +326,21 @@ export const MaxLines: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 400 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 400 }}>
<div> <div>
<Typography variant="label" gutterBottom>maxLines=1</Typography> <Typography variant="label" gutterBottom>
maxLines=1
</Typography>
<Typography maxLines={1}> <Typography maxLines={1}>
H. Parsons Funeral Directors trusted by Australian families for over 30 years, H. Parsons Funeral Directors trusted by Australian families for over 30 years, providing
providing compassionate and transparent funeral services. compassionate and transparent funeral services.
</Typography> </Typography>
</div> </div>
<div> <div>
<Typography variant="label" gutterBottom>maxLines=2</Typography> <Typography variant="label" gutterBottom>
maxLines=2
</Typography>
<Typography maxLines={2}> <Typography maxLines={2}>
H. Parsons Funeral Directors trusted by Australian families for over 30 years, H. Parsons Funeral Directors trusted by Australian families for over 30 years, providing
providing compassionate and transparent funeral services across metropolitan compassionate and transparent funeral services across metropolitan and regional areas.
and regional areas.
</Typography> </Typography>
</div> </div>
</div> </div>
@@ -276,23 +356,27 @@ export const RealisticContent: Story = {
<Typography variant="overline">Funeral planning</Typography> <Typography variant="overline">Funeral planning</Typography>
<Typography variant="display3">Compare funeral services in your area</Typography> <Typography variant="display3">Compare funeral services in your area</Typography>
<Typography variant="bodyLg" color="textSecondary"> <Typography variant="bodyLg" color="textSecondary">
Transparent pricing and service comparison to help you make informed Transparent pricing and service comparison to help you make informed decisions during a
decisions during a difficult time. difficult time.
</Typography> </Typography>
<Typography variant="h2" sx={{ mt: 2 }}>How it works</Typography> <Typography variant="h2" sx={{ mt: 2 }}>
<Typography> How it works
Enter your suburb or postcode to find funeral directors near you. Each
listing includes a full price breakdown, service inclusions, and reviews
from families who have used their services.
</Typography> </Typography>
<Typography variant="h3" sx={{ mt: 1 }}>Step 1: Browse packages</Typography>
<Typography> <Typography>
Compare packages side by side. Each package clearly shows what is and Enter your suburb or postcode to find funeral directors near you. Each listing includes a
isn't included, so there are no surprises. 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>
<Typography variant="caption" color="textSecondary" sx={{ mt: 2 }}> <Typography variant="caption" color="textSecondary" sx={{ mt: 2 }}>
Prices are indicative and current as of March 2026. Contact the funeral Prices are indicative and current as of March 2026. Contact the funeral director for a
director for a binding quote. binding quote.
</Typography> </Typography>
</div> </div>
), ),
@@ -333,7 +417,11 @@ export const CompleteScale: Story = {
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{variants.map(({ variant, label }) => ( {variants.map(({ variant, label }) => (
<div key={variant} style={{ display: 'flex', alignItems: 'baseline', gap: 16 }}> <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} {label}
</Typography> </Typography>
<Typography variant={variant}>{SAMPLE}</Typography> <Typography variant={variant}>{SAMPLE}</Typography>

View File

@@ -150,7 +150,8 @@ export const ServiceAddOns: Story = {
export const WithoutPrice: Story = { export const WithoutPrice: Story = {
args: { args: {
name: 'Order of service booklet', 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 = { export const WithoutDescription: Story = {
render: function Render() { render: function Render() {
const [checked, setChecked] = React.useState(false); const [checked, setChecked] = React.useState(false);
return ( return <AddOnOption name="Include GST in pricing" checked={checked} onChange={setChecked} />;
<AddOnOption
name="Include GST in pricing"
checked={checked}
onChange={setChecked}
/>
);
}, },
}; };
@@ -176,7 +171,8 @@ export const WithoutDescription: Story = {
export const Disabled: Story = { export const Disabled: Story = {
args: { args: {
name: 'Catering', 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, price: 1200,
disabled: true, disabled: true,
}, },

View File

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

View File

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

View File

@@ -68,7 +68,8 @@ export const Default: Story = {
reviewCount: 127, reviewCount: 127,
capabilityLabel: 'Online Arrangement', capabilityLabel: 'Online Arrangement',
capabilityColor: 'success', 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, startingPrice: 900,
}, },
}; };
@@ -311,11 +312,7 @@ export const EdgeCases: Story = {
onClick={() => {}} onClick={() => {}}
/> />
{/* Minimal — just name and location */} {/* Minimal — just name and location */}
<ProviderCard <ProviderCard name="Minimal Card" location="Hobart" onClick={() => {}} />
name="Minimal Card"
location="Hobart"
onClick={() => {}}
/>
</Box> </Box>
), ),
}; };

View File

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

View File

@@ -2,7 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { ProviderCardCompact } from './ProviderCardCompact'; 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> = { const meta: Meta<typeof ProviderCardCompact> = {
title: 'Molecules/ProviderCardCompact', title: 'Molecules/ProviderCardCompact',

View File

@@ -101,10 +101,7 @@ export const ProviderCardCompact = React.forwardRef<HTMLDivElement, ProviderCard
{/* Location */} {/* Location */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<LocationOnOutlinedIcon <LocationOnOutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} aria-hidden />
sx={{ fontSize: 16, color: 'text.secondary' }}
aria-hidden
/>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{location} {location}
</Typography> </Typography>
@@ -118,7 +115,10 @@ export const ProviderCardCompact = React.forwardRef<HTMLDivElement, ProviderCard
aria-hidden aria-hidden
/> />
<Typography variant="caption" color="text.secondary"> <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> </Typography>
</Box> </Box>
)} )}

View File

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

View File

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

View File

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

View File

@@ -106,7 +106,15 @@ export const Interactive: Story = {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<StepIndicator steps={arrangementSteps} currentStep={step} /> <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 }}> <Typography variant="h5" sx={{ mb: 1 }}>
{arrangementSteps[step].label} {arrangementSteps[step].label}
</Typography> </Typography>

View File

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

View File

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

View File

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

View File

@@ -64,19 +64,7 @@ export interface FooterProps {
* ``` * ```
*/ */
export const Footer = React.forwardRef<HTMLDivElement, 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 year = new Date().getFullYear();
const copyrightText = copyright || `\u00A9 ${year} Funeral Arranger. All rights reserved.`; 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}> <Typography variant="overlineSm" sx={overlineSx}>
Email Email
</Typography> </Typography>
<Link <Link href={`mailto:${email}`} sx={contactLinkSx}>
href={`mailto:${email}`}
sx={contactLinkSx}
>
{email} {email}
</Link> </Link>
</Box> </Box>
@@ -157,7 +142,15 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
{/* Link group columns */} {/* Link group columns */}
{linkGroups.map((group) => ( {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 <Typography
variant="label" variant="label"
sx={{ sx={{
@@ -170,7 +163,14 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
</Typography> </Typography>
<Box <Box
component="ul" 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) => ( {group.links.map((link) => (
<li key={link.label}> <li key={link.label}>
@@ -206,10 +206,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
py: 3, py: 3,
}} }}
> >
<Typography <Typography variant="captionSm" sx={{ color: 'var(--fa-color-brand-400)' }}>
variant="captionSm"
sx={{ color: 'var(--fa-color-brand-400)' }}
>
{copyrightText} {copyrightText}
</Typography> </Typography>

View File

@@ -9,7 +9,12 @@ import { Typography } from '../../atoms/Typography';
const funeralTypes = [ const funeralTypes = [
{ id: 'cremation', label: 'Cremation', hasServiceOption: true }, { id: 'cremation', label: 'Cremation', hasServiceOption: true },
{ id: 'burial', label: 'Burial', 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 = [ const themeOptions = [
@@ -152,8 +157,8 @@ export const InHeroDesktop: Story = {
color="text.secondary" color="text.secondary"
sx={{ textAlign: 'center', mb: 4, maxWidth: 440, mx: 'auto' }} sx={{ textAlign: 'center', mb: 4, maxWidth: 440, mx: 'auto' }}
> >
Whether you're thinking ahead or arranging for a loved one, find Whether you're thinking ahead or arranging for a loved one, find trusted local
trusted local providers with transparent pricing. providers with transparent pricing.
</Typography> </Typography>
<FuneralFinder <FuneralFinder
funeralTypes={funeralTypes} funeralTypes={funeralTypes}
@@ -199,7 +204,11 @@ export const InHeroMobile: Story = {
]} ]}
/> />
<Box sx={{ bgcolor: 'var(--fa-color-brand-100)', px: 3, py: 4, textAlign: 'center' }}> <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 Discover, Explore, and Plan Funerals in Minutes
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
@@ -210,7 +219,8 @@ export const InHeroMobile: Story = {
sx={{ sx={{
height: 180, height: 180,
bgcolor: 'var(--fa-color-brand-200)', 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', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
}} }}

View File

@@ -146,7 +146,11 @@ function ChoiceCard({
</Typography> </Typography>
</Box> </Box>
{description && ( {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} {description}
</Typography> </Typography>
)} )}
@@ -214,12 +218,20 @@ function TypeCard({
</Typography> </Typography>
</Box> </Box>
{description && ( {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} {description}
</Typography> </Typography>
)} )}
{note && ( {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} {note}
</Typography> </Typography>
)} )}
@@ -261,7 +273,13 @@ function CompletedRow({
onClick={onChangeClick} onClick={onChangeClick}
underline="hover" underline="hover"
aria-label={`Change ${question.toLowerCase()}`} 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 Change
</Link> </Link>
@@ -348,9 +366,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
const typeSummary = [typeLabel, themeSuffix].filter(Boolean).join(', '); const typeSummary = [typeLabel, themeSuffix].filter(Boolean).join(', ');
const serviceLabel = const serviceLabel =
servicePref === 'with-service' ? 'With a service' servicePref === 'with-service'
: servicePref === 'without-service' ? 'No service' ? 'With a service'
: 'Flexible'; : servicePref === 'without-service'
? 'No service'
: 'Flexible';
// ─── Handlers ─────────────────────────────────────────────────── // ─── Handlers ───────────────────────────────────────────────────
const selectIntent = (value: Intent) => { const selectIntent = (value: Intent) => {
@@ -409,7 +429,7 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
intent, intent,
planningFor: needsPlanningFor ? (planningFor ?? undefined) : undefined, planningFor: needsPlanningFor ? (planningFor ?? undefined) : undefined,
funeralTypeId: isExploreAll ? null : (typeSelection ?? null), funeralTypeId: isExploreAll ? null : (typeSelection ?? null),
servicePreference: (showServiceStep && serviceAnswered) ? servicePref : 'either', servicePreference: showServiceStep && serviceAnswered ? servicePref : 'either',
themes: selectedThemes, themes: selectedThemes,
location: location.trim(), location: location.trim(),
}); });
@@ -448,16 +468,32 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
{/* ── Completed rows ─────────────────────────────────────── */} {/* ── Completed rows ─────────────────────────────────────── */}
<Collapse in={intent !== null && activeStep !== 1} timeout={250}> <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>
<Collapse in={needsPlanningFor && planningFor !== null && activeStep !== 2} timeout={250}> <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>
<Collapse in={typeSelected && activeStep !== 3} timeout={250}> <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>
<Collapse in={showServiceStep && serviceAnswered && activeStep !== 4} timeout={250}> <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> </Collapse>
{/* ── Step 1: Intent ─────────────────────────────────────── */} {/* ── Step 1: Intent ─────────────────────────────────────── */}
@@ -467,13 +503,22 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Typography <Typography
variant="caption" variant="caption"
role="alert" 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 Please let us know how we can help
</Typography> </Typography>
)} )}
<StepHeading>How can we help you today?</StepHeading> <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 <ChoiceCard
label="Arrange a funeral now" label="Arrange a funeral now"
description="Someone has passed and I need to make arrangements" 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}> <Collapse in={activeStep === 2 && needsPlanningFor} timeout={250}>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<StepHeading>Who are you planning for?</StepHeading> <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 <ChoiceCard
label="Myself" label="Myself"
description="I want to plan my own funeral in advance" 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}> <Collapse in={activeStep === 3} timeout={250}>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<StepHeading>What type of funeral are you considering?</StepHeading> <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) => ( {funeralTypes.map((ft) => (
<TypeCard <TypeCard
key={ft.id} key={ft.id}
@@ -543,11 +596,20 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Typography variant="body2" component="span" sx={{ fontWeight: 600 }}> <Typography variant="body2" component="span" sx={{ fontWeight: 600 }}>
Any preferences? Any preferences?
</Typography> </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) (optional)
</Typography> </Typography>
</Box> </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) => { {themeOptions.map((theme) => {
const isSelected = selectedThemes.includes(theme.id); const isSelected = selectedThemes.includes(theme.id);
return ( return (
@@ -573,7 +635,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
<Collapse in={activeStep === 4 && showServiceStep} timeout={250}> <Collapse in={activeStep === 4 && showServiceStep} timeout={250}>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<StepHeading>Would you like a service?</StepHeading> <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) => ( {SERVICE_OPTIONS.map((opt) => (
<Chip <Chip
key={opt.value} key={opt.value}
@@ -583,7 +649,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
onClick={() => selectService(opt.value)} onClick={() => selectService(opt.value)}
clickable clickable
aria-pressed={serviceAnswered && servicePref === opt.value} 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> </Box>

View File

@@ -41,12 +41,8 @@ export const BelowMasthead: Story = {
textAlign: 'center', textAlign: 'center',
}} }}
> >
<Box sx={{ fontSize: '2rem', fontWeight: 700, mb: 1 }}> <Box sx={{ fontSize: '2rem', fontWeight: 700, mb: 1 }}>Funeral Arranger</Box>
Funeral Arranger <Box sx={{ opacity: 0.8 }}>Find trusted funeral directors near you</Box>
</Box>
<Box sx={{ opacity: 0.8 }}>
Find trusted funeral directors near you
</Box>
</Box> </Box>
{/* Widget below masthead */} {/* Widget below masthead */}
<Box sx={{ maxWidth: 560, mx: 'auto', mt: -4, px: 2, position: 'relative', zIndex: 1 }}> <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', transition: 'background-color 200ms ease, color 200ms ease',
...(usePrimary ...(usePrimary
? { bgcolor: 'var(--fa-color-brand-500)', color: 'common.white' } ? { 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 // Connector line from bottom of this circle toward the next
...(showConnector && { ...(showConnector && {
'&::after': { '&::after': {
@@ -245,7 +248,8 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
} }
: { lookingTo: false, planningFor: false, funeralType: false, location: false }; : { 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 ──────────────────────────────────────────────── // ─── Handlers ────────────────────────────────────────────────
const handleLookingTo = (e: SelectChangeEvent<string>) => { const handleLookingTo = (e: SelectChangeEvent<string>) => {
@@ -311,11 +315,7 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
> >
{heading} {heading}
</Typography> </Typography>
<Typography <Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mb: 0 }}>
variant="body2"
color="text.secondary"
sx={{ textAlign: 'center', mb: 0 }}
>
{subheading} {subheading}
</Typography> </Typography>
<Divider sx={{ my: 3.5 }} /> <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' }}> <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={1} completed={!!lookingTo} active showConnector /> <StepCircle step={1} completed={!!lookingTo} active showConnector />
<Box sx={{ flex: 1 }}> <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; I&rsquo;m looking to&hellip;
</Typography> </Typography>
<Select <Select
@@ -352,7 +355,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}> <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={2} completed={!!planningFor} showConnector /> <StepCircle step={2} completed={!!planningFor} showConnector />
<Box sx={{ flex: 1 }}> <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 I&rsquo;m planning for
</Typography> </Typography>
<Select <Select
@@ -383,7 +393,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}> <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={3} completed={!!funeralType} showConnector /> <StepCircle step={3} completed={!!funeralType} showConnector />
<Box sx={{ flex: 1 }}> <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 Type of funeral
</Typography> </Typography>
<Select <Select
@@ -410,7 +427,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}> <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
<StepCircle step={4} completed={location.trim().length >= 3} /> <StepCircle step={4} completed={location.trim().length >= 3} />
<Box sx={{ flex: 1 }}> <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 Looking for providers in
</Typography> </Typography>
<Input <Input

View File

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

View File

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

View File

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

View File

@@ -10,44 +10,136 @@ import { Button } from '../../atoms/Button';
import { Navigation } from '../Navigation'; import { Navigation } from '../Navigation';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; 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 = [ 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: 'Accommodation',
{ name: 'Doctor Fee for Cremation', price: 1500, info: 'Statutory medical referee fee required for all cremations in NSW.' }, price: 1500,
{ name: 'NSW Government Levy — Cremation', price: 1500, info: 'NSW Government cremation levy as set by the Department of Health.' }, info: 'Refrigerated holding of the deceased prior to the funeral service.',
{ 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: 'Death Registration Certificate',
{ name: 'Allowance for Crematorium', price: 1500, isAllowance: true, info: 'Allowance for crematorium fees — varies by location.' }, price: 1500,
{ name: 'Allowance for Hearse', price: 1500, isAllowance: true, info: 'Allowance for hearse transfer — distance surcharges may apply.' }, 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 = [ 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.' }, { name: 'Viewing Fee', info: 'One private family viewing — included at no charge.' },
]; ];
const extras = { const extras = {
heading: 'Extras', heading: 'Extras',
items: [ 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: 'Allowance for Flowers',
{ name: 'After Business Hours Service Surcharge', price: 1500, info: 'Additional fee for services held outside standard business hours.' }, price: 1500,
{ name: 'After Hours Prayers', price: 1500, info: 'Evening prayer service at the funeral home.' }, isAllowance: true,
{ name: 'Coffin Bearing by Funeral Directors', price: 1500, info: 'Professional pallbearing by funeral directors.' }, info: 'Seasonal floral arrangements for the service.',
{ name: 'Digital Recording', price: 1500, info: 'Professional video recording of the funeral 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 = [ 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: 'everyday',
{ id: 'essential', name: 'Essential Funeral Package', price: 600, description: 'A simple, dignified service covering all necessary arrangements.' }, name: 'Everyday Funeral Package',
{ id: 'catholic', name: 'Catholic Service', price: 950, description: 'A service tailored for Catholic traditions including prayers and church ceremony.' }, 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']; const funeralTypes = ['All', 'Cremation', 'Burial', 'Memorial', 'Catholic', 'Direct Cremation'];
@@ -101,9 +193,7 @@ export const CompareLoading: Story = {
args: { args: {
name: 'Everyday Funeral Package', name: 'Everyday Funeral Package',
price: 900, price: 900,
sections: [ sections: [{ heading: 'Essentials', items: essentials.slice(0, 4) }],
{ heading: 'Essentials', items: essentials.slice(0, 4) },
],
total: 6000, total: 6000,
onArrange: () => alert('Make Arrangement'), onArrange: () => alert('Make Arrangement'),
onCompare: () => {}, onCompare: () => {},

View File

@@ -155,15 +155,14 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
<Typography variant="h3" component="h2"> <Typography variant="h3" component="h2">
{name} {name}
</Typography> </Typography>
<Typography <Typography variant="h5" sx={{ mt: 0.5, color: 'primary.main', fontWeight: 600 }}>
variant="h5"
sx={{ mt: 0.5, color: 'primary.main', fontWeight: 600 }}
>
${price.toLocaleString('en-AU')} ${price.toLocaleString('en-AU')}
</Typography> </Typography>
{/* CTA buttons */} {/* 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 <Button
variant="contained" variant="contained"
size="large" size="large"
@@ -198,9 +197,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
))} ))}
{/* Total — separates included content from extras */} {/* Total — separates included content from extras */}
{total != null && ( {total != null && <LineItem name="Total" price={total} variant="total" />}
<LineItem name="Total" price={total} variant="total" />
)}
{/* Extras — additional cost items after the total */} {/* Extras — additional cost items after the total */}
{extras && extras.items.length > 0 && ( {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 = [ 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: 'eco',
{ id: 'premium', name: 'Premium Oak', price: 2200, description: 'Quarter-sawn oak with high-gloss lacquer and gold-plated handles.' }, name: 'Eco Willow',
{ id: 'simple', name: 'Simple Pine', price: 600, description: 'Unfinished pine. Can be personalised with paint, photos, or messages.' }, 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> = { const meta: Meta<typeof ServiceSelector> = {
@@ -72,7 +92,7 @@ type Story = StoryObj<typeof ServiceSelector>;
export const Default: Story = { export const Default: Story = {
args: { args: {
heading: 'Choose a service type', 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, items: serviceTypes,
continueLabel: 'Continue', continueLabel: 'Continue',
}, },
@@ -180,7 +200,13 @@ export const WithDisabledOptions: Story = {
const [selected, setSelected] = useState<string | undefined>(); const [selected, setSelected] = useState<string | undefined>();
const items = serviceTypes.map((item) => 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 ( return (
@@ -227,7 +253,11 @@ export const InArrangementFlow: Story = {
maxDescriptionLines={2} 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. All prices are estimates and may vary based on your specific requirements.
</Typography> </Typography>
</Box> </Box>

View File

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

View File

@@ -8,7 +8,9 @@ const App = () => (
<CssBaseline /> <CssBaseline />
<div style={{ padding: 32 }}> <div style={{ padding: 32 }}>
<h1>FA Design System</h1> <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> </div>
</ThemeProvider> </ThemeProvider>
); );
@@ -16,5 +18,5 @@ const App = () => (
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>,
); );

View File

@@ -356,7 +356,8 @@ export const theme = createTheme({
textTransform: 'none' as const, textTransform: 'none' as const,
fontWeight: 600, fontWeight: 600,
letterSpacing: '0.02em', 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': { '&:focus-visible': {
outline: `2px solid ${t.ColorInteractiveFocus}`, outline: `2px solid ${t.ColorInteractiveFocus}`,
outlineOffset: '2px', outlineOffset: '2px',
@@ -482,7 +483,8 @@ export const theme = createTheme({
// Reserve 2px border on ALL cards (transparent for elevated, coloured for outlined). // Reserve 2px border on ALL cards (transparent for elevated, coloured for outlined).
// Prevents layout shift when toggling selected state. // Prevents layout shift when toggling selected state.
border: '2px solid transparent', 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: [ variants: [
@@ -512,9 +514,10 @@ export const theme = createTheme({
transition: 'border-color 150ms ease-in-out', transition: 'border-color 150ms ease-in-out',
}, },
// Hover — darker border (skip when focused, error, or disabled) // Hover — darker border (skip when focused, error, or disabled)
'&:hover:not(.Mui-focused):not(.Mui-error):not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': { '&:hover:not(.Mui-focused):not(.Mui-error):not(.Mui-disabled) .MuiOutlinedInput-notchedOutline':
borderColor: t.ColorNeutral400, {
}, borderColor: t.ColorNeutral400,
},
// Focus — brand gold border + double ring (white gap + brand ring) // Focus — brand gold border + double ring (white gap + brand ring)
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: t.ColorBrand500, borderColor: t.ColorBrand500,
@@ -586,7 +589,8 @@ export const theme = createTheme({
borderRadius: parseInt(t.ChipBorderRadiusDefault, 10), borderRadius: parseInt(t.ChipBorderRadiusDefault, 10),
fontWeight: 500, fontWeight: 500,
letterSpacing: '0.01em', 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': { '&:focus-visible': {
outline: `2px solid ${t.ColorInteractiveFocus}`, outline: `2px solid ${t.ColorInteractiveFocus}`,
outlineOffset: '2px', outlineOffset: '2px',
@@ -597,27 +601,39 @@ export const theme = createTheme({
fontSize: t.ChipFontSizeMd, fontSize: t.ChipFontSizeMd,
'& .MuiChip-label': { paddingLeft: t.ChipPaddingXMd, paddingRight: t.ChipPaddingXMd }, '& .MuiChip-label': { paddingLeft: t.ChipPaddingXMd, paddingRight: t.ChipPaddingXMd },
'& .MuiChip-icon': { fontSize: t.ChipIconSizeMd, marginLeft: 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: { sizeSmall: {
height: parseInt(t.ChipHeightSm, 10), height: parseInt(t.ChipHeightSm, 10),
fontSize: t.ChipFontSizeSm, fontSize: t.ChipFontSizeSm,
'& .MuiChip-label': { paddingLeft: t.ChipPaddingXMd, paddingRight: t.ChipPaddingXMd }, '& .MuiChip-label': { paddingLeft: t.ChipPaddingXMd, paddingRight: t.ChipPaddingXMd },
'& .MuiChip-icon': { fontSize: t.ChipIconSizeSm, marginLeft: t.ChipPaddingXSm }, '& .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: { filled: {
'&.MuiChip-colorDefault': { '&.MuiChip-colorDefault': {
backgroundColor: t.ColorNeutral200, backgroundColor: t.ColorNeutral200,
color: t.ColorNeutral700, color: t.ColorNeutral700,
'&:hover': { backgroundColor: t.ColorNeutral300 }, '&:hover': { backgroundColor: t.ColorNeutral300 },
'& .MuiChip-deleteIcon': { color: t.ColorNeutral500, '&:hover': { color: t.ColorNeutral700 } }, '& .MuiChip-deleteIcon': {
color: t.ColorNeutral500,
'&:hover': { color: t.ColorNeutral700 },
},
}, },
'&.MuiChip-colorPrimary': { '&.MuiChip-colorPrimary': {
backgroundColor: t.ColorBrand200, backgroundColor: t.ColorBrand200,
color: t.ColorBrand700, color: t.ColorBrand700,
'&:hover': { backgroundColor: t.ColorBrand300 }, '&:hover': { backgroundColor: t.ColorBrand300 },
'& .MuiChip-deleteIcon': { color: t.ColorBrand400, '&:hover': { color: t.ColorBrand700 } }, '& .MuiChip-deleteIcon': {
color: t.ColorBrand400,
'&:hover': { color: t.ColorBrand700 },
},
}, },
}, },
outlined: { outlined: {
@@ -625,13 +641,19 @@ export const theme = createTheme({
borderColor: t.ColorNeutral300, borderColor: t.ColorNeutral300,
color: t.ColorNeutral700, color: t.ColorNeutral700,
'&:hover': { backgroundColor: t.ColorNeutral100, borderColor: t.ColorNeutral400 }, '&: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': { '&.MuiChip-colorPrimary': {
borderColor: t.ColorBrand400, borderColor: t.ColorBrand400,
color: t.ColorBrand700, color: t.ColorBrand700,
'&:hover': { backgroundColor: t.ColorBrand100, borderColor: t.ColorBrand500 }, '&: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 },
},
}, },
}, },
}, },