Filter panel UX refinements: headings, layout, location chip, padding

- DialogShell: bump px from 3 to 5 (20px) for more breathing room
- FilterPanel: "Done" → "Apply", move "Clear all" to footer as "Reset filters"
- ProvidersStep filters:
  - Section headings: labelLg + fontWeight 600 for visual hierarchy
  - Funeral type chips: horizontal scroll instead of wrap
  - Location section: chip showing current search + editable input
  - Price inputs: compact fontSize 0.875rem + tighter padding
  - Service tradition: 'None' added as first option
  - Active count includes location search
  - Reset clears search query too

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 16:07:32 +11:00
parent 7dea9f5855
commit 89ba86565a
4 changed files with 97 additions and 38 deletions

View File

@@ -108,7 +108,7 @@ export const DialogShell = React.forwardRef<HTMLDivElement, DialogShellProps>(
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
px: 3, px: 5,
pt: 2.5, pt: 2.5,
pb: 2, pb: 2,
flexShrink: 0, flexShrink: 0,
@@ -149,7 +149,7 @@ export const DialogShell = React.forwardRef<HTMLDivElement, DialogShellProps>(
{/* Scrollable body */} {/* Scrollable body */}
<Box <Box
sx={{ sx={{
px: 3, px: 5,
py: 2.5, py: 2.5,
overflowY: 'auto', overflowY: 'auto',
flex: 1, flex: 1,
@@ -162,7 +162,7 @@ export const DialogShell = React.forwardRef<HTMLDivElement, DialogShellProps>(
{footer && ( {footer && (
<> <>
<Divider /> <Divider />
<Box sx={{ px: 3, py: 2, flexShrink: 0 }}>{footer}</Box> <Box sx={{ px: 5, py: 2, flexShrink: 0 }}>{footer}</Box>
</> </>
)} )}
</Dialog> </Dialog>

View File

@@ -5,7 +5,6 @@ import type { SxProps, Theme } from '@mui/material/styles';
import { DialogShell } from '../../atoms/DialogShell'; import { DialogShell } from '../../atoms/DialogShell';
import { Button } from '../../atoms/Button'; import { Button } from '../../atoms/Button';
import { Badge } from '../../atoms/Badge'; import { Badge } from '../../atoms/Badge';
import { Link } from '../../atoms/Link';
// ─── Types ─────────────────────────────────────────────────────────────────── // ─── Types ───────────────────────────────────────────────────────────────────
@@ -75,25 +74,18 @@ export const FilterPanel = React.forwardRef<HTMLDivElement, FilterPanelProps>(
<DialogShell <DialogShell
open={open} open={open}
onClose={handleClose} onClose={handleClose}
title={ title={label}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{label}
{onClear && activeCount > 0 && (
<Link
component="button"
onClick={() => onClear()}
underline="hover"
sx={{ fontSize: (theme: Theme) => theme.typography.caption.fontSize }}
>
Clear all
</Link>
)}
</Box>
}
footer={ footer={
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{onClear && activeCount > 0 ? (
<Button variant="text" size="small" color="secondary" onClick={() => onClear()}>
Reset filters
</Button>
) : (
<Box />
)}
<Button variant="contained" size="small" onClick={handleClose}> <Button variant="contained" size="small" onClick={handleClose}>
Done Apply
</Button> </Button>
</Box> </Box>
} }

View File

@@ -140,10 +140,10 @@ export const Default: Story = {
// ─── With active filters ──────────────────────────────────────────────────── // ─── With active filters ────────────────────────────────────────────────────
/** Filters pre-applied — verified only + price cap */ /** Filters pre-applied — location, tradition, type, verified, price cap */
export const WithActiveFilters: Story = { export const WithActiveFilters: Story = {
render: () => { render: () => {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('Wollongong, NSW');
const [filters, setFilters] = useState<ProviderFilterValues>({ const [filters, setFilters] = useState<ProviderFilterValues>({
tradition: 'Catholic', tradition: 'Catholic',
funeralTypes: ['service_and_cremation'], funeralTypes: ['service_and_cremation'],

View File

@@ -15,7 +15,7 @@ import { Switch } from '../../atoms/Switch';
import { Typography } from '../../atoms/Typography'; import { Typography } from '../../atoms/Typography';
import { Divider } from '../../atoms/Divider'; import { Divider } from '../../atoms/Divider';
// ──<EFBFBD><EFBFBD> Types ─────────────────<EFBFBD><EFBFBD><EFBFBD>───────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────────
/** Provider data for display in the list */ /** Provider data for display in the list */
export interface ProviderData { export interface ProviderData {
@@ -105,9 +105,10 @@ export interface ProvidersStepProps {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
// ─── Defaults ────<EFBFBD><EFBFBD>──────────────────────<EFBFBD><EFBFBD>─────────────────────────────────── // ─── Defaults ────────────────────────────────────────────────────────────────
const DEFAULT_TRADITIONS = [ const DEFAULT_TRADITIONS = [
'None',
'Anglican', 'Anglican',
"Bahá'í", "Bahá'í",
'Baptist', 'Baptist',
@@ -147,7 +148,28 @@ export const EMPTY_FILTER_VALUES: ProviderFilterValues = {
priceRange: [0, 15000], priceRange: [0, 15000],
}; };
// ──<EFBFBD><EFBFBD><EFBFBD> Component ─────────────────────────────────────────────────────<EFBFBD><EFBFBD><EFBFBD>───────── // ─── Shared styles ───────────────────────────────────────────────────────────
/** Section heading inside the filter panel */
const sectionHeadingSx = {
mb: 1.5,
display: 'block',
fontWeight: 600,
color: 'text.primary',
} as const;
/** Horizontal scrolling chip row — hides scrollbar, no wrap */
const chipScrollSx = {
display: 'flex',
gap: 1,
overflowX: 'auto',
flexWrap: 'nowrap',
pb: 0.5,
'&::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
} as const;
// ─── Component ───────────────────────────────────────────────────────────────
/** /**
* Step 2 — Provider selection page for the FA arrangement wizard. * Step 2 — Provider selection page for the FA arrangement wizard.
@@ -159,9 +181,9 @@ export const EMPTY_FILTER_VALUES: ProviderFilterValues = {
* Click-to-navigate (D-D): clicking a provider card triggers * Click-to-navigate (D-D): clicking a provider card triggers
* navigation directly — no selection state or Continue button. * navigation directly — no selection state or Continue button.
* *
* Filters: service tradition (autocomplete), funeral type (chips), * Filters: location (chip + search), service tradition (autocomplete),
* verified only (switch), online arrangements (switch), price range * funeral type (horizontal scroll chips), verified only (switch),
* (dual-knob slider with editable inputs). * online arrangements (switch), price range (slider + compact inputs).
* *
* Pure presentation component — props in, callbacks out. * Pure presentation component — props in, callbacks out.
* *
@@ -219,6 +241,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
// ─── Active filter count ─── // ─── Active filter count ───
const activeCount = const activeCount =
(searchQuery.trim() ? 1 : 0) +
(filterValues.tradition ? 1 : 0) + (filterValues.tradition ? 1 : 0) +
filterValues.funeralTypes.length + filterValues.funeralTypes.length +
(filterValues.verifiedOnly ? 1 : 0) + (filterValues.verifiedOnly ? 1 : 0) +
@@ -226,6 +249,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
(filterValues.priceRange[0] !== minPrice || filterValues.priceRange[1] !== maxPrice ? 1 : 0); (filterValues.priceRange[0] !== minPrice || filterValues.priceRange[1] !== maxPrice ? 1 : 0);
const handleClear = () => { const handleClear = () => {
onSearchChange('');
onFilterChange({ onFilterChange({
...EMPTY_FILTER_VALUES, ...EMPTY_FILTER_VALUES,
priceRange: [minPrice, maxPrice], priceRange: [minPrice, maxPrice],
@@ -318,9 +342,49 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
{/* Filters — right-aligned below search */} {/* Filters — right-aligned below search */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<FilterPanel activeCount={activeCount} onClear={handleClear}> <FilterPanel activeCount={activeCount} onClear={handleClear}>
{/* ── Location ── */}
<Box>
<Typography variant="labelLg" sx={sectionHeadingSx}>
Location
</Typography>
{searchQuery.trim() && (
<Box sx={{ mb: 1 }}>
<Chip
label={searchQuery.trim()}
onDelete={() => onSearchChange('')}
variant="filled"
color="primary"
size="small"
/>
</Box>
)}
<TextField
placeholder="Search a town or suburb..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && onSearch) {
e.preventDefault();
onSearch(searchQuery);
}
}}
fullWidth
size="small"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LocationOnOutlinedIcon sx={{ color: 'text.secondary', fontSize: 18 }} />
</InputAdornment>
),
}}
/>
</Box>
<Divider />
{/* ── Service tradition ── */} {/* ── Service tradition ── */}
<Box> <Box>
<Typography variant="label" sx={{ mb: 1, display: 'block' }}> <Typography variant="labelLg" sx={sectionHeadingSx}>
Service tradition Service tradition
</Typography> </Typography>
<Autocomplete <Autocomplete
@@ -339,10 +403,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
{/* ── Funeral type ── */} {/* ── Funeral type ── */}
<Box> <Box>
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}> <Typography variant="labelLg" sx={sectionHeadingSx}>
Funeral type Funeral type
</Typography> </Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}> <Box sx={chipScrollSx}>
{funeralTypeOptions.map((option) => ( {funeralTypeOptions.map((option) => (
<Chip <Chip
key={option.value} key={option.value}
@@ -351,6 +415,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
onClick={() => handleFuneralTypeToggle(option.value)} onClick={() => handleFuneralTypeToggle(option.value)}
variant="outlined" variant="outlined"
size="small" size="small"
sx={{ flexShrink: 0 }}
/> />
))} ))}
</Box> </Box>
@@ -390,10 +455,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
{/* ── Price range ── */} {/* ── Price range ── */}
<Box> <Box>
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}> <Typography variant="labelLg" sx={sectionHeadingSx}>
Price range Price range
</Typography> </Typography>
<Box sx={{ px: 1, mb: 1.5 }}> <Box sx={{ px: 1, mb: 1 }}>
<Slider <Slider
value={filterValues.priceRange} value={filterValues.priceRange}
onChange={(_, newValue) => onChange={(_, newValue) =>
@@ -410,7 +475,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
color="primary" color="primary"
/> />
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField <TextField
size="small" size="small"
value={priceMinInput} value={priceMinInput}
@@ -423,10 +488,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
inputProps={{ inputProps={{
inputMode: 'numeric', inputMode: 'numeric',
'aria-label': 'Minimum price', 'aria-label': 'Minimum price',
style: { padding: '6px 0' },
}} }}
sx={{ flex: 1 }} sx={{ flex: 1, '& .MuiOutlinedInput-root': { fontSize: '0.875rem' } }}
/> />
<Typography variant="body2" color="text.secondary" sx={{ alignSelf: 'center' }}> <Typography variant="caption" color="text.secondary">
</Typography> </Typography>
<TextField <TextField
@@ -441,8 +507,9 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
inputProps={{ inputProps={{
inputMode: 'numeric', inputMode: 'numeric',
'aria-label': 'Maximum price', 'aria-label': 'Maximum price',
style: { padding: '6px 0' },
}} }}
sx={{ flex: 1 }} sx={{ flex: 1, '& .MuiOutlinedInput-root': { fontSize: '0.875rem' } }}
/> />
</Box> </Box>
</Box> </Box>