Standardize create-flow option steps on one question-and-options layout and Selection sizes. #86
No files matched your search
@@ -15,7 +15,9 @@ alwaysApply: false
|
|||||||
- Step id ↔ layout kind mapping is declared in
|
- Step id ↔ layout kind mapping is declared in
|
||||||
`app/(app)/create/utils/createFlowScreenRegistry.ts`. Never branch on layout kind
|
`app/(app)/create/utils/createFlowScreenRegistry.ts`. Never branch on layout kind
|
||||||
inside a screen — pick the matching shell (`CreateFlowStepShell` /
|
inside a screen — pick the matching shell (`CreateFlowStepShell` /
|
||||||
`CreateFlowTwoColumnSelectShell`).
|
`CreateFlowTwoColumnSelectShell`). Question-and-options steps (select, card,
|
||||||
|
right-rail) all use `CreateFlowTwoColumnSelectShell` plus `Selection` size
|
||||||
|
variants (`s` chip / `m` tile / `l` row).
|
||||||
- Keep create-flow step routing centralized in
|
- Keep create-flow step routing centralized in
|
||||||
`app/(app)/create/utils/createFlowPaths.ts` (`createFlowStepPath`,
|
`app/(app)/create/utils/createFlowPaths.ts` (`createFlowStepPath`,
|
||||||
`CREATE_ROUTES`) — do not introduce new hardcoded `/create/...` literals.
|
`CREATE_ROUTES`) — do not introduce new hardcoded `/create/...` literals.
|
||||||
|
|||||||
@@ -41,10 +41,7 @@ import {
|
|||||||
createFlowStepPathWithSyncDraft,
|
createFlowStepPathWithSyncDraft,
|
||||||
} from "./utils/createFlowPaths";
|
} from "./utils/createFlowPaths";
|
||||||
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
|
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
|
||||||
import {
|
import { createFlowStepUsesCenteredTextLayout } from "./utils/createFlowScreenRegistry";
|
||||||
createFlowStepUsesCenteredTextLayout,
|
|
||||||
createFlowStepUsesCardLayout,
|
|
||||||
} from "./utils/createFlowScreenRegistry";
|
|
||||||
import Button from "../../components/buttons/Button";
|
import Button from "../../components/buttons/Button";
|
||||||
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
|
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
|
||||||
import { buildCreateFlowDraftPayload } from "../../../lib/create/buildCreateFlowDraftPayload";
|
import { buildCreateFlowDraftPayload } from "../../../lib/create/buildCreateFlowDraftPayload";
|
||||||
@@ -557,20 +554,19 @@ function CreateFlowLayoutContent({
|
|||||||
const isFinalReviewLike =
|
const isFinalReviewLike =
|
||||||
currentStep === "final-review" || currentStep === "edit-rule";
|
currentStep === "final-review" || currentStep === "edit-rule";
|
||||||
const isEditRuleStep = currentStep === "edit-rule";
|
const isEditRuleStep = currentStep === "edit-rule";
|
||||||
const isCardLayoutStep = createFlowStepUsesCardLayout(currentStep);
|
/** Two-column question-and-options: below `lg` main scrolls; at `lg+` only the right column scrolls. */
|
||||||
/** Two-column select / right-rail: below `lg` main scrolls; at `lg+` only the right column scrolls. */
|
|
||||||
const isSelectSplitScrollStep = createFlowStepUsesSelectSplitScroll(
|
const isSelectSplitScrollStep = createFlowStepUsesSelectSplitScroll(
|
||||||
currentStep,
|
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
|
const mainContentClass = isCompletedStep
|
||||||
? "items-stretch overflow-y-auto md:overflow-hidden"
|
? "items-stretch overflow-y-auto md:overflow-hidden"
|
||||||
: isSelectSplitScrollStep
|
: isSelectSplitScrollStep
|
||||||
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
|
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
|
||||||
: isCreateTemplatesPickerRoute
|
: isCreateTemplatesPickerRoute
|
||||||
? "items-start justify-start overflow-y-auto"
|
? "items-start justify-start overflow-y-auto"
|
||||||
: isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep
|
: isTemplateReviewRoute || isFinalReviewLike
|
||||||
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
|
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
|
||||||
: "items-start justify-center overflow-y-auto md:items-center";
|
: "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";
|
} from "./CreateFlowStepShell";
|
||||||
import {
|
import {
|
||||||
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||||
|
CREATE_FLOW_SCROLL_END_PAD_CLASS,
|
||||||
CREATE_FLOW_SCROLL_REGION_CLASS,
|
CREATE_FLOW_SCROLL_REGION_CLASS,
|
||||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
} from "./createFlowLayoutTokens";
|
} from "./createFlowLayoutTokens";
|
||||||
@@ -23,15 +24,17 @@ interface CreateFlowTwoColumnSelectShellProps {
|
|||||||
contentTopBelowMd?: CreateFlowContentTopBelowMd;
|
contentTopBelowMd?: CreateFlowContentTopBelowMd;
|
||||||
/**
|
/**
|
||||||
* At `lg+`, layout variant: `"center"` = vertically centered pair (community size/structure).
|
* 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`
|
* `"start"` = full-height split with a scrollable right column (core values, method cards,
|
||||||
* so the right column gets a bounded height; `items-start` would grow with content and break scroll.
|
* 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;
|
lgVerticalAlign?: CreateFlowSelectShellLgVerticalAlign;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Question-and-options layout for create-flow select steps (community size/structure,
|
* 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.
|
* Below `lg` (1024px), one column uses available width and main scrolls.
|
||||||
* At `lg+`, static header column + scrollable options column (`min-h-0` + overflow).
|
* At `lg+`, static header column + scrollable options column (`min-h-0` + overflow).
|
||||||
*/
|
*/
|
||||||
@@ -45,9 +48,6 @@ export function CreateFlowTwoColumnSelectShell({
|
|||||||
const rowLgCrossAlignClass =
|
const rowLgCrossAlignClass =
|
||||||
lgVerticalAlign === "start" ? "lg:items-stretch" : "lg:items-center";
|
lgVerticalAlign === "start" ? "lg:items-stretch" : "lg:items-center";
|
||||||
|
|
||||||
const leftLgMainJustifyClass =
|
|
||||||
lgVerticalAlign === "start" ? "lg:justify-start" : "lg:justify-center";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell
|
<CreateFlowStepShell
|
||||||
variant="centeredNarrow"
|
variant="centeredNarrow"
|
||||||
@@ -68,7 +68,7 @@ export function CreateFlowTwoColumnSelectShell({
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`flex w-full min-w-0 shrink-0 flex-col items-start gap-[var(--measures-spacing-200,8px)] ` +
|
`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}
|
{header}
|
||||||
@@ -76,7 +76,7 @@ export function CreateFlowTwoColumnSelectShell({
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`relative flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-800,32px)] ` +
|
`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}`
|
`${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 =
|
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";
|
"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;
|
* Single column: full width below `lg` so tablet widths use available space;
|
||||||
* 640px from `--breakpoint-lg` up (two-column layouts start at `lg`).
|
* 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 =
|
export const CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS =
|
||||||
"w-full min-w-0 lg:max-w-[1328px]";
|
"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]";
|
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";
|
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
|
* Inner max-width for step content and the action bar, so the primary button
|
||||||
* lines up with the nearest content column above 1440.
|
* 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) {
|
switch (CREATE_FLOW_SCREEN_REGISTRY[options.step].layoutKind) {
|
||||||
case "select":
|
case "select":
|
||||||
case "right-rail":
|
case "right-rail":
|
||||||
|
case "card":
|
||||||
case "completed":
|
case "completed":
|
||||||
case "review":
|
case "review":
|
||||||
return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS;
|
return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS;
|
||||||
case "card":
|
|
||||||
return CREATE_FLOW_WIDE_MAX_CLASS;
|
|
||||||
default:
|
default:
|
||||||
return CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS;
|
return CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* `communication-methods` step — Figma “Flow — Compact Card Stack” (node `20246-15828`).
|
* `communication-methods` step — Figma “Flow — Compact Card Stack” (node `20246-15828`).
|
||||||
* Registry: `layoutKind: "card"` (`CREATE_FLOW_SCREEN_REGISTRY["communication-methods"]`).
|
* Registry: `layoutKind: "card"` (`CREATE_FLOW_SCREEN_REGISTRY["communication-methods"]`).
|
||||||
*
|
*
|
||||||
* Lives under `screens/card/` (not `select/`): Figma **card stack** layout is a distinct shell from
|
* Page chrome is the shared question-and-options shell
|
||||||
* two-column chip **select** frames. Future card-stack steps get their own `*Screen.tsx` here and
|
* ({@link CreateFlowTwoColumnSelectShell}); options are {@link CardStack} with
|
||||||
* reuse `CardStack` / `CreateFlowCardStackStepShell` as needed.
|
* `layout="singleStack"` and {@link Selection} size `l`.
|
||||||
*
|
*
|
||||||
* Card click opens the Figma create modal (node `20246-15829`) with three
|
* Card click opens the Figma create modal (node `20246-15829`) with three
|
||||||
* editable sections rendered by {@link CommunicationMethodEditFields}. The
|
* editable sections rendered by {@link CommunicationMethodEditFields}. The
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useState, useCallback, useMemo, useRef } from "react";
|
||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
|
||||||
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
||||||
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
||||||
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
|
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 CardStack from "../../../../components/cards/CardStack";
|
||||||
import Create from "../../../../components/modals/Create";
|
import Create from "../../../../components/modals/Create";
|
||||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
@@ -61,7 +61,6 @@ export function CommunicationMethodsScreen() {
|
|||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const comm = m.create.customRule.communication;
|
const comm = m.create.customRule.communication;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
|
||||||
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
@@ -120,17 +119,9 @@ export function CommunicationMethodsScreen() {
|
|||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
const title = expanded ? comm.page.expandedTitle : comm.page.compactTitle;
|
const title = comm.page.compactTitle;
|
||||||
|
|
||||||
const description = expanded ? (
|
const description = (
|
||||||
<>
|
|
||||||
{comm.page.expandedDescriptionBefore}
|
|
||||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
|
||||||
{comm.page.compactDescriptionLinkLabel}
|
|
||||||
</InlineTextButton>
|
|
||||||
{comm.page.expandedDescriptionAfter}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
{comm.page.compactDescriptionBefore}
|
{comm.page.compactDescriptionBefore}
|
||||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||||
@@ -694,30 +685,41 @@ export function CommunicationMethodsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateFlowCardStackStepShell
|
<CreateFlowTwoColumnSelectShell
|
||||||
lockupTitle={title}
|
contentTopBelowMd="space-800"
|
||||||
lockupDescription={description}
|
lgVerticalAlign="start"
|
||||||
stackBusy={!recommendationsReady}
|
header={
|
||||||
>
|
<CreateFlowHeaderLockup
|
||||||
{recommendationsReady && (
|
title={title}
|
||||||
<CardStack
|
description={description}
|
||||||
cards={sampleCards}
|
justification="left"
|
||||||
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"}
|
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
</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
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* `conflict-management` step — Figma compact card stack (node `20879-15979`).
|
* `conflict-management` step — Figma compact card stack (node `20879-15979`).
|
||||||
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["conflict-management"]`.
|
* 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`)
|
* Card click opens the Figma "Add Approach" create modal (node `20874-172292`)
|
||||||
* with four controls rendered by {@link ConflictManagementEditFields}: Core
|
* with four controls rendered by {@link ConflictManagementEditFields}: Core
|
||||||
* Principle, Applicable Scope (text area), Process Protocol, and Restoration
|
* Principle, Applicable Scope (text area), Process Protocol, and Restoration
|
||||||
@@ -15,11 +18,11 @@
|
|||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useState, useCallback, useMemo, useRef } from "react";
|
||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
|
||||||
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
||||||
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
||||||
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
|
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 CardStack from "../../../../components/cards/CardStack";
|
||||||
import Create from "../../../../components/modals/Create";
|
import Create from "../../../../components/modals/Create";
|
||||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
@@ -58,7 +61,6 @@ export function ConflictManagementScreen() {
|
|||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const cm = m.create.customRule.conflictManagement;
|
const cm = m.create.customRule.conflictManagement;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
|
||||||
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
@@ -117,17 +119,9 @@ export function ConflictManagementScreen() {
|
|||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
const title = expanded ? cm.page.expandedTitle : cm.page.compactTitle;
|
const title = cm.page.compactTitle;
|
||||||
|
|
||||||
const description = expanded ? (
|
const description = (
|
||||||
<>
|
|
||||||
{cm.page.expandedDescriptionBefore}
|
|
||||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
|
||||||
{cm.page.compactDescriptionLinkLabel}
|
|
||||||
</InlineTextButton>
|
|
||||||
{cm.page.expandedDescriptionAfter}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
{cm.page.compactDescriptionBefore}
|
{cm.page.compactDescriptionBefore}
|
||||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||||
@@ -695,30 +689,41 @@ export function ConflictManagementScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateFlowCardStackStepShell
|
<CreateFlowTwoColumnSelectShell
|
||||||
lockupTitle={title}
|
contentTopBelowMd="space-800"
|
||||||
lockupDescription={description}
|
lgVerticalAlign="start"
|
||||||
stackBusy={!recommendationsReady}
|
header={
|
||||||
>
|
<CreateFlowHeaderLockup
|
||||||
{recommendationsReady && (
|
title={title}
|
||||||
<CardStack
|
description={description}
|
||||||
cards={sampleCards}
|
justification="left"
|
||||||
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"}
|
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
</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
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* `membership-methods` step — Figma compact card stack (node `20858-13947`).
|
* `membership-methods` step — Figma compact card stack (node `20858-13947`).
|
||||||
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["membership-methods"]`.
|
* 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
|
* Card click opens the Figma create modal (node `20858-13948`) with three
|
||||||
* editable sections rendered by {@link MembershipMethodEditFields}. The same
|
* editable sections rendered by {@link MembershipMethodEditFields}. The same
|
||||||
* field set is reused on `/create/final-review` — see `FinalReviewChipEditModal`.
|
* field set is reused on `/create/final-review` — see `FinalReviewChipEditModal`.
|
||||||
@@ -16,11 +19,11 @@
|
|||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useState, useCallback, useMemo, useRef } from "react";
|
||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
|
||||||
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
||||||
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
||||||
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
|
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 CardStack from "../../../../components/cards/CardStack";
|
||||||
import Create from "../../../../components/modals/Create";
|
import Create from "../../../../components/modals/Create";
|
||||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
@@ -59,7 +62,6 @@ export function MembershipMethodsScreen() {
|
|||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const mem = m.create.customRule.membership;
|
const mem = m.create.customRule.membership;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
|
||||||
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
@@ -118,17 +120,9 @@ export function MembershipMethodsScreen() {
|
|||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
const title = expanded ? mem.page.expandedTitle : mem.page.compactTitle;
|
const title = mem.page.compactTitle;
|
||||||
|
|
||||||
const description = expanded ? (
|
const description = (
|
||||||
<>
|
|
||||||
{mem.page.expandedDescriptionBefore}
|
|
||||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
|
||||||
{mem.page.compactDescriptionLinkLabel}
|
|
||||||
</InlineTextButton>
|
|
||||||
{mem.page.expandedDescriptionAfter}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
{mem.page.compactDescriptionBefore}
|
{mem.page.compactDescriptionBefore}
|
||||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||||
@@ -688,30 +682,41 @@ export function MembershipMethodsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateFlowCardStackStepShell
|
<CreateFlowTwoColumnSelectShell
|
||||||
lockupTitle={title}
|
contentTopBelowMd="space-800"
|
||||||
lockupDescription={description}
|
lgVerticalAlign="start"
|
||||||
stackBusy={!recommendationsReady}
|
header={
|
||||||
>
|
<CreateFlowHeaderLockup
|
||||||
{recommendationsReady && (
|
title={title}
|
||||||
<CardStack
|
description={description}
|
||||||
cards={sampleCards}
|
justification="left"
|
||||||
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"}
|
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
</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
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
|
|||||||
@@ -766,7 +766,7 @@ export function DecisionApproachesScreen() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<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}
|
aria-busy={!recommendationsReady}
|
||||||
>
|
>
|
||||||
{recommendationsReady && (
|
{recommendationsReady && (
|
||||||
@@ -796,7 +796,7 @@ export function DecisionApproachesScreen() {
|
|||||||
layout="singleStack"
|
layout="singleStack"
|
||||||
compactRecommendedLimit={5}
|
compactRecommendedLimit={5}
|
||||||
compactCardIds={compactCardIds}
|
compactCardIds={compactCardIds}
|
||||||
className="w-full"
|
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export function createFlowStepUsesCenteredTextLayout(
|
|||||||
return CREATE_FLOW_SCREEN_REGISTRY[step].centeredBodyBelowMd;
|
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(
|
export function createFlowStepUsesCardLayout(
|
||||||
step: CreateFlowStep | null,
|
step: CreateFlowStep | null,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
|||||||
@@ -201,7 +201,10 @@ export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] =
|
|||||||
"community-size",
|
"community-size",
|
||||||
"community-structure",
|
"community-structure",
|
||||||
"core-values",
|
"core-values",
|
||||||
|
"communication-methods",
|
||||||
|
"membership-methods",
|
||||||
"decision-approaches",
|
"decision-approaches",
|
||||||
|
"conflict-management",
|
||||||
"confirm-stakeholders",
|
"confirm-stakeholders",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Utility / CardStack"; canonical code under `cards/`.
|
* 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.
|
* an optional "see all"/"show less" expand toggle.
|
||||||
* Expanded default layout matches Flow — Expanded Card Stack (`20768:14820`):
|
* Create-flow method steps use `layout="singleStack"` (size `l`) in the shared
|
||||||
* 281×142 CardSelection tiles in two columns.
|
* 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>(
|
const CardStackContainer = memo<CardStackProps>(
|
||||||
({
|
({
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ export interface CardStackProps {
|
|||||||
compactDesktopLayout?: "grid" | "flexWrap" | "pyramidFive";
|
compactDesktopLayout?: "grid" | "flexWrap" | "pyramidFive";
|
||||||
/** Optional title/description lockup size (create-flow passes `md`-matched `L`/`M`). Defaults to `L`. */
|
/** Optional title/description lockup size (create-flow passes `md`-matched `L`/`M`). Defaults to `L`. */
|
||||||
headerLockupSize?: HeaderLockupSizeValue;
|
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";
|
toggleAlignment?: "center" | "end";
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Optional “Add” entry (e.g. custom method card wizard). */
|
/** Optional “Add” entry (e.g. custom method card wizard). */
|
||||||
|
|||||||
@@ -126,11 +126,15 @@ export function CardStackView({
|
|||||||
.slice(0, compactRecommendedLimit);
|
.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") {
|
if (layout === "singleStack") {
|
||||||
const displayedCards = expanded ? cards : compactCards;
|
const displayedCards = expanded ? cards : compactCards;
|
||||||
return (
|
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
|
<CardStackHeaderLockup
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
@@ -138,7 +142,7 @@ export function CardStackView({
|
|||||||
size={lockupSize}
|
size={lockupSize}
|
||||||
wrapperClassName="min-w-0 shrink-0"
|
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) => (
|
{displayedCards.map((item) => (
|
||||||
<Selection
|
<Selection
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -147,7 +151,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="vertical"
|
size="l"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
@@ -158,7 +162,7 @@ export function CardStackView({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggleExpand}
|
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"
|
toggleAlignment === "end" ? "self-end" : "self-center"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -189,7 +193,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="min-h-[142px]"
|
className="min-h-[142px]"
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -206,7 +210,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -230,7 +234,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="min-h-[142px]"
|
className="min-h-[142px]"
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -254,7 +258,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -273,7 +277,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -292,7 +296,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -308,7 +312,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -323,7 +327,7 @@ export function CardStackView({
|
|||||||
supportText={compactCards[4].supportText}
|
supportText={compactCards[4].supportText}
|
||||||
recommended={compactCards[4].recommended ?? false}
|
recommended={compactCards[4].recommended ?? false}
|
||||||
selected={isSelected(compactCards[4].id)}
|
selected={isSelected(compactCards[4].id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(compactCards[4].id)}
|
onClick={() => onCardSelect(compactCards[4].id)}
|
||||||
@@ -349,7 +353,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="min-h-[142px]"
|
className="min-h-[142px]"
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -370,7 +374,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -384,7 +388,7 @@ export function CardStackView({
|
|||||||
supportText={compactCards[2].supportText}
|
supportText={compactCards[2].supportText}
|
||||||
recommended={compactCards[2].recommended ?? false}
|
recommended={compactCards[2].recommended ?? false}
|
||||||
selected={isSelected(compactCards[2].id)}
|
selected={isSelected(compactCards[2].id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(compactCards[2].id)}
|
onClick={() => onCardSelect(compactCards[2].id)}
|
||||||
@@ -403,7 +407,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -429,7 +433,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-full max-w-[281px]"
|
className="h-[142px] min-h-[142px] max-h-[142px] w-full max-w-[281px]"
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
@@ -456,7 +460,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="vertical"
|
size="l"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
@@ -482,7 +486,7 @@ export function CardStackView({
|
|||||||
supportText={item.supportText}
|
supportText={item.supportText}
|
||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
size="m"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,11 +2,22 @@
|
|||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { SelectionView } from "./Selection.view";
|
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).
|
* 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>(
|
const SelectionContainer = memo<SelectionProps>(
|
||||||
({
|
({
|
||||||
@@ -14,12 +25,15 @@ const SelectionContainer = memo<SelectionProps>(
|
|||||||
supportText = "",
|
supportText = "",
|
||||||
recommended = false,
|
recommended = false,
|
||||||
selected = false,
|
selected = false,
|
||||||
orientation = "horizontal",
|
size: sizeProp,
|
||||||
|
orientation,
|
||||||
showInfoIcon = false,
|
showInfoIcon = false,
|
||||||
id,
|
id,
|
||||||
className = "",
|
className = "",
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
const size = resolveSelectionSize(sizeProp, orientation);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (onClick) onClick();
|
if (onClick) onClick();
|
||||||
};
|
};
|
||||||
@@ -37,7 +51,7 @@ const SelectionContainer = memo<SelectionProps>(
|
|||||||
supportText={supportText}
|
supportText={supportText}
|
||||||
recommended={recommended}
|
recommended={recommended}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
orientation={orientation}
|
size={size}
|
||||||
showInfoIcon={showInfoIcon}
|
showInfoIcon={showInfoIcon}
|
||||||
id={id}
|
id={id}
|
||||||
className={className}
|
className={className}
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
|
import type {
|
||||||
|
SelectionOrientationValue,
|
||||||
|
SelectionSizeValue,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
|
export type { SelectionOrientationValue, SelectionSizeValue };
|
||||||
|
|
||||||
export interface SelectionProps {
|
export interface SelectionProps {
|
||||||
label: string;
|
label: string;
|
||||||
supportText?: string;
|
supportText?: string;
|
||||||
recommended?: boolean;
|
recommended?: boolean;
|
||||||
selected?: 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. */
|
/** Off in product until tooltip behavior ships. */
|
||||||
showInfoIcon?: boolean;
|
showInfoIcon?: boolean;
|
||||||
/** Optional id for the 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). */
|
||||||
@@ -17,7 +36,7 @@ export interface SelectionViewProps {
|
|||||||
supportText: string;
|
supportText: string;
|
||||||
recommended: boolean;
|
recommended: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
orientation: "horizontal" | "vertical";
|
size: SelectionSizeValue;
|
||||||
showInfoIcon: boolean;
|
showInfoIcon: boolean;
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
className: string;
|
className: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Chip from "../../controls/Chip";
|
||||||
import Tag from "../../utility/Tag";
|
import Tag from "../../utility/Tag";
|
||||||
import type { SelectionViewProps } from "./Selection.types";
|
import type { SelectionViewProps } from "./Selection.types";
|
||||||
|
|
||||||
@@ -26,34 +27,54 @@ function SelectionTag({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionAriaLabel(label: string, supportText: string): string {
|
||||||
|
return supportText ? `${label}: ${supportText}` : label;
|
||||||
|
}
|
||||||
|
|
||||||
export function SelectionView({
|
export function SelectionView({
|
||||||
label,
|
label,
|
||||||
supportText,
|
supportText,
|
||||||
recommended,
|
recommended,
|
||||||
selected,
|
selected,
|
||||||
orientation,
|
size,
|
||||||
showInfoIcon,
|
showInfoIcon,
|
||||||
id: selectionId,
|
id: selectionId,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}: SelectionViewProps) {
|
}: 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 borderClass = "border border-[var(--color-border-default-primary)]";
|
||||||
const selectedBorder = selected
|
const selectedBorder = selected
|
||||||
? "outline outline-4 outline-dashed outline-black outline-offset-[-4px]"
|
? "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).
|
if (size === "m") {
|
||||||
// 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}`;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
{...(selectionId ? { "data-card-id": selectionId } : {})}
|
{...cardIdProp}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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`}
|
className={`${baseClasses} flex min-h-0 w-full flex-col items-start justify-center gap-1`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
@@ -75,10 +96,12 @@ export function SelectionView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...(selectionId ? { "data-card-id": selectionId } : {})}
|
{...cardIdProp}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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`}
|
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ The Figma **Create Community** sequence is the **source of truth** for the first
|
|||||||
| **Create Custom CommunityRule** | Author the CommunityRule content and structure (core values + four card-stack categories). | `core-values` → `communication-methods` → `membership-methods` → `decision-approaches` → `conflict-management` |
|
| **Create Custom CommunityRule** | Author the CommunityRule content and structure (core values + four card-stack categories). | `core-values` → `communication-methods` → `membership-methods` → `decision-approaches` → `conflict-management` |
|
||||||
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders` → `final-review` → `completed` |
|
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders` → `final-review` → `completed` |
|
||||||
|
|
||||||
Treat these stages as the **canonical product sections** when adding chrome (e.g. stage headers, progress copy), breaking work across teams, or reusing flows in other surfaces. **Layout kind** is **not** encoded in the URL; it lives in [`CREATE_FLOW_SCREEN_REGISTRY`](../app/(app)/create/utils/createFlowScreenRegistry.ts) (Figma node id + `layoutKind` per step). Figma defines eight layout kinds: **informational**, **text**, **select**, **upload**, **review**, **card**, **right-rail**, **completed** — `CreateFlowLayoutKind` and [`app/(app)/create/screens/`](../app/(app)/create/screens/) mirror that list (one folder per kind; multiple steps may share a kind, e.g. several **select** screens).
|
Treat these stages as the **canonical product sections** when adding chrome (e.g. stage headers, progress copy), breaking work across teams, or reusing flows in other surfaces. **Layout kind** is **not** encoded in the URL; it lives in [`CREATE_FLOW_SCREEN_REGISTRY`](../app/(app)/create/utils/createFlowScreenRegistry.ts) (Figma node id + `layoutKind` per step). Figma still names eight layout kinds: **informational**, **text**, **select**, **upload**, **review**, **card**, **right-rail**, **completed**. Question-and-options steps (**select**, **card**, **right-rail**) share [`CreateFlowTwoColumnSelectShell`](../app/(app)/create/components/CreateFlowTwoColumnSelectShell.tsx) and [`Selection`](../app/components/cards/Selection/) size variants (`s` chip, `m` tile, `l` row). `app/(app)/create/screens/` still mirrors the Figma kind names.
|
||||||
|
|
||||||
**Create from template (future):** A full **template-driven** create path is **not** finalized; it will likely live on **additional route(s)** (and may reuse these stages where it overlaps the custom trajectory). Today, **`/create/review-template/[slug]`** is only an auxiliary **preview** in the create shell; it is **not** a Figma stage and not the final template-create entry. See **Out of scope** in [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo).
|
**Create from template (future):** A full **template-driven** create path is **not** finalized; it will likely live on **additional route(s)** (and may reuse these stages where it overlaps the custom trajectory). Today, **`/create/review-template/[slug]`** is only an auxiliary **preview** in the create shell; it is **not** a Figma stage and not the final template-create entry. See **Out of scope** in [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo).
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,16 @@ export type ChipPaletteValue = (typeof CHIP_PALETTE_OPTIONS)[number];
|
|||||||
export const CHIP_SIZE_OPTIONS = ["s", "m"] as const;
|
export const CHIP_SIZE_OPTIONS = ["s", "m"] as const;
|
||||||
export type ChipSizeValue = (typeof CHIP_SIZE_OPTIONS)[number];
|
export type ChipSizeValue = (typeof CHIP_SIZE_OPTIONS)[number];
|
||||||
|
|
||||||
|
export const SELECTION_SIZE_OPTIONS = ["s", "m", "l"] as const;
|
||||||
|
export type SelectionSizeValue = (typeof SELECTION_SIZE_OPTIONS)[number];
|
||||||
|
|
||||||
|
export const SELECTION_ORIENTATION_OPTIONS = [
|
||||||
|
"horizontal",
|
||||||
|
"vertical",
|
||||||
|
] as const;
|
||||||
|
export type SelectionOrientationValue =
|
||||||
|
(typeof SELECTION_ORIENTATION_OPTIONS)[number];
|
||||||
|
|
||||||
export const MULTI_SELECT_SIZE_OPTIONS = ["s", "m"] as const;
|
export const MULTI_SELECT_SIZE_OPTIONS = ["s", "m"] as const;
|
||||||
export type MultiSelectSizeValue = (typeof MULTI_SELECT_SIZE_OPTIONS)[number];
|
export type MultiSelectSizeValue = (typeof MULTI_SELECT_SIZE_OPTIONS)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Selection from "../../app/components/cards/Selection";
|
import Selection from "../../app/components/cards/Selection";
|
||||||
|
import { SELECTION_SIZE_OPTIONS } from "../../lib/propNormalization";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Cards/Selection",
|
title: "Components/Cards/Selection",
|
||||||
@@ -8,7 +9,7 @@ export default {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
component:
|
component:
|
||||||
"Create flow selection card with support text, recommended/selected states, and horizontal or vertical orientation. Use for communication approaches and similar choices.",
|
"Selectable option with size variants: `s` (chip), `m` (compact tile), and `l` (full-width row). Used in the create-flow question-and-options layout.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -29,10 +30,15 @@ export default {
|
|||||||
control: { type: "boolean" },
|
control: { type: "boolean" },
|
||||||
description: "Show black SELECTED pill and dotted border",
|
description: "Show black SELECTED pill and dotted border",
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: [...SELECTION_SIZE_OPTIONS],
|
||||||
|
description: "s = chip, m = compact tile, l = full-width row",
|
||||||
|
},
|
||||||
orientation: {
|
orientation: {
|
||||||
control: { type: "select" },
|
control: { type: "select" },
|
||||||
options: ["horizontal", "vertical"],
|
options: ["horizontal", "vertical"],
|
||||||
description: "Layout orientation",
|
description: "Legacy layout flag. Prefer size (`horizontal` → m, `vertical` → l).",
|
||||||
},
|
},
|
||||||
showInfoIcon: {
|
showInfoIcon: {
|
||||||
control: { type: "boolean" },
|
control: { type: "boolean" },
|
||||||
@@ -50,11 +56,21 @@ export const Default = {
|
|||||||
supportText: "Members vote to resolve a dispute democratically.",
|
supportText: "Members vote to resolve a dispute democratically.",
|
||||||
recommended: true,
|
recommended: true,
|
||||||
selected: false,
|
selected: false,
|
||||||
orientation: "horizontal",
|
size: "m",
|
||||||
showInfoIcon: false,
|
showInfoIcon: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SizeChip = {
|
||||||
|
args: {
|
||||||
|
label: "Worker-owned",
|
||||||
|
supportText: "Hidden on the chip; used for the accessible name.",
|
||||||
|
recommended: false,
|
||||||
|
selected: false,
|
||||||
|
size: "s",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const HorizontalRecommended = {
|
export const HorizontalRecommended = {
|
||||||
args: {
|
args: {
|
||||||
label: "Label",
|
label: "Label",
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
"What method should this community use to communicate with eachother?",
|
"How should this community communicate with each-other?",
|
||||||
),
|
),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -126,6 +126,18 @@ describe("CardStack Component", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("single-stack See all / Show less is centered, not end-aligned", () => {
|
||||||
|
render(
|
||||||
|
<CardStack cards={SAMPLE_CARDS} hasMore={true} layout="singleStack" />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("button", {
|
||||||
|
name: "See all communication approaches",
|
||||||
|
});
|
||||||
|
expect(toggle.className).toMatch(/\bself-center\b/);
|
||||||
|
expect(toggle.className).not.toMatch(/\bself-end\b/);
|
||||||
|
});
|
||||||
|
|
||||||
test("calls onCardSelect when a card is clicked", () => {
|
test("calls onCardSelect when a card is clicked", () => {
|
||||||
const onCardSelect = vi.fn();
|
const onCardSelect = vi.fn();
|
||||||
render(<CardStack cards={SAMPLE_CARDS} onCardSelect={onCardSelect} />);
|
render(<CardStack cards={SAMPLE_CARDS} onCardSelect={onCardSelect} />);
|
||||||
|
|||||||
@@ -54,6 +54,33 @@ describe("Selection Component", () => {
|
|||||||
|
|
||||||
const card = screen.getByRole("button");
|
const card = screen.getByRole("button");
|
||||||
expect(card).toHaveClass("flex-row");
|
expect(card).toHaveClass("flex-row");
|
||||||
|
expect(card).toHaveAttribute("data-selection-size", "l");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps size s to a chip with accessible selected state", () => {
|
||||||
|
render(
|
||||||
|
<Selection
|
||||||
|
label="Open membership"
|
||||||
|
supportText="Anyone can join"
|
||||||
|
size="s"
|
||||||
|
selected
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const chip = screen.getByRole("button", {
|
||||||
|
name: "Open membership: Anyone can join",
|
||||||
|
});
|
||||||
|
expect(chip).toHaveAttribute("aria-pressed", "true");
|
||||||
|
expect(chip).toHaveTextContent("Open membership");
|
||||||
|
expect(screen.queryByText("Anyone can join")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses size m for compact tiles and exposes aria-pressed", () => {
|
||||||
|
render(<Selection {...defaultProps} size="m" selected />);
|
||||||
|
|
||||||
|
const card = screen.getByRole("button");
|
||||||
|
expect(card).toHaveAttribute("data-selection-size", "m");
|
||||||
|
expect(card).toHaveAttribute("aria-pressed", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render a helper icon by default", () => {
|
it("does not render a helper icon by default", () => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
|
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
|
||||||
CREATE_FLOW_PAGE_GUTTER_CLASS,
|
CREATE_FLOW_PAGE_GUTTER_CLASS,
|
||||||
CREATE_FLOW_FIXED_TOP_BANNER_OFFSET_CLASS,
|
CREATE_FLOW_FIXED_TOP_BANNER_OFFSET_CLASS,
|
||||||
|
CREATE_FLOW_SCROLL_END_PAD_CLASS,
|
||||||
CREATE_FLOW_VIEWPORT_FRAME_CLASS,
|
CREATE_FLOW_VIEWPORT_FRAME_CLASS,
|
||||||
getCreateFlowContentMaxClass,
|
getCreateFlowContentMaxClass,
|
||||||
} from "../../app/(app)/create/components/createFlowLayoutTokens";
|
} from "../../app/(app)/create/components/createFlowLayoutTokens";
|
||||||
@@ -39,7 +40,11 @@ describe("createFlowLayoutTokens", () => {
|
|||||||
expect(CREATE_FLOW_VIEWPORT_FRAME_CLASS).toContain("max-h-dvh");
|
expect(CREATE_FLOW_VIEWPORT_FRAME_CLASS).toContain("max-h-dvh");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("centers lockup+card and card-stack steps in the nav–footer band from md", () => {
|
it("pads option scrollports past the footer scrim", () => {
|
||||||
|
expect(CREATE_FLOW_SCROLL_END_PAD_CLASS).toBe("pb-12");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("centers review lockup+card steps in the nav–footer band from md", () => {
|
||||||
expect(CREATE_FLOW_MD_CENTERED_MAIN_CLASS).toBe(
|
expect(CREATE_FLOW_MD_CENTERED_MAIN_CLASS).toBe(
|
||||||
"items-start justify-center overflow-y-auto",
|
"items-start justify-center overflow-y-auto",
|
||||||
);
|
);
|
||||||
@@ -55,7 +60,7 @@ describe("createFlowLayoutTokens", () => {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
getCreateFlowContentMaxClass({ step: "communication-methods" }),
|
getCreateFlowContentMaxClass({ step: "communication-methods" }),
|
||||||
).toBe(CREATE_FLOW_WIDE_MAX_CLASS);
|
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
|
||||||
expect(getCreateFlowContentMaxClass({ step: "final-review" })).toBe(
|
expect(getCreateFlowContentMaxClass({ step: "final-review" })).toBe(
|
||||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { CREATE_FLOW_SCREEN_REGISTRY } from "../../app/(app)/create/utils/createFlowScreenRegistry";
|
import {
|
||||||
|
CREATE_FLOW_SCREEN_REGISTRY,
|
||||||
|
createFlowStepUsesCardLayout,
|
||||||
|
} from "../../app/(app)/create/utils/createFlowScreenRegistry";
|
||||||
import { VALID_STEPS } from "../../app/(app)/create/utils/flowSteps";
|
import { VALID_STEPS } from "../../app/(app)/create/utils/flowSteps";
|
||||||
|
|
||||||
describe("create flow registry vs valid steps (CR-92 §3)", () => {
|
describe("create flow registry vs valid steps (CR-92 §3)", () => {
|
||||||
@@ -10,4 +13,11 @@ describe("create flow registry vs valid steps (CR-92 §3)", () => {
|
|||||||
}
|
}
|
||||||
expect(keys.size).toBe(VALID_STEPS.length);
|
expect(keys.size).toBe(VALID_STEPS.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps Figma card-kind ids on the method-card steps", () => {
|
||||||
|
expect(createFlowStepUsesCardLayout("communication-methods")).toBe(true);
|
||||||
|
expect(createFlowStepUsesCardLayout("membership-methods")).toBe(true);
|
||||||
|
expect(createFlowStepUsesCardLayout("conflict-management")).toBe(true);
|
||||||
|
expect(createFlowStepUsesCardLayout("decision-approaches")).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -234,10 +234,19 @@ describe("flowSteps", () => {
|
|||||||
expect(isDirectTemplateReviewEntry(null)).toBe(false);
|
expect(isDirectTemplateReviewEntry(null)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses split-column scroll for select, right-rail, and stakeholders", () => {
|
it("uses split-column scroll for select, method-card, right-rail, and stakeholders", () => {
|
||||||
expect(createFlowStepUsesSelectSplitScroll("community-structure")).toBe(
|
expect(createFlowStepUsesSelectSplitScroll("community-structure")).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
expect(createFlowStepUsesSelectSplitScroll("communication-methods")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(createFlowStepUsesSelectSplitScroll("membership-methods")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(createFlowStepUsesSelectSplitScroll("conflict-management")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(createFlowStepUsesSelectSplitScroll("confirm-stakeholders")).toBe(
|
expect(createFlowStepUsesSelectSplitScroll("confirm-stakeholders")).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user