Open each core value as a card of meaning and signals steps, so groups write their own words from an empty field with Generate draft, skip values without blocking the flow, and Customize without keeping helper text as the description.

This commit is contained in:
adilallo committed 2026-11-05 21:28:00 -07:00
1 parent 2a7b9e6da7
commit 64a542f456
25 files changed
+877 -354

No files matched your search

+53 -28
View File
@@ -1065,35 +1065,60 @@ function CreateFlowLayoutContent({
</div>
) : showCustomRuleFooterConfirm &&
customRuleConfirmFooter ? (
<Button
buttonType="filled"
palette="default"
size="xsmall"
disabled={
isPublishing ||
customRuleConfirmFooter.selectionIds(state).length === 0
}
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
onClick={() => {
const cf = customRuleConfirmFooter;
const facet = methodCardFacetSectionForConfirmStep(cf.step);
if (facet != null && cf.selectionIds(state).length > 0) {
setMethodSectionsPinCommitted(facet, true);
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
{customRuleConfirmFooter.step === "core-values" ? (
<Button
buttonType="ghost"
palette="default"
size="xsmall"
disabled={isPublishing}
className={CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS}
onClick={() => {
if (reviewReturnTarget) {
router.push(
createFlowStepPathAfterStrippingReviewReturn(
reviewReturnTarget,
searchParams,
),
);
return;
}
goToNextStep();
}}
>
{footer.skipForNow}
</Button>
) : null}
<Button
buttonType="filled"
palette="default"
size="xsmall"
disabled={
isPublishing ||
customRuleConfirmFooter.selectionIds(state).length === 0
}
if (reviewReturnTarget) {
router.push(
createFlowStepPathAfterStrippingReviewReturn(
reviewReturnTarget,
searchParams,
),
);
return;
}
goToNextStep();
}}
>
{footer[customRuleConfirmFooter.footerMessageKey]}
</Button>
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
onClick={() => {
const cf = customRuleConfirmFooter;
const facet = methodCardFacetSectionForConfirmStep(cf.step);
if (facet != null && cf.selectionIds(state).length > 0) {
setMethodSectionsPinCommitted(facet, true);
}
if (reviewReturnTarget) {
router.push(
createFlowStepPathAfterStrippingReviewReturn(
reviewReturnTarget,
searchParams,
),
);
return;
}
goToNextStep();
}}
>
{footer[customRuleConfirmFooter.footerMessageKey]}
</Button>
</div>
) : isConfirmStakeholdersManagePublished ? (
<Button
buttonType="filled"
@@ -244,7 +244,6 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
titleTrim.length > 0 &&
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
const descriptionOk =
descriptionTrim.length > 0 &&
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS;
if (wizardStep === 1) return titleOk;
if (wizardStep === 2) return descriptionOk;
@@ -1,10 +1,9 @@
"use client";
/**
* Final-review chip modal: **Core values** and **method** facets share the
* kebab → **Duplicate** (values only when under the cap) / **Remove** pattern
* from the create-card facet modals (`Create` +
* {@link buildCustomRuleModalKebabMenu}), plus a footer **Remove** when the
* Final-review chip modal: **Core values** use {@link ValueDialog} (Deselect +
* Cancel + Save; kebab Customize / Duplicate under the cap). **Method** facets
* keep `Create` with kebab Duplicate/Remove plus a footer **Remove** when the
* chip is already in the selection. Values and method chips also offer
* **Customize**, which opens {@link CustomMethodCardWizard} prefilled from the
* chip. Fields are editable on open; Save persists body edits without renaming.
@@ -22,10 +21,10 @@ import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
import {
CommunicationMethodEditFields,
ConflictManagementEditFields,
CoreValueEditFields,
DecisionApproachEditFields,
MembershipMethodEditFields,
} from "./methodEditFields";
import { ValueDialog } from "./ValueDialog";
import CustomMethodCardModalBody from "./CustomMethodCardModalBody";
import { buildCustomRuleModalKebabMenu } from "./customRuleModalKebabMenu";
import { useDiscardCustomizeConfirm } from "../hooks/useDiscardCustomizeConfirm";
@@ -34,6 +33,7 @@ import {
communicationPresetFor,
conflictManagementPresetFor,
coreValuePresetFor,
coreValuePresetForLabel,
decisionApproachPresetFor,
emptyCommunicationDetails,
emptyConflictManagementDetails,
@@ -485,8 +485,7 @@ export function FinalReviewChipEditModal({
buildMethodCardWizardInitialValues({
cardId: target.overrideKey,
fallbackTitle: target.chipLabel,
fallbackDescription:
value.supportText?.trim() || tCv.detailModal.subtitle,
fallbackDescription: value.supportText?.trim() || "",
meta: {},
persistedBlocks: state.customMethodCardFieldBlocksById,
draftFieldBlocks: null,
@@ -559,7 +558,6 @@ export function FinalReviewChipEditModal({
tComm.sectionHeadings,
tCv.detailModal.meaningLabel,
tCv.detailModal.signalsLabel,
tCv.detailModal.subtitle,
tDa.sectionHeadings,
tMem.sectionHeadings,
target,
@@ -730,8 +728,6 @@ export function FinalReviewChipEditModal({
(state.selectedCoreValueIds ?? []).length >= 5
? undefined
: handleDuplicateCoreValue,
showRemove: true,
onRemove: handleRemoveCoreValueFromModal,
});
}
if (!isMethodFacetGroup(target.groupKey)) return [];
@@ -744,7 +740,6 @@ export function FinalReviewChipEditModal({
}, [
handleCustomize,
handleDuplicateCoreValue,
handleRemoveCoreValueFromModal,
handleRemoveSelectedFromModal,
isChipInSelection,
modalKebabMenu,
@@ -1080,12 +1075,61 @@ export function FinalReviewChipEditModal({
const showFooterRemove =
target != null &&
(target.groupKey === "coreValues" || isChipInSelection);
target.groupKey !== "coreValues" &&
isChipInSelection;
const isCoreValuesTarget = target?.groupKey === "coreValues";
const coreDraftValue =
draft?.groupKey === "coreValues" ? draft.value : { meaning: "", signals: "" };
return (
<>
<ValueDialog
isOpen={isOpen && !addCustomWizardOpen && isCoreValuesTarget}
valueLabel={target?.chipLabel ?? ""}
session="editing"
draft={coreDraftValue}
onDraftChange={(value) => setDraft({ groupKey: "coreValues", value })}
generateDraft={
target
? catalogCoreValueDraft(target.overrideKey, target.chipLabel)
: undefined
}
onClose={() => {
void handleModalClose();
}}
onConfirm={handleNext}
onDeselect={handleRemoveCoreValueFromModal}
nextButtonDisabled={coreCustomizeSaveDisabled}
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
kebabMenuItems={
isCoreValuesTarget && kebabMenuItems.length > 0
? kebabMenuItems
: undefined
}
ariaLabel={target?.chipLabel || "Edit chip details"}
>
{isCoreValuesTarget && modalUsesWizardFieldBlocksBody && target ? (
<CustomMethodCardModalBody
cardId={target.overrideKey}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: handleWizardFieldBlocksChange
}
/>
) : undefined}
</ValueDialog>
<Create
isOpen={isOpen && !addCustomWizardOpen}
isOpen={
isOpen && !addCustomWizardOpen && !isCoreValuesTarget
}
onClose={handleModalClose}
onBack={handleModalClose}
showBackButton={!showFooterRemove}
@@ -1113,29 +1157,6 @@ export function FinalReviewChipEditModal({
ariaLabel={target?.chipLabel || "Edit chip details"}
>
<div className="flex flex-col gap-[var(--space-600)] pb-2">
{draft?.groupKey === "coreValues" &&
target &&
(modalUsesWizardFieldBlocksBody ? (
<CustomMethodCardModalBody
cardId={target.overrideKey}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: handleWizardFieldBlocksChange
}
/>
) : (
<CoreValueEditFields
value={draft.value}
onChange={(value) => setDraft({ groupKey: "coreValues", value })}
generateDraft={coreValuePresetFor(target.overrideKey)}
/>
))}
{draft?.groupKey === "communication" &&
target &&
(modalUsesWizardFieldBlocksBody ? (
@@ -1451,3 +1472,12 @@ function findMethodSupportText(
}
return "";
}
function catalogCoreValueDraft(
chipId: string,
label: string,
): CoreValueDetailEntry {
const fromId = coreValuePresetFor(chipId);
if (fromId.meaning.trim() || fromId.signals.trim()) return fromId;
return coreValuePresetForLabel(label);
}
@@ -16,6 +16,8 @@ export interface ModalTextAreaFieldProps {
label: string;
/** Show the help "?" icon next to the label. Off until tooltip behavior ships. */
helpIcon?: boolean;
/** Hide the label when the modal title already asks the question. */
hideLabel?: boolean;
/** Current text value. */
value: string;
/** Fired on every change with the new value (no event). */
@@ -35,6 +37,12 @@ export interface ModalTextAreaFieldProps {
generateDraftLabel?: string;
/** Hint beside Generate draft. */
generateDraftHint?: string;
/**
* Shown under a filled field instead of Generate draft (picked value
* field view).
*/
filledHint?: string;
autoFocus?: boolean;
className?: string;
}
@@ -63,6 +71,7 @@ function PlusGlyph() {
function ModalTextAreaFieldComponent({
label,
helpIcon = false,
hideLabel = false,
value,
onChange,
rows = 4,
@@ -71,26 +80,34 @@ function ModalTextAreaFieldComponent({
generateDraftText,
generateDraftLabel,
generateDraftHint,
filledHint,
autoFocus = false,
className = "",
}: ModalTextAreaFieldProps) {
const labelId = useId();
const draftSource = generateDraftText?.trim() ?? "";
const filled = value.trim().length > 0;
const showFilledHint = filled && Boolean(filledHint) && !disabled;
const showGenerateDraft =
draftSource.length > 0 && Boolean(generateDraftLabel);
const compose = showGenerateDraft;
!showFilledHint &&
draftSource.length > 0 &&
Boolean(generateDraftLabel);
const compose = showGenerateDraft || showFilledHint || hideLabel;
return (
<div
className={`flex flex-col ${compose ? "gap-[12px]" : "gap-2"} ${className}`.trim()}
>
<div id={labelId}>
<InputLabel
label={label}
helpIcon={helpIcon}
size="s"
palette="default"
/>
</div>
{hideLabel ? null : (
<div id={labelId}>
<InputLabel
label={label}
helpIcon={helpIcon}
size="s"
palette="default"
/>
</div>
)}
<TextArea
formHeader={false}
value={value}
@@ -100,7 +117,9 @@ function ModalTextAreaFieldComponent({
appearance={compose ? "compose" : "embedded"}
placeholder={placeholder}
disabled={disabled}
aria-labelledby={labelId}
autoFocus={autoFocus}
aria-label={hideLabel ? label : undefined}
aria-labelledby={hideLabel ? undefined : labelId}
/>
{showGenerateDraft && !disabled ? (
<div className="flex items-center">
@@ -119,6 +138,11 @@ function ModalTextAreaFieldComponent({
) : null}
</div>
) : null}
{showFilledHint ? (
<p className="text-[length:var(--sizing-300)] leading-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
{filledHint}
</p>
) : null}
</div>
);
}
+181
View File
@@ -0,0 +1,181 @@
"use client";
/**
* Figma: Overlay / Modal / Card (24973:1852) with Has Text off and Has Steps
* on. A value opens a card of meaning + signals rows; each row opens that
* field's Step Modal (23746:49780) 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 type { CoreValueDetailEntry } from "../types";
import ModalTextAreaField from "./ModalTextAreaField";
import { ValueDialogStepCard } from "./ValueDialogStepCard";
export type ValueDialogSession = "pending" | "editing";
export type ValueDialogStep = "meaning" | "signals";
export type ValueDialogProps = {
isOpen: boolean;
valueLabel: string;
session: ValueDialogSession | null;
draft: CoreValueDetailEntry;
onDraftChange: (_next: CoreValueDetailEntry) => void;
generateDraft?: CoreValueDetailEntry;
onClose: () => void;
onConfirm: () => void;
onDeselect?: () => void;
nextButtonDisabled?: boolean;
kebabTriggerAriaLabel?: string;
kebabMenuAriaLabel?: string;
kebabMenuItems?: ModalHeaderMenuItem[];
ariaLabel?: string;
/** Customized field-blocks body. Replaces the meaning/signals step cards. */
children?: ReactNode;
};
function ValueDialogComponent({
isOpen,
valueLabel,
session,
draft,
onDraftChange,
generateDraft,
onClose,
onConfirm,
onDeselect,
nextButtonDisabled = false,
kebabTriggerAriaLabel,
kebabMenuAriaLabel,
kebabMenuItems,
ariaLabel,
children,
}: ValueDialogProps) {
const m = useMessages();
const t = m.create.customRule.coreValues.detailModal;
const draftField = m.create.draftField;
const [activeStep, setActiveStep] = useState<ValueDialogStep | null>(null);
useEffect(() => {
if (!isOpen) {
setActiveStep(null);
}
}, [isOpen]);
const handleRequestClose = useCallback(() => {
if (activeStep != null) {
setActiveStep(null);
return;
}
onClose();
}, [activeStep, onClose]);
const meaningDone = draft.meaning.trim().length > 0;
const signalsDone = draft.signals.trim().length > 0;
const useCustomBody = Boolean(children);
const showSteps = !useCustomBody && activeStep == null;
const editing = session === "editing";
const primaryLabel = editing ? t.save : t.addToRule;
const stepTitle =
activeStep === "meaning"
? t.meaningStepTitle.replace("{value}", valueLabel)
: activeStep === "signals"
? t.signalsLabel
: valueLabel;
const stepDescription =
activeStep === "meaning"
? t.meaningDescription
: activeStep === "signals"
? t.signalsStepDescription.replace("{value}", valueLabel)
: undefined;
return (
<Create
isOpen={isOpen}
onClose={handleRequestClose}
title={stepTitle}
description={activeStep != null ? stepDescription : undefined}
showBackButton={false}
showCancelButton
showNextButton
showRemoveButton={editing && activeStep == null}
onCancel={handleRequestClose}
onNext={onConfirm}
onRemove={onDeselect}
cancelButtonText={t.cancel}
nextButtonText={primaryLabel}
removeButtonText={t.deselect}
nextButtonDisabled={nextButtonDisabled}
stepper={false}
backdropVariant="blurredYellow"
kebabTriggerAriaLabel={kebabTriggerAriaLabel}
kebabMenuAriaLabel={kebabMenuAriaLabel}
kebabMenuItems={
activeStep == null && kebabMenuItems && kebabMenuItems.length > 0
? kebabMenuItems
: undefined
}
ariaLabel={ariaLabel || valueLabel}
>
{useCustomBody ? (
children
) : showSteps ? (
<div className="flex w-full flex-col gap-[8px]">
<ValueDialogStepCard
stepNumber={1}
stepBadgeLabel={t.stepBadge}
title={t.meaningLabel}
description={
meaningDone ? draft.meaning : t.meaningDescription
}
done={meaningDone}
onClick={() => setActiveStep("meaning")}
/>
<ValueDialogStepCard
stepNumber={2}
stepBadgeLabel={t.stepBadge}
title={t.signalsLabel}
description={
signalsDone ? draft.signals : t.signalsDescription
}
done={signalsDone}
onClick={() => setActiveStep("signals")}
/>
</div>
) : activeStep === "meaning" ? (
<ModalTextAreaField
hideLabel
label={t.meaningLabel}
value={draft.meaning}
onChange={(meaning) => onDraftChange({ ...draft, meaning })}
placeholder={t.placeholder}
generateDraftText={generateDraft?.meaning}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={t.generateDraftHint}
filledHint={t.filledHint}
autoFocus
/>
) : (
<ModalTextAreaField
hideLabel
label={t.signalsLabel}
value={draft.signals}
onChange={(signals) => onDraftChange({ ...draft, signals })}
placeholder={t.placeholder}
generateDraftText={generateDraft?.signals}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={t.generateDraftHint}
filledHint={t.filledHint}
autoFocus
/>
)}
</Create>
);
}
ValueDialogComponent.displayName = "ValueDialog";
export const ValueDialog = memo(ValueDialogComponent);
@@ -0,0 +1,100 @@
"use client";
/**
* Figma: ".utility / Step Card" (23742:49583). One row in Overlay / Modal / Card
* with Has Steps on — open (step badge) or done (check).
*/
import { memo } from "react";
import Icon from "../../../components/asset/icon";
export type ValueDialogStepCardProps = {
stepNumber: number;
stepBadgeLabel: string;
title: string;
description: string;
done: boolean;
onClick: () => void;
};
function CheckGlyph() {
return (
<svg
aria-hidden
width="10"
height="10"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-[var(--color-content-invert-primary,white)]"
>
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function ValueDialogStepCardComponent({
stepNumber,
stepBadgeLabel,
title,
description,
done,
onClick,
}: ValueDialogStepCardProps) {
return (
<button
type="button"
onClick={onClick}
aria-label={title}
data-figma-node={done ? "23740:49582" : "23740:49581"}
className={`flex w-full shrink-0 items-center gap-[8px] rounded-[12px] border border-solid p-[12px] text-left ${
done
? "border-[var(--color-border-default-positive-primary,#45ea97)] bg-[var(--color-surface-default-secondary,#141414)]"
: "border-[var(--color-border-default-secondary,#303030)] bg-[var(--color-surface-default-secondary,#141414)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)]"
} 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-[var(--color-surface-default-primary)]`}
>
{done ? (
<span
aria-hidden
className="flex size-4 shrink-0 items-center justify-center rounded-[8px] bg-[var(--color-surface-default-positive-primary,#16c472)]"
>
<CheckGlyph />
</span>
) : (
<span
aria-hidden
className="flex w-[48px] shrink-0 items-center justify-center rounded-[12px] bg-[var(--color-surface-default-primary)] py-[4px] text-[length:var(--sizing-250)] font-medium leading-[12px] text-[color:var(--color-content-default-secondary,#c2c2c2)]"
>
{stepBadgeLabel.replace("{n}", String(stepNumber))}
</span>
)}
<span className="flex min-w-0 flex-1 flex-col items-start gap-[2px]">
<span className="w-full text-x-small-paragraph text-[color:var(--color-content-default-primary)]">
{title}
</span>
<span
className={`w-full text-[length:var(--sizing-250)] leading-[14px] ${
done
? "text-[color:var(--color-content-default-secondary,#c2c2c2)]"
: "text-[color:var(--color-content-default-tertiary,#a4a4a4)]"
}`}
>
{description}
</span>
</span>
<span className="flex size-4 shrink-0 items-center justify-center text-[color:var(--color-content-default-primary)]">
<Icon name="chevron_right" size={16} />
</span>
</button>
);
}
ValueDialogStepCardComponent.displayName = "ValueDialogStepCard";
export const ValueDialogStepCard = memo(ValueDialogStepCardComponent);
@@ -1,74 +0,0 @@
"use client";
/**
* Controlled meaning/signals field set for a core-value chip. Rendered both
* by `core-values` (custom-rule selection step) and `final-review` (chip
* edit modal). Holds no state — the parent owns the draft and decides when
* to persist (`updateState`) or discard.
*/
import { memo, useCallback } from "react";
import { useMessages } from "../../../../contexts/MessagesContext";
import ModalTextAreaField from "../ModalTextAreaField";
import type { CoreValueDetailEntry } from "../../types";
export interface CoreValueEditFieldsProps {
value: CoreValueDetailEntry;
onChange: (_next: CoreValueDetailEntry) => void;
/** Disable meaning/signals. Facet and final-review callers omit this so fields stay editable on open. */
readOnly?: boolean;
/** Catalog copy inserted by Generate draft. */
generateDraft?: CoreValueDetailEntry;
}
function CoreValueEditFieldsComponent({
value,
onChange,
readOnly = false,
generateDraft,
}: CoreValueEditFieldsProps) {
const m = useMessages();
const t = m.create.customRule.coreValues.detailModal;
const draftField = m.create.draftField;
const patch = useCallback(
<K extends keyof CoreValueDetailEntry>(
key: K,
next: CoreValueDetailEntry[K],
) => {
onChange({ ...value, [key]: next });
},
[value, onChange],
);
return (
<div className="flex flex-col gap-[var(--space-600)] pb-2">
<ModalTextAreaField
label={t.meaningLabel}
value={value.meaning}
onChange={(v) => patch("meaning", v)}
rows={4}
disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.meaning}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/>
<ModalTextAreaField
label={t.signalsLabel}
value={value.signals}
onChange={(v) => patch("signals", v)}
rows={4}
disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.signals}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/>
</div>
);
}
CoreValueEditFieldsComponent.displayName = "CoreValueEditFields";
export default memo(CoreValueEditFieldsComponent);
@@ -1,6 +1,3 @@
export { default as CoreValueEditFields } from "./CoreValueEditFields";
export type { CoreValueEditFieldsProps } from "./CoreValueEditFields";
export { default as CommunicationMethodEditFields } from "./CommunicationMethodEditFields";
export type { CommunicationMethodEditFieldsProps } from "./CommunicationMethodEditFields";
@@ -3,8 +3,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import MultiSelect from "../../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import Create from "../../../../components/modals/Create";
import ContentLockup from "../../../../components/type/ContentLockup";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { useMessages } from "../../../../contexts/MessagesContext";
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
import { useCreateFlow } from "../../context/CreateFlowContext";
@@ -16,8 +15,8 @@ import type {
} from "../../types";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import { CoreValueEditFields } from "../../components/methodEditFields";
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
import { ValueDialog } from "../../components/ValueDialog";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { usesWizardFieldBlocksModalBody } from "../../../../../lib/create/usesWizardFieldBlocksModalBody";
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
@@ -49,8 +48,8 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
* - `editing` — chip already exists & is selected; modal reopened to
* tweak meaning/signals. Dismiss = no-op (chip stays as-is).
*
* **Add value** (and the header "add" link) opens an empty custom-policy
* wizard. Finalize writes a selected chip named from the wizard title.
* The header **add your own** link opens an empty custom-policy wizard.
* Finalize writes a selected chip named from the wizard title.
*/
type ModalSession = "pending" | "customPending" | "editing";
@@ -98,7 +97,7 @@ function chipOptionsToSnapshotRows(
const EMPTY_DETAIL: CoreValueDetailEntry = { meaning: "", signals: "" };
/** Create Custom — Core Values (Figma `20264:68378`). Up to six selections; preset list + custom chips. */
/** Create Custom — Core Values (Figma `20264:68378`). Overlay / Modal / Card `24973:1852`. Up to six selections; preset list + custom chips. */
export function CoreValuesSelectScreen() {
const m = useMessages();
const cv = m.create.customRule.coreValues;
@@ -448,8 +447,6 @@ export function CoreValuesSelectScreen() {
}),
);
const showFooterPrimary = Boolean(modalSession);
const handleCustomize = useCallback(() => {
if (!activeModalChipId) return;
markCreateFlowInteraction();
@@ -457,8 +454,7 @@ export function CoreValuesSelectScreen() {
buildMethodCardWizardInitialValues({
cardId: activeModalChipId,
fallbackTitle: modalChipLabel,
fallbackDescription:
draft.supportText?.trim() || cv.detailModal.subtitle,
fallbackDescription: draft.supportText?.trim() || "",
meta: {},
persistedBlocks: state.customMethodCardFieldBlocksById,
draftFieldBlocks,
@@ -478,7 +474,6 @@ export function CoreValuesSelectScreen() {
activeModalChipId,
cv.detailModal.meaningLabel,
cv.detailModal.signalsLabel,
cv.detailModal.subtitle,
draft,
draftFieldBlocks,
markCreateFlowInteraction,
@@ -606,15 +601,12 @@ export function CoreValuesSelectScreen() {
atSelectionLimit
? undefined
: handleDuplicateCoreChip,
showRemove: modalSession === "editing",
onRemove: handleRemoveFromKebab,
});
}, [
activeModalChipId,
atSelectionLimit,
handleCustomize,
handleDuplicateCoreChip,
handleRemoveFromKebab,
modalKebabMenu,
modalSession,
state.editingPublishedRuleId,
@@ -677,7 +669,7 @@ export function CoreValuesSelectScreen() {
queueMicrotask(() => {
syncCoreValuesToDraft(next);
// Both branches treat the chip as a brand-new draft until the
// user confirms via Add Value — dismissal removes it.
// user confirms via Add to Rule — dismissal removes it.
openModal(chipId, "customPending", value);
});
return next;
@@ -695,26 +687,17 @@ export function CoreValuesSelectScreen() {
const description = (
<>
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
{cv.header.descriptionLead}{" "}
</span>
<button
type="button"
{cv.header.descriptionLead}
<InlineTextButton
onClick={addHandlers.onAddClick}
disabled={atSelectionLimit}
className="cursor-pointer font-normal leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] hover:opacity-90 disabled:cursor-not-allowed disabled:no-underline disabled:opacity-60 disabled:hover:opacity-60"
>
{cv.header.addLink}
</button>
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
{" "}
{cv.header.descriptionTrail}
</span>
</InlineTextButton>
{cv.header.descriptionTrail}
</>
);
const detailModal = cv.detailModal;
return (
<>
<CreateFlowTwoColumnSelectShell
@@ -735,75 +718,54 @@ export function CoreValuesSelectScreen() {
onAddClick={addHandlers.onAddClick}
onCustomChipConfirm={addHandlers.onCustomChipConfirm}
onCustomChipClose={addHandlers.onCustomChipClose}
addButton
addButtonText={cv.multiSelect.addButtonText}
addButton={false}
maxSelections={MAX_CORE_VALUES}
selectionCountText={selectionCountText}
limitReachedAnnouncement={cv.multiSelect.limitReached}
/>
{detailModal && (
<Create
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
onClose={handleModalDismiss}
backdropVariant="blurredYellow"
headerContent={
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={modalChipLabel}
description={
draft.supportText?.trim() || detailModal.subtitle
}
variant="modal"
alignment="left"
/>
</div>
}
showBackButton={false}
showNextButton={showFooterPrimary}
showRemoveButton={modalSession === "editing"}
onRemove={handleRemoveFromKebab}
onNext={handleModalConfirm}
nextButtonText={
modalSession === "editing"
? modalKebabMenu.saveEdits
: detailModal.addValueButton
}
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
kebabMenuItems={
kebabMenuItems.length > 0 ? kebabMenuItems : undefined
}
ariaLabel={modalChipLabel || "Core value details"}
>
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
<CustomMethodCardModalBody
cardId={activeModalChipId}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: (next) => {
setDraftFieldBlocks(next);
setDraft(
coreValueDetailsFromWizardFieldBlocks(next, draft),
);
}
}
/>
) : (
<CoreValueEditFields
value={draft}
onChange={handleDraftChange}
generateDraft={getPresetTexts(modalChipLabel)}
/>
)}
</Create>
)}
<ValueDialog
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
valueLabel={modalChipLabel}
session={modalSession === "editing" ? "editing" : "pending"}
draft={draft}
onDraftChange={handleDraftChange}
generateDraft={getPresetTexts(modalChipLabel)}
onClose={() => {
void handleModalDismiss();
}}
onConfirm={handleModalConfirm}
onDeselect={
modalSession === "editing" ? handleRemoveFromKebab : undefined
}
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
kebabMenuItems={
kebabMenuItems.length > 0 ? kebabMenuItems : undefined
}
ariaLabel={modalChipLabel || "Core value details"}
>
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
<CustomMethodCardModalBody
cardId={activeModalChipId}
blocksById={state.customMethodCardFieldBlocksById}
blocksOverride={
draftFieldBlocks !== null ? draftFieldBlocks : undefined
}
showPolicyContentLockupWhenNoBlocks={false}
onFieldBlocksChange={
draftFieldBlocks === null
? undefined
: (next) => {
setDraftFieldBlocks(next);
setDraft(
coreValueDetailsFromWizardFieldBlocks(next, draft),
);
}
}
/>
) : undefined}
</ValueDialog>
</CreateFlowTwoColumnSelectShell>
<CustomMethodCardWizard
isOpen={addCustomWizardOpen}