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

View File

@@ -5,7 +5,6 @@ import type { SxProps, Theme } from '@mui/material/styles';
import { DialogShell } from '../../atoms/DialogShell';
import { Button } from '../../atoms/Button';
import { Badge } from '../../atoms/Badge';
import { Link } from '../../atoms/Link';
// ─── Types ───────────────────────────────────────────────────────────────────
@@ -75,25 +74,18 @@ export const FilterPanel = React.forwardRef<HTMLDivElement, FilterPanelProps>(
<DialogShell
open={open}
onClose={handleClose}
title={
<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>
}
title={label}
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}>
Done
Apply
</Button>
</Box>
}

View File

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

View File

@@ -15,7 +15,7 @@ import { Switch } from '../../atoms/Switch';
import { Typography } from '../../atoms/Typography';
import { Divider } from '../../atoms/Divider';
// ──<EFBFBD><EFBFBD> Types ─────────────────<EFBFBD><EFBFBD><EFBFBD>─────────────────────────────────────────────────
// ── Types ──────────────────────────────────────────────────────────────────
/** Provider data for display in the list */
export interface ProviderData {
@@ -105,9 +105,10 @@ export interface ProvidersStepProps {
sx?: SxProps<Theme>;
}
// ─── Defaults ────<EFBFBD><EFBFBD>──────────────────────<EFBFBD><EFBFBD>───────────────────────────────────
// ─── Defaults ────────────────────────────────────────────────────────────────
const DEFAULT_TRADITIONS = [
'None',
'Anglican',
"Bahá'í",
'Baptist',
@@ -147,7 +148,28 @@ export const EMPTY_FILTER_VALUES: ProviderFilterValues = {
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.
@@ -159,9 +181,9 @@ export const EMPTY_FILTER_VALUES: ProviderFilterValues = {
* Click-to-navigate (D-D): clicking a provider card triggers
* navigation directly — no selection state or Continue button.
*
* Filters: service tradition (autocomplete), funeral type (chips),
* verified only (switch), online arrangements (switch), price range
* (dual-knob slider with editable inputs).
* Filters: location (chip + search), service tradition (autocomplete),
* funeral type (horizontal scroll chips), verified only (switch),
* online arrangements (switch), price range (slider + compact inputs).
*
* Pure presentation component — props in, callbacks out.
*
@@ -219,6 +241,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
// ─── Active filter count ───
const activeCount =
(searchQuery.trim() ? 1 : 0) +
(filterValues.tradition ? 1 : 0) +
filterValues.funeralTypes.length +
(filterValues.verifiedOnly ? 1 : 0) +
@@ -226,6 +249,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
(filterValues.priceRange[0] !== minPrice || filterValues.priceRange[1] !== maxPrice ? 1 : 0);
const handleClear = () => {
onSearchChange('');
onFilterChange({
...EMPTY_FILTER_VALUES,
priceRange: [minPrice, maxPrice],
@@ -318,9 +342,49 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
{/* Filters — right-aligned below search */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<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 ── */}
<Box>
<Typography variant="label" sx={{ mb: 1, display: 'block' }}>
<Typography variant="labelLg" sx={sectionHeadingSx}>
Service tradition
</Typography>
<Autocomplete
@@ -339,10 +403,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
{/* ── Funeral type ── */}
<Box>
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}>
<Typography variant="labelLg" sx={sectionHeadingSx}>
Funeral type
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
<Box sx={chipScrollSx}>
{funeralTypeOptions.map((option) => (
<Chip
key={option.value}
@@ -351,6 +415,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
onClick={() => handleFuneralTypeToggle(option.value)}
variant="outlined"
size="small"
sx={{ flexShrink: 0 }}
/>
))}
</Box>
@@ -390,10 +455,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
{/* ── Price range ── */}
<Box>
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}>
<Typography variant="labelLg" sx={sectionHeadingSx}>
Price range
</Typography>
<Box sx={{ px: 1, mb: 1.5 }}>
<Box sx={{ px: 1, mb: 1 }}>
<Slider
value={filterValues.priceRange}
onChange={(_, newValue) =>
@@ -410,7 +475,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
color="primary"
/>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField
size="small"
value={priceMinInput}
@@ -423,10 +488,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
inputProps={{
inputMode: 'numeric',
'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>
<TextField
@@ -441,8 +507,9 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
inputProps={{
inputMode: 'numeric',
'aria-label': 'Maximum price',
style: { padding: '6px 0' },
}}
sx={{ flex: 1 }}
sx={{ flex: 1, '& .MuiOutlinedInput-root': { fontSize: '0.875rem' } }}
/>
</Box>
</Box>