Feedback iteration: DialogShell, page consistency, popup standardisation

- Add DialogShell atom — shared dialog container (header, scrollable body, footer)
- Refactor FilterPanel to use DialogShell (Popover → centered Dialog)
- Refactor ArrangementDialog to use DialogShell
- Remove PreviewStep + AuthGateStep pages (consolidated into ArrangementDialog, D-E)
- IntroStep: static subheading, top-left aligned toggle button content
- ProvidersStep: h4 heading "Find a funeral director", location search with pin icon,
  filter moved below search right-aligned, map fill fix, hover scrollbar
- VenueStep: same consistency fixes (h4 heading, filter layout, location icon, map fix)
- PackagesStep: grouped packages ("Matching your preferences" / "Other packages from
  [Provider]"), removed budget filter + Most Popular badge, clickable provider card,
  onArrange replaces onContinue, h4 heading
- WizardLayout: list-map left panel gets thin scrollbar visible on hover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 12:20:26 +11:00
parent 5c3e0c4e56
commit 1faa320f4b
22 changed files with 904 additions and 1721 deletions

View File

@@ -16,7 +16,6 @@ const meta: Meta<typeof FilterPanel> = {
argTypes: {
label: { control: 'text' },
activeCount: { control: 'number' },
minWidth: { control: 'number' },
},
decorators: [
(Story) => (
@@ -64,7 +63,6 @@ export const SelectFilters: Story = {
args: {
activeCount: 1,
onClear: () => {},
minWidth: 300,
children: (
<>
<TextField select label="Category" value="solid_timber" fullWidth>

View File

@@ -1,13 +1,11 @@
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 { DialogShell } from '../../atoms/DialogShell';
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 ───────────────────────────────────────────────────────────────────
@@ -17,12 +15,10 @@ export interface FilterPanelProps {
label?: string;
/** Number of active filters (shown as count on the trigger) */
activeCount?: number;
/** Filter controls — rendered inside the Popover body */
/** Filter controls — rendered inside the dialog 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>;
}
@@ -32,29 +28,18 @@ export interface FilterPanelProps {
/**
* Reusable filter panel for the FA arrangement wizard.
*
* Renders a trigger button ("Filters") that opens a Popover containing
* Renders a trigger button ("Filters") that opens a DialogShell 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.
*/
export const FilterPanel = React.forwardRef<HTMLDivElement, FilterPanelProps>(
({ label = 'Filters', activeCount = 0, children, onClear, minWidth = 280, sx }, ref) => {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const uniqueId = React.useId();
const popoverId = `filter-panel-${uniqueId}`;
const headingId = `filter-panel-heading-${uniqueId}`;
({ label = 'Filters', activeCount = 0, children, onClear, sx }, ref) => {
const [open, setOpen] = React.useState(false);
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<>
@@ -66,7 +51,6 @@ export const FilterPanel = React.forwardRef<HTMLDivElement, FilterPanelProps>(
size="small"
startIcon={<TuneIcon />}
onClick={handleOpen}
aria-controls={open ? popoverId : undefined}
aria-expanded={open}
aria-haspopup="dialog"
>
@@ -87,94 +71,35 @@ export const FilterPanel = React.forwardRef<HTMLDivElement, FilterPanelProps>(
</Button>
</Box>
{/* Popover panel */}
<Popover
id={popoverId}
{/* Filter dialog */}
<DialogShell
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
slotProps={{
paper: {
sx: {
minWidth,
maxHeight: '70vh',
mt: 1,
borderRadius: 2,
boxShadow: 3,
display: 'flex',
flexDirection: 'column',
},
},
}}
PaperProps={{
role: 'dialog' as const,
'aria-labelledby': headingId,
}}
>
{/* Header */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2.5,
pt: 2,
pb: 1.5,
flexShrink: 0,
}}
>
<Typography id={headingId} variant="h6">
title={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{label}
</Typography>
{onClear && activeCount > 0 && (
<Link
component="button"
onClick={() => {
onClear();
}}
underline="hover"
sx={{ fontSize: (theme: Theme) => theme.typography.caption.fontSize }}
>
Clear all
</Link>
)}
</Box>
<Divider />
{/* Filter controls */}
<Box
sx={{
px: 2.5,
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2.5,
overflowY: 'auto',
flex: 1,
}}
>
{children}
</Box>
<Divider />
{/* Footer — done button */}
<Box
sx={{ px: 2.5, py: 1.5, display: 'flex', justifyContent: 'flex-end', flexShrink: 0 }}
>
<Button
variant="contained"
size="small"
onClick={handleClose}
aria-label="Close filters"
>
Done
</Button>
</Box>
</Popover>
{onClear && activeCount > 0 && (
<Link
component="button"
onClick={() => onClear()}
underline="hover"
sx={{ fontSize: (theme: Theme) => theme.typography.caption.fontSize }}
>
Clear all
</Link>
)}
</Box>
}
footer={
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button variant="contained" size="small" onClick={handleClose}>
Done
</Button>
</Box>
}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>{children}</Box>
</DialogShell>
</>
);
},