Implement modals across create flow
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Shared "Applicable Scope" field used by the `decision-approaches` and
|
||||
* `conflict-management` create flow modals. Pairs an `InputLabel` with a
|
||||
* horizontally-wrapping list of toggle-chips plus an inline "+ Add" affordance
|
||||
* that reveals a pill text input for creating new scope values.
|
||||
*/
|
||||
|
||||
import { memo, useState } from "react";
|
||||
import Chip from "../../components/controls/Chip";
|
||||
import InputLabel from "../../components/utility/InputLabel";
|
||||
|
||||
export interface ApplicableScopeFieldProps {
|
||||
/** Label rendered above the capsule row. */
|
||||
label: string;
|
||||
/** Text for the "+ Add …" affordance (e.g. "Add Applicable Scope"). */
|
||||
addLabel: string;
|
||||
/**
|
||||
* The full list of chip values shown to the user. Each value is a unique
|
||||
* string (chip label).
|
||||
*/
|
||||
scopes: string[];
|
||||
/** Values currently toggled on (rendered in the Chip "Selected" state). */
|
||||
selectedScopes: string[];
|
||||
/** Fired when a chip is clicked; caller toggles inclusion in `selectedScopes`. */
|
||||
onToggleScope: (_scope: string) => void;
|
||||
/**
|
||||
* Fired when the user submits a new scope via the inline input. Duplicate
|
||||
* values (already in `scopes`) are filtered out before the callback fires.
|
||||
*/
|
||||
onAddScope: (_scope: string) => void;
|
||||
/**
|
||||
* Optional placeholder for the inline input. Defaults to `addLabel`.
|
||||
*/
|
||||
inputPlaceholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ApplicableScopeFieldComponent({
|
||||
label,
|
||||
addLabel,
|
||||
scopes,
|
||||
selectedScopes,
|
||||
onToggleScope,
|
||||
onAddScope,
|
||||
inputPlaceholder,
|
||||
className = "",
|
||||
}: ApplicableScopeFieldProps) {
|
||||
const [draft, setDraft] = useState("");
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
|
||||
const submitDraft = () => {
|
||||
const trimmed = draft.trim();
|
||||
if (!trimmed) {
|
||||
setIsAdding(false);
|
||||
setDraft("");
|
||||
return;
|
||||
}
|
||||
if (!scopes.includes(trimmed)) {
|
||||
onAddScope(trimmed);
|
||||
}
|
||||
setDraft("");
|
||||
setIsAdding(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 ${className}`.trim()}>
|
||||
<InputLabel label={label} helpIcon size="s" palette="default" />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{scopes.map((scope) => {
|
||||
const isSelected = selectedScopes.includes(scope);
|
||||
return (
|
||||
<Chip
|
||||
key={scope}
|
||||
label={scope}
|
||||
state={isSelected ? "Selected" : "Disabled"}
|
||||
palette="Default"
|
||||
size="S"
|
||||
disabled={false}
|
||||
onClick={() => onToggleScope(scope)}
|
||||
ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isAdding ? (
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={submitDraft}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
submitDraft();
|
||||
} else if (e.key === "Escape") {
|
||||
setDraft("");
|
||||
setIsAdding(false);
|
||||
}
|
||||
}}
|
||||
placeholder={inputPlaceholder ?? addLabel}
|
||||
aria-label={inputPlaceholder ?? addLabel}
|
||||
className="h-[30px] rounded-[9999px] border border-[var(--color-border-default-tertiary)] bg-transparent px-3 font-inter text-[length:var(--sizing-300,12px)] font-medium leading-[14px] text-[color:var(--color-content-default-primary)] outline-none placeholder:text-[color:var(--color-content-default-tertiary)] focus-visible:border-[var(--color-border-default-brand-primary)]"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAdding(true)}
|
||||
className="inline-flex items-center gap-[var(--measures-spacing-050,2px)] rounded-[var(--measures-radius-full,9999px)] px-[var(--space-250,10px)] py-[var(--measures-spacing-200,8px)] font-inter text-[length:var(--sizing-300,12px)] font-medium leading-[14px] text-[color:var(--color-content-default-primary)] hover:bg-[var(--color-surface-default-secondary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent"
|
||||
>
|
||||
<AddGlyph />
|
||||
{addLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddGlyph() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 24 24"
|
||||
className="block size-[14px]"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 5v14M5 12h14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
ApplicableScopeFieldComponent.displayName = "ApplicableScopeField";
|
||||
|
||||
export default memo(ApplicableScopeFieldComponent);
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Shared "labelled text area" field used by every create flow modal section.
|
||||
* Pairs an `InputLabel` (with help icon) with a `TextArea` set to the embedded
|
||||
* appearance — matching the Figma "Control / Text Area" pattern.
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import TextArea from "../../components/controls/TextArea";
|
||||
import InputLabel from "../../components/utility/InputLabel";
|
||||
|
||||
export interface ModalTextAreaFieldProps {
|
||||
/** Label rendered above the text area. */
|
||||
label: string;
|
||||
/** Show the help "?" icon next to the label (default `true`). */
|
||||
helpIcon?: boolean;
|
||||
/** Current text value. */
|
||||
value: string;
|
||||
/** Fired on every change with the new value (no event). */
|
||||
onChange: (_value: string) => void;
|
||||
/** Optional rows for the underlying `<textarea>` (default 4). */
|
||||
rows?: number;
|
||||
/** Optional placeholder. */
|
||||
placeholder?: string;
|
||||
/** Disable the field. */
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ModalTextAreaFieldComponent({
|
||||
label,
|
||||
helpIcon = true,
|
||||
value,
|
||||
onChange,
|
||||
rows = 4,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
className = "",
|
||||
}: ModalTextAreaFieldProps) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 ${className}`.trim()}>
|
||||
<InputLabel label={label} helpIcon={helpIcon} size="s" palette="default" />
|
||||
<TextArea
|
||||
formHeader={false}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
size="large"
|
||||
rows={rows}
|
||||
appearance="embedded"
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ModalTextAreaFieldComponent.displayName = "ModalTextAreaField";
|
||||
|
||||
export default memo(ModalTextAreaFieldComponent);
|
||||
@@ -14,7 +14,7 @@ import { FinalReviewScreen } from "./review/FinalReviewScreen";
|
||||
import { CommunicationMethodsScreen } from "./card/CommunicationMethodsScreen";
|
||||
import { MembershipMethodsScreen } from "./card/MembershipMethodsScreen";
|
||||
import { ConflictManagementScreen } from "./card/ConflictManagementScreen";
|
||||
import { RightRailScreen } from "./right-rail/RightRailScreen";
|
||||
import { DecisionApproachesScreen } from "./right-rail/DecisionApproachesScreen";
|
||||
import { CompletedScreen } from "./completed/CompletedScreen";
|
||||
|
||||
/**
|
||||
@@ -76,7 +76,7 @@ export function CreateFlowScreenView({
|
||||
case "membership-methods":
|
||||
return <MembershipMethodsScreen />;
|
||||
case "decision-approaches":
|
||||
return <RightRailScreen />;
|
||||
return <DecisionApproachesScreen />;
|
||||
case "conflict-management":
|
||||
return <ConflictManagementScreen />;
|
||||
case "confirm-stakeholders":
|
||||
|
||||
@@ -16,23 +16,18 @@ import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import CardStack from "../../../components/utility/CardStack";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import TextArea from "../../../components/controls/TextArea";
|
||||
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 ADD_PLATFORM_CARD_IDS = [
|
||||
IN_PERSON_CARD_ID,
|
||||
SIGNAL_CARD_ID,
|
||||
VIDEO_MEETINGS_CARD_ID,
|
||||
] as const;
|
||||
|
||||
const SECTION_FIELDS = [
|
||||
"corePrinciple",
|
||||
"logisticsAdmin",
|
||||
@@ -50,40 +45,6 @@ const COMMUNICATION_CARD_ORDER = [
|
||||
"7",
|
||||
] as const;
|
||||
|
||||
function CreateModalSection({
|
||||
title,
|
||||
value: _value,
|
||||
onChange,
|
||||
}: {
|
||||
title: string;
|
||||
value: string;
|
||||
onChange: (_value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold leading-tight text-[var(--color-content-default-primary)]">
|
||||
{title}
|
||||
</h3>
|
||||
<span
|
||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-[var(--color-content-invert-brand-secondary)] bg-transparent text-[10px] font-medium leading-none text-[var(--color-content-invert-brand-secondary)]"
|
||||
aria-hidden
|
||||
>
|
||||
?
|
||||
</span>
|
||||
</div>
|
||||
<TextArea
|
||||
formHeader={false}
|
||||
value={_value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
size="large"
|
||||
rows={6}
|
||||
appearance="embedded"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddPlatformModalContent({
|
||||
platformCardId,
|
||||
}: {
|
||||
@@ -92,15 +53,15 @@ function AddPlatformModalContent({
|
||||
const { markCreateFlowInteraction } = useCreateFlow();
|
||||
const m = useMessages();
|
||||
const comm = m.create.communication;
|
||||
const modal = comm.modals[platformCardId as keyof typeof comm.modals];
|
||||
const defaults =
|
||||
modal && "sections" in modal
|
||||
? modal.sections
|
||||
: {
|
||||
corePrinciple: "",
|
||||
logisticsAdmin: "",
|
||||
codeOfConduct: "",
|
||||
};
|
||||
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>
|
||||
@@ -118,14 +79,13 @@ function AddPlatformModalContent({
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
if (!modal || !("sections" in modal)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{SECTION_FIELDS.map((field) => (
|
||||
<CreateModalSection
|
||||
<ModalTextAreaField
|
||||
key={field}
|
||||
title={comm.sectionHeadings[field]}
|
||||
label={comm.sectionHeadings[field]}
|
||||
rows={6}
|
||||
value={sectionValues[field]}
|
||||
onChange={(v) => updateSection(field, v)}
|
||||
/>
|
||||
@@ -134,13 +94,6 @@ function AddPlatformModalContent({
|
||||
);
|
||||
}
|
||||
|
||||
function isAddPlatformCard(cardId: string | null): boolean {
|
||||
return (
|
||||
cardId !== null &&
|
||||
(ADD_PLATFORM_CARD_IDS as readonly string[]).includes(cardId)
|
||||
);
|
||||
}
|
||||
|
||||
export function CommunicationMethodsScreen() {
|
||||
const m = useMessages();
|
||||
const comm = m.create.communication;
|
||||
@@ -180,42 +133,55 @@ export function CommunicationMethodsScreen() {
|
||||
) : (
|
||||
<>
|
||||
{comm.page.compactDescriptionBefore}
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 font-inherit text-[length:inherit] leading-[inherit] text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2 hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]"
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
{comm.page.compactDescriptionLinkLabel}
|
||||
</button>
|
||||
</InlineTextButton>
|
||||
{comm.page.compactDescriptionAfter}
|
||||
</>
|
||||
);
|
||||
|
||||
const modalConfig =
|
||||
pendingCardId && 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,
|
||||
};
|
||||
})()
|
||||
: {
|
||||
title: comm.confirmModal.title,
|
||||
description: comm.confirmModal.description,
|
||||
nextButtonText: comm.confirmModal.nextButtonText,
|
||||
showBackButton: false as const,
|
||||
currentStep: undefined,
|
||||
totalSteps: undefined,
|
||||
};
|
||||
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) => {
|
||||
@@ -286,8 +252,9 @@ export function CommunicationMethodsScreen() {
|
||||
showBackButton={modalConfig.showBackButton}
|
||||
currentStep={modalConfig.currentStep}
|
||||
totalSteps={modalConfig.totalSteps}
|
||||
backdropVariant="loginYellow"
|
||||
>
|
||||
{isAddPlatformCard(pendingCardId) && pendingCardId ? (
|
||||
{pendingCardId ? (
|
||||
<AddPlatformModalContent
|
||||
key={pendingCardId}
|
||||
platformCardId={pendingCardId}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
/**
|
||||
* `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";
|
||||
@@ -12,11 +18,14 @@ 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",
|
||||
@@ -29,6 +38,92 @@ const CONFLICT_CARD_ORDER = [
|
||||
"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;
|
||||
@@ -68,41 +163,55 @@ export function ConflictManagementScreen() {
|
||||
) : (
|
||||
<>
|
||||
{cm.page.compactDescriptionBefore}
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 font-inherit text-[length:inherit] leading-[inherit] text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2 hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]"
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
{cm.page.compactDescriptionLinkLabel}
|
||||
</button>
|
||||
</InlineTextButton>
|
||||
{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 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) => {
|
||||
@@ -173,7 +282,15 @@ export function ConflictManagementScreen() {
|
||||
showBackButton={modalConfig.showBackButton}
|
||||
currentStep={modalConfig.currentStep}
|
||||
totalSteps={modalConfig.totalSteps}
|
||||
/>
|
||||
backdropVariant="loginYellow"
|
||||
>
|
||||
{pendingCardId ? (
|
||||
<AddConflictApproachModalContent
|
||||
key={pendingCardId}
|
||||
approachCardId={pendingCardId}
|
||||
/>
|
||||
) : null}
|
||||
</Create>
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
/**
|
||||
* `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";
|
||||
@@ -12,11 +18,20 @@ 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",
|
||||
@@ -29,6 +44,55 @@ const MEMBERSHIP_CARD_ORDER = [
|
||||
"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;
|
||||
@@ -68,41 +132,55 @@ export function MembershipMethodsScreen() {
|
||||
) : (
|
||||
<>
|
||||
{mem.page.compactDescriptionBefore}
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 font-inherit text-[length:inherit] leading-[inherit] text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2 hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]"
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
{mem.page.compactDescriptionLinkLabel}
|
||||
</button>
|
||||
</InlineTextButton>
|
||||
{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 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) => {
|
||||
@@ -173,7 +251,15 @@ export function MembershipMethodsScreen() {
|
||||
showBackButton={modalConfig.showBackButton}
|
||||
currentStep={modalConfig.currentStep}
|
||||
totalSteps={modalConfig.totalSteps}
|
||||
/>
|
||||
backdropVariant="loginYellow"
|
||||
>
|
||||
{pendingCardId ? (
|
||||
<AddMembershipModalContent
|
||||
key={pendingCardId}
|
||||
membershipCardId={pendingCardId}
|
||||
/>
|
||||
) : null}
|
||||
</Create>
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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/Incrementer";
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
"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";
|
||||
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";
|
||||
|
||||
export function RightRailScreen() {
|
||||
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 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 sidebarDescription = (
|
||||
<>
|
||||
{rr.sidebar.descriptionBefore}
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 font-inherit text-[length:inherit] leading-[inherit] text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2 hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]"
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
{rr.sidebar.descriptionLinkLabel}
|
||||
</button>
|
||||
{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();
|
||||
setSelectedIds(
|
||||
selectedIds.includes(id)
|
||||
? selectedIds.filter((x) => x !== id)
|
||||
: [...selectedIds, id],
|
||||
);
|
||||
},
|
||||
[markCreateFlowInteraction, selectedIds, setSelectedIds],
|
||||
);
|
||||
|
||||
const handleToggleExpand = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
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>
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user