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>
This commit is contained in:
@@ -1,17 +1,16 @@
|
||||
import React, { useId, useState } from 'react';
|
||||
import React, { useId, useState, useRef, useCallback } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
|
||||
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { WizardLayout } from '../../templates/WizardLayout';
|
||||
import { ComparisonTable, type ComparisonPackage } from '../../organisms/ComparisonTable';
|
||||
import { ComparisonPackageCard } from '../../molecules/ComparisonPackageCard';
|
||||
import { ComparisonTabCard } from '../../molecules/ComparisonTabCard';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -60,6 +59,8 @@ export const ComparisonPageV1 = React.forwardRef<HTMLDivElement, ComparisonPageV
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const tablistId = useId();
|
||||
const railRef = useRef<HTMLDivElement>(null);
|
||||
const tabRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const allPackages = React.useMemo(() => {
|
||||
const result = [...packages];
|
||||
@@ -78,6 +79,33 @@ export const ComparisonPageV1 = React.forwardRef<HTMLDivElement, ComparisonPageV
|
||||
? `Comparing ${packages.length} package${packages.length !== 1 ? 's' : ''} from different providers`
|
||||
: `Comparing ${packages.length} package${packages.length !== 1 ? 's' : ''}`;
|
||||
|
||||
const hasRecommended = allPackages.some((p) => p.isRecommended);
|
||||
|
||||
const scrollToCenter = useCallback((idx: number) => {
|
||||
const tab = tabRefs.current[idx];
|
||||
if (tab && railRef.current) {
|
||||
const rail = railRef.current;
|
||||
const tabCenter = tab.offsetLeft + tab.offsetWidth / 2;
|
||||
const railCenter = rail.offsetWidth / 2;
|
||||
rail.scrollTo({ left: tabCenter - railCenter, behavior: 'smooth' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(idx: number) => {
|
||||
setActiveTabIdx(idx);
|
||||
scrollToCenter(idx);
|
||||
},
|
||||
[scrollToCenter],
|
||||
);
|
||||
|
||||
// Center the default tab on mount
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => scrollToCenter(0), 50);
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box ref={ref} sx={sx}>
|
||||
<WizardLayout
|
||||
@@ -145,8 +173,9 @@ export const ComparisonPageV1 = React.forwardRef<HTMLDivElement, ComparisonPageV
|
||||
{/* Mobile: Tab rail + card view */}
|
||||
{isMobile && allPackages.length > 0 && (
|
||||
<>
|
||||
{/* Tab rail — mini cards showing provider + package name */}
|
||||
{/* Tab rail — mini cards showing provider + package + price */}
|
||||
<Box
|
||||
ref={railRef}
|
||||
role="tablist"
|
||||
id={tablistId}
|
||||
aria-label="Packages to compare"
|
||||
@@ -154,86 +183,30 @@ export const ComparisonPageV1 = React.forwardRef<HTMLDivElement, ComparisonPageV
|
||||
display: 'flex',
|
||||
gap: 1.5,
|
||||
overflowX: 'auto',
|
||||
pb: 1,
|
||||
mb: 2.5,
|
||||
py: 2,
|
||||
px: 2,
|
||||
mx: -2,
|
||||
mt: 1,
|
||||
mb: 3,
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}}
|
||||
>
|
||||
{allPackages.map((pkg, idx) => {
|
||||
const isActive = idx === activeTabIdx;
|
||||
return (
|
||||
<Card
|
||||
key={pkg.id}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
aria-controls={`comparison-tabpanel-${idx}`}
|
||||
id={`comparison-tab-${idx}`}
|
||||
variant="outlined"
|
||||
selected={isActive}
|
||||
padding="none"
|
||||
onClick={() => setActiveTabIdx(idx)}
|
||||
interactive
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
minWidth: 150,
|
||||
maxWidth: 200,
|
||||
cursor: 'pointer',
|
||||
...(pkg.isRecommended &&
|
||||
!isActive && {
|
||||
borderColor: 'var(--fa-color-brand-500)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box sx={{ px: 2, py: 1.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
mb: 0.25,
|
||||
}}
|
||||
>
|
||||
{pkg.isRecommended && (
|
||||
<StarRoundedIcon
|
||||
aria-label="Recommended"
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
color: 'var(--fa-color-brand-600)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
variant="labelSm"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{pkg.provider.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{pkg.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{allPackages.map((pkg, idx) => (
|
||||
<ComparisonTabCard
|
||||
key={pkg.id}
|
||||
ref={(el: HTMLDivElement | null) => {
|
||||
tabRefs.current[idx] = el;
|
||||
}}
|
||||
pkg={pkg}
|
||||
isActive={idx === activeTabIdx}
|
||||
hasRecommended={hasRecommended}
|
||||
tabId={`comparison-tab-${idx}`}
|
||||
tabPanelId={`comparison-tabpanel-${idx}`}
|
||||
onClick={() => handleTabClick(idx)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{activePackage && (
|
||||
|
||||
Reference in New Issue
Block a user