- New molecule: ComparisonColumnCard — desktop column header card extracted from ComparisonTable (~150 lines removed from organism) - New molecule: ComparisonTabCard — mobile tab rail card extracted from ComparisonPage (shared by V1 and V2) - CellValue "unknown" restyled: icon+text in neutral grey (was Badge), InfoOutlinedIcon on right at 14px matching item info icons - Unverified provider story data: all items set to unknown across all story files (no dashes in essentials) - Mobile tab rail: recommended badge (replaces star), package price, shadow/glow, center-on-select scroll, overflow clipping fixed - ComparisonPackageCard: added shadow, reduced CTA button to medium - ComparisonTable first column: inline info icon pattern (non-breaking space + nowrap span) prevents icon orphaning on line wrap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
import React from 'react';
|
|
import Box from '@mui/material/Box';
|
|
import type { SxProps, Theme } from '@mui/material/styles';
|
|
import { Typography } from '../../atoms/Typography';
|
|
import { Badge } from '../../atoms/Badge';
|
|
import { Card } from '../../atoms/Card';
|
|
import type { ComparisonPackage } from '../../organisms/ComparisonTable';
|
|
|
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
|
|
export interface ComparisonTabCardProps {
|
|
/** Package data to render */
|
|
pkg: ComparisonPackage;
|
|
/** Whether this tab is the currently active/selected one */
|
|
isActive: boolean;
|
|
/** Whether any package in the rail is recommended — controls spacer for alignment */
|
|
hasRecommended: boolean;
|
|
/** ARIA: id for the tab element */
|
|
tabId: string;
|
|
/** ARIA: id of the controlled tabpanel */
|
|
tabPanelId: string;
|
|
/** Called when the tab card is clicked */
|
|
onClick: () => void;
|
|
/** MUI sx prop for outer wrapper */
|
|
sx?: SxProps<Theme>;
|
|
}
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function formatPrice(amount: number): string {
|
|
return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`;
|
|
}
|
|
|
|
// ─── Component ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Mini tab card for the mobile ComparisonPage tab rail.
|
|
*
|
|
* Shows provider name, package name, and price. Recommended packages get a
|
|
* floating badge (in normal flow with negative margin overlap) and a warm
|
|
* brand glow. Non-recommended cards get a spacer to keep vertical alignment
|
|
* when a recommended card is present in the rail.
|
|
*
|
|
* The page component owns scroll/centering behaviour — this is purely visual.
|
|
*/
|
|
export const ComparisonTabCard = React.forwardRef<HTMLDivElement, ComparisonTabCardProps>(
|
|
({ pkg, isActive, hasRecommended, tabId, tabPanelId, onClick, sx }, ref) => {
|
|
return (
|
|
<Box
|
|
ref={ref}
|
|
sx={[
|
|
{
|
|
flexShrink: 0,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
},
|
|
...(Array.isArray(sx) ? sx : [sx]),
|
|
]}
|
|
>
|
|
{/* Recommended badge in normal flow — overlaps card via negative mb */}
|
|
{pkg.isRecommended ? (
|
|
<Badge
|
|
color="brand"
|
|
variant="soft"
|
|
size="small"
|
|
sx={{
|
|
mb: '-10px',
|
|
zIndex: 1,
|
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
Recommended
|
|
</Badge>
|
|
) : (
|
|
// Spacer keeps cards aligned when a recommended card is present
|
|
hasRecommended && <Box sx={{ height: 12 }} />
|
|
)}
|
|
|
|
<Card
|
|
role="tab"
|
|
aria-selected={isActive}
|
|
aria-controls={tabPanelId}
|
|
id={tabId}
|
|
variant="outlined"
|
|
selected={isActive}
|
|
padding="none"
|
|
onClick={onClick}
|
|
interactive
|
|
sx={{
|
|
width: 210,
|
|
cursor: 'pointer',
|
|
boxShadow: 'var(--fa-shadow-sm)',
|
|
...(pkg.isRecommended && {
|
|
borderColor: 'var(--fa-color-brand-500)',
|
|
boxShadow: '0 0 12px rgba(186, 131, 78, 0.3)',
|
|
}),
|
|
...(isActive && {
|
|
boxShadow: pkg.isRecommended
|
|
? '0 0 14px rgba(186, 131, 78, 0.4)'
|
|
: 'var(--fa-shadow-md)',
|
|
}),
|
|
}}
|
|
>
|
|
<Box sx={{ px: 2, pt: 2.4, pb: 2 }}>
|
|
<Typography
|
|
variant="labelSm"
|
|
sx={{
|
|
fontWeight: 600,
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
display: 'block',
|
|
mb: 0.25,
|
|
}}
|
|
>
|
|
{pkg.provider.name}
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
color="text.secondary"
|
|
sx={{
|
|
display: 'block',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{pkg.name}
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
display: 'block',
|
|
fontWeight: 600,
|
|
color: 'primary.main',
|
|
mt: 0.5,
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{formatPrice(pkg.price)}
|
|
</Typography>
|
|
</Box>
|
|
</Card>
|
|
</Box>
|
|
);
|
|
},
|
|
);
|
|
|
|
ComparisonTabCard.displayName = 'ComparisonTabCard';
|
|
export default ComparisonTabCard;
|