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

@@ -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";