diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c69c82c..0ab1db8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` in `.env` so **signed-in** users can ### Create flow URLs (custom wizard) -The **custom** create-rule wizard lives under **`/create/…`**. The header links to **`/create`**, which redirects to the first step. **Semantic** URL segments (e.g. `community-name`, `community-size`) match Figma intent; order is **`FLOW_STEP_ORDER`** in `app/create/utils/flowSteps.ts`, with UI from **`app/create/[screenId]/page.tsx`** and **`CREATE_FLOW_SCREEN_REGISTRY`** for Figma traceability. **Figma** stages: **Create Community** (through `review`), **Create Custom CommunityRule** (`cards`–`right-rail`), **Review and complete** (`confirm-stakeholders`–`completed`). **`/create/review-template/[slug]`** is a template **preview** only. Full tables and persistence are in **[docs/create-flow.md](docs/create-flow.md)**; engineering tracking: Linear **CR-89** / Ticket 17 in [docs/backend-linear-tickets.md](docs/backend-linear-tickets.md). +The **custom** create-rule wizard lives under **`/create/…`**. The header links to **`/create`**, which redirects to the first step. **Semantic** URL segments (e.g. `community-name`, `community-size`) match Figma intent; order is **`FLOW_STEP_ORDER`** in `app/create/utils/flowSteps.ts`, with UI from **`app/create/[screenId]/page.tsx`** and **`CREATE_FLOW_SCREEN_REGISTRY`** for Figma traceability. **Figma** stages: **Create Community** (through `review`), **Create Custom CommunityRule** (`communication-methods`–`right-rail`), **Review and complete** (`confirm-stakeholders`–`completed`). **`/create/review-template/[slug]`** is a template **preview** only. Full tables and persistence are in **[docs/create-flow.md](docs/create-flow.md)**; engineering tracking: Linear **CR-89** / Ticket 17 in [docs/backend-linear-tickets.md](docs/backend-linear-tickets.md). ## Frontend / tests diff --git a/app/components/progress/ProportionBar/ProportionBar.types.ts b/app/components/progress/ProportionBar/ProportionBar.types.ts index 2223bf8..970e708 100644 --- a/app/components/progress/ProportionBar/ProportionBar.types.ts +++ b/app/components/progress/ProportionBar/ProportionBar.types.ts @@ -10,6 +10,7 @@ export type ProportionBarState = | "2-0" | "2-1" | "2-2" + | "2-3" | "3-0" | "3-1" | "3-2"; @@ -20,7 +21,9 @@ export interface ProportionBarProps { progress?: ProportionBarState; className?: string; /** - * `segmented` (Figma: create-flow footer): pill-shaped partial fills inside each segment. + * Kept for backwards compatibility. Both `default` and `segmented` render the + * same fill geometry (square leading edges, matching Figma). Future variants + * can differentiate here without API changes. */ variant?: ProportionBarVariant; } diff --git a/app/components/progress/ProportionBar/ProportionBar.view.tsx b/app/components/progress/ProportionBar/ProportionBar.view.tsx index 9ac4315..998af73 100644 --- a/app/components/progress/ProportionBar/ProportionBar.view.tsx +++ b/app/components/progress/ProportionBar/ProportionBar.view.tsx @@ -1,24 +1,41 @@ import type { ProportionBarViewProps } from "./ProportionBar.types"; +/** + * Per-step fill ratio for the second (middle) segment at `2-X` progress states. + * Values are taken directly from Figma (`17861:33241`, `18861:15250`, `21434:17632`) + * and are intentionally non-uniform — they are NOT `X/3`. + */ +const SECOND_SEGMENT_FILL_RATIO: Record = { + 0: 0, + 1: 1 / 4, + 2: 1 / 2, + 3: 3 / 4, +}; + +function getSecondSegmentFillRatio(partial: number): number { + return SECOND_SEGMENT_FILL_RATIO[partial] ?? 0; +} + export function ProportionBarView({ progress, className, barClasses, - variant, + // `variant` is kept in the prop API for callers, but both `default` and + // `segmented` now render identical fill geometry (square leading edges). + variant: _variant, }: ProportionBarViewProps) { // Proportion bar type const [fullSegments, partialSegment] = progress.split("-").map(Number); - const segmented = variant === "segmented"; // Calculate total progress: // - For 1-X: first section is (X+1)/6 filled - // - For 2-X: first section full, second section X/3 filled + // - For 2-X: first section full, second section filled per Figma ratios (see `SECOND_SEGMENT_FILL_RATIO`) // - For 3-X: first two sections full, third section X/3 filled // Max is 3 full segments = 9 units let totalProgress = 0; if (fullSegments === 1) { totalProgress = (partialSegment + 1) / 6; // 1/6 to 6/6 of first section } else if (fullSegments === 2) { - totalProgress = 1 + partialSegment / 3; // 1 full + 0/3 to 2/3 of second + totalProgress = 1 + getSecondSegmentFillRatio(partialSegment); } else if (fullSegments === 3) { totalProgress = 2 + partialSegment / 3; // 2 full + 0/3 to 2/3 of third } @@ -55,33 +72,32 @@ export function ProportionBarView({ {/* Fill layer - always show 3 sections, fill amount varies */} + {/* + The leading (right) edge of every partial fill is a straight (square) edge — + only the outermost left/right edges of the whole bar can round to match the + background capsule. + */}
{/* First section - for 1-X: (X+1)/6 filled, for 2-X and 3-X: fully filled */}
{fullSegments === 1 ? (
) : fullSegments >= 2 ? (
) : null}
- {/* Second section - for 2-X: X/3 filled, for 3-X: fully filled, otherwise empty */} + {/* Second section — for 2-X: Figma ratio fill (see `SECOND_SEGMENT_FILL_RATIO`); for 3-X: full; otherwise empty. */}
{fullSegments === 2 ? ( partialSegment > 0 ? (
) : null ) : fullSegments >= 3 ? ( @@ -89,16 +105,12 @@ export function ProportionBarView({ ) : null}
{/* Third section - for 3-X: X/3 filled, otherwise empty */} - {/* Round right corner when at 100% (third section fully filled, partialSegment === 3) */} + {/* Round right corner only when the fill reaches the absolute right edge of the bar (partialSegment >= 3) */}
{fullSegments === 3 && partialSegment > 0 ? (
= 3 - ? "rounded-r-[var(--radius-full)]" - : "" + partialSegment >= 3 ? "rounded-r-[var(--radius-full)]" : "" }`.trim()} style={{ width: `${Math.min((partialSegment / 3) * 100, 100)}%` }} /> diff --git a/app/components/utility/CardStack/CardStack.container.tsx b/app/components/utility/CardStack/CardStack.container.tsx index d684867..7c07fee 100644 --- a/app/components/utility/CardStack/CardStack.container.tsx +++ b/app/components/utility/CardStack/CardStack.container.tsx @@ -21,7 +21,10 @@ const CardStackContainer = memo( title = "", description = "", layout = "default", + compactRecommendedLimit = 5, + compactDesktopLayout: compactDesktopLayoutProp = "grid", headerLockupSize, + toggleAlignment = "center", className = "", }) => { const [internalExpanded, setInternalExpanded] = useState(false); @@ -75,7 +78,10 @@ const CardStackContainer = memo( title={title} description={description} layout={layout} + compactRecommendedLimit={compactRecommendedLimit} + compactDesktopLayout={compactDesktopLayoutProp} headerLockupSize={headerLockupSize} + toggleAlignment={toggleAlignment} className={className} /> ); diff --git a/app/components/utility/CardStack/CardStack.types.ts b/app/components/utility/CardStack/CardStack.types.ts index 6109af5..6c81314 100644 --- a/app/components/utility/CardStack/CardStack.types.ts +++ b/app/components/utility/CardStack/CardStack.types.ts @@ -21,8 +21,19 @@ export interface CardStackProps { description?: string; /** "default" = compact grid/column + expanded grid; "singleStack" = always one column, expand shows more in same stack */ layout?: "default" | "singleStack"; + /** + * Max recommended cards in compact (non-expanded) mode. Default 5; Figma compact stack uses 3. + */ + compactRecommendedLimit?: number; + /** + * At `md+`, how compact recommended cards are laid out. `flexWrap` matches Figma Flow — Compact Card Stack (three cards in a row). + * `pyramidFive` = two rows (3 + 2) centered for five recommended cards (membership step). + */ + compactDesktopLayout?: "grid" | "flexWrap" | "pyramidFive"; /** Optional title/description lockup size (create-flow passes `md`-matched `L`/`M`). Defaults to `L`. */ headerLockupSize?: HeaderLockupSizeValue; + /** Alignment of the expand/collapse control in `singleStack` layout (Figma right-rail: end). */ + toggleAlignment?: "center" | "end"; className?: string; } @@ -38,6 +49,9 @@ export interface CardStackViewProps { title: string; description: string; layout: "default" | "singleStack"; + compactRecommendedLimit: number; + compactDesktopLayout: "grid" | "flexWrap" | "pyramidFive"; headerLockupSize: HeaderLockupSizeValue | undefined; + toggleAlignment: "center" | "end"; className: string; } diff --git a/app/components/utility/CardStack/CardStack.view.tsx b/app/components/utility/CardStack/CardStack.view.tsx index ad74746..27b28ad 100644 --- a/app/components/utility/CardStack/CardStack.view.tsx +++ b/app/components/utility/CardStack/CardStack.view.tsx @@ -16,13 +16,18 @@ export function CardStackView({ title, description, layout, + compactRecommendedLimit, + compactDesktopLayout, headerLockupSize, + toggleAlignment, className, }: CardStackViewProps) { const lockupSize = headerLockupSize ?? "L"; const isSelected = (id: string) => selectedIds.includes(id); - // Compact: recommended only (up to 5). Expanded: all cards. - const compactCards = cards.filter((c) => c.recommended ?? false).slice(0, 5); + // Compact: recommended only (default up to 5). Expanded: all cards. + const compactCards = cards + .filter((c) => c.recommended ?? false) + .slice(0, compactRecommendedLimit); // Single stack: always one column; expand reveals more in same stack (scrollable) if (layout === "singleStack") { @@ -39,7 +44,7 @@ export function CardStackView({ />
) : null} -
+
{displayedCards.map((item) => ( {expanded ? showLessLabel : toggleLabel} @@ -81,7 +88,7 @@ export function CardStackView({ ) : null} {expanded ? ( -
+
{cards.map((item) => ( ))}
+ ) : compactDesktopLayout === "pyramidFive" ? ( + <> +
+ {compactCards.map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+
+ {/* + lg+: fixed 3 + 2 rows (no flex-wrap on the top row — avoids 2+1+2 when the first row wraps). + md–lg: same shell as the 3-card step — each row is `flex justify-center gap-2` so cards + stay a tight cluster with gap-2 until lg expands to the 3+2 pyramid. + */} +
+
+ {compactCards.slice(0, 3).map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+ {compactCards.length > 3 ? ( +
+ {compactCards + .slice(3, compactRecommendedLimit) + .map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+ ) : null} +
+
+
+ {compactCards.slice(0, 2).map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+
+ {compactCards.slice(2, 4).map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+ {compactCards[4] ? ( +
+ onCardSelect(compactCards[4].id)} + /> +
+ ) : null} +
+
+ + ) : compactDesktopLayout === "flexWrap" ? ( + <> +
+ {compactCards.map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+ {/* md–lg: pyramid (2 + 1), each row centered; lg+: one centered row (not edge-to-edge in a 2-col grid) */} + {compactCards.length === 3 ? ( + <> +
+
+ {compactCards.slice(0, 2).map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+
+ onCardSelect(compactCards[2].id)} + /> +
+
+
+ {compactCards.map((item) => ( + onCardSelect(item.id)} + /> + ))} +
+ + ) : ( +
+ {compactCards.map((item) => ( +
+ onCardSelect(item.id)} + /> +
+ ))} +
+ )} + ) : ( <> {/* Compact under 640: single column, up to 5 recommended cards */} -
+
{compactCards.map((item) => ( = SAVE_EXIT_FROM_STEP_INDEX; @@ -575,6 +576,70 @@ function CreateFlowLayoutContent({ > {footer.confirmCoreValues} + ) : currentStep === "communication-methods" && nextStep ? ( + + ) : currentStep === "membership-methods" && nextStep ? ( + + ) : currentStep === "decision-approaches" && nextStep ? ( + + ) : currentStep === "conflict-management" && nextStep ? ( + ) : nextStep ? ( + {comm.page.compactDescriptionAfter} + + ); const modalConfig = pendingCardId && pendingCardId in comm.modals @@ -198,13 +234,15 @@ export function CardsScreen() { const handleCreateModalConfirm = useCallback(() => { markCreateFlowInteraction(); if (pendingCardId) { - setSelectedIds((prev) => - prev.includes(pendingCardId) ? prev : [...prev, pendingCardId], + setSelectedIds( + selectedIds.includes(pendingCardId) + ? selectedIds + : [...selectedIds, pendingCardId], ); } setCreateModalOpen(false); setPendingCardId(null); - }, [markCreateFlowInteraction, pendingCardId]); + }, [markCreateFlowInteraction, pendingCardId, selectedIds, setSelectedIds]); return (
-
+
diff --git a/app/create/screens/card/ConflictManagementScreen.tsx b/app/create/screens/card/ConflictManagementScreen.tsx new file mode 100644 index 0000000..026bb70 --- /dev/null +++ b/app/create/screens/card/ConflictManagementScreen.tsx @@ -0,0 +1,179 @@ +"use client"; + +/** + * `conflict-management` step — Figma compact card stack (node `20879-15979`). + * Registry: `CREATE_FLOW_SCREEN_REGISTRY["conflict-management"]`. + */ + +import { useState, useCallback, useMemo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import { useCreateFlow } from "../../context/CreateFlowContext"; +import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; +import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; +import CardStack from "../../../components/utility/CardStack"; +import Create from "../../../components/modals/Create"; +import { CreateFlowStepShell } from "../../components/CreateFlowStepShell"; +import { + CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS, + CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS, +} from "../../components/createFlowLayoutTokens"; + +const CONFLICT_CARD_ORDER = [ + "peer-mediation", + "conflict-resolution-council", + "facilitated-negotiation", + "ad-hoc-arbitration", + "conflict-workshops", + "6", + "7", + "8", +] as const; + +export function ConflictManagementScreen() { + const m = useMessages(); + const cm = m.create.conflictManagement; + const mdUp = useCreateFlowMdUp(); + const { state, updateState, markCreateFlowInteraction } = useCreateFlow(); + const [expanded, setExpanded] = useState(false); + const [createModalOpen, setCreateModalOpen] = useState(false); + const [pendingCardId, setPendingCardId] = useState(null); + + const selectedIds = state.selectedConflictManagementIds ?? []; + + const setSelectedIds = useCallback( + (next: string[]) => { + updateState({ selectedConflictManagementIds: next }); + }, + [updateState], + ); + + const sampleCards = useMemo( + () => + CONFLICT_CARD_ORDER.map((id) => { + const row = cm.cards[id as keyof typeof cm.cards]; + return { + id, + label: row.label, + supportText: row.supportText, + recommended: true, + }; + }), + [cm], + ); + + const title = expanded ? cm.page.expandedTitle : cm.page.compactTitle; + + const description = expanded ? ( + cm.page.expandedDescription + ) : ( + <> + {cm.page.compactDescriptionBefore} + + {cm.page.compactDescriptionAfter} + + ); + + const modalConfig = + pendingCardId && pendingCardId in cm.modals + ? (() => { + const modal = cm.modals[pendingCardId as keyof typeof cm.modals]; + return { + title: modal.title, + description: modal.description, + nextButtonText: cm.confirmModal.nextButtonText, + showBackButton: false as const, + currentStep: undefined, + totalSteps: undefined, + }; + })() + : { + title: cm.confirmModal.title, + description: cm.confirmModal.description, + nextButtonText: cm.confirmModal.nextButtonText, + showBackButton: false as const, + currentStep: undefined, + totalSteps: undefined, + }; + + const handleCardClick = useCallback( + (id: string) => { + markCreateFlowInteraction(); + setPendingCardId(id); + setCreateModalOpen(true); + }, + [markCreateFlowInteraction], + ); + + const handleCreateModalClose = useCallback(() => { + setCreateModalOpen(false); + setPendingCardId(null); + }, []); + + const handleCreateModalConfirm = useCallback(() => { + markCreateFlowInteraction(); + if (pendingCardId) { + setSelectedIds( + selectedIds.includes(pendingCardId) + ? selectedIds + : [...selectedIds, pendingCardId], + ); + } + setCreateModalOpen(false); + setPendingCardId(null); + }, [markCreateFlowInteraction, pendingCardId, selectedIds, setSelectedIds]); + + return ( + +
+
+ +
+
+ { + markCreateFlowInteraction(); + setExpanded((prev) => !prev); + }} + hasMore={true} + toggleLabel={cm.page.seeAllLink} + compactRecommendedLimit={5} + compactDesktopLayout="pyramidFive" + headerLockupSize={mdUp ? "L" : "M"} + /> +
+
+ + +
+ ); +} diff --git a/app/create/screens/card/MembershipMethodsScreen.tsx b/app/create/screens/card/MembershipMethodsScreen.tsx new file mode 100644 index 0000000..e67f464 --- /dev/null +++ b/app/create/screens/card/MembershipMethodsScreen.tsx @@ -0,0 +1,179 @@ +"use client"; + +/** + * `membership-methods` step — Figma compact card stack (node `20858-13947`). + * Registry: `CREATE_FLOW_SCREEN_REGISTRY["membership-methods"]`. + */ + +import { useState, useCallback, useMemo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import { useCreateFlow } from "../../context/CreateFlowContext"; +import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; +import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; +import CardStack from "../../../components/utility/CardStack"; +import Create from "../../../components/modals/Create"; +import { CreateFlowStepShell } from "../../components/CreateFlowStepShell"; +import { + CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS, + CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS, +} from "../../components/createFlowLayoutTokens"; + +const MEMBERSHIP_CARD_ORDER = [ + "open-access", + "orientation-required", + "invitation-only", + "contribution-based", + "mentorship", + "6", + "7", + "8", +] as const; + +export function MembershipMethodsScreen() { + const m = useMessages(); + const mem = m.create.membership; + const mdUp = useCreateFlowMdUp(); + const { state, updateState, markCreateFlowInteraction } = useCreateFlow(); + const [expanded, setExpanded] = useState(false); + const [createModalOpen, setCreateModalOpen] = useState(false); + const [pendingCardId, setPendingCardId] = useState(null); + + const selectedIds = state.selectedMembershipMethodIds ?? []; + + const setSelectedIds = useCallback( + (next: string[]) => { + updateState({ selectedMembershipMethodIds: next }); + }, + [updateState], + ); + + const sampleCards = useMemo( + () => + MEMBERSHIP_CARD_ORDER.map((id) => { + const row = mem.cards[id as keyof typeof mem.cards]; + return { + id, + label: row.label, + supportText: row.supportText, + recommended: true, + }; + }), + [mem], + ); + + const title = expanded ? mem.page.expandedTitle : mem.page.compactTitle; + + const description = expanded ? ( + mem.page.expandedDescription + ) : ( + <> + {mem.page.compactDescriptionBefore} + + {mem.page.compactDescriptionAfter} + + ); + + const modalConfig = + pendingCardId && pendingCardId in mem.modals + ? (() => { + const modal = mem.modals[pendingCardId as keyof typeof mem.modals]; + return { + title: modal.title, + description: modal.description, + nextButtonText: mem.confirmModal.nextButtonText, + showBackButton: false as const, + currentStep: undefined, + totalSteps: undefined, + }; + })() + : { + title: mem.confirmModal.title, + description: mem.confirmModal.description, + nextButtonText: mem.confirmModal.nextButtonText, + showBackButton: false as const, + currentStep: undefined, + totalSteps: undefined, + }; + + const handleCardClick = useCallback( + (id: string) => { + markCreateFlowInteraction(); + setPendingCardId(id); + setCreateModalOpen(true); + }, + [markCreateFlowInteraction], + ); + + const handleCreateModalClose = useCallback(() => { + setCreateModalOpen(false); + setPendingCardId(null); + }, []); + + const handleCreateModalConfirm = useCallback(() => { + markCreateFlowInteraction(); + if (pendingCardId) { + setSelectedIds( + selectedIds.includes(pendingCardId) + ? selectedIds + : [...selectedIds, pendingCardId], + ); + } + setCreateModalOpen(false); + setPendingCardId(null); + }, [markCreateFlowInteraction, pendingCardId, selectedIds, setSelectedIds]); + + return ( + +
+
+ +
+
+ { + markCreateFlowInteraction(); + setExpanded((prev) => !prev); + }} + hasMore={true} + toggleLabel={mem.page.seeAllLink} + compactRecommendedLimit={5} + compactDesktopLayout="pyramidFive" + headerLockupSize={mdUp ? "L" : "M"} + /> +
+
+ + +
+ ); +} diff --git a/app/create/screens/right-rail/RightRailScreen.tsx b/app/create/screens/right-rail/RightRailScreen.tsx index a76dc22..b43cb6f 100644 --- a/app/create/screens/right-rail/RightRailScreen.tsx +++ b/app/create/screens/right-rail/RightRailScreen.tsx @@ -1,5 +1,13 @@ "use client"; +/** + * `decision-approaches` step — Figma “Flow — Right Rail” (node `20523-23509`). + * Registry: `CREATE_FLOW_SCREEN_REGISTRY["decision-approaches"]` (`layoutKind: "right-rail"`). + * + * Layout matches {@link CreateFlowTwoColumnSelectShell}: one column below `lg` (1024px), two columns + * at `lg+` with a scrollable rail — same breakpoint and height chain as select steps, distinct content. + */ + import { useState, useCallback, useMemo } from "react"; import DecisionMakingSidebar from "../../../components/utility/DecisionMakingSidebar"; import CardStack from "../../../components/utility/CardStack"; @@ -8,22 +16,27 @@ import type { CardStackItem } from "../../../components/utility/CardStack/CardSt import { useMessages } from "../../../contexts/MessagesContext"; import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; -import { - CREATE_FLOW_MD_UP_GRID_CELL_CLASS, - CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS, -} from "../../components/createFlowLayoutTokens"; +import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell"; export function RightRailScreen() { const m = useMessages(); const rr = m.create.rightRail; const mdUp = useCreateFlowMdUp(); - const { markCreateFlowInteraction } = useCreateFlow(); + const { state, updateState, markCreateFlowInteraction } = useCreateFlow(); const [messageBoxCheckedIds, setMessageBoxCheckedIds] = useState( [], ); - const [selectedIds, setSelectedIds] = useState([]); const [expanded, setExpanded] = useState(false); + const selectedIds = state.selectedDecisionApproachIds ?? []; + + const setSelectedIds = useCallback( + (next: string[]) => { + updateState({ selectedDecisionApproachIds: next }); + }, + [updateState], + ); + const messageBoxItems: InfoMessageBoxItem[] = useMemo( () => rr.messageBox.items.map((item) => ({ @@ -47,7 +60,16 @@ export function RightRailScreen() { const sidebarDescription = ( <> {rr.sidebar.descriptionBefore} - {rr.sidebar.descriptionLink} + {rr.sidebar.descriptionAfter} ); @@ -65,11 +87,13 @@ export function RightRailScreen() { const handleCardSelect = useCallback( (id: string) => { markCreateFlowInteraction(); - setSelectedIds((prev) => - prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], + setSelectedIds( + selectedIds.includes(id) + ? selectedIds.filter((x) => x !== id) + : [...selectedIds, id], ); }, - [markCreateFlowInteraction], + [markCreateFlowInteraction, selectedIds, setSelectedIds], ); const handleToggleExpand = useCallback(() => { @@ -78,48 +102,40 @@ export function RightRailScreen() { }, [markCreateFlowInteraction]); return ( -
-
-
-
- -
-
-
- -
-
-
+ + } + > +
+
-
+ ); } diff --git a/app/create/types.ts b/app/create/types.ts index 1ec4ce4..68393b5 100644 --- a/app/create/types.ts +++ b/app/create/types.ts @@ -19,8 +19,10 @@ export type CreateFlowStep = | "community-save" | "review" | "core-values" - | "cards" - | "right-rail" + | "communication-methods" + | "membership-methods" + | "decision-approaches" + | "conflict-management" | "confirm-stakeholders" | "final-review" | "completed"; @@ -83,6 +85,14 @@ export interface CreateFlowState { coreValuesChipsSnapshot?: CommunityStructureChipSnapshotRow[]; /** User-authored detail text keyed by chip id (preset ids or custom UUIDs). */ coreValueDetailsByChipId?: Record; + /** Create Custom — communication methods step (`/create/communication-methods`); card ids from `create.communication` presets. */ + selectedCommunicationMethodIds?: string[]; + /** Create Custom — membership / join patterns (`/create/membership-methods`); card ids from `create.membership` presets. */ + selectedMembershipMethodIds?: string[]; + /** Create Custom — decision approaches (`/create/decision-approaches`); card ids from `create.rightRail` presets. */ + selectedDecisionApproachIds?: string[]; + /** Create Custom — conflict management (`/create/conflict-management`); card ids from `create.conflictManagement` presets. */ + selectedConflictManagementIds?: string[]; currentStep?: CreateFlowStep; /** Section drafts; structure will tighten as steps persist real shapes. */ sections?: Record[]; diff --git a/app/create/utils/createFlowProportionProgress.ts b/app/create/utils/createFlowProportionProgress.ts index 2541e39..017986b 100644 --- a/app/create/utils/createFlowProportionProgress.ts +++ b/app/create/utils/createFlowProportionProgress.ts @@ -16,8 +16,10 @@ const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [ "2-0", // community-save "2-0", // review (Figma Flow — Review `19706:12135`: same segment fill as end of Create Community) "2-0", // core-values (same segment as review / end of Create Community) - "2-2", // cards - "3-0", // right-rail + "2-1", // communication-methods (Figma — Compact Card Stack) + "2-2", // membership-methods (Figma — Compact Card Stack `20858:13947`) + "2-3", // decision-approaches (Figma Flow — Right Rail `20523:23509`) + "3-0", // conflict-management (Figma Flow — Compact Card Stack `20879:15979`; start of Review segment) "3-1", // confirm-stakeholders "3-2", // final-review "3-2", // completed diff --git a/app/create/utils/createFlowScreenRegistry.ts b/app/create/utils/createFlowScreenRegistry.ts index 35719b2..7af764e 100644 --- a/app/create/utils/createFlowScreenRegistry.ts +++ b/app/create/utils/createFlowScreenRegistry.ts @@ -2,7 +2,8 @@ import type { CreateFlowStep } from "../types"; /** * Figma layout families for the create flow (not encoded in the URL). - * Registry and `app/create/screens/` are organized by these kinds. + * `app/create/screens//` mirrors these names: e.g. `layoutKind: "select"` → `screens/select/`, + * `"card"` → `screens/card/` (compact card-stack frames, distinct from two-column chip selects). */ export type CreateFlowLayoutKind = | "informational" @@ -90,18 +91,30 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record< messageNamespace: "create.coreValues", centeredBodyBelowMd: false, }, - cards: { + "communication-methods": { layoutKind: "card", - figmaNodeId: "TBD-cards", + figmaNodeId: "20246-15828", messageNamespace: "create.communication", centeredBodyBelowMd: false, }, - "right-rail": { + "membership-methods": { + layoutKind: "card", + figmaNodeId: "20858-13947", + messageNamespace: "create.membership", + centeredBodyBelowMd: false, + }, + "decision-approaches": { layoutKind: "right-rail", - figmaNodeId: "TBD-right-rail", + figmaNodeId: "20523-23509", messageNamespace: "create.rightRail", centeredBodyBelowMd: false, }, + "conflict-management": { + layoutKind: "card", + figmaNodeId: "20879-15979", + messageNamespace: "create.conflictManagement", + centeredBodyBelowMd: false, + }, "confirm-stakeholders": { layoutKind: "select", figmaNodeId: "21104-46594", @@ -128,3 +141,11 @@ export function createFlowStepUsesCenteredTextLayout( if (!step) return false; return CREATE_FLOW_SCREEN_REGISTRY[step].centeredBodyBelowMd; } + +/** Steps whose main area uses the CardStack-style layout (`layoutKind: "card"`). */ +export function createFlowStepUsesCardLayout( + step: CreateFlowStep | null, +): boolean { + if (!step) return false; + return CREATE_FLOW_SCREEN_REGISTRY[step].layoutKind === "card"; +} diff --git a/app/create/utils/flowSteps.ts b/app/create/utils/flowSteps.ts index 818a3fc..fad4baa 100644 --- a/app/create/utils/flowSteps.ts +++ b/app/create/utils/flowSteps.ts @@ -21,8 +21,10 @@ export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [ "community-save", "review", "core-values", - "cards", - "right-rail", + "communication-methods", + "membership-methods", + "decision-approaches", + "conflict-management", "confirm-stakeholders", "final-review", "completed", diff --git a/docs/create-flow.md b/docs/create-flow.md index 031ad0f..94e3d2b 100644 --- a/docs/create-flow.md +++ b/docs/create-flow.md @@ -11,7 +11,7 @@ The Figma **Create Community** sequence is the **source of truth** for the first | Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) | | --- | --- | --- | | **Create Community** | Intro, naming, structure, context, size, upload, save progress (email), then community review. | `informational` → `community-name` → `community-structure` → `community-context` → `community-size` → `community-upload` → `community-save` → `review` | -| **Create Custom CommunityRule** | Author the CommunityRule content and structure. | `cards` → `right-rail` | +| **Create Custom CommunityRule** | Author the CommunityRule content and structure. | `core-values` → `communication-methods` → `right-rail` (further card-stack steps get their own `screenId` and `screens/card/*Screen.tsx`; `right-rail` uses `layoutKind: "right-rail"`) | | **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/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/create/screens/`](../app/create/screens/) mirror that list (one folder per kind; multiple steps may share a kind, e.g. several **select** screens). @@ -34,11 +34,12 @@ Order is defined in code by [`FLOW_STEP_ORDER`](../app/create/utils/flowSteps.ts | 6 | Create Community | `community-upload` | `/create/community-upload` | | 7 | Create Community | `community-save` | `/create/community-save` | | 8 | Create Community (review frame) | `review` | `/create/review` | -| 9 | Create Custom CommunityRule | `cards` | `/create/cards` | -| 10 | Create Custom CommunityRule | `right-rail` | `/create/right-rail` | -| 11 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` | -| 12 | Review and complete | `final-review` | `/create/final-review` | -| 13 | Review and complete | `completed` | `/create/completed` | +| 9 | Create Custom CommunityRule | `core-values` | `/create/core-values` | +| 10 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` | +| 11 | Create Custom CommunityRule | `right-rail` | `/create/right-rail` | +| 12 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` | +| 13 | Review and complete | `final-review` | `/create/final-review` | +| 14 | Review and complete | `completed` | `/create/completed` | **Primary entry:** marketing header “Create rule” navigates to **`/create`**, which redirects to **`/create/informational`** (see [`TopNav.container.tsx`](../app/components/navigation/TopNav/TopNav.container.tsx)). diff --git a/lib/create/migrateLegacyCreateFlowState.ts b/lib/create/migrateLegacyCreateFlowState.ts index 7c87e49..bf6d08a 100644 --- a/lib/create/migrateLegacyCreateFlowState.ts +++ b/lib/create/migrateLegacyCreateFlowState.ts @@ -1,25 +1,24 @@ import type { CreateFlowState } from "../../app/create/types"; +/** Legacy `currentStep` values mapped to the current `CreateFlowStep` id. */ +const LEGACY_CREATE_FLOW_STEP_RENAMES: Readonly> = { + "right-rail": "decision-approaches", +}; + /** - * Maps pre-rename draft keys and step ids (`community-reflection` → `community-save`). - * Safe to run on any parsed draft payload before merging into context. + * Normalizes parsed draft JSON before merging into create-flow context. + * Renames deprecated step ids so old drafts and bookmarks stay valid. */ export function migrateLegacyCreateFlowState( raw: Record | null | undefined, ): CreateFlowState { if (!raw || typeof raw !== "object") return {}; - const next: Record = { ...raw }; - if (typeof next.communityReflection === "string") { - if ( - next.communitySaveEmail === undefined || - next.communitySaveEmail === "" - ) { - next.communitySaveEmail = next.communityReflection; + const step = raw.currentStep; + if (typeof step === "string") { + const next = LEGACY_CREATE_FLOW_STEP_RENAMES[step]; + if (next !== undefined) { + return { ...raw, currentStep: next } as CreateFlowState; } } - delete next.communityReflection; - if (next.currentStep === "community-reflection") { - next.currentStep = "community-save"; - } - return next as CreateFlowState; + return raw as CreateFlowState; } diff --git a/lib/server/validation/createFlowSchemas.ts b/lib/server/validation/createFlowSchemas.ts index b59c3d7..2a7d013 100644 --- a/lib/server/validation/createFlowSchemas.ts +++ b/lib/server/validation/createFlowSchemas.ts @@ -63,6 +63,10 @@ export const createFlowStateSchema = z coreValueDetailsByChipId: z .record(coreValueDetailEntrySchema) .optional(), + selectedCommunicationMethodIds: z.array(z.string()).max(200).optional(), + selectedMembershipMethodIds: z.array(z.string()).max(200).optional(), + selectedDecisionApproachIds: z.array(z.string()).max(200).optional(), + selectedConflictManagementIds: z.array(z.string()).max(200).optional(), currentStep: createFlowStepSchema.optional(), sections: z.array(z.unknown()).optional(), stakeholders: z.array(z.unknown()).optional(), diff --git a/messages/en/create/communication.json b/messages/en/create/communication.json index 99a2976..8225070 100644 --- a/messages/en/create/communication.json +++ b/messages/en/create/communication.json @@ -2,7 +2,9 @@ "_comment": "Create flow – communication step: page, cards, and add-platform modals", "page": { "compactTitle": "How should this community communicate with each-other?", - "compactDescription": "You can select multiple methods for different needs or add your own", + "compactDescriptionBefore": "You can select multiple methods for different needs or ", + "compactDescriptionLinkLabel": "add", + "compactDescriptionAfter": " your own", "expandedTitle": "What method should this community use to communicate with eachother?", "expandedDescription": "You can select multiple methods for different needs or add your own", "seeAllLink": "See all communication approaches" diff --git a/messages/en/create/conflictManagement.json b/messages/en/create/conflictManagement.json new file mode 100644 index 0000000..2a8b625 --- /dev/null +++ b/messages/en/create/conflictManagement.json @@ -0,0 +1,76 @@ +{ + "_comment": "Create flow — conflict management (Figma Flow — Compact Card Stack `20879:15979`)", + "page": { + "compactTitle": "How should conflicts be managed\nin your group?", + "compactDescriptionBefore": "You can also combine or ", + "compactDescriptionLinkLabel": "add", + "compactDescriptionAfter": " new approaches to the list", + "expandedTitle": "How should conflicts be managed in your group?", + "expandedDescription": "You can also combine or add new approaches to the list", + "seeAllLink": "See all conflict management approaches" + }, + "confirmModal": { + "title": "Confirm selection", + "description": "Confirm to select this option.", + "nextButtonText": "Confirm" + }, + "cards": { + "peer-mediation": { + "label": "Peer Mediation", + "supportText": "Trained members within the organization mediate disputes among peers." + }, + "conflict-resolution-council": { + "label": "Conflict Resolution Council", + "supportText": "Senior members with institutional knowledge provide guidance or decisions." + }, + "facilitated-negotiation": { + "label": "Facilitated Negotiation", + "supportText": "A neutral facilitator helps guide the negotiation process." + }, + "ad-hoc-arbitration": { + "label": "Ad Hoc Arbitration", + "supportText": "Arbitrators are chosen specifically for a particular case." + }, + "conflict-workshops": { + "label": "Conflict Workshops", + "supportText": "Structured sessions where parties collaboratively resolve disputes and improve future interactions." + }, + "6": { + "label": "Label", + "supportText": "Additional conflict management approach." + }, + "7": { + "label": "Label", + "supportText": "Additional conflict management approach." + }, + "8": { + "label": "Label", + "supportText": "Additional conflict management approach." + } + }, + "modals": { + "peer-mediation": { + "title": "Peer Mediation", + "description": "Trained members within the organization mediate disputes among peers." + }, + "conflict-resolution-council": { + "title": "Conflict Resolution Council", + "description": "Senior members with institutional knowledge provide guidance or decisions." + }, + "facilitated-negotiation": { + "title": "Facilitated Negotiation", + "description": "A neutral facilitator helps guide the negotiation process." + }, + "ad-hoc-arbitration": { + "title": "Ad Hoc Arbitration", + "description": "Arbitrators are chosen specifically for a particular case." + }, + "conflict-workshops": { + "title": "Conflict Workshops", + "description": "Structured sessions where parties collaboratively resolve disputes and improve future interactions." + }, + "6": { "title": "Label", "description": "Additional conflict management approach." }, + "7": { "title": "Label", "description": "Additional conflict management approach." }, + "8": { "title": "Label", "description": "Additional conflict management approach." } + } +} diff --git a/messages/en/create/footer.json b/messages/en/create/footer.json index 1708b87..24d4b23 100644 --- a/messages/en/create/footer.json +++ b/messages/en/create/footer.json @@ -11,5 +11,9 @@ "confirmMembers": "Confirm members", "finalizeCommunityRule": "Finalize CommunityRule", "confirmStakeholders": "Confirm Stakeholders", - "confirmCoreValues": "Confirm values" + "confirmCoreValues": "Confirm values", + "confirmCommunication": "Confirm", + "confirmMembership": "Confirm", + "confirmRightRail": "Confirm", + "confirmConflictManagement": "Confirm" } diff --git a/messages/en/create/membership.json b/messages/en/create/membership.json new file mode 100644 index 0000000..66b3959 --- /dev/null +++ b/messages/en/create/membership.json @@ -0,0 +1,76 @@ +{ + "_comment": "Create flow — membership / how members join (compact card stack)", + "page": { + "compactTitle": "How do new members join\nand get connected?", + "compactDescriptionBefore": "You can select multiple methods for different needs or ", + "compactDescriptionLinkLabel": "add", + "compactDescriptionAfter": " your own", + "expandedTitle": "How should new members join and get connected?", + "expandedDescription": "You can select multiple methods for different needs or add your own", + "seeAllLink": "See all membership approaches" + }, + "confirmModal": { + "title": "Confirm selection", + "description": "Confirm to select this option.", + "nextButtonText": "Confirm" + }, + "cards": { + "open-access": { + "label": "Open Access", + "supportText": "Maximum inclusion. Anyone can join immediately by simply showing up." + }, + "orientation-required": { + "label": "Orientation Required", + "supportText": "Newcomers must attend a training or orientation session." + }, + "invitation-only": { + "label": "Invitation Only", + "supportText": "New members can only join if they are 'vouched for' by existing members." + }, + "contribution-based": { + "label": "Contribution Based", + "supportText": "Membership is reserved for people contributing their labor." + }, + "mentorship": { + "label": "Mentorship", + "supportText": "New members are paired with 'Mentors' to guide them through a probationary period." + }, + "6": { + "label": "Label", + "supportText": "Additional membership approach." + }, + "7": { + "label": "Label", + "supportText": "Additional membership approach." + }, + "8": { + "label": "Label", + "supportText": "Additional membership approach." + } + }, + "modals": { + "open-access": { + "title": "Open Access", + "description": "Maximum inclusion. Anyone can join immediately by simply showing up." + }, + "orientation-required": { + "title": "Orientation Required", + "description": "Newcomers must attend a training or orientation session." + }, + "invitation-only": { + "title": "Invitation Only", + "description": "New members can only join if they are 'vouched for' by existing members." + }, + "contribution-based": { + "title": "Contribution Based", + "description": "Membership is reserved for people contributing their labor." + }, + "mentorship": { + "title": "Mentorship", + "description": "New members are paired with 'Mentors' to guide them through a probationary period." + }, + "6": { "title": "Label", "description": "Additional membership approach." }, + "7": { "title": "Label", "description": "Additional membership approach." }, + "8": { "title": "Label", "description": "Additional membership approach." } + } +} diff --git a/messages/en/create/rightRail.json b/messages/en/create/rightRail.json index a9a1753..4855828 100644 --- a/messages/en/create/rightRail.json +++ b/messages/en/create/rightRail.json @@ -1,9 +1,10 @@ { + "_comment": "Create flow — right rail / decision approaches (Figma Flow — Right Rail `20523:23509`)", "sidebar": { - "title": "How should conflicts be resolved?", - "descriptionBefore": "You can also combine or ", - "descriptionLink": "add", - "descriptionAfter": " new approaches to the list" + "title": "How should this community make difficult decisions?", + "descriptionBefore": "Select as many as you need to describe how your group makes decisions. You can also ", + "descriptionLinkLabel": "add", + "descriptionAfter": " new decision making approaches or interact with the categories below to filter." }, "messageBox": { "title": "Consider defining approaches to steward key resources:", @@ -16,99 +17,97 @@ }, "cardStack": { "toggleSeeAll": "See all decision approaches", - "toggleShowLess": "Show less", - "emptyTitle": "", - "emptyDescription": "" + "toggleShowLess": "Show less" }, "cards": [ { - "id": "mediation", - "label": "Mediation", - "supportText": "Collaborative work to reach a resolution that all parties can agree upon.", + "id": "lazy-consensus", + "label": "Lazy Consensus", + "supportText": "A decision is assumed approved unless objections are raised within a specified timeframe.", "recommended": true }, { - "id": "facilitation", - "label": "Facilitated dialogue", - "supportText": "Structured sessions where parties collaboratively resolve disputes.", + "id": "do-ocracy", + "label": "Do-ocracy", + "supportText": "Decisions are made by those who take initiative and carry out the work.", "recommended": true }, { - "id": "invite-only", - "label": "Invite-only", - "supportText": "Private discussions with selected participants.", + "id": "consensus-decision-making", + "label": "Consensus Decision-Making", + "supportText": "All members must agree. Best for important decisions in small groups. Does not work well for low stakes decisions.", "recommended": true }, { - "id": "arbitration", - "label": "Arbitration", - "supportText": "Arbitrators are chosen specifically for a particular case.", + "id": "rotational-leadership", + "label": "Rotational Leadership", + "supportText": "Decision-making responsibilities rotate among members.", "recommended": true }, { - "id": "direct-dialogue", - "label": "Direct dialogue", - "supportText": "Encouraging direct, respectful dialogue between those involved.", + "id": "modified-consensus", + "label": "Modified Consensus", + "supportText": "Attempts to reach full agreement first, but falls back to voting if consensus isn’t possible.", "recommended": true }, { "id": "label-1", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-2", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-3", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-4", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-5", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-6", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-7", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-8", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-9", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false }, { "id": "label-10", "label": "Label", - "supportText": "", + "supportText": "Additional decision approach.", "recommended": false } ] diff --git a/messages/en/index.ts b/messages/en/index.ts index 57455fe..32d362d 100644 --- a/messages/en/index.ts +++ b/messages/en/index.ts @@ -19,6 +19,8 @@ import profile from "./pages/profile.json"; import navigation from "./navigation.json"; import metadata from "./metadata.json"; import communication from "./create/communication.json"; +import createMembership from "./create/membership.json"; +import createConflictManagement from "./create/conflictManagement.json"; import createInformational from "./create/informational.json"; import createCommunityName from "./create/communityName.json"; import createCommunitySize from "./create/communitySize.json"; @@ -61,6 +63,8 @@ export default { }, create: { communication, + membership: createMembership, + conflictManagement: createConflictManagement, informational: createInformational, communityName: createCommunityName, communitySize: createCommunitySize, diff --git a/next.config.mjs b/next.config.mjs index 8129e98..afa0af8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -97,6 +97,15 @@ const nextConfig = { return config; }, + async redirects() { + return [ + { + source: "/create/right-rail", + destination: "/create/decision-approaches", + permanent: true, + }, + ]; + }, }; const withMDX = createMDX({ diff --git a/stories/pages/CardsPage.stories.js b/stories/pages/CommunicationMethodsPage.stories.js similarity index 52% rename from stories/pages/CardsPage.stories.js rename to stories/pages/CommunicationMethodsPage.stories.js index b9260a4..b85396c 100644 --- a/stories/pages/CardsPage.stories.js +++ b/stories/pages/CommunicationMethodsPage.stories.js @@ -1,20 +1,20 @@ -import { CardsScreen } from "../../app/create/screens/card/CardsScreen"; +import { CommunicationMethodsScreen } from "../../app/create/screens/card/CommunicationMethodsScreen"; export default { - title: "Pages/Create Flow/Cards", - component: CardsScreen, + title: "Pages/Create Flow/Communication methods", + component: CommunicationMethodsScreen, parameters: { layout: "fullscreen", docs: { description: { component: - "Communication / card selection step with modals and responsive layout.", + "Communication methods step (`/create/communication-methods`): card stack, modals, responsive layout.", }, }, }, decorators: [ (Story) => ( -
+
), diff --git a/stories/progress/ProportionBar.stories.js b/stories/progress/ProportionBar.stories.js index 7b243af..9907056 100644 --- a/stories/progress/ProportionBar.stories.js +++ b/stories/progress/ProportionBar.stories.js @@ -31,6 +31,7 @@ export default { "2-0", "2-1", "2-2", + "2-3", "3-0", "3-1", "3-2", @@ -86,6 +87,7 @@ export const AllStates = { + diff --git a/tests/components/ProportionBar.test.tsx b/tests/components/ProportionBar.test.tsx index 182699b..abd28bb 100644 --- a/tests/components/ProportionBar.test.tsx +++ b/tests/components/ProportionBar.test.tsx @@ -40,8 +40,8 @@ describe("ProportionBar (behavioral tests)", () => { it("renders proportion bar with correct progress value", () => { render(); const progressbar = screen.getByRole("progressbar"); - // 2-1: First section full (1) + second section 1/3 filled = 1 + 1/3 ≈ 1.333 - expect(progressbar).toHaveAttribute("aria-valuenow", "1.3333333333333333"); + // 2-1 (Figma `17861:33241`): first section full + second section 1/4 filled = 1.25. + expect(progressbar).toHaveAttribute("aria-valuenow", "1.25"); expect(progressbar).toHaveAttribute("aria-valuemin", "0"); expect(progressbar).toHaveAttribute("aria-valuemax", "3"); }); @@ -63,7 +63,8 @@ describe("ProportionBar (behavioral tests)", () => { { progress: "1-0" as const, expected: 1 / 6 }, // First section 1/6 filled { progress: "1-5" as const, expected: 1 }, // First section 6/6 filled (fully filled) { progress: "2-0" as const, expected: 1 }, // First section full, second empty - { progress: "2-2" as const, expected: 1 + 2 / 3 }, // First section full, second section 2/3 filled + { progress: "2-2" as const, expected: 1 + 1 / 2 }, // 1 + 1/2 per Figma `18861:15250` + { progress: "2-3" as const, expected: 1 + 3 / 4 }, // 1 + 3/4 per Figma `21434:17632` { progress: "3-0" as const, expected: 2 }, // First two sections full, third empty { progress: "3-2" as const, expected: 2 + 2 / 3 }, // First two sections full, third section 2/3 filled ]; diff --git a/tests/pages/cards.test.jsx b/tests/pages/communication-methods.test.jsx similarity index 76% rename from tests/pages/cards.test.jsx rename to tests/pages/communication-methods.test.jsx index ef0efc9..7c8441e 100644 --- a/tests/pages/cards.test.jsx +++ b/tests/pages/communication-methods.test.jsx @@ -6,16 +6,16 @@ import { } from "../utils/test-utils"; import userEvent from "@testing-library/user-event"; import { describe, test, expect, afterEach } from "vitest"; -import { CardsScreen } from "../../app/create/screens/card/CardsScreen"; +import { CommunicationMethodsScreen } from "../../app/create/screens/card/CommunicationMethodsScreen"; afterEach(() => { cleanup(); }); -describe("Create flow cards page", () => { +describe("Create flow communication-methods page", () => { test("clicking a card opens the Create modal", async () => { const user = userEvent.setup(); - render(); + render(); const signalCards = screen.getAllByRole("button", { name: /Signal: Encrypted messaging/, @@ -29,7 +29,7 @@ describe("Create flow cards page", () => { }); test("renders without error", () => { - render(); + render(); expect( screen.getByText( @@ -39,13 +39,12 @@ describe("Create flow cards page", () => { }); test("renders HeaderLockup and CardStack content", () => { - render(); + render(); expect( - screen.getByText( - "You can select multiple methods for different needs or add your own", - ), + screen.getByText(/You can select multiple methods for different needs or/), ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "add" })).toBeInTheDocument(); expect( screen.getByRole("button", { name: "See all communication approaches" }), ).toBeInTheDocument(); @@ -53,7 +52,7 @@ describe("Create flow cards page", () => { test("toggle expands and shows Show less", async () => { const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole("button", { name: "See all communication approaches", diff --git a/tests/pages/right-rail.test.jsx b/tests/pages/right-rail.test.jsx index 2b1386f..472df10 100644 --- a/tests/pages/right-rail.test.jsx +++ b/tests/pages/right-rail.test.jsx @@ -18,7 +18,7 @@ describe("Create flow right-rail page", () => { expect( screen.getByRole("heading", { - name: "How should conflicts be resolved?", + name: "How should this community make difficult decisions?", }), ).toBeInTheDocument(); }); @@ -30,9 +30,9 @@ describe("Create flow right-rail page", () => { if (element?.tagName !== "P") return false; const text = element.textContent ?? ""; return ( - text.includes("You can also combine or") && + text.includes("Select as many as you need") && text.includes("add") && - text.includes("new approaches to the list") + text.includes("new decision making approaches") ); }); expect(description).toBeInTheDocument(); @@ -77,17 +77,17 @@ describe("Create flow right-rail page", () => { expect( screen.getByRole("button", { - name: /Mediation: Collaborative work to reach a resolution/, + name: /Lazy Consensus: A decision is assumed approved/, }), ).toBeInTheDocument(); expect( screen.getByRole("button", { - name: /Facilitated dialogue: Structured sessions/, + name: /Do-ocracy: Decisions are made by those who take initiative/, }), ).toBeInTheDocument(); expect( screen.getByRole("button", { - name: /Invite-only: Private discussions with selected participants/, + name: /Consensus Decision-Making: All members must agree/, }), ).toBeInTheDocument(); }); @@ -123,10 +123,10 @@ describe("Create flow right-rail page", () => { const user = userEvent.setup(); render(); - const mediationCard = screen.getByRole("button", { - name: /Mediation: Collaborative work to reach a resolution/, + const card = screen.getByRole("button", { + name: /Lazy Consensus: A decision is assumed approved/, }); - await user.click(mediationCard); + await user.click(card); expect(screen.getByText("SELECTED")).toBeInTheDocument(); }); diff --git a/tests/unit/createFlowProportionProgress.test.ts b/tests/unit/createFlowProportionProgress.test.ts index aa0534c..2733a50 100644 --- a/tests/unit/createFlowProportionProgress.test.ts +++ b/tests/unit/createFlowProportionProgress.test.ts @@ -26,4 +26,28 @@ describe("getProportionBarProgressForCreateFlowStep", () => { "2-0", ); }); + + it("uses 2-1 on communication-methods", () => { + expect( + getProportionBarProgressForCreateFlowStep("communication-methods"), + ).toBe("2-1"); + }); + + it("uses 2-2 on membership-methods", () => { + expect( + getProportionBarProgressForCreateFlowStep("membership-methods"), + ).toBe("2-2"); + }); + + it("uses 2-3 on decision-approaches (Figma Flow — Right Rail)", () => { + expect( + getProportionBarProgressForCreateFlowStep("decision-approaches"), + ).toBe("2-3"); + }); + + it("uses 3-0 on conflict-management (start of Review segment)", () => { + expect( + getProportionBarProgressForCreateFlowStep("conflict-management"), + ).toBe("3-0"); + }); }); diff --git a/tests/unit/createFlowValidation.test.ts b/tests/unit/createFlowValidation.test.ts index 363ec0c..45fe037 100644 --- a/tests/unit/createFlowValidation.test.ts +++ b/tests/unit/createFlowValidation.test.ts @@ -56,7 +56,7 @@ describe("createFlowStateSchema", () => { it("accepts known fields and passthrough keys", () => { const r = createFlowStateSchema.safeParse({ title: "My rule", - currentStep: "cards", + currentStep: "communication-methods", customField: { nested: [1, 2] }, }); expect(r.success).toBe(true); diff --git a/tests/unit/flowSteps.test.ts b/tests/unit/flowSteps.test.ts index cfc0cec..f4d3b49 100644 --- a/tests/unit/flowSteps.test.ts +++ b/tests/unit/flowSteps.test.ts @@ -16,7 +16,10 @@ describe("flowSteps", () => { }); it("getNextStep returns next step in order", () => { - expect(getNextStep("right-rail")).toBe("confirm-stakeholders"); + expect(getNextStep("communication-methods")).toBe("membership-methods"); + expect(getNextStep("membership-methods")).toBe("decision-approaches"); + expect(getNextStep("decision-approaches")).toBe("conflict-management"); + expect(getNextStep("conflict-management")).toBe("confirm-stakeholders"); expect(getNextStep("confirm-stakeholders")).toBe("final-review"); }); @@ -67,6 +70,6 @@ describe("flowSteps", () => { const opts = { skipCommunitySave: true } as const; expect(getNextStep("community-size", opts)).toBe("community-upload"); expect(getNextStep("review", opts)).toBe("core-values"); - expect(getPreviousStep("cards", opts)).toBe("core-values"); + expect(getPreviousStep("communication-methods", opts)).toBe("core-values"); }); }); diff --git a/tests/unit/migrateLegacyCreateFlowState.test.ts b/tests/unit/migrateLegacyCreateFlowState.test.ts index acd2fae..a06b829 100644 --- a/tests/unit/migrateLegacyCreateFlowState.test.ts +++ b/tests/unit/migrateLegacyCreateFlowState.test.ts @@ -2,27 +2,12 @@ import { describe, it, expect } from "vitest"; import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState"; describe("migrateLegacyCreateFlowState", () => { - it("maps communityReflection to communitySaveEmail when save email empty", () => { + it("passes through object payloads", () => { const out = migrateLegacyCreateFlowState({ title: "T", - communityReflection: "old@example.com", - }); - expect(out.communitySaveEmail).toBe("old@example.com"); - expect("communityReflection" in out).toBe(false); - }); - - it("does not overwrite existing communitySaveEmail", () => { - const out = migrateLegacyCreateFlowState({ - communityReflection: "old@example.com", - communitySaveEmail: "kept@example.com", - }); - expect(out.communitySaveEmail).toBe("kept@example.com"); - }); - - it("rewrites currentStep slug", () => { - const out = migrateLegacyCreateFlowState({ - currentStep: "community-reflection", + currentStep: "community-save", }); + expect(out.title).toBe("T"); expect(out.currentStep).toBe("community-save"); }); @@ -30,4 +15,13 @@ describe("migrateLegacyCreateFlowState", () => { expect(migrateLegacyCreateFlowState(null)).toEqual({}); expect(migrateLegacyCreateFlowState(undefined)).toEqual({}); }); + + it("renames legacy right-rail step to decision-approaches", () => { + const out = migrateLegacyCreateFlowState({ + currentStep: "right-rail", + title: "T", + }); + expect(out.currentStep).toBe("decision-approaches"); + expect(out.title).toBe("T"); + }); });