Show seven cards on the four method decks and open each as a routing overlay of its parts, so groups write from empty fields with Generate draft, search the full deck, and see unselected scope chips with enough contrast.

This commit is contained in:
adilallo committed 2026-11-05 22:43:30 -07:00
1 parent dea7dd7fbd
commit 46656f52d3
28 files changed
+1349 -318

No files matched your search

@@ -0,0 +1,144 @@
"use client";
/**
* Figma: Overlay / Modal / Card (24973:1852) with Has Steps on. A deck card
* opens as a routing view of its parts; each row opens that field's Step
* Modal with Generate draft on an empty field.
*/
import { memo, useCallback, useEffect, useState, type ReactNode } from "react";
import Create from "../../../components/modals/Create";
import { useMessages } from "../../../contexts/MessagesContext";
import type { ModalHeaderMenuItem } from "../../../components/modals/ModalHeader/ModalHeader.types";
import { ValueDialogStepCard } from "./ValueDialogStepCard";
export type MethodCardPolicyStep = {
id: string;
title: string;
emptyDescription: string;
done: boolean;
summary: string;
renderEditor: () => ReactNode;
};
export type MethodCardPolicyDialogProps = {
isOpen: boolean;
cardLabel: string;
description?: string;
editing: boolean;
steps: MethodCardPolicyStep[];
onClose: () => void;
onConfirm: () => void;
onDeselect?: () => void;
kebabTriggerAriaLabel?: string;
kebabMenuAriaLabel?: string;
kebabMenuItems?: ModalHeaderMenuItem[];
ariaLabel?: string;
/** Customized field-blocks body. Replaces the routing steps. */
children?: ReactNode;
};
function MethodCardPolicyDialogComponent({
isOpen,
cardLabel,
description = "",
editing,
steps,
onClose,
onConfirm,
onDeselect,
kebabTriggerAriaLabel,
kebabMenuAriaLabel,
kebabMenuItems,
ariaLabel,
children,
}: MethodCardPolicyDialogProps) {
const m = useMessages();
const t = m.create.customRule.cardDialog;
const [activeStepId, setActiveStepId] = useState<string | null>(null);
useEffect(() => {
if (!isOpen) {
setActiveStepId(null);
}
}, [isOpen]);
const activeStep =
activeStepId == null
? null
: (steps.find((step) => step.id === activeStepId) ?? null);
const useCustomBody = Boolean(children);
const showSteps = !useCustomBody && activeStep == null;
const handleRequestClose = useCallback(() => {
if (activeStepId != null) {
setActiveStepId(null);
return;
}
onClose();
}, [activeStepId, onClose]);
const handleNext = useCallback(() => {
if (activeStepId != null) {
setActiveStepId(null);
return;
}
onConfirm();
}, [activeStepId, onConfirm]);
const title = activeStep ? activeStep.title : cardLabel;
const primaryLabel = activeStepId != null || editing ? t.save : t.addToRule;
return (
<Create
isOpen={isOpen}
onClose={handleRequestClose}
title={title}
description={activeStep ? undefined : description || undefined}
showBackButton={false}
showCancelButton
showNextButton
showRemoveButton={editing && activeStepId == null}
onCancel={handleRequestClose}
onNext={handleNext}
onRemove={onDeselect}
cancelButtonText={t.cancel}
nextButtonText={primaryLabel}
removeButtonText={t.deselect}
stepper={false}
backdropVariant="blurredYellow"
kebabTriggerAriaLabel={kebabTriggerAriaLabel}
kebabMenuAriaLabel={kebabMenuAriaLabel}
kebabMenuItems={
activeStepId == null && kebabMenuItems && kebabMenuItems.length > 0
? kebabMenuItems
: undefined
}
ariaLabel={ariaLabel || cardLabel}
>
{useCustomBody ? (
children
) : showSteps ? (
<div className="flex w-full flex-col gap-[8px]">
{steps.map((step, index) => (
<ValueDialogStepCard
key={step.id}
stepNumber={index + 1}
stepBadgeLabel={t.stepBadge}
title={step.title}
description={step.done ? step.summary : step.emptyDescription}
done={step.done}
onClick={() => setActiveStepId(step.id)}
/>
))}
</div>
) : (
activeStep?.renderEditor()
)}
</Create>
);
}
MethodCardPolicyDialogComponent.displayName = "MethodCardPolicyDialog";
export const MethodCardPolicyDialog = memo(MethodCardPolicyDialogComponent);
@@ -0,0 +1,325 @@
"use client";
import IncrementerBlock from "../../../components/controls/IncrementerBlock";
import { useMessages } from "../../../contexts/MessagesContext";
import { formatConflictApplicableScopeForTextarea } from "../../../../lib/create/ruleSectionsFromMethodSelections";
import { withDecisionApproachKeyResourceScopes } from "../../../../lib/create/decisionApproachKeyResources";
import type {
CommunicationMethodDetailEntry,
ConflictManagementDetailEntry,
DecisionApproachDetailEntry,
MembershipMethodDetailEntry,
} from "../types";
import ApplicableScopeField from "./ApplicableScopeField";
import type { MethodCardPolicyStep } from "./MethodCardPolicyDialog";
import ModalTextAreaField from "./ModalTextAreaField";
const CONSENSUS_LEVEL_MIN = 0;
const CONSENSUS_LEVEL_MAX = 100;
const CONSENSUS_LEVEL_STEP = 5;
function textStep(args: {
id: string;
title: string;
emptyDescription: string;
value: string;
onChange: (next: string) => void;
generateDraftText?: string;
generateDraftLabel: string;
generateDraftHint: string;
filledHint: string;
placeholder: string;
}): MethodCardPolicyStep {
const done = args.value.trim().length > 0;
return {
id: args.id,
title: args.title,
emptyDescription: args.emptyDescription,
done,
summary: args.value,
renderEditor: () => (
<ModalTextAreaField
hideLabel
label={args.title}
value={args.value}
onChange={args.onChange}
placeholder={args.placeholder}
generateDraftText={args.generateDraftText}
generateDraftLabel={args.generateDraftLabel}
generateDraftHint={args.generateDraftHint}
filledHint={args.filledHint}
autoFocus
/>
),
};
}
function usePolicyFieldCopy() {
const m = useMessages();
return {
dialog: m.create.customRule.cardDialog,
draftField: m.create.draftField,
};
}
export function useCommunicationPolicySteps(
value: CommunicationMethodDetailEntry,
onChange: (next: CommunicationMethodDetailEntry) => void,
generateDraft?: CommunicationMethodDetailEntry,
): MethodCardPolicyStep[] {
const { dialog, draftField } = usePolicyFieldCopy();
const headings = useMessages().create.customRule.communication.sectionHeadings;
const fields: Array<keyof CommunicationMethodDetailEntry> = [
"corePrinciple",
"logisticsAdmin",
"codeOfConduct",
];
return fields.map((field) =>
textStep({
id: field,
title: headings[field],
emptyDescription: dialog.emptyStepDescription,
value: value[field],
onChange: (next) => onChange({ ...value, [field]: next }),
generateDraftText: generateDraft?.[field],
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
);
}
export function useMembershipPolicySteps(
value: MembershipMethodDetailEntry,
onChange: (next: MembershipMethodDetailEntry) => void,
generateDraft?: MembershipMethodDetailEntry,
): MethodCardPolicyStep[] {
const { dialog, draftField } = usePolicyFieldCopy();
const headings = useMessages().create.customRule.membership.sectionHeadings;
const fields: Array<keyof MembershipMethodDetailEntry> = [
"eligibility",
"joiningProcess",
"expectations",
];
return fields.map((field) =>
textStep({
id: field,
title: headings[field],
emptyDescription: dialog.emptyStepDescription,
value: value[field],
onChange: (next) => onChange({ ...value, [field]: next }),
generateDraftText: generateDraft?.[field],
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
);
}
export function useConflictPolicySteps(
value: ConflictManagementDetailEntry,
onChange: (next: ConflictManagementDetailEntry) => void,
generateDraft?: ConflictManagementDetailEntry,
): MethodCardPolicyStep[] {
const { dialog, draftField } = usePolicyFieldCopy();
const t = useMessages().create.customRule.conflictManagement;
const scopeText = formatConflictApplicableScopeForTextarea(
value.selectedApplicableScope,
value.applicableScope,
);
const generateScope =
generateDraft && generateDraft.applicableScope.length > 0
? generateDraft.applicableScope.join("\n")
: generateDraft
? formatConflictApplicableScopeForTextarea(
generateDraft.selectedApplicableScope,
generateDraft.applicableScope,
)
: undefined;
return [
textStep({
id: "corePrinciple",
title: t.sectionHeadings.corePrinciple,
emptyDescription: dialog.emptyStepDescription,
value: value.corePrinciple,
onChange: (next) => onChange({ ...value, corePrinciple: next }),
generateDraftText: generateDraft?.corePrinciple,
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
textStep({
id: "applicableScope",
title: t.sectionHeadings.applicableScope,
emptyDescription: t.applicableScopePlaceholder,
value: scopeText,
onChange: (next) => {
const lines = next
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
onChange({
...value,
applicableScope: lines,
selectedApplicableScope: [...lines],
});
},
generateDraftText: generateScope,
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: t.applicableScopePlaceholder,
}),
textStep({
id: "processProtocol",
title: t.sectionHeadings.processProtocol,
emptyDescription: dialog.emptyStepDescription,
value: value.processProtocol,
onChange: (next) => onChange({ ...value, processProtocol: next }),
generateDraftText: generateDraft?.processProtocol,
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
textStep({
id: "restorationFallbacks",
title: t.sectionHeadings.restorationFallbacks,
emptyDescription: dialog.emptyStepDescription,
value: value.restorationFallbacks,
onChange: (next) => onChange({ ...value, restorationFallbacks: next }),
generateDraftText: generateDraft?.restorationFallbacks,
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
];
}
export function useDecisionApproachPolicySteps(
value: DecisionApproachDetailEntry,
onChange: (next: DecisionApproachDetailEntry) => void,
generateDraft?: DecisionApproachDetailEntry,
): MethodCardPolicyStep[] {
const { dialog, draftField } = usePolicyFieldCopy();
const t = useMessages().create.customRule.decisionApproaches;
const scopes = withDecisionApproachKeyResourceScopes(value.applicableScope);
const selectedSummary = value.selectedApplicableScope.join(", ");
const consensusStep: MethodCardPolicyStep = {
id: "consensusLevel",
title: t.sectionHeadings.consensusLevel,
emptyDescription: t.consensusLevelDisplay.supermajorityShare,
done: true,
summary:
value.consensusLevel === undefined
? t.consensusLevelDisplay.supermajorityShare
: value.consensusLevel === 0
? t.consensusLevelDisplay.noSetShare
: `${value.consensusLevel}%`,
renderEditor: () =>
value.consensusLevel === undefined ? (
<ModalTextAreaField
hideLabel
label={t.sectionHeadings.consensusLevel}
value={t.consensusLevelDisplay.supermajorityShare}
onChange={() => {}}
disabled
rows={1}
/>
) : (
<IncrementerBlock
label={t.sectionHeadings.consensusLevel}
helpIcon={false}
value={value.consensusLevel}
min={CONSENSUS_LEVEL_MIN}
max={CONSENSUS_LEVEL_MAX}
step={CONSENSUS_LEVEL_STEP}
onChange={(next) => onChange({ ...value, consensusLevel: next })}
formatValue={(v) =>
v === 0 ? t.consensusLevelDisplay.noSetShare : `${v}%`
}
decrementAriaLabel={dialog.decreaseConsensusLevel}
incrementAriaLabel={dialog.increaseConsensusLevel}
/>
),
};
const scopeStep: MethodCardPolicyStep = {
id: "applicableScope",
title: t.sectionHeadings.applicableScope,
emptyDescription: dialog.scopeEmptyDescription,
done: value.selectedApplicableScope.length > 0,
summary: selectedSummary,
renderEditor: () => (
<ApplicableScopeField
label={t.sectionHeadings.applicableScope}
addLabel={t.scopeAddButtonLabel}
scopes={scopes}
selectedScopes={value.selectedApplicableScope}
onToggleScope={(scope) =>
onChange({
...value,
selectedApplicableScope: value.selectedApplicableScope.includes(
scope,
)
? value.selectedApplicableScope.filter((item) => item !== scope)
: [...value.selectedApplicableScope, scope],
})
}
onAddScope={(scope) =>
onChange({
...value,
applicableScope: [...value.applicableScope, scope],
})
}
/>
),
};
return [
textStep({
id: "corePrinciple",
title: t.sectionHeadings.corePrinciple,
emptyDescription: dialog.emptyStepDescription,
value: value.corePrinciple,
onChange: (next) => onChange({ ...value, corePrinciple: next }),
generateDraftText: generateDraft?.corePrinciple,
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
scopeStep,
textStep({
id: "stepByStepInstructions",
title: t.sectionHeadings.stepByStepInstructions,
emptyDescription: dialog.emptyStepDescription,
value: value.stepByStepInstructions,
onChange: (next) => onChange({ ...value, stepByStepInstructions: next }),
generateDraftText: generateDraft?.stepByStepInstructions,
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
consensusStep,
textStep({
id: "objectionsDeadlocks",
title: t.sectionHeadings.objectionsDeadlocks,
emptyDescription: dialog.emptyStepDescription,
value: value.objectionsDeadlocks,
onChange: (next) => onChange({ ...value, objectionsDeadlocks: next }),
generateDraftText: generateDraft?.objectionsDeadlocks,
generateDraftLabel: draftField.generateDraft,
generateDraftHint: draftField.generateDraftHint,
filledHint: dialog.filledHint,
placeholder: draftField.placeholder,
}),
];
}