Card compact and expanded template

This commit is contained in:
adilallo
2026-02-11 22:02:10 -07:00
parent f60df15c2b
commit b2ed1d438c
44 changed files with 1920 additions and 48 deletions
@@ -0,0 +1,79 @@
"use client";
import { memo, useCallback, useState } from "react";
import { CardStackView } from "./CardStack.view";
import type { CardStackProps } from "./CardStack.types";
const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
const DEFAULT_SHOW_LESS_LABEL = "Show less";
const CardStackContainer = memo<CardStackProps>(
({
cards,
selectedId: controlledSelectedId,
selectedIds: controlledSelectedIds,
onCardSelect: controlledOnCardSelect,
expanded: controlledExpanded,
onToggleExpand: controlledOnToggleExpand,
hasMore = true,
toggleLabel = DEFAULT_TOGGLE_LABEL,
showLessLabel = DEFAULT_SHOW_LESS_LABEL,
title = "",
description = "",
className = "",
}) => {
const [internalExpanded, setInternalExpanded] = useState(false);
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>([]);
const expanded =
controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
const handleToggleExpand = useCallback(() => {
if (controlledOnToggleExpand) {
controlledOnToggleExpand();
} else {
setInternalExpanded((prev) => !prev);
}
}, [controlledOnToggleExpand]);
const selectedIds =
controlledSelectedIds !== undefined
? controlledSelectedIds
: controlledSelectedId !== undefined
? (controlledSelectedId ? [controlledSelectedId] : [])
: internalSelectedIds;
const handleCardSelect = useCallback(
(id: string) => {
if (controlledOnCardSelect) {
controlledOnCardSelect(id);
} else {
setInternalSelectedIds((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
);
}
},
[controlledOnCardSelect],
);
return (
<CardStackView
cards={cards}
selectedIds={selectedIds}
onCardSelect={handleCardSelect}
expanded={expanded}
onToggleExpand={handleToggleExpand}
hasMore={hasMore}
toggleLabel={toggleLabel}
showLessLabel={showLessLabel}
title={title}
description={description}
className={className}
/>
);
},
);
CardStackContainer.displayName = "CardStack";
export default CardStackContainer;
@@ -0,0 +1,35 @@
export interface CardStackItem {
id: string;
label: string;
supportText?: string;
recommended?: boolean;
}
export interface CardStackProps {
cards: CardStackItem[];
selectedId?: string | null;
selectedIds?: string[];
onCardSelect?: (id: string) => void;
expanded?: boolean;
onToggleExpand?: () => void;
hasMore?: boolean;
toggleLabel?: string;
showLessLabel?: string;
title?: string;
description?: string;
className?: string;
}
export interface CardStackViewProps {
cards: CardStackItem[];
selectedIds: string[];
onCardSelect: (id: string) => void;
expanded: boolean;
onToggleExpand: () => void;
hasMore: boolean;
toggleLabel: string;
showLessLabel: string;
title: string;
description: string;
className: string;
}
@@ -0,0 +1,114 @@
"use client";
import HeaderLockup from "../../type/HeaderLockup";
import Card from "../../cards/Card";
import type { CardStackViewProps } from "./CardStack.types";
export function CardStackView({
cards,
selectedIds,
onCardSelect,
expanded,
onToggleExpand,
hasMore,
toggleLabel,
showLessLabel,
title,
description,
className,
}: CardStackViewProps) {
const isSelected = (id: string) => selectedIds.includes(id);
// Compact: recommended only (up to 5). Expanded: all cards.
const compactCards = cards
.filter((c) => c.recommended ?? false)
.slice(0, 5);
return (
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
{(title || description) ? (
<div className="min-w-0">
<HeaderLockup
title={title}
description={description}
justification="center"
size="L"
/>
</div>
) : null}
{expanded ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-6 w-full">
{cards.map((item) => (
<Card
key={item.id}
id={item.id}
label={item.label}
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="vertical"
showInfoIcon={true}
onClick={() => onCardSelect(item.id)}
/>
))}
</div>
) : (
<>
{/* Compact under 640: single column, up to 5 recommended cards */}
<div className="flex flex-col gap-6 w-full md:hidden">
{compactCards.map((item) => (
<Card
key={item.id}
id={item.id}
label={item.label}
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="vertical"
showInfoIcon={true}
onClick={() => onCardSelect(item.id)}
/>
))}
</div>
{/* Compact 640+: 6-col grid so each card spans 2; second row centered (cols 23 and 45) */}
<div className="hidden md:grid grid-cols-6 gap-x-4 gap-y-6 w-full">
{compactCards.map((item, index) => {
const colClass =
index <= 2
? "md:col-span-2"
: index === 3 && compactCards.length === 4
? "md:col-start-3 md:col-span-2"
: index === 3
? "md:col-start-2 md:col-span-2"
: "md:col-start-4 md:col-span-2";
return (
<div key={item.id} className={colClass}>
<Card
id={item.id}
label={item.label}
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
showInfoIcon={false}
onClick={() => onCardSelect(item.id)}
/>
</div>
);
})}
</div>
</>
)}
{hasMore ? (
<button
type="button"
onClick={onToggleExpand}
className="font-inter text-base font-normal leading-6 text-[var(--color-gray-000)] underline hover:opacity-90 focus:outline-none self-center cursor-pointer"
>
{expanded ? showLessLabel : toggleLabel}
</button>
) : null}
</div>
);
}
@@ -0,0 +1,2 @@
export { default } from "./CardStack.container";
export type { CardStackProps, CardStackItem } from "./CardStack.types";