ProvidersStep: extract MapProviderDrawer + unify control chrome + fix search button drift

- New molecule MapProviderDrawer lifts the mobile-map bottom drawer out
  of ProvidersStep (~120 lines): Paper + close-X header + single-pin
  ProviderCard content / cluster-list content + slide-up animation.
  Props: `active: ProviderMapActiveState | null`, `onClose`,
  `onSelectProvider`, `onDrillIntoProvider`. Three Storybook states
  (SingleProvider, Cluster, ClusterPair, Closed) so the drawer can be
  iterated without a live map. ProvidersStep now consumes it as a
  single line wired to mapRef.clearActive + mapRef.drillIntoProvider.

- Shared visual tokens for the control cluster (Search, Filters, Sort by,
  List/Map toggle) factored into a CONTROL_CHROME constant and three
  typed sx objects (controlButtonSx, controlToggleSx, controlInputSx,
  filterTriggerSx) so all four controls share the same outline, radius,
  fill, and shadow across mobile list, mobile map, and desktop. Desktop
  map-panel floating toggle also re-threaded through controlToggleSx.

- Mobile list control order now matches mobile map: Sort by is grouped
  left next to Filters (not pushed right with a ml:auto wrapper), and
  the List/Map toggle is right-pinned via ml:auto on xs. Desktop keeps
  Sort pushed right (no toggle rendered on desktop in this slot).

- Fix: the magnifying-glass commit button was drifting 19–30px left as
  the input filled with chips / draft text. Root cause: overriding
  `InputProps.endAdornment` on Autocomplete bypasses MUI's
  `.MuiAutocomplete-endAdornment` absolute positioning, leaving our
  `.MuiInputAdornment-positionEnd` as `position: static` in flex flow.
  controlInputSx now re-absolutely-anchors the end adornment at the
  right edge and reserves `pr: 5` so input content can't slide under it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 09:39:52 +10:00
parent 6434d11384
commit 30ec88ceaf
6 changed files with 566 additions and 368 deletions

View File

@@ -28,7 +28,7 @@ duplicates) and MUST update it after completing one.
| Card | done | elevated, outlined × default, compact, none padding × interactive × selected | card.borderRadius/padding/shadow/border/background, color.surface.raised/subtle/warm, color.border.default/brand, shadow.md/lg | Content container. Elevated (shadow) or outlined (border). Interactive adds hover bg fill + shadow lift. Selected adds brand border + warm bg. Three padding presets. |
| Switch | done | bordered style × checked, unchecked, disabled | switch.track.width/height/borderRadius, switch.thumb.size, color.interactive.*, color.neutral.400 | Toggle for add-ons/options. Wraps MUI Switch. Bordered pill, brand.500 fill when active. From Parsons 1.0 Figma Style One. |
| Radio | done | checked, unchecked, disabled | radio.size/dotSize, color.interactive.*, color.neutral.400 | Single-select option. Wraps MUI Radio. Brand.500 fill when selected. From Parsons 1.0 Figma. |
| MapPin | done | name+price × verified, unverified | mapPin.paddingX/borderRadius/nub.size, color.brand-200/700, color.neutral-100-800 | Two-line label map marker: name (bold, required, truncated 180px) + "From $X" (centred, semibold). Verified = promoted brand palette (brand-700 bg, white text, brand-200 price). Unverified = neutral grey pill (neutral-100 bg, neutral-800 text). No active state — selection handled at the organism level (ProviderMap swaps pin → MapPopup on click). Pure CSS. role="button" + keyboard + focus ring. Name-only / price-only variants dropped in D042 (production providers always have both). |
| MapPin | done | name+price × verified, unverified | mapPin.paddingX/borderRadius/nub.size, color.brand-200/700, color.neutral-100-800 | Two-line label map marker: [verified tick (inline SVG) +] name (bold, required, truncated 210px) + "From $X" (centred, semibold). Verified providers get a Material Verified icon to the left of the name, same colour as the name text. Verified = promoted brand palette (brand-700 bg, white text, brand-200 price). Unverified = neutral grey pill (neutral-100 bg, neutral-800 text). No active state — selection handled at the organism level (ProviderMap swaps pin → MapPopup on click, or emits via `onActiveChange` when `externalisePopups` is set). Pure CSS + inline SVG (no MUI icon — mounted via createRoot outside ThemeProvider). role="button" + keyboard + focus ring. Name-only / price-only variants dropped in D042. |
| ClusterMarker | done | count × verified (promoted), unverified (neutral) | color.brand-700, color.neutral-100-800, mapPin.nub.size | Circular 36px count badge for pin clusters. `hasVerified` flag drives the palette (if any provider in the cluster is verified, use the promoted brand-700 palette; else neutral grey). Same nub treatment + shadow tokens as MapPin for visual cohesion. Pure CSS + SVG. role="button" + keyboard + focus ring. Designed as the `render`-ed output of `@googlemaps/markerclusterer`. See D043. |
| ColourToggle | planned | inactive, hover, active, locked × single, two-colour × desktop, mobile | | Circular colour swatch picker for products. Custom component. Deferred until product detail organisms. |
| Slider | planned | single, range × desktop, mobile | | Price range filter. Wraps MUI Slider. Deferred until search/filtering molecules. |
@@ -76,7 +76,7 @@ duplicates) and MUST update it after completing one.
| ArrangementForm | planned | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Deferred — build remaining atoms/molecules first. |
| Navigation | done | AppBar + Link + IconButton + Button + Divider + Drawer | Responsive site header. Desktop: logo left, links right, optional CTA. Mobile: hamburger + drawer with nav items, CTA, help footer. Sticky, grey surface bg (surface.subtle). Real FA logo from brandassets/. Maps to Figma Main Nav (14:108) + Mobile Header (2391:41508). |
| Footer | done | Link × n + Typography + Divider + Container + Grid | Light grey (surface.subtle) site footer — matches header. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). |
| ProviderMap | done | MapPin + ClusterMarker + MapPopup + ClusterPopup + `@vis.gl/react-google-maps` + `@googlemaps/markerclusterer` | Google Maps provider map with clustering. **Individual pins**: `MapPin` per provider — click morphs it into a `MapPopup` at the same coord. **Clusters**: pins within 70px of each other collapse into a `ClusterMarker` (count badge) *only while zoomed out at level 13 or below* — past that, every pin shows individually. Click a cluster → `ClusterPopup` list at the cluster centroid, verified-first. **Click a row → map pans + zooms to that provider's coords (zoom 15) and opens their `MapPopup`**; cluster state cleared (no back-to-list). Map-background click reverts to pins. Markers are rendered imperatively (via `createRoot` into AdvancedMarker content elements) so `markerclusterer` can group them; popup layer remains declarative React. `selectedProviderId` force-opens a popup for external selection. Subtle empty state when no API key or no providers have coords (no throw). API key read from `VITE_GOOGLE_MAPS_API_KEY`. Desktop-only for now; mobile map sheet deferred. See D041, D042, D043, D044. |
| ProviderMap | done | MapPin + ClusterMarker + MapPopup + ClusterPopup + `@vis.gl/react-google-maps` + `@googlemaps/markerclusterer` | Google Maps provider map with clustering. **Individual pins**: `MapPin` per provider — click morphs it into a `MapPopup` at the same coord. **Clusters**: pins within 70px of each other collapse into a `ClusterMarker` (count badge) *only while zoomed out at level 13 or below* — past that, every pin shows individually. Click a cluster → `ClusterPopup` list at the cluster centroid, verified-first. **Click a row → map pans + zooms to that provider's coords (zoom 15) and opens their `MapPopup`**; cluster state cleared (no back-to-list). Map-background click reverts to pins. Markers are rendered imperatively (via `createRoot` into AdvancedMarker content elements) so `markerclusterer` can group them; popup layer remains declarative React. `selectedProviderId` force-opens a popup for external selection. **Externalisable popups (D047):** opt-in `externalisePopups` prop suppresses the internal MapPopup/ClusterPopup rendering; `onActiveChange` callback emits the active provider/cluster/exiting state; imperative ref exposes `ProviderMapHandle` with `clearActive()` + `drillIntoProvider(id)` so external callers (e.g. the mobile bottom drawer) can render the popup content themselves. `forwardRef` type is `ProviderMapHandle`, not `HTMLDivElement`. Subtle empty state when no API key or no providers have coords (no throw). API key read from `VITE_GOOGLE_MAPS_API_KEY`. See D041, D042, D043, D044, D047. |
## Templates
@@ -89,8 +89,8 @@ duplicates) and MUST update it after completing one.
| Component | Status | Composed of | Notes |
|-----------|--------|-------------|-------|
| IntroStep | done | WizardLayout (centered-form) + ToggleButtonGroup × 2 + Collapse + Typography + Button + Divider | Wizard step 1 — entry point. forWhom (Myself/Someone else) + hasPassedAway (Yes/No) with progressive disclosure. Auto-sets hasPassedAway="no" for "Myself". `<form>` wrapper, aria-live subheading, grief-sensitive copy. Pure presentation. Audit: 18/20 → 20/20 after fixes. |
| ProvidersStep | done | WizardLayout (list-map) + ProviderCard + SearchBar + Chip + Typography + Button | Wizard step 2 — provider selection. List-map split: provider cards w/ radiogroup + search + filter chips (left), map slot (right). aria-live results count, back link. ProviderCard extended with HTML/ARIA passthrough. Audit: 18/20. |
| PackagesStep | done | WizardLayout (list-detail) + ProviderCardCompact + ServiceOption + NearbyPackageCard + PackageDetail + Divider + Link + Typography | Wizard step 3 — package selection. **Tier-aware unified page** (replaces the old PackagesStep + UnverifiedPackageT2 + UnverifiedPackageT3 trio, 2026-04-17). `providerTier: 'verified' \| 'tier3' \| 'tier2'` drives heading, subhead, `arrangeLabel`, `priceDisclaimer`, and `itemizedUnavailable` via a `TIER_COPY` map. Discriminated `secondaryList`: `same-provider-more` (ServiceOption list, verified) or `nearby-verified` (NearbyPackageCard list, unverified). Same-provider-more **shows top 3 inline**; at >3 shows 3 + `See all N packages from [Provider] →` Link that fires `onSeeAllPackages`. `showAllFromProvider` prop renders a flat "All packages from [Provider]" variant (no grouping, no secondary list, preserves `selectedPackageId`). Primary list suppresses the "Matching your preferences" accent-bar heading when no secondary list is present (so the label only appears when there's something to contrast against). Desktop polished; mobile polish pending. |
| ProvidersStep | done | WizardLayout (list-map) + ProviderCard + ProviderMap + FilterPanel + Autocomplete (chip search) + Chip + Typography + Button + ToggleButtonGroup | Wizard step 2 — provider selection. **Desktop + mobile-list**: list-map WizardLayout — provider cards (click-to-navigate) + sticky bar with committed-chip search (Autocomplete multiple+freeSolo capped to 1, primary-filled search commit button — D046) + Filters dialog + `Sort: <value>` button + mobile-only `List|Map` toggle. Results count bolded. **Mobile + viewMode=map (D045)**: custom layout — full-bleed map + floating card-shaped control strip (search + Filters + `Sort by` + `List|Map` toggle, all white-fill/neutral-300/shadow-sm/32px/14px-600, matching chrome across modes) + bottom drawer (slides up on pin/cluster tap, close X in a 40px header strip). Single-pin drawer renders ProviderCard edge-to-edge with top-only rounded corners; cluster drawer renders inline list of verified-slot + name + location + rating + "From $X" rows, tap a row → drill-in via `mapRef.drillIntoProvider`. Drawer close fully clears via `mapRef.clearActive`. Header + subhead hidden on mobile map. Filter dialog children extracted into a shared `filterDialogChildren` JSX used by both desktop + mobile FilterPanel instances; Location field removed, Funeral-type chips size=medium, Reset filters always visible (disabled when 0 active), provider-feature switches align to first text line. ProviderMap instantiated internally for mobile map view (with `externalisePopups` + `onActiveChange` + `ref`); desktop continues to use the `mapPanel` slot. Audit: 18/20 (pre-2026-04-23 expansion). |
| PackagesStep | done | WizardLayout (list-detail) + ProviderCardCompact + ServiceOption + NearbyPackageCard + PackageDetail + Divider + Link + Typography | Wizard step 3 — package selection. **Tier-aware unified page** (replaces the old PackagesStep + UnverifiedPackageT2 + UnverifiedPackageT3 trio, 2026-04-17). `providerTier: 'verified' \| 'tier3' \| 'tier2'` drives heading, subhead, `arrangeLabel`, `priceDisclaimer`, and `itemizedUnavailable` via a `TIER_COPY` map. Discriminated `secondaryList`: `same-provider-more` (ServiceOption list, verified) or `nearby-verified` (NearbyPackageCard list, unverified). Same-provider-more **shows top 3 inline**; at >3 shows 3 + `See all N packages from [Provider] →` Link that fires `onSeeAllPackages`. `showAllFromProvider` prop renders a flat "All packages from [Provider]" variant. Primary list suppresses the "Matching your preferences" accent-bar heading when no secondary list is present. **Mobile drill-in (2026-04-23 fix):** local `hasDrilledIn` flag — the mobile layout only swaps to the detail view after an explicit user tap on a package, so parent-seeded `selectedPackageId` (common on desktop for auto-display) doesn't force mobile users straight into detail. Back resets the flag. |
| ~~PreviewStep~~ | removed | — | Replaced by ArrangementDialog organism (D-E). Package preview + "what's next" checklist now in the dialog's preview step. |
| ~~AuthGateStep~~ | removed | — | Replaced by ArrangementDialog organism (D-E). SSO/email auth flow now in the dialog's auth step. |
| DateTimeStep | done | WizardLayout (centered-form) + Input + TextField (date) + RadioGroup + Collapse + Divider + Button + Link | Wizard step 6 — details & scheduling. Deceased name (Input atom, external label) + preferred dates (up to 3, progressive disclosure) + time-of-day radios. Service tradition removed (flows from provider/package). Dividers between sections. Grief-sensitive labels. Save-and-exit CTA. |

View File

@@ -26,6 +26,38 @@ Each entry follows this structure:
## Sessions
### Session 2026-04-23 — ProvidersStep polish + mobile map-first layout + deploy
**Agent(s):** Claude Opus 4.7 (1M context)
**Work completed:**
- **ProvidersStep desktop polish** (Track 1 of the 2026-04-22 handoff):
- Sort button now reads `Sort: <value>` (was a bare "Recommended" indistinguishable from a filter); price sort labels cleaned of their internal colons to avoid double-colon rendering.
- Results count bolds the number in primary text.
- `viewMode` toggle on the map panel kept as the mobile affordance (desktop toggle still visible but unchanged per user).
- **Sticky search — committed-chip pattern (D046):** replaced the raw `TextField` with an `Autocomplete multiple + freeSolo` capped at 1 location. Typing produces a draft; Enter or the right-hand primary search-icon button commits to an FA Chip (grey/neutral), tap X to clear. Icon spacing tightened, focus ring stripped on the search + Filters + Sort by controls per user call.
- **Mobile map-first layout (D045):** on `xs` + `viewMode=map`, ProvidersStep branches to a custom layout: nav + full-bleed map + floating control card (search + Filters + Sort by + `List|Map` toggle) + bottom drawer + help bar. h1 + subhead dropped on mobile map view to save vertical space; remain on desktop and mobile list. Drawer slides up on pin/cluster tap, slides down on close X. Single-pin drawer renders a `ProviderCard` edge-to-edge (entire card clickable → onSelectProvider); cluster drawer renders an inline list of verified-slot + name + location + rating + "From $X" rows, tap a row to drill in (pan+zoom + swap drawer content to the single-pin card). Close X lives in a 40px header strip so it never overlaps the Verified badge.
- **ProviderMap externalisable popups (D047):** opt-in `externalisePopups` prop + `onActiveChange` callback + imperative `ProviderMapHandle` (`clearActive`, `drillIntoProvider`). Desktop behaviour unchanged when these aren't used; mobile drawer consumes them. `forwardRef` type changed from `HTMLDivElement` to `ProviderMapHandle`; no existing callsite passes a DOM ref so safe.
- **MapPin verified icon:** inline Material Verified (outlined) SVG on the left of the name for verified providers. Inline SVG, not `@mui/icons-material`, because MapPin is mounted via `createRoot` outside the ThemeProvider. Max label width bumped 180→210px.
- **Mobile cluster drawer rows:** verified icon now aligns with the name's top line (matches desktop `ClusterPopup` fix from D043 refinement) + new right-aligned "From $X" price column.
- **Controls unified across mobile views:** mobile list-view sticky controls match the map-view floating chips — white fill, neutral-300 border, shadow-sm, 32px height, 14px/600 text. Mobile sort label switches from "Sort: <value>" to compact "Sort by" (desktop keeps verbose label). Desktop map-panel floating toggle resized to match Filters/Sort button height + type.
- **Filter dialog cleanup (desktop + mobile):** Location field removed (the sticky search is primary); Funeral-type chips bumped small → medium; Reset filters button always renders (disabled when no filters active); provider-feature switches (Verified only, Online arrangements) align to the first text line.
- **PackagesStep mobile drill-in bug fix:** added a local `hasDrilledIn` flag so the mobile layout only swaps to the detail view after an explicit user tap on a package. Previously the demo route seeding `selectedPackageId` to the first matching package (for desktop auto-display) also forced mobile into the detail view — users arriving from the map drawer saw a single package instead of the list. Back/forward from the detail resets the flag.
- **Drawer close animation:** `drawerOpen` now excludes the `exiting` phase so tapping the close X slides the drawer down immediately instead of lingering with an opacity fade-back-in (bug was: transform and opacity transitions interleaving wrong).
- **Whitelist localhost** for the Google Maps API key (user added `http://localhost:5180/*` and `http://localhost:6006/*` to HTTP referrer restrictions), unblocking local map/drawer end-to-end testing.
- **Preflight + deploy:** all checks pass (TS, Storybook build, token sync, ESLint, Prettier). Hex-colour warnings all pre-existing in HomePage. Deployed to `https://parsons.tensordesign.com.au/arrangement/`.
**Decisions made:** D045, D046, D047 (see decisions-log).
**Open questions:**
- User flagged "still more edits to be done" at checkpoint — pending list will come in the next session.
**Next steps:**
- Pick up the user's remaining ProvidersStep polish items.
- Process the `00-Inbox/Mobile list-mode layout for ProvidersStep + ProviderMap.md` note (now implemented — can be archived when the user runs `/process-inbox`).
---
### Session 2026-04-22 — Session-end handoff
**Status at end of session:** Map work reached a stable checkpoint. User asked to resume in a new session rather than continue accumulating context. All work committed.