Standardize create-flow option steps on one question-and-options layout and Selection sizes.

Method cards, chips, and the right rail now share the two-column shell; last options clear the footer fade, See all stays under the stack, and the left heading no longer jumps when the list expands.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilalloandCursor committed 2026-10-01 17:18:04 -06:00
1 parent 2fc7fbf818
commit 24a9615c0c
26 files changed
+354 -245

No files matched your search

+4 -8
View File
@@ -41,10 +41,7 @@ import {
createFlowStepPathWithSyncDraft,
} from "./utils/createFlowPaths";
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
import {
createFlowStepUsesCenteredTextLayout,
createFlowStepUsesCardLayout,
} from "./utils/createFlowScreenRegistry";
import { createFlowStepUsesCenteredTextLayout } from "./utils/createFlowScreenRegistry";
import Button from "../../components/buttons/Button";
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
import { buildCreateFlowDraftPayload } from "../../../lib/create/buildCreateFlowDraftPayload";
@@ -557,20 +554,19 @@ function CreateFlowLayoutContent({
const isFinalReviewLike =
currentStep === "final-review" || currentStep === "edit-rule";
const isEditRuleStep = currentStep === "edit-rule";
const isCardLayoutStep = createFlowStepUsesCardLayout(currentStep);
/** Two-column select / right-rail: below `lg` main scrolls; at `lg+` only the right column scrolls. */
/** Two-column question-and-options: below `lg` main scrolls; at `lg+` only the right column scrolls. */
const isSelectSplitScrollStep = createFlowStepUsesSelectSplitScroll(
currentStep,
);
/** Lockup+card / card-stack: `items-start` + shell `my-auto` so overflow scrolls from the top. */
/** Lockup+card review: `items-start` + shell `my-auto` so overflow scrolls from the top. */
const mainContentClass = isCompletedStep
? "items-stretch overflow-y-auto md:overflow-hidden"
: isSelectSplitScrollStep
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
: isCreateTemplatesPickerRoute
? "items-start justify-start overflow-y-auto"
: isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep
: isTemplateReviewRoute || isFinalReviewLike
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
: "items-start justify-center overflow-y-auto md:items-center";
@@ -1,56 +0,0 @@
"use client";
import type { ReactNode } from "react";
import { CreateFlowHeaderLockup } from "./CreateFlowHeaderLockup";
import { CreateFlowStepShell } from "./CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
CREATE_FLOW_WIDE_MAX_CLASS,
} from "./createFlowLayoutTokens";
type CreateFlowCardStackStepShellProps = {
lockupTitle: string;
lockupDescription?: ReactNode;
/** When true, the stack region exposes `aria-busy` while recommendations load. */
stackBusy?: boolean;
children: ReactNode;
};
/**
* Centered lockup + card-stack layout shared by communication, membership, and
* conflict-management steps (Figma compact card stack).
*/
export function CreateFlowCardStackStepShell({
lockupTitle,
lockupDescription,
stackBusy = false,
children,
}: CreateFlowCardStackStepShellProps) {
return (
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-800"
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
>
<div
className={`flex w-full min-w-0 flex-col items-center gap-6 ${CREATE_FLOW_WIDE_MAX_CLASS}`}
>
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
<CreateFlowHeaderLockup
title={lockupTitle}
description={lockupDescription}
justification="center"
/>
</div>
<div
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
aria-busy={stackBusy}
>
{children}
</div>
</div>
</CreateFlowStepShell>
);
}
@@ -7,6 +7,7 @@ import {
} from "./CreateFlowStepShell";
import {
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
CREATE_FLOW_SCROLL_END_PAD_CLASS,
CREATE_FLOW_SCROLL_REGION_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "./createFlowLayoutTokens";
@@ -23,15 +24,17 @@ interface CreateFlowTwoColumnSelectShellProps {
contentTopBelowMd?: CreateFlowContentTopBelowMd;
/**
* At `lg+`, layout variant: `"center"` = vertically centered pair (community size/structure).
* `"start"` = top-weighted layout with a scrollable right column (core values, right-rail): uses `items-stretch`
* so the right column gets a bounded height; `items-start` would grow with content and break scroll.
* `"start"` = full-height split with a scrollable right column (core values, method cards,
* right-rail). Both columns stretch so the left lockup stays in one place when the right
* list grows (Figma Flow — Right Rail / Expanded Card Stack). The lockup is centered in
* the left pane; `items-start` on the row would let the title track card-stack height.
*/
lgVerticalAlign?: CreateFlowSelectShellLgVerticalAlign;
}
/**
* Question-and-options layout for create-flow select steps (community size/structure,
* core values, stakeholders) and {@link DecisionApproachesScreen}.
* core values, stakeholders), method-card steps, and {@link DecisionApproachesScreen}.
* Below `lg` (1024px), one column uses available width and main scrolls.
* At `lg+`, static header column + scrollable options column (`min-h-0` + overflow).
*/
@@ -45,9 +48,6 @@ export function CreateFlowTwoColumnSelectShell({
const rowLgCrossAlignClass =
lgVerticalAlign === "start" ? "lg:items-stretch" : "lg:items-center";
const leftLgMainJustifyClass =
lgVerticalAlign === "start" ? "lg:justify-start" : "lg:justify-center";
return (
<CreateFlowStepShell
variant="centeredNarrow"
@@ -68,7 +68,7 @@ export function CreateFlowTwoColumnSelectShell({
<div
className={
`flex w-full min-w-0 shrink-0 flex-col items-start gap-[var(--measures-spacing-200,8px)] ` +
`lg:flex-1 ${leftLgMainJustifyClass} lg:py-[12px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`
`lg:flex-1 lg:justify-center lg:py-[12px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`
}
>
{header}
@@ -76,7 +76,7 @@ export function CreateFlowTwoColumnSelectShell({
<div
className={
`relative flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-800,32px)] ` +
`overflow-x-hidden lg:min-h-0 lg:flex-1 lg:overflow-y-auto lg:pb-[var(--measures-spacing-300,12px)] ` +
`overflow-x-hidden lg:min-h-0 lg:flex-1 lg:overflow-y-auto ${CREATE_FLOW_SCROLL_END_PAD_CLASS} ` +
`${CREATE_FLOW_SCROLL_REGION_CLASS} ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`
}
>
@@ -31,6 +31,12 @@ export const CREATE_FLOW_SCROLL_REGION_CLASS = "scrollbar-design";
export const CREATE_FLOW_FOOTER_SCRIM_CLASS =
"pointer-events-none absolute inset-x-0 top-0 z-[1] h-8 -translate-y-full bg-gradient-to-t from-[var(--color-surface-default-primary,#000)] to-transparent";
/**
* Bottom padding in create-flow option scrollports so the last control can
* rest above the footer scrim (`h-8`) instead of sitting in the fade.
*/
export const CREATE_FLOW_SCROLL_END_PAD_CLASS = "pb-12";
/**
* Single column: full width below `lg` so tablet widths use available space;
* 640px from `--breakpoint-lg` up (two-column layouts start at `lg`).
@@ -46,7 +52,9 @@ export const CREATE_FLOW_MD_UP_GRID_CELL_CLASS =
export const CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS =
"w-full min-w-0 lg:max-w-[1328px]";
/** Review / card-stack frames (Figma Flow — Review max 1440). */
/**
* Review frames (Figma Flow — Review max 1440).
*/
export const CREATE_FLOW_WIDE_MAX_CLASS = "w-full min-w-0 lg:max-w-[1440px]";
/**
@@ -62,14 +70,6 @@ export const CREATE_FLOW_MD_CENTERED_MAIN_CLASS =
*/
export const CREATE_FLOW_MD_CENTERED_SHELL_CLASS = "md:my-auto md:pt-0";
/**
* Card-stack steps only (Figma compact card stack): wider than header lockup so the card grid /
* pyramid fits (max 860px). Header lockup stays {@link CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}.
* Card–card gap uses `gap-2` in `CardStack` (same on mobile and md+).
*/
export const CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS =
"w-full min-w-0 md:max-w-[min(100%,860px)]";
/**
* Inner max-width for step content and the action bar, so the primary button
* lines up with the nearest content column above 1440.
@@ -88,11 +88,10 @@ export function getCreateFlowContentMaxClass(options: {
switch (CREATE_FLOW_SCREEN_REGISTRY[options.step].layoutKind) {
case "select":
case "right-rail":
case "card":
case "completed":
case "review":
return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS;
case "card":
return CREATE_FLOW_WIDE_MAX_CLASS;
default:
return CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS;
}
@@ -4,9 +4,9 @@
* `communication-methods` step — Figma “Flow — Compact Card Stack” (node `20246-15828`).
* Registry: `layoutKind: "card"` (`CREATE_FLOW_SCREEN_REGISTRY["communication-methods"]`).
*
* Lives under `screens/card/` (not `select/`): Figma **card stack** layout is a distinct shell from
* two-column chip **select** frames. Future card-stack steps get their own `*Screen.tsx` here and
* reuse `CardStack` / `CreateFlowCardStackStepShell` as needed.
* Page chrome is the shared question-and-options shell
* ({@link CreateFlowTwoColumnSelectShell}); options are {@link CardStack} with
* `layout="singleStack"` and {@link Selection} size `l`.
*
* Card click opens the Figma create modal (node `20246-15829`) with three
* editable sections rendered by {@link CommunicationMethodEditFields}. The
@@ -18,11 +18,11 @@
import { useState, useCallback, useMemo, useRef } from "react";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
import { CreateFlowCardStackStepShell } from "../../components/CreateFlowCardStackStepShell";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
@@ -61,7 +61,6 @@ export function CommunicationMethodsScreen() {
const m = useMessages();
const comm = m.create.customRule.communication;
const modalKebabMenu = m.create.customRule.modalKebabMenu;
const mdUp = useCreateFlowMdUp();
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
const { state, updateState, replaceState, markCreateFlowInteraction } =
useCreateFlow();
@@ -120,17 +119,9 @@ export function CommunicationMethodsScreen() {
setAddCustomWizardOpen(true);
}, [markCreateFlowInteraction]);
const title = expanded ? comm.page.expandedTitle : comm.page.compactTitle;
const title = comm.page.compactTitle;
const description = expanded ? (
<>
{comm.page.expandedDescriptionBefore}
<InlineTextButton onClick={handleOpenAddWizard}>
{comm.page.compactDescriptionLinkLabel}
</InlineTextButton>
{comm.page.expandedDescriptionAfter}
</>
) : (
const description = (
<>
{comm.page.compactDescriptionBefore}
<InlineTextButton onClick={handleOpenAddWizard}>
@@ -694,30 +685,41 @@ export function CommunicationMethodsScreen() {
return (
<>
<CreateFlowCardStackStepShell
lockupTitle={title}
lockupDescription={description}
stackBusy={!recommendationsReady}
>
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={comm.page.seeAllLink}
compactRecommendedLimit={5}
compactCardIds={compactCardIds}
compactDesktopLayout="flexWrap"
headerLockupSize={mdUp ? "L" : "M"}
<CreateFlowTwoColumnSelectShell
contentTopBelowMd="space-800"
lgVerticalAlign="start"
header={
<CreateFlowHeaderLockup
title={title}
description={description}
justification="left"
/>
)}
</CreateFlowCardStackStepShell>
}
>
<div
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0 lg:h-full lg:min-h-0 lg:flex-1"
aria-busy={!recommendationsReady}
>
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={comm.page.seeAllLink}
compactRecommendedLimit={5}
compactCardIds={compactCardIds}
layout="singleStack"
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
/>
)}
</div>
</CreateFlowTwoColumnSelectShell>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
@@ -4,6 +4,9 @@
* `conflict-management` step — Figma compact card stack (node `20879-15979`).
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["conflict-management"]`.
*
* Page chrome is the shared question-and-options shell
* (`CreateFlowTwoColumnSelectShell`); options are CardStack `singleStack`.
*
* Card click opens the Figma "Add Approach" create modal (node `20874-172292`)
* with four controls rendered by {@link ConflictManagementEditFields}: Core
* Principle, Applicable Scope (text area), Process Protocol, and Restoration
@@ -15,11 +18,11 @@
import { useState, useCallback, useMemo, useRef } from "react";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
import { CreateFlowCardStackStepShell } from "../../components/CreateFlowCardStackStepShell";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
@@ -58,7 +61,6 @@ export function ConflictManagementScreen() {
const m = useMessages();
const cm = m.create.customRule.conflictManagement;
const modalKebabMenu = m.create.customRule.modalKebabMenu;
const mdUp = useCreateFlowMdUp();
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
const { state, updateState, replaceState, markCreateFlowInteraction } =
useCreateFlow();
@@ -117,17 +119,9 @@ export function ConflictManagementScreen() {
setAddCustomWizardOpen(true);
}, [markCreateFlowInteraction]);
const title = expanded ? cm.page.expandedTitle : cm.page.compactTitle;
const title = cm.page.compactTitle;
const description = expanded ? (
<>
{cm.page.expandedDescriptionBefore}
<InlineTextButton onClick={handleOpenAddWizard}>
{cm.page.compactDescriptionLinkLabel}
</InlineTextButton>
{cm.page.expandedDescriptionAfter}
</>
) : (
const description = (
<>
{cm.page.compactDescriptionBefore}
<InlineTextButton onClick={handleOpenAddWizard}>
@@ -695,30 +689,41 @@ export function ConflictManagementScreen() {
return (
<>
<CreateFlowCardStackStepShell
lockupTitle={title}
lockupDescription={description}
stackBusy={!recommendationsReady}
>
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={cm.page.seeAllLink}
compactRecommendedLimit={5}
compactCardIds={compactCardIds}
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
<CreateFlowTwoColumnSelectShell
contentTopBelowMd="space-800"
lgVerticalAlign="start"
header={
<CreateFlowHeaderLockup
title={title}
description={description}
justification="left"
/>
)}
</CreateFlowCardStackStepShell>
}
>
<div
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0 lg:h-full lg:min-h-0 lg:flex-1"
aria-busy={!recommendationsReady}
>
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={cm.page.seeAllLink}
compactRecommendedLimit={5}
compactCardIds={compactCardIds}
layout="singleStack"
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
/>
)}
</div>
</CreateFlowTwoColumnSelectShell>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
@@ -4,6 +4,9 @@
* `membership-methods` step — Figma compact card stack (node `20858-13947`).
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["membership-methods"]`.
*
* Page chrome is the shared question-and-options shell
* (`CreateFlowTwoColumnSelectShell`); options are CardStack `singleStack`.
*
* Card click opens the Figma create modal (node `20858-13948`) with three
* editable sections rendered by {@link MembershipMethodEditFields}. The same
* field set is reused on `/create/final-review` — see `FinalReviewChipEditModal`.
@@ -16,11 +19,11 @@
import { useState, useCallback, useMemo, useRef } from "react";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
import { CreateFlowCardStackStepShell } from "../../components/CreateFlowCardStackStepShell";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
@@ -59,7 +62,6 @@ export function MembershipMethodsScreen() {
const m = useMessages();
const mem = m.create.customRule.membership;
const modalKebabMenu = m.create.customRule.modalKebabMenu;
const mdUp = useCreateFlowMdUp();
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
const { state, updateState, replaceState, markCreateFlowInteraction } =
useCreateFlow();
@@ -118,17 +120,9 @@ export function MembershipMethodsScreen() {
setAddCustomWizardOpen(true);
}, [markCreateFlowInteraction]);
const title = expanded ? mem.page.expandedTitle : mem.page.compactTitle;
const title = mem.page.compactTitle;
const description = expanded ? (
<>
{mem.page.expandedDescriptionBefore}
<InlineTextButton onClick={handleOpenAddWizard}>
{mem.page.compactDescriptionLinkLabel}
</InlineTextButton>
{mem.page.expandedDescriptionAfter}
</>
) : (
const description = (
<>
{mem.page.compactDescriptionBefore}
<InlineTextButton onClick={handleOpenAddWizard}>
@@ -688,30 +682,41 @@ export function MembershipMethodsScreen() {
return (
<>
<CreateFlowCardStackStepShell
lockupTitle={title}
lockupDescription={description}
stackBusy={!recommendationsReady}
>
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={mem.page.seeAllLink}
compactRecommendedLimit={5}
compactCardIds={compactCardIds}
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
<CreateFlowTwoColumnSelectShell
contentTopBelowMd="space-800"
lgVerticalAlign="start"
header={
<CreateFlowHeaderLockup
title={title}
description={description}
justification="left"
/>
)}
</CreateFlowCardStackStepShell>
}
>
<div
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0 lg:h-full lg:min-h-0 lg:flex-1"
aria-busy={!recommendationsReady}
>
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={mem.page.seeAllLink}
compactRecommendedLimit={5}
compactCardIds={compactCardIds}
layout="singleStack"
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
/>
)}
</div>
</CreateFlowTwoColumnSelectShell>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
@@ -766,7 +766,7 @@ export function DecisionApproachesScreen() {
}
>
<div
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0"
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0 lg:h-full lg:min-h-0 lg:flex-1"
aria-busy={!recommendationsReady}
>
{recommendationsReady && (
@@ -796,7 +796,7 @@ export function DecisionApproachesScreen() {
layout="singleStack"
compactRecommendedLimit={5}
compactCardIds={compactCardIds}
className="w-full"
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
headerLockupSize={mdUp ? "L" : "M"}
/>
)}
@@ -150,7 +150,7 @@ export function createFlowStepUsesCenteredTextLayout(
return CREATE_FLOW_SCREEN_REGISTRY[step].centeredBodyBelowMd;
}
/** Steps whose main area uses the CardStack-style layout (`layoutKind: "card"`). */
/** Steps whose Figma frame is a compact card stack (`layoutKind: "card"`). */
export function createFlowStepUsesCardLayout(
step: CreateFlowStep | null,
): boolean {
+3
View File
@@ -201,7 +201,10 @@ export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] =
"community-size",
"community-structure",
"core-values",
"communication-methods",
"membership-methods",
"decision-approaches",
"conflict-management",
"confirm-stakeholders",
] as const;
@@ -9,10 +9,11 @@ const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
/**
* Figma: "Utility / CardStack"; canonical code under `cards/`.
* Selectable stack of cards with
* Selectable stack of {@link Selection} options (`size` `m` tiles or `l` rows) with
* an optional "see all"/"show less" expand toggle.
* Expanded default layout matches Flow — Expanded Card Stack (`20768:14820`):
* 281×142 CardSelection tiles in two columns.
* Create-flow method steps use `layout="singleStack"` (size `l`) in the shared
* question-and-options shell. Expanded default layout still matches Flow —
* Expanded Card Stack (`20768:14820`): 281×142 size-`m` tiles in two columns.
*/
const CardStackContainer = memo<CardStackProps>(
({
@@ -43,7 +43,10 @@ export interface CardStackProps {
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). */
/**
* Alignment of the expand/collapse control in `singleStack` layout.
* Figma Flow — Compact Card Stack, Expanded Card Stack, and Right Rail center the control.
*/
toggleAlignment?: "center" | "end";
className?: string;
/** Optional “Add” entry (e.g. custom method card wizard). */
@@ -126,11 +126,15 @@ export function CardStackView({
.slice(0, compactRecommendedLimit);
})();
// Single stack: always one column; expand reveals more in same stack (scrollable)
// 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;
return (
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
<div
className={`flex w-full min-w-0 flex-col gap-6 lg:h-full lg:max-h-full lg:min-h-0 lg:flex-1 lg:overflow-hidden ${className}`}
>
<CardStackHeaderLockup
title={title}
description={description}
@@ -138,7 +142,7 @@ export function CardStackView({
size={lockupSize}
wrapperClassName="min-w-0 shrink-0"
/>
<div className="flex w-full min-w-0 flex-col gap-2">
<div className="flex w-full min-w-0 flex-col gap-2 lg:min-h-0 lg:flex-1 lg:overflow-y-auto scrollbar-design">
{displayedCards.map((item) => (
<Selection
key={item.id}
@@ -147,7 +151,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="vertical"
size="l"
showInfoIcon={false}
onClick={() => onCardSelect(item.id)}
/>
@@ -158,7 +162,7 @@ export function CardStackView({
<button
type="button"
onClick={onToggleExpand}
className={`text-medium-paragraph text-[var(--color-gray-000)] underline hover:opacity-90 focus:outline-none cursor-pointer ${
className={`shrink-0 text-medium-paragraph text-[var(--color-gray-000)] underline hover:opacity-90 focus:outline-none cursor-pointer ${
toggleAlignment === "end" ? "self-end" : "self-center"
}`}
>
@@ -189,7 +193,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className="min-h-[142px]"
onClick={() => onCardSelect(item.id)}
@@ -206,7 +210,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(item.id)}
@@ -230,7 +234,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className="min-h-[142px]"
onClick={() => onCardSelect(item.id)}
@@ -254,7 +258,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(item.id)}
@@ -273,7 +277,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(item.id)}
@@ -292,7 +296,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(item.id)}
@@ -308,7 +312,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(item.id)}
@@ -323,7 +327,7 @@ export function CardStackView({
supportText={compactCards[4].supportText}
recommended={compactCards[4].recommended ?? false}
selected={isSelected(compactCards[4].id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(compactCards[4].id)}
@@ -349,7 +353,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className="min-h-[142px]"
onClick={() => onCardSelect(item.id)}
@@ -370,7 +374,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(item.id)}
@@ -384,7 +388,7 @@ export function CardStackView({
supportText={compactCards[2].supportText}
recommended={compactCards[2].recommended ?? false}
selected={isSelected(compactCards[2].id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(compactCards[2].id)}
@@ -403,7 +407,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className={FIGMA_CARD_SELECTION_TILE_CLASS}
onClick={() => onCardSelect(item.id)}
@@ -429,7 +433,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
className="h-[142px] min-h-[142px] max-h-[142px] w-full max-w-[281px]"
onClick={() => onCardSelect(item.id)}
@@ -456,7 +460,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="vertical"
size="l"
showInfoIcon={false}
onClick={() => onCardSelect(item.id)}
/>
@@ -482,7 +486,7 @@ export function CardStackView({
supportText={item.supportText}
recommended={item.recommended ?? false}
selected={isSelected(item.id)}
orientation="horizontal"
size="m"
showInfoIcon={false}
onClick={() => onCardSelect(item.id)}
/>
@@ -2,11 +2,22 @@
import { memo } from "react";
import { SelectionView } from "./Selection.view";
import type { SelectionProps } from "./Selection.types";
import type { SelectionProps, SelectionSizeValue } from "./Selection.types";
function resolveSelectionSize(
size: SelectionProps["size"],
orientation: SelectionProps["orientation"],
): SelectionSizeValue {
if (size) return size;
if (orientation === "vertical") return "l";
return "m";
}
/**
* Figma: "Card / CardSelection" — stacked tile e.g. `16775:28762` (recommended + label + supportText).
* `orientation="horizontal"` selects that vertical stack; `"vertical"` is label + tag on the right (CardStack `singleStack` / right-rail). Helper `?` stays off until tooltip behavior ships.
* Size `s` is Control / Chip; `m` is the compact tile (`orientation="horizontal"`); `l` is the
* full-width row (`orientation="vertical"`, CardStack `singleStack` / right-rail). Helper `?`
* stays off until tooltip behavior ships.
*/
const SelectionContainer = memo<SelectionProps>(
({
@@ -14,12 +25,15 @@ const SelectionContainer = memo<SelectionProps>(
supportText = "",
recommended = false,
selected = false,
orientation = "horizontal",
size: sizeProp,
orientation,
showInfoIcon = false,
id,
className = "",
onClick,
}) => {
const size = resolveSelectionSize(sizeProp, orientation);
const handleClick = () => {
if (onClick) onClick();
};
@@ -37,7 +51,7 @@ const SelectionContainer = memo<SelectionProps>(
supportText={supportText}
recommended={recommended}
selected={selected}
orientation={orientation}
size={size}
showInfoIcon={showInfoIcon}
id={id}
className={className}
@@ -1,9 +1,28 @@
import type {
SelectionOrientationValue,
SelectionSizeValue,
} from "../../../../lib/propNormalization";
export type { SelectionOrientationValue, SelectionSizeValue };
export interface SelectionProps {
label: string;
supportText?: string;
recommended?: boolean;
selected?: boolean;
orientation: "horizontal" | "vertical";
/**
* Size of the selectable option:
* - `s` — chip (label only)
* - `m` — compact tile (label + support text stacked)
* - `l` — full-width row (label + support text, tag at the end)
*
* When omitted, `orientation` maps `horizontal` → `m` and `vertical` → `l`.
*/
size?: SelectionSizeValue;
/**
* Legacy layout flag. Prefer `size`. `horizontal` is size `m`; `vertical` is size `l`.
*/
orientation?: SelectionOrientationValue;
/** Off in product until tooltip behavior ships. */
showInfoIcon?: boolean;
/** Optional id for the root (e.g. `data-card-id` for focus after modal close). */
@@ -17,7 +36,7 @@ export interface SelectionViewProps {
supportText: string;
recommended: boolean;
selected: boolean;
orientation: "horizontal" | "vertical";
size: SelectionSizeValue;
showInfoIcon: boolean;
id: string | undefined;
className: string;
@@ -1,5 +1,6 @@
"use client";
import Chip from "../../controls/Chip";
import Tag from "../../utility/Tag";
import type { SelectionViewProps } from "./Selection.types";
@@ -26,34 +27,54 @@ function SelectionTag({
return null;
}
function optionAriaLabel(label: string, supportText: string): string {
return supportText ? `${label}: ${supportText}` : label;
}
export function SelectionView({
label,
supportText,
recommended,
selected,
orientation,
size,
showInfoIcon,
id: selectionId,
className,
onClick,
onKeyDown,
}: SelectionViewProps) {
const ariaLabel = optionAriaLabel(label, supportText);
if (size === "s") {
return (
<Chip
label={label}
state={selected ? "selected" : "unselected"}
size="m"
ariaLabel={ariaLabel}
className={className}
onClick={() => onClick()}
/>
);
}
const borderClass = "border border-[var(--color-border-default-primary)]";
const selectedBorder = selected
? "outline outline-4 outline-dashed outline-black outline-offset-[-4px]"
: "";
const cardIdProp = selectionId ? { "data-card-id": selectionId } : {};
// Figma: "Card / CardSelection" vertical stack — node `16775:28762` (dev).
// Prop `orientation="horizontal"` is this stacked layout (historical naming).
if (orientation === "horizontal") {
if (size === "m") {
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 } : {})}
{...cardIdProp}
role="button"
tabIndex={0}
aria-label={supportText ? `${label}: ${supportText}` : label}
aria-label={ariaLabel}
aria-pressed={selected}
data-selection-size="m"
className={`${baseClasses} flex min-h-0 w-full flex-col items-start justify-center gap-1`}
onClick={onClick}
onKeyDown={onKeyDown}
@@ -75,10 +96,12 @@ export function SelectionView({
return (
<div
{...(selectionId ? { "data-card-id": selectionId } : {})}
{...cardIdProp}
role="button"
tabIndex={0}
aria-label={supportText ? `${label}: ${supportText}` : label}
aria-label={ariaLabel}
aria-pressed={selected}
data-selection-size="l"
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
onClick={onClick}
onKeyDown={onKeyDown}