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:
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
}}
|
||||
>
|
||||
<Box component="span" sx={{ color: 'text.secondary', fontWeight: 400, mr: 0.5 }}>
|
||||
Sort:
|
||||
</Box>
|
||||
{SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Recommended'}
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user