Let core-value meaning and signals be edited on first open with Add Value, confirm discard when closing without adding, and pin Keep editing to the footer back slot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-19 21:56:52 -06:00
co-authored by Cursor
parent d6d6a5e055
commit e9951f260b
9 changed files with 136 additions and 245 deletions
@@ -8,7 +8,7 @@ import ContentLockup from "../../../../components/type/ContentLockup";
import { useMessages } from "../../../../contexts/MessagesContext";
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
import type {
CommunityStructureChipSnapshotRow,
CoreValueDetailEntry,
@@ -16,14 +16,7 @@ import type {
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import { CoreValueEditFields } from "../../components/methodEditFields";
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
import {
captureMethodCardCustomizeSnapshot,
isMethodCardCustomizeSessionDirty,
type MethodCardCustomizeSnapshot,
type MethodCardHeaderDraft,
} from "../../../../../lib/create/methodCardCustomizeSession";
import {
duplicateCoreValueChipInDraft,
MAX_SELECTED_CORE_VALUES,
@@ -37,8 +30,7 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
* Why three sessions, not two:
*
* - `pending` — preset chip just selected; modal opened to capture
* meaning/signals. Dismiss = unselect the chip (keep it in the
* preset row, just not selected).
* meaning/signals. Close (X) confirms, then unselects the chip.
* - `customPending` — brand-new custom chip just created via the Add
* value flow; modal opened with empty fields. Dismiss = drop the
* chip entirely (it was never confirmed via the Add Value button).
@@ -101,13 +93,11 @@ export function CoreValuesSelectScreen() {
[cv.values],
);
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
useDiscardCustomizeConfirm();
const { requestConfirm, confirmDialog } = useAsyncConfirm();
const { markCreateFlowInteraction, updateState, replaceState, state } =
useCreateFlow();
const coreCustomizeSnapshotRef =
useRef<MethodCardCustomizeSnapshot<CoreValueDetailEntry> | null>(null);
const initialDraftRef = useRef<CoreValueDetailEntry | null>(null);
const pendingEphemeralCoreDuplicateRef = useRef<string | null>(null);
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(() =>
@@ -123,9 +113,6 @@ export function CoreValuesSelectScreen() {
);
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
useState<MethodCardHeaderDraft | null>(null);
useEffect(() => {
setCoreValueOptions(
@@ -190,12 +177,11 @@ export function CoreValuesSelectScreen() {
valueLabel: string,
seedDetail?: CoreValueDetailEntry,
) => {
setDraft(seedDetail ?? getInitialTexts(chipId, valueLabel));
const initial = seedDetail ?? getInitialTexts(chipId, valueLabel);
initialDraftRef.current = { ...initial };
setDraft(initial);
setActiveModalChipId(chipId);
setModalSession(session);
setModalEditUnlocked(false);
setCustomizeHeaderDraft(null);
coreCustomizeSnapshotRef.current = null;
markCreateFlowInteraction();
},
[getInitialTexts, markCreateFlowInteraction],
@@ -209,87 +195,36 @@ export function CoreValuesSelectScreen() {
[markCreateFlowInteraction],
);
const resetCustomizeSession = useCallback(() => {
coreCustomizeSnapshotRef.current = null;
setModalEditUnlocked(false);
setCustomizeHeaderDraft(null);
}, []);
const finalizeModalDismiss = useCallback(() => {
pendingEphemeralCoreDuplicateRef.current = null;
resetCustomizeSession();
initialDraftRef.current = null;
setActiveModalChipId(null);
setModalSession(null);
}, [resetCustomizeSession]);
}, []);
const handleCustomize = useCallback(() => {
if (!activeModalChipId) return;
const chipLabelNow =
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
if (!chipLabelNow) return;
markCreateFlowInteraction();
const headerDraft: MethodCardHeaderDraft = {
title: chipLabelNow,
description: "",
};
coreCustomizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
draft,
null,
headerDraft,
);
setCustomizeHeaderDraft(headerDraft);
setModalEditUnlocked(true);
}, [activeModalChipId, coreValueOptions, draft, markCreateFlowInteraction]);
const handleCancelCustomize = useCallback(async () => {
if (!modalEditUnlocked) return;
const snap = coreCustomizeSnapshotRef.current;
if (!snap) {
resetCustomizeSession();
return;
const confirmLeaveWithoutSaving = useCallback(async () => {
const isPendingAdd =
modalSession === "pending" || modalSession === "customPending";
const initial = initialDraftRef.current;
const editingDirty =
modalSession === "editing" &&
initial != null &&
(draft.meaning !== initial.meaning || draft.signals !== initial.signals);
if (!isPendingAdd && !editingDirty) {
return true;
}
if (
!(await confirmDirtyCustomizeCancel(
snap,
draft,
null,
customizeHeaderDraft,
))
) {
return;
}
setDraft(structuredClone(snap.pendingDraft));
resetCustomizeSession();
}, [
confirmDirtyCustomizeCancel,
customizeHeaderDraft,
draft,
modalEditUnlocked,
resetCustomizeSession,
]);
return requestConfirm({
title: cv.detailModal.discardTitle,
description: isPendingAdd
? cv.detailModal.discardPendingDescription
: cv.detailModal.discardEditsDescription,
proceedText: cv.detailModal.discardProceed,
cancelText: cv.detailModal.discardKeepEditing,
});
}, [cv.detailModal, draft, modalSession, requestConfirm]);
const syncLabelFromCustomizeHeaderToOptions = useCallback(() => {
if (!activeModalChipId || !customizeHeaderDraft) return coreValueOptions;
const trimmed = customizeHeaderDraft.title.trim();
if (!trimmed) return coreValueOptions;
return coreValueOptions.map((opt) =>
opt.id === activeModalChipId ? { ...opt, label: trimmed } : opt,
);
}, [activeModalChipId, customizeHeaderDraft, coreValueOptions]);
const handleDuplicateCoreChip = useCallback(async () => {
const handleDuplicateCoreChip = useCallback(() => {
if (!activeModalChipId || !modalSession) return;
if (
!(await confirmDiscard(
modalEditUnlocked,
coreCustomizeSnapshotRef.current,
draft,
null,
customizeHeaderDraft,
))
) {
return;
}
markCreateFlowInteraction();
const priorEphemeral = pendingEphemeralCoreDuplicateRef.current;
let outcome: ReturnType<typeof duplicateCoreValueChipInDraft> | null = null;
@@ -312,7 +247,6 @@ export function CoreValuesSelectScreen() {
if (!outcome) {
return;
}
resetCustomizeSession();
pendingEphemeralCoreDuplicateRef.current = outcome.newId;
openModal(
outcome.newId,
@@ -322,30 +256,15 @@ export function CoreValuesSelectScreen() {
);
}, [
activeModalChipId,
confirmDiscard,
customizeHeaderDraft,
draft,
markCreateFlowInteraction,
modalEditUnlocked,
modalKebabMenu.duplicateTitleSuffix,
modalSession,
openModal,
replaceState,
resetCustomizeSession,
]);
const handleRemoveFromKebab = useCallback(async () => {
if (
!(await confirmDiscard(
modalEditUnlocked,
coreCustomizeSnapshotRef.current,
draft,
null,
customizeHeaderDraft,
))
) {
return;
}
const handleRemoveFromKebab = useCallback(() => {
markCreateFlowInteraction();
const ep = pendingEphemeralCoreDuplicateRef.current;
@@ -386,28 +305,16 @@ export function CoreValuesSelectScreen() {
finalizeModalDismiss();
}, [
activeModalChipId,
confirmDiscard,
coreValueOptions,
customizeHeaderDraft,
draft,
finalizeModalDismiss,
markCreateFlowInteraction,
modalEditUnlocked,
modalSession,
persistCoreValues,
replaceState,
]);
const handleModalDismiss = useCallback(async () => {
if (
!(await confirmDiscard(
modalEditUnlocked,
coreCustomizeSnapshotRef.current,
draft,
null,
customizeHeaderDraft,
))
) {
if (!(await confirmLeaveWithoutSaving())) {
return;
}
@@ -436,93 +343,39 @@ export function CoreValuesSelectScreen() {
finalizeModalDismiss();
}, [
activeModalChipId,
confirmDiscard,
confirmLeaveWithoutSaving,
coreValueOptions,
customizeHeaderDraft,
draft,
finalizeModalDismiss,
modalEditUnlocked,
modalSession,
persistCoreValues,
replaceState,
]);
const coreCustomizeSaveDisabled = useMemo(() => {
if (!modalEditUnlocked) return false;
const snap = coreCustomizeSnapshotRef.current;
if (!snap) return true;
return !isMethodCardCustomizeSessionDirty(
snap,
draft,
null,
customizeHeaderDraft,
);
}, [customizeHeaderDraft, draft, modalEditUnlocked]);
const handleModalConfirm = useCallback(() => {
if (!activeModalChipId || !modalSession) return;
if (modalEditUnlocked && customizeHeaderDraft) {
if (coreCustomizeSaveDisabled) {
return;
}
markCreateFlowInteraction();
pendingEphemeralCoreDuplicateRef.current = null;
const nextOpts = syncLabelFromCustomizeHeaderToOptions();
persistCoreValues(nextOpts);
updateState({
coreValueDetailsByChipId: {
...(state.coreValueDetailsByChipId ?? {}),
[activeModalChipId]: draft,
},
});
resetCustomizeSession();
return;
}
if (modalSession === "pending" || modalSession === "customPending") {
markCreateFlowInteraction();
pendingEphemeralCoreDuplicateRef.current = null;
updateState({
coreValueDetailsByChipId: {
...(state.coreValueDetailsByChipId ?? {}),
[activeModalChipId]: draft,
},
});
resetCustomizeSession();
setActiveModalChipId(null);
setModalSession(null);
}
markCreateFlowInteraction();
pendingEphemeralCoreDuplicateRef.current = null;
updateState({
coreValueDetailsByChipId: {
...(state.coreValueDetailsByChipId ?? {}),
[activeModalChipId]: draft,
},
});
finalizeModalDismiss();
}, [
activeModalChipId,
coreCustomizeSaveDisabled,
customizeHeaderDraft,
draft,
finalizeModalDismiss,
markCreateFlowInteraction,
modalEditUnlocked,
modalSession,
persistCoreValues,
resetCustomizeSession,
state.coreValueDetailsByChipId,
syncLabelFromCustomizeHeaderToOptions,
updateState,
]);
const modalChipLabel =
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
const modalFieldsLocked =
!modalEditUnlocked &&
Boolean(
modalSession === "pending" ||
modalSession === "customPending" ||
modalSession === "editing",
);
const showFooterPrimary =
modalEditUnlocked ||
modalSession === "pending" ||
modalSession === "customPending";
const showFooterPrimary = Boolean(modalSession);
const kebabMenuItems = useMemo(() => {
if (!modalSession || !activeModalChipId) return [];
@@ -530,8 +383,6 @@ export function CoreValuesSelectScreen() {
(o) => o.state === "selected",
).length;
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
showCustomize: !modalEditUnlocked,
onCustomize: handleCustomize,
onDuplicate:
modalSession !== "editing" || selectedCount >= MAX_CORE_VALUES
? undefined
@@ -542,10 +393,8 @@ export function CoreValuesSelectScreen() {
}, [
activeModalChipId,
coreValueOptions,
handleCustomize,
handleDuplicateCoreChip,
handleRemoveFromKebab,
modalEditUnlocked,
modalKebabMenu,
modalSession,
]);
@@ -675,41 +524,22 @@ export function CoreValuesSelectScreen() {
onClose={handleModalDismiss}
backdropVariant="blurredYellow"
headerContent={
modalEditUnlocked && customizeHeaderDraft ? (
<MethodCardCustomizeModalHeader
titleLabel={detailModal.customizeValueNameLabel}
descriptionLabel=""
titleValue={customizeHeaderDraft.title}
descriptionValue=""
onTitleChange={(title) =>
setCustomizeHeaderDraft((prev) =>
prev ? { ...prev, title } : null,
)
}
onDescriptionChange={() => {}}
showDescription={false}
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={modalChipLabel}
description={detailModal.subtitle}
variant="modal"
alignment="left"
/>
) : (
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={modalChipLabel}
description={detailModal.subtitle}
variant="modal"
alignment="left"
/>
</div>
)
</div>
}
showBackButton={modalEditUnlocked}
onBack={handleCancelCustomize}
backButtonText={modalKebabMenu.cancelCustomize}
showBackButton={false}
showNextButton={showFooterPrimary}
nextButtonDisabled={
modalEditUnlocked && coreCustomizeSaveDisabled
}
onNext={handleModalConfirm}
nextButtonText={
modalEditUnlocked ? modalKebabMenu.saveEdits : detailModal.addValueButton
modalSession === "editing"
? modalKebabMenu.saveEdits
: detailModal.addValueButton
}
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
@@ -719,7 +549,6 @@ export function CoreValuesSelectScreen() {
ariaLabel={modalChipLabel || "Core value details"}
>
<CoreValueEditFields
readOnly={modalFieldsLocked}
value={draft}
onChange={handleDraftChange}
/>