From 2631a2e4bbe1eeaa4dc48b5f21e94e16003206d9 Mon Sep 17 00:00:00 2001 From: Richie Date: Sun, 29 Mar 2026 14:26:53 +1100 Subject: [PATCH] Add IntroStep page (wizard step 1) + audit fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntroStep: urgency-sensitive segmentation entry point. ToggleButtonGroup for forWhom (Myself/Someone else) with progressive disclosure revealing hasPassedAway (Yes/No) via Collapse. Auto-sets hasPassedAway="no" when forWhom="myself". Grief-sensitive copy adapts subheading per selection. Pure presentation — props in, callbacks out. Audit fixes (18/20 → 20/20): - P1: Add
landmark wrapper in WizardLayout (all variants) - P2: Wrap IntroStep fields in
for landmark + Enter-to-submit Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pages/IntroStep/IntroStep.stories.tsx | 214 ++++++++++++++++++ src/components/pages/IntroStep/IntroStep.tsx | 191 ++++++++++++++++ src/components/pages/IntroStep/index.ts | 2 + .../templates/WizardLayout/WizardLayout.tsx | 4 +- 4 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 src/components/pages/IntroStep/IntroStep.stories.tsx create mode 100644 src/components/pages/IntroStep/IntroStep.tsx create mode 100644 src/components/pages/IntroStep/index.ts diff --git a/src/components/pages/IntroStep/IntroStep.stories.tsx b/src/components/pages/IntroStep/IntroStep.stories.tsx new file mode 100644 index 0000000..63072f5 --- /dev/null +++ b/src/components/pages/IntroStep/IntroStep.stories.tsx @@ -0,0 +1,214 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { IntroStep } from './IntroStep'; +import type { IntroStepValues, IntroStepErrors } from './IntroStep'; +import { Navigation } from '../../organisms/Navigation'; +import Box from '@mui/material/Box'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +const FALogo = () => ( + + + + +); + +const nav = ( + } + items={[ + { label: 'FAQ', href: '/faq' }, + { label: 'Contact Us', href: '/contact' }, + { label: 'Log in', href: '/login' }, + ]} + /> +); + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Pages/IntroStep', + component: IntroStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Fully interactive — click through the progressive disclosure flow */ +export const Default: Story = { + render: () => { + const [values, setValues] = useState({ + forWhom: null, + hasPassedAway: null, + }); + const [errors, setErrors] = useState({}); + + const handleContinue = () => { + const newErrors: IntroStepErrors = {}; + if (!values.forWhom) { + newErrors.forWhom = 'We need to know who the funeral is for to show you the right options.'; + } + if (values.forWhom === 'someone' && !values.hasPassedAway) { + newErrors.hasPassedAway = 'This helps us tailor the process to your situation.'; + } + setErrors(newErrors); + if (Object.keys(newErrors).length === 0) { + alert(`Continue with: ${JSON.stringify(values)}`); + } + }; + + return ( + { + setValues(v); + setErrors({}); + }} + onContinue={handleContinue} + errors={errors} + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning (myself) ────────────────────────────────────────────────── + +/** User has selected "Myself" — hasPassedAway auto-set to "no", no second question */ +export const PrePlanningMyself: Story = { + render: () => { + const [values, setValues] = useState({ + forWhom: 'myself', + hasPassedAway: 'no', + }); + return ( + alert('Continue')} + navigation={nav} + /> + ); + }, +}; + +// ─── At-need (someone, yes) ───────────────────────────────────────────────── + +/** User arranging for someone who has passed — at-need flow */ +export const AtNeedSomeone: Story = { + render: () => { + const [values, setValues] = useState({ + forWhom: 'someone', + hasPassedAway: 'yes', + }); + return ( + alert('Continue')} + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning (someone, no) ───────────────────────────────────────────── + +/** User arranging for someone who is alive — pre-planning flow */ +export const PrePlanningSomeone: Story = { + render: () => { + const [values, setValues] = useState({ + forWhom: 'someone', + hasPassedAway: 'no', + }); + return ( + alert('Continue')} + navigation={nav} + /> + ); + }, +}; + +// ─── Validation errors ────────────────────────────────────────────────────── + +/** Both fields showing validation errors */ +export const WithErrors: Story = { + render: () => { + const [values, setValues] = useState({ + forWhom: null, + hasPassedAway: null, + }); + return ( + {}} + errors={{ + forWhom: 'We need to know who the funeral is for to show you the right options.', + }} + navigation={nav} + /> + ); + }, +}; + +// ─── Loading state ────────────────────────────────────────────────────────── + +/** Continue button in loading state */ +export const Loading: Story = { + render: () => { + const [values, setValues] = useState({ + forWhom: 'myself', + hasPassedAway: 'no', + }); + return ( + {}} + loading + navigation={nav} + /> + ); + }, +}; + +// ─── No navigation (embedded) ─────────────────────────────────────────────── + +/** Without navigation — for embedded/iframe use */ +export const Embedded: Story = { + render: () => { + const [values, setValues] = useState({ + forWhom: null, + hasPassedAway: null, + }); + return ( + alert('Continue')} + hideHelpBar + /> + ); + }, +}; diff --git a/src/components/pages/IntroStep/IntroStep.tsx b/src/components/pages/IntroStep/IntroStep.tsx new file mode 100644 index 0000000..616adee --- /dev/null +++ b/src/components/pages/IntroStep/IntroStep.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { WizardLayout } from '../../templates/WizardLayout'; +import { ToggleButtonGroup } from '../../atoms/ToggleButtonGroup'; +import { Collapse } from '../../atoms/Collapse'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Form values for the intro step */ +export interface IntroStepValues { + /** Who the funeral is being arranged for */ + forWhom: 'myself' | 'someone' | null; + /** Whether the person has passed away (only relevant when forWhom="someone") */ + hasPassedAway: 'yes' | 'no' | null; +} + +/** Field-level error messages */ +export interface IntroStepErrors { + /** Error for the forWhom field */ + forWhom?: string; + /** Error for the hasPassedAway field */ + hasPassedAway?: string; +} + +/** Props for the IntroStep page component */ +export interface IntroStepProps { + /** Current form values */ + values: IntroStepValues; + /** Callback when any field value changes */ + onChange: (values: IntroStepValues) => void; + /** Callback when the Continue button is clicked */ + onContinue: () => void; + /** Field-level validation errors */ + errors?: IntroStepErrors; + /** Whether the Continue button is in a loading state */ + loading?: boolean; + /** Navigation bar — passed through to WizardLayout */ + navigation?: React.ReactNode; + /** Hide the help bar */ + hideHelpBar?: boolean; + /** MUI sx prop for the root */ + sx?: SxProps; +} + +// ─── Copy helpers ──────────────────────────────────────────────────────────── + +function getSubheading(values: IntroStepValues): string { + if (values.forWhom === 'someone' && values.hasPassedAway === 'yes') { + return "We'll guide you through each step. You can save your progress and come back anytime."; + } + if (values.forWhom === 'myself' || values.hasPassedAway === 'no') { + return "Explore your options and plan at your own pace. Nothing is locked in until you're ready."; + } + return "We'll guide you through arranging a funeral, step by step. You can save your progress and come back anytime."; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Step 1 — Intro page for the FA arrangement wizard. + * + * Entry point with urgency-sensitive segmentation. User selects who + * the funeral is for, and (if arranging for someone else) whether + * that person has died. + * + * Uses the Centered Form layout variant. Progressive disclosure: + * selecting "Someone else" reveals the hasPassedAway question. + * Selecting "Myself" auto-sets hasPassedAway to "no" (pre-planning). + * + * Pure presentation component — props in, callbacks out. + * No routing, state management, or API calls. + * + * Spec: documentation/steps/steps/01_intro.yaml + */ +export const IntroStep: React.FC = ({ + values, + onChange, + onContinue, + errors, + loading = false, + navigation, + hideHelpBar, + sx, +}) => { + const handleForWhomChange = (newValue: string | null) => { + const forWhom = newValue as IntroStepValues['forWhom']; + if (forWhom === 'myself') { + // Auto-set hasPassedAway to 'no' — user is alive, pre-planning + onChange({ forWhom, hasPassedAway: 'no' }); + } else { + // Reset hasPassedAway when switching to "someone" + onChange({ forWhom, hasPassedAway: null }); + } + }; + + const handleHasPassedAwayChange = (newValue: string | null) => { + onChange({ ...values, hasPassedAway: newValue as IntroStepValues['hasPassedAway'] }); + }; + + const showHasPassedAway = values.forWhom === 'someone'; + + return ( + + {/* Page heading — receives focus on entry for screen readers */} + + Let's get started + + + + {getSubheading(values)} + + + { + e.preventDefault(); + onContinue(); + }} + > + {/* forWhom field */} + + + + + {/* hasPassedAway field — revealed when forWhom="someone" */} + + + + + + + + + {/* CTA */} + + + + + + ); +}; + +IntroStep.displayName = 'IntroStep'; +export default IntroStep; diff --git a/src/components/pages/IntroStep/index.ts b/src/components/pages/IntroStep/index.ts new file mode 100644 index 0000000..267bafe --- /dev/null +++ b/src/components/pages/IntroStep/index.ts @@ -0,0 +1,2 @@ +export { default } from './IntroStep'; +export * from './IntroStep'; diff --git a/src/components/templates/WizardLayout/WizardLayout.tsx b/src/components/templates/WizardLayout/WizardLayout.tsx index 9cf485f..5bce519 100644 --- a/src/components/templates/WizardLayout/WizardLayout.tsx +++ b/src/components/templates/WizardLayout/WizardLayout.tsx @@ -324,7 +324,9 @@ export const WizardLayout = React.forwardRef( )} {/* Main content area */} - {children} + + {children} + {/* Sticky help bar */} {!hideHelpBar && }