Fix layout variants for VenueStep, CoffinsStep, CoffinDetailsStep

- VenueStep: centered-form → list-map (venue cards left, map slot right)
  Matches ProvidersStep pattern with vertical card stack + map placeholder
- CoffinsStep: centered-form → grid-sidebar (filter sidebar left, card grid right)
  Filters now in dedicated sidebar, cards fill the wider main area
- CoffinDetailsStep: centered-form → detail-toggles (profile left, options right)
  Coffin image + specs on left, option RadioGroups on right

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 15:16:01 +11:00
parent a6524a82fe
commit 1e91929411
3 changed files with 311 additions and 301 deletions

View File

@@ -188,106 +188,87 @@ export const CoffinDetailsStep: React.FC<CoffinDetailsStepProps> = ({
hideHelpBar, hideHelpBar,
sx, sx,
}) => { }) => {
return ( // ─── Left panel: Coffin profile (image + specs) ───
<WizardLayout const profilePanel = (
variant="centered-form" <Box>
navigation={navigation} {/* Image */}
progressStepper={progressStepper} <Box
runningTotal={runningTotal} role="img"
showBackLink={!!onBack} aria-label={`Photo of ${coffin.name}`}
backLabel="Back" sx={{
onBack={onBack} width: '100%',
hideHelpBar={hideHelpBar} height: { xs: 240, md: 320 },
sx={sx} borderRadius: 2,
> backgroundImage: `url(${coffin.imageUrl})`,
{/* Page heading */} backgroundSize: 'cover',
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}> backgroundPosition: 'center',
backgroundColor: 'var(--fa-color-surface-subtle)',
mb: 3,
}}
/>
<Typography variant="h4" sx={{ mb: 1 }}>
{coffin.name}
</Typography>
{coffin.description && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{coffin.description}
</Typography>
)}
{/* Specs */}
{coffin.specs && coffin.specs.length > 0 && (
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gap: 0.5,
columnGap: 2,
mb: 2,
}}
>
{coffin.specs.map((spec) => (
<React.Fragment key={spec.label}>
<Typography variant="labelSm" color="text.secondary">
{spec.label}
</Typography>
<Typography variant="body2">{spec.value}</Typography>
</React.Fragment>
))}
</Box>
)}
{/* Price */}
<Typography variant="h5" color="primary">
${coffin.price.toLocaleString('en-AU')}
</Typography>
{coffin.priceNote && (
<Typography variant="body2" color="text.secondary">
{coffin.priceNote}
</Typography>
)}
</Box>
);
// ─── Right panel: Option selectors + CTAs ───
const optionsPanel = (
<Box>
<Typography variant="h4" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
Coffin details Coffin details
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{isPrePlanning {isPrePlanning
? 'These options let you personalise the coffin. You can change these later.' ? 'These options let you personalise the coffin. You can change these later.'
: 'Personalise your chosen coffin with handles, lining, and a name plate.'} : 'Personalise your chosen coffin with handles, lining, and a name plate.'}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}> <Typography variant="caption" color="text.secondary" sx={{ mb: 3, display: 'block' }}>
Each option shows the price impact on your plan total. Options within your package allowance Each option shows the price impact on your plan total. Options within your package allowance
are included at no extra cost. are included at no extra cost.
</Typography> </Typography>
{/* ─── Coffin profile ─── */}
<Paper variant="outlined" sx={{ p: 3, mb: 4, overflow: 'hidden' }}>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
gap: 3,
}}
>
{/* Image */}
<Box
role="img"
aria-label={`Photo of ${coffin.name}`}
sx={{
width: { xs: '100%', sm: 240 },
height: { xs: 200, sm: 180 },
flexShrink: 0,
borderRadius: 1,
backgroundImage: `url(${coffin.imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundColor: 'var(--fa-color-surface-subtle)',
}}
/>
{/* Details */}
<Box sx={{ flex: 1 }}>
<Typography variant="h4" sx={{ mb: 1 }}>
{coffin.name}
</Typography>
{coffin.description && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{coffin.description}
</Typography>
)}
{/* Specs */}
{coffin.specs && coffin.specs.length > 0 && (
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gap: 0.5,
columnGap: 2,
mb: 2,
}}
>
{coffin.specs.map((spec) => (
<React.Fragment key={spec.label}>
<Typography variant="labelSm" color="text.secondary">
{spec.label}
</Typography>
<Typography variant="body2">{spec.value}</Typography>
</React.Fragment>
))}
</Box>
)}
{/* Price */}
<Typography variant="h5" color="primary">
${coffin.price.toLocaleString('en-AU')}
</Typography>
{coffin.priceNote && (
<Typography variant="body2" color="text.secondary">
{coffin.priceNote}
</Typography>
)}
</Box>
</Box>
</Paper>
<Box <Box
component="form" component="form"
noValidate noValidate
@@ -296,7 +277,6 @@ export const CoffinDetailsStep: React.FC<CoffinDetailsStepProps> = ({
onContinue(); onContinue();
}} }}
> >
{/* ─── Option sections ─── */}
<OptionSection <OptionSection
legend="Handle style" legend="Handle style"
options={handleOptions} options={handleOptions}
@@ -342,6 +322,23 @@ export const CoffinDetailsStep: React.FC<CoffinDetailsStepProps> = ({
</Button> </Button>
</Box> </Box>
</Box> </Box>
</Box>
);
return (
<WizardLayout
variant="detail-toggles"
navigation={navigation}
progressStepper={progressStepper}
runningTotal={runningTotal}
showBackLink={!!onBack}
backLabel="Back"
onBack={onBack}
hideHelpBar={hideHelpBar}
sx={sx}
secondaryPanel={optionsPanel}
>
{profilePanel}
</WizardLayout> </WizardLayout>
); );
}; };

View File

@@ -147,9 +147,202 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
onChange({ ...values, [field]: value, page: 1 }); onChange({ ...values, [field]: value, page: 1 });
}; };
// ─── Sidebar content (filters) ───
const sidebar = (
<Box sx={{ py: { xs: 0, md: 2 } }}>
<Typography variant="h5" sx={{ mb: 2, display: { xs: 'none', md: 'block' } }}>
Filters
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
select
label="Categories"
value={values.categoryFilter}
onChange={(e) => handleFilterChange('categoryFilter', e.target.value)}
fullWidth
>
{categories.map((cat) => (
<MenuItem key={cat.value} value={cat.value}>
{cat.label}
</MenuItem>
))}
</TextField>
<TextField
select
label="Price range"
value={values.priceFilter}
onChange={(e) => handleFilterChange('priceFilter', e.target.value)}
fullWidth
>
{priceRanges.map((range) => (
<MenuItem key={range.value} value={range.value}>
{range.label}
</MenuItem>
))}
</TextField>
</Box>
</Box>
);
// ─── Main content (card grid) ───
const mainContent = (
<Box
component="form"
noValidate
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
onContinue();
}}
>
{/* Page heading */}
<Typography variant="h4" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
Choose a coffin
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{isPrePlanning
? 'Browse the range to get an idea of styles and pricing. You can change your selection later.'
: 'Browse the range available with your selected provider. Use the filters to narrow your options.'}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mb: 3, display: 'block' }}>
Selecting a coffin within your package allowance won&apos;t change your total. Coffins
outside the allowance will adjust the price.
</Typography>
{/* Results count */}
<Typography
variant="caption"
color="text.secondary"
sx={{ mb: 2, display: 'block' }}
aria-live="polite"
>
Showing {displayCount} coffin{displayCount !== 1 ? 's' : ''}
</Typography>
{/* Coffin card grid */}
<Box
role="radiogroup"
aria-label="Available coffins"
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
lg: 'repeat(3, 1fr)',
},
gap: 2,
mb: 3,
}}
>
{coffins.map((coffin, index) => (
<Card
key={coffin.id}
interactive
selected={coffin.id === values.selectedCoffinId}
padding="none"
onClick={() => onChange({ ...values, selectedCoffinId: coffin.id })}
role="radio"
aria-checked={coffin.id === values.selectedCoffinId}
tabIndex={
values.selectedCoffinId === null
? index === 0
? 0
: -1
: coffin.id === values.selectedCoffinId
? 0
: -1
}
sx={{ overflow: 'hidden' }}
>
{/* Image */}
<Box
sx={{
position: 'relative',
height: 180,
backgroundImage: `url(${coffin.imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundColor: 'var(--fa-color-surface-subtle)',
}}
role="img"
aria-label={`Photo of ${coffin.name}`}
>
{coffin.isPopular && (
<Box sx={{ position: 'absolute', top: 8, left: 8 }}>
<Badge variant="soft" color="brand" aria-label="Most popular choice">
Most Popular
</Badge>
</Box>
)}
</Box>
{/* Content */}
<Box sx={{ p: 2 }}>
<Typography variant="h6" maxLines={2} sx={{ mb: 0.5 }}>
{coffin.name}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{coffin.category}
</Typography>
<Typography variant="h6" color="primary">
${coffin.price.toLocaleString('en-AU')}
</Typography>
</Box>
</Card>
))}
</Box>
{/* Validation error */}
{errors?.selectedCoffinId && (
<Typography variant="body2" color="error" sx={{ mb: 2 }} role="alert">
{errors.selectedCoffinId}
</Typography>
)}
{/* Pagination */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}>
<Pagination
count={totalPages}
page={values.page}
onChange={(_, page) => onChange({ ...values, page })}
color="primary"
/>
</Box>
)}
<Divider sx={{ my: 3 }} />
{/* CTAs */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: { xs: 'column-reverse', sm: 'row' },
gap: 2,
}}
>
{onSaveAndExit ? (
<Button variant="text" color="secondary" onClick={onSaveAndExit} type="button">
Save and continue later
</Button>
) : (
<Box />
)}
<Button type="submit" variant="contained" size="large" loading={loading}>
Continue
</Button>
</Box>
</Box>
);
return ( return (
<WizardLayout <WizardLayout
variant="centered-form" variant="grid-sidebar"
navigation={navigation} navigation={navigation}
progressStepper={progressStepper} progressStepper={progressStepper}
runningTotal={runningTotal} runningTotal={runningTotal}
@@ -158,190 +351,9 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
onBack={onBack} onBack={onBack}
hideHelpBar={hideHelpBar} hideHelpBar={hideHelpBar}
sx={sx} sx={sx}
secondaryPanel={mainContent}
> >
{/* Page heading */} {sidebar}
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
Choose a coffin
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
{isPrePlanning
? 'Browse the range to get an idea of styles and pricing. You can change your selection later.'
: 'Browse the range available with your selected provider. Use the filters to narrow your options.'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
Selecting a coffin within your package allowance won&apos;t change your total. Coffins
outside the allowance will adjust the price.
</Typography>
<Box
component="form"
noValidate
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
onContinue();
}}
>
{/* ─── Filters ─── */}
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
gap: 2,
mb: 3,
}}
>
<TextField
select
label="Categories"
value={values.categoryFilter}
onChange={(e) => handleFilterChange('categoryFilter', e.target.value)}
sx={{ minWidth: 200 }}
>
{categories.map((cat) => (
<MenuItem key={cat.value} value={cat.value}>
{cat.label}
</MenuItem>
))}
</TextField>
<TextField
select
label="Price range"
value={values.priceFilter}
onChange={(e) => handleFilterChange('priceFilter', e.target.value)}
sx={{ minWidth: 200 }}
>
{priceRanges.map((range) => (
<MenuItem key={range.value} value={range.value}>
{range.label}
</MenuItem>
))}
</TextField>
</Box>
{/* ─── Results count ─── */}
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }} aria-live="polite">
Showing {displayCount} coffin{displayCount !== 1 ? 's' : ''}
</Typography>
{/* ─── Coffin card grid ─── */}
<Box
role="radiogroup"
aria-label="Available coffins"
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
},
gap: 2,
mb: 3,
}}
>
{coffins.map((coffin, index) => (
<Card
key={coffin.id}
interactive
selected={coffin.id === values.selectedCoffinId}
padding="none"
onClick={() => onChange({ ...values, selectedCoffinId: coffin.id })}
role="radio"
aria-checked={coffin.id === values.selectedCoffinId}
tabIndex={
values.selectedCoffinId === null
? index === 0
? 0
: -1
: coffin.id === values.selectedCoffinId
? 0
: -1
}
sx={{ overflow: 'hidden' }}
>
{/* Image */}
<Box
sx={{
position: 'relative',
height: 180,
backgroundImage: `url(${coffin.imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundColor: 'var(--fa-color-surface-subtle)',
}}
role="img"
aria-label={`Photo of ${coffin.name}`}
>
{coffin.isPopular && (
<Box sx={{ position: 'absolute', top: 8, left: 8 }}>
<Badge variant="soft" color="brand" aria-label="Most popular choice">
Most Popular
</Badge>
</Box>
)}
</Box>
{/* Content */}
<Box sx={{ p: 2 }}>
<Typography variant="h6" maxLines={2} sx={{ mb: 0.5 }}>
{coffin.name}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{coffin.category}
</Typography>
<Typography variant="h6" color="primary">
${coffin.price.toLocaleString('en-AU')}
</Typography>
</Box>
</Card>
))}
</Box>
{/* Validation error */}
{errors?.selectedCoffinId && (
<Typography variant="body2" color="error" sx={{ mb: 2 }} role="alert">
{errors.selectedCoffinId}
</Typography>
)}
{/* Pagination */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}>
<Pagination
count={totalPages}
page={values.page}
onChange={(_, page) => onChange({ ...values, page })}
color="primary"
/>
</Box>
)}
<Divider sx={{ my: 3 }} />
{/* CTAs */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: { xs: 'column-reverse', sm: 'row' },
gap: 2,
}}
>
{onSaveAndExit ? (
<Button variant="text" color="secondary" onClick={onSaveAndExit} type="button">
Save and continue later
</Button>
) : (
<Box />
)}
<Button type="submit" variant="contained" size="large" loading={loading}>
Continue
</Button>
</Box>
</Box>
</WizardLayout> </WizardLayout>
); );
}; };

View File

@@ -13,7 +13,6 @@ import { Collapse } from '../../atoms/Collapse';
import { Chip } from '../../atoms/Chip'; import { Chip } from '../../atoms/Chip';
import { Typography } from '../../atoms/Typography'; import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button'; import { Button } from '../../atoms/Button';
import { Divider } from '../../atoms/Divider';
// ─── Types ─────────────────────────────────────────────────────────────────── // ─── Types ───────────────────────────────────────────────────────────────────
@@ -86,6 +85,8 @@ export interface VenueStepProps {
locationName?: string; locationName?: string;
/** Whether this is a pre-planning flow */ /** Whether this is a pre-planning flow */
isPrePlanning?: boolean; isPrePlanning?: boolean;
/** Map panel content — slot for map integration */
mapPanel?: React.ReactNode;
/** Navigation bar — passed through to WizardLayout */ /** Navigation bar — passed through to WizardLayout */
navigation?: React.ReactNode; navigation?: React.ReactNode;
/** Progress stepper — passed through to WizardLayout */ /** Progress stepper — passed through to WizardLayout */
@@ -131,6 +132,7 @@ export const VenueStep: React.FC<VenueStepProps> = ({
], ],
locationName, locationName,
isPrePlanning = false, isPrePlanning = false,
mapPanel,
navigation, navigation,
progressStepper, progressStepper,
runningTotal, runningTotal,
@@ -162,7 +164,7 @@ export const VenueStep: React.FC<VenueStepProps> = ({
return ( return (
<WizardLayout <WizardLayout
variant="centered-form" variant="list-map"
navigation={navigation} navigation={navigation}
progressStepper={progressStepper} progressStepper={progressStepper}
runningTotal={runningTotal} runningTotal={runningTotal}
@@ -171,13 +173,32 @@ export const VenueStep: React.FC<VenueStepProps> = ({
onBack={onBack} onBack={onBack}
hideHelpBar={hideHelpBar} hideHelpBar={hideHelpBar}
sx={sx} sx={sx}
secondaryPanel={
mapPanel || (
<Box
sx={{
bgcolor: 'var(--fa-color-sage-50)',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderLeft: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="body1" color="text.secondary">
Map coming soon
</Typography>
</Box>
)
}
> >
{/* Page heading */} {/* Page heading */}
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}> <Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
Where would you like the service? Where would you like the service?
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{isPrePlanning {isPrePlanning
? 'Browse available venues. Your choice can be changed later.' ? 'Browse available venues. Your choice can be changed later.'
: 'Choose a venue for the funeral service. You can filter by location, features, and religion.'} : 'Choose a venue for the funeral service. You can filter by location, features, and religion.'}
@@ -230,15 +251,7 @@ export const VenueStep: React.FC<VenueStepProps> = ({
<Box <Box
role="radiogroup" role="radiogroup"
aria-label="Available venues" aria-label="Available venues"
sx={{ sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
},
gap: 2,
mb: 3,
}}
> >
{venues.map((venue, index) => ( {venues.map((venue, index) => (
<VenueCard <VenueCard
@@ -377,24 +390,12 @@ export const VenueStep: React.FC<VenueStepProps> = ({
</Box> </Box>
</Collapse> </Collapse>
<Divider sx={{ my: 3 }} />
{/* CTAs */} {/* CTAs */}
<Box <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, pb: 2 }}>
sx={{ {onSaveAndExit && (
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: { xs: 'column-reverse', sm: 'row' },
gap: 2,
}}
>
{onSaveAndExit ? (
<Button variant="text" color="secondary" onClick={onSaveAndExit} type="button"> <Button variant="text" color="secondary" onClick={onSaveAndExit} type="button">
Save and continue later Save and exit
</Button> </Button>
) : (
<Box />
)} )}
<Button type="submit" variant="contained" size="large" loading={loading}> <Button type="submit" variant="contained" size="large" loading={loading}>
Continue Continue