From 1e7fbc0dc5a92ab363d98bf4955937988f4881aa Mon Sep 17 00:00:00 2001 From: Richie Date: Wed, 25 Mar 2026 20:52:15 +1100 Subject: [PATCH] =?UTF-8?q?Add=20AddOnOption=20molecule=20=E2=80=94=20togg?= =?UTF-8?q?leable=20add-on=20with=20Switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Composes Card (interactive, selected) + Typography + Switch - Maps to Figma ListItemAddItem (2350:40658) — toggle for optional extras - Heading + optional price + description + Switch right-aligned - Selected state via Card brand border + warm bg when checked - Clicking anywhere on card toggles the switch (stopPropagation on switch) - aria-labelledby connects switch to heading text - 7 stories: Default, Checked, ServiceAddOns (interactive list with total), WithoutPrice, WithoutDescription, Disabled, EdgeCases Co-Authored-By: Claude Opus 4.6 (1M context) --- .../AddOnOption/AddOnOption.stories.tsx | 216 ++++++++++++++++++ .../molecules/AddOnOption/AddOnOption.tsx | 142 ++++++++++++ src/components/molecules/AddOnOption/index.ts | 2 + 3 files changed, 360 insertions(+) create mode 100644 src/components/molecules/AddOnOption/AddOnOption.stories.tsx create mode 100644 src/components/molecules/AddOnOption/AddOnOption.tsx create mode 100644 src/components/molecules/AddOnOption/index.ts diff --git a/src/components/molecules/AddOnOption/AddOnOption.stories.tsx b/src/components/molecules/AddOnOption/AddOnOption.stories.tsx new file mode 100644 index 0000000..7e3f998 --- /dev/null +++ b/src/components/molecules/AddOnOption/AddOnOption.stories.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { AddOnOption } from './AddOnOption'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; + +const meta: Meta = { + title: 'Molecules/AddOnOption', + component: AddOnOption, + tags: ['autodocs'], + parameters: { + layout: 'centered', + design: { + type: 'figma', + url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=2350-40658', + }, + }, + argTypes: { + name: { control: 'text' }, + description: { control: 'text' }, + price: { control: 'number' }, + checked: { control: 'boolean' }, + disabled: { control: 'boolean' }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// ─── Default ──────────────────────────────────────────────────────────────── + +/** Default — unchecked add-on with description */ +export const Default: Story = { + args: { + name: 'Memorial video', + description: + 'Professional video tribute played during the service, compiled from family photos and footage.', + price: 350, + }, +}; + +// ─── Checked ──────────────────────────────────────────────────────────────── + +/** Checked — add-on is enabled, card shows selected state */ +export const Checked: Story = { + args: { + name: 'Memorial video', + description: + 'Professional video tribute played during the service, compiled from family photos and footage.', + price: 350, + checked: true, + }, +}; + +// ─── Service Add-Ons ──────────────────────────────────────────────────────── + +/** Realistic — arrangement flow add-ons list */ +export const ServiceAddOns: Story = { + render: function Render() { + const [addOns, setAddOns] = React.useState({ + catering: false, + video: true, + flowers: false, + transport: false, + webcast: false, + }); + + const toggle = (key: keyof typeof addOns) => (checked: boolean) => + setAddOns({ ...addOns, [key]: checked }); + + const total = [ + addOns.catering ? 1200 : 0, + addOns.video ? 350 : 0, + addOns.flowers ? 280 : 0, + addOns.transport ? 450 : 0, + addOns.webcast ? 150 : 0, + ].reduce((a, b) => a + b, 0); + + return ( + + Optional extras + + Customise the service with additional options. All prices are GST inclusive. + + + + + + + + + + + + + + + Extras total + + + ${total.toLocaleString('en-AU')} + + + + ); + }, +}; + +// ─── Without Price ────────────────────────────────────────────────────────── + +/** No price — some add-ons are complimentary */ +export const WithoutPrice: Story = { + args: { + name: 'Order of service booklet', + description: 'Complimentary printed booklet with the service programme and a photo of your loved one.', + }, +}; + +// ─── Without Description ──────────────────────────────────────────────────── + +/** No description — compact variant for simple toggles */ +export const WithoutDescription: Story = { + render: function Render() { + const [checked, setChecked] = React.useState(false); + return ( + + ); + }, +}; + +// ─── Disabled ─────────────────────────────────────────────────────────────── + +/** Disabled — add-on unavailable (e.g. venue restriction) */ +export const Disabled: Story = { + args: { + name: 'Catering', + description: 'Not available at this venue. Please contact the venue directly for catering options.', + price: 1200, + disabled: true, + }, +}; + +// ─── Edge Cases ───────────────────────────────────────────────────────────── + +/** Edge cases — long text, high prices, missing fields */ +export const EdgeCases: Story = { + render: function Render() { + const [checks, setChecks] = React.useState({ a: false, b: true, c: false }); + + return ( + + setChecks({ ...checks, a: v })} + /> + setChecks({ ...checks, b: v })} + /> + setChecks({ ...checks, c: v })} + /> + + ); + }, +}; diff --git a/src/components/molecules/AddOnOption/AddOnOption.tsx b/src/components/molecules/AddOnOption/AddOnOption.tsx new file mode 100644 index 0000000..995274c --- /dev/null +++ b/src/components/molecules/AddOnOption/AddOnOption.tsx @@ -0,0 +1,142 @@ +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'; +import { Switch } from '../../atoms/Switch'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Props for the FA AddOnOption molecule */ +export interface AddOnOptionProps { + /** Add-on name/heading */ + name: string; + /** Description text explaining the add-on */ + description?: string; + /** Price in dollars — shown after the heading */ + price?: number; + /** Whether this add-on is currently enabled */ + checked?: boolean; + /** Called when the toggle changes */ + onChange?: (checked: boolean) => void; + /** Whether this add-on is disabled/unavailable */ + disabled?: boolean; + /** MUI sx prop for style overrides */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Toggleable add-on option for the FA design system. + * + * Used in the arrangement flow for optional extras — catering, memorial + * video, flowers, transport upgrades, etc. Users toggle the switch to + * include or exclude; multiple add-ons can be active simultaneously. + * + * Composes Card + Typography + Switch. Maps to the Figma + * "ListItemAddItem" component (desktop + mobile viewports). + * + * For mutually exclusive options (single-select), use ServiceOption instead. + * + * Usage: + * ```tsx + * setAddOns({ ...addOns, memorialVideo: on })} + * /> + * ``` + */ +export const AddOnOption = React.forwardRef( + ({ name, description, price, checked = false, onChange, disabled = false, sx }, ref) => { + const switchId = React.useId(); + + const handleToggle = () => { + if (!disabled && onChange) { + onChange(!checked); + } + }; + + const handleSwitchChange = (_e: React.ChangeEvent, value: boolean) => { + if (onChange) { + onChange(value); + } + }; + + return ( + + {/* Heading row: name + optional price + switch */} + + + + {name} + + {price != null && ( + + ${price.toLocaleString('en-AU')} + + )} + + + e.stopPropagation()} + inputProps={{ 'aria-labelledby': `${switchId}-label` }} + sx={{ flexShrink: 0 }} + /> + + + {/* Description */} + {description && ( + + {description} + + )} + + ); + }, +); + +AddOnOption.displayName = 'AddOnOption'; +export default AddOnOption; diff --git a/src/components/molecules/AddOnOption/index.ts b/src/components/molecules/AddOnOption/index.ts new file mode 100644 index 0000000..1d33ce1 --- /dev/null +++ b/src/components/molecules/AddOnOption/index.ts @@ -0,0 +1,2 @@ +export { AddOnOption } from './AddOnOption'; +export type { AddOnOptionProps } from './AddOnOption';