Strip AI tooling and working docs for dev push

This commit is contained in:
2026-05-22 11:55:25 +10:00
parent abd8d2d2da
commit 62a9db4e64
29 changed files with 0 additions and 4527 deletions

View File

@@ -1,129 +0,0 @@
# Component registry
Tracks the status, specification, and key details of every component in the
design system. Agents MUST check this before building a component (to avoid
duplicates) and MUST update it after completing one.
## Status definitions
- **planned**: Component is identified but not yet started
- **in-progress**: Component is being built
- **review**: Component is built, awaiting human review
- **done**: Component is reviewed and approved
- **needs-revision**: Component needs changes based on review feedback
## Atoms
| Component | Status | Variants | Tokens used | Notes |
|-----------|--------|----------|-------------|-------|
| Button | done | contained, soft, outlined, text × xs, small, medium, large × primary, secondary + loading, underline, fullWidth | button.height/paddingX/paddingY/fontSize/iconSize/iconGap/borderRadius, color.interactive.*, color.brand.100-300, color.neutral.200-700 | Primary interactive element. Merges Text Button from Figma. Soft variant = Figma's Secondary/Brand & Secondary/Grey. |
| IconButton | done | default, primary, secondary, error × small, medium, large | Reuses button.height/iconSize tokens, color.interactive.*, color.neutral.* | Icon-only button (close, menu, actions). Wraps MUI IconButton. Rounded rect, brand hover, focus ring. |
| Typography | done | displayHero, display1-3, displaySm, h1-h6, bodyLg, body1, body2, bodyXs, labelLg, label, labelSm, caption, captionSm, overline, overlineSm + maxLines, gutterBottom | typography.* (all semantic typography tokens), fontFamily.body, fontFamily.display | Text display system. Thin MUI wrapper with maxLines truncation. |
| Input | done | medium, small × default, hover, focus, error, success, disabled + startIcon, endIcon, required, multiline | input.height/paddingX/paddingY/fontSize/borderRadius/gap/iconSize, color.neutral.300-400, color.brand.500, color.feedback.error/success, color.text.secondary | External label pattern, branded focus ring, two sizes aligned with Button. Adds startIcon/endIcon and success state beyond Figma. |
| Badge | done | soft, filled × default, brand, success, warning, error, info × small, medium + icon | badge.height/paddingX/fontSize/iconSize/iconGap/borderRadius, color.feedback.*, color.brand.200/700 | Status indicator pill. Soft (tonal) or filled (solid). 6 colours, 2 sizes, optional leading icon. |
| Icon | planned | various sizes | | Icon wrapper component |
| Avatar | planned | image, initials, icon × small, medium, large | | User/entity representation |
| Divider | done | horizontal, vertical × fullWidth, inset, middle + text, flexItem | color.border.default (via palette.divider) | Visual separator. Wraps MUI Divider. Supports text children and orientation. |
| Chip | done | filled, outlined × small, medium × clickable, deletable, selected × default, primary | chip.height/paddingX/fontSize/iconSize/deleteIconSize/iconGap/borderRadius, color.neutral.200-700, color.brand.200-700 | Interactive tag. Wraps MUI Chip with FA tokens. Selected state promotes to brand colour. Filled uses soft tonal bg (like Badge). |
| 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: [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. |
| Link | done | underline: hover/always/none × any MUI colour | color.text.brand (copper brand.600, 4.8:1), color.interactive.active | Navigation text link. Wraps MUI Link. Copper default, underline on hover, focus ring. |
| Collapse | done | in/out × unmountOnExit | (none — uses MUI defaults) | Progressive disclosure wrapper. Thin MUI Collapse wrapper with unmountOnExit default. Slide-down animation for wizard field reveal. |
| DialogShell | done | open/closed × with/without back button × with/without footer | (theme defaults — borderRadius, palette) | Standard dialog container. Header (title + optional back + close), divider, scrollable body, optional footer. Used by FilterPanel and ArrangementDialog. |
| ToggleButtonGroup | done | exclusive single-select × small, medium, large × error × fullWidth + descriptions | color.neutral.100-200, color.brand.50/100, color.interactive.focus, color.feedback.error | Button-select for binary/small-set choices. Fieldset/legend a11y, external label, helper/error text. Brand styling on selected. |
## Molecules
| Component | Status | Composed of | Notes |
|-----------|--------|-------------|-------|
| MiniCard | done | Card + Typography + Badge + Tooltip | Compact vertical card for grids, recommendations, map popups. Image + title + optional price/badges/chips/meta (location, rating, capacity). Verified = icon-only circle badge in image. Hierarchy: title → meta → price → badges → chips. Truncated title shows tooltip. 3 component tokens. Audit: 20/20. |
| MapPopup | done | Paper + Typography + Tooltip | Floating map popup anchored to MapPin. Clickable card (onClick). Image + name (1 line, tooltip) + meta + price. Verified = icon-only circle badge in image (matches MiniCard). Hierarchy matches MiniCard. Nub + drop-shadow. 260px wide. |
| FormField | planned | Input + Typography (label) + Typography (helper) | Standard form field with label and validation |
| ProviderCard | done | Card + Typography + Badge + Tooltip | Provider listing card. Verified: image + logo (64px rounded rect) + "Verified" badge. Unverified: text-only with top accent bar. Capability badges with info icon + tooltip. Price split typography. No footer. 4 component tokens. |
| VenueCard | done | Card + Typography | Venue listing card. Always has photo + location + capacity ("X guests") + price ("From $X"). No verification tiers, no logo, no badges. 3 component tokens. Critique: 33/40. |
| ~~MapCard~~ | superseded | — | Retired 2026-04-21. Role (compact map popup card) fully covered by `MapPopup`. See D041. |
| ServiceOption | done | Card (interactive, selected) + Typography | Selectable service option for arrangement flow. Heading + optional price (right-aligned) + optional description. role="radio" + aria-checked. Disabled state with opacity token. Maps to Figma ListItemPurchaseOption. |
| SearchBar | done | Input + IconButton + Button | Search input with optional submit button. Enter-to-submit, progressive clear button, inline loading spinner. Guards empty submissions, refocuses after clear. role="search" landmark. Critique: 35/40. |
| AddOnOption | done | Card (interactive, selected) + Typography + Switch | Toggleable add-on for arrangement flow extras. Heading + optional price + description + Switch. Click-anywhere toggle. Maps to Figma ListItemAddItem (2350:40658). |
| StepIndicator | done | Typography + Box | Horizontal segmented progress bar. Brand gold for completed/current steps, grey for upcoming. Responsive bar height (10px/6px). Maps to Figma Progress Bar - Steps (2375:47468). |
| 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). |
| 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
| Component | Status | Composed of | Notes |
|-----------|--------|-------------|-------|
| 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. |
| PricingTable | planned | PriceCard × n + Typography | Comparative pricing display |
| 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). |
| FuneralFinder V1 | archived | Typography + Button + Chip + Input + Divider + Link + custom ChoiceCard/TypeCard/CompletedRow/StepHeading | Archived — viewable in Storybook under Archive/. Stepped conversational flow. Audit: 14/20. Critique: 29/40. |
| FuneralFinder V2 | archived | Typography + Button + Input + Divider + Select + MenuItem + custom StepCircle | Archived — viewable in Storybook under Archive/. Quick-form with step circles. Audit: 18/20. Critique: 33/40. |
| FuneralFinder V4 | archived | Typography + Button + Input + Divider + Select + MenuItem + custom StepIndicator/FieldError | Archived. Based on V2 with: 3 numbered steps (48px circles, outline-to-fill + tick), ungated location field, no heading/subheading, "Search" CTA, inline copper error messages. |
| 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. **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
| Component | Status | Composed of | Notes |
|-----------|--------|-------------|-------|
| WizardLayout | done | Container + Box + Link + Typography + Navigation (slot) + StepIndicator (slot) | Page-level layout for arrangement wizard. **6 variants**: centered-form, wide-form, list-map, list-detail, grid-sidebar, detail-toggles, **bleed**. `bleed` = viewport-locked + `<main>` as single scroll container (both axes) + no inner Container; back link routed into children; scroll host marked `data-wizard-scroll` so descendants can find it (IntersectionObserver roots, etc.). Used by ComparisonPage. Nav slot, sticky help bar, optional back link, optional progress stepper + running total. `<main>` landmark wrapper. |
## Pages
| 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 + 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. |
| 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. |
| VenueStep | done | WizardLayout (centered-form) + VenueCard + AddOnOption + Collapse + Chip + TextField + Divider + Button | Wizard step 7a — venue browsing. Click-to-navigate card grid with search/filters. Leads to VenueDetailStep. |
| VenueDetailStep | done | WizardLayout (detail-toggles) + ImageGallery + Card + Chip + Typography + Button + Divider | Wizard step 7b — venue detail. Two-panel: gallery/description/features/location (left), name/meta/price/CTA/religions (right). Informational service preview. |
| VenueServicesStep | done | WizardLayout (centered-form) + AddOnOption + Card + Typography + Button + Divider | Wizard step 7c — venue services. Compact venue card, availability notices, AddOnOption toggles with "View more" for long descriptions. Follows VenueDetailStep. |
| CrematoriumStep | done | WizardLayout (centered-form) + Card + Badge + ToggleButtonGroup + Typography + Button + Divider | Wizard step 8 — crematorium. Two variants: Service & Cremation (compact card + witness Yes/No toggle), Cremation Only (compact card + "Cremation Only" badge + "Included in Package" notice). Single pre-selected crematorium, no multi-select. |
| CemeteryStep | done | WizardLayout (centered-form) + ToggleButtonGroup + Collapse + TextField (select) + Typography + Button + Divider | Wizard step 9 — cemetery. ToggleButtonGroups (Yes/No/Not sure) with progressive disclosure. Own plot → locate dropdown. No plot → preference? → select dropdown. No card grid. |
| CoffinsStep | done | WizardLayout (grid-sidebar) + Card + Badge + Collapse + Slider + TextField + Pagination + Divider + Link | Wizard step 10 — coffin browsing. Grid-sidebar: filter sidebar (categories with expandable subcategories, dual-knob price slider with editable inputs, sort by) + 3-col card grid. CoffinCard with thumbnail hover preview. Equal-height cards, subtle bg for white-bg product photos. Card click → CoffinDetailsStep (no Continue). 20/page max. Conditional allowance info bubble. |
| CoffinDetailsStep | done | WizardLayout (detail-toggles) + ImageGallery + Divider + Button | Wizard step 11 — coffin detail. Two-panel: gallery + product details dl (left), name + description + colour swatches + allowance-aware price + CTA (right). Allowance logic: fully covered / partially covered / no allowance. Colour selection does not affect price. |
| ~~AdditionalServicesStep~~ | removed | — | Replaced by IncludedServicesStep + ExtrasStep. Split for clearer distinction between free inclusions and paid extras. |
| IncludedServicesStep | done | WizardLayout (centered-form) + AddOnOption + RadioGroup + Collapse + Divider + Button | Wizard step 12a — included services. Package inclusions at no additional cost: dressing, viewing (with same-venue sub-option), prayers/vigil, funeral announcement. Sub-options render inside parent card. |
| ExtrasStep | done | WizardLayout (centered-form) + AddOnOption + Card + Switch + RadioGroup + Collapse + Divider + Button | Wizard step 12b — optional extras. Lead-gen interest capture: catering, music (inline live musician toggle + musician type), coffin bearing (toggle + bearer type), newspaper notice. POA via `priceLabel`. Tally of priced selections. No nested cards. |
| SummaryStep | done | WizardLayout (centered-form) + Card + Paper + DialogShell + Button + Link + Divider | Wizard step 13 — plan review. Visual cart layout: arrangement details (2-col grid), compact cards with thumbnails for provider/venue/crematorium/coffin, checklist for included services, priced list for extras. Allowance display (fully covered vs remaining). Share dialog (multi-email). Location pin icons. Full-width CTA. |
| PaymentStep | done | WizardLayout (centered-form) + ToggleButtonGroup + Paper + Collapse + Checkbox + Divider + Button | Wizard step 14 — payment. Plan (full/deposit) + method (card/bank). PayWay iframe slot. Bank transfer details. Terms checkbox. |
| ConfirmationStep | done | WizardLayout (centered-form) + Button | Wizard step 15 — confirmation. Terminal page. At-need: "submitted" + callback. Pre-planning: "saved" + return-anytime. Muted success icon. |
| UnverifiedProviderStep | done | WizardLayout (list-detail) + ProviderCardCompact + ProviderCard + Badge + Button + Divider + Typography | Unverified provider detail. Left: compact card + "Listing" badge + available info (conditional dl) + verified recommendations. Right: warm header band + detail rows + "Make an Enquiry" CTA. Graceful degradation (no data → straight to enquiry). 4 story variants. |
| HomePage | done | FuneralFinderV3/V4 (via finderSlot) + ProviderCardCompact + Button + Typography + Accordion + Divider + Navigation (prop) + Footer (prop) | Marketing landing page. 4 archived versions: V1 (split hero), V2 (full-bleed parsonshero.png), V3 (hero-3.png + updated copy + logo bar + venue photos + warm CTA gradient), V4 (same as V3 but with FuneralFinderV4 stepped form via finderSlot). `finderSlot` prop allows swapping finder widget. Light grey footer (surface.subtle). |
| ComparisonPage (V2) | done | WizardLayout (**bleed** desktop / wide-form mobile) + ComparisonTable + ComparisonTabCard + ComparisonPackageCard + Divider + Typography + Button + Link | **Production version.** Package comparison page, restructured 2026-04-17. **Desktop**: `bleed` WizardLayout variant. Structure: centred page-header container (maxWidth 1200 = `COMPARISON_TABLE_COL_WIDTH × 4`) with own Back link → `<Divider>` → full-bleed `[spacer][ComparisonTable][spacer]` flex row. When viewport > 1200 spacers centre the table; when a 4th+ package makes the table wider than viewport, spacers collapse to their min-width (16/24px matching the page-header padding) and the table extends rightward with the content's left edge pinned. Recommended package as first (leftmost) column with warm tint + 2px brand-600 border + filled Recommended badge. **Mobile**: `<Divider>` between page header and tab rail. "Choose a package to view" h2 heading (user-centric), serves as `aria-labelledby` for the tablist. Horizontal tab rail (role="tablist") of `ComparisonTabCard` (235px each) + **dot indicator** below (8px grey dots, active expands to 24×8 brand-600 pill, aria-hidden, tabIndex=-1 — visual supplement to the tab rail which is canonical accessible nav). Single `ComparisonPackageCard` in role="tabpanel"; recommended tab is first in rail, first user-selected package is initially active. Share + Print in page header (desktop). Back link, help bar. |
| ComparisonPage V1 | archived | WizardLayout + ComparisonTable + Card + Typography + Button + Divider | Archived — viewable in Storybook under Archive/. Recommended package as **last** column/tab. Same component tree as V2. |
## Future enhancements
Deferred items that should be addressed when the relevant components or patterns
are needed. Check this section before building new components — an item here may
be relevant to your current work.
| Item | Relates to | Trigger | Notes |
|------|-----------|---------|-------|
| Destructive button colours | Button | When building delete/cancel flows | `color="error"` already works via MUI palette. May need `soft` variant styling for error/warning/success colours. |
| Link-as-button | Button | When building Navigation or link-heavy pages | Use MUI's `component="a"` or `href` prop. May warrant a separate Link atom or a `Button` story showing the pattern. |
| ~~IconButton atom~~ | ~~IconButton~~ | ~~Resolved~~ | ~~Built as atom. Rounded rect, 3 sizes, 4 colours, focus ring.~~ |
| ~~Google Fonts loading~~ | ~~Typography~~ | ~~Resolved~~ | ~~Added to .storybook/preview-head.html and index.html~~ |

File diff suppressed because it is too large Load Diff

View File

@@ -1,177 +0,0 @@
# Client demo deploy — runbook
How to set up `parsons.tensordesign.com.au` for the first time, and how to push updates after.
Companion to [`client-demo-hosting-plan.md`](./client-demo-hosting-plan.md), which has the why and the architecture. This file is the actionable how.
---
## One-time server setup
You'll do this once. After that, deploys are a single script.
### 1. DNS
Point `parsons.tensordesign.com.au` at your home IP (or DDNS hostname). One A-record, same as your other subdomains on `tensordesign.com.au`.
Verify after propagation (can take minutes):
```bash
dig +short parsons.tensordesign.com.au
```
Should return your home IP.
### 2. swag — add `parsons` to SUBDOMAINS
In your swag container's environment (compose file or `docker run` flags), add `parsons` to the comma-separated `SUBDOMAINS` list. Then restart the container:
```bash
docker compose restart swag
# or: docker restart swag
```
Watch the logs until you see Let's Encrypt issue the cert:
```bash
docker logs -f swag
# look for: "Certificate for parsons.tensordesign.com.au issued"
```
### 3. Document root on the host
Pick where the static files live on the host filesystem. Suggested:
```bash
sudo mkdir -p /srv/parsons-demos
sudo chown -R "$USER:$USER" /srv/parsons-demos
```
Make sure swag has access to it. Either:
- **Mount it into swag** at `/config/www/parsons-demos/` (preferred — keeps swag's container view tidy):
```yaml
# in your swag compose service:
volumes:
- /srv/parsons-demos:/config/www/parsons-demos:ro
```
Restart swag after editing compose.
- Or symlink inside the existing swag config volume — works but messier.
### 4. Drop the nginx config in
The repo has the conf at `nginx/parsons-demos.conf`. Copy it into swag's `site-confs/` directory:
```bash
cp nginx/parsons-demos.conf /path/to/swag/config/nginx/site-confs/
docker compose exec swag nginx -t # syntax check
docker compose exec swag nginx -s reload
```
If `nginx -t` complains, fix before reloading (a bad config will take swag down).
### 5. Create the htpasswd
Pick a username (suggestion: `client`) and a strong shared password:
```bash
docker compose exec swag htpasswd -c /config/nginx/.htpasswd-parsons client
# prompts for password
```
For additional users later (e.g. one credential per client engagement), drop the `-c`:
```bash
docker compose exec swag htpasswd /config/nginx/.htpasswd-parsons another-user
```
### 6. Verify the auth + 404
Visit `https://parsons.tensordesign.com.au/` in a fresh browser. You should see:
1. SSL is valid (no cert warning)
2. Browser asks for username + password
3. After auth: empty page or 404 (no slices deployed yet — that's fine)
If you see this far, the server is ready.
---
## Per-deploy workflow
Once setup is done, the loop is two commands:
```bash
# 1. Build the slice
npm run demo:build -- --mode arrangement
# 2. Push it up
./scripts/deploy-demo.sh arrangement
```
The deploy script:
- Verifies `dist-demo/arrangement/` exists and isn't empty (aborts if not — won't rsync a half-built bundle over a working demo)
- `rsync -az --delete` to the server (removes stale asset hashes)
- Prints the URL to visit
**Before first deploy:** edit `scripts/deploy-demo.sh` and set:
- `TARGET_HOST="<your-ssh-user>@tensordesign.com.au"`
- `TARGET_BASE="/srv/parsons-demos"` (or wherever you put the document root)
Make sure SSH key auth works (`ssh "$TARGET_HOST" echo ok` should succeed without a password prompt) so rsync doesn't stall.
The script lives in `scripts/` which is gitignored, so your server-specific paths won't leak into the repo.
---
## Adding a second slice later
1. Build the new app under `src/demo/apps/<new-slice>/` (mirror `arrangement/`'s structure).
2. `npm run demo:build -- --mode <new-slice>`.
3. `./scripts/deploy-demo.sh <new-slice>`.
The nginx config catches `/<anything>/...` automatically — no server changes needed for new slices.
---
## Optional: landing page at `/`
Until you have one, `https://parsons.tensordesign.com.au/` returns 404. To add a tiny index listing available slices:
```bash
cat > /srv/parsons-demos/index.html <<'EOF'
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Parsons demos</title></head>
<body style="font-family: system-ui; max-width: 40em; margin: 4em auto; padding: 0 1em">
<h1>Parsons demos</h1>
<ul>
<li><a href="/arrangement/">Arrangement flow</a></li>
</ul>
</body>
</html>
EOF
```
Add `<li>` entries as new slices ship.
---
## Troubleshooting
**Cert didn't issue.** Check swag logs (`docker logs swag`). Common causes: DNS not propagated yet, port 80 blocked by router, `SUBDOMAINS` value missing or misspelled.
**`nginx -t` fails after dropping the conf.** Most likely a path mismatch — `/config/www/parsons-demos` doesn't exist inside the container because the bind mount isn't set. Check `docker compose config` to confirm the mount is in effect.
**Auth prompt loops / 401s after correct password.** htpasswd file path mismatch between conf and `htpasswd -c` location. Both must agree.
**Demo loads but assets 404.** Vite `base` path mismatch. The build must use `--mode <slice>` so assets are prefixed with `/<slice>/`. Re-run the build and check `dist-demo/<slice>/index.html` — script src should look like `/<slice>/assets/index-XXX.js`.
**`rsync` stalls or asks for password.** SSH key auth not set up. Run `ssh-copy-id <TARGET_HOST>` once.
**Want to roll back a deploy.** rsync with `--delete` is irreversible — there's no built-in undo. Either keep the previous build locally and re-deploy, or rebuild the previous git commit. For demo-grade work this is fine; if you need versioned deploys later, switch to dated subfolders + a symlink swap.

View File

@@ -1,218 +0,0 @@
# Client demo hosting plan
**Status:** scoped, not implemented.
**Target:** share self-contained, interactive demos of the FA design system with clients via `parsons.tensordesign.com.au/<slice>`, gated behind basic auth, independently buildable per slice.
---
## Why not Storybook or Chromatic alone
- **Storybook** — great for isolated components and per-story state, but cross-page flows with persistent state (comparison basket, map selections, route navigation) are outside its shape. You can fake flows with story parameters, but it's brittle and doesn't feel like a product.
- **Chromatic** — built for visual regression diffs + internal review. UX is Storybook-shaped, which is noisy for non-technical clients. Keep Chromatic for the internal review workflow; use self-hosted demos for client-facing previews.
The demo-hosting solution is **additive** — it doesn't replace either of those.
---
## Goals
1. Multiple demo "slices" per project — e.g. `/arrangement`, `/home`, `/compare` — each independently buildable and deployable.
2. Each slice behaves like a real product: real URLs, real navigation, real state (comparison basket persists across pages, selections survive drill-in, etc.).
3. Dummy data only — no CMS, no real users, no auth beyond a single shared htpasswd for the whole demo host.
4. Zero disruption to the component library — demos consume the existing page components as-is.
5. Demos live alongside the codebase but build into their own output tree (`dist-demo/<slice>/`) so neither the component library nor Storybook is affected.
---
## URL shape
**Single subdomain, subpath per slice:**
- `parsons.tensordesign.com.au/` → tiny index page listing available demos (optional, but handy once there are 3+)
- `parsons.tensordesign.com.au/arrangement` → Providers → Packages → Comparison flow
- `parsons.tensordesign.com.au/home` → homepage exploration
- `parsons.tensordesign.com.au/<future slice>`
One SSL cert (Let's Encrypt), one nginx server block, one htpasswd covering the whole host.
**Why subpath-per-slice over subdomain-per-slice:** subdomain-per-slice needs wildcard DNS + wildcard cert + per-demo nginx blocks. More moving parts for no client-facing benefit. Subpath works cleanly when each Vite build declares its own `base` at build time.
**Why this host over a wildcard demo subdomain:** `parsons.tensordesign.com.au` reads clearly to clients ("this is the Parsons project"). If you later run demos for other clients, add siblings (`acme.tensordesign.com.au`) rather than splitting Parsons across multiple hosts.
---
## Project structure
```
src/demo/
shared/
fixtures/ # cross-slice mock data (providers, packages, venues)
state/ # common stores — comparison basket, nav shell
theme/ # ThemeProvider wrapper (mirrors Storybook decorators)
apps/
arrangement/
main.tsx # Vite entry
App.tsx # Router shell
routes/
providers.tsx
packages.tsx
comparison.tsx
fixtures/ # arrangement-specific overrides
home/
main.tsx
App.tsx
routes/
landing.tsx
index-demo.html # template — slot in slice-specific title/base
scripts/
build-demo.sh <name> # vite build -c vite.demo.config.ts --mode <name>
deploy-demo.sh <name> # rsync dist-demo/<name>/ to server path
vite.demo.config.ts # reads slice name from --mode, sets base + outDir
docs/reference/
client-demo-hosting-plan.md # this file
client-demo-deploy.md # once implemented — ops runbook
```
**Fixture-sharing rule:** anything that could plausibly appear in two slices lives in `src/demo/shared/fixtures/`. Slice-specific overrides live in `apps/<slice>/fixtures/`. Stories continue to use their own fixtures unchanged — no cross-contamination.
---
## State shape (the one thing worth being deliberate about)
The comparison basket is cross-page state. Whatever we pick here will likely inform the real app's state layer later, so treat it as the prototype for production, not throwaway glue.
**Recommended: Zustand with URL-persistence for package IDs.**
```ts
// src/demo/shared/state/useComparisonBasket.ts
interface ComparisonBasket {
packageIds: string[]; // ordered — insertion order = display order
add: (id: string) => void;
remove: (id: string) => void;
clear: () => void;
isFull: () => boolean; // 4 max per FA convention
}
```
- **Zustand** over Context: less boilerplate, better Devtools story, selector-based subscriptions avoid re-render cascades.
- **URL-persistence** (`?compare=a,b,c`) so a client can bookmark a specific comparison and reload. Easy `useEffect` hook that syncs store ↔ URL search param.
- **Insertion order preserved** — matches ComparisonPage columns left-to-right.
- **No localStorage** — keeps demo stateless between sessions unless client explicitly shares a URL. Makes client demos predictable ("click here, you'll see exactly what I saw").
Other state that might need a shared store: selected provider (persists across routes), map viewport (so map doesn't reset on drill-in). Start with just the basket; add others when a route actually needs them.
---
## Vite build
One config file, slice name passed via `--mode`:
```ts
// vite.demo.config.ts
export default defineConfig(({ mode }) => ({
root: `src/demo/apps/${mode}`,
base: `/${mode}/`,
build: {
outDir: `../../../../dist-demo/${mode}`,
emptyOutDir: true,
},
// ... shared plugins, resolve aliases identical to main vite.config
}));
```
`npm run build:demo -- --mode arrangement` produces `dist-demo/arrangement/` ready to ship.
**Shared scripts:**
```json
"scripts": {
"demo:dev": "vite -c vite.demo.config.ts",
"demo:build": "vite build -c vite.demo.config.ts",
"demo:deploy": "./scripts/deploy-demo.sh"
}
```
---
## Deploy
```bash
# scripts/deploy-demo.sh <slice>
SLICE=$1
rsync -az --delete dist-demo/$SLICE/ richie@tensordesign.com.au:/var/www/parsons-demos/$SLICE/
```
Slice-by-slice deploy means polishing one demo doesn't require rebuilding or redeploying others. Add the index page as a trivial static HTML (no build) committed to `scripts/demo-index.html` and rsynced to the host root.
Consider a tiny pre-flight check in the deploy script — abort if the build output is missing or empty, to avoid rsync-ing a half-built bundle over a working demo.
---
## nginx + basic auth
```nginx
server {
listen 443 ssl http2;
server_name parsons.tensordesign.com.au;
ssl_certificate /etc/letsencrypt/live/parsons.tensordesign.com.au/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/parsons.tensordesign.com.au/privkey.pem;
root /var/www/parsons-demos;
index index.html;
auth_basic "Parsons demos";
auth_basic_user_file /etc/nginx/htpasswd/parsons;
# SPA fallback per slice — each demo has its own index.html
location ~ ^/(?<slice>[^/]+)/ {
try_files $uri $uri/ /$slice/index.html;
}
# Root lists available demos
location = / {
try_files /index.html =404;
}
}
```
htpasswd setup:
```bash
sudo htpasswd -c /etc/nginx/htpasswd/parsons client
```
Single credential shared across all slices. If a client project ever needs isolated access, split at the location block with per-slice `auth_basic_user_file`.
---
## Rollout order (next session)
1. **Scaffold** `src/demo/shared/` (fixtures extracted from `PackagesStep.stories.tsx` + `ComparisonPage.stories.tsx`) and the Zustand basket store.
2. **First slice — `arrangement`** with three routes (Providers → Packages → Comparison). Prove the basket persists across navigation, the back button works, drill-in still fires on mobile.
3. **`vite.demo.config.ts` + `build-demo.sh`** — confirm `dist-demo/arrangement/` builds and serves standalone via `npx serve dist-demo/arrangement`.
4. **nginx + htpasswd + Let's Encrypt** on the server. One-time ops setup.
5. **Deploy script** — rsync wired to a single command.
6. **Test end-to-end** — visit `parsons.tensordesign.com.au/arrangement`, click through the flow, verify basket state, verify URL bookmark restores state.
7. **Optional: index page + second slice** once the first is proven.
Estimated effort: half-day scaffold + ~day for the arrangement slice + half-day ops setup. Total ~2 days before the first client-shareable link.
---
## Non-goals (for this iteration)
- No real backend, no CMS, no user accounts, no persistence beyond URL params.
- No analytics or telemetry (demos are short-lived; instrumenting them is noise).
- No E2E tests for demo routes (fixtures + trusted component library = low ROI).
- No automated deploy on merge — manual `npm run demo:deploy arrangement` is fine at this volume. Revisit if demos are updated daily.
- No custom domain per client — stick with subpath-per-slice under `parsons.tensordesign.com.au`.
---
## Open questions for next session
- Exact path for the server-side document root (`/var/www/parsons-demos/` is a placeholder — confirm what tensordesign.com.au's existing nginx expects).
- Whether to seed the `home` slice or a different one as the second demo after `arrangement` is proven.
- Whether the comparison basket should persist across browser tabs (probably no — URL-only is cleaner for demos) or across reloads without a URL (probably no for the same reason).

View File

@@ -1,154 +0,0 @@
# Component Lifecycle
Every component follows this lifecycle. Skills are run in order — each stage must
pass before moving to the next. This prevents ad-hoc back-and-forth tweaking.
## The Stages
```
┌─────────────────────────────────────────────────────────────┐
│ 1. BUILD /build-atom, /build-molecule, /build-organism │
│ 2. STORIES /write-stories │
│ 3. INTERNAL QA /audit → /critique → /harden │
│ 4. FIX Fix all P0 and P1 issues from stage 3 │
│ 5. POLISH /polish → /typeset → /adapt │
│ 6. PRESENT Show to user in Storybook │
│ 7. ITERATE User feedback → targeted fixes (1-2 rounds) │
│ 8. NORMALIZE /normalize (cross-component consistency) │
│ 9. PREFLIGHT /preflight │
│ 10. COMMIT git add → commit → push │
└─────────────────────────────────────────────────────────────┘
```
## When to use each skill
### Stage 1 — BUILD
**Skill:** `/build-atom`, `/build-molecule`, `/build-organism`
**When:** Starting a new component. The skill handles reading memory files,
checking the registry, creating the file structure, and writing the code.
**Output:** Component .tsx + stories .tsx + index.ts
### Stage 2 — STORIES
**Skill:** `/write-stories`
**When:** If the build skill didn't produce comprehensive stories, or if stories
need updating after changes. Stories must cover: default, all variants, all
sizes, disabled, loading, error, long content, minimal content.
**Output:** Complete story coverage in Storybook
### Stage 3 — INTERNAL QA (run before showing to user)
Three skills, run in this order:
1. **`/audit`** — Technical quality (a11y, performance, theming, responsive, design).
Produces a score out of 20 and P0-P3 issues.
2. **`/critique`** — UX design review (hierarchy, emotion, cognitive load, composition).
Produces a score out of 40 and priority issues.
3. **`/harden`** — Edge cases (error states, empty states, loading, boundaries, disabled).
Ensures robustness for real-world data.
**Exit criteria:** No P0 issues remaining. P1 issues documented.
### Stage 4 — FIX
**No skill — just implementation work.**
**When:** Fix all P0 and P1 issues found in stage 3.
Then re-run the relevant check (e.g., if the fix was an a11y issue, re-run
`/audit` to verify). Don't re-run all three unless the fixes were broad.
**Exit criteria:** P0 = 0, P1 = 0 (or documented as intentional with rationale).
### Stage 5 — POLISH
Three skills, run as needed based on the component:
1. **`/polish`** — Visual alignment, spacing, transitions, copy, micro-details.
Run on every component.
2. **`/typeset`** — Typography: hierarchy, line length, weight, readability.
Run on text-heavy components (cards, forms, detail panels).
3. **`/adapt`** — Responsive: touch targets, overflow, mobile spacing.
Run on layout components (organisms, cards, navigation).
**Optional context-specific skills:**
- **`/quieter`** — Run on components that handle sensitive moments (pricing,
commitment steps, error messaging). Not needed for utility atoms.
- **`/clarify`** — Run on components with decision points or complex information
(FuneralFinder, ArrangementForm, PricingTable). Not needed for simple atoms.
### Stage 6 — PRESENT
**No skill — show in Storybook.**
**When:** All internal QA is done. The component should be in its best state
before the user sees it. Present with a brief summary of what it does, key
design decisions, and scores from audit/critique.
### Stage 7 — ITERATE
**No skill — targeted fixes from user feedback.**
**When:** User reviews in Storybook and gives feedback. This should be 1-2 rounds
max because stages 3-5 caught most issues. If feedback requires major changes,
go back to stage 1. Minor tweaks stay here.
**Exit criteria:** User approves.
### Stage 8 — NORMALIZE
**Skill:** `/normalize`
**When:** After user approval, run against the component's tier (e.g., `/normalize atoms`)
to check it's consistent with its peers. This catches: token access patterns (D031),
transition timing, focus styles, spacing methods, displayName, exports.
**Note:** This is a cross-component check, so it's most valuable after several
components in a tier are done. Can be batched.
### Stage 9 — PREFLIGHT
**Skill:** `/preflight`
**When:** Before committing. Verifies TypeScript, Storybook build, token sync,
hardcoded values, exports, ESLint, Prettier.
**Exit criteria:** All critical checks pass.
### Stage 10 — COMMIT
**No skill — git workflow.**
Stage, commit with descriptive message, push. Husky runs lint-staged automatically.
---
## Shorthand for quick reference
| Stage | Skill(s) | Who triggers | Blocking? |
|-------|----------|-------------|-----------|
| Build | /build-{tier} | User requests | — |
| Stories | /write-stories | Auto in build | — |
| Internal QA | /audit → /critique → /harden | Agent (auto) | P0 = blocking |
| Fix | — | Agent | Until P0/P1 = 0 |
| Polish | /polish + /typeset + /adapt | Agent (auto) | — |
| Present | — | Agent → User | — |
| Iterate | — | User feedback | 1-2 rounds |
| Normalize | /normalize | Agent (batch OK) | — |
| Preflight | /preflight | Agent (auto) | Critical = blocking |
| Commit | — | Agent | — |
**"Agent (auto)"** means I should run these proactively without being asked.
**"Agent (batch OK)"** means it can be deferred and run across multiple components.
---
## Which skills are optional vs required?
| Skill | Required for | Optional for |
|-------|-------------|-------------|
| /audit | All components | — |
| /critique | All molecules + organisms | Simple atoms (Button, Divider) |
| /harden | All interactive components | Display-only atoms (Typography, Badge) |
| /polish | All components | — |
| /typeset | Text-heavy components | Icon-only or structural components |
| /adapt | Layout components, organisms | Small inline atoms |
| /quieter | Sensitive context components | Utility atoms |
| /clarify | Decision-point components | Simple atoms |
| /normalize | All (batched by tier) | — |
| /preflight | All (before commit) | — |
---
## For existing components
Components built before this lifecycle was defined can be retroactively
reviewed using a condensed process:
1. `/normalize {tier}` — Scan the tier for consistency issues
2. `/audit {component}` — Score each component
3. Fix P0/P1 issues only (don't re-polish what's already working)
4. `/preflight` → commit
This is lighter than the full lifecycle because these components have already
been through user review and iteration.

View File

@@ -1,203 +0,0 @@
# FuneralFinder — Flow Logic Reference
Technical reference for the FuneralFinder stepped search widget.
Use this when modifying the flow, adding steps, or integrating with a backend.
## Architecture Overview
The widget is a **single React component** with internal state. No external state
management required. The parent only needs to provide `funeralTypes`, optional
`themeOptions`, and an `onSearch` callback.
```
┌─────────────────────────────────────────┐
│ Header (h2 display + subheading) │
│ ───────────────────────────────── │
│ │
│ CompletedRows (stack of answered steps)│
│ │
│ Active Step (one at a time, Collapse) │
│ Step 1 │ Step 2 │ Step 3 │ Step 4 │
│ │
│ ─── always visible ─────────────────── │
│ Location input │
│ [Find funeral providers] CTA │
│ Free to use · No obligation │
└─────────────────────────────────────────┘
```
## State
| State variable | Type | Default | Purpose |
|---|---|---|---|
| `intent` | `'arrange' \| 'preplan' \| null` | `null` | Step 1 answer |
| `planningFor` | `'myself' \| 'someone-else' \| null` | `null` | Step 2 answer (preplan only) |
| `typeSelection` | `string \| null` | `null` | Step 3 answer — funeral type ID or `'all'` |
| `servicePref` | `'with-service' \| 'without-service' \| 'either'` | `'either'` | Step 4 answer |
| `serviceAnswered` | `boolean` | `false` | Whether step 4 was explicitly answered |
| `selectedThemes` | `string[]` | `[]` | Optional theme filter IDs (multi-select) |
| `location` | `string` | `''` | Location input value |
| `locationError` | `string` | `''` | Validation error for location |
| `showIntentPrompt` | `boolean` | `false` | Show nudge when CTA clicked without intent |
| `editingStep` | `number \| null` | `null` | Which step is being re-edited (via "Change") |
## Step Flow
### Active Step Calculation
```typescript
const activeStep = (() => {
if (editingStep !== null) return editingStep; // User clicked "Change"
if (!intent) return 1; // Need intent
if (needsPlanningFor && !planningFor) return 2; // Need planning-for (preplan only)
if (!typeSelection) return 3; // Need funeral type
if (showServiceStep && !serviceAnswered) return 4; // Need service pref
return 0; // All complete
})();
```
`activeStep === 0` means all optional steps are answered. Only CompletedRows +
location + CTA are visible.
### Step Details
| Step | Question | Options | Auto-advances? | Conditional? |
|---|---|---|---|---|
| 1 | How can we help you today? | Arrange now / Pre-plan | Yes, on click | Always shown |
| 2 | Who are you planning for? | Myself / Someone else | Yes, on click | Only when `intent === 'preplan'` |
| 3 | What type of funeral? | TypeCards + Explore All + theme chips | Yes, on type card click | Always shown |
| 4 | Would you like a service? | With / No / Flexible (chips) | Yes, on chip click | Only when selected type has `hasServiceOption: true` |
### Auto-advance Mechanic
Steps 1, 2, and 4 auto-advance because selecting an option sets the state and
clears `editingStep`. The `activeStep` recalculation on the next render
determines the new step.
Step 3 also auto-advances when a type card is clicked. Theme preferences within
step 3 are optional — they're captured at whatever state they're in when the
type card click triggers collapse.
### Editing (reverting to a previous step)
Clicking "Change" on a CompletedRow calls `revertTo(stepNumber)`, which sets
`editingStep`. This overrides the `activeStep` calculation, reopening that step.
When the user makes a new selection, the handler clears `editingStep` and the
flow recalculates.
**Key behaviour:** Editing a step does NOT reset downstream answers. If you
change from Cremation to Burial (both have `hasServiceOption`), the service
preference carries forward. If you change to a type without `hasServiceOption`
(or to "Explore all"), `servicePref` resets to `'either'` and `serviceAnswered`
resets to `false`.
## CTA and Search Logic
### Minimum Requirements
The CTA button is **always visible and always enabled** (except during loading).
Minimum search requirements: **intent + location (3+ chars)**.
### Submit Behaviour
```
User clicks "Find funeral providers"
├─ intent is null?
│ → Show intent prompt (role="alert"), keep step 1 visible
│ → Return (don't search)
├─ location < 3 chars?
│ → Show error on location input
│ → Return (don't search)
└─ Both present?
→ Call onSearch() with smart defaults for missing optional fields
```
### Smart Defaults
| Field | If not explicitly answered | Default value |
|---|---|---|
| `funeralTypeId` | User didn't select a type | `null` (= show all types) |
| `servicePreference` | User didn't answer service step | `'either'` (= show all) |
| `themes` | User didn't select any themes | `[]` (= no filter) |
| `planningFor` | User on preplan path but didn't answer step 2 | `undefined` |
This means a user can: select intent → type location → click CTA. Everything
else defaults to "show all."
### Search Params Shape
```typescript
interface FuneralSearchParams {
intent: 'arrange' | 'preplan';
planningFor?: 'myself' | 'someone-else'; // Only on preplan path
funeralTypeId: string | null; // null = all types
servicePreference: 'with-service' | 'without-service' | 'either';
themes: string[]; // May be empty
location: string; // Trimmed, 3+ chars
}
```
## Conditional Logic Map
```
intent === 'preplan'
└─ Shows step 2 (planning-for)
typeSelection !== 'all' && selectedType.hasServiceOption === true
└─ Shows step 4 (service preference)
typeSelection !== null
└─ CompletedRow for type shows (with theme summary if any selected)
serviceAnswered && showServiceStep
└─ CompletedRow for service shows
themeOptions.length > 0
└─ Theme chips appear within step 3 (always, not gated by type selection)
loading === true
└─ CTA button shows spinner, button disabled
```
## Props Reference
| Prop | Type | Default | Notes |
|---|---|---|---|
| `funeralTypes` | `FuneralTypeOption[]` | required | Each has `id`, `label`, optional `description`, `note`, `hasServiceOption` |
| `themeOptions` | `ThemeOption[]` | `[]` | Each has `id`, `label`. Shown as optional chips in step 3 |
| `onSearch` | `(params: FuneralSearchParams) => void` | — | Called on valid submit |
| `loading` | `boolean` | `false` | Shows spinner on CTA, disables button |
| `heading` | `string` | `'Find funeral directors near you'` | Main h2 heading |
| `subheading` | `string` | `'Tell us a little about...'` | Below heading |
| `showExploreAll` | `boolean` | `true` | Show "Explore all options" TypeCard |
| `sx` | `SxProps<Theme>` | — | MUI sx override on root card |
## Sub-components (internal)
| Component | Purpose | Used in |
|---|---|---|
| `StepHeading` | Centered bodyLg heading with bottom margin | Steps 1-4 |
| `ChoiceCard` | Full-width radio card with label + description | Steps 1, 2 |
| `TypeCard` | Compact radio card with label + optional description/note | Step 3 |
| `CompletedRow` | Summary row: question + bold answer + "Change" link | All completed steps |
## Adding a New Step
1. Add state variable(s) for the new step's answer
2. Add a condition in `activeStep` calculation (between existing steps)
3. Add a `<Collapse in={activeStep === N}>` block in the render
4. Add a `<Collapse>` for the CompletedRow (with appropriate visibility condition)
5. Include the new data in `handleSubmit``onSearch()` params
6. Update `FuneralSearchParams` type
## Known Limitations (deferred)
- **No progress indicator** — users can't see how many steps remain
- **No roving tabindex** — radiogroups use button elements with `role="radio"` but
arrow-key navigation between options is not implemented
- **No location autocomplete** — free text input only, validated on length
- **CSS vars used directly** — some styling uses `var(--fa-*)` tokens instead of
MUI theme paths; works but doesn't support dynamic theme switching