Add FuneralFinder organism — hero search with stepped flow

Procedural stepped search widget for the homepage:
1. "I'm here to" — Arrange now / Pre-plan (radiogroup)
2. "I'm planning for" — Myself / Someone else (conditional, pre-plan only)
3. "Type of funeral" — dynamic Chip list from prop
4. Location — suburb or postcode input
5. CTA: "Find funeral directors" (disabled until complete)

Features:
- Progressive disclosure: steps unlock sequentially
- Brand checkmarks + edit icon on completed steps
- Click completed value to revert (only resets dependents)
- Step numbering adjusts when conditional step hidden
- Trust signal: "Free to use · No obligation"

Audit: 17/20 (Good). P1/P2 fixes applied:
- Location input aria-label for screen readers
- Option groups wrapped in role="radiogroup"
- Completed values have edit icon affordance
- Heading uses display font CSS variable
- CheckCircleIcon aria-hidden

5 stories: Default, FewerTypes, CustomHeading, InHeroDesktop, InHeroMobile

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 09:15:07 +11:00
parent 83e3809752
commit d7dddb0773
4 changed files with 705 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
import type { Meta, StoryObj } from '@storybook/react';
import Box from '@mui/material/Box';
import { FuneralFinder } from './FuneralFinder';
import { Navigation } from '../Navigation';
import { Typography } from '../../atoms/Typography';
const funeralTypes = [
{ id: 'cremation', label: 'Cremation' },
{ id: 'burial', label: 'Burial' },
{ id: 'memorial', label: 'Memorial' },
{ id: 'catholic', label: 'Catholic' },
{ id: 'direct-cremation', label: 'Direct Cremation' },
{ id: 'natural-burial', label: 'Natural Burial' },
];
const FALogoNav = () => (
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 28 }} />
);
const meta: Meta<typeof FuneralFinder> = {
title: 'Organisms/FuneralFinder',
component: FuneralFinder,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
decorators: [
(Story) => (
<Box sx={{ maxWidth: 520, width: '100%' }}>
<Story />
</Box>
),
],
};
export default meta;
type Story = StoryObj<typeof FuneralFinder>;
// --- Default -----------------------------------------------------------------
/** Initial state — step 1 active, all others locked */
export const Default: Story = {
args: {
funeralTypes,
onSearch: (params) => alert(JSON.stringify(params, null, 2)),
},
};
// --- Fewer Funeral Types -----------------------------------------------------
/** With only 3 funeral types — shows compact chip row */
export const FewerTypes: Story = {
args: {
funeralTypes: funeralTypes.slice(0, 3),
onSearch: (params) => alert(JSON.stringify(params, null, 2)),
},
};
// --- Custom Heading ----------------------------------------------------------
/** With custom heading and subheading */
export const CustomHeading: Story = {
args: {
funeralTypes,
heading: 'Compare funeral directors in your area',
subheading: 'Transparent pricing · No hidden fees · 24/7',
onSearch: (params) => alert(JSON.stringify(params, null, 2)),
},
};
// --- In Hero Context (Desktop) -----------------------------------------------
/** As it appears in the homepage hero — desktop layout */
export const InHeroDesktop: Story = {
decorators: [
(Story) => (
<Box sx={{ maxWidth: 'none', width: '100%' }}>
<Story />
</Box>
),
],
render: () => (
<Box>
<Navigation
logo={<FALogoNav />}
items={[
{ label: 'Provider Portal', href: '/provider-portal' },
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Log in', href: '/login' },
]}
/>
{/* Hero section */}
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
minHeight: { md: 600 },
bgcolor: 'var(--fa-color-brand-100)',
}}
>
{/* Left: heading + search widget */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
px: { xs: 2, md: 6 },
py: { xs: 4, md: 6 },
}}
>
<Box sx={{ maxWidth: 520, width: '100%' }}>
<Typography
variant="displaySm"
component="h1"
sx={{
textAlign: 'center',
mb: 2,
color: 'var(--fa-color-brand-950)',
}}
>
Discover, Explore, and Plan Funerals in Minutes
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{ textAlign: 'center', mb: 4, maxWidth: 440, mx: 'auto' }}
>
Whether you're thinking ahead or arranging for a loved one, find
trusted local providers with transparent pricing.
</Typography>
<FuneralFinder
funeralTypes={funeralTypes}
onSearch={(params) => alert(JSON.stringify(params, null, 2))}
/>
</Box>
</Box>
{/* Right: hero image placeholder */}
<Box
sx={{
display: { xs: 'none', md: 'block' },
bgcolor: 'var(--fa-color-brand-200)',
backgroundImage: 'url(https://images.unsplash.com/photo-1516733968668-dbdce39c0571?w=800&h=600&fit=crop)',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
</Box>
</Box>
),
};
// --- In Hero Context (Mobile) ------------------------------------------------
/** Mobile viewport — stacked layout with image above search */
export const InHeroMobile: Story = {
decorators: [
(Story) => (
<Box sx={{ maxWidth: 420, width: '100%', mx: 'auto' }}>
<Story />
</Box>
),
],
render: () => (
<Box>
<Navigation
logo={<FALogoNav />}
items={[
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Log in', href: '/login' },
]}
/>
{/* Hero heading */}
<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)' }}
>
Discover, Explore, and Plan Funerals in Minutes
</Typography>
<Typography variant="body2" color="text.secondary">
Find trusted local providers with transparent pricing, at your own pace.
</Typography>
</Box>
{/* Hero image */}
<Box
sx={{
height: 180,
bgcolor: 'var(--fa-color-brand-200)',
backgroundImage: 'url(https://images.unsplash.com/photo-1516733968668-dbdce39c0571?w=800&h=400&fit=crop)',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
{/* Search widget — overlaps image slightly */}
<Box sx={{ px: 2, mt: -3, pb: 4, bgcolor: 'var(--fa-color-brand-100)' }}>
<FuneralFinder
funeralTypes={funeralTypes}
onSearch={(params) => alert(JSON.stringify(params, null, 2))}
/>
</Box>
</Box>
),
};