Add LineItem, ProviderCardCompact, PackageDetail for Package Select page

LineItem (molecule):
- Name + optional info tooltip + optional price
- Allowance asterisk, total variant (bold + top border)
- Reusable for package contents, order summaries, invoices

ProviderCardCompact (molecule):
- Horizontal layout: image left, name + location + rating right
- Used at top of Package Select page to show selected provider

PackageDetail (organism):
- Right-side detail panel for Package Select page
- Name/price header, Make Arrangement + Compare CTAs
- Grouped LineItem sections, total row, T&C footer
- PackageSelectPage story: full page with filter chips, package
  list (ServiceOption), sticky detail panel, and Navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 22:51:40 +11:00
parent 6f59468057
commit 377ff41aac
10 changed files with 925 additions and 0 deletions

View File

@@ -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<Theme>;
}
// ─── 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
* <PackageDetail
* name="Everyday Funeral Package"
* price={2700}
* sections={[
* { heading: 'Essentials', items: [...] },
* { heading: 'Complimentary Items', items: [...] },
* ]}
* total={2700}
* terms="* This package includes a funeral service at a chapel..."
* onArrange={() => startArrangement(pkg.id)}
* onCompare={() => addToCompare(pkg.id)}
* />
* ```
*/
export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps>(
(
{
name,
price,
sections,
total,
terms,
onArrange,
onCompare,
arrangeDisabled = false,
sx,
},
ref,
) => {
return (
<Box
ref={ref}
sx={[
{
border: '1px solid',
borderColor: 'divider',
borderRadius: 'var(--fa-card-border-radius-default)',
overflow: 'hidden',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{/* Main content area */}
<Box sx={{ px: { xs: 2, sm: 3 }, py: 2 }}>
{/* Header: name + price */}
<Box sx={{ mb: 2 }}>
<Typography variant="h4" component="h2">
{name}
</Typography>
<Typography variant="h6" color="text.secondary" sx={{ mt: 0.5 }}>
${price.toLocaleString('en-AU')}
</Typography>
</Box>
{/* CTA buttons */}
<Box sx={{ display: 'flex', gap: 1.5, mb: 3 }}>
<Button
variant="contained"
size="large"
fullWidth
disabled={arrangeDisabled}
onClick={onArrange}
>
Make Arrangement
</Button>
{onCompare && (
<Button
variant="soft"
color="secondary"
size="large"
onClick={onCompare}
sx={{ flexShrink: 0 }}
>
Compare
</Button>
)}
</Box>
<Divider sx={{ mb: 2 }} />
{/* Sections */}
{sections.map((section, sectionIdx) => (
<Box key={section.heading} sx={{ mb: sectionIdx < sections.length - 1 ? 2 : 0 }}>
<Typography variant="label" sx={{ display: 'block', mb: 1.5 }}>
{section.heading}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{section.items.map((item) => (
<LineItem
key={item.name}
name={item.name}
info={item.info}
price={item.price}
isAllowance={item.isAllowance}
/>
))}
</Box>
</Box>
))}
{/* Total */}
{total != null && (
<LineItem name="Total" price={total} variant="total" />
)}
</Box>
{/* Terms & Conditions footer */}
{terms && (
<Box
sx={{
bgcolor: 'var(--fa-color-surface-subtle)',
px: { xs: 2, sm: 3 },
py: 2,
}}
>
<Typography variant="captionSm" color="text.secondary" sx={{ lineHeight: 1.5 }}>
{terms}
</Typography>
</Box>
)}
</Box>
);
},
);
PackageDetail.displayName = 'PackageDetail';
export default PackageDetail;