diff --git a/docs/memory/component-registry.md b/docs/memory/component-registry.md index 17f4247..50084ab 100644 --- a/docs/memory/component-registry.md +++ b/docs/memory/component-registry.md @@ -44,6 +44,8 @@ duplicates) and MUST update it after completing one. | 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 | review | Typography + Tooltip + InfoOutlinedIcon | Name + optional info tooltip + optional price. Supports allowance asterisk, total variant (bold + border). Used in PackageDetail, order summaries, invoices. | +| ProviderCardCompact | review | Card (outlined) + Typography | Horizontal compact provider card — image left, name + location + rating right. Used at top of Package Select page. Separate from vertical ProviderCard. | ## Organisms @@ -51,6 +53,7 @@ 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. | | PricingTable | planned | PriceCard × n + Typography | Comparative pricing display | +| PackageDetail | review | LineItem × n + Typography + Button + Divider | Right-side package detail panel. Name/price header, Make Arrangement + Compare buttons, grouped LineItem sections (Essentials, Complimentary, Extras), total row, T&C footer. Maps to Figma Package Select (5405:181955) right column. | | 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 | Dark espresso (brand.950) site footer. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). | diff --git a/src/components/molecules/LineItem/LineItem.stories.tsx b/src/components/molecules/LineItem/LineItem.stories.tsx new file mode 100644 index 0000000..ed87387 --- /dev/null +++ b/src/components/molecules/LineItem/LineItem.stories.tsx @@ -0,0 +1,129 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { LineItem } from './LineItem'; +import { Typography } from '../../atoms/Typography'; +import { Divider } from '../../atoms/Divider'; + +const meta: Meta = { + title: 'Molecules/LineItem', + component: LineItem, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// --- Default ----------------------------------------------------------------- + +/** Basic line item with name and price */ +export const Default: Story = { + args: { + name: 'Professional Service Fee', + price: 1500, + info: 'Covers coordination of the entire funeral service, liaising with clergy, cemetery, and crematorium.', + }, +}; + +// --- Allowance --------------------------------------------------------------- + +/** Price marked with asterisk — indicates an allowance that can be customised */ +export const Allowance: Story = { + args: { + name: 'Allowance for Coffin', + price: 1500, + isAllowance: true, + info: 'This is an allowance amount. You may upgrade or change the coffin selection during your arrangement.', + }, +}; + +// --- No Price ---------------------------------------------------------------- + +/** Complimentary/included item — no price shown */ +export const Complimentary: Story = { + args: { + name: 'Dressing Fee', + info: 'Included at no additional charge with this package.', + }, +}; + +// --- Custom Price Label ------------------------------------------------------ + +/** Custom text instead of dollar amount */ +export const CustomPriceLabel: Story = { + args: { + name: 'Transfer of Deceased', + priceLabel: 'Included', + info: 'Transfer within 50km of the funeral home.', + }, +}; + +// --- Total Row --------------------------------------------------------------- + +/** Summary total — bold with top border */ +export const Total: Story = { + args: { + name: 'Total', + price: 2700, + variant: 'total', + }, +}; + +// --- Package Contents -------------------------------------------------------- + +/** Realistic package breakdown as seen on the Package Select page */ +export const PackageContents: Story = { + render: () => ( + + + Essentials + + + + + + + + + + + + + + + + + Complimentary Items + + + + + + + + + + + + Extras + + + + + + + + + + + ), +}; diff --git a/src/components/molecules/LineItem/LineItem.tsx b/src/components/molecules/LineItem/LineItem.tsx new file mode 100644 index 0000000..4beeefe --- /dev/null +++ b/src/components/molecules/LineItem/LineItem.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { Typography } from '../../atoms/Typography'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Props for the FA LineItem molecule */ +export interface LineItemProps { + /** Item name/label */ + name: string; + /** Optional tooltip text explaining the item (shown via info icon) */ + info?: string; + /** Price in dollars — omit for complimentary/included items */ + price?: number; + /** Whether the price is an allowance (shows asterisk) */ + isAllowance?: boolean; + /** Custom price display — overrides `price` formatting (e.g. "Included", "TBC") */ + priceLabel?: string; + /** Visual weight — "default" for regular items, "total" for summary rows */ + variant?: 'default' | 'total'; + /** MUI sx prop for the root element */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * A single line item showing a name, optional info tooltip, and optional price. + * + * Used in package contents, order summaries, and invoices. The `info` prop + * renders a small info icon with a tooltip — used by providers to explain + * what each inclusion covers. + * + * Composes Typography + Tooltip. + * + * Usage: + * ```tsx + * + * + * + * + * ``` + */ +export const LineItem = React.forwardRef( + ({ name, info, price, isAllowance = false, priceLabel, variant = 'default', sx }, ref) => { + const isTotal = variant === 'total'; + + const formattedPrice = priceLabel + ?? (price != null ? `$${price.toLocaleString('en-AU')}${isAllowance ? '*' : ''}` : undefined); + + return ( + + {/* Name + optional info icon */} + + + {name} + + + {info && ( + + + + )} + + + {/* Price */} + {formattedPrice && ( + + {formattedPrice} + + )} + + ); + }, +); + +LineItem.displayName = 'LineItem'; +export default LineItem; diff --git a/src/components/molecules/LineItem/index.ts b/src/components/molecules/LineItem/index.ts new file mode 100644 index 0000000..9ae1b9e --- /dev/null +++ b/src/components/molecules/LineItem/index.ts @@ -0,0 +1 @@ +export { LineItem, type LineItemProps } from './LineItem'; diff --git a/src/components/molecules/ProviderCardCompact/ProviderCardCompact.stories.tsx b/src/components/molecules/ProviderCardCompact/ProviderCardCompact.stories.tsx new file mode 100644 index 0000000..bdfa271 --- /dev/null +++ b/src/components/molecules/ProviderCardCompact/ProviderCardCompact.stories.tsx @@ -0,0 +1,113 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { ProviderCardCompact } from './ProviderCardCompact'; + +const DEMO_IMAGE = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop'; + +const meta: Meta = { + title: 'Molecules/ProviderCardCompact', + component: ProviderCardCompact, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// --- Default ----------------------------------------------------------------- + +/** Compact provider card with image, name, location, and rating */ +export const Default: Story = { + args: { + name: 'H.Parsons', + location: 'Wentworth', + imageUrl: DEMO_IMAGE, + rating: 4.5, + reviewCount: 11, + }, +}; + +// --- Without Image ----------------------------------------------------------- + +/** Provider without a photo — text only */ +export const WithoutImage: Story = { + args: { + name: 'Smith & Sons Funerals', + location: 'Parramatta', + rating: 4.2, + reviewCount: 38, + }, +}; + +// --- Without Rating ---------------------------------------------------------- + +/** Provider without reviews */ +export const WithoutRating: Story = { + args: { + name: 'Peaceful Rest Funerals', + location: 'Mildura', + imageUrl: DEMO_IMAGE, + }, +}; + +// --- Interactive ------------------------------------------------------------- + +/** Clickable — navigates back to provider details */ +export const Interactive: Story = { + args: { + name: 'H.Parsons', + location: 'Wentworth', + imageUrl: DEMO_IMAGE, + rating: 4.5, + reviewCount: 11, + onClick: () => alert('Navigate to provider details'), + }, +}; + +// --- In Context -------------------------------------------------------------- + +/** As it appears at the top of the Package Select page */ +export const InContext: Story = { + render: () => ( + + {}} + sx={{ + background: 'none', + border: 'none', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: 0.5, + mb: 3, + p: 0, + color: 'text.secondary', + fontFamily: 'inherit', + fontSize: '1rem', + '&:hover': { color: 'text.primary' }, + }} + > + ← Back + + Select a package + alert('View provider')} + /> + + ), +}; diff --git a/src/components/molecules/ProviderCardCompact/ProviderCardCompact.tsx b/src/components/molecules/ProviderCardCompact/ProviderCardCompact.tsx new file mode 100644 index 0000000..8806380 --- /dev/null +++ b/src/components/molecules/ProviderCardCompact/ProviderCardCompact.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import type { SxProps, Theme } from '@mui/material/styles'; +import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; +import StarRoundedIcon from '@mui/icons-material/StarRounded'; +import { Card } from '../../atoms/Card'; +import { Typography } from '../../atoms/Typography'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Props for the FA ProviderCardCompact molecule */ +export interface ProviderCardCompactProps { + /** Provider display name */ + name: string; + /** Location text (suburb, city) */ + location: string; + /** Hero image URL — shown on the left */ + imageUrl?: string; + /** Average rating (e.g. 4.5). Omit to hide. */ + rating?: number; + /** Number of reviews. Omit to hide review count. */ + reviewCount?: number; + /** Click handler — makes the card interactive */ + onClick?: () => void; + /** MUI sx prop for the root element */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Compact horizontal provider card for the FA design system. + * + * Used at the top of the Package Select page to show which provider + * the user has selected. Horizontal layout with image on the left, + * name + meta on the right. + * + * For the full vertical listing card, use ProviderCard instead. + * + * Composes Card + Typography. + * + * Usage: + * ```tsx + * + * ``` + */ +export const ProviderCardCompact = React.forwardRef( + ({ name, location, imageUrl, rating, reviewCount, onClick, sx }, ref) => { + return ( + + {/* Image */} + {imageUrl && ( + + )} + + {/* Content */} + + + {name} + + + {/* Location */} + + + + {location} + + + + {/* Rating */} + {rating != null && ( + + + + {rating} Rating{reviewCount != null ? ` (${reviewCount} ${reviewCount === 1 ? 'Review' : 'Reviews'})` : ''} + + + )} + + + ); + }, +); + +ProviderCardCompact.displayName = 'ProviderCardCompact'; +export default ProviderCardCompact; diff --git a/src/components/molecules/ProviderCardCompact/index.ts b/src/components/molecules/ProviderCardCompact/index.ts new file mode 100644 index 0000000..0f93e23 --- /dev/null +++ b/src/components/molecules/ProviderCardCompact/index.ts @@ -0,0 +1 @@ +export { ProviderCardCompact, type ProviderCardCompactProps } from './ProviderCardCompact'; diff --git a/src/components/organisms/PackageDetail/PackageDetail.stories.tsx b/src/components/organisms/PackageDetail/PackageDetail.stories.tsx new file mode 100644 index 0000000..757dd28 --- /dev/null +++ b/src/components/organisms/PackageDetail/PackageDetail.stories.tsx @@ -0,0 +1,228 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { PackageDetail } from './PackageDetail'; +import { ServiceOption } from '../../molecules/ServiceOption'; +import { ProviderCardCompact } from '../../molecules/ProviderCardCompact'; +import { Chip } from '../../atoms/Chip'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Navigation } from '../Navigation'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +const DEMO_IMAGE = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop'; + +const essentials = [ + { name: 'Accommodation', price: 1500, info: 'Refrigerated holding of the deceased prior to the funeral service.' }, + { name: 'Death Registration Certificate', price: 1500, info: 'Lodgement of death registration with NSW Registry of Births, Deaths & Marriages.' }, + { name: 'Doctor Fee for Cremation', price: 1500, info: 'Statutory medical referee fee required for all cremations in NSW.' }, + { name: 'NSW Government Levy — Cremation', price: 1500, info: 'NSW Government cremation levy as set by the Department of Health.' }, + { name: 'Professional Mortuary Care', price: 1500, info: 'Preparation and care of the deceased.' }, + { name: 'Professional Service Fee', price: 1500, info: 'Coordination of all funeral arrangements and services.' }, + { name: 'Allowance for Coffin', price: 1500, isAllowance: true, info: 'Allowance amount — upgrade options available during arrangement.' }, + { name: 'Allowance for Crematorium', price: 1500, isAllowance: true, info: 'Allowance for crematorium fees — varies by location.' }, + { name: 'Allowance for Hearse', price: 1500, isAllowance: true, info: 'Allowance for hearse transfer — distance surcharges may apply.' }, +]; + +const complimentary = [ + { name: 'Dressing Fee', info: 'Dressing and preparation of the deceased — included at no charge.' }, + { name: 'Viewing Fee', info: 'One private family viewing — included at no charge.' }, +]; + +const extras = [ + { name: 'Allowance for Flowers', price: 1500, isAllowance: true, info: 'Seasonal floral arrangements for the service.' }, + { name: 'Allowance for Master of Ceremonies', price: 1500, isAllowance: true, info: 'Professional celebrant or MC for the funeral service.' }, + { name: 'After Business Hours Service Surcharge', price: 1500, info: 'Additional fee for services held outside standard business hours.' }, + { name: 'After Hours Prayers', price: 1500, info: 'Evening prayer service at the funeral home.' }, + { name: 'Coffin Bearing by Funeral Directors', price: 1500, info: 'Professional pallbearing by funeral directors.' }, + { name: 'Digital Recording', price: 1500, info: 'Professional video recording of the funeral service.' }, +]; + +const termsText = '* This package includes a funeral service at a chapel or a church with a funeral procession following to the crematorium. It includes many of the most commonly selected funeral options preselected for you. Many people choose this package for the extended funeral rituals — of course, you can tailor the funeral service to meet your needs and budget as you go through the selections.'; + +const packages = [ + { id: 'everyday', name: 'Everyday Funeral Package', price: 900, description: 'Our most popular package with all essential services included. Suitable for a traditional chapel or church service.' }, + { id: 'deluxe', name: 'Deluxe Funeral Package', price: 1200, description: 'An enhanced package with premium coffin and additional floral arrangements.' }, + { id: 'essential', name: 'Essential Funeral Package', price: 600, description: 'A simple, dignified service covering all necessary arrangements.' }, + { id: 'catholic', name: 'Catholic Service', price: 950, description: 'A service tailored for Catholic traditions including prayers and church ceremony.' }, +]; + +const funeralTypes = ['All', 'Cremation', 'Burial', 'Memorial', 'Catholic', 'Direct Cremation']; + +const FALogoNav = () => ( + +); + +const meta: Meta = { + title: 'Organisms/PackageDetail', + component: PackageDetail, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// --- Default ----------------------------------------------------------------- + +/** Full package detail panel with all sections */ +export const Default: Story = { + args: { + name: 'Everyday Funeral Package', + price: 900, + sections: [ + { heading: 'Essentials', items: essentials }, + { heading: 'Complimentary Items', items: complimentary }, + { heading: 'Extras', items: extras }, + ], + total: 2700, + terms: termsText, + }, +}; + +// --- Without Extras ---------------------------------------------------------- + +/** Simpler package with essentials only */ +export const WithoutExtras: Story = { + args: { + name: 'Essential Funeral Package', + price: 600, + sections: [ + { heading: 'Essentials', items: essentials.slice(0, 6) }, + { heading: 'Complimentary Items', items: complimentary }, + ], + total: 9000, + terms: termsText, + }, +}; + +// --- Package Select Page Layout ---------------------------------------------- + +/** Full page layout — left: package list, right: detail panel */ +export const PackageSelectPage: Story = { + decorators: [ + (Story) => ( + + + + ), + ], + render: () => { + const [selectedPkg, setSelectedPkg] = useState('everyday'); + const [activeFilter, setActiveFilter] = useState('Cremation'); + + return ( + + } + items={[ + { label: 'Provider Portal', href: '/provider-portal' }, + { label: 'FAQ', href: '/faq' }, + { label: 'Contact Us', href: '/contact' }, + { label: 'Log in', href: '/login' }, + ]} + /> + + + {/* Left column: back, heading, provider, filter, packages */} + + + + + Select a package + + + + + {/* Funeral type filter */} + + {funeralTypes.map((type) => ( + setActiveFilter(type)} + size="small" + /> + ))} + + + + Packages + + + + {packages.map((pkg) => ( + setSelectedPkg(pkg.id)} + maxDescriptionLines={2} + /> + ))} + + + + {/* Right column: package detail */} + + p.id === selectedPkg)?.name ?? ''} + price={packages.find((p) => p.id === selectedPkg)?.price ?? 0} + sections={[ + { heading: 'Essentials', items: essentials }, + { heading: 'Complimentary Items', items: complimentary }, + { heading: 'Extras', items: extras }, + ]} + total={2700} + terms={termsText} + onArrange={() => alert(`Making arrangement for: ${selectedPkg}`)} + onCompare={() => alert(`Added ${selectedPkg} to compare`)} + /> + + + + ); + }, +}; diff --git a/src/components/organisms/PackageDetail/PackageDetail.tsx b/src/components/organisms/PackageDetail/PackageDetail.tsx new file mode 100644 index 0000000..126199f --- /dev/null +++ b/src/components/organisms/PackageDetail/PackageDetail.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; +import { LineItem } from '../../molecules/LineItem'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** A single item within a package section */ +export interface PackageLineItem { + /** Item name */ + name: string; + /** Tooltip description — clients have laboured over these, display them all */ + info?: string; + /** Price in dollars — omit for complimentary items */ + price?: number; + /** Whether this is an allowance (shows asterisk) */ + isAllowance?: boolean; +} + +/** A section of items within a package (e.g. "Essentials", "Extras") */ +export interface PackageSection { + /** Section heading */ + heading: string; + /** Items in this section */ + items: PackageLineItem[]; +} + +/** Props for the FA PackageDetail organism */ +export interface PackageDetailProps { + /** Package name */ + name: string; + /** Package price in dollars */ + price: number; + /** Grouped sections of package contents */ + sections: PackageSection[]; + /** Package total — usually the sum of priced essentials */ + total?: number; + /** Terms and conditions text — required by providers */ + terms?: string; + /** Called when user clicks "Make Arrangement" */ + onArrange?: () => void; + /** Called when user clicks "Compare" */ + onCompare?: () => void; + /** Whether the arrange button is disabled */ + arrangeDisabled?: boolean; + /** MUI sx prop for the root element */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Package detail panel for the FA design system. + * + * Displays the full contents of a funeral package — name, price, CTA buttons, + * grouped line items (Essentials, Complimentary, Extras), total, and T&Cs. + * + * Used as the right-side panel on the Package Select page. The contents and + * T&Cs are provider-authored and must be displayed in full. + * + * "Make Arrangement" is the FA term for selecting/committing to a package. + * + * Composes Typography + Button + Divider + LineItem. + * + * Usage: + * ```tsx + * startArrangement(pkg.id)} + * onCompare={() => addToCompare(pkg.id)} + * /> + * ``` + */ +export const PackageDetail = React.forwardRef( + ( + { + name, + price, + sections, + total, + terms, + onArrange, + onCompare, + arrangeDisabled = false, + sx, + }, + ref, + ) => { + return ( + + {/* Main content area */} + + {/* Header: name + price */} + + + {name} + + + ${price.toLocaleString('en-AU')} + + + + {/* CTA buttons */} + + + {onCompare && ( + + )} + + + + + {/* Sections */} + {sections.map((section, sectionIdx) => ( + + + {section.heading} + + + {section.items.map((item) => ( + + ))} + + + ))} + + {/* Total */} + {total != null && ( + + )} + + + {/* Terms & Conditions footer */} + {terms && ( + + + {terms} + + + )} + + ); + }, +); + +PackageDetail.displayName = 'PackageDetail'; +export default PackageDetail; diff --git a/src/components/organisms/PackageDetail/index.ts b/src/components/organisms/PackageDetail/index.ts new file mode 100644 index 0000000..967163e --- /dev/null +++ b/src/components/organisms/PackageDetail/index.ts @@ -0,0 +1 @@ +export { PackageDetail, type PackageDetailProps, type PackageSection, type PackageLineItem } from './PackageDetail';