"use client"; import type { ReactNode } from "react"; import HeaderLockup from "../../type/HeaderLockup"; import type { HeaderLockupSizeValue } from "../../type/HeaderLockup/HeaderLockup.types"; import Selection from "../Selection"; import type { CardStackItem, CardStackViewProps } from "./CardStack.types"; /** Figma Card / CardSelection on Compact + Expanded Card Stack (`281×142`). */ const FIGMA_CARD_SELECTION_TILE_CLASS = "h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"; function CardStackHeaderLockup({ title, description, justification, size, wrapperClassName, }: { title: string; description: ReactNode; justification: "center" | "left"; size: HeaderLockupSizeValue; wrapperClassName?: string; }) { if (!title && !description) { return null; } return (
); } function matchesSearch(item: CardStackItem, query: string): boolean { if (!query) return true; const haystack = `${item.label} ${item.supportText ?? ""}`.toLowerCase(); return haystack.includes(query.toLowerCase()); } function SearchGlyph() { return ( ); } function StackCard({ item, expanded, selected, size, className, onSelect, onAlternativeSelect, alternativePrefix, }: { item: CardStackItem; expanded: boolean; selected: boolean; size: "m" | "l"; className?: string; onSelect: (id: string) => void; onAlternativeSelect?: (id: string) => void; alternativePrefix: string; }) { const supportText = !expanded && item.compactSupportText ? item.compactSupportText : item.supportText; const recommended = Boolean(expanded && item.recommended); const alternative = !expanded && item.alternative && onAlternativeSelect ? (

{alternativePrefix}{" "}

) : null; return (
onSelect(item.id)} /> {alternative}
); } function CardStackAddCardButton({ label, ariaLabel, onClick, }: { label: string; ariaLabel: string; onClick: () => void; }) { return ( ); } export function CardStackView({ cards, selectedIds, onCardSelect, expanded, onToggleExpand, hasMore, toggleLabel, showLessLabel, title, description, layout, compactRecommendedLimit, compactCardIds, compactDesktopLayout, headerLockupSize, toggleAlignment, className, showAddCard, addCardLabel, addCardAriaLabel, onAddCard, deckLine, searchPlaceholder, searchQuery, onSearchQueryChange, alternativePrefix, onAlternativeSelect, }: CardStackViewProps) { const addTile = showAddCard && onAddCard && addCardLabel.length > 0 ? ( ) : null; const lockupSize = headerLockupSize ?? "L"; const isSelected = (id: string) => selectedIds.includes(id); // Compact: explicit `compactCardIds` (caller-driven, used by create-flow // facet ranker) takes precedence over the legacy `recommended`-filter so // the screen can show un-tagged cards in the compact slot when there is // no facet signal yet (CR-88 §10). const compactCards = (() => { if (compactCardIds && compactCardIds.length > 0) { const byId = new Map(cards.map((c) => [c.id, c])); return compactCardIds .map((id) => byId.get(id)) .filter((c): c is (typeof cards)[number] => c !== undefined) .slice(0, compactRecommendedLimit); } return cards .filter((c) => c.recommended ?? false) .slice(0, compactRecommendedLimit); })(); // Single stack: always one column; expand reveals more in the same stack. // At lg+ the cards scroll inside this column and the See all / Show less // control stays pinned below (Figma Flow — Right Rail). if (layout === "singleStack") { const displayedCards = (expanded ? cards : compactCards).filter((item) => expanded ? matchesSearch(item, searchQuery) : true, ); return (
{!expanded && deckLine ? (

{deckLine}

) : null} {expanded && searchPlaceholder ? (
onSearchQueryChange(event.target.value)} placeholder={searchPlaceholder} aria-label={searchPlaceholder} className="h-10 w-full rounded-[var(--radius-200)] border border-[var(--color-border-default-control)] bg-[var(--color-surface-default-secondary)] px-3 py-2.5 pr-11 text-medium-paragraph text-[var(--color-content-default-primary)] placeholder:text-[var(--color-content-default-tertiary)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)]" />
) : null}
{displayedCards.map((item) => ( ))} {addTile}
{hasMore ? ( ) : null}
); } return (
{expanded ? ( <>
{cards.map((item) => ( onCardSelect(item.id)} /> ))} {addTile}
{cards.map((item) => ( onCardSelect(item.id)} /> ))} {addTile ? (
{addTile}
) : null}
) : compactDesktopLayout === "pyramidFive" ? ( <>
{compactCards.map((item) => ( onCardSelect(item.id)} /> ))} {addTile}
{/* lg+: fixed 3 + 2 rows (no flex-wrap on the top row — avoids 2+1+2 when the first row wraps). md–lg: same shell as the 3-card step — each row is `flex justify-center gap-2` so cards stay a tight cluster with gap-2 until lg expands to the 3+2 pyramid. */}
{compactCards.slice(0, 3).map((item) => ( onCardSelect(item.id)} /> ))}
{compactCards.length > 3 ? (
{compactCards .slice(3, compactRecommendedLimit) .map((item) => ( onCardSelect(item.id)} /> ))}
) : null}
{compactCards.slice(0, 2).map((item) => ( onCardSelect(item.id)} /> ))}
{compactCards.slice(2, 4).map((item) => ( onCardSelect(item.id)} /> ))}
{compactCards[4] ? (
onCardSelect(compactCards[4].id)} />
) : null}
{addTile ? (
{addTile}
) : null} ) : compactDesktopLayout === "flexWrap" ? ( <>
{compactCards.map((item) => ( onCardSelect(item.id)} /> ))} {addTile}
{/* md–lg: pyramid (2 + 1), each row centered; lg+: one centered row (not edge-to-edge in a 2-col grid) */} {compactCards.length === 3 ? ( <>
{compactCards.slice(0, 2).map((item) => ( onCardSelect(item.id)} /> ))}
onCardSelect(compactCards[2].id)} />
{addTile ? (
{addTile}
) : null}
{compactCards.map((item) => ( onCardSelect(item.id)} /> ))}
{addTile ? (
{addTile}
) : null} ) : (
{compactCards.map((item) => (
onCardSelect(item.id)} />
))} {addTile ? (
{addTile}
) : null}
)} ) : ( <> {/* Compact under 640: single column, up to 5 recommended cards */}
{compactCards.map((item) => ( onCardSelect(item.id)} /> ))} {addTile}
{/* Compact 640+: 6-col grid so each card spans 2; second row centered (cols 2–3 and 4–5) */}
{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 (
onCardSelect(item.id)} />
); })} {addTile ? (
{addTile}
) : null}
)} {hasMore ? ( ) : null}
); }