Compare commits

..

2 Commits

Author SHA1 Message Date
62a9db4e64 Strip AI tooling and working docs for dev push 2026-05-22 11:55:25 +10:00
abd8d2d2da ProviderMap: verified providers always show as MapPopup; drawer + list polish
Verified providers render as always-on MapPopup cards inside the marker
layer (clusterable), while unverified keep click-to-reveal pins. Mobile
taps open the drawer instead of navigating directly. MapProviderDrawer
gets a "View Packages" CTA button, light-grey header bar, and the
provider list doubles card spacing. Fixes demo:dev --mode flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 11:53:25 +10:00
4 changed files with 94 additions and 46 deletions

View File

@@ -19,7 +19,7 @@
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"chromatic": "chromatic --exit-zero-on-changes --build-script-name=build:storybook",
"demo:dev": "vite -c vite.demo.config.ts",
"demo:dev": "vite -c vite.demo.config.ts --mode arrangement",
"demo:build": "vite build -c vite.demo.config.ts",
"demo:publish": "npm run demo:build -- --mode arrangement && ./scripts/deploy-demo.sh arrangement",
"prepare": "husky"

View File

@@ -6,6 +6,7 @@ import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import type { SxProps, Theme } from '@mui/material/styles';
import { Button } from '../../atoms/Button';
import { IconButton } from '../../atoms/IconButton';
import { Typography } from '../../atoms/Typography';
import { ProviderCard } from '../ProviderCard';
@@ -198,6 +199,9 @@ export const MapProviderDrawer = React.forwardRef<HTMLDivElement, MapProviderDra
px: 2,
py: 0.5,
gap: 1,
bgcolor: 'var(--fa-color-surface-subtle)',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
{cluster && !provider && (
@@ -221,10 +225,10 @@ export const MapProviderDrawer = React.forwardRef<HTMLDivElement, MapProviderDra
</IconButton>
</Box>
{/* Single-provider content — entire card clickable. Card runs
edge-to-edge with all corners squared; the drawer Paper provides
the top radius. */}
{/* Single-provider content — card is display-only; a CTA button
below handles navigation to the provider's packages. */}
{provider && (
<Box>
<ProviderCard
name={provider.name}
location={provider.location}
@@ -234,10 +238,14 @@ export const MapProviderDrawer = React.forwardRef<HTMLDivElement, MapProviderDra
rating={provider.rating}
reviewCount={provider.reviewCount}
startingPrice={provider.startingPrice}
onClick={() => onSelectProvider(provider.id)}
aria-label={`${provider.name}, ${provider.location}. Tap to view packages.`}
sx={{ borderRadius: 0, boxShadow: 'none', border: 'none' }}
/>
<Box sx={{ px: 2, pb: 2, pt: 1 }}>
<Button variant="contained" fullWidth onClick={() => onSelectProvider(provider.id)}>
View Packages
</Button>
</Box>
</Box>
)}
{/* Cluster list content — tap a row to drill in */}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { createRoot, type Root } from 'react-dom/client';
import Box from '@mui/material/Box';
import type { SxProps, Theme } from '@mui/material/styles';
import { ThemeProvider, useTheme } from '@mui/material/styles';
import {
APIProvider,
Map as GoogleMap,
@@ -139,20 +140,33 @@ const MapRefCapture: React.FC<{
const MarkerLayer: React.FC<{
providers: ProviderData[];
hiddenIds: Set<string>;
theme: Theme;
externalisePopups: boolean;
onPinClick: (id: string) => void;
onSelectProvider: (id: string) => void;
onClusterClick: (providers: ProviderData[], position: google.maps.LatLngLiteral) => void;
}> = ({ providers, hiddenIds, onPinClick, onClusterClick }) => {
}> = ({
providers,
hiddenIds,
theme,
externalisePopups,
onPinClick,
onSelectProvider,
onClusterClick,
}) => {
const map = useMap();
const markerLibrary = useMapsLibrary('marker');
// Stash callbacks in a ref so the effect below doesn't re-run (and rebuild
// every marker) when the parent passes fresh arrow-function references.
const onPinClickRef = React.useRef(onPinClick);
const onSelectProviderRef = React.useRef(onSelectProvider);
const onClusterClickRef = React.useRef(onClusterClick);
React.useEffect(() => {
onPinClickRef.current = onPinClick;
onSelectProviderRef.current = onSelectProvider;
onClusterClickRef.current = onClusterClick;
}, [onPinClick, onClusterClick]);
}, [onPinClick, onSelectProvider, onClusterClick]);
React.useEffect(() => {
if (!map || !markerLibrary) return;
@@ -165,9 +179,26 @@ const MarkerLayer: React.FC<{
.map((p) => {
const el = document.createElement('div');
const root = createRoot(el);
// MapPin's own onClick stays for keyboard a11y (Enter/Space via its
// onKeyDown). stopPropagation guards against the DOM click bubbling
// to the Map's onClick and closing the popup the same frame it opens.
if (p.verified) {
root.render(
<ThemeProvider theme={theme}>
<MapPopup
name={p.name}
imageUrl={p.imageUrl}
price={p.startingPrice}
location={p.location}
rating={p.rating}
verified
onClick={() =>
externalisePopups
? onPinClickRef.current(p.id)
: onSelectProviderRef.current(p.id)
}
/>
</ThemeProvider>,
);
} else {
root.render(
<MapPin
name={p.name}
@@ -179,6 +210,7 @@ const MarkerLayer: React.FC<{
}}
/>,
);
}
roots.push(root);
const marker = new markerLibrary.AdvancedMarkerElement({
@@ -186,13 +218,12 @@ const MarkerLayer: React.FC<{
content: el,
gmpClickable: true,
});
// Also listen at the Google Maps level + stop the GMaps event so
// Map's onClick can't fire when a pin is clicked via mouse. Safe to
// fire twice with keyboard — handlePinClick is idempotent.
if (!p.verified) {
marker.addListener('click', (event: google.maps.MapMouseEvent) => {
event.stop();
onPinClickRef.current(p.id);
});
}
markerToProvider.set(marker, p);
return marker;
});
@@ -319,6 +350,7 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
},
ref,
) => {
const muiTheme = useTheme();
const [activeProviderId, setActiveProviderId] = React.useState<string | null>(null);
const [activeCluster, setActiveCluster] = React.useState<ActiveCluster | null>(null);
const [exiting, setExiting] = React.useState(false);
@@ -354,14 +386,18 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
);
// Pins hidden from the map (because their popup is showing instead).
// Verified providers are excluded — their marker IS the MapPopup.
const hiddenIds = React.useMemo(() => {
const s = new Set<string>();
if (effectiveProviderId) s.add(effectiveProviderId);
if (effectiveProviderId) {
const p = withCoords.find((prov) => prov.id === effectiveProviderId);
if (p && !p.verified) s.add(effectiveProviderId);
}
if (activeCluster) {
activeCluster.providers.forEach((p) => s.add(p.id));
}
return s;
}, [effectiveProviderId, activeCluster]);
}, [effectiveProviderId, activeCluster, withCoords]);
const handlePinClick = React.useCallback(
(id: string) => {
@@ -497,13 +533,17 @@ export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>
<MarkerLayer
providers={withCoords}
hiddenIds={hiddenIds}
theme={muiTheme}
externalisePopups={externalisePopups}
onPinClick={handlePinClick}
onSelectProvider={onSelectProvider}
onClusterClick={handleClusterClick}
/>
{/* Internal popups — skipped when caller externalises them (e.g.
mobile drawer). Active state still flows via onActiveChange. */}
{!externalisePopups && activeProvider && (
{/* Click-to-reveal popup for unverified providers. Verified
providers are always rendered as MapPopup inside MarkerLayer,
so they don't need this path. */}
{!externalisePopups && activeProvider && !activeProvider.verified && (
<AdvancedMarker position={activeProvider.coords!} zIndex={1000}>
<MapPopup
name={activeProvider.name}

View File

@@ -807,7 +807,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
gap: 4,
pb: 3,
pt: 2,
px: { xs: 2, md: 3 },