Add ComparisonPage V2 with recommended package on the left

Archive the current ComparisonPage as V1 (viewable under Archive/ in
Storybook) and build V2 as the new production version. In V2, the
recommended package is prepended instead of appended: it appears as the
first column on desktop and the first tab in the mobile rail. On mobile
the initially active tab is the first user-selected package, not the
recommendation — the recommended tab is surfaced as a visible suggestion
rather than the default view, which felt too upsell-y for the audience.

Both V1 and V2 now use a StarRoundedIcon (brand-600) in the mobile tab
label instead of a text star, so the "recommended" marker reads cleanly
against both selected and unselected tab backgrounds.

See decisions-log D040 for rationale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 14:59:28 +10:00
parent c3c0beadb9
commit cd0f79f2f5
4 changed files with 1039 additions and 14 deletions

View File

@@ -303,13 +303,19 @@ function MobilePackageCard({
// ─── Component ──────────────────────────────────────────────────────────────
/**
* Package comparison page for the FA design system.
* Package comparison page for the FA design system (V2 — production).
*
* Desktop: Full ComparisonTable with info card, floating verified badges,
* section tables with left accent borders.
* Mobile: Tabbed card view with horizontal chip rail.
* section tables with left accent borders. **Recommended package appears as
* the first (leftmost) column.**
* Mobile: Tabbed card view with horizontal tab rail. **Recommended package is
* the first tab in the rail, but the first user-selected package is the
* initially active tab** — the recommended tab is a suggestion, not the
* default view.
*
* Share + Print utility actions in the page header.
*
* See `ComparisonPageV1.tsx` for the archived V1 (recommended-last) layout.
*/
export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPageProps>(
(
@@ -321,14 +327,18 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
const tablistId = useId();
const allPackages = React.useMemo(() => {
const result = [...packages];
const result: ComparisonPackage[] = [];
if (recommendedPackage) {
result.push({ ...recommendedPackage, isRecommended: true });
}
result.push(...packages);
return result;
}, [packages, recommendedPackage]);
const [activeTabIdx, setActiveTabIdx] = useState(0);
// On mobile, default the active tab to the first user-selected package
// (not the recommended). Recommended is first in the rail as a suggestion.
const defaultTabIdx = recommendedPackage ? 1 : 0;
const [activeTabIdx, setActiveTabIdx] = useState(defaultTabIdx);
const activePackage = allPackages[activeTabIdx] ?? allPackages[0];
const providerCount = new Set(allPackages.map((p) => p.provider.name)).size;
@@ -446,18 +456,37 @@ export const ComparisonPage = React.forwardRef<HTMLDivElement, ComparisonPagePro
}}
>
<Box sx={{ px: 2, py: 1.5 }}>
<Typography
variant="labelSm"
<Box
sx={{
fontWeight: 600,
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
gap: 0.5,
mb: 0.25,
}}
>
{pkg.isRecommended ? `${pkg.provider.name}` : pkg.provider.name}
</Typography>
{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"