App reorganization

This commit is contained in:
adilallo
2026-04-18 14:12:49 -06:00
parent f866d11ff8
commit e9dab04b34
288 changed files with 2698 additions and 5029 deletions
@@ -0,0 +1,93 @@
"use client";
import type { ReactNode } from "react";
import type { CreateFlowStep } from "../types";
import { InformationalScreen } from "./informational/InformationalScreen";
import { CreateFlowTextFieldScreen } from "./text/CreateFlowTextFieldScreen";
import { CommunitySizeSelectScreen } from "./select/CommunitySizeSelectScreen";
import { CommunityStructureSelectScreen } from "./select/CommunityStructureSelectScreen";
import { CoreValuesSelectScreen } from "./select/CoreValuesSelectScreen";
import { ConfirmStakeholdersScreen } from "./select/ConfirmStakeholdersScreen";
import { CommunityUploadScreen } from "./upload/CommunityUploadScreen";
import { CommunityReviewScreen } from "./review/CommunityReviewScreen";
import { FinalReviewScreen } from "./review/FinalReviewScreen";
import { CommunicationMethodsScreen } from "./card/CommunicationMethodsScreen";
import { MembershipMethodsScreen } from "./card/MembershipMethodsScreen";
import { ConflictManagementScreen } from "./card/ConflictManagementScreen";
import { DecisionApproachesScreen } from "./right-rail/DecisionApproachesScreen";
import { CompletedScreen } from "./completed/CompletedScreen";
/**
* Maps each wizard `screenId` to its screen component.
*
* **Folder rule (Figma):** subfolders match `CREATE_FLOW_SCREEN_REGISTRY[].layoutKind`
* — `select/` (two-column chip flows), `card/` (compact card-stack steps), `text/`, etc.
* The URL segment (`communication-methods`) is not the folder name; see `createFlowScreenRegistry.ts`.
*/
export function CreateFlowScreenView({
screenId,
}: {
screenId: CreateFlowStep;
}): ReactNode {
switch (screenId) {
case "informational":
return <InformationalScreen />;
case "community-name":
return (
<CreateFlowTextFieldScreen
messageNamespace="create.communityName"
stateField="title"
maxLength={48}
/>
);
case "community-structure":
return <CommunityStructureSelectScreen />;
case "community-context":
return (
<CreateFlowTextFieldScreen
messageNamespace="create.communityContext"
stateField="communityContext"
maxLength={48}
mainAlign="center"
/>
);
case "community-size":
return <CommunitySizeSelectScreen />;
case "community-upload":
return <CommunityUploadScreen />;
case "community-save":
return (
<CreateFlowTextFieldScreen
messageNamespace="create.communitySave"
stateField="communitySaveEmail"
maxLength={254}
mainAlign="center"
inputType="email"
showCharacterCount={false}
headerJustification="center"
/>
);
case "review":
return <CommunityReviewScreen />;
case "core-values":
return <CoreValuesSelectScreen />;
case "communication-methods":
return <CommunicationMethodsScreen />;
case "membership-methods":
return <MembershipMethodsScreen />;
case "decision-approaches":
return <DecisionApproachesScreen />;
case "conflict-management":
return <ConflictManagementScreen />;
case "confirm-stakeholders":
return <ConfirmStakeholdersScreen />;
case "final-review":
return <FinalReviewScreen />;
case "completed":
return <CompletedScreen />;
default: {
const _exhaustive: never = screenId;
return _exhaustive;
}
}
}
@@ -0,0 +1,266 @@
"use client";
/**
* `communication-methods` step — Figma “Flow — Compact Card Stack” (node `20246-15828`).
* Registry: `layoutKind: "card"` (`CREATE_FLOW_SCREEN_REGISTRY["communication-methods"]`).
*
* Lives under `screens/card/` (not `select/`): Figma **card stack** layout is a distinct shell from
* two-column chip **select** frames. Future card-stack steps get their own `*Screen.tsx` here and
* reuse `CardStack` / `CreateFlowStepShell` as needed.
*/
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 InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import ModalTextAreaField from "../../components/ModalTextAreaField";
const IN_PERSON_CARD_ID = "in-person-meetings";
const SIGNAL_CARD_ID = "signal";
const VIDEO_MEETINGS_CARD_ID = "video-meetings";
const SECTION_FIELDS = [
"corePrinciple",
"logisticsAdmin",
"codeOfConduct",
] as const;
type SectionField = (typeof SECTION_FIELDS)[number];
const COMMUNICATION_CARD_ORDER = [
IN_PERSON_CARD_ID,
SIGNAL_CARD_ID,
VIDEO_MEETINGS_CARD_ID,
"4",
"5",
"6",
"7",
] as const;
function AddPlatformModalContent({
platformCardId,
}: {
platformCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const comm = m.create.communication;
const modal =
platformCardId in comm.modals
? comm.modals[platformCardId as keyof typeof comm.modals]
: null;
const defaults = modal?.sections ?? {
corePrinciple: "",
logisticsAdmin: "",
codeOfConduct: "",
};
const [sectionValues, setSectionValues] = useState<
Record<SectionField, string>
>(() => ({
corePrinciple: defaults.corePrinciple,
logisticsAdmin: defaults.logisticsAdmin,
codeOfConduct: defaults.codeOfConduct,
}));
const updateSection = useCallback(
(key: SectionField, value: string) => {
markCreateFlowInteraction();
setSectionValues((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
{SECTION_FIELDS.map((field) => (
<ModalTextAreaField
key={field}
label={comm.sectionHeadings[field]}
rows={6}
value={sectionValues[field]}
onChange={(v) => updateSection(field, v)}
/>
))}
</div>
);
}
export function CommunicationMethodsScreen() {
const m = useMessages();
const comm = m.create.communication;
const mdUp = useCreateFlowMdUp();
const { state, updateState, markCreateFlowInteraction } = useCreateFlow();
const [expanded, setExpanded] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
const selectedIds = state.selectedCommunicationMethodIds ?? [];
const setSelectedIds = useCallback(
(next: string[]) => {
updateState({ selectedCommunicationMethodIds: next });
},
[updateState],
);
const sampleCards = useMemo(
() =>
COMMUNICATION_CARD_ORDER.map((id) => {
const row = comm.cards[id as keyof typeof comm.cards];
return {
id,
label: row.label,
supportText: row.supportText,
recommended: true,
};
}),
[comm],
);
const title = expanded ? comm.page.expandedTitle : comm.page.compactTitle;
const description = expanded ? (
comm.page.expandedDescription
) : (
<>
{comm.page.compactDescriptionBefore}
<InlineTextButton
onClick={() => {
markCreateFlowInteraction();
setExpanded(true);
}}
>
{comm.page.compactDescriptionLinkLabel}
</InlineTextButton>
{comm.page.compactDescriptionAfter}
</>
);
const modalConfig = (() => {
if (!pendingCardId) {
return {
title: comm.confirmModal.title,
description: comm.confirmModal.description,
nextButtonText: comm.confirmModal.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
if (pendingCardId in comm.modals) {
const modal = comm.modals[pendingCardId as keyof typeof comm.modals];
return {
title: modal.title,
description: modal.description,
nextButtonText: comm.addPlatform.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
const cardRow =
pendingCardId in comm.cards
? comm.cards[pendingCardId as keyof typeof comm.cards]
: null;
return {
title: cardRow?.label ?? comm.confirmModal.title,
description: cardRow?.supportText ?? comm.confirmModal.description,
nextButtonText: comm.addPlatform.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 (
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-800"
>
<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}>
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={comm.page.seeAllLink}
compactRecommendedLimit={3}
compactDesktopLayout="flexWrap"
headerLockupSize={mdUp ? "L" : "M"}
/>
</div>
</div>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
onNext={handleCreateModalConfirm}
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={modalConfig.showBackButton}
currentStep={modalConfig.currentStep}
totalSteps={modalConfig.totalSteps}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddPlatformModalContent
key={pendingCardId}
platformCardId={pendingCardId}
/>
) : null}
</Create>
</CreateFlowStepShell>
);
}
@@ -0,0 +1,296 @@
"use client";
/**
* `conflict-management` step — Figma compact card stack (node `20879-15979`).
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["conflict-management"]`.
*
* Card click opens the Figma "Add Approach" create modal (node `20874-172292`) with four
* controls: Core Principle, Applicable Scope (capsules), Process Protocol, and Restoration
* & Fallbacks. Section defaults are sourced from
* `messages/en/create/conflictManagement.json` and will be replaced with DB-driven
* content; labels are hard-coded per the Figma design.
*/
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 InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import ModalTextAreaField from "../../components/ModalTextAreaField";
import ApplicableScopeField from "../../components/ApplicableScopeField";
const CONFLICT_CARD_ORDER = [
"peer-mediation",
"conflict-resolution-council",
"facilitated-negotiation",
"ad-hoc-arbitration",
"conflict-workshops",
"6",
"7",
"8",
] as const;
type ConflictModalSections = {
corePrinciple: string;
applicableScope: string[];
selectedApplicableScope: string[];
processProtocol: string;
restorationFallbacks: string;
};
function AddConflictApproachModalContent({
approachCardId,
}: {
approachCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const cm = m.create.conflictManagement;
const modal =
approachCardId in cm.modals
? cm.modals[approachCardId as keyof typeof cm.modals]
: null;
const modalSections = modal?.sections;
const defaults: ConflictModalSections = {
corePrinciple: modalSections?.corePrinciple ?? "",
applicableScope: modalSections?.applicableScope ?? [],
selectedApplicableScope: [],
processProtocol: modalSections?.processProtocol ?? "",
restorationFallbacks: modalSections?.restorationFallbacks ?? "",
};
const [sections, setSections] = useState<ConflictModalSections>(() => ({
corePrinciple: defaults.corePrinciple,
applicableScope: [...defaults.applicableScope],
selectedApplicableScope: [...defaults.selectedApplicableScope],
processProtocol: defaults.processProtocol,
restorationFallbacks: defaults.restorationFallbacks,
}));
const patch = useCallback(
<K extends keyof ConflictModalSections>(
key: K,
value: ConflictModalSections[K],
) => {
markCreateFlowInteraction();
setSections((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
<ModalTextAreaField
label={cm.sectionHeadings.corePrinciple}
value={sections.corePrinciple}
onChange={(v) => patch("corePrinciple", v)}
/>
<ApplicableScopeField
label={cm.sectionHeadings.applicableScope}
addLabel={cm.scopeAddButtonLabel}
scopes={sections.applicableScope}
selectedScopes={sections.selectedApplicableScope}
onToggleScope={(scope) =>
patch(
"selectedApplicableScope",
sections.selectedApplicableScope.includes(scope)
? sections.selectedApplicableScope.filter((s) => s !== scope)
: [...sections.selectedApplicableScope, scope],
)
}
onAddScope={(scope) =>
patch("applicableScope", [...sections.applicableScope, scope])
}
/>
<ModalTextAreaField
label={cm.sectionHeadings.processProtocol}
value={sections.processProtocol}
onChange={(v) => patch("processProtocol", v)}
/>
<ModalTextAreaField
label={cm.sectionHeadings.restorationFallbacks}
value={sections.restorationFallbacks}
onChange={(v) => patch("restorationFallbacks", v)}
/>
</div>
);
}
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<string | null>(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}
<InlineTextButton
onClick={() => {
markCreateFlowInteraction();
setExpanded(true);
}}
>
{cm.page.compactDescriptionLinkLabel}
</InlineTextButton>
{cm.page.compactDescriptionAfter}
</>
);
const modalConfig = (() => {
if (!pendingCardId) {
return {
title: cm.confirmModal.title,
description: cm.confirmModal.description,
nextButtonText: cm.confirmModal.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
if (pendingCardId in cm.modals) {
const modal = cm.modals[pendingCardId as keyof typeof cm.modals];
return {
title: modal.title,
description: modal.description,
nextButtonText: cm.addApproach.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
const cardRow =
pendingCardId in cm.cards
? cm.cards[pendingCardId as keyof typeof cm.cards]
: null;
return {
title: cardRow?.label ?? cm.confirmModal.title,
description: cardRow?.supportText ?? cm.confirmModal.description,
nextButtonText: cm.addApproach.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 (
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-800"
>
<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}>
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={cm.page.seeAllLink}
compactRecommendedLimit={5}
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
/>
</div>
</div>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
onNext={handleCreateModalConfirm}
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={modalConfig.showBackButton}
currentStep={modalConfig.currentStep}
totalSteps={modalConfig.totalSteps}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddConflictApproachModalContent
key={pendingCardId}
approachCardId={pendingCardId}
/>
) : null}
</Create>
</CreateFlowStepShell>
);
}
@@ -0,0 +1,265 @@
"use client";
/**
* `membership-methods` step — Figma compact card stack (node `20858-13947`).
* Registry: `CREATE_FLOW_SCREEN_REGISTRY["membership-methods"]`.
*
* Card click opens the Figma create modal (node `20858-13948`) with three
* editable sections — Eligibility & Philosophy, Joining Process, and
* Expectations & Removal. Section defaults come from
* `messages/en/create/membership.json` and will be replaced with DB-driven
* content.
*/
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 InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
} from "../../components/createFlowLayoutTokens";
import ModalTextAreaField from "../../components/ModalTextAreaField";
const SECTION_FIELDS = [
"eligibility",
"joiningProcess",
"expectations",
] as const;
type SectionField = (typeof SECTION_FIELDS)[number];
const MEMBERSHIP_CARD_ORDER = [
"open-access",
"orientation-required",
"invitation-only",
"contribution-based",
"mentorship",
"6",
"7",
"8",
] as const;
function AddMembershipModalContent({
membershipCardId,
}: {
membershipCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const mem = m.create.membership;
const modal =
membershipCardId in mem.modals
? mem.modals[membershipCardId as keyof typeof mem.modals]
: null;
const defaults = modal?.sections ?? {
eligibility: "",
joiningProcess: "",
expectations: "",
};
const [sectionValues, setSectionValues] = useState<
Record<SectionField, string>
>(() => ({
eligibility: defaults.eligibility,
joiningProcess: defaults.joiningProcess,
expectations: defaults.expectations,
}));
const updateSection = useCallback(
(key: SectionField, value: string) => {
markCreateFlowInteraction();
setSectionValues((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
{SECTION_FIELDS.map((field) => (
<ModalTextAreaField
key={field}
label={mem.sectionHeadings[field]}
rows={6}
value={sectionValues[field]}
onChange={(v) => updateSection(field, v)}
/>
))}
</div>
);
}
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<string | null>(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}
<InlineTextButton
onClick={() => {
markCreateFlowInteraction();
setExpanded(true);
}}
>
{mem.page.compactDescriptionLinkLabel}
</InlineTextButton>
{mem.page.compactDescriptionAfter}
</>
);
const modalConfig = (() => {
if (!pendingCardId) {
return {
title: mem.confirmModal.title,
description: mem.confirmModal.description,
nextButtonText: mem.confirmModal.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
if (pendingCardId in mem.modals) {
const modal = mem.modals[pendingCardId as keyof typeof mem.modals];
return {
title: modal.title,
description: modal.description,
nextButtonText: mem.addPlatform.nextButtonText,
showBackButton: false as const,
currentStep: undefined,
totalSteps: undefined,
};
}
const cardRow =
pendingCardId in mem.cards
? mem.cards[pendingCardId as keyof typeof mem.cards]
: null;
return {
title: cardRow?.label ?? mem.confirmModal.title,
description: cardRow?.supportText ?? mem.confirmModal.description,
nextButtonText: mem.addPlatform.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 (
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-800"
>
<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}>
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardClick}
expanded={expanded}
onToggleExpand={() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}}
hasMore={true}
toggleLabel={mem.page.seeAllLink}
compactRecommendedLimit={5}
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
/>
</div>
</div>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
onNext={handleCreateModalConfirm}
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={modalConfig.showBackButton}
currentStep={modalConfig.currentStep}
totalSteps={modalConfig.totalSteps}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddMembershipModalContent
key={pendingCardId}
membershipCardId={pendingCardId}
/>
) : null}
</Create>
</CreateFlowStepShell>
);
}
@@ -0,0 +1,108 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import CommunityRuleDocument from "../../../../components/sections/CommunityRuleDocument";
import type { CommunityRuleDocumentSection } from "../../../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
import Alert from "../../../../components/modals/Alert";
import { useMessages } from "../../../../contexts/MessagesContext";
import { parseDocumentSectionsForDisplay } from "../../../../../lib/create/buildPublishPayload";
import { readLastPublishedRule } from "../../../../../lib/create/lastPublishedRule";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import {
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "../../components/createFlowLayoutTokens";
export function CompletedScreen() {
const mdUp = useCreateFlowMdUp();
const m = useMessages();
const completed = m.create.completed;
const fallbackSections = useMemo(
() =>
[...completed.fallbackDocumentSections] as CommunityRuleDocumentSection[],
[completed.fallbackDocumentSections],
);
const [toastDismissed, setToastDismissed] = useState(false);
const [headerTitle, setHeaderTitle] = useState(
() => completed.fallbackTitle,
);
const [headerDescription, setHeaderDescription] = useState<
string | undefined
>(() => completed.fallbackDescription);
const [documentSections, setDocumentSections] =
useState<CommunityRuleDocumentSection[]>(fallbackSections);
useEffect(() => {
const stored = readLastPublishedRule();
if (!stored) return;
const parsed = parseDocumentSectionsForDisplay(stored.document);
if (parsed.length === 0) return;
queueMicrotask(() => {
setDocumentSections(parsed);
setHeaderTitle(stored.title);
const sum =
typeof stored.summary === "string" ? stored.summary.trim() : "";
setHeaderDescription(sum.length > 0 ? sum : undefined);
});
}, []);
const toast = !toastDismissed ? (
<div
className="fixed bottom-0 left-0 right-0 z-10 w-full"
role="status"
aria-live="polite"
>
<Alert
type="toast"
status="default"
title={completed.toastTitle}
description={completed.toastDescription}
hasLeadingIcon
hasBodyText
onClose={() => setToastDismissed(true)}
className="w-full"
/>
</div>
) : null;
return (
<>
<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:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 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}`}
>
<div
className={`flex flex-col justify-start overflow-hidden md:justify-center md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
>
<CreateFlowHeaderLockup
title={headerTitle}
description={headerDescription}
justification="left"
size="L"
palette="inverse"
/>
</div>
<div
className={`scrollbar-hide relative flex min-h-0 flex-col overflow-x-hidden md:overflow-y-auto ${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"
aria-hidden
/>
<div className="w-full min-w-0 py-0 md:pb-8">
<CommunityRuleDocument
sections={documentSections}
useCardStyle={!mdUp}
className={mdUp ? "min-w-0" : "w-full min-w-0 p-4"}
/>
</div>
</div>
</div>
</div>
{toast}
</>
);
}
@@ -0,0 +1,67 @@
"use client";
import type { ReactNode } from "react";
import NumberedList from "../../../../components/type/NumberedList";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
/**
* Create Community — frame 1 (Figma [20094-16005](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20094-16005)).
* URL: /create/informational
*/
export function InformationalScreen() {
const mdUp = useCreateFlowMdUp();
const copy = useMessages().create.informational;
const items = [
{
title: copy.steps["0"].title,
description: copy.steps["0"].description,
},
{
title: copy.steps["1"].title,
description: copy.steps["1"].description,
},
{
title: copy.steps["2"].title,
description: copy.steps["2"].description,
},
];
const description: ReactNode = (
<>
{copy.descriptionLead}{" "}
<a
href="#"
className="font-inter font-normal text-[var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] cursor-pointer"
onClick={(e) => {
e.preventDefault();
}}
>
{copy.workshopLabel}
</a>{" "}
{copy.descriptionTrail}
</>
);
return (
<CreateFlowStepShell
variant="centeredNarrow"
contentTopBelowMd="space-1400"
>
<div
className={`flex flex-col items-center gap-[var(--measures-spacing-1200,48px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
>
<CreateFlowHeaderLockup
title={copy.title}
description={description}
justification="left"
/>
<NumberedList items={items} size={mdUp ? "M" : "S"} />
</div>
</CreateFlowStepShell>
);
}
@@ -0,0 +1,61 @@
"use client";
import RuleCard from "../../../../components/cards/RuleCard";
import { useTranslation } from "../../../../contexts/MessagesContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowLgUp } from "../../hooks/useCreateFlowLgUp";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "../../components/createFlowLayoutTokens";
/** Create Community review — Figma `19706:12135` (`/create/review`; two columns from `lg:`; column caps in `createFlowLayoutTokens`). */
export function CommunityReviewScreen() {
const lgUp = useCreateFlowLgUp();
const t = useTranslation("create.review");
const { state } = useCreateFlow();
const cardTitle =
typeof state.title === "string" && state.title.trim().length > 0
? state.title.trim()
: t("ruleCard.title");
const cardDescription =
typeof state.communityContext === "string" &&
state.communityContext.trim().length > 0
? state.communityContext.trim()
: t("ruleCard.description");
return (
<CreateFlowStepShell
variant="wideGridLoosePadding"
contentTopBelowMd="space-1400"
>
<div
className={`flex w-full min-w-0 flex-col items-center gap-6 lg:mx-auto lg:w-full lg:grid lg:grid-cols-2 lg:items-center lg:justify-items-center lg:gap-x-[var(--measures-spacing-1200,48px)] lg:gap-y-6 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
>
<div
className={`flex flex-col justify-center lg:min-h-[212px] ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
>
<CreateFlowHeaderLockup
title={t("header.title")}
description={t("header.description")}
/>
</div>
<div className={CREATE_FLOW_MD_UP_GRID_CELL_CLASS}>
<RuleCard
title={cardTitle}
description={cardDescription}
size={lgUp ? "L" : "M"}
expanded={false}
backgroundColor="bg-[var(--color-teal-teal50,#c9fef9)]"
logoUrl="/assets/Vector_MutualAid.svg"
logoAlt={cardTitle}
className="rounded-[24px]"
/>
</div>
</div>
</CreateFlowStepShell>
);
}
@@ -0,0 +1,68 @@
"use client";
import { useMemo } from "react";
import RuleCard from "../../../../components/cards/RuleCard";
import type { Category } from "../../../../components/cards/RuleCard/RuleCard.types";
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import {
CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS,
CreateFlowLockupCardStepShell,
} from "../../components/CreateFlowLockupCardStepShell";
function buildFinalReviewCategories(
rows: { name: string; chips: string[] }[],
): Category[] {
return rows.map((cat) => ({
name: cat.name,
chipOptions: cat.chips.map((label, idx) => ({
id: `${cat.name}-${idx}`,
label,
state: "unselected" as const,
})),
}));
}
export function FinalReviewScreen() {
const { state } = useCreateFlow();
const mdUp = useCreateFlowMdUp();
const t = useTranslation("create.finalReview");
const m = useMessages();
const finalReviewCategories = useMemo(
() => buildFinalReviewCategories(m.create.finalReview.categories),
[m.create.finalReview.categories],
);
const ruleCardTitle = useMemo(() => {
const raw = typeof state.title === "string" ? state.title.trim() : "";
return raw.length > 0 ? raw : t("ruleCardTitleFallback");
}, [state.title, t]);
const ruleCardDescription = useMemo(() => {
const raw =
typeof state.summary === "string" ? state.summary.trim() : "";
return raw.length > 0 ? raw : t("ruleCardDescriptionFallback");
}, [state.summary, t]);
return (
<CreateFlowLockupCardStepShell
lockupTitle={t("title")}
lockupDescription={t("description")}
>
<RuleCard
title={ruleCardTitle}
description={ruleCardDescription}
size={mdUp ? "L" : "M"}
expanded={true}
backgroundColor="bg-[#c9fef9]"
logoUrl="/assets/Vector_MutualAid.svg"
logoAlt={ruleCardTitle}
categories={finalReviewCategories}
className={CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS}
onClick={() => {}}
/>
</CreateFlowLockupCardStepShell>
);
}
@@ -0,0 +1,320 @@
"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.
*
* Card click opens the Figma "Add Approach" create modal (node `20870-72155`) with five controls:
* Core Principle, Applicable Scope, Step-by-Step Instructions, Consensus Level, and Objections &
* Deadlocks. Section defaults are sourced from `messages/en/create/rightRail.json` and will be
* replaced with DB-driven content; labels are hard-coded per the Figma design.
*/
import { useState, useCallback, useMemo } from "react";
import DecisionMakingSidebar from "../../../../components/utility/DecisionMakingSidebar";
import CardStack from "../../../../components/utility/CardStack";
import Create from "../../../../components/modals/Create";
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import type { InfoMessageBoxItem } from "../../../../components/utility/InfoMessageBox/InfoMessageBox.types";
import type { CardStackItem } from "../../../../components/utility/CardStack/CardStack.types";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import ModalTextAreaField from "../../components/ModalTextAreaField";
import ApplicableScopeField from "../../components/ApplicableScopeField";
const CONSENSUS_LEVEL_MIN = 0;
const CONSENSUS_LEVEL_MAX = 100;
const CONSENSUS_LEVEL_STEP = 5;
const CONSENSUS_LEVEL_DEFAULT = 75;
type RightRailModalSections = {
corePrinciple: string;
applicableScope: string[];
selectedApplicableScope: string[];
stepByStepInstructions: string;
consensusLevel: number;
objectionsDeadlocks: string;
};
function AddDecisionApproachModalContent({
approachCardId,
}: {
approachCardId: string;
}) {
const { markCreateFlowInteraction } = useCreateFlow();
const m = useMessages();
const rr = m.create.rightRail;
const modal =
approachCardId in rr.modals
? rr.modals[approachCardId as keyof typeof rr.modals]
: null;
const modalSections = modal?.sections;
const defaults: RightRailModalSections = {
corePrinciple: modalSections?.corePrinciple ?? "",
applicableScope: modalSections?.applicableScope ?? [],
selectedApplicableScope: [],
stepByStepInstructions: modalSections?.stepByStepInstructions ?? "",
consensusLevel: modalSections?.consensusLevel ?? CONSENSUS_LEVEL_DEFAULT,
objectionsDeadlocks: modalSections?.objectionsDeadlocks ?? "",
};
const [sections, setSections] = useState<RightRailModalSections>(() => ({
corePrinciple: defaults.corePrinciple,
applicableScope: [...defaults.applicableScope],
selectedApplicableScope: [...defaults.selectedApplicableScope],
stepByStepInstructions: defaults.stepByStepInstructions,
consensusLevel: defaults.consensusLevel,
objectionsDeadlocks: defaults.objectionsDeadlocks,
}));
const patch = useCallback(
<K extends keyof RightRailModalSections>(
key: K,
value: RightRailModalSections[K],
) => {
markCreateFlowInteraction();
setSections((prev) => ({ ...prev, [key]: value }));
},
[markCreateFlowInteraction],
);
return (
<div className="flex flex-col gap-6">
<ModalTextAreaField
label={rr.sectionHeadings.corePrinciple}
value={sections.corePrinciple}
onChange={(v) => patch("corePrinciple", v)}
/>
<ApplicableScopeField
label={rr.sectionHeadings.applicableScope}
addLabel={rr.scopeAddButtonLabel}
scopes={sections.applicableScope}
selectedScopes={sections.selectedApplicableScope}
onToggleScope={(scope) =>
patch(
"selectedApplicableScope",
sections.selectedApplicableScope.includes(scope)
? sections.selectedApplicableScope.filter((s) => s !== scope)
: [...sections.selectedApplicableScope, scope],
)
}
onAddScope={(scope) =>
patch("applicableScope", [...sections.applicableScope, scope])
}
/>
<ModalTextAreaField
label={rr.sectionHeadings.stepByStepInstructions}
value={sections.stepByStepInstructions}
onChange={(v) => patch("stepByStepInstructions", v)}
/>
<IncrementerBlock
label={rr.sectionHeadings.consensusLevel}
value={sections.consensusLevel}
min={CONSENSUS_LEVEL_MIN}
max={CONSENSUS_LEVEL_MAX}
step={CONSENSUS_LEVEL_STEP}
onChange={(next) => patch("consensusLevel", next)}
formatValue={(v) => `${v}%`}
decrementAriaLabel="Decrease consensus level"
incrementAriaLabel="Increase consensus level"
/>
<ModalTextAreaField
label={rr.sectionHeadings.objectionsDeadlocks}
value={sections.objectionsDeadlocks}
onChange={(v) => patch("objectionsDeadlocks", v)}
/>
</div>
);
}
export function DecisionApproachesScreen() {
const m = useMessages();
const rr = m.create.rightRail;
const mdUp = useCreateFlowMdUp();
const { state, updateState, markCreateFlowInteraction } = useCreateFlow();
const [messageBoxCheckedIds, setMessageBoxCheckedIds] = useState<string[]>(
[],
);
const [expanded, setExpanded] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
const selectedIds = state.selectedDecisionApproachIds ?? [];
const setSelectedIds = useCallback(
(next: string[]) => {
updateState({ selectedDecisionApproachIds: next });
},
[updateState],
);
const messageBoxItems: InfoMessageBoxItem[] = useMemo(
() =>
rr.messageBox.items.map((item) => ({
id: item.id,
label: item.label,
})),
[rr.messageBox.items],
);
const sampleCards: CardStackItem[] = useMemo(
() =>
rr.cards.map((c) => ({
id: c.id,
label: c.label,
supportText: c.supportText,
recommended: c.recommended,
})),
[rr.cards],
);
const cardById = useMemo(
() => new Map(rr.cards.map((c) => [c.id, c])),
[rr.cards],
);
const sidebarDescription = (
<>
{rr.sidebar.descriptionBefore}
<InlineTextButton
onClick={() => {
markCreateFlowInteraction();
setExpanded(true);
}}
>
{rr.sidebar.descriptionLinkLabel}
</InlineTextButton>
{rr.sidebar.descriptionAfter}
</>
);
const handleMessageBoxCheckboxChange = useCallback(
(id: string, checked: boolean) => {
markCreateFlowInteraction();
setMessageBoxCheckedIds((prev) =>
checked ? [...prev, id] : prev.filter((x) => x !== id),
);
},
[markCreateFlowInteraction],
);
const handleCardSelect = useCallback(
(id: string) => {
markCreateFlowInteraction();
setPendingCardId(id);
setCreateModalOpen(true);
},
[markCreateFlowInteraction],
);
const handleToggleExpand = useCallback(() => {
markCreateFlowInteraction();
setExpanded((prev) => !prev);
}, [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]);
const modalConfig = (() => {
if (!pendingCardId) {
return {
title: rr.confirmModal.title,
description: rr.confirmModal.description,
nextButtonText: rr.confirmModal.nextButtonText,
};
}
if (pendingCardId in rr.modals) {
const modal = rr.modals[pendingCardId as keyof typeof rr.modals];
return {
title: modal.title,
description: modal.description,
nextButtonText: rr.addApproach.nextButtonText,
};
}
const card = cardById.get(pendingCardId);
return {
title: card?.label ?? rr.confirmModal.title,
description: card?.supportText ?? rr.confirmModal.description,
nextButtonText: rr.addApproach.nextButtonText,
};
})();
return (
<CreateFlowTwoColumnSelectShell
contentTopBelowMd="space-800"
lgVerticalAlign="start"
header={
<DecisionMakingSidebar
title={rr.sidebar.title}
description={sidebarDescription}
messageBoxTitle={rr.messageBox.title}
messageBoxItems={messageBoxItems}
messageBoxCheckedIds={messageBoxCheckedIds}
onMessageBoxCheckboxChange={handleMessageBoxCheckboxChange}
size={mdUp ? "L" : "M"}
justification={mdUp ? "left" : "center"}
/>
}
>
<div className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0">
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
onCardSelect={handleCardSelect}
expanded={expanded}
onToggleExpand={handleToggleExpand}
hasMore={true}
toggleLabel={rr.cardStack.toggleSeeAll}
showLessLabel={rr.cardStack.toggleShowLess}
title=""
description=""
layout="singleStack"
compactRecommendedLimit={5}
className="w-full"
headerLockupSize={mdUp ? "L" : "M"}
/>
</div>
<Create
isOpen={createModalOpen}
onClose={handleCreateModalClose}
onNext={handleCreateModalConfirm}
title={modalConfig.title}
description={modalConfig.description}
nextButtonText={modalConfig.nextButtonText}
showBackButton={false}
backdropVariant="loginYellow"
>
{pendingCardId ? (
<AddDecisionApproachModalContent
key={pendingCardId}
approachCardId={pendingCardId}
/>
) : null}
</Create>
</CreateFlowTwoColumnSelectShell>
);
}
@@ -0,0 +1,106 @@
"use client";
import { useState, useEffect } from "react";
import MultiSelect from "../../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
function chipRowsFromLabels(
rows: readonly { label: string }[],
): ChipOption[] {
return rows.map((row, i) => ({
id: String(i + 1),
label: row.label,
state: "unselected" as const,
}));
}
function selectedIdsFromOptions(options: ChipOption[]): string[] {
return options
.filter((o) => o.state === "selected")
.map((o) => o.id);
}
/** Create Community — Figma `20094:41317`, chips only (layout tokens shared with structure select). */
export function CommunitySizeSelectScreen() {
const m = useMessages();
const cs = m.create.communitySize;
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
const [communitySizeOptions, setCommunitySizeOptions] = useState<
ChipOption[]
>(() => {
const base = chipRowsFromLabels(cs.communitySizes);
const selected = new Set(state.selectedCommunitySizeIds ?? []);
return base.map((opt) => ({
...opt,
state: selected.has(opt.id) ? ("selected" as const) : ("unselected" as const),
}));
});
useEffect(() => {
const selected = new Set(state.selectedCommunitySizeIds ?? []);
setCommunitySizeOptions((prev) =>
prev.map((opt) =>
opt.state === "custom"
? opt
: {
...opt,
state: selected.has(opt.id)
? ("selected" as const)
: ("unselected" as const),
},
),
);
}, [state.selectedCommunitySizeIds]);
const persistSelection = (next: ChipOption[]) => {
markCreateFlowInteraction();
setCommunitySizeOptions(next);
updateState({
selectedCommunitySizeIds: selectedIdsFromOptions(next),
});
};
const handleCommunitySizeClick = (chipId: string) => {
const next: ChipOption[] = communitySizeOptions.map((opt) =>
opt.id === chipId
? {
...opt,
state:
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
);
persistSelection(next);
};
const multiSelectBlock = (
<MultiSelect
formHeader={false}
size="m"
options={communitySizeOptions}
onChipClick={handleCommunitySizeClick}
addButton={false}
/>
);
return (
<CreateFlowTwoColumnSelectShell
header={
<CreateFlowHeaderLockup
title={cs.header.title}
description={cs.header.description}
justification="left"
/>
}
>
{multiSelectBlock}
</CreateFlowTwoColumnSelectShell>
);
}
@@ -0,0 +1,350 @@
"use client";
import {
useState,
useMemo,
useEffect,
type Dispatch,
type SetStateAction,
} from "react";
import MultiSelect from "../../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import type { CommunityStructureChipSnapshotRow } from "../../types";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
function createListCustomHandlers(
setList: Dispatch<SetStateAction<ChipOption[]>>,
confirmState: "unselected" | "selected",
onInteraction?: () => void,
) {
const touch = () => onInteraction?.();
return {
onAddClick: () => {
touch();
setList((prev) => [
...prev,
{ id: crypto.randomUUID(), label: "", state: "custom" },
]);
},
onCustomChipConfirm: (chipId: string, value: string) => {
touch();
setList((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: confirmState }
: opt,
),
);
},
onCustomChipClose: (chipId: string) => {
touch();
setList((prev) => prev.filter((o) => o.id !== chipId));
},
};
}
function chipRowsFromLabels(
rows: readonly { label: string }[],
): ChipOption[] {
return rows.map((row, i) => ({
id: String(i + 1),
label: row.label,
state: "unselected" as const,
}));
}
function applySavedSelection(
options: ChipOption[],
saved: string[] | undefined,
): ChipOption[] {
const selected = new Set(saved ?? []);
return options.map((opt) =>
opt.state === "custom"
? opt
: {
...opt,
state: selected.has(opt.id)
? ("selected" as const)
: ("unselected" as const),
},
);
}
function selectedIdsFromOptions(options: ChipOption[]): string[] {
return options
.filter((o) => o.state === "selected")
.map((o) => o.id);
}
function chipOptionsToSnapshotRows(
options: ChipOption[],
): CommunityStructureChipSnapshotRow[] {
return options.map((o) => ({
id: o.id,
label: o.label,
...(o.state !== undefined ? { state: o.state } : {}),
}));
}
/** Returns chips when a draft snapshot exists; otherwise null (use preset rows + selected ids). */
function snapshotRowsToChipOptions(
rows: CommunityStructureChipSnapshotRow[] | undefined,
): ChipOption[] | null {
if (!Array.isArray(rows) || rows.length === 0) return null;
return rows.map((r) => ({
id: r.id,
label: r.label,
...(r.state !== undefined
? { state: r.state as ChipOption["state"] }
: {}),
}));
}
/** Create Community step 3 — Figma `20094:18244` (responsive grid + column caps via `createFlowLayoutTokens`). */
export function CommunityStructureSelectScreen() {
const m = useMessages();
const cs = m.create.communityStructure;
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
ChipOption[]
>(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.organizationTypes,
);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromLabels(cs.organizationTypes),
state.selectedOrganizationTypeIds,
);
});
const [scaleOptions, setScaleOptions] = useState<ChipOption[]>(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.scale,
);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromLabels(cs.scaleOptions),
state.selectedScaleIds,
);
});
const [maturityOptions, setMaturityOptions] = useState<ChipOption[]>(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.maturity,
);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromLabels(cs.maturityOptions),
state.selectedMaturityIds,
);
});
useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.organizationTypes,
);
if (fromSnap) {
setOrganizationTypeOptions(fromSnap);
return;
}
setOrganizationTypeOptions((prev) =>
applySavedSelection(prev, state.selectedOrganizationTypeIds),
);
}, [
state.communityStructureChipSnapshots?.organizationTypes,
state.selectedOrganizationTypeIds,
]);
useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.scale,
);
if (fromSnap) {
setScaleOptions(fromSnap);
return;
}
setScaleOptions((prev) => applySavedSelection(prev, state.selectedScaleIds));
}, [
state.communityStructureChipSnapshots?.scale,
state.selectedScaleIds,
]);
useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.maturity,
);
if (fromSnap) {
setMaturityOptions(fromSnap);
return;
}
setMaturityOptions((prev) =>
applySavedSelection(prev, state.selectedMaturityIds),
);
}, [
state.communityStructureChipSnapshots?.maturity,
state.selectedMaturityIds,
]);
const organizationCustomHandlers = useMemo(
() =>
createListCustomHandlers(
setOrganizationTypeOptions,
"unselected",
markCreateFlowInteraction,
),
[markCreateFlowInteraction],
);
const scaleCustomHandlers = useMemo(
() =>
createListCustomHandlers(
setScaleOptions,
"unselected",
markCreateFlowInteraction,
),
[markCreateFlowInteraction],
);
const maturityCustomHandlers = useMemo(
() =>
createListCustomHandlers(
setMaturityOptions,
"unselected",
markCreateFlowInteraction,
),
[markCreateFlowInteraction],
);
const persistOrg = (next: ChipOption[]) => {
markCreateFlowInteraction();
setOrganizationTypeOptions(next);
updateState({
selectedOrganizationTypeIds: selectedIdsFromOptions(next),
communityStructureChipSnapshots: {
organizationTypes: chipOptionsToSnapshotRows(next),
},
});
};
const persistScale = (next: ChipOption[]) => {
markCreateFlowInteraction();
setScaleOptions(next);
updateState({
selectedScaleIds: selectedIdsFromOptions(next),
communityStructureChipSnapshots: {
scale: chipOptionsToSnapshotRows(next),
},
});
};
const persistMaturity = (next: ChipOption[]) => {
markCreateFlowInteraction();
setMaturityOptions(next);
updateState({
selectedMaturityIds: selectedIdsFromOptions(next),
communityStructureChipSnapshots: {
maturity: chipOptionsToSnapshotRows(next),
},
});
};
const handleOrganizationTypeClick = (chipId: string) => {
persistOrg(
organizationTypeOptions.map((opt) =>
opt.id === chipId
? {
...opt,
state:
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
),
);
};
const handleScaleClick = (chipId: string) => {
persistScale(
scaleOptions.map((opt) =>
opt.id === chipId
? {
...opt,
state:
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
),
);
};
const handleMaturityClick = (chipId: string) => {
persistMaturity(
maturityOptions.map((opt) =>
opt.id === chipId
? {
...opt,
state:
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
),
);
};
const multiSelectBlock = (
<>
<MultiSelect
label={cs.organizationMultiSelect.label}
showHelpIcon
size="s"
options={organizationTypeOptions}
onChipClick={handleOrganizationTypeClick}
{...organizationCustomHandlers}
addButton
addButtonText={cs.organizationMultiSelect.addButtonText}
/>
<MultiSelect
label={cs.scaleMultiSelect.label}
showHelpIcon
size="s"
options={scaleOptions}
onChipClick={handleScaleClick}
{...scaleCustomHandlers}
addButton
addButtonText={cs.scaleMultiSelect.addButtonText}
/>
<MultiSelect
label={cs.maturityMultiSelect.label}
showHelpIcon
size="s"
options={maturityOptions}
onChipClick={handleMaturityClick}
{...maturityCustomHandlers}
addButton
addButtonText={cs.maturityMultiSelect.addButtonText}
/>
</>
);
return (
<CreateFlowTwoColumnSelectShell
header={
<CreateFlowHeaderLockup
title={cs.header.title}
description={cs.header.description}
justification="left"
/>
}
>
{multiSelectBlock}
</CreateFlowTwoColumnSelectShell>
);
}
@@ -0,0 +1,98 @@
"use client";
import { useState } from "react";
import MultiSelect from "../../../../components/controls/MultiSelect";
import Alert from "../../../../components/modals/Alert";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import { useTranslation } from "../../../../contexts/MessagesContext";
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";
export function ConfirmStakeholdersScreen() {
const { markCreateFlowInteraction } = useCreateFlow();
const t = useTranslation("create.confirmStakeholders");
const [toastDismissed, setToastDismissed] = useState(false);
const [stakeholderOptions, setStakeholderOptions] = useState<ChipOption[]>(
[],
);
const handleAddStakeholder = () => {
markCreateFlowInteraction();
setStakeholderOptions((prev) => [
...prev,
{ id: crypto.randomUUID(), label: "", state: "custom" },
]);
};
const handleCustomChipConfirm = (chipId: string, value: string) => {
markCreateFlowInteraction();
setStakeholderOptions((prev) =>
prev.map((opt) =>
opt.id === chipId ? { ...opt, label: value, state: "selected" } : opt,
),
);
};
const handleCustomChipClose = (chipId: string) => {
markCreateFlowInteraction();
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
};
const handleChipClick = (chipId: string) => {
markCreateFlowInteraction();
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
};
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>
<MultiSelect
formHeader={false}
showHelpIcon={false}
size="s"
options={stakeholderOptions}
onChipClick={handleChipClick}
onAddClick={handleAddStakeholder}
onCustomChipConfirm={handleCustomChipConfirm}
onCustomChipClose={handleCustomChipClose}
addButton
addButtonText={t("addStakeholder")}
/>
</div>
</CreateFlowStepShell>
{!toastDismissed && (
<div
className="fixed bottom-[5.25rem] left-1/2 z-10 w-[min(640px,calc(100%-2.5rem))] max-w-[640px] -translate-x-1/2 md:bottom-[5.5rem]"
role="status"
aria-live="polite"
>
<Alert
type="banner"
status="positive"
title={t("draftToastTitle")}
hasLeadingIcon={false}
hasBodyText={false}
onClose={() => setToastDismissed(true)}
className="w-full !px-[var(--space-600,24px)] !py-[var(--space-400,16px)] md:!py-4"
/>
</div>
)}
</>
);
}
@@ -0,0 +1,401 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import MultiSelect from "../../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import TextArea from "../../../../components/controls/TextArea";
import Create from "../../../../components/modals/Create";
import ContentLockup from "../../../../components/type/ContentLockup";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import type { CommunityStructureChipSnapshotRow } from "../../types";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
const MAX_CORE_VALUES = 5;
type ModalSession = "pending" | "editing";
/** Row in `coreValues.json` `values` — string (legacy) or `{ label, meaning, signals }`. */
type CoreValuePresetJson =
| string
| { label: string; meaning?: string; signals?: string };
type CoreValuePreset = {
label: string;
meaning: string;
signals: string;
};
function normalizeCoreValuePresets(
values: readonly CoreValuePresetJson[],
): CoreValuePreset[] {
return values.map((v) => {
if (typeof v === "string") {
return { label: v, meaning: "", signals: "" };
}
return {
label: v.label,
meaning: typeof v.meaning === "string" ? v.meaning : "",
signals: typeof v.signals === "string" ? v.signals : "",
};
});
}
function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[] {
return presets.map((row, i) => ({
id: String(i + 1),
label: row.label,
state: "unselected" as const,
}));
}
function applySavedSelection(
options: ChipOption[],
saved: string[] | undefined,
): ChipOption[] {
const selected = new Set(saved ?? []);
return options.map((opt) =>
opt.state === "custom"
? opt
: {
...opt,
state: selected.has(opt.id)
? ("selected" as const)
: ("unselected" as const),
},
);
}
function selectedIdsFromOptions(options: ChipOption[]): string[] {
return options
.filter((o) => o.state === "selected")
.map((o) => o.id);
}
function chipOptionsToSnapshotRows(
options: ChipOption[],
): CommunityStructureChipSnapshotRow[] {
return options.map((o) => ({
id: o.id,
label: o.label,
...(o.state !== undefined ? { state: o.state } : {}),
}));
}
function snapshotRowsToChipOptions(
rows: CommunityStructureChipSnapshotRow[] | undefined,
): ChipOption[] | null {
if (!Array.isArray(rows) || rows.length === 0) return null;
return rows.map((r) => ({
id: r.id,
label: r.label,
...(r.state !== undefined
? { state: r.state as ChipOption["state"] }
: {}),
}));
}
/** Create Custom — Core Values (Figma `20264:68378`). Up to five selections; preset list + custom chips. */
export function CoreValuesSelectScreen() {
const m = useMessages();
const cv = m.create.coreValues;
const presets = useMemo(
() => normalizeCoreValuePresets(cv.values as CoreValuePresetJson[]),
[cv.values],
);
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(
() => {
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromPresets(presets),
state.selectedCoreValueIds,
);
},
);
const [activeModalChipId, setActiveModalChipId] = useState<string | null>(
null,
);
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
const [draftMeaning, setDraftMeaning] = useState("");
const [draftSignals, setDraftSignals] = useState("");
useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
if (fromSnap) {
setCoreValueOptions(fromSnap);
return;
}
setCoreValueOptions((prev) =>
applySavedSelection(prev, state.selectedCoreValueIds),
);
}, [state.coreValuesChipsSnapshot, state.selectedCoreValueIds]);
/** Sync chips to create-flow draft. Never call `updateState` from inside a `setCoreValueOptions` updater — defer with `queueMicrotask`. */
const syncCoreValuesToDraft = useCallback(
(next: ChipOption[]) => {
updateState({
selectedCoreValueIds: selectedIdsFromOptions(next),
coreValuesChipsSnapshot: chipOptionsToSnapshotRows(next),
});
},
[updateState],
);
const persistCoreValues = useCallback(
(next: ChipOption[]) => {
markCreateFlowInteraction();
setCoreValueOptions(next);
syncCoreValuesToDraft(next);
},
[markCreateFlowInteraction, syncCoreValuesToDraft],
);
/** Default meaning/signals from `coreValues.json` `values` for each preset label. */
const getPresetTexts = useCallback(
(valueLabel: string): { meaning: string; signals: string } => {
const row = presets.find((p) => p.label === valueLabel);
if (!row) return { meaning: "", signals: "" };
return { meaning: row.meaning, signals: row.signals };
},
[presets],
);
const getInitialTexts = useCallback(
(chipId: string, valueLabel: string) => {
const saved = state.coreValueDetailsByChipId?.[chipId];
const preset = getPresetTexts(valueLabel);
return {
meaning: saved?.meaning ?? preset.meaning,
signals: saved?.signals ?? preset.signals,
};
},
[state.coreValueDetailsByChipId, getPresetTexts],
);
const openModal = useCallback(
(chipId: string, session: ModalSession, valueLabel: string) => {
const initial = getInitialTexts(chipId, valueLabel);
setDraftMeaning(initial.meaning);
setDraftSignals(initial.signals);
setActiveModalChipId(chipId);
setModalSession(session);
markCreateFlowInteraction();
},
[getInitialTexts, markCreateFlowInteraction],
);
const handleModalDismiss = useCallback(() => {
if (activeModalChipId && modalSession === "pending") {
const next = coreValueOptions.map((opt) =>
opt.id === activeModalChipId
? { ...opt, state: "unselected" as const }
: opt,
);
persistCoreValues(next);
}
setActiveModalChipId(null);
setModalSession(null);
}, [activeModalChipId, modalSession, coreValueOptions, persistCoreValues]);
const handleModalConfirm = useCallback(() => {
if (!activeModalChipId) return;
markCreateFlowInteraction();
updateState({
coreValueDetailsByChipId: {
[activeModalChipId]: {
meaning: draftMeaning,
signals: draftSignals,
},
},
});
setActiveModalChipId(null);
setModalSession(null);
}, [
activeModalChipId,
draftMeaning,
draftSignals,
markCreateFlowInteraction,
updateState,
]);
const handleChipClick = (chipId: string) => {
const target = coreValueOptions.find((o) => o.id === chipId);
if (!target || target.state === "custom") return;
const selectedCount = coreValueOptions.filter(
(o) => o.state === "selected",
).length;
if (target.state === "selected") {
const next: ChipOption[] = coreValueOptions.map((opt) =>
opt.id === chipId
? { ...opt, state: "unselected" as const }
: opt,
);
persistCoreValues(next);
return;
}
if (selectedCount >= MAX_CORE_VALUES) return;
const next: ChipOption[] = coreValueOptions.map((opt) =>
opt.id === chipId
? { ...opt, state: "selected" as const }
: opt,
);
persistCoreValues(next);
openModal(chipId, "pending", target.label);
};
const addHandlers = {
onAddClick: () => {
markCreateFlowInteraction();
setCoreValueOptions((prev) => {
const next: ChipOption[] = [
...prev,
{ id: crypto.randomUUID(), label: "", state: "custom" },
];
queueMicrotask(() => syncCoreValuesToDraft(next));
return next;
});
},
onCustomChipConfirm: (chipId: string, value: string) => {
markCreateFlowInteraction();
setCoreValueOptions((prev) => {
const withLabel = prev.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "unselected" as const }
: opt,
);
const selectedCount = withLabel.filter(
(o) => o.state === "selected",
).length;
const canSelect = selectedCount < MAX_CORE_VALUES;
const next = canSelect
? withLabel.map((opt) =>
opt.id === chipId
? { ...opt, state: "selected" as const }
: opt,
)
: withLabel;
queueMicrotask(() => {
syncCoreValuesToDraft(next);
if (canSelect) {
openModal(chipId, "pending", value);
} else {
openModal(chipId, "editing", value);
}
});
return next;
});
},
onCustomChipClose: (chipId: string) => {
markCreateFlowInteraction();
setCoreValueOptions((prev) => {
const next = prev.filter((o) => o.id !== chipId);
queueMicrotask(() => syncCoreValuesToDraft(next));
return next;
});
},
};
const modalChipLabel =
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
const description = (
<>
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
{cv.header.descriptionLead}{" "}
</span>
<button
type="button"
onClick={addHandlers.onAddClick}
className="cursor-pointer font-inter font-normal leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] hover:opacity-90"
>
{cv.header.addLink}
</button>
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
{" "}
{cv.header.descriptionTrail}
</span>
</>
);
const detailModal = cv.detailModal;
return (
<CreateFlowTwoColumnSelectShell
lgVerticalAlign="start"
header={
<CreateFlowHeaderLockup
title={cv.header.title}
description={description}
justification="left"
/>
}
>
<MultiSelect
formHeader={false}
size="m"
options={coreValueOptions}
onChipClick={handleChipClick}
onAddClick={addHandlers.onAddClick}
onCustomChipConfirm={addHandlers.onCustomChipConfirm}
onCustomChipClose={addHandlers.onCustomChipClose}
addButton
addButtonText={cv.multiSelect.addButtonText}
/>
{detailModal && (
<Create
isOpen={activeModalChipId !== null}
onClose={handleModalDismiss}
backdropVariant="loginYellow"
headerContent={
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={modalChipLabel}
description={detailModal.subtitle}
variant="modal"
alignment="left"
/>
</div>
}
showBackButton={false}
showNextButton
onNext={handleModalConfirm}
nextButtonText={detailModal.addValueButton}
ariaLabel={modalChipLabel || "Core value details"}
>
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
<TextArea
label={detailModal.meaningLabel}
showHelpIcon
appearance="embedded"
size="medium"
value={draftMeaning}
onChange={(e) => setDraftMeaning(e.target.value)}
rows={4}
/>
<TextArea
label={detailModal.signalsLabel}
showHelpIcon
appearance="embedded"
size="medium"
value={draftSignals}
onChange={(e) => setDraftSignals(e.target.value)}
rows={4}
/>
</div>
</Create>
)}
</CreateFlowTwoColumnSelectShell>
);
}
@@ -0,0 +1,108 @@
"use client";
import { useState, useEffect, type HTMLInputTypeAttribute } from "react";
import TextInput from "../../../../components/controls/TextInput";
import type { HeaderLockupJustificationValue } from "../../../../components/type/HeaderLockup/HeaderLockup.types";
import { useTranslation } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import {
CreateFlowStepShell,
type CreateFlowContentTopBelowMd,
} from "../../components/CreateFlowStepShell";
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
import type { CreateFlowTextStateField } from "../../types";
type Props = {
messageNamespace: string;
stateField: CreateFlowTextStateField;
maxLength: number;
/** Figma Flow — Text (`20094:41243`): main column `items-center` + horizontal padding token. */
mainAlign?: "start" | "center";
inputType?: HTMLInputTypeAttribute;
showCharacterCount?: boolean;
headerJustification?: HeaderLockupJustificationValue;
/** Top spacing under top chrome (`CreateFlowStepShell` / `CreateFlowContentTopBelowMd`). */
contentTopBelowMd?: CreateFlowContentTopBelowMd;
};
/**
* Shared narrow-column + TextInput pattern for Create Community text frames.
*/
export function CreateFlowTextFieldScreen({
messageNamespace,
stateField,
maxLength,
mainAlign = "start",
inputType = "text",
showCharacterCount = true,
headerJustification = "left",
contentTopBelowMd = "space-1400",
}: Props) {
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
const mdUp = useCreateFlowMdUp();
const t = useTranslation(messageNamespace);
const readFromState = (): string => {
const raw = state[stateField];
return typeof raw === "string" ? raw : "";
};
const [value, setValue] = useState(() => readFromState());
useEffect(() => {
const incoming = readFromState();
if (incoming.length === 0) return;
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync when context hydrates from server/local
setValue((prev) => (prev === "" ? incoming : prev));
}, [state, stateField]);
const characterCount = value.length;
const hint =
showCharacterCount === false
? false
: t("characterCountTemplate")
.replace("{current}", String(characterCount))
.replace("{max}", String(maxLength));
const mainItems =
mainAlign === "center" ? "items-center" : "items-start";
return (
<CreateFlowStepShell
variant="centeredNarrow"
contentTopBelowMd={contentTopBelowMd}
>
<div
className={`flex flex-col gap-[18px] ${mainItems} ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
>
<div className="w-full">
<CreateFlowHeaderLockup
title={t("title")}
description={t("description")}
justification={headerJustification}
/>
</div>
<div className="w-full">
<TextInput
className="!transition-none"
type={inputType}
placeholder={t("placeholder")}
value={value}
onChange={(e) => {
const v = e.target.value;
setValue(v);
markCreateFlowInteraction();
updateState({ [stateField]: v } as Record<string, string>);
}}
inputSize={mdUp ? "medium" : "small"}
formHeader={false}
textHint={hint}
maxLength={maxLength}
/>
</div>
</div>
</CreateFlowStepShell>
);
}
@@ -0,0 +1,46 @@
"use client";
import Upload from "../../../../components/controls/Upload";
import { useMessages } from "../../../../contexts/MessagesContext";
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";
/** Create Community — Figma Flow — Upload `20094:41524`. */
export function CommunityUploadScreen() {
const m = useMessages();
const u = m.create.communityUpload;
const { markCreateFlowInteraction } = useCreateFlow();
const handleUploadClick = () => {
markCreateFlowInteraction();
};
return (
<CreateFlowStepShell
variant="centeredNarrow"
contentTopBelowMd="space-1400"
>
<div
className={`flex flex-col items-center gap-[18px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
>
<div className="w-full">
<CreateFlowHeaderLockup
title={u.title}
description={u.description}
justification="center"
/>
</div>
<div className="w-full">
<Upload
active={true}
showHelpIcon={false}
hintText={u.hintText}
onClick={handleUploadClick}
/>
</div>
</div>
</CreateFlowStepShell>
);
}