Files
Parsons/src/components/molecules/ComparisonTabCard/ComparisonTabCard.tsx
Richie e89ac360e8 Extract ComparisonColumnCard + ComparisonTabCard molecules, refine comparison UI
- 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>
2026-04-12 21:10:17 +10:00

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;