ProvidersStep + MapPin: verified-icon alignment, filter tidy, drawer fixes

MapPin (both tiers)
- Verified providers now get an inline verified tick on the left of
  the name, matching the tick colour to the name so it reads as part
  of the label. Inline SVG (not @mui/icons-material) because MapPin
  is mounted via createRoot outside the ThemeProvider.
- Max label width bumped 180 → 210px to accommodate the icon without
  aggressively truncating long names.

Mobile cluster drawer rows
- Verified icon aligns with the name's top line (flex-start + 1.25em
  icon slot) — matches the desktop ClusterPopup layout.
- New right-aligned "From $X" price column, copper for verified.

Controls
- Mobile List/Map toggle: text labels (List, Map) instead of icons.
- Desktop List/Map toggle: resized to 32px height matching Filters +
  Sort buttons; bigger type, more padding.
- Search input corner radius now matches the button radius (8px)
  instead of the input radius (4px) so it reads as part of the chip
  set rather than a separate control.

Filter dialog (desktop + mobile)
- Remove the Location field — the sticky search bar is primary.
- Funeral-type chips bump small → medium.
- Reset filters button always renders; disabled when no filters are
  active (was previously hidden), so the affordance is discoverable.
- Provider-feature switches (Verified only, Online arrangements)
  align to the first text line so wrapped labels don't sink the
  switch visually below the second line.

Mobile drawer close
- drawerOpen now excludes the `exiting` phase, so tapping the X slides
  the drawer down immediately instead of lingering with a stale
  opacity fade. Visibility flips to hidden only after the slide ends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 22:31:12 +10:00
parent 7feb6582c4
commit 22d14ef9bc
3 changed files with 104 additions and 69 deletions

View File

@@ -25,7 +25,7 @@ export interface MapPinProps {
const PIN_PX = 'var(--fa-map-pin-padding-x)'; const PIN_PX = 'var(--fa-map-pin-padding-x)';
const PIN_RADIUS = 'var(--fa-map-pin-border-radius)'; const PIN_RADIUS = 'var(--fa-map-pin-border-radius)';
const NUB_SIZE = 'var(--fa-map-pin-nub-size)'; const NUB_SIZE = 'var(--fa-map-pin-nub-size)';
const MAX_WIDTH = 180; const MAX_WIDTH = 210;
// ─── Colour sets ──────────────────────────────────────────────────────────── // ─── Colour sets ────────────────────────────────────────────────────────────
@@ -141,7 +141,30 @@ export const MapPin = React.forwardRef<HTMLDivElement, MapPinProps>(
boxShadow: 'var(--fa-shadow-sm)', boxShadow: 'var(--fa-shadow-sm)',
}} }}
> >
{/* Name */} {/* Name row — verified icon (left) + name */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
maxWidth: '100%',
}}
>
{verified && (
// Inline SVG of Material's Verified (outlined) icon. Kept as
// inline SVG because MapPin is mounted via createRoot outside
// the MUI ThemeProvider, so @mui/icons-material wouldn't pick
// up theme defaults.
<svg
aria-hidden
width="12"
height="12"
viewBox="0 0 24 24"
style={{ flexShrink: 0, fill: palette.name }}
>
<path d="M23 11.99l-2.44-2.79.34-3.69-3.61-.82-1.89-3.2L12 2.96 8.6 1.49 6.71 4.69 3.1 5.5l.34 3.7L1 11.99l2.44 2.79-.34 3.7 3.61.82 1.89 3.2L12 21.03l3.4 1.47 1.89-3.2 3.61-.82-.34-3.69L23 11.99zm-12.91 4.72l-3.8-3.81 1.48-1.48 2.32 2.33 5.85-5.87 1.48 1.48-7.33 7.35z" />
</svg>
)}
<Box <Box
component="span" component="span"
sx={{ sx={{
@@ -153,11 +176,12 @@ export const MapPin = React.forwardRef<HTMLDivElement, MapPinProps>(
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
maxWidth: '100%', minWidth: 0,
}} }}
> >
{name} {name}
</Box> </Box>
</Box>
{/* Price line */} {/* Price line */}
{hasPrice && ( {hasPrice && (

View File

@@ -77,8 +77,14 @@ export const FilterPanel = React.forwardRef<HTMLDivElement, FilterPanelProps>(
title={label} title={label}
footer={ footer={
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{onClear && activeCount > 0 ? ( {onClear ? (
<Button variant="text" size="small" color="secondary" onClick={() => onClear()}> <Button
variant="text"
size="small"
color="secondary"
onClick={() => onClear()}
disabled={activeCount === 0}
>
Reset filters Reset filters
</Button> </Button>
) : ( ) : (

View File

@@ -337,44 +337,6 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
* and the mobile-map floating FilterPanel. */ * and the mobile-map floating FilterPanel. */
const filterDialogChildren = ( const filterDialogChildren = (
<> <>
{/* ── Location ── */}
<Box>
<Typography variant="labelLg" sx={sectionHeadingSx}>
Location
</Typography>
<Autocomplete
multiple
freeSolo
value={searchQuery.trim() ? [searchQuery.trim()] : []}
onChange={(_, newValue) => {
const last = newValue[newValue.length - 1] ?? '';
onSearchChange(typeof last === 'string' ? last : '');
}}
options={[]}
renderInput={(params) => (
<TextField
{...params}
placeholder={searchQuery.trim() ? '' : 'Search a town or suburb...'}
size="small"
InputProps={{
...params.InputProps,
startAdornment: (
<>
<InputAdornment position="start" sx={{ ml: 0.5 }}>
<LocationOnOutlinedIcon sx={{ color: 'text.secondary', fontSize: 18 }} />
</InputAdornment>
{params.InputProps.startAdornment}
</>
),
}}
/>
)}
size="small"
/>
</Box>
<Divider />
{/* ── Service tradition ── */} {/* ── Service tradition ── */}
<Box> <Box>
<Typography variant="labelLg" sx={sectionHeadingSx}> <Typography variant="labelLg" sx={sectionHeadingSx}>
@@ -407,7 +369,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
selected={filterValues.funeralTypes.includes(option.value)} selected={filterValues.funeralTypes.includes(option.value)}
onClick={() => handleFuneralTypeToggle(option.value)} onClick={() => handleFuneralTypeToggle(option.value)}
variant="outlined" variant="outlined"
size="small" size="medium"
/> />
))} ))}
</Box> </Box>
@@ -415,7 +377,8 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
<Divider /> <Divider />
{/* ── Provider features ── */} {/* ── Provider features ── Switch aligned to the first text line so
wrapped labels read cleanly on narrow screens */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<FormControlLabel <FormControlLabel
control={ control={
@@ -425,7 +388,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
/> />
} }
label="Verified providers only" label="Verified providers only"
sx={{ mx: 0 }} sx={{
mx: 0,
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': { pt: 0.75 },
}}
/> />
<FormControlLabel <FormControlLabel
control={ control={
@@ -437,7 +404,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
/> />
} }
label="Online arrangements available" label="Online arrangements available"
sx={{ mx: 0 }} sx={{
mx: 0,
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': { pt: 0.75 },
}}
/> />
</Box> </Box>
@@ -510,7 +481,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
if (showMobileMapLayout) { if (showMobileMapLayout) {
const active = mapActive ?? null; const active = mapActive ?? null;
const drawerOpen = !!(active && (active.provider || active.cluster)); // Drawer is "open" only when there's an active selection AND the map
// isn't in the middle of its exit animation. Flipping to false on
// `exiting` kicks off the slide-down transform immediately, so the user
// sees the drawer leave as soon as they tap the close X.
const drawerOpen = !!(active && !active.exiting && (active.provider || active.cluster));
const drawerProvider = active?.provider ?? null; const drawerProvider = active?.provider ?? null;
const drawerCluster = active?.cluster ?? null; const drawerCluster = active?.cluster ?? null;
@@ -624,6 +599,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
bgcolor: 'background.paper', bgcolor: 'background.paper',
boxShadow: 'var(--fa-shadow-sm)', boxShadow: 'var(--fa-shadow-sm)',
borderRadius: 'var(--fa-button-border-radius-default)',
}, },
'& .MuiOutlinedInput-notchedOutline': { '& .MuiOutlinedInput-notchedOutline': {
borderColor: 'var(--fa-color-neutral-300)', borderColor: 'var(--fa-color-neutral-300)',
@@ -710,7 +686,8 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
))} ))}
</Menu> </Menu>
{/* View toggle — icon-only, aligned height with the buttons */} {/* View toggle — text labels on mobile, aligned height with
the buttons */}
<ToggleButtonGroup <ToggleButtonGroup
value={viewMode} value={viewMode}
exclusive exclusive
@@ -723,8 +700,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
boxShadow: 'var(--fa-shadow-sm)', boxShadow: 'var(--fa-shadow-sm)',
'& .MuiToggleButton-root': { '& .MuiToggleButton-root': {
height: 32, height: 32,
px: 1, px: 1.25,
py: 0, py: 0,
textTransform: 'none',
fontSize: '0.8125rem',
fontWeight: 500,
borderColor: 'var(--fa-color-neutral-300)', borderColor: 'var(--fa-color-neutral-300)',
bgcolor: 'background.paper', bgcolor: 'background.paper',
'&:hover': { bgcolor: 'background.paper' }, '&:hover': { bgcolor: 'background.paper' },
@@ -737,10 +717,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
}} }}
> >
<ToggleButton value="list" aria-label="List view"> <ToggleButton value="list" aria-label="List view">
<ViewListOutlinedIcon sx={{ fontSize: 16 }} /> List
</ToggleButton> </ToggleButton>
<ToggleButton value="map" aria-label="Map view"> <ToggleButton value="map" aria-label="Map view">
<MapOutlinedIcon sx={{ fontSize: 16 }} /> Map
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</Box> </Box>
@@ -771,9 +751,9 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
borderTopRightRadius: 16, borderTopRightRadius: 16,
boxShadow: 'var(--fa-shadow-lg)', boxShadow: 'var(--fa-shadow-lg)',
transform: drawerOpen ? 'translateY(0)' : 'translateY(100%)', transform: drawerOpen ? 'translateY(0)' : 'translateY(100%)',
opacity: mapActive?.exiting ? 0 : 1, transition: 'transform 220ms ease-out',
transition: 'transform 220ms ease-out, opacity 180ms ease-out',
pointerEvents: drawerOpen ? 'auto' : 'none', pointerEvents: drawerOpen ? 'auto' : 'none',
visibility: drawerOpen || mapActive?.exiting ? 'visible' : 'hidden',
}} }}
> >
{/* Drawer header — holds the close X (and the cluster count when {/* Drawer header — holds the close X (and the cluster count when
@@ -845,16 +825,21 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
px: 2, px: 2,
py: 1.25, py: 1.25,
gap: 1, gap: 1,
// Start-align so the verified icon sits on the
// name's baseline (matches desktop ClusterPopup)
alignItems: 'flex-start',
borderBottom: '1px solid', borderBottom: '1px solid',
borderColor: 'divider', borderColor: 'divider',
'&:last-of-type': { borderBottom: 'none' }, '&:last-of-type': { borderBottom: 'none' },
'&:hover': { bgcolor: 'var(--fa-color-surface-subtle)' }, '&:hover': { bgcolor: 'var(--fa-color-surface-subtle)' },
}} }}
> >
{/* Verified-icon slot (aligns all names) */} {/* Verified-icon slot — height tuned to the name's
line-box so the tick aligns with the title top */}
<Box <Box
sx={{ sx={{
width: 18, width: 18,
height: '1.25em',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexShrink: 0, flexShrink: 0,
@@ -892,6 +877,26 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
)} )}
</Box> </Box>
</Box> </Box>
{/* Price column — right-aligned "From $X" */}
{p.startingPrice != null && (
<Box sx={{ flexShrink: 0, textAlign: 'right', pl: 1 }}>
<Typography
variant="caption"
sx={{ display: 'block', color: 'text.secondary' }}
>
From
</Typography>
<Typography
variant="body2"
sx={{
fontWeight: 600,
color: p.verified ? 'primary.main' : 'text.primary',
}}
>
${p.startingPrice.toLocaleString('en-AU')}
</Typography>
</Box>
)}
</ButtonBase> </ButtonBase>
))} ))}
</Box> </Box>
@@ -938,7 +943,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
sx={sx} sx={sx}
secondaryPanel={ secondaryPanel={
<Box sx={{ position: 'relative', flex: 1, display: 'flex' }}> <Box sx={{ position: 'relative', flex: 1, display: 'flex' }}>
{/* Floating view toggle */} {/* Floating view toggle — sized to match Filters/Sort buttons */}
<ToggleButtonGroup <ToggleButtonGroup
value={viewMode} value={viewMode}
exclusive exclusive
@@ -952,13 +957,13 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
zIndex: 1, zIndex: 1,
bgcolor: 'background.paper', bgcolor: 'background.paper',
boxShadow: 'var(--fa-shadow-md)', boxShadow: 'var(--fa-shadow-md)',
borderRadius: 1,
'& .MuiToggleButton-root': { '& .MuiToggleButton-root': {
height: 'var(--fa-button-height-sm)',
px: 1.5, px: 1.5,
py: 0.5, py: 0,
fontSize: '0.75rem', fontSize: '0.8125rem',
fontWeight: 500, fontWeight: 500,
gap: 0.5, gap: 0.75,
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
textTransform: 'none', textTransform: 'none',