diff --git a/src/components/molecules/CartButton/CartButton.stories.tsx b/src/components/molecules/CartButton/CartButton.stories.tsx new file mode 100644 index 0000000..16ce84d --- /dev/null +++ b/src/components/molecules/CartButton/CartButton.stories.tsx @@ -0,0 +1,97 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { CartButton } from './CartButton'; +import { StepIndicator } from '../StepIndicator'; +import type { CartItem } from './CartButton'; + +const sampleItems: CartItem[] = [ + { section: 'Funeral Provider', name: 'H. Parsons — Essential Package', price: 4950 }, + { section: 'Service Venue', name: 'West Chapel', price: 900 }, + { section: 'Service Venue', name: 'Photo presentation', price: 150 }, + { section: 'Crematorium', name: 'Warrill Park Crematorium', price: 850 }, + { section: 'Coffin', name: 'Richmond Rosewood', price: 1750 }, + { section: 'Optional Extras', name: 'Live musician — Vocalist', price: 450 }, + { section: 'Optional Extras', name: 'Catering', priceLabel: 'Price on application' }, +]; + +const meta: Meta = { + title: 'Molecules/CartButton', + component: CartButton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Default ──────────────────────────────────────────────────────────────── + +/** Full cart with multiple sections */ +export const Default: Story = { + args: { + total: 9050, + items: sampleItems, + }, +}; + +// ─── Empty ────────────────────────────────────────────────────────────────── + +/** Empty plan — no items selected yet */ +export const Empty: Story = { + args: { + total: 0, + items: [], + }, +}; + +// ─── Single item ──────────────────────────────────────────────────────────── + +/** Just the package selected */ +export const SingleItem: Story = { + args: { + total: 4950, + items: [{ section: 'Funeral Provider', name: 'H. Parsons — Essential Package', price: 4950 }], + }, +}; + +// ─── In progress bar context ──────────────────────────────────────────────── + +/** How it looks inside the wizard progress bar */ +export const InProgressBar: Story = { + render: () => ( + + + + + + + ), + parameters: { + layout: 'padded', + }, +}; diff --git a/src/components/molecules/CartButton/CartButton.tsx b/src/components/molecules/CartButton/CartButton.tsx new file mode 100644 index 0000000..24b450b --- /dev/null +++ b/src/components/molecules/CartButton/CartButton.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { DialogShell } from '../../atoms/DialogShell'; +import { Divider } from '../../atoms/Divider'; +import { LineItem } from '../LineItem'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** A single item in the plan cart */ +export interface CartItem { + /** Section heading (e.g. "Funeral Provider", "Venue") */ + section: string; + /** Item name */ + name: string; + /** Price in dollars — omit for included/complimentary items */ + price?: number; + /** Custom price label (e.g. "Price on application", "Included") */ + priceLabel?: string; +} + +/** Props for the CartButton molecule */ +export interface CartButtonProps { + /** Running total in dollars */ + total: number; + /** Cart items grouped by section */ + items?: CartItem[]; + /** Override the structured dialog body with custom content */ + children?: React.ReactNode; + /** MUI sx prop for the trigger button */ + sx?: SxProps; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** Group items by their section heading */ +const groupBySection = (items: CartItem[]) => { + const groups: { section: string; items: CartItem[] }[] = []; + for (const item of items) { + const last = groups[groups.length - 1]; + if (last && last.section === item.section) { + last.items.push(item); + } else { + groups.push({ section: item.section, items: [item] }); + } + } + return groups; +}; + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Cart button for the arrangement wizard progress bar. + * + * Shows the running plan total in a compact trigger button. Clicking opens + * a DialogShell with the plan contents — items grouped by section using + * LineItem molecules. + * + * Sits in the `runningTotal` slot of WizardLayout. + * + * Usage: + * ```tsx + * + * ``` + */ +export const CartButton = React.forwardRef( + ({ total, items = [], children, sx }, ref) => { + const [open, setOpen] = React.useState(false); + const formattedTotal = `$${total.toLocaleString('en-AU')}`; + const groups = groupBySection(items); + + return ( + <> + {/* Trigger */} + + + {/* Dialog */} + setOpen(false)} + title="Your plan so far" + maxWidth="xs" + footer={ + + + + } + > + {children || ( + + {items.length === 0 ? ( + + Your plan is empty. Selections will appear here as you build your arrangement. + + ) : ( + <> + {groups.map((group, gi) => ( + + + {group.section} + + {group.items.map((item, ii) => ( + + ))} + + ))} + + + + + )} + + )} + + + ); + }, +); + +CartButton.displayName = 'CartButton'; +export default CartButton; diff --git a/src/components/molecules/CartButton/index.ts b/src/components/molecules/CartButton/index.ts new file mode 100644 index 0000000..d918967 --- /dev/null +++ b/src/components/molecules/CartButton/index.ts @@ -0,0 +1,2 @@ +export { default } from './CartButton'; +export * from './CartButton'; diff --git a/src/components/pages/DateTimeStep/DateTimeStep.tsx b/src/components/pages/DateTimeStep/DateTimeStep.tsx index 7369adc..87f1c3d 100644 --- a/src/components/pages/DateTimeStep/DateTimeStep.tsx +++ b/src/components/pages/DateTimeStep/DateTimeStep.tsx @@ -70,6 +70,10 @@ export interface DateTimeStepProps { showScheduling?: boolean; /** Navigation bar — passed through to WizardLayout */ navigation?: React.ReactNode; + /** Progress stepper */ + progressStepper?: React.ReactNode; + /** Running total widget (e.g. CartButton) */ + runningTotal?: React.ReactNode; /** Hide the help bar */ hideHelpBar?: boolean; /** MUI sx prop for the root */ @@ -115,6 +119,8 @@ export const DateTimeStep: React.FC = ({ showNameFields = true, showScheduling = true, navigation, + progressStepper, + runningTotal, hideHelpBar, sx, }) => { @@ -154,6 +160,8 @@ export const DateTimeStep: React.FC = ({ = ({ cardFormSlot, navigation, progressStepper, + runningTotal, hideHelpBar, sx, }) => { @@ -118,6 +121,7 @@ export const PaymentStep: React.FC = ({ variant="centered-form" navigation={navigation} progressStepper={progressStepper} + runningTotal={runningTotal} showBackLink={!!onBack} backLabel="Back" onBack={onBack} diff --git a/src/components/pages/SummaryStep/SummaryStep.tsx b/src/components/pages/SummaryStep/SummaryStep.tsx index 9c14360..9264f5b 100644 --- a/src/components/pages/SummaryStep/SummaryStep.tsx +++ b/src/components/pages/SummaryStep/SummaryStep.tsx @@ -101,6 +101,8 @@ export interface SummaryStepProps { navigation?: React.ReactNode; /** Progress stepper */ progressStepper?: React.ReactNode; + /** Running total widget (e.g. CartButton) */ + runningTotal?: React.ReactNode; /** Hide the help bar */ hideHelpBar?: boolean; /** MUI sx prop */ @@ -287,6 +289,7 @@ export const SummaryStep: React.FC = ({ isPrePlanning = false, navigation, progressStepper, + runningTotal, hideHelpBar, sx, }) => { @@ -297,6 +300,7 @@ export const SummaryStep: React.FC = ({ variant="centered-form" navigation={navigation} progressStepper={progressStepper} + runningTotal={runningTotal} showBackLink={!!onBack} backLabel="Back" onBack={onBack} diff --git a/src/components/pages/VenueStep/VenueStep.tsx b/src/components/pages/VenueStep/VenueStep.tsx index c53cadb..ff863eb 100644 --- a/src/components/pages/VenueStep/VenueStep.tsx +++ b/src/components/pages/VenueStep/VenueStep.tsx @@ -52,6 +52,10 @@ export interface VenueStepProps { mapPanel?: React.ReactNode; /** Navigation bar — passed through to WizardLayout */ navigation?: React.ReactNode; + /** Progress stepper */ + progressStepper?: React.ReactNode; + /** Running total widget (e.g. CartButton) */ + runningTotal?: React.ReactNode; /** MUI sx prop for the root */ sx?: SxProps; } @@ -90,6 +94,8 @@ export const VenueStep: React.FC = ({ isPrePlanning = false, mapPanel, navigation, + progressStepper, + runningTotal, sx, }) => { const subheading = isPrePlanning @@ -100,6 +106,8 @@ export const VenueStep: React.FC = ({ ( ( @@ -418,7 +417,6 @@ export const WizardLayout = React.forwardRef( ref, ) => { const LayoutComponent = LAYOUT_MAP[variant]; - const showStepper = STEPPER_VARIANTS.includes(variant); return ( ( {navigation} {/* Stepper + running total bar (grid-sidebar, detail-toggles only) */} - {showStepper && } + {/* Back link — inside left panel for list-map/detail-toggles, above content for others */} {showBackLink && variant !== 'list-map' && variant !== 'detail-toggles' && (