Component cleanup
This commit is contained in:
@@ -1,101 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Tag from "../../utility/Tag";
|
||||
import type { CardViewProps } from "./Card.types";
|
||||
|
||||
function InfoIcon() {
|
||||
return (
|
||||
<span
|
||||
className="flex h-[var(--spacing-scale-016)] w-[var(--spacing-scale-016)] shrink-0 items-center justify-center rounded-full border border-[var(--color-content-invert-brand-secondary)] bg-transparent font-inter text-[10px] font-medium leading-none text-[var(--color-content-invert-brand-secondary)]"
|
||||
aria-hidden
|
||||
>
|
||||
?
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTag({
|
||||
recommended,
|
||||
selected,
|
||||
}: {
|
||||
recommended: boolean;
|
||||
selected: boolean;
|
||||
}) {
|
||||
if (selected) return <Tag variant="selected" />;
|
||||
if (recommended) return <Tag variant="recommended" />;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CardView({
|
||||
label,
|
||||
supportText,
|
||||
recommended,
|
||||
selected,
|
||||
orientation,
|
||||
showInfoIcon,
|
||||
id: cardId,
|
||||
className,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
}: CardViewProps) {
|
||||
const borderClass = "border border-[var(--color-border-default-primary)]";
|
||||
const selectedBorder = selected
|
||||
? "outline outline-2 outline-dashed outline-black outline-offset-[-2px]"
|
||||
: "";
|
||||
const baseClasses = `select-none rounded-[var(--radius-measures-radius-small)] bg-[#FFFFFF] p-4 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`;
|
||||
|
||||
if (orientation === "horizontal") {
|
||||
return (
|
||||
<div
|
||||
{...(cardId ? { "data-card-id": cardId } : {})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
className={baseClasses}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<div className="flex flex-col gap-2 items-start w-full">
|
||||
<CardTag recommended={recommended} selected={selected} />
|
||||
<span className="font-inter text-base font-semibold leading-6 text-black w-full">
|
||||
{label}
|
||||
</span>
|
||||
{supportText ? (
|
||||
<p className="font-inter text-sm font-normal leading-5 text-black w-full">
|
||||
{supportText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(cardId ? { "data-card-id": cardId } : {})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<div className="min-w-0 flex-1 flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-inter text-base font-semibold leading-6 text-black">
|
||||
{label}
|
||||
</span>
|
||||
{showInfoIcon ? <InfoIcon /> : null}
|
||||
</div>
|
||||
{supportText ? (
|
||||
<p className="font-inter text-sm font-normal leading-5 text-black">
|
||||
{supportText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="shrink-0 w-[6rem]">
|
||||
<CardTag recommended={recommended} selected={selected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./Card.container";
|
||||
export type { CardProps } from "./Card.types";
|
||||
@@ -0,0 +1,100 @@
|
||||
"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";
|
||||
|
||||
/**
|
||||
* Figma: "Utility / CardStack"; canonical code under `cards/`.
|
||||
* Selectable stack of cards with
|
||||
* an optional "see all"/"show less" expand toggle.
|
||||
*/
|
||||
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 = "",
|
||||
layout = "default",
|
||||
compactRecommendedLimit = 5,
|
||||
compactCardIds,
|
||||
compactDesktopLayout: compactDesktopLayoutProp = "grid",
|
||||
headerLockupSize,
|
||||
toggleAlignment = "center",
|
||||
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}
|
||||
layout={layout}
|
||||
compactRecommendedLimit={compactRecommendedLimit}
|
||||
compactCardIds={compactCardIds}
|
||||
compactDesktopLayout={compactDesktopLayoutProp}
|
||||
headerLockupSize={headerLockupSize}
|
||||
toggleAlignment={toggleAlignment}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CardStackContainer.displayName = "CardStack";
|
||||
|
||||
export default CardStackContainer;
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { HeaderLockupSizeValue } from "../../type/HeaderLockup/HeaderLockup.types";
|
||||
|
||||
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;
|
||||
/** "default" = compact grid/column + expanded grid; "singleStack" = always one column, expand shows more in same stack */
|
||||
layout?: "default" | "singleStack";
|
||||
/**
|
||||
* Max recommended cards in compact (non-expanded) mode. Default 5; Figma compact stack uses 3.
|
||||
*/
|
||||
compactRecommendedLimit?: number;
|
||||
/**
|
||||
* Optional explicit list of card ids to render in the compact slot, in
|
||||
* order. When provided, this overrides the default
|
||||
* `cards.filter(c => c.recommended)` selection — the `recommended` flag
|
||||
* then only controls the visual "Recommended" badge. Used by the
|
||||
* create-flow card-deck steps so facet scores can pick the compact set
|
||||
* (and badge only the truly matched subset). Cards whose ids are not in
|
||||
* `cards` are silently dropped.
|
||||
*/
|
||||
compactCardIds?: string[];
|
||||
/**
|
||||
* At `md+`, how compact recommended cards are laid out. `flexWrap` matches Figma Flow — Compact Card Stack (three cards in a row).
|
||||
* `pyramidFive` = two rows (3 + 2) centered for five recommended cards (membership step).
|
||||
*/
|
||||
compactDesktopLayout?: "grid" | "flexWrap" | "pyramidFive";
|
||||
/** Optional title/description lockup size (create-flow passes `md`-matched `L`/`M`). Defaults to `L`. */
|
||||
headerLockupSize?: HeaderLockupSizeValue;
|
||||
/** Alignment of the expand/collapse control in `singleStack` layout (Figma right-rail: end). */
|
||||
toggleAlignment?: "center" | "end";
|
||||
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;
|
||||
layout: "default" | "singleStack";
|
||||
compactRecommendedLimit: number;
|
||||
compactCardIds: string[] | undefined;
|
||||
compactDesktopLayout: "grid" | "flexWrap" | "pyramidFive";
|
||||
headerLockupSize: HeaderLockupSizeValue | undefined;
|
||||
toggleAlignment: "center" | "end";
|
||||
className: string;
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
"use client";
|
||||
|
||||
import HeaderLockup from "../../type/HeaderLockup";
|
||||
import Selection from "../Selection";
|
||||
import type { CardStackViewProps } from "./CardStack.types";
|
||||
|
||||
export function CardStackView({
|
||||
cards,
|
||||
selectedIds,
|
||||
onCardSelect,
|
||||
expanded,
|
||||
onToggleExpand,
|
||||
hasMore,
|
||||
toggleLabel,
|
||||
showLessLabel,
|
||||
title,
|
||||
description,
|
||||
layout,
|
||||
compactRecommendedLimit,
|
||||
compactCardIds,
|
||||
compactDesktopLayout,
|
||||
headerLockupSize,
|
||||
toggleAlignment,
|
||||
className,
|
||||
}: CardStackViewProps) {
|
||||
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 same stack (scrollable)
|
||||
if (layout === "singleStack") {
|
||||
const displayedCards = expanded ? cards : compactCards;
|
||||
return (
|
||||
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
|
||||
{title || description ? (
|
||||
<div className="min-w-0 shrink-0">
|
||||
<HeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
size={lockupSize}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex w-full min-w-0 flex-col gap-2">
|
||||
{displayedCards.map((item) => (
|
||||
<Selection
|
||||
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>
|
||||
{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 cursor-pointer ${
|
||||
toggleAlignment === "end" ? "self-end" : "self-center"
|
||||
}`}
|
||||
>
|
||||
{expanded ? showLessLabel : toggleLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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={lockupSize}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{expanded ? (
|
||||
<div className="mx-auto grid w-full max-w-[min(100%,860px)] grid-cols-1 gap-x-4 gap-y-6 md:grid-cols-2">
|
||||
{cards.map((item) => (
|
||||
<Selection
|
||||
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>
|
||||
) : compactDesktopLayout === "pyramidFive" ? (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-2 md:hidden">
|
||||
{compactCards.map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="min-h-[142px]"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] md:block">
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
<div className="hidden flex-col gap-2 lg:flex">
|
||||
<div className="flex justify-center gap-2">
|
||||
{compactCards.slice(0, 3).map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{compactCards.length > 3 ? (
|
||||
<div className="flex justify-center gap-2">
|
||||
{compactCards
|
||||
.slice(3, compactRecommendedLimit)
|
||||
.map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="hidden flex-col gap-2 md:flex lg:hidden">
|
||||
<div className="flex justify-center gap-2">
|
||||
{compactCards.slice(0, 2).map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center gap-2">
|
||||
{compactCards.slice(2, 4).map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{compactCards[4] ? (
|
||||
<div className="flex justify-center gap-2">
|
||||
<Selection
|
||||
id={compactCards[4].id}
|
||||
label={compactCards[4].label}
|
||||
supportText={compactCards[4].supportText}
|
||||
recommended={compactCards[4].recommended ?? false}
|
||||
selected={isSelected(compactCards[4].id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(compactCards[4].id)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : compactDesktopLayout === "flexWrap" ? (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-2 md:hidden">
|
||||
{compactCards.map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="min-h-[142px]"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* md–lg: pyramid (2 + 1), each row centered; lg+: one centered row (not edge-to-edge in a 2-col grid) */}
|
||||
{compactCards.length === 3 ? (
|
||||
<>
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-col gap-2 md:flex lg:hidden">
|
||||
<div className="flex justify-center gap-2">
|
||||
{compactCards.slice(0, 2).map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Selection
|
||||
id={compactCards[2].id}
|
||||
label={compactCards[2].label}
|
||||
supportText={compactCards[2].supportText}
|
||||
recommended={compactCards[2].recommended ?? false}
|
||||
selected={isSelected(compactCards[2].id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(compactCards[2].id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-wrap justify-center gap-2 lg:flex">
|
||||
{compactCards.map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-wrap justify-center gap-2 md:flex">
|
||||
{compactCards.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex w-full min-w-0 shrink-0 justify-center md:w-[281px] md:max-w-[281px]"
|
||||
>
|
||||
<Selection
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-full max-w-[281px]"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Compact under 640: single column, up to 5 recommended cards */}
|
||||
<div className="flex w-full flex-col gap-2 md:hidden">
|
||||
{compactCards.map((item) => (
|
||||
<Selection
|
||||
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 2–3 and 4–5) */}
|
||||
<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}>
|
||||
<Selection
|
||||
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";
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { IconCardView } from "./IconCard.view";
|
||||
import type { IconCardProps } from "./IconCard.types";
|
||||
import { IconView } from "./Icon.view";
|
||||
import type { IconProps } from "./Icon.types";
|
||||
|
||||
const IconCardContainer = memo<IconCardProps>(
|
||||
const IconContainer = memo<IconProps>(
|
||||
({ icon, title, description, className = "", onClick }) => {
|
||||
const handleClick = () => {
|
||||
if (onClick) onClick();
|
||||
@@ -18,7 +18,7 @@ const IconCardContainer = memo<IconCardProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<IconCardView
|
||||
<IconView
|
||||
icon={icon}
|
||||
title={title}
|
||||
description={description}
|
||||
@@ -30,6 +30,6 @@ const IconCardContainer = memo<IconCardProps>(
|
||||
},
|
||||
);
|
||||
|
||||
IconCardContainer.displayName = "IconCard";
|
||||
IconContainer.displayName = "Icon";
|
||||
|
||||
export default IconCardContainer;
|
||||
export default IconContainer;
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
export interface IconCardProps {
|
||||
export interface IconProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -6,7 +6,7 @@ export interface IconCardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface IconCardViewProps {
|
||||
export interface IconViewProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import type { IconCardViewProps } from "./IconCard.types";
|
||||
import type { IconViewProps } from "./Icon.types";
|
||||
|
||||
export function IconCardView({
|
||||
export function IconView({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
}: IconCardViewProps) {
|
||||
}: IconViewProps) {
|
||||
return (
|
||||
<div
|
||||
className={`border border-[var(--color-border-default-primary)] flex flex-col h-[350px] items-start justify-between p-[var(--measures-spacing-020)] relative w-[288px] bg-transparent cursor-pointer transition-all duration-200 hover:scale-[1.02] hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-2 ${className}`}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Icon.container";
|
||||
export type { IconProps } from "./Icon.types";
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./IconCard.container";
|
||||
export type { IconCardProps } from "./IconCard.types";
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import MiniCardView from "./MiniCard.view";
|
||||
import type { MiniCardProps } from "./MiniCard.types";
|
||||
import MiniView from "./Mini.view";
|
||||
import type { MiniProps } from "./Mini.types";
|
||||
|
||||
const MiniCardContainer = memo<MiniCardProps>(
|
||||
const MiniContainer = memo<MiniProps>(
|
||||
({
|
||||
children,
|
||||
className = "",
|
||||
@@ -75,7 +75,7 @@ const MiniCardContainer = memo<MiniCardProps>(
|
||||
}, [href, onClick, computedAriaLabel]);
|
||||
|
||||
return (
|
||||
<MiniCardView
|
||||
<MiniView
|
||||
className={className}
|
||||
backgroundColor={backgroundColor}
|
||||
panelContent={panelContent}
|
||||
@@ -87,11 +87,11 @@ const MiniCardContainer = memo<MiniCardProps>(
|
||||
wrapperProps={wrapperProps}
|
||||
>
|
||||
{children}
|
||||
</MiniCardView>
|
||||
</MiniView>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MiniCardContainer.displayName = "MiniCard";
|
||||
MiniContainer.displayName = "Mini";
|
||||
|
||||
export default MiniCardContainer;
|
||||
export default MiniContainer;
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
export interface MiniCardProps {
|
||||
export interface MiniProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
backgroundColor?: string;
|
||||
@@ -11,7 +11,7 @@ export interface MiniCardProps {
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface MiniCardViewProps {
|
||||
export interface MiniViewProps {
|
||||
children?: React.ReactNode;
|
||||
className: string;
|
||||
backgroundColor: string;
|
||||
+5
-5
@@ -2,9 +2,9 @@
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
import type { MiniCardViewProps } from "./MiniCard.types";
|
||||
import type { MiniViewProps } from "./Mini.types";
|
||||
|
||||
function MiniCardView({
|
||||
function MiniView({
|
||||
children,
|
||||
className,
|
||||
backgroundColor,
|
||||
@@ -15,7 +15,7 @@ function MiniCardView({
|
||||
computedAriaLabel,
|
||||
wrapperElement,
|
||||
wrapperProps,
|
||||
}: MiniCardViewProps) {
|
||||
}: MiniViewProps) {
|
||||
const cardContentElement = (
|
||||
<div className={`h-[186px] flex flex-col gap-[7px] ${className}`}>
|
||||
{/* Top part - Inner panel */}
|
||||
@@ -81,6 +81,6 @@ function MiniCardView({
|
||||
);
|
||||
}
|
||||
|
||||
MiniCardView.displayName = "MiniCardView";
|
||||
MiniView.displayName = "MiniView";
|
||||
|
||||
export default memo(MiniCardView);
|
||||
export default memo(MiniView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Mini.container";
|
||||
export type { MiniProps } from "./Mini.types";
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./MiniCard.container";
|
||||
export type { MiniCardProps } from "./MiniCard.types";
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { RuleCardView } from "./RuleCard.view";
|
||||
import type { RuleCardProps } from "./RuleCard.types";
|
||||
import { RuleView } from "./Rule.view";
|
||||
import type { RuleProps } from "./Rule.types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -21,7 +21,7 @@ declare global {
|
||||
* Figma: "Card / Rule" — e.g. profile `22143:900771` when **Has bottom link** is on
|
||||
* (`hasBottomLinks` + `bottomLinks` / optional `bottomStatusLabel`).
|
||||
*/
|
||||
const RuleCardContainer = memo<RuleCardProps>(
|
||||
const RuleContainer = memo<RuleProps>(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
@@ -72,7 +72,7 @@ const RuleCardContainer = memo<RuleCardProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<RuleCardView
|
||||
<RuleView
|
||||
title={title}
|
||||
description={description}
|
||||
icon={icon}
|
||||
@@ -95,6 +95,6 @@ const RuleCardContainer = memo<RuleCardProps>(
|
||||
},
|
||||
);
|
||||
|
||||
RuleCardContainer.displayName = "RuleCard";
|
||||
RuleContainer.displayName = "Rule";
|
||||
|
||||
export default RuleCardContainer;
|
||||
export default RuleContainer;
|
||||
+8
-7
@@ -1,4 +1,5 @@
|
||||
import type { ChipOption } from "../../controls/MultiSelect/MultiSelect.types";
|
||||
import type { RuleSizeValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export interface Category {
|
||||
name: string;
|
||||
@@ -14,14 +15,14 @@ export interface Category {
|
||||
}
|
||||
|
||||
/** Bottom row for `Card / Rule` when Figma **Has bottom link** is on (profile, etc.). */
|
||||
export interface RuleCardBottomLink {
|
||||
export interface RuleBottomLink {
|
||||
id: string;
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface RuleCardProps {
|
||||
export interface RuleProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
@@ -29,7 +30,7 @@ export interface RuleCardProps {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
expanded?: boolean;
|
||||
size?: "XS" | "S" | "M" | "L";
|
||||
size?: RuleSizeValue;
|
||||
categories?: Category[];
|
||||
logoUrl?: string;
|
||||
logoAlt?: string;
|
||||
@@ -44,10 +45,10 @@ export interface RuleCardProps {
|
||||
hasBottomLinks?: boolean;
|
||||
/** Uppercase chip (e.g. IN PROGRESS); omit when no left badge. */
|
||||
bottomStatusLabel?: string;
|
||||
bottomLinks?: RuleCardBottomLink[];
|
||||
bottomLinks?: RuleBottomLink[];
|
||||
}
|
||||
|
||||
export interface RuleCardViewProps {
|
||||
export interface RuleViewProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
@@ -56,7 +57,7 @@ export interface RuleCardViewProps {
|
||||
onClick?: () => void;
|
||||
onKeyDown?: (_event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
expanded: boolean;
|
||||
size: "XS" | "S" | "M" | "L";
|
||||
size: RuleSizeValue;
|
||||
categories?: Category[];
|
||||
logoUrl?: string;
|
||||
logoAlt?: string;
|
||||
@@ -64,5 +65,5 @@ export interface RuleCardViewProps {
|
||||
hideCategoryAddButton?: boolean;
|
||||
hasBottomLinks?: boolean;
|
||||
bottomStatusLabel?: string;
|
||||
bottomLinks?: RuleCardBottomLink[];
|
||||
bottomLinks?: RuleBottomLink[];
|
||||
}
|
||||
+4
-4
@@ -4,9 +4,9 @@ import Image from "next/image";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import MultiSelect from "../../controls/MultiSelect";
|
||||
import NavigationLink from "../../navigation/Link";
|
||||
import type { RuleCardBottomLink, RuleCardViewProps } from "./RuleCard.types";
|
||||
import type { RuleBottomLink, RuleViewProps } from "./Rule.types";
|
||||
|
||||
export function RuleCardView({
|
||||
export function RuleView({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
@@ -24,7 +24,7 @@ export function RuleCardView({
|
||||
hasBottomLinks = false,
|
||||
bottomStatusLabel,
|
||||
bottomLinks,
|
||||
}: RuleCardViewProps) {
|
||||
}: RuleViewProps) {
|
||||
const t = useTranslation("ruleCard");
|
||||
const ariaLabel = t("ariaLabel")?.replace("{title}", title) || title;
|
||||
const interactiveCard = !hasBottomLinks;
|
||||
@@ -181,7 +181,7 @@ export function RuleCardView({
|
||||
? "rounded-[var(--measures-radius-300,12px)]"
|
||||
: "rounded-[var(--radius-measures-radius-small)]";
|
||||
|
||||
function renderBottomLink(link: RuleCardBottomLink) {
|
||||
function renderBottomLink(link: RuleBottomLink) {
|
||||
const shared = {
|
||||
variant: "paragraph" as const,
|
||||
type: "primary" as const,
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Rule.container";
|
||||
export type { Category, RuleBottomLink, RuleProps } from "./Rule.types";
|
||||
@@ -1,5 +0,0 @@
|
||||
export { default } from "./RuleCard.container";
|
||||
export type {
|
||||
RuleCardBottomLink,
|
||||
RuleCardProps,
|
||||
} from "./RuleCard.types";
|
||||
+10
-6
@@ -1,10 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { CardView } from "./Card.view";
|
||||
import type { CardProps } from "./Card.types";
|
||||
import { SelectionView } from "./Selection.view";
|
||||
import type { SelectionProps } from "./Selection.types";
|
||||
|
||||
const CardContainer = memo<CardProps>(
|
||||
/**
|
||||
* Figma: "Card / CardSelection" — stacked tile e.g. `16775:28762` (recommended + label + supportText).
|
||||
* `orientation="horizontal"` selects that vertical stack; `"vertical"` is label + optional info icon with tag on the right (CardStack expanded / single-column).
|
||||
*/
|
||||
const SelectionContainer = memo<SelectionProps>(
|
||||
({
|
||||
label,
|
||||
supportText = "",
|
||||
@@ -28,7 +32,7 @@ const CardContainer = memo<CardProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<CardView
|
||||
<SelectionView
|
||||
label={label}
|
||||
supportText={supportText}
|
||||
recommended={recommended}
|
||||
@@ -44,6 +48,6 @@ const CardContainer = memo<CardProps>(
|
||||
},
|
||||
);
|
||||
|
||||
CardContainer.displayName = "Card";
|
||||
SelectionContainer.displayName = "Selection";
|
||||
|
||||
export default CardContainer;
|
||||
export default SelectionContainer;
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
export interface CardProps {
|
||||
export interface SelectionProps {
|
||||
label: string;
|
||||
supportText?: string;
|
||||
recommended?: boolean;
|
||||
selected?: boolean;
|
||||
orientation: "horizontal" | "vertical";
|
||||
showInfoIcon?: boolean;
|
||||
/** Optional id for the card root (e.g. data-card-id for focus after modal close). */
|
||||
/** Optional id for the root (e.g. `data-card-id` for focus after modal close). */
|
||||
id?: string;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface CardViewProps {
|
||||
export interface SelectionViewProps {
|
||||
label: string;
|
||||
supportText: string;
|
||||
recommended: boolean;
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import Tag from "../../utility/Tag";
|
||||
import type { SelectionViewProps } from "./Selection.types";
|
||||
|
||||
function InfoIcon() {
|
||||
return (
|
||||
<span
|
||||
className="flex h-[var(--spacing-scale-016)] w-[var(--spacing-scale-016)] shrink-0 items-center justify-center rounded-full border border-[var(--color-content-invert-brand-secondary)] bg-transparent font-inter text-[10px] font-medium leading-none text-[var(--color-content-invert-brand-secondary)]"
|
||||
aria-hidden
|
||||
>
|
||||
?
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectionTag({
|
||||
recommended,
|
||||
selected,
|
||||
}: {
|
||||
recommended: boolean;
|
||||
selected: boolean;
|
||||
}) {
|
||||
if (selected) return <Tag variant="selected" />;
|
||||
if (recommended) return <Tag variant="recommended" />;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function SelectionView({
|
||||
label,
|
||||
supportText,
|
||||
recommended,
|
||||
selected,
|
||||
orientation,
|
||||
showInfoIcon,
|
||||
id: selectionId,
|
||||
className,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
}: SelectionViewProps) {
|
||||
const borderClass = "border border-[var(--color-border-default-primary)]";
|
||||
const selectedBorder = selected
|
||||
? "outline outline-2 outline-dashed outline-black outline-offset-[-2px]"
|
||||
: "";
|
||||
|
||||
// Figma: "Card / CardSelection" vertical stack — node `16775:28762` (dev).
|
||||
// Prop `orientation="horizontal"` is this stacked layout (historical naming).
|
||||
if (orientation === "horizontal") {
|
||||
const baseClasses = `select-none rounded-[var(--measures-radius-200,8px)] bg-[var(--color-gray-000)] px-4 py-3 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(selectionId ? { "data-card-id": selectionId } : {})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
className={`${baseClasses} flex min-h-0 w-full flex-col items-start justify-center gap-1`}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<SelectionTag recommended={recommended} selected={selected} />
|
||||
<span className="w-full font-inter text-base font-medium leading-5 text-[var(--color-content-invert-secondary)]">
|
||||
{label}
|
||||
</span>
|
||||
{supportText ? (
|
||||
<p className="w-full font-inter text-xs font-normal leading-4 text-[var(--color-content-invert-tertiary)]">
|
||||
{supportText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const baseClasses = `select-none rounded-[var(--measures-radius-200,8px)] bg-[var(--color-gray-000)] p-4 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(selectionId ? { "data-card-id": selectionId } : {})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-inter text-base font-medium leading-5 text-[var(--color-content-invert-secondary)]">
|
||||
{label}
|
||||
</span>
|
||||
{showInfoIcon ? <InfoIcon /> : null}
|
||||
</div>
|
||||
{supportText ? (
|
||||
<p className="font-inter text-xs font-normal leading-4 text-[var(--color-content-invert-tertiary)]">
|
||||
{supportText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<SelectionTag recommended={recommended} selected={selected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Selection.container";
|
||||
export type { SelectionProps } from "./Selection.types";
|
||||
@@ -1,19 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import SectionNumber from "../sections/SectionNumber";
|
||||
import type { StepSizeValue } from "../../../../lib/propNormalization";
|
||||
import SectionNumber from "../../sections/SectionNumber";
|
||||
|
||||
export type NumberCardSizeValue = "small" | "medium" | "large" | "xlarge";
|
||||
|
||||
interface NumberCardProps {
|
||||
interface StepProps {
|
||||
number: number;
|
||||
text: string;
|
||||
size?: NumberCardSizeValue;
|
||||
size?: StepSizeValue;
|
||||
iconShape?: string;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
const Step = memo<StepProps>(({ number, text, size: sizeProp }) => {
|
||||
const baseClasses =
|
||||
"bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
|
||||
|
||||
@@ -101,6 +100,6 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
);
|
||||
});
|
||||
|
||||
NumberCard.displayName = "NumberCard";
|
||||
Step.displayName = "Step";
|
||||
|
||||
export default NumberCard;
|
||||
export default Step;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Step";
|
||||
export type { StepSizeValue } from "../../../../lib/propNormalization";
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useMemo } from "react";
|
||||
import Create from "../../modals/Create";
|
||||
import Chip from "../../controls/Chip";
|
||||
import InputLabel from "../../utility/InputLabel";
|
||||
import InputLabel from "../../type/InputLabel";
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import ModalTextAreaField from "../../../(app)/create/components/ModalTextAreaField";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import RuleCard from "../RuleCard";
|
||||
import type {
|
||||
Category,
|
||||
RuleCardProps,
|
||||
} from "../RuleCard/RuleCard.types";
|
||||
import Rule from "../Rule";
|
||||
import type { Category, RuleProps } from "../Rule";
|
||||
import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import type { RuleTemplateDto } from "../../../../lib/create/fetchTemplates";
|
||||
import {
|
||||
@@ -21,14 +18,14 @@ import { TemplateChipDetailModal } from "./TemplateChipDetailModal";
|
||||
|
||||
export interface TemplateReviewCardProps {
|
||||
template: RuleTemplateDto;
|
||||
/** Merged onto RuleCard `className` (e.g. final-review desktop vs mobile radius/padding). */
|
||||
/** Merged onto Rule `className` (e.g. final-review desktop vs mobile radius/padding). */
|
||||
ruleCardClassName?: string;
|
||||
/** RuleCard size; create-flow passes `L` at/above `md`, `M` below (640px). */
|
||||
size?: RuleCardProps["size"];
|
||||
/** Rule size; create-flow passes `L` at/above `md`, `M` below (640px). */
|
||||
size?: RuleProps["size"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expanded RuleCard for template review: surfaces + icon from Figma catalog (21764-16435);
|
||||
* Expanded Rule for template review: surfaces + icon from Figma catalog (21764-16435);
|
||||
* tag rows from API `body`. Chip clicks open a read-only detail modal per
|
||||
* facet group (values / communication / membership / decision-making / conflict
|
||||
* management) so reviewers can see what each chip means without editing.
|
||||
@@ -56,7 +53,7 @@ export function TemplateReviewCard({
|
||||
setActiveChipId(chipId);
|
||||
},
|
||||
})),
|
||||
[rawCategories],
|
||||
[rawCategories, setActiveChipId],
|
||||
);
|
||||
|
||||
const activeDetail =
|
||||
@@ -64,7 +61,7 @@ export function TemplateReviewCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<RuleCard
|
||||
<Rule
|
||||
title={template.title}
|
||||
description={summary}
|
||||
expanded
|
||||
|
||||
Reference in New Issue
Block a user