@templatical/editor 0.27.1 → 0.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AiChatSidebar-iVJDzqjr.js → AiChatSidebar-inzMb2Vi.js} +1 -1
- package/dist/{AiFeatureMenu-DrLGc2H2.js → AiFeatureMenu-BTdgjwdx.js} +3 -3
- package/dist/{BlockIssueBadge-BHysq7XQ.js → BlockIssueBadge-a7jENlL4.js} +2 -2
- package/dist/{BlockPreviewCanvas-2WASf5A4.js → BlockPreviewCanvas-CMuRmhQg.js} +2 -2
- package/dist/{CloudHeaderExtras-DWApyc4P.js → CloudHeaderExtras-Bgr-Ox9M.js} +2 -2
- package/dist/{CloudPanels-DvOhGdRj.js → CloudPanels-oFNlTOO5.js} +3 -3
- package/dist/{CollaboratorBar-C6MC0UCk.js → CollaboratorBar-BHja900c.js} +2 -2
- package/dist/{ColorPicker-Di8L5TLQ.js → ColorPicker-ClUa_hMj.js} +3 -3
- package/dist/{CommentsPanel-D5wQB1Kf.js → CommentsPanel-CB1-poNb.js} +1 -1
- package/dist/{CommentsSidebar-BoTIxpLE.js → CommentsSidebar-B6mV8ft4.js} +2 -2
- package/dist/{CountdownBlock-BFX8iU90.js → CountdownBlock-qKtJVRIV.js} +1 -1
- package/dist/{CountdownToolbar-d9QM4m5x.js → CountdownToolbar-BpnKUXN_.js} +2 -2
- package/dist/{DesignReferenceSidebar-DKsx2TiL.js → DesignReferenceSidebar-BC5UxrJl.js} +2 -2
- package/dist/{IssuesPanel-flKKwKpM.js → IssuesPanel-BFczwZkL.js} +38 -37
- package/dist/{MergeTagModeToggle-PKCoKAoF.js → MergeTagModeToggle-koHNtEJe.js} +2 -2
- package/dist/{ParagraphEditor-iwTjkjtx.js → ParagraphEditor-cKosDi_l.js} +7 -7
- package/dist/{RestoreVersionDialog-DyMPGfsr.js → RestoreVersionDialog-DpZmcFuf.js} +2 -2
- package/dist/{RichTextEditorContent-BVXG9oF4.js → RichTextEditorContent-Br3WBNOs.js} +4 -4
- package/dist/{SaveBlockDialog-BMCAIfK9.js → SaveBlockDialog-C7mSGScN.js} +5 -5
- package/dist/{SavedBlocksBrowserModal-p5ma1qay.js → SavedBlocksBrowserModal-D7MYzuVP.js} +6 -6
- package/dist/{SavedBlocksPanels-BsiSstsZ.js → SavedBlocksPanels-D5T63WyR.js} +1 -1
- package/dist/{SavedBlocksPickBar-fU4Mjm-8.js → SavedBlocksPickBar-BluJS58M.js} +2 -2
- package/dist/{TemplateScoringPanel-Bv_bBsJi.js → TemplateScoringPanel-C0z27t2p.js} +2 -2
- package/dist/{TestEmailModal-EFYcrmTk.js → TestEmailModal-DoSIuHXe.js} +6 -6
- package/dist/{TestEmailPanel-ljriUqs1.js → TestEmailPanel-3Y6uUX3Z.js} +1 -1
- package/dist/{TitleEditor-BqRW7HTz.js → TitleEditor-D9IWtoH3.js} +6 -6
- package/dist/{TplModal-CTC9-w6B.js → TplModal-CyBCFn5s.js} +3 -3
- package/dist/{VersionHistoryMenu-Cq4rH56t.js → VersionHistoryMenu-8nd_aOnh.js} +1 -1
- package/dist/{VersionHistoryPanels-GkxkBiSi.js → VersionHistoryPanels-Cm7MxfrI.js} +1 -1
- package/dist/{VersionPreviewBanner-BdtTu9mI.js → VersionPreviewBanner-B9w773jz.js} +1 -1
- package/dist/{blockTypeIcons-CIrMr1SJ.js → blockTypeIcons-BRCuWioG.js} +1 -1
- package/dist/bundle-stats.json +7 -7
- package/dist/cdn/chunks/{AiChatSidebar-DTUoBH5a.js → AiChatSidebar-BIvQg9Q5.js} +2 -2
- package/dist/cdn/chunks/{AiChatSidebar-DTUoBH5a.js.map → AiChatSidebar-BIvQg9Q5.js.map} +1 -1
- package/dist/cdn/chunks/{AiFeatureMenu-BQz9Kczt.js → AiFeatureMenu-BMvcmvDZ.js} +4 -4
- package/dist/cdn/chunks/{AiFeatureMenu-BQz9Kczt.js.map → AiFeatureMenu-BMvcmvDZ.js.map} +1 -1
- package/dist/cdn/chunks/{BlockIssueBadge-UzSSOM_2.js → BlockIssueBadge-Drrmjqd7.js} +3 -3
- package/dist/cdn/chunks/{BlockIssueBadge-UzSSOM_2.js.map → BlockIssueBadge-Drrmjqd7.js.map} +1 -1
- package/dist/cdn/chunks/{BlockPreviewCanvas-40ocpXDD.js → BlockPreviewCanvas-CuWJLv8h.js} +3 -3
- package/dist/cdn/chunks/{BlockPreviewCanvas-40ocpXDD.js.map → BlockPreviewCanvas-CuWJLv8h.js.map} +1 -1
- package/dist/cdn/chunks/{CloudHeaderExtras-F_ySlW1S.js → CloudHeaderExtras-Ctat5U0M.js} +3 -3
- package/dist/cdn/chunks/{CloudHeaderExtras-F_ySlW1S.js.map → CloudHeaderExtras-Ctat5U0M.js.map} +1 -1
- package/dist/cdn/chunks/{CloudPanels-C34bcHpc.js → CloudPanels-YRYmXE9O.js} +4 -4
- package/dist/cdn/chunks/{CloudPanels-C34bcHpc.js.map → CloudPanels-YRYmXE9O.js.map} +1 -1
- package/dist/cdn/chunks/{CollaboratorBar-WBtrPPnM.js → CollaboratorBar-DgdxBCkN.js} +3 -3
- package/dist/cdn/chunks/{CollaboratorBar-WBtrPPnM.js.map → CollaboratorBar-DgdxBCkN.js.map} +1 -1
- package/dist/cdn/chunks/{ColorPicker-CxKYd_8N.js → ColorPicker-B-we7_bC.js} +4 -4
- package/dist/cdn/chunks/{ColorPicker-CxKYd_8N.js.map → ColorPicker-B-we7_bC.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsPanel-jzZ2X0P4.js → CommentsPanel-D7Y-74Nj.js} +2 -2
- package/dist/cdn/chunks/{CommentsPanel-jzZ2X0P4.js.map → CommentsPanel-D7Y-74Nj.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsSidebar-Dxke-vUP.js → CommentsSidebar-DxTobvYS.js} +3 -3
- package/dist/cdn/chunks/{CommentsSidebar-Dxke-vUP.js.map → CommentsSidebar-DxTobvYS.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownBlock-BYSrJWxg.js → CountdownBlock-BKrYGRiT.js} +2 -2
- package/dist/cdn/chunks/{CountdownBlock-BYSrJWxg.js.map → CountdownBlock-BKrYGRiT.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownToolbar-pVbkPNVo.js → CountdownToolbar-D8QWL-jK.js} +3 -3
- package/dist/cdn/chunks/{CountdownToolbar-pVbkPNVo.js.map → CountdownToolbar-D8QWL-jK.js.map} +1 -1
- package/dist/cdn/chunks/{DesignReferenceSidebar-sZWciECR.js → DesignReferenceSidebar-D9Lm4Al0.js} +3 -3
- package/dist/cdn/chunks/{DesignReferenceSidebar-sZWciECR.js.map → DesignReferenceSidebar-D9Lm4Al0.js.map} +1 -1
- package/dist/cdn/chunks/{IssuesPanel-CHtetM9i.js → IssuesPanel-Dz90c1C6.js} +36 -34
- package/dist/cdn/chunks/IssuesPanel-Dz90c1C6.js.map +1 -0
- package/dist/cdn/chunks/{MergeTagModeToggle-ZwW-5EeN.js → MergeTagModeToggle-CfFrfYNC.js} +3 -3
- package/dist/cdn/chunks/{MergeTagModeToggle-ZwW-5EeN.js.map → MergeTagModeToggle-CfFrfYNC.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-BBcIQQK2.js → ParagraphEditor-Be-BO5hD.js} +8 -8
- package/dist/cdn/chunks/{ParagraphEditor-BBcIQQK2.js.map → ParagraphEditor-Be-BO5hD.js.map} +1 -1
- package/dist/cdn/chunks/{RestoreVersionDialog-DNUZHFa_.js → RestoreVersionDialog-CKtz_FEz.js} +3 -3
- package/dist/cdn/chunks/{RestoreVersionDialog-DNUZHFa_.js.map → RestoreVersionDialog-CKtz_FEz.js.map} +1 -1
- package/dist/cdn/chunks/{RichTextEditorContent-Cl-acKAD.js → RichTextEditorContent-D3eWZ793.js} +5 -5
- package/dist/cdn/chunks/{RichTextEditorContent-Cl-acKAD.js.map → RichTextEditorContent-D3eWZ793.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-BXTZ729e.js → SaveBlockDialog-C5nT7Fxi.js} +6 -6
- package/dist/cdn/chunks/{SaveBlockDialog-BXTZ729e.js.map → SaveBlockDialog-C5nT7Fxi.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-BqGNBpRR.js → SavedBlocksBrowserModal-Cvyewimb.js} +6 -6
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-BqGNBpRR.js.map → SavedBlocksBrowserModal-Cvyewimb.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPanels-nbK0UQTp.js → SavedBlocksPanels-B7aBlxKW.js} +2 -2
- package/dist/cdn/chunks/SavedBlocksPanels-B7aBlxKW.js.map +1 -0
- package/dist/cdn/chunks/{SavedBlocksPickBar-B9mbw3Gk.js → SavedBlocksPickBar-D0opM9P9.js} +3 -3
- package/dist/cdn/chunks/{SavedBlocksPickBar-B9mbw3Gk.js.map → SavedBlocksPickBar-D0opM9P9.js.map} +1 -1
- package/dist/cdn/chunks/{TemplateScoringPanel-Xdkp2EMY.js → TemplateScoringPanel-DEsNZlHe.js} +3 -3
- package/dist/cdn/chunks/{TemplateScoringPanel-Xdkp2EMY.js.map → TemplateScoringPanel-DEsNZlHe.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailModal-DE6fi3he.js → TestEmailModal-BANpGbEP.js} +7 -7
- package/dist/cdn/chunks/{TestEmailModal-DE6fi3he.js.map → TestEmailModal-BANpGbEP.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailPanel-CAjQDXle.js → TestEmailPanel-DItZMP3B.js} +2 -2
- package/dist/cdn/chunks/TestEmailPanel-DItZMP3B.js.map +1 -0
- package/dist/cdn/chunks/{TitleEditor-BP0ewaWF.js → TitleEditor-D_NJ4Ww8.js} +7 -7
- package/dist/cdn/chunks/{TitleEditor-BP0ewaWF.js.map → TitleEditor-D_NJ4Ww8.js.map} +1 -1
- package/dist/cdn/chunks/{TplModal-C4kJduTr.js → TplModal-C51W8LTn.js} +4 -4
- package/dist/cdn/chunks/{TplModal-C4kJduTr.js.map → TplModal-C51W8LTn.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryMenu-_MAj3qZj.js → VersionHistoryMenu-Cv23A0uu.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryMenu-_MAj3qZj.js.map → VersionHistoryMenu-Cv23A0uu.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryPanels-DMq2PRee.js → VersionHistoryPanels-DHDN1owI.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryPanels-DMq2PRee.js.map → VersionHistoryPanels-DHDN1owI.js.map} +1 -1
- package/dist/cdn/chunks/{VersionPreviewBanner-04CL1SGY.js → VersionPreviewBanner-Dw4kFkrh.js} +2 -2
- package/dist/cdn/chunks/VersionPreviewBanner-Dw4kFkrh.js.map +1 -0
- package/dist/cdn/chunks/{createCloudRuntime-gXA8_QRU.js → createCloudRuntime-D75oJ9j-.js} +2 -2
- package/dist/cdn/chunks/{createCloudRuntime-gXA8_QRU.js.map → createCloudRuntime-D75oJ9j-.js.map} +1 -1
- package/dist/cdn/chunks/{extensions-CMcVw2uu.js → extensions-Byo2sA1r.js} +3 -3
- package/dist/cdn/chunks/{extensions-CMcVw2uu.js.map → extensions-Byo2sA1r.js.map} +1 -1
- package/dist/cdn/chunks/{keys-CbQuRX85.js → keys-oJdkdtPn.js} +2 -2
- package/dist/cdn/chunks/{keys-CbQuRX85.js.map → keys-oJdkdtPn.js.map} +1 -1
- package/dist/cdn/chunks/{src-Ct4Fsx3t.js → src-B_pCqXyM.js} +3 -3
- package/dist/cdn/chunks/src-B_pCqXyM.js.map +1 -0
- package/dist/cdn/chunks/{useCloudI18n-NVa1aeY8.js → useCloudI18n-Ch-SiRjU.js} +3 -3
- package/dist/cdn/chunks/{useCloudI18n-NVa1aeY8.js.map → useCloudI18n-Ch-SiRjU.js.map} +1 -1
- package/dist/cdn/chunks/{useEditorCore-DC8gmfs6.js → useEditorCore-BtEO1qms.js} +127 -134
- package/dist/cdn/chunks/useEditorCore-BtEO1qms.js.map +1 -0
- package/dist/cdn/chunks/useEditorRoot-Cxnmf5hM.js +10 -0
- package/dist/cdn/chunks/useEditorRoot-Cxnmf5hM.js.map +1 -0
- package/dist/cdn/chunks/{useI18n-DVWCJJFH.js → useI18n-4sUbTM3Y.js} +3 -3
- package/dist/cdn/chunks/useI18n-4sUbTM3Y.js.map +1 -0
- package/dist/cdn/chunks/{useMergeTag-BfZBRPQp.js → useMergeTag-DxjTEyin.js} +2 -2
- package/dist/cdn/chunks/{useMergeTag-BfZBRPQp.js.map → useMergeTag-DxjTEyin.js.map} +1 -1
- package/dist/cdn/chunks/{usePopoverPosition-BC-HA7TK.js → usePopoverPosition-C1N2kaiZ.js} +2 -2
- package/dist/cdn/chunks/{usePopoverPosition-BC-HA7TK.js.map → usePopoverPosition-C1N2kaiZ.js.map} +1 -1
- package/dist/cdn/chunks/useScrollToBlock-BvWAfWd8.js +22 -0
- package/dist/cdn/chunks/useScrollToBlock-BvWAfWd8.js.map +1 -0
- package/dist/cdn/editor.css +1 -1
- package/dist/cdn/editor.js +1108 -971
- package/dist/cdn/editor.js.map +1 -1
- package/dist/{createCloudRuntime-Bt8W-ajI.js → createCloudRuntime-FEZG-Gve.js} +1 -1
- package/dist/{extensions-CX87G4aL.js → extensions-BV9qfsYr.js} +2 -2
- package/dist/{keys-BxixS0TN.js → keys-CVpi8xCT.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/templatical-editor.js +1261 -1125
- package/dist/{useCloudI18n-CAZTZ9_a.js → useCloudI18n-DmWo4nzs.js} +2 -2
- package/dist/{useEditorCore-CFLzobo_.js → useEditorCore-DoKDh27o.js} +469 -476
- package/dist/useEditorRoot-DKyG95pb.js +8 -0
- package/dist/{useI18n-BfXff_Sy.js → useI18n-CvYvat9_.js} +2 -2
- package/dist/{useMergeTag-Dd4I7kge.js → useMergeTag-D6Z6cyDa.js} +1 -1
- package/dist/{usePopoverPosition-CxhCX3SQ.js → usePopoverPosition-WslxM_PU.js} +1 -1
- package/dist/useScrollToBlock-KA65jhQ_.js +44 -0
- package/package.json +8 -7
- package/dist/cdn/chunks/IssuesPanel-CHtetM9i.js.map +0 -1
- package/dist/cdn/chunks/SavedBlocksPanels-nbK0UQTp.js.map +0 -1
- package/dist/cdn/chunks/TestEmailPanel-CAjQDXle.js.map +0 -1
- package/dist/cdn/chunks/VersionPreviewBanner-04CL1SGY.js.map +0 -1
- package/dist/cdn/chunks/src-Ct4Fsx3t.js.map +0 -1
- package/dist/cdn/chunks/useEditorCore-DC8gmfs6.js.map +0 -1
- package/dist/cdn/chunks/useI18n-DVWCJJFH.js.map +0 -1
- package/dist/list-checks-jw-ZlvWY.js +0 -25
package/dist/cdn/chunks/{createCloudRuntime-gXA8_QRU.js.map → createCloudRuntime-D75oJ9j-.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createCloudRuntime-gXA8_QRU.js","names":[],"sources":["../../../src/cloud/composables/useCloudFeatureFlags.ts","../../../src/cloud/composables/useCloudMediaLibrary.ts","../../../src/cloud/composables/useCloudPanelState.ts","../../../src/cloud/composables/useCloudSaveGate.ts","../../../src/cloud/composables/useCollabUndoWarning.ts","../../../src/cloud/collectSavedBlockIds.ts","../../../src/cloud/createCloudRuntime.ts"],"sourcesContent":["import { computed, type ComputedRef } from \"vue\";\nimport type {\n UsePlanConfigReturn,\n UseAiConfigReturn,\n} from \"@templatical/core/cloud\";\n\nexport interface UseCloudFeatureFlagsOptions {\n planConfigInstance: UsePlanConfigReturn;\n aiConfig: UseAiConfigReturn;\n /** The loaded template's id, or `null` before one exists. */\n getTemplateId: () => string | null;\n}\n\nexport interface UseCloudFeatureFlagsReturn {\n canUseAiGeneration: ComputedRef<boolean>;\n canSendTestEmail: ComputedRef<boolean>;\n hasTemplateSaved: ComputedRef<boolean>;\n templateLimit: ComputedRef<number | null>;\n templateCount: ComputedRef<number>;\n}\n\n/**\n * Every entitlement check Cloud makes, in one place.\n *\n * The containment rule this exists for: entitlement checks go through here,\n * never inline in a component and never across a package boundary. With five\n * plan features left there is little excuse to break it.\n *\n * Save status is deliberately not here — it lives on `useTemplatesFeature`, which\n * the one shared header reads, and was never an entitlement. Footer branding is\n * likewise not gated: `config.branding !== false` decides it on any plan.\n */\nexport function useCloudFeatureFlags(\n options: UseCloudFeatureFlagsOptions,\n): UseCloudFeatureFlagsReturn {\n const { planConfigInstance, aiConfig, getTemplateId } = options;\n\n const canUseAiGeneration = computed(\n () =>\n planConfigInstance.hasFeature(\"ai_generation\") &&\n aiConfig.hasAnyMenuFeature.value,\n );\n const canSendTestEmail = computed(() =>\n planConfigInstance.hasFeature(\"test_email\"),\n );\n const hasTemplateSaved = computed(() => getTemplateId() !== null);\n const templateLimit = computed(\n () => planConfigInstance.config.value?.limits.max_templates ?? null,\n );\n const templateCount = computed(\n () => planConfigInstance.config.value?.template_count ?? 0,\n );\n\n return {\n canUseAiGeneration,\n canSendTestEmail,\n hasTemplateSaved,\n templateLimit,\n templateCount,\n };\n}\n","import { onScopeDispose, type Ref } from \"vue\";\nimport type {\n MediaCategory,\n MediaConfig,\n MediaItem,\n MediaRequestContext,\n} from \"@templatical/media-library\";\nimport type { AuthManager } from \"@templatical/core/cloud\";\nimport type { MediaResult } from \"@templatical/types\";\n\nexport interface UseCloudMediaLibraryOptions {\n onRequestMedia?: (context: MediaRequestContext) => Promise<MediaItem | null>;\n mediaLibraryOpen: Ref<boolean>;\n mediaLibraryAccept: Ref<MediaCategory[] | undefined>;\n /** Auth used by the built-in drag-and-drop upload (#229). */\n authManager: AuthManager;\n /**\n * Active plan's media config, read at upload time for a client-side\n * pre-check (MIME + size) before hitting the network. Returns `null` until\n * the plan config has loaded; the server stays the authoritative validator.\n */\n getMediaConfig?: () => MediaConfig | null;\n /** Surface upload/validation failures to the consumer's error handler. */\n onError?: (error: Error) => void;\n}\n\nexport interface UseCloudMediaLibraryReturn {\n handleRequestMedia: (\n context?: MediaRequestContext,\n ) => Promise<MediaResult | null>;\n handleMediaSelect: (item: MediaItem) => void;\n handleMediaLibraryClose: () => void;\n}\n\nexport function useCloudMediaLibrary(\n options: UseCloudMediaLibraryOptions,\n): UseCloudMediaLibraryReturn {\n const {\n onRequestMedia,\n mediaLibraryOpen,\n mediaLibraryAccept,\n authManager,\n getMediaConfig,\n onError,\n } = options;\n\n let mediaResolve: ((result: MediaResult | null) => void) | null = null;\n\n function toMediaResult(item: MediaItem): MediaResult {\n return { url: item.url, alt: item.alt_text || undefined };\n }\n\n async function handleRequestMedia(\n context?: MediaRequestContext,\n ): Promise<MediaResult | null> {\n const accept = context?.accept ?? [\"images\"];\n\n // Drag-and-drop upload path (#229). A dropped file is forwarded to a\n // consumer handler if one is configured; otherwise it's uploaded directly\n // to the cloud and the resulting media item becomes the block's image.\n if (context?.files?.length) {\n if (onRequestMedia) {\n const item = await onRequestMedia({ accept, files: context.files });\n return item ? toMediaResult(item) : null;\n }\n return uploadDroppedFile(context.files[0]);\n }\n\n // If consumer provides a custom media handler, use it.\n if (onRequestMedia) {\n const item = await onRequestMedia({ accept });\n return item ? toMediaResult(item) : null;\n }\n\n // Otherwise open the built-in media library. If a previous request\n // is still pending (e.g. consumer fired two requests back-to-back),\n // settle it with null so its caller doesn't hang forever.\n if (mediaResolve) {\n mediaResolve(null);\n mediaResolve = null;\n }\n mediaLibraryAccept.value = accept;\n mediaLibraryOpen.value = true;\n return new Promise<MediaResult | null>((resolve) => {\n mediaResolve = (result) => {\n resolve(result);\n };\n });\n }\n\n async function uploadDroppedFile(file: File): Promise<MediaResult | null> {\n const validationError = validateFile(file);\n if (validationError) {\n onError?.(validationError);\n return null;\n }\n try {\n // media-library is an optional, cloud-only peer — import lazily so OSS\n // bundles never pull it in. Cloud consumers always have it installed.\n const { MediaApiClient } = await import(\"@templatical/media-library\");\n const item = await new MediaApiClient(authManager).uploadMedia(file);\n return toMediaResult(item);\n } catch (error) {\n onError?.(error as Error);\n return null;\n }\n }\n\n function validateFile(file: File): Error | null {\n const media = getMediaConfig?.() ?? null;\n if (!media) return null; // config not loaded yet → defer to server.\n const acceptedTypes = media.categories?.images?.mime_types ?? [];\n if (acceptedTypes.length > 0 && !acceptedTypes.includes(file.type)) {\n return new Error(\n `Unsupported image type: ${file.type || \"unknown\"}. Accepted: ${acceptedTypes.join(\", \")}.`,\n );\n }\n if (media.max_file_size > 0 && file.size > media.max_file_size) {\n return new Error(\n `Image is too large (${file.size} bytes). Maximum allowed is ${media.max_file_size} bytes.`,\n );\n }\n return null;\n }\n\n function handleMediaSelect(item: MediaItem): void {\n mediaLibraryOpen.value = false;\n mediaResolve?.(toMediaResult(item));\n mediaResolve = null;\n }\n\n function handleMediaLibraryClose(): void {\n mediaLibraryOpen.value = false;\n mediaResolve?.(null);\n mediaResolve = null;\n }\n\n onScopeDispose(() => {\n if (mediaResolve) {\n mediaResolve(null);\n mediaResolve = null;\n }\n });\n\n return {\n handleRequestMedia,\n handleMediaSelect,\n handleMediaLibraryClose,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { onClickOutside } from \"@vueuse/core\";\nimport type { AiFeature } from \"../components/AiFeatureMenu.vue\";\nimport type { MediaCategory } from \"@templatical/media-library\";\n\nexport interface UseCloudPanelStateReturn {\n activePanel: Ref<RightPanel | null>;\n aiChatOpen: ComputedRef<boolean> & { value: boolean };\n scoringPanelOpen: ComputedRef<boolean> & { value: boolean };\n designReferenceOpen: ComputedRef<boolean> & { value: boolean };\n commentsOpen: ComputedRef<boolean> & { value: boolean };\n mediaLibraryOpen: Ref<boolean>;\n mediaLibraryAccept: Ref<MediaCategory[] | undefined>;\n aiMenuOpen: Ref<boolean>;\n aiMenuRef: Ref<HTMLElement | null>;\n rightPanelOpen: ComputedRef<boolean>;\n activeAiFeature: ComputedRef<AiFeature | null>;\n aiButtonActive: ComputedRef<boolean>;\n toggleAiMenu: () => void;\n handleAiFeatureSelect: (feature: AiFeature) => void;\n}\n\ntype RightPanel = \"ai-chat\" | \"scoring\" | \"design-reference\" | \"comments\";\n\nexport function useCloudPanelState(): UseCloudPanelStateReturn {\n const activePanel = ref<RightPanel | null>(null);\n\n const aiChatOpen = computed({\n get: () => activePanel.value === \"ai-chat\",\n set: (v) => (activePanel.value = v ? \"ai-chat\" : null),\n });\n const scoringPanelOpen = computed({\n get: () => activePanel.value === \"scoring\",\n set: (v) => (activePanel.value = v ? \"scoring\" : null),\n });\n const designReferenceOpen = computed({\n get: () => activePanel.value === \"design-reference\",\n set: (v) => (activePanel.value = v ? \"design-reference\" : null),\n });\n const commentsOpen = computed({\n get: () => activePanel.value === \"comments\",\n set: (v) => (activePanel.value = v ? \"comments\" : null),\n });\n\n const mediaLibraryOpen = ref(false);\n const mediaLibraryAccept = ref<MediaCategory[] | undefined>(undefined);\n const aiMenuOpen = ref(false);\n const aiMenuRef = ref<HTMLElement | null>(null);\n\n const rightPanelOpen = computed(() => activePanel.value !== null);\n\n const activeAiFeature = computed<AiFeature | null>(() => {\n const p = activePanel.value;\n if (p === \"ai-chat\" || p === \"design-reference\" || p === \"scoring\")\n return p;\n return null;\n });\n\n const aiButtonActive = computed(\n () =>\n aiMenuOpen.value ||\n activePanel.value === \"ai-chat\" ||\n activePanel.value === \"design-reference\" ||\n activePanel.value === \"scoring\",\n );\n\n function toggleAiMenu(): void {\n aiMenuOpen.value = !aiMenuOpen.value;\n }\n\n function handleAiFeatureSelect(feature: AiFeature): void {\n aiMenuOpen.value = false;\n activePanel.value = activePanel.value === feature ? null : feature;\n }\n\n onClickOutside(aiMenuRef, () => {\n aiMenuOpen.value = false;\n });\n\n return {\n activePanel,\n aiChatOpen,\n scoringPanelOpen,\n designReferenceOpen,\n commentsOpen,\n mediaLibraryOpen,\n mediaLibraryAccept,\n aiMenuOpen,\n aiMenuRef,\n rightPanelOpen,\n activeAiFeature,\n aiButtonActive,\n toggleAiMenu,\n handleAiFeatureSelect,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport type { PlanConfig } from \"@templatical/types\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\nexport interface UseCloudSaveGateOptions {\n /** Reactive accessibility issues from `useTemplateLint`. */\n issues: Ref<LintIssue[]>;\n /** Reactive plan config from the server. */\n planConfig: Ref<PlanConfig | null>;\n}\n\nexport interface UseCloudSaveGateReturn {\n /** True when the gate would block a save click given current state. */\n shouldBlock: ComputedRef<boolean>;\n /** Errors that would block the save. Empty when `shouldBlock` is false. */\n blockingIssues: ComputedRef<LintIssue[]>;\n /** Modal visibility — toggled by `tryRunSave` and `cancel`. */\n modalOpen: Ref<boolean>;\n /**\n * Wraps a save callback. Returns `true` when the save was invoked\n * (blocking disabled or no errors), `false` when the modal opened\n * instead. Caller can `await` the returned promise to know when the\n * save settled.\n */\n tryRunSave: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n /**\n * Runs the save only when the gate would not block, and does **nothing** when\n * it would — no modal, no save, nothing pending. Returns whether it ran.\n *\n * This is the autosave path. A modal on a debounce timer would interrupt\n * typing, but simply saving anyway would demote `blockOnError` from a server\n * policy to a manual-save-only speed bump. Skipping leaves the template dirty\n * and the header saying \"unsaved\", which is true, and the reason stays visible\n * in the Issues panel. The next edit re-arms the debounce, so fixing the issue\n * is what lets the save through — no retry timer needed.\n */\n runUnlessBlocked: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n /** Force-run the pending save, bypassing the gate. */\n confirmAndSave: () => Promise<void>;\n /** Dismiss the modal without saving. */\n cancel: () => void;\n}\n\n/**\n * Cloud-only save-gate. When `planConfig.accessibility.blockOnError === true`\n * and the linter reports any error-severity issues, intercept the save\n * click and surface a confirmation modal listing the blockers. The user\n * can dismiss to fix issues, or click \"Save anyway\" to bypass — the gate\n * does NOT enforce a hard block, it surfaces a deliberate choice.\n *\n * Server policy is the source of truth for `blockOnError`; consumer-side\n * `accessibility` options can't override it.\n */\nexport function useCloudSaveGate(\n opts: UseCloudSaveGateOptions,\n): UseCloudSaveGateReturn {\n const modalOpen = ref(false);\n let pending: (() => Promise<unknown> | unknown) | null = null;\n\n const blockOnError = computed(\n () => opts.planConfig.value?.accessibility?.blockOnError === true,\n );\n\n const blockingIssues = computed(() =>\n blockOnError.value\n ? opts.issues.value.filter((i) => i.severity === \"error\")\n : [],\n );\n\n const shouldBlock = computed(() => blockingIssues.value.length > 0);\n\n async function tryRunSave(\n run: () => Promise<unknown> | unknown,\n ): Promise<boolean> {\n if (!shouldBlock.value) {\n await run();\n return true;\n }\n pending = run;\n modalOpen.value = true;\n return false;\n }\n\n async function runUnlessBlocked(\n run: () => Promise<unknown> | unknown,\n ): Promise<boolean> {\n if (shouldBlock.value) return false;\n await run();\n return true;\n }\n\n async function confirmAndSave(): Promise<void> {\n const run = pending;\n pending = null;\n modalOpen.value = false;\n if (run) {\n await run();\n }\n }\n\n function cancel(): void {\n pending = null;\n modalOpen.value = false;\n }\n\n return {\n shouldBlock,\n blockingIssues,\n modalOpen,\n tryRunSave,\n runUnlessBlocked,\n confirmAndSave,\n cancel,\n };\n}\n","import { type ComputedRef, type Ref, ref } from \"vue\";\nimport { useTimeoutFn } from \"@vueuse/core\";\nimport { COLLAB_UNDO_WARNING_MS } from \"../../constants/timeouts\";\n\nexport interface UseCollabUndoWarningOptions {\n /** Whether collaboration is currently enabled (reactive). */\n isCollaborationEnabled: ComputedRef<boolean>;\n /** Returns the current list of collaborators. */\n getCollaboratorCount: () => number;\n /** Whether the history stack has entries to undo. */\n canUndo: ComputedRef<boolean>;\n}\n\nexport interface UseCollabUndoWarningReturn {\n collabUndoWarningVisible: Ref<boolean>;\n showCollabUndoWarning: () => void;\n}\n\nexport function useCollabUndoWarning(\n options: UseCollabUndoWarningOptions,\n): UseCollabUndoWarningReturn {\n const { isCollaborationEnabled, getCollaboratorCount, canUndo } = options;\n\n const collabUndoWarningFired = ref(false);\n const collabUndoWarningVisible = ref(false);\n\n const { start: startCollabUndoWarningTimeout } = useTimeoutFn(\n () => {\n collabUndoWarningVisible.value = false;\n },\n COLLAB_UNDO_WARNING_MS,\n { immediate: false },\n );\n\n function showCollabUndoWarning(): void {\n if (\n collabUndoWarningFired.value ||\n !isCollaborationEnabled.value ||\n getCollaboratorCount() === 0 ||\n !canUndo.value\n ) {\n return;\n }\n\n collabUndoWarningFired.value = true;\n collabUndoWarningVisible.value = true;\n startCollabUndoWarningTimeout();\n }\n\n return {\n collabUndoWarningVisible,\n showCollabUndoWarning,\n };\n}\n","/**\n * Just enough of a stored template to walk its ids — deliberately structural\n * rather than `Template`, because the caller reads `editor.state`, which is\n * `DeepReadonly` and so not assignable to the mutable contract type.\n */\ninterface SavedBlockIdSource {\n content?: {\n blocks?: readonly {\n id: string;\n type: string;\n children?: readonly (readonly { id: string }[])[];\n }[];\n };\n}\n\n/**\n * Every block id present in the **stored** template, sections' children\n * included.\n *\n * This is the last thing the deleted Cloud editor core carried over the OSS one\n * (`savedBlockIds`), and it never belonged there: its only reader gates a\n * comment filter on whether the block exists server-side, which makes it a\n * *comments* dependency. It travels with that feature accordingly, and reaches\n * `CommentsSidebar` through `capabilities.comments.isBlockSaved`.\n *\n * Derived on demand rather than memoised: `state.template` only changes on a\n * create / load / save round-trip, and the one caller runs on a user filtering\n * the comments panel.\n */\nexport function collectSavedBlockIds(\n template: SavedBlockIdSource | null | undefined,\n): Set<string> {\n const ids = new Set<string>();\n const blocks = template?.content?.blocks;\n if (!blocks) return ids;\n\n for (const block of blocks) {\n ids.add(block.id);\n if (block.type === \"section\" && block.children) {\n for (const column of block.children) {\n for (const child of column) {\n ids.add(child.id);\n }\n }\n }\n }\n return ids;\n}\n","import { computed, provide, ref, watch } from \"vue\";\nimport {\n AuthManager,\n isFatalAuthError,\n performHealthCheck,\n useAiConfig,\n useCollaboration,\n useCollaborationBroadcast,\n useExport,\n useMcpListener,\n usePlanConfig,\n resolveExportFonts,\n resolveWebSocketConfig,\n createCloudCommentsProvider,\n createCloudRenderProvider,\n createCloudSavedBlocksProvider,\n createCloudTemplatesProvider,\n createCloudTestEmailProvider,\n createCloudVersionHistoryProvider,\n useTemplateScoring,\n useTestEmail,\n useWebSocket,\n type RealtimeChannel,\n} from \"@templatical/core/cloud\";\nimport type { UseEditorReturn } from \"@templatical/core\";\nimport type {\n CommentsProvider,\n EditorUser,\n RenderProvider,\n SavedBlocksProvider,\n Template,\n TemplateContent,\n TemplatePatch,\n TemplatesProvider,\n TestEmailProvider,\n VersionHistoryProvider,\n} from \"@templatical/types\";\n\nimport type { CloudTranslations } from \"../i18n\";\nimport { preRenderCustomBlocks } from \"../utils/preRenderCustomBlocks\";\nimport { logger } from \"../utils/logger\";\nimport type { UseEditorCoreReturn } from \"../composables/useEditorCore\";\nimport {\n AI_CONFIG_KEY,\n AUTH_MANAGER_KEY,\n CLOUD_TRANSLATIONS_KEY,\n SCORING_KEY,\n} from \"../keys\";\n\nimport { useCloudFeatureFlags } from \"./composables/useCloudFeatureFlags\";\nimport {\n useCloudMediaLibrary,\n type UseCloudMediaLibraryReturn,\n} from \"./composables/useCloudMediaLibrary\";\nimport { useCloudPanelState } from \"./composables/useCloudPanelState\";\nimport {\n useCloudSaveGate,\n type UseCloudSaveGateReturn,\n} from \"./composables/useCloudSaveGate\";\nimport {\n useCollabUndoWarning,\n type UseCollabUndoWarningReturn,\n} from \"./composables/useCollabUndoWarning\";\nimport { collectSavedBlockIds } from \"./collectSavedBlockIds\";\nimport type {\n CloudAttachContext,\n CloudAttachment,\n CloudCollaborationInstance,\n CloudReady,\n CloudReadyContext,\n CloudRuntime,\n} from \"./runtime\";\nimport type { TemplaticalCloudEditorConfig } from \"./cloudConfig\";\n\nexport interface BootstrapCloudOptions {\n config: TemplaticalCloudEditorConfig;\n cloudTranslations: CloudTranslations;\n}\n\n/**\n * What {@link bootstrapCloud} hands back: a set of ordinary `init()` config\n * values, plus the runtime for the few things that must run inside the editor\n * component's `setup()`.\n */\nexport interface CloudBootstrap {\n runtime: CloudRuntime;\n providers: {\n templates: TemplatesProvider;\n render: RenderProvider;\n versionHistory: VersionHistoryProvider;\n /** Absent when `savedBlocks: false` — the feature is then off entirely. */\n savedBlocks?: SavedBlocksProvider;\n testEmail: TestEmailProvider;\n comments: CommentsProvider;\n };\n /**\n * Who is editing, from the JWT — `init({ user })`'s value for a Cloud session.\n *\n * `undefined` when the project's token carries no `user` claim, which makes\n * comments unavailable rather than anonymous. Not a key `initCloud()` accepts:\n * Cloud's comment writes are signed against this same claim, so a\n * consumer-supplied identity would only be able to disagree with it.\n */\n user?: EditorUser;\n}\n\n/**\n * Cloud's bootstrap and adapter wiring, in one place, run **before the editor\n * mounts**.\n *\n * This replaces `useCloudInitialization`, which owned a second `useEditor`, a\n * second `useEditorCore` and a post-mount async dance with its own loading and\n * error states. The dance is gone: auth, the health check and the plan fetch all\n * resolve here, and a failure rejects `initCloud()` rather than mounting an\n * editor that cannot work. What is left is adapter construction.\n *\n * Two things deliberately stay Cloud-only:\n *\n * - **Entitlements.** In an all-BYO world there are no plans, so `planConfig` is\n * Cloud's commercial layer above the provider set rather than a provider\n * itself. It is down to five booleans, each metering a resource Cloud buys.\n * - **Mid-session failure.** `init()` cannot fail after it mounts and should not\n * grow the ability, so the error overlay lives on this side of the seam.\n */\nexport async function bootstrapCloud(\n options: BootstrapCloudOptions,\n): Promise<CloudBootstrap> {\n const { config, cloudTranslations } = options;\n\n // --- Late-bound editor access ---------------------------------------------\n //\n // The providers below are built before there is an editor to read. Each\n // reaches it through these holders, filled by `attach()` / `ready()` — which is\n // what lets them be handed to `init()` as ordinary config keys.\n let editorRef: UseEditorReturn | null = null;\n let coreRef: UseEditorCoreReturn | null = null;\n let websocketRef: ReturnType<typeof useWebSocket> | null = null;\n let mediaLibRef: UseCloudMediaLibraryReturn | null = null;\n let collaboration: CloudCollaborationInstance | null = null;\n let collabWarning: UseCollabUndoWarningReturn | null = null;\n let saveGate: UseCloudSaveGateReturn | null = null;\n\n const sessionError = ref<Error | null>(null);\n const lockedBlocks = ref<Map<string, unknown>>(new Map());\n /**\n * Cloud's presence channel, forward-declared for the comments adapter's\n * `subscribe`. `useWebSocket` can only be built in `attach()` (it needs the\n * editor), while the provider is handed to `init()` before that — so the adapter\n * watches this and rebinds when the real channel lands.\n */\n const commentChannel = ref<RealtimeChannel | null>(null);\n\n function requireEditor(): UseEditorReturn {\n if (!editorRef) {\n throw new Error(\"[Templatical] The Cloud editor is not mounted yet.\");\n }\n return editorRef;\n }\n\n function getTemplateId(): string | null {\n return editorRef?.state.template?.id ?? null;\n }\n\n // --- Auth + plan ----------------------------------------------------------\n\n const authManager = new AuthManager({\n ...config.auth,\n // Only a *fatal* refusal raises the overlay. A 4xx means the endpoint\n // rejected the credentials, so every subsequent request fails the same way\n // and the session really is over. A network blip or a 5xx resolves on its\n // own, and blanking a mounted editor over one interrupts someone mid-edit on\n // a template that is probably unsaved — a worse outcome than the blip.\n //\n // The consumer hears about both: `onError` fires either way, and\n // `AuthManager` re-throws regardless, so a caller that cannot proceed still\n // finds out.\n onError: (error: Error) => {\n if (isFatalAuthError(error)) {\n sessionError.value = error;\n }\n config.onError?.(error);\n },\n });\n\n const planConfigInstance = usePlanConfig({\n authManager,\n onError: config.onError,\n });\n\n await authManager.initialize();\n\n const healthResult = await performHealthCheck({ authManager });\n if (!healthResult.api.ok) {\n throw new Error(\"Health check failed: API is not reachable\");\n }\n if (!healthResult.auth.ok) {\n throw new Error(\n `Health check failed: authentication error${healthResult.auth.error ? ` - ${healthResult.auth.error}` : \"\"}`,\n );\n }\n if (!healthResult.websocket.ok) {\n logger.warn(\n \"WebSocket health check failed:\",\n healthResult.websocket.error ?? \"unknown error\",\n \"-- real-time features will be disabled.\",\n );\n }\n\n await planConfigInstance.fetchConfig();\n\n // The bootstrap itself is what the overlay would have covered, and it just\n // succeeded — so anything `onError` recorded on the way here is stale.\n sessionError.value = null;\n\n // --- Rejected consumer keys ----------------------------------------------\n //\n // None is declared on `TemplaticalCloudEditorConfig`, so these only fire for\n // JavaScript consumers — warned rather than silently dropped, because the\n // symptom (a store that stays empty while Cloud's fills up) reads as a broken\n // provider rather than a rejected one.\n if ((config as { templates?: unknown }).templates) {\n logger.warn(\n \"initCloud does not accept a `templates` provider — the template id is \" +\n \"the join key for collaboration, version history, comments, AI rewrite, \" +\n \"scoring and the server-side export, so an id Cloud never issued would \" +\n \"degrade all six. The supplied provider is ignored. Use init() to bring \" +\n \"your own.\",\n );\n }\n if ((config as { versionHistory?: unknown }).versionHistory) {\n logger.warn(\n \"initCloud does not accept a `versionHistory` provider — a version is \" +\n \"keyed to a template id Cloud issued, so Cloud owns its history. The \" +\n \"supplied provider is ignored. Use init() to bring your own.\",\n );\n }\n if ((config as { comments?: unknown }).comments) {\n logger.warn(\n \"initCloud does not accept a `comments` provider — a comment is keyed to \" +\n \"a template id Cloud issued, and its author is signed by the auth token, \" +\n \"so Cloud owns the conversation. The supplied provider is ignored. Use \" +\n \"init() to bring your own.\",\n );\n }\n // Rejected for a different reason than the three above: not the join key, but\n // because Cloud renders on its own for test email, scheduled sends and API\n // exports (`createCloudTestEmailProvider` calls `exportHtml` directly). A\n // consumer provider would have changed `toMjml()` / `toHtml()` and nothing\n // else, so what the user previewed and exported was not what Cloud delivered —\n // and Cloud's output is a deliberate superset (countdown GIFs, composited video\n // thumbnails), so the consumer's was also worse for those blocks.\n if ((config as { render?: unknown }).render) {\n logger.warn(\n \"initCloud does not accept a `render` provider — Cloud renders \" +\n \"server-side for test email, sends and exports, so a supplied renderer \" +\n \"would change only what you preview and export, never what Cloud \" +\n \"delivers. The supplied provider is ignored. For your own MJML, call \" +\n \"renderToMjml(editor.getContent()) from @templatical/renderer.\",\n );\n }\n\n // --- Providers ------------------------------------------------------------\n\n function connectRealtime(template: Template): void {\n const websocketConfig = planConfigInstance.config.value?.websocket;\n if (!websocketRef || !websocketConfig) return;\n websocketRef.connect(template.id, resolveWebSocketConfig(websocketConfig));\n }\n\n const baseTemplates = createCloudTemplatesProvider(authManager);\n const baseCreate = baseTemplates.create;\n const baseSave = baseTemplates.save;\n\n /**\n * Cloud's templates adapter plus the choreography the template id anchors: the\n * websocket joins the template's presence channel, and the consumer's\n * `onCreate` / `onLoad` fire.\n *\n * That choreography belongs in the adapter rather than in an editor-side\n * lifecycle composable, because it *is* adapter business — Cloud is what keys\n * realtime off the id it issued. `useCloudLifecycle` existed only to hold it.\n *\n * `save` pre-renders custom blocks, which is why it is wrapped too.\n */\n const templates: TemplatesProvider = {\n load: async (id: string) => {\n const template = await baseTemplates.load(id);\n connectRealtime(template);\n config.onLoad?.(template);\n return template;\n },\n create:\n typeof baseCreate === \"function\"\n ? async (input: { name?: string; content: TemplateContent }) => {\n const template = await baseCreate(input);\n connectRealtime(template);\n config.onCreate?.(template);\n return template;\n }\n : false,\n save:\n typeof baseSave === \"function\"\n ? async (id: string, patch: TemplatePatch) => {\n if (coreRef && editorRef) {\n await preRenderCustomBlocks(\n editorRef.content.value,\n coreRef.registry,\n );\n }\n return baseSave(id, patch);\n }\n : false,\n };\n\n // Always Cloud's — see the rejected-keys block above. Cloud renders\n // independently for delivery, so a second renderer could only disagree with it.\n const render: RenderProvider = createCloudRenderProvider({\n authManager,\n getTemplateId,\n // Plain `editor.save()`: the templates adapter below already pre-renders\n // custom blocks on every save, which is exactly what the render endpoint\n // needs, since it exports from the stored copy rather than from\n // `payload.content`.\n save: () => requireEditor().save(),\n });\n\n const versionHistory = createCloudVersionHistoryProvider(authManager);\n\n // Realtime is the adapter's own business: `subscribe` binds the presence channel\n // above, so `useCommentListener` in `@templatical/core` never learns that Pusher\n // exists and a BYO provider with an SSE stream slots into the same seam.\n const comments = createCloudCommentsProvider({\n authManager,\n channel: commentChannel,\n getSocketId: () => websocketRef?.getSocketId() ?? null,\n });\n\n // From the JWT, not from config. Cloud signs comment writes against this same\n // claim, so a consumer-supplied identity could only disagree with it — and a\n // token without the claim leaves comments unavailable rather than anonymous.\n const cloudUser: EditorUser | undefined = authManager.userConfig\n ? { id: authManager.userConfig.id, name: authManager.userConfig.name }\n : undefined;\n\n // A consumer may pass their own store instead of a boolean, in which case it\n // replaces Cloud's. That path is deliberately NOT plan-gated: `saved_modules`\n // licenses Cloud's *storage*, and someone else's backend isn't Cloud's to sell.\n const consumerSavedBlocks =\n typeof config.savedBlocks === \"object\" && config.savedBlocks !== null\n ? config.savedBlocks\n : null;\n const savedBlocks: SavedBlocksProvider | undefined =\n config.savedBlocks === false\n ? undefined\n : (consumerSavedBlocks ?? createCloudSavedBlocksProvider(authManager));\n\n // Cloud's test-email *config* — plan enablement plus the signed allowlist, both\n // carried by the JWT `authManager.initialize()` already resolved above. Hence\n // the constant `isAuthReady`: there is no window where it is false.\n const testEmailConfig = useTestEmail({\n authManager,\n isAuthReady: ref(true),\n });\n\n // Cloud's own send path renders through the export endpoint directly: the\n // adapter derives HTML from the *saved* template and applies\n // `onBeforeTestEmail` to it, which is a narrower job than the render contract.\n const exporter = useExport({ authManager });\n\n const consumerTestEmail = config.testEmail ?? null;\n const testEmail: TestEmailProvider =\n consumerTestEmail ??\n createCloudTestEmailProvider({\n authManager,\n getTemplateId,\n save: () => requireEditor().save(),\n exportHtml: (templateId: string) =>\n exporter.exportHtml(templateId, resolveExportFonts(config.fonts)),\n allowedEmails: testEmailConfig.allowedEmails,\n getSignature: testEmailConfig.getSignature,\n onBeforeTestEmail: config.onBeforeTestEmail,\n });\n\n // --- Setup-time wiring ----------------------------------------------------\n\n const aiConfig = useAiConfig(config.ai);\n const scoringInstance = useTemplateScoring({ authManager, getTemplateId });\n\n const featureFlags = useCloudFeatureFlags({\n planConfigInstance,\n aiConfig,\n getTemplateId,\n });\n\n const isCollaborationEnabled = computed(\n () =>\n !!config.collaboration?.enabled &&\n planConfigInstance.hasFeature(\"collaboration\"),\n );\n\n function attach(context: CloudAttachContext): CloudAttachment {\n const editor = context.editor;\n editorRef = editor;\n\n provide(CLOUD_TRANSLATIONS_KEY, cloudTranslations);\n provide(AUTH_MANAGER_KEY, authManager);\n provide(AI_CONFIG_KEY, aiConfig);\n provide(SCORING_KEY, scoringInstance);\n\n const websocket = useWebSocket({ authManager, onError: config.onError });\n websocketRef = websocket;\n\n // Feeds the comments adapter's `subscribe`, which was handed to `init()` before\n // this socket existed.\n watch(\n websocket.channel,\n (channel) => {\n commentChannel.value = channel;\n },\n { immediate: true },\n );\n\n if (config.mcp?.enabled) {\n useMcpListener({\n editor,\n channel: websocket.channel,\n onOperation: config.mcp.onOperation,\n });\n }\n\n // Order matters: the broadcast wrapper replaces the editor's mutators so they\n // publish to peers, and `useEditorCore`'s history interceptor then wraps them\n // again to push history entries. Reversing it would push history for remote\n // operations and drift the two peers apart.\n if (config.collaboration?.enabled) {\n collaboration = useCollaboration({\n authManager,\n editor,\n channel: websocket.channel,\n onError: config.onError,\n onCollaboratorJoined: config.collaboration.onCollaboratorJoined,\n onCollaboratorLeft: config.collaboration.onCollaboratorLeft,\n onBlockLocked: config.collaboration.onBlockLocked,\n onBlockUnlocked: config.collaboration.onBlockUnlocked,\n }) as CloudCollaborationInstance;\n\n watch(\n () => collaboration!.lockedBlocks.value,\n (next) => {\n lockedBlocks.value = next;\n },\n { immediate: true },\n );\n\n useCollaborationBroadcast(editor, collaboration);\n }\n\n const panelState = useCloudPanelState();\n\n const mediaLib = useCloudMediaLibrary({\n onRequestMedia: config.onRequestMedia,\n mediaLibraryOpen: panelState.mediaLibraryOpen,\n mediaLibraryAccept: panelState.mediaLibraryAccept,\n authManager,\n getMediaConfig: () => planConfigInstance.config.value?.media ?? null,\n onError: config.onError,\n });\n mediaLibRef = mediaLib;\n\n return {\n websocket,\n panelState,\n mediaLib,\n // Handed to `MediaLibraryModal` as props by `CloudPanels`. A bare-string\n // injection would resolve to `undefined` silently; see\n // `CloudMediaBrowserContext`.\n mediaBrowser: {\n authManager,\n projectId: authManager.projectId,\n planConfig: planConfigInstance,\n },\n featureFlags,\n collaboration,\n isCollaborationEnabled,\n };\n }\n\n function ready(context: CloudReadyContext): CloudReady {\n const { core, capabilities } = context;\n coreRef = core;\n\n saveGate = useCloudSaveGate({\n issues: core.templateLint ? core.templateLint.issues : ref([]),\n planConfig: planConfigInstance.config,\n });\n\n collabWarning = useCollabUndoWarning({\n isCollaborationEnabled,\n getCollaboratorCount: () =>\n collaboration?.collaborators.value.length ?? 0,\n canUndo: core.history.canUndo,\n });\n\n // Mutated in place rather than re-provided: `useEditorCore` already provided\n // this exact object, so components injecting it see Cloud's entries on their\n // first render. Same pattern `versionHistory` uses in `Editor.vue`.\n //\n // `capabilities.comments` is *not* set here any more: comments became a shared\n // provider-backed feature, so `Editor.vue` builds the capability from\n // `useCommentsFeature` exactly as it does for saved blocks and test email, and\n // Cloud contributes only the two predicates below.\n capabilities.plan = planConfigInstance;\n capabilities.ai = aiConfig;\n\n // Registered unconditionally. `custom_blocks` and `theme_customization` used\n // to gate these — entitlements on editor capability the free editor grants,\n // and the only two things `applyPlanGates` would ever have had to do.\n if (config.customBlocks?.length) {\n core.registerCustomBlocks(config.customBlocks);\n }\n if (config.theme) {\n core.themeOverrides.value = config.theme;\n }\n\n return {\n saveGate,\n collabWarning,\n sessionError,\n retry: async () => {\n try {\n await authManager.initialize();\n sessionError.value = null;\n } catch (error) {\n sessionError.value =\n error instanceof Error ? error : new Error(\"Authentication failed\");\n }\n },\n };\n }\n\n const runtime: CloudRuntime = {\n lockedBlocks,\n onRequestMedia: (context) =>\n mediaLibRef\n ? mediaLibRef.handleRequestMedia(context)\n : Promise.resolve(null),\n isSavedBlocksAvailable: consumerSavedBlocks\n ? () => true\n : () =>\n config.savedBlocks !== false &&\n planConfigInstance.hasFeature(\"saved_modules\"),\n isTestEmailAvailable: consumerTestEmail\n ? // Their sender, their rules — the `test_email` plan feature licenses\n // Cloud's sending, and \"the template must be saved\" is a constraint of\n // Cloud's server-side render, not of an arbitrary backend.\n () => true\n : // All three matter and none implies another: the plan may grant the\n // feature while the project's token omits a recipient config, and Cloud\n // renders from the *saved* template so there must be one.\n () =>\n testEmailConfig.isEnabled.value &&\n featureFlags.canSendTestEmail.value &&\n featureFlags.hasTemplateSaved.value,\n // Three conditions, none implying another: the consumer may switch the feature\n // off, the plan meters comment storage and its realtime fan-out, and Cloud\n // anchors a comment to the *saved* template. (Whether a `user` exists is the\n // shared feature's own gate, not Cloud's.)\n isCommentsAvailable: () =>\n config.commenting !== false &&\n planConfigInstance.hasFeature(\"commenting\") &&\n featureFlags.hasTemplateSaved.value,\n // A comment is anchored server-side, so a block that exists only on the canvas\n // has nothing stored to show. This is the last thing the deleted cloud editor\n // core carried (`savedBlockIds`), and it was always a comments concern rather\n // than an editor one.\n isBlockSaved: (blockId: string) =>\n collectSavedBlockIds(editorRef?.state.template ?? null).has(blockId),\n getSaveGate: () => saveGate,\n isRemoteOperation: () =>\n collaboration?._isProcessingRemoteOperation() ?? false,\n onBeforeUndo: () => collabWarning?.showCollabUndoWarning(),\n attach,\n ready,\n destroy() {\n websocketRef?.disconnect();\n config.onUnmount?.();\n },\n };\n\n return {\n runtime,\n providers: {\n templates,\n render,\n versionHistory,\n ...(savedBlocks ? { savedBlocks } : {}),\n testEmail,\n comments,\n },\n user: cloudUser,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createCloudRuntime-D75oJ9j-.js","names":[],"sources":["../../../src/cloud/composables/useCloudFeatureFlags.ts","../../../src/cloud/composables/useCloudMediaLibrary.ts","../../../src/cloud/composables/useCloudPanelState.ts","../../../src/cloud/composables/useCloudSaveGate.ts","../../../src/cloud/composables/useCollabUndoWarning.ts","../../../src/cloud/collectSavedBlockIds.ts","../../../src/cloud/createCloudRuntime.ts"],"sourcesContent":["import { computed, type ComputedRef } from \"vue\";\nimport type {\n UsePlanConfigReturn,\n UseAiConfigReturn,\n} from \"@templatical/core/cloud\";\n\nexport interface UseCloudFeatureFlagsOptions {\n planConfigInstance: UsePlanConfigReturn;\n aiConfig: UseAiConfigReturn;\n /** The loaded template's id, or `null` before one exists. */\n getTemplateId: () => string | null;\n}\n\nexport interface UseCloudFeatureFlagsReturn {\n canUseAiGeneration: ComputedRef<boolean>;\n canSendTestEmail: ComputedRef<boolean>;\n hasTemplateSaved: ComputedRef<boolean>;\n templateLimit: ComputedRef<number | null>;\n templateCount: ComputedRef<number>;\n}\n\n/**\n * Every entitlement check Cloud makes, in one place.\n *\n * The containment rule this exists for: entitlement checks go through here,\n * never inline in a component and never across a package boundary. With five\n * plan features left there is little excuse to break it.\n *\n * Save status is deliberately not here — it lives on `useTemplatesFeature`, which\n * the one shared header reads, and was never an entitlement. Footer branding is\n * likewise not gated: `config.branding !== false` decides it on any plan.\n */\nexport function useCloudFeatureFlags(\n options: UseCloudFeatureFlagsOptions,\n): UseCloudFeatureFlagsReturn {\n const { planConfigInstance, aiConfig, getTemplateId } = options;\n\n const canUseAiGeneration = computed(\n () =>\n planConfigInstance.hasFeature(\"ai_generation\") &&\n aiConfig.hasAnyMenuFeature.value,\n );\n const canSendTestEmail = computed(() =>\n planConfigInstance.hasFeature(\"test_email\"),\n );\n const hasTemplateSaved = computed(() => getTemplateId() !== null);\n const templateLimit = computed(\n () => planConfigInstance.config.value?.limits.max_templates ?? null,\n );\n const templateCount = computed(\n () => planConfigInstance.config.value?.template_count ?? 0,\n );\n\n return {\n canUseAiGeneration,\n canSendTestEmail,\n hasTemplateSaved,\n templateLimit,\n templateCount,\n };\n}\n","import { onScopeDispose, type Ref } from \"vue\";\nimport type {\n MediaCategory,\n MediaConfig,\n MediaItem,\n MediaRequestContext,\n} from \"@templatical/media-library\";\nimport type { AuthManager } from \"@templatical/core/cloud\";\nimport type { MediaResult } from \"@templatical/types\";\n\nexport interface UseCloudMediaLibraryOptions {\n onRequestMedia?: (context: MediaRequestContext) => Promise<MediaItem | null>;\n mediaLibraryOpen: Ref<boolean>;\n mediaLibraryAccept: Ref<MediaCategory[] | undefined>;\n /** Auth used by the built-in drag-and-drop upload (#229). */\n authManager: AuthManager;\n /**\n * Active plan's media config, read at upload time for a client-side\n * pre-check (MIME + size) before hitting the network. Returns `null` until\n * the plan config has loaded; the server stays the authoritative validator.\n */\n getMediaConfig?: () => MediaConfig | null;\n /** Surface upload/validation failures to the consumer's error handler. */\n onError?: (error: Error) => void;\n}\n\nexport interface UseCloudMediaLibraryReturn {\n handleRequestMedia: (\n context?: MediaRequestContext,\n ) => Promise<MediaResult | null>;\n handleMediaSelect: (item: MediaItem) => void;\n handleMediaLibraryClose: () => void;\n}\n\nexport function useCloudMediaLibrary(\n options: UseCloudMediaLibraryOptions,\n): UseCloudMediaLibraryReturn {\n const {\n onRequestMedia,\n mediaLibraryOpen,\n mediaLibraryAccept,\n authManager,\n getMediaConfig,\n onError,\n } = options;\n\n let mediaResolve: ((result: MediaResult | null) => void) | null = null;\n\n function toMediaResult(item: MediaItem): MediaResult {\n return { url: item.url, alt: item.alt_text || undefined };\n }\n\n async function handleRequestMedia(\n context?: MediaRequestContext,\n ): Promise<MediaResult | null> {\n const accept = context?.accept ?? [\"images\"];\n\n // Drag-and-drop upload path (#229). A dropped file is forwarded to a\n // consumer handler if one is configured; otherwise it's uploaded directly\n // to the cloud and the resulting media item becomes the block's image.\n if (context?.files?.length) {\n if (onRequestMedia) {\n const item = await onRequestMedia({ accept, files: context.files });\n return item ? toMediaResult(item) : null;\n }\n return uploadDroppedFile(context.files[0]);\n }\n\n // If consumer provides a custom media handler, use it.\n if (onRequestMedia) {\n const item = await onRequestMedia({ accept });\n return item ? toMediaResult(item) : null;\n }\n\n // Otherwise open the built-in media library. If a previous request\n // is still pending (e.g. consumer fired two requests back-to-back),\n // settle it with null so its caller doesn't hang forever.\n if (mediaResolve) {\n mediaResolve(null);\n mediaResolve = null;\n }\n mediaLibraryAccept.value = accept;\n mediaLibraryOpen.value = true;\n return new Promise<MediaResult | null>((resolve) => {\n mediaResolve = (result) => {\n resolve(result);\n };\n });\n }\n\n async function uploadDroppedFile(file: File): Promise<MediaResult | null> {\n const validationError = validateFile(file);\n if (validationError) {\n onError?.(validationError);\n return null;\n }\n try {\n // media-library is an optional, cloud-only peer — import lazily so OSS\n // bundles never pull it in. Cloud consumers always have it installed.\n const { MediaApiClient } = await import(\"@templatical/media-library\");\n const item = await new MediaApiClient(authManager).uploadMedia(file);\n return toMediaResult(item);\n } catch (error) {\n onError?.(error as Error);\n return null;\n }\n }\n\n function validateFile(file: File): Error | null {\n const media = getMediaConfig?.() ?? null;\n if (!media) return null; // config not loaded yet → defer to server.\n const acceptedTypes = media.categories?.images?.mime_types ?? [];\n if (acceptedTypes.length > 0 && !acceptedTypes.includes(file.type)) {\n return new Error(\n `Unsupported image type: ${file.type || \"unknown\"}. Accepted: ${acceptedTypes.join(\", \")}.`,\n );\n }\n if (media.max_file_size > 0 && file.size > media.max_file_size) {\n return new Error(\n `Image is too large (${file.size} bytes). Maximum allowed is ${media.max_file_size} bytes.`,\n );\n }\n return null;\n }\n\n function handleMediaSelect(item: MediaItem): void {\n mediaLibraryOpen.value = false;\n mediaResolve?.(toMediaResult(item));\n mediaResolve = null;\n }\n\n function handleMediaLibraryClose(): void {\n mediaLibraryOpen.value = false;\n mediaResolve?.(null);\n mediaResolve = null;\n }\n\n onScopeDispose(() => {\n if (mediaResolve) {\n mediaResolve(null);\n mediaResolve = null;\n }\n });\n\n return {\n handleRequestMedia,\n handleMediaSelect,\n handleMediaLibraryClose,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { onClickOutside } from \"@vueuse/core\";\nimport type { AiFeature } from \"../components/AiFeatureMenu.vue\";\nimport type { MediaCategory } from \"@templatical/media-library\";\n\nexport interface UseCloudPanelStateReturn {\n activePanel: Ref<RightPanel | null>;\n aiChatOpen: ComputedRef<boolean> & { value: boolean };\n scoringPanelOpen: ComputedRef<boolean> & { value: boolean };\n designReferenceOpen: ComputedRef<boolean> & { value: boolean };\n commentsOpen: ComputedRef<boolean> & { value: boolean };\n mediaLibraryOpen: Ref<boolean>;\n mediaLibraryAccept: Ref<MediaCategory[] | undefined>;\n aiMenuOpen: Ref<boolean>;\n aiMenuRef: Ref<HTMLElement | null>;\n rightPanelOpen: ComputedRef<boolean>;\n activeAiFeature: ComputedRef<AiFeature | null>;\n aiButtonActive: ComputedRef<boolean>;\n toggleAiMenu: () => void;\n handleAiFeatureSelect: (feature: AiFeature) => void;\n}\n\ntype RightPanel = \"ai-chat\" | \"scoring\" | \"design-reference\" | \"comments\";\n\nexport function useCloudPanelState(): UseCloudPanelStateReturn {\n const activePanel = ref<RightPanel | null>(null);\n\n const aiChatOpen = computed({\n get: () => activePanel.value === \"ai-chat\",\n set: (v) => (activePanel.value = v ? \"ai-chat\" : null),\n });\n const scoringPanelOpen = computed({\n get: () => activePanel.value === \"scoring\",\n set: (v) => (activePanel.value = v ? \"scoring\" : null),\n });\n const designReferenceOpen = computed({\n get: () => activePanel.value === \"design-reference\",\n set: (v) => (activePanel.value = v ? \"design-reference\" : null),\n });\n const commentsOpen = computed({\n get: () => activePanel.value === \"comments\",\n set: (v) => (activePanel.value = v ? \"comments\" : null),\n });\n\n const mediaLibraryOpen = ref(false);\n const mediaLibraryAccept = ref<MediaCategory[] | undefined>(undefined);\n const aiMenuOpen = ref(false);\n const aiMenuRef = ref<HTMLElement | null>(null);\n\n const rightPanelOpen = computed(() => activePanel.value !== null);\n\n const activeAiFeature = computed<AiFeature | null>(() => {\n const p = activePanel.value;\n if (p === \"ai-chat\" || p === \"design-reference\" || p === \"scoring\")\n return p;\n return null;\n });\n\n const aiButtonActive = computed(\n () =>\n aiMenuOpen.value ||\n activePanel.value === \"ai-chat\" ||\n activePanel.value === \"design-reference\" ||\n activePanel.value === \"scoring\",\n );\n\n function toggleAiMenu(): void {\n aiMenuOpen.value = !aiMenuOpen.value;\n }\n\n function handleAiFeatureSelect(feature: AiFeature): void {\n aiMenuOpen.value = false;\n activePanel.value = activePanel.value === feature ? null : feature;\n }\n\n onClickOutside(aiMenuRef, () => {\n aiMenuOpen.value = false;\n });\n\n return {\n activePanel,\n aiChatOpen,\n scoringPanelOpen,\n designReferenceOpen,\n commentsOpen,\n mediaLibraryOpen,\n mediaLibraryAccept,\n aiMenuOpen,\n aiMenuRef,\n rightPanelOpen,\n activeAiFeature,\n aiButtonActive,\n toggleAiMenu,\n handleAiFeatureSelect,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport type { PlanConfig } from \"@templatical/types\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\nexport interface UseCloudSaveGateOptions {\n /** Reactive accessibility issues from `useTemplateLint`. */\n issues: Ref<LintIssue[]>;\n /** Reactive plan config from the server. */\n planConfig: Ref<PlanConfig | null>;\n}\n\nexport interface UseCloudSaveGateReturn {\n /** True when the gate would block a save click given current state. */\n shouldBlock: ComputedRef<boolean>;\n /** Errors that would block the save. Empty when `shouldBlock` is false. */\n blockingIssues: ComputedRef<LintIssue[]>;\n /** Modal visibility — toggled by `tryRunSave` and `cancel`. */\n modalOpen: Ref<boolean>;\n /**\n * Wraps a save callback. Returns `true` when the save was invoked\n * (blocking disabled or no errors), `false` when the modal opened\n * instead. Caller can `await` the returned promise to know when the\n * save settled.\n */\n tryRunSave: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n /**\n * Runs the save only when the gate would not block, and does **nothing** when\n * it would — no modal, no save, nothing pending. Returns whether it ran.\n *\n * This is the autosave path. A modal on a debounce timer would interrupt\n * typing, but simply saving anyway would demote `blockOnError` from a server\n * policy to a manual-save-only speed bump. Skipping leaves the template dirty\n * and the header saying \"unsaved\", which is true, and the reason stays visible\n * in the Issues panel. The next edit re-arms the debounce, so fixing the issue\n * is what lets the save through — no retry timer needed.\n */\n runUnlessBlocked: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n /** Force-run the pending save, bypassing the gate. */\n confirmAndSave: () => Promise<void>;\n /** Dismiss the modal without saving. */\n cancel: () => void;\n}\n\n/**\n * Cloud-only save-gate. When `planConfig.accessibility.blockOnError === true`\n * and the linter reports any error-severity issues, intercept the save\n * click and surface a confirmation modal listing the blockers. The user\n * can dismiss to fix issues, or click \"Save anyway\" to bypass — the gate\n * does NOT enforce a hard block, it surfaces a deliberate choice.\n *\n * Server policy is the source of truth for `blockOnError`; consumer-side\n * `accessibility` options can't override it.\n */\nexport function useCloudSaveGate(\n opts: UseCloudSaveGateOptions,\n): UseCloudSaveGateReturn {\n const modalOpen = ref(false);\n let pending: (() => Promise<unknown> | unknown) | null = null;\n\n const blockOnError = computed(\n () => opts.planConfig.value?.accessibility?.blockOnError === true,\n );\n\n const blockingIssues = computed(() =>\n blockOnError.value\n ? opts.issues.value.filter((i) => i.severity === \"error\")\n : [],\n );\n\n const shouldBlock = computed(() => blockingIssues.value.length > 0);\n\n async function tryRunSave(\n run: () => Promise<unknown> | unknown,\n ): Promise<boolean> {\n if (!shouldBlock.value) {\n await run();\n return true;\n }\n pending = run;\n modalOpen.value = true;\n return false;\n }\n\n async function runUnlessBlocked(\n run: () => Promise<unknown> | unknown,\n ): Promise<boolean> {\n if (shouldBlock.value) return false;\n await run();\n return true;\n }\n\n async function confirmAndSave(): Promise<void> {\n const run = pending;\n pending = null;\n modalOpen.value = false;\n if (run) {\n await run();\n }\n }\n\n function cancel(): void {\n pending = null;\n modalOpen.value = false;\n }\n\n return {\n shouldBlock,\n blockingIssues,\n modalOpen,\n tryRunSave,\n runUnlessBlocked,\n confirmAndSave,\n cancel,\n };\n}\n","import { type ComputedRef, type Ref, ref } from \"vue\";\nimport { useTimeoutFn } from \"@vueuse/core\";\nimport { COLLAB_UNDO_WARNING_MS } from \"../../constants/timeouts\";\n\nexport interface UseCollabUndoWarningOptions {\n /** Whether collaboration is currently enabled (reactive). */\n isCollaborationEnabled: ComputedRef<boolean>;\n /** Returns the current list of collaborators. */\n getCollaboratorCount: () => number;\n /** Whether the history stack has entries to undo. */\n canUndo: ComputedRef<boolean>;\n}\n\nexport interface UseCollabUndoWarningReturn {\n collabUndoWarningVisible: Ref<boolean>;\n showCollabUndoWarning: () => void;\n}\n\nexport function useCollabUndoWarning(\n options: UseCollabUndoWarningOptions,\n): UseCollabUndoWarningReturn {\n const { isCollaborationEnabled, getCollaboratorCount, canUndo } = options;\n\n const collabUndoWarningFired = ref(false);\n const collabUndoWarningVisible = ref(false);\n\n const { start: startCollabUndoWarningTimeout } = useTimeoutFn(\n () => {\n collabUndoWarningVisible.value = false;\n },\n COLLAB_UNDO_WARNING_MS,\n { immediate: false },\n );\n\n function showCollabUndoWarning(): void {\n if (\n collabUndoWarningFired.value ||\n !isCollaborationEnabled.value ||\n getCollaboratorCount() === 0 ||\n !canUndo.value\n ) {\n return;\n }\n\n collabUndoWarningFired.value = true;\n collabUndoWarningVisible.value = true;\n startCollabUndoWarningTimeout();\n }\n\n return {\n collabUndoWarningVisible,\n showCollabUndoWarning,\n };\n}\n","/**\n * Just enough of a stored template to walk its ids — deliberately structural\n * rather than `Template`, because the caller reads `editor.state`, which is\n * `DeepReadonly` and so not assignable to the mutable contract type.\n */\ninterface SavedBlockIdSource {\n content?: {\n blocks?: readonly {\n id: string;\n type: string;\n children?: readonly (readonly { id: string }[])[];\n }[];\n };\n}\n\n/**\n * Every block id present in the **stored** template, sections' children\n * included.\n *\n * This is the last thing the deleted Cloud editor core carried over the OSS one\n * (`savedBlockIds`), and it never belonged there: its only reader gates a\n * comment filter on whether the block exists server-side, which makes it a\n * *comments* dependency. It travels with that feature accordingly, and reaches\n * `CommentsSidebar` through `capabilities.comments.isBlockSaved`.\n *\n * Derived on demand rather than memoised: `state.template` only changes on a\n * create / load / save round-trip, and the one caller runs on a user filtering\n * the comments panel.\n */\nexport function collectSavedBlockIds(\n template: SavedBlockIdSource | null | undefined,\n): Set<string> {\n const ids = new Set<string>();\n const blocks = template?.content?.blocks;\n if (!blocks) return ids;\n\n for (const block of blocks) {\n ids.add(block.id);\n if (block.type === \"section\" && block.children) {\n for (const column of block.children) {\n for (const child of column) {\n ids.add(child.id);\n }\n }\n }\n }\n return ids;\n}\n","import { computed, provide, ref, watch } from \"vue\";\nimport {\n AuthManager,\n isFatalAuthError,\n performHealthCheck,\n useAiConfig,\n useCollaboration,\n useCollaborationBroadcast,\n useExport,\n useMcpListener,\n usePlanConfig,\n resolveExportFonts,\n resolveWebSocketConfig,\n createCloudCommentsProvider,\n createCloudRenderProvider,\n createCloudSavedBlocksProvider,\n createCloudTemplatesProvider,\n createCloudTestEmailProvider,\n createCloudVersionHistoryProvider,\n useTemplateScoring,\n useTestEmail,\n useWebSocket,\n type RealtimeChannel,\n} from \"@templatical/core/cloud\";\nimport type { UseEditorReturn } from \"@templatical/core\";\nimport type {\n CommentsProvider,\n EditorUser,\n RenderProvider,\n SavedBlocksProvider,\n Template,\n TemplateContent,\n TemplatePatch,\n TemplatesProvider,\n TestEmailProvider,\n VersionHistoryProvider,\n} from \"@templatical/types\";\n\nimport type { CloudTranslations } from \"../i18n\";\nimport { preRenderCustomBlocks } from \"../utils/preRenderCustomBlocks\";\nimport { logger } from \"../utils/logger\";\nimport type { UseEditorCoreReturn } from \"../composables/useEditorCore\";\nimport {\n AI_CONFIG_KEY,\n AUTH_MANAGER_KEY,\n CLOUD_TRANSLATIONS_KEY,\n SCORING_KEY,\n} from \"../keys\";\n\nimport { useCloudFeatureFlags } from \"./composables/useCloudFeatureFlags\";\nimport {\n useCloudMediaLibrary,\n type UseCloudMediaLibraryReturn,\n} from \"./composables/useCloudMediaLibrary\";\nimport { useCloudPanelState } from \"./composables/useCloudPanelState\";\nimport {\n useCloudSaveGate,\n type UseCloudSaveGateReturn,\n} from \"./composables/useCloudSaveGate\";\nimport {\n useCollabUndoWarning,\n type UseCollabUndoWarningReturn,\n} from \"./composables/useCollabUndoWarning\";\nimport { collectSavedBlockIds } from \"./collectSavedBlockIds\";\nimport type {\n CloudAttachContext,\n CloudAttachment,\n CloudCollaborationInstance,\n CloudReady,\n CloudReadyContext,\n CloudRuntime,\n} from \"./runtime\";\nimport type { TemplaticalCloudEditorConfig } from \"./cloudConfig\";\n\nexport interface BootstrapCloudOptions {\n config: TemplaticalCloudEditorConfig;\n cloudTranslations: CloudTranslations;\n}\n\n/**\n * What {@link bootstrapCloud} hands back: a set of ordinary `init()` config\n * values, plus the runtime for the few things that must run inside the editor\n * component's `setup()`.\n */\nexport interface CloudBootstrap {\n runtime: CloudRuntime;\n providers: {\n templates: TemplatesProvider;\n render: RenderProvider;\n versionHistory: VersionHistoryProvider;\n /** Absent when `savedBlocks: false` — the feature is then off entirely. */\n savedBlocks?: SavedBlocksProvider;\n testEmail: TestEmailProvider;\n comments: CommentsProvider;\n };\n /**\n * Who is editing, from the JWT — `init({ user })`'s value for a Cloud session.\n *\n * `undefined` when the project's token carries no `user` claim, which makes\n * comments unavailable rather than anonymous. Not a key `initCloud()` accepts:\n * Cloud's comment writes are signed against this same claim, so a\n * consumer-supplied identity would only be able to disagree with it.\n */\n user?: EditorUser;\n}\n\n/**\n * Cloud's bootstrap and adapter wiring, in one place, run **before the editor\n * mounts**.\n *\n * This replaces `useCloudInitialization`, which owned a second `useEditor`, a\n * second `useEditorCore` and a post-mount async dance with its own loading and\n * error states. The dance is gone: auth, the health check and the plan fetch all\n * resolve here, and a failure rejects `initCloud()` rather than mounting an\n * editor that cannot work. What is left is adapter construction.\n *\n * Two things deliberately stay Cloud-only:\n *\n * - **Entitlements.** In an all-BYO world there are no plans, so `planConfig` is\n * Cloud's commercial layer above the provider set rather than a provider\n * itself. It is down to five booleans, each metering a resource Cloud buys.\n * - **Mid-session failure.** `init()` cannot fail after it mounts and should not\n * grow the ability, so the error overlay lives on this side of the seam.\n */\nexport async function bootstrapCloud(\n options: BootstrapCloudOptions,\n): Promise<CloudBootstrap> {\n const { config, cloudTranslations } = options;\n\n // --- Late-bound editor access ---------------------------------------------\n //\n // The providers below are built before there is an editor to read. Each\n // reaches it through these holders, filled by `attach()` / `ready()` — which is\n // what lets them be handed to `init()` as ordinary config keys.\n let editorRef: UseEditorReturn | null = null;\n let coreRef: UseEditorCoreReturn | null = null;\n let websocketRef: ReturnType<typeof useWebSocket> | null = null;\n let mediaLibRef: UseCloudMediaLibraryReturn | null = null;\n let collaboration: CloudCollaborationInstance | null = null;\n let collabWarning: UseCollabUndoWarningReturn | null = null;\n let saveGate: UseCloudSaveGateReturn | null = null;\n\n const sessionError = ref<Error | null>(null);\n const lockedBlocks = ref<Map<string, unknown>>(new Map());\n /**\n * Cloud's presence channel, forward-declared for the comments adapter's\n * `subscribe`. `useWebSocket` can only be built in `attach()` (it needs the\n * editor), while the provider is handed to `init()` before that — so the adapter\n * watches this and rebinds when the real channel lands.\n */\n const commentChannel = ref<RealtimeChannel | null>(null);\n\n function requireEditor(): UseEditorReturn {\n if (!editorRef) {\n throw new Error(\"[Templatical] The Cloud editor is not mounted yet.\");\n }\n return editorRef;\n }\n\n function getTemplateId(): string | null {\n return editorRef?.state.template?.id ?? null;\n }\n\n // --- Auth + plan ----------------------------------------------------------\n\n const authManager = new AuthManager({\n ...config.auth,\n // Only a *fatal* refusal raises the overlay. A 4xx means the endpoint\n // rejected the credentials, so every subsequent request fails the same way\n // and the session really is over. A network blip or a 5xx resolves on its\n // own, and blanking a mounted editor over one interrupts someone mid-edit on\n // a template that is probably unsaved — a worse outcome than the blip.\n //\n // The consumer hears about both: `onError` fires either way, and\n // `AuthManager` re-throws regardless, so a caller that cannot proceed still\n // finds out.\n onError: (error: Error) => {\n if (isFatalAuthError(error)) {\n sessionError.value = error;\n }\n config.onError?.(error);\n },\n });\n\n const planConfigInstance = usePlanConfig({\n authManager,\n onError: config.onError,\n });\n\n await authManager.initialize();\n\n const healthResult = await performHealthCheck({ authManager });\n if (!healthResult.api.ok) {\n throw new Error(\"Health check failed: API is not reachable\");\n }\n if (!healthResult.auth.ok) {\n throw new Error(\n `Health check failed: authentication error${healthResult.auth.error ? ` - ${healthResult.auth.error}` : \"\"}`,\n );\n }\n if (!healthResult.websocket.ok) {\n logger.warn(\n \"WebSocket health check failed:\",\n healthResult.websocket.error ?? \"unknown error\",\n \"-- real-time features will be disabled.\",\n );\n }\n\n await planConfigInstance.fetchConfig();\n\n // The bootstrap itself is what the overlay would have covered, and it just\n // succeeded — so anything `onError` recorded on the way here is stale.\n sessionError.value = null;\n\n // --- Rejected consumer keys ----------------------------------------------\n //\n // None is declared on `TemplaticalCloudEditorConfig`, so these only fire for\n // JavaScript consumers — warned rather than silently dropped, because the\n // symptom (a store that stays empty while Cloud's fills up) reads as a broken\n // provider rather than a rejected one.\n if ((config as { templates?: unknown }).templates) {\n logger.warn(\n \"initCloud does not accept a `templates` provider — the template id is \" +\n \"the join key for collaboration, version history, comments, AI rewrite, \" +\n \"scoring and the server-side export, so an id Cloud never issued would \" +\n \"degrade all six. The supplied provider is ignored. Use init() to bring \" +\n \"your own.\",\n );\n }\n if ((config as { versionHistory?: unknown }).versionHistory) {\n logger.warn(\n \"initCloud does not accept a `versionHistory` provider — a version is \" +\n \"keyed to a template id Cloud issued, so Cloud owns its history. The \" +\n \"supplied provider is ignored. Use init() to bring your own.\",\n );\n }\n if ((config as { comments?: unknown }).comments) {\n logger.warn(\n \"initCloud does not accept a `comments` provider — a comment is keyed to \" +\n \"a template id Cloud issued, and its author is signed by the auth token, \" +\n \"so Cloud owns the conversation. The supplied provider is ignored. Use \" +\n \"init() to bring your own.\",\n );\n }\n // Rejected for a different reason than the three above: not the join key, but\n // because Cloud renders on its own for test email, scheduled sends and API\n // exports (`createCloudTestEmailProvider` calls `exportHtml` directly). A\n // consumer provider would have changed `toMjml()` / `toHtml()` and nothing\n // else, so what the user previewed and exported was not what Cloud delivered —\n // and Cloud's output is a deliberate superset (countdown GIFs, composited video\n // thumbnails), so the consumer's was also worse for those blocks.\n if ((config as { render?: unknown }).render) {\n logger.warn(\n \"initCloud does not accept a `render` provider — Cloud renders \" +\n \"server-side for test email, sends and exports, so a supplied renderer \" +\n \"would change only what you preview and export, never what Cloud \" +\n \"delivers. The supplied provider is ignored. For your own MJML, call \" +\n \"renderToMjml(editor.getContent()) from @templatical/renderer.\",\n );\n }\n\n // --- Providers ------------------------------------------------------------\n\n function connectRealtime(template: Template): void {\n const websocketConfig = planConfigInstance.config.value?.websocket;\n if (!websocketRef || !websocketConfig) return;\n websocketRef.connect(template.id, resolveWebSocketConfig(websocketConfig));\n }\n\n const baseTemplates = createCloudTemplatesProvider(authManager);\n const baseCreate = baseTemplates.create;\n const baseSave = baseTemplates.save;\n\n /**\n * Cloud's templates adapter plus the choreography the template id anchors: the\n * websocket joins the template's presence channel, and the consumer's\n * `onCreate` / `onLoad` fire.\n *\n * That choreography belongs in the adapter rather than in an editor-side\n * lifecycle composable, because it *is* adapter business — Cloud is what keys\n * realtime off the id it issued. `useCloudLifecycle` existed only to hold it.\n *\n * `save` pre-renders custom blocks, which is why it is wrapped too.\n */\n const templates: TemplatesProvider = {\n load: async (id: string) => {\n const template = await baseTemplates.load(id);\n connectRealtime(template);\n config.onLoad?.(template);\n return template;\n },\n create:\n typeof baseCreate === \"function\"\n ? async (input: { name?: string; content: TemplateContent }) => {\n const template = await baseCreate(input);\n connectRealtime(template);\n config.onCreate?.(template);\n return template;\n }\n : false,\n save:\n typeof baseSave === \"function\"\n ? async (id: string, patch: TemplatePatch) => {\n if (coreRef && editorRef) {\n await preRenderCustomBlocks(\n editorRef.content.value,\n coreRef.registry,\n );\n }\n return baseSave(id, patch);\n }\n : false,\n };\n\n // Always Cloud's — see the rejected-keys block above. Cloud renders\n // independently for delivery, so a second renderer could only disagree with it.\n const render: RenderProvider = createCloudRenderProvider({\n authManager,\n getTemplateId,\n // Plain `editor.save()`: the templates adapter below already pre-renders\n // custom blocks on every save, which is exactly what the render endpoint\n // needs, since it exports from the stored copy rather than from\n // `payload.content`.\n save: () => requireEditor().save(),\n });\n\n const versionHistory = createCloudVersionHistoryProvider(authManager);\n\n // Realtime is the adapter's own business: `subscribe` binds the presence channel\n // above, so `useCommentListener` in `@templatical/core` never learns that Pusher\n // exists and a BYO provider with an SSE stream slots into the same seam.\n const comments = createCloudCommentsProvider({\n authManager,\n channel: commentChannel,\n getSocketId: () => websocketRef?.getSocketId() ?? null,\n });\n\n // From the JWT, not from config. Cloud signs comment writes against this same\n // claim, so a consumer-supplied identity could only disagree with it — and a\n // token without the claim leaves comments unavailable rather than anonymous.\n const cloudUser: EditorUser | undefined = authManager.userConfig\n ? { id: authManager.userConfig.id, name: authManager.userConfig.name }\n : undefined;\n\n // A consumer may pass their own store instead of a boolean, in which case it\n // replaces Cloud's. That path is deliberately NOT plan-gated: `saved_modules`\n // licenses Cloud's *storage*, and someone else's backend isn't Cloud's to sell.\n const consumerSavedBlocks =\n typeof config.savedBlocks === \"object\" && config.savedBlocks !== null\n ? config.savedBlocks\n : null;\n const savedBlocks: SavedBlocksProvider | undefined =\n config.savedBlocks === false\n ? undefined\n : (consumerSavedBlocks ?? createCloudSavedBlocksProvider(authManager));\n\n // Cloud's test-email *config* — plan enablement plus the signed allowlist, both\n // carried by the JWT `authManager.initialize()` already resolved above. Hence\n // the constant `isAuthReady`: there is no window where it is false.\n const testEmailConfig = useTestEmail({\n authManager,\n isAuthReady: ref(true),\n });\n\n // Cloud's own send path renders through the export endpoint directly: the\n // adapter derives HTML from the *saved* template and applies\n // `onBeforeTestEmail` to it, which is a narrower job than the render contract.\n const exporter = useExport({ authManager });\n\n const consumerTestEmail = config.testEmail ?? null;\n const testEmail: TestEmailProvider =\n consumerTestEmail ??\n createCloudTestEmailProvider({\n authManager,\n getTemplateId,\n save: () => requireEditor().save(),\n exportHtml: (templateId: string) =>\n exporter.exportHtml(templateId, resolveExportFonts(config.fonts)),\n allowedEmails: testEmailConfig.allowedEmails,\n getSignature: testEmailConfig.getSignature,\n onBeforeTestEmail: config.onBeforeTestEmail,\n });\n\n // --- Setup-time wiring ----------------------------------------------------\n\n const aiConfig = useAiConfig(config.ai);\n const scoringInstance = useTemplateScoring({ authManager, getTemplateId });\n\n const featureFlags = useCloudFeatureFlags({\n planConfigInstance,\n aiConfig,\n getTemplateId,\n });\n\n const isCollaborationEnabled = computed(\n () =>\n !!config.collaboration?.enabled &&\n planConfigInstance.hasFeature(\"collaboration\"),\n );\n\n function attach(context: CloudAttachContext): CloudAttachment {\n const editor = context.editor;\n editorRef = editor;\n\n provide(CLOUD_TRANSLATIONS_KEY, cloudTranslations);\n provide(AUTH_MANAGER_KEY, authManager);\n provide(AI_CONFIG_KEY, aiConfig);\n provide(SCORING_KEY, scoringInstance);\n\n const websocket = useWebSocket({ authManager, onError: config.onError });\n websocketRef = websocket;\n\n // Feeds the comments adapter's `subscribe`, which was handed to `init()` before\n // this socket existed.\n watch(\n websocket.channel,\n (channel) => {\n commentChannel.value = channel;\n },\n { immediate: true },\n );\n\n if (config.mcp?.enabled) {\n useMcpListener({\n editor,\n channel: websocket.channel,\n onOperation: config.mcp.onOperation,\n });\n }\n\n // Order matters: the broadcast wrapper replaces the editor's mutators so they\n // publish to peers, and `useEditorCore`'s history interceptor then wraps them\n // again to push history entries. Reversing it would push history for remote\n // operations and drift the two peers apart.\n if (config.collaboration?.enabled) {\n collaboration = useCollaboration({\n authManager,\n editor,\n channel: websocket.channel,\n onError: config.onError,\n onCollaboratorJoined: config.collaboration.onCollaboratorJoined,\n onCollaboratorLeft: config.collaboration.onCollaboratorLeft,\n onBlockLocked: config.collaboration.onBlockLocked,\n onBlockUnlocked: config.collaboration.onBlockUnlocked,\n }) as CloudCollaborationInstance;\n\n watch(\n () => collaboration!.lockedBlocks.value,\n (next) => {\n lockedBlocks.value = next;\n },\n { immediate: true },\n );\n\n useCollaborationBroadcast(editor, collaboration);\n }\n\n const panelState = useCloudPanelState();\n\n const mediaLib = useCloudMediaLibrary({\n onRequestMedia: config.onRequestMedia,\n mediaLibraryOpen: panelState.mediaLibraryOpen,\n mediaLibraryAccept: panelState.mediaLibraryAccept,\n authManager,\n getMediaConfig: () => planConfigInstance.config.value?.media ?? null,\n onError: config.onError,\n });\n mediaLibRef = mediaLib;\n\n return {\n websocket,\n panelState,\n mediaLib,\n // Handed to `MediaLibraryModal` as props by `CloudPanels`. A bare-string\n // injection would resolve to `undefined` silently; see\n // `CloudMediaBrowserContext`.\n mediaBrowser: {\n authManager,\n projectId: authManager.projectId,\n planConfig: planConfigInstance,\n },\n featureFlags,\n collaboration,\n isCollaborationEnabled,\n };\n }\n\n function ready(context: CloudReadyContext): CloudReady {\n const { core, capabilities } = context;\n coreRef = core;\n\n saveGate = useCloudSaveGate({\n issues: core.templateLint ? core.templateLint.issues : ref([]),\n planConfig: planConfigInstance.config,\n });\n\n collabWarning = useCollabUndoWarning({\n isCollaborationEnabled,\n getCollaboratorCount: () =>\n collaboration?.collaborators.value.length ?? 0,\n canUndo: core.history.canUndo,\n });\n\n // Mutated in place rather than re-provided: `useEditorCore` already provided\n // this exact object, so components injecting it see Cloud's entries on their\n // first render. Same pattern `versionHistory` uses in `Editor.vue`.\n //\n // `capabilities.comments` is *not* set here any more: comments became a shared\n // provider-backed feature, so `Editor.vue` builds the capability from\n // `useCommentsFeature` exactly as it does for saved blocks and test email, and\n // Cloud contributes only the two predicates below.\n capabilities.plan = planConfigInstance;\n capabilities.ai = aiConfig;\n\n // Registered unconditionally. `custom_blocks` and `theme_customization` used\n // to gate these — entitlements on editor capability the free editor grants,\n // and the only two things `applyPlanGates` would ever have had to do.\n if (config.customBlocks?.length) {\n core.registerCustomBlocks(config.customBlocks);\n }\n if (config.theme) {\n core.themeOverrides.value = config.theme;\n }\n\n return {\n saveGate,\n collabWarning,\n sessionError,\n retry: async () => {\n try {\n await authManager.initialize();\n sessionError.value = null;\n } catch (error) {\n sessionError.value =\n error instanceof Error ? error : new Error(\"Authentication failed\");\n }\n },\n };\n }\n\n const runtime: CloudRuntime = {\n lockedBlocks,\n onRequestMedia: (context) =>\n mediaLibRef\n ? mediaLibRef.handleRequestMedia(context)\n : Promise.resolve(null),\n isSavedBlocksAvailable: consumerSavedBlocks\n ? () => true\n : () =>\n config.savedBlocks !== false &&\n planConfigInstance.hasFeature(\"saved_modules\"),\n isTestEmailAvailable: consumerTestEmail\n ? // Their sender, their rules — the `test_email` plan feature licenses\n // Cloud's sending, and \"the template must be saved\" is a constraint of\n // Cloud's server-side render, not of an arbitrary backend.\n () => true\n : // All three matter and none implies another: the plan may grant the\n // feature while the project's token omits a recipient config, and Cloud\n // renders from the *saved* template so there must be one.\n () =>\n testEmailConfig.isEnabled.value &&\n featureFlags.canSendTestEmail.value &&\n featureFlags.hasTemplateSaved.value,\n // Three conditions, none implying another: the consumer may switch the feature\n // off, the plan meters comment storage and its realtime fan-out, and Cloud\n // anchors a comment to the *saved* template. (Whether a `user` exists is the\n // shared feature's own gate, not Cloud's.)\n isCommentsAvailable: () =>\n config.commenting !== false &&\n planConfigInstance.hasFeature(\"commenting\") &&\n featureFlags.hasTemplateSaved.value,\n // A comment is anchored server-side, so a block that exists only on the canvas\n // has nothing stored to show. This is the last thing the deleted cloud editor\n // core carried (`savedBlockIds`), and it was always a comments concern rather\n // than an editor one.\n isBlockSaved: (blockId: string) =>\n collectSavedBlockIds(editorRef?.state.template ?? null).has(blockId),\n getSaveGate: () => saveGate,\n isRemoteOperation: () =>\n collaboration?._isProcessingRemoteOperation() ?? false,\n onBeforeUndo: () => collabWarning?.showCollabUndoWarning(),\n attach,\n ready,\n destroy() {\n websocketRef?.disconnect();\n config.onUnmount?.();\n },\n };\n\n return {\n runtime,\n providers: {\n templates,\n render,\n versionHistory,\n ...(savedBlocks ? { savedBlocks } : {}),\n testEmail,\n comments,\n },\n user: cloudUser,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { H as e, N as t, Q as n, S as r, U as i, at as a, c as o, ct as s, f as c, g as l, h as u, k as d, l as f, lt as p, m, o as h, p as g, ut as _ } from "./draggable-BfpsXFg7.js";
|
|
2
2
|
import { a as v, d as y, o as b, r as x } from "./src-B_QJduQD.js";
|
|
3
|
-
import { t as S } from "./useI18n-
|
|
4
|
-
import { t as C } from "./useMergeTag-
|
|
3
|
+
import { t as S } from "./useI18n-4sUbTM3Y.js";
|
|
4
|
+
import { t as C } from "./useMergeTag-DxjTEyin.js";
|
|
5
5
|
import { _ as w, g as T, h as E, n as D, t as O, v as k, y as A } from "./tiptap-DmpGap5g.js";
|
|
6
6
|
import { i as j, r as M, t as N } from "./MergeTagSuggestion-DhllqTA3.js";
|
|
7
7
|
//#region src/extensions/FontSize.ts
|
|
@@ -442,4 +442,4 @@ var W = w.create({
|
|
|
442
442
|
//#endregion
|
|
443
443
|
export { P as FontSize, F as LetterSpacing, I as LineBoundaryKeys, L as LineHeight, W as LogicMergeTagNode, J as MergeTagNode, N as MergeTagSuggestion, M as filterMergeTags, j as handleSuggestionKeyDown };
|
|
444
444
|
|
|
445
|
-
//# sourceMappingURL=extensions-
|
|
445
|
+
//# sourceMappingURL=extensions-Byo2sA1r.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extensions-CMcVw2uu.js","names":[],"sources":["../../../src/extensions/FontSize.ts","../../../src/extensions/LetterSpacing.ts","../../../src/extensions/LineBoundaryKeys.ts","../../../src/extensions/LineHeight.ts","../../../src/extensions/LogicMergeTagNodeView.vue","../../../src/extensions/LogicMergeTagNodeView.vue","../../../src/extensions/isNodeSelected.ts","../../../src/extensions/renderVueNodeView.ts","../../../src/extensions/LogicMergeTagNode.ts","../../../src/extensions/MergeTagNodeView.vue","../../../src/extensions/MergeTagNodeView.vue","../../../src/extensions/MergeTagNode.ts"],"sourcesContent":["import { Extension } from \"@tiptap/core\";\n\nexport interface FontSizeOptions {\n types: string[];\n}\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n fontSize: {\n setFontSize: (size: string) => ReturnType;\n unsetFontSize: () => ReturnType;\n };\n }\n}\n\nexport const FontSize = Extension.create<FontSizeOptions>({\n name: \"fontSize\",\n\n addOptions() {\n return {\n types: [\"textStyle\"],\n };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n fontSize: {\n default: null,\n parseHTML: (element) =>\n element.style.fontSize?.replace(/['\"]+/g, \"\") || null,\n renderHTML: (attributes) => {\n if (!attributes.fontSize) {\n return {};\n }\n return {\n style: `font-size: ${attributes.fontSize}`,\n };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setFontSize:\n (size: string) =>\n ({ chain }) => {\n return chain().setMark(\"textStyle\", { fontSize: size }).run();\n },\n unsetFontSize:\n () =>\n ({ chain }) => {\n return chain()\n .setMark(\"textStyle\", { fontSize: null })\n .removeEmptyTextStyle()\n .run();\n },\n };\n },\n});\n","import { Extension } from \"@tiptap/core\";\n\nexport interface LetterSpacingOptions {\n types: string[];\n}\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n letterSpacing: {\n setLetterSpacing: (spacing: string) => ReturnType;\n unsetLetterSpacing: () => ReturnType;\n };\n }\n}\n\nexport const LetterSpacing = Extension.create<LetterSpacingOptions>({\n name: \"letterSpacing\",\n\n addOptions() {\n return {\n types: [\"textStyle\"],\n };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n letterSpacing: {\n default: null,\n parseHTML: (element) =>\n element.style.letterSpacing?.replace(/['\"]+/g, \"\") || null,\n renderHTML: (attributes) => {\n if (!attributes.letterSpacing) {\n return {};\n }\n return {\n style: `letter-spacing: ${attributes.letterSpacing}`,\n };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setLetterSpacing:\n (spacing: string) =>\n ({ chain }) => {\n return chain().setMark(\"textStyle\", { letterSpacing: spacing }).run();\n },\n unsetLetterSpacing:\n () =>\n ({ chain }) => {\n return chain()\n .setMark(\"textStyle\", { letterSpacing: null })\n .removeEmptyTextStyle()\n .run();\n },\n };\n },\n});\n","import { Extension } from \"@tiptap/core\";\n\n/**\n * Routes plain End/Home through `Selection.modify` instead of the browser's\n * native caret movement.\n *\n * Chromium (verified 140–151, including stable Chrome) has a bug where a\n * native End or Home pressed while a triple-click selection is active puts\n * Blink's selection state into a mode where the next typed character\n * natively smooth-scrolls the nearest scrollable ancestor (the editor's\n * canvas, `.tpl-body`) to its very bottom (End) or top (Home), dragging the\n * caret out of view. The state cannot be repaired after the fact — clearing\n * or re-setting the DOM selection, blur/refocus, and preventDefault on the\n * clicks all fail — but a programmatic caret move never arms it.\n *\n * `Selection.modify(\"move\", direction, \"lineboundary\")` is the exact\n * equivalent of the native key (visual line boundary, bidi-aware via the\n * logical forward/backward directions), so behavior is unchanged for users.\n * Shift+End / Shift+Home extend selections natively and do not trigger the\n * bug, so only the plain keys are bound — a keymap entry without modifiers\n * never matches the shifted keys.\n */\nexport const LineBoundaryKeys = Extension.create({\n name: \"lineBoundaryKeys\",\n\n addKeyboardShortcuts() {\n const moveToLineBoundary =\n (direction: \"forward\" | \"backward\") =>\n ({ editor }: { editor: { view: unknown } }): boolean => {\n const view = editor.view as {\n composing: boolean;\n root: DocumentOrShadowRoot & {\n getSelection?: () => Selection | null;\n };\n dom: HTMLElement;\n };\n // During IME composition End/Home commit the composition; leave\n // that to the browser.\n if (!view || view.composing) return false;\n // `view.root` is the editable's Document or ShadowRoot, so the\n // selection resolves correctly in both mount modes. ShadowRoot's\n // getSelection is Chromium-only; elsewhere fall back to the owner\n // document (Chromium is also the only engine with the bug).\n const selection =\n typeof view.root?.getSelection === \"function\"\n ? view.root.getSelection()\n : (view.dom?.ownerDocument?.getSelection?.() ?? null);\n // `modify` is non-standard; without it, or with a selection that\n // is not ours, native handling is the safer default.\n if (!selection || typeof selection.modify !== \"function\") return false;\n if (!selection.anchorNode || !view.dom.contains(selection.anchorNode)) {\n return false;\n }\n selection.modify(\"move\", direction, \"lineboundary\");\n return true;\n };\n\n return {\n End: moveToLineBoundary(\"forward\"),\n Home: moveToLineBoundary(\"backward\"),\n };\n },\n});\n","import { Extension } from \"@tiptap/core\";\n\nexport interface LineHeightOptions {\n types: string[];\n defaultLineHeight: string;\n}\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n lineHeight: {\n setLineHeight: (lineHeight: string) => ReturnType;\n unsetLineHeight: () => ReturnType;\n };\n }\n}\n\nexport const LineHeight = Extension.create<LineHeightOptions>({\n name: \"lineHeight\",\n\n addOptions() {\n return {\n types: [\"paragraph\"],\n defaultLineHeight: \"1.5\",\n };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n lineHeight: {\n default: null,\n parseHTML: (element) => element.style.lineHeight || null,\n renderHTML: (attributes) => {\n if (!attributes.lineHeight) {\n return {};\n }\n return {\n style: `line-height: ${attributes.lineHeight}`,\n };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setLineHeight:\n (lineHeight: string) =>\n ({ commands }) => {\n return this.options.types.every((type) =>\n commands.updateAttributes(type, { lineHeight }),\n );\n },\n unsetLineHeight:\n () =>\n ({ commands }) => {\n return this.options.types.every((type) =>\n commands.resetAttributes(type, \"lineHeight\"),\n );\n },\n };\n },\n});\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n} from \"@templatical/types\";\nimport type { Editor } from \"@tiptap/core\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n value: string;\n keyword: string;\n };\n };\n editor: Editor;\n getPos: () => number;\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { syntax } = useMergeTag();\nconst { t } = useI18n();\n\nconst isValid = computed(() =>\n isLogicMergeTagValue(props.node.attrs.value, syntax),\n);\nconst displayKeyword = computed(() =>\n getLogicMergeTagKeyword(props.node.attrs.value, syntax),\n);\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\nlet handled = false;\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n handled = false;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n if (handled) {\n return;\n }\n handled = true;\n\n const newValue = editValue.value.trim();\n if (!newValue) {\n isEditing.value = false;\n return;\n }\n\n if (newValue !== props.node.attrs.value) {\n props.updateAttributes({\n value: newValue,\n keyword: isLogicMergeTagValue(newValue, syntax)\n ? getLogicMergeTagKeyword(newValue, syntax)\n : \"\",\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n :class=\"\n isValid\n ? 'tpl-logic-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase tpl:select-none'\n : ''\n \"\n :style=\"\n isValid\n ? 'background-color: transparent; border: 1.5px solid color-mix(in srgb, var(--tpl-primary) 50%, transparent); color: var(--tpl-primary);'\n : ''\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-40 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:normal-case tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode: valid merge tag -->\n <span\n v-else-if=\"isValid\"\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayKeyword }}\n </span>\n <!-- Display mode: invalid (plain text) -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ node.attrs.value }}\n </span>\n <button\n v-if=\"isValid\"\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n} from \"@templatical/types\";\nimport type { Editor } from \"@tiptap/core\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n value: string;\n keyword: string;\n };\n };\n editor: Editor;\n getPos: () => number;\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { syntax } = useMergeTag();\nconst { t } = useI18n();\n\nconst isValid = computed(() =>\n isLogicMergeTagValue(props.node.attrs.value, syntax),\n);\nconst displayKeyword = computed(() =>\n getLogicMergeTagKeyword(props.node.attrs.value, syntax),\n);\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\nlet handled = false;\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n handled = false;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n if (handled) {\n return;\n }\n handled = true;\n\n const newValue = editValue.value.trim();\n if (!newValue) {\n isEditing.value = false;\n return;\n }\n\n if (newValue !== props.node.attrs.value) {\n props.updateAttributes({\n value: newValue,\n keyword: isLogicMergeTagValue(newValue, syntax)\n ? getLogicMergeTagKeyword(newValue, syntax)\n : \"\",\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n :class=\"\n isValid\n ? 'tpl-logic-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase tpl:select-none'\n : ''\n \"\n :style=\"\n isValid\n ? 'background-color: transparent; border: 1.5px solid color-mix(in srgb, var(--tpl-primary) 50%, transparent); color: var(--tpl-primary);'\n : ''\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-40 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:normal-case tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode: valid merge tag -->\n <span\n v-else-if=\"isValid\"\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayKeyword }}\n </span>\n <!-- Display mode: invalid (plain text) -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ node.attrs.value }}\n </span>\n <button\n v-if=\"isValid\"\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","import type { Editor } from \"@tiptap/core\";\n\n/**\n * Decides whether MergeTagNode / LogicMergeTagNode should swallow a\n * Backspace/Delete keystroke (returning `true` from a TipTap keymap\n * suppresses the default behavior).\n *\n * Only suppresses in the cursor-adjacent case so the \"first press\n * selects the atom, second press deletes it\" UX works. Range selections\n * (Cmd+A, drag-select, double-click-word) explicitly fall through —\n * default `replaceSelection` then deletes the entire range including\n * any merge-tag atoms inside it.\n *\n * Earlier revisions also returned `true` whenever `nodesBetween($from,\n * $to)` found a merge tag anywhere in the selection range — which\n * silently broke Cmd+A + Backspace for any paragraph containing a\n * merge tag (entire deletion was cancelled, nothing happened). The\n * cursor-adjacent protection is the only piece that should survive.\n */\nexport function isNodeSelected(editor: Editor, nodeTypeName: string): boolean {\n const { $from, $to } = editor.state.selection;\n\n // Range selection: let TipTap's default range-delete run. Even when\n // the range contains atom nodes, ProseMirror's replaceSelection\n // handles atomic deletion correctly.\n if ($from.pos !== $to.pos) return false;\n\n // Cursor-adjacent atom: protect from single-keystroke deletion.\n // (`$from.pos > 0` guards against reading `nodeBefore` at doc start\n // where ProseMirror's index would be undefined for nodeBefore.)\n if ($from.pos > 0 && $from.nodeBefore?.type.name === nodeTypeName) {\n return true;\n }\n if ($from.nodeAfter?.type.name === nodeTypeName) {\n return true;\n }\n\n return false;\n}\n","import type { Component } from \"vue\";\nimport { VueNodeViewRenderer } from \"@tiptap/vue-3\";\n\n/**\n * Typed wrapper for VueNodeViewRenderer that handles the known type mismatch\n * between Vue SFC default exports and TipTap's expected component type.\n */\nexport function renderVueNodeView(component: Component) {\n return VueNodeViewRenderer(\n component as Parameters<typeof VueNodeViewRenderer>[0],\n );\n}\n","import LogicMergeTagNodeView from \"./LogicMergeTagNodeView.vue\";\nimport type { SyntaxPreset } from \"@templatical/types\";\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n SYNTAX_PRESETS,\n} from \"@templatical/types\";\nimport { InputRule, mergeAttributes, Node, PasteRule } from \"@tiptap/core\";\nimport { isNodeSelected } from \"./isNodeSelected\";\nimport { renderVueNodeView } from \"./renderVueNodeView\";\n\nexport interface LogicMergeTagNodeOptions {\n syntax: SyntaxPreset;\n}\n\nexport const LogicMergeTagNode = Node.create<LogicMergeTagNodeOptions>({\n name: \"logicMergeTagNode\",\n\n group: \"inline\",\n\n inline: true,\n\n atom: true,\n\n addOptions() {\n return {\n syntax: SYNTAX_PRESETS.liquid,\n };\n },\n\n addAttributes() {\n return {\n // Both attributes are state, not markup, same as `MergeTagNode`:\n // `renderHTML()` emits the canonical `data-logic-merge-tag` /\n // `data-keyword` pair and `parseHTML` reads only those. `rendered: false`\n // keeps TipTap from also serializing each attribute under its own name,\n // which would put a write-only duplicate of the same data on every tag.\n value: {\n default: \"\",\n rendered: false,\n parseHTML: (element) =>\n element.getAttribute(\"data-logic-merge-tag\") || \"\",\n },\n keyword: {\n default: \"\",\n rendered: false,\n parseHTML: (element) =>\n element.getAttribute(\"data-keyword\") || element.textContent || \"\",\n },\n };\n },\n\n parseHTML() {\n return [\n {\n tag: \"span[data-logic-merge-tag]\",\n },\n ];\n },\n\n renderHTML({ node, HTMLAttributes }) {\n if (!isLogicMergeTagValue(node.attrs.value, this.options.syntax)) {\n return [\"span\", {}, node.attrs.value];\n }\n\n const keyword = getLogicMergeTagKeyword(\n node.attrs.value,\n this.options.syntax,\n );\n\n return [\n \"span\",\n mergeAttributes(HTMLAttributes, {\n \"data-logic-merge-tag\": node.attrs.value,\n \"data-keyword\": keyword,\n }),\n keyword,\n ];\n },\n\n addNodeView() {\n return renderVueNodeView(LogicMergeTagNodeView);\n },\n\n addKeyboardShortcuts() {\n return {\n Backspace: () => isNodeSelected(this.editor, this.name),\n Delete: () => isNodeSelected(this.editor, this.name),\n };\n },\n\n addInputRules() {\n const inputRegex = new RegExp(this.options.syntax.logic.source + \"$\", \"\");\n\n return [\n new InputRule({\n find: inputRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n if (!isLogicMergeTagValue(fullValue, this.options.syntax)) {\n return;\n }\n\n const keyword = getLogicMergeTagKeyword(\n fullValue,\n this.options.syntax,\n );\n\n const node = this.type.create({\n value: fullValue,\n keyword,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n\n addPasteRules() {\n const pasteRegex = new RegExp(this.options.syntax.logic.source, \"g\");\n\n return [\n new PasteRule({\n find: pasteRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n if (!isLogicMergeTagValue(fullValue, this.options.syntax)) {\n return;\n }\n\n const keyword = getLogicMergeTagKeyword(\n fullValue,\n this.options.syntax,\n );\n\n const node = this.type.create({\n value: fullValue,\n keyword,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n});\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n label: string;\n value: string;\n };\n };\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { getMergeTagLabel } = useMergeTag();\nconst { t } = useI18n();\n\nconst displayLabel = computed(() => getMergeTagLabel(props.node.attrs.value));\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n const newValue = editValue.value.trim();\n if (newValue && newValue !== props.node.attrs.value) {\n // Update with new value and derive label from it\n props.updateAttributes({\n value: newValue,\n label: getMergeTagLabel(newValue),\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n class=\"tpl-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium tpl:select-none tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-primary) 20%, transparent);\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-32 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayLabel }}\n </span>\n <button\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n label: string;\n value: string;\n };\n };\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { getMergeTagLabel } = useMergeTag();\nconst { t } = useI18n();\n\nconst displayLabel = computed(() => getMergeTagLabel(props.node.attrs.value));\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n const newValue = editValue.value.trim();\n if (newValue && newValue !== props.node.attrs.value) {\n // Update with new value and derive label from it\n props.updateAttributes({\n value: newValue,\n label: getMergeTagLabel(newValue),\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n class=\"tpl-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium tpl:select-none tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-primary) 20%, transparent);\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-32 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayLabel }}\n </span>\n <button\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","import MergeTagNodeView from \"./MergeTagNodeView.vue\";\nimport type { MergeTag, SyntaxPreset } from \"@templatical/types\";\nimport { getMergeTagLabel, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { InputRule, mergeAttributes, Node, PasteRule } from \"@tiptap/core\";\nimport { isNodeSelected } from \"./isNodeSelected\";\nimport { renderVueNodeView } from \"./renderVueNodeView\";\n\nexport interface MergeTagNodeOptions {\n mergeTags: MergeTag[];\n syntax: SyntaxPreset;\n}\n\n/**\n * Inserted-node attrs are intentionally limited to `label` + `value`. The\n * `MergeTag` interface also carries optional `group` / `description` for\n * the built-in picker, but those are display-only and must NOT leak into\n * the document JSON — they would bloat templates and pollute serialized\n * state. `Pick<>` makes the constraint explicit at the type level.\n */\nexport type InsertMergeTagAttrs = Pick<MergeTag, \"label\" | \"value\">;\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n mergeTagNode: {\n insertMergeTag: (attrs: InsertMergeTagAttrs) => ReturnType;\n };\n }\n}\n\nexport const MergeTagNode = Node.create<MergeTagNodeOptions>({\n name: \"mergeTagNode\",\n\n group: \"inline\",\n\n inline: true,\n\n atom: true,\n\n addOptions() {\n return {\n mergeTags: [],\n syntax: SYNTAX_PRESETS.liquid,\n };\n },\n\n addAttributes() {\n return {\n // Both attributes are state, not markup: `renderHTML()` below emits the\n // canonical `data-merge-tag` / `data-label` pair and `parseHTML` reads\n // only those. `rendered: false` keeps TipTap from also serializing each\n // attribute under its own name, which would put a write-only duplicate\n // of the same data on every tag — invalid on a `<span>`, and paid again\n // through storage, autosave, snapshots and version history.\n label: {\n default: \"\",\n rendered: false,\n parseHTML: (element) =>\n element.getAttribute(\"data-label\") || element.textContent || \"\",\n },\n value: {\n default: \"\",\n rendered: false,\n parseHTML: (element) => element.getAttribute(\"data-merge-tag\") || \"\",\n },\n };\n },\n\n parseHTML() {\n return [\n {\n tag: \"span[data-merge-tag]\",\n },\n ];\n },\n\n renderHTML({ node, HTMLAttributes }) {\n const label = getMergeTagLabel(node.attrs.value, this.options.mergeTags);\n\n return [\n \"span\",\n mergeAttributes(HTMLAttributes, {\n \"data-merge-tag\": node.attrs.value,\n \"data-label\": label,\n }),\n label,\n ];\n },\n\n addNodeView() {\n return renderVueNodeView(MergeTagNodeView);\n },\n\n addCommands() {\n return {\n insertMergeTag:\n (attrs: InsertMergeTagAttrs) =>\n ({ commands }) => {\n // Whitelist explicitly — even if a caller smuggles extra keys via\n // a wider type assertion, only label/value reach the node attrs.\n return commands.insertContent({\n type: this.name,\n attrs: { label: attrs.label, value: attrs.value },\n });\n },\n };\n },\n\n addKeyboardShortcuts() {\n return {\n Backspace: () => isNodeSelected(this.editor, this.name),\n Delete: () => isNodeSelected(this.editor, this.name),\n };\n },\n\n addInputRules() {\n const inputRegex = new RegExp(this.options.syntax.value.source + \"$\", \"\");\n\n return [\n new InputRule({\n find: inputRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n const label = getMergeTagLabel(fullValue, this.options.mergeTags);\n\n const node = this.type.create({\n label,\n value: fullValue,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n\n addPasteRules() {\n const pasteRegex = new RegExp(this.options.syntax.value.source, \"g\");\n\n return [\n new PasteRule({\n find: pasteRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n const label = getMergeTagLabel(fullValue, this.options.mergeTags);\n\n const node = this.type.create({\n label,\n value: fullValue,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"extensions-Byo2sA1r.js","names":[],"sources":["../../../src/extensions/FontSize.ts","../../../src/extensions/LetterSpacing.ts","../../../src/extensions/LineBoundaryKeys.ts","../../../src/extensions/LineHeight.ts","../../../src/extensions/LogicMergeTagNodeView.vue","../../../src/extensions/LogicMergeTagNodeView.vue","../../../src/extensions/isNodeSelected.ts","../../../src/extensions/renderVueNodeView.ts","../../../src/extensions/LogicMergeTagNode.ts","../../../src/extensions/MergeTagNodeView.vue","../../../src/extensions/MergeTagNodeView.vue","../../../src/extensions/MergeTagNode.ts"],"sourcesContent":["import { Extension } from \"@tiptap/core\";\n\nexport interface FontSizeOptions {\n types: string[];\n}\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n fontSize: {\n setFontSize: (size: string) => ReturnType;\n unsetFontSize: () => ReturnType;\n };\n }\n}\n\nexport const FontSize = Extension.create<FontSizeOptions>({\n name: \"fontSize\",\n\n addOptions() {\n return {\n types: [\"textStyle\"],\n };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n fontSize: {\n default: null,\n parseHTML: (element) =>\n element.style.fontSize?.replace(/['\"]+/g, \"\") || null,\n renderHTML: (attributes) => {\n if (!attributes.fontSize) {\n return {};\n }\n return {\n style: `font-size: ${attributes.fontSize}`,\n };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setFontSize:\n (size: string) =>\n ({ chain }) => {\n return chain().setMark(\"textStyle\", { fontSize: size }).run();\n },\n unsetFontSize:\n () =>\n ({ chain }) => {\n return chain()\n .setMark(\"textStyle\", { fontSize: null })\n .removeEmptyTextStyle()\n .run();\n },\n };\n },\n});\n","import { Extension } from \"@tiptap/core\";\n\nexport interface LetterSpacingOptions {\n types: string[];\n}\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n letterSpacing: {\n setLetterSpacing: (spacing: string) => ReturnType;\n unsetLetterSpacing: () => ReturnType;\n };\n }\n}\n\nexport const LetterSpacing = Extension.create<LetterSpacingOptions>({\n name: \"letterSpacing\",\n\n addOptions() {\n return {\n types: [\"textStyle\"],\n };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n letterSpacing: {\n default: null,\n parseHTML: (element) =>\n element.style.letterSpacing?.replace(/['\"]+/g, \"\") || null,\n renderHTML: (attributes) => {\n if (!attributes.letterSpacing) {\n return {};\n }\n return {\n style: `letter-spacing: ${attributes.letterSpacing}`,\n };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setLetterSpacing:\n (spacing: string) =>\n ({ chain }) => {\n return chain().setMark(\"textStyle\", { letterSpacing: spacing }).run();\n },\n unsetLetterSpacing:\n () =>\n ({ chain }) => {\n return chain()\n .setMark(\"textStyle\", { letterSpacing: null })\n .removeEmptyTextStyle()\n .run();\n },\n };\n },\n});\n","import { Extension } from \"@tiptap/core\";\n\n/**\n * Routes plain End/Home through `Selection.modify` instead of the browser's\n * native caret movement.\n *\n * Chromium (verified 140–151, including stable Chrome) has a bug where a\n * native End or Home pressed while a triple-click selection is active puts\n * Blink's selection state into a mode where the next typed character\n * natively smooth-scrolls the nearest scrollable ancestor (the editor's\n * canvas, `.tpl-body`) to its very bottom (End) or top (Home), dragging the\n * caret out of view. The state cannot be repaired after the fact — clearing\n * or re-setting the DOM selection, blur/refocus, and preventDefault on the\n * clicks all fail — but a programmatic caret move never arms it.\n *\n * `Selection.modify(\"move\", direction, \"lineboundary\")` is the exact\n * equivalent of the native key (visual line boundary, bidi-aware via the\n * logical forward/backward directions), so behavior is unchanged for users.\n * Shift+End / Shift+Home extend selections natively and do not trigger the\n * bug, so only the plain keys are bound — a keymap entry without modifiers\n * never matches the shifted keys.\n */\nexport const LineBoundaryKeys = Extension.create({\n name: \"lineBoundaryKeys\",\n\n addKeyboardShortcuts() {\n const moveToLineBoundary =\n (direction: \"forward\" | \"backward\") =>\n ({ editor }: { editor: { view: unknown } }): boolean => {\n const view = editor.view as {\n composing: boolean;\n root: DocumentOrShadowRoot & {\n getSelection?: () => Selection | null;\n };\n dom: HTMLElement;\n };\n // During IME composition End/Home commit the composition; leave\n // that to the browser.\n if (!view || view.composing) return false;\n // `view.root` is the editable's Document or ShadowRoot, so the\n // selection resolves correctly in both mount modes. ShadowRoot's\n // getSelection is Chromium-only; elsewhere fall back to the owner\n // document (Chromium is also the only engine with the bug).\n const selection =\n typeof view.root?.getSelection === \"function\"\n ? view.root.getSelection()\n : (view.dom?.ownerDocument?.getSelection?.() ?? null);\n // `modify` is non-standard; without it, or with a selection that\n // is not ours, native handling is the safer default.\n if (!selection || typeof selection.modify !== \"function\") return false;\n if (!selection.anchorNode || !view.dom.contains(selection.anchorNode)) {\n return false;\n }\n selection.modify(\"move\", direction, \"lineboundary\");\n return true;\n };\n\n return {\n End: moveToLineBoundary(\"forward\"),\n Home: moveToLineBoundary(\"backward\"),\n };\n },\n});\n","import { Extension } from \"@tiptap/core\";\n\nexport interface LineHeightOptions {\n types: string[];\n defaultLineHeight: string;\n}\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n lineHeight: {\n setLineHeight: (lineHeight: string) => ReturnType;\n unsetLineHeight: () => ReturnType;\n };\n }\n}\n\nexport const LineHeight = Extension.create<LineHeightOptions>({\n name: \"lineHeight\",\n\n addOptions() {\n return {\n types: [\"paragraph\"],\n defaultLineHeight: \"1.5\",\n };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n lineHeight: {\n default: null,\n parseHTML: (element) => element.style.lineHeight || null,\n renderHTML: (attributes) => {\n if (!attributes.lineHeight) {\n return {};\n }\n return {\n style: `line-height: ${attributes.lineHeight}`,\n };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setLineHeight:\n (lineHeight: string) =>\n ({ commands }) => {\n return this.options.types.every((type) =>\n commands.updateAttributes(type, { lineHeight }),\n );\n },\n unsetLineHeight:\n () =>\n ({ commands }) => {\n return this.options.types.every((type) =>\n commands.resetAttributes(type, \"lineHeight\"),\n );\n },\n };\n },\n});\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n} from \"@templatical/types\";\nimport type { Editor } from \"@tiptap/core\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n value: string;\n keyword: string;\n };\n };\n editor: Editor;\n getPos: () => number;\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { syntax } = useMergeTag();\nconst { t } = useI18n();\n\nconst isValid = computed(() =>\n isLogicMergeTagValue(props.node.attrs.value, syntax),\n);\nconst displayKeyword = computed(() =>\n getLogicMergeTagKeyword(props.node.attrs.value, syntax),\n);\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\nlet handled = false;\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n handled = false;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n if (handled) {\n return;\n }\n handled = true;\n\n const newValue = editValue.value.trim();\n if (!newValue) {\n isEditing.value = false;\n return;\n }\n\n if (newValue !== props.node.attrs.value) {\n props.updateAttributes({\n value: newValue,\n keyword: isLogicMergeTagValue(newValue, syntax)\n ? getLogicMergeTagKeyword(newValue, syntax)\n : \"\",\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n :class=\"\n isValid\n ? 'tpl-logic-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase tpl:select-none'\n : ''\n \"\n :style=\"\n isValid\n ? 'background-color: transparent; border: 1.5px solid color-mix(in srgb, var(--tpl-primary) 50%, transparent); color: var(--tpl-primary);'\n : ''\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-40 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:normal-case tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode: valid merge tag -->\n <span\n v-else-if=\"isValid\"\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayKeyword }}\n </span>\n <!-- Display mode: invalid (plain text) -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ node.attrs.value }}\n </span>\n <button\n v-if=\"isValid\"\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n} from \"@templatical/types\";\nimport type { Editor } from \"@tiptap/core\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n value: string;\n keyword: string;\n };\n };\n editor: Editor;\n getPos: () => number;\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { syntax } = useMergeTag();\nconst { t } = useI18n();\n\nconst isValid = computed(() =>\n isLogicMergeTagValue(props.node.attrs.value, syntax),\n);\nconst displayKeyword = computed(() =>\n getLogicMergeTagKeyword(props.node.attrs.value, syntax),\n);\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\nlet handled = false;\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n handled = false;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n if (handled) {\n return;\n }\n handled = true;\n\n const newValue = editValue.value.trim();\n if (!newValue) {\n isEditing.value = false;\n return;\n }\n\n if (newValue !== props.node.attrs.value) {\n props.updateAttributes({\n value: newValue,\n keyword: isLogicMergeTagValue(newValue, syntax)\n ? getLogicMergeTagKeyword(newValue, syntax)\n : \"\",\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n :class=\"\n isValid\n ? 'tpl-logic-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase tpl:select-none'\n : ''\n \"\n :style=\"\n isValid\n ? 'background-color: transparent; border: 1.5px solid color-mix(in srgb, var(--tpl-primary) 50%, transparent); color: var(--tpl-primary);'\n : ''\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-40 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:normal-case tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode: valid merge tag -->\n <span\n v-else-if=\"isValid\"\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayKeyword }}\n </span>\n <!-- Display mode: invalid (plain text) -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ node.attrs.value }}\n </span>\n <button\n v-if=\"isValid\"\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","import type { Editor } from \"@tiptap/core\";\n\n/**\n * Decides whether MergeTagNode / LogicMergeTagNode should swallow a\n * Backspace/Delete keystroke (returning `true` from a TipTap keymap\n * suppresses the default behavior).\n *\n * Only suppresses in the cursor-adjacent case so the \"first press\n * selects the atom, second press deletes it\" UX works. Range selections\n * (Cmd+A, drag-select, double-click-word) explicitly fall through —\n * default `replaceSelection` then deletes the entire range including\n * any merge-tag atoms inside it.\n *\n * Earlier revisions also returned `true` whenever `nodesBetween($from,\n * $to)` found a merge tag anywhere in the selection range — which\n * silently broke Cmd+A + Backspace for any paragraph containing a\n * merge tag (entire deletion was cancelled, nothing happened). The\n * cursor-adjacent protection is the only piece that should survive.\n */\nexport function isNodeSelected(editor: Editor, nodeTypeName: string): boolean {\n const { $from, $to } = editor.state.selection;\n\n // Range selection: let TipTap's default range-delete run. Even when\n // the range contains atom nodes, ProseMirror's replaceSelection\n // handles atomic deletion correctly.\n if ($from.pos !== $to.pos) return false;\n\n // Cursor-adjacent atom: protect from single-keystroke deletion.\n // (`$from.pos > 0` guards against reading `nodeBefore` at doc start\n // where ProseMirror's index would be undefined for nodeBefore.)\n if ($from.pos > 0 && $from.nodeBefore?.type.name === nodeTypeName) {\n return true;\n }\n if ($from.nodeAfter?.type.name === nodeTypeName) {\n return true;\n }\n\n return false;\n}\n","import type { Component } from \"vue\";\nimport { VueNodeViewRenderer } from \"@tiptap/vue-3\";\n\n/**\n * Typed wrapper for VueNodeViewRenderer that handles the known type mismatch\n * between Vue SFC default exports and TipTap's expected component type.\n */\nexport function renderVueNodeView(component: Component) {\n return VueNodeViewRenderer(\n component as Parameters<typeof VueNodeViewRenderer>[0],\n );\n}\n","import LogicMergeTagNodeView from \"./LogicMergeTagNodeView.vue\";\nimport type { SyntaxPreset } from \"@templatical/types\";\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n SYNTAX_PRESETS,\n} from \"@templatical/types\";\nimport { InputRule, mergeAttributes, Node, PasteRule } from \"@tiptap/core\";\nimport { isNodeSelected } from \"./isNodeSelected\";\nimport { renderVueNodeView } from \"./renderVueNodeView\";\n\nexport interface LogicMergeTagNodeOptions {\n syntax: SyntaxPreset;\n}\n\nexport const LogicMergeTagNode = Node.create<LogicMergeTagNodeOptions>({\n name: \"logicMergeTagNode\",\n\n group: \"inline\",\n\n inline: true,\n\n atom: true,\n\n addOptions() {\n return {\n syntax: SYNTAX_PRESETS.liquid,\n };\n },\n\n addAttributes() {\n return {\n // Both attributes are state, not markup, same as `MergeTagNode`:\n // `renderHTML()` emits the canonical `data-logic-merge-tag` /\n // `data-keyword` pair and `parseHTML` reads only those. `rendered: false`\n // keeps TipTap from also serializing each attribute under its own name,\n // which would put a write-only duplicate of the same data on every tag.\n value: {\n default: \"\",\n rendered: false,\n parseHTML: (element) =>\n element.getAttribute(\"data-logic-merge-tag\") || \"\",\n },\n keyword: {\n default: \"\",\n rendered: false,\n parseHTML: (element) =>\n element.getAttribute(\"data-keyword\") || element.textContent || \"\",\n },\n };\n },\n\n parseHTML() {\n return [\n {\n tag: \"span[data-logic-merge-tag]\",\n },\n ];\n },\n\n renderHTML({ node, HTMLAttributes }) {\n if (!isLogicMergeTagValue(node.attrs.value, this.options.syntax)) {\n return [\"span\", {}, node.attrs.value];\n }\n\n const keyword = getLogicMergeTagKeyword(\n node.attrs.value,\n this.options.syntax,\n );\n\n return [\n \"span\",\n mergeAttributes(HTMLAttributes, {\n \"data-logic-merge-tag\": node.attrs.value,\n \"data-keyword\": keyword,\n }),\n keyword,\n ];\n },\n\n addNodeView() {\n return renderVueNodeView(LogicMergeTagNodeView);\n },\n\n addKeyboardShortcuts() {\n return {\n Backspace: () => isNodeSelected(this.editor, this.name),\n Delete: () => isNodeSelected(this.editor, this.name),\n };\n },\n\n addInputRules() {\n const inputRegex = new RegExp(this.options.syntax.logic.source + \"$\", \"\");\n\n return [\n new InputRule({\n find: inputRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n if (!isLogicMergeTagValue(fullValue, this.options.syntax)) {\n return;\n }\n\n const keyword = getLogicMergeTagKeyword(\n fullValue,\n this.options.syntax,\n );\n\n const node = this.type.create({\n value: fullValue,\n keyword,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n\n addPasteRules() {\n const pasteRegex = new RegExp(this.options.syntax.logic.source, \"g\");\n\n return [\n new PasteRule({\n find: pasteRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n if (!isLogicMergeTagValue(fullValue, this.options.syntax)) {\n return;\n }\n\n const keyword = getLogicMergeTagKeyword(\n fullValue,\n this.options.syntax,\n );\n\n const node = this.type.create({\n value: fullValue,\n keyword,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n});\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n label: string;\n value: string;\n };\n };\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { getMergeTagLabel } = useMergeTag();\nconst { t } = useI18n();\n\nconst displayLabel = computed(() => getMergeTagLabel(props.node.attrs.value));\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n const newValue = editValue.value.trim();\n if (newValue && newValue !== props.node.attrs.value) {\n // Update with new value and derive label from it\n props.updateAttributes({\n value: newValue,\n label: getMergeTagLabel(newValue),\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n class=\"tpl-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium tpl:select-none tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-primary) 20%, transparent);\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-32 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayLabel }}\n </span>\n <button\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useMergeTag } from \"../composables/useMergeTag\";\nimport { NodeViewWrapper } from \"@tiptap/vue-3\";\nimport { computed, nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n node: {\n attrs: {\n label: string;\n value: string;\n };\n };\n deleteNode: () => void;\n updateAttributes: (attrs: Record<string, unknown>) => void;\n}>();\n\nconst { getMergeTagLabel } = useMergeTag();\nconst { t } = useI18n();\n\nconst displayLabel = computed(() => getMergeTagLabel(props.node.attrs.value));\n\nconst isEditing = ref(false);\nconst editValue = ref(\"\");\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nfunction startEditing(): void {\n editValue.value = props.node.attrs.value;\n isEditing.value = true;\n nextTick(() => {\n inputRef.value?.focus();\n inputRef.value?.select();\n });\n}\n\nfunction finishEditing(): void {\n const newValue = editValue.value.trim();\n if (newValue && newValue !== props.node.attrs.value) {\n // Update with new value and derive label from it\n props.updateAttributes({\n value: newValue,\n label: getMergeTagLabel(newValue),\n });\n }\n isEditing.value = false;\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n finishEditing();\n } else if (event.key === \"Escape\") {\n isEditing.value = false;\n }\n}\n</script>\n\n<template>\n <NodeViewWrapper\n as=\"span\"\n class=\"tpl-merge-tag-node tpl:group tpl:mx-0.5 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium tpl:select-none tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-primary) 20%, transparent);\n \"\n contenteditable=\"false\"\n >\n <!-- Edit mode -->\n <input\n v-if=\"isEditing\"\n ref=\"inputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"tpl:w-32 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-0.5 tpl:py-0 tpl:text-[1em] tpl:font-medium tpl:outline-none tpl:text-[var(--tpl-primary)]\"\n @blur=\"finishEditing\"\n @keydown=\"handleKeydown\"\n />\n <!-- Display mode -->\n <span\n v-else\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.editValue\"\n class=\"tpl-tooltip tpl:cursor-pointer\"\n :data-tooltip=\"node.attrs.value\"\n @click.stop=\"startEditing\"\n @keydown.enter.stop=\"startEditing\"\n @keydown.space.prevent.stop=\"startEditing\"\n >\n {{ displayLabel }}\n </span>\n <button\n type=\"button\"\n :aria-label=\"t.mergeTag.deleteMergeTag\"\n class=\"tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all tpl:hover:opacity-100 tpl:text-[var(--tpl-primary)]\"\n contenteditable=\"false\"\n @click.stop.prevent=\"deleteNode\"\n >\n <svg\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n aria-hidden=\"true\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </NodeViewWrapper>\n</template>\n","import MergeTagNodeView from \"./MergeTagNodeView.vue\";\nimport type { MergeTag, SyntaxPreset } from \"@templatical/types\";\nimport { getMergeTagLabel, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { InputRule, mergeAttributes, Node, PasteRule } from \"@tiptap/core\";\nimport { isNodeSelected } from \"./isNodeSelected\";\nimport { renderVueNodeView } from \"./renderVueNodeView\";\n\nexport interface MergeTagNodeOptions {\n mergeTags: MergeTag[];\n syntax: SyntaxPreset;\n}\n\n/**\n * Inserted-node attrs are intentionally limited to `label` + `value`. The\n * `MergeTag` interface also carries optional `group` / `description` for\n * the built-in picker, but those are display-only and must NOT leak into\n * the document JSON — they would bloat templates and pollute serialized\n * state. `Pick<>` makes the constraint explicit at the type level.\n */\nexport type InsertMergeTagAttrs = Pick<MergeTag, \"label\" | \"value\">;\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n mergeTagNode: {\n insertMergeTag: (attrs: InsertMergeTagAttrs) => ReturnType;\n };\n }\n}\n\nexport const MergeTagNode = Node.create<MergeTagNodeOptions>({\n name: \"mergeTagNode\",\n\n group: \"inline\",\n\n inline: true,\n\n atom: true,\n\n addOptions() {\n return {\n mergeTags: [],\n syntax: SYNTAX_PRESETS.liquid,\n };\n },\n\n addAttributes() {\n return {\n // Both attributes are state, not markup: `renderHTML()` below emits the\n // canonical `data-merge-tag` / `data-label` pair and `parseHTML` reads\n // only those. `rendered: false` keeps TipTap from also serializing each\n // attribute under its own name, which would put a write-only duplicate\n // of the same data on every tag — invalid on a `<span>`, and paid again\n // through storage, autosave, snapshots and version history.\n label: {\n default: \"\",\n rendered: false,\n parseHTML: (element) =>\n element.getAttribute(\"data-label\") || element.textContent || \"\",\n },\n value: {\n default: \"\",\n rendered: false,\n parseHTML: (element) => element.getAttribute(\"data-merge-tag\") || \"\",\n },\n };\n },\n\n parseHTML() {\n return [\n {\n tag: \"span[data-merge-tag]\",\n },\n ];\n },\n\n renderHTML({ node, HTMLAttributes }) {\n const label = getMergeTagLabel(node.attrs.value, this.options.mergeTags);\n\n return [\n \"span\",\n mergeAttributes(HTMLAttributes, {\n \"data-merge-tag\": node.attrs.value,\n \"data-label\": label,\n }),\n label,\n ];\n },\n\n addNodeView() {\n return renderVueNodeView(MergeTagNodeView);\n },\n\n addCommands() {\n return {\n insertMergeTag:\n (attrs: InsertMergeTagAttrs) =>\n ({ commands }) => {\n // Whitelist explicitly — even if a caller smuggles extra keys via\n // a wider type assertion, only label/value reach the node attrs.\n return commands.insertContent({\n type: this.name,\n attrs: { label: attrs.label, value: attrs.value },\n });\n },\n };\n },\n\n addKeyboardShortcuts() {\n return {\n Backspace: () => isNodeSelected(this.editor, this.name),\n Delete: () => isNodeSelected(this.editor, this.name),\n };\n },\n\n addInputRules() {\n const inputRegex = new RegExp(this.options.syntax.value.source + \"$\", \"\");\n\n return [\n new InputRule({\n find: inputRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n const label = getMergeTagLabel(fullValue, this.options.mergeTags);\n\n const node = this.type.create({\n label,\n value: fullValue,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n\n addPasteRules() {\n const pasteRegex = new RegExp(this.options.syntax.value.source, \"g\");\n\n return [\n new PasteRule({\n find: pasteRegex,\n handler: ({ state, range, match }) => {\n const fullValue = match[0];\n const label = getMergeTagLabel(fullValue, this.options.mergeTags);\n\n const node = this.type.create({\n label,\n value: fullValue,\n });\n\n state.tr.replaceWith(range.from, range.to, node);\n },\n }),\n ];\n },\n});\n"],"mappings":"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"}
|
|
@@ -3,10 +3,10 @@ import { E as e } from "./draggable-BfpsXFg7.js";
|
|
|
3
3
|
var t = Symbol("translations"), n = Symbol("editor"), r = Symbol("history"), i = Symbol("blockActions"), a = Symbol("conditionPreview"), o = Symbol("appliesConditionFilter"), s = Symbol("fontsManager"), c = Symbol("themeStyles"), l = Symbol("tplUiTheme"), u = Symbol("blockDefaults"), d = Symbol("blockRegistry"), f = Symbol("customBlockDefinitions"), p = Symbol("paletteBlocks"), m = Symbol("htmlBlockPreview"), h = Symbol("colors"), g = Symbol("customBlockStylesheets"), _ = Symbol("mergeTags"), v = Symbol("mergeTagSyntax"), y = Symbol("onRequestMergeTag"), b = Symbol("mergeTagAutocomplete"), x = Symbol("mergeTagSampleMode"), S = Symbol("useMergeTagSamples"), C = Symbol("resolvePreview"), w = Symbol("previewResolution"), T = Symbol("mergeTagPicker"), E = Symbol("logicTags"), D = Symbol("logicPairs"), O = Symbol("logicPicker"), k = Symbol("onRequestLogic"), A = Symbol("onRequestMedia"), j = Symbol("imageUrlResolver"), M = Symbol("displayConditions"), N = Symbol("allowCustomConditions"), P = Symbol("capabilities"), F = Symbol("keyboardReorder"), I = Symbol("templateLint"), L = Symbol("editorRoot"), R = Symbol("popoverRoot"), z = Symbol("savedBlocks"), B = Symbol("authManager"), V = Symbol("aiConfig"), H = Symbol("scoring"), U = Symbol("cloudTranslations");
|
|
4
4
|
function W(t, n) {
|
|
5
5
|
let r = e(t, null);
|
|
6
|
-
if (r == null) throw Error(`${n} requires a provider for ${t.description ?? "unknown key"}. Ensure it
|
|
6
|
+
if (r == null) throw Error(`${n} requires a provider for ${t.description ?? "unknown key"}. Ensure it renders inside the editor, below Editor.vue.`);
|
|
7
7
|
return r;
|
|
8
8
|
}
|
|
9
9
|
//#endregion
|
|
10
10
|
export { k as A, c as B, E as C, T as D, b as E, w as F, l as H, C as I, z as L, y as M, p as N, x as O, R as P, H as R, D as S, _ as T, S as U, t as V, W, s as _, i as a, j as b, P as c, a as d, f, L as g, n as h, B as i, A as j, v as k, U as l, M as m, N as n, u as o, g as p, o as r, d as s, V as t, h as u, r as v, O as w, F as x, m as y, I as z };
|
|
11
11
|
|
|
12
|
-
//# sourceMappingURL=keys-
|
|
12
|
+
//# sourceMappingURL=keys-oJdkdtPn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys-CbQuRX85.js","names":[],"sources":["../../../src/keys.ts"],"sourcesContent":["import { inject, type InjectionKey, type ComputedRef, type Ref } from \"vue\";\nimport type {\n UseHistoryReturn,\n UseBlockActionsReturn,\n UseConditionPreviewReturn,\n} from \"@templatical/core\";\nimport type {\n BlockDefaults,\n CustomBlockDefinition,\n DisplayCondition,\n LogicPair,\n LogicTag,\n MergeTag,\n ResolvePreview,\n SyntaxPreset,\n} from \"@templatical/types\";\nimport type { Translations, CloudTranslations } from \"./i18n\";\nimport type {\n AuthManager,\n UseAiConfigReturn,\n UseTemplateScoringReturn,\n} from \"@templatical/core/cloud\";\nimport type { UseSavedBlocksReturn } from \"@templatical/core\";\nimport type { BaseEditorReturn } from \"./composables/useEditorCore\";\nimport type { ImageUrlResolver } from \"./composables/useImageUrlResolver\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\nimport type { UseBlockRegistryReturn } from \"./composables/useBlockRegistry\";\nimport type { UseKeyboardReorderReturn } from \"./composables/useKeyboardReorder\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { OnRequestMedia } from \"./index\";\n\n// ---------------------------------------------------------------------------\n// Shared keys (provided by useEditorCore, consumed by all components)\n// ---------------------------------------------------------------------------\n\nexport const TRANSLATIONS_KEY: InjectionKey<Translations> =\n Symbol(\"translations\");\n\nexport const EDITOR_KEY: InjectionKey<BaseEditorReturn> = Symbol(\"editor\");\n\nexport const HISTORY_KEY: InjectionKey<UseHistoryReturn> = Symbol(\"history\");\n\nexport const BLOCK_ACTIONS_KEY: InjectionKey<UseBlockActionsReturn> =\n Symbol(\"blockActions\");\n\nexport const CONDITION_PREVIEW_KEY: InjectionKey<UseConditionPreviewReturn> =\n Symbol(\"conditionPreview\");\n\n/**\n * Whether the hand-toggled display-condition filter applies on this surface —\n * provided per-surface, exactly like `USE_MERGE_TAG_SAMPLES_KEY`, and for the\n * same reason: the rule folds in both `previewMode` and whether a resolver owns\n * the preview, so a component that re-derived it from `CONDITION_PREVIEW_KEY`\n * plus the editor's `previewMode` could get one half wrong on its own.\n *\n * Absent means \"applies\" — a surface with no provider filters as it always has.\n */\nexport const APPLIES_CONDITION_FILTER_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"appliesConditionFilter\");\n\nexport const FONTS_MANAGER_KEY: InjectionKey<UseFontsReturn> =\n Symbol(\"fontsManager\");\n\nexport const THEME_STYLES_KEY: InjectionKey<\n ComputedRef<Record<string, string>>\n> = Symbol(\"themeStyles\");\n\nexport const UI_THEME_KEY: InjectionKey<ComputedRef<string>> =\n Symbol(\"tplUiTheme\");\n\nexport const BLOCK_DEFAULTS_KEY: InjectionKey<BlockDefaults | undefined> =\n Symbol(\"blockDefaults\");\n\nexport const BLOCK_REGISTRY_KEY: InjectionKey<UseBlockRegistryReturn> =\n Symbol(\"blockRegistry\");\n\nexport const CUSTOM_BLOCK_DEFINITIONS_KEY: InjectionKey<\n CustomBlockDefinition[]\n> = Symbol(\"customBlockDefinitions\");\n\n/**\n * Consumer-supplied block-palette allowlist + order (`config.paletteBlocks`).\n * `undefined` means the default full palette. Consumed by `Sidebar.vue`.\n */\nexport const PALETTE_BLOCKS_KEY: InjectionKey<string[] | undefined> =\n Symbol(\"paletteBlocks\");\n\n/**\n * Whether HTML blocks render a live sandboxed-iframe preview in the canvas\n * (`config.htmlBlockPreview`), normalized to a boolean. `false` (the default)\n * keeps the static placeholder card. Consumed by `HtmlBlock.vue`.\n */\nexport const HTML_BLOCK_PREVIEW_KEY: InjectionKey<boolean> =\n Symbol(\"htmlBlockPreview\");\n\n/**\n * Consumer-supplied color-picker palette (`config.colors`), normalized to\n * `{ presets, allowCustom, allowCustomIgnored }` by `resolveColorsConfig`.\n * Provided by `useEditorCore`, consumed by every `ColorPicker`. The inject\n * default (`DEFAULT_RESOLVED_COLORS` = `{ presets: [], allowCustom: true,\n * allowCustomIgnored: false }`) makes an unconfigured picker render exactly\n * as before.\n */\nexport const COLORS_KEY: InjectionKey<\n import(\"./utils/resolveColorsConfig\").ResolvedColors\n> = Symbol(\"colors\");\n\nexport const CUSTOM_BLOCK_STYLESHEETS_KEY: InjectionKey<ComputedRef<string[]>> =\n Symbol(\"customBlockStylesheets\");\n\nexport const MERGE_TAGS_KEY: InjectionKey<MergeTag[]> = Symbol(\"mergeTags\");\n\nexport const MERGE_TAG_SYNTAX_KEY: InjectionKey<SyntaxPreset> =\n Symbol(\"mergeTagSyntax\");\n\nexport const ON_REQUEST_MERGE_TAG_KEY: InjectionKey<\n (() => Promise<MergeTag | null>) | null\n> = Symbol(\"onRequestMergeTag\");\n\nexport const MERGE_TAG_AUTOCOMPLETE_KEY: InjectionKey<boolean> = Symbol(\n \"mergeTagAutocomplete\",\n);\n\n/**\n * The user's choice between the previews' two merge-tag views: `true` for\n * Sample (tags replaced by their `MergeTag.sample`, rendered as ordinary text)\n * and `false` for Label (tags shown as their label with the usual cue).\n *\n * Session state owned by `useEditorCore` and driven by a toggle that exists\n * only on preview surfaces. Defaults to Sample.\n */\nexport const MERGE_TAG_SAMPLE_MODE_KEY: InjectionKey<Ref<boolean>> =\n Symbol(\"mergeTagSampleMode\");\n\n/**\n * Whether **the current render surface** substitutes samples.\n *\n * Provided per surface rather than globally, and that is the whole point:\n * substitution must never happen while editing, so `Canvas.vue` folds in its\n * `previewMode` flag and provides `false` whenever the canvas is editable, no\n * matter what the mode ref says. Preview surfaces provide the mode directly.\n *\n * Block components inject **this** key. They must never read\n * `MERGE_TAG_SAMPLE_MODE_KEY`, which would bypass the editing guard.\n */\nexport const USE_MERGE_TAG_SAMPLES_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"useMergeTagSamples\");\n\n/**\n * The `resolvePreview` seam — resolved content plus its loading and failure\n * state. Provided by `useEditorCore` so every preview surface shares one\n * lifecycle; `null` when the consumer configured no hook.\n *\n * Surfaces read `content` from here instead of `editor.content`, and gate the\n * Sample/Label toggle on `supersedesSamples`.\n */\n/**\n * The raw `config.resolvePreview` hook.\n *\n * Provided alongside PREVIEW_RESOLUTION_KEY because the test-email dialog needs\n * its **own** resolution instance — it resolves for the selected recipient,\n * which the editor's canvas has no notion of — and so cannot reuse the shared\n * one. `undefined` when the consumer configured no hook.\n */\nexport const RESOLVE_PREVIEW_KEY: InjectionKey<ResolvePreview | undefined> =\n Symbol(\"resolvePreview\");\n\nexport const PREVIEW_RESOLUTION_KEY: InjectionKey<\n import(\"./composables/usePreviewResolution\").UsePreviewResolutionReturn | null\n> = Symbol(\"previewResolution\");\n\n/**\n * Singleton state for the built-in merge tag picker modal. Provided by\n * `useEditorCore`, consumed by `useMergeTag.requestMergeTag()` (to open\n * the modal as a fall-through when only `mergeTags.tags` is configured)\n * and by `MergeTagPickerModal.vue` (to render and resolve).\n */\nexport const MERGE_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useMergeTagPicker\").UseMergeTagPickerReturn\n> = Symbol(\"mergeTagPicker\");\n\n// ---------------------------------------------------------------------------\n// Logic tags — a standalone feature, separate from merge tags. Native\n// highlighting (LogicMergeTagNode) is always on; these drive the dedicated\n// logic picker + insertion.\n// ---------------------------------------------------------------------------\n\nexport const LOGIC_TAGS_KEY: InjectionKey<LogicTag[]> = Symbol(\"logicTags\");\n\nexport const LOGIC_PAIRS_KEY: InjectionKey<LogicPair[]> = Symbol(\"logicPairs\");\n\n/**\n * Singleton state for the built-in logic picker modal. Provided by\n * `useEditorCore`, consumed by `useLogicTag.requestLogicTag()` (to open the modal)\n * and by `LogicTagPickerModal.vue` (to render and resolve).\n */\nexport const LOGIC_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useLogicTagPicker\").UseLogicTagPickerReturn\n> = Symbol(\"logicPicker\");\n\nexport const ON_REQUEST_LOGIC_TAG_KEY: InjectionKey<\n (() => Promise<LogicTag | LogicPair | null>) | null\n> = Symbol(\"onRequestLogic\");\n\nexport const ON_REQUEST_MEDIA_KEY: InjectionKey<OnRequestMedia | null> =\n Symbol(\"onRequestMedia\");\n\n/**\n * Per-editor display-only image URL resolver (`config.resolveImageUrl`),\n * wrapped with a per-src cache by `createImageUrlResolver`. `null` when the\n * host doesn't resolve — the canvas then displays canonical src values\n * verbatim. Consumed via `useResolvedImageSrc` in image-displaying blocks.\n */\nexport const IMAGE_URL_RESOLVER_KEY: InjectionKey<ImageUrlResolver | null> =\n Symbol(\"imageUrlResolver\");\n\nexport const DISPLAY_CONDITIONS_KEY: InjectionKey<DisplayCondition[]> =\n Symbol(\"displayConditions\");\n\nexport const ALLOW_CUSTOM_CONDITIONS_KEY: InjectionKey<boolean> = Symbol(\n \"allowCustomConditions\",\n);\n\nexport const CAPABILITIES_KEY: InjectionKey<EditorCapabilities> =\n Symbol(\"capabilities\");\n\nexport const KEYBOARD_REORDER_KEY: InjectionKey<UseKeyboardReorderReturn> =\n Symbol(\"keyboardReorder\");\n\nexport const TEMPLATE_LINT_KEY: InjectionKey<\n import(\"./composables/useTemplateLint\").UseTemplateLintReturn | null\n> = Symbol(\"templateLint\");\n\n/**\n * The editor's effective DOM root — `Document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true`. Composables that read `document.activeElement`,\n * popup mount targets, etc. should inject this and use the root-aware API\n * (both `Document` and `ShadowRoot` expose `activeElement` etc.).\n */\nexport const EDITOR_ROOT_KEY: InjectionKey<Document | ShadowRoot> =\n Symbol(\"editorRoot\");\n\n/**\n * Mount target for popovers, toolbars, and modal dialogs — never\n * `document.body`. Provided by `useEditorCore` as a ref bound\n * to a `<div class=\"tpl-popover-root\" />` rendered at the top level of the\n * editor template. Teleports use `:to=\"popoverRoot\"` so popups land inside\n * the editor's effective root — `document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true` — instead of escaping the shadow boundary.\n *\n * Null until the editor's template mounts; consumers must guard with\n * `v-if=\"popoverRoot\"` before passing to `<Teleport>`.\n */\nexport const POPOVER_ROOT_KEY: InjectionKey<Ref<HTMLElement | null>> =\n Symbol(\"popoverRoot\");\n\n/**\n * Reactive saved-blocks state, provided by `useSavedBlocksFeature` when a\n * `SavedBlocksProvider` is configured. Shared by OSS and Cloud — the storage\n * transport lives behind the provider, so this key is not cloud-specific.\n */\nexport const SAVED_BLOCKS_KEY: InjectionKey<UseSavedBlocksReturn> =\n Symbol(\"savedBlocks\");\n\n// ---------------------------------------------------------------------------\n// Cloud-only keys (provided by CloudEditor, consumed by cloud components)\n// ---------------------------------------------------------------------------\n\nexport const AUTH_MANAGER_KEY: InjectionKey<AuthManager> =\n Symbol(\"authManager\");\n\nexport const AI_CONFIG_KEY: InjectionKey<UseAiConfigReturn> =\n Symbol(\"aiConfig\");\n\n// Comments are deliberately not injected: the panel takes `useCommentsFeature`'s\n// return as a **prop** the way `TestEmailPanel` and `SavedBlocksPanels` do, and the\n// per-block indicators read the comments entry on `CAPABILITIES_KEY`. Nothing\n// injects the composable itself.\n\nexport const SCORING_KEY: InjectionKey<UseTemplateScoringReturn> =\n Symbol(\"scoring\");\n\nexport const CLOUD_TRANSLATIONS_KEY: InjectionKey<CloudTranslations> =\n Symbol(\"cloudTranslations\");\n\n// ---------------------------------------------------------------------------\n// Helper for required injections with explicit null default + throw\n// ---------------------------------------------------------------------------\n\nexport function requireInject<T>(\n key: InjectionKey<T>,\n componentName: string,\n): T {\n const value = inject(key, null) as T | null;\n if (value === null || value === undefined) {\n throw new Error(\n `${componentName} requires a provider for ${key.description ?? \"unknown key\"}. Ensure it is a descendant of Editor or CloudEditor.`,\n );\n }\n return value;\n}\n"],"mappings":";;AAmCA,IAAa,IACX,OAAO,cAAc,GAEV,IAA6C,OAAO,QAAQ,GAE5D,IAA8C,OAAO,SAAS,GAE9D,IACX,OAAO,cAAc,GAEV,IACX,OAAO,kBAAkB,GAWd,IACX,OAAO,wBAAwB,GAEpB,IACX,OAAO,cAAc,GAEV,IAET,OAAO,aAAa,GAEX,IACX,OAAO,YAAY,GAER,IACX,OAAO,eAAe,GAEX,IACX,OAAO,eAAe,GAEX,IAET,OAAO,wBAAwB,GAMtB,IACX,OAAO,eAAe,GAOX,IACX,OAAO,kBAAkB,GAUd,IAET,OAAO,QAAQ,GAEN,IACX,OAAO,wBAAwB,GAEpB,IAA2C,OAAO,WAAW,GAE7D,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAEjB,IAAoD,OAC/D,sBACF,GAUa,IACX,OAAO,oBAAoB,GAahB,IACX,OAAO,oBAAoB,GAkBhB,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAQjB,IAET,OAAO,gBAAgB,GAQd,IAA2C,OAAO,WAAW,GAE7D,IAA6C,OAAO,YAAY,GAOhE,IAET,OAAO,aAAa,GAEX,IAET,OAAO,gBAAgB,GAEd,IACX,OAAO,gBAAgB,GAQZ,IACX,OAAO,kBAAkB,GAEd,IACX,OAAO,mBAAmB,GAEf,IAAqD,OAChE,uBACF,GAEa,IACX,OAAO,cAAc,GAEV,IACX,OAAO,iBAAiB,GAEb,IAET,OAAO,cAAc,GAQZ,IACX,OAAO,YAAY,GAaR,IACX,OAAO,aAAa,GAOT,IACX,OAAO,aAAa,GAMT,IACX,OAAO,aAAa,GAET,IACX,OAAO,UAAU,GAON,IACX,OAAO,SAAS,GAEL,IACX,OAAO,mBAAmB;AAM5B,SAAgB,EACd,GACA,GACG;CACH,IAAM,IAAQ,EAAO,GAAK,IAAI;CAC9B,IAAI,KAAU,MACZ,MAAU,MACR,GAAG,EAAc,2BAA2B,EAAI,eAAe,cAAc,sDAC/E;CAEF,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"keys-oJdkdtPn.js","names":[],"sources":["../../../src/keys.ts"],"sourcesContent":["import { inject, type InjectionKey, type ComputedRef, type Ref } from \"vue\";\nimport type {\n UseHistoryReturn,\n UseBlockActionsReturn,\n UseConditionPreviewReturn,\n} from \"@templatical/core\";\nimport type {\n BlockDefaults,\n CustomBlockDefinition,\n DisplayCondition,\n LogicPair,\n LogicTag,\n MergeTag,\n ResolvePreview,\n SyntaxPreset,\n} from \"@templatical/types\";\nimport type { Translations, CloudTranslations } from \"./i18n\";\nimport type {\n AuthManager,\n UseAiConfigReturn,\n UseTemplateScoringReturn,\n} from \"@templatical/core/cloud\";\nimport type { UseSavedBlocksReturn } from \"@templatical/core\";\nimport type { BaseEditorReturn } from \"./composables/useEditorCore\";\nimport type { ImageUrlResolver } from \"./composables/useImageUrlResolver\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\nimport type { UseBlockRegistryReturn } from \"./composables/useBlockRegistry\";\nimport type { UseKeyboardReorderReturn } from \"./composables/useKeyboardReorder\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { OnRequestMedia } from \"./index\";\n\n// ---------------------------------------------------------------------------\n// Shared keys (provided by useEditorCore, consumed by all components)\n// ---------------------------------------------------------------------------\n\nexport const TRANSLATIONS_KEY: InjectionKey<Translations> =\n Symbol(\"translations\");\n\nexport const EDITOR_KEY: InjectionKey<BaseEditorReturn> = Symbol(\"editor\");\n\nexport const HISTORY_KEY: InjectionKey<UseHistoryReturn> = Symbol(\"history\");\n\nexport const BLOCK_ACTIONS_KEY: InjectionKey<UseBlockActionsReturn> =\n Symbol(\"blockActions\");\n\nexport const CONDITION_PREVIEW_KEY: InjectionKey<UseConditionPreviewReturn> =\n Symbol(\"conditionPreview\");\n\n/**\n * Whether the hand-toggled display-condition filter applies on this surface —\n * provided per-surface, exactly like `USE_MERGE_TAG_SAMPLES_KEY`, and for the\n * same reason: the rule folds in both `previewMode` and whether a resolver owns\n * the preview, so a component that re-derived it from `CONDITION_PREVIEW_KEY`\n * plus the editor's `previewMode` could get one half wrong on its own.\n *\n * Absent means \"applies\" — a surface with no provider filters as it always has.\n */\nexport const APPLIES_CONDITION_FILTER_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"appliesConditionFilter\");\n\nexport const FONTS_MANAGER_KEY: InjectionKey<UseFontsReturn> =\n Symbol(\"fontsManager\");\n\nexport const THEME_STYLES_KEY: InjectionKey<\n ComputedRef<Record<string, string>>\n> = Symbol(\"themeStyles\");\n\nexport const UI_THEME_KEY: InjectionKey<ComputedRef<string>> =\n Symbol(\"tplUiTheme\");\n\nexport const BLOCK_DEFAULTS_KEY: InjectionKey<BlockDefaults | undefined> =\n Symbol(\"blockDefaults\");\n\nexport const BLOCK_REGISTRY_KEY: InjectionKey<UseBlockRegistryReturn> =\n Symbol(\"blockRegistry\");\n\nexport const CUSTOM_BLOCK_DEFINITIONS_KEY: InjectionKey<\n CustomBlockDefinition[]\n> = Symbol(\"customBlockDefinitions\");\n\n/**\n * Consumer-supplied block-palette allowlist + order (`config.paletteBlocks`).\n * `undefined` means the default full palette. Consumed by `Sidebar.vue`.\n */\nexport const PALETTE_BLOCKS_KEY: InjectionKey<string[] | undefined> =\n Symbol(\"paletteBlocks\");\n\n/**\n * Whether HTML blocks render a live sandboxed-iframe preview in the canvas\n * (`config.htmlBlockPreview`), normalized to a boolean. `false` (the default)\n * keeps the static placeholder card. Consumed by `HtmlBlock.vue`.\n */\nexport const HTML_BLOCK_PREVIEW_KEY: InjectionKey<boolean> =\n Symbol(\"htmlBlockPreview\");\n\n/**\n * Consumer-supplied color-picker palette (`config.colors`), normalized to\n * `{ presets, allowCustom, allowCustomIgnored }` by `resolveColorsConfig`.\n * Provided by `useEditorCore`, consumed by every `ColorPicker`. The inject\n * default (`DEFAULT_RESOLVED_COLORS` = `{ presets: [], allowCustom: true,\n * allowCustomIgnored: false }`) makes an unconfigured picker render exactly\n * as before.\n */\nexport const COLORS_KEY: InjectionKey<\n import(\"./utils/resolveColorsConfig\").ResolvedColors\n> = Symbol(\"colors\");\n\nexport const CUSTOM_BLOCK_STYLESHEETS_KEY: InjectionKey<ComputedRef<string[]>> =\n Symbol(\"customBlockStylesheets\");\n\nexport const MERGE_TAGS_KEY: InjectionKey<MergeTag[]> = Symbol(\"mergeTags\");\n\nexport const MERGE_TAG_SYNTAX_KEY: InjectionKey<SyntaxPreset> =\n Symbol(\"mergeTagSyntax\");\n\nexport const ON_REQUEST_MERGE_TAG_KEY: InjectionKey<\n (() => Promise<MergeTag | null>) | null\n> = Symbol(\"onRequestMergeTag\");\n\nexport const MERGE_TAG_AUTOCOMPLETE_KEY: InjectionKey<boolean> = Symbol(\n \"mergeTagAutocomplete\",\n);\n\n/**\n * The user's choice between the previews' two merge-tag views: `true` for\n * Sample (tags replaced by their `MergeTag.sample`, rendered as ordinary text)\n * and `false` for Label (tags shown as their label with the usual cue).\n *\n * Session state owned by `useEditorCore` and driven by a toggle that exists\n * only on preview surfaces. Defaults to Sample.\n */\nexport const MERGE_TAG_SAMPLE_MODE_KEY: InjectionKey<Ref<boolean>> =\n Symbol(\"mergeTagSampleMode\");\n\n/**\n * Whether **the current render surface** substitutes samples.\n *\n * Provided per surface rather than globally, and that is the whole point:\n * substitution must never happen while editing, so `Canvas.vue` folds in its\n * `previewMode` flag and provides `false` whenever the canvas is editable, no\n * matter what the mode ref says. Preview surfaces provide the mode directly.\n *\n * Block components inject **this** key. They must never read\n * `MERGE_TAG_SAMPLE_MODE_KEY`, which would bypass the editing guard.\n */\nexport const USE_MERGE_TAG_SAMPLES_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"useMergeTagSamples\");\n\n/**\n * The `resolvePreview` seam — resolved content plus its loading and failure\n * state. Provided by `useEditorCore` so every preview surface shares one\n * lifecycle; `null` when the consumer configured no hook.\n *\n * Surfaces read `content` from here instead of `editor.content`, and gate the\n * Sample/Label toggle on `supersedesSamples`.\n */\n/**\n * The raw `config.resolvePreview` hook.\n *\n * Provided alongside PREVIEW_RESOLUTION_KEY because the test-email dialog needs\n * its **own** resolution instance — it resolves for the selected recipient,\n * which the editor's canvas has no notion of — and so cannot reuse the shared\n * one. `undefined` when the consumer configured no hook.\n */\nexport const RESOLVE_PREVIEW_KEY: InjectionKey<ResolvePreview | undefined> =\n Symbol(\"resolvePreview\");\n\nexport const PREVIEW_RESOLUTION_KEY: InjectionKey<\n import(\"./composables/usePreviewResolution\").UsePreviewResolutionReturn | null\n> = Symbol(\"previewResolution\");\n\n/**\n * Singleton state for the built-in merge tag picker modal. Provided by\n * `useEditorCore`, consumed by `useMergeTag.requestMergeTag()` (to open\n * the modal as a fall-through when only `mergeTags.tags` is configured)\n * and by `MergeTagPickerModal.vue` (to render and resolve).\n */\nexport const MERGE_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useMergeTagPicker\").UseMergeTagPickerReturn\n> = Symbol(\"mergeTagPicker\");\n\n// ---------------------------------------------------------------------------\n// Logic tags — a standalone feature, separate from merge tags. Native\n// highlighting (LogicMergeTagNode) is always on; these drive the dedicated\n// logic picker + insertion.\n// ---------------------------------------------------------------------------\n\nexport const LOGIC_TAGS_KEY: InjectionKey<LogicTag[]> = Symbol(\"logicTags\");\n\nexport const LOGIC_PAIRS_KEY: InjectionKey<LogicPair[]> = Symbol(\"logicPairs\");\n\n/**\n * Singleton state for the built-in logic picker modal. Provided by\n * `useEditorCore`, consumed by `useLogicTag.requestLogicTag()` (to open the modal)\n * and by `LogicTagPickerModal.vue` (to render and resolve).\n */\nexport const LOGIC_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useLogicTagPicker\").UseLogicTagPickerReturn\n> = Symbol(\"logicPicker\");\n\nexport const ON_REQUEST_LOGIC_TAG_KEY: InjectionKey<\n (() => Promise<LogicTag | LogicPair | null>) | null\n> = Symbol(\"onRequestLogic\");\n\nexport const ON_REQUEST_MEDIA_KEY: InjectionKey<OnRequestMedia | null> =\n Symbol(\"onRequestMedia\");\n\n/**\n * Per-editor display-only image URL resolver (`config.resolveImageUrl`),\n * wrapped with a per-src cache by `createImageUrlResolver`. `null` when the\n * host doesn't resolve — the canvas then displays canonical src values\n * verbatim. Consumed via `useResolvedImageSrc` in image-displaying blocks.\n */\nexport const IMAGE_URL_RESOLVER_KEY: InjectionKey<ImageUrlResolver | null> =\n Symbol(\"imageUrlResolver\");\n\nexport const DISPLAY_CONDITIONS_KEY: InjectionKey<DisplayCondition[]> =\n Symbol(\"displayConditions\");\n\nexport const ALLOW_CUSTOM_CONDITIONS_KEY: InjectionKey<boolean> = Symbol(\n \"allowCustomConditions\",\n);\n\nexport const CAPABILITIES_KEY: InjectionKey<EditorCapabilities> =\n Symbol(\"capabilities\");\n\nexport const KEYBOARD_REORDER_KEY: InjectionKey<UseKeyboardReorderReturn> =\n Symbol(\"keyboardReorder\");\n\nexport const TEMPLATE_LINT_KEY: InjectionKey<\n import(\"./composables/useTemplateLint\").UseTemplateLintReturn | null\n> = Symbol(\"templateLint\");\n\n/**\n * The editor's effective DOM root — `Document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true`. Composables that read `document.activeElement`,\n * popup mount targets, etc. should inject this and use the root-aware API\n * (both `Document` and `ShadowRoot` expose `activeElement` etc.).\n */\nexport const EDITOR_ROOT_KEY: InjectionKey<Document | ShadowRoot> =\n Symbol(\"editorRoot\");\n\n/**\n * Mount target for popovers, toolbars, and modal dialogs — never\n * `document.body`. Provided by `useEditorCore` as a ref bound\n * to a `<div class=\"tpl-popover-root\" />` rendered at the top level of the\n * editor template. Teleports use `:to=\"popoverRoot\"` so popups land inside\n * the editor's effective root — `document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true` — instead of escaping the shadow boundary.\n *\n * Null until the editor's template mounts; consumers must guard with\n * `v-if=\"popoverRoot\"` before passing to `<Teleport>`.\n */\nexport const POPOVER_ROOT_KEY: InjectionKey<Ref<HTMLElement | null>> =\n Symbol(\"popoverRoot\");\n\n/**\n * Reactive saved-blocks state, provided by `useSavedBlocksFeature` when a\n * `SavedBlocksProvider` is configured. Shared by OSS and Cloud — the storage\n * transport lives behind the provider, so this key is not cloud-specific.\n */\nexport const SAVED_BLOCKS_KEY: InjectionKey<UseSavedBlocksReturn> =\n Symbol(\"savedBlocks\");\n\n// ---------------------------------------------------------------------------\n// Cloud-only keys (provided by createCloudRuntime, consumed by cloud components)\n// ---------------------------------------------------------------------------\n\nexport const AUTH_MANAGER_KEY: InjectionKey<AuthManager> =\n Symbol(\"authManager\");\n\nexport const AI_CONFIG_KEY: InjectionKey<UseAiConfigReturn> =\n Symbol(\"aiConfig\");\n\n// Comments are deliberately not injected: the panel takes `useCommentsFeature`'s\n// return as a **prop** the way `TestEmailPanel` and `SavedBlocksPanels` do, and the\n// per-block indicators read the comments entry on `CAPABILITIES_KEY`. Nothing\n// injects the composable itself.\n\nexport const SCORING_KEY: InjectionKey<UseTemplateScoringReturn> =\n Symbol(\"scoring\");\n\nexport const CLOUD_TRANSLATIONS_KEY: InjectionKey<CloudTranslations> =\n Symbol(\"cloudTranslations\");\n\n// ---------------------------------------------------------------------------\n// Helper for required injections with explicit null default + throw\n// ---------------------------------------------------------------------------\n\nexport function requireInject<T>(\n key: InjectionKey<T>,\n componentName: string,\n): T {\n const value = inject(key, null) as T | null;\n if (value === null || value === undefined) {\n throw new Error(\n `${componentName} requires a provider for ${key.description ?? \"unknown key\"}. Ensure it renders inside the editor, below Editor.vue.`,\n );\n }\n return value;\n}\n"],"mappings":";;AAmCA,IAAa,IACX,OAAO,cAAc,GAEV,IAA6C,OAAO,QAAQ,GAE5D,IAA8C,OAAO,SAAS,GAE9D,IACX,OAAO,cAAc,GAEV,IACX,OAAO,kBAAkB,GAWd,IACX,OAAO,wBAAwB,GAEpB,IACX,OAAO,cAAc,GAEV,IAET,OAAO,aAAa,GAEX,IACX,OAAO,YAAY,GAER,IACX,OAAO,eAAe,GAEX,IACX,OAAO,eAAe,GAEX,IAET,OAAO,wBAAwB,GAMtB,IACX,OAAO,eAAe,GAOX,IACX,OAAO,kBAAkB,GAUd,IAET,OAAO,QAAQ,GAEN,IACX,OAAO,wBAAwB,GAEpB,IAA2C,OAAO,WAAW,GAE7D,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAEjB,IAAoD,OAC/D,sBACF,GAUa,IACX,OAAO,oBAAoB,GAahB,IACX,OAAO,oBAAoB,GAkBhB,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAQjB,IAET,OAAO,gBAAgB,GAQd,IAA2C,OAAO,WAAW,GAE7D,IAA6C,OAAO,YAAY,GAOhE,IAET,OAAO,aAAa,GAEX,IAET,OAAO,gBAAgB,GAEd,IACX,OAAO,gBAAgB,GAQZ,IACX,OAAO,kBAAkB,GAEd,IACX,OAAO,mBAAmB,GAEf,IAAqD,OAChE,uBACF,GAEa,IACX,OAAO,cAAc,GAEV,IACX,OAAO,iBAAiB,GAEb,IAET,OAAO,cAAc,GAQZ,IACX,OAAO,YAAY,GAaR,IACX,OAAO,aAAa,GAOT,IACX,OAAO,aAAa,GAMT,IACX,OAAO,aAAa,GAET,IACX,OAAO,UAAU,GAON,IACX,OAAO,SAAS,GAEL,IACX,OAAO,mBAAmB;AAM5B,SAAgB,EACd,GACA,GACG;CACH,IAAM,IAAQ,EAAO,GAAK,IAAI;CAC9B,IAAI,KAAU,MACZ,MAAU,MACR,GAAG,EAAc,2BAA2B,EAAI,eAAe,cAAc,yDAC/E;CAEF,OAAO;AACT"}
|