Session log entry for today (2026-04-23c) covers the 8-commit demo
update pass: MiniCard-based nearby-verified grid, package fixtures
cleanup (drop 'unknown' treatment) + 10 new verified packages, Vite
envDir fix for Google Maps, always-recommended Comparison route,
responsive PackageDetail CTAs, ComparisonTable tier-aware Unknown
rendering, and info-card un-stick.
Plus the pre-drafted 2026-04-23b session-log entry (extractions,
CompareBar pattern, PackageDetail toggle, basket persistence) and the
matching component-registry updates for those prior-session changes.
Registry today: NearbyPackageCard row removed (now orphaned and deleted
— no consumers after MiniCard swap). PackagesStep, PackageDetail, and
ComparisonTable rows updated for this session's changes.
Decisions-log D052–D056 live locally; decisions-log.md is gitignored so
those additions aren't in this commit. Flagging the gitignore mismatch
— the session log references decision IDs that won't resolve for anyone
pulling from the remote until that's sorted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -54,12 +54,15 @@ duplicates) and MUST update it after completing one.
| LineItem | done | Typography + Tooltip + InfoOutlinedIcon | Name + optional info tooltip + optional price. Supports allowance asterisk, total variant (bold + top border). Font weight 500 (D019), prices text.secondary for readability hierarchy. Audit: 19/20. |
| ProviderCardCompact | done | Card (outlined) + Typography | Horizontal compact provider card — image left, name + location + rating right. Used at top of Package Select page. Separate from vertical ProviderCard. |
| CartButton | done | Button + DialogShell + LineItem + Divider + Typography | Outlined pill trigger: receipt icon + "Your Plan" + formatted total in brand colour. Click opens DialogShell with items grouped by section via LineItem, total row. Mobile: icon + price only. Lives in WizardLayout `runningTotal` slot. |
| CompareBar | done | Badge + Button + IconButton + Typography + Paper + Slide + Chevron{Left/Right}RoundedIcon | Floating comparison basket pill. Fixed bottom, slide-up via MUI Slide on initial appearance (count: 0 → ≥1). Package count badge (`N/3`) + status text + Compare CTA. Max 3 user packages; disabled CTA when <2; inline error for max-reached. **Sizing**: Badge large / body1 text / Button medium on md+; Badge medium-circle / body2 / Button small on xs. **Mobile collapse (D049)**: single Paper right-anchored via `right: t.spacing(4); left: auto`; grey-filled right-chevron retracts the pill by animating the middle content Box to `max-width: 0` — the pill's right edge stays pinned, so the whole thing appears to shrink into the corner as one unit. Left-chevron expands. Collapsed fraction-badge shows just `count` (not `count/3`) with pinned min-width for circular look. Auto-peek: new add while collapsed re-expands the full bar for 3s, then re-collapses. Desktop stays permanently expanded. **Centering**: uses `left: 0; right: 0; mx: auto; width: fit-content` (the `left:50%; translateX(-50%)` centering trick is clobbered by Slide's own transform). **z-index**: `theme.zIndex.drawer` (1200) — below MapProviderDrawer's `modal` (1300) so the drawer covers it on mobile map view. `bottom: theme.spacing(16)` (64px, FA 4px base) clears the sticky HelpBar with ~25px breathing. See D049. |
| ComparisonPackageCard | done | Card + Button + Divider + Typography + Tooltip + LocationOnOutlinedIcon + VerifiedOutlinedIcon + StarRoundedIcon + CheckCircleOutlineIcon + InfoOutlinedIcon | Mobile full-width package card for ComparisonPage tabpanels. Provider header (inline VerifiedOutlinedIcon left of name when verified, name, location + rating, divider, package name, price block, full-width CTA) + itemised sections with left-accent headings. **Warm tint confined to header only** (not Card body) — Card is white (`background.paper`), header has `surface-warm` (recommended) or `surface-subtle` (verified) bg. **2px brand-600 border** when recommended (matches desktop ComparisonColumnCard). Header `px: 3, pt: 3, pb: 4`. Package-info subgroup (name/label/price) in tight nested flex columns. Generous section spacing (`mb: 5` between sections, `py: 2` per item). Recommended banner at top. Shadow (shadow-sm). Medium full-width button. Reuses `ComparisonPackage` type from ComparisonTable. Shared by ComparisonPage V2 and V1 (extracted 2026-04-09). |
| ComparisonColumnCard | done | Card + Badge + Button + Divider + Typography + Tooltip + Link + StarRoundedIcon + VerifiedOutlinedIcon | Desktop column header card for ComparisonTable. Floating badge: **medium** (26px) filled brand + StarRoundedIcon for recommended; soft brand + VerifiedOutlinedIcon for verified. Provider name **wraps to 2 lines** (`WebkitLineClamp: 2`) in a reserved 36px minHeight slot bottom-aligned so 1-line names anchor with location/rating/price at a consistent baseline. Recommended card: 2px brand-600 border + warm `selected` Card state + inline VerifiedOutlinedIcon left of name. `pt: 5` (40px breathing above name), uniform regardless of verified/recommended. Remove link always renders as the same Link element (visibility-hidden when not applicable) so CTA+footer align across all cards. Per-column wrapper in ComparisonTable is `display: flex` with `flex: 1` passed to the card root so all cards stretch to row height. Extracted from ComparisonTable (2026-04-12). |
| ComparisonTabCard | done | Card + Badge + Typography + StarRoundedIcon | Mobile tab rail card for ComparisonPage. Provider name + package name + price. Recommended badge in normal flow with negative margin overlap — **filled brand + StarRoundedIcon** (matches desktop ComparisonColumnCard treatment, size="small" at 14px icon). **Fixed 235px width** (was 210). Border `brand-600` when recommended (consistent with primary). No glow — uses standard `shadow-sm` like other cards. `pt: 3.5` inside card. Shared by V1 and V2 (extracted 2026-04-12). |
| NearbyPackageCard | done | Card (outlined, interactive) + Typography + StarRoundedIcon + LocationOnOutlinedIcon | Compact card representing a package offered by a nearby verified provider — package name + price + provider + rating + location. Used in the "Similar packages from verified providers nearby" section of PackagesStep for unverified tiers. Click is a route change to that verified provider's PackagesStep with this package loaded. Extracted from UnverifiedPackageT2/T3 during 2026-04-17 consolidation. |
| ClusterPopup | done | Paper + Typography + IconButton + ButtonBase + inline provider-rows (fixed verified-icon slot + name + location + rating) | Cluster list popup — appears when a cluster marker is clicked. Header bar ("N providers in this area" + close X), scrollable stack of image-free provider rows (reserved verified-icon slot so titles align across tiers, name in copper for verified / neutral for unverified, location + rating meta). Verified-first sort order. 320px wide, matches MapPopup's card + nub + drop-shadow. Click on a row calls `onSelectProvider(id)`; in the ProviderMap flow that pans+zooms the map to the provider's coords (zoom 15) and opens their single-provider popup — there's no back-to-list. See D043, D044. |
| MapProviderDrawer | done | Paper + IconButton + ProviderCard + inline cluster-rows (verified-slot + name + location + rating + "From $X") | Bottom drawer surfacing ProviderMap's popup content outside the map (used by the mobile map-first layout on ProvidersStep). Two content states driven by `active: ProviderMapActiveState \| null`: (a) `active.provider` → full-width ProviderCard edge-to-edge, entire card clickable → `onSelectProvider(id)`; (b) `active.cluster` → verified-first list of rows with verified-icon slot aligned to name top-line (`alignItems: flex-start` + `1.25em` slot height + `lineHeight: 1.25` on name), name copper for verified, right-aligned "From $X" price column; row click → `onDrillIntoProvider(id)` (typically wired to map's imperative `drillIntoProvider` — pans + zooms + swaps `active` to that provider). 40px header strip holds the close X + cluster count heading; `px: 2` aligns with row content beneath. Slides up via `transform: translateY()` + 220ms transition; stays in DOM during exit via `visibility`. z-index `theme.zIndex.modal` (1300) so it covers the floating CompareBar on mobile map view. Requires a relatively-positioned parent container. See D045, D047. |
| LocationSearchInput | done | Autocomplete (multiple + freeSolo capped to 1) + TextField + IconButton + Chip + LocationOn/Search icons | Committed-chip location search. Typing produces a local draft; Enter or the primary-filled circular magnifying-glass commit button promotes the draft to an FA Chip rendered inside the input. Tap the chip's X → clears to empty. Molecule owns the non-obvious correctness CSS: the end-adornment is absolute-anchored via selector `.MuiAutocomplete-inputRoot .MuiInputAdornment-positionEnd` (MUI's stock Autocomplete does this on `.MuiAutocomplete-endAdornment` but overriding `InputProps.endAdornment` bypasses that, leaving the button sliding left as content fills); `pr: 5` reserves the right-edge lane so chips can't run under. Chrome (bgcolor, shadow, border, radius) is caller-controlled via `sx` — internal selector keys use `.MuiAutocomplete-inputRoot` so they don't collide with caller sx for `.MuiOutlinedInput-root`. API: `value`, `onChange` (fires on commit OR chip-delete), `onCommit?` (explicit commit only), `placeholder?`, `aria-label?`, `sx?`. Extracted from ProvidersStep (D046, D048). |
| HelpBar | done | Box + Typography + Link + PhoneIcon | Sticky help footer — phone-icon prefix + "Need help? Call us on" + tel-linked support number. White fill, top border, `position: sticky; bottom: 0; z-index: 10`. Responsive px (2 / 4) and centered text. Used by WizardLayout and by pages that bypass WizardLayout's chrome (ProvidersStep mobile-map branch). Promoted from a WizardLayout-internal component so both sources render an identical footer — preventing drift if the phone number or style ever changes. API: `phone?` (defaults to '1800 987 888', spaces preserved in label, stripped in tel href), `sx?`. See D048. |
| SortMenu | done | Button (outlined, secondary, small) + Menu + MenuItem + SwapVertIcon (verbose only) | Dropdown sort control. Tap the trigger Button → anchored Menu opens at bottom-right; pick an option → menu closes and `onChange(value)` fires. Two label variants: `compact` (default) renders just "Sort by" — current value surfaces only in the menu's selected-state + aria-label (best for narrow surfaces); `verbose` renders `Sort: <current label>` with a leading swap-vertical icon (best for desktop). Non-generic string-typed API so `forwardRef` stays clean; callers with typed sort unions cast at the boundary. `aria-haspopup="listbox"` + `aria-label` carries the current sort for SR users. API: `value`, `onChange`, `options: SortOption[]`, `variant?: 'compact' \| 'verbose'`, `sx?`. Extracted from ProvidersStep (D048); intended for VenueStep, CoffinsStep reuse. |
## Organisms
@@ -67,8 +70,8 @@ duplicates) and MUST update it after completing one.
|-----------|--------|-------------|-------|
| ServiceSelector | done | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. |
| PackageDetail | done | LineItem × n + Typography + Button + Divider | Right-side package detail panel. Warm header band (surface.warm) with "Package" overline, name, price (brand colour), Make Arrangement + Compare (with loading) buttons. Sections: Essentials + Optionals (before total) + total + Extras (after total, with subtext). `priceLabel` pass-through to LineItem (D039). T&C grey footer. Audit: 19/20. |
| ComparisonTable | done | Typography + Card + Tooltip + ComparisonColumnCard | Side-by-side package comparison. **Fixed column widths** (300px row-label + 300px per package, exported as `COMPARISON_TABLE_COL_WIDTH`). Natural width = `300 × (n+1)`. **Sticky-left** on row-label column across every per-section mini-table. **Tiered hover**: base cells → `surface-subtle`, recommended column cells → `surface-warm` (resting 50% opacity, promotes to full on row hover via color-mix). Per-section mini-tables with left-accent brand heading. **"Not Included"** rendering: `unavailable` cell values in `Optionals`/`Extras` sections render "Not Included" (neutral-500 weight 500); `Essentials` keeps em-dash. `CellIconText` local helper applies `lineHeight: 1` to icon+text rows for optical centre alignment. ARIA table roles. Desktop only (mobile in ComparisonPage). Audit: 17/20 (pre-restructure). |
| PackageDetail | done | LineItem × n + Typography + Button + Divider + CheckRoundedIcon | Right-side package detail panel. Warm header band (surface.warm) with "Package" overline, name, price (brand colour), Make Arrangement + Compare buttons. Sections: Essentials + Optionals (before total) + total + Extras (after total, with subtext). `priceLabel` pass-through to LineItem (D039). T&C grey footer.**Compare toggle (D050)**: `inCart?: boolean` prop controls the Compare button state. Both states share the same soft/secondary chrome and "Compare" label; when `inCart=true`, a trailing `CheckRoundedIcon` is added (endIcon). Click fires `onCompare` in both states — caller wires it to a toggle (e.g. `basket.toggle(key)`). `aria-pressed={inCart}` + aria-label ("Add to comparison" / "Remove from comparison") for SR users. Rejected alternatives: inert brand-tinted pill (too much space); "Added" label swap (cognitive overhead). **CTAs responsive (2026-04-23c)**: `flexDirection: 'row'` on all viewports — never stacks. `size: isMobile ? 'medium' : 'large'` (40/48px) via `useMediaQuery(theme.breakpoints.down('sm'))` so Make Arrangement + Compare fit side-by-side in a ~360px mobile column. Audit: 19/20 (pre-D050). |
| ComparisonTable | done | Typography + Card + Tooltip + ComparisonColumnCard | Side-by-side package comparison. **Fixed column widths** (300px row-label + 300px per package, exported as `COMPARISON_TABLE_COL_WIDTH`). Natural width = `300 × (n+1)`. **Sticky-left** on row-label column across every per-section mini-table. Info card (top-left "Package Comparison" card) **scrolls** with the package columns (was sticky-left — that pinned it over the recommended column per D040).**Tiered hover**: base cells → `surface-subtle`, recommended column cells → `surface-warm` (resting 50% opacity, promotes to full on row hover via color-mix). Per-section mini-tables with left-accent brand heading. **Tier-aware missing-cell rendering (D054)**: `lookupValue` branches on `pkg.provider.verified` — unverified packages render missing cells as `{ type: 'unknown' }` ("Unknown" + trailing info icon, neutral-500); verified packages render as `{ type: 'unavailable' }` → "Not Included" in Optionals/Extras, em-dash in Essentials (safety net — canonical-essentials rule means verified providers itemise all 9, so this path shouldn't fire in practice). No disclaimer footer. `CellIconText` local helper applies `lineHeight: 1` to icon+text rows for optical centre alignment. ARIA table roles. Desktop only (mobile in ComparisonPage). Audit: 17/20 (pre-restructure). |
| FuneralFinder (V3) | done | Typography + Button + Divider + Select + MenuItem + OutlinedInput + custom StatusCard/SectionLabel | **Production version.** Hero search widget — clean form with status cards. Standard card container (surface.raised, card shadow). "How Can We Help" section: two side-by-side StatusCards (Immediate Need default-selected / Pre-planning) — white bg, neutral border, brand border + warm bg when selected, stack on mobile. "Funeral Type" Select + "Location" OutlinedInput with pin icon — standard outlined fields, no focus ring (per design). Overline section labels (text.secondary). CTA "Find Funeral Directors →" always active — validates on click, scrolls to first missing field. Required: status + location. Funeral type defaults to "show all". Dividers after header and before CTA. WAI-ARIA roving tabindex on radiogroup. aria-labelledby via useId(). Critique: 33/40 (Good). Audit: 18/20 (Excellent). |
| 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. |
| ProvidersStep | done | WizardLayout (list-map) + ProviderCard + ProviderMap + FilterPanel + LocationSearchInput + SortMenu + HelpBar + MapProviderDrawer + Chip + Typography + ToggleButtonGroup | Wizard step 2 — provider selection. **Desktop + mobile-list**: list-map WizardLayout — provider cards (click-to-navigate) + sticky bar with LocationSearchInput (committed-chip search — D046/D048) + FilterPanel trigger + SortMenu (`variant: isMobile ? 'compact' : 'verbose'` — D048) + mobile-only `List\|Map` toggle. Results count bolded. **Mobile + viewMode=map (D045)**: custom layout — full-bleed map + floating control strip (search + Filters + Sort by + `List\|Map` toggle) + MapProviderDrawer + shared HelpBar molecule. Header + subhead hidden on mobile map. **Control chrome**: page-local `CONTROL_CHROME` const (height 32 / neutral-300 border / button-radius / paper fill / shadow-sm) + derived `controlButtonSx` / `controlToggleSx` / `controlInputSx` / `filterTriggerSx` objects applied across Search + Filters + SortMenu + ToggleGroup so all four controls read as one coherent chip set. Filter dialog children in a shared `filterDialogChildren` JSX used by both desktop + mobile FilterPanel instances; Location field removed (sticky search is primary), 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` + imperative ref); desktop continues to use the `mapPanel` slot. Audit: 18/20 (pre-2026-04-23 expansion). |
| PackagesStep | done | WizardLayout (list-detail) + ProviderCardCompact + ServiceOption + MiniCard + 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, `packages` field) or `nearby-verified` (MiniCard grid, unverified, `providers` field). **Nearby-verified (D052)**: 2-col `repeat(2, 1fr)` grid on sm+, 1-col on xs, capped at 4 via `NEARBY_VERIFIED_LIMIT`. Each card is a verified provider (image + `verified={true}` implicit + location + rating + "From $X"); click routes to that provider's PackagesStep via `onNearbyProviderClick(id)`. Heading: "Similar packages from verified providers" (`VerifiedOutlinedIcon` aligned to top line via `alignItems: flex-start` + `mt: '3px'`). 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.`selectedPackage` lookup falls back from primary `packages` to `same-provider-more` secondary list so tapping a package in "Other packages from X" surfaces PackageDetail correctly (2026-04-23c fix).**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. |
**Context:** User walked through a series of demo-visible issues after reviewing `https://parsons.tensordesign.com.au/arrangement/`. Each landed as its own commit + deploy; total 8 commits.
**Work completed:**
- **Nearby-verified section on PackagesStep now surfaces providers, not packages (D052):** the `nearby-verified``SecondaryList` arm was rebuilt around `MiniCard`. Data shape renamed `NearbyVerifiedPackage` → `NearbyVerifiedProvider` (`id, name, imageUrl, location, startingPrice, rating?, reviewCount?`); `verified` is implicit, not a data field, since the section is verified-only by definition. Callback `onNearbyPackageClick` → `onNearbyProviderClick`, routing directly on provider id. **Layout**: 2-col CSS grid (`repeat(2, 1fr)`) on sm+, 1-col on xs; capped at 4 via `NEARBY_VERIFIED_LIMIT`. Heading changed "Similar packages from verified providers nearby" → "Similar packages from verified providers" (dropped "nearby"). Heading icon aligned to top line of multi-line text via `alignItems: flex-start` + `mt: '3px'` (FA icon convention). Demo fixture `nearbyVerifiedSamples` replaced by `nearbyVerifiedProviders` derived from the main `providers` fixture filtered to `tier === 'verified' && imageUrl`. `NearbyPackageCard` molecule is orphaned and retired in the same pass (registry row + folder deleted).
- **Packages fixtures cleanup + 10 new verified packages:**
- **Dashes (D053):** removed all 10 fixture entries with `treatment: 'unknown'` from Optionals. Narrowed `Optional['treatment']` from `IncludedTreatment | 'unknown'` to `IncludedTreatment`. Simplified `optionalsForStep` / `optionalsForComparison` helpers — the `'unknown'` branches are gone. PackageDetail no longer renders bare em-dash rows; ComparisonTable's `lookupValue` already returned `unavailable` (→ "Not Included") for missing items via the section cross-join.
- **New packages:** distribution randomised, max 5 per verified provider. Parsons 3 → 5 (+ Traditional Burial, Memorial Service), Rankins 2 → 3 (+ Direct Cremation), Killick 2 → 3 (+ Traditional Burial), Mackay 1 → 4 (+ Premium, Simple Cremation, Memorial Service), Mannings 1 → 4 (+ Premium, Simple Cremation, Direct Cremation). Each new package follows the canonical-Essentials rule (same 9 line items, per-package prices/treatments). Mackay + Mannings comparison maps rewritten from the single-package `.map(pkg => ({...}))` shape to the indexed-array pattern used by parsons/rankins/killick. Their `packagesByProvider` entries now `slice(0,1)`/`slice(1)` so "Other packages from this provider" renders.
- **PackagesStep selection lookup** bug fix — `selectedPackage` was derived only from the primary `packages` array, so clicking a package in "Other packages from X" updated `selectedPackageId` but the detail panel stayed empty. Added a fallback to the `same-provider-more` secondary list.
- **Vite demo config: `envDir = __dirname` (D056).** The demo's Google Maps API key was never in the production bundle. `vite.demo.config.ts` sets `root: src/demo/apps/<slice>/`, and Vite's default envDir is that `root` — no env files there, so `.env.local` at the repo root was silently ignored and `VITE_GOOGLE_MAPS_API_KEY` came out undefined. ProviderMap fell through to its "no API key" empty state on prod. Adding `envDir: __dirname` fixes it for every current and future slice.
- **Comparison route: always include a recommended column (D055).** Demo's Comparison.tsx resolves a static `DEMO_RECOMMENDED_KEY = 'parsons:deluxe'` and passes it as `recommendedPackage`. Dedupes against basket so if the user has Parsons Deluxe selected it only appears once (as the recommended column). Basket mechanics unchanged — `useComparisonBasket` still caps user selections at 3; the recommended is layered as an editorial suggestion. Empty state demoted to defensive (now only fires when both `packages` AND `recommendedPackage` miss).
- **PackageDetail CTAs side-by-side on mobile.** Buttons stacked on xs because of `flexDirection: { xs: 'column', sm: 'row' }` + `size="large"` — labels couldn't fit a ~360px column side by side. Fixed `flexDirection: 'row'` for all viewports and `size: isMobile ? 'medium' : 'large'` (40px/48px). `useMediaQuery(theme.breakpoints.down('sm'))` drives the switch.
- **ComparisonTable: unverified providers show "Unknown" (D054).** `lookupValue` now checks `pkg.provider.verified` — unverified missing cells return `{ type: 'unknown' }` (existing "Unknown" + info-icon render), verified missing cells keep `{ type: 'unavailable' }` (→ "Not Included" in Optionals/Extras; em-dash in Essentials as the safety net — canonical-essentials rule means that path shouldn't fire in practice). Dropped the "* Some providers have not provided an itemised pricing breakdown. Their items are shown as '—' above." footer and the now-unused `Z_HEADER_ROW` constant.
- **ComparisonTable info card un-stuck.** The top-left "Package Comparison" info card had `position: sticky, left: 0` which pinned it over the leftmost package column (the recommended one since D040) on horizontal scroll. Dropped the sticky positioning — info card scrolls with the package headers; row-label column below stays sticky on its own (those labels genuinely help orient the reader).
**Decisions made:** D052, D053, D054, D055, D056.
**Verified visually (user eyes, Playwright not used):**
- MiniCard grid renders 2-col desktop / 1-col mobile, max 4; icon aligns with heading top line on mobile.
- New packages visible in each provider's "Other packages from X" section; "Other from Rankins → Premium" selection now surfaces PackageDetail.
- Dashes gone from verified-provider PackageDetail Optionals.
- Google Maps loads on prod (grep of built bundle confirmed `AIzaSy` key baked in).
- ComparisonPage shows Parsons Deluxe as leftmost recommended column with the filled-brand + star badge on every visit.
- CTAs side-by-side on mobile at medium size.
- Botanical (tier2) comparison cells render "Unknown" — no more em-dashes except the canonical-Essentials safety-net path (which verified providers never trigger in practice).
- Info card scrolls away on horizontal scroll; recommended card fully visible.
**Preflight + deploy:** Each commit preflighted (TS + ESLint + Prettier clean; Storybook built once) and deployed via `npm run demo:publish` to `https://parsons.tensordesign.com.au/arrangement/`. Hardcoded-hex warnings all pre-existing in HomePage / FuneralFinderV2-4 / PackageDetail — none introduced this session.
**Open questions:**
- None.
**Next steps:**
- Push commits to `backup` + `fa-dev` git remotes at `git.tensordesign.com.au`.
**Context:** Continuation of 2026-04-23 work. User asked for a polish + cleanup pass on the comparison feature and its surrounding components, ending with a deploy. Several discrete changes delivered across component + demo layers.
**Work completed:**
- **Three molecule extractions (D048)** from ProvidersStep, each with its own barrel export + Storybook stories:
-`LocationSearchInput` (`src/components/molecules/LocationSearchInput/`) — committed-chip location search. Owns the subtle endAdornment absolute-anchor CSS so future Autocomplete-with-custom-endAdornment users don't rediscover it.
-`HelpBar` (`src/components/molecules/HelpBar/`) — promoted from a WizardLayout-internal component so WizardLayout and ProvidersStep's mobile-map branch share one source of truth (prevents the phone/styling drift that had already crept in).
-`SortMenu` (`src/components/molecules/SortMenu/`) — trigger Button + anchored Menu with `variant: 'compact' \| 'verbose'`. Intended for VenueStep / CoffinsStep reuse; today only ProvidersStep consumes it.
ProvidersStep shed ~240 lines net (1087 → ~850) and now reads as a composition of small molecules.
- **CompareBar**:
- Sized up: Badge large (32px), body1 text, Button medium (40px) on desktop; responsive size-down on xs.
- Compare icon removed — label alone at the larger size.
- Horizontal centering fixed — was 171px off-centre on desktop because Slide's internal transform was clobbering `translateX(-50%)`. Switched to `left:0; right:0; mx:auto; width:fit-content` which Slide doesn't fight.
-`bottom` bumped from `theme.spacing(9)` to `theme.spacing(16)` — the FA theme uses a 4px spacing base, so 9 was only 36px (overlapping the ~40px HelpBar). 16 = 64px with clean 25px gap.
- **Mobile collapse with auto-peek on add (D049)**: single-Paper width-animation approach. Right-anchored on xs, middle content (status + CTA) animates to `max-width: 0` while the pill's right edge stays pinned — visually retracts to the corner as one unit. Grey-filled chevron swaps between right (collapse) and left (expand) in place. Auto-peek: new add while collapsed → bar peeks back in for 3s. Collapsed badge shows just `count` (not `count/3`) with pinned min-width for circular look.
- z-index dropped from `snackbar` (1400) to `drawer` (1200). `MapProviderDrawer` raised from `3` to `modal` (1300). Drawer now visually covers CompareBar on mobile map view when a pin/cluster is active.
- **PackageDetail `inCart` toggle (D050)**: new `inCart?: boolean` prop. Both states share the same soft/secondary chrome + "Compare" label; added state adds a trailing `CheckRoundedIcon`. Click is a toggle (caller wires to `basket.toggle(key)`). Two earlier patterns were implemented then rejected by the user: an inert brand-tinted pill (too much space) and an "Added" label swap (cognitive overhead). `aria-pressed` + `aria-label` carry state for SR. Threaded via `PackagesStep.isSelectedPackageInCart` → `Packages.tsx` computes `basket.has(key)`.
- **Basket URL-sync sticky (D051)**: `useBasketUrlSync` pre-fix silently wiped the basket on back-navigation from `/providers/:id/packages?compare=foo` to `/` because the new empty `?compare=` was treated as a "set to empty" signal. Fix: when in-app navigation drops the param but the store still has items, re-attach the store's keys to the new URL instead of wiping. Shared links + manual URL edits still hydrate the store. Demo-only change.
- **Minor polish**:
- MapProviderDrawer cluster rows: verified-icon alignment fix + 1.25 lineHeight on name + px:2 header padding aligns heading with row content (was px:1.5).
- Deployed checkpoint mid-session (commit `6434d113`) and end-of-session.
**Decisions made:** D048, D049, D050, D051.
**Verified visually (Playwright):**
- Basket persists across provider → map → provider → map (URL carries `?compare=...` through every route).
- Mobile CompareBar collapses/expands as one unit; collapsed badge is circular; grey chevron visible; auto-peek fires on add; HelpBar overlap gone.
- Drawer z-index: drawer covers CompareBar when active; CompareBar reappears when drawer dismissed.
- PackageDetail toggle: chrome identical between default and added states; trailing tick on right when `inCart`.
**Preflight + deploy (early checkpoint):** all critical checks pass (TS, Storybook build, token sync, ESLint, Prettier). Deployed to `https://parsons.tensordesign.com.au/arrangement/`.
**Open questions:**
- None blocking. If slide/peek on mobile feels wrong after user testing, the pattern is one `git revert` away.
**Next steps:**
- Memory / registry catch-up pass (in progress at session end), then a second deploy.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.