Restructure ComparisonPage with bleed variant + sticky-left columns
Adopts a single-scroll-container layout for the desktop Comparison page (motivated by a Figma Make exploration). The page header sits in a max-width container matching the table's natural width, with flex spacers either side of the table — when the viewport is wider than the table, spacers centre it; when a 4th+ package pushes the table wider than viewport, spacers collapse and the table extends rightward from the page header's left edge. - New WizardLayout variant `bleed` — viewport-locked, no inner Container, main is the single scroll host, back link routed into children, `data-wizard-scroll` marker for descendants. - ComparisonTable: fixed 300px column widths exposed as COMPARISON_TABLE_COL_WIDTH; sticky-left on row-label column across every per-section mini-table; tiered hover (surface-subtle base / surface-warm recommended column); recommended column carries a resting 50%-opacity warm tint; "Not Included" copy replaces em-dash for unavailable cells in Optionals/Extras sections; CellIconText helper applies lineHeight: 1 so icon+text rows align optically. - ComparisonColumnCard: uniform pt: 5 (40px); medium badge (26px) with star/verified icon; 2px brand-600 border for recommended; provider name wraps to 2 lines in a reserved 36px bottom-aligned slot so 1-line names keep subsequent content on a consistent baseline; Remove link always rendered as the same Link element (visibility-hidden when not applicable) so CTA+footer align across all cards. - Mackay test data extended to exercise 2-line wrap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,17 +67,17 @@ export const ComparisonColumnCard = React.forwardRef<HTMLDivElement, ComparisonC
|
|||||||
<Badge
|
<Badge
|
||||||
color="brand"
|
color="brand"
|
||||||
variant={pkg.isRecommended ? 'filled' : 'soft'}
|
variant={pkg.isRecommended ? 'filled' : 'soft'}
|
||||||
size="small"
|
size="medium"
|
||||||
icon={
|
icon={
|
||||||
pkg.isRecommended ? (
|
pkg.isRecommended ? (
|
||||||
<StarRoundedIcon sx={{ fontSize: 14 }} />
|
<StarRoundedIcon sx={{ fontSize: 16 }} />
|
||||||
) : (
|
) : (
|
||||||
<VerifiedOutlinedIcon sx={{ fontSize: 14 }} />
|
<VerifiedOutlinedIcon sx={{ fontSize: 16 }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: -12,
|
top: -13,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
@@ -109,20 +109,23 @@ export const ComparisonColumnCard = React.forwardRef<HTMLDivElement, ComparisonC
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
px: 2.5,
|
px: 2.5,
|
||||||
py: 2.5,
|
pt: 5,
|
||||||
pt: pkg.provider.verified || pkg.isRecommended ? 3 : 2.5,
|
pb: 3,
|
||||||
gap: 0.5,
|
gap: 1,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Provider name with optional verified icon (truncated with tooltip) */}
|
{/* Provider name — always reserves space for 2 lines (via minHeight),
|
||||||
|
content bottom-aligned so single-line names sit flush with the
|
||||||
|
next item below rather than floating high in the slot. */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'flex-end',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 0.75,
|
gap: 0.75,
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
|
minHeight: 36, // 2 × (14px label × 1.286 line-height)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pkg.isRecommended && (
|
{pkg.isRecommended && (
|
||||||
@@ -131,6 +134,7 @@ export const ComparisonColumnCard = React.forwardRef<HTMLDivElement, ComparisonC
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: 'var(--fa-color-brand-600)',
|
color: 'var(--fa-color-brand-600)',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
mb: '2px',
|
||||||
}}
|
}}
|
||||||
aria-label="Verified provider"
|
aria-label="Verified provider"
|
||||||
/>
|
/>
|
||||||
@@ -139,15 +143,17 @@ export const ComparisonColumnCard = React.forwardRef<HTMLDivElement, ComparisonC
|
|||||||
title={pkg.provider.name}
|
title={pkg.provider.name}
|
||||||
arrow
|
arrow
|
||||||
placement="top"
|
placement="top"
|
||||||
disableHoverListener={pkg.provider.name.length < 24}
|
disableHoverListener={pkg.provider.name.length < 50}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="label"
|
variant="label"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -179,18 +185,29 @@ export const ComparisonColumnCard = React.forwardRef<HTMLDivElement, ComparisonC
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider sx={{ width: '100%', my: 1 }} />
|
<Divider sx={{ width: '100%', my: 1.5 }} />
|
||||||
|
|
||||||
<Typography variant="h6" component="p">
|
<Typography variant="h6" component="p">
|
||||||
{pkg.name}
|
{pkg.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>
|
{/* Price subgroup — tighter internal spacing than the outer gap
|
||||||
|
so the label sits close to the amount it describes. */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
Total package price
|
Total package price
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h5" sx={{ color: 'primary.main', fontWeight: 700 }}>
|
<Typography variant="h5" sx={{ color: 'primary.main', fontWeight: 700 }}>
|
||||||
{formatPrice(pkg.price)}
|
{formatPrice(pkg.price)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Spacer pushes CTA to bottom across all cards */}
|
{/* Spacer pushes CTA to bottom across all cards */}
|
||||||
<Box sx={{ flex: 1 }} />
|
<Box sx={{ flex: 1 }} />
|
||||||
@@ -200,28 +217,33 @@ export const ComparisonColumnCard = React.forwardRef<HTMLDivElement, ComparisonC
|
|||||||
color={pkg.provider.verified ? 'primary' : 'secondary'}
|
color={pkg.provider.verified ? 'primary' : 'secondary'}
|
||||||
size="medium"
|
size="medium"
|
||||||
onClick={() => onArrange(pkg.id)}
|
onClick={() => onArrange(pkg.id)}
|
||||||
sx={{ mt: 1.5, px: 4 }}
|
sx={{ px: 4 }}
|
||||||
>
|
>
|
||||||
{pkg.provider.verified ? 'Make Arrangement' : 'Make Enquiry'}
|
{pkg.provider.verified ? 'Make Arrangement' : 'Make Enquiry'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{!pkg.isRecommended && onRemove ? (
|
{/* Always render the same Link element; hide when no Remove action
|
||||||
|
applies (recommended or no handler). Keeps the footer row
|
||||||
|
identical across all cards so CTAs align. */}
|
||||||
|
{(() => {
|
||||||
|
const canRemove = !pkg.isRecommended && !!onRemove;
|
||||||
|
return (
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
onClick={() => onRemove(pkg.id)}
|
onClick={canRemove ? () => onRemove!(pkg.id) : undefined}
|
||||||
sx={{ mt: 0.5 }}
|
tabIndex={canRemove ? 0 : -1}
|
||||||
|
aria-hidden={!canRemove}
|
||||||
|
sx={{
|
||||||
|
...(!canRemove && { visibility: 'hidden', pointerEvents: 'none' }),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
);
|
||||||
/* Invisible spacer keeps CTA aligned with cards that show Remove */
|
})()}
|
||||||
<Box sx={{ mt: 0.5, visibility: 'hidden' }} aria-hidden>
|
|
||||||
<Typography variant="caption">Remove</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -63,7 +63,55 @@ function formatPrice(amount: number): string {
|
|||||||
return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`;
|
return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CellValue({ value }: { value: ComparisonCellValue }) {
|
/**
|
||||||
|
* Inline icon + label wrapper with optically aligned centres.
|
||||||
|
*
|
||||||
|
* body2's line-height adds vertical padding above/below the glyphs. Flex
|
||||||
|
* centring then aligns geometric centres, which puts the icon slightly
|
||||||
|
* above the text's visual centre. Setting `lineHeight: 1` on the row
|
||||||
|
* collapses the text line-box to the font size so geometric and visual
|
||||||
|
* centres match.
|
||||||
|
*/
|
||||||
|
function CellIconText({
|
||||||
|
icon,
|
||||||
|
iconPosition = 'leading',
|
||||||
|
color,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
iconPosition?: 'leading' | 'trailing';
|
||||||
|
color: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{iconPosition === 'leading' && icon}
|
||||||
|
<Typography variant="body2" sx={{ color, fontWeight: 500, lineHeight: 1 }} component="span">
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
{iconPosition === 'trailing' && icon}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sections where a missing item is better expressed as "Not Included"
|
||||||
|
* than a bare em-dash — these are opt-in items, so absence is meaningful. */
|
||||||
|
const OPTIONAL_SECTION_HEADINGS = new Set(['Optionals', 'Extras']);
|
||||||
|
|
||||||
|
function CellValue({
|
||||||
|
value,
|
||||||
|
sectionHeading,
|
||||||
|
}: {
|
||||||
|
value: ComparisonCellValue;
|
||||||
|
sectionHeading: string;
|
||||||
|
}) {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case 'price':
|
case 'price':
|
||||||
return (
|
return (
|
||||||
@@ -79,33 +127,31 @@ function CellValue({ value }: { value: ComparisonCellValue }) {
|
|||||||
);
|
);
|
||||||
case 'complimentary':
|
case 'complimentary':
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
|
<CellIconText
|
||||||
|
color="var(--fa-color-feedback-success)"
|
||||||
|
icon={
|
||||||
<CheckCircleOutlineIcon
|
<CheckCircleOutlineIcon
|
||||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<Typography
|
}
|
||||||
variant="body2"
|
|
||||||
sx={{ color: 'var(--fa-color-feedback-success)', fontWeight: 500 }}
|
|
||||||
>
|
>
|
||||||
Complimentary
|
Complimentary
|
||||||
</Typography>
|
</CellIconText>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
case 'included':
|
case 'included':
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
|
<CellIconText
|
||||||
|
color="var(--fa-color-feedback-success)"
|
||||||
|
icon={
|
||||||
<CheckCircleOutlineIcon
|
<CheckCircleOutlineIcon
|
||||||
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
sx={{ fontSize: 16, color: 'var(--fa-color-feedback-success)' }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<Typography
|
}
|
||||||
variant="body2"
|
|
||||||
sx={{ color: 'var(--fa-color-feedback-success)', fontWeight: 500 }}
|
|
||||||
>
|
>
|
||||||
Included
|
Included
|
||||||
</Typography>
|
</CellIconText>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
case 'poa':
|
case 'poa':
|
||||||
return (
|
return (
|
||||||
@@ -115,20 +161,30 @@ function CellValue({ value }: { value: ComparisonCellValue }) {
|
|||||||
);
|
);
|
||||||
case 'unknown':
|
case 'unknown':
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
|
<CellIconText
|
||||||
<Typography
|
color="var(--fa-color-neutral-500)"
|
||||||
variant="body2"
|
iconPosition="trailing"
|
||||||
sx={{ color: 'var(--fa-color-neutral-500)', fontWeight: 500 }}
|
icon={
|
||||||
>
|
|
||||||
Unknown
|
|
||||||
</Typography>
|
|
||||||
<InfoOutlinedIcon
|
<InfoOutlinedIcon
|
||||||
sx={{ fontSize: 14, color: 'var(--fa-color-neutral-500)' }}
|
sx={{ fontSize: 14, color: 'var(--fa-color-neutral-500)' }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
</Box>
|
}
|
||||||
|
>
|
||||||
|
Unknown
|
||||||
|
</CellIconText>
|
||||||
);
|
);
|
||||||
case 'unavailable':
|
case 'unavailable':
|
||||||
|
if (OPTIONAL_SECTION_HEADINGS.has(sectionHeading)) {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: 'var(--fa-color-neutral-500)', fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
Not Included
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Typography variant="body2" sx={{ color: 'var(--fa-color-neutral-400)' }}>
|
<Typography variant="body2" sx={{ color: 'var(--fa-color-neutral-400)' }}>
|
||||||
—
|
—
|
||||||
@@ -207,6 +263,19 @@ const tableSx = {
|
|||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed column width for both the row-label column and each package column.
|
||||||
|
* Natural table width = COMPARISON_TABLE_COL_WIDTH × (packages.length + 1).
|
||||||
|
* Exposed so ComparisonPage can size its width-matching page header container
|
||||||
|
* to align left edges with the table on horizontal overflow.
|
||||||
|
*/
|
||||||
|
export const COMPARISON_TABLE_COL_WIDTH = 300;
|
||||||
|
|
||||||
|
/** z-index scale for sticky layers inside the table. */
|
||||||
|
const Z_HEADER_ROW = 30;
|
||||||
|
const Z_STICKY_LEFT = 20;
|
||||||
|
const Z_STICKY_LEFT_SECTION = 25; // section heading left cell above body cells
|
||||||
|
|
||||||
// ─── Component ──────────────────────────────────────────────────────────────
|
// ─── Component ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,10 +288,10 @@ const tableSx = {
|
|||||||
*/
|
*/
|
||||||
export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableProps>(
|
export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableProps>(
|
||||||
({ packages, onArrange, onRemove, sx }, ref) => {
|
({ packages, onArrange, onRemove, sx }, ref) => {
|
||||||
const colCount = packages.length + 1;
|
|
||||||
const mergedSections = buildMergedSections(packages);
|
const mergedSections = buildMergedSections(packages);
|
||||||
const gridCols = `minmax(220px, 280px) repeat(${packages.length}, minmax(200px, 1fr))`;
|
const colCount = packages.length + 1;
|
||||||
const minW = packages.length > 3 ? 960 : packages.length > 2 ? 800 : 600;
|
const gridCols = `${COMPARISON_TABLE_COL_WIDTH}px repeat(${packages.length}, ${COMPARISON_TABLE_COL_WIDTH}px)`;
|
||||||
|
const recommendedColIdx = packages.findIndex((p) => p.isRecommended);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -232,32 +301,39 @@ export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableP
|
|||||||
sx={[
|
sx={[
|
||||||
{
|
{
|
||||||
display: { xs: 'none', md: 'block' },
|
display: { xs: 'none', md: 'block' },
|
||||||
overflowX: 'auto',
|
width: COMPARISON_TABLE_COL_WIDTH * colCount,
|
||||||
},
|
},
|
||||||
...(Array.isArray(sx) ? sx : [sx]),
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Box sx={{ minWidth: minW }}>
|
|
||||||
{/* ── Package header cards ── */}
|
{/* ── Package header cards ── */}
|
||||||
<Box
|
<Box
|
||||||
role="row"
|
role="row"
|
||||||
sx={{
|
sx={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: gridCols,
|
gridTemplateColumns: gridCols,
|
||||||
gap: 2,
|
|
||||||
mb: 4,
|
mb: 4,
|
||||||
alignItems: 'stretch',
|
alignItems: 'stretch',
|
||||||
pt: 3, // Room for floating verified badges
|
pt: 3, // Room for floating verified badges
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Info card — stretches to match package card height, text at top */}
|
{/* Info card — sticky-left, matches the row-label column below */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0,
|
||||||
|
zIndex: Z_HEADER_ROW,
|
||||||
|
bgcolor: 'background.default',
|
||||||
|
px: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Card
|
<Card
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
padding="default"
|
padding="default"
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: 'var(--fa-color-surface-subtle)',
|
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||||
alignSelf: 'stretch',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
@@ -276,24 +352,52 @@ export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableP
|
|||||||
Review and compare features side-by-side to find the right fit.
|
Review and compare features side-by-side to find the right fit.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Package column header cards */}
|
|
||||||
{packages.map((pkg) => (
|
{packages.map((pkg) => (
|
||||||
|
<Box key={pkg.id} sx={{ px: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||||
<ComparisonColumnCard
|
<ComparisonColumnCard
|
||||||
key={pkg.id}
|
|
||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
onArrange={onArrange}
|
onArrange={onArrange}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* ── Section tables (each separate with left accent headings) ── */}
|
{/* ── Section tables (each separate with left accent headings) ── */}
|
||||||
{mergedSections.map((section) => (
|
{mergedSections.map((section) => (
|
||||||
<Box key={section.heading} sx={{ ...tableSx, gridTemplateColumns: gridCols, mb: 3 }}>
|
<Box key={section.heading} sx={{ ...tableSx, gridTemplateColumns: gridCols, mb: 3 }}>
|
||||||
<Box role="row" sx={{ gridColumn: `1 / ${colCount + 1}` }}>
|
{/* Section heading row — left cell sticky so label stays visible on horizontal scroll */}
|
||||||
|
<Box
|
||||||
|
role="row"
|
||||||
|
sx={{
|
||||||
|
gridColumn: `1 / ${colCount + 1}`,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'subgrid',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0,
|
||||||
|
zIndex: Z_STICKY_LEFT_SECTION,
|
||||||
|
gridColumn: '1 / 2',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SectionHeading>{section.heading}</SectionHeading>
|
<SectionHeading>{section.heading}</SectionHeading>
|
||||||
</Box>
|
</Box>
|
||||||
|
{/* Background continuation for the remaining columns so they
|
||||||
|
share the heading's surface-subtle wash. */}
|
||||||
|
<Box
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
gridColumn: `2 / ${colCount + 1}`,
|
||||||
|
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{section.items.map((item) => (
|
{section.items.map((item) => (
|
||||||
<Box
|
<Box
|
||||||
@@ -303,17 +407,30 @@ export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableP
|
|||||||
gridColumn: `1 / ${colCount + 1}`,
|
gridColumn: `1 / ${colCount + 1}`,
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'subgrid',
|
gridTemplateColumns: 'subgrid',
|
||||||
transition: 'background-color 0.15s ease',
|
// Tiered hover: base cells go to surface-subtle, recommended
|
||||||
'&:hover': { bgcolor: 'var(--fa-color-brand-50)' },
|
// column cells inherit a warmer surface-warm tint on row hover.
|
||||||
|
'&:hover .comparison-cell': {
|
||||||
|
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||||
|
},
|
||||||
|
'&:hover .comparison-cell--recommended': {
|
||||||
|
bgcolor: 'var(--fa-color-surface-warm)',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Row-label cell — sticky-left */}
|
||||||
<Box
|
<Box
|
||||||
role="cell"
|
role="cell"
|
||||||
|
className="comparison-cell comparison-cell--label"
|
||||||
sx={{
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0,
|
||||||
|
zIndex: Z_STICKY_LEFT,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
px: 3,
|
px: 3,
|
||||||
py: 2,
|
py: 2,
|
||||||
borderTop: '1px solid',
|
borderTop: '1px solid',
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
|
transition: 'background-color 0.15s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="text.secondary" component="span">
|
<Typography variant="body2" color="text.secondary" component="span">
|
||||||
@@ -337,10 +454,15 @@ export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableP
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{packages.map((pkg) => (
|
{packages.map((pkg, idx) => {
|
||||||
|
const isRecommended = idx === recommendedColIdx;
|
||||||
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={pkg.id}
|
key={pkg.id}
|
||||||
role="cell"
|
role="cell"
|
||||||
|
className={
|
||||||
|
'comparison-cell' + (isRecommended ? ' comparison-cell--recommended' : '')
|
||||||
|
}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -351,11 +473,22 @@ export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableP
|
|||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
borderLeft: '1px solid',
|
borderLeft: '1px solid',
|
||||||
borderLeftColor: 'divider',
|
borderLeftColor: 'divider',
|
||||||
|
transition: 'background-color 0.15s ease',
|
||||||
|
// Resting tint for the recommended column so it reads
|
||||||
|
// as the default column even without hover.
|
||||||
|
...(isRecommended && {
|
||||||
|
bgcolor:
|
||||||
|
'color-mix(in srgb, var(--fa-color-surface-warm) 50%, transparent)',
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CellValue value={lookupValue(pkg, section.heading, item.name)} />
|
<CellValue
|
||||||
|
value={lookupValue(pkg, section.heading, item.name)}
|
||||||
|
sectionHeading={section.heading}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -363,12 +496,11 @@ export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableP
|
|||||||
|
|
||||||
{packages.some((p) => p.itemizedAvailable === false) && mergedSections.length > 0 && (
|
{packages.some((p) => p.itemizedAvailable === false) && mergedSections.length > 0 && (
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
||||||
* Some providers have not provided an itemised pricing breakdown. Their items are
|
* Some providers have not provided an itemised pricing breakdown. Their items are shown
|
||||||
shown as "—" above.
|
as "—" above.
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { ComparisonTable, default } from './ComparisonTable';
|
export { ComparisonTable, COMPARISON_TABLE_COL_WIDTH, default } from './ComparisonTable';
|
||||||
export type {
|
export type {
|
||||||
ComparisonTableProps,
|
ComparisonTableProps,
|
||||||
ComparisonPackage,
|
ComparisonPackage,
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ const pkgMackay: ComparisonPackage = {
|
|||||||
name: 'Everyday Funeral Package',
|
name: 'Everyday Funeral Package',
|
||||||
price: 5495.45,
|
price: 5495.45,
|
||||||
provider: {
|
provider: {
|
||||||
name: 'Mackay Family Funerals',
|
name: 'Mackay Family Funeral Directors & Cremation Services',
|
||||||
location: 'Inglewood',
|
location: 'Inglewood',
|
||||||
logoUrl: DEMO_LOGO,
|
logoUrl: DEMO_LOGO,
|
||||||
rating: 4.6,
|
rating: 4.6,
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import React, { useId, useState, useRef, useCallback } from 'react';
|
import React, { useId, useState, useRef, useCallback } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
|
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
|
||||||
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import { Typography } from '../../atoms/Typography';
|
import { Typography } from '../../atoms/Typography';
|
||||||
import { Button } from '../../atoms/Button';
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Link } from '../../atoms/Link';
|
||||||
import { WizardLayout } from '../../templates/WizardLayout';
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
import { ComparisonTable, type ComparisonPackage } from '../../organisms/ComparisonTable';
|
import {
|
||||||
|
ComparisonTable,
|
||||||
|
COMPARISON_TABLE_COL_WIDTH,
|
||||||
|
type ComparisonPackage,
|
||||||
|
} from '../../organisms/ComparisonTable';
|
||||||
import { ComparisonPackageCard } from '../../molecules/ComparisonPackageCard';
|
import { ComparisonPackageCard } from '../../molecules/ComparisonPackageCard';
|
||||||
import { ComparisonTabCard } from '../../molecules/ComparisonTabCard';
|
import { ComparisonTabCard } from '../../molecules/ComparisonTabCard';
|
||||||
|
|
||||||
@@ -113,17 +120,56 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Natural table width = (row-label col) + (pkg col × n), matches page header maxWidth.
|
||||||
|
// Page header container reaches this same width so the table's left edge aligns
|
||||||
|
// with the page header's left edge when the table overflows horizontally.
|
||||||
|
const tableNaturalWidth = COMPARISON_TABLE_COL_WIDTH * (allPackages.length + 1);
|
||||||
|
const pageMaxWidth = COMPARISON_TABLE_COL_WIDTH * 4; // fits 3-package case flush
|
||||||
|
|
||||||
|
// Matching horizontal padding between the page header container and the
|
||||||
|
// table-zone spacers keeps inner-content left edges aligned on all viewports.
|
||||||
|
const edgePadding = { xs: 16, md: 24 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box ref={ref} sx={sx}>
|
<Box ref={ref} sx={sx}>
|
||||||
<WizardLayout
|
<WizardLayout
|
||||||
variant="wide-form"
|
variant={isMobile ? 'wide-form' : 'bleed'}
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
showBackLink
|
showBackLink={isMobile}
|
||||||
backLabel="Back"
|
backLabel="Back"
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
>
|
>
|
||||||
{/* Page header with Share/Print actions */}
|
{!isMobile && (
|
||||||
<Box sx={{ mb: { xs: 3, md: 5 } }}>
|
<>
|
||||||
|
{/* Page header zone — centred, bounded to the table's natural width */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: pageMaxWidth,
|
||||||
|
px: { xs: `${edgePadding.xs}px`, md: `${edgePadding.md}px` },
|
||||||
|
pt: { xs: 2, md: 3 },
|
||||||
|
pb: { xs: 3, md: 5 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
onClick={onBack}
|
||||||
|
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 }} />
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -142,7 +188,6 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Share + Print */}
|
|
||||||
{(onShare || onPrint) && (
|
{(onShare || onPrint) && (
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
|
<Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
|
||||||
{onShare && (
|
{onShare && (
|
||||||
@@ -171,21 +216,74 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Desktop: ComparisonTable */}
|
<Divider />
|
||||||
{!isMobile && (
|
|
||||||
<ComparisonTable packages={allPackages} onArrange={onArrange} onRemove={onRemove} />
|
{/* Table zone — width-matching spacers centre the table when room
|
||||||
|
allows, collapse to the minimum when table is wider than
|
||||||
|
viewport so overflow extends rightward from the page's
|
||||||
|
content column. */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: 'max-content',
|
||||||
|
minWidth: '100%',
|
||||||
|
py: { xs: 3, md: 5 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: { xs: `${edgePadding.xs}px`, md: `${edgePadding.md}px` },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flexShrink: 0, width: tableNaturalWidth }}>
|
||||||
|
<ComparisonTable
|
||||||
|
packages={allPackages}
|
||||||
|
onArrange={onArrange}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: { xs: `${edgePadding.xs}px`, md: `${edgePadding.md}px` },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile: Tab rail + card view */}
|
{/* Mobile: Tab rail + card view */}
|
||||||
{isMobile && allPackages.length > 0 && (
|
{isMobile && allPackages.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{/* Tab rail — mini cards showing provider + package + price */}
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="h2" component="h1" sx={{ mb: 1 }}>
|
||||||
|
Compare packages
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" aria-live="polite">
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 3 }} />
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
id="comparison-rail-heading"
|
||||||
|
variant="label"
|
||||||
|
component="h2"
|
||||||
|
sx={{ fontWeight: 600, display: 'block', mb: 1.5 }}
|
||||||
|
>
|
||||||
|
Choose a package to view
|
||||||
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
ref={railRef}
|
ref={railRef}
|
||||||
role="tablist"
|
role="tablist"
|
||||||
id={tablistId}
|
id={tablistId}
|
||||||
aria-label="Packages to compare"
|
aria-labelledby="comparison-rail-heading"
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
@@ -193,8 +291,7 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
|||||||
py: 2,
|
py: 2,
|
||||||
px: 2,
|
px: 2,
|
||||||
mx: -2,
|
mx: -2,
|
||||||
mt: 1,
|
mb: 1.5,
|
||||||
mb: 3,
|
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
'&::-webkit-scrollbar': { display: 'none' },
|
'&::-webkit-scrollbar': { display: 'none' },
|
||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
@@ -216,6 +313,54 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Dot indicator — position + count. Purely visual supplement;
|
||||||
|
the tab rail above is the accessible navigation, so dots
|
||||||
|
are aria-hidden and skipped by keyboard tab-order. */}
|
||||||
|
<Box
|
||||||
|
aria-hidden="true"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{allPackages.map((_, idx) => {
|
||||||
|
const isActive = idx === activeTabIdx;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={idx}
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => handleTabClick(idx)}
|
||||||
|
sx={{
|
||||||
|
appearance: 'none',
|
||||||
|
border: 0,
|
||||||
|
background: 'transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
p: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
'& > span': {
|
||||||
|
display: 'block',
|
||||||
|
width: isActive ? 24 : 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
bgcolor: isActive
|
||||||
|
? 'var(--fa-color-brand-600)'
|
||||||
|
: 'var(--fa-color-neutral-300)',
|
||||||
|
transition: 'width 0.2s ease, background-color 0.2s ease',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{activePackage && (
|
{activePackage && (
|
||||||
<Box
|
<Box
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export type WizardLayoutVariant =
|
|||||||
| 'list-map'
|
| 'list-map'
|
||||||
| 'list-detail'
|
| 'list-detail'
|
||||||
| 'grid-sidebar'
|
| 'grid-sidebar'
|
||||||
| 'detail-toggles';
|
| 'detail-toggles'
|
||||||
|
| 'bleed';
|
||||||
|
|
||||||
/** Props for the WizardLayout template */
|
/** Props for the WizardLayout template */
|
||||||
export interface WizardLayoutProps {
|
export interface WizardLayoutProps {
|
||||||
@@ -362,6 +363,30 @@ const DetailTogglesLayout: React.FC<{
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Bleed: full-width scroll host. Main becomes the single scroll container
|
||||||
|
* (both axes). No inner Container — children are full-bleed. Back link is
|
||||||
|
* passed into children so it scrolls with the page content. Used by pages
|
||||||
|
* that own their own width + alignment logic (e.g. ComparisonPage). */
|
||||||
|
const BleedLayout: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
backLink?: React.ReactNode;
|
||||||
|
}> = ({ children, backLink }) => (
|
||||||
|
<Box
|
||||||
|
id="wizard-scroll"
|
||||||
|
data-wizard-scroll
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
overflow: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{backLink}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
// ─── Variant map ─────────────────────────────────────────────────────────────
|
// ─── Variant map ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const LAYOUT_MAP: Record<
|
const LAYOUT_MAP: Record<
|
||||||
@@ -378,6 +403,7 @@ const LAYOUT_MAP: Record<
|
|||||||
'list-detail': ListDetailLayout,
|
'list-detail': ListDetailLayout,
|
||||||
'grid-sidebar': GridSidebarLayout,
|
'grid-sidebar': GridSidebarLayout,
|
||||||
'detail-toggles': DetailTogglesLayout,
|
'detail-toggles': DetailTogglesLayout,
|
||||||
|
bleed: BleedLayout,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Stepper bar renders on any variant when progressStepper or runningTotal is provided */
|
/* Stepper bar renders on any variant when progressStepper or runningTotal is provided */
|
||||||
@@ -387,12 +413,15 @@ const LAYOUT_MAP: Record<
|
|||||||
/**
|
/**
|
||||||
* Page-level layout template for the FA arrangement wizard.
|
* Page-level layout template for the FA arrangement wizard.
|
||||||
*
|
*
|
||||||
* Provides 5 layout variants matching the wizard page templates:
|
* Provides 6 layout variants matching the wizard page templates:
|
||||||
* - **centered-form**: Single centered column for form steps (intro, auth, date/time, etc.)
|
* - **centered-form**: Single centered column for form steps (intro, auth, date/time, etc.)
|
||||||
|
* - **wide-form**: Wider single column for card grids (coffins, etc.)
|
||||||
* - **list-map**: Split view with scrollable card list and map panel (providers)
|
* - **list-map**: Split view with scrollable card list and map panel (providers)
|
||||||
* - **list-detail**: Master-detail split for selection + detail (packages, preview)
|
* - **list-detail**: Master-detail split for selection + detail (packages, preview)
|
||||||
* - **grid-sidebar**: Filter sidebar + card grid (coffins)
|
* - **grid-sidebar**: Filter sidebar + card grid (coffins)
|
||||||
* - **detail-toggles**: Hero image + info column (venue, coffin details)
|
* - **detail-toggles**: Hero image + info column (venue, coffin details)
|
||||||
|
* - **bleed**: Viewport-locked, full-width scroll host with no inner container —
|
||||||
|
* the page owns its own alignment (comparison page)
|
||||||
*
|
*
|
||||||
* All variants share: navigation slot, optional back link, sticky help bar,
|
* All variants share: navigation slot, optional back link, sticky help bar,
|
||||||
* and optional progress stepper + running total bar (shown when props provided).
|
* and optional progress stepper + running total bar (shown when props provided).
|
||||||
@@ -426,8 +455,8 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
bgcolor: 'background.default',
|
bgcolor: 'background.default',
|
||||||
// list-map + detail-toggles: lock to viewport so panels scroll independently
|
// list-map + detail-toggles + bleed: lock to viewport so panels scroll independently
|
||||||
...((variant === 'list-map' || variant === 'detail-toggles') && {
|
...((variant === 'list-map' || variant === 'detail-toggles' || variant === 'bleed') && {
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}),
|
}),
|
||||||
@@ -445,8 +474,12 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
|
|||||||
{/* Stepper + running total bar (grid-sidebar, detail-toggles only) */}
|
{/* Stepper + running total bar (grid-sidebar, detail-toggles only) */}
|
||||||
<StepperBar stepper={progressStepper} total={runningTotal} />
|
<StepperBar stepper={progressStepper} total={runningTotal} />
|
||||||
|
|
||||||
{/* Back link — inside left panel for list-map/detail-toggles, above content for others */}
|
{/* Back link — inside children for list-map/detail-toggles/bleed (scrolls with content),
|
||||||
{showBackLink && variant !== 'list-map' && variant !== 'detail-toggles' && (
|
above content for other variants */}
|
||||||
|
{showBackLink &&
|
||||||
|
variant !== 'list-map' &&
|
||||||
|
variant !== 'detail-toggles' &&
|
||||||
|
variant !== 'bleed' && (
|
||||||
<Container
|
<Container
|
||||||
maxWidth={variant === 'centered-form' ? 'sm' : 'lg'}
|
maxWidth={variant === 'centered-form' ? 'sm' : 'lg'}
|
||||||
sx={{ pt: 2, px: { xs: 4, md: 3 } }}
|
sx={{ pt: 2, px: { xs: 4, md: 3 } }}
|
||||||
@@ -463,7 +496,8 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
|
|||||||
<LayoutComponent
|
<LayoutComponent
|
||||||
secondaryPanel={secondaryPanel}
|
secondaryPanel={secondaryPanel}
|
||||||
backLink={
|
backLink={
|
||||||
showBackLink && (variant === 'list-map' || variant === 'detail-toggles') ? (
|
showBackLink &&
|
||||||
|
(variant === 'list-map' || variant === 'detail-toggles' || variant === 'bleed') ? (
|
||||||
<Box sx={{ pt: 1.5 }}>
|
<Box sx={{ pt: 1.5 }}>
|
||||||
<BackLink label={backLabel} onClick={onBack} />
|
<BackLink label={backLabel} onClick={onBack} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user