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