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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user