diff --git a/.cursor/rules/coding-guidelines.mdc b/.cursor/rules/coding-guidelines.mdc index 3119783..74401cb 100644 --- a/.cursor/rules/coding-guidelines.mdc +++ b/.cursor/rules/coding-guidelines.mdc @@ -47,7 +47,15 @@ When your changes create orphans: 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//CR--…` (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.** diff --git a/.gitignore b/.gitignore index bf17aa0..33c81de 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,12 @@ npm-cache/ !public/favicon-32x32.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) !tests/e2e/visual-regression.spec.ts-snapshots/ !tests/e2e/visual-regression.spec.ts-snapshots/*.png diff --git a/AGENTS.md b/AGENTS.md index 766871d..b987047 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ priors — they reflect deliberate decisions. 6. **No new pathname-sniffing chrome.** Compose chrome via group/nested layouts, not `usePathname()` checks. (`ConditionalNavigation` is the sole tolerated exception — it carries SSR session state.) +7. **No Linear ticket IDs in comments, commits, or PR copy.** Branch names + still carry `CR-`. See `coding-guidelines.mdc`. ## Legacy / scaffolding diff --git a/app/(app)/create/CreateFlowLayoutClient.tsx b/app/(app)/create/CreateFlowLayoutClient.tsx index 2af41dd..7be3c32 100644 --- a/app/(app)/create/CreateFlowLayoutClient.tsx +++ b/app/(app)/create/CreateFlowLayoutClient.tsx @@ -70,6 +70,7 @@ import { CREATE_FLOW_FOOTER_BUTTON_CLASS, CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS, } from "./utils/createFlowFooterClassNames"; +import { CREATE_FLOW_MD_CENTERED_MAIN_CLASS } from "./components/createFlowLayoutTokens"; import { CUSTOM_RULE_CONFIRM_FOOTER_STEP_BY_STEP, methodCardFacetSectionForConfirmStep, @@ -83,6 +84,7 @@ import { SignedInDraftHydration } from "./SignedInDraftHydration"; import { CreateFlowPendingAvatarFlush } from "./components/CreateFlowPendingAvatarFlush"; import Alert from "../../components/modals/Alert"; import Create from "../../components/modals/Create"; +import { MODAL_FOOTER_START_SLOT_CLASS } from "../../components/modals/ModalFooter"; import Share from "../../components/modals/Share"; import { CreateFlowDraftSaveBannerProvider, @@ -467,13 +469,13 @@ function CreateFlowLayoutContent({ ); 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 ? "items-stretch overflow-y-auto md:overflow-hidden" : isSelectSplitScrollStep ? "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 - ? "items-start justify-center overflow-y-auto" + : isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep + ? CREATE_FLOW_MD_CENTERED_MAIN_CLASS : "items-start justify-center overflow-y-auto md:items-center"; const isTextStep = createFlowStepUsesCenteredTextLayout(currentStep); @@ -636,14 +638,16 @@ function CreateFlowLayoutContent({ nextButtonText={messages.create.topNav.leaveConfirmProceed} onNext={() => closeLeaveConfirm(true)} footerContent={ - +
+ +
} backdropVariant="blurredYellow" ariaLabel={messages.create.topNav.leaveConfirmTitle} diff --git a/app/(app)/create/components/ApplicableScopeField.tsx b/app/(app)/create/components/ApplicableScopeField.tsx index dff8c6b..bdb0635 100644 --- a/app/(app)/create/components/ApplicableScopeField.tsx +++ b/app/(app)/create/components/ApplicableScopeField.tsx @@ -70,7 +70,7 @@ function ApplicableScopeFieldComponent({ return (
- +
{scopes.map((scope) => { const isSelected = selectedScopes.includes(scope); @@ -84,6 +84,7 @@ function ApplicableScopeFieldComponent({ disabled={readOnly} onClick={() => !readOnly && onToggleScope(scope)} ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`} + className="max-w-full" /> ); })} diff --git a/app/(app)/create/components/CreateFlowLockupCardStepShell.tsx b/app/(app)/create/components/CreateFlowLockupCardStepShell.tsx index 14c05d1..b9cfc7a 100644 --- a/app/(app)/create/components/CreateFlowLockupCardStepShell.tsx +++ b/app/(app)/create/components/CreateFlowLockupCardStepShell.tsx @@ -4,6 +4,7 @@ import type { ReactNode } from "react"; import { CreateFlowHeaderLockup } from "./CreateFlowHeaderLockup"; import { CreateFlowStepShell } from "./CreateFlowStepShell"; import { + CREATE_FLOW_MD_CENTERED_SHELL_CLASS, CREATE_FLOW_MD_UP_GRID_CELL_CLASS, CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS, } from "./createFlowLayoutTokens"; @@ -18,14 +19,18 @@ type CreateFlowLockupCardStepShellProps = { 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({ lockupTitle, lockupDescription, children, }: CreateFlowLockupCardStepShellProps) { return ( - +
diff --git a/app/(app)/create/components/CustomMethodCardFieldBlocksSummary/CustomMethodCardFieldBlocksSummary.view.tsx b/app/(app)/create/components/CustomMethodCardFieldBlocksSummary/CustomMethodCardFieldBlocksSummary.view.tsx index bd9e7be..06230a6 100644 --- a/app/(app)/create/components/CustomMethodCardFieldBlocksSummary/CustomMethodCardFieldBlocksSummary.view.tsx +++ b/app/(app)/create/components/CustomMethodCardFieldBlocksSummary/CustomMethodCardFieldBlocksSummary.view.tsx @@ -60,7 +60,6 @@ function CustomMethodCardFieldBlocksSummaryViewComponent({
@@ -123,7 +122,6 @@ function CustomMethodCardFieldBlocksSummaryViewComponent({
@@ -169,6 +167,7 @@ function CustomMethodCardFieldBlocksSummaryViewComponent({ @@ -77,6 +76,7 @@ function CustomMethodCardUploadBlockRowViewComponent({ ) : ( diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx index fabdee3..4c5f8bd 100644 --- a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx @@ -5,8 +5,12 @@ import { useMessages, useTranslation, } from "../../../../contexts/MessagesContext"; +import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm"; 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 { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types"; import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view"; @@ -17,12 +21,13 @@ import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types * `20066:14748`, `20094:48551`, `20066:14361`). */ const CustomMethodCardWizardContainer = memo( - ({ isOpen, onClose, onFinalize, onPersistCustomUploadFile }) => { + ({ isOpen, onClose, onFinalize, onPersistCustomUploadFile, initialValues }) => { const m = useMessages(); const t = useTranslation("common"); const tUpload = useTranslation("create.upload"); const w = m.create.customRule.customMethodCardWizard; const menuCopy = m.create.customRule.modalKebabMenu; + const { requestConfirm, confirmDialog } = useAsyncConfirm(); const copy = useMemo( () => ({ @@ -68,6 +73,7 @@ const CustomMethodCardWizardContainer = memo( const [draftFieldBlocks, setDraftFieldBlocks] = useState< CustomMethodCardFieldBlock[] >([]); + const [editingBlockId, setEditingBlockId] = useState(null); const [textBlockTitle, setTextBlockTitle] = useState(""); const [textPlaceholderBody, setTextPlaceholderBody] = useState(""); @@ -88,6 +94,14 @@ const CustomMethodCardWizardContainer = memo( const [proportionDefault, setProportionDefault] = useState(50); const fileInputRef = useRef(null); + const initialValuesRef = useRef(initialValues); + initialValuesRef.current = initialValues; + const openSnapshotRef = useRef<{ + title: string; + description: string; + fieldBlocks: string; + } | null>(null); + const fieldModalSnapshotRef = useRef(null); const resetFieldTypeDrafts = useCallback(() => { setTextBlockTitle(""); @@ -112,21 +126,112 @@ const CustomMethodCardWizardContainer = memo( setPolicyDescription(""); setAddFieldExpanded(false); setFieldTypeModal(null); + setEditingBlockId(null); setDraftFieldBlocks([]); + openSnapshotRef.current = null; + fieldModalSnapshotRef.current = null; resetFieldTypeDrafts(); }, [resetFieldTypeDrafts]); useEffect(() => { if (!isOpen) { 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(() => { reset(); onClose(); }, [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 descriptionTrim = policyDescription.trim(); @@ -136,7 +241,7 @@ const CustomMethodCardWizardContainer = memo( titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS; const descriptionOk = 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 === 2) return descriptionOk; return titleOk && descriptionOk; @@ -213,7 +318,9 @@ const CustomMethodCardWizardContainer = memo( const shellDescription = fieldModalHeader?.description ?? headerDescription; const nextLabel = fieldTypeModal - ? copy.fieldModals.addField + ? editingBlockId + ? copy.fieldModals.saveField + : copy.fieldModals.addField : wizardStep === 3 ? copy.footerFinalize : t("buttons.next"); @@ -222,33 +329,125 @@ const CustomMethodCardWizardContainer = memo( ? !fieldModalStepValid : !stepValid; - const handleShellClose = useCallback(() => { + const handleShellClose = useCallback(async () => { if (fieldTypeModal) { + if ( + fieldModalSnapshotRef.current != null && + fieldModalDraftSignature() !== fieldModalSnapshotRef.current && + !(await confirmAbandonWizardEdits()) + ) { + return; + } setFieldTypeModal(null); + setEditingBlockId(null); + fieldModalSnapshotRef.current = null; + return; + } + if (!(await confirmAbandonWizardEdits())) { return; } dismiss(); - }, [dismiss, fieldTypeModal]); + }, [ + confirmAbandonWizardEdits, + dismiss, + fieldModalDraftSignature, + fieldTypeModal, + ]); const kebabMenuItems = useMemo(() => [], []); - const handleBack = useCallback(() => { + const handleBack = useCallback(async () => { if (fieldTypeModal) { + if ( + fieldModalSnapshotRef.current != null && + fieldModalDraftSignature() !== fieldModalSnapshotRef.current && + !(await confirmAbandonWizardEdits()) + ) { + return; + } setFieldTypeModal(null); + setEditingBlockId(null); + fieldModalSnapshotRef.current = null; return; } if (wizardStep === 1) { + if (!(await confirmAbandonWizardEdits())) { + return; + } dismiss(); return; } setWizardStep((s) => (s === 2 ? 1 : 2)); - }, [dismiss, fieldTypeModal, wizardStep]); + }, [ + confirmAbandonWizardEdits, + dismiss, + fieldModalDraftSignature, + fieldTypeModal, + wizardStep, + ]); const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => { + setEditingBlockId(null); + setAddFieldExpanded(false); resetFieldTypeDrafts(); setFieldTypeModal(ft); + fieldModalSnapshotRef.current = JSON.stringify({ + fieldTypeModal: ft, + textBlockTitle: "", + textPlaceholderBody: "", + badgeBlockTitle: "", + badgeOptions: [], + uploadBlockTitle: "", + uploadFileName: undefined, + uploadAssetUrl: undefined, + proportionBlockTitle: "", + proportionDefault: 50, + }); }, [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( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -285,9 +484,9 @@ const CustomMethodCardWizardContainer = memo( ); }, []); - const appendFieldBlock = useCallback(() => { + const commitFieldBlock = useCallback(() => { if (!fieldTypeModal || !fieldModalStepValid) return; - const id = crypto.randomUUID(); + const id = editingBlockId ?? crypto.randomUUID(); let block: CustomMethodCardFieldBlock; switch (fieldTypeModal) { case "text": @@ -325,11 +524,20 @@ const CustomMethodCardWizardContainer = memo( defaultPercent: proportionDefault, }; } - setDraftFieldBlocks((prev) => [...prev, block]); + setDraftFieldBlocks((prev) => { + if (editingBlockId) { + return prev.map((row) => (row.id === editingBlockId ? block : row)); + } + return [...prev, block]; + }); setFieldTypeModal(null); + setEditingBlockId(null); + setAddFieldExpanded(false); + fieldModalSnapshotRef.current = null; }, [ badgeBlockTitle, badgeOptions, + editingBlockId, fieldModalStepValid, fieldTypeModal, proportionBlockTitle, @@ -343,7 +551,7 @@ const CustomMethodCardWizardContainer = memo( const handleNext = useCallback(() => { if (fieldTypeModal) { - appendFieldBlock(); + commitFieldBlock(); return; } if (!stepValid) return; @@ -358,7 +566,7 @@ const CustomMethodCardWizardContainer = memo( } setWizardStep((s) => (s === 1 ? 2 : 3)); }, [ - appendFieldBlock, + commitFieldBlock, descriptionTrim, dismiss, draftFieldBlocks, @@ -370,6 +578,7 @@ const CustomMethodCardWizardContainer = memo( ]); return ( + <> ( policyDescription={policyDescription} addFieldExpanded={addFieldExpanded} 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} onPolicyDescriptionChange={setPolicyDescription} onPressAddCustomField={() => setAddFieldExpanded(true)} @@ -420,10 +630,13 @@ const CustomMethodCardWizardContainer = memo( stepper={!fieldTypeModal} draftFieldBlocks={draftFieldBlocks} onDraftFieldBlocksReorder={setDraftFieldBlocks} + onEditFieldBlock={handleEditFieldBlock} kebabMoreOptionsAriaLabel={menuCopy.triggerAriaLabel} kebabMenuAriaLabel={menuCopy.menuAriaLabel} kebabMenuItems={kebabMenuItems} /> + {confirmDialog} + ); }, ); diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts index ee618dc..b436136 100644 --- a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts @@ -2,6 +2,7 @@ import type { RefObject } from "react"; import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types"; import type { ModalHeaderMenuItem } from "../../../../components/modals/ModalHeader/ModalHeader.types"; import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; +import type { MethodCardWizardInitialValues } from "../../../../../lib/create/methodCardWizardPrefill"; export interface CustomMethodCardWizardFieldBodiesCopy { requiredHint: string; @@ -47,6 +48,7 @@ export interface CustomMethodCardWizardCopy { footerFinalize: string; fieldModals: { addField: string; + saveField: string; requiredHint: string; text: CustomMethodCardWizardFieldBodiesCopy["text"] & { title: string; @@ -70,6 +72,8 @@ export interface CustomMethodCardWizardCopy { export interface CustomMethodCardWizardProps { isOpen: boolean; 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. */ onFinalize: (payload: { title: string; @@ -123,7 +127,8 @@ export interface CustomMethodCardWizardViewProps { policyDescription: string; addFieldExpanded: boolean; copy: CustomMethodCardWizardCopy; - maxChars: number; + maxTitleChars: number; + maxDescriptionChars: number; onPolicyTitleChange: (v: string) => void; onPolicyDescriptionChange: (v: string) => void; onPressAddCustomField: () => void; @@ -136,6 +141,7 @@ export interface CustomMethodCardWizardViewProps { >; draftFieldBlocks: CustomMethodCardFieldBlock[]; onDraftFieldBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void; + onEditFieldBlock: (_block: CustomMethodCardFieldBlock) => void; nextDisabled: boolean; nextLabel: string; showBackButton: boolean; diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx index aeefb01..7d80080 100644 --- a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx @@ -19,7 +19,8 @@ function CustomMethodCardWizardViewComponent({ policyDescription, addFieldExpanded, copy, - maxChars, + maxTitleChars, + maxDescriptionChars, onPolicyTitleChange, onPolicyDescriptionChange, onPressAddCustomField, @@ -35,6 +36,7 @@ function CustomMethodCardWizardViewComponent({ stepper, draftFieldBlocks, onDraftFieldBlocksReorder, + onEditFieldBlock, kebabMoreOptionsAriaLabel, kebabMenuAriaLabel, kebabMenuItems, @@ -71,7 +73,7 @@ function CustomMethodCardWizardViewComponent({ placeholder={copy.step1.fieldPlaceholder} value={policyTitle} onChange={onPolicyTitleChange} - maxLength={maxChars} + maxLength={maxTitleChars} /> ) : null} {!fieldTypeModal && wizardStep === 2 ? ( @@ -80,15 +82,15 @@ function CustomMethodCardWizardViewComponent({ formHeader={false} placeholder={copy.step2.fieldPlaceholder} value={policyDescription} - maxLength={maxChars} + maxLength={maxDescriptionChars} onChange={(e) => onPolicyDescriptionChange(e.target.value)} - textHint={`${policyDescription.length}/${maxChars}`} + textHint={`${policyDescription.length}/${maxDescriptionChars}`} className="w-full" rows={4} /> ) : null} {!fieldTypeModal && wizardStep === 3 ? ( -
+
{draftFieldBlocks.length > 0 ? ( ) : null} (null); const [overIndex, setOverIndex] = useState(null); + const draggingIndexRef = useRef(null); + const dragFromHandleRef = useRef(false); const clearDragUi = useCallback(() => { + draggingIndexRef.current = null; + dragFromHandleRef.current = false; setDraggingIndex(null); setOverIndex(null); }, []); + const handleHandlePointerDown = useCallback(() => { + dragFromHandleRef.current = true; + }, []); + const handleDragStart = useCallback( (index: number) => (e: DragEvent) => { + if (!dragFromHandleRef.current) { + e.preventDefault(); + return; + } e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(index)); + draggingIndexRef.current = index; setDraggingIndex(index); }, [], @@ -40,8 +54,11 @@ function CustomMethodCardWizardBlocksListContainerComponent({ const handleDrop = useCallback( (index: number) => (e: DragEvent) => { e.preventDefault(); - const from = Number.parseInt(e.dataTransfer.getData("text/plain"), 10); - if (Number.isNaN(from)) { + const fromData = Number.parseInt(e.dataTransfer.getData("text/plain"), 10); + const from = Number.isNaN(fromData) + ? draggingIndexRef.current + : fromData; + if (from == null || Number.isNaN(from)) { clearDragUi(); return; } @@ -66,6 +83,8 @@ function CustomMethodCardWizardBlocksListContainerComponent({ onDragOver={handleDragOver} onDrop={handleDrop} onDragEnd={clearDragUi} + onHandlePointerDown={handleHandlePointerDown} + onEditBlock={onEditBlock} /> ); } diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.types.ts b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.types.ts index 8714348..87cf468 100644 --- a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.types.ts +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.types.ts @@ -8,6 +8,7 @@ export interface CustomMethodCardWizardBlocksListProps { dragHandleAriaLabel: string; listLabel: string; onBlocksReorder: (_next: CustomMethodCardFieldBlock[]) => void; + onEditBlock: (_block: CustomMethodCardFieldBlock) => void; } export interface CustomMethodCardWizardBlocksListViewProps @@ -18,4 +19,5 @@ export interface CustomMethodCardWizardBlocksListViewProps onDragOver: (_index: number) => (_e: DragEvent) => void; onDrop: (_index: number) => (_e: DragEvent) => void; onDragEnd: () => void; + onHandlePointerDown: () => void; } diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.view.tsx b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.view.tsx index d1554c7..ad7e875 100644 --- a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.view.tsx +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardBlocksList.view.tsx @@ -38,10 +38,12 @@ function CustomMethodCardWizardBlocksListViewComponent({ onDragOver, onDrop, onDragEnd, + onHandlePointerDown, + onEditBlock, }: CustomMethodCardWizardBlocksListViewProps) { return (
    {blocks.map((block, index) => { @@ -53,6 +55,7 @@ function CustomMethodCardWizardBlocksListViewComponent({ return (
  • - + +
+ ); })} diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardFieldBodies.view.tsx b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardFieldBodies.view.tsx index 279381c..1c471f5 100644 --- a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardFieldBodies.view.tsx +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardFieldBodies.view.tsx @@ -51,15 +51,11 @@ function CustomMethodCardWizardFieldBodiesViewComponent({ value={textBlockTitle} onChange={onTextBlockTitleChange} maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS} - showHelpIcon />
- +