ProvidersStep mobile: unify list-view controls + PackagesStep drill-in fix

Controls
- Mobile list view's Filters/Sort/view-toggle now match the mobile map
  view's floating-chip treatment: white fill, neutral-300 border,
  shadow-sm. On mobile the sort label switches from "Sort: <value>" to
  a compact "Sort by"; desktop keeps its verbose label.
- List/Map toggle font bumped to 14px / 600 (button-small token), so
  it reads on the same line as Filters + Sort by both on mobile and
  on the desktop floating pill over the map panel.

PackagesStep drill-in
- Added a local hasDrilledIn flag so the mobile layout only swaps to
  the detail view after an explicit user tap on a package. Previously
  any pre-selection (the demo route seeds selectedPackageId to the
  first matching package for desktop auto-display) also forced mobile
  into the detail view, so users arriving from the map drawer saw a
  single package instead of the list. Back/forward from the detail
  resets the flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 22:55:26 +10:00
parent 22d14ef9bc
commit 6434d11384
2 changed files with 78 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
@@ -206,17 +206,25 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
const copy = TIER_COPY[providerTier];
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
// Mobile drill-in: when a package is selected on mobile, swap the list view
// for the detail view. Back button clears selection to return to the list.
// Mobile drill-in: on mobile, the list is the default view — only when the
// user explicitly taps a package do we swap in the detail panel. This
// distinguishes "parent pre-selected first package for desktop auto-display"
// (which should NOT jump to detail on mobile) from "user tapped a package".
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const mobileShowDetail = isMobile && selectedPackageId != null;
const [hasDrilledIn, setHasDrilledIn] = useState(false);
const mobileShowDetail = isMobile && hasDrilledIn && selectedPackageId != null;
const handleSelectPackage = (id: string | null) => {
setHasDrilledIn(id != null);
onSelectPackage(id);
};
useEffect(() => {
if (mobileShowDetail) window.scrollTo({ top: 0, behavior: 'auto' });
}, [mobileShowDetail]);
const handleLayoutBack = mobileShowDetail ? () => onSelectPackage(null) : onBack;
const handleLayoutBack = mobileShowDetail ? () => handleSelectPackage(null) : onBack;
const layoutBackLabel = mobileShowDetail ? 'Back to packages' : 'Back';
// Secondary list suppressed in "show all" mode.
@@ -349,7 +357,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
onClick={() => handleSelectPackage(pkg.id)}
/>
))}
@@ -385,7 +393,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
onClick={() => handleSelectPackage(pkg.id)}
/>
))}
</Box>

View File

@@ -687,7 +687,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
</Menu>
{/* View toggle — text labels on mobile, aligned height with
the buttons */}
the buttons; font matches Filters/Sort (14px / 600) */}
<ToggleButtonGroup
value={viewMode}
exclusive
@@ -700,11 +700,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
boxShadow: 'var(--fa-shadow-sm)',
'& .MuiToggleButton-root': {
height: 32,
px: 1.25,
px: 1.5,
py: 0,
textTransform: 'none',
fontSize: '0.8125rem',
fontWeight: 500,
fontSize: 'var(--fa-button-font-size-sm)',
fontWeight: 600,
borderColor: 'var(--fa-color-neutral-300)',
bgcolor: 'background.paper',
'&:hover': { bgcolor: 'background.paper' },
@@ -961,8 +961,8 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
height: 'var(--fa-button-height-sm)',
px: 1.5,
py: 0,
fontSize: '0.8125rem',
fontWeight: 500,
fontSize: 'var(--fa-button-font-size-sm)',
fontWeight: 600,
gap: 0.75,
border: '1px solid',
borderColor: 'divider',
@@ -1134,31 +1134,64 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
gap: 1,
}}
>
{/* Filters */}
{/* Filters — on mobile, matches the map-view floating chip style
(white fill, neutral-300 border, shadow-sm). On desktop,
default Button small look. */}
<FilterPanel
activeCount={activeCount}
onClear={handleClear}
sx={{ '& .MuiButton-root:focus-visible': { outline: 'none' } }}
sx={{
'& .MuiButton-root': {
height: { xs: 32, md: undefined },
bgcolor: { xs: 'background.paper', md: undefined },
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
boxShadow: { xs: 'var(--fa-shadow-sm)', md: 'none' },
'&:hover': {
bgcolor: { xs: 'background.paper', md: undefined },
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
},
'&:focus-visible': { outline: 'none' },
},
}}
>
{filterDialogChildren}
</FilterPanel>
{/* Sort — compact menu button, pushed right */}
{/* Sort — mobile shows a compact "Sort by" text button matching
the Filters chip style; desktop keeps the full "Sort: <value>"
label with its swap icon. */}
<Box sx={{ ml: 'auto' }}>
<Button
variant="outlined"
color="secondary"
size="small"
startIcon={<SwapVertIcon sx={{ fontSize: 16 }} />}
startIcon={isMobile ? undefined : <SwapVertIcon sx={{ fontSize: 16 }} />}
onClick={(e) => setSortAnchor(e.currentTarget)}
aria-haspopup="listbox"
aria-label={`Sort by ${SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Recommended'}`}
sx={{ textTransform: 'none', '&:focus-visible': { outline: 'none' } }}
sx={{
textTransform: 'none',
height: { xs: 32, md: undefined },
bgcolor: { xs: 'background.paper', md: undefined },
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
boxShadow: { xs: 'var(--fa-shadow-sm)', md: 'none' },
'&:hover': {
bgcolor: { xs: 'background.paper', md: undefined },
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
},
'&:focus-visible': { outline: 'none' },
}}
>
{isMobile ? (
'Sort by'
) : (
<>
<Box component="span" sx={{ color: 'text.secondary', fontWeight: 400, mr: 0.5 }}>
Sort:
</Box>
{SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Recommended'}
</>
)}
</Button>
<Menu
anchorEl={sortAnchor}
@@ -1183,7 +1216,9 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
</Menu>
</Box>
{/* Mobile-only view toggle — switches to the map-first layout */}
{/* Mobile-only view toggle — matches the map-view floating toggle:
text labels (List / Map), white fill, neutral-300 border, shadow,
14px / 600 type to align with the Filters + Sort by buttons. */}
<ToggleButtonGroup
value={viewMode}
exclusive
@@ -1192,13 +1227,18 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
aria-label="View mode"
sx={{
display: { xs: 'inline-flex', md: 'none' },
flexShrink: 0,
boxShadow: 'var(--fa-shadow-sm)',
'& .MuiToggleButton-root': {
px: 1,
py: 0.5,
gap: 0.5,
height: 32,
px: 1.5,
py: 0,
textTransform: 'none',
fontSize: '0.75rem',
fontWeight: 500,
fontSize: 'var(--fa-button-font-size-sm)',
fontWeight: 600,
borderColor: 'var(--fa-color-neutral-300)',
bgcolor: 'background.paper',
'&:hover': { bgcolor: 'background.paper' },
'&.Mui-selected': {
bgcolor: 'var(--fa-color-brand-100)',
color: 'primary.main',
@@ -1208,10 +1248,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
}}
>
<ToggleButton value="list" aria-label="List view">
<ViewListOutlinedIcon sx={{ fontSize: 16 }} />
List
</ToggleButton>
<ToggleButton value="map" aria-label="Map view">
<MapOutlinedIcon sx={{ fontSize: 16 }} />
Map
</ToggleButton>
</ToggleButtonGroup>
</Box>