Unify create-flow gutters, viewport height, and action-bar width so tablet and wide layouts stay aligned with step content. #78

Merged
an.di merged 1 commits from adilallo/fix/CR-182-builder-layout into main 2026-09-10 20:03:44 +00:00
20 changed files with 387 additions and 253 deletions
+14 -4
View File
@@ -76,7 +76,13 @@ import {
CREATE_FLOW_FOOTER_BUTTON_CLASS,
CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS,
} from "./utils/createFlowFooterClassNames";
import { CREATE_FLOW_MD_CENTERED_MAIN_CLASS } from "./components/createFlowLayoutTokens";
import {
CREATE_FLOW_MD_CENTERED_MAIN_CLASS,
CREATE_FLOW_PAGE_GUTTER_CLASS,
CREATE_FLOW_SCROLL_REGION_CLASS,
CREATE_FLOW_VIEWPORT_FRAME_CLASS,
getCreateFlowContentMaxClass,
} from "./components/createFlowLayoutTokens";
import {
CUSTOM_RULE_CONFIRM_FOOTER_STEP_BY_STEP,
methodCardFacetSectionForConfirmStep,
@@ -668,10 +674,10 @@ function CreateFlowLayoutContent({
].filter((b): b is NonNullable<typeof b> => b !== null);
return (
<div className="relative flex h-screen min-h-0 flex-col overflow-hidden bg-black">
<div className={`${CREATE_FLOW_VIEWPORT_FRAME_CLASS} bg-black`}>
{topBanners.length > 0 ? (
<div
className="pointer-events-none fixed left-0 right-0 top-0 z-[200] flex flex-col gap-2 px-[var(--spacing-measures-spacing-500,20px)] pt-[var(--spacing-measures-spacing-300,12px)] md:px-[var(--measures-spacing-1800,64px)]"
className={`pointer-events-none fixed left-0 right-0 top-0 z-[200] flex flex-col gap-2 ${CREATE_FLOW_PAGE_GUTTER_CLASS} pt-[var(--spacing-measures-spacing-300,12px)]`}
aria-live="polite"
>
{topBanners.map((b) => (
@@ -798,13 +804,17 @@ function CreateFlowLayoutContent({
/>
) : null}
<main
className={`flex min-h-0 min-w-0 flex-1 w-full max-w-full overflow-x-hidden ${mainContentClass} ${mainResponsiveLayout}`}
className={`flex min-h-0 min-w-0 flex-1 w-full max-w-full overflow-x-hidden ${CREATE_FLOW_SCROLL_REGION_CLASS} ${mainContentClass} ${mainResponsiveLayout}`}
>
{children}
</main>
{!isCompletedStep && (
<CreateFlowFooter
className="shrink-0"
contentMaxClass={getCreateFlowContentMaxClass({
step: currentStep,
isTemplateReview: isTemplateReviewRoute,
})}
progressBar={
!isTemplateReviewRoute &&
!isFinalReviewLike &&
+2 -1
View File
@@ -11,6 +11,7 @@ import {
import { useCreateFlow } from "./context/CreateFlowContext";
import { fetchDraftFromServer } from "../../../lib/create/api";
import messages from "../../../messages/en/index";
import { CREATE_FLOW_PAGE_GUTTER_CLASS } from "./components/createFlowLayoutTokens";
import Alert from "../../components/modals/Alert";
import {
isValidStep,
@@ -161,7 +162,7 @@ export function SignedInDraftHydration({
if (!loadingHydration) return null;
return (
<div className="pointer-events-none fixed left-0 right-0 top-14 z-[170] flex justify-center px-[var(--spacing-measures-spacing-500,20px)] pt-2 md:top-16 md:px-[var(--measures-spacing-1800,64px)]">
<div className={`pointer-events-none fixed left-0 right-0 top-14 z-[170] flex justify-center ${CREATE_FLOW_PAGE_GUTTER_CLASS} pt-2 md:top-16`}>
<div className="pointer-events-auto w-full max-w-[960px]">
<Alert
type="banner"
@@ -0,0 +1,56 @@
"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>
);
}
@@ -1,6 +1,7 @@
"use client";
import type { ReactNode } from "react";
import { CREATE_FLOW_PAGE_GUTTER_CLASS } from "./createFlowLayoutTokens";
export type CreateFlowStepShellVariant =
| "centeredNarrow"
@@ -13,15 +14,12 @@ export type CreateFlowStepShellVariant =
export type CreateFlowContentTopBelowMd = "none" | "space-1400" | "space-800";
const outerByVariant: Record<CreateFlowStepShellVariant, string> = {
centeredNarrow:
"flex w-full min-w-0 flex-col items-center px-5 md:px-16",
centeredNarrowBottomPad:
"flex w-full min-w-0 flex-col items-center px-5 pb-28 md:px-[var(--measures-spacing-1800,64px)] md:pb-32",
/** Wide two-column steps; 1328px = two 640px columns + 48px gutter. */
wideGrid: "w-full min-w-0 max-w-[1328px] shrink-0 px-5 md:px-12",
/** Create Community review + card grid (Figma Flow — Review `19706:12135`): max width 1440. */
wideGridLoosePadding:
"w-full min-w-0 max-w-[1440px] shrink-0 px-5 md:px-16",
centeredNarrow: `flex w-full min-w-0 flex-col items-center ${CREATE_FLOW_PAGE_GUTTER_CLASS}`,
centeredNarrowBottomPad: `flex w-full min-w-0 flex-col items-center ${CREATE_FLOW_PAGE_GUTTER_CLASS} pb-28 md:pb-32`,
/** Wide two-column steps; inner content supplies the 1328px cap. */
wideGrid: `flex w-full min-w-0 shrink-0 flex-col items-center ${CREATE_FLOW_PAGE_GUTTER_CLASS}`,
/** Create Community review + card grid; inner content supplies the 1440px cap. */
wideGridLoosePadding: `flex w-full min-w-0 shrink-0 flex-col items-center ${CREATE_FLOW_PAGE_GUTTER_CLASS}`,
bare: "w-full min-w-0",
};
@@ -41,7 +39,8 @@ interface CreateFlowStepShellProps {
/**
* Shared horizontal padding and width constraints for create-flow step pages.
* Horizontal padding uses Tailwind `md:` so it tracks `--breakpoint-md` (640px in `app/tailwind.css`).
* Gutters come from {@link CREATE_FLOW_PAGE_GUTTER_CLASS} (`md` / `lg` track
* `--breakpoint-md` and `--breakpoint-lg` in `app/tailwind.css`).
*/
export function CreateFlowStepShell({
children,
@@ -5,7 +5,11 @@ import {
CreateFlowStepShell,
type CreateFlowContentTopBelowMd,
} from "./CreateFlowStepShell";
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "./createFlowLayoutTokens";
import {
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
CREATE_FLOW_SCROLL_REGION_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "./createFlowLayoutTokens";
export type CreateFlowSelectShellLgVerticalAlign = "center" | "start";
@@ -26,10 +30,10 @@ interface CreateFlowTwoColumnSelectShellProps {
}
/**
* Two-column layout for create-flow select steps (community size/structure, core values) and
* {@link DecisionApproachesScreen} (decision approaches). Below `lg` (1024px), one column + main scrolls.
* At `lg+`, mirrors {@link CompletedScreen}: static header column + scrollable controls column
* (`min-h-0` + `overflow-y-auto` height chain; see completed page right rail).
* Question-and-options layout for create-flow select steps (community size/structure,
* core values, stakeholders) 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).
*/
export function CreateFlowTwoColumnSelectShell({
header,
@@ -56,24 +60,24 @@ export function CreateFlowTwoColumnSelectShell({
>
<div
className={
"flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-400,16px)] md:max-w-[640px] " +
"max-lg:flex-none lg:max-h-full lg:max-w-[1328px] lg:min-h-0 lg:flex-1 lg:flex-row lg:flex-nowrap " +
`${rowLgCrossAlignClass} lg:justify-center lg:gap-[var(--measures-spacing-1200,48px)] lg:overflow-hidden`
"flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-400,16px)] " +
"max-lg:flex-none lg:max-h-full lg:min-h-0 lg:flex-1 lg:flex-row lg:flex-nowrap " +
`${rowLgCrossAlignClass} lg:justify-center lg:gap-[var(--measures-spacing-1200,48px)] lg:overflow-hidden ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`
}
>
<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] lg:max-w-[640px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`
`lg:flex-1 ${leftLgMainJustifyClass} lg:py-[12px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`
}
>
{header}
</div>
<div
className={
`scrollbar-hide 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)] ` +
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS
`${CREATE_FLOW_SCROLL_REGION_CLASS} ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`
}
>
{children}
@@ -1,13 +1,47 @@
/** Single column/section: full width under `md`, max 640px from `--breakpoint-md` up. */
import type { CreateFlowStep } from "../types";
import { CREATE_FLOW_SCREEN_REGISTRY } from "../utils/createFlowScreenRegistry";
/**
* Shared page gutters for create-flow chrome and step shells.
* Six viewport bands collapse to three values: 20px below `md`, 48px from `md`,
* 64px from `lg` (320639 / 6401023 / 1024+).
*/
export const CREATE_FLOW_PAGE_GUTTER_CLASS = "px-5 md:px-12 lg:px-16";
/**
* Wizard viewport frame. `dvh` tracks mobile browser chrome so the action bar
* stays on-screen; `max-h-dvh` keeps the flex column from overflowing it.
*/
export const CREATE_FLOW_VIEWPORT_FRAME_CLASS =
"relative flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden";
/** Design-system scrollbar for create-flow scroll regions (not `scrollbar-hide`). */
export const CREATE_FLOW_SCROLL_REGION_CLASS = "scrollbar-design";
/**
* Fade above the fixed action bar so content that continues below the fold is
* visible as a continuation cue (Figma Scrim / footer overlay).
*/
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";
/**
* Single column: full width below `lg` so tablet widths use available space;
* 640px from `--breakpoint-lg` up (two-column layouts start at `lg`).
*/
export const CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS =
"w-full min-w-0 md:max-w-[640px]";
"w-full min-w-0 lg:max-w-[640px]";
/** Grid cell: same cap as column max, centered when the track is wider than 640px. */
export const CREATE_FLOW_MD_UP_GRID_CELL_CLASS =
"w-full min-w-0 md:mx-auto md:max-w-[640px]";
"w-full min-w-0 lg:mx-auto lg:max-w-[640px]";
/** Two 640px columns + `--measures-spacing-1200` (48px) gutter. */
export const CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS = "md:max-w-[1328px]";
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). */
export const CREATE_FLOW_WIDE_MAX_CLASS = "w-full min-w-0 lg:max-w-[1440px]";
/**
* Lockup+card and card-stack `<main>`: keep `items-start` so a tall card can
@@ -29,3 +63,30 @@ export const CREATE_FLOW_MD_CENTERED_SHELL_CLASS = "md:my-auto md:pt-0";
*/
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.
*/
export function getCreateFlowContentMaxClass(options: {
step: CreateFlowStep | null | undefined;
isTemplateReview?: boolean;
}): string {
if (options.isTemplateReview) {
return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS;
}
if (!options.step) {
return CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS;
}
switch (CREATE_FLOW_SCREEN_REGISTRY[options.step].layoutKind) {
case "select":
case "right-rail":
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;
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
export default function CreateFlowRouteLoading() {
return (
<div
className="flex h-screen min-h-0 flex-col overflow-hidden bg-[var(--color-surface-default-primary)]"
className="flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden bg-[var(--color-surface-default-primary)]"
aria-busy="true"
aria-live="polite"
>
@@ -6,7 +6,7 @@
*
* 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` / `CreateFlowStepShell` as needed.
* reuse `CardStack` / `CreateFlowCardStackStepShell` as needed.
*
* Card click opens the Figma create modal (node `20246-15829`) with three
* editable sections rendered by {@link CommunicationMethodEditFields}. The
@@ -22,16 +22,10 @@ import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowCardStackStepShell } from "../../components/CreateFlowCardStackStepShell";
import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import { CommunicationMethodEditFields } from "../../components/methodEditFields";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
@@ -700,44 +694,30 @@ export function CommunicationMethodsScreen() {
return (
<>
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-800"
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
<CreateFlowCardStackStepShell
lockupTitle={title}
lockupDescription={description}
stackBusy={!recommendationsReady}
>
<div className="flex w-full min-w-0 flex-col items-center gap-6">
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
<CreateFlowHeaderLockup
title={title}
description={description}
justification="center"
/>
</div>
<div
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
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}
compactDesktopLayout="flexWrap"
headerLockupSize={mdUp ? "L" : "M"}
/>
)}
</div>
</div>
{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"}
/>
)}
</CreateFlowCardStackStepShell>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
@@ -780,7 +760,6 @@ export function CommunicationMethodsScreen() {
)
) : null}
</Create>
</CreateFlowStepShell>
<CustomMethodCardWizard
isOpen={addCustomWizardOpen}
onClose={handleCloseAddWizard}
@@ -19,16 +19,10 @@ import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowCardStackStepShell } from "../../components/CreateFlowCardStackStepShell";
import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import { ConflictManagementEditFields } from "../../components/methodEditFields";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
@@ -701,44 +695,30 @@ export function ConflictManagementScreen() {
return (
<>
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-800"
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
<CreateFlowCardStackStepShell
lockupTitle={title}
lockupDescription={description}
stackBusy={!recommendationsReady}
>
<div className="flex w-full min-w-0 flex-col items-center gap-6">
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
<CreateFlowHeaderLockup
title={title}
description={description}
justification="center"
/>
</div>
<div
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
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}
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
/>
)}
</div>
</div>
{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"}
/>
)}
</CreateFlowCardStackStepShell>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
@@ -781,7 +761,6 @@ export function ConflictManagementScreen() {
)
) : null}
</Create>
</CreateFlowStepShell>
<CustomMethodCardWizard
isOpen={addCustomWizardOpen}
onClose={handleCloseAddWizard}
@@ -20,16 +20,10 @@ import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowCardStackStepShell } from "../../components/CreateFlowCardStackStepShell";
import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import { MembershipMethodEditFields } from "../../components/methodEditFields";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
@@ -694,44 +688,30 @@ export function MembershipMethodsScreen() {
return (
<>
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-800"
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
<CreateFlowCardStackStepShell
lockupTitle={title}
lockupDescription={description}
stackBusy={!recommendationsReady}
>
<div className="flex w-full min-w-0 flex-col items-center gap-6">
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
<CreateFlowHeaderLockup
title={title}
description={description}
justification="center"
/>
</div>
<div
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
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}
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
/>
)}
</div>
</div>
{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"}
/>
)}
</CreateFlowCardStackStepShell>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
@@ -774,7 +754,6 @@ export function MembershipMethodsScreen() {
)
) : null}
</Create>
</CreateFlowStepShell>
<CustomMethodCardWizard
isOpen={addCustomWizardOpen}
onClose={handleCloseAddWizard}
@@ -16,6 +16,8 @@ import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import {
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
CREATE_FLOW_PAGE_GUTTER_CLASS,
CREATE_FLOW_SCROLL_REGION_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "../../components/createFlowLayoutTokens";
import {
@@ -163,7 +165,7 @@ export function CompletedScreen() {
<>
<div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-[var(--color-teal-teal50,#c9fef9)] md:h-full">
<div
className={`mx-auto grid min-h-0 w-full grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:overflow-y-auto max-md:overscroll-y-contain max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:grid-rows-1 md:items-stretch md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
className={`mx-auto grid min-h-0 w-full grid-cols-1 gap-4 ${CREATE_FLOW_PAGE_GUTTER_CLASS} max-md:overflow-y-auto max-md:overscroll-y-contain max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:grid-rows-1 md:items-stretch md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
>
<div
className={`flex flex-col justify-start max-md:min-h-min max-md:overflow-visible min-h-0 overflow-hidden md:justify-center md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
@@ -177,7 +179,7 @@ export function CompletedScreen() {
/>
</div>
<div
className={`scrollbar-hide relative flex min-h-0 flex-col self-stretch overflow-x-hidden md:max-h-full md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
className={`relative flex min-h-0 flex-col self-stretch overflow-x-hidden md:max-h-full md:overflow-y-auto ${CREATE_FLOW_SCROLL_REGION_CLASS} ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
>
<div
className="pointer-events-none sticky top-0 z-10 hidden h-5 shrink-0 bg-gradient-to-b from-[var(--color-teal-teal50,#c9fef9)]/55 from-0% via-[var(--color-teal-teal50,#c9fef9)]/20 via-50% to-transparent md:block"
@@ -9,8 +9,7 @@ import { useTranslation } from "../../../../contexts/MessagesContext";
import { MAX_STAKEHOLDER_EMAILS } from "../../../../../lib/create/stakeholderLimits";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import {
CREATE_FLOW_MANAGE_STAKEHOLDERS_QUERY,
CREATE_FLOW_MANAGE_STAKEHOLDERS_VALUE,
@@ -131,64 +130,52 @@ export function ConfirmStakeholdersScreen() {
if (managePublishedMode) {
return (
<CreateFlowStepShell
variant="centeredNarrowBottomPad"
contentTopBelowMd="space-1400"
<CreateFlowTwoColumnSelectShell
header={
<CreateFlowHeaderLockup
title={t("managePublished.lockupTitle")}
description={t("managePublished.lockupDescription")}
justification="left"
/>
}
>
<div
className={`flex flex-col items-start gap-[var(--measures-spacing-300,12px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
>
<div className="flex w-full flex-col gap-[var(--measures-spacing-200,8px)] py-[12px]">
<CreateFlowHeaderLockup
title={t("managePublished.lockupTitle")}
description={t("managePublished.lockupDescription")}
justification="left"
/>
</div>
<PublishedStakeholdersManagePanel ruleId={editingPublishedRuleId} />
</div>
</CreateFlowStepShell>
<PublishedStakeholdersManagePanel ruleId={editingPublishedRuleId} />
</CreateFlowTwoColumnSelectShell>
);
}
return (
<>
<CreateFlowStepShell
variant="centeredNarrowBottomPad"
contentTopBelowMd="space-1400"
>
<div
className={`flex flex-col items-start gap-[var(--measures-spacing-300,12px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
>
<div className="flex w-full flex-col gap-[var(--measures-spacing-200,8px)] py-[12px]">
<CreateFlowHeaderLockup
title={t("title")}
description={t("description")}
justification="left"
/>
</div>
{chipError ? (
<p
className="text-small-paragraph text-[var(--color-border-default-utility-negative)]"
role="alert"
>
{chipError}
</p>
) : null}
<MultiSelect
formHeader={false}
showHelpIcon={false}
size="s"
options={stakeholderOptions}
onChipClick={handleChipClick}
onAddClick={handleAddStakeholder}
onCustomChipConfirm={handleCustomChipConfirm}
onCustomChipClose={handleCustomChipClose}
addButton
addButtonText={t("addStakeholder")}
<CreateFlowTwoColumnSelectShell
header={
<CreateFlowHeaderLockup
title={t("title")}
description={t("description")}
justification="left"
/>
</div>
</CreateFlowStepShell>
}
>
{chipError ? (
<p
className="text-small-paragraph text-[var(--color-border-default-utility-negative)]"
role="alert"
>
{chipError}
</p>
) : null}
<MultiSelect
formHeader={false}
showHelpIcon={false}
size="s"
options={stakeholderOptions}
onChipClick={handleChipClick}
onAddClick={handleAddStakeholder}
onCustomChipConfirm={handleCustomChipConfirm}
onCustomChipClose={handleCustomChipClose}
addButton
addButtonText={t("addStakeholder")}
/>
</CreateFlowTwoColumnSelectShell>
{!toastDismissed && (
<div
+2 -1
View File
@@ -195,13 +195,14 @@ export function shouldOfferCreateFlowSaveAndExit(
/**
* Steps where below `lg` the main column scrolls with split layout
* (`CreateFlowLayoutClient` — Linear CR-92 §4).
* (`CreateFlowLayoutClient`).
*/
export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] = [
"community-size",
"community-structure",
"core-values",
"decision-approaches",
"confirm-stakeholders",
] as const;
export function createFlowStepUsesSelectSplitScroll(
@@ -17,6 +17,7 @@ const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
proportionBarVariant,
onBackClick,
className = "",
contentMaxClass,
footerAriaLabel,
}) => {
const t = useTranslation("controlsChrome");
@@ -29,6 +30,7 @@ const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
proportionBarVariant={proportionBarVariant}
onBackClick={onBackClick}
className={className}
contentMaxClass={contentMaxClass}
footerAriaLabel={footerAriaLabel ?? t("createFlowFooterAriaLabel")}
/>
);
@@ -36,6 +36,11 @@ export interface CreateFlowFooterProps {
* Additional CSS classes
*/
className?: string;
/**
* Inner max-width for the progress bar and actions, matching the step
* content column so the primary button does not drift at wide viewports.
*/
contentMaxClass?: string;
/**
* Accessible name for the footer landmark.
*/
@@ -1,6 +1,11 @@
import ProportionBar from "../../progress/ProportionBar";
import Button from "../../buttons/Button";
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
import {
CREATE_FLOW_FOOTER_SCRIM_CLASS,
CREATE_FLOW_PAGE_GUTTER_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "../../../(app)/create/components/createFlowLayoutTokens";
export function CreateFlowFooterView({
secondButton,
@@ -9,41 +14,43 @@ export function CreateFlowFooterView({
proportionBarVariant: proportionBarVariantProp,
onBackClick,
className = "",
contentMaxClass = CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
footerAriaLabel,
}: CreateFlowFooterProps) {
const proportionBarVariant = proportionBarVariantProp ?? "default";
return (
<footer
className={`bg-black w-full ${className}`}
className={`relative bg-black w-full pb-[env(safe-area-inset-bottom,0px)] ${className}`}
role="contentinfo"
aria-label={footerAriaLabel}
>
{/* Progress Bar - Top */}
{progressBar && (
<div className="px-[var(--spacing-measures-spacing-500,20px)] md:px-[var(--spacing-measures-spacing-1200,48px)] pt-[var(--spacing-measures-spacing-300,12px)]">
<ProportionBar
progress={proportionBarProgress}
variant={proportionBarVariant}
/>
<div className={CREATE_FLOW_FOOTER_SCRIM_CLASS} aria-hidden />
<div className={CREATE_FLOW_PAGE_GUTTER_CLASS}>
<div className={`mx-auto w-full ${contentMaxClass}`}>
{progressBar && (
<div className="pt-[var(--spacing-measures-spacing-300,12px)]">
<ProportionBar
progress={proportionBarProgress}
variant={proportionBarVariant}
/>
</div>
)}
<div className="flex items-center justify-between py-[var(--spacing-measures-spacing-300,12px)] gap-[var(--spacing-measures-spacing-300,12px)]">
<Button
buttonType="ghost"
palette="default"
size="xsmall"
className="!text-x-small-label md:!text-small-label !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={onBackClick}
disabled={!onBackClick}
>
Back
</Button>
{secondButton && <div className="flex-shrink-0">{secondButton}</div>}
</div>
</div>
)}
{/* Buttons Container */}
<div className="flex items-center justify-between mx-auto max-w-[639px] md:max-w-[1920px] px-[var(--spacing-measures-spacing-500,20px)] md:px-[var(--spacing-measures-spacing-1200,48px)] py-[var(--spacing-measures-spacing-300,12px)] gap-[var(--spacing-measures-spacing-300,12px)]">
{/* Back Button - Left */}
<Button
buttonType="ghost"
palette="default"
size="xsmall"
className="!text-x-small-label md:!text-small-label !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={onBackClick}
disabled={!onBackClick}
>
Back
</Button>
{/* Second Button - Right */}
{secondButton && <div className="flex-shrink-0">{secondButton}</div>}
</div>
</footer>
);
@@ -5,6 +5,7 @@ import Button from "../../buttons/Button";
import ListItem from "../../layout/ListItem";
import Popover from "../../modals/Popover";
import type { CreateFlowTopNavViewProps } from "./CreateFlowTopNav.types";
import { CREATE_FLOW_PAGE_GUTTER_CLASS } from "../../../(app)/create/components/createFlowLayoutTokens";
const outlineButtonClass =
"md:!text-x-small-label !text-xx-small-label !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]";
@@ -229,7 +230,7 @@ export function CreateFlowTopNavView({
aria-label={bannerAriaLabel}
>
<nav
className="flex items-center justify-between mx-auto max-w-[639px] md:max-w-[1920px] px-[var(--spacing-measures-spacing-500,20px)] md:px-[48px] py-[var(--spacing-measures-spacing-300,12px)] md:py-[var(--spacing-measures-spacing-016,16px)]"
className={`flex items-center justify-between mx-auto w-full max-w-[1920px] ${CREATE_FLOW_PAGE_GUTTER_CLASS} py-[var(--spacing-measures-spacing-300,12px)] md:py-[var(--spacing-measures-spacing-016,16px)]`}
role="navigation"
aria-label={navAriaLabel}
>
@@ -86,4 +86,17 @@ describe("CreateFlowFooter (behavioral tests)", () => {
expect(buttons).toHaveLength(1);
expect(buttons[0]).toHaveTextContent("Back");
});
it("constrains the action row to the step content max-width", () => {
const { container } = render(
<CreateFlowFooter contentMaxClass="w-full min-w-0 lg:max-w-[640px]" />,
);
const footer = screen.getByRole("contentinfo", {
name: "Create Flow Footer",
});
expect(footer.className).toContain("pb-[env(safe-area-inset-bottom,0px)]");
expect(
container.querySelector('[class*="lg:max-w-[640px]"]'),
).not.toBeNull();
});
});
+40 -3
View File
@@ -3,19 +3,35 @@ import {
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
CREATE_FLOW_WIDE_MAX_CLASS,
CREATE_FLOW_MD_CENTERED_MAIN_CLASS,
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
CREATE_FLOW_PAGE_GUTTER_CLASS,
CREATE_FLOW_VIEWPORT_FRAME_CLASS,
getCreateFlowContentMaxClass,
} from "../../app/(app)/create/components/createFlowLayoutTokens";
describe("createFlowLayoutTokens", () => {
it("exports create-flow column and two-column max class strings", () => {
expect(CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS).toBe(
"w-full min-w-0 md:max-w-[640px]",
"w-full min-w-0 lg:max-w-[640px]",
);
expect(CREATE_FLOW_MD_UP_GRID_CELL_CLASS).toBe(
"w-full min-w-0 md:mx-auto md:max-w-[640px]",
"w-full min-w-0 lg:mx-auto lg:max-w-[640px]",
);
expect(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS).toBe("md:max-w-[1328px]");
expect(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS).toBe(
"w-full min-w-0 lg:max-w-[1328px]",
);
expect(CREATE_FLOW_WIDE_MAX_CLASS).toBe("w-full min-w-0 lg:max-w-[1440px]");
});
it("uses one gutter scale across the six viewport bands", () => {
expect(CREATE_FLOW_PAGE_GUTTER_CLASS).toBe("px-5 md:px-12 lg:px-16");
});
it("sizes the wizard frame with dynamic viewport height", () => {
expect(CREATE_FLOW_VIEWPORT_FRAME_CLASS).toContain("h-dvh");
expect(CREATE_FLOW_VIEWPORT_FRAME_CLASS).toContain("max-h-dvh");
});
it("centers lockup+card and card-stack steps in the navfooter band from md", () => {
@@ -24,4 +40,25 @@ describe("createFlowLayoutTokens", () => {
);
expect(CREATE_FLOW_MD_CENTERED_SHELL_CLASS).toBe("md:my-auto md:pt-0");
});
it("aligns the action bar max-width with the step content column", () => {
expect(
getCreateFlowContentMaxClass({ step: "community-name" }),
).toBe(CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS);
expect(
getCreateFlowContentMaxClass({ step: "community-structure" }),
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
expect(
getCreateFlowContentMaxClass({ step: "communication-methods" }),
).toBe(CREATE_FLOW_WIDE_MAX_CLASS);
expect(
getCreateFlowContentMaxClass({ step: "final-review" }),
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
expect(
getCreateFlowContentMaxClass({
step: "informational",
isTemplateReview: true,
}),
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
});
});
+11
View File
@@ -10,6 +10,7 @@ import {
resolveCreateFlowBackTarget,
shouldOfferCreateFlowSaveAndExit,
isDirectTemplateReviewEntry,
createFlowStepUsesSelectSplitScroll,
TEMPLATES_FACET_RECOMMEND_QUERY,
TEMPLATES_FACET_RECOMMEND_VALUE,
TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY,
@@ -221,4 +222,14 @@ describe("flowSteps", () => {
).toBe(false);
expect(isDirectTemplateReviewEntry(null)).toBe(false);
});
it("uses split-column scroll for select, right-rail, and stakeholders", () => {
expect(createFlowStepUsesSelectSplitScroll("community-structure")).toBe(
true,
);
expect(createFlowStepUsesSelectSplitScroll("confirm-stakeholders")).toBe(
true,
);
expect(createFlowStepUsesSelectSplitScroll("community-name")).toBe(false);
});
});