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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user