Files
Parsons/src/components/molecules/CompareBar/CompareBar.tsx
Richie 52fd0f199a Add package comparison feature: CompareBar, ComparisonTable, ComparisonPage
New components for side-by-side funeral package comparison:

- CompareBar molecule: floating bottom pill with fraction badge (1/3, 2/3, 3/3),
  contextual copy, Compare CTA. For ProvidersStep and PackagesStep.
- ComparisonTable organism: CSS Grid comparison with info card, floating verified
  badges, separate section tables (Essentials/Optionals/Extras) with left accent
  borders, row hover, horizontal scroll on narrow desktops, font hierarchy.
- ComparisonPage: WizardLayout wide-form with Share/Print actions. Desktop shows
  ComparisonTable, mobile shows mini-card tab rail + single package card view.
  Recommended package as separate prop (D038).

Also fixes PackageDetail: adds priceLabel pass-through (D039), updates stories
to Essentials/Optionals/Extras section naming (D035).

Decisions: D035-D039 logged. Audits: CompareBar 18/20, ComparisonTable 17/20.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:17:34 +10:00

115 lines
3.7 KiB
TypeScript

import React from 'react';
import Paper from '@mui/material/Paper';
import Slide from '@mui/material/Slide';
import CompareArrowsIcon from '@mui/icons-material/CompareArrows';
import type { SxProps, Theme } from '@mui/material/styles';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
import { Badge } from '../../atoms/Badge';
// ─── Types ───────────────────────────────────────────────────────────────────
/** A package in the comparison basket */
export interface CompareBarPackage {
/** Unique package ID */
id: string;
/** Package display name */
name: string;
/** Provider name */
providerName: string;
}
/** Props for the CompareBar molecule */
export interface CompareBarProps {
/** Packages currently in the comparison basket (max 3 user-selected) */
packages: CompareBarPackage[];
/** Called when user clicks "Compare" CTA */
onCompare: () => void;
/** Error/status message shown inline (e.g. "Maximum 3 packages") */
error?: string;
/** MUI sx prop for the root wrapper */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Floating comparison basket pill for the FA design system.
*
* Shows a fraction badge (1/3, 2/3, 3/3), contextual copy, and a Compare CTA.
* Present on both ProvidersStep and PackagesStep.
*
* Composes Badge + Button + Typography.
*/
export const CompareBar = React.forwardRef<HTMLDivElement, CompareBarProps>(
({ packages, onCompare, error, sx }, ref) => {
const count = packages.length;
const visible = count > 0;
const canCompare = count >= 2;
const statusText = count === 1 ? 'Add another to compare' : 'Ready to compare';
return (
<Slide direction="up" in={visible} mountOnEnter unmountOnExit>
<Paper
ref={ref}
elevation={8}
role="status"
aria-live="polite"
aria-label={`${count} of 3 packages selected for comparison`}
sx={[
(theme: Theme) => ({
position: 'fixed',
bottom: theme.spacing(3),
left: '50%',
transform: 'translateX(-50%)',
zIndex: theme.zIndex.snackbar,
borderRadius: '9999px',
display: 'flex',
alignItems: 'center',
gap: 1.5,
px: 2.5,
py: 1.25,
maxWidth: { xs: 'calc(100vw - 32px)', md: 420 },
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{/* Fraction badge — 1/3, 2/3, 3/3 */}
<Badge color="brand" variant="soft" size="small" sx={{ flexShrink: 0 }}>
{count}/3
</Badge>
{/* Status text */}
<Typography
variant="body2"
role={error ? 'alert' : undefined}
sx={{
fontWeight: 500,
whiteSpace: 'nowrap',
color: error ? 'var(--fa-color-text-brand)' : 'text.primary',
}}
>
{error || statusText}
</Typography>
{/* Compare CTA */}
<Button
variant="contained"
size="small"
startIcon={<CompareArrowsIcon />}
onClick={onCompare}
disabled={!canCompare}
sx={{ flexShrink: 0, borderRadius: '9999px' }}
>
Compare
</Button>
</Paper>
</Slide>
);
},
);
CompareBar.displayName = 'CompareBar';
export default CompareBar;