diff --git a/docs/memory/component-registry.md b/docs/memory/component-registry.md index 9f46a7d..ff8bfec 100644 --- a/docs/memory/component-registry.md +++ b/docs/memory/component-registry.md @@ -49,11 +49,11 @@ duplicates) and MUST update it after completing one. | Component | Status | Composed of | Notes | |-----------|--------|-------------|-------| -| ServiceSelector | review | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. | +| ServiceSelector | done | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. | | PricingTable | planned | PriceCard × n + Typography | Comparative pricing display | -| ArrangementForm | planned | FormField × n + StepIndicator + Button | Multi-step arrangement flow | +| ArrangementForm | review | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Controlled by parent (currentStep/onNext/onBack). Each step renders arbitrary content with consistent nav buttons. canContinue per step. Back/Continue/Complete labels configurable. | | 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 | review | 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). | +| 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). | ## Future enhancements diff --git a/src/components/organisms/ArrangementForm/ArrangementForm.stories.tsx b/src/components/organisms/ArrangementForm/ArrangementForm.stories.tsx new file mode 100644 index 0000000..62719a9 --- /dev/null +++ b/src/components/organisms/ArrangementForm/ArrangementForm.stories.tsx @@ -0,0 +1,380 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { ArrangementForm } from './ArrangementForm'; +import { ServiceSelector } from '../ServiceSelector'; +import { AddOnOption } from '../../molecules/AddOnOption'; +import { Typography } from '../../atoms/Typography'; +import { Navigation } from '../Navigation'; + +// ─── Shared data ───────────────────────────────────────────────────────────── + +const serviceTypes = [ + { id: 'burial', name: 'Traditional Burial', price: 4200, description: 'Full service with chapel ceremony, viewing, hearse, and graveside committal.' }, + { id: 'cremation', name: 'Cremation with Service', price: 2800, description: 'Chapel ceremony followed by cremation. Ashes returned in a standard urn.' }, + { id: 'direct-cremation', name: 'Direct Cremation', price: 1600, description: 'Simple cremation without a formal service. Ashes returned within 5 business days.' }, +]; + +const coffinOptions = [ + { id: 'eco', name: 'Eco Willow', price: 850, description: 'Handwoven natural willow. Biodegradable and sustainable.' }, + { id: 'classic', name: 'Classic Maple', price: 1400, description: 'Solid maple with satin finish and brass handles.' }, + { id: 'premium', name: 'Premium Oak', price: 2200, description: 'Quarter-sawn oak with high-gloss lacquer and gold-plated handles.' }, +]; + +const addOns = [ + { id: 'flowers', name: 'Floral Arrangements', price: 450, description: 'Seasonal flowers for the chapel and casket spray.' }, + { id: 'video', name: 'Memorial Video', price: 350, description: 'Professional video tribute with photos and music, played during the service.' }, + { id: 'catering', name: 'Wake Catering', price: 800, description: 'Light refreshments for up to 50 guests after the service.' }, + { id: 'transport', name: 'Family Limousine', price: 300, description: 'Luxury vehicle for immediate family to and from the service.' }, + { id: 'death-notice', name: 'Newspaper Death Notice', price: 180, description: 'Published in one major metropolitan newspaper of your choice.' }, +]; + +const FALogoNav = () => ( + +); + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Organisms/ArrangementForm', + component: ArrangementForm, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// --- Default (Static) -------------------------------------------------------- + +/** Static view at step 1 — shows structure without interactivity */ +export const Default: Story = { + args: { + heading: 'Plan your arrangement', + subheading: 'We\'ll guide you through each step. You can go back and change your selections at any time.', + steps: [ + { label: 'Service', content: Service selection content }, + { label: 'Coffin', content: Coffin selection content }, + { label: 'Extras', content: Optional extras content }, + { label: 'Review', content: Review summary content }, + ], + currentStep: 0, + }, +}; + +// --- Interactive Full Flow --------------------------------------------------- + +/** Complete 4-step arrangement flow with real components */ +export const InteractiveFlow: Story = { + render: () => { + const [step, setStep] = useState(0); + const [serviceId, setServiceId] = useState(); + const [coffinId, setCoffinId] = useState(); + const [selectedAddOns, setSelectedAddOns] = useState>({}); + + const toggleAddOn = (id: string, checked: boolean) => { + setSelectedAddOns((prev) => ({ ...prev, [id]: checked })); + }; + + const getServiceName = () => serviceTypes.find((s) => s.id === serviceId)?.name ?? '—'; + const getServicePrice = () => serviceTypes.find((s) => s.id === serviceId)?.price ?? 0; + const getCoffinName = () => coffinOptions.find((c) => c.id === coffinId)?.name ?? '—'; + const getCoffinPrice = () => coffinOptions.find((c) => c.id === coffinId)?.price ?? 0; + const getAddOnTotal = () => + addOns.filter((a) => selectedAddOns[a.id]).reduce((sum, a) => sum + a.price, 0); + const getTotal = () => getServicePrice() + getCoffinPrice() + getAddOnTotal(); + + const steps = [ + { + label: 'Service', + canContinue: !!serviceId, + content: ( + + ), + }, + { + label: 'Coffin', + canContinue: !!coffinId, + content: ( + + ), + }, + { + label: 'Extras', + canContinue: true, + content: ( + + + Optional extras + + + Add any optional services. You can skip this step if none are needed. + + + {addOns.map((addOn) => ( + toggleAddOn(addOn.id, checked)} + maxDescriptionLines={1} + /> + ))} + + + ), + }, + { + label: 'Review', + canContinue: true, + content: ( + + + Review your arrangement + + + {/* Service */} + + + Service + + + {getServiceName()} + + ${getServicePrice().toLocaleString('en-AU')} + + + + + {/* Coffin */} + + + Coffin + + + {getCoffinName()} + + ${getCoffinPrice().toLocaleString('en-AU')} + + + + + {/* Extras */} + {addOns.filter((a) => selectedAddOns[a.id]).length > 0 && ( + + + Extras + + {addOns + .filter((a) => selectedAddOns[a.id]) + .map((addOn) => ( + + {addOn.name} + + ${addOn.price.toLocaleString('en-AU')} + + + ))} + + )} + + {/* Total */} + + Estimated total + + ${getTotal().toLocaleString('en-AU')} + + + + + This is an estimate only. Final pricing will be confirmed by your chosen funeral director. + + + ), + }, + ]; + + return ( + setStep((s) => Math.min(s + 1, steps.length - 1))} + onBack={() => setStep((s) => Math.max(s - 1, 0))} + onComplete={() => alert(`Arrangement complete! Total: $${getTotal().toLocaleString('en-AU')}`)} + /> + ); + }, +}; + +// --- Mid-Flow ---------------------------------------------------------------- + +/** Starts at step 2 with prior selections visible */ +export const MidFlow: Story = { + args: { + steps: [ + { label: 'Service', content: Completed }, + { label: 'Coffin', content: Completed }, + { + label: 'Extras', + content: ( + + + Optional extras + + + Add any optional services you'd like. + + + {addOns.slice(0, 3).map((addOn) => ( + + ))} + + + ), + }, + { label: 'Review', content: Review step }, + ], + currentStep: 2, + }, +}; + +// --- Two Steps --------------------------------------------------------------- + +/** Simplified 2-step flow for simpler arrangements */ +export const TwoSteps: Story = { + render: () => { + const [step, setStep] = useState(0); + const [serviceId, setServiceId] = useState(); + + return ( + + ), + }, + { + label: 'Review', + content: ( + + + Confirm your selection + + + {serviceTypes.find((s) => s.id === serviceId)?.name ?? 'Nothing selected'} + + + ), + }, + ]} + currentStep={step} + onNext={() => setStep(1)} + onBack={() => setStep(0)} + onComplete={() => alert('Complete!')} + completeLabel="Confirm arrangement" + /> + ); + }, +}; + +// --- In Page Context --------------------------------------------------------- + +/** Full page with Navigation wrapping the arrangement flow */ +export const InPageContext: Story = { + decorators: [ + (Story) => ( + + + + ), + ], + render: () => { + const [step, setStep] = useState(0); + const [serviceId, setServiceId] = useState(); + + return ( + + } + items={[ + { label: 'FAQ', href: '/faq' }, + { label: 'Contact Us', href: '/contact' }, + ]} + /> + + + ), + }, + { label: 'Coffin', content: Coffin step... }, + { label: 'Extras', content: Extras step... }, + { label: 'Review', content: Review step... }, + ]} + currentStep={step} + onNext={() => setStep((s) => s + 1)} + onBack={() => setStep((s) => s - 1)} + onComplete={() => alert('Done!')} + /> + + + ); + }, +}; diff --git a/src/components/organisms/ArrangementForm/ArrangementForm.tsx b/src/components/organisms/ArrangementForm/ArrangementForm.tsx new file mode 100644 index 0000000..7969ef8 --- /dev/null +++ b/src/components/organisms/ArrangementForm/ArrangementForm.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { StepIndicator } from '../../molecules/StepIndicator'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** A single step in the arrangement flow */ +export interface ArrangementStep { + /** Step label shown in the StepIndicator */ + label: string; + /** Step content — rendered when this step is active */ + content: React.ReactNode; + /** Whether the user can proceed past this step. Defaults to true. */ + canContinue?: boolean; +} + +/** Props for the FA ArrangementForm organism */ +export interface ArrangementFormProps { + /** The steps in the arrangement flow */ + steps: ArrangementStep[]; + /** Current step index (0-based). Controlled by parent. */ + currentStep: number; + /** Called when the user advances to the next step */ + onNext?: () => void; + /** Called when the user goes back to the previous step */ + onBack?: () => void; + /** Called when the user completes the final step */ + onComplete?: () => void; + /** Label for the next button — defaults to "Continue" */ + nextLabel?: string; + /** Label for the back button — defaults to "Back" */ + backLabel?: string; + /** Label for the final step's button — defaults to "Review arrangement" */ + completeLabel?: string; + /** Optional heading above the step content */ + heading?: string; + /** Optional subheading below the heading */ + subheading?: string; + /** MUI sx prop for the root element */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Multi-step arrangement form for the FA design system. + * + * The core planning flow — guides users through service selection, + * coffin choice, venue, extras, and review. Each step renders + * arbitrary content (ServiceSelector, AddOnOption lists, forms, etc.) + * with consistent navigation and progress indication. + * + * Composes StepIndicator + Typography + Button + step content. + * + * State is controlled by the parent — the form doesn't own step + * state or selection state. This keeps it composable and testable. + * + * Usage: + * ```tsx + * , canContinue: !!selected }, + * { label: 'Coffin', content: }, + * { label: 'Extras', content: }, + * { label: 'Review', content: }, + * ]} + * currentStep={step} + * onNext={() => setStep(s => s + 1)} + * onBack={() => setStep(s => s - 1)} + * onComplete={() => submit()} + * /> + * ``` + */ +export const ArrangementForm = React.forwardRef( + ( + { + steps, + currentStep, + onNext, + onBack, + onComplete, + nextLabel = 'Continue', + backLabel = 'Back', + completeLabel = 'Review arrangement', + heading, + subheading, + sx, + }, + ref, + ) => { + const isFirstStep = currentStep === 0; + const isLastStep = currentStep === steps.length - 1; + const activeStep = steps[currentStep]; + const canContinue = activeStep?.canContinue ?? true; + + const stepLabels = steps.map((s) => ({ label: s.label })); + + return ( + + {/* Progress indicator */} + + + {/* Optional heading */} + {heading && ( + + + {heading} + + {subheading && ( + + {subheading} + + )} + + )} + + {/* Step content */} + + {activeStep?.content} + + + {/* Navigation buttons */} + + {!isFirstStep ? ( + + ) : ( + + )} + + + + + ); + }, +); + +ArrangementForm.displayName = 'ArrangementForm'; +export default ArrangementForm; diff --git a/src/components/organisms/ArrangementForm/index.ts b/src/components/organisms/ArrangementForm/index.ts new file mode 100644 index 0000000..2390bfe --- /dev/null +++ b/src/components/organisms/ArrangementForm/index.ts @@ -0,0 +1 @@ +export { ArrangementForm, type ArrangementFormProps, type ArrangementStep } from './ArrangementForm';