From c7a8d9e906417949c7e0d0a30bdb7bcb5b7748c5 Mon Sep 17 00:00:00 2001 From: Richie Date: Tue, 31 Mar 2026 11:26:29 +1100 Subject: [PATCH] CoffinDetailsStep: rewrite to match VenueDetailStep layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Two-panel detail-toggles: gallery + specs (left), product info + CTA (right) - Colour swatch picker (circular buttons, controlled selection, no price impact) - Allowance-aware pricing: fully covered shows "no change to plan total", partially covered shows breakdown with additional cost to plan - Product details as semantic dl list (bold label above value) - Removed: old specs grid, termsText prop, priceNote prop, info bubble - Added: CoffinColour type, allowanceAmount prop, onAddCoffin callback - Fixed heading hierarchy (h1 → h2, price as p not h5) — 0 a11y violations - Stories: FullyCovered, PartiallyCovered, NoAllowance, NoColours, Minimal, PrePlanning, Loading Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CoffinDetailsStep.stories.tsx | 162 ++++++++--- .../CoffinDetailsStep/CoffinDetailsStep.tsx | 259 +++++++++++++----- .../pages/CoffinDetailsStep/index.ts | 8 +- 3 files changed, 325 insertions(+), 104 deletions(-) diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx index f4d3dd4..c6067b8 100644 --- a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { CoffinDetailsStep } from './CoffinDetailsStep'; import type { CoffinProfile } from './CoffinDetailsStep'; @@ -34,19 +35,59 @@ const nav = ( ); const sampleCoffin: CoffinProfile = { - name: 'Cedar Classic', + name: 'Richmond Rosewood Coffin', imageUrl: 'https://images.unsplash.com/photo-1618220179428-22790b461013?w=600&h=400&fit=crop', - description: - 'A beautifully crafted solid cedar coffin with a natural satin finish. Handmade with care and attention to detail.', - specs: [ - { label: 'Range', value: 'Solid Timber' }, - { label: 'Shape', value: 'Coffin' }, - { label: 'Wood', value: 'Cedar' }, - { label: 'Finish', value: 'Satin' }, - { label: 'Hardware', value: 'Brass' }, + images: [ + { + src: 'https://images.unsplash.com/photo-1618220179428-22790b461013?w=600&h=400&fit=crop', + alt: 'Richmond Rosewood Coffin — front view', + }, + { + src: 'https://placehold.co/600x400/F5F5F0/8B8B7E?text=Interior+Lining', + alt: 'Richmond Rosewood Coffin — interior', + }, + { + src: 'https://placehold.co/600x400/F5F5F0/8B8B7E?text=Handle+Detail', + alt: 'Richmond Rosewood Coffin — handle detail', + }, + { + src: 'https://placehold.co/600x400/F5F5F0/8B8B7E?text=Side+View', + alt: 'Richmond Rosewood Coffin — side view', + }, ], - price: 2800, - priceNote: 'Selecting this coffin does not change your plan total.', + description: + 'Flat lid MDF coffin with wide mould on sides and lid, fitted with 6 silver handles with a satin trim, paper veneered wood grain finish with a gloss finish.', + specs: [ + { label: 'Range', value: 'Custom Board' }, + { label: 'Shape', value: 'Coffin' }, + { label: 'Colour', value: 'Rosewood' }, + { label: 'Finish', value: 'Gloss' }, + { label: 'Hardware', value: 'Silver' }, + ], + price: 1750, + colours: [ + { id: 'natural', name: 'Natural Oak', hex: '#D4A76A' }, + { id: 'walnut', name: 'Walnut', hex: '#6B4226' }, + { id: 'ebony', name: 'Ebony', hex: '#2C2C2C' }, + { id: 'mahogany', name: 'Mahogany', hex: '#4E2728' }, + { id: 'white', name: 'White Wash', hex: '#E8E4DC' }, + ], +}; + +// ─── Wrapper for controlled colour state ──────────────────────────────────── + +const ColourWrapper: React.FC> = (props) => { + const [selectedColourId, setSelectedColourId] = React.useState( + props.selectedColourId ?? props.coffin.colours?.[0]?.id, + ); + + return ( + + ); }; // ─── Meta ──────────────────────────────────────────────────────────────────── @@ -63,15 +104,79 @@ const meta: Meta = { export default meta; type Story = StoryObj; -// ─── Default ──────────────────────────────────────────────────────────────── +// ─── Fully covered (allowance >= price) ───────────────────────────────────── -/** Coffin details — simplified view with profile + CTA (D-G) */ -export const Default: Story = { +/** Allowance fully covers this coffin — no additional cost */ +export const FullyCovered: Story = { + render: (args) => , args: { - coffin: sampleCoffin, - onContinue: () => alert('Continue'), + coffin: { ...sampleCoffin, price: 900 }, + onAddCoffin: () => alert('Add coffin'), onBack: () => alert('Back to coffin selection'), onSaveAndExit: () => alert('Save'), + allowanceAmount: 1000, + navigation: nav, + }, +}; + +// ─── Partially covered (allowance < price) ────────────────────────────────── + +/** Allowance partially covers this coffin — shows additional cost */ +export const PartiallyCovered: Story = { + render: (args) => , + args: { + coffin: sampleCoffin, + onAddCoffin: () => alert('Add coffin'), + onBack: () => alert('Back to coffin selection'), + onSaveAndExit: () => alert('Save'), + allowanceAmount: 500, + navigation: nav, + }, +}; + +// ─── No allowance ─────────────────────────────────────────────────────────── + +/** No package allowance — price shown without context */ +export const NoAllowance: Story = { + render: (args) => , + args: { + coffin: sampleCoffin, + onAddCoffin: () => alert('Add coffin'), + onBack: () => alert('Back'), + onSaveAndExit: () => alert('Save'), + navigation: nav, + }, +}; + +// ─── No colours ───────────────────────────────────────────────────────────── + +/** Coffin with no colour options */ +export const NoColours: Story = { + args: { + coffin: { + ...sampleCoffin, + colours: undefined, + }, + onAddCoffin: () => alert('Add coffin'), + onBack: () => alert('Back'), + onSaveAndExit: () => alert('Save'), + allowanceAmount: 500, + navigation: nav, + }, +}; + +// ─── Minimal ──────────────────────────────────────────────────────────────── + +/** Coffin with no specs, colours, or description */ +export const Minimal: Story = { + args: { + coffin: { + name: 'Standard White', + imageUrl: 'https://placehold.co/600x400/F5F5F0/8B8B7E?text=Standard+White', + price: 1200, + }, + onAddCoffin: () => alert('Add coffin'), + onBack: () => alert('Back'), navigation: nav, }, }; @@ -80,40 +185,27 @@ export const Default: Story = { /** Pre-planning variant — softer copy */ export const PrePlanning: Story = { + render: (args) => , args: { coffin: sampleCoffin, - onContinue: () => alert('Continue'), + onAddCoffin: () => alert('Select coffin'), onBack: () => alert('Back'), isPrePlanning: true, - navigation: nav, - }, -}; - -// ─── Minimal info ─────────────────────────────────────────────────────────── - -/** Coffin with no specs or description */ -export const MinimalInfo: Story = { - args: { - coffin: { - name: 'Standard White', - imageUrl: 'https://placehold.co/600x400/F5F5F0/8B8B7E?text=Standard+White', - price: 1200, - }, - onContinue: () => alert('Continue'), - onBack: () => alert('Back'), + allowanceAmount: 500, navigation: nav, }, }; // ─── Loading ──────────────────────────────────────────────────────────────── -/** Continue button loading */ +/** CTA button in loading state */ export const Loading: Story = { args: { coffin: sampleCoffin, - onContinue: () => {}, + onAddCoffin: () => {}, onBack: () => alert('Back'), loading: true, + allowanceAmount: 500, navigation: nav, }, }; diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx index ef64c32..83436cf 100644 --- a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx @@ -6,9 +6,18 @@ import { ImageGallery } from '../../molecules/ImageGallery'; import type { GalleryImage } from '../../molecules/ImageGallery'; import { Typography } from '../../atoms/Typography'; import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; // ─── Types ─────────────────────────────────────────────────────────────────── +/** Colour option for a coffin */ +export interface CoffinColour { + id: string; + name: string; + /** Hex colour value for the swatch */ + hex: string; +} + /** Coffin specification key-value pair */ export interface CoffinSpec { label: string; @@ -19,26 +28,38 @@ export interface CoffinSpec { export interface CoffinProfile { name: string; imageUrl: string; - /** Additional gallery images (if provided, imageUrl becomes the first) */ + /** Additional gallery images */ images?: GalleryImage[]; + /** Short description shown on the right panel */ description?: string; + /** Product specifications (Range, Shape, Colour, Finish, Hardware) */ specs?: CoffinSpec[]; price: number; - priceNote?: string; + /** Available colour options */ + colours?: CoffinColour[]; } +/** Package coffin allowance amount (shown in info bubble, omit if no allowance) */ +export type CoffinAllowanceAmount = number; + /** Props for the CoffinDetailsStep page component */ export interface CoffinDetailsStepProps { - /** Callback when the Continue button is clicked */ - onContinue: () => void; + /** The selected coffin profile */ + coffin: CoffinProfile; + /** Callback when "Add Coffin" is clicked */ + onAddCoffin: () => void; /** Callback for back navigation */ onBack?: () => void; /** Callback for save-and-exit */ onSaveAndExit?: () => void; - /** Whether the Continue button is in a loading state */ + /** Whether the CTA is in a loading state */ loading?: boolean; - /** The selected coffin profile */ - coffin: CoffinProfile; + /** Selected colour ID (controlled) */ + selectedColourId?: string; + /** Colour change callback */ + onColourChange?: (colourId: string) => void; + /** Package coffin allowance amount (shown in info bubble, omit if no allowance) */ + allowanceAmount?: number; /** Whether this is a pre-planning flow */ isPrePlanning?: boolean; /** Navigation bar */ @@ -53,27 +74,72 @@ export interface CoffinDetailsStepProps { sx?: SxProps; } +// ─── Internal: Colour Swatch ──────────────────────────────────────────────── + +const SWATCH_SIZE = 36; + +const ColourSwatch: React.FC<{ + colour: CoffinColour; + selected: boolean; + onClick: () => void; +}> = ({ colour, selected, onClick }) => ( + +); + // ─── Component ─────────────────────────────────────────────────────────────── /** * Step 11 — Coffin Details for the FA arrangement wizard. * - * Detail-toggles layout: coffin image + description on the left, - * name, specs, price, and CTA on the right. + * Detail-toggles layout matching VenueDetailStep pattern: + * scrollable left panel with image gallery and specs, + * sticky right panel with name, description, colour picker, + * price, allowance info, and CTA. * - * Customisation options (handles, lining, nameplate) have been - * deferred as a future enhancement (D-G). + * Reached by clicking a coffin card on CoffinsStep. + * Colour selection does not affect price. * * Pure presentation component — props in, callbacks out. * * Spec: documentation/steps/steps/11_coffin_details.yaml */ export const CoffinDetailsStep: React.FC = ({ - onContinue, + coffin, + onAddCoffin, onBack, onSaveAndExit, loading = false, - coffin, + selectedColourId, + onColourChange, + allowanceAmount, isPrePlanning = false, navigation, progressStepper, @@ -81,6 +147,13 @@ export const CoffinDetailsStep: React.FC = ({ hideHelpBar, sx, }) => { + const selectedColour = coffin.colours?.find((c) => c.id === selectedColourId); + + // Allowance impact + const hasAllowance = allowanceAmount != null; + const isFullyCovered = hasAllowance && allowanceAmount >= coffin.price; + const additionalCost = hasAllowance && !isFullyCovered ? coffin.price - allowanceAmount : 0; + return ( = ({ sx={sx} secondaryPanel={ - {/* Coffin name */} + {/* ─── Coffin name ─── */} {coffin.name} - {/* Price */} - - ${coffin.price.toLocaleString('en-AU')} - - - {coffin.priceNote && ( + {/* ─── Short description ─── */} + {coffin.description && ( - {coffin.priceNote} + {coffin.description} )} - {/* Add coffin CTA */} + {/* ─── Colour picker ─── */} + {coffin.colours && coffin.colours.length > 0 && onColourChange && ( + + + Colour + + + {coffin.colours.map((colour) => ( + onColourChange(colour.id)} + /> + ))} + + {selectedColour && ( + + {selectedColour.name} + + )} + + )} + + {/* ─── Price ─── */} + + ${coffin.price.toLocaleString('en-AU', { minimumFractionDigits: 2 })} + + + {/* ─── Allowance impact ─── */} + {isFullyCovered && ( + + Included in your package allowance — no change to your plan total. + + )} + + {hasAllowance && !isFullyCovered && ( + + + ${allowanceAmount.toLocaleString('en-AU')} package allowance applied + + + +${additionalCost.toLocaleString('en-AU', { minimumFractionDigits: 2 })} to your + plan total + + + )} + + {/* ─── Add Coffin CTA ─── */} { e.preventDefault(); - if (!loading) onContinue(); + if (!loading) onAddCoffin(); }} > - + {/* ─── Save and exit ─── */} {onSaveAndExit && ( - - )} - - {/* Specs */} - {coffin.specs && coffin.specs.length > 0 && ( - - {coffin.specs.map((spec) => ( - - - {spec.label} - - {spec.value} - - ))} - + <> + + + )} } > - {/* Left panel: image gallery + description */} + {/* ═══════ LEFT PANEL: scrollable content ═══════ */} + + {/* ─── Image gallery ─── */} 0 ? coffin.images : [{ src: coffin.imageUrl, alt: `Photo of ${coffin.name}` }] } - heroHeight={{ xs: 280, md: 400 }} + heroHeight={{ xs: 280, md: 420 }} sx={{ mb: 3 }} /> - {coffin.description && ( - - {coffin.description} - + {/* ─── Product details ─── */} + {coffin.specs && coffin.specs.length > 0 && ( + <> + + + {coffin.specs.map((spec) => ( + + + {spec.label} + + + {spec.value} + + + ))} + + )} ); diff --git a/src/components/pages/CoffinDetailsStep/index.ts b/src/components/pages/CoffinDetailsStep/index.ts index 8573423..a3f7c8d 100644 --- a/src/components/pages/CoffinDetailsStep/index.ts +++ b/src/components/pages/CoffinDetailsStep/index.ts @@ -1,2 +1,8 @@ export { CoffinDetailsStep, default } from './CoffinDetailsStep'; -export type { CoffinDetailsStepProps, CoffinProfile, CoffinSpec } from './CoffinDetailsStep'; +export type { + CoffinDetailsStepProps, + CoffinProfile, + CoffinSpec, + CoffinColour, + CoffinAllowanceAmount, +} from './CoffinDetailsStep';