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:
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user