Batch 2: List-map layout rework — 420px fixed column, sticky headers
- WizardLayout ListMapLayout: 420px fixed left column (D-B), flex:1 right panel, back link rendered inside left panel instead of above the split (eliminates gap above map) - LAYOUT_MAP type updated to accept backLink prop for list-map variant - ProvidersStep: heading + search + filters wrapped in sticky Box that pins at top of scrollable left panel while card list scrolls - VenueStep: same sticky header treatment, heading moved inside form for consistent wrapper structure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,50 +146,62 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||
Choose a funeral provider
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subheading}
|
||||
</Typography>
|
||||
|
||||
{/* Search bar */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
onSearch={onSearch}
|
||||
placeholder="Search providers..."
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Filter chips */}
|
||||
{filters && filters.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
|
||||
{filters.map((filter, index) => (
|
||||
<Chip
|
||||
key={filter.label}
|
||||
label={filter.label}
|
||||
selected={filter.active}
|
||||
onClick={onFilterToggle ? () => onFilterToggle(index) : undefined}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results count */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
aria-live="polite"
|
||||
{/* Sticky header — stays pinned while card list scrolls */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
bgcolor: 'background.default',
|
||||
pb: 1,
|
||||
mx: -3,
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
{providers.length} provider{providers.length !== 1 ? 's' : ''} found
|
||||
</Typography>
|
||||
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||
Choose a funeral provider
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subheading}
|
||||
</Typography>
|
||||
|
||||
{/* Search bar */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
onSearch={onSearch}
|
||||
placeholder="Search providers..."
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Filter chips */}
|
||||
{filters && filters.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
|
||||
{filters.map((filter, index) => (
|
||||
<Chip
|
||||
key={filter.label}
|
||||
label={filter.label}
|
||||
selected={filter.active}
|
||||
onClick={onFilterToggle ? () => onFilterToggle(index) : undefined}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results count */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 0, display: 'block' }}
|
||||
aria-live="polite"
|
||||
>
|
||||
{providers.length} provider{providers.length !== 1 ? 's' : ''} found
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
|
||||
@@ -193,17 +193,6 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Page heading */}
|
||||
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||
Where would you like the service?
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{isPrePlanning
|
||||
? 'Browse available venues. Your choice can be changed later.'
|
||||
: 'Choose a venue for the funeral service. You can filter by location, features, and religion.'}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
@@ -213,40 +202,63 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
||||
if (!loading) onContinue();
|
||||
}}
|
||||
>
|
||||
{/* ─── Search + Filters ─── */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
placeholder="Search a town or suburb..."
|
||||
value={values.search}
|
||||
onChange={(e) => onChange({ ...values, search: e.target.value })}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ color: 'text.secondary' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
{/* Sticky header — stays pinned while card list scrolls */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
bgcolor: 'background.default',
|
||||
pb: 1,
|
||||
mx: -3,
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||
Where would you like the service?
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
{filterOptions.map((filter) => (
|
||||
<Chip
|
||||
key={filter.key}
|
||||
label={filter.label}
|
||||
onClick={() => handleFilterToggle(filter.key)}
|
||||
selected={values.activeFilters.includes(filter.key)}
|
||||
/>
|
||||
))}
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{isPrePlanning
|
||||
? 'Browse available venues. Your choice can be changed later.'
|
||||
: 'Choose a venue for the funeral service. You can filter by location, features, and religion.'}
|
||||
</Typography>
|
||||
|
||||
{/* ─── Search + Filters ─── */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
placeholder="Search a town or suburb..."
|
||||
value={values.search}
|
||||
onChange={(e) => onChange({ ...values, search: e.target.value })}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ color: 'text.secondary' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
{filterOptions.map((filter) => (
|
||||
<Chip
|
||||
key={filter.key}
|
||||
label={filter.label}
|
||||
onClick={() => handleFilterToggle(filter.key)}
|
||||
selected={values.activeFilters.includes(filter.key)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ─── Results count ─── */}
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }} aria-live="polite">
|
||||
Found {venues.length} venue{venues.length !== 1 ? 's' : ''}
|
||||
{locationName ? ` near ${locationName}` : ''}
|
||||
</Typography>
|
||||
{/* ─── Results count ─── */}
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 0 }} aria-live="polite">
|
||||
Found {venues.length} venue{venues.length !== 1 ? 's' : ''}
|
||||
{locationName ? ` near ${locationName}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* ─── Venue card grid ─── */}
|
||||
<Box
|
||||
|
||||
@@ -142,11 +142,12 @@ const CenteredFormLayout: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
</Container>
|
||||
);
|
||||
|
||||
/** List + Map: ~40% scrollable list (left) / ~60% map (right) */
|
||||
/** List + Map: 420px fixed scrollable list (left) / flex map (right) — D-B */
|
||||
const ListMapLayout: React.FC<{
|
||||
children: React.ReactNode;
|
||||
secondaryPanel?: React.ReactNode;
|
||||
}> = ({ children, secondaryPanel }) => (
|
||||
backLink?: React.ReactNode;
|
||||
}> = ({ children, secondaryPanel, backLink }) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -156,18 +157,20 @@ const ListMapLayout: React.FC<{
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', md: '40%' },
|
||||
width: { xs: '100%', md: 420 },
|
||||
flexShrink: 0,
|
||||
overflowY: 'auto',
|
||||
px: { xs: 2, md: 3 },
|
||||
py: 3,
|
||||
}}
|
||||
>
|
||||
{backLink}
|
||||
{children}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'block' },
|
||||
width: '60%',
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
@@ -246,7 +249,11 @@ const DetailTogglesLayout: React.FC<{
|
||||
|
||||
const LAYOUT_MAP: Record<
|
||||
WizardLayoutVariant,
|
||||
React.FC<{ children: React.ReactNode; secondaryPanel?: React.ReactNode }>
|
||||
React.FC<{
|
||||
children: React.ReactNode;
|
||||
secondaryPanel?: React.ReactNode;
|
||||
backLink?: React.ReactNode;
|
||||
}>
|
||||
> = {
|
||||
'centered-form': CenteredFormLayout,
|
||||
'list-map': ListMapLayout,
|
||||
@@ -313,8 +320,8 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
|
||||
{/* Stepper + running total bar (grid-sidebar, detail-toggles only) */}
|
||||
{showStepper && <StepperBar stepper={progressStepper} total={runningTotal} />}
|
||||
|
||||
{/* Back link — inside a container for consistent alignment */}
|
||||
{showBackLink && (
|
||||
{/* Back link — inside left panel for list-map, above content for others */}
|
||||
{showBackLink && variant !== 'list-map' && (
|
||||
<Container
|
||||
maxWidth={variant === 'centered-form' ? 'sm' : 'lg'}
|
||||
sx={{ pt: 2, px: { xs: 4, md: 3 } }}
|
||||
@@ -325,7 +332,16 @@ export const WizardLayout = React.forwardRef<HTMLDivElement, WizardLayoutProps>(
|
||||
|
||||
{/* Main content area */}
|
||||
<Box component="main" sx={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<LayoutComponent secondaryPanel={secondaryPanel}>{children}</LayoutComponent>
|
||||
<LayoutComponent
|
||||
secondaryPanel={secondaryPanel}
|
||||
backLink={
|
||||
showBackLink && variant === 'list-map' ? (
|
||||
<BackLink label={backLabel} onClick={onBack} />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</LayoutComponent>
|
||||
</Box>
|
||||
|
||||
{/* Sticky help bar */}
|
||||
|
||||
Reference in New Issue
Block a user