QA pass: layout, create-flow, and About books #68
@@ -47,7 +47,15 @@ When your changes create orphans:
|
|||||||
|
|
||||||
The test: Every changed line should trace directly to the user's request.
|
The test: Every changed line should trace directly to the user's request.
|
||||||
|
|
||||||
## 4. Goal-Driven Execution
|
## 4. Comments, commits, and PRs
|
||||||
|
|
||||||
|
**Let the code talk. Keep tickets out of prose.**
|
||||||
|
|
||||||
|
- No comment when the names already say it.
|
||||||
|
- No Linear IDs (`CR-123`) in comments, commit messages, or PR titles/bodies. Branch names still use `adilallo/<type>/CR-<n>-…` (see `branch-naming.mdc`). Link the issue from Linear, not from the PR copy.
|
||||||
|
- Don't narrate Figma→code mappings the prop already encodes (`palette="inverse"` needs no "this is Invert" comment).
|
||||||
|
|
||||||
|
## 5. Goal-Driven Execution
|
||||||
|
|
||||||
**Define success criteria. Loop until verified.**
|
**Define success criteria. Loop until verified.**
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ npm-cache/
|
|||||||
!public/favicon-32x32.png
|
!public/favicon-32x32.png
|
||||||
!public/apple-touch-icon.png
|
!public/apple-touch-icon.png
|
||||||
|
|
||||||
|
# Per-article body ornaments (Figma Content page Template image fills)
|
||||||
|
!public/content/blog/*-ornament-*.png
|
||||||
|
|
||||||
|
# Marketing book cover (raster; see docs/guides/static-assets.md)
|
||||||
|
!public/assets/marketing/community-rules-cover.png
|
||||||
|
|
||||||
# Visual regression snapshots (allow these)
|
# Visual regression snapshots (allow these)
|
||||||
!tests/e2e/visual-regression.spec.ts-snapshots/
|
!tests/e2e/visual-regression.spec.ts-snapshots/
|
||||||
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
|
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ priors — they reflect deliberate decisions.
|
|||||||
6. **No new pathname-sniffing chrome.** Compose chrome via group/nested
|
6. **No new pathname-sniffing chrome.** Compose chrome via group/nested
|
||||||
layouts, not `usePathname()` checks. (`ConditionalNavigation` is the
|
layouts, not `usePathname()` checks. (`ConditionalNavigation` is the
|
||||||
sole tolerated exception — it carries SSR session state.)
|
sole tolerated exception — it carries SSR session state.)
|
||||||
|
7. **No Linear ticket IDs in comments, commits, or PR copy.** Branch names
|
||||||
|
still carry `CR-<n>`. See `coding-guidelines.mdc`.
|
||||||
|
|
||||||
## Legacy / scaffolding
|
## Legacy / scaffolding
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import {
|
|||||||
CREATE_FLOW_FOOTER_BUTTON_CLASS,
|
CREATE_FLOW_FOOTER_BUTTON_CLASS,
|
||||||
CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS,
|
CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS,
|
||||||
} from "./utils/createFlowFooterClassNames";
|
} from "./utils/createFlowFooterClassNames";
|
||||||
|
import { CREATE_FLOW_MD_CENTERED_MAIN_CLASS } from "./components/createFlowLayoutTokens";
|
||||||
import {
|
import {
|
||||||
CUSTOM_RULE_CONFIRM_FOOTER_STEP_BY_STEP,
|
CUSTOM_RULE_CONFIRM_FOOTER_STEP_BY_STEP,
|
||||||
methodCardFacetSectionForConfirmStep,
|
methodCardFacetSectionForConfirmStep,
|
||||||
@@ -83,6 +84,7 @@ import { SignedInDraftHydration } from "./SignedInDraftHydration";
|
|||||||
import { CreateFlowPendingAvatarFlush } from "./components/CreateFlowPendingAvatarFlush";
|
import { CreateFlowPendingAvatarFlush } from "./components/CreateFlowPendingAvatarFlush";
|
||||||
import Alert from "../../components/modals/Alert";
|
import Alert from "../../components/modals/Alert";
|
||||||
import Create from "../../components/modals/Create";
|
import Create from "../../components/modals/Create";
|
||||||
|
import { MODAL_FOOTER_START_SLOT_CLASS } from "../../components/modals/ModalFooter";
|
||||||
import Share from "../../components/modals/Share";
|
import Share from "../../components/modals/Share";
|
||||||
import {
|
import {
|
||||||
CreateFlowDraftSaveBannerProvider,
|
CreateFlowDraftSaveBannerProvider,
|
||||||
@@ -467,13 +469,13 @@ function CreateFlowLayoutContent({
|
|||||||
);
|
);
|
||||||
const stepIdx = currentStep != null ? getStepIndex(currentStep) : -1;
|
const stepIdx = currentStep != null ? getStepIndex(currentStep) : -1;
|
||||||
|
|
||||||
/** At `md+`, main cross-axis: center by default; exceptions stay top-aligned (see product spec). */
|
/** Lockup+card / card-stack: `items-start` + shell `my-auto` so overflow scrolls from the top. */
|
||||||
const mainContentClass = isCompletedStep
|
const mainContentClass = isCompletedStep
|
||||||
? "items-stretch overflow-y-auto md:overflow-hidden"
|
? "items-stretch overflow-y-auto md:overflow-hidden"
|
||||||
: isSelectSplitScrollStep
|
: isSelectSplitScrollStep
|
||||||
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
|
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
|
||||||
: isFinalReviewLike || isCardLayoutStep || isTemplateReviewRoute
|
: isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep
|
||||||
? "items-start justify-center overflow-y-auto"
|
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
|
||||||
: "items-start justify-center overflow-y-auto md:items-center";
|
: "items-start justify-center overflow-y-auto md:items-center";
|
||||||
|
|
||||||
const isTextStep = createFlowStepUsesCenteredTextLayout(currentStep);
|
const isTextStep = createFlowStepUsesCenteredTextLayout(currentStep);
|
||||||
@@ -636,14 +638,16 @@ function CreateFlowLayoutContent({
|
|||||||
nextButtonText={messages.create.topNav.leaveConfirmProceed}
|
nextButtonText={messages.create.topNav.leaveConfirmProceed}
|
||||||
onNext={() => closeLeaveConfirm(true)}
|
onNext={() => closeLeaveConfirm(true)}
|
||||||
footerContent={
|
footerContent={
|
||||||
<Button
|
<div className={MODAL_FOOTER_START_SLOT_CLASS}>
|
||||||
buttonType="ghost"
|
<Button
|
||||||
palette="default"
|
buttonType="ghost"
|
||||||
size="xsmall"
|
palette="default"
|
||||||
onClick={() => closeLeaveConfirm(false)}
|
size="xsmall"
|
||||||
>
|
onClick={() => closeLeaveConfirm(false)}
|
||||||
{messages.create.topNav.leaveConfirmCancel}
|
>
|
||||||
</Button>
|
{messages.create.topNav.leaveConfirmCancel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
ariaLabel={messages.create.topNav.leaveConfirmTitle}
|
ariaLabel={messages.create.topNav.leaveConfirmTitle}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function ApplicableScopeFieldComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-2 ${className}`.trim()}>
|
<div className={`flex flex-col gap-2 ${className}`.trim()}>
|
||||||
<InputLabel label={label} helpIcon size="s" palette="default" />
|
<InputLabel label={label} size="s" palette="default" />
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{scopes.map((scope) => {
|
{scopes.map((scope) => {
|
||||||
const isSelected = selectedScopes.includes(scope);
|
const isSelected = selectedScopes.includes(scope);
|
||||||
@@ -84,6 +84,7 @@ function ApplicableScopeFieldComponent({
|
|||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onClick={() => !readOnly && onToggleScope(scope)}
|
onClick={() => !readOnly && onToggleScope(scope)}
|
||||||
ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`}
|
ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`}
|
||||||
|
className="max-w-full"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ReactNode } from "react";
|
|||||||
import { CreateFlowHeaderLockup } from "./CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "./CreateFlowHeaderLockup";
|
||||||
import { CreateFlowStepShell } from "./CreateFlowStepShell";
|
import { CreateFlowStepShell } from "./CreateFlowStepShell";
|
||||||
import {
|
import {
|
||||||
|
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
|
||||||
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||||
} from "./createFlowLayoutTokens";
|
} from "./createFlowLayoutTokens";
|
||||||
@@ -18,14 +19,18 @@ type CreateFlowLockupCardStepShellProps = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Final-review layout: `wideGrid`, two columns from `md:`, column widths from `createFlowLayoutTokens`. */
|
/** Lockup + card: `wideGrid`, two columns from `md:`, centered in the nav–footer band from `md`. */
|
||||||
export function CreateFlowLockupCardStepShell({
|
export function CreateFlowLockupCardStepShell({
|
||||||
lockupTitle,
|
lockupTitle,
|
||||||
lockupDescription,
|
lockupDescription,
|
||||||
children,
|
children,
|
||||||
}: CreateFlowLockupCardStepShellProps) {
|
}: CreateFlowLockupCardStepShellProps) {
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
<CreateFlowStepShell
|
||||||
|
variant="wideGrid"
|
||||||
|
contentTopBelowMd="space-800"
|
||||||
|
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`mx-auto flex w-full min-w-0 flex-col gap-4 md:grid md:w-full md:grid-cols-2 md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
className={`mx-auto flex w-full min-w-0 flex-col gap-4 md:grid md:w-full md:grid-cols-2 md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-2
@@ -60,7 +60,6 @@ function CustomMethodCardFieldBlocksSummaryViewComponent({
|
|||||||
<div key={block.id} className="flex flex-col gap-2">
|
<div key={block.id} className="flex flex-col gap-2">
|
||||||
<InputLabel
|
<InputLabel
|
||||||
label={block.blockTitle}
|
label={block.blockTitle}
|
||||||
helpIcon
|
|
||||||
size="s"
|
size="s"
|
||||||
palette="default"
|
palette="default"
|
||||||
/>
|
/>
|
||||||
@@ -123,7 +122,6 @@ function CustomMethodCardFieldBlocksSummaryViewComponent({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<InputLabel
|
<InputLabel
|
||||||
label={block.blockTitle}
|
label={block.blockTitle}
|
||||||
helpIcon
|
|
||||||
size="s"
|
size="s"
|
||||||
palette="default"
|
palette="default"
|
||||||
/>
|
/>
|
||||||
@@ -169,6 +167,7 @@ function CustomMethodCardFieldBlocksSummaryViewComponent({
|
|||||||
<IncrementerBlock
|
<IncrementerBlock
|
||||||
key={block.id}
|
key={block.id}
|
||||||
label={block.blockTitle}
|
label={block.blockTitle}
|
||||||
|
helpIcon={false}
|
||||||
value={block.defaultPercent}
|
value={block.defaultPercent}
|
||||||
min={1}
|
min={1}
|
||||||
max={100}
|
max={100}
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,6 @@ function CustomMethodCardUploadBlockRowViewComponent({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<InputLabel
|
<InputLabel
|
||||||
label={block.blockTitle}
|
label={block.blockTitle}
|
||||||
helpIcon
|
|
||||||
size="s"
|
size="s"
|
||||||
palette="default"
|
palette="default"
|
||||||
/>
|
/>
|
||||||
@@ -77,6 +76,7 @@ function CustomMethodCardUploadBlockRowViewComponent({
|
|||||||
) : (
|
) : (
|
||||||
<Upload
|
<Upload
|
||||||
active={!busy}
|
active={!busy}
|
||||||
|
showHelpIcon={false}
|
||||||
hintText={busy ? uploadingHint : uploadHint}
|
hintText={busy ? uploadingHint : uploadHint}
|
||||||
onClick={onUploadClick}
|
onClick={onUploadClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+228
-15
@@ -5,8 +5,12 @@ import {
|
|||||||
useMessages,
|
useMessages,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from "../../../../contexts/MessagesContext";
|
} from "../../../../contexts/MessagesContext";
|
||||||
|
import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
|
||||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
import { CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS } from "../../../../../lib/create/customMethodCardWizardConstants";
|
import {
|
||||||
|
CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS,
|
||||||
|
CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS,
|
||||||
|
} from "../../../../../lib/create/customMethodCardWizardConstants";
|
||||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||||
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
||||||
import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view";
|
import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view";
|
||||||
@@ -17,12 +21,13 @@ import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types
|
|||||||
* `20066:14748`, `20094:48551`, `20066:14361`).
|
* `20066:14748`, `20094:48551`, `20066:14361`).
|
||||||
*/
|
*/
|
||||||
const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||||
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile }) => {
|
({ isOpen, onClose, onFinalize, onPersistCustomUploadFile, initialValues }) => {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const t = useTranslation("common");
|
const t = useTranslation("common");
|
||||||
const tUpload = useTranslation("create.upload");
|
const tUpload = useTranslation("create.upload");
|
||||||
const w = m.create.customRule.customMethodCardWizard;
|
const w = m.create.customRule.customMethodCardWizard;
|
||||||
const menuCopy = m.create.customRule.modalKebabMenu;
|
const menuCopy = m.create.customRule.modalKebabMenu;
|
||||||
|
const { requestConfirm, confirmDialog } = useAsyncConfirm();
|
||||||
|
|
||||||
const copy = useMemo(
|
const copy = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -68,6 +73,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[]
|
CustomMethodCardFieldBlock[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [textBlockTitle, setTextBlockTitle] = useState("");
|
const [textBlockTitle, setTextBlockTitle] = useState("");
|
||||||
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
|
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
|
||||||
@@ -88,6 +94,14 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
const [proportionDefault, setProportionDefault] = useState(50);
|
const [proportionDefault, setProportionDefault] = useState(50);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const initialValuesRef = useRef(initialValues);
|
||||||
|
initialValuesRef.current = initialValues;
|
||||||
|
const openSnapshotRef = useRef<{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
fieldBlocks: string;
|
||||||
|
} | null>(null);
|
||||||
|
const fieldModalSnapshotRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const resetFieldTypeDrafts = useCallback(() => {
|
const resetFieldTypeDrafts = useCallback(() => {
|
||||||
setTextBlockTitle("");
|
setTextBlockTitle("");
|
||||||
@@ -112,21 +126,112 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
setPolicyDescription("");
|
setPolicyDescription("");
|
||||||
setAddFieldExpanded(false);
|
setAddFieldExpanded(false);
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
setDraftFieldBlocks([]);
|
setDraftFieldBlocks([]);
|
||||||
|
openSnapshotRef.current = null;
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
resetFieldTypeDrafts();
|
resetFieldTypeDrafts();
|
||||||
}, [resetFieldTypeDrafts]);
|
}, [resetFieldTypeDrafts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
reset();
|
reset();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [isOpen, reset]);
|
const init = initialValuesRef.current;
|
||||||
|
setWizardStep(1);
|
||||||
|
setPolicyTitle(init?.title ?? "");
|
||||||
|
setPolicyDescription(init?.description ?? "");
|
||||||
|
setAddFieldExpanded(false);
|
||||||
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
|
setDraftFieldBlocks(
|
||||||
|
init?.fieldBlocks ? structuredClone(init.fieldBlocks) : [],
|
||||||
|
);
|
||||||
|
openSnapshotRef.current = {
|
||||||
|
title: init?.title ?? "",
|
||||||
|
description: init?.description ?? "",
|
||||||
|
fieldBlocks: JSON.stringify(init?.fieldBlocks ?? []),
|
||||||
|
};
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
|
resetFieldTypeDrafts();
|
||||||
|
}, [isOpen, reset, resetFieldTypeDrafts]);
|
||||||
|
|
||||||
const dismiss = useCallback(() => {
|
const dismiss = useCallback(() => {
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose, reset]);
|
}, [onClose, reset]);
|
||||||
|
|
||||||
|
const fieldModalDraftSignature = useCallback(() => {
|
||||||
|
return JSON.stringify({
|
||||||
|
fieldTypeModal,
|
||||||
|
textBlockTitle,
|
||||||
|
textPlaceholderBody,
|
||||||
|
badgeBlockTitle,
|
||||||
|
badgeOptions,
|
||||||
|
uploadBlockTitle,
|
||||||
|
uploadFileName,
|
||||||
|
uploadAssetUrl,
|
||||||
|
proportionBlockTitle,
|
||||||
|
proportionDefault,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
badgeBlockTitle,
|
||||||
|
badgeOptions,
|
||||||
|
fieldTypeModal,
|
||||||
|
proportionBlockTitle,
|
||||||
|
proportionDefault,
|
||||||
|
textBlockTitle,
|
||||||
|
textPlaceholderBody,
|
||||||
|
uploadAssetUrl,
|
||||||
|
uploadBlockTitle,
|
||||||
|
uploadFileName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isWizardSessionDirty = useCallback(() => {
|
||||||
|
const snap = openSnapshotRef.current;
|
||||||
|
if (!snap) return false;
|
||||||
|
if (policyTitle !== snap.title || policyDescription !== snap.description) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (JSON.stringify(draftFieldBlocks) !== snap.fieldBlocks) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fieldTypeModal &&
|
||||||
|
fieldModalSnapshotRef.current != null &&
|
||||||
|
fieldModalDraftSignature() !== fieldModalSnapshotRef.current
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [
|
||||||
|
draftFieldBlocks,
|
||||||
|
fieldModalDraftSignature,
|
||||||
|
fieldTypeModal,
|
||||||
|
policyDescription,
|
||||||
|
policyTitle,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const confirmAbandonWizardEdits = useCallback(async () => {
|
||||||
|
if (!isWizardSessionDirty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return requestConfirm({
|
||||||
|
title: menuCopy.discardUnsavedCustomizeChangesTitle,
|
||||||
|
description: menuCopy.discardUnsavedCustomizeChangesDescription,
|
||||||
|
proceedText: menuCopy.discardUnsavedCustomizeChangesProceed,
|
||||||
|
cancelText: menuCopy.discardUnsavedCustomizeChangesCancel,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
isWizardSessionDirty,
|
||||||
|
menuCopy.discardUnsavedCustomizeChangesCancel,
|
||||||
|
menuCopy.discardUnsavedCustomizeChangesDescription,
|
||||||
|
menuCopy.discardUnsavedCustomizeChangesProceed,
|
||||||
|
menuCopy.discardUnsavedCustomizeChangesTitle,
|
||||||
|
requestConfirm,
|
||||||
|
]);
|
||||||
|
|
||||||
const titleTrim = policyTitle.trim();
|
const titleTrim = policyTitle.trim();
|
||||||
const descriptionTrim = policyDescription.trim();
|
const descriptionTrim = policyDescription.trim();
|
||||||
|
|
||||||
@@ -136,7 +241,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||||
const descriptionOk =
|
const descriptionOk =
|
||||||
descriptionTrim.length > 0 &&
|
descriptionTrim.length > 0 &&
|
||||||
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS;
|
||||||
if (wizardStep === 1) return titleOk;
|
if (wizardStep === 1) return titleOk;
|
||||||
if (wizardStep === 2) return descriptionOk;
|
if (wizardStep === 2) return descriptionOk;
|
||||||
return titleOk && descriptionOk;
|
return titleOk && descriptionOk;
|
||||||
@@ -213,7 +318,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
const shellDescription = fieldModalHeader?.description ?? headerDescription;
|
const shellDescription = fieldModalHeader?.description ?? headerDescription;
|
||||||
|
|
||||||
const nextLabel = fieldTypeModal
|
const nextLabel = fieldTypeModal
|
||||||
? copy.fieldModals.addField
|
? editingBlockId
|
||||||
|
? copy.fieldModals.saveField
|
||||||
|
: copy.fieldModals.addField
|
||||||
: wizardStep === 3
|
: wizardStep === 3
|
||||||
? copy.footerFinalize
|
? copy.footerFinalize
|
||||||
: t("buttons.next");
|
: t("buttons.next");
|
||||||
@@ -222,33 +329,125 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
? !fieldModalStepValid
|
? !fieldModalStepValid
|
||||||
: !stepValid;
|
: !stepValid;
|
||||||
|
|
||||||
const handleShellClose = useCallback(() => {
|
const handleShellClose = useCallback(async () => {
|
||||||
if (fieldTypeModal) {
|
if (fieldTypeModal) {
|
||||||
|
if (
|
||||||
|
fieldModalSnapshotRef.current != null &&
|
||||||
|
fieldModalDraftSignature() !== fieldModalSnapshotRef.current &&
|
||||||
|
!(await confirmAbandonWizardEdits())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(await confirmAbandonWizardEdits())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
}, [dismiss, fieldTypeModal]);
|
}, [
|
||||||
|
confirmAbandonWizardEdits,
|
||||||
|
dismiss,
|
||||||
|
fieldModalDraftSignature,
|
||||||
|
fieldTypeModal,
|
||||||
|
]);
|
||||||
|
|
||||||
const kebabMenuItems = useMemo<ModalHeaderMenuItem[]>(() => [], []);
|
const kebabMenuItems = useMemo<ModalHeaderMenuItem[]>(() => [], []);
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(async () => {
|
||||||
if (fieldTypeModal) {
|
if (fieldTypeModal) {
|
||||||
|
if (
|
||||||
|
fieldModalSnapshotRef.current != null &&
|
||||||
|
fieldModalDraftSignature() !== fieldModalSnapshotRef.current &&
|
||||||
|
!(await confirmAbandonWizardEdits())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (wizardStep === 1) {
|
if (wizardStep === 1) {
|
||||||
|
if (!(await confirmAbandonWizardEdits())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setWizardStep((s) => (s === 2 ? 1 : 2));
|
setWizardStep((s) => (s === 2 ? 1 : 2));
|
||||||
}, [dismiss, fieldTypeModal, wizardStep]);
|
}, [
|
||||||
|
confirmAbandonWizardEdits,
|
||||||
|
dismiss,
|
||||||
|
fieldModalDraftSignature,
|
||||||
|
fieldTypeModal,
|
||||||
|
wizardStep,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
|
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
|
||||||
|
setEditingBlockId(null);
|
||||||
|
setAddFieldExpanded(false);
|
||||||
resetFieldTypeDrafts();
|
resetFieldTypeDrafts();
|
||||||
setFieldTypeModal(ft);
|
setFieldTypeModal(ft);
|
||||||
|
fieldModalSnapshotRef.current = JSON.stringify({
|
||||||
|
fieldTypeModal: ft,
|
||||||
|
textBlockTitle: "",
|
||||||
|
textPlaceholderBody: "",
|
||||||
|
badgeBlockTitle: "",
|
||||||
|
badgeOptions: [],
|
||||||
|
uploadBlockTitle: "",
|
||||||
|
uploadFileName: undefined,
|
||||||
|
uploadAssetUrl: undefined,
|
||||||
|
proportionBlockTitle: "",
|
||||||
|
proportionDefault: 50,
|
||||||
|
});
|
||||||
}, [resetFieldTypeDrafts]);
|
}, [resetFieldTypeDrafts]);
|
||||||
|
|
||||||
|
const handleEditFieldBlock = useCallback(
|
||||||
|
(block: CustomMethodCardFieldBlock) => {
|
||||||
|
resetFieldTypeDrafts();
|
||||||
|
setEditingBlockId(block.id);
|
||||||
|
setAddFieldExpanded(false);
|
||||||
|
switch (block.kind) {
|
||||||
|
case "text":
|
||||||
|
setTextBlockTitle(block.blockTitle);
|
||||||
|
setTextPlaceholderBody(block.placeholderText);
|
||||||
|
break;
|
||||||
|
case "badges":
|
||||||
|
setBadgeBlockTitle(block.blockTitle);
|
||||||
|
setBadgeOptions([...block.options]);
|
||||||
|
break;
|
||||||
|
case "upload":
|
||||||
|
setUploadBlockTitle(block.blockTitle);
|
||||||
|
setUploadFileName(block.fileName);
|
||||||
|
setUploadAssetUrl(block.assetUrl);
|
||||||
|
break;
|
||||||
|
case "proportion":
|
||||||
|
setProportionBlockTitle(block.blockTitle);
|
||||||
|
setProportionDefault(block.defaultPercent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setFieldTypeModal(block.kind);
|
||||||
|
fieldModalSnapshotRef.current = JSON.stringify({
|
||||||
|
fieldTypeModal: block.kind,
|
||||||
|
textBlockTitle: block.kind === "text" ? block.blockTitle : "",
|
||||||
|
textPlaceholderBody:
|
||||||
|
block.kind === "text" ? block.placeholderText : "",
|
||||||
|
badgeBlockTitle: block.kind === "badges" ? block.blockTitle : "",
|
||||||
|
badgeOptions: block.kind === "badges" ? [...block.options] : [],
|
||||||
|
uploadBlockTitle: block.kind === "upload" ? block.blockTitle : "",
|
||||||
|
uploadFileName: block.kind === "upload" ? block.fileName : undefined,
|
||||||
|
uploadAssetUrl: block.kind === "upload" ? block.assetUrl : undefined,
|
||||||
|
proportionBlockTitle:
|
||||||
|
block.kind === "proportion" ? block.blockTitle : "",
|
||||||
|
proportionDefault: block.kind === "proportion" ? block.defaultPercent : 50,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[resetFieldTypeDrafts],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFileChosen = useCallback(
|
const handleFileChosen = useCallback(
|
||||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -285,9 +484,9 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const appendFieldBlock = useCallback(() => {
|
const commitFieldBlock = useCallback(() => {
|
||||||
if (!fieldTypeModal || !fieldModalStepValid) return;
|
if (!fieldTypeModal || !fieldModalStepValid) return;
|
||||||
const id = crypto.randomUUID();
|
const id = editingBlockId ?? crypto.randomUUID();
|
||||||
let block: CustomMethodCardFieldBlock;
|
let block: CustomMethodCardFieldBlock;
|
||||||
switch (fieldTypeModal) {
|
switch (fieldTypeModal) {
|
||||||
case "text":
|
case "text":
|
||||||
@@ -325,11 +524,20 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
defaultPercent: proportionDefault,
|
defaultPercent: proportionDefault,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setDraftFieldBlocks((prev) => [...prev, block]);
|
setDraftFieldBlocks((prev) => {
|
||||||
|
if (editingBlockId) {
|
||||||
|
return prev.map((row) => (row.id === editingBlockId ? block : row));
|
||||||
|
}
|
||||||
|
return [...prev, block];
|
||||||
|
});
|
||||||
setFieldTypeModal(null);
|
setFieldTypeModal(null);
|
||||||
|
setEditingBlockId(null);
|
||||||
|
setAddFieldExpanded(false);
|
||||||
|
fieldModalSnapshotRef.current = null;
|
||||||
}, [
|
}, [
|
||||||
badgeBlockTitle,
|
badgeBlockTitle,
|
||||||
badgeOptions,
|
badgeOptions,
|
||||||
|
editingBlockId,
|
||||||
fieldModalStepValid,
|
fieldModalStepValid,
|
||||||
fieldTypeModal,
|
fieldTypeModal,
|
||||||
proportionBlockTitle,
|
proportionBlockTitle,
|
||||||
@@ -343,7 +551,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
if (fieldTypeModal) {
|
if (fieldTypeModal) {
|
||||||
appendFieldBlock();
|
commitFieldBlock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!stepValid) return;
|
if (!stepValid) return;
|
||||||
@@ -358,7 +566,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
}
|
}
|
||||||
setWizardStep((s) => (s === 1 ? 2 : 3));
|
setWizardStep((s) => (s === 1 ? 2 : 3));
|
||||||
}, [
|
}, [
|
||||||
appendFieldBlock,
|
commitFieldBlock,
|
||||||
descriptionTrim,
|
descriptionTrim,
|
||||||
dismiss,
|
dismiss,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
@@ -370,6 +578,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<CustomMethodCardWizardView
|
<CustomMethodCardWizardView
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onDismiss={handleShellClose}
|
onDismiss={handleShellClose}
|
||||||
@@ -380,7 +589,8 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
policyDescription={policyDescription}
|
policyDescription={policyDescription}
|
||||||
addFieldExpanded={addFieldExpanded}
|
addFieldExpanded={addFieldExpanded}
|
||||||
copy={copy}
|
copy={copy}
|
||||||
maxChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
maxTitleChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||||
|
maxDescriptionChars={CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS}
|
||||||
onPolicyTitleChange={setPolicyTitle}
|
onPolicyTitleChange={setPolicyTitle}
|
||||||
onPolicyDescriptionChange={setPolicyDescription}
|
onPolicyDescriptionChange={setPolicyDescription}
|
||||||
onPressAddCustomField={() => setAddFieldExpanded(true)}
|
onPressAddCustomField={() => setAddFieldExpanded(true)}
|
||||||
@@ -420,10 +630,13 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
stepper={!fieldTypeModal}
|
stepper={!fieldTypeModal}
|
||||||
draftFieldBlocks={draftFieldBlocks}
|
draftFieldBlocks={draftFieldBlocks}
|
||||||
onDraftFieldBlocksReorder={setDraftFieldBlocks}
|
onDraftFieldBlocksReorder={setDraftFieldBlocks}
|
||||||
|
onEditFieldBlock={handleEditFieldBlock}
|
||||||
kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel}
|
kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel}
|
||||||
kebabMenuAriaLabel={menuCopy.menuAriaLabel}
|
kebabMenuAriaLabel={menuCopy.menuAriaLabel}
|
||||||
kebabMenuItems={kebabMenuItems}
|
kebabMenuItems={kebabMenuItems}
|
||||||
/>
|
/>
|
||||||
|
{confirmDialog}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
|
|||||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||||
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types";
|
||||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
|
||||||
export interface CustomMethodCardWizardFieldBodiesCopy {
|
export interface CustomMethodCardWizardFieldBodiesCopy {
|
||||||
requiredHint: string;
|
requiredHint: string;
|
||||||
@@ -47,6 +48,7 @@ export interface CustomMethodCardWizardCopy {
|
|||||||
footerFinalize: string;
|
footerFinalize: string;
|
||||||
fieldModals: {
|
fieldModals: {
|
||||||
addField: string;
|
addField: string;
|
||||||
|
saveField: string;
|
||||||
requiredHint: string;
|
requiredHint: string;
|
||||||
text: CustomMethodCardWizardFieldBodiesCopy["text"] & {
|
text: CustomMethodCardWizardFieldBodiesCopy["text"] & {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -70,6 +72,8 @@ export interface CustomMethodCardWizardCopy {
|
|||||||
export interface CustomMethodCardWizardProps {
|
export interface CustomMethodCardWizardProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** When set, seeds title, description, and field blocks; wizard still starts at step 1. */
|
||||||
|
initialValues?: MethodCardWizardInitialValues | null;
|
||||||
/** Called when the user completes step 3; parent assigns id and persists state. */
|
/** Called when the user completes step 3; parent assigns id and persists state. */
|
||||||
onFinalize: (payload: {
|
onFinalize: (payload: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -123,7 +127,8 @@ export interface CustomMethodCardWizardViewProps {
|
|||||||
policyDescription: string;
|
policyDescription: string;
|
||||||
addFieldExpanded: boolean;
|
addFieldExpanded: boolean;
|
||||||
copy: CustomMethodCardWizardCopy;
|
copy: CustomMethodCardWizardCopy;
|
||||||
maxChars: number;
|
maxTitleChars: number;
|
||||||
|
maxDescriptionChars: number;
|
||||||
onPolicyTitleChange: (v: string) => void;
|
onPolicyTitleChange: (v: string) => void;
|
||||||
onPolicyDescriptionChange: (v: string) => void;
|
onPolicyDescriptionChange: (v: string) => void;
|
||||||
onPressAddCustomField: () => void;
|
onPressAddCustomField: () => void;
|
||||||
@@ -136,6 +141,7 @@ export interface CustomMethodCardWizardViewProps {
|
|||||||
>;
|
>;
|
||||||
draftFieldBlocks: CustomMethodCardFieldBlock[];
|
draftFieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
onDraftFieldBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
onDraftFieldBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||||
|
onEditFieldBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||||
nextDisabled: boolean;
|
nextDisabled: boolean;
|
||||||
nextLabel: string;
|
nextLabel: string;
|
||||||
showBackButton: boolean;
|
showBackButton: boolean;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
policyDescription,
|
policyDescription,
|
||||||
addFieldExpanded,
|
addFieldExpanded,
|
||||||
copy,
|
copy,
|
||||||
maxChars,
|
maxTitleChars,
|
||||||
|
maxDescriptionChars,
|
||||||
onPolicyTitleChange,
|
onPolicyTitleChange,
|
||||||
onPolicyDescriptionChange,
|
onPolicyDescriptionChange,
|
||||||
onPressAddCustomField,
|
onPressAddCustomField,
|
||||||
@@ -35,6 +36,7 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
stepper,
|
stepper,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
onDraftFieldBlocksReorder,
|
onDraftFieldBlocksReorder,
|
||||||
|
onEditFieldBlock,
|
||||||
kebabMoreOptionsAriaLabel,
|
kebabMoreOptionsAriaLabel,
|
||||||
kebabMenuAriaLabel,
|
kebabMenuAriaLabel,
|
||||||
kebabMenuItems,
|
kebabMenuItems,
|
||||||
@@ -71,7 +73,7 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
placeholder={copy.step1.fieldPlaceholder}
|
placeholder={copy.step1.fieldPlaceholder}
|
||||||
value={policyTitle}
|
value={policyTitle}
|
||||||
onChange={onPolicyTitleChange}
|
onChange={onPolicyTitleChange}
|
||||||
maxLength={maxChars}
|
maxLength={maxTitleChars}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!fieldTypeModal && wizardStep === 2 ? (
|
{!fieldTypeModal && wizardStep === 2 ? (
|
||||||
@@ -80,15 +82,15 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
formHeader={false}
|
formHeader={false}
|
||||||
placeholder={copy.step2.fieldPlaceholder}
|
placeholder={copy.step2.fieldPlaceholder}
|
||||||
value={policyDescription}
|
value={policyDescription}
|
||||||
maxLength={maxChars}
|
maxLength={maxDescriptionChars}
|
||||||
onChange={(e) => onPolicyDescriptionChange(e.target.value)}
|
onChange={(e) => onPolicyDescriptionChange(e.target.value)}
|
||||||
textHint={`${policyDescription.length}/${maxChars}`}
|
textHint={`${policyDescription.length}/${maxDescriptionChars}`}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!fieldTypeModal && wizardStep === 3 ? (
|
{!fieldTypeModal && wizardStep === 3 ? (
|
||||||
<div className="flex w-full flex-col gap-4 pt-1">
|
<div className="flex w-full flex-col gap-6">
|
||||||
{draftFieldBlocks.length > 0 ? (
|
{draftFieldBlocks.length > 0 ? (
|
||||||
<CustomMethodCardWizardBlocksList
|
<CustomMethodCardWizardBlocksList
|
||||||
blocks={draftFieldBlocks}
|
blocks={draftFieldBlocks}
|
||||||
@@ -96,6 +98,7 @@ function CustomMethodCardWizardViewComponent({
|
|||||||
dragHandleAriaLabel={copy.step3BlocksList.dragHandleAriaLabel}
|
dragHandleAriaLabel={copy.step3BlocksList.dragHandleAriaLabel}
|
||||||
listLabel={copy.step3BlocksList.listLabel}
|
listLabel={copy.step3BlocksList.listLabel}
|
||||||
onBlocksReorder={onDraftFieldBlocksReorder}
|
onBlocksReorder={onDraftFieldBlocksReorder}
|
||||||
|
onEditBlock={onEditFieldBlock}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<AddCustomField
|
<AddCustomField
|
||||||
|
|||||||
+22
-3
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo, useCallback, useState, type DragEvent } from "react";
|
import { memo, useCallback, useRef, useState, type DragEvent } from "react";
|
||||||
import { reorderCustomMethodCardFieldBlocks } from "../../../../../lib/create/reorderCustomMethodCardFieldBlocks";
|
import { reorderCustomMethodCardFieldBlocks } from "../../../../../lib/create/reorderCustomMethodCardFieldBlocks";
|
||||||
import { CustomMethodCardWizardBlocksListView } from "./CustomMethodCardWizardBlocksList.view";
|
import { CustomMethodCardWizardBlocksListView } from "./CustomMethodCardWizardBlocksList.view";
|
||||||
import type { CustomMethodCardWizardBlocksListProps } from "./CustomMethodCardWizardBlocksList.types";
|
import type { CustomMethodCardWizardBlocksListProps } from "./CustomMethodCardWizardBlocksList.types";
|
||||||
@@ -11,19 +11,33 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
|||||||
dragHandleAriaLabel,
|
dragHandleAriaLabel,
|
||||||
listLabel,
|
listLabel,
|
||||||
onBlocksReorder,
|
onBlocksReorder,
|
||||||
|
onEditBlock,
|
||||||
}: CustomMethodCardWizardBlocksListProps) {
|
}: CustomMethodCardWizardBlocksListProps) {
|
||||||
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
||||||
const [overIndex, setOverIndex] = useState<number | null>(null);
|
const [overIndex, setOverIndex] = useState<number | null>(null);
|
||||||
|
const draggingIndexRef = useRef<number | null>(null);
|
||||||
|
const dragFromHandleRef = useRef(false);
|
||||||
|
|
||||||
const clearDragUi = useCallback(() => {
|
const clearDragUi = useCallback(() => {
|
||||||
|
draggingIndexRef.current = null;
|
||||||
|
dragFromHandleRef.current = false;
|
||||||
setDraggingIndex(null);
|
setDraggingIndex(null);
|
||||||
setOverIndex(null);
|
setOverIndex(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleHandlePointerDown = useCallback(() => {
|
||||||
|
dragFromHandleRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(index: number) => (e: DragEvent) => {
|
(index: number) => (e: DragEvent) => {
|
||||||
|
if (!dragFromHandleRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
e.dataTransfer.setData("text/plain", String(index));
|
e.dataTransfer.setData("text/plain", String(index));
|
||||||
|
draggingIndexRef.current = index;
|
||||||
setDraggingIndex(index);
|
setDraggingIndex(index);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@@ -40,8 +54,11 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
|||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(index: number) => (e: DragEvent) => {
|
(index: number) => (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const from = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
const fromData = Number.parseInt(e.dataTransfer.getData("text/plain"), 10);
|
||||||
if (Number.isNaN(from)) {
|
const from = Number.isNaN(fromData)
|
||||||
|
? draggingIndexRef.current
|
||||||
|
: fromData;
|
||||||
|
if (from == null || Number.isNaN(from)) {
|
||||||
clearDragUi();
|
clearDragUi();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,6 +83,8 @@ function CustomMethodCardWizardBlocksListContainerComponent({
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragEnd={clearDragUi}
|
onDragEnd={clearDragUi}
|
||||||
|
onHandlePointerDown={handleHandlePointerDown}
|
||||||
|
onEditBlock={onEditBlock}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -8,6 +8,7 @@ export interface CustomMethodCardWizardBlocksListProps {
|
|||||||
dragHandleAriaLabel: string;
|
dragHandleAriaLabel: string;
|
||||||
listLabel: string;
|
listLabel: string;
|
||||||
onBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
onBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||||
|
onEditBlock: (_block: CustomMethodCardFieldBlock) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomMethodCardWizardBlocksListViewProps
|
export interface CustomMethodCardWizardBlocksListViewProps
|
||||||
@@ -18,4 +19,5 @@ export interface CustomMethodCardWizardBlocksListViewProps
|
|||||||
onDragOver: (_index: number) => (_e: DragEvent) => void;
|
onDragOver: (_index: number) => (_e: DragEvent) => void;
|
||||||
onDrop: (_index: number) => (_e: DragEvent) => void;
|
onDrop: (_index: number) => (_e: DragEvent) => void;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
|
onHandlePointerDown: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-8
@@ -38,10 +38,12 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
|||||||
onDragOver,
|
onDragOver,
|
||||||
onDrop,
|
onDrop,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
|
onHandlePointerDown,
|
||||||
|
onEditBlock,
|
||||||
}: CustomMethodCardWizardBlocksListViewProps) {
|
}: CustomMethodCardWizardBlocksListViewProps) {
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
className="flex list-none flex-col gap-2 p-0 pt-1"
|
className="flex list-none flex-col gap-2 p-0"
|
||||||
aria-label={listLabel}
|
aria-label={listLabel}
|
||||||
>
|
>
|
||||||
{blocks.map((block, index) => {
|
{blocks.map((block, index) => {
|
||||||
@@ -53,6 +55,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={block.id}
|
key={block.id}
|
||||||
|
draggable
|
||||||
aria-grabbed={isDragging ? true : undefined}
|
aria-grabbed={isDragging ? true : undefined}
|
||||||
className={`group flex min-h-[52px] items-stretch gap-2 rounded-[var(--measures-radius-medium,8px)] border-2 bg-[var(--color-surface-default-secondary)] py-2 pl-1 pr-3 transition-[border-color,box-shadow,opacity] ${
|
className={`group flex min-h-[52px] items-stretch gap-2 rounded-[var(--measures-radius-medium,8px)] border-2 bg-[var(--color-surface-default-secondary)] py-2 pl-1 pr-3 transition-[border-color,box-shadow,opacity] ${
|
||||||
isDropTarget
|
isDropTarget
|
||||||
@@ -61,19 +64,26 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
|||||||
? "relative z-20 border-[var(--color-border-default-primary)] opacity-60 shadow-[0_4px_12px_rgba(0,0,0,0.12)]"
|
? "relative z-20 border-[var(--color-border-default-primary)] opacity-60 shadow-[0_4px_12px_rgba(0,0,0,0.12)]"
|
||||||
: "border-[var(--color-border-default-primary)] hover:border-[var(--color-content-default-secondary)]"
|
: "border-[var(--color-border-default-primary)] hover:border-[var(--color-content-default-secondary)]"
|
||||||
}`}
|
}`}
|
||||||
|
onDragStart={onDragStart(index)}
|
||||||
onDragOver={onDragOver(index)}
|
onDragOver={onDragOver(index)}
|
||||||
onDrop={onDrop(index)}
|
onDrop={onDrop(index)}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
>
|
>
|
||||||
<button
|
<span
|
||||||
type="button"
|
role="button"
|
||||||
draggable
|
tabIndex={0}
|
||||||
onDragStart={onDragStart(index)}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
aria-label={dragHandleAriaLabel}
|
aria-label={dragHandleAriaLabel}
|
||||||
className="flex shrink-0 cursor-grab touch-manipulation items-center self-stretch rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-1 text-[var(--color-content-default-secondary)] transition-colors active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)] group-hover:text-[var(--color-content-default-primary)]"
|
onPointerDown={onHandlePointerDown}
|
||||||
|
className="flex shrink-0 cursor-grab touch-manipulation items-center self-stretch rounded-[var(--measures-radius-200,8px)] px-1 text-[var(--color-content-default-secondary)] transition-colors active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)] group-hover:text-[var(--color-content-default-primary)]"
|
||||||
>
|
>
|
||||||
<DragHandleGlyph />
|
<DragHandleGlyph />
|
||||||
</button>
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={title}
|
||||||
|
onClick={() => onEditBlock(block)}
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-2 self-stretch rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-0 text-left cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)]"
|
||||||
|
>
|
||||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center self-center">
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center self-center">
|
||||||
<Icon
|
<Icon
|
||||||
name={ADD_CUSTOM_FIELD_TYPE_ICONS[kind]}
|
name={ADD_CUSTOM_FIELD_TYPE_ICONS[kind]}
|
||||||
@@ -89,6 +99,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
|
|||||||
{typeLabel}
|
{typeLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+5
-10
@@ -51,15 +51,11 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
|||||||
value={textBlockTitle}
|
value={textBlockTitle}
|
||||||
onChange={onTextBlockTitleChange}
|
onChange={onTextBlockTitleChange}
|
||||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||||
showHelpIcon
|
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<InputLabel
|
<label className="text-[14px] leading-[20px] font-medium text-[var(--color-content-default-primary)]">
|
||||||
label={copy.text.placeholderLabel}
|
{copy.text.placeholderLabel}
|
||||||
helpIcon
|
</label>
|
||||||
size="s"
|
|
||||||
palette="default"
|
|
||||||
/>
|
|
||||||
<TextArea
|
<TextArea
|
||||||
formHeader={false}
|
formHeader={false}
|
||||||
appearance="embedded"
|
appearance="embedded"
|
||||||
@@ -82,7 +78,6 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<InputLabel
|
<InputLabel
|
||||||
label={copy.badges.blockTitleLabel}
|
label={copy.badges.blockTitleLabel}
|
||||||
helpIcon
|
|
||||||
helperText={copy.requiredHint}
|
helperText={copy.requiredHint}
|
||||||
size="s"
|
size="s"
|
||||||
palette="default"
|
palette="default"
|
||||||
@@ -127,7 +122,6 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
|||||||
value={uploadBlockTitle}
|
value={uploadBlockTitle}
|
||||||
onChange={onUploadBlockTitleChange}
|
onChange={onUploadBlockTitleChange}
|
||||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||||
showHelpIcon
|
|
||||||
/>
|
/>
|
||||||
{hasUploadPreview ? (
|
{hasUploadPreview ? (
|
||||||
<div className="relative inline-block max-w-full">
|
<div className="relative inline-block max-w-full">
|
||||||
@@ -158,6 +152,7 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
|||||||
) : (
|
) : (
|
||||||
<Upload
|
<Upload
|
||||||
active={!uploadPersisting}
|
active={!uploadPersisting}
|
||||||
|
showHelpIcon={false}
|
||||||
hintText={
|
hintText={
|
||||||
uploadPersisting && uploadBusyHint
|
uploadPersisting && uploadBusyHint
|
||||||
? uploadBusyHint
|
? uploadBusyHint
|
||||||
@@ -188,10 +183,10 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
|||||||
value={proportionBlockTitle}
|
value={proportionBlockTitle}
|
||||||
onChange={onProportionBlockTitleChange}
|
onChange={onProportionBlockTitleChange}
|
||||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||||
showHelpIcon
|
|
||||||
/>
|
/>
|
||||||
<IncrementerBlock
|
<IncrementerBlock
|
||||||
label={copy.proportion.defaultLabel}
|
label={copy.proportion.defaultLabel}
|
||||||
|
helpIcon={false}
|
||||||
value={proportionDefault}
|
value={proportionDefault}
|
||||||
min={1}
|
min={1}
|
||||||
max={100}
|
max={100}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ export default function MethodCardCustomizeModalHeader({
|
|||||||
value={titleValue}
|
value={titleValue}
|
||||||
onChange={(e) => onTitleChange(e.target.value)}
|
onChange={(e) => onTitleChange(e.target.value)}
|
||||||
inputSize="medium"
|
inputSize="medium"
|
||||||
|
showHelpIcon={false}
|
||||||
/>
|
/>
|
||||||
{showDescription ? (
|
{showDescription ? (
|
||||||
<ModalTextAreaField
|
<ModalTextAreaField
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared "labelled text area" field used by every create flow modal section.
|
* Shared "labelled text area" field used by every create flow modal section.
|
||||||
* Pairs an `InputLabel` (with help icon) with a `TextArea` set to the embedded
|
* Pairs an `InputLabel` with a `TextArea` set to the embedded appearance —
|
||||||
* appearance — matching the Figma "Control / Text Area" pattern.
|
* matching the Figma "Control / Text Area" pattern. Section help marks stay
|
||||||
|
* off until tooltip behavior ships.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { memo, useId } from "react";
|
import { memo, useId } from "react";
|
||||||
@@ -13,7 +14,7 @@ import InputLabel from "../../../components/type/InputLabel";
|
|||||||
export interface ModalTextAreaFieldProps {
|
export interface ModalTextAreaFieldProps {
|
||||||
/** Label rendered above the text area. */
|
/** Label rendered above the text area. */
|
||||||
label: string;
|
label: string;
|
||||||
/** Show the help "?" icon next to the label (default `true`). */
|
/** Show the help "?" icon next to the label. Off until tooltip behavior ships. */
|
||||||
helpIcon?: boolean;
|
helpIcon?: boolean;
|
||||||
/** Current text value. */
|
/** Current text value. */
|
||||||
value: string;
|
value: string;
|
||||||
@@ -30,7 +31,7 @@ export interface ModalTextAreaFieldProps {
|
|||||||
|
|
||||||
function ModalTextAreaFieldComponent({
|
function ModalTextAreaFieldComponent({
|
||||||
label,
|
label,
|
||||||
helpIcon = true,
|
helpIcon = false,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
rows = 4,
|
rows = 4,
|
||||||
|
|||||||
@@ -9,6 +9,19 @@ export const CREATE_FLOW_MD_UP_GRID_CELL_CLASS =
|
|||||||
/** Two 640px columns + `--measures-spacing-1200` (48px) gutter. */
|
/** Two 640px columns + `--measures-spacing-1200` (48px) gutter. */
|
||||||
export const CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS = "md:max-w-[1328px]";
|
export const CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS = "md:max-w-[1328px]";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lockup+card and card-stack `<main>`: keep `items-start` so a tall card can
|
||||||
|
* scroll from the top. Pair with {@link CREATE_FLOW_MD_CENTERED_SHELL_CLASS}.
|
||||||
|
*/
|
||||||
|
export const CREATE_FLOW_MD_CENTERED_MAIN_CLASS =
|
||||||
|
"items-start justify-center overflow-y-auto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Center the step in the nav–footer band from `md` when it fits (`my-auto`),
|
||||||
|
* and drop the mobile top inset so that centering is not biased downward.
|
||||||
|
*/
|
||||||
|
export const CREATE_FLOW_MD_CENTERED_SHELL_CLASS = "md:my-auto md:pt-0";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Card-stack steps only (Figma compact card stack): wider than header lockup so the card grid /
|
* Card-stack steps only (Figma compact card stack): wider than header lockup so the card grid /
|
||||||
* pyramid fits (max 860px). Header lockup stays {@link CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}.
|
* pyramid fits (max 860px). Header lockup stays {@link CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type { CoreValueDetailEntry } from "../../types";
|
|||||||
export interface CoreValueEditFieldsProps {
|
export interface CoreValueEditFieldsProps {
|
||||||
value: CoreValueDetailEntry;
|
value: CoreValueDetailEntry;
|
||||||
onChange: (_next: CoreValueDetailEntry) => void;
|
onChange: (_next: CoreValueDetailEntry) => void;
|
||||||
/** View mode until the user taps **Customize**. */
|
/** Disable meaning/signals. Create-flow core-values omits this; final-review locks until Customize. */
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
* `markCreateFlowInteraction` live in the parent.
|
* `markCreateFlowInteraction` live in the parent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback, useMemo } from "react";
|
||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
import ModalTextAreaField from "../ModalTextAreaField";
|
import ModalTextAreaField from "../ModalTextAreaField";
|
||||||
import ApplicableScopeField from "../ApplicableScopeField";
|
import ApplicableScopeField from "../ApplicableScopeField";
|
||||||
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
|
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
|
||||||
import type { DecisionApproachDetailEntry } from "../../types";
|
import type { DecisionApproachDetailEntry } from "../../types";
|
||||||
|
import { withDecisionApproachKeyResourceScopes } from "../../../../../lib/create/decisionApproachKeyResources";
|
||||||
|
|
||||||
export interface DecisionApproachEditFieldsProps {
|
export interface DecisionApproachEditFieldsProps {
|
||||||
value: DecisionApproachDetailEntry;
|
value: DecisionApproachDetailEntry;
|
||||||
@@ -32,6 +33,11 @@ function DecisionApproachEditFieldsComponent({
|
|||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const t = m.create.customRule.decisionApproaches;
|
const t = m.create.customRule.decisionApproaches;
|
||||||
|
|
||||||
|
const scopes = useMemo(
|
||||||
|
() => withDecisionApproachKeyResourceScopes(value.applicableScope),
|
||||||
|
[value.applicableScope],
|
||||||
|
);
|
||||||
|
|
||||||
const patch = useCallback(
|
const patch = useCallback(
|
||||||
<K extends keyof DecisionApproachDetailEntry>(
|
<K extends keyof DecisionApproachDetailEntry>(
|
||||||
key: K,
|
key: K,
|
||||||
@@ -53,7 +59,7 @@ function DecisionApproachEditFieldsComponent({
|
|||||||
<ApplicableScopeField
|
<ApplicableScopeField
|
||||||
label={t.sectionHeadings.applicableScope}
|
label={t.sectionHeadings.applicableScope}
|
||||||
addLabel={t.scopeAddButtonLabel}
|
addLabel={t.scopeAddButtonLabel}
|
||||||
scopes={value.applicableScope}
|
scopes={scopes}
|
||||||
selectedScopes={value.selectedApplicableScope}
|
selectedScopes={value.selectedApplicableScope}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onToggleScope={(scope) =>
|
onToggleScope={(scope) =>
|
||||||
@@ -76,6 +82,7 @@ function DecisionApproachEditFieldsComponent({
|
|||||||
/>
|
/>
|
||||||
<IncrementerBlock
|
<IncrementerBlock
|
||||||
label={t.sectionHeadings.consensusLevel}
|
label={t.sectionHeadings.consensusLevel}
|
||||||
|
helpIcon={false}
|
||||||
value={value.consensusLevel}
|
value={value.consensusLevel}
|
||||||
min={CONSENSUS_LEVEL_MIN}
|
min={CONSENSUS_LEVEL_MIN}
|
||||||
max={CONSENSUS_LEVEL_MAX}
|
max={CONSENSUS_LEVEL_MAX}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import { mergeCompactCardIdsWithPinnedSelected } from "../../../../lib/create/methodCardDisplayOrder";
|
||||||
mergeCompactCardIdsWithPinnedSelected,
|
|
||||||
orderRankedMethodsWithPinnedSelection,
|
|
||||||
} from "../../../../lib/create/methodCardDisplayOrder";
|
|
||||||
import {
|
import {
|
||||||
deriveCompactCards,
|
deriveCompactCards,
|
||||||
rankMethodsByScore,
|
rankMethodsByScore,
|
||||||
@@ -15,10 +12,10 @@ import {
|
|||||||
type MethodEntry = { id: string; label: string; supportText: string };
|
type MethodEntry = { id: string; label: string; supportText: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies score ranking, compact-slot rules, then surfaces selected ids first in
|
* Applies score ranking and compact-slot rules. Expanded CardStack order stays
|
||||||
* `selected*Ids` order (most-recent add at index 0 via
|
* the ranked catalog (selected cards keep their place). Compact slots still
|
||||||
* {@link moveFacetSelectionIdToFront}). Selection-first applies whenever the facet
|
* pin selected ids first so a pick outside the unpinned top-N remains visible
|
||||||
* has any selection — not only after footer Confirm (`methodSectionsPinCommitted`).
|
* when the stack is collapsed.
|
||||||
*/
|
*/
|
||||||
export function useMethodCardDeckOrdering(
|
export function useMethodCardDeckOrdering(
|
||||||
section: RecommendationSection,
|
section: RecommendationSection,
|
||||||
@@ -35,16 +32,7 @@ export function useMethodCardDeckOrdering(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectionShowcaseActive = selectedIds.length > 0;
|
const selectionShowcaseActive = selectedIds.length > 0;
|
||||||
|
const displayMethods = rankedMethods;
|
||||||
const displayMethods = useMemo(
|
|
||||||
() =>
|
|
||||||
orderRankedMethodsWithPinnedSelection(
|
|
||||||
rankedMethods,
|
|
||||||
selectedIds,
|
|
||||||
selectionShowcaseActive,
|
|
||||||
),
|
|
||||||
[rankedMethods, selectedIds, selectionShowcaseActive],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { compactCardIds: baseCompactCardIds, recommendedIds } = useMemo(
|
const { compactCardIds: baseCompactCardIds, recommendedIds } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -60,13 +48,13 @@ export function useMethodCardDeckOrdering(
|
|||||||
const compactCardIds = useMemo(
|
const compactCardIds = useMemo(
|
||||||
() =>
|
() =>
|
||||||
mergeCompactCardIdsWithPinnedSelected(
|
mergeCompactCardIdsWithPinnedSelected(
|
||||||
displayMethods.map((m) => m.id),
|
rankedMethods.map((m) => m.id),
|
||||||
baseCompactCardIds,
|
baseCompactCardIds,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
selectionShowcaseActive,
|
selectionShowcaseActive,
|
||||||
5,
|
5,
|
||||||
),
|
),
|
||||||
[displayMethods, baseCompactCardIds, selectedIds, selectionShowcaseActive],
|
[rankedMethods, baseCompactCardIds, selectedIds, selectionShowcaseActive],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sampleCards = useMemo(
|
const sampleCards = useMemo(
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ import {
|
|||||||
CreateFlowLockupCardStepShell,
|
CreateFlowLockupCardStepShell,
|
||||||
} from "../../components/CreateFlowLockupCardStepShell";
|
} from "../../components/CreateFlowLockupCardStepShell";
|
||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
import {
|
||||||
|
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||||
|
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
|
||||||
|
} from "../../components/createFlowLayoutTokens";
|
||||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Template review route — same shell/grid as final-review; Figma `22142-898702`. */
|
/** Template review — lockup + card, vertically centered (Figma `22142-898702`). */
|
||||||
export default function ReviewTemplatePage({ params }: PageProps) {
|
export default function ReviewTemplatePage({ params }: PageProps) {
|
||||||
const { slug: rawSlug } = use(params);
|
const { slug: rawSlug } = use(params);
|
||||||
const slug = decodeURIComponent(rawSlug);
|
const slug = decodeURIComponent(rawSlug);
|
||||||
@@ -72,7 +75,11 @@ export default function ReviewTemplatePage({ params }: PageProps) {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
<CreateFlowStepShell
|
||||||
|
variant="wideGrid"
|
||||||
|
contentTopBelowMd="space-800"
|
||||||
|
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex shrink-0 items-center justify-start pb-16 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
className={`flex shrink-0 items-center justify-start pb-16 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
>
|
>
|
||||||
@@ -101,7 +108,11 @@ export default function ReviewTemplatePage({ params }: PageProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
<CreateFlowStepShell
|
||||||
|
variant="wideGrid"
|
||||||
|
contentTopBelowMd="space-800"
|
||||||
|
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`min-h-[40vh] shrink-0 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
className={`min-h-[40vh] shrink-0 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
|||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
import {
|
import {
|
||||||
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
||||||
|
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
|
||||||
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||||
} from "../../components/createFlowLayoutTokens";
|
} from "../../components/createFlowLayoutTokens";
|
||||||
import { CommunicationMethodEditFields } from "../../components/methodEditFields";
|
import { CommunicationMethodEditFields } from "../../components/methodEditFields";
|
||||||
@@ -52,20 +53,20 @@ import type { CommunicationMethodDetailEntry } from "../../types";
|
|||||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function CommunicationMethodsScreen() {
|
export function CommunicationMethodsScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const comm = m.create.customRule.communication;
|
const comm = m.create.customRule.communication;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
@@ -78,12 +79,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<CommunicationMethodDetailEntry | null>(null);
|
useState<CommunicationMethodDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||||
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
|
const [wizardCustomizeCardId, setWizardCustomizeCardId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [wizardInitialValues, setWizardInitialValues] =
|
||||||
|
useState<MethodCardWizardInitialValues | null>(null);
|
||||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedCommunicationMethodIds ?? [];
|
const selectedIds = state.selectedCommunicationMethodIds ?? [];
|
||||||
|
|
||||||
@@ -106,6 +109,8 @@ export function CommunicationMethodsScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -143,15 +148,40 @@ export function CommunicationMethodsScreen() {
|
|||||||
const handleCardClick = useCallback(
|
const handleCardClick = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
Array.isArray(persistedBlocks) && persistedBlocks.length > 0
|
||||||
|
? structuredClone(persistedBlocks)
|
||||||
|
: null;
|
||||||
|
const method = methodById.get(id);
|
||||||
|
const meta = state.customMethodCardMetaById?.[id];
|
||||||
|
const headerDraft: MethodCardHeaderDraft = {
|
||||||
|
title: meta?.label ?? method?.label ?? comm.confirmModal.title,
|
||||||
|
description:
|
||||||
|
meta?.supportText ??
|
||||||
|
method?.supportText ??
|
||||||
|
comm.confirmModal.description,
|
||||||
|
};
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
draft,
|
||||||
|
initialBlocks,
|
||||||
|
headerDraft,
|
||||||
|
);
|
||||||
setPendingCardId(id);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
comm.confirmModal.description,
|
||||||
|
comm.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -164,9 +194,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -188,7 +216,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -196,7 +224,6 @@ export function CommunicationMethodsScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -206,11 +233,11 @@ export function CommunicationMethodsScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -238,53 +265,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
replaceState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCancelCustomize = useCallback(async () => {
|
|
||||||
if (!modalEditUnlocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const snap = customizeSnapshotRef.current;
|
|
||||||
if (!snap) {
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!(await confirmDirtyCustomizeCancel(
|
|
||||||
snap,
|
|
||||||
pendingDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPendingDraft(structuredClone(snap.pendingDraft));
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
|
||||||
confirmDirtyCustomizeCancel,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleRemoveSelectedFromModal = useCallback(async () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -292,11 +280,11 @@ export function CommunicationMethodsScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -312,11 +300,9 @@ export function CommunicationMethodsScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -326,37 +312,36 @@ export function CommunicationMethodsScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const persistedBlocks =
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [];
|
|
||||||
const initialFieldBlocks =
|
|
||||||
persistedBlocks.length > 0
|
|
||||||
? structuredClone(persistedBlocks)
|
|
||||||
: isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? []
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? comm.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? comm.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? comm.confirmModal.description,
|
||||||
comm.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "communication",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: comm.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
comm.confirmModal.description,
|
comm.confirmModal.description,
|
||||||
comm.confirmModal.title,
|
comm.confirmModal.title,
|
||||||
|
comm.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -381,9 +366,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
() => communicationPresetFor(newId),
|
() => communicationPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -413,16 +396,15 @@ export function CommunicationMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
state.communicationMethodDetailsById,
|
state.communicationMethodDetailsById,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -448,9 +430,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
() => communicationPresetFor(newId),
|
() => communicationPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -480,14 +460,13 @@ export function CommunicationMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -500,7 +479,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -520,7 +499,6 @@ export function CommunicationMethodsScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -539,7 +517,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
comm.confirmModal.description,
|
comm.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: comm.addPlatform.nextButtonText,
|
: comm.addPlatform.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -551,8 +529,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -565,6 +549,42 @@ export function CommunicationMethodsScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
const existingId = wizardCustomizeCardId;
|
||||||
|
if (existingId) {
|
||||||
|
updateState({
|
||||||
|
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
|
||||||
|
selectedIds,
|
||||||
|
existingId,
|
||||||
|
),
|
||||||
|
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
existingId,
|
||||||
|
{ title, description },
|
||||||
|
),
|
||||||
|
...(pendingDraft
|
||||||
|
? {
|
||||||
|
communicationMethodDetailsById: {
|
||||||
|
...(state.communicationMethodDetailsById ?? {}),
|
||||||
|
[existingId]: pendingDraft,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[existingId]: fieldBlocks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (pendingDraft) {
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
pendingDraft,
|
||||||
|
fieldBlocks,
|
||||||
|
{ title, description },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setDraftFieldBlocks(structuredClone(fieldBlocks));
|
||||||
|
setAddCustomWizardOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
|
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
|
||||||
@@ -587,91 +607,29 @@ export function CommunicationMethodsScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.communicationMethodDetailsById,
|
state.communicationMethodDetailsById,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
const persistWizardBlocks =
|
||||||
if (modalEditUnlocked) {
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
if (!customizeHeaderDraft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (pendingDraft) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
communicationMethodDetailsById: {
|
|
||||||
...(state.communicationMethodDetailsById ?? {}),
|
|
||||||
[pendingCardId]: pendingDraft,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
@@ -679,22 +637,20 @@ export function CommunicationMethodsScreen() {
|
|||||||
});
|
});
|
||||||
} else if (pendingDraft) {
|
} else if (pendingDraft) {
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
communicationMethodDetailsById: {
|
communicationMethodDetailsById: {
|
||||||
...(state.communicationMethodDetailsById ?? {}),
|
...(state.communicationMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setModalEditUnlocked(false);
|
void handleCreateModalClose();
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
updateState({
|
||||||
@@ -706,15 +662,23 @@ export function CommunicationMethodsScreen() {
|
|||||||
...(state.communicationMethodDetailsById ?? {}),
|
...(state.communicationMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -727,6 +691,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
<CreateFlowStepShell
|
<CreateFlowStepShell
|
||||||
variant="wideGridLoosePadding"
|
variant="wideGridLoosePadding"
|
||||||
contentTopBelowMd="space-800"
|
contentTopBelowMd="space-800"
|
||||||
|
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
|
||||||
>
|
>
|
||||||
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
||||||
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
||||||
@@ -764,33 +729,11 @@ export function CommunicationMethodsScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
headerContent={
|
|
||||||
modalEditUnlocked && customizeHeaderDraft ? (
|
|
||||||
<MethodCardCustomizeModalHeader
|
|
||||||
titleLabel={modalKebabMenu.customizePolicyTitleLabel}
|
|
||||||
descriptionLabel={modalKebabMenu.customizePolicyDescriptionLabel}
|
|
||||||
titleValue={customizeHeaderDraft.title}
|
|
||||||
descriptionValue={customizeHeaderDraft.description}
|
|
||||||
onTitleChange={(title) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, title } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDescriptionChange={(description) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, description } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
onNext={handleCreateModalPrimary}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
showBackButton={false}
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -803,14 +746,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -819,7 +762,6 @@ export function CommunicationMethodsScreen() {
|
|||||||
<CommunicationMethodEditFields
|
<CommunicationMethodEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -828,6 +770,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
|||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
import {
|
import {
|
||||||
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
||||||
|
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
|
||||||
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||||
} from "../../components/createFlowLayoutTokens";
|
} from "../../components/createFlowLayoutTokens";
|
||||||
import { ConflictManagementEditFields } from "../../components/methodEditFields";
|
import { ConflictManagementEditFields } from "../../components/methodEditFields";
|
||||||
@@ -49,20 +50,20 @@ import type { ConflictManagementDetailEntry } from "../../types";
|
|||||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function ConflictManagementScreen() {
|
export function ConflictManagementScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const cm = m.create.customRule.conflictManagement;
|
const cm = m.create.customRule.conflictManagement;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
@@ -75,12 +76,14 @@ export function ConflictManagementScreen() {
|
|||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<ConflictManagementDetailEntry | null>(null);
|
useState<ConflictManagementDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||||
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
|
const [wizardCustomizeCardId, setWizardCustomizeCardId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [wizardInitialValues, setWizardInitialValues] =
|
||||||
|
useState<MethodCardWizardInitialValues | null>(null);
|
||||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedConflictManagementIds ?? [];
|
const selectedIds = state.selectedConflictManagementIds ?? [];
|
||||||
|
|
||||||
@@ -103,6 +106,8 @@ export function ConflictManagementScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -144,15 +149,40 @@ export function ConflictManagementScreen() {
|
|||||||
const handleCardClick = useCallback(
|
const handleCardClick = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
Array.isArray(persistedBlocks) && persistedBlocks.length > 0
|
||||||
|
? structuredClone(persistedBlocks)
|
||||||
|
: null;
|
||||||
|
const method = methodById.get(id);
|
||||||
|
const meta = state.customMethodCardMetaById?.[id];
|
||||||
|
const headerDraft: MethodCardHeaderDraft = {
|
||||||
|
title: meta?.label ?? method?.label ?? cm.confirmModal.title,
|
||||||
|
description:
|
||||||
|
meta?.supportText ??
|
||||||
|
method?.supportText ??
|
||||||
|
cm.confirmModal.description,
|
||||||
|
};
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
draft,
|
||||||
|
initialBlocks,
|
||||||
|
headerDraft,
|
||||||
|
);
|
||||||
setPendingCardId(id);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
cm.confirmModal.description,
|
||||||
|
cm.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -165,9 +195,7 @@ export function ConflictManagementScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -189,7 +217,7 @@ export function ConflictManagementScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -197,7 +225,6 @@ export function ConflictManagementScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -207,11 +234,11 @@ export function ConflictManagementScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -239,53 +266,14 @@ export function ConflictManagementScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
replaceState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCancelCustomize = useCallback(async () => {
|
|
||||||
if (!modalEditUnlocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const snap = customizeSnapshotRef.current;
|
|
||||||
if (!snap) {
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!(await confirmDirtyCustomizeCancel(
|
|
||||||
snap,
|
|
||||||
pendingDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPendingDraft(structuredClone(snap.pendingDraft));
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
|
||||||
confirmDirtyCustomizeCancel,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleRemoveSelectedFromModal = useCallback(async () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -293,11 +281,11 @@ export function ConflictManagementScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -313,11 +301,9 @@ export function ConflictManagementScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -327,35 +313,36 @@ export function ConflictManagementScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const initialFieldBlocks =
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? structuredClone(
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [],
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? cm.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? cm.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? cm.confirmModal.description,
|
||||||
cm.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "conflictManagement",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: cm.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
cm.confirmModal.description,
|
cm.confirmModal.description,
|
||||||
cm.confirmModal.title,
|
cm.confirmModal.title,
|
||||||
|
cm.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -380,9 +367,7 @@ export function ConflictManagementScreen() {
|
|||||||
() => conflictManagementPresetFor(newId),
|
() => conflictManagementPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -412,13 +397,12 @@ export function ConflictManagementScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -447,9 +431,7 @@ export function ConflictManagementScreen() {
|
|||||||
() => conflictManagementPresetFor(newId),
|
() => conflictManagementPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -479,14 +461,13 @@ export function ConflictManagementScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -499,7 +480,7 @@ export function ConflictManagementScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -519,7 +500,6 @@ export function ConflictManagementScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -538,7 +518,7 @@ export function ConflictManagementScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
cm.confirmModal.description,
|
cm.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: cm.addApproach.nextButtonText,
|
: cm.addApproach.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -550,8 +530,14 @@ export function ConflictManagementScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -564,6 +550,42 @@ export function ConflictManagementScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
const existingId = wizardCustomizeCardId;
|
||||||
|
if (existingId) {
|
||||||
|
updateState({
|
||||||
|
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
||||||
|
selectedIds,
|
||||||
|
existingId,
|
||||||
|
),
|
||||||
|
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
existingId,
|
||||||
|
{ title, description },
|
||||||
|
),
|
||||||
|
...(pendingDraft
|
||||||
|
? {
|
||||||
|
conflictManagementDetailsById: {
|
||||||
|
...(state.conflictManagementDetailsById ?? {}),
|
||||||
|
[existingId]: pendingDraft,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[existingId]: fieldBlocks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (pendingDraft) {
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
pendingDraft,
|
||||||
|
fieldBlocks,
|
||||||
|
{ title, description },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setDraftFieldBlocks(structuredClone(fieldBlocks));
|
||||||
|
setAddCustomWizardOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
||||||
@@ -586,91 +608,29 @@ export function ConflictManagementScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.conflictManagementDetailsById,
|
state.conflictManagementDetailsById,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
const persistWizardBlocks =
|
||||||
if (modalEditUnlocked) {
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
if (!customizeHeaderDraft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (pendingDraft) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
conflictManagementDetailsById: {
|
|
||||||
...(state.conflictManagementDetailsById ?? {}),
|
|
||||||
[pendingCardId]: pendingDraft,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
@@ -678,22 +638,20 @@ export function ConflictManagementScreen() {
|
|||||||
});
|
});
|
||||||
} else if (pendingDraft) {
|
} else if (pendingDraft) {
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
conflictManagementDetailsById: {
|
conflictManagementDetailsById: {
|
||||||
...(state.conflictManagementDetailsById ?? {}),
|
...(state.conflictManagementDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setModalEditUnlocked(false);
|
void handleCreateModalClose();
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
updateState({
|
||||||
@@ -705,15 +663,23 @@ export function ConflictManagementScreen() {
|
|||||||
...(state.conflictManagementDetailsById ?? {}),
|
...(state.conflictManagementDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -726,6 +692,7 @@ export function ConflictManagementScreen() {
|
|||||||
<CreateFlowStepShell
|
<CreateFlowStepShell
|
||||||
variant="wideGridLoosePadding"
|
variant="wideGridLoosePadding"
|
||||||
contentTopBelowMd="space-800"
|
contentTopBelowMd="space-800"
|
||||||
|
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
|
||||||
>
|
>
|
||||||
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
||||||
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
||||||
@@ -763,33 +730,11 @@ export function ConflictManagementScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
headerContent={
|
|
||||||
modalEditUnlocked && customizeHeaderDraft ? (
|
|
||||||
<MethodCardCustomizeModalHeader
|
|
||||||
titleLabel={modalKebabMenu.customizePolicyTitleLabel}
|
|
||||||
descriptionLabel={modalKebabMenu.customizePolicyDescriptionLabel}
|
|
||||||
titleValue={customizeHeaderDraft.title}
|
|
||||||
descriptionValue={customizeHeaderDraft.description}
|
|
||||||
onTitleChange={(title) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, title } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDescriptionChange={(description) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, description } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
onNext={handleCreateModalPrimary}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
showBackButton={false}
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -802,14 +747,14 @@ export function ConflictManagementScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -818,7 +763,6 @@ export function ConflictManagementScreen() {
|
|||||||
<ConflictManagementEditFields
|
<ConflictManagementEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -827,6 +771,7 @@ export function ConflictManagementScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
|||||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||||
import {
|
import {
|
||||||
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
||||||
|
CREATE_FLOW_MD_CENTERED_SHELL_CLASS,
|
||||||
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||||
} from "../../components/createFlowLayoutTokens";
|
} from "../../components/createFlowLayoutTokens";
|
||||||
import { MembershipMethodEditFields } from "../../components/methodEditFields";
|
import { MembershipMethodEditFields } from "../../components/methodEditFields";
|
||||||
@@ -50,20 +51,20 @@ import type { MembershipMethodDetailEntry } from "../../types";
|
|||||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function MembershipMethodsScreen() {
|
export function MembershipMethodsScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const mem = m.create.customRule.membership;
|
const mem = m.create.customRule.membership;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
@@ -76,12 +77,14 @@ export function MembershipMethodsScreen() {
|
|||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<MembershipMethodDetailEntry | null>(null);
|
useState<MembershipMethodDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||||
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
|
const [wizardCustomizeCardId, setWizardCustomizeCardId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [wizardInitialValues, setWizardInitialValues] =
|
||||||
|
useState<MethodCardWizardInitialValues | null>(null);
|
||||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedMembershipMethodIds ?? [];
|
const selectedIds = state.selectedMembershipMethodIds ?? [];
|
||||||
|
|
||||||
@@ -104,6 +107,8 @@ export function MembershipMethodsScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -141,15 +146,40 @@ export function MembershipMethodsScreen() {
|
|||||||
const handleCardClick = useCallback(
|
const handleCardClick = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
Array.isArray(persistedBlocks) && persistedBlocks.length > 0
|
||||||
|
? structuredClone(persistedBlocks)
|
||||||
|
: null;
|
||||||
|
const method = methodById.get(id);
|
||||||
|
const meta = state.customMethodCardMetaById?.[id];
|
||||||
|
const headerDraft: MethodCardHeaderDraft = {
|
||||||
|
title: meta?.label ?? method?.label ?? mem.confirmModal.title,
|
||||||
|
description:
|
||||||
|
meta?.supportText ??
|
||||||
|
method?.supportText ??
|
||||||
|
mem.confirmModal.description,
|
||||||
|
};
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
draft,
|
||||||
|
initialBlocks,
|
||||||
|
headerDraft,
|
||||||
|
);
|
||||||
setPendingCardId(id);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
mem.confirmModal.description,
|
||||||
|
mem.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -162,9 +192,7 @@ export function MembershipMethodsScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -186,7 +214,7 @@ export function MembershipMethodsScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -194,7 +222,6 @@ export function MembershipMethodsScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -204,11 +231,11 @@ export function MembershipMethodsScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -236,53 +263,14 @@ export function MembershipMethodsScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
replaceState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCancelCustomize = useCallback(async () => {
|
|
||||||
if (!modalEditUnlocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const snap = customizeSnapshotRef.current;
|
|
||||||
if (!snap) {
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!(await confirmDirtyCustomizeCancel(
|
|
||||||
snap,
|
|
||||||
pendingDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPendingDraft(structuredClone(snap.pendingDraft));
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
|
||||||
confirmDirtyCustomizeCancel,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleRemoveSelectedFromModal = useCallback(async () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -290,11 +278,11 @@ export function MembershipMethodsScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -306,11 +294,9 @@ export function MembershipMethodsScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -320,35 +306,36 @@ export function MembershipMethodsScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const initialFieldBlocks =
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? structuredClone(
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [],
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? mem.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? mem.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? mem.confirmModal.description,
|
||||||
mem.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "membership",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: mem.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
mem.confirmModal.description,
|
mem.confirmModal.description,
|
||||||
mem.confirmModal.title,
|
mem.confirmModal.title,
|
||||||
|
mem.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -373,9 +360,7 @@ export function MembershipMethodsScreen() {
|
|||||||
() => membershipPresetFor(newId),
|
() => membershipPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -405,13 +390,12 @@ export function MembershipMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -440,9 +424,7 @@ export function MembershipMethodsScreen() {
|
|||||||
() => membershipPresetFor(newId),
|
() => membershipPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -472,14 +454,13 @@ export function MembershipMethodsScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -492,7 +473,7 @@ export function MembershipMethodsScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -512,7 +493,6 @@ export function MembershipMethodsScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -531,7 +511,7 @@ export function MembershipMethodsScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
mem.confirmModal.description,
|
mem.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: mem.addPlatform.nextButtonText,
|
: mem.addPlatform.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -543,8 +523,14 @@ export function MembershipMethodsScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -557,6 +543,42 @@ export function MembershipMethodsScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
const existingId = wizardCustomizeCardId;
|
||||||
|
if (existingId) {
|
||||||
|
updateState({
|
||||||
|
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
|
||||||
|
selectedIds,
|
||||||
|
existingId,
|
||||||
|
),
|
||||||
|
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
existingId,
|
||||||
|
{ title, description },
|
||||||
|
),
|
||||||
|
...(pendingDraft
|
||||||
|
? {
|
||||||
|
membershipMethodDetailsById: {
|
||||||
|
...(state.membershipMethodDetailsById ?? {}),
|
||||||
|
[existingId]: pendingDraft,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[existingId]: fieldBlocks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (pendingDraft) {
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
pendingDraft,
|
||||||
|
fieldBlocks,
|
||||||
|
{ title, description },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setDraftFieldBlocks(structuredClone(fieldBlocks));
|
||||||
|
setAddCustomWizardOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
|
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
|
||||||
@@ -579,91 +601,29 @@ export function MembershipMethodsScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
state.membershipMethodDetailsById,
|
state.membershipMethodDetailsById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
const persistWizardBlocks =
|
||||||
if (modalEditUnlocked) {
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
if (!customizeHeaderDraft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (pendingDraft) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
membershipMethodDetailsById: {
|
|
||||||
...(state.membershipMethodDetailsById ?? {}),
|
|
||||||
[pendingCardId]: pendingDraft,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
@@ -671,22 +631,20 @@ export function MembershipMethodsScreen() {
|
|||||||
});
|
});
|
||||||
} else if (pendingDraft) {
|
} else if (pendingDraft) {
|
||||||
updateState({
|
updateState({
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
membershipMethodDetailsById: {
|
membershipMethodDetailsById: {
|
||||||
...(state.membershipMethodDetailsById ?? {}),
|
...(state.membershipMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setModalEditUnlocked(false);
|
void handleCreateModalClose();
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
updateState({
|
||||||
@@ -698,15 +656,23 @@ export function MembershipMethodsScreen() {
|
|||||||
...(state.membershipMethodDetailsById ?? {}),
|
...(state.membershipMethodDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: pendingDraft,
|
||||||
},
|
},
|
||||||
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -719,6 +685,7 @@ export function MembershipMethodsScreen() {
|
|||||||
<CreateFlowStepShell
|
<CreateFlowStepShell
|
||||||
variant="wideGridLoosePadding"
|
variant="wideGridLoosePadding"
|
||||||
contentTopBelowMd="space-800"
|
contentTopBelowMd="space-800"
|
||||||
|
className={CREATE_FLOW_MD_CENTERED_SHELL_CLASS}
|
||||||
>
|
>
|
||||||
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
||||||
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
||||||
@@ -756,33 +723,11 @@ export function MembershipMethodsScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
headerContent={
|
|
||||||
modalEditUnlocked && customizeHeaderDraft ? (
|
|
||||||
<MethodCardCustomizeModalHeader
|
|
||||||
titleLabel={modalKebabMenu.customizePolicyTitleLabel}
|
|
||||||
descriptionLabel={modalKebabMenu.customizePolicyDescriptionLabel}
|
|
||||||
titleValue={customizeHeaderDraft.title}
|
|
||||||
descriptionValue={customizeHeaderDraft.description}
|
|
||||||
onTitleChange={(title) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, title } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDescriptionChange={(description) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, description } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
onNext={handleCreateModalPrimary}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
showBackButton={false}
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -795,14 +740,14 @@ export function MembershipMethodsScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -811,7 +756,6 @@ export function MembershipMethodsScreen() {
|
|||||||
<MembershipMethodEditFields
|
<MembershipMethodEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -820,6 +764,7 @@ export function MembershipMethodsScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ export function FinalReviewScreen({
|
|||||||
* Two modals coexist on this screen:
|
* Two modals coexist on this screen:
|
||||||
*
|
*
|
||||||
* - {@link FinalReviewChipEditModal} — core values + method chips: kebab
|
* - {@link FinalReviewChipEditModal} — core values + method chips: kebab
|
||||||
* Customize / Remove; values also offer Duplicate under the five-chip cap.
|
* Remove; values also offer Duplicate under the five-chip cap; method chips
|
||||||
* Save respects the same unlock/dirty rules as the facet create modals;
|
* offer Customize (prefilled custom-policy wizard). Fields are editable on
|
||||||
* writes `{group}DetailsById`, snapshot label (values), `customMethodCardMetaById`,
|
* open. Save writes `{group}DetailsById` and field blocks; wizard Finalize
|
||||||
* and field blocks on Save.
|
* also writes `customMethodCardMetaById`.
|
||||||
* - {@link TemplateChipDetailModal} — read-only fallback for chips we
|
* - {@link TemplateChipDetailModal} — read-only fallback for chips we
|
||||||
* can't map to an override key (e.g. template body entries on the
|
* can't map to an override key (e.g. template body entries on the
|
||||||
* "Use without changes" path where no preset matches the title).
|
* "Use without changes" path where no preset matches the title).
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ import { DecisionApproachEditFields } from "../../components/methodEditFields";
|
|||||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||||
import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
||||||
|
import {
|
||||||
|
applyDecisionApproachKeyResources,
|
||||||
|
decisionApproachKeyResourceCheckboxIds,
|
||||||
|
selectedKeyResourceLabelsFromCheckedIds,
|
||||||
|
} from "../../../../../lib/create/decisionApproachKeyResources";
|
||||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
||||||
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
||||||
@@ -51,43 +56,48 @@ import type { DecisionApproachDetailEntry } from "../../types";
|
|||||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { buildMethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
import {
|
import {
|
||||||
captureMethodCardCustomizeSnapshot,
|
captureMethodCardCustomizeSnapshot,
|
||||||
type MethodCardCustomizeSnapshot,
|
type MethodCardCustomizeSnapshot,
|
||||||
type MethodCardHeaderDraft,
|
type MethodCardHeaderDraft,
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
} from "../../../../../lib/create/methodCardCustomizeSession";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
|
||||||
|
|
||||||
export function DecisionApproachesScreen() {
|
export function DecisionApproachesScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const da = m.create.customRule.decisionApproaches;
|
const da = m.create.customRule.decisionApproaches;
|
||||||
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
const modalKebabMenu = m.create.customRule.modalKebabMenu;
|
||||||
const mdUp = useCreateFlowMdUp();
|
const mdUp = useCreateFlowMdUp();
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { confirmDiscard, confirmDialog } = useDiscardCustomizeConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
const { state, updateState, replaceState, markCreateFlowInteraction } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
const pendingEphemeralDuplicateIdRef = useRef<string | null>(null);
|
||||||
const customizeSnapshotRef = useRef<
|
const customizeSnapshotRef = useRef<
|
||||||
MethodCardCustomizeSnapshot<DecisionApproachDetailEntry> | null
|
MethodCardCustomizeSnapshot<DecisionApproachDetailEntry> | null
|
||||||
>(null);
|
>(null);
|
||||||
const [messageBoxCheckedIds, setMessageBoxCheckedIds] = useState<string[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
||||||
const [pendingDraft, setPendingDraft] =
|
const [pendingDraft, setPendingDraft] =
|
||||||
useState<DecisionApproachDetailEntry | null>(null);
|
useState<DecisionApproachDetailEntry | null>(null);
|
||||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||||
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
|
const [wizardCustomizeCardId, setWizardCustomizeCardId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [wizardInitialValues, setWizardInitialValues] =
|
||||||
|
useState<MethodCardWizardInitialValues | null>(null);
|
||||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
CustomMethodCardFieldBlock[] | null
|
CustomMethodCardFieldBlock[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
|
||||||
|
|
||||||
const selectedIds = state.selectedDecisionApproachIds ?? [];
|
const selectedIds = state.selectedDecisionApproachIds ?? [];
|
||||||
|
const messageBoxCheckedIds = decisionApproachKeyResourceCheckboxIds({
|
||||||
|
detailsById: state.decisionApproachDetailsById,
|
||||||
|
selectedApproachIds: selectedIds,
|
||||||
|
reminderIds: state.selectedDecisionKeyResourceIds,
|
||||||
|
openDraft: pendingDraft,
|
||||||
|
});
|
||||||
|
|
||||||
const messageBoxItems: InfoMessageBoxItem[] = useMemo(
|
const messageBoxItems: InfoMessageBoxItem[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -117,6 +127,8 @@ export function DecisionApproachesScreen() {
|
|||||||
|
|
||||||
const handleOpenAddWizard = useCallback(() => {
|
const handleOpenAddWizard = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
setAddCustomWizardOpen(true);
|
setAddCustomWizardOpen(true);
|
||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
@@ -133,22 +145,34 @@ export function DecisionApproachesScreen() {
|
|||||||
const handleMessageBoxCheckboxChange = useCallback(
|
const handleMessageBoxCheckboxChange = useCallback(
|
||||||
(id: string, checked: boolean) => {
|
(id: string, checked: boolean) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setMessageBoxCheckedIds((prev) =>
|
const nextCheckedIds = checked
|
||||||
checked ? [...prev, id] : prev.filter((x) => x !== id),
|
? [...messageBoxCheckedIds, id]
|
||||||
);
|
: messageBoxCheckedIds.filter((x) => x !== id);
|
||||||
|
const nextLabels =
|
||||||
|
selectedKeyResourceLabelsFromCheckedIds(nextCheckedIds);
|
||||||
|
if (pendingDraft) {
|
||||||
|
setPendingDraft(
|
||||||
|
applyDecisionApproachKeyResources(pendingDraft, nextLabels),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateState({
|
||||||
|
selectedDecisionKeyResourceIds: nextCheckedIds,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction],
|
[
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
messageBoxCheckedIds,
|
||||||
|
pendingDraft,
|
||||||
|
updateState,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const seedDraft = useCallback(
|
const seedDraft = useCallback(
|
||||||
(id: string): DecisionApproachDetailEntry => {
|
(id: string): DecisionApproachDetailEntry => {
|
||||||
const saved = state.decisionApproachDetailsById?.[id];
|
const saved = state.decisionApproachDetailsById?.[id];
|
||||||
if (saved) {
|
if (saved) {
|
||||||
return {
|
return structuredClone(saved);
|
||||||
...saved,
|
|
||||||
applicableScope: [...saved.applicableScope],
|
|
||||||
selectedApplicableScope: [...saved.selectedApplicableScope],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return decisionApproachPresetFor(id);
|
return decisionApproachPresetFor(id);
|
||||||
},
|
},
|
||||||
@@ -158,15 +182,40 @@ export function DecisionApproachesScreen() {
|
|||||||
const handleCardSelect = useCallback(
|
const handleCardSelect = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
customizeSnapshotRef.current = null;
|
const draft = seedDraft(id);
|
||||||
setModalEditUnlocked(false);
|
const persistedBlocks = state.customMethodCardFieldBlocksById?.[id];
|
||||||
setDraftFieldBlocks(null);
|
const initialBlocks =
|
||||||
setCustomizeHeaderDraft(null);
|
Array.isArray(persistedBlocks) && persistedBlocks.length > 0
|
||||||
|
? structuredClone(persistedBlocks)
|
||||||
|
: null;
|
||||||
|
const method = methodById.get(id);
|
||||||
|
const meta = state.customMethodCardMetaById?.[id];
|
||||||
|
const headerDraft: MethodCardHeaderDraft = {
|
||||||
|
title: meta?.label ?? method?.label ?? da.confirmModal.title,
|
||||||
|
description:
|
||||||
|
meta?.supportText ??
|
||||||
|
method?.supportText ??
|
||||||
|
da.confirmModal.description,
|
||||||
|
};
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
draft,
|
||||||
|
initialBlocks,
|
||||||
|
headerDraft,
|
||||||
|
);
|
||||||
setPendingCardId(id);
|
setPendingCardId(id);
|
||||||
setPendingDraft(seedDraft(id));
|
setPendingDraft(draft);
|
||||||
|
setDraftFieldBlocks(initialBlocks);
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
},
|
},
|
||||||
[markCreateFlowInteraction, seedDraft],
|
[
|
||||||
|
da.confirmModal.description,
|
||||||
|
da.confirmModal.title,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
methodById,
|
||||||
|
seedDraft,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -179,9 +228,7 @@ export function DecisionApproachesScreen() {
|
|||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const fieldsLocked = !modalEditUnlocked;
|
const showMethodModalPrimary = true;
|
||||||
|
|
||||||
const showMethodModalPrimary = !isSelectedCardModal || modalEditUnlocked;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -203,7 +250,7 @@ export function DecisionApproachesScreen() {
|
|||||||
methodId: pendingCardId,
|
methodId: pendingCardId,
|
||||||
meta: state.customMethodCardMetaById,
|
meta: state.customMethodCardMetaById,
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
modalEditUnlocked,
|
modalEditUnlocked: false,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
}),
|
}),
|
||||||
@@ -211,7 +258,6 @@ export function DecisionApproachesScreen() {
|
|||||||
[
|
[
|
||||||
customFacetDetailsMatchPreset,
|
customFacetDetailsMatchPreset,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -221,11 +267,11 @@ export function DecisionApproachesScreen() {
|
|||||||
const handleCreateModalClose = useCallback(async () => {
|
const handleCreateModalClose = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -253,53 +299,14 @@ export function DecisionApproachesScreen() {
|
|||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setPendingCardId(null);
|
setPendingCardId(null);
|
||||||
setPendingDraft(null);
|
setPendingDraft(null);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
setDraftFieldBlocks(null);
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
replaceState,
|
replaceState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCancelCustomize = useCallback(async () => {
|
|
||||||
if (!modalEditUnlocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const snap = customizeSnapshotRef.current;
|
|
||||||
if (!snap) {
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!(await confirmDirtyCustomizeCancel(
|
|
||||||
snap,
|
|
||||||
pendingDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPendingDraft(structuredClone(snap.pendingDraft));
|
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
customizeSnapshotRef.current = null;
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, [
|
|
||||||
confirmDirtyCustomizeCancel,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleRemoveSelectedFromModal = useCallback(async () => {
|
const handleRemoveSelectedFromModal = useCallback(async () => {
|
||||||
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
if (!pendingCardId || !selectedIds.includes(pendingCardId)) {
|
||||||
return;
|
return;
|
||||||
@@ -307,11 +314,11 @@ export function DecisionApproachesScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (
|
if (
|
||||||
!(await confirmDiscard(
|
!(await confirmDiscard(
|
||||||
modalEditUnlocked,
|
true,
|
||||||
customizeSnapshotRef.current,
|
customizeSnapshotRef.current,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
customizeHeaderDraft,
|
customizeSnapshotRef.current?.headerDraft ?? null,
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -327,11 +334,9 @@ export function DecisionApproachesScreen() {
|
|||||||
await handleCreateModalClose();
|
await handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
confirmDiscard,
|
confirmDiscard,
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -341,35 +346,36 @@ export function DecisionApproachesScreen() {
|
|||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
if (!pendingDraft || !pendingCardId) {
|
if (!pendingCardId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const initialFieldBlocks =
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? structuredClone(
|
|
||||||
state.customMethodCardFieldBlocksById?.[pendingCardId] ?? [],
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const method = methodById.get(pendingCardId);
|
const method = methodById.get(pendingCardId);
|
||||||
const meta = state.customMethodCardMetaById?.[pendingCardId];
|
setWizardInitialValues(
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
buildMethodCardWizardInitialValues({
|
||||||
title: meta?.label ?? method?.label ?? da.confirmModal.title,
|
cardId: pendingCardId,
|
||||||
description:
|
fallbackTitle: method?.label ?? da.confirmModal.title,
|
||||||
meta?.supportText ??
|
fallbackDescription:
|
||||||
method?.supportText ??
|
method?.supportText ?? da.confirmModal.description,
|
||||||
da.confirmModal.description,
|
meta: state.customMethodCardMetaById,
|
||||||
};
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
setCustomizeHeaderDraft(headerDraft);
|
draftFieldBlocks,
|
||||||
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
facetPrefill: pendingDraft
|
||||||
pendingDraft,
|
? {
|
||||||
initialFieldBlocks,
|
group: "decisionApproaches",
|
||||||
headerDraft,
|
draft: pendingDraft,
|
||||||
|
headings: da.sectionHeadings,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setDraftFieldBlocks(initialFieldBlocks);
|
setWizardCustomizeCardId(pendingCardId);
|
||||||
setModalEditUnlocked(true);
|
setCreateModalOpen(false);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
}, [
|
}, [
|
||||||
da.confirmModal.description,
|
da.confirmModal.description,
|
||||||
da.confirmModal.title,
|
da.confirmModal.title,
|
||||||
|
da.sectionHeadings,
|
||||||
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
@@ -394,9 +400,7 @@ export function DecisionApproachesScreen() {
|
|||||||
() => decisionApproachPresetFor(newId),
|
() => decisionApproachPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -426,13 +430,12 @@ export function DecisionApproachesScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -461,9 +464,7 @@ export function DecisionApproachesScreen() {
|
|||||||
() => decisionApproachPresetFor(newId),
|
() => decisionApproachPresetFor(newId),
|
||||||
);
|
);
|
||||||
const blocksClone = structuredClone(
|
const blocksClone = structuredClone(
|
||||||
modalEditUnlocked &&
|
draftFieldBlocks !== null
|
||||||
draftFieldBlocks !== null &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById)
|
|
||||||
? draftFieldBlocks
|
? draftFieldBlocks
|
||||||
: cloneMethodCardBlocksForDuplicate(
|
: cloneMethodCardBlocksForDuplicate(
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
@@ -493,14 +494,13 @@ export function DecisionApproachesScreen() {
|
|||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setPendingCardId(newId);
|
setPendingCardId(newId);
|
||||||
setPendingDraft(structuredClone(detailsClone));
|
setPendingDraft(structuredClone(detailsClone));
|
||||||
setModalEditUnlocked(false);
|
setDraftFieldBlocks(
|
||||||
setDraftFieldBlocks(null);
|
blocksClone.length > 0 ? structuredClone(blocksClone) : null,
|
||||||
setCustomizeHeaderDraft(null);
|
);
|
||||||
}, [
|
}, [
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
methodById,
|
methodById,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
@@ -513,7 +513,7 @@ export function DecisionApproachesScreen() {
|
|||||||
const kebabMenuItems = useMemo(
|
const kebabMenuItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" || !pendingCardId
|
||||||
@@ -533,7 +533,6 @@ export function DecisionApproachesScreen() {
|
|||||||
handleDuplicatePrefabCard,
|
handleDuplicatePrefabCard,
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isSelectedCardModal,
|
isSelectedCardModal,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
@@ -547,8 +546,14 @@ export function DecisionApproachesScreen() {
|
|||||||
}, [markCreateFlowInteraction]);
|
}, [markCreateFlowInteraction]);
|
||||||
|
|
||||||
const handleCloseAddWizard = useCallback(() => {
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
const resumeCardId = wizardCustomizeCardId;
|
||||||
setAddCustomWizardOpen(false);
|
setAddCustomWizardOpen(false);
|
||||||
}, []);
|
setWizardCustomizeCardId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
if (resumeCardId && pendingCardId === resumeCardId) {
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingCardId, wizardCustomizeCardId]);
|
||||||
|
|
||||||
const handleFinalizeCustomCard = useCallback(
|
const handleFinalizeCustomCard = useCallback(
|
||||||
({
|
({
|
||||||
@@ -561,6 +566,42 @@ export function DecisionApproachesScreen() {
|
|||||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
}) => {
|
}) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
const existingId = wizardCustomizeCardId;
|
||||||
|
if (existingId) {
|
||||||
|
updateState({
|
||||||
|
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||||
|
selectedIds,
|
||||||
|
existingId,
|
||||||
|
),
|
||||||
|
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
||||||
|
state.customMethodCardMetaById,
|
||||||
|
existingId,
|
||||||
|
{ title, description },
|
||||||
|
),
|
||||||
|
...(pendingDraft
|
||||||
|
? {
|
||||||
|
decisionApproachDetailsById: {
|
||||||
|
...(state.decisionApproachDetailsById ?? {}),
|
||||||
|
[existingId]: pendingDraft,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[existingId]: fieldBlocks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (pendingDraft) {
|
||||||
|
customizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
|
||||||
|
pendingDraft,
|
||||||
|
fieldBlocks,
|
||||||
|
{ title, description },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setDraftFieldBlocks(structuredClone(fieldBlocks));
|
||||||
|
setAddCustomWizardOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
updateState({
|
updateState({
|
||||||
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||||
@@ -583,139 +624,89 @@ export function DecisionApproachesScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
state.decisionApproachDetailsById,
|
state.decisionApproachDetailsById,
|
||||||
updateState,
|
updateState,
|
||||||
|
wizardCustomizeCardId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateModalPrimary = useCallback(() => {
|
const handleCreateModalPrimary = useCallback(() => {
|
||||||
if (!pendingCardId) {
|
if (!pendingCardId) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
|
const persistWizardBlocks =
|
||||||
|
modalUsesWizardFieldBlocksBody && draftFieldBlocks !== null;
|
||||||
|
|
||||||
if (selectedIds.includes(pendingCardId)) {
|
if (selectedIds.includes(pendingCardId)) {
|
||||||
if (modalEditUnlocked) {
|
replaceState((prev) => {
|
||||||
if (!customizeHeaderDraft) {
|
if (persistWizardBlocks) {
|
||||||
return;
|
return {
|
||||||
}
|
...prev,
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(prev.customMethodCardFieldBlocksById ?? {}),
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
} else if (pendingDraft) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
decisionApproachDetailsById: {
|
|
||||||
...(state.decisionApproachDetailsById ?? {}),
|
|
||||||
[pendingCardId]: pendingDraft,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
customizeSnapshotRef.current = null;
|
if (!pendingDraft) {
|
||||||
setModalEditUnlocked(false);
|
return prev;
|
||||||
setDraftFieldBlocks(null);
|
}
|
||||||
setCustomizeHeaderDraft(null);
|
return {
|
||||||
return;
|
...prev,
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalEditUnlocked) {
|
|
||||||
if (!customizeHeaderDraft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMeta = methodCardMetaWithCustomizeHeader(
|
|
||||||
state.customMethodCardMetaById,
|
|
||||||
pendingCardId,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pendingCardId &&
|
|
||||||
isCustomMethodCardId(pendingCardId, state.customMethodCardMetaById) &&
|
|
||||||
usesWizardFieldBlocksModalBody({
|
|
||||||
methodId: pendingCardId,
|
|
||||||
meta: state.customMethodCardMetaById,
|
|
||||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
|
||||||
modalEditUnlocked,
|
|
||||||
draftFieldBlocks,
|
|
||||||
customFacetDetailsMatchPreset,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
customMethodCardFieldBlocksById: {
|
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
|
||||||
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (pendingDraft) {
|
|
||||||
updateState({
|
|
||||||
customMethodCardMetaById: nextMeta,
|
|
||||||
decisionApproachDetailsById: {
|
decisionApproachDetailsById: {
|
||||||
...(state.decisionApproachDetailsById ?? {}),
|
...(prev.decisionApproachDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: structuredClone(pendingDraft),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
customizeSnapshotRef.current = null;
|
customizeSnapshotRef.current = null;
|
||||||
setModalEditUnlocked(false);
|
void handleCreateModalClose();
|
||||||
setDraftFieldBlocks(null);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingDraft) {
|
if (!pendingDraft) {
|
||||||
handleCreateModalClose();
|
void handleCreateModalClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState({
|
replaceState((prev) => ({
|
||||||
|
...prev,
|
||||||
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||||
selectedIds,
|
prev.selectedDecisionApproachIds ?? [],
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
),
|
),
|
||||||
decisionApproachDetailsById: {
|
decisionApproachDetailsById: {
|
||||||
...(state.decisionApproachDetailsById ?? {}),
|
...(prev.decisionApproachDetailsById ?? {}),
|
||||||
[pendingCardId]: pendingDraft,
|
[pendingCardId]: structuredClone(pendingDraft),
|
||||||
},
|
},
|
||||||
});
|
...(persistWizardBlocks
|
||||||
|
? {
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(prev.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[pendingCardId]: structuredClone(draftFieldBlocks ?? []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}));
|
||||||
pendingEphemeralDuplicateIdRef.current = null;
|
pendingEphemeralDuplicateIdRef.current = null;
|
||||||
handleCreateModalClose();
|
customizeSnapshotRef.current = null;
|
||||||
|
void handleCreateModalClose();
|
||||||
}, [
|
}, [
|
||||||
customizeHeaderDraft,
|
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
handleCreateModalClose,
|
handleCreateModalClose,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
modalUsesWizardFieldBlocksBody,
|
||||||
pendingCardId,
|
pendingCardId,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
|
replaceState,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
state,
|
|
||||||
updateState,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const modalConfig = pendingCardId
|
const modalConfig = pendingCardId
|
||||||
@@ -729,7 +720,7 @@ export function DecisionApproachesScreen() {
|
|||||||
meta?.supportText ??
|
meta?.supportText ??
|
||||||
method?.supportText ??
|
method?.supportText ??
|
||||||
da.confirmModal.description,
|
da.confirmModal.description,
|
||||||
nextButtonText: modalEditUnlocked
|
nextButtonText: isSelectedCardModal
|
||||||
? saveLabel
|
? saveLabel
|
||||||
: da.addApproach.nextButtonText,
|
: da.addApproach.nextButtonText,
|
||||||
};
|
};
|
||||||
@@ -802,33 +793,11 @@ export function DecisionApproachesScreen() {
|
|||||||
<Create
|
<Create
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
headerContent={
|
|
||||||
modalEditUnlocked && customizeHeaderDraft ? (
|
|
||||||
<MethodCardCustomizeModalHeader
|
|
||||||
titleLabel={modalKebabMenu.customizePolicyTitleLabel}
|
|
||||||
descriptionLabel={modalKebabMenu.customizePolicyDescriptionLabel}
|
|
||||||
titleValue={customizeHeaderDraft.title}
|
|
||||||
descriptionValue={customizeHeaderDraft.description}
|
|
||||||
onTitleChange={(title) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, title } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDescriptionChange={(description) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, description } : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
onNext={handleCreateModalPrimary}
|
onNext={handleCreateModalPrimary}
|
||||||
title={modalConfig.title}
|
title={modalConfig.title}
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
nextButtonText={modalConfig.nextButtonText}
|
||||||
showBackButton={modalEditUnlocked}
|
showBackButton={false}
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showMethodModalPrimary}
|
showNextButton={showMethodModalPrimary}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
@@ -841,14 +810,14 @@ export function DecisionApproachesScreen() {
|
|||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
blocksOverride={
|
blocksOverride={
|
||||||
modalEditUnlocked && draftFieldBlocks !== null
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
? draftFieldBlocks
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
policyMeta={state.customMethodCardMetaById?.[pendingCardId]}
|
||||||
showPolicyContentLockupWhenNoBlocks={!modalEditUnlocked}
|
showPolicyContentLockupWhenNoBlocks={
|
||||||
|
draftFieldBlocks === null || draftFieldBlocks.length === 0
|
||||||
|
}
|
||||||
onFieldBlocksChange={
|
onFieldBlocksChange={
|
||||||
fieldsLocked
|
draftFieldBlocks === null
|
||||||
? undefined
|
? undefined
|
||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
@@ -857,7 +826,6 @@ export function DecisionApproachesScreen() {
|
|||||||
<DecisionApproachEditFields
|
<DecisionApproachEditFields
|
||||||
value={pendingDraft}
|
value={pendingDraft}
|
||||||
onChange={handleDraftChange}
|
onChange={handleDraftChange}
|
||||||
readOnly={fieldsLocked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
@@ -866,6 +834,7 @@ export function DecisionApproachesScreen() {
|
|||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
onFinalize={handleFinalizeCustomCard}
|
onFinalize={handleFinalizeCustomCard}
|
||||||
onPersistCustomUploadFile={(file) =>
|
onPersistCustomUploadFile={(file) =>
|
||||||
uploadCreateFlowFile(file, "customMethodAttachment")
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ContentLockup from "../../../../components/type/ContentLockup";
|
|||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
|
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
|
||||||
import type {
|
import type {
|
||||||
CommunityStructureChipSnapshotRow,
|
CommunityStructureChipSnapshotRow,
|
||||||
CoreValueDetailEntry,
|
CoreValueDetailEntry,
|
||||||
@@ -16,20 +16,25 @@ import type {
|
|||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||||
import { CoreValueEditFields } from "../../components/methodEditFields";
|
import { CoreValueEditFields } from "../../components/methodEditFields";
|
||||||
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
|
import { usesWizardFieldBlocksModalBody } from "../../../../../lib/create/usesWizardFieldBlocksModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||||
import {
|
|
||||||
captureMethodCardCustomizeSnapshot,
|
|
||||||
isMethodCardCustomizeSessionDirty,
|
|
||||||
type MethodCardCustomizeSnapshot,
|
|
||||||
type MethodCardHeaderDraft,
|
|
||||||
} from "../../../../../lib/create/methodCardCustomizeSession";
|
|
||||||
import {
|
import {
|
||||||
duplicateCoreValueChipInDraft,
|
duplicateCoreValueChipInDraft,
|
||||||
MAX_SELECTED_CORE_VALUES,
|
MAX_SELECTED_CORE_VALUES,
|
||||||
removeCoreValueChipFromDraft,
|
removeCoreValueChipFromDraft,
|
||||||
} from "../../../../../lib/create/coreValueChipFacet";
|
} from "../../../../../lib/create/coreValueChipFacet";
|
||||||
import { omitIdFromStringRecord } from "../../../../../lib/create/duplicateMethodCardModalDraft";
|
import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch";
|
||||||
|
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
||||||
|
import {
|
||||||
|
buildMethodCardWizardInitialValues,
|
||||||
|
coreValueDetailsFromWizardFieldBlocks,
|
||||||
|
overlayFacetPrefillValues,
|
||||||
|
type MethodCardWizardInitialValues,
|
||||||
|
} from "../../../../../lib/create/methodCardWizardPrefill";
|
||||||
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||||
|
|
||||||
const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
|
const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
|
||||||
|
|
||||||
@@ -37,13 +42,14 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
|
|||||||
* Why three sessions, not two:
|
* Why three sessions, not two:
|
||||||
*
|
*
|
||||||
* - `pending` — preset chip just selected; modal opened to capture
|
* - `pending` — preset chip just selected; modal opened to capture
|
||||||
* meaning/signals. Dismiss = unselect the chip (keep it in the
|
* meaning/signals. Close (X) confirms, then unselects the chip.
|
||||||
* preset row, just not selected).
|
* - `customPending` — leftover inline custom-chip drafts (older sessions).
|
||||||
* - `customPending` — brand-new custom chip just created via the Add
|
* Dismiss = drop the chip entirely.
|
||||||
* value flow; modal opened with empty fields. Dismiss = drop the
|
|
||||||
* chip entirely (it was never confirmed via the Add Value button).
|
|
||||||
* - `editing` — chip already exists & is selected; modal reopened to
|
* - `editing` — chip already exists & is selected; modal reopened to
|
||||||
* tweak meaning/signals. Dismiss = no-op (chip stays as-is).
|
* 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.
|
||||||
*/
|
*/
|
||||||
type ModalSession = "pending" | "customPending" | "editing";
|
type ModalSession = "pending" | "customPending" | "editing";
|
||||||
|
|
||||||
@@ -101,13 +107,11 @@ export function CoreValuesSelectScreen() {
|
|||||||
[cv.values],
|
[cv.values],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } =
|
const { requestConfirm, confirmDialog } = useAsyncConfirm();
|
||||||
useDiscardCustomizeConfirm();
|
|
||||||
const { markCreateFlowInteraction, updateState, replaceState, state } =
|
const { markCreateFlowInteraction, updateState, replaceState, state } =
|
||||||
useCreateFlow();
|
useCreateFlow();
|
||||||
|
|
||||||
const coreCustomizeSnapshotRef =
|
const initialDraftRef = useRef<CoreValueDetailEntry | null>(null);
|
||||||
useRef<MethodCardCustomizeSnapshot<CoreValueDetailEntry> | null>(null);
|
|
||||||
const pendingEphemeralCoreDuplicateRef = useRef<string | null>(null);
|
const pendingEphemeralCoreDuplicateRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(() =>
|
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(() =>
|
||||||
@@ -123,9 +127,15 @@ export function CoreValuesSelectScreen() {
|
|||||||
);
|
);
|
||||||
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
||||||
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
|
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
|
||||||
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
|
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||||
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
|
const [wizardCustomizeChipId, setWizardCustomizeChipId] = useState<
|
||||||
useState<MethodCardHeaderDraft | null>(null);
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [wizardInitialValues, setWizardInitialValues] =
|
||||||
|
useState<MethodCardWizardInitialValues | null>(null);
|
||||||
|
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||||
|
CustomMethodCardFieldBlock[] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCoreValueOptions(
|
setCoreValueOptions(
|
||||||
@@ -178,6 +188,7 @@ export function CoreValuesSelectScreen() {
|
|||||||
return {
|
return {
|
||||||
meaning: saved?.meaning ?? preset.meaning,
|
meaning: saved?.meaning ?? preset.meaning,
|
||||||
signals: saved?.signals ?? preset.signals,
|
signals: saved?.signals ?? preset.signals,
|
||||||
|
...(saved?.supportText ? { supportText: saved.supportText } : {}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[state.coreValueDetailsByChipId, getPresetTexts],
|
[state.coreValueDetailsByChipId, getPresetTexts],
|
||||||
@@ -190,15 +201,24 @@ export function CoreValuesSelectScreen() {
|
|||||||
valueLabel: string,
|
valueLabel: string,
|
||||||
seedDetail?: CoreValueDetailEntry,
|
seedDetail?: CoreValueDetailEntry,
|
||||||
) => {
|
) => {
|
||||||
setDraft(seedDetail ?? getInitialTexts(chipId, valueLabel));
|
const initial = seedDetail ?? getInitialTexts(chipId, valueLabel);
|
||||||
|
initialDraftRef.current = { ...initial };
|
||||||
|
const persisted = state.customMethodCardFieldBlocksById?.[chipId];
|
||||||
|
setDraftFieldBlocks(
|
||||||
|
Array.isArray(persisted) && persisted.length > 0
|
||||||
|
? structuredClone(persisted)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
setDraft(initial);
|
||||||
setActiveModalChipId(chipId);
|
setActiveModalChipId(chipId);
|
||||||
setModalSession(session);
|
setModalSession(session);
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
coreCustomizeSnapshotRef.current = null;
|
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
},
|
},
|
||||||
[getInitialTexts, markCreateFlowInteraction],
|
[
|
||||||
|
getInitialTexts,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDraftChange = useCallback(
|
const handleDraftChange = useCallback(
|
||||||
@@ -209,87 +229,36 @@ export function CoreValuesSelectScreen() {
|
|||||||
[markCreateFlowInteraction],
|
[markCreateFlowInteraction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetCustomizeSession = useCallback(() => {
|
|
||||||
coreCustomizeSnapshotRef.current = null;
|
|
||||||
setModalEditUnlocked(false);
|
|
||||||
setCustomizeHeaderDraft(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const finalizeModalDismiss = useCallback(() => {
|
const finalizeModalDismiss = useCallback(() => {
|
||||||
pendingEphemeralCoreDuplicateRef.current = null;
|
pendingEphemeralCoreDuplicateRef.current = null;
|
||||||
resetCustomizeSession();
|
initialDraftRef.current = null;
|
||||||
setActiveModalChipId(null);
|
setActiveModalChipId(null);
|
||||||
setModalSession(null);
|
setModalSession(null);
|
||||||
}, [resetCustomizeSession]);
|
setDraftFieldBlocks(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const confirmLeaveWithoutSaving = useCallback(async () => {
|
||||||
if (!activeModalChipId) return;
|
const initial = initialDraftRef.current;
|
||||||
const chipLabelNow =
|
const fieldsDirty =
|
||||||
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
|
initial != null &&
|
||||||
if (!chipLabelNow) return;
|
(draft.meaning !== initial.meaning || draft.signals !== initial.signals);
|
||||||
markCreateFlowInteraction();
|
if (!fieldsDirty) {
|
||||||
const headerDraft: MethodCardHeaderDraft = {
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if (
|
const isPendingAdd =
|
||||||
!(await confirmDirtyCustomizeCancel(
|
modalSession === "pending" || modalSession === "customPending";
|
||||||
snap,
|
return requestConfirm({
|
||||||
draft,
|
title: cv.detailModal.discardTitle,
|
||||||
null,
|
description: isPendingAdd
|
||||||
customizeHeaderDraft,
|
? cv.detailModal.discardPendingDescription
|
||||||
))
|
: cv.detailModal.discardEditsDescription,
|
||||||
) {
|
proceedText: cv.detailModal.discardProceed,
|
||||||
return;
|
cancelText: cv.detailModal.discardKeepEditing,
|
||||||
}
|
});
|
||||||
setDraft(structuredClone(snap.pendingDraft));
|
}, [cv.detailModal, draft, modalSession, requestConfirm]);
|
||||||
resetCustomizeSession();
|
|
||||||
}, [
|
|
||||||
confirmDirtyCustomizeCancel,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
draft,
|
|
||||||
modalEditUnlocked,
|
|
||||||
resetCustomizeSession,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const syncLabelFromCustomizeHeaderToOptions = useCallback(() => {
|
const handleDuplicateCoreChip = 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 () => {
|
|
||||||
if (!activeModalChipId || !modalSession) return;
|
if (!activeModalChipId || !modalSession) return;
|
||||||
if (
|
|
||||||
!(await confirmDiscard(
|
|
||||||
modalEditUnlocked,
|
|
||||||
coreCustomizeSnapshotRef.current,
|
|
||||||
draft,
|
|
||||||
null,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
const priorEphemeral = pendingEphemeralCoreDuplicateRef.current;
|
const priorEphemeral = pendingEphemeralCoreDuplicateRef.current;
|
||||||
let outcome: ReturnType<typeof duplicateCoreValueChipInDraft> | null = null;
|
let outcome: ReturnType<typeof duplicateCoreValueChipInDraft> | null = null;
|
||||||
@@ -312,7 +281,6 @@ export function CoreValuesSelectScreen() {
|
|||||||
if (!outcome) {
|
if (!outcome) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetCustomizeSession();
|
|
||||||
pendingEphemeralCoreDuplicateRef.current = outcome.newId;
|
pendingEphemeralCoreDuplicateRef.current = outcome.newId;
|
||||||
openModal(
|
openModal(
|
||||||
outcome.newId,
|
outcome.newId,
|
||||||
@@ -322,30 +290,15 @@ export function CoreValuesSelectScreen() {
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
confirmDiscard,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
draft,
|
draft,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu.duplicateTitleSuffix,
|
modalKebabMenu.duplicateTitleSuffix,
|
||||||
modalSession,
|
modalSession,
|
||||||
openModal,
|
openModal,
|
||||||
replaceState,
|
replaceState,
|
||||||
resetCustomizeSession,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleRemoveFromKebab = useCallback(async () => {
|
const handleRemoveFromKebab = useCallback(() => {
|
||||||
if (
|
|
||||||
!(await confirmDiscard(
|
|
||||||
modalEditUnlocked,
|
|
||||||
coreCustomizeSnapshotRef.current,
|
|
||||||
draft,
|
|
||||||
null,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
|
|
||||||
const ep = pendingEphemeralCoreDuplicateRef.current;
|
const ep = pendingEphemeralCoreDuplicateRef.current;
|
||||||
@@ -375,39 +328,23 @@ export function CoreValuesSelectScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
replaceState((prev) => ({
|
replaceState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedCoreValueIds: selectedIdsFromOptions(nextFiltered),
|
...removeCoreValueChipFromDraft(prev, activeModalChipId),
|
||||||
coreValuesChipsSnapshot:
|
|
||||||
chipOptionsToSnapshotRows(nextFiltered),
|
|
||||||
coreValueDetailsByChipId:
|
|
||||||
omitIdFromStringRecord(prev.coreValueDetailsByChipId, activeModalChipId),
|
|
||||||
}));
|
}));
|
||||||
setCoreValueOptions(nextFiltered);
|
setCoreValueOptions(nextFiltered);
|
||||||
}
|
}
|
||||||
finalizeModalDismiss();
|
finalizeModalDismiss();
|
||||||
}, [
|
}, [
|
||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
confirmDiscard,
|
|
||||||
coreValueOptions,
|
coreValueOptions,
|
||||||
customizeHeaderDraft,
|
|
||||||
draft,
|
|
||||||
finalizeModalDismiss,
|
finalizeModalDismiss,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalSession,
|
modalSession,
|
||||||
persistCoreValues,
|
persistCoreValues,
|
||||||
replaceState,
|
replaceState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleModalDismiss = useCallback(async () => {
|
const handleModalDismiss = useCallback(async () => {
|
||||||
if (
|
if (!(await confirmLeaveWithoutSaving())) {
|
||||||
!(await confirmDiscard(
|
|
||||||
modalEditUnlocked,
|
|
||||||
coreCustomizeSnapshotRef.current,
|
|
||||||
draft,
|
|
||||||
null,
|
|
||||||
customizeHeaderDraft,
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,93 +373,210 @@ export function CoreValuesSelectScreen() {
|
|||||||
finalizeModalDismiss();
|
finalizeModalDismiss();
|
||||||
}, [
|
}, [
|
||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
confirmDiscard,
|
confirmLeaveWithoutSaving,
|
||||||
coreValueOptions,
|
coreValueOptions,
|
||||||
customizeHeaderDraft,
|
|
||||||
draft,
|
|
||||||
finalizeModalDismiss,
|
finalizeModalDismiss,
|
||||||
modalEditUnlocked,
|
|
||||||
modalSession,
|
modalSession,
|
||||||
persistCoreValues,
|
persistCoreValues,
|
||||||
replaceState,
|
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(() => {
|
const handleModalConfirm = useCallback(() => {
|
||||||
if (!activeModalChipId || !modalSession) return;
|
if (!activeModalChipId || !modalSession) return;
|
||||||
|
markCreateFlowInteraction();
|
||||||
if (modalEditUnlocked && customizeHeaderDraft) {
|
pendingEphemeralCoreDuplicateRef.current = null;
|
||||||
if (coreCustomizeSaveDisabled) {
|
const existingBlocks =
|
||||||
return;
|
draftFieldBlocks && draftFieldBlocks.length > 0
|
||||||
}
|
? draftFieldBlocks
|
||||||
markCreateFlowInteraction();
|
: state.customMethodCardFieldBlocksById?.[activeModalChipId];
|
||||||
pendingEphemeralCoreDuplicateRef.current = null;
|
updateState({
|
||||||
const nextOpts = syncLabelFromCustomizeHeaderToOptions();
|
coreValueDetailsByChipId: {
|
||||||
persistCoreValues(nextOpts);
|
...(state.coreValueDetailsByChipId ?? {}),
|
||||||
updateState({
|
[activeModalChipId]: draft,
|
||||||
coreValueDetailsByChipId: {
|
},
|
||||||
...(state.coreValueDetailsByChipId ?? {}),
|
...(existingBlocks && existingBlocks.length > 0
|
||||||
[activeModalChipId]: draft,
|
? {
|
||||||
},
|
customMethodCardFieldBlocksById: {
|
||||||
});
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
resetCustomizeSession();
|
[activeModalChipId]: overlayFacetPrefillValues(existingBlocks, {
|
||||||
return;
|
group: "coreValues",
|
||||||
}
|
draft,
|
||||||
|
headings: {
|
||||||
if (modalSession === "pending" || modalSession === "customPending") {
|
meaning: cv.detailModal.meaningLabel,
|
||||||
markCreateFlowInteraction();
|
signals: cv.detailModal.signalsLabel,
|
||||||
pendingEphemeralCoreDuplicateRef.current = null;
|
},
|
||||||
updateState({
|
}),
|
||||||
coreValueDetailsByChipId: {
|
},
|
||||||
...(state.coreValueDetailsByChipId ?? {}),
|
}
|
||||||
[activeModalChipId]: draft,
|
: {}),
|
||||||
},
|
});
|
||||||
});
|
finalizeModalDismiss();
|
||||||
resetCustomizeSession();
|
|
||||||
setActiveModalChipId(null);
|
|
||||||
setModalSession(null);
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
coreCustomizeSaveDisabled,
|
cv.detailModal.meaningLabel,
|
||||||
customizeHeaderDraft,
|
cv.detailModal.signalsLabel,
|
||||||
draft,
|
draft,
|
||||||
|
draftFieldBlocks,
|
||||||
|
finalizeModalDismiss,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
modalEditUnlocked,
|
|
||||||
modalSession,
|
modalSession,
|
||||||
persistCoreValues,
|
|
||||||
resetCustomizeSession,
|
|
||||||
state.coreValueDetailsByChipId,
|
state.coreValueDetailsByChipId,
|
||||||
syncLabelFromCustomizeHeaderToOptions,
|
state.customMethodCardFieldBlocksById,
|
||||||
updateState,
|
updateState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const modalChipLabel =
|
const modalChipLabel =
|
||||||
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
|
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
|
||||||
|
|
||||||
const modalFieldsLocked =
|
const modalUsesWizardFieldBlocksBody = Boolean(
|
||||||
!modalEditUnlocked &&
|
activeModalChipId &&
|
||||||
Boolean(
|
usesWizardFieldBlocksModalBody({
|
||||||
modalSession === "pending" ||
|
methodId: activeModalChipId,
|
||||||
modalSession === "customPending" ||
|
meta: {},
|
||||||
modalSession === "editing",
|
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||||
);
|
modalEditUnlocked: true,
|
||||||
|
draftFieldBlocks,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const showFooterPrimary =
|
const showFooterPrimary = Boolean(modalSession);
|
||||||
modalEditUnlocked ||
|
|
||||||
modalSession === "pending" ||
|
const handleCustomize = useCallback(() => {
|
||||||
modalSession === "customPending";
|
if (!activeModalChipId) return;
|
||||||
|
markCreateFlowInteraction();
|
||||||
|
setWizardInitialValues(
|
||||||
|
buildMethodCardWizardInitialValues({
|
||||||
|
cardId: activeModalChipId,
|
||||||
|
fallbackTitle: modalChipLabel,
|
||||||
|
fallbackDescription:
|
||||||
|
draft.supportText?.trim() || cv.detailModal.subtitle,
|
||||||
|
meta: {},
|
||||||
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
|
draftFieldBlocks,
|
||||||
|
facetPrefill: {
|
||||||
|
group: "coreValues",
|
||||||
|
draft,
|
||||||
|
headings: {
|
||||||
|
meaning: cv.detailModal.meaningLabel,
|
||||||
|
signals: cv.detailModal.signalsLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setWizardCustomizeChipId(activeModalChipId);
|
||||||
|
setAddCustomWizardOpen(true);
|
||||||
|
}, [
|
||||||
|
activeModalChipId,
|
||||||
|
cv.detailModal.meaningLabel,
|
||||||
|
cv.detailModal.signalsLabel,
|
||||||
|
cv.detailModal.subtitle,
|
||||||
|
draft,
|
||||||
|
draftFieldBlocks,
|
||||||
|
markCreateFlowInteraction,
|
||||||
|
modalChipLabel,
|
||||||
|
state.customMethodCardFieldBlocksById,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleCloseAddWizard = useCallback(() => {
|
||||||
|
setAddCustomWizardOpen(false);
|
||||||
|
setWizardCustomizeChipId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFinalizeCustomCard = useCallback(
|
||||||
|
({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
fieldBlocks,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||||
|
}) => {
|
||||||
|
const trimmedTitle = title.trim();
|
||||||
|
const trimmedDescription = description.trim();
|
||||||
|
if (!trimmedTitle) return;
|
||||||
|
markCreateFlowInteraction();
|
||||||
|
pendingEphemeralCoreDuplicateRef.current = null;
|
||||||
|
|
||||||
|
const existingId = wizardCustomizeChipId;
|
||||||
|
if (!existingId) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const nextDetails = {
|
||||||
|
...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, EMPTY_DETAIL),
|
||||||
|
...(trimmedDescription.length > 0
|
||||||
|
? { supportText: trimmedDescription }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
replaceState((prev) => {
|
||||||
|
const sel = prev.selectedCoreValueIds ?? [];
|
||||||
|
if (sel.length >= MAX_CORE_VALUES) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const snap = [...(prev.coreValuesChipsSnapshot ?? [])];
|
||||||
|
snap.push({
|
||||||
|
id,
|
||||||
|
label: trimmedTitle,
|
||||||
|
state: "selected",
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
selectedCoreValueIds: moveFacetSelectionIdToFront(sel, id),
|
||||||
|
coreValuesChipsSnapshot: snap,
|
||||||
|
coreValueDetailsByChipId: {
|
||||||
|
...(prev.coreValueDetailsByChipId ?? {}),
|
||||||
|
[id]: nextDetails,
|
||||||
|
},
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(prev.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[id]: structuredClone(fieldBlocks),
|
||||||
|
},
|
||||||
|
customMethodCardMetaById: methodCardMetaWithCustomizeHeader(
|
||||||
|
prev.customMethodCardMetaById,
|
||||||
|
id,
|
||||||
|
{ title: trimmedTitle, description: trimmedDescription },
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setAddCustomWizardOpen(false);
|
||||||
|
setWizardCustomizeChipId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextDetails = {
|
||||||
|
...coreValueDetailsFromWizardFieldBlocks(fieldBlocks, draft),
|
||||||
|
...(trimmedDescription.length > 0
|
||||||
|
? { supportText: trimmedDescription }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
replaceState((prev) => {
|
||||||
|
const snap = [...(prev.coreValuesChipsSnapshot ?? [])];
|
||||||
|
const i = snap.findIndex((r) => r.id === existingId);
|
||||||
|
if (i >= 0) {
|
||||||
|
snap[i] = { ...snap[i], label: trimmedTitle };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
coreValuesChipsSnapshot: snap,
|
||||||
|
coreValueDetailsByChipId: {
|
||||||
|
...(prev.coreValueDetailsByChipId ?? {}),
|
||||||
|
[existingId]: nextDetails,
|
||||||
|
},
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
...(prev.customMethodCardFieldBlocksById ?? {}),
|
||||||
|
[existingId]: structuredClone(fieldBlocks),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setDraft(nextDetails);
|
||||||
|
setDraftFieldBlocks(structuredClone(fieldBlocks));
|
||||||
|
setAddCustomWizardOpen(false);
|
||||||
|
setWizardCustomizeChipId(null);
|
||||||
|
setWizardInitialValues(null);
|
||||||
|
},
|
||||||
|
[draft, markCreateFlowInteraction, replaceState, wizardCustomizeChipId],
|
||||||
|
);
|
||||||
|
|
||||||
const kebabMenuItems = useMemo(() => {
|
const kebabMenuItems = useMemo(() => {
|
||||||
if (!modalSession || !activeModalChipId) return [];
|
if (!modalSession || !activeModalChipId) return [];
|
||||||
@@ -530,13 +584,14 @@ export function CoreValuesSelectScreen() {
|
|||||||
(o) => o.state === "selected",
|
(o) => o.state === "selected",
|
||||||
).length;
|
).length;
|
||||||
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
|
||||||
showCustomize: !modalEditUnlocked,
|
showCustomize: true,
|
||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
modalSession !== "editing" || selectedCount >= MAX_CORE_VALUES
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
|
||||||
|
selectedCount >= MAX_CORE_VALUES
|
||||||
? undefined
|
? undefined
|
||||||
: handleDuplicateCoreChip,
|
: handleDuplicateCoreChip,
|
||||||
showRemove: true,
|
showRemove: modalSession === "editing",
|
||||||
onRemove: handleRemoveFromKebab,
|
onRemove: handleRemoveFromKebab,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
@@ -545,9 +600,9 @@ export function CoreValuesSelectScreen() {
|
|||||||
handleCustomize,
|
handleCustomize,
|
||||||
handleDuplicateCoreChip,
|
handleDuplicateCoreChip,
|
||||||
handleRemoveFromKebab,
|
handleRemoveFromKebab,
|
||||||
modalEditUnlocked,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
modalSession,
|
modalSession,
|
||||||
|
state.editingPublishedRuleId,
|
||||||
]);
|
]);
|
||||||
const handleChipClick = (chipId: string) => {
|
const handleChipClick = (chipId: string) => {
|
||||||
const target = coreValueOptions.find((o) => o.id === chipId);
|
const target = coreValueOptions.find((o) => o.id === chipId);
|
||||||
@@ -575,15 +630,14 @@ export function CoreValuesSelectScreen() {
|
|||||||
|
|
||||||
const addHandlers = {
|
const addHandlers = {
|
||||||
onAddClick: () => {
|
onAddClick: () => {
|
||||||
|
const selectedCount = coreValueOptions.filter(
|
||||||
|
(o) => o.state === "selected",
|
||||||
|
).length;
|
||||||
|
if (selectedCount >= MAX_CORE_VALUES) return;
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setCoreValueOptions((prev) => {
|
setWizardCustomizeChipId(null);
|
||||||
const next: ChipOption[] = [
|
setWizardInitialValues(null);
|
||||||
...prev,
|
setAddCustomWizardOpen(true);
|
||||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
|
||||||
];
|
|
||||||
queueMicrotask(() => syncCoreValuesToDraft(next));
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onCustomChipConfirm: (chipId: string, value: string) => {
|
onCustomChipConfirm: (chipId: string, value: string) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
@@ -671,45 +725,28 @@ export function CoreValuesSelectScreen() {
|
|||||||
|
|
||||||
{detailModal && (
|
{detailModal && (
|
||||||
<Create
|
<Create
|
||||||
isOpen={activeModalChipId !== null}
|
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
||||||
onClose={handleModalDismiss}
|
onClose={handleModalDismiss}
|
||||||
backdropVariant="blurredYellow"
|
backdropVariant="blurredYellow"
|
||||||
headerContent={
|
headerContent={
|
||||||
modalEditUnlocked && customizeHeaderDraft ? (
|
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||||
<MethodCardCustomizeModalHeader
|
<ContentLockup
|
||||||
titleLabel={detailModal.customizeValueNameLabel}
|
title={modalChipLabel}
|
||||||
descriptionLabel=""
|
description={
|
||||||
titleValue={customizeHeaderDraft.title}
|
draft.supportText?.trim() || detailModal.subtitle
|
||||||
descriptionValue=""
|
|
||||||
onTitleChange={(title) =>
|
|
||||||
setCustomizeHeaderDraft((prev) =>
|
|
||||||
prev ? { ...prev, title } : null,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
onDescriptionChange={() => {}}
|
variant="modal"
|
||||||
showDescription={false}
|
alignment="left"
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
showBackButton={modalEditUnlocked}
|
showBackButton={false}
|
||||||
onBack={handleCancelCustomize}
|
|
||||||
backButtonText={modalKebabMenu.cancelCustomize}
|
|
||||||
showNextButton={showFooterPrimary}
|
showNextButton={showFooterPrimary}
|
||||||
nextButtonDisabled={
|
|
||||||
modalEditUnlocked && coreCustomizeSaveDisabled
|
|
||||||
}
|
|
||||||
onNext={handleModalConfirm}
|
onNext={handleModalConfirm}
|
||||||
nextButtonText={
|
nextButtonText={
|
||||||
modalEditUnlocked ? modalKebabMenu.saveEdits : detailModal.addValueButton
|
modalSession === "editing"
|
||||||
|
? modalKebabMenu.saveEdits
|
||||||
|
: detailModal.addValueButton
|
||||||
}
|
}
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||||
@@ -718,14 +755,43 @@ export function CoreValuesSelectScreen() {
|
|||||||
}
|
}
|
||||||
ariaLabel={modalChipLabel || "Core value details"}
|
ariaLabel={modalChipLabel || "Core value details"}
|
||||||
>
|
>
|
||||||
<CoreValueEditFields
|
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
|
||||||
readOnly={modalFieldsLocked}
|
<CustomMethodCardModalBody
|
||||||
value={draft}
|
cardId={activeModalChipId}
|
||||||
onChange={handleDraftChange}
|
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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Create>
|
</Create>
|
||||||
)}
|
)}
|
||||||
</CreateFlowTwoColumnSelectShell>
|
</CreateFlowTwoColumnSelectShell>
|
||||||
|
<CustomMethodCardWizard
|
||||||
|
isOpen={addCustomWizardOpen}
|
||||||
|
onClose={handleCloseAddWizard}
|
||||||
|
initialValues={wizardInitialValues}
|
||||||
|
onFinalize={handleFinalizeCustomCard}
|
||||||
|
onPersistCustomUploadFile={(file) =>
|
||||||
|
uploadCreateFlowFile(file, "customMethodAttachment")
|
||||||
|
}
|
||||||
|
/>
|
||||||
{confirmDialog}
|
{confirmDialog}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export type CommunityStructureChipSnapshotRow = {
|
|||||||
export type CoreValueDetailEntry = {
|
export type CoreValueDetailEntry = {
|
||||||
meaning: string;
|
meaning: string;
|
||||||
signals: string;
|
signals: string;
|
||||||
|
supportText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,6 +165,12 @@ export interface CreateFlowState {
|
|||||||
>;
|
>;
|
||||||
membershipMethodDetailsById?: Record<string, MembershipMethodDetailEntry>;
|
membershipMethodDetailsById?: Record<string, MembershipMethodDetailEntry>;
|
||||||
decisionApproachDetailsById?: Record<string, DecisionApproachDetailEntry>;
|
decisionApproachDetailsById?: Record<string, DecisionApproachDetailEntry>;
|
||||||
|
/**
|
||||||
|
* Checked “key resource” ids from the decision-approaches InfoMessageBox
|
||||||
|
* (`amend`, `finances`, `project`, `discipline`). Selecting one also selects
|
||||||
|
* the matching Applicable Scope chip on every decision approach.
|
||||||
|
*/
|
||||||
|
selectedDecisionKeyResourceIds?: string[];
|
||||||
conflictManagementDetailsById?: Record<
|
conflictManagementDetailsById?: Record<
|
||||||
string,
|
string,
|
||||||
ConflictManagementDetailEntry
|
ConflictManagementDetailEntry
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ export function readCoreValueDetailsFromLocalStorage(): Record<
|
|||||||
if (typeof o.meaning !== "string" || typeof o.signals !== "string") {
|
if (typeof o.meaning !== "string" || typeof o.signals !== "string") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
out[k] = { meaning: o.meaning, signals: o.signals };
|
out[k] = {
|
||||||
|
meaning: o.meaning,
|
||||||
|
signals: o.signals,
|
||||||
|
...(typeof o.supportText === "string"
|
||||||
|
? { supportText: o.supportText }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Figma: Content page Template body ornaments (19003:23305).
|
||||||
|
* Hidden below md (640). From md, shapes sit in the side gutters and the
|
||||||
|
* page frame clips anything that bleeds past the viewport.
|
||||||
|
*
|
||||||
|
* - 19003:23575 / 19000:23008 — right
|
||||||
|
* - 19003:23576 / 19000:23009 — left
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getAssetPath,
|
||||||
|
howItWorksOrnamentLeftPath,
|
||||||
|
howItWorksOrnamentRightPath,
|
||||||
|
resolveArticleOrnamentPath,
|
||||||
|
} from "../../../lib/assetUtils";
|
||||||
|
|
||||||
|
type ContentTemplateDecorativeShapesVariant = "guide" | "article";
|
||||||
|
|
||||||
|
type ContentTemplateDecorativeShapesProps = {
|
||||||
|
variant: ContentTemplateDecorativeShapesVariant;
|
||||||
|
slug?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RIGHT_GUIDE_CLASS =
|
||||||
|
"pointer-events-none absolute z-0 hidden aspect-[155/250] md:block left-[87.19%] right-0 top-[266px] lg:left-[84.86%] lg:top-[255px]";
|
||||||
|
const LEFT_GUIDE_CLASS =
|
||||||
|
"pointer-events-none absolute z-0 hidden aspect-[119/136] md:block left-0 right-[88.38%] top-[811px]";
|
||||||
|
const RIGHT_ARTICLE_CLASS =
|
||||||
|
"pointer-events-none absolute z-0 hidden aspect-square md:block left-[87.19%] right-[-8.13%] top-[266px] lg:left-[84.86%] lg:right-[-9.28%] lg:top-[255px]";
|
||||||
|
const LEFT_ARTICLE_CLASS =
|
||||||
|
"pointer-events-none absolute z-0 hidden aspect-square md:block left-[-1.66%] right-[88.38%] top-[811px]";
|
||||||
|
|
||||||
|
export default function ContentTemplateDecorativeShapes({
|
||||||
|
variant,
|
||||||
|
slug,
|
||||||
|
}: ContentTemplateDecorativeShapesProps) {
|
||||||
|
const isArticle = variant === "article";
|
||||||
|
const rightSrc = isArticle
|
||||||
|
? resolveArticleOrnamentPath(slug, "right")
|
||||||
|
: getAssetPath(howItWorksOrnamentRightPath());
|
||||||
|
const leftSrc = isArticle
|
||||||
|
? resolveArticleOrnamentPath(slug, "left")
|
||||||
|
: getAssetPath(howItWorksOrnamentLeftPath());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className={isArticle ? RIGHT_ARTICLE_CLASS : RIGHT_GUIDE_CLASS}
|
||||||
|
data-node-id="19003:23575"
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={rightSrc}
|
||||||
|
alt=""
|
||||||
|
className="pointer-events-none size-full max-w-none object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className={isArticle ? LEFT_ARTICLE_CLASS : LEFT_GUIDE_CLASS}
|
||||||
|
data-node-id="19003:23576"
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={leftSrc}
|
||||||
|
alt=""
|
||||||
|
className="pointer-events-none size-full max-w-none object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ type LegalDocumentPageProps = {
|
|||||||
const INLINE_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
|
const INLINE_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||||
|
|
||||||
const ARTICLE_COLUMN_CLASS =
|
const ARTICLE_COLUMN_CLASS =
|
||||||
"mx-auto flex w-full flex-col gap-[var(--space-800,32px)] text-[var(--color-content-default-primary)] text-medium-paragraph sm:max-w-[390px] sm:text-large-paragraph md:max-w-[472px] lg:max-w-[700px] lg:text-x-large-paragraph xl:max-w-[904px] xl:text-xx-large-paragraph";
|
"mx-auto flex w-full flex-col gap-[var(--space-800,32px)] text-[var(--color-content-default-primary)] text-medium-paragraph sm:max-w-[390px] sm:text-large-paragraph md:max-w-[472px] lg:max-w-[700px] lg:text-x-large-paragraph xl:max-w-none xl:text-xx-large-paragraph";
|
||||||
|
|
||||||
const LINK_CLASS = "underline decoration-solid underline-offset-2";
|
const LINK_CLASS = "underline decoration-solid underline-offset-2";
|
||||||
|
|
||||||
@@ -153,21 +153,17 @@ export default function LegalDocumentPage({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative min-h-screen overflow-x-hidden bg-transparent">
|
<div className="relative min-h-screen overflow-x-hidden bg-transparent">
|
||||||
<ContentBanner post={post} variant="guide" />
|
<ContentBanner
|
||||||
|
post={post}
|
||||||
|
variant="guide"
|
||||||
|
updatedLabel={page.updated}
|
||||||
|
/>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]"
|
className="relative z-10 flex w-full justify-center p-[var(--spacing-scale-024)] sm:px-0 sm:py-[var(--spacing-scale-032)] xl:px-[calc(268/1440*100%)]"
|
||||||
data-node-id="19003:23574"
|
data-node-id="19003:23574"
|
||||||
>
|
>
|
||||||
<div className={`${ARTICLE_COLUMN_CLASS} -mt-[var(--spacing-scale-048)]`}>
|
<div className={ARTICLE_COLUMN_CLASS}>
|
||||||
<div
|
|
||||||
className="flex flex-wrap items-end gap-[var(--measures-spacing-008,8px)] text-xx-small-paragraph text-[var(--color-content-default-secondary)] md:text-x-small-paragraph lg:text-small-paragraph xl:text-large-paragraph"
|
|
||||||
data-name="Metadata Container"
|
|
||||||
>
|
|
||||||
<span>{page.banner.author}</span>
|
|
||||||
<span>{page.updated}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{page.intro.length > 0 ? (
|
{page.intro.length > 0 ? (
|
||||||
<div className="flex flex-col gap-[1em]">
|
<div className="flex flex-col gap-[1em]">
|
||||||
{page.intro.map((paragraph) => (
|
{page.intro.map((paragraph) => (
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import messages from "../../../messages/en/index";
|
import messages from "../../../messages/en/index";
|
||||||
import { getAssetPath, governanceBookletPath } from "../../../lib/assetUtils";
|
import {
|
||||||
|
ASSETS,
|
||||||
|
getAssetPath,
|
||||||
|
governanceBookletPath,
|
||||||
|
structureBeforeCrisisPath,
|
||||||
|
} from "../../../lib/assetUtils";
|
||||||
import { getTranslation } from "../../../lib/i18n/getTranslation";
|
import { getTranslation } from "../../../lib/i18n/getTranslation";
|
||||||
import AboutHeader from "../../components/type/AboutHeader";
|
import AboutHeader from "../../components/type/AboutHeader";
|
||||||
import type { AboutHeaderSegment } from "../../components/type/AboutHeader";
|
import type { AboutHeaderSegment } from "../../components/type/AboutHeader";
|
||||||
@@ -53,11 +58,20 @@ export default function AboutPage() {
|
|||||||
/>
|
/>
|
||||||
<TripleTextBlock columns={tripleColumns} />
|
<TripleTextBlock columns={tripleColumns} />
|
||||||
<Book
|
<Book
|
||||||
title={page.book.title}
|
title={page.structureBeforeCrisis.title}
|
||||||
description={page.book.description}
|
description={page.structureBeforeCrisis.description}
|
||||||
buttonText={page.book.buttonText}
|
buttonText={page.structureBeforeCrisis.buttonText}
|
||||||
|
buttonHref={getAssetPath(structureBeforeCrisisPath())}
|
||||||
|
imageSrc={getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER)}
|
||||||
|
imageAlt={page.structureBeforeCrisis.imageAlt}
|
||||||
|
/>
|
||||||
|
<Book
|
||||||
|
title={page.communityRules.title}
|
||||||
|
description={page.communityRules.description}
|
||||||
|
buttonText={page.communityRules.buttonText}
|
||||||
buttonHref={getAssetPath(governanceBookletPath())}
|
buttonHref={getAssetPath(governanceBookletPath())}
|
||||||
imageAlt={page.book.imageAlt}
|
imageSrc={getAssetPath(ASSETS.COMMUNITY_RULES_COVER)}
|
||||||
|
imageAlt={page.communityRules.imageAlt}
|
||||||
/>
|
/>
|
||||||
<FaqAccordion title={page.faq.title} items={faqItems} />
|
<FaqAccordion title={page.faq.title} items={faqItems} />
|
||||||
<QuoteBlock
|
<QuoteBlock
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { logger } from "../../../../lib/logger";
|
import { logger } from "../../../../lib/logger";
|
||||||
import ContentBanner from "../../../components/sections/ContentBanner";
|
import ContentBanner from "../../../components/sections/ContentBanner";
|
||||||
import AskOrganizer from "../../../components/sections/AskOrganizer";
|
import AskOrganizer from "../../../components/sections/AskOrganizer";
|
||||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
import ContentTemplateDecorativeShapes from "../../_components/ContentTemplateDecorativeShapes";
|
||||||
import "../blog.css";
|
import "../blog.css";
|
||||||
|
|
||||||
// Code split RelatedArticles - blog-specific, below the fold
|
// Code split RelatedArticles - blog-specific, below the fold
|
||||||
@@ -205,58 +205,29 @@ export default async function BlogPostPage({ params }: PageProps) {
|
|||||||
className="relative min-h-screen overflow-x-clip"
|
className="relative min-h-screen overflow-x-clip"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
>
|
>
|
||||||
{/* Content Banner */}
|
|
||||||
<ContentBanner post={post} />
|
<ContentBanner post={post} />
|
||||||
|
|
||||||
{/* Decorative Shapes */}
|
<div className="relative w-full overflow-x-clip">
|
||||||
{/* Right Side Shape (3/4 up the page) */}
|
<ContentTemplateDecorativeShapes
|
||||||
<div
|
variant="article"
|
||||||
className="hidden md:block absolute top-1/4 right-0 pointer-events-none z-10"
|
slug={post.slug}
|
||||||
style={{ transform: "translateX(40%)" }}
|
|
||||||
>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={getAssetPath(ASSETS.CONTENT_SHAPE_1)}
|
|
||||||
alt=""
|
|
||||||
className="w-auto h-auto max-w-none"
|
|
||||||
style={{
|
|
||||||
width: "clamp(120px, 15vw, 200px)",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Left Side Shape (3/4 down the page) */}
|
<article
|
||||||
<div
|
data-node-id="19031:10426"
|
||||||
className="hidden md:block absolute top-1/2 left-0 pointer-events-none z-10"
|
className="
|
||||||
style={{ transform: "translateX(-10%)" }}
|
relative z-10 flex w-full justify-center
|
||||||
>
|
p-[var(--spacing-scale-024)]
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
sm:px-0 sm:py-[var(--spacing-scale-032)]
|
||||||
<img
|
xl:px-[calc(268/1440*100%)]
|
||||||
src={getAssetPath(ASSETS.CONTENT_SHAPE_2)}
|
"
|
||||||
alt=""
|
>
|
||||||
className="w-auto h-auto max-w-none"
|
<div className="post-body w-full text-[var(--color-content-inverse-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-none">
|
||||||
style={{
|
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
|
||||||
width: "clamp(100px, 12vw, 180px)",
|
</div>
|
||||||
height: "auto",
|
</article>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content — Figma Content page Template (19003:23305) article body instances */}
|
|
||||||
<article
|
|
||||||
data-node-id="19031:10426"
|
|
||||||
className="
|
|
||||||
relative z-[2] flex w-full justify-center
|
|
||||||
p-[var(--spacing-scale-024)]
|
|
||||||
sm:px-0 sm:py-[var(--spacing-scale-032)]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div className="post-body w-full text-[var(--color-content-inverse-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]">
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* Related Articles Section */}
|
{/* Related Articles Section */}
|
||||||
<RelatedArticles
|
<RelatedArticles
|
||||||
relatedPosts={relatedArticles}
|
relatedPosts={relatedArticles}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* Figma: "A Guide to CommunityRule" body ornaments (22078:791901)
|
|
||||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22078-791901
|
|
||||||
*
|
|
||||||
* - 19003:23575 — concentric circles, right (`how-shape-2.svg`)
|
|
||||||
* - 19003:23576 — loop mark, left (`how-shape-1.svg`)
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
getAssetPath,
|
|
||||||
howItWorksOrnamentLeftPath,
|
|
||||||
howItWorksOrnamentRightPath,
|
|
||||||
} from "../../../../lib/assetUtils";
|
|
||||||
|
|
||||||
export default function HowItWorksDecorativeShapes() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[84.86%] right-[-9.28%] top-[clamp(200px,20vw,255px)]"
|
|
||||||
data-node-id="19003:23575"
|
|
||||||
>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={getAssetPath(howItWorksOrnamentRightPath())}
|
|
||||||
alt=""
|
|
||||||
className="pointer-events-none size-full max-w-none object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[-1.66%] right-[88.38%] top-[clamp(520px,55vw,811px)]"
|
|
||||||
data-node-id="19003:23576"
|
|
||||||
>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={getAssetPath(howItWorksOrnamentLeftPath())}
|
|
||||||
alt=""
|
|
||||||
className="pointer-events-none size-full max-w-none object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
HOW_IT_WORKS_SENTINEL_SLUG,
|
HOW_IT_WORKS_SENTINEL_SLUG,
|
||||||
} from "../../../lib/howItWorksSyntheticPost";
|
} from "../../../lib/howItWorksSyntheticPost";
|
||||||
import ContentBanner from "../../components/sections/ContentBanner";
|
import ContentBanner from "../../components/sections/ContentBanner";
|
||||||
import HowItWorksDecorativeShapes from "./_components/HowItWorksDecorativeShapes";
|
import ContentTemplateDecorativeShapes from "../_components/ContentTemplateDecorativeShapes";
|
||||||
import AskOrganizer from "../../components/sections/AskOrganizer";
|
import AskOrganizer from "../../components/sections/AskOrganizer";
|
||||||
import "../blog/blog.css";
|
import "../blog/blog.css";
|
||||||
|
|
||||||
@@ -103,15 +103,15 @@ export default function HowItWorksPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative min-h-screen overflow-x-hidden bg-transparent">
|
<div className="relative min-h-screen overflow-x-clip bg-transparent">
|
||||||
<ContentBanner post={syntheticPost} variant="guide" />
|
<ContentBanner post={syntheticPost} variant="guide" />
|
||||||
|
|
||||||
<div className="relative w-full">
|
<div className="relative w-full overflow-x-clip">
|
||||||
<HowItWorksDecorativeShapes />
|
<ContentTemplateDecorativeShapes variant="guide" />
|
||||||
|
|
||||||
<article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
|
<article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)] xl:px-[calc(268/1440*100%)]">
|
||||||
<div
|
<div
|
||||||
className="post-body -mt-[var(--spacing-scale-048)] text-[var(--color-content-default-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:mx-auto sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]"
|
className="post-body text-[var(--color-content-default-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:mx-auto sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-none"
|
||||||
dangerouslySetInnerHTML={{ __html: syntheticPost.htmlContent }}
|
dangerouslySetInnerHTML={{ __html: syntheticPost.htmlContent }}
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ export default function LearnPage() {
|
|||||||
|
|
||||||
{/*
|
{/*
|
||||||
* Single responsive render: ContentThumbnailTemplate variant="responsive"
|
* Single responsive render: ContentThumbnailTemplate variant="responsive"
|
||||||
* uses <picture> to swap horizontal/vertical art at smd (530px). The
|
* uses <picture> to swap horizontal/vertical art at smd (530px). Wrap and
|
||||||
* container switches from a vertical flex stack (<smd) to a grid (≥smd),
|
* center leftover cards so a short last row is not left-aligned.
|
||||||
* matching the prior twin-region layout without doubling the DOM.
|
|
||||||
*/}
|
*/}
|
||||||
<div className="flex flex-col space-y-[var(--spacing-scale-002)] sm:space-y-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-024)] sm:pb-[var(--spacing-scale-024)] smd:grid smd:grid-cols-2 smd:gap-[var(--spacing-scale-008)] smd:space-y-0 smd:px-[var(--spacing-scale-020)] smd:pt-[var(--spacing-scale-024)] smd:pb-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-016)] md:px-[var(--spacing-scale-032)] xmd:grid-cols-3 xmd:gap-[var(--spacing-scale-012)] lg:grid-cols-3 lg:gap-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-064)] lg:pt-[var(--spacing-scale-032)] lg:pb-[var(--spacing-scale-064)] lg2:grid-cols-4 lg2:gap-x-[var(--spacing-scale-016)] lg2:gap-y-[var(--spacing-scale-024)] xl:grid-cols-5 xl:gap-x-[var(--spacing-scale-016)] xl:gap-y-[var(--spacing-scale-016)] [&>*]:min-w-0">
|
<div
|
||||||
|
data-testid="learn-article-grid"
|
||||||
|
className="flex flex-col space-y-[var(--spacing-scale-002)] sm:space-y-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-024)] sm:pb-[var(--spacing-scale-024)] smd:flex-row smd:flex-wrap smd:justify-center smd:gap-[var(--spacing-scale-008)] smd:space-y-0 smd:px-[var(--spacing-scale-020)] smd:pt-[var(--spacing-scale-024)] smd:pb-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-016)] md:px-[var(--spacing-scale-032)] xmd:gap-[var(--spacing-scale-012)] lg:gap-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-064)] lg:pt-[var(--spacing-scale-032)] lg:pb-[var(--spacing-scale-064)] lg2:gap-x-[var(--spacing-scale-016)] lg2:gap-y-[var(--spacing-scale-024)] xl:gap-x-[var(--spacing-scale-016)] xl:gap-y-[var(--spacing-scale-016)] [&>*]:min-w-0 [&>*]:w-full smd:[&>*]:w-[calc((100%-var(--spacing-scale-008))/2)] md:[&>*]:w-[calc((100%-var(--spacing-scale-016))/2)] xmd:[&>*]:w-[calc((100%-2*var(--spacing-scale-012))/3)] lg:[&>*]:w-[calc((100%-2*var(--spacing-scale-016))/3)] lg2:[&>*]:w-[calc((100%-3*var(--spacing-scale-016))/4)] xl:[&>*]:w-[calc((100%-4*var(--spacing-scale-016))/5)]"
|
||||||
|
>
|
||||||
{allPosts.map((post) => (
|
{allPosts.map((post) => (
|
||||||
<ContentThumbnailTemplate
|
<ContentThumbnailTemplate
|
||||||
key={post.slug}
|
key={post.slug}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import dynamic from "next/dynamic";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import messages from "../../../messages/en/index";
|
import messages from "../../../messages/en/index";
|
||||||
import { getAllBlogPosts } from "../../../lib/content";
|
import { CONTENT_CATALOG_SLUG_ORDER } from "../../../lib/assetUtils";
|
||||||
|
import { getAllBlogPosts, getRelatedBlogPosts } from "../../../lib/content";
|
||||||
import PageHeader from "../../components/type/PageHeader";
|
import PageHeader from "../../components/type/PageHeader";
|
||||||
import CaseStudy from "../../components/cards/CaseStudy";
|
import CaseStudy from "../../components/cards/CaseStudy";
|
||||||
import UseCasesOrgs from "../../components/sections/UseCasesOrgs";
|
import UseCasesOrgs from "../../components/sections/UseCasesOrgs";
|
||||||
@@ -102,7 +103,10 @@ export default function UseCasesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const allPosts = getAllBlogPosts();
|
const allPosts = getAllBlogPosts();
|
||||||
const relatedPosts = allPosts.slice(0, 8);
|
const relatedPosts = getRelatedBlogPosts(
|
||||||
|
USE_CASES_RELATED_SENTINEL_SLUG,
|
||||||
|
CONTENT_CATALOG_SLUG_ORDER.slice(0, 3),
|
||||||
|
);
|
||||||
const slugOrder = allPosts.map((p) => p.slug);
|
const slugOrder = allPosts.map((p) => p.slug);
|
||||||
|
|
||||||
const tripleStepSteps = asArray<{ title: string; body: string }>(
|
const tripleStepSteps = asArray<{ title: string; body: string }>(
|
||||||
|
|||||||
@@ -36,54 +36,54 @@ const Logo = memo<LogoProps>(
|
|||||||
({ size = "default", palette = "default", wordmark = true }) => {
|
({ size = "default", palette = "default", wordmark = true }) => {
|
||||||
const t = useTranslation("controlsChrome");
|
const t = useTranslation("controlsChrome");
|
||||||
|
|
||||||
// Size configurations
|
// Figma: "Asset / Logo" (4002:6735) — 40×6×27 at 1x; nav/footer/create-flow
|
||||||
|
// instances scale that lockup.
|
||||||
const sizes: Record<string, SizeConfig> = {
|
const sizes: Record<string, SizeConfig> = {
|
||||||
default: {
|
default: {
|
||||||
containerHeight: "h-[41px]",
|
containerHeight: "h-[var(--spacing-scale-040)]",
|
||||||
gap: "gap-[8.28px]",
|
gap: "gap-[var(--spacing-scale-006)]",
|
||||||
textSize: "text-[21.97px]",
|
textSize: "text-[21.97px]",
|
||||||
lineHeight: "leading-[27.05px]",
|
lineHeight: "leading-[27.05px]",
|
||||||
iconSize: "w-[27.05px] h-[27.05px]",
|
iconSize: "w-[27px] h-[27px]",
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
containerHeight:
|
containerHeight: "h-[41px] md:h-[56px] lg:h-[82px]",
|
||||||
"h-[41px] md:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]",
|
gap: "gap-[6.21px] md:gap-[8.49px] lg:gap-[12.43px]",
|
||||||
gap: "gap-[8.28px] md:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]",
|
|
||||||
textSize:
|
textSize:
|
||||||
"text-[21.97px] md:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]",
|
"text-[22.75px] md:text-[31.08px] lg:text-[45.51px]",
|
||||||
lineHeight:
|
lineHeight:
|
||||||
"leading-[27.05px] md:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]",
|
"leading-[28px] md:leading-[38.25px] lg:leading-[56.01px]",
|
||||||
iconSize:
|
iconSize:
|
||||||
"w-[27.05px] h-[27.05px] md:w-[calc(27.05px*1.37)] md:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]",
|
"w-[27.96px] h-[27.96px] md:w-[38.18px] md:h-[38.18px] lg:w-[55.91px] lg:h-[55.91px]",
|
||||||
},
|
},
|
||||||
createFlow: {
|
createFlow: {
|
||||||
containerHeight: "h-[30px] md:h-[41px]",
|
containerHeight: "h-[30px] md:h-[var(--spacing-scale-040)]",
|
||||||
gap: "gap-[6px] md:gap-[8.28px]",
|
gap: "gap-[4.5px] md:gap-[var(--spacing-scale-006)]",
|
||||||
textSize: "text-[16.48px] md:text-[21.97px]",
|
textSize: "text-[16.48px] md:text-[21.97px]",
|
||||||
lineHeight: "leading-[20.28px] md:leading-[27.05px]",
|
lineHeight: "leading-[20.28px] md:leading-[27.05px]",
|
||||||
iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]",
|
iconSize: "w-[20.25px] h-[20.25px] md:w-[27px] md:h-[27px]",
|
||||||
},
|
},
|
||||||
topNavFolderTop: {
|
topNavFolderTop: {
|
||||||
containerHeight:
|
containerHeight:
|
||||||
"h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]",
|
"h-[14.1px] sm:h-[21.35px] md:h-[var(--spacing-scale-032)] lg:h-[var(--spacing-scale-040)] xl:h-[50.35px]",
|
||||||
gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]",
|
gap: "gap-0 sm:gap-[3.2px] md:gap-[4.8px] lg:gap-[var(--spacing-scale-006)] xl:gap-[7.5px]",
|
||||||
textSize:
|
textSize:
|
||||||
"text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]",
|
"text-[11.57px] sm:text-[11.72px] md:text-[17.58px] lg:text-[21.97px] xl:text-[27.47px]",
|
||||||
lineHeight:
|
lineHeight:
|
||||||
"leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]",
|
"leading-[14.24px] sm:leading-[14.42px] md:leading-[21.64px] lg:leading-[27.05px] xl:leading-[33.81px]",
|
||||||
iconSize:
|
iconSize:
|
||||||
"w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]",
|
"w-[14.1px] h-[14.1px] sm:w-[14.4px] sm:h-[14.4px] md:w-[21.6px] md:h-[21.6px] lg:w-[27px] lg:h-[27px] xl:w-[33.75px] xl:h-[33.75px]",
|
||||||
},
|
},
|
||||||
topNavHeader: {
|
topNavHeader: {
|
||||||
containerHeight:
|
containerHeight:
|
||||||
"h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]",
|
"h-[14.2px] sm:h-[21.22px] md:h-[var(--spacing-scale-032)] lg:h-[40.69px] xl:h-[50.35px]",
|
||||||
gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]",
|
gap: "gap-0 sm:gap-[3.18px] md:gap-[4.85px] lg:gap-[6.06px] xl:gap-[7.5px]",
|
||||||
textSize:
|
textSize:
|
||||||
"text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]",
|
"text-[11.57px] sm:text-[11.66px] md:text-[17.76px] lg:text-[22.2px] xl:text-[27.47px]",
|
||||||
lineHeight:
|
lineHeight:
|
||||||
"leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]",
|
"leading-[14.24px] sm:leading-[14.35px] md:leading-[21.86px] lg:leading-[27.32px] xl:leading-[33.81px]",
|
||||||
iconSize:
|
iconSize:
|
||||||
"w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]",
|
"w-[14.2px] h-[14.2px] sm:w-[14.33px] sm:h-[14.33px] md:w-[21.82px] md:h-[21.82px] lg:w-[27.27px] lg:h-[27.27px] xl:w-[33.75px] xl:h-[33.75px]",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,8 +121,8 @@ const Logo = memo<LogoProps>(
|
|||||||
<img
|
<img
|
||||||
src={getAssetPath(ASSETS.LOGO)}
|
src={getAssetPath(ASSETS.LOGO)}
|
||||||
alt={t("logoAlt")}
|
alt={t("logoAlt")}
|
||||||
width={27.05}
|
width={27}
|
||||||
height={27.05}
|
height={27}
|
||||||
className={`flex-shrink-0 ${config.iconSize} origin-center transition-transform duration-200 ease-out group-hover:scale-110 ${
|
className={`flex-shrink-0 ${config.iconSize} origin-center transition-transform duration-200 ease-out group-hover:scale-110 ${
|
||||||
isInverse ? "brightness-0" : ""
|
isInverse ? "brightness-0" : ""
|
||||||
}`}
|
}`}
|
||||||
@@ -28,7 +28,7 @@ function VerticalComponent({
|
|||||||
"data-testid": dataTestId,
|
"data-testid": dataTestId,
|
||||||
}: VerticalProps) {
|
}: VerticalProps) {
|
||||||
const base =
|
const base =
|
||||||
"box-border flex w-[90px] shrink-0 cursor-pointer flex-col items-center gap-[var(--spacing-scale-008)] rounded-[var(--spacing-scale-004)] border border-solid border-[var(--color-border-default-brand-primary)] bg-transparent px-[var(--spacing-scale-008)] py-[var(--spacing-scale-012)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] disabled:cursor-not-allowed disabled:opacity-60";
|
"box-border flex w-full min-w-[90px] cursor-pointer flex-col items-center gap-[var(--spacing-scale-008)] rounded-[var(--spacing-scale-004)] border border-solid border-[var(--color-border-default-brand-primary)] bg-transparent px-[var(--spacing-scale-008)] py-[var(--spacing-scale-012)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] disabled:cursor-not-allowed disabled:opacity-60";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
|
|||||||
* Figma: "Utility / CardStack"; canonical code under `cards/`.
|
* Figma: "Utility / CardStack"; canonical code under `cards/`.
|
||||||
* Selectable stack of cards with
|
* Selectable stack of cards with
|
||||||
* an optional "see all"/"show less" expand toggle.
|
* an optional "see all"/"show less" expand toggle.
|
||||||
|
* Expanded default layout matches Flow — Expanded Card Stack (`20768:14820`):
|
||||||
|
* 281×142 CardSelection tiles in two columns.
|
||||||
*/
|
*/
|
||||||
const CardStackContainer = memo<CardStackProps>(
|
const CardStackContainer = memo<CardStackProps>(
|
||||||
({
|
({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface CardStackProps {
|
|||||||
showLessLabel?: string;
|
showLessLabel?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
/** "default" = compact grid/column + expanded grid; "singleStack" = always one column, expand shows more in same stack */
|
/** "default" = compact grid/column + expanded 2-col 142px tiles; "singleStack" = always one column, expand shows more in same stack */
|
||||||
layout?: "default" | "singleStack";
|
layout?: "default" | "singleStack";
|
||||||
/**
|
/**
|
||||||
* Max recommended cards in compact (non-expanded) mode. Default 5; Figma compact stack uses 3.
|
* Max recommended cards in compact (non-expanded) mode. Default 5; Figma compact stack uses 3.
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import type { HeaderLockupSizeValue } from "../../type/HeaderLockup/HeaderLockup
|
|||||||
import Selection from "../Selection";
|
import Selection from "../Selection";
|
||||||
import type { CardStackViewProps } from "./CardStack.types";
|
import type { CardStackViewProps } from "./CardStack.types";
|
||||||
|
|
||||||
|
/** Figma Card / CardSelection on Compact + Expanded Card Stack (`281×142`). */
|
||||||
|
const FIGMA_CARD_SELECTION_TILE_CLASS =
|
||||||
|
"h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0";
|
||||||
|
|
||||||
function CardStackHeaderLockup({
|
function CardStackHeaderLockup({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -144,7 +148,7 @@ export function CardStackView({
|
|||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
showInfoIcon={true}
|
showInfoIcon={false}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -175,24 +179,46 @@ export function CardStackView({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<div className="mx-auto grid w-full max-w-[min(100%,860px)] grid-cols-1 gap-x-4 gap-y-6 md:grid-cols-2">
|
<>
|
||||||
{cards.map((item) => (
|
<div className="flex w-full flex-col gap-2 md:hidden">
|
||||||
<Selection
|
{cards.map((item) => (
|
||||||
key={item.id}
|
<Selection
|
||||||
id={item.id}
|
key={item.id}
|
||||||
label={item.label}
|
id={item.id}
|
||||||
supportText={item.supportText}
|
label={item.label}
|
||||||
recommended={item.recommended ?? false}
|
supportText={item.supportText}
|
||||||
selected={isSelected(item.id)}
|
recommended={item.recommended ?? false}
|
||||||
orientation="vertical"
|
selected={isSelected(item.id)}
|
||||||
showInfoIcon={true}
|
orientation="horizontal"
|
||||||
onClick={() => onCardSelect(item.id)}
|
showInfoIcon={false}
|
||||||
/>
|
className="min-h-[142px]"
|
||||||
))}
|
onClick={() => onCardSelect(item.id)}
|
||||||
{addTile ? (
|
/>
|
||||||
<div className="min-w-0 md:col-span-2">{addTile}</div>
|
))}
|
||||||
) : null}
|
{addTile}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mx-auto hidden w-full max-w-[min(100%,570px)] flex-wrap justify-center gap-2 md:flex">
|
||||||
|
{cards.map((item) => (
|
||||||
|
<Selection
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
label={item.label}
|
||||||
|
supportText={item.supportText}
|
||||||
|
recommended={item.recommended ?? false}
|
||||||
|
selected={isSelected(item.id)}
|
||||||
|
orientation="horizontal"
|
||||||
|
showInfoIcon={false}
|
||||||
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
|
onClick={() => onCardSelect(item.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{addTile ? (
|
||||||
|
<div className="flex w-full min-w-0 shrink-0 justify-center md:flex-[1_1_100%]">
|
||||||
|
{addTile}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : compactDesktopLayout === "pyramidFive" ? (
|
) : compactDesktopLayout === "pyramidFive" ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-2 md:hidden">
|
<div className="flex w-full flex-col gap-2 md:hidden">
|
||||||
@@ -230,7 +256,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -249,7 +275,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -268,7 +294,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -284,7 +310,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -299,7 +325,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(compactCards[4].id)}
|
selected={isSelected(compactCards[4].id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(compactCards[4].id)}
|
onClick={() => onCardSelect(compactCards[4].id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -346,7 +372,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -360,7 +386,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(compactCards[2].id)}
|
selected={isSelected(compactCards[2].id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(compactCards[2].id)}
|
onClick={() => onCardSelect(compactCards[2].id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -379,7 +405,7 @@ export function CardStackView({
|
|||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
showInfoIcon={false}
|
showInfoIcon={false}
|
||||||
className="h-[142px] min-h-[142px] max-h-[142px] w-[281px] max-w-[281px] shrink-0"
|
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -431,7 +457,7 @@ export function CardStackView({
|
|||||||
recommended={item.recommended ?? false}
|
recommended={item.recommended ?? false}
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
showInfoIcon={true}
|
showInfoIcon={false}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onClick={() => onCardSelect(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -108,10 +108,11 @@ export function RuleView({
|
|||||||
const showRecommendedTag = recommended && !expanded;
|
const showRecommendedTag = recommended && !expanded;
|
||||||
const titleClass = templateGridFigmaShell
|
const titleClass = templateGridFigmaShell
|
||||||
? `
|
? `
|
||||||
max-[639px]:text-x-small-heading
|
font-bricolage-grotesque
|
||||||
min-[640px]:max-[1023px]:font-bricolage-grotesque min-[640px]:max-[1023px]:text-medium-heading
|
max-[639px]:text-small-heading
|
||||||
min-[1024px]:max-[1439px]:font-bricolage-grotesque min-[1024px]:max-[1439px]:text-x-large-heading
|
min-[640px]:max-[767px]:text-x-large-heading
|
||||||
min-[1440px]:font-bricolage-grotesque min-[1440px]:text-x-large-heading
|
min-[768px]:max-[1023px]:text-small-heading
|
||||||
|
min-[1024px]:text-x-large-heading
|
||||||
`
|
`
|
||||||
: `
|
: `
|
||||||
max-[639px]:text-x-small-heading
|
max-[639px]:text-x-small-heading
|
||||||
@@ -120,15 +121,18 @@ export function RuleView({
|
|||||||
min-[1440px]:font-bricolage-grotesque min-[1440px]:text-x-large-heading
|
min-[1440px]:font-bricolage-grotesque min-[1440px]:text-x-large-heading
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const descriptionClass = isLarge
|
const descriptionClass = templateGridFigmaShell
|
||||||
? "text-large-label"
|
? `
|
||||||
: isMedium
|
max-[639px]:text-small-label max-[639px]:leading-[18px]
|
||||||
? templateGridFigmaShell
|
min-[640px]:max-[767px]:text-large-label
|
||||||
? "text-small-label min-[1024px]:max-[1439px]:text-large-label"
|
min-[768px]:max-[1023px]:text-small-label min-[768px]:max-[1023px]:leading-[18px]
|
||||||
: "text-small-label"
|
min-[1024px]:text-large-label
|
||||||
: isSmall
|
`
|
||||||
? "text-small-label" // S: 14px, medium, Inter
|
: isLarge
|
||||||
: "text-x-small-label"; // XS: 12px, medium, Inter
|
? "text-large-label"
|
||||||
|
: isExtraSmall
|
||||||
|
? "text-x-small-label"
|
||||||
|
: "text-small-label";
|
||||||
|
|
||||||
const headerIconCellClass = templateGridFigmaShell
|
const headerIconCellClass = templateGridFigmaShell
|
||||||
? `
|
? `
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { SelectionProps } from "./Selection.types";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Card / CardSelection" — stacked tile e.g. `16775:28762` (recommended + label + supportText).
|
* Figma: "Card / CardSelection" — stacked tile e.g. `16775:28762` (recommended + label + supportText).
|
||||||
* `orientation="horizontal"` selects that vertical stack; `"vertical"` is label + optional info icon with tag on the right (CardStack expanded / single-column).
|
* `orientation="horizontal"` selects that vertical stack; `"vertical"` is label + tag on the right (CardStack `singleStack` / right-rail). Helper `?` stays off until tooltip behavior ships.
|
||||||
*/
|
*/
|
||||||
const SelectionContainer = memo<SelectionProps>(
|
const SelectionContainer = memo<SelectionProps>(
|
||||||
({
|
({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface SelectionProps {
|
|||||||
recommended?: boolean;
|
recommended?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
orientation: "horizontal" | "vertical";
|
orientation: "horizontal" | "vertical";
|
||||||
|
/** Off in product until tooltip behavior ships. */
|
||||||
showInfoIcon?: boolean;
|
showInfoIcon?: boolean;
|
||||||
/** Optional id for the root (e.g. `data-card-id` for focus after modal close). */
|
/** Optional id for the root (e.g. `data-card-id` for focus after modal close). */
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
@@ -84,14 +84,14 @@ export function SelectionView({
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
<span className="text-medium-label text-[var(--color-content-invert-secondary)]">
|
<span className="min-w-0 break-words text-medium-label text-[var(--color-content-invert-secondary)]">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{showInfoIcon ? <InfoIcon /> : null}
|
{showInfoIcon ? <InfoIcon /> : null}
|
||||||
</div>
|
</div>
|
||||||
{supportText ? (
|
{supportText ? (
|
||||||
<p className="text-x-small-paragraph text-[var(--color-content-invert-tertiary)]">
|
<p className="min-w-0 break-words text-x-small-paragraph text-[var(--color-content-invert-tertiary)]">
|
||||||
{supportText}
|
{supportText}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ const Step = memo<StepProps>(({ number, text, size: sizeProp }) => {
|
|||||||
small:
|
small:
|
||||||
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[var(--color-gray-900)]",
|
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[var(--color-gray-900)]",
|
||||||
medium:
|
medium:
|
||||||
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[var(--color-gray-900)]",
|
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[var(--color-gray-900)]",
|
||||||
large:
|
large:
|
||||||
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[var(--color-gray-900)]",
|
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[var(--color-gray-900)]",
|
||||||
xlarge:
|
xlarge:
|
||||||
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[var(--color-gray-900)]",
|
"font-bricolage-grotesque font-medium text-[32px] leading-[1.1] text-[var(--color-gray-900)] [font-variation-settings:'opsz'_14,'wdth'_100]",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sectionNumberWrapperClasses = {
|
const sectionNumberWrapperClasses = {
|
||||||
@@ -92,7 +92,7 @@ const Step = memo<StepProps>(({ number, text, size: sizeProp }) => {
|
|||||||
|
|
||||||
{/* Card Content - Responsive positioning */}
|
{/* Card Content - Responsive positioning */}
|
||||||
<div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
|
<div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
|
||||||
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] sm:leading-[24px] sm:text-[24px] lg:text-[24px] lg:leading-[24px] xl:text-[32px] xl:leading-[32px] text-[var(--color-gray-900)]">
|
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] xl:text-[32px] xl:leading-[1.1] xl:[font-variation-settings:'opsz'_14,'wdth'_100] text-[var(--color-gray-900)]">
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ function ReadOnlyScopeField({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<InputLabel label={label} helpIcon size="s" palette="default" />
|
<InputLabel label={label} size="s" palette="default" />
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{scopes.map((scope) => (
|
{scopes.map((scope) => (
|
||||||
<Chip
|
<Chip
|
||||||
@@ -317,7 +317,7 @@ function ReadOnlyValueField({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<InputLabel label={label} helpIcon size="s" palette="default" />
|
<InputLabel label={label} size="s" palette="default" />
|
||||||
<span className="text-medium-label text-[color:var(--color-content-default-primary)]">
|
<span className="text-medium-label text-[color:var(--color-content-default-primary)]">
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ const ContentContainerContainer = memo<ContentContainerProps>(
|
|||||||
|
|
||||||
const titleClasses =
|
const titleClasses =
|
||||||
size === "xs"
|
size === "xs"
|
||||||
? `font-bricolage font-medium text-[18px] leading-[22px] transition-colors ${titleColor}`
|
? `font-bricolage-grotesque font-medium text-[18px] leading-[22px] transition-colors ${titleColor}`
|
||||||
: `font-bricolage font-medium text-xx-small-display sm:text-x-small-display md:text-[32px] md:leading-[110%] lg:text-medium-display xl:text-x-large-display transition-colors ${titleColor}`;
|
: `font-bricolage-grotesque font-medium text-xx-small-display sm:text-x-small-display md:text-[32px] md:leading-[110%] lg:text-medium-display xl:text-x-large-display transition-colors ${titleColor}`;
|
||||||
|
|
||||||
const descriptionClasses =
|
const descriptionClasses =
|
||||||
size === "xs"
|
size === "xs"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function ContentThumbnailTemplateView({
|
|||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute left-[4.375%] top-[6.099%] z-20 w-[71.875%] smd:left-[6.923%] smd:top-[4.615%] smd:w-[76.923%]">
|
<div className="absolute left-[4.375%] top-[6.099%] z-20 w-[71.875%] max-h-[48%] overflow-hidden smd:left-[6.923%] smd:top-[4.615%] smd:w-[76.923%]">
|
||||||
<ContentContainer post={post} size="xs" />
|
<ContentContainer post={post} size="xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@ function ContentThumbnailTemplateView({
|
|||||||
className="pointer-events-none size-full object-cover"
|
className="pointer-events-none size-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute left-[6.923%] top-[4.615%] z-20 w-[76.923%]">
|
<div className="absolute left-[6.923%] top-[4.615%] z-20 w-[76.923%] max-h-[48%] overflow-hidden">
|
||||||
<ContentContainer post={post} size="xs" />
|
<ContentContainer post={post} size="xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +124,7 @@ function ContentThumbnailTemplateView({
|
|||||||
className="pointer-events-none size-full object-cover"
|
className="pointer-events-none size-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute left-[4.375%] top-[6.099%] z-20 w-[71.875%]">
|
<div className="absolute left-[4.375%] top-[6.099%] z-20 w-[71.875%] max-h-[48%] overflow-hidden">
|
||||||
<ContentContainer post={post} size="xs" />
|
<ContentContainer post={post} size="xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { AddCustomFieldProps, AddCustomFieldType } from "./AddCustomField.t
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Add Custom Field" control — Community Rule System (`20235:12994`).
|
* Figma: "Add Custom Field" control — Community Rule System (`20235:12994`).
|
||||||
* Collapsed CTA expands to a 2×2 field-type picker (per-type modals deferred).
|
* Collapsed CTA expands to a four-tile field-type picker.
|
||||||
*/
|
*/
|
||||||
const AddCustomFieldContainer = memo<AddCustomFieldProps>(
|
const AddCustomFieldContainer = memo<AddCustomFieldProps>(
|
||||||
({ active, onPressAdd, onSelectFieldType, className = "" }) => {
|
({ active, onPressAdd, onSelectFieldType, className = "" }) => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const ADD_CUSTOM_FIELD_TYPE_ICONS = {
|
|||||||
} as const satisfies Record<AddCustomFieldType, IconName>;
|
} as const satisfies Record<AddCustomFieldType, IconName>;
|
||||||
|
|
||||||
export interface AddCustomFieldProps {
|
export interface AddCustomFieldProps {
|
||||||
/** When true, show the 2×2 field-type grid; when false, show the primary CTA. */
|
/** When true, show the four field-type tiles; when false, show the primary CTA. */
|
||||||
active: boolean;
|
active: boolean;
|
||||||
onPressAdd?: () => void;
|
onPressAdd?: () => void;
|
||||||
onSelectFieldType?: (type: AddCustomFieldType) => void;
|
onSelectFieldType?: (type: AddCustomFieldType) => void;
|
||||||
|
|||||||
@@ -38,15 +38,6 @@ function FieldTypeButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stable block height for collapsed vs expanded so the Create dialog (`top-1/2 -translate-y-1/2`)
|
|
||||||
* does not shrink and re-center when toggling `active`.
|
|
||||||
*
|
|
||||||
* - Collapsed CTA: `py-12` (48+48) + inner row (`py-3` + 20px icon/line) ≈ 140px border-box.
|
|
||||||
* - Expanded: inner `p-4` (32) + Vertical tile (py 12+12, gap 8, 32px icon, 18px label) ≈ 114px — shorter without this floor.
|
|
||||||
*/
|
|
||||||
const ADD_CUSTOM_FIELD_SHELL_MIN_H_PX = 140;
|
|
||||||
|
|
||||||
function AddCustomFieldViewComponent({
|
function AddCustomFieldViewComponent({
|
||||||
active,
|
active,
|
||||||
onPressAdd,
|
onPressAdd,
|
||||||
@@ -55,17 +46,12 @@ function AddCustomFieldViewComponent({
|
|||||||
fieldTypeLabels,
|
fieldTypeLabels,
|
||||||
className,
|
className,
|
||||||
}: AddCustomFieldViewProps) {
|
}: AddCustomFieldViewProps) {
|
||||||
const shellStyle = {
|
|
||||||
minHeight: ADD_CUSTOM_FIELD_SHELL_MIN_H_PX,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onPressAdd}
|
onClick={onPressAdd}
|
||||||
style={shellStyle}
|
className={`flex h-[88px] w-full shrink-0 cursor-pointer items-center justify-center rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] px-6 text-medium-label text-[var(--color-content-default-primary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] ${className ?? ""}`.trim()}
|
||||||
className={`flex w-full shrink-0 cursor-pointer items-center justify-center rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] px-6 py-12 text-medium-label text-[var(--color-content-default-primary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] ${className ?? ""}`.trim()}
|
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-[var(--spacing-scale-006)] rounded-full px-4 py-3">
|
<span className="flex items-center gap-[var(--spacing-scale-006)] rounded-full px-4 py-3">
|
||||||
<svg
|
<svg
|
||||||
@@ -89,35 +75,33 @@ function AddCustomFieldViewComponent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expandedShellClasses = ["flex w-full shrink-0 flex-col", className ?? ""]
|
const expandedShellClasses = ["flex w-full shrink-0", className ?? ""]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={expandedShellClasses} style={shellStyle}>
|
<div className={expandedShellClasses}>
|
||||||
<div className="flex w-full flex-col gap-3 rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] p-4">
|
<div className="grid w-full grid-cols-4 gap-3">
|
||||||
<div className="flex w-full flex-row flex-nowrap justify-center gap-3 overflow-x-auto max-sm:justify-start">
|
<FieldTypeButton
|
||||||
<FieldTypeButton
|
type="text"
|
||||||
type="text"
|
label={fieldTypeLabels.text}
|
||||||
label={fieldTypeLabels.text}
|
onSelect={onSelectFieldType}
|
||||||
onSelect={onSelectFieldType}
|
/>
|
||||||
/>
|
<FieldTypeButton
|
||||||
<FieldTypeButton
|
type="badges"
|
||||||
type="badges"
|
label={fieldTypeLabels.badges}
|
||||||
label={fieldTypeLabels.badges}
|
onSelect={onSelectFieldType}
|
||||||
onSelect={onSelectFieldType}
|
/>
|
||||||
/>
|
<FieldTypeButton
|
||||||
<FieldTypeButton
|
type="upload"
|
||||||
type="upload"
|
label={fieldTypeLabels.upload}
|
||||||
label={fieldTypeLabels.upload}
|
onSelect={onSelectFieldType}
|
||||||
onSelect={onSelectFieldType}
|
/>
|
||||||
/>
|
<FieldTypeButton
|
||||||
<FieldTypeButton
|
type="proportion"
|
||||||
type="proportion"
|
label={fieldTypeLabels.proportion}
|
||||||
label={fieldTypeLabels.proportion}
|
onSelect={onSelectFieldType}
|
||||||
onSelect={onSelectFieldType}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function CheckboxView({
|
|||||||
}: CheckboxViewProps) {
|
}: CheckboxViewProps) {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={`inline-flex items-center gap-[8px] cursor-pointer select-none ${
|
className={`inline-flex max-w-full min-w-0 items-center gap-[8px] cursor-pointer select-none ${
|
||||||
disabled ? "opacity-60 cursor-not-allowed" : ""
|
disabled ? "opacity-60 cursor-not-allowed" : ""
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
@@ -51,7 +51,7 @@ export function CheckboxView({
|
|||||||
{label && (
|
{label && (
|
||||||
<span
|
<span
|
||||||
id={labelId}
|
id={labelId}
|
||||||
className="text-[14px] leading-[18px]"
|
className="min-w-0 flex-1 text-[14px] leading-[18px] whitespace-normal break-words"
|
||||||
style={{ color: labelColor }}
|
style={{ color: labelColor }}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ function ChipView({
|
|||||||
|
|
||||||
const baseClasses = `
|
const baseClasses = `
|
||||||
inline-flex
|
inline-flex
|
||||||
|
max-w-full
|
||||||
items-center
|
items-center
|
||||||
justify-center
|
justify-center
|
||||||
rounded-[var(--measures-radius-full,9999px)]
|
rounded-[var(--measures-radius-full,9999px)]
|
||||||
@@ -264,7 +265,7 @@ function ChipView({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...sharedA11y}
|
{...sharedA11y}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center">{label}</span>
|
<span className="min-w-0 truncate">{label}</span>
|
||||||
{onRemove && !isDisabled && (
|
{onRemove && !isDisabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -45,30 +45,30 @@ function InfoMessageBoxView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-[12px] p-[var(--spacing-measures-spacing-500,20px)] rounded-[var(--measures-radius-300,12px)] border-l-2 border-solid border-[var(--color-border-default-secondary,#1f1f1f)] bg-[var(--color-content-inverse-secondary,#1f1f1f)] w-full min-w-0 ${className}`}
|
className={`flex w-full min-w-0 items-start gap-[var(--measures-spacing-200,8px)] p-[var(--spacing-measures-spacing-500,20px)] rounded-[var(--measures-radius-300,12px)] border-l-2 border-solid border-[var(--color-border-default-secondary,#1f1f1f)] bg-[var(--color-content-inverse-secondary,#1f1f1f)] ${className}`}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-[var(--measures-spacing-200,8px)] min-w-0">
|
<div
|
||||||
<div
|
className="relative shrink-0 size-6 flex items-center justify-center"
|
||||||
className="relative shrink-0 size-6 flex items-center justify-center"
|
data-name="Asset / Icon / exclamation"
|
||||||
data-name="Asset / Icon / exclamation"
|
>
|
||||||
>
|
{icon ?? <ExclamationIconInline />}
|
||||||
{icon ?? <ExclamationIconInline />}
|
</div>
|
||||||
</div>
|
<div className="flex min-w-0 flex-1 flex-col gap-[12px]">
|
||||||
<p className="text-small-label text-[var(--color-content-default-primary,white)] min-w-0">
|
<p className="text-small-label text-[var(--color-content-default-primary,white)] min-w-0 break-words">
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div className="flex min-w-0 w-full flex-col gap-[12px] [&_label]:w-full [&_label]:min-w-0 [&_label]:gap-[6px] [&_label_span]:text-x-small-paragraph [&_label_span]:opacity-80">
|
||||||
<div className="flex flex-col gap-[12px] [&_label]:gap-[6px] [&_label_span]:text-x-small-paragraph [&_label_span]:opacity-80 pl-8">
|
<CheckboxGroup
|
||||||
<CheckboxGroup
|
mode="standard"
|
||||||
mode="standard"
|
value={checkedIds}
|
||||||
value={checkedIds}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
options={options}
|
||||||
options={options}
|
aria-label={title}
|
||||||
aria-label={title}
|
className="flex w-full min-w-0 flex-col gap-[12px] !space-y-0"
|
||||||
className="flex flex-col gap-[12px] !space-y-0"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,22 +11,22 @@ const SIZE_CLASSES: Record<
|
|||||||
> = {
|
> = {
|
||||||
s: {
|
s: {
|
||||||
header:
|
header:
|
||||||
"gap-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-020)] items-center",
|
"min-h-[88px] gap-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-020)] items-center",
|
||||||
title: "text-[14px] font-medium leading-[18px]",
|
title: "text-[14px] font-medium leading-[18px]",
|
||||||
subhead: "text-x-small-label",
|
subhead: "text-x-small-label",
|
||||||
},
|
},
|
||||||
/** Figma: Layout / Accordion — Medium (22135-890258; header gap/px/py + Large/Label 18/24). */
|
/** Figma: Layout / Accordion — Medium (22135-890258; header gap/px/py + Large/Label 18/24). */
|
||||||
m: {
|
m: {
|
||||||
header:
|
header:
|
||||||
"gap-[var(--spacing-scale-024)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-024)] items-center",
|
"min-h-[104px] gap-[var(--spacing-scale-024)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-024)] items-center",
|
||||||
title: "text-large-label",
|
title: "text-large-label font-medium leading-6",
|
||||||
subhead: "text-[14px] leading-[18px] font-medium",
|
subhead: "text-[14px] leading-[18px] font-medium",
|
||||||
},
|
},
|
||||||
l: {
|
l: {
|
||||||
header:
|
header:
|
||||||
"gap-[var(--spacing-scale-048)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-032)] items-center",
|
"min-h-[135px] gap-[var(--spacing-scale-048)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-032)] items-center",
|
||||||
/** Figma Large: X Large Label 24 Regular, lh 28 (21842-2869). */
|
/** Figma Large: X Large Label 24 Regular, lh 28 (21842-2869). */
|
||||||
title: "text-x-large-label",
|
title: "text-x-large-label font-normal leading-7",
|
||||||
subhead: "text-large-label",
|
subhead: "text-large-label",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -137,10 +137,14 @@ function AccordionView({
|
|||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`flex size-6 shrink-0 items-center justify-center text-[var(--color-content-default-primary,white)] transition-transform ${isOpen ? "-rotate-90" : "rotate-90"}`}
|
className="flex h-6 w-12 shrink-0 items-center justify-end text-[var(--color-content-default-primary,white)]"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
<Icon name="chevron_right" size={24} />
|
<span
|
||||||
|
className={`flex size-6 items-center justify-center transition-transform ${isOpen ? "-rotate-90" : "rotate-90"}`}
|
||||||
|
>
|
||||||
|
<Icon name="chevron_right" size={24} />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function CreateView({
|
|||||||
{headerContent !== undefined ? (
|
{headerContent !== undefined ? (
|
||||||
<div className="shrink-0">{headerContent}</div>
|
<div className="shrink-0">{headerContent}</div>
|
||||||
) : title || description ? (
|
) : title || description ? (
|
||||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
<div className="bg-[var(--color-surface-default-primary)] px-[24px] shrink-0">
|
||||||
<ContentLockup
|
<ContentLockup
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import TextInput from "../../controls/TextInput";
|
|||||||
import ContentLockup from "../../type/ContentLockup";
|
import ContentLockup from "../../type/ContentLockup";
|
||||||
import Alert from "../Alert";
|
import Alert from "../Alert";
|
||||||
import { requestMagicLink } from "../../../../lib/create/api";
|
import { requestMagicLink } from "../../../../lib/create/api";
|
||||||
|
import { EMAIL_MAX_LEN } from "../../../../lib/create/isValidCreateFlowSaveEmail";
|
||||||
import { buildCreateFlowDraftPayload } from "../../../../lib/create/buildCreateFlowDraftPayload";
|
import { buildCreateFlowDraftPayload } from "../../../../lib/create/buildCreateFlowDraftPayload";
|
||||||
import { safeInternalPath } from "../../../../lib/safeInternalPath";
|
import { safeInternalPath } from "../../../../lib/safeInternalPath";
|
||||||
import {
|
import {
|
||||||
@@ -232,6 +233,7 @@ export default function LoginForm({
|
|||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
error={Boolean(emailError)}
|
error={Boolean(emailError)}
|
||||||
showHelpIcon
|
showHelpIcon
|
||||||
|
maxLength={EMAIL_MAX_LEN}
|
||||||
/>
|
/>
|
||||||
{emailError ? (
|
{emailError ? (
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -23,3 +23,7 @@ export interface ModalFooterProps {
|
|||||||
footerContent?: React.ReactNode;
|
footerContent?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Left footer slot for confirm-dialog cancel; matches Back (`left` / `top` 16 / 12). */
|
||||||
|
export const MODAL_FOOTER_START_SLOT_CLASS =
|
||||||
|
"absolute left-[16px] top-[12px] flex min-h-[40px] items-center";
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { default } from "./ModalFooter.container";
|
export { default } from "./ModalFooter.container";
|
||||||
export type { ModalFooterProps } from "./ModalFooter.types";
|
export type { ModalFooterProps } from "./ModalFooter.types";
|
||||||
|
export { MODAL_FOOTER_START_SLOT_CLASS } from "./ModalFooter.types";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Logo from "../../asset/logo";
|
import Logo from "../../asset/Logo";
|
||||||
import Button from "../../buttons/Button";
|
import Button from "../../buttons/Button";
|
||||||
import ListItem from "../../layout/ListItem";
|
import ListItem from "../../layout/ListItem";
|
||||||
import Popover from "../../modals/Popover";
|
import Popover from "../../modals/Popover";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { useTranslation } from "../../contexts/MessagesContext";
|
import { useTranslation } from "../../contexts/MessagesContext";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Logo from "../asset/logo";
|
import Logo from "../asset/Logo";
|
||||||
import Divider from "../utility/Divider";
|
import Divider from "../utility/Divider";
|
||||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||||
|
|
||||||
|
|||||||
@@ -189,15 +189,11 @@ const TopContainer = memo<TopProps>(
|
|||||||
containerSize: "small" | "medium" | "large" | "xlarge",
|
containerSize: "small" | "medium" | "large" | "xlarge",
|
||||||
avatarSize: "small" | "medium" | "large" | "xlarge",
|
avatarSize: "small" | "medium" | "large" | "xlarge",
|
||||||
) => {
|
) => {
|
||||||
// Use ghost type when folderTop is true, filled (default) otherwise
|
|
||||||
const buttonType = folderTop ? "ghost" : "filled";
|
|
||||||
const palette = "default";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size={buttonSize}
|
size={buttonSize}
|
||||||
buttonType={buttonType}
|
buttonType="filled"
|
||||||
palette={palette}
|
palette="inverse"
|
||||||
onClick={handleCreateRuleClick}
|
onClick={handleCreateRuleClick}
|
||||||
ariaLabel={t("ariaLabels.createNewRule")}
|
ariaLabel={t("ariaLabels.createNewRule")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ASSETS, getAssetPath } from "../../../../lib/assetUtils";
|
|||||||
import Menu from "../Menu";
|
import Menu from "../Menu";
|
||||||
import type { TopViewProps } from "./Top.types";
|
import type { TopViewProps } from "./Top.types";
|
||||||
|
|
||||||
import Logo from "../../asset/logo";
|
import Logo from "../../asset/Logo";
|
||||||
|
|
||||||
function TopView({
|
function TopView({
|
||||||
folderTop,
|
folderTop,
|
||||||
@@ -170,13 +170,10 @@ function TopView({
|
|||||||
<nav
|
<nav
|
||||||
className="relative flex w-full items-center
|
className="relative flex w-full items-center
|
||||||
px-[var(--spacing-scale-016)]
|
px-[var(--spacing-scale-016)]
|
||||||
py-[var(--spacing-scale-016)]
|
py-[var(--spacing-scale-008)]
|
||||||
sm:px-[var(--spacing-measures-spacing-016)]
|
sm:px-[var(--spacing-measures-spacing-016)]
|
||||||
sm:py-[var(--spacing-scale-016)]
|
|
||||||
lg:px-[var(--spacing-measures-spacing-64,64px)]
|
lg:px-[var(--spacing-measures-spacing-64,64px)]
|
||||||
lg:py-[var(--spacing-scale-016)]
|
lg:py-[var(--spacing-scale-016)]"
|
||||||
xl:py-[var(--spacing-scale-016)]
|
|
||||||
min-h-[var(--spacing-scale-040)]"
|
|
||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label={t("ariaLabels.mainNavigation")}
|
aria-label={t("ariaLabels.mainNavigation")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { FaqAccordionProps, FaqAccordionViewProps } from "./Accordion.types
|
|||||||
import type { AccordionSizeValue } from "../../layout/Accordion";
|
import type { AccordionSizeValue } from "../../layout/Accordion";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Sections / Accordion" (22130-889248). Rows: **s** / **m** at `lg` (22135-890258); **Large** (`l`) at `xl` (22135:890328).
|
* Figma: "Sections / Accordion" (22130-889248). Rows: **s** through `md` (22130:889249); **m** at `lg` (22135-890258); **l** at `xl` (22135:890328).
|
||||||
*/
|
*/
|
||||||
const FaqAccordionContainer = memo<FaqAccordionProps>(
|
const FaqAccordionContainer = memo<FaqAccordionProps>(
|
||||||
({ size: sizeProp = "s", lgSize: lgSizeProp = "m", xlSize: xlSizeProp = "l", ...props }) => {
|
({ size: sizeProp = "s", lgSize: lgSizeProp = "m", xlSize: xlSizeProp = "l", ...props }) => {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import LayoutAccordion from "../../layout/Accordion";
|
|||||||
import type { FaqAccordionViewProps } from "./Accordion.types";
|
import type { FaqAccordionViewProps } from "./Accordion.types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Sections / Accordion" (22130-889248; mobile FAQ 22132-889380). **xl** rows **Large** via `xlSize` (22135:890328).
|
* Figma: "Sections / Accordion" (22130-889248; mobile FAQ 22132-889380).
|
||||||
* Section title: Large Heading (32px, lh 40) below `lg`; X Large Heading (36px, lh 44) at `lg`; XX Large Heading (40px, lh 52) at `xl` (Figma desktop frame 22135:890398).
|
* Inset: **4/32** stacked, **96/64** from `md` (22130:889249), **160/96** from `lg` (22130:889247 / 22135:890328).
|
||||||
|
* Rows: **s** below `lg`, **m** at `lg`, **l** at `xl` via `xlSize`.
|
||||||
|
* Section title: Large Heading below `lg`; X Large Heading at `lg`; XX Large Heading at `xl` (22135:890398).
|
||||||
*/
|
*/
|
||||||
function FaqAccordionView({
|
function FaqAccordionView({
|
||||||
title,
|
title,
|
||||||
@@ -20,12 +22,12 @@ function FaqAccordionView({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-labelledby={headingId}
|
aria-labelledby={headingId}
|
||||||
className={`bg-[var(--color-surface-default-secondary)] px-[var(--spacing-scale-004)] py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-160)] md:py-[var(--spacing-scale-096)] ${className}`.trim()}
|
className={`bg-[var(--color-surface-default-secondary)] px-[var(--spacing-scale-004)] py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-096)] md:py-[var(--spacing-scale-064)] lg:px-[var(--spacing-scale-160)] lg:py-[var(--spacing-scale-096)] ${className}`.trim()}
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col items-center gap-[var(--spacing-scale-096)] md:gap-[var(--spacing-scale-040)]">
|
<div className="mx-auto flex w-full flex-col items-center gap-[var(--spacing-scale-096)] md:gap-[var(--spacing-scale-040)]">
|
||||||
<h2
|
<h2
|
||||||
id={headingId}
|
id={headingId}
|
||||||
className="w-full px-[var(--spacing-scale-016)] text-center font-bricolage-grotesque text-[length:var(--text-large-heading)] font-bold leading-[length:var(--text-large-heading--line-height)] text-[var(--color-content-default-brand-primary,#fefcc9)] md:px-0 lg:text-[length:var(--text-x-large-heading)] lg:leading-[length:var(--text-x-large-heading--line-height)] xl:text-[length:var(--text-xx-large-heading)] xl:leading-[length:var(--text-xx-large-heading--line-height)] xl:tracking-[var(--text-xx-large-heading--letter-spacing)]"
|
className="w-full text-center font-bricolage-grotesque text-[length:var(--text-large-heading)] font-bold leading-[length:var(--text-large-heading--line-height)] text-[var(--color-content-default-brand-primary,#fefcc9)] lg:text-[length:var(--text-x-large-heading)] lg:leading-[length:var(--text-x-large-heading--line-height)] xl:text-[length:var(--text-xx-large-heading)] xl:leading-[length:var(--text-xx-large-heading--line-height)] xl:tracking-[var(--text-xx-large-heading--letter-spacing)]"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ function BookView({
|
|||||||
headingId,
|
headingId,
|
||||||
className = "",
|
className = "",
|
||||||
}: BookViewProps) {
|
}: BookViewProps) {
|
||||||
const coverSrc = imageSrc ?? getAssetPath(ASSETS.COMMUNITYRULES_COVER);
|
const coverSrc =
|
||||||
|
imageSrc ?? getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function CardStepsView({
|
|||||||
/>
|
/>
|
||||||
<section className="bg-transparent py-[var(--spacing-scale-032)] px-[var(--spacing-scale-020)] sm:py-[var(--spacing-scale-048)] sm:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)] lg:px-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-076)] xl:px-[var(--spacing-scale-064)]">
|
<section className="bg-transparent py-[var(--spacing-scale-032)] px-[var(--spacing-scale-020)] sm:py-[var(--spacing-scale-048)] sm:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)] lg:px-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-076)] xl:px-[var(--spacing-scale-064)]">
|
||||||
<div className="max-w-[var(--spacing-measures-max-width-lg)] mx-auto">
|
<div className="max-w-[var(--spacing-measures-max-width-lg)] mx-auto">
|
||||||
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-032)] sm:gap-y-[var(--spacing-scale-048)] lg:gap-y-[var(--spacing-scale-056)]">
|
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-024)] sm:gap-y-[var(--spacing-scale-032)] lg:gap-y-[var(--spacing-scale-056)]">
|
||||||
<div>
|
<div>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
variant="multi-line"
|
variant="multi-line"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const ContentBannerContainer = memo<ContentBannerProps>(
|
|||||||
leadingImageAlt,
|
leadingImageAlt,
|
||||||
rulePreview,
|
rulePreview,
|
||||||
contentTone,
|
contentTone,
|
||||||
|
updatedLabel,
|
||||||
}) => {
|
}) => {
|
||||||
const variant = variantProp;
|
const variant = variantProp;
|
||||||
const tUseCase = useTranslation("pages.useCasesCompletedRule");
|
const tUseCase = useTranslation("pages.useCasesCompletedRule");
|
||||||
@@ -81,6 +82,7 @@ const ContentBannerContainer = memo<ContentBannerProps>(
|
|||||||
rulePreview={rulePreview}
|
rulePreview={rulePreview}
|
||||||
contentTone={contentTone}
|
contentTone={contentTone}
|
||||||
ruleCardLinkAriaLabel={ruleCardLinkAriaLabel}
|
ruleCardLinkAriaLabel={ruleCardLinkAriaLabel}
|
||||||
|
updatedLabel={updatedLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface ContentBannerProps {
|
|||||||
rulePreview?: ContentBannerRulePreview;
|
rulePreview?: ContentBannerRulePreview;
|
||||||
/** `useCase` only: ContentContainer text tokens (default `onLight`). */
|
/** `useCase` only: ContentContainer text tokens (default `onLight`). */
|
||||||
contentTone?: ContentContainerToneValue;
|
contentTone?: ContentContainerToneValue;
|
||||||
|
/** `guide` only: author + this label under the title. */
|
||||||
|
updatedLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentBannerViewProps {
|
export interface ContentBannerViewProps {
|
||||||
@@ -43,4 +45,6 @@ export interface ContentBannerViewProps {
|
|||||||
contentTone?: ContentContainerToneValue;
|
contentTone?: ContentContainerToneValue;
|
||||||
/** `useCase` only: aria-label for linked rule preview. */
|
/** `useCase` only: aria-label for linked rule preview. */
|
||||||
ruleCardLinkAriaLabel?: string;
|
ruleCardLinkAriaLabel?: string;
|
||||||
|
/** `guide` only: attribution row under the title. */
|
||||||
|
updatedLabel?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,41 +17,49 @@ import type { ContentBannerViewProps } from "./ContentBanner.types";
|
|||||||
*/
|
*/
|
||||||
function ContentBannerGuideView({
|
function ContentBannerGuideView({
|
||||||
post,
|
post,
|
||||||
}: Pick<ContentBannerViewProps, "post">) {
|
updatedLabel,
|
||||||
const { title, description } = post.frontmatter;
|
}: Pick<ContentBannerViewProps, "post" | "updatedLabel">) {
|
||||||
|
const { title, description, author } = post.frontmatter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="relative w-full overflow-clip px-[var(--spacing-scale-020)] py-[var(--spacing-scale-024)] sm:px-[var(--spacing-scale-032)] sm:py-[var(--spacing-scale-032)] lg:px-[var(--spacing-scale-048)] lg:py-[var(--spacing-scale-040)]"
|
className="relative w-full overflow-clip px-[var(--spacing-scale-020)] py-[var(--spacing-scale-024)] sm:px-[var(--spacing-scale-032)] sm:py-[var(--spacing-scale-032)] lg:px-[var(--spacing-scale-048)] lg:py-[var(--spacing-scale-040)] xl:px-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-076)]"
|
||||||
aria-labelledby="content-banner-title"
|
aria-labelledby="content-banner-title"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx-auto flex w-full max-w-[1024px] flex-col items-start gap-[var(--spacing-scale-024)] md:flex-row md:items-center md:gap-[var(--spacing-scale-032)]"
|
className="mx-auto flex w-full max-w-[1024px] flex-col items-start gap-[var(--spacing-scale-024)] md:flex-row md:items-start md:gap-[var(--spacing-scale-024)] lg:items-center lg:gap-[var(--spacing-scale-032)] xl:max-w-[1312px] xl:gap-[var(--spacing-scale-056)]"
|
||||||
data-node-id="19189:9358"
|
data-node-id="19189:9358"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex w-full max-w-[365px] shrink-0 flex-col items-start justify-center gap-[var(--spacing-scale-024)]"
|
className="flex w-full shrink-0 flex-col items-start justify-center gap-[var(--measures-spacing-016)] md:max-w-[280px] lg:max-w-[365px] xl:max-w-[623px]"
|
||||||
data-node-id="19189:9171"
|
data-node-id="19189:9171"
|
||||||
>
|
>
|
||||||
<div className="flex w-full flex-col items-start gap-[var(--measures-spacing-016)]">
|
<div className="flex w-full flex-col items-start gap-[var(--measures-spacing-004)] text-left text-[var(--color-content-default-primary)]">
|
||||||
<div className="flex w-full flex-col items-start gap-[var(--measures-spacing-004)] text-left text-[var(--color-content-default-primary)]">
|
<h1
|
||||||
<h1
|
id="content-banner-title"
|
||||||
id="content-banner-title"
|
className="w-full font-bricolage font-medium text-[32px] leading-[110%] sm:text-[40px] lg:text-medium-display"
|
||||||
className="w-full font-bricolage font-medium text-[32px] leading-[110%] sm:text-[40px] lg:text-medium-display"
|
>
|
||||||
>
|
{title}
|
||||||
{title}
|
</h1>
|
||||||
</h1>
|
{description ? (
|
||||||
{description ? (
|
<p className="w-full font-normal text-[16px] leading-[130%] sm:text-large-paragraph">
|
||||||
<p className="w-full font-normal text-[16px] leading-[130%] sm:text-large-paragraph">
|
{description}
|
||||||
{description}
|
</p>
|
||||||
</p>
|
) : null}
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{updatedLabel ? (
|
||||||
|
<div
|
||||||
|
className="flex w-full flex-wrap items-end gap-[var(--measures-spacing-008,8px)] text-xx-small-paragraph text-[var(--color-content-default-secondary)] md:text-x-small-paragraph lg:text-small-paragraph xl:text-large-paragraph"
|
||||||
|
data-name="Metadata Container"
|
||||||
|
>
|
||||||
|
<span>{author}</span>
|
||||||
|
<span>{updatedLabel}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex w-full shrink-0 items-center justify-center md:flex-1"
|
className="flex w-full shrink-0 items-center justify-center max-sm:pb-[var(--spacing-scale-024)] md:flex-1 md:justify-end"
|
||||||
data-node-id="22078:806960"
|
data-node-id="22078:806960"
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
@@ -253,7 +261,7 @@ function ContentBannerUseCaseView({
|
|||||||
|
|
||||||
function ContentBannerView(props: ContentBannerViewProps) {
|
function ContentBannerView(props: ContentBannerViewProps) {
|
||||||
if (props.variant === "guide") {
|
if (props.variant === "guide") {
|
||||||
return <ContentBannerGuideView post={props.post} />;
|
return <ContentBannerGuideView post={props.post} updatedLabel={props.updatedLabel} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.variant === "useCase") {
|
if (props.variant === "useCase") {
|
||||||
|
|||||||
@@ -1,84 +1,23 @@
|
|||||||
"use client";
|
import { memo } from "react";
|
||||||
|
|
||||||
import { memo, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface HeroDecorProps {
|
interface HeroDecorProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeroDecor = memo<HeroDecorProps>(({ className = "" }) => {
|
const HeroDecor = memo<HeroDecorProps>(({ className = "" }) => {
|
||||||
const [grainEnabled, setGrainEnabled] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// feTurbulence forces tiled rasterization that reads as top-down segments on
|
|
||||||
// first paint. Flat shapes render immediately; grain applies after paint.
|
|
||||||
const frame = requestAnimationFrame(() => {
|
|
||||||
setGrainEnabled(true);
|
|
||||||
});
|
|
||||||
return () => cancelAnimationFrame(frame);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
|
className={`text-[var(--color-surface-default-brand-darker-accent)] ${className}`}
|
||||||
viewBox="0 0 1540 645"
|
viewBox="0 0 1540 645"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
overflow="visible"
|
overflow="visible"
|
||||||
preserveAspectRatio="xMidYMid slice"
|
preserveAspectRatio="xMidYMid slice"
|
||||||
>
|
>
|
||||||
<g fill="currentColor">
|
<g fill="currentColor">
|
||||||
<defs>
|
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
||||||
<filter
|
<path d="M674.066 209.121C728.443 209.121 772.525 302.748 772.525 418.242C772.525 533.737 728.443 627.363 674.066 627.363C619.688 627.363 575.607 533.737 575.607 418.242C575.607 302.748 619.688 209.121 674.066 209.121Z" />
|
||||||
id="grain"
|
<path d="M290.328 0C344.705 0 388.787 93.6267 388.787 209.121C388.787 211.519 388.765 213.907 388.728 216.285C401.725 133.082 438.661 73.0664 482.197 73.0664C536.574 73.0664 580.656 166.693 580.656 282.188C580.656 397.682 536.574 491.309 482.197 491.309C427.819 491.309 383.738 397.682 383.738 282.188C383.738 279.79 383.758 277.401 383.796 275.023C370.798 358.226 333.864 418.242 290.328 418.242C246.792 418.242 209.856 358.226 196.859 275.023C196.897 277.401 196.918 279.79 196.918 282.188C196.918 397.682 152.836 491.309 98.459 491.309C44.0816 491.309 0 397.682 0 282.188C0 166.693 44.0816 73.0664 98.459 73.0664C141.995 73.0664 178.929 133.082 191.927 216.285C191.889 213.907 191.869 211.519 191.869 209.121C191.869 93.6267 235.95 0 290.328 0Z" />
|
||||||
filterUnits="objectBoundingBox"
|
<path d="M1057.8 0C1112.18 0 1156.26 93.6267 1156.26 209.121C1156.26 211.519 1156.24 213.907 1156.2 216.285C1169.2 133.082 1206.14 73.0664 1249.67 73.0664C1304.05 73.0664 1348.13 166.693 1348.13 282.188C1348.13 397.682 1304.05 491.309 1249.67 491.309C1195.29 491.309 1151.21 397.682 1151.21 282.188C1151.21 279.79 1151.23 277.401 1151.27 275.023C1138.27 358.226 1101.34 418.242 1057.8 418.242C1014.27 418.242 977.332 358.226 964.334 275.023C964.372 277.401 964.393 279.79 964.393 282.188C964.393 397.682 920.312 491.309 865.934 491.309C811.557 491.309 767.475 397.682 767.475 282.188C767.475 166.693 811.557 73.0664 865.934 73.0664C909.47 73.0664 946.405 133.082 959.402 216.285C959.365 213.907 959.344 211.519 959.344 209.121C959.344 93.6267 1003.43 0 1057.8 0Z" />
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="1"
|
|
||||||
height="1"
|
|
||||||
colorInterpolationFilters="sRGB"
|
|
||||||
>
|
|
||||||
{/* 1) make noise */}
|
|
||||||
<feTurbulence
|
|
||||||
type="fractalNoise"
|
|
||||||
baseFrequency="0.4"
|
|
||||||
numOctaves="3"
|
|
||||||
seed="7"
|
|
||||||
stitchTiles="stitch"
|
|
||||||
result="noise"
|
|
||||||
/>
|
|
||||||
{/* 2) attenuate */}
|
|
||||||
<feColorMatrix
|
|
||||||
in="noise"
|
|
||||||
result="softNoise"
|
|
||||||
type="matrix"
|
|
||||||
values="
|
|
||||||
0.8 0 0 0 0.3
|
|
||||||
0 0.6 0 0 0.2
|
|
||||||
0 0 1.0 0 0.4
|
|
||||||
0 0 0 0.25 0"
|
|
||||||
/>
|
|
||||||
{/* 3) MASK noise to the element's alpha only */}
|
|
||||||
<feComposite
|
|
||||||
in="softNoise"
|
|
||||||
in2="SourceAlpha"
|
|
||||||
operator="in"
|
|
||||||
result="maskedNoise"
|
|
||||||
/>
|
|
||||||
{/* 4) blend masked noise over the shapes */}
|
|
||||||
<feBlend in="SourceGraphic" in2="maskedNoise" mode="multiply" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
{/* apply filter only to the decoration paths */}
|
|
||||||
<g fill="currentColor" filter={grainEnabled ? "url(#grain)" : undefined}>
|
|
||||||
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
|
||||||
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
|
||||||
<path d="M674.066 209.121C728.443 209.121 772.525 302.748 772.525 418.242C772.525 533.737 728.443 627.363 674.066 627.363C619.688 627.363 575.607 533.737 575.607 418.242C575.607 302.748 619.688 209.121 674.066 209.121Z" />
|
|
||||||
<path d="M674.066 209.121C728.443 209.121 772.525 302.748 772.525 418.242C772.525 533.737 728.443 627.363 674.066 627.363C619.688 627.363 575.607 533.737 575.607 418.242C575.607 302.748 619.688 209.121 674.066 209.121Z" />
|
|
||||||
<path d="M290.328 0C344.705 0 388.787 93.6267 388.787 209.121C388.787 211.519 388.765 213.907 388.728 216.285C401.725 133.082 438.661 73.0664 482.197 73.0664C536.574 73.0664 580.656 166.693 580.656 282.188C580.656 397.682 536.574 491.309 482.197 491.309C427.819 491.309 383.738 397.682 383.738 282.188C383.738 279.79 383.758 277.401 383.796 275.023C370.798 358.226 333.864 418.242 290.328 418.242C246.792 418.242 209.856 358.226 196.859 275.023C196.897 277.401 196.918 279.79 196.918 282.188C196.918 397.682 152.836 491.309 98.459 491.309C44.0816 491.309 0 397.682 0 282.188C0 166.693 44.0816 73.0664 98.459 73.0664C141.995 73.0664 178.929 133.082 191.927 216.285C191.889 213.907 191.869 211.519 191.869 209.121C191.869 93.6267 235.95 0 290.328 0Z" />
|
|
||||||
<path d="M290.328 0C344.705 0 388.787 93.6267 388.787 209.121C388.787 211.519 388.765 213.907 388.728 216.285C401.725 133.082 438.661 73.0664 482.197 73.0664C536.574 73.0664 580.656 166.693 580.656 282.188C580.656 397.682 536.574 491.309 482.197 491.309C427.819 491.309 383.738 397.682 383.738 282.188C383.738 279.79 383.758 277.401 383.796 275.023C370.798 358.226 333.864 418.242 290.328 418.242C246.792 418.242 209.856 358.226 196.859 275.023C196.897 277.401 196.918 279.79 196.918 282.188C196.918 397.682 152.836 491.309 98.459 491.309C44.0816 491.309 0 397.682 0 282.188C0 166.693 44.0816 73.0664 98.459 73.0664C141.995 73.0664 178.929 133.082 191.927 216.285C191.889 213.907 191.869 211.519 191.869 209.121C191.869 93.6267 235.95 0 290.328 0Z" />
|
|
||||||
<path d="M1057.8 0C1112.18 0 1156.26 93.6267 1156.26 209.121C1156.26 211.519 1156.24 213.907 1156.2 216.285C1169.2 133.082 1206.14 73.0664 1249.67 73.0664C1304.05 73.0664 1348.13 166.693 1348.13 282.188C1348.13 397.682 1304.05 491.309 1249.67 491.309C1195.29 491.309 1151.21 397.682 1151.21 282.188C1151.21 279.79 1151.23 277.401 1151.27 275.023C1138.27 358.226 1101.34 418.242 1057.8 418.242C1014.27 418.242 977.332 358.226 964.334 275.023C964.372 277.401 964.393 279.79 964.393 282.188C964.393 397.682 920.312 491.309 865.934 491.309C811.557 491.309 767.475 397.682 767.475 282.188C767.475 166.693 811.557 73.0664 865.934 73.0664C909.47 73.0664 946.405 133.082 959.402 216.285C959.365 213.907 959.344 211.519 959.344 209.121C959.344 93.6267 1003.43 0 1057.8 0Z" />
|
|
||||||
</g>
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Sections / LogoWall" (see registry)
|
* Figma: "Section / Logo Wall" (18847:22366)
|
||||||
|
* Index frames: 320–429 2×3 grid, 430–639 3×2 grid, 640+ space-between row.
|
||||||
|
* Logo heights from those instances (1× / 1.25 at lg / 1.5625 at xl).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { memo, useState, useEffect, useMemo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||||
import { getAssetPath, partnerLogoPath } from "../../../../lib/assetUtils";
|
import { getAssetPath, partnerLogoPath } from "../../../../lib/assetUtils";
|
||||||
import LogoWallView from "./LogoWall.view";
|
import LogoWallView from "./LogoWall.view";
|
||||||
@@ -12,39 +14,44 @@ import type { LogoWallProps } from "./LogoWall.types";
|
|||||||
|
|
||||||
const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
|
const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
|
||||||
const t = useTranslation("logoWall");
|
const t = useTranslation("logoWall");
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
|
|
||||||
const defaultLogos = useMemo(
|
const defaultLogos = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
src: getAssetPath(partnerLogoPath("food-not-bombs")),
|
src: getAssetPath(partnerLogoPath("food-not-bombs")),
|
||||||
alt: t("partners.foodNotBombs"),
|
alt: t("partners.foodNotBombs"),
|
||||||
size: "h-11 lg:h-14 xl:h-[70px]",
|
size: "h-[48px] lg:h-[60px] xl:h-[74.6px]",
|
||||||
|
order: "sm:order-4 md:order-4",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: getAssetPath(partnerLogoPath("start-coop")),
|
src: getAssetPath(partnerLogoPath("start-coop")),
|
||||||
alt: t("partners.startCoop"),
|
alt: t("partners.startCoop"),
|
||||||
size: "h-[42px] lg:h-[53px] xl:h-[66px]",
|
size: "h-[51.4px] lg:h-[64.2px] xl:h-[80.3px]",
|
||||||
|
order: "sm:order-1 md:order-3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: getAssetPath(partnerLogoPath("metagov")),
|
src: getAssetPath(partnerLogoPath("metagov")),
|
||||||
alt: t("partners.metagov"),
|
alt: t("partners.metagov"),
|
||||||
size: "h-6 lg:h-8 xl:h-[41px]",
|
size: "h-[26.2px] lg:h-[33px] xl:h-[41px]",
|
||||||
|
order: "sm:order-6 md:order-1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: getAssetPath(partnerLogoPath("open-civics")),
|
src: getAssetPath(partnerLogoPath("open-civics")),
|
||||||
alt: t("partners.openCivics"),
|
alt: t("partners.openCivics"),
|
||||||
size: "h-8 lg:h-10 xl:h-[50px]",
|
size: "h-[50.1px] lg:h-[61.1px] xl:h-[72px]",
|
||||||
|
order: "sm:order-5 md:order-2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: getAssetPath(partnerLogoPath("mutual-aid-co")),
|
src: getAssetPath(partnerLogoPath("mutual-aid-co")),
|
||||||
alt: t("partners.mutualAidCo"),
|
alt: t("partners.mutualAidCo"),
|
||||||
size: "h-11 lg:h-14 xl:h-[70px]",
|
size: "h-[51.1px] lg:h-[63.6px] xl:h-[79.5px]",
|
||||||
|
order: "sm:order-2 md:order-5",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: getAssetPath(partnerLogoPath("cu-boulder")),
|
src: getAssetPath(partnerLogoPath("cu-boulder")),
|
||||||
alt: t("partners.cuBoulder"),
|
alt: t("partners.cuBoulder"),
|
||||||
size: "h-10 lg:h-12 xl:h-[60px]",
|
size: "h-[44.6px] lg:h-[55.8px] xl:h-[69.7px]",
|
||||||
|
order: "sm:order-3 md:order-6",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t],
|
[t],
|
||||||
@@ -55,18 +62,9 @@ const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
|
|||||||
[logos, defaultLogos],
|
[logos, defaultLogos],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Trigger fade-in animation after component mounts
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setIsVisible(true);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogoWallView
|
<LogoWallView
|
||||||
isVisible={isVisible}
|
heading={t("heading")}
|
||||||
displayLogos={displayLogos}
|
displayLogos={displayLogos}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface LogoWallProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LogoWallViewProps {
|
export interface LogoWallViewProps {
|
||||||
isVisible: boolean;
|
heading: string;
|
||||||
displayLogos: Array<{
|
displayLogos: Array<{
|
||||||
src: string;
|
src: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
|
|||||||
@@ -5,25 +5,24 @@ import Image from "next/image";
|
|||||||
import type { LogoWallViewProps } from "./LogoWall.types";
|
import type { LogoWallViewProps } from "./LogoWall.types";
|
||||||
|
|
||||||
function LogoWallView({
|
function LogoWallView({
|
||||||
isVisible,
|
heading,
|
||||||
displayLogos,
|
displayLogos,
|
||||||
className,
|
className,
|
||||||
}: LogoWallViewProps) {
|
}: LogoWallViewProps) {
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={`p-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-032)] lg:px-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-048)] xl:px-[160px] xl:py-[var(--spacing-scale-064)] ${className}`}
|
className={`bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-032)] lg:px-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-048)] xl:px-[160px] xl:py-[var(--spacing-scale-064)] ${className}`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-024)] xl:gap-[var(--spacing-scale-032)]">
|
<div className="flex flex-col items-center gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-024)] xl:gap-[var(--spacing-scale-032)]">
|
||||||
<div
|
<p className="font-inter text-center text-[10px] font-medium uppercase leading-[12px] text-[var(--color-content-default-primary)] xl:text-[14px] xl:leading-[12px]">
|
||||||
className={`transition-opacity duration-500 ${
|
{heading}
|
||||||
isVisible ? "opacity-60" : "opacity-0"
|
</p>
|
||||||
}`}
|
<div className="w-full opacity-60">
|
||||||
>
|
<div className="grid grid-cols-2 grid-rows-3 sm:grid-cols-3 sm:grid-rows-2 md:flex md:w-full md:flex-nowrap md:items-center md:justify-between gap-[38.4px] lg:gap-[var(--spacing-scale-048)]">
|
||||||
<div className="grid grid-cols-2 grid-rows-3 sm:grid-cols-3 sm:grid-rows-2 md:flex md:flex-wrap md:justify-center md:items-center gap-x-[var(--spacing-scale-032)] gap-y-[var(--spacing-scale-032)] sm:gap-y-[var(--spacing-scale-048)]">
|
|
||||||
{displayLogos.map((logo, index) => (
|
{displayLogos.map((logo, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`flex items-center justify-center ${
|
className={`flex items-center justify-center md:shrink-0 ${
|
||||||
logo.order || ""
|
logo.order || ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
|||||||
heading,
|
heading,
|
||||||
}) => {
|
}) => {
|
||||||
const messages = useMessages();
|
const messages = useMessages();
|
||||||
|
const isUseCases = variant === "useCases";
|
||||||
// Memoize filtered posts to prevent unnecessary re-computations
|
// Memoize filtered posts to prevent unnecessary re-computations
|
||||||
const filteredPosts = useMemo(
|
const filteredPosts = useMemo(
|
||||||
() => relatedPosts.filter((post) => post.slug !== currentPostSlug),
|
() => relatedPosts.filter((post) => post.slug !== currentPostSlug),
|
||||||
@@ -29,6 +30,8 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
|||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const autoScroll = !isUseCases && isMobile;
|
||||||
|
const manualScroll = isUseCases || !isMobile;
|
||||||
|
|
||||||
// Memoize the mouse down handler to prevent unnecessary re-renders
|
// Memoize the mouse down handler to prevent unnecessary re-renders
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
@@ -57,14 +60,15 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
|||||||
// Memoize transform style to prevent unnecessary recalculations
|
// Memoize transform style to prevent unnecessary recalculations
|
||||||
const transformStyle = useMemo(
|
const transformStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
transform: isMobile
|
transform:
|
||||||
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
|
!isUseCases && isMobile
|
||||||
: "none",
|
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
|
||||||
scrollBehavior: (!isMobile
|
: "none",
|
||||||
|
scrollBehavior: (manualScroll
|
||||||
? "smooth"
|
? "smooth"
|
||||||
: "auto") as React.CSSProperties["scrollBehavior"],
|
: "auto") as React.CSSProperties["scrollBehavior"],
|
||||||
}),
|
}),
|
||||||
[isMobile, currentIndex],
|
[isMobile, currentIndex, isUseCases, manualScroll],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize progress bar style calculation
|
// Memoize progress bar style calculation
|
||||||
@@ -80,9 +84,9 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
|||||||
[currentIndex, progress],
|
[currentIndex, progress],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-advance every 3 seconds (only on mobile)
|
// Auto-advance every 3 seconds (blog carousel on small viewports only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredPosts.length <= 1 || !isMobile) return;
|
if (!autoScroll || filteredPosts.length <= 1) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
@@ -90,11 +94,11 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [filteredPosts.length, isMobile]);
|
}, [autoScroll, filteredPosts.length]);
|
||||||
|
|
||||||
// Progress animation (only on mobile)
|
// Progress animation (only on mobile)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredPosts.length <= 1 || !isMobile) return;
|
if (!autoScroll || filteredPosts.length <= 1) return;
|
||||||
|
|
||||||
const progressInterval = setInterval(() => {
|
const progressInterval = setInterval(() => {
|
||||||
setProgress((prev) => {
|
setProgress((prev) => {
|
||||||
@@ -106,18 +110,18 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
|||||||
}, 30); // 30ms intervals for smooth animation
|
}, 30); // 30ms intervals for smooth animation
|
||||||
|
|
||||||
return () => clearInterval(progressInterval);
|
return () => clearInterval(progressInterval);
|
||||||
}, [currentIndex, filteredPosts.length, isMobile]);
|
}, [autoScroll, currentIndex, filteredPosts.length]);
|
||||||
|
|
||||||
const useCasesHeadingLines =
|
const useCasesHeadingLines = isUseCases
|
||||||
variant === "useCases"
|
? messages.pages.useCases.relatedArticles.title
|
||||||
? messages.pages.useCases.relatedArticles.title
|
: undefined;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RelatedArticlesView
|
<RelatedArticlesView
|
||||||
filteredPosts={filteredPosts}
|
filteredPosts={filteredPosts}
|
||||||
slugOrder={slugOrder}
|
slugOrder={slugOrder}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
manualScroll={manualScroll}
|
||||||
transformStyle={transformStyle}
|
transformStyle={transformStyle}
|
||||||
getProgressStyle={getProgressStyle}
|
getProgressStyle={getProgressStyle}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface RelatedArticlesViewProps {
|
|||||||
filteredPosts: BlogPost[];
|
filteredPosts: BlogPost[];
|
||||||
slugOrder: string[];
|
slugOrder: string[];
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
manualScroll: boolean;
|
||||||
transformStyle: React.CSSProperties;
|
transformStyle: React.CSSProperties;
|
||||||
getProgressStyle: (_index: number) => React.CSSProperties;
|
getProgressStyle: (_index: number) => React.CSSProperties;
|
||||||
onMouseDown?: (_e: React.MouseEvent<HTMLDivElement>) => void;
|
onMouseDown?: (_e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export function RelatedArticlesView({
|
|||||||
filteredPosts,
|
filteredPosts,
|
||||||
slugOrder,
|
slugOrder,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
manualScroll,
|
||||||
transformStyle,
|
transformStyle,
|
||||||
getProgressStyle,
|
getProgressStyle,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
@@ -76,12 +77,13 @@ export function RelatedArticlesView({
|
|||||||
? "lg:gap-[var(--spacing-scale-012)] lg:pl-[var(--spacing-scale-024)]"
|
? "lg:gap-[var(--spacing-scale-012)] lg:pl-[var(--spacing-scale-024)]"
|
||||||
: ""
|
: ""
|
||||||
} ${
|
} ${
|
||||||
!isMobile
|
manualScroll
|
||||||
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
|
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
|
data-testid="related-articles-track"
|
||||||
style={transformStyle}
|
style={transformStyle}
|
||||||
onMouseDown={!isMobile ? onMouseDown : undefined}
|
onMouseDown={manualScroll ? onMouseDown : undefined}
|
||||||
>
|
>
|
||||||
{filteredPosts.map((relatedPost) => (
|
{filteredPosts.map((relatedPost) => (
|
||||||
<div
|
<div
|
||||||
@@ -100,8 +102,7 @@ export function RelatedArticlesView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress bars - only show on mobile */}
|
{isMobile && !isUseCases && (
|
||||||
{isMobile && (
|
|
||||||
<div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]">
|
<div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]">
|
||||||
{filteredPosts.map((relatedPost, index) => (
|
{filteredPosts.map((relatedPost, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ export function RuleStackView({
|
|||||||
w-full bg-transparent flex flex-col
|
w-full bg-transparent flex flex-col
|
||||||
px-[20px] py-[32px]
|
px-[20px] py-[32px]
|
||||||
min-[640px]:px-[32px] min-[640px]:py-[48px]
|
min-[640px]:px-[32px] min-[640px]:py-[48px]
|
||||||
min-[768px]:py-[56px]
|
|
||||||
min-[1024px]:px-[64px] min-[1024px]:py-[64px]
|
min-[1024px]:px-[64px] min-[1024px]:py-[64px]
|
||||||
min-[1440px]:px-[96px]
|
min-[1440px]:px-[96px]
|
||||||
gap-[24px]
|
gap-[24px]
|
||||||
min-[640px]:gap-[32px]
|
|
||||||
min-[1024px]:gap-[40px]
|
min-[1024px]:gap-[40px]
|
||||||
${className}
|
${className}
|
||||||
`}
|
`}
|
||||||
@@ -53,14 +51,7 @@ export function RuleStackView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div className="flex w-full justify-center">
|
||||||
className="
|
|
||||||
flex justify-center w-full
|
|
||||||
max-[767px]:mt-[var(--measures-spacing-600,24px)]
|
|
||||||
min-[768px]:max-[1023px]:mt-[var(--measures-spacing-800,32px)]
|
|
||||||
min-[1024px]:mt-[var(--measures-spacing-1000,40px)]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
buttonType="outline"
|
buttonType="outline"
|
||||||
palette="default"
|
palette="default"
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ const SectionNumber = memo<SectionNumberProps>(({ number }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative size-[40px] overflow-visible -rotate-[15deg]">
|
<div className="relative size-[40px] overflow-visible">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element -- dynamic src from getImageSrc */}
|
{/* eslint-disable-next-line @next/next/no-img-element -- dynamic src from getImageSrc */}
|
||||||
<img
|
<img
|
||||||
src={getImageSrc(number)}
|
src={getImageSrc(number)}
|
||||||
alt={`Section ${number}`}
|
alt={`Section ${number}`}
|
||||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 size-[47.37px] max-w-none"
|
className="absolute left-1/2 top-1/2 size-[47.37px] max-w-none -translate-x-1/2 -translate-y-1/2 rotate-15"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<span className="text-[var(--font-size-body-small)] font-[var(--font-weight-bold)] text-[var(--color-content-inverse-primary)]">
|
<span className="text-large-label text-[var(--color-content-invert-primary)]">
|
||||||
{number}
|
{number}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import AboutHeaderView from "./AboutHeader.view";
|
|||||||
import type { AboutHeaderProps } from "./AboutHeader.types";
|
import type { AboutHeaderProps } from "./AboutHeader.types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Type / AboutHeader" (22135-889654).
|
* Figma: "Section / About Header" (22135-889654) — 320-639 / 640-1440 / 1440+.
|
||||||
*/
|
*/
|
||||||
const AboutHeaderContainer = memo<AboutHeaderProps>((props) => {
|
const AboutHeaderContainer = memo<AboutHeaderProps>((props) => {
|
||||||
const titleId = useId();
|
const titleId = useId();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function assetRelativeForInlineIcon(icon: "arrow" | "about"): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Type / AboutHeader" (22135-889654).
|
* Figma: "Section / About Header" (22135-889654) — inline marks 32/29, 64/58, 80/80.
|
||||||
*/
|
*/
|
||||||
function AboutHeaderView({
|
function AboutHeaderView({
|
||||||
segments,
|
segments,
|
||||||
@@ -39,19 +39,15 @@ function AboutHeaderView({
|
|||||||
key={`${segment.icon}-${index}`}
|
key={`${segment.icon}-${index}`}
|
||||||
className={
|
className={
|
||||||
segment.icon === "arrow"
|
segment.icon === "arrow"
|
||||||
? "inline-flex h-[0.92em] min-h-[24px] max-h-[52px] shrink-0 items-center self-center md:max-h-[64px]"
|
? "inline-flex size-[32px] shrink-0 items-center self-center md:size-[64px] xl:size-[80px]"
|
||||||
: "inline-flex size-[29px] shrink-0 items-center justify-center md:size-[80px]"
|
: "inline-flex size-[29px] shrink-0 items-center justify-center md:size-[58px] xl:size-[80px]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element -- decorative inline vector */}
|
{/* eslint-disable-next-line @next/next/no-img-element -- decorative inline vector */}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(assetRelativeForInlineIcon(segment.icon))}
|
src={getAssetPath(assetRelativeForInlineIcon(segment.icon))}
|
||||||
alt=""
|
alt=""
|
||||||
className={
|
className="size-full object-contain"
|
||||||
segment.icon === "arrow"
|
|
||||||
? "h-full w-auto max-w-[4.75rem] object-contain md:max-w-[6rem]"
|
|
||||||
: "size-full object-contain"
|
|
||||||
}
|
|
||||||
role="presentation"
|
role="presentation"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ const ContentLockupContainer = memo<ContentLockupProps>(
|
|||||||
titleContainer:
|
titleContainer:
|
||||||
"flex gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-010)] items-center",
|
"flex gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-010)] items-center",
|
||||||
title:
|
title:
|
||||||
"font-bricolage-grotesque font-medium max-sm:text-[32px] max-sm:leading-[32px] sm:text-large-display md:text-medium-display lg:text-x-large-display xl:text-xx-large-display text-[var(--color-content-inverse-primary)]",
|
"font-bricolage-grotesque font-medium max-sm:text-[32px] max-sm:leading-[110%] sm:text-large-display md:text-medium-display lg:text-x-large-display xl:text-xx-large-display text-[var(--color-content-inverse-primary)] [font-variation-settings:'opsz'_14,'wdth'_100]",
|
||||||
subtitle:
|
subtitle:
|
||||||
"font-bricolage-grotesque font-medium max-sm:text-[32px] max-sm:leading-[32px] sm:text-large-display md:text-medium-display lg:text-x-large-display xl:text-xx-large-display text-[var(--color-content-inverse-primary)]",
|
"font-bricolage-grotesque font-medium max-sm:text-[32px] max-sm:leading-[110%] sm:text-large-display md:text-medium-display lg:text-x-large-display xl:text-xx-large-display text-[var(--color-content-inverse-primary)] [font-variation-settings:'opsz'_14,'wdth'_100]",
|
||||||
description:
|
description:
|
||||||
"text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph text-[var(--color-content-inverse-primary)]",
|
"text-small-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph text-[var(--color-content-inverse-primary)]",
|
||||||
shape:
|
shape:
|
||||||
"w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]",
|
"w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]",
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
|
|||||||
titleGroup:
|
titleGroup:
|
||||||
"flex flex-col gap-[var(--spacing-scale-008)] lg:gap-[var(--spacing-scale-004)]",
|
"flex flex-col gap-[var(--spacing-scale-008)] lg:gap-[var(--spacing-scale-004)]",
|
||||||
titleContainer:
|
titleContainer:
|
||||||
"flex flex-wrap items-center gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-010)]",
|
"flex flex-wrap items-center gap-[var(--spacing-scale-010)] xl:gap-[var(--spacing-scale-012)]",
|
||||||
title:
|
title:
|
||||||
"font-bricolage-grotesque font-medium text-[32px] leading-[1.1] md:text-large-display xl:text-x-large-display text-[var(--color-content-default-primary,white)]",
|
"font-bricolage-grotesque font-medium text-[32px] leading-[1.1] md:text-large-display xl:text-x-large-display text-[var(--color-content-default-primary,white)]",
|
||||||
subtitle:
|
subtitle:
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function ContentLockupView({
|
|||||||
{titleContent ? (
|
{titleContent ? (
|
||||||
<h1
|
<h1
|
||||||
id={titleId}
|
id={titleId}
|
||||||
className={`${styles.title} flex flex-wrap items-center gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-010)]`}
|
className={`${styles.title} flex flex-wrap items-center gap-[var(--spacing-scale-010)] xl:gap-[var(--spacing-scale-012)]`}
|
||||||
>
|
>
|
||||||
{titleContent}
|
{titleContent}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -94,9 +94,9 @@ function ContentLockupView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Link for feature variant */}
|
{/* Link for feature variant */}
|
||||||
{variant === "feature" && linkText && (
|
{variant === "feature" && linkText && linkHref && (
|
||||||
<a
|
<a
|
||||||
href={linkHref || "#"}
|
href={linkHref}
|
||||||
className="text-medium-underline underline text-[var(--color-content-default-primary)] hover:text-[var(--color-content-default-secondary)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-secondary)] rounded-sm px-1 py-0.5"
|
className="text-medium-underline underline text-[var(--color-content-default-primary)] hover:text-[var(--color-content-default-secondary)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-secondary)] rounded-sm px-1 py-0.5"
|
||||||
>
|
>
|
||||||
{linkText}
|
{linkText}
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ const SectionHeader = memo<SectionHeaderProps>(
|
|||||||
variant === "multi-line"
|
variant === "multi-line"
|
||||||
? ruleStackDesktopTypeScale
|
? ruleStackDesktopTypeScale
|
||||||
? splitFromMd
|
? splitFromMd
|
||||||
? "font-bricolage-grotesque text-medium-heading text-[var(--color-content-default-primary)] md:w-full md:max-w-none md:text-left md:text-large-heading xl:text-xx-large-heading"
|
? "font-bricolage-grotesque text-medium-heading text-[var(--color-content-default-primary)] md:w-full md:max-w-[571px] md:text-left md:text-large-heading xl:text-xx-large-heading"
|
||||||
: "font-bricolage-grotesque text-medium-heading md:text-large-heading lg:w-full lg:max-w-none lg:text-left lg:text-large-heading xl:text-xx-large-heading text-[var(--color-content-default-primary)]"
|
: "font-bricolage-grotesque text-medium-heading md:text-large-heading lg:w-full lg:max-w-[571px] lg:text-left lg:text-large-heading xl:text-xx-large-heading text-[var(--color-content-default-primary)]"
|
||||||
: "font-bricolage-grotesque text-medium-heading md:text-large-heading lg:w-[410px] lg:text-left xl:text-xx-large-heading text-[var(--color-content-default-primary)]"
|
: "font-bricolage-grotesque text-medium-heading md:text-large-heading lg:w-[410px] lg:text-left xl:text-xx-large-heading text-[var(--color-content-default-primary)]"
|
||||||
: "font-bricolage-grotesque text-medium-heading sm:text-large-heading lg:text-large-heading lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-xx-large-heading xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
|
: "font-bricolage-grotesque text-medium-heading sm:text-large-heading lg:text-large-heading lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-xx-large-heading xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import TripleStepView from "./TripleStep.view";
|
|||||||
import type { TripleStepProps } from "./TripleStep.types";
|
import type { TripleStepProps } from "./TripleStep.types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: **Section / Triple Step** ([22084-859405](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22084-859405&m=dev)); type baseline ([22112-871527](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22112-871527&m=dev)); **md+** two-column + **`triple-step.svg`**.
|
* Figma: **Section / Triple Step** ([22084-859405](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22084-859405&m=dev)); type baseline ([22112-871527](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22112-871527&m=dev)); **md+** two-column + **`triple-step.svg`** in a 1:2 frame (tablet [22084-859257](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22084-859257&m=dev); desktop [22084-858823](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22084-858823&m=dev)).
|
||||||
*/
|
*/
|
||||||
const TripleStepContainer = memo<TripleStepProps>((props) => {
|
const TripleStepContainer = memo<TripleStepProps>((props) => {
|
||||||
const reactId = useId();
|
const reactId = useId();
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ function TripleStepView({
|
|||||||
<section
|
<section
|
||||||
data-figma-node="22084-859405"
|
data-figma-node="22084-859405"
|
||||||
aria-labelledby={headingId}
|
aria-labelledby={headingId}
|
||||||
className={`bg-transparent p-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-048)] lg:px-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-048)] ${className}`.trim()}
|
className={`bg-transparent px-[var(--spacing-scale-032)] py-[var(--spacing-scale-032)] md:px-0 md:py-[var(--spacing-scale-048)] ${className}`.trim()}
|
||||||
>
|
>
|
||||||
<div className="mx-auto grid w-full min-w-0 max-w-[560px] grid-cols-1 gap-[var(--spacing-scale-032)] md:max-w-[1400px] md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] md:items-stretch md:gap-[var(--spacing-scale-060)] lg:items-center">
|
<div className="mx-auto grid w-full min-w-0 max-w-[560px] grid-cols-1 items-center gap-[var(--spacing-scale-032)] md:max-w-none md:grid-cols-2 md:gap-0">
|
||||||
<div className="flex w-full min-w-0 flex-col gap-[var(--spacing-scale-040)] break-words md:self-start lg:self-center">
|
<div className="flex w-full min-w-0 flex-col gap-[var(--spacing-scale-040)] break-words md:pl-[var(--spacing-scale-032)] lg:min-w-[358px] lg:pl-[var(--spacing-scale-064)]">
|
||||||
<h2
|
<h2
|
||||||
id={headingId}
|
id={headingId}
|
||||||
className="font-bricolage-grotesque text-medium-heading text-[var(--color-content-default-primary)] md:text-large-heading"
|
className="font-bricolage-grotesque text-medium-heading text-[var(--color-content-default-primary)] md:text-large-heading"
|
||||||
@@ -73,16 +73,16 @@ function TripleStepView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="hidden min-h-0 min-w-0 w-full md:flex md:items-center md:justify-center"
|
className="relative hidden aspect-[1/2] min-w-0 w-full md:block"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={shapeSrc}
|
src={shapeSrc}
|
||||||
alt=""
|
alt=""
|
||||||
width={288}
|
fill
|
||||||
height={576}
|
|
||||||
unoptimized
|
unoptimized
|
||||||
className="pointer-events-none h-auto w-full max-w-full min-w-0 select-none object-contain"
|
sizes="(min-width: 640px) 50vw, 100vw"
|
||||||
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,11 +112,12 @@ function TripleTextBlockColumnLockup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Section horizontal padding adds **+ Scale/096** below `xl` (outer frame inset); **use cases `xl`** uses **Scale/160** only ([22085:860414](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22085-860414&m=dev)).
|
* Horizontal padding matches the Figma frames: **32** stacked, **96** from `md`/`lg`, **160** at `xl`.
|
||||||
*
|
*
|
||||||
* Figma: use cases **`lg`** [22037:26994](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22037-26994&m=dev);
|
* Figma: use cases **`lg`** [22037:26994](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22037-26994&m=dev);
|
||||||
* baseline **22112:871529** / **22085:860366**; **`md`** [22085:862437](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22085-862437&m=dev); stacked **22137:890676**;
|
* **`xl`** [22085:860414](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22085-860414&m=dev);
|
||||||
* lg 3-col **22128:888715**; xl **22135:889705** (default preset).
|
* **`md`** [22085:862437](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22085-862437&m=dev);
|
||||||
|
* baseline **22112:871529**; default lg **22128:888715**; default xl **22135:889705**.
|
||||||
*/
|
*/
|
||||||
function TripleTextBlockView({
|
function TripleTextBlockView({
|
||||||
title = "",
|
title = "",
|
||||||
@@ -135,17 +136,13 @@ function TripleTextBlockView({
|
|||||||
<section
|
<section
|
||||||
{...(isUseCases ? { "data-figma-node": "22085-860414" } : {})}
|
{...(isUseCases ? { "data-figma-node": "22085-860414" } : {})}
|
||||||
aria-labelledby={hasSectionTitle ? headingId : undefined}
|
aria-labelledby={hasSectionTitle ? headingId : undefined}
|
||||||
className={`bg-black py-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-064)] ${
|
className={`bg-black py-[var(--spacing-scale-064)] px-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-096)] lg:px-[var(--spacing-scale-096)] xl:px-[var(--spacing-scale-160)] ${className}`.trim()}
|
||||||
isUseCases
|
|
||||||
? "px-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-096)] lg:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] xl:px-[var(--spacing-scale-160)]"
|
|
||||||
: "px-[var(--spacing-scale-032)] md:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] lg:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] xl:px-[calc(var(--spacing-scale-160)+var(--spacing-scale-096))]"
|
|
||||||
} ${className}`.trim()}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isUseCases
|
isUseCases
|
||||||
? "mx-auto flex w-full max-w-[1440px] flex-col items-start gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-048)] lg:items-center lg:gap-[var(--spacing-scale-064)]"
|
? "mx-auto flex w-full flex-col items-start gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-048)] lg:items-center lg:gap-[var(--spacing-scale-064)]"
|
||||||
: "mx-auto flex w-full max-w-[1440px] flex-col items-start gap-[var(--spacing-scale-032)]"
|
: "mx-auto flex w-full flex-col items-start gap-[var(--spacing-scale-032)]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{hasSectionTitle ? (
|
{hasSectionTitle ? (
|
||||||
@@ -153,7 +150,7 @@ function TripleTextBlockView({
|
|||||||
id={headingId}
|
id={headingId}
|
||||||
className={
|
className={
|
||||||
isUseCases
|
isUseCases
|
||||||
? "w-full text-left font-bricolage-grotesque text-medium-heading text-[var(--color-content-default-primary,white)] md:text-large-heading lg:mx-auto lg:max-w-[693px] lg:text-center lg:text-x-large-heading xl:text-xx-large-heading"
|
? "w-full text-left font-bricolage-grotesque text-medium-heading text-[var(--color-content-default-primary,white)] md:text-large-heading lg:mx-auto lg:max-w-[693px] lg:text-center lg:text-x-large-heading xl:max-w-none xl:text-xx-large-heading"
|
||||||
: "w-full text-left font-bricolage-grotesque text-[32px] font-medium leading-[1.1] text-[var(--color-content-default-primary,white)]"
|
: "w-full text-left font-bricolage-grotesque text-[32px] font-medium leading-[1.1] text-[var(--color-content-default-primary,white)]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import Login from "../components/modals/Login";
|
import Login from "../components/modals/Login";
|
||||||
import LoginForm from "../components/modals/Login/LoginForm";
|
import LoginForm from "../components/modals/Login/LoginForm";
|
||||||
import { useTranslation } from "./MessagesContext";
|
|
||||||
|
|
||||||
export type AuthModalLoginVariant = "default" | "saveProgress";
|
export type AuthModalLoginVariant = "default" | "saveProgress";
|
||||||
|
|
||||||
@@ -34,7 +32,6 @@ const AuthModalContext = createContext<AuthModalContextValue | null>(null);
|
|||||||
export function AuthModalProvider({ children }: { children: ReactNode }) {
|
export function AuthModalProvider({ children }: { children: ReactNode }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [opts, setOpts] = useState<OpenLoginOptions>({});
|
const [opts, setOpts] = useState<OpenLoginOptions>({});
|
||||||
const t = useTranslation("pages.login");
|
|
||||||
|
|
||||||
const openLogin = useCallback((o?: OpenLoginOptions) => {
|
const openLogin = useCallback((o?: OpenLoginOptions) => {
|
||||||
setOpts(o ?? {});
|
setOpts(o ?? {});
|
||||||
@@ -62,15 +59,6 @@ export function AuthModalProvider({ children }: { children: ReactNode }) {
|
|||||||
backdropVariant={backdropVariant}
|
backdropVariant={backdropVariant}
|
||||||
usePortal
|
usePortal
|
||||||
ariaLabelledBy="login-modal-heading"
|
ariaLabelledBy="login-modal-heading"
|
||||||
belowCard={
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="text-small-paragraph text-[var(--color-content-invert-tertiary,#2d2d2d)] text-center hover:opacity-90"
|
|
||||||
onClick={() => closeLogin()}
|
|
||||||
>
|
|
||||||
{t("backToHome")}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
variant={opts.variant ?? "default"}
|
variant={opts.variant ?? "default"}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import Button from "../components/buttons/Button";
|
import Button from "../components/buttons/Button";
|
||||||
import Create from "../components/modals/Create";
|
import Create from "../components/modals/Create";
|
||||||
|
import { MODAL_FOOTER_START_SLOT_CLASS } from "../components/modals/ModalFooter";
|
||||||
import type { CreateModalBackdropVariant } from "../components/modals/Create/CreateModalFrame.view";
|
import type { CreateModalBackdropVariant } from "../components/modals/Create/CreateModalFrame.view";
|
||||||
|
|
||||||
export type AsyncConfirmOptions = {
|
export type AsyncConfirmOptions = {
|
||||||
@@ -58,14 +59,16 @@ export function useAsyncConfirm() {
|
|||||||
nextButtonText={options.proceedText}
|
nextButtonText={options.proceedText}
|
||||||
onNext={() => close(true)}
|
onNext={() => close(true)}
|
||||||
footerContent={
|
footerContent={
|
||||||
<Button
|
<div className={MODAL_FOOTER_START_SLOT_CLASS}>
|
||||||
buttonType="ghost"
|
<Button
|
||||||
palette="default"
|
buttonType="ghost"
|
||||||
size="xsmall"
|
palette="default"
|
||||||
onClick={() => close(false)}
|
size="xsmall"
|
||||||
>
|
onClick={() => close(false)}
|
||||||
{options.cancelText}
|
>
|
||||||
</Button>
|
{options.cancelText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
backdropVariant={options.backdropVariant ?? "blurredYellow"}
|
backdropVariant={options.backdropVariant ?? "blurredYellow"}
|
||||||
ariaLabel={options.ariaLabel ?? options.title}
|
ariaLabel={options.ariaLabel ?? options.title}
|
||||||
|
|||||||
+5
-2
@@ -26,7 +26,8 @@ const inter = Inter({
|
|||||||
|
|
||||||
const bricolageGrotesque = Bricolage_Grotesque({
|
const bricolageGrotesque = Bricolage_Grotesque({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500", "700", "800"],
|
weight: "variable",
|
||||||
|
axes: ["opsz"],
|
||||||
variable: "--font-bricolage-grotesque",
|
variable: "--font-bricolage-grotesque",
|
||||||
display: "swap",
|
display: "swap",
|
||||||
preload: true,
|
preload: true,
|
||||||
@@ -145,7 +146,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|||||||
<body
|
<body
|
||||||
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable}`}
|
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable}`}
|
||||||
>
|
>
|
||||||
<div className="min-h-screen flex flex-col">{children}</div>
|
<div className="mx-auto flex min-h-screen w-full max-w-[1920px] flex-col">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ thumbnail:
|
|||||||
vertical: "making-decisions-without-hierarchy-vertical.svg"
|
vertical: "making-decisions-without-hierarchy-vertical.svg"
|
||||||
horizontal: "making-decisions-without-hierarchy-horizontal.svg"
|
horizontal: "making-decisions-without-hierarchy-horizontal.svg"
|
||||||
background:
|
background:
|
||||||
color: "#F3F3F1"
|
color: "#ECC98E"
|
||||||
---
|
---
|
||||||
|
|
||||||
Many groups try to work without bosses, managers, or traditional leadership structures. But when no one's in charge, how do decisions actually get made? Non-hierarchical groups often rely on collective processes that prioritize trust, transparency, and shared responsibility. These approaches can take more time upfront, but they help build stronger, more equitable communities in the long run.
|
Many groups try to work without bosses, managers, or traditional leadership structures. But when no one's in charge, how do decisions actually get made? Non-hierarchical groups often rely on collective processes that prioritize trust, transparency, and shared responsibility. These approaches can take more time upfront, but they help build stronger, more equitable communities in the long run.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ background:
|
|||||||
- **related**: Array of article slugs (use filename without .md)
|
- **related**: Array of article slugs (use filename without .md)
|
||||||
- **thumbnail**: Custom images for article thumbnails (optional)
|
- **thumbnail**: Custom images for article thumbnails (optional)
|
||||||
- **banner.horizontal**: Section banner for md+ breakpoints (optional; defaults to `{slug}-section.svg`)
|
- **banner.horizontal**: Section banner for md+ breakpoints (optional; defaults to `{slug}-section.svg`)
|
||||||
- **background.color**: Page background color as a hex code (e.g., `#F4F3F1`)
|
- **background.color**: Fallback page color when there is no `{slug}-section.svg`. If that banner exists, the page uses its 1920×672 rect fill so the body cannot drift from the hero.
|
||||||
|
|
||||||
### Related Articles
|
### Related Articles
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ Each article uses SVGs in `public/content/blog/`. For a new article, duplicate a
|
|||||||
- `{slug}-vertical.svg` — 260px × 390px
|
- `{slug}-vertical.svg` — 260px × 390px
|
||||||
- `{slug}-horizontal.svg` — 320px × 225.5px (minimum width)
|
- `{slug}-horizontal.svg` — 320px × 225.5px (minimum width)
|
||||||
- `{slug}-section.svg` — optional md+ banner (1920×672)
|
- `{slug}-section.svg` — optional md+ banner (1920×672)
|
||||||
|
- `{slug}-ornament-right.png` / `{slug}-ornament-left.png` — body gutter shapes
|
||||||
- `{slug}-tag.svg` — tag mark for content cards
|
- `{slug}-tag.svg` — tag mark for content cards
|
||||||
3. **Customize** the copied SVGs for your article
|
3. **Customize** the copied SVGs for your article
|
||||||
4. **Add to frontmatter**:
|
4. **Add to frontmatter**:
|
||||||
@@ -107,11 +108,16 @@ If you omit custom SVGs, the site reuses assets from an existing catalog article
|
|||||||
|
|
||||||
## Background Color
|
## Background Color
|
||||||
|
|
||||||
Set the content page background using a hex color in frontmatter:
|
The article page background is the fill of the 1920×672 rect in
|
||||||
|
`public/content/blog/{slug}-section.svg` (the md+ ContentBanner). That keeps
|
||||||
|
the body the same color as the hero without a second hex to maintain.
|
||||||
|
|
||||||
|
`background.color` in frontmatter is only used when that section SVG is
|
||||||
|
missing:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
background:
|
background:
|
||||||
color: "#F4F3F1" # Use any valid hex code
|
color: "#F4F3F1" # Fallback when there is no {slug}-section.svg
|
||||||
```
|
```
|
||||||
|
|
||||||
## File Naming
|
## File Naming
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ Do not duplicate the same glyph in both places unless migrating between systems.
|
|||||||
`shareIconPath`, `featurePanelPath`, `ASSETS`, …) over hardcoded
|
`shareIconPath`, `featurePanelPath`, `ASSETS`, …) over hardcoded
|
||||||
`/assets/...` strings in components.
|
`/assets/...` strings in components.
|
||||||
- **Blog art** stays under `public/content/blog/` with
|
- **Blog art** stays under `public/content/blog/` with
|
||||||
`{slug}-vertical.svg`, `-horizontal.svg`, `-section.svg`, `-tag.svg`.
|
`{slug}-vertical.svg`, `-horizontal.svg`, `-section.svg`, `-tag.svg`,
|
||||||
|
and `-ornament-right.png` / `-ornament-left.png` (body gutter shapes).
|
||||||
- **Favicon** reuses `assets/logos/community-rule.svg` (`ASSETS.LOGO` in
|
- **Favicon** reuses `assets/logos/community-rule.svg` (`ASSETS.LOGO` in
|
||||||
`app/layout.tsx` metadata) plus generated root binaries for Safari/iOS:
|
`app/layout.tsx` metadata) plus generated root binaries for Safari/iOS:
|
||||||
`public/favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`, and
|
`public/favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`, and
|
||||||
@@ -64,7 +65,10 @@ stage. Raster → SVG conversion is tracked in
|
|||||||
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
|
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
|
||||||
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG |
|
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG |
|
||||||
| `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster |
|
| `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster |
|
||||||
| `marketing/governance-booklet.pdf` | About / Book | **Done** — PDF (`governanceBookletPath()`) |
|
| `marketing/communityrules-cover.svg` | About / Book | **Done** — Structure Before Crisis cover (`ASSETS.STRUCTURE_BEFORE_CRISIS_COVER`; Figma **22137:891197**) |
|
||||||
|
| `marketing/community-rules-cover.png` | About / Book | **Done** — Community Rules (2021) cover (`ASSETS.COMMUNITY_RULES_COVER`; gitignore exception) |
|
||||||
|
| `marketing/governance-booklet.pdf` | About / Book | **Done** — Community Rules (2021) PDF (`governanceBookletPath()`) |
|
||||||
|
| `marketing/structure-before-crisis.pdf` | About / Book | **Done** — Structure Before Crisis (2025) PDF (`structureBeforeCrisisPath()`) |
|
||||||
| `logos/community-rule.svg` | Logo + favicon (`ASSETS.LOGO`) | **Done** — SVG |
|
| `logos/community-rule.svg` | Logo + favicon (`ASSETS.LOGO`) | **Done** — SVG |
|
||||||
| `share/*.svg` (×5) | Share modal | **Done** — SVG (`shareIconPath()`) |
|
| `share/*.svg` (×5) | Share modal | **Done** — SVG (`shareIconPath()`) |
|
||||||
| `logos/gitlab.svg` | Footer / social | **Done** — SVG |
|
| `logos/gitlab.svg` | Footer / social | **Done** — SVG |
|
||||||
|
|||||||
+34
-3
@@ -97,11 +97,16 @@ export function sectionNumberPath(n: 1 | 2 | 3): string {
|
|||||||
return `assets/marketing/section-number-${n}.svg`;
|
return `assets/marketing/section-number-${n}.svg`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Downloadable governance booklet PDF (About / Sections / Book). */
|
/** Community Rules (2021) booklet PDF (About / Sections / Book). */
|
||||||
export function governanceBookletPath(): string {
|
export function governanceBookletPath(): string {
|
||||||
return "assets/marketing/governance-booklet.pdf";
|
return "assets/marketing/governance-booklet.pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Structure Before Crisis (2025) PDF (About / Sections / Book). */
|
||||||
|
export function structureBeforeCrisisPath(): string {
|
||||||
|
return "assets/marketing/structure-before-crisis.pdf";
|
||||||
|
}
|
||||||
|
|
||||||
/** Home feature grid panel art in `public/assets/marketing/`. */
|
/** Home feature grid panel art in `public/assets/marketing/`. */
|
||||||
export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools";
|
export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools";
|
||||||
|
|
||||||
@@ -170,6 +175,14 @@ export function contentBlogTagPath(slug: string): string {
|
|||||||
return `/content/blog/${slug}-tag.svg`;
|
return `/content/blog/${slug}-tag.svg`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Body gutter ornaments (Figma Content page Template 19003:23575 / 19003:23576). */
|
||||||
|
export function contentBlogOrnamentPath(
|
||||||
|
slug: string,
|
||||||
|
side: "right" | "left",
|
||||||
|
): string {
|
||||||
|
return `/content/blog/${slug}-ornament-${side}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Stable catalog slug order for blog asset fallbacks when an article has no custom SVGs. */
|
/** Stable catalog slug order for blog asset fallbacks when an article has no custom SVGs. */
|
||||||
export const CONTENT_CATALOG_SLUG_ORDER = [
|
export const CONTENT_CATALOG_SLUG_ORDER = [
|
||||||
"resolving-active-conflicts",
|
"resolving-active-conflicts",
|
||||||
@@ -182,6 +195,21 @@ export const CONTENT_CATALOG_SLUG_ORDER = [
|
|||||||
"knowledge-management-and-institutional-amnesia",
|
"knowledge-management-and-institutional-amnesia",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export function resolveArticleOrnamentPath(
|
||||||
|
slug: string | undefined,
|
||||||
|
side: "right" | "left",
|
||||||
|
): string {
|
||||||
|
const hasCatalogArt =
|
||||||
|
!!slug &&
|
||||||
|
(CONTENT_CATALOG_SLUG_ORDER as readonly string[]).includes(slug);
|
||||||
|
if (hasCatalogArt && slug) {
|
||||||
|
return contentBlogOrnamentPath(slug, side);
|
||||||
|
}
|
||||||
|
return getAssetPath(
|
||||||
|
side === "right" ? ASSETS.CONTENT_SHAPE_1 : ASSETS.CONTENT_SHAPE_2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Pick a catalog article whose `public/content/blog/` SVGs can stand in as fallbacks. */
|
/** Pick a catalog article whose `public/content/blog/` SVGs can stand in as fallbacks. */
|
||||||
export function contentCatalogSlugForFallback(
|
export function contentCatalogSlugForFallback(
|
||||||
slug: string,
|
slug: string,
|
||||||
@@ -222,8 +250,11 @@ export const ASSETS = {
|
|||||||
/** Quote block default avatar. */
|
/** Quote block default avatar. */
|
||||||
QUOTE_AVATAR: "assets/marketing/quote-avatar.svg",
|
QUOTE_AVATAR: "assets/marketing/quote-avatar.svg",
|
||||||
|
|
||||||
/** Sections / Book cover (Figma **22137:891197**). */
|
/** Structure Before Crisis cover (Figma **22137:891197**; file name is historical). */
|
||||||
COMMUNITYRULES_COVER: "assets/marketing/communityrules-cover.svg",
|
STRUCTURE_BEFORE_CRISIS_COVER: "assets/marketing/communityrules-cover.svg",
|
||||||
|
|
||||||
|
/** Community Rules (2021) cover. */
|
||||||
|
COMMUNITY_RULES_COVER: "assets/marketing/community-rules-cover.png",
|
||||||
|
|
||||||
// Marketing
|
// Marketing
|
||||||
HERO_IMAGE: "assets/marketing/hero-image.png",
|
HERO_IMAGE: "assets/marketing/hero-image.png",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const SECTION_RECT =
|
||||||
|
/<rect\b[^>]*\bwidth="1920"[^>]*\bheight="672"[^>]*\bfill="(#[0-9A-Fa-f]{3,8})"/i;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First 1920×672 rect fill in a Figma Section banner SVG.
|
||||||
|
* Clip-path rects with fill="white" come later and are ignored.
|
||||||
|
*/
|
||||||
|
export function extractSectionBannerFill(svg: string): string | null {
|
||||||
|
const match = svg.match(SECTION_RECT);
|
||||||
|
return match?.[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionBannerDiskPath(slug: string, bannerFileName?: string): string {
|
||||||
|
return path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"public/content/blog",
|
||||||
|
bannerFileName ?? `${slug}-section.svg`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page background for an article: section banner fill when that SVG exists,
|
||||||
|
* otherwise the markdown `background.color`.
|
||||||
|
*/
|
||||||
|
export function resolveBlogPageBackgroundColor({
|
||||||
|
slug,
|
||||||
|
bannerFileName,
|
||||||
|
frontmatterColor,
|
||||||
|
}: {
|
||||||
|
slug: string;
|
||||||
|
bannerFileName?: string;
|
||||||
|
frontmatterColor?: string;
|
||||||
|
}): string | undefined {
|
||||||
|
try {
|
||||||
|
const svg = fs.readFileSync(
|
||||||
|
sectionBannerDiskPath(slug, bannerFileName),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const fill = extractSectionBannerFill(svg);
|
||||||
|
if (fill) {
|
||||||
|
return fill;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No section SVG yet — fall through to frontmatter.
|
||||||
|
}
|
||||||
|
|
||||||
|
return frontmatterColor;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user