From 2690a231f6f72d8478d51daa347186f13fb651ec Mon Sep 17 00:00:00 2001 From: Richie Date: Thu, 26 Mar 2026 18:32:26 +1100 Subject: [PATCH] =?UTF-8?q?Add=20FuneralFinder=20v3=20=E2=80=94=20clean=20?= =?UTF-8?q?form=20with=20status=20cards=20+=20glassmorphism?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Two side-by-side StatusCards (Immediate Need / Pre-planning) with warm fills - Glassmorphism container (backdrop-blur, semi-transparent white, deep shadow) - Overline section labels, warm tonal field backgrounds (brand-100, no border) - Funeral type Select + location Input with pin icon, no focus ring per design - CTA always active — validates on click, scrolls to first missing field - WAI-ARIA roving tabindex on radiogroup, aria-labelledby via useId() - Semantic tokens throughout (border-brand, surface-warm, text-brand, etc.) - Critique: 33/40 (Good), Audit: 18/20 (Excellent) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/memory/component-registry.md | 1 + docs/memory/session-log.md | 43 ++ .../FuneralFinder/FuneralFinderV3.stories.tsx | 110 ++++ .../FuneralFinder/FuneralFinderV3.tsx | 578 ++++++++++++++++++ .../organisms/FuneralFinder/index.ts | 6 + 5 files changed, 738 insertions(+) create mode 100644 src/components/organisms/FuneralFinder/FuneralFinderV3.stories.tsx create mode 100644 src/components/organisms/FuneralFinder/FuneralFinderV3.tsx diff --git a/docs/memory/component-registry.md b/docs/memory/component-registry.md index 1fe5212..f8a5114 100644 --- a/docs/memory/component-registry.md +++ b/docs/memory/component-registry.md @@ -56,6 +56,7 @@ duplicates) and MUST update it after completing one. | PackageDetail | done | LineItem × n + Typography + Button + Divider | Right-side package detail panel. Warm header band (surface.warm) with "Package" overline, name, price (brand colour), Make Arrangement + Compare (with loading) buttons. Sections (before total) + total + extras (after total, with subtext). T&C grey footer. Audit: 19/20. Maps to Figma Package Select (5405:181955). | | FuneralFinder | done | Typography + Button + Chip + Input + Divider + Link + custom ChoiceCard/TypeCard/CompletedRow/StepHeading | Hero search widget v1. Full stepped conversational flow: Intent → Planning For (conditional) → Funeral Type + preferences (optional themes) → Service preference (conditional, auto-advance) → Location + CTA (always visible). Smart defaults — minimum search: intent + location. Types: Cremation, Burial, Water Burial (QLD only), Explore All. Service: With/No/Flexible. Themes: eco-friendly, budget-friendly, religious specialisation. Loading prop, location validation. Audit: 14/20 (Good). Critique: 29/40 (Good). Future: progress indicator, roving tabindex, location autocomplete. | | FuneralFinderV2 | done | Typography + Button + Input + Divider + Select + MenuItem + custom StepCircle | Hero search widget v2 — quick-form approach. 4-step vertical form with numbered circles (48px, brand-200 default → brand-500 completed) and connector lines. Steps: (1) Intent — 3 options, (2) Planning for — conditional auto-set for arrange-now, (3) Funeral type — 5 options, (4) Location — text input. Sequential unlock: each step enables when previous is filled. Display serif heading + subheading with divider. CTA disabled until location filled. Trust signal below CTA. Critique: 33/40 (Good). Audit: 18/20 (Excellent). | +| FuneralFinderV3 | done | Typography + Button + Divider + Select + MenuItem + OutlinedInput + custom StatusCard/SectionLabel | Hero search widget v3 — clean form with status cards. Glassmorphism container (backdrop-blur, semi-transparent white). Two side-by-side tappable StatusCards (Immediate Need / Pre-planning) with warm tonal fills. Funeral type Select + location OutlinedInput with location pin icon — both use warm brand-100 fill, no border, no focus ring (per design). Overline section labels (brand-800). CTA always active — validates on click, scrolls to first missing field. Required: status + location. Funeral type defaults to "show all". WAI-ARIA roving tabindex on radiogroup. aria-labelledby via useId(). Critique: 33/40 (Good). Audit: 18/20 (Excellent). | | ArrangementForm | planned | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Deferred — build remaining atoms/molecules first. | | Navigation | done | AppBar + Link + IconButton + Button + Divider + Drawer | Responsive site header. Desktop: logo left, links right, optional CTA. Mobile: hamburger + drawer with nav items, CTA, help footer. Sticky, grey surface bg (surface.subtle). Real FA logo from brandassets/. Maps to Figma Main Nav (14:108) + Mobile Header (2391:41508). | | Footer | done | Link × n + Typography + Divider + Container + Grid | Dark espresso (brand.950) site footer. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). | diff --git a/docs/memory/session-log.md b/docs/memory/session-log.md index cb701e6..537f5b8 100644 --- a/docs/memory/session-log.md +++ b/docs/memory/session-log.md @@ -23,6 +23,49 @@ Each entry follows this structure: ## Sessions +### Session 2026-03-26f — FuneralFinder v3 build + +**Agent(s):** Claude Opus 4.6 (1M context) + +**Work completed:** +- Created FuneralFinderV3 — clean vertical form approach based on user's Figma mockup (5919:29445) +- Two side-by-side StatusCards (Immediate Need / Pre-planning) replace V2's dropdown + step circles +- Glassmorphism container (backdrop-blur, semi-transparent white, warm-tinted border, deep shadow) +- Overline section labels (uppercase, brand-800) for "Current Status", "Funeral Type", "Location" +- Warm tonal field backgrounds (brand-100) with no visible border, no focus ring (per user request) +- Location input with location pin icon (LocationOnOutlined) +- CTA always active — validates on click, scrolls to first missing field (status or location) +- Funeral type options: same as V2 + "Show all options" +- WAI-ARIA roving tabindex on radiogroup (arrow-key navigation between status cards) +- aria-labelledby via React.useId() for all fields +- Semantic token usage: border-brand, surface-warm, text-brand, interactive-focus, text-disabled +- Error messages conditionally rendered in aria-live regions (copper/brand tone, not red — gentle validation) +- First pass scored Critique 33/40, Audit 13/20 +- Iterated on all P1-P3 findings, re-audit scored 18/20 + +**Decisions made:** +- Status cards replace V2's step-circle + dropdown pattern — simpler, more visual, side-by-side on desktop +- Glassmorphism is a V3 differentiator — works over hero images, degrades gracefully to opaque white +- No sequential unlock — all fields accessible immediately (V2 locked steps until previous was filled) +- CTA always active (V2 disabled CTA until location was filled) — validates on click, scrolls to missing field +- Form simplified to 3 fields (status, funeral type, location) vs V2's 4 (intent, planning-for, type, location) +- "Planning for" folded into StatusCard descriptions rather than separate field +- Focus ring suppressed on Select/Input per user requirement — status cards retain focus-visible +- Error colour uses text.brand (copper) not feedback.error (red) — intentional gentle tone for funeral context +- Warm brand-tinted CTA shadow (brand.600 at 20% opacity) — no token for coloured shadows, commented + +**Open questions:** +- User to review V3 in Storybook — compare against V1 and V2 +- Location autocomplete integration still pending across all versions +- Consider adding component tokens for the glassmorphism treatment if reused elsewhere + +**Next steps:** +- User review of V3 in Storybook +- Decision: V1 vs V2 vs V3 for production +- If V3 chosen: location autocomplete, possible mobile refinements + +--- + ### Session 2026-03-26e — FuneralFinder v2 polish + consistency fixes **Agent(s):** Claude Opus 4.6 (1M context) diff --git a/src/components/organisms/FuneralFinder/FuneralFinderV3.stories.tsx b/src/components/organisms/FuneralFinder/FuneralFinderV3.stories.tsx new file mode 100644 index 0000000..bb1ba6b --- /dev/null +++ b/src/components/organisms/FuneralFinder/FuneralFinderV3.stories.tsx @@ -0,0 +1,110 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { FuneralFinderV3 } from './FuneralFinderV3'; + +const meta: Meta = { + title: 'Organisms/FuneralFinderV3', + component: FuneralFinderV3, + parameters: { + layout: 'padded', + }, + args: { + onSearch: (params) => { + console.log('Search params:', params); + }, + }, +}; + +export default meta; +type Story = StoryObj; + +/** Default empty state — all fields ready for input */ +export const Default: Story = {}; + +/** Loading state — CTA shows spinner */ +export const Loading: Story = { + args: { loading: true }, +}; + +/** Over a hero image — demonstrates the glassmorphism effect */ +export const OnHeroImage: Story = { + decorators: [ + (Story) => ( + + + + + + ), + ], + parameters: { + layout: 'fullscreen', + }, +}; + +/** Below a masthead — overlapping hero section */ +export const BelowMasthead: Story = { + decorators: [ + (Story) => ( + + + + Funeral Arranger + + + Find trusted funeral directors near you + + + + + + + ), + ], +}; + +/** Constrained width — typical sidebar or narrow column */ +export const Narrow: Story = { + decorators: [ + (Story) => ( + + + + ), + ], +}; diff --git a/src/components/organisms/FuneralFinder/FuneralFinderV3.tsx b/src/components/organisms/FuneralFinder/FuneralFinderV3.tsx new file mode 100644 index 0000000..2e0cebe --- /dev/null +++ b/src/components/organisms/FuneralFinder/FuneralFinderV3.tsx @@ -0,0 +1,578 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Select, { type SelectChangeEvent } from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import InputAdornment from '@mui/material/InputAdornment'; +import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +type Status = 'immediate' | 'preplanning'; +type FuneralType = + | 'cremation-funeral' + | 'cremation-only' + | 'burial-funeral' + | 'graveside-only' + | 'water-cremation' + | 'show-all'; + +/** Search parameters returned on form submission */ +export interface FuneralFinderV3SearchParams { + /** User's current situation */ + status: Status; + /** Type of funeral selected (defaults to show-all if not chosen) */ + funeralType: FuneralType; + /** Suburb or postcode */ + location: string; +} + +/** Props for the FuneralFinder v3 organism */ +export interface FuneralFinderV3Props { + /** Called when the user submits with valid data */ + onSearch?: (params: FuneralFinderV3SearchParams) => void; + /** Shows loading state on the CTA */ + loading?: boolean; + /** Optional heading override */ + heading?: string; + /** Optional subheading override */ + subheading?: string; + /** MUI sx override for the root container */ + sx?: SxProps; +} + +// ─── Options ───────────────────────────────────────────────────────────────── + +const STATUS_OPTIONS: { key: Status; title: string; description: string }[] = [ + { + key: 'immediate', + title: 'Immediate Need', + description: 'For a recent or expected loss', + }, + { + key: 'preplanning', + title: 'Pre-planning', + description: 'For yourself or a loved one', + }, +]; + +const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [ + { value: 'cremation-funeral', label: 'Cremation with funeral' }, + { + value: 'cremation-only', + label: 'Cremation only (no funeral, no attendance)', + }, + { value: 'burial-funeral', label: 'Burial with funeral' }, + { value: 'graveside-only', label: 'Graveside burial only' }, + { value: 'water-cremation', label: 'Water cremation (QLD only)' }, + { value: 'show-all', label: 'Show all options' }, +]; + +/** Hoisted outside component to avoid re-creation on render */ +const selectPlaceholder = ( + + Select funeral type + +); + +// ─── Sub-components ────────────────────────────────────────────────────────── + +/** Uppercase section label — overline style */ +function SectionLabel({ + children, + id, +}: { + children: React.ReactNode; + id?: string; +}) { + return ( + + {children} + + ); +} + +/** Tappable status card with roving tabindex support for radiogroup pattern */ +const StatusCard = React.forwardRef< + HTMLButtonElement, + { + title: string; + description: string; + selected: boolean; + onClick: () => void; + tabIndex?: number; + onKeyDown?: React.KeyboardEventHandler; + } +>(({ title, description, selected, onClick, tabIndex, onKeyDown }, ref) => ( + + + {title} + + + {description} + + +)); +StatusCard.displayName = 'StatusCard'; + +// ─── Shared field styles ──────────────────────────────────────────────────── + +/** Warm tonal fill, no visible border, no focus ring (per design spec) */ +const fieldBaseSx = { + width: '100%', + bgcolor: 'var(--fa-color-brand-100, #F7ECDF)', + borderRadius: 'var(--fa-border-radius-md, 8px)', + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '&.Mui-focused': { + boxShadow: 'none', + }, +}; + +const fieldInputStyles = { + py: '14px', + px: 2, + fontSize: '1rem', + fontFamily: 'var(--fa-font-family-body)', +}; + +const selectMenuProps = { + PaperProps: { + sx: { + mt: 0.5, + borderRadius: 'var(--fa-border-radius-md, 8px)', + boxShadow: 'var(--fa-shadow-md)', + '& .MuiMenuItem-root': { + py: 1.5, + px: 2, + minHeight: 44, + fontSize: '0.9375rem', + fontFamily: 'var(--fa-font-family-body)', + whiteSpace: 'normal' as const, + '&:hover': { bgcolor: 'var(--fa-color-surface-warm)' }, + '&.Mui-selected': { + bgcolor: 'var(--fa-color-surface-warm)', + fontWeight: 600, + '&:hover': { bgcolor: 'var(--fa-color-surface-warm)' }, + }, + }, + }, + }, +}; + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Hero search widget v3 — clean vertical form with status cards. + * + * Two tappable status cards (Immediate Need / Pre-planning), a funeral type + * dropdown, a location input, and a CTA. Glassmorphism container with warm + * tonal field backgrounds. Overline section labels. CTA is always active — + * clicking it with missing required fields scrolls to the first gap. + * + * Required fields: status + location (min 3 chars). + * Funeral type defaults to "show all" if not selected. + */ +export const FuneralFinderV3 = React.forwardRef< + HTMLDivElement, + FuneralFinderV3Props +>((props, ref) => { + const { + onSearch, + loading = false, + heading = 'Find funeral directors near you', + subheading = + "Tell us what you need and we\u2019ll show options in your area.", + sx, + } = props; + + // ─── IDs for aria-labelledby ────────────────────────────── + const id = React.useId(); + const statusLabelId = `${id}-status`; + const funeralTypeLabelId = `${id}-funeral-type`; + const locationLabelId = `${id}-location`; + + // ─── State ─────────────────────────────────────────────── + const [status, setStatus] = React.useState(''); + const [funeralType, setFuneralType] = React.useState(''); + const [location, setLocation] = React.useState(''); + const [errors, setErrors] = React.useState<{ + status?: boolean; + location?: boolean; + }>({}); + + // ─── Refs ──────────────────────────────────────────────── + const statusSectionRef = React.useRef(null); + const locationSectionRef = React.useRef(null); + const locationInputRef = React.useRef(null); + const cardRefs = React.useRef<(HTMLButtonElement | null)[]>([null, null]); + + // ─── Clear errors as fields are filled ─────────────────── + const prevStatus = React.useRef(status); + React.useEffect(() => { + if (status !== prevStatus.current) { + prevStatus.current = status; + if (status && errors.status) { + setErrors((prev) => ({ ...prev, status: false })); + } + } + }, [status, errors.status]); + + const prevLocation = React.useRef(location); + React.useEffect(() => { + if (location !== prevLocation.current) { + prevLocation.current = location; + if (location.trim().length >= 3 && errors.location) { + setErrors((prev) => ({ ...prev, location: false })); + } + } + }, [location, errors.location]); + + // ─── Radiogroup keyboard nav (WAI-ARIA pattern) ────────── + const activeStatusIndex = status + ? STATUS_OPTIONS.findIndex((o) => o.key === status) + : 0; + + const handleStatusKeyDown = (e: React.KeyboardEvent) => { + const isNext = e.key === 'ArrowRight' || e.key === 'ArrowDown'; + const isPrev = e.key === 'ArrowLeft' || e.key === 'ArrowUp'; + if (!isNext && !isPrev) return; + e.preventDefault(); + const current = cardRefs.current.indexOf( + e.target as HTMLButtonElement, + ); + if (current === -1) return; + const next = isNext + ? Math.min(current + 1, STATUS_OPTIONS.length - 1) + : Math.max(current - 1, 0); + if (next !== current) { + cardRefs.current[next]?.focus(); + setStatus(STATUS_OPTIONS[next].key); + } + }; + + // ─── Handlers ──────────────────────────────────────────── + const handleFuneralType = (e: SelectChangeEvent) => { + setFuneralType(e.target.value as FuneralType); + }; + + const handleSubmit = () => { + if (!status) { + setErrors({ status: true }); + statusSectionRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + return; + } + if (location.trim().length < 3) { + setErrors({ location: true }); + locationSectionRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + locationInputRef.current?.focus(); + return; + } + setErrors({}); + onSearch?.({ + status, + funeralType: funeralType || 'show-all', + location: location.trim(), + }); + }; + + // ─── Render ────────────────────────────────────────────── + return ( + + {/* ── Header ──────────────────────────────────────────── */} + + + {heading} + + + {subheading} + + + + + + {/* ── Current Status ──────────────────────────────────── */} + + Current Status + + {STATUS_OPTIONS.map((opt, i) => ( + { + 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} + /> + ))} + + + {errors.status && ( + + Please select your current situation + + )} + + + + {/* ── Funeral Type ────────────────────────────────────── */} + + Funeral Type + + + + + + {/* ── Location ────────────────────────────────────────── */} + + Location + + setLocation(e.target.value)} + placeholder="Enter suburb or postcode" + inputRef={locationInputRef} + startAdornment={ + + + + } + 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, + }} + /> + + + {errors.location && ( + + Please enter a suburb or postcode + + )} + + + + {/* ── CTA ─────────────────────────────────────────────── */} + + + + Free to use · No obligation + + + + ); +}); + +FuneralFinderV3.displayName = 'FuneralFinderV3'; +export default FuneralFinderV3; diff --git a/src/components/organisms/FuneralFinder/index.ts b/src/components/organisms/FuneralFinder/index.ts index 0dc7c6e..1ca7b8c 100644 --- a/src/components/organisms/FuneralFinder/index.ts +++ b/src/components/organisms/FuneralFinder/index.ts @@ -10,3 +10,9 @@ export { type FuneralFinderV2Props, type FuneralFinderV2SearchParams, } from './FuneralFinderV2'; + +export { + FuneralFinderV3, + type FuneralFinderV3Props, + type FuneralFinderV3SearchParams, +} from './FuneralFinderV3';