Batch 3: FilterPanel molecule + integration across 3 steps (D-C, D-F)
New molecule: - FilterPanel: Popover-based reusable filter trigger with active count badge, Clear all, Done actions. D-C: Popover for MVP. Step integrations: - ProvidersStep: inline Chip filter bar → FilterPanel Popover, search bar + filter button side-by-side in sticky header - VenueStep: same pattern, filter chips moved into Popover - CoffinsStep (D-F): grid-sidebar layout → wide-form (full-width 4-col grid), category + price selects moved into FilterPanel WizardLayout: - Added wide-form variant (maxWidth lg, single column) for card grids that benefit from full width - wide-form included in STEPPER_VARIANTS for progress bar Storybook: - FilterPanel stories: Default, WithActiveFilters, SelectFilters, CustomLabel Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,35 @@ Each entry follows this structure:
|
|||||||
|
|
||||||
## Sessions
|
## Sessions
|
||||||
|
|
||||||
|
### Session 2026-03-29e — Feedback iteration Batches 1 & 2
|
||||||
|
|
||||||
|
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||||
|
|
||||||
|
**Work completed:**
|
||||||
|
- **Batch 1: Atom + Template Foundation**
|
||||||
|
- ToggleButtonGroup: label-to-options spacing `mb: 1→2`, top-align content `flex-start`, fixed selected border CSS specificity (added `&.Mui-selected` in grouped selector)
|
||||||
|
- Heading standardisation: all 6 split-layout steps changed from `h4` → `display3` (ProvidersStep, PackagesStep, PreviewStep, VenueStep, CoffinsStep, CoffinDetailsStep) per D-A
|
||||||
|
- DateTimeStep: normalised section gaps (scheduling fieldset `mb: 3→4`)
|
||||||
|
- CrematoriumStep: added subheading for consistency, normalised witness section `mb: 3→4`
|
||||||
|
- PackagesStep + DateTimeStep: fixed input label clipping (`pt: 0.5` on TextField containers)
|
||||||
|
- **Batch 2: List-Map Layout Rework (D-B)**
|
||||||
|
- WizardLayout ListMapLayout: 420px fixed left column, `flex: 1` right panel
|
||||||
|
- Back link rendered inside left panel (not above split) — eliminates gap above map
|
||||||
|
- LAYOUT_MAP type updated to accept `backLink` prop for list-map variant
|
||||||
|
- ProvidersStep + VenueStep: sticky header (heading + search + filters pinned at top of scrollable left panel)
|
||||||
|
|
||||||
|
**Decisions made:**
|
||||||
|
- None new (implementing D-A heading standardisation and D-B list-map layout from previous session)
|
||||||
|
|
||||||
|
**Open questions:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
- Batch 3: FilterPanel component (reusable Popover-based filter for providers, venues, coffins)
|
||||||
|
- Remaining batches 4–7 from iteration plan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Session 2026-03-29c — Grooming pass: critique/harden/polish all 15 steps
|
### Session 2026-03-29c — Grooming pass: critique/harden/polish all 15 steps
|
||||||
|
|
||||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||||
|
|||||||
100
src/components/molecules/FilterPanel/FilterPanel.stories.tsx
Normal file
100
src/components/molecules/FilterPanel/FilterPanel.stories.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { FilterPanel } from './FilterPanel';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { Chip } from '../../atoms/Chip';
|
||||||
|
|
||||||
|
const meta: Meta<typeof FilterPanel> = {
|
||||||
|
title: 'Molecules/FilterPanel',
|
||||||
|
component: FilterPanel,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
label: { control: 'text' },
|
||||||
|
activeCount: { control: 'number' },
|
||||||
|
minWidth: { control: 'number' },
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ p: 4, minHeight: 400 }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof FilterPanel>;
|
||||||
|
|
||||||
|
/** Default state — no active filters */
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
activeCount: 0,
|
||||||
|
children: (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<Chip label="Verified providers" variant="outlined" size="small" />
|
||||||
|
<Chip label="Within 10km" variant="outlined" size="small" />
|
||||||
|
<Chip label="Reviews 4+★" variant="outlined" size="small" />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** With active filters — badge count shown */
|
||||||
|
export const WithActiveFilters: Story = {
|
||||||
|
args: {
|
||||||
|
activeCount: 2,
|
||||||
|
onClear: () => {},
|
||||||
|
children: (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<Chip label="Verified providers" variant="outlined" size="small" selected />
|
||||||
|
<Chip label="Within 10km" variant="outlined" size="small" selected />
|
||||||
|
<Chip label="Reviews 4+★" variant="outlined" size="small" />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Select-based filters — category + price (CoffinsStep pattern) */
|
||||||
|
export const SelectFilters: Story = {
|
||||||
|
args: {
|
||||||
|
activeCount: 1,
|
||||||
|
onClear: () => {},
|
||||||
|
minWidth: 300,
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<TextField select label="Category" value="solid_timber" fullWidth>
|
||||||
|
<MenuItem value="all">All categories</MenuItem>
|
||||||
|
<MenuItem value="solid_timber">Solid Timber</MenuItem>
|
||||||
|
<MenuItem value="environmental">Environmental</MenuItem>
|
||||||
|
<MenuItem value="designer">Designer</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField select label="Price range" value="all" fullWidth>
|
||||||
|
<MenuItem value="all">All prices</MenuItem>
|
||||||
|
<MenuItem value="under_2000">Under $2,000</MenuItem>
|
||||||
|
<MenuItem value="2000_4000">$2,000 – $4,000</MenuItem>
|
||||||
|
<MenuItem value="over_4000">Over $4,000</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Custom label */
|
||||||
|
export const CustomLabel: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Sort & Filter',
|
||||||
|
activeCount: 0,
|
||||||
|
children: (
|
||||||
|
<TextField select label="Sort by" value="popular" fullWidth>
|
||||||
|
<MenuItem value="popular">Most popular</MenuItem>
|
||||||
|
<MenuItem value="price_low">Price: Low to high</MenuItem>
|
||||||
|
<MenuItem value="price_high">Price: High to low</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
166
src/components/molecules/FilterPanel/FilterPanel.tsx
Normal file
166
src/components/molecules/FilterPanel/FilterPanel.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Popover from '@mui/material/Popover';
|
||||||
|
import TuneIcon from '@mui/icons-material/Tune';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Badge } from '../../atoms/Badge';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Link } from '../../atoms/Link';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FilterPanel molecule */
|
||||||
|
export interface FilterPanelProps {
|
||||||
|
/** Trigger button label */
|
||||||
|
label?: string;
|
||||||
|
/** Number of active filters (shown as count on the trigger) */
|
||||||
|
activeCount?: number;
|
||||||
|
/** Filter controls — rendered inside the Popover body */
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** Callback when "Clear all" is clicked */
|
||||||
|
onClear?: () => void;
|
||||||
|
/** Popover min-width */
|
||||||
|
minWidth?: number;
|
||||||
|
/** MUI sx prop for the trigger button */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable filter panel for the FA arrangement wizard.
|
||||||
|
*
|
||||||
|
* Renders a trigger button ("Filters") that opens a Popover containing
|
||||||
|
* arbitrary filter controls (chips, selects, sliders, etc.) passed as
|
||||||
|
* children. Active filter count shown as a badge on the trigger.
|
||||||
|
*
|
||||||
|
* D-C: Popover for desktop MVP. Mobile Drawer variant planned for later.
|
||||||
|
*
|
||||||
|
* Used in ProvidersStep, VenueStep, and CoffinsStep.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```tsx
|
||||||
|
* <FilterPanel activeCount={2} onClear={handleClear}>
|
||||||
|
* <TextField select label="Category" ... />
|
||||||
|
* <TextField select label="Price" ... />
|
||||||
|
* </FilterPanel>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const FilterPanel: React.FC<FilterPanelProps> = ({
|
||||||
|
label = 'Filters',
|
||||||
|
activeCount = 0,
|
||||||
|
children,
|
||||||
|
onClear,
|
||||||
|
minWidth = 280,
|
||||||
|
sx,
|
||||||
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const popoverId = open ? 'filter-panel-popover' : undefined;
|
||||||
|
|
||||||
|
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Trigger button */}
|
||||||
|
<Box sx={[{ display: 'inline-flex' }, ...(Array.isArray(sx) ? sx : [sx])]}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
startIcon={<TuneIcon />}
|
||||||
|
onClick={handleOpen}
|
||||||
|
aria-describedby={popoverId}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{activeCount > 0 && (
|
||||||
|
<Badge
|
||||||
|
variant="filled"
|
||||||
|
color="brand"
|
||||||
|
size="small"
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
aria-label={`${activeCount} active filter${activeCount !== 1 ? 's' : ''}`}
|
||||||
|
>
|
||||||
|
{activeCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Popover panel */}
|
||||||
|
<Popover
|
||||||
|
id={popoverId}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
|
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: {
|
||||||
|
minWidth,
|
||||||
|
mt: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
px: 2.5,
|
||||||
|
pt: 2,
|
||||||
|
pb: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Filters</Typography>
|
||||||
|
{onClear && activeCount > 0 && (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
onClick={() => {
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
underline="hover"
|
||||||
|
sx={{ fontSize: '0.8125rem' }}
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Filter controls */}
|
||||||
|
<Box sx={{ px: 2.5, py: 2, display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Footer — done button */}
|
||||||
|
<Box sx={{ px: 2.5, py: 1.5, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button variant="contained" size="small" onClick={handleClose}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterPanel.displayName = 'FilterPanel';
|
||||||
|
export default FilterPanel;
|
||||||
3
src/components/molecules/FilterPanel/index.ts
Normal file
3
src/components/molecules/FilterPanel/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { FilterPanel } from './FilterPanel';
|
||||||
|
export type { FilterPanelProps } from './FilterPanel';
|
||||||
|
export { default } from './FilterPanel';
|
||||||
@@ -5,6 +5,7 @@ import MenuItem from '@mui/material/MenuItem';
|
|||||||
import Pagination from '@mui/material/Pagination';
|
import Pagination from '@mui/material/Pagination';
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
import { WizardLayout } from '../../templates/WizardLayout';
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
|
import { FilterPanel } from '../../molecules/FilterPanel';
|
||||||
import { Card } from '../../atoms/Card';
|
import { Card } from '../../atoms/Card';
|
||||||
import { Badge } from '../../atoms/Badge';
|
import { Badge } from '../../atoms/Badge';
|
||||||
import { Typography } from '../../atoms/Typography';
|
import { Typography } from '../../atoms/Typography';
|
||||||
@@ -142,52 +143,29 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
|
|||||||
sx,
|
sx,
|
||||||
}) => {
|
}) => {
|
||||||
const displayCount = totalCount ?? coffins.length;
|
const displayCount = totalCount ?? coffins.length;
|
||||||
|
const activeFilterCount =
|
||||||
|
(values.categoryFilter !== 'all' ? 1 : 0) + (values.priceFilter !== 'all' ? 1 : 0);
|
||||||
|
|
||||||
const handleFilterChange = (field: 'categoryFilter' | 'priceFilter', value: string) => {
|
const handleFilterChange = (field: 'categoryFilter' | 'priceFilter', value: string) => {
|
||||||
onChange({ ...values, [field]: value, page: 1 });
|
onChange({ ...values, [field]: value, page: 1 });
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Sidebar content (filters) ───
|
const handleFilterClear = () => {
|
||||||
const sidebar = (
|
onChange({ ...values, categoryFilter: 'all', priceFilter: 'all', page: 1 });
|
||||||
<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 }}>
|
return (
|
||||||
<TextField
|
<WizardLayout
|
||||||
select
|
variant="wide-form"
|
||||||
label="Categories"
|
navigation={navigation}
|
||||||
value={values.categoryFilter}
|
progressStepper={progressStepper}
|
||||||
onChange={(e) => handleFilterChange('categoryFilter', e.target.value)}
|
runningTotal={runningTotal}
|
||||||
fullWidth
|
showBackLink={!!onBack}
|
||||||
|
backLabel="Back"
|
||||||
|
onBack={onBack}
|
||||||
|
hideHelpBar={hideHelpBar}
|
||||||
|
sx={sx}
|
||||||
>
|
>
|
||||||
{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
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
@@ -213,17 +191,51 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
|
|||||||
outside the allowance will adjust the price.
|
outside the allowance will adjust the price.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Results count */}
|
{/* Filter button + results count */}
|
||||||
<Typography
|
<Box
|
||||||
variant="caption"
|
sx={{
|
||||||
color="text.secondary"
|
display: 'flex',
|
||||||
sx={{ mb: 2, display: 'block' }}
|
alignItems: 'center',
|
||||||
aria-live="polite"
|
justifyContent: 'space-between',
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<FilterPanel activeCount={activeFilterCount} onClear={handleFilterClear} minWidth={300}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Category"
|
||||||
|
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>
|
||||||
|
</FilterPanel>
|
||||||
|
|
||||||
|
<Typography variant="caption" color="text.secondary" aria-live="polite">
|
||||||
Showing {displayCount} coffin{displayCount !== 1 ? 's' : ''}
|
Showing {displayCount} coffin{displayCount !== 1 ? 's' : ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Coffin card grid */}
|
{/* Coffin card grid — full width (D-F) */}
|
||||||
<Box
|
<Box
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-label="Available coffins"
|
aria-label="Available coffins"
|
||||||
@@ -232,7 +244,8 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
|
|||||||
gridTemplateColumns: {
|
gridTemplateColumns: {
|
||||||
xs: '1fr',
|
xs: '1fr',
|
||||||
sm: 'repeat(2, 1fr)',
|
sm: 'repeat(2, 1fr)',
|
||||||
lg: 'repeat(3, 1fr)',
|
md: 'repeat(3, 1fr)',
|
||||||
|
lg: 'repeat(4, 1fr)',
|
||||||
},
|
},
|
||||||
gap: 2,
|
gap: 2,
|
||||||
mb: 3,
|
mb: 3,
|
||||||
@@ -354,22 +367,6 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WizardLayout
|
|
||||||
variant="grid-sidebar"
|
|
||||||
navigation={navigation}
|
|
||||||
progressStepper={progressStepper}
|
|
||||||
runningTotal={runningTotal}
|
|
||||||
showBackLink={!!onBack}
|
|
||||||
backLabel="Back"
|
|
||||||
onBack={onBack}
|
|
||||||
hideHelpBar={hideHelpBar}
|
|
||||||
sx={sx}
|
|
||||||
secondaryPanel={mainContent}
|
|
||||||
>
|
|
||||||
{sidebar}
|
|
||||||
</WizardLayout>
|
</WizardLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { SxProps, Theme } from '@mui/material/styles';
|
|||||||
import { WizardLayout } from '../../templates/WizardLayout';
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
import { ProviderCard } from '../../molecules/ProviderCard';
|
import { ProviderCard } from '../../molecules/ProviderCard';
|
||||||
import { SearchBar } from '../../molecules/SearchBar';
|
import { SearchBar } from '../../molecules/SearchBar';
|
||||||
|
import { FilterPanel } from '../../molecules/FilterPanel';
|
||||||
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';
|
||||||
@@ -62,6 +63,8 @@ export interface ProvidersStepProps {
|
|||||||
filters?: ProviderFilter[];
|
filters?: ProviderFilter[];
|
||||||
/** Callback when a filter chip is toggled */
|
/** Callback when a filter chip is toggled */
|
||||||
onFilterToggle?: (index: number) => void;
|
onFilterToggle?: (index: number) => void;
|
||||||
|
/** Callback to clear all filters */
|
||||||
|
onFilterClear?: () => void;
|
||||||
/** Callback for the Continue button */
|
/** Callback for the Continue button */
|
||||||
onContinue: () => void;
|
onContinue: () => void;
|
||||||
/** Callback for the Back button */
|
/** Callback for the Back button */
|
||||||
@@ -105,6 +108,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
onSearch,
|
onSearch,
|
||||||
filters,
|
filters,
|
||||||
onFilterToggle,
|
onFilterToggle,
|
||||||
|
onFilterClear,
|
||||||
onContinue,
|
onContinue,
|
||||||
onBack,
|
onBack,
|
||||||
error,
|
error,
|
||||||
@@ -165,8 +169,9 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
{subheading}
|
{subheading}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Search bar */}
|
{/* Search bar + filter button */}
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ display: 'flex', gap: 1, mb: 2, alignItems: 'flex-start' }}>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={onSearchChange}
|
onChange={onSearchChange}
|
||||||
@@ -175,10 +180,12 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Filter chips */}
|
|
||||||
{filters && filters.length > 0 && (
|
{filters && filters.length > 0 && (
|
||||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
|
<FilterPanel
|
||||||
|
activeCount={filters.filter((f) => f.active).length}
|
||||||
|
onClear={onFilterClear}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
{filters.map((filter, index) => (
|
{filters.map((filter, index) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={filter.label}
|
key={filter.label}
|
||||||
@@ -190,7 +197,9 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
</FilterPanel>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Results count */}
|
{/* Results count */}
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { SxProps, Theme } from '@mui/material/styles';
|
|||||||
import { WizardLayout } from '../../templates/WizardLayout';
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
import { VenueCard } from '../../molecules/VenueCard';
|
import { VenueCard } from '../../molecules/VenueCard';
|
||||||
import { AddOnOption } from '../../molecules/AddOnOption';
|
import { AddOnOption } from '../../molecules/AddOnOption';
|
||||||
|
import { FilterPanel } from '../../molecules/FilterPanel';
|
||||||
import { Collapse } from '../../atoms/Collapse';
|
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';
|
||||||
@@ -81,6 +82,8 @@ export interface VenueStepProps {
|
|||||||
services?: VenueService[];
|
services?: VenueService[];
|
||||||
/** Filter chip options */
|
/** Filter chip options */
|
||||||
filterOptions?: Array<{ key: string; label: string }>;
|
filterOptions?: Array<{ key: string; label: string }>;
|
||||||
|
/** Callback to clear all filters */
|
||||||
|
onFilterClear?: () => void;
|
||||||
/** Location name for the results count */
|
/** Location name for the results count */
|
||||||
locationName?: string;
|
locationName?: string;
|
||||||
/** Whether this is a pre-planning flow */
|
/** Whether this is a pre-planning flow */
|
||||||
@@ -130,6 +133,7 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
|||||||
{ key: 'features', label: 'Venue Features' },
|
{ key: 'features', label: 'Venue Features' },
|
||||||
{ key: 'religion', label: 'Religion' },
|
{ key: 'religion', label: 'Religion' },
|
||||||
],
|
],
|
||||||
|
onFilterClear,
|
||||||
locationName,
|
locationName,
|
||||||
isPrePlanning = false,
|
isPrePlanning = false,
|
||||||
mapPanel,
|
mapPanel,
|
||||||
@@ -224,8 +228,8 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
|||||||
: '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.'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* ─── Search + Filters ─── */}
|
{/* ─── Search + Filter button ─── */}
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ display: 'flex', gap: 1, mb: 2, alignItems: 'flex-start' }}>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Search a town or suburb..."
|
placeholder="Search a town or suburb..."
|
||||||
value={values.search}
|
value={values.search}
|
||||||
@@ -238,10 +242,9 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
|||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
sx={{ mb: 2 }}
|
|
||||||
/>
|
/>
|
||||||
|
<FilterPanel activeCount={values.activeFilters.length} onClear={onFilterClear}>
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
{filterOptions.map((filter) => (
|
{filterOptions.map((filter) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={filter.key}
|
key={filter.key}
|
||||||
@@ -251,6 +254,7 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
</FilterPanel>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* ─── Results count ─── */}
|
{/* ─── Results count ─── */}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { Typography } from '../../atoms/Typography';
|
|||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Layout variant matching the 5 wizard page templates */
|
/** Layout variant matching the wizard page templates */
|
||||||
export type WizardLayoutVariant =
|
export type WizardLayoutVariant =
|
||||||
| 'centered-form'
|
| 'centered-form'
|
||||||
|
| 'wide-form'
|
||||||
| 'list-map'
|
| 'list-map'
|
||||||
| 'list-detail'
|
| 'list-detail'
|
||||||
| 'grid-sidebar'
|
| 'grid-sidebar'
|
||||||
@@ -142,6 +143,20 @@ const CenteredFormLayout: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Wide Form: single column maxWidth "lg", for card grids (coffins, etc.) */
|
||||||
|
const WideFormLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<Container
|
||||||
|
maxWidth="lg"
|
||||||
|
sx={{
|
||||||
|
py: { xs: 4, md: 6 },
|
||||||
|
px: { xs: 2, md: 3 },
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
/** List + Map: 420px fixed scrollable list (left) / flex map (right) — D-B */
|
/** List + Map: 420px fixed scrollable list (left) / flex map (right) — D-B */
|
||||||
const ListMapLayout: React.FC<{
|
const ListMapLayout: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -256,6 +271,7 @@ const LAYOUT_MAP: Record<
|
|||||||
}>
|
}>
|
||||||
> = {
|
> = {
|
||||||
'centered-form': CenteredFormLayout,
|
'centered-form': CenteredFormLayout,
|
||||||
|
'wide-form': WideFormLayout,
|
||||||
'list-map': ListMapLayout,
|
'list-map': ListMapLayout,
|
||||||
'list-detail': ListDetailLayout,
|
'list-detail': ListDetailLayout,
|
||||||
'grid-sidebar': GridSidebarLayout,
|
'grid-sidebar': GridSidebarLayout,
|
||||||
@@ -263,7 +279,7 @@ const LAYOUT_MAP: Record<
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Variants that show the stepper/total bar */
|
/** Variants that show the stepper/total bar */
|
||||||
const STEPPER_VARIANTS: WizardLayoutVariant[] = ['grid-sidebar', 'detail-toggles'];
|
const STEPPER_VARIANTS: WizardLayoutVariant[] = ['wide-form', 'grid-sidebar', 'detail-toggles'];
|
||||||
|
|
||||||
// ─── Component ───────────────────────────────────────────────────────────────
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user