From dfa599d5675ab1c98b4ed863d5f7c9b6f5ee1873 Mon Sep 17 00:00:00 2001 From: Richie Date: Wed, 25 Mar 2026 20:34:33 +1100 Subject: [PATCH] =?UTF-8?q?Add=20ServiceOption=20molecule=20=E2=80=94=20se?= =?UTF-8?q?lectable=20service=20item=20for=20arrangement=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Composes Card (interactive, selected) + Typography - Heading row with name + optional price (brand colour, right-aligned) - Optional description text below heading - Selected state via Card's built-in brand border + warm bg - Disabled state with opacity token + pointer-events: none - role="radio" + aria-checked for single-select group semantics - 7 stories: Default, Selected, ServiceTypeSelection (interactive), CoffinSelection (interactive), WithoutPrice, Disabled, EdgeCases - Maps to Figma ListItemPurchaseOption (2349:39505) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ServiceOption/ServiceOption.stories.tsx | 233 ++++++++++++++++++ .../molecules/ServiceOption/ServiceOption.tsx | 118 +++++++++ .../molecules/ServiceOption/index.ts | 2 + 3 files changed, 353 insertions(+) create mode 100644 src/components/molecules/ServiceOption/ServiceOption.stories.tsx create mode 100644 src/components/molecules/ServiceOption/ServiceOption.tsx create mode 100644 src/components/molecules/ServiceOption/index.ts diff --git a/src/components/molecules/ServiceOption/ServiceOption.stories.tsx b/src/components/molecules/ServiceOption/ServiceOption.stories.tsx new file mode 100644 index 0000000..23a3a52 --- /dev/null +++ b/src/components/molecules/ServiceOption/ServiceOption.stories.tsx @@ -0,0 +1,233 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { ServiceOption } from './ServiceOption'; +import Box from '@mui/material/Box'; + +const meta: Meta = { + title: 'Molecules/ServiceOption', + component: ServiceOption, + tags: ['autodocs'], + parameters: { + layout: 'centered', + design: { + type: 'figma', + url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=2349-39505', + }, + }, + argTypes: { + name: { control: 'text' }, + description: { control: 'text' }, + price: { control: 'number' }, + selected: { control: 'boolean' }, + disabled: { control: 'boolean' }, + onClick: { action: 'clicked' }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// ─── Default ──────────────────────────────────────────────────────────────── + +export const Default: Story = { + args: { + name: 'Traditional Burial', + description: + 'Full service with chapel ceremony and graveside committal. Includes hearse, pallbearers, and flowers.', + price: 4200, + }, +}; + +// ─── Selected ─────────────────────────────────────────────────────────────── + +export const Selected: Story = { + args: { + name: 'Traditional Burial', + description: + 'Full service with chapel ceremony and graveside committal. Includes hearse, pallbearers, and flowers.', + price: 4200, + selected: true, + }, +}; + +// ─── Service Type Selection ───────────────────────────────────────────────── + +/** + * Interactive single-select group — click to choose a service type. + * Only one option is active at a time (radio semantics). + */ +export const ServiceTypeSelection: Story = { + name: 'Service Type Selection', + render: () => { + const [selected, setSelected] = useState('burial'); + + return ( + + setSelected('burial')} + /> + setSelected('cremation')} + /> + setSelected('direct')} + /> + + ); + }, +}; + +// ─── Coffin Selection ─────────────────────────────────────────────────────── + +/** Coffin/casket options with varied pricing */ +export const CoffinSelection: Story = { + name: 'Coffin Selection', + render: () => { + const [selected, setSelected] = useState(''); + + return ( + + setSelected('chipboard')} + /> + setSelected('timber')} + /> + setSelected('premium')} + /> + setSelected('eco')} + /> + + ); + }, +}; + +// ─── Without Price ────────────────────────────────────────────────────────── + +/** Options without prices — used when pricing varies or is bundled */ +export const WithoutPrice: Story = { + name: 'Without Price', + render: () => ( + + {}} + /> + {}} + /> + {}} + /> + + ), +}; + +// ─── Disabled ─────────────────────────────────────────────────────────────── + +/** Disabled option — unavailable service */ +export const Disabled: Story = { + render: () => ( + + {}} + /> + + {}} + /> + + ), +}; + +// ─── Edge Cases ───────────────────────────────────────────────────────────── + +/** Edge cases: long text, no description, high price */ +export const EdgeCases: Story = { + name: 'Edge Cases', + render: () => ( + + {/* Long name + description */} + {}} + /> + {/* No description */} + {}} + /> + {/* No price, no description */} + {}} + /> + + ), +}; diff --git a/src/components/molecules/ServiceOption/ServiceOption.tsx b/src/components/molecules/ServiceOption/ServiceOption.tsx new file mode 100644 index 0000000..bf93bfe --- /dev/null +++ b/src/components/molecules/ServiceOption/ServiceOption.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { Card } from '../../atoms/Card'; +import { Typography } from '../../atoms/Typography'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Props for the FA ServiceOption molecule */ +export interface ServiceOptionProps { + /** Option name/heading */ + name: string; + /** Description text explaining the option */ + description?: string; + /** Price in dollars — shown to the right of the heading */ + price?: number; + /** Whether this option is currently selected */ + selected?: boolean; + /** Whether this option is disabled/unavailable */ + disabled?: boolean; + /** Click handler — toggles selection */ + onClick?: () => void; + /** MUI sx prop for style overrides */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Selectable service option for the FA design system. + * + * Used in the arrangement flow for choosing between mutually exclusive + * options — coffin type, service style, transport method, etc. + * Users click to select; only one option is active at a time within + * a group (managed by parent). + * + * Composes Card (interactive, selected) + Typography. Maps to the + * Figma "ListItemPurchaseOption" component (4 states × 2 viewports). + * + * For add-on toggles (multi-select with Switch), use a future + * AddOnOption molecule or compose Card + Switch directly. + * + * Usage: + * ```tsx + * setSelected('burial')} + * /> + * ``` + */ +export const ServiceOption = React.forwardRef( + ({ name, description, price, selected = false, disabled = false, onClick, sx }, ref) => { + return ( + + {/* Heading row: name + optional price */} + + + {name} + + {price != null && ( + + ${price.toLocaleString('en-AU')} + + )} + + + {/* Description */} + {description && ( + + {description} + + )} + + ); + }, +); + +ServiceOption.displayName = 'ServiceOption'; +export default ServiceOption; diff --git a/src/components/molecules/ServiceOption/index.ts b/src/components/molecules/ServiceOption/index.ts new file mode 100644 index 0000000..3225ff2 --- /dev/null +++ b/src/components/molecules/ServiceOption/index.ts @@ -0,0 +1,2 @@ +export { ServiceOption, default } from './ServiceOption'; +export type { ServiceOptionProps } from './ServiceOption';