Edit add feature refined

This commit is contained in:
adilallo
2026-04-29 18:38:40 -06:00
parent fc845d8308
commit c4c74ecdb4
6 changed files with 195 additions and 82 deletions
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import { useCreateFlow } from "../context/CreateFlowContext";
import type { CreateFlowMethodCardFacetSection } from "../types";
import {
@@ -18,14 +18,17 @@ type MethodEntry = { id: string; label: string; supportText: string };
/**
* Applies score ranking, compact-slot rules, optional “pinned selection” showcase
* order, and clears the pin draft flag when a section loses all selections.
* order. Rows stay pinned across navigation while `methodSectionsPinCommitted` is true
* and the section still has selections; we do **not** clear the flag when selection
* arrays briefly go empty during draft hydration (`replaceState` / merge flashes) —
* display order already ignores the pin until `pinActive` is true again.
*/
export function useMethodCardDeckOrdering(
section: RecommendationSection,
methods: readonly MethodEntry[],
selectedIds: readonly string[],
) {
const { state, setMethodSectionsPinCommitted } = useCreateFlow();
const { state } = useCreateFlow();
const facetKey = section as CreateFlowMethodCardFacetSection;
const { scoresBySlug, hasAnyFacets } = useFacetRecommendations(section);
@@ -33,17 +36,6 @@ export function useMethodCardDeckOrdering(
state.methodSectionsPinCommitted?.[facetKey] === true;
const pinActive = Boolean(pinStored && selectedIds.length > 0);
useEffect(() => {
if (selectedIds.length > 0) return;
if (!pinStored) return;
setMethodSectionsPinCommitted(facetKey, false);
}, [
facetKey,
pinStored,
selectedIds.length,
setMethodSectionsPinCommitted,
]);
const rankedMethods = useMemo(
() => rankMethodsByScore(methods, scoresBySlug),
[methods, scoresBySlug],
@@ -6,6 +6,7 @@ import type { ChipOption } from "../../../../components/controls/MultiSelect/Mul
import Create from "../../../../components/modals/Create";
import ContentLockup from "../../../../components/type/ContentLockup";
import { useMessages } from "../../../../contexts/MessagesContext";
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
import { useCreateFlow } from "../../context/CreateFlowContext";
import type {
CommunityStructureChipSnapshotRow,
@@ -57,31 +58,6 @@ function normalizeCoreValuePresets(
});
}
function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[] {
return presets.map((row, i) => ({
id: String(i + 1),
label: row.label,
state: "unselected" as const,
}));
}
function applySavedSelection(
options: ChipOption[],
saved: string[] | undefined,
): ChipOption[] {
const selected = new Set(saved ?? []);
return options.map((opt) =>
opt.state === "custom"
? opt
: {
...opt,
state: selected.has(opt.id)
? ("selected" as const)
: ("unselected" as const),
},
);
}
function selectedIdsFromOptions(options: ChipOption[]): string[] {
return options
.filter((o) => o.state === "selected")
@@ -98,19 +74,6 @@ function chipOptionsToSnapshotRows(
}));
}
function snapshotRowsToChipOptions(
rows: CommunityStructureChipSnapshotRow[] | undefined,
): ChipOption[] | null {
if (!Array.isArray(rows) || rows.length === 0) return null;
return rows.map((r) => ({
id: r.id,
label: r.label,
...(r.state !== undefined
? { state: r.state as ChipOption["state"] }
: {}),
}));
}
const EMPTY_DETAIL: CoreValueDetailEntry = { meaning: "", signals: "" };
/** Create Custom — Core Values (Figma `20264:68378`). Up to five selections; preset list + custom chips. */
@@ -124,15 +87,12 @@ export function CoreValuesSelectScreen() {
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(
() => {
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromPresets(presets),
state.selectedCoreValueIds,
);
},
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(() =>
buildCoreValueChipOptionsFromDraft(
presets,
state.coreValuesChipsSnapshot,
state.selectedCoreValueIds,
),
);
const [activeModalChipId, setActiveModalChipId] = useState<string | null>(
@@ -142,15 +102,18 @@ export function CoreValuesSelectScreen() {
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
if (fromSnap) {
setCoreValueOptions(fromSnap);
return;
}
setCoreValueOptions((prev) =>
applySavedSelection(prev, state.selectedCoreValueIds),
setCoreValueOptions(
buildCoreValueChipOptionsFromDraft(
presets,
state.coreValuesChipsSnapshot,
state.selectedCoreValueIds,
),
);
}, [state.coreValuesChipsSnapshot, state.selectedCoreValueIds]);
}, [
presets,
state.coreValuesChipsSnapshot,
state.selectedCoreValueIds,
]);
/** Sync chips to create-flow draft. Never call `updateState` from inside a `setCoreValueOptions` updater — defer with `queueMicrotask`. */
const syncCoreValuesToDraft = useCallback(
@@ -83,13 +83,6 @@ export type ProfilePageViewProps = {
const profileSectionHeadingClass =
"font-bricolage text-base font-bold leading-[22px] text-[var(--color-content-default-primary)] md:font-inter md:text-xl md:font-bold md:leading-7 xl:font-bricolage-grotesque xl:font-bold xl:text-[28px] xl:leading-9";
/**
* Sticky `top` for page content below the product {@link Top} (standard variant).
* Must match `Top.view.tsx`: nav `h` 40px → `lg` 84px → `xl` 88px, plus `header` `border-b` (+1px).
*/
const stickyBelowTopTopClass =
"top-[41px] lg:top-[85px] xl:top-[89px]";
export type ProfilePageSignedOutViewProps = {
onSignIn: () => void;
/** `min-width: 1024px` — welcome uses {@link HeaderLockup} `L` per Figma `21962:17220`. */
@@ -113,8 +106,8 @@ export function ProfilePageSignedOutView({
<header
className={
profileLgUp
? `sticky z-10 bg-[var(--color-surface-default-primary)] ${stickyBelowTopTopClass}`
: `flex flex-col gap-1 py-3 md:sticky md:top-[41px] md:z-10 md:bg-[var(--color-surface-default-primary)]`
? "sticky top-0 z-10 bg-[var(--color-surface-default-primary)]"
: "flex flex-col gap-1 py-3 md:sticky md:top-0 md:z-10 md:bg-[var(--color-surface-default-primary)]"
}
>
{profileLgUp ? (
@@ -266,8 +259,8 @@ export function ProfilePageView({
<header
className={
profileLgUp
? `lg:sticky lg:z-10 lg:bg-[var(--color-surface-default-primary)] lg:top-[85px] xl:top-[89px]`
: `flex flex-col gap-1 py-3 md:sticky md:top-[41px] md:z-10 md:bg-[var(--color-surface-default-primary)]`
? "lg:sticky lg:top-0 lg:z-10 lg:bg-[var(--color-surface-default-primary)]"
: "flex flex-col gap-1 py-3 md:sticky md:top-0 md:z-10 md:bg-[var(--color-surface-default-primary)]"
}
>
{profileLgUp ? (
@@ -89,9 +89,9 @@ function MultiSelectView({
className={
!addButtonText
? // Circular button with border (Rule style)
`bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
`cursor-pointer bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
`flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${
`cursor-pointer flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${
isSmall
? "gap-[var(--measures-spacing-100,4px)] px-[var(--measures-spacing-300,12px)] py-[var(--measures-spacing-200,8px)]"
: "gap-[var(--measures-spacing-150,6px)] px-[var(--space-400,16px)] py-[var(--measures-spacing-300,12px)]"