Add WizardLayout template with 5 layout variants
- centered-form: single column ~600px for form steps (intro, auth, etc.) - list-map: 40/60 split for provider search (card list + map) - list-detail: 40/60 master-detail for package selection - grid-sidebar: 25/75 filter sidebar + card grid (coffins) - detail-toggles: 50/50 hero image + product info (venue/coffin details) Common elements: nav slot, sticky help bar, optional back link, optional progress stepper + running total (grid-sidebar, detail-toggles). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
424
src/components/templates/WizardLayout/WizardLayout.stories.tsx
Normal file
424
src/components/templates/WizardLayout/WizardLayout.stories.tsx
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { WizardLayout } from './WizardLayout';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
import { Navigation } from '../../organisms/Navigation';
|
||||||
|
import { StepIndicator } from '../../molecules/StepIndicator';
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const FALogo = () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-full.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-short.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nav = (
|
||||||
|
<Navigation
|
||||||
|
logo={<FALogo />}
|
||||||
|
items={[
|
||||||
|
{ label: 'FAQ', href: '/faq' },
|
||||||
|
{ label: 'Contact Us', href: '/contact' },
|
||||||
|
{ label: 'Log in', href: '/login' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const stepper = (
|
||||||
|
<StepIndicator
|
||||||
|
steps={[
|
||||||
|
{ label: 'Details' },
|
||||||
|
{ label: 'Service' },
|
||||||
|
{ label: 'Venue' },
|
||||||
|
{ label: 'Coffin' },
|
||||||
|
{ label: 'Extras' },
|
||||||
|
]}
|
||||||
|
currentStep={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const runningTotal = (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 1,
|
||||||
|
px: 2,
|
||||||
|
py: 0.75,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Your plan
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="primary.main">
|
||||||
|
$3,600
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlaceholderCard: React.FC<{ title: string; height?: number }> = ({ title, height = 100 }) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
mb: 2,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
minHeight: height,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">{title}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Placeholder content for layout demonstration.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const meta: Meta<typeof WizardLayout> = {
|
||||||
|
title: 'Templates/WizardLayout',
|
||||||
|
component: WizardLayout,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['centered-form', 'list-map', 'list-detail', 'grid-sidebar', 'detail-toggles'],
|
||||||
|
description: 'Layout variant',
|
||||||
|
table: { defaultValue: { summary: 'centered-form' } },
|
||||||
|
},
|
||||||
|
showBackLink: { control: 'boolean' },
|
||||||
|
hideHelpBar: { control: 'boolean' },
|
||||||
|
backLabel: { control: 'text' },
|
||||||
|
helpPhone: { control: 'text' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof WizardLayout>;
|
||||||
|
|
||||||
|
// ─── Centered Form ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Step 1 (Intro) style — single centered column, no back link, no progress stepper */
|
||||||
|
export const CenteredForm: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'centered-form',
|
||||||
|
navigation: nav,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<WizardLayout {...args}>
|
||||||
|
<Typography variant="h4" sx={{ mb: 1 }}>
|
||||||
|
Let's get started
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
We'll guide you through arranging a funeral, step by step.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="label" sx={{ mb: 1, display: 'block' }}>
|
||||||
|
Who is this funeral being arranged for?
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Button variant="soft" color="secondary" size="large" fullWidth>
|
||||||
|
Myself
|
||||||
|
</Button>
|
||||||
|
<Button variant="soft" color="secondary" size="large" fullWidth>
|
||||||
|
Someone else
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 4 }} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button variant="contained" size="large">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</WizardLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Centered Form with Back ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Form step with back link (e.g. date/time, payment) */
|
||||||
|
export const CenteredFormWithBack: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'centered-form',
|
||||||
|
navigation: nav,
|
||||||
|
showBackLink: true,
|
||||||
|
backLabel: 'Back',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<WizardLayout {...args}>
|
||||||
|
<Typography variant="h4" sx={{ mb: 1 }}>
|
||||||
|
Date and time
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
When would you like the service to take place?
|
||||||
|
</Typography>
|
||||||
|
<PlaceholderCard title="Date picker" height={200} />
|
||||||
|
<PlaceholderCard title="Time selector" />
|
||||||
|
<Divider sx={{ my: 4 }} />
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button variant="contained" size="large">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</WizardLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── List + Map ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Provider search — scrollable card list with map panel */
|
||||||
|
export const ListMap: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'list-map',
|
||||||
|
navigation: nav,
|
||||||
|
showBackLink: true,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<WizardLayout
|
||||||
|
{...args}
|
||||||
|
secondaryPanel={
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--fa-color-sage-200)',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Map placeholder
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||||
|
Choose a funeral provider
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Explore providers near you
|
||||||
|
</Typography>
|
||||||
|
<PlaceholderCard title="Search bar + filters" height={48} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}>
|
||||||
|
Showing results from 5 providers
|
||||||
|
</Typography>
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<PlaceholderCard key={i} title={`Provider ${i + 1}`} />
|
||||||
|
))}
|
||||||
|
</WizardLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── List + Detail ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Package selection — list left, detail panel right */
|
||||||
|
export const ListDetail: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'list-detail',
|
||||||
|
navigation: nav,
|
||||||
|
showBackLink: true,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<WizardLayout
|
||||||
|
{...args}
|
||||||
|
secondaryPanel={
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{ mb: 1 }}>
|
||||||
|
Everyday Funeral Package
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" color="primary.main" sx={{ mb: 3 }}>
|
||||||
|
$2,700
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" size="large" fullWidth sx={{ mb: 3 }}>
|
||||||
|
Select Package
|
||||||
|
</Button>
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
{['Essentials', 'Complimentary Items', 'Extras'].map((section) => (
|
||||||
|
<Box key={section} sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="label" sx={{ mb: 1, display: 'block' }}>
|
||||||
|
{section}
|
||||||
|
</Typography>
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<PlaceholderCard key={i} title={`Item ${i + 1}`} height={40} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlaceholderCard title="Provider card (compact)" height={80} />
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
|
Packages
|
||||||
|
</Typography>
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<PlaceholderCard key={i} title={`Package ${i + 1} — $${900 * (i + 1)}`} />
|
||||||
|
))}
|
||||||
|
</WizardLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Grid + Sidebar ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Coffin selection — filter sidebar + responsive card grid */
|
||||||
|
export const GridSidebar: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'grid-sidebar',
|
||||||
|
navigation: nav,
|
||||||
|
showBackLink: true,
|
||||||
|
progressStepper: stepper,
|
||||||
|
runningTotal: runningTotal,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<WizardLayout
|
||||||
|
{...args}
|
||||||
|
secondaryPanel={
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||||
|
Coffins
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Browse our selection of bespoke designer coffins.
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', md: '1fr 1fr 1fr' },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<PlaceholderCard key={i} title={`Coffin ${i + 1}`} height={160} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
|
Categories
|
||||||
|
</Typography>
|
||||||
|
{['Materials', 'Colour', 'Environmental', 'Religious'].map((cat) => (
|
||||||
|
<PlaceholderCard key={cat} title={cat} height={40} />
|
||||||
|
))}
|
||||||
|
<Typography variant="h6" sx={{ mt: 3, mb: 2 }}>
|
||||||
|
Price
|
||||||
|
</Typography>
|
||||||
|
<PlaceholderCard title="Price range slider" height={48} />
|
||||||
|
</WizardLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Detail + Toggles ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Venue/coffin detail — hero image + product info */
|
||||||
|
export const DetailToggles: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'detail-toggles',
|
||||||
|
navigation: nav,
|
||||||
|
showBackLink: true,
|
||||||
|
progressStepper: stepper,
|
||||||
|
runningTotal: runningTotal,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<WizardLayout
|
||||||
|
{...args}
|
||||||
|
secondaryPanel={
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" sx={{ mb: 0.5 }}>
|
||||||
|
West Chapel
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
Wentworth, NSW
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Capacity: 120 guests
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
|
A beautiful heritage chapel set in peaceful gardens, offering a serene setting for
|
||||||
|
farewell services.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5" color="primary.main" sx={{ mb: 2 }}>
|
||||||
|
$900
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" size="large" fullWidth>
|
||||||
|
Add to package
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--fa-color-sage-200)',
|
||||||
|
borderRadius: 2,
|
||||||
|
height: 300,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Venue image placeholder
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<Box
|
||||||
|
key={i}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--fa-color-sage-200)',
|
||||||
|
borderRadius: 1,
|
||||||
|
width: 60,
|
||||||
|
height: 48,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</WizardLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── No Navigation ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Minimal — no nav, no help bar (for embedded use) */
|
||||||
|
export const Minimal: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'centered-form',
|
||||||
|
hideHelpBar: true,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<WizardLayout {...args}>
|
||||||
|
<Typography variant="h4" sx={{ mb: 1 }}>
|
||||||
|
Embedded form
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
No navigation or help bar — for iframe or modal contexts.
|
||||||
|
</Typography>
|
||||||
|
</WizardLayout>
|
||||||
|
),
|
||||||
|
};
|
||||||
337
src/components/templates/WizardLayout/WizardLayout.tsx
Normal file
337
src/components/templates/WizardLayout/WizardLayout.tsx
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
import PhoneIcon from '@mui/icons-material/Phone';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { Link } from '../../atoms/Link';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Layout variant matching the 5 wizard page templates */
|
||||||
|
export type WizardLayoutVariant =
|
||||||
|
| 'centered-form'
|
||||||
|
| 'list-map'
|
||||||
|
| 'list-detail'
|
||||||
|
| 'grid-sidebar'
|
||||||
|
| 'detail-toggles';
|
||||||
|
|
||||||
|
/** Props for the WizardLayout template */
|
||||||
|
export interface WizardLayoutProps {
|
||||||
|
/** Which layout variant to render */
|
||||||
|
variant: WizardLayoutVariant;
|
||||||
|
/** Navigation bar — rendered at the top of the page */
|
||||||
|
navigation?: React.ReactNode;
|
||||||
|
/** Optional progress stepper — shown below nav on grid-sidebar and detail-toggles variants */
|
||||||
|
progressStepper?: React.ReactNode;
|
||||||
|
/** Optional running total widget — shown in a bar below nav (grid-sidebar, detail-toggles) */
|
||||||
|
runningTotal?: React.ReactNode;
|
||||||
|
/** Show a back link above the content area */
|
||||||
|
showBackLink?: boolean;
|
||||||
|
/** Label for the back link */
|
||||||
|
backLabel?: string;
|
||||||
|
/** Click handler for the back link */
|
||||||
|
onBack?: () => void;
|
||||||
|
/** Help bar phone number */
|
||||||
|
helpPhone?: string;
|
||||||
|
/** Hide the sticky help bar */
|
||||||
|
hideHelpBar?: boolean;
|
||||||
|
|
||||||
|
// ─── Slot content ───
|
||||||
|
|
||||||
|
/** Main content — for centered-form this is the form; for split layouts this is the primary (left) panel */
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** Secondary panel content — right panel for split layouts (map, detail, grid) */
|
||||||
|
secondaryPanel?: React.ReactNode;
|
||||||
|
|
||||||
|
/** MUI sx prop for the root container */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Help bar ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const HelpBar: React.FC<{ phone: string }> = ({ phone }) => (
|
||||||
|
<Box
|
||||||
|
component="footer"
|
||||||
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
py: 1.5,
|
||||||
|
px: { xs: 2, md: 4 },
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="text.secondary" component="span">
|
||||||
|
<PhoneIcon sx={{ fontSize: 16, verticalAlign: 'text-bottom', mr: 0.5 }} />
|
||||||
|
Need help? Call us on{' '}
|
||||||
|
<Link href={`tel:${phone.replace(/\s/g, '')}`} sx={{ fontWeight: 600 }}>
|
||||||
|
{phone}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Back link ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const BackLink: React.FC<{ label: string; onClick?: () => void }> = ({ label, onClick }) => (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
onClick={onClick}
|
||||||
|
underline="hover"
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
color: 'text.secondary',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
mb: 2,
|
||||||
|
'&:hover': { color: 'text.primary' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon sx={{ fontSize: 18 }} />
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Stepper + total bar ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const StepperBar: React.FC<{
|
||||||
|
stepper?: React.ReactNode;
|
||||||
|
total?: React.ReactNode;
|
||||||
|
}> = ({ stepper, total }) => {
|
||||||
|
if (!stepper && !total) return null;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
px: { xs: 2, md: 4 },
|
||||||
|
py: 1.5,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ flex: 1 }}>{stepper}</Box>
|
||||||
|
{total && <Box sx={{ flexShrink: 0 }}>{total}</Box>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Layout variants ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Centered Form: single column ~600px, heading + fields + CTA */
|
||||||
|
const CenteredFormLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<Container
|
||||||
|
maxWidth="sm"
|
||||||
|
sx={{
|
||||||
|
py: { xs: 6, md: 10 },
|
||||||
|
px: { xs: 4, md: 3 },
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** List + Map: ~40% scrollable list (left) / ~60% map (right) */
|
||||||
|
const ListMapLayout: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
secondaryPanel?: React.ReactNode;
|
||||||
|
}> = ({ children, secondaryPanel }) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: { xs: '100%', md: '40%' },
|
||||||
|
overflowY: 'auto',
|
||||||
|
px: { xs: 2, md: 3 },
|
||||||
|
py: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
|
width: '60%',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{secondaryPanel}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** List + Detail: ~40% selection list (left) / ~60% detail panel (right) */
|
||||||
|
const ListDetailLayout: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
secondaryPanel?: React.ReactNode;
|
||||||
|
}> = ({ children, secondaryPanel }) => (
|
||||||
|
<Container maxWidth="lg" sx={{ flex: 1, py: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: { xs: 0, md: 4 },
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: { xs: '100%', md: '40%' } }}>{children}</Box>
|
||||||
|
<Box sx={{ width: { xs: '100%', md: '60%' } }}>{secondaryPanel}</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Grid + Sidebar: ~25% filter sidebar (left) / ~75% card grid (right) */
|
||||||
|
const GridSidebarLayout: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
secondaryPanel?: React.ReactNode;
|
||||||
|
}> = ({ children, secondaryPanel }) => (
|
||||||
|
<Container maxWidth="lg" sx={{ flex: 1, py: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: { xs: 0, md: 3 },
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="aside"
|
||||||
|
sx={{
|
||||||
|
width: { xs: '100%', md: '25%' },
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1 }}>{secondaryPanel}</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Detail + Toggles: two-column hero (image left / info right), full-width section below */
|
||||||
|
const DetailTogglesLayout: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
secondaryPanel?: React.ReactNode;
|
||||||
|
}> = ({ children, secondaryPanel }) => (
|
||||||
|
<Container maxWidth="lg" sx={{ flex: 1, py: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: { xs: 0, md: 4 },
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
|
mb: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: { xs: '100%', md: '50%' } }}>{children}</Box>
|
||||||
|
<Box sx={{ width: { xs: '100%', md: '50%' } }}>{secondaryPanel}</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Variant map ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const LAYOUT_MAP: Record<
|
||||||
|
WizardLayoutVariant,
|
||||||
|
React.FC<{ children: React.ReactNode; secondaryPanel?: React.ReactNode }>
|
||||||
|
> = {
|
||||||
|
'centered-form': CenteredFormLayout,
|
||||||
|
'list-map': ListMapLayout,
|
||||||
|
'list-detail': ListDetailLayout,
|
||||||
|
'grid-sidebar': GridSidebarLayout,
|
||||||
|
'detail-toggles': DetailTogglesLayout,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Variants that show the stepper/total bar */
|
||||||
|
const STEPPER_VARIANTS: WizardLayoutVariant[] = ['grid-sidebar', 'detail-toggles'];
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page-level layout template for the FA arrangement wizard.
|
||||||
|
*
|
||||||
|
* Provides 5 layout variants matching the wizard page templates:
|
||||||
|
* - **centered-form**: Single centered column for form steps (intro, auth, date/time, etc.)
|
||||||
|
* - **list-map**: Split view with scrollable card list and map panel (providers)
|
||||||
|
* - **list-detail**: Master-detail split for selection + detail (packages, preview)
|
||||||
|
* - **grid-sidebar**: Filter sidebar + card grid (coffins)
|
||||||
|
* - **detail-toggles**: Hero image + info column (venue, coffin details)
|
||||||
|
*
|
||||||
|
* All variants share: navigation slot, optional back link, sticky help bar.
|
||||||
|
* Grid-sidebar and detail-toggles add: progress stepper, running total widget.
|
||||||
|
*/
|
||||||
|
export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
variant,
|
||||||
|
navigation,
|
||||||
|
progressStepper,
|
||||||
|
runningTotal,
|
||||||
|
showBackLink = false,
|
||||||
|
backLabel = 'Back',
|
||||||
|
onBack,
|
||||||
|
helpPhone = '1800 987 888',
|
||||||
|
hideHelpBar = false,
|
||||||
|
children,
|
||||||
|
secondaryPanel,
|
||||||
|
sx,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const LayoutComponent = LAYOUT_MAP[variant];
|
||||||
|
const showStepper = STEPPER_VARIANTS.includes(variant);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '100vh',
|
||||||
|
bgcolor: 'background.default',
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Navigation */}
|
||||||
|
{navigation}
|
||||||
|
|
||||||
|
{/* Stepper + running total bar (grid-sidebar, detail-toggles only) */}
|
||||||
|
{showStepper && <StepperBar stepper={progressStepper} total={runningTotal} />}
|
||||||
|
|
||||||
|
{/* Back link — inside a container for consistent alignment */}
|
||||||
|
{showBackLink && (
|
||||||
|
<Container
|
||||||
|
maxWidth={variant === 'centered-form' ? 'sm' : 'lg'}
|
||||||
|
sx={{ pt: 2, px: { xs: 4, md: 3 } }}
|
||||||
|
>
|
||||||
|
<BackLink label={backLabel} onClick={onBack} />
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main content area */}
|
||||||
|
<LayoutComponent secondaryPanel={secondaryPanel}>{children}</LayoutComponent>
|
||||||
|
|
||||||
|
{/* Sticky help bar */}
|
||||||
|
{!hideHelpBar && <HelpBar phone={helpPhone} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
WizardLayout.displayName = 'WizardLayout';
|
||||||
|
export default WizardLayout;
|
||||||
2
src/components/templates/WizardLayout/index.ts
Normal file
2
src/components/templates/WizardLayout/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './WizardLayout';
|
||||||
|
export * from './WizardLayout';
|
||||||
Reference in New Issue
Block a user