@templatical/editor 0.0.2 → 0.0.4
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-XBj5Rw2l.js → AiChatSidebar-CjfhTZwo.js} +1 -1
- package/dist/{AiFeatureMenu-my1mZ9DL.js → AiFeatureMenu-K44aZa_P.js} +3 -3
- package/dist/CloudEditor-DFyuRxUV.js +926 -0
- package/dist/{CollaboratorBar-BZq_Gv38.js → CollaboratorBar-BuCEcdbB.js} +25 -21
- package/dist/{CommentsSidebar-D9oxqO6l.js → CommentsSidebar-2lcqMfIP.js} +2 -2
- package/dist/{DesignReferenceSidebar-CyHq4SWt.js → DesignReferenceSidebar-CNMu4Zrx.js} +55 -54
- package/dist/{ModuleBrowserModal-Cus2Hdwl.js → ModuleBrowserModal-CvQ0xyQf.js} +6 -6
- package/dist/{ModulePreviewCanvas-DaByXKY_.js → ModulePreviewCanvas-Be2B3Y07.js} +3 -3
- package/dist/ParagraphEditor-CcMPnbDr.js +652 -0
- package/dist/{RichTextEditorContent-BRpjJ6SV.js → RichTextEditorContent-CHJlh7HJ.js} +8 -4
- package/dist/{SaveModuleDialog-BIZBQrd8.js → SaveModuleDialog-BaaeH5Xm.js} +5 -5
- package/dist/{SnapshotHistory-Ds1-QNbx.js → SnapshotHistory-BPfjiuu1.js} +2 -2
- package/dist/{TemplateScoringPanel-C8XSk_Ys.js → TemplateScoringPanel-D58A23Vq.js} +3 -3
- package/dist/{TestEmailModal-CCVfaFgV.js → TestEmailModal-DIAlB3e_.js} +3 -3
- package/dist/{TitleEditor-V4qaEULF.js → TitleEditor-D9DPjQkX.js} +7 -7
- package/dist/{TplModal-LT3FXlgs.js → TplModal-CmTSvCY-.js} +2 -2
- package/dist/{blockTypeIcons-BujoY5Dl.js → blockTypeIcons-D1RTWOkx.js} +1 -1
- package/dist/cdn/chunks/AiChatSidebar-CmPTbTFG.js +2 -0
- package/dist/cdn/chunks/AiFeatureMenu-lxVm1RjH.js +59 -0
- package/dist/cdn/chunks/AiFeatureMenu-lxVm1RjH.js.map +1 -0
- package/dist/cdn/chunks/CloudEditor-Bmp5IlWi.js +900 -0
- package/dist/cdn/chunks/CloudEditor-Bmp5IlWi.js.map +1 -0
- package/dist/cdn/chunks/CollaboratorBar-D2PKtlOw.js +51 -0
- package/dist/cdn/chunks/CollaboratorBar-D2PKtlOw.js.map +1 -0
- package/dist/cdn/chunks/CommentsSidebar-BOelj4Ca.js +2 -0
- package/dist/cdn/chunks/DesignReferenceSidebar-Bf6rg0A7.js +2 -0
- package/dist/cdn/chunks/ModuleBrowserModal-CxDXzkKS.js +195 -0
- package/dist/cdn/chunks/ModuleBrowserModal-CxDXzkKS.js.map +1 -0
- package/dist/cdn/chunks/ModulePreviewCanvas-DEfHampA.js +107 -0
- package/dist/cdn/chunks/ModulePreviewCanvas-DEfHampA.js.map +1 -0
- package/dist/cdn/chunks/ParagraphEditor-DHdu6lb3.js +503 -0
- package/dist/cdn/chunks/ParagraphEditor-DHdu6lb3.js.map +1 -0
- package/dist/cdn/chunks/RichTextEditorContent-DWUzizsC.js +106 -0
- package/dist/cdn/chunks/RichTextEditorContent-DWUzizsC.js.map +1 -0
- package/dist/cdn/chunks/SaveModuleDialog-DVna2xUl.js +119 -0
- package/dist/cdn/chunks/SaveModuleDialog-DVna2xUl.js.map +1 -0
- package/dist/cdn/chunks/SnapshotHistory-BFF2SsTN.js +2 -0
- package/dist/cdn/chunks/TemplateScoringPanel-gi8wc_m7.js +2 -0
- package/dist/cdn/chunks/TestEmailModal-Qtd6aC-6.js +2 -0
- package/dist/cdn/chunks/TitleEditor-DDf_OcHS.js +166 -0
- package/dist/cdn/chunks/TitleEditor-DDf_OcHS.js.map +1 -0
- package/dist/cdn/chunks/_rolldown_dynamic_import_helper-DMEI4TQ3.js +9 -0
- package/dist/cdn/chunks/blockTypeIcons-BnobReQm.js +22 -0
- package/dist/cdn/chunks/blockTypeIcons-BnobReQm.js.map +1 -0
- package/dist/cdn/chunks/de-BB3dgVOc.js +700 -0
- package/dist/cdn/chunks/de-BB3dgVOc.js.map +1 -0
- package/dist/cdn/chunks/de-BvYD17KT.js +89 -0
- package/dist/cdn/chunks/de-BvYD17KT.js.map +1 -0
- package/dist/cdn/chunks/dist-B6AUkMyh.js +2 -0
- package/dist/cdn/chunks/dist-B878xb_62.js +457 -0
- package/dist/cdn/chunks/dist-B878xb_62.js.map +1 -0
- package/dist/cdn/chunks/dist-Bf1Op9A1.js +2 -0
- package/dist/cdn/chunks/dist-BkETaOfw.js +2 -0
- package/dist/cdn/chunks/dist-CJcMnY7o.js +2 -0
- package/dist/cdn/chunks/dist-CWsl6S1K.js +2 -0
- package/dist/cdn/chunks/dist-CllLxIMQ.js +2 -0
- package/dist/cdn/chunks/dist-Cs0wFwdw.js +2 -0
- package/dist/cdn/chunks/dist-DLWHlekl.js +2 -0
- package/dist/cdn/chunks/dist-DS3_HVpX.js +2 -0
- package/dist/cdn/chunks/dist-DTXopj1a.js +2 -0
- package/dist/cdn/chunks/dist-DnwLoNLm.js +2 -0
- package/dist/cdn/chunks/draggable-BQNU47zu.js +11544 -0
- package/dist/cdn/chunks/{draggable-ClUwYCFL.js.map → draggable-BQNU47zu.js.map} +1 -1
- package/dist/cdn/chunks/emojiData-BVEJHcNH.js +19 -0
- package/dist/cdn/chunks/{emojiData-6fVLNqeH.js.map → emojiData-BVEJHcNH.js.map} +1 -1
- package/dist/cdn/chunks/en-CpotcOPr.js +89 -0
- package/dist/cdn/chunks/en-CpotcOPr.js.map +1 -0
- package/dist/cdn/chunks/en-DeDcpnoS.js +700 -0
- package/dist/cdn/chunks/en-DeDcpnoS.js.map +1 -0
- package/dist/cdn/chunks/extensions-B_kcV0tK.js +419 -0
- package/dist/cdn/chunks/{extensions-BfjbWqOx.js.map → extensions-B_kcV0tK.js.map} +1 -1
- package/dist/cdn/chunks/features-ofOGnSC0.js +6700 -0
- package/dist/cdn/chunks/features-ofOGnSC0.js.map +1 -0
- package/dist/cdn/chunks/icons-bIb7PBOE.js +653 -0
- package/dist/cdn/chunks/icons-bIb7PBOE.js.map +1 -0
- package/dist/cdn/chunks/liquid.browser-C1VIYISn.js +3272 -0
- package/dist/cdn/chunks/liquid.browser-C1VIYISn.js.map +1 -0
- package/dist/cdn/chunks/media-library-BIYzV2Y2.js +6005 -0
- package/dist/cdn/chunks/media-library-BIYzV2Y2.js.map +1 -0
- package/dist/cdn/chunks/pusher-DJPhQnE8.js +2505 -0
- package/dist/cdn/chunks/pusher-DJPhQnE8.js.map +1 -0
- package/dist/cdn/chunks/readableTextColor-Cd_cgWO_.js +32 -0
- package/dist/cdn/chunks/readableTextColor-Cd_cgWO_.js.map +1 -0
- package/dist/cdn/chunks/rolldown-runtime-DPITmOBR.js +20 -0
- package/dist/cdn/chunks/src-BuW9oYtm.js +494 -0
- package/dist/cdn/chunks/src-BuW9oYtm.js.map +1 -0
- package/dist/cdn/chunks/styleConstants-1KwsBMxJ.js +57 -0
- package/dist/cdn/chunks/{styleConstants-UTJ94gco.js.map → styleConstants-1KwsBMxJ.js.map} +1 -1
- package/dist/cdn/chunks/styles-DQFExz-T.js +3222 -0
- package/dist/cdn/chunks/styles-DQFExz-T.js.map +1 -0
- package/dist/cdn/chunks/tiptap-DplY-S-k.js +14208 -0
- package/dist/cdn/chunks/{tiptap-Cya4P9CN.js.map → tiptap-DplY-S-k.js.map} +1 -1
- package/dist/cdn/editor.css +2 -1
- package/dist/cdn/editor.js +260 -1
- package/dist/cdn/editor.js.map +1 -1
- package/dist/{de-B4Ob4vCo.js → de-D7TLGIPA.js} +20 -4
- package/dist/{dist-DNjZKe2Z.js → dist-BkIys9zn.js} +1 -1
- package/dist/{en-YXsspZJG.js → en-DvtiEMwP.js} +20 -4
- package/dist/{extensions-BA4NshZQ.js → extensions-DEjfEFhD.js} +3 -3
- package/dist/keys-C0MQRs8d.js +10 -0
- package/dist/readableTextColor-LDlmVEUv.js +30 -0
- package/dist/{styleConstants-CgtFM9hQ.js → styleConstants-D4SOZGBV.js} +53 -2
- package/dist/{styles-hQgJKM4i.js → styles-CgLaxDfu.js} +1091 -1069
- package/dist/templatical-editor.css +1 -1
- package/dist/templatical-editor.js +113 -89
- package/dist/templatical-editor.umd.cjs +59 -59
- package/dist/{useEditorCore-DVp5qmtC.js → useEditorCore-CjwRMl7K.js} +1185 -1043
- package/dist/{useI18n-DzH4KXDk.js → useI18n-D6m7ZUgY.js} +2 -2
- package/dist/{useMergeTag-D9zQVE-e.js → useMergeTag-BZ3X0bNr.js} +2 -2
- package/package.json +2 -2
- package/dist/CloudEditor-BVjgKwq3.js +0 -940
- package/dist/ParagraphEditor-BSyk5B6S.js +0 -670
- package/dist/cdn/chunks/ParagraphEditor-BkJQO-ZW.js +0 -3
- package/dist/cdn/chunks/ParagraphEditor-BkJQO-ZW.js.map +0 -1
- package/dist/cdn/chunks/RichTextEditorContent-UGQorJm_.js +0 -2
- package/dist/cdn/chunks/RichTextEditorContent-UGQorJm_.js.map +0 -1
- package/dist/cdn/chunks/TitleEditor-CC3Adjai.js +0 -3
- package/dist/cdn/chunks/TitleEditor-CC3Adjai.js.map +0 -1
- package/dist/cdn/chunks/dist-0UheN8rK.js +0 -1
- package/dist/cdn/chunks/dist-55mmbGQ9.js +0 -1
- package/dist/cdn/chunks/dist-B31mxKyP.js +0 -1
- package/dist/cdn/chunks/dist-B5JI9nIg.js +0 -1
- package/dist/cdn/chunks/dist-B93vLKhU.js +0 -1
- package/dist/cdn/chunks/dist-BDt3FJvj.js +0 -1
- package/dist/cdn/chunks/dist-BJRuFHmi.js +0 -1
- package/dist/cdn/chunks/dist-BKSzrf0L.js +0 -1
- package/dist/cdn/chunks/dist-BL8c5gYQ.js +0 -1
- package/dist/cdn/chunks/dist-CYThWMP5.js +0 -1
- package/dist/cdn/chunks/dist-DxZbPJYt.js +0 -1
- package/dist/cdn/chunks/draggable-ClUwYCFL.js +0 -17
- package/dist/cdn/chunks/emojiData-6fVLNqeH.js +0 -2
- package/dist/cdn/chunks/extensions-BfjbWqOx.js +0 -2
- package/dist/cdn/chunks/icons-vmLJTaJk.js +0 -2
- package/dist/cdn/chunks/icons-vmLJTaJk.js.map +0 -1
- package/dist/cdn/chunks/rolldown-runtime-BakkzWXw.js +0 -1
- package/dist/cdn/chunks/styleConstants-UTJ94gco.js +0 -2
- package/dist/cdn/chunks/tiptap-Cya4P9CN.js +0 -145
- package/dist/cdn/chunks/useEditorCore-DRhPKq_z.js +0 -2
- package/dist/cdn/chunks/useEditorCore-DRhPKq_z.js.map +0 -1
- package/dist/cdn/chunks/useMergeTag-CBSlcqnk.js +0 -2
- package/dist/cdn/chunks/useMergeTag-CBSlcqnk.js.map +0 -1
- package/dist/i18n-ikyi28RU.js +0 -23
- package/dist/keys-8B5MFafK.js +0 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloudEditor-Bmp5IlWi.js","names":[],"sources":["../../../src/cloud/composables/useSnapshotPreview.ts","../../../src/cloud/composables/useCloudPanelState.ts","../../../src/cloud/composables/useCollabUndoWarning.ts","../../../src/cloud/composables/useCloudFeatureFlags.ts","../../../src/cloud/composables/useCloudMediaLibrary.ts","../../../src/cloud/components/CloudLoadingOverlay.vue","../../../src/cloud/components/CloudLoadingOverlay.vue","../../../src/cloud/components/CloudErrorOverlay.vue","../../../src/cloud/components/CloudErrorOverlay.vue","../../../src/cloud/components/SnapshotPreviewBanner.vue","../../../src/cloud/components/SnapshotPreviewBanner.vue","../../../src/cloud/components/CollabUndoToast.vue","../../../src/cloud/components/CollabUndoToast.vue","../../../src/cloud/CloudEditor.vue","../../../src/cloud/CloudEditor.vue"],"sourcesContent":["import { computed, shallowRef, ref, type ComputedRef, type Ref } from \"vue\";\nimport type { TemplateContent, TemplateSnapshot } from \"@templatical/types\";\nimport type {\n UseHistoryReturn,\n UseConditionPreviewReturn,\n UseAutoSaveReturn,\n} from \"@templatical/core\";\nimport {\n useSnapshotHistory,\n type AuthManager,\n type UseSnapshotHistoryReturn,\n} from \"@templatical/core/cloud\";\nimport type { BaseEditorReturn } from \"../../composables/useEditorCore\";\n\nexport interface UseSnapshotPreviewOptions {\n authManager: AuthManager;\n editor: BaseEditorReturn & {\n hasTemplate: () => boolean;\n createSnapshot: () => Promise<void>;\n };\n history: UseHistoryReturn;\n conditionPreview: UseConditionPreviewReturn;\n autoSave: UseAutoSaveReturn | null;\n onError?: (error: Error) => void;\n}\n\nexport interface UseSnapshotPreviewReturn {\n snapshotHistoryInstance: Ref<UseSnapshotHistoryReturn | null>;\n previewingSnapshot: Ref<TemplateSnapshot | null>;\n contentBeforePreview: Ref<TemplateContent | null>;\n isPreviewingSnapshot: ComputedRef<boolean>;\n snapshotHistorySnapshots: ComputedRef<TemplateSnapshot[]>;\n snapshotHistoryIsLoading: ComputedRef<boolean>;\n snapshotHistoryIsRestoring: ComputedRef<boolean>;\n initSnapshotHistory: () => void;\n handleRestore: (template: { content: TemplateContent }) => void;\n handleSnapshotNavigate: (snapshot: TemplateSnapshot) => Promise<void>;\n confirmRestoreSnapshot: () => Promise<void>;\n cancelPreview: () => void;\n loadSnapshotHistory: () => Promise<void>;\n}\n\nexport function useSnapshotPreview(\n options: UseSnapshotPreviewOptions,\n): UseSnapshotPreviewReturn {\n const { authManager, editor, history, conditionPreview, autoSave, onError } =\n options;\n\n const snapshotHistoryInstance = shallowRef<UseSnapshotHistoryReturn | null>(\n null,\n );\n const previewingSnapshot = ref<TemplateSnapshot | null>(null);\n const contentBeforePreview = ref<TemplateContent | null>(null);\n\n const isPreviewingSnapshot = computed(\n () => previewingSnapshot.value !== null,\n );\n const snapshotHistorySnapshots = computed(\n () => snapshotHistoryInstance.value?.snapshots.value ?? [],\n );\n const snapshotHistoryIsLoading = computed(\n () => snapshotHistoryInstance.value?.isLoading.value ?? false,\n );\n const snapshotHistoryIsRestoring = computed(\n () => snapshotHistoryInstance.value?.isRestoring.value ?? false,\n );\n\n function initSnapshotHistory(): void {\n if (editor.state.template?.id && !snapshotHistoryInstance.value) {\n snapshotHistoryInstance.value = useSnapshotHistory({\n authManager,\n templateId: editor.state.template.id,\n onRestore: handleRestore,\n onError,\n });\n snapshotHistoryInstance.value.loadSnapshots();\n }\n }\n\n function handleRestore(template: { content: TemplateContent }): void {\n editor.setContent(template.content, false);\n history.clear();\n conditionPreview.reset();\n }\n\n async function handleSnapshotNavigate(\n snapshot: TemplateSnapshot,\n ): Promise<void> {\n if (previewingSnapshot.value) {\n previewingSnapshot.value = snapshot;\n editor.setContent(snapshot.content, false);\n return;\n }\n\n if (editor.state.isDirty && editor.hasTemplate()) {\n await editor.createSnapshot();\n }\n\n contentBeforePreview.value = structuredClone(editor.content.value);\n\n autoSave?.pause();\n previewingSnapshot.value = snapshot;\n editor.setContent(snapshot.content, false);\n }\n\n async function confirmRestoreSnapshot(): Promise<void> {\n if (!previewingSnapshot.value || !snapshotHistoryInstance.value) return;\n\n try {\n await snapshotHistoryInstance.value.restoreSnapshot(\n previewingSnapshot.value.id,\n );\n await snapshotHistoryInstance.value.loadSnapshots();\n } finally {\n previewingSnapshot.value = null;\n contentBeforePreview.value = null;\n autoSave?.resume();\n }\n }\n\n function cancelPreview(): void {\n if (!previewingSnapshot.value || !contentBeforePreview.value) return;\n\n editor.setContent(contentBeforePreview.value, false);\n\n previewingSnapshot.value = null;\n contentBeforePreview.value = null;\n\n autoSave?.resume();\n }\n\n async function loadSnapshotHistory(): Promise<void> {\n if (snapshotHistoryInstance.value) {\n await snapshotHistoryInstance.value.loadSnapshots();\n }\n }\n\n return {\n snapshotHistoryInstance,\n previewingSnapshot,\n contentBeforePreview,\n isPreviewingSnapshot,\n snapshotHistorySnapshots,\n snapshotHistoryIsLoading,\n snapshotHistoryIsRestoring,\n initSnapshotHistory,\n handleRestore,\n handleSnapshotNavigate,\n confirmRestoreSnapshot,\n cancelPreview,\n loadSnapshotHistory,\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 testEmailModalOpen: Ref<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 testEmailModalOpen = ref(false);\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 testEmailModalOpen,\n mediaLibraryOpen,\n mediaLibraryAccept,\n aiMenuOpen,\n aiMenuRef,\n rightPanelOpen,\n activeAiFeature,\n aiButtonActive,\n toggleAiMenu,\n handleAiFeatureSelect,\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","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { useTimeoutFn } from \"@vueuse/core\";\nimport type {\n UsePlanConfigReturn,\n UseAiConfigReturn,\n} from \"@templatical/core/cloud\";\n\nexport interface UseCloudFeatureFlagsOptions {\n planConfigInstance: UsePlanConfigReturn;\n aiConfig: UseAiConfigReturn;\n editor: {\n state: {\n readonly template?: { id: string } | null;\n };\n };\n}\n\nexport interface UseCloudFeatureFlagsReturn {\n canUseAiGeneration: ComputedRef<boolean>;\n canSendTestEmail: ComputedRef<boolean>;\n hasTemplateSaved: ComputedRef<boolean>;\n isWhiteLabeled: ComputedRef<boolean>;\n templateLimit: ComputedRef<number | null>;\n templateCount: ComputedRef<number>;\n isSaveExporting: Ref<boolean>;\n saveStatus: Ref<\"idle\" | \"saved\" | \"error\">;\n saveErrorMessage: Ref<string>;\n startSaveStatusClear: () => void;\n}\n\nexport function useCloudFeatureFlags(\n options: UseCloudFeatureFlagsOptions,\n): UseCloudFeatureFlagsReturn {\n const { planConfigInstance, aiConfig, editor } = 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(() => !!editor.state.template?.id);\n const isWhiteLabeled = computed(() =>\n planConfigInstance.hasFeature(\"white_label\"),\n );\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 const isSaveExporting = ref(false);\n const saveStatus = ref<\"idle\" | \"saved\" | \"error\">(\"idle\");\n const saveErrorMessage = ref(\"\");\n\n const { start: startSaveStatusClear } = useTimeoutFn(\n () => {\n saveStatus.value = \"idle\";\n },\n 3000,\n { immediate: false },\n );\n\n return {\n canUseAiGeneration,\n canSendTestEmail,\n hasTemplateSaved,\n isWhiteLabeled,\n templateLimit,\n templateCount,\n isSaveExporting,\n saveStatus,\n saveErrorMessage,\n startSaveStatusClear,\n };\n}\n","import { onScopeDispose, type Ref } from \"vue\";\nimport type {\n MediaCategory,\n MediaItem,\n MediaRequestContext,\n} from \"@templatical/media-library\";\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}\n\nexport interface UseCloudMediaLibraryReturn {\n handleRequestMedia: () => Promise<MediaResult | null>;\n handleMediaSelect: (item: MediaItem) => void;\n handleMediaLibraryClose: () => void;\n}\n\nexport function useCloudMediaLibrary(\n options: UseCloudMediaLibraryOptions,\n): UseCloudMediaLibraryReturn {\n const { onRequestMedia, mediaLibraryOpen, mediaLibraryAccept } = options;\n\n let mediaResolve: ((result: MediaResult | null) => void) | null = null;\n\n async function handleRequestMedia(): Promise<MediaResult | null> {\n // If consumer provides a custom media handler, use it\n if (onRequestMedia) {\n const item = await onRequestMedia({ accept: [\"images\"] });\n if (!item) return null;\n return { url: item.url, alt: item.alt_text || undefined };\n }\n\n // Otherwise open the built-in media library\n mediaLibraryAccept.value = [\"images\"];\n mediaLibraryOpen.value = true;\n return new Promise<MediaResult | null>((resolve) => {\n mediaResolve = (result) => {\n resolve(result);\n };\n });\n }\n\n function handleMediaSelect(item: MediaItem): void {\n mediaLibraryOpen.value = false;\n mediaResolve?.({ url: item.url, alt: item.alt_text || undefined });\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","<script setup lang=\"ts\">\ndefineProps<{\n visible: boolean;\n}>();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-loading tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Skeleton header -->\n <div\n class=\"tpl:flex tpl:h-14 tpl:shrink-0 tpl:items-center tpl:justify-between tpl:px-4 tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-5 tpl:w-28 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl:flex tpl:gap-3\">\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n </div>\n <!-- Skeleton body -->\n <div class=\"tpl:flex tpl:flex-1 tpl:overflow-hidden\">\n <!-- Left sidebar rail -->\n <div\n class=\"tpl:flex tpl:w-12 tpl:shrink-0 tpl:flex-col tpl:items-center tpl:gap-4 tpl:py-5 tpl:border-r tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-for=\"n in 5\"\n :key=\"n\"\n class=\"tpl-shimmer tpl:size-7 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <!-- Canvas area -->\n <div\n class=\"tpl:flex tpl:flex-1 tpl:items-start tpl:justify-center tpl:overflow-auto tpl:p-8 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <div\n class=\"tpl:w-full tpl:max-w-[600px] tpl:rounded-[var(--tpl-radius)] tpl:p-6 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)]\"\n >\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-3/4 tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-5/6 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-44 tpl:w-full tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-2/3 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-10 tpl:w-36 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/2 tpl:rounded\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/3 tpl:rounded\"\n ></div>\n </div>\n </div>\n </div>\n <!-- Right panel -->\n <div\n class=\"tpl:flex tpl:w-[320px] tpl:shrink-0 tpl:flex-col tpl:gap-4 tpl:p-4 tpl:border-l tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\ndefineProps<{\n visible: boolean;\n}>();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-loading tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Skeleton header -->\n <div\n class=\"tpl:flex tpl:h-14 tpl:shrink-0 tpl:items-center tpl:justify-between tpl:px-4 tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-5 tpl:w-28 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl:flex tpl:gap-3\">\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n </div>\n <!-- Skeleton body -->\n <div class=\"tpl:flex tpl:flex-1 tpl:overflow-hidden\">\n <!-- Left sidebar rail -->\n <div\n class=\"tpl:flex tpl:w-12 tpl:shrink-0 tpl:flex-col tpl:items-center tpl:gap-4 tpl:py-5 tpl:border-r tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-for=\"n in 5\"\n :key=\"n\"\n class=\"tpl-shimmer tpl:size-7 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <!-- Canvas area -->\n <div\n class=\"tpl:flex tpl:flex-1 tpl:items-start tpl:justify-center tpl:overflow-auto tpl:p-8 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <div\n class=\"tpl:w-full tpl:max-w-[600px] tpl:rounded-[var(--tpl-radius)] tpl:p-6 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)]\"\n >\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-3/4 tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-5/6 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-44 tpl:w-full tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-2/3 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-10 tpl:w-36 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/2 tpl:rounded\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/3 tpl:rounded\"\n ></div>\n </div>\n </div>\n </div>\n <!-- Right panel -->\n <div\n class=\"tpl:flex tpl:w-[320px] tpl:shrink-0 tpl:flex-col tpl:gap-4 tpl:p-4 tpl:border-l tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { CircleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n error: Error | null;\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"retry\"): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction getErrorMessage(error: Error): string {\n if (\n \"isUnauthorized\" in error &&\n (error as { isUnauthorized: boolean }).isUnauthorized\n ) {\n return t.error.authFailed;\n }\n if (\"isNotFound\" in error && (error as { isNotFound: boolean }).isNotFound) {\n return t.error.templateNotFound;\n }\n return t.error.defaultMessage;\n}\n\nfunction isNotFoundError(error: Error): boolean {\n return (\n \"isNotFound\" in error && !!(error as { isNotFound: boolean }).isNotFound\n );\n}\n</script>\n\n<template>\n <div\n v-if=\"visible && error\"\n role=\"alert\"\n class=\"tpl-error tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-6 tpl:px-8 tpl:bg-[var(--tpl-bg)]\"\n >\n <div\n class=\"tpl:flex tpl:size-16 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-[var(--tpl-danger-light)]\"\n >\n <CircleAlert\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n />\n </div>\n <div\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-2 tpl:text-center\"\n >\n <h2 class=\"tpl:text-lg tpl:font-semibold tpl:text-[var(--tpl-text)]\">\n {{ t.error.title }}\n </h2>\n <p class=\"tpl:max-w-md tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ getErrorMessage(error) }}\n </p>\n </div>\n <button\n v-if=\"!isNotFoundError(error)\"\n class=\"tpl-btn tpl-btn-primary tpl:inline-flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('retry')\"\n >\n {{ t.error.retry }}\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { CircleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n error: Error | null;\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"retry\"): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction getErrorMessage(error: Error): string {\n if (\n \"isUnauthorized\" in error &&\n (error as { isUnauthorized: boolean }).isUnauthorized\n ) {\n return t.error.authFailed;\n }\n if (\"isNotFound\" in error && (error as { isNotFound: boolean }).isNotFound) {\n return t.error.templateNotFound;\n }\n return t.error.defaultMessage;\n}\n\nfunction isNotFoundError(error: Error): boolean {\n return (\n \"isNotFound\" in error && !!(error as { isNotFound: boolean }).isNotFound\n );\n}\n</script>\n\n<template>\n <div\n v-if=\"visible && error\"\n role=\"alert\"\n class=\"tpl-error tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-6 tpl:px-8 tpl:bg-[var(--tpl-bg)]\"\n >\n <div\n class=\"tpl:flex tpl:size-16 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-[var(--tpl-danger-light)]\"\n >\n <CircleAlert\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n />\n </div>\n <div\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-2 tpl:text-center\"\n >\n <h2 class=\"tpl:text-lg tpl:font-semibold tpl:text-[var(--tpl-text)]\">\n {{ t.error.title }}\n </h2>\n <p class=\"tpl:max-w-md tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ getErrorMessage(error) }}\n </p>\n </div>\n <button\n v-if=\"!isNotFoundError(error)\"\n class=\"tpl-btn tpl-btn-primary tpl:inline-flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('retry')\"\n >\n {{ t.error.retry }}\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Clock } from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"cancel\"): void;\n (e: \"confirm\"): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-preview-banner tpl:absolute tpl:top-14 tpl:right-0 tpl:left-0 tpl:z-40 tpl:flex tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-4 tpl:py-3 tpl:bg-[var(--tpl-primary-light)] tpl:border-b tpl:border-[var(--tpl-primary)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:text-[var(--tpl-text)]\"\n >\n <Clock\n :size=\"18\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n />\n <span>{{ t.snapshotPreview.message }}</span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"emit('cancel')\"\n >\n {{ t.snapshotPreview.cancel }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('confirm')\"\n >\n {{ t.snapshotPreview.restore }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Clock } from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"cancel\"): void;\n (e: \"confirm\"): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-preview-banner tpl:absolute tpl:top-14 tpl:right-0 tpl:left-0 tpl:z-40 tpl:flex tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-4 tpl:py-3 tpl:bg-[var(--tpl-primary-light)] tpl:border-b tpl:border-[var(--tpl-primary)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:text-[var(--tpl-text)]\"\n >\n <Clock\n :size=\"18\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n />\n <span>{{ t.snapshotPreview.message }}</span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"emit('cancel')\"\n >\n {{ t.snapshotPreview.cancel }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('confirm')\"\n >\n {{ t.snapshotPreview.restore }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n role=\"status\"\n aria-live=\"polite\"\n class=\"tpl:absolute tpl:top-16 tpl:left-1/2 tpl:z-toast tpl:-translate-x-1/2 tpl:rounded-[var(--tpl-radius)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:shadow-lg\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-text);\n border: 1px solid var(--tpl-warning);\n \"\n >\n {{ t.history.collabWarning }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n role=\"status\"\n aria-live=\"polite\"\n class=\"tpl:absolute tpl:top-16 tpl:left-1/2 tpl:z-toast tpl:-translate-x-1/2 tpl:rounded-[var(--tpl-radius)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:shadow-lg\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-text);\n border: 1px solid var(--tpl-warning);\n \"\n >\n {{ t.history.collabWarning }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n Block,\n CollaborationConfig,\n CommentEvent,\n CustomBlockDefinition,\n DisplayConditionsConfig,\n FontsConfig,\n McpConfig,\n MergeTagsConfig,\n SaveResult,\n Template,\n TemplateContent,\n ThemeOverrides,\n UiTheme,\n} from \"@templatical/types\";\nimport type {\n MediaItem,\n MediaRequestContext,\n} from \"@templatical/media-library\";\nimport { cloneBlock, isCustomBlock } from \"@templatical/types\";\nimport type { CustomBlock } from \"@templatical/types\";\n\nimport {\n AuthManager,\n performHealthCheck,\n resolveWebSocketConfig,\n useAiConfig,\n useCollaboration,\n useCollaborationBroadcast,\n useCommentListener,\n useComments,\n useEditor,\n useExport,\n useMcpListener,\n usePlanConfig,\n useSavedModules,\n useTemplateScoring,\n useTestEmail,\n useWebSocket,\n type UseCollaborationReturn,\n} from \"@templatical/core/cloud\";\nimport type { UseFontsReturn } from \"../composables/useFonts\";\nimport type { McpOperationPayload } from \"@templatical/types\";\nimport {\n computed,\n defineAsyncComponent,\n nextTick,\n onMounted,\n onUnmounted,\n provide,\n ref,\n watch,\n} from \"vue\";\nimport {\n Check,\n CircleAlert,\n LoaderCircle,\n MessageCircle,\n RotateCcw,\n Save,\n Send,\n Sparkles,\n} from \"@lucide/vue\";\nimport type { Translations } from \"../i18n\";\n\nimport { useEditorCore } from \"../composables/useEditorCore\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\nimport {\n ON_REQUEST_MEDIA_KEY,\n AUTH_MANAGER_KEY,\n AI_CONFIG_KEY,\n COMMENTS_KEY,\n SAVED_MODULES_HEADLESS_KEY,\n SCORING_KEY,\n CAPABILITIES_KEY,\n} from \"../keys\";\nimport type { UseSnapshotPreviewReturn } from \"./composables/useSnapshotPreview\";\nimport { useSnapshotPreview } from \"./composables/useSnapshotPreview\";\nimport { useCloudPanelState } from \"./composables/useCloudPanelState\";\nimport { useCollabUndoWarning } from \"./composables/useCollabUndoWarning\";\nimport { useCloudFeatureFlags } from \"./composables/useCloudFeatureFlags\";\nimport { useCloudMediaLibrary } from \"./composables/useCloudMediaLibrary\";\nimport { useDragDrop } from \"../composables/useDragDrop\";\nimport { DEFAULT_AUTO_SAVE_DEBOUNCE_MS } from \"../constants/timeouts\";\nimport { headerBtnClass } from \"../constants/styleConstants\";\n\nimport Canvas from \"../components/Canvas.vue\";\nimport Sidebar from \"../components/Sidebar.vue\";\nimport RightSidebar from \"../components/RightSidebar.vue\";\nimport ViewportToggle from \"../components/ViewportToggle.vue\";\nimport PreviewToggle from \"../components/PreviewToggle.vue\";\nimport DarkModeToggle from \"../components/DarkModeToggle.vue\";\nimport CloudLoadingOverlay from \"./components/CloudLoadingOverlay.vue\";\nimport CloudErrorOverlay from \"./components/CloudErrorOverlay.vue\";\nimport SnapshotPreviewBanner from \"./components/SnapshotPreviewBanner.vue\";\nimport CollabUndoToast from \"./components/CollabUndoToast.vue\";\nimport \"../styles/index.css\";\n\n// Cloud async components\nconst AiChatSidebar = defineAsyncComponent(\n () => import(\"./components/AiChatSidebar.vue\"),\n);\nconst CommentsSidebar = defineAsyncComponent(\n () => import(\"./components/CommentsSidebar.vue\"),\n);\nconst DesignReferenceSidebar = defineAsyncComponent(\n () => import(\"./components/DesignReferenceSidebar.vue\"),\n);\nconst TemplateScoringPanel = defineAsyncComponent(\n () => import(\"./components/TemplateScoringPanel.vue\"),\n);\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./components/TestEmailModal.vue\"),\n);\nconst SaveModuleDialog = defineAsyncComponent(\n () => import(\"./components/SaveModuleDialog.vue\"),\n);\nconst ModuleBrowserModal = defineAsyncComponent(\n () => import(\"./components/ModuleBrowserModal.vue\"),\n);\nconst SnapshotHistory = defineAsyncComponent(\n () => import(\"./components/SnapshotHistory.vue\"),\n);\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./components/CollaboratorBar.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(\n () => import(\"./components/AiFeatureMenu.vue\"),\n);\nconst MediaLibraryModal = defineAsyncComponent(async () => {\n const m = await import(\"@templatical/media-library\");\n return m.MediaLibraryModal;\n});\n\n// ---------------------------------------------------------------------------\n// Config type — flat cloud config extending OSS\n// ---------------------------------------------------------------------------\n\nexport interface TemplaticalCloudEditorConfig {\n container: string | HTMLElement;\n content?: TemplateContent;\n\n auth: {\n url: string;\n baseUrl?: string;\n requestOptions?: {\n method?: \"GET\" | \"POST\";\n headers?: Record<string, string>;\n body?: Record<string, unknown>;\n credentials?: RequestCredentials;\n };\n };\n\n theme?: ThemeOverrides;\n uiTheme?: UiTheme;\n locale?: string;\n\n ai?: import(\"@templatical/types\").AiConfig | false;\n commenting?: boolean;\n collaboration?: CollaborationConfig;\n mcp?: McpConfig;\n blockDefaults?: import(\"@templatical/types\").BlockDefaults;\n templateDefaults?: import(\"@templatical/types\").TemplateDefaults;\n\n modules?: boolean;\n autoSave?: boolean;\n autoSaveDebounce?: number;\n\n mergeTags?: MergeTagsConfig;\n displayConditions?: DisplayConditionsConfig;\n customBlocks?: CustomBlockDefinition[];\n fonts?: FontsConfig;\n onChange?: (content: TemplateContent) => void;\n onSave?: (result: SaveResult) => void;\n onCreate?: (template: Template) => void;\n onLoad?: (template: Template) => void;\n onError?: (error: Error) => void;\n onComment?: (event: CommentEvent) => void;\n onUnmount?: () => void;\n\n onRequestMedia?: (context: MediaRequestContext) => Promise<MediaItem | null>;\n onBeforeTestEmail?: (html: string) => string | Promise<string>;\n}\n\nconst props = defineProps<{\n config: TemplaticalCloudEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n}>();\nconst emit = defineEmits<{\n (e: \"ready\"): void;\n}>();\n\n// ---------------------------------------------------------------------------\n// Cloud initialization state\n// ---------------------------------------------------------------------------\n\nconst isInitializing = ref(true);\nconst isAuthReady = ref(false);\nconst initError = ref<Error | null>(null);\n\n// Tracks whether the component has been unmounted. Checked after every await\n// in async lifecycle functions to prevent post-unmount side effects.\nlet _destroyed = false;\n\n// ---------------------------------------------------------------------------\n// 1. AuthManager + PlanConfig (infrastructure)\n// ---------------------------------------------------------------------------\n\nconst authManager = new AuthManager({\n ...props.config.auth,\n onError: props.config.onError,\n});\n\nconst planConfigInstance = usePlanConfig({\n authManager,\n onError: props.config.onError,\n});\n\n// ---------------------------------------------------------------------------\n// 2. Collaboration locked blocks ref\n// ---------------------------------------------------------------------------\n\nconst collaborationLockedBlocks = ref<Map<string, unknown>>(new Map());\n\n// ---------------------------------------------------------------------------\n// 3. Cloud editor (API-backed)\n// ---------------------------------------------------------------------------\n\nconst editor = useEditor({\n authManager,\n defaultFontFamily: props.config.fonts?.defaultFont,\n templateDefaults: props.config.templateDefaults,\n onError: props.config.onError,\n lockedBlocks: collaborationLockedBlocks,\n});\n\n// ---------------------------------------------------------------------------\n// 4. WebSocket + MCP listener\n// ---------------------------------------------------------------------------\n\nconst websocket = useWebSocket({\n authManager,\n onError: props.config.onError,\n});\n\nif (props.config.mcp?.enabled) {\n useMcpListener({\n editor,\n channel: websocket.channel,\n onOperation: props.config.mcp.onOperation,\n });\n}\n\n// ---------------------------------------------------------------------------\n// 5. Collaboration — MUST be before useEditorCore so broadcast wraps\n// editor methods first, then useHistoryInterceptor wraps AFTER\n// ---------------------------------------------------------------------------\n\nlet collaboration:\n | (UseCollaborationReturn & {\n _broadcastOperation: (payload: McpOperationPayload) => void;\n _isProcessingRemoteOperation: () => boolean;\n })\n | null = null;\n\nif (props.config.collaboration?.enabled) {\n collaboration = useCollaboration({\n authManager,\n editor,\n channel: websocket.channel,\n onError: props.config.onError,\n onCollaboratorJoined: props.config.collaboration.onCollaboratorJoined,\n onCollaboratorLeft: props.config.collaboration.onCollaboratorLeft,\n onBlockLocked: props.config.collaboration.onBlockLocked,\n onBlockUnlocked: props.config.collaboration.onBlockUnlocked,\n });\n\n // Sync locked blocks from collaboration to editor\n watch(\n () => collaboration!.lockedBlocks.value,\n (newLockedBlocks) => {\n collaborationLockedBlocks.value = newLockedBlocks;\n },\n { immediate: true },\n );\n\n // Wrap editor methods to broadcast operations to peers\n useCollaborationBroadcast(editor, collaboration);\n}\n\nconst isCollaborationEnabled = computed(\n () =>\n !!props.config.collaboration?.enabled &&\n planConfigInstance.hasFeature(\"collaboration\"),\n);\n\n// ---------------------------------------------------------------------------\n// 6. useEditorCore — shared composables, provides, plugins, keyboard\n// ---------------------------------------------------------------------------\n\n// Forward references for circular dependencies resolved after setup\nlet snapshotPreviewRef: UseSnapshotPreviewReturn | null = null;\nlet collabWarningRef: ReturnType<typeof useCollabUndoWarning> | null = null;\n\nconst core = useEditorCore({\n editor,\n config: {\n uiTheme: props.config.uiTheme,\n theme: undefined, // Cloud applies theme in initialize() after plan check\n blockDefaults: props.config.blockDefaults,\n customBlocks: [], // Cloud defers registration to initialize()\n mergeTags: props.config.mergeTags,\n displayConditions: props.config.displayConditions,\n onRequestMedia: null, // Cloud uses handleRequestMedia via media library composable\n onSave: () => {\n saveTemplate().catch((err) => {\n props.config.onError?.(err as Error);\n });\n },\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n historyOptions: collaboration\n ? { isRemoteOperation: () => collaboration!._isProcessingRemoteOperation() }\n : undefined,\n autoSaveOptions: {\n onChange: async () => {\n if (editor.hasTemplate()) {\n await editor.createSnapshot();\n snapshotPreviewRef?.snapshotHistoryInstance.value?.loadSnapshots();\n }\n },\n debounce: props.config.autoSaveDebounce ?? DEFAULT_AUTO_SAVE_DEBOUNCE_MS,\n enabled: () =>\n props.config.autoSave !== false &&\n planConfigInstance.hasFeature(\"auto_save\"),\n },\n themeExtraStyles: () => ({\n \"--tpl-drop-text\": `\"${props.translations.canvas.dropHere}\"`,\n }),\n keyboardOptions: {\n onBeforeUndo: () => collabWarningRef?.showCollabUndoWarning(),\n },\n});\n\n// ---------------------------------------------------------------------------\n// 7. Collab undo warning (created after core so it can use core.history.canUndo)\n// ---------------------------------------------------------------------------\n\nconst collabWarning = useCollabUndoWarning({\n isCollaborationEnabled,\n getCollaboratorCount: () => collaboration?.collaborators.value.length ?? 0,\n canUndo: core.history.canUndo,\n});\ncollabWarningRef = collabWarning;\n\n// ---------------------------------------------------------------------------\n// 8. Snapshot preview (needs core.autoSave for pause/resume)\n// ---------------------------------------------------------------------------\n\nconst snapshotPreview = useSnapshotPreview({\n authManager,\n editor,\n history: core.history,\n conditionPreview: core.conditionPreview,\n autoSave: core.autoSave,\n onError: props.config.onError,\n});\n\n// Connect forward reference for autoSave onChange\nsnapshotPreviewRef = snapshotPreview;\n\n// ---------------------------------------------------------------------------\n// 9. Remaining cloud composables\n// ---------------------------------------------------------------------------\n\nconst panelState = useCloudPanelState();\n\nconst aiConfig = useAiConfig(props.config.ai);\n\nconst featureFlags = useCloudFeatureFlags({\n planConfigInstance,\n aiConfig,\n editor,\n});\n\nconst mediaLib = useCloudMediaLibrary({\n onRequestMedia: props.config.onRequestMedia,\n mediaLibraryOpen: panelState.mediaLibraryOpen,\n mediaLibraryAccept: panelState.mediaLibraryAccept,\n});\n\nconst _dragDrop = useDragDrop({\n onBlockMove: editor.moveBlock,\n onBlockAdd: editor.addBlock,\n});\n\nconst exporter = useExport({\n authManager,\n getFontsConfig: () => props.config.fonts,\n canUseCustomFonts: () => planConfigInstance.hasFeature(\"custom_fonts\"),\n});\n\nconst testEmail = useTestEmail({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n save: () => editor.save(),\n exportHtml: (templateId: string) => exporter.exportHtml(templateId),\n onError: props.config.onError,\n isAuthReady,\n onBeforeTestEmail: props.config.onBeforeTestEmail,\n});\n\nconst commentsInstance = useComments({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n getSocketId: () => websocket.getSocketId(),\n onComment: props.config.onComment,\n onError: props.config.onError,\n isAuthReady,\n hasCommentingFeature: () =>\n props.config.commenting !== false &&\n planConfigInstance.hasFeature(\"commenting\"),\n});\n\nuseCommentListener({\n comments: commentsInstance,\n channel: websocket.channel,\n});\n\nconst savedModulesHeadless = useSavedModules({\n authManager,\n onError: props.config.onError,\n});\nconst showSaveModuleDialog = ref(false);\nconst saveModulePreSelectedBlockId = ref<string | null>(null);\nconst showModuleBrowserModal = ref(false);\n\nconst scoringInstance = useTemplateScoring({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n});\n\n// ---------------------------------------------------------------------------\n// 10. Cloud-only provides\n// ---------------------------------------------------------------------------\n\nprovide(ON_REQUEST_MEDIA_KEY, mediaLib.handleRequestMedia);\nprovide(AUTH_MANAGER_KEY, authManager);\nprovide(AI_CONFIG_KEY, aiConfig);\nprovide(COMMENTS_KEY, commentsInstance);\nprovide(SAVED_MODULES_HEADLESS_KEY, savedModulesHeadless);\nprovide(SCORING_KEY, scoringInstance);\n\n// Override the default empty capabilities from useEditorCore with cloud capabilities.\n// OSS components use this single inject instead of individual cloud injects.\nprovide(CAPABILITIES_KEY, {\n plan: planConfigInstance,\n ai: aiConfig,\n comments: {\n getBlockCount: (blockId: string) =>\n commentsInstance.commentCountByBlock.value.get(blockId) ?? 0,\n openForBlock: openCommentsForBlock,\n },\n savedModules: {\n openSaveDialog: (blockId: string) => {\n saveModulePreSelectedBlockId.value = blockId ?? null;\n showSaveModuleDialog.value = true;\n },\n openBrowser: () => {\n showModuleBrowserModal.value = true;\n },\n moduleCount: computed(() => savedModulesHeadless.modules.value.length),\n },\n} satisfies EditorCapabilities);\n\n// ---------------------------------------------------------------------------\n// Theme overrides (plan-gated)\n// ---------------------------------------------------------------------------\n\nfunction setThemeOverrides(overrides: ThemeOverrides): void {\n if (!planConfigInstance.hasFeature(\"theme_customization\")) {\n return;\n }\n core.themeOverrides.value = overrides;\n}\n\nfunction setUiTheme(theme: UiTheme): void {\n editor.setUiTheme(theme);\n}\n\n// ---------------------------------------------------------------------------\n// Comments sidebar ref for block filtering\n// ---------------------------------------------------------------------------\n\nconst commentsSidebarRef = ref<InstanceType<typeof CommentsSidebar> | null>(\n null,\n);\n\nfunction openCommentsForBlock(blockId: string): void {\n panelState.commentsOpen.value = true;\n nextTick(() => {\n commentsSidebarRef.value?.filterByBlock(blockId);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Test email handler\n// ---------------------------------------------------------------------------\n\nasync function handleSendTestEmail(recipient: string): Promise<void> {\n try {\n await testEmail.sendTestEmail(recipient);\n panelState.testEmailModalOpen.value = false;\n } catch {\n // Error is already handled in the composable\n }\n}\n\n// ---------------------------------------------------------------------------\n// Module insert handler\n// ---------------------------------------------------------------------------\n\nfunction handleModuleInsert(\n module: { content: Block[] },\n insertIndex: number | undefined,\n): void {\n for (let i = 0; i < module.content.length; i++) {\n const cloned = cloneBlock(module.content[i]);\n const position = insertIndex !== undefined ? insertIndex + i : undefined;\n editor.addBlock(cloned, undefined, undefined, position);\n }\n showModuleBrowserModal.value = false;\n}\n\n// ---------------------------------------------------------------------------\n// Custom blocks pre-render for save\n// ---------------------------------------------------------------------------\n\nasync function preRenderCustomBlocks(content: TemplateContent): Promise<void> {\n const renderBlock = async (block: Block): Promise<void> => {\n if (isCustomBlock(block)) {\n const customBlock = block as CustomBlock;\n try {\n customBlock.renderedHtml =\n await core.registry.renderCustomBlock(customBlock);\n } catch {\n customBlock.renderedHtml = `<!-- Custom block render error: ${customBlock.customType} -->`;\n }\n }\n\n if (block.type === \"section\" && \"children\" in block) {\n const sectionBlock = block as { children: Block[][] };\n for (const column of sectionBlock.children) {\n for (const child of column) {\n await renderBlock(child);\n }\n }\n }\n };\n\n for (const block of content.blocks) {\n await renderBlock(block);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cloud initialization\n// ---------------------------------------------------------------------------\n\nasync function initialize(): Promise<void> {\n isInitializing.value = true;\n initError.value = null;\n\n try {\n // Auth\n await authManager.initialize();\n if (_destroyed) return;\n isAuthReady.value = true;\n\n // Health check\n const healthResult = await performHealthCheck({ authManager });\n if (_destroyed) return;\n\n if (!healthResult.api.ok) {\n throw new Error(\"Health check failed: API is not reachable\");\n }\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\n if (!healthResult.websocket.ok) {\n console.warn(\n \"[Templatical] WebSocket health check failed:\",\n healthResult.websocket.error ?? \"unknown error\",\n \"-- real-time features will be disabled.\",\n );\n }\n\n // Plan config\n await planConfigInstance.fetchConfig();\n if (_destroyed) return;\n\n // Update fonts\n props.fontsManager.setCustomFontsEnabled(\n planConfigInstance.hasFeature(\"custom_fonts\"),\n );\n\n // Register custom blocks if feature is enabled and definitions provided\n if (\n props.config.customBlocks?.length &&\n planConfigInstance.hasFeature(\"custom_blocks\")\n ) {\n core.registerCustomBlocks(props.config.customBlocks);\n }\n\n // Apply theme\n if (\n props.config.theme &&\n planConfigInstance.hasFeature(\"theme_customization\")\n ) {\n core.themeOverrides.value = props.config.theme;\n }\n\n // Load saved modules\n if (\n props.config.modules !== false &&\n planConfigInstance.hasFeature(\"saved_modules\")\n ) {\n savedModulesHeadless.loadModules();\n }\n\n emit(\"ready\");\n } catch (error) {\n if (_destroyed) return;\n const wrappedError =\n error instanceof Error\n ? error\n : new Error(\"Initialization failed\", { cause: error });\n initError.value = wrappedError;\n props.config.onError?.(wrappedError);\n } finally {\n if (!_destroyed) {\n isInitializing.value = false;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Template lifecycle methods\n// ---------------------------------------------------------------------------\n\nfunction getWebSocketConfig() {\n return resolveWebSocketConfig(planConfigInstance.config.value!.websocket);\n}\n\nasync function createTemplate(content?: TemplateContent): Promise<Template> {\n const template = await editor.create(content);\n if (_destroyed) return template;\n props.config.onCreate?.(template);\n snapshotPreview.initSnapshotHistory();\n websocket.connect(template.id, getWebSocketConfig());\n return template;\n}\n\nasync function loadTemplate(templateId: string): Promise<Template> {\n const template = await editor.load(templateId);\n if (_destroyed) return template;\n props.config.onLoad?.(template);\n snapshotPreview.initSnapshotHistory();\n websocket.connect(template.id, getWebSocketConfig());\n return template;\n}\n\nasync function saveTemplate(): Promise<SaveResult> {\n featureFlags.isSaveExporting.value = true;\n featureFlags.saveStatus.value = \"idle\";\n try {\n // Pre-render custom blocks so backend can include them in MJML export\n await preRenderCustomBlocks(editor.content.value);\n if (_destroyed) throw new Error(\"Component unmounted during save\");\n\n const template = await editor.save();\n if (_destroyed) throw new Error(\"Component unmounted during save\");\n\n snapshotPreview.initSnapshotHistory();\n\n if (snapshotPreview.snapshotHistoryInstance.value) {\n snapshotPreview.snapshotHistoryInstance.value.loadSnapshots();\n }\n\n const exportResult = await exporter.exportHtml(template.id);\n if (_destroyed) throw new Error(\"Component unmounted during save\");\n\n const saveResult: SaveResult = {\n templateId: template.id,\n html: exportResult.html,\n mjml: exportResult.mjml,\n content: template.content,\n };\n\n props.config.onSave?.(saveResult);\n\n featureFlags.saveStatus.value = \"saved\";\n featureFlags.startSaveStatusClear();\n\n return saveResult;\n } catch (error) {\n if (!_destroyed) {\n featureFlags.saveStatus.value = \"error\";\n featureFlags.saveErrorMessage.value =\n error instanceof Error ? error.message : \"Save failed\";\n }\n throw error;\n } finally {\n if (!_destroyed) {\n featureFlags.isSaveExporting.value = false;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lifecycle\n// ---------------------------------------------------------------------------\n\nonMounted(() => {\n initialize();\n});\n\nonUnmounted(() => {\n _destroyed = true;\n props.fontsManager.cleanupFontLinks();\n websocket.disconnect();\n core.destroy();\n props.config.onUnmount?.();\n});\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: setUiTheme,\n setThemeOverrides: setThemeOverrides,\n create: createTemplate,\n load: loadTemplate,\n save: saveTemplate,\n sendTestEmail: testEmail.sendTestEmail,\n});\n</script>\n\n<template>\n <div\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n >\n <!-- Loading overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-100\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudLoadingOverlay\n :visible=\"isInitializing || editor.state.isLoading\"\n />\n </Transition>\n\n <!-- Error overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudErrorOverlay\n :error=\"initError\"\n :visible=\"!!initError && !isInitializing\"\n @retry=\"initialize\"\n />\n </Transition>\n\n <!-- Header — absolute, full width, above everything -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n box-shadow: var(--tpl-shadow-md);\n border-bottom: 1px solid var(--tpl-border);\n \"\n >\n <!-- Left: Logo + template count -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-3\"\n >\n <div\n v-if=\"!featureFlags.isWhiteLabeled.value\"\n class=\"tpl-logo tpl:flex tpl:items-center tpl:gap-2.5 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <img\n :src=\"authManager.resolveUrl('/logo.svg')\"\n alt=\"Templatical\"\n width=\"24\"\n height=\"24\"\n class=\"tpl:shrink-0\"\n />\n <span style=\"letter-spacing: -0.01em\">{{ core.t.header.title }}</span>\n </div>\n <span\n v-if=\"featureFlags.templateLimit.value !== null\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n core.format(core.t.header.templatesUsed, {\n used: featureFlags.templateCount.value,\n max: featureFlags.templateLimit.value,\n })\n }}\n </span>\n </div>\n\n <!-- Center: viewport + preview + dark mode + collaboration + snapshots -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <CollaboratorBar\n v-if=\"collaboration && isCollaborationEnabled\"\n :collaborators=\"collaboration.collaborators.value\"\n :is-connected=\"websocket.isConnected.value\"\n />\n <SnapshotHistory\n v-if=\"snapshotPreview.snapshotHistoryInstance.value\"\n :snapshots=\"snapshotPreview.snapshotHistorySnapshots.value\"\n :is-loading=\"snapshotPreview.snapshotHistoryIsLoading.value\"\n :is-restoring=\"snapshotPreview.snapshotHistoryIsRestoring.value\"\n @load=\"snapshotPreview.loadSnapshotHistory\"\n @navigate=\"snapshotPreview.handleSnapshotNavigate\"\n />\n </div>\n\n <!-- Right: Cloud actions -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Save status indicator -->\n <div\n v-if=\"featureFlags.saveStatus.value === 'error'\"\n aria-live=\"assertive\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"featureFlags.saveErrorMessage.value\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ core.t.header.saveFailed }}\n </div>\n <div\n v-else-if=\"featureFlags.saveStatus.value === 'saved'\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ core.t.header.saved }}\n </div>\n <div\n v-else-if=\"editor.state.isDirty\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ core.t.header.unsaved }}\n </div>\n\n <!-- Comments button -->\n <button\n v-if=\"\n commentsInstance.isEnabled.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :aria-label=\"\n commentsInstance.unresolvedCount.value > 0\n ? `${core.t.comments.button} (${commentsInstance.unresolvedCount.value})`\n : core.t.comments.button\n \"\n :aria-expanded=\"panelState.commentsOpen.value\"\n :class=\"headerBtnClass\"\n :style=\"{\n backgroundColor: panelState.commentsOpen.value\n ? 'var(--tpl-primary)'\n : 'transparent',\n color: panelState.commentsOpen.value\n ? 'var(--tpl-bg)'\n : 'var(--tpl-primary)',\n borderColor: 'var(--tpl-primary)',\n }\"\n @click=\"\n panelState.commentsOpen.value = !panelState.commentsOpen.value\n \"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.comments.button }}\n <span\n v-if=\"\n commentsInstance.unresolvedCount.value > 0 &&\n !panelState.commentsOpen.value\n \"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n >\n {{ commentsInstance.unresolvedCount.value }}\n </span>\n </button>\n\n <!-- AI button + menu -->\n <div\n v-if=\"\n featureFlags.canUseAiGeneration.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :ref=\"(el) => (panelState.aiMenuRef.value = el as HTMLElement | null)\"\n class=\"tpl:relative\"\n >\n <button\n :aria-expanded=\"panelState.aiMenuOpen.value\"\n class=\"tpl-ai-btn tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-4 tpl:py-2 tpl:text-sm tpl:font-semibold tpl:whitespace-nowrap tpl:transition-all tpl:duration-200\"\n :class=\"\n panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ core.t.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-150 tpl:ease-out\"\n enter-from-class=\"tpl:scale-95 tpl:opacity-0\"\n enter-to-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-100 tpl:ease-in\"\n leave-from-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-to-class=\"tpl:scale-95 tpl:opacity-0\"\n >\n <div\n v-if=\"panelState.aiMenuOpen.value\"\n class=\"tpl:absolute tpl:right-0 tpl:top-full tpl:z-50 tpl:mt-1 tpl:origin-top-right\"\n >\n <AiFeatureMenu\n :active-feature=\"panelState.activeAiFeature.value\"\n @select=\"panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n\n <!-- Test email button -->\n <button\n v-if=\"\n testEmail.isEnabled.value && featureFlags.canSendTestEmail.value\n \"\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"\n testEmail.isSending.value || !featureFlags.hasTemplateSaved.value\n \"\n @click=\"panelState.testEmailModalOpen.value = true\"\n >\n <Send\n v-if=\"!testEmail.isSending.value\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n <LoaderCircle\n v-else\n class=\"tpl-spinner\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n {{ core.t.testEmail.button }}\n </button>\n\n <!-- Save button -->\n <button\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"\n editor.state.isSaving ||\n featureFlags.isSaveExporting.value ||\n !editor.state.isDirty\n \"\n @click=\"\n saveTemplate().catch((err) => props.config.onError?.(err as Error))\n \"\n >\n <Save\n v-if=\"!editor.state.isSaving && !featureFlags.isSaveExporting.value\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n <LoaderCircle\n v-else\n class=\"tpl-spinner\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n {{\n editor.state.isSaving || featureFlags.isSaveExporting.value\n ? core.t.header.saving\n : core.t.header.save\n }}\n </button>\n </div>\n </header>\n\n <!-- Snapshot preview banner -->\n <SnapshotPreviewBanner\n :visible=\"snapshotPreview.isPreviewingSnapshot.value\"\n @cancel=\"snapshotPreview.cancelPreview\"\n @confirm=\"snapshotPreview.confirmRestoreSnapshot\"\n />\n\n <!-- Collaboration undo warning toast -->\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-200 tpl:ease-out\"\n enter-from-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n enter-to-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-300 tpl:ease-in\"\n leave-from-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-to-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n >\n <CollabUndoToast\n :visible=\"collabWarning.collabUndoWarningVisible.value\"\n />\n </Transition>\n\n <!-- Left sidebar -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas body -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto\"\n style=\"\n transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);\n background-color: var(--tpl-canvas-bg);\n \"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n snapshotPreview.isPreviewingSnapshot.value\n ? 'tpl:top-[104px]'\n : 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-warning);\n border-color: var(--tpl-warning);\n backdrop-filter: blur(8px);\n \"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"collaboration?.lockedBlocks.value ?? undefined\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"panelState.aiChatOpen.value = true\"\n @open-design-reference=\"panelState.designReferenceOpen.value = true\"\n />\n </main>\n </div>\n\n <!-- Footer — powered-by branding (hidden when white-labeled) -->\n <footer\n v-if=\"!featureFlags.isWhiteLabeled.value\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:bottom-0 tpl:z-50 tpl:flex tpl:h-8 tpl:items-center tpl:justify-end tpl:pr-4 tpl:text-[9px] tpl:opacity-90 tpl:transition-all tpl:duration-300 tpl:text-[var(--tpl-text-dim)]\"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n >\n <div\n class=\"tpl:pointer-events-auto tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-tl-lg tpl:p-1\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-canvas-bg) 85%,\n transparent\n );\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n >\n <span>{{ core.t.footer.poweredBy }}</span>\n <a\n href=\"https://templatical.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1 tpl:font-medium tpl:transition-colors tpl:duration-150 hover:tpl:opacity-80 tpl:text-[var(--tpl-text-muted)]\"\n style=\"text-decoration: none\"\n >\n <img\n width=\"14\"\n height=\"14\"\n src=\"https://templatical.com/logo.svg\"\n alt=\"\"\n />\n Templatical\n </a>\n <span class=\"tpl:text-[var(--tpl-border)]\">·</span>\n <a\n href=\"https://github.com/templatical/sdk\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:transition-colors tpl:duration-150 hover:tpl:opacity-80 tpl:text-[var(--tpl-text-dim)]\"\n style=\"text-decoration: none\"\n >\n {{ core.t.footer.openSource }}\n </a>\n </div>\n </footer>\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"panelState.rightPanelOpen.value\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.selectedBlock.value!.id, updates)\n \"\n @delete-block=\"\n core.blockActions.deleteBlock(editor.selectedBlock.value!.id)\n \"\n @duplicate-block=\"\n core.blockActions.duplicateBlock(editor.selectedBlock.value!)\n \"\n @update-settings=\"editor.updateSettings\"\n />\n\n <!-- Cloud sidebars + modals — only mount after cloud init completes -->\n <template v-if=\"!isInitializing && isAuthReady\">\n <AiChatSidebar\n :visible=\"panelState.aiChatOpen.value\"\n :on-apply=\"\n (content: TemplateContent) => {\n core.history.record();\n editor.setContent(content);\n core.conditionPreview.reset();\n }\n \"\n @close=\"panelState.aiChatOpen.value = false\"\n />\n\n <TemplateScoringPanel\n :visible=\"panelState.scoringPanelOpen.value\"\n @close=\"panelState.scoringPanelOpen.value = false\"\n />\n\n <DesignReferenceSidebar\n :visible=\"panelState.designReferenceOpen.value\"\n :has-existing-blocks=\"editor.content.value.blocks.length > 0\"\n @close=\"panelState.designReferenceOpen.value = false\"\n @apply=\"\n (content: TemplateContent) => {\n core.history.record();\n editor.setContent(content);\n core.conditionPreview.reset();\n }\n \"\n />\n\n <CommentsSidebar\n ref=\"commentsSidebarRef\"\n :visible=\"panelState.commentsOpen.value\"\n @close=\"panelState.commentsOpen.value = false\"\n />\n\n <TestEmailModal\n :visible=\"panelState.testEmailModalOpen.value\"\n :allowed-emails=\"testEmail.allowedEmails.value\"\n :is-sending=\"testEmail.isSending.value\"\n :error=\"testEmail.error.value\"\n @send=\"handleSendTestEmail\"\n @close=\"panelState.testEmailModalOpen.value = false\"\n />\n\n <SaveModuleDialog\n v-if=\"\n planConfigInstance.hasFeature('saved_modules') &&\n props.config.modules !== false\n \"\n :visible=\"showSaveModuleDialog\"\n :pre-selected-block-id=\"saveModulePreSelectedBlockId\"\n @close=\"\n showSaveModuleDialog = false;\n saveModulePreSelectedBlockId = null;\n \"\n @saved=\"savedModulesHeadless.loadModules()\"\n />\n\n <ModuleBrowserModal\n v-if=\"\n planConfigInstance.hasFeature('saved_modules') &&\n props.config.modules !== false\n \"\n :visible=\"showModuleBrowserModal\"\n @close=\"showModuleBrowserModal = false\"\n @insert=\"handleModuleInsert\"\n />\n\n <MediaLibraryModal\n :visible=\"panelState.mediaLibraryOpen.value\"\n :accept=\"panelState.mediaLibraryAccept.value\"\n @select=\"mediaLib.handleMediaSelect\"\n @close=\"mediaLib.handleMediaLibraryClose\"\n />\n </template>\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","<script setup lang=\"ts\">\nimport type {\n Block,\n CollaborationConfig,\n CommentEvent,\n CustomBlockDefinition,\n DisplayConditionsConfig,\n FontsConfig,\n McpConfig,\n MergeTagsConfig,\n SaveResult,\n Template,\n TemplateContent,\n ThemeOverrides,\n UiTheme,\n} from \"@templatical/types\";\nimport type {\n MediaItem,\n MediaRequestContext,\n} from \"@templatical/media-library\";\nimport { cloneBlock, isCustomBlock } from \"@templatical/types\";\nimport type { CustomBlock } from \"@templatical/types\";\n\nimport {\n AuthManager,\n performHealthCheck,\n resolveWebSocketConfig,\n useAiConfig,\n useCollaboration,\n useCollaborationBroadcast,\n useCommentListener,\n useComments,\n useEditor,\n useExport,\n useMcpListener,\n usePlanConfig,\n useSavedModules,\n useTemplateScoring,\n useTestEmail,\n useWebSocket,\n type UseCollaborationReturn,\n} from \"@templatical/core/cloud\";\nimport type { UseFontsReturn } from \"../composables/useFonts\";\nimport type { McpOperationPayload } from \"@templatical/types\";\nimport {\n computed,\n defineAsyncComponent,\n nextTick,\n onMounted,\n onUnmounted,\n provide,\n ref,\n watch,\n} from \"vue\";\nimport {\n Check,\n CircleAlert,\n LoaderCircle,\n MessageCircle,\n RotateCcw,\n Save,\n Send,\n Sparkles,\n} from \"@lucide/vue\";\nimport type { Translations } from \"../i18n\";\n\nimport { useEditorCore } from \"../composables/useEditorCore\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\nimport {\n ON_REQUEST_MEDIA_KEY,\n AUTH_MANAGER_KEY,\n AI_CONFIG_KEY,\n COMMENTS_KEY,\n SAVED_MODULES_HEADLESS_KEY,\n SCORING_KEY,\n CAPABILITIES_KEY,\n} from \"../keys\";\nimport type { UseSnapshotPreviewReturn } from \"./composables/useSnapshotPreview\";\nimport { useSnapshotPreview } from \"./composables/useSnapshotPreview\";\nimport { useCloudPanelState } from \"./composables/useCloudPanelState\";\nimport { useCollabUndoWarning } from \"./composables/useCollabUndoWarning\";\nimport { useCloudFeatureFlags } from \"./composables/useCloudFeatureFlags\";\nimport { useCloudMediaLibrary } from \"./composables/useCloudMediaLibrary\";\nimport { useDragDrop } from \"../composables/useDragDrop\";\nimport { DEFAULT_AUTO_SAVE_DEBOUNCE_MS } from \"../constants/timeouts\";\nimport { headerBtnClass } from \"../constants/styleConstants\";\n\nimport Canvas from \"../components/Canvas.vue\";\nimport Sidebar from \"../components/Sidebar.vue\";\nimport RightSidebar from \"../components/RightSidebar.vue\";\nimport ViewportToggle from \"../components/ViewportToggle.vue\";\nimport PreviewToggle from \"../components/PreviewToggle.vue\";\nimport DarkModeToggle from \"../components/DarkModeToggle.vue\";\nimport CloudLoadingOverlay from \"./components/CloudLoadingOverlay.vue\";\nimport CloudErrorOverlay from \"./components/CloudErrorOverlay.vue\";\nimport SnapshotPreviewBanner from \"./components/SnapshotPreviewBanner.vue\";\nimport CollabUndoToast from \"./components/CollabUndoToast.vue\";\nimport \"../styles/index.css\";\n\n// Cloud async components\nconst AiChatSidebar = defineAsyncComponent(\n () => import(\"./components/AiChatSidebar.vue\"),\n);\nconst CommentsSidebar = defineAsyncComponent(\n () => import(\"./components/CommentsSidebar.vue\"),\n);\nconst DesignReferenceSidebar = defineAsyncComponent(\n () => import(\"./components/DesignReferenceSidebar.vue\"),\n);\nconst TemplateScoringPanel = defineAsyncComponent(\n () => import(\"./components/TemplateScoringPanel.vue\"),\n);\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./components/TestEmailModal.vue\"),\n);\nconst SaveModuleDialog = defineAsyncComponent(\n () => import(\"./components/SaveModuleDialog.vue\"),\n);\nconst ModuleBrowserModal = defineAsyncComponent(\n () => import(\"./components/ModuleBrowserModal.vue\"),\n);\nconst SnapshotHistory = defineAsyncComponent(\n () => import(\"./components/SnapshotHistory.vue\"),\n);\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./components/CollaboratorBar.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(\n () => import(\"./components/AiFeatureMenu.vue\"),\n);\nconst MediaLibraryModal = defineAsyncComponent(async () => {\n const m = await import(\"@templatical/media-library\");\n return m.MediaLibraryModal;\n});\n\n// ---------------------------------------------------------------------------\n// Config type — flat cloud config extending OSS\n// ---------------------------------------------------------------------------\n\nexport interface TemplaticalCloudEditorConfig {\n container: string | HTMLElement;\n content?: TemplateContent;\n\n auth: {\n url: string;\n baseUrl?: string;\n requestOptions?: {\n method?: \"GET\" | \"POST\";\n headers?: Record<string, string>;\n body?: Record<string, unknown>;\n credentials?: RequestCredentials;\n };\n };\n\n theme?: ThemeOverrides;\n uiTheme?: UiTheme;\n locale?: string;\n\n ai?: import(\"@templatical/types\").AiConfig | false;\n commenting?: boolean;\n collaboration?: CollaborationConfig;\n mcp?: McpConfig;\n blockDefaults?: import(\"@templatical/types\").BlockDefaults;\n templateDefaults?: import(\"@templatical/types\").TemplateDefaults;\n\n modules?: boolean;\n autoSave?: boolean;\n autoSaveDebounce?: number;\n\n mergeTags?: MergeTagsConfig;\n displayConditions?: DisplayConditionsConfig;\n customBlocks?: CustomBlockDefinition[];\n fonts?: FontsConfig;\n onChange?: (content: TemplateContent) => void;\n onSave?: (result: SaveResult) => void;\n onCreate?: (template: Template) => void;\n onLoad?: (template: Template) => void;\n onError?: (error: Error) => void;\n onComment?: (event: CommentEvent) => void;\n onUnmount?: () => void;\n\n onRequestMedia?: (context: MediaRequestContext) => Promise<MediaItem | null>;\n onBeforeTestEmail?: (html: string) => string | Promise<string>;\n}\n\nconst props = defineProps<{\n config: TemplaticalCloudEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n}>();\nconst emit = defineEmits<{\n (e: \"ready\"): void;\n}>();\n\n// ---------------------------------------------------------------------------\n// Cloud initialization state\n// ---------------------------------------------------------------------------\n\nconst isInitializing = ref(true);\nconst isAuthReady = ref(false);\nconst initError = ref<Error | null>(null);\n\n// Tracks whether the component has been unmounted. Checked after every await\n// in async lifecycle functions to prevent post-unmount side effects.\nlet _destroyed = false;\n\n// ---------------------------------------------------------------------------\n// 1. AuthManager + PlanConfig (infrastructure)\n// ---------------------------------------------------------------------------\n\nconst authManager = new AuthManager({\n ...props.config.auth,\n onError: props.config.onError,\n});\n\nconst planConfigInstance = usePlanConfig({\n authManager,\n onError: props.config.onError,\n});\n\n// ---------------------------------------------------------------------------\n// 2. Collaboration locked blocks ref\n// ---------------------------------------------------------------------------\n\nconst collaborationLockedBlocks = ref<Map<string, unknown>>(new Map());\n\n// ---------------------------------------------------------------------------\n// 3. Cloud editor (API-backed)\n// ---------------------------------------------------------------------------\n\nconst editor = useEditor({\n authManager,\n defaultFontFamily: props.config.fonts?.defaultFont,\n templateDefaults: props.config.templateDefaults,\n onError: props.config.onError,\n lockedBlocks: collaborationLockedBlocks,\n});\n\n// ---------------------------------------------------------------------------\n// 4. WebSocket + MCP listener\n// ---------------------------------------------------------------------------\n\nconst websocket = useWebSocket({\n authManager,\n onError: props.config.onError,\n});\n\nif (props.config.mcp?.enabled) {\n useMcpListener({\n editor,\n channel: websocket.channel,\n onOperation: props.config.mcp.onOperation,\n });\n}\n\n// ---------------------------------------------------------------------------\n// 5. Collaboration — MUST be before useEditorCore so broadcast wraps\n// editor methods first, then useHistoryInterceptor wraps AFTER\n// ---------------------------------------------------------------------------\n\nlet collaboration:\n | (UseCollaborationReturn & {\n _broadcastOperation: (payload: McpOperationPayload) => void;\n _isProcessingRemoteOperation: () => boolean;\n })\n | null = null;\n\nif (props.config.collaboration?.enabled) {\n collaboration = useCollaboration({\n authManager,\n editor,\n channel: websocket.channel,\n onError: props.config.onError,\n onCollaboratorJoined: props.config.collaboration.onCollaboratorJoined,\n onCollaboratorLeft: props.config.collaboration.onCollaboratorLeft,\n onBlockLocked: props.config.collaboration.onBlockLocked,\n onBlockUnlocked: props.config.collaboration.onBlockUnlocked,\n });\n\n // Sync locked blocks from collaboration to editor\n watch(\n () => collaboration!.lockedBlocks.value,\n (newLockedBlocks) => {\n collaborationLockedBlocks.value = newLockedBlocks;\n },\n { immediate: true },\n );\n\n // Wrap editor methods to broadcast operations to peers\n useCollaborationBroadcast(editor, collaboration);\n}\n\nconst isCollaborationEnabled = computed(\n () =>\n !!props.config.collaboration?.enabled &&\n planConfigInstance.hasFeature(\"collaboration\"),\n);\n\n// ---------------------------------------------------------------------------\n// 6. useEditorCore — shared composables, provides, plugins, keyboard\n// ---------------------------------------------------------------------------\n\n// Forward references for circular dependencies resolved after setup\nlet snapshotPreviewRef: UseSnapshotPreviewReturn | null = null;\nlet collabWarningRef: ReturnType<typeof useCollabUndoWarning> | null = null;\n\nconst core = useEditorCore({\n editor,\n config: {\n uiTheme: props.config.uiTheme,\n theme: undefined, // Cloud applies theme in initialize() after plan check\n blockDefaults: props.config.blockDefaults,\n customBlocks: [], // Cloud defers registration to initialize()\n mergeTags: props.config.mergeTags,\n displayConditions: props.config.displayConditions,\n onRequestMedia: null, // Cloud uses handleRequestMedia via media library composable\n onSave: () => {\n saveTemplate().catch((err) => {\n props.config.onError?.(err as Error);\n });\n },\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n historyOptions: collaboration\n ? { isRemoteOperation: () => collaboration!._isProcessingRemoteOperation() }\n : undefined,\n autoSaveOptions: {\n onChange: async () => {\n if (editor.hasTemplate()) {\n await editor.createSnapshot();\n snapshotPreviewRef?.snapshotHistoryInstance.value?.loadSnapshots();\n }\n },\n debounce: props.config.autoSaveDebounce ?? DEFAULT_AUTO_SAVE_DEBOUNCE_MS,\n enabled: () =>\n props.config.autoSave !== false &&\n planConfigInstance.hasFeature(\"auto_save\"),\n },\n themeExtraStyles: () => ({\n \"--tpl-drop-text\": `\"${props.translations.canvas.dropHere}\"`,\n }),\n keyboardOptions: {\n onBeforeUndo: () => collabWarningRef?.showCollabUndoWarning(),\n },\n});\n\n// ---------------------------------------------------------------------------\n// 7. Collab undo warning (created after core so it can use core.history.canUndo)\n// ---------------------------------------------------------------------------\n\nconst collabWarning = useCollabUndoWarning({\n isCollaborationEnabled,\n getCollaboratorCount: () => collaboration?.collaborators.value.length ?? 0,\n canUndo: core.history.canUndo,\n});\ncollabWarningRef = collabWarning;\n\n// ---------------------------------------------------------------------------\n// 8. Snapshot preview (needs core.autoSave for pause/resume)\n// ---------------------------------------------------------------------------\n\nconst snapshotPreview = useSnapshotPreview({\n authManager,\n editor,\n history: core.history,\n conditionPreview: core.conditionPreview,\n autoSave: core.autoSave,\n onError: props.config.onError,\n});\n\n// Connect forward reference for autoSave onChange\nsnapshotPreviewRef = snapshotPreview;\n\n// ---------------------------------------------------------------------------\n// 9. Remaining cloud composables\n// ---------------------------------------------------------------------------\n\nconst panelState = useCloudPanelState();\n\nconst aiConfig = useAiConfig(props.config.ai);\n\nconst featureFlags = useCloudFeatureFlags({\n planConfigInstance,\n aiConfig,\n editor,\n});\n\nconst mediaLib = useCloudMediaLibrary({\n onRequestMedia: props.config.onRequestMedia,\n mediaLibraryOpen: panelState.mediaLibraryOpen,\n mediaLibraryAccept: panelState.mediaLibraryAccept,\n});\n\nconst _dragDrop = useDragDrop({\n onBlockMove: editor.moveBlock,\n onBlockAdd: editor.addBlock,\n});\n\nconst exporter = useExport({\n authManager,\n getFontsConfig: () => props.config.fonts,\n canUseCustomFonts: () => planConfigInstance.hasFeature(\"custom_fonts\"),\n});\n\nconst testEmail = useTestEmail({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n save: () => editor.save(),\n exportHtml: (templateId: string) => exporter.exportHtml(templateId),\n onError: props.config.onError,\n isAuthReady,\n onBeforeTestEmail: props.config.onBeforeTestEmail,\n});\n\nconst commentsInstance = useComments({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n getSocketId: () => websocket.getSocketId(),\n onComment: props.config.onComment,\n onError: props.config.onError,\n isAuthReady,\n hasCommentingFeature: () =>\n props.config.commenting !== false &&\n planConfigInstance.hasFeature(\"commenting\"),\n});\n\nuseCommentListener({\n comments: commentsInstance,\n channel: websocket.channel,\n});\n\nconst savedModulesHeadless = useSavedModules({\n authManager,\n onError: props.config.onError,\n});\nconst showSaveModuleDialog = ref(false);\nconst saveModulePreSelectedBlockId = ref<string | null>(null);\nconst showModuleBrowserModal = ref(false);\n\nconst scoringInstance = useTemplateScoring({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n});\n\n// ---------------------------------------------------------------------------\n// 10. Cloud-only provides\n// ---------------------------------------------------------------------------\n\nprovide(ON_REQUEST_MEDIA_KEY, mediaLib.handleRequestMedia);\nprovide(AUTH_MANAGER_KEY, authManager);\nprovide(AI_CONFIG_KEY, aiConfig);\nprovide(COMMENTS_KEY, commentsInstance);\nprovide(SAVED_MODULES_HEADLESS_KEY, savedModulesHeadless);\nprovide(SCORING_KEY, scoringInstance);\n\n// Override the default empty capabilities from useEditorCore with cloud capabilities.\n// OSS components use this single inject instead of individual cloud injects.\nprovide(CAPABILITIES_KEY, {\n plan: planConfigInstance,\n ai: aiConfig,\n comments: {\n getBlockCount: (blockId: string) =>\n commentsInstance.commentCountByBlock.value.get(blockId) ?? 0,\n openForBlock: openCommentsForBlock,\n },\n savedModules: {\n openSaveDialog: (blockId: string) => {\n saveModulePreSelectedBlockId.value = blockId ?? null;\n showSaveModuleDialog.value = true;\n },\n openBrowser: () => {\n showModuleBrowserModal.value = true;\n },\n moduleCount: computed(() => savedModulesHeadless.modules.value.length),\n },\n} satisfies EditorCapabilities);\n\n// ---------------------------------------------------------------------------\n// Theme overrides (plan-gated)\n// ---------------------------------------------------------------------------\n\nfunction setThemeOverrides(overrides: ThemeOverrides): void {\n if (!planConfigInstance.hasFeature(\"theme_customization\")) {\n return;\n }\n core.themeOverrides.value = overrides;\n}\n\nfunction setUiTheme(theme: UiTheme): void {\n editor.setUiTheme(theme);\n}\n\n// ---------------------------------------------------------------------------\n// Comments sidebar ref for block filtering\n// ---------------------------------------------------------------------------\n\nconst commentsSidebarRef = ref<InstanceType<typeof CommentsSidebar> | null>(\n null,\n);\n\nfunction openCommentsForBlock(blockId: string): void {\n panelState.commentsOpen.value = true;\n nextTick(() => {\n commentsSidebarRef.value?.filterByBlock(blockId);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Test email handler\n// ---------------------------------------------------------------------------\n\nasync function handleSendTestEmail(recipient: string): Promise<void> {\n try {\n await testEmail.sendTestEmail(recipient);\n panelState.testEmailModalOpen.value = false;\n } catch {\n // Error is already handled in the composable\n }\n}\n\n// ---------------------------------------------------------------------------\n// Module insert handler\n// ---------------------------------------------------------------------------\n\nfunction handleModuleInsert(\n module: { content: Block[] },\n insertIndex: number | undefined,\n): void {\n for (let i = 0; i < module.content.length; i++) {\n const cloned = cloneBlock(module.content[i]);\n const position = insertIndex !== undefined ? insertIndex + i : undefined;\n editor.addBlock(cloned, undefined, undefined, position);\n }\n showModuleBrowserModal.value = false;\n}\n\n// ---------------------------------------------------------------------------\n// Custom blocks pre-render for save\n// ---------------------------------------------------------------------------\n\nasync function preRenderCustomBlocks(content: TemplateContent): Promise<void> {\n const renderBlock = async (block: Block): Promise<void> => {\n if (isCustomBlock(block)) {\n const customBlock = block as CustomBlock;\n try {\n customBlock.renderedHtml =\n await core.registry.renderCustomBlock(customBlock);\n } catch {\n customBlock.renderedHtml = `<!-- Custom block render error: ${customBlock.customType} -->`;\n }\n }\n\n if (block.type === \"section\" && \"children\" in block) {\n const sectionBlock = block as { children: Block[][] };\n for (const column of sectionBlock.children) {\n for (const child of column) {\n await renderBlock(child);\n }\n }\n }\n };\n\n for (const block of content.blocks) {\n await renderBlock(block);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cloud initialization\n// ---------------------------------------------------------------------------\n\nasync function initialize(): Promise<void> {\n isInitializing.value = true;\n initError.value = null;\n\n try {\n // Auth\n await authManager.initialize();\n if (_destroyed) return;\n isAuthReady.value = true;\n\n // Health check\n const healthResult = await performHealthCheck({ authManager });\n if (_destroyed) return;\n\n if (!healthResult.api.ok) {\n throw new Error(\"Health check failed: API is not reachable\");\n }\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\n if (!healthResult.websocket.ok) {\n console.warn(\n \"[Templatical] WebSocket health check failed:\",\n healthResult.websocket.error ?? \"unknown error\",\n \"-- real-time features will be disabled.\",\n );\n }\n\n // Plan config\n await planConfigInstance.fetchConfig();\n if (_destroyed) return;\n\n // Update fonts\n props.fontsManager.setCustomFontsEnabled(\n planConfigInstance.hasFeature(\"custom_fonts\"),\n );\n\n // Register custom blocks if feature is enabled and definitions provided\n if (\n props.config.customBlocks?.length &&\n planConfigInstance.hasFeature(\"custom_blocks\")\n ) {\n core.registerCustomBlocks(props.config.customBlocks);\n }\n\n // Apply theme\n if (\n props.config.theme &&\n planConfigInstance.hasFeature(\"theme_customization\")\n ) {\n core.themeOverrides.value = props.config.theme;\n }\n\n // Load saved modules\n if (\n props.config.modules !== false &&\n planConfigInstance.hasFeature(\"saved_modules\")\n ) {\n savedModulesHeadless.loadModules();\n }\n\n emit(\"ready\");\n } catch (error) {\n if (_destroyed) return;\n const wrappedError =\n error instanceof Error\n ? error\n : new Error(\"Initialization failed\", { cause: error });\n initError.value = wrappedError;\n props.config.onError?.(wrappedError);\n } finally {\n if (!_destroyed) {\n isInitializing.value = false;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Template lifecycle methods\n// ---------------------------------------------------------------------------\n\nfunction getWebSocketConfig() {\n return resolveWebSocketConfig(planConfigInstance.config.value!.websocket);\n}\n\nasync function createTemplate(content?: TemplateContent): Promise<Template> {\n const template = await editor.create(content);\n if (_destroyed) return template;\n props.config.onCreate?.(template);\n snapshotPreview.initSnapshotHistory();\n websocket.connect(template.id, getWebSocketConfig());\n return template;\n}\n\nasync function loadTemplate(templateId: string): Promise<Template> {\n const template = await editor.load(templateId);\n if (_destroyed) return template;\n props.config.onLoad?.(template);\n snapshotPreview.initSnapshotHistory();\n websocket.connect(template.id, getWebSocketConfig());\n return template;\n}\n\nasync function saveTemplate(): Promise<SaveResult> {\n featureFlags.isSaveExporting.value = true;\n featureFlags.saveStatus.value = \"idle\";\n try {\n // Pre-render custom blocks so backend can include them in MJML export\n await preRenderCustomBlocks(editor.content.value);\n if (_destroyed) throw new Error(\"Component unmounted during save\");\n\n const template = await editor.save();\n if (_destroyed) throw new Error(\"Component unmounted during save\");\n\n snapshotPreview.initSnapshotHistory();\n\n if (snapshotPreview.snapshotHistoryInstance.value) {\n snapshotPreview.snapshotHistoryInstance.value.loadSnapshots();\n }\n\n const exportResult = await exporter.exportHtml(template.id);\n if (_destroyed) throw new Error(\"Component unmounted during save\");\n\n const saveResult: SaveResult = {\n templateId: template.id,\n html: exportResult.html,\n mjml: exportResult.mjml,\n content: template.content,\n };\n\n props.config.onSave?.(saveResult);\n\n featureFlags.saveStatus.value = \"saved\";\n featureFlags.startSaveStatusClear();\n\n return saveResult;\n } catch (error) {\n if (!_destroyed) {\n featureFlags.saveStatus.value = \"error\";\n featureFlags.saveErrorMessage.value =\n error instanceof Error ? error.message : \"Save failed\";\n }\n throw error;\n } finally {\n if (!_destroyed) {\n featureFlags.isSaveExporting.value = false;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lifecycle\n// ---------------------------------------------------------------------------\n\nonMounted(() => {\n initialize();\n});\n\nonUnmounted(() => {\n _destroyed = true;\n props.fontsManager.cleanupFontLinks();\n websocket.disconnect();\n core.destroy();\n props.config.onUnmount?.();\n});\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: setUiTheme,\n setThemeOverrides: setThemeOverrides,\n create: createTemplate,\n load: loadTemplate,\n save: saveTemplate,\n sendTestEmail: testEmail.sendTestEmail,\n});\n</script>\n\n<template>\n <div\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n >\n <!-- Loading overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-100\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudLoadingOverlay\n :visible=\"isInitializing || editor.state.isLoading\"\n />\n </Transition>\n\n <!-- Error overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudErrorOverlay\n :error=\"initError\"\n :visible=\"!!initError && !isInitializing\"\n @retry=\"initialize\"\n />\n </Transition>\n\n <!-- Header — absolute, full width, above everything -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n box-shadow: var(--tpl-shadow-md);\n border-bottom: 1px solid var(--tpl-border);\n \"\n >\n <!-- Left: Logo + template count -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-3\"\n >\n <div\n v-if=\"!featureFlags.isWhiteLabeled.value\"\n class=\"tpl-logo tpl:flex tpl:items-center tpl:gap-2.5 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <img\n :src=\"authManager.resolveUrl('/logo.svg')\"\n alt=\"Templatical\"\n width=\"24\"\n height=\"24\"\n class=\"tpl:shrink-0\"\n />\n <span style=\"letter-spacing: -0.01em\">{{ core.t.header.title }}</span>\n </div>\n <span\n v-if=\"featureFlags.templateLimit.value !== null\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n core.format(core.t.header.templatesUsed, {\n used: featureFlags.templateCount.value,\n max: featureFlags.templateLimit.value,\n })\n }}\n </span>\n </div>\n\n <!-- Center: viewport + preview + dark mode + collaboration + snapshots -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <CollaboratorBar\n v-if=\"collaboration && isCollaborationEnabled\"\n :collaborators=\"collaboration.collaborators.value\"\n :is-connected=\"websocket.isConnected.value\"\n />\n <SnapshotHistory\n v-if=\"snapshotPreview.snapshotHistoryInstance.value\"\n :snapshots=\"snapshotPreview.snapshotHistorySnapshots.value\"\n :is-loading=\"snapshotPreview.snapshotHistoryIsLoading.value\"\n :is-restoring=\"snapshotPreview.snapshotHistoryIsRestoring.value\"\n @load=\"snapshotPreview.loadSnapshotHistory\"\n @navigate=\"snapshotPreview.handleSnapshotNavigate\"\n />\n </div>\n\n <!-- Right: Cloud actions -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Save status indicator -->\n <div\n v-if=\"featureFlags.saveStatus.value === 'error'\"\n aria-live=\"assertive\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"featureFlags.saveErrorMessage.value\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ core.t.header.saveFailed }}\n </div>\n <div\n v-else-if=\"featureFlags.saveStatus.value === 'saved'\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ core.t.header.saved }}\n </div>\n <div\n v-else-if=\"editor.state.isDirty\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ core.t.header.unsaved }}\n </div>\n\n <!-- Comments button -->\n <button\n v-if=\"\n commentsInstance.isEnabled.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :aria-label=\"\n commentsInstance.unresolvedCount.value > 0\n ? `${core.t.comments.button} (${commentsInstance.unresolvedCount.value})`\n : core.t.comments.button\n \"\n :aria-expanded=\"panelState.commentsOpen.value\"\n :class=\"headerBtnClass\"\n :style=\"{\n backgroundColor: panelState.commentsOpen.value\n ? 'var(--tpl-primary)'\n : 'transparent',\n color: panelState.commentsOpen.value\n ? 'var(--tpl-bg)'\n : 'var(--tpl-primary)',\n borderColor: 'var(--tpl-primary)',\n }\"\n @click=\"\n panelState.commentsOpen.value = !panelState.commentsOpen.value\n \"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.comments.button }}\n <span\n v-if=\"\n commentsInstance.unresolvedCount.value > 0 &&\n !panelState.commentsOpen.value\n \"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n >\n {{ commentsInstance.unresolvedCount.value }}\n </span>\n </button>\n\n <!-- AI button + menu -->\n <div\n v-if=\"\n featureFlags.canUseAiGeneration.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :ref=\"(el) => (panelState.aiMenuRef.value = el as HTMLElement | null)\"\n class=\"tpl:relative\"\n >\n <button\n :aria-expanded=\"panelState.aiMenuOpen.value\"\n class=\"tpl-ai-btn tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-4 tpl:py-2 tpl:text-sm tpl:font-semibold tpl:whitespace-nowrap tpl:transition-all tpl:duration-200\"\n :class=\"\n panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ core.t.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-150 tpl:ease-out\"\n enter-from-class=\"tpl:scale-95 tpl:opacity-0\"\n enter-to-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-100 tpl:ease-in\"\n leave-from-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-to-class=\"tpl:scale-95 tpl:opacity-0\"\n >\n <div\n v-if=\"panelState.aiMenuOpen.value\"\n class=\"tpl:absolute tpl:right-0 tpl:top-full tpl:z-50 tpl:mt-1 tpl:origin-top-right\"\n >\n <AiFeatureMenu\n :active-feature=\"panelState.activeAiFeature.value\"\n @select=\"panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n\n <!-- Test email button -->\n <button\n v-if=\"\n testEmail.isEnabled.value && featureFlags.canSendTestEmail.value\n \"\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"\n testEmail.isSending.value || !featureFlags.hasTemplateSaved.value\n \"\n @click=\"panelState.testEmailModalOpen.value = true\"\n >\n <Send\n v-if=\"!testEmail.isSending.value\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n <LoaderCircle\n v-else\n class=\"tpl-spinner\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n {{ core.t.testEmail.button }}\n </button>\n\n <!-- Save button -->\n <button\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"\n editor.state.isSaving ||\n featureFlags.isSaveExporting.value ||\n !editor.state.isDirty\n \"\n @click=\"\n saveTemplate().catch((err) => props.config.onError?.(err as Error))\n \"\n >\n <Save\n v-if=\"!editor.state.isSaving && !featureFlags.isSaveExporting.value\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n <LoaderCircle\n v-else\n class=\"tpl-spinner\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n {{\n editor.state.isSaving || featureFlags.isSaveExporting.value\n ? core.t.header.saving\n : core.t.header.save\n }}\n </button>\n </div>\n </header>\n\n <!-- Snapshot preview banner -->\n <SnapshotPreviewBanner\n :visible=\"snapshotPreview.isPreviewingSnapshot.value\"\n @cancel=\"snapshotPreview.cancelPreview\"\n @confirm=\"snapshotPreview.confirmRestoreSnapshot\"\n />\n\n <!-- Collaboration undo warning toast -->\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-200 tpl:ease-out\"\n enter-from-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n enter-to-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-300 tpl:ease-in\"\n leave-from-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-to-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n >\n <CollabUndoToast\n :visible=\"collabWarning.collabUndoWarningVisible.value\"\n />\n </Transition>\n\n <!-- Left sidebar -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas body -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto\"\n style=\"\n transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);\n background-color: var(--tpl-canvas-bg);\n \"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n snapshotPreview.isPreviewingSnapshot.value\n ? 'tpl:top-[104px]'\n : 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-warning);\n border-color: var(--tpl-warning);\n backdrop-filter: blur(8px);\n \"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"collaboration?.lockedBlocks.value ?? undefined\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"panelState.aiChatOpen.value = true\"\n @open-design-reference=\"panelState.designReferenceOpen.value = true\"\n />\n </main>\n </div>\n\n <!-- Footer — powered-by branding (hidden when white-labeled) -->\n <footer\n v-if=\"!featureFlags.isWhiteLabeled.value\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:bottom-0 tpl:z-50 tpl:flex tpl:h-8 tpl:items-center tpl:justify-end tpl:pr-4 tpl:text-[9px] tpl:opacity-90 tpl:transition-all tpl:duration-300 tpl:text-[var(--tpl-text-dim)]\"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n >\n <div\n class=\"tpl:pointer-events-auto tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-tl-lg tpl:p-1\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-canvas-bg) 85%,\n transparent\n );\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n >\n <span>{{ core.t.footer.poweredBy }}</span>\n <a\n href=\"https://templatical.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1 tpl:font-medium tpl:transition-colors tpl:duration-150 hover:tpl:opacity-80 tpl:text-[var(--tpl-text-muted)]\"\n style=\"text-decoration: none\"\n >\n <img\n width=\"14\"\n height=\"14\"\n src=\"https://templatical.com/logo.svg\"\n alt=\"\"\n />\n Templatical\n </a>\n <span class=\"tpl:text-[var(--tpl-border)]\">·</span>\n <a\n href=\"https://github.com/templatical/sdk\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:transition-colors tpl:duration-150 hover:tpl:opacity-80 tpl:text-[var(--tpl-text-dim)]\"\n style=\"text-decoration: none\"\n >\n {{ core.t.footer.openSource }}\n </a>\n </div>\n </footer>\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"panelState.rightPanelOpen.value\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.selectedBlock.value!.id, updates)\n \"\n @delete-block=\"\n core.blockActions.deleteBlock(editor.selectedBlock.value!.id)\n \"\n @duplicate-block=\"\n core.blockActions.duplicateBlock(editor.selectedBlock.value!)\n \"\n @update-settings=\"editor.updateSettings\"\n />\n\n <!-- Cloud sidebars + modals — only mount after cloud init completes -->\n <template v-if=\"!isInitializing && isAuthReady\">\n <AiChatSidebar\n :visible=\"panelState.aiChatOpen.value\"\n :on-apply=\"\n (content: TemplateContent) => {\n core.history.record();\n editor.setContent(content);\n core.conditionPreview.reset();\n }\n \"\n @close=\"panelState.aiChatOpen.value = false\"\n />\n\n <TemplateScoringPanel\n :visible=\"panelState.scoringPanelOpen.value\"\n @close=\"panelState.scoringPanelOpen.value = false\"\n />\n\n <DesignReferenceSidebar\n :visible=\"panelState.designReferenceOpen.value\"\n :has-existing-blocks=\"editor.content.value.blocks.length > 0\"\n @close=\"panelState.designReferenceOpen.value = false\"\n @apply=\"\n (content: TemplateContent) => {\n core.history.record();\n editor.setContent(content);\n core.conditionPreview.reset();\n }\n \"\n />\n\n <CommentsSidebar\n ref=\"commentsSidebarRef\"\n :visible=\"panelState.commentsOpen.value\"\n @close=\"panelState.commentsOpen.value = false\"\n />\n\n <TestEmailModal\n :visible=\"panelState.testEmailModalOpen.value\"\n :allowed-emails=\"testEmail.allowedEmails.value\"\n :is-sending=\"testEmail.isSending.value\"\n :error=\"testEmail.error.value\"\n @send=\"handleSendTestEmail\"\n @close=\"panelState.testEmailModalOpen.value = false\"\n />\n\n <SaveModuleDialog\n v-if=\"\n planConfigInstance.hasFeature('saved_modules') &&\n props.config.modules !== false\n \"\n :visible=\"showSaveModuleDialog\"\n :pre-selected-block-id=\"saveModulePreSelectedBlockId\"\n @close=\"\n showSaveModuleDialog = false;\n saveModulePreSelectedBlockId = null;\n \"\n @saved=\"savedModulesHeadless.loadModules()\"\n />\n\n <ModuleBrowserModal\n v-if=\"\n planConfigInstance.hasFeature('saved_modules') &&\n props.config.modules !== false\n \"\n :visible=\"showModuleBrowserModal\"\n @close=\"showModuleBrowserModal = false\"\n @insert=\"handleModuleInsert\"\n />\n\n <MediaLibraryModal\n :visible=\"panelState.mediaLibraryOpen.value\"\n :accept=\"panelState.mediaLibraryAccept.value\"\n @select=\"mediaLib.handleMediaSelect\"\n @close=\"mediaLib.handleMediaLibraryClose\"\n />\n </template>\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { F as e, N as t, S as n, _ as r, at as i, ct as a, d as o, g as s, h as c, lt as l, m as u, p as d } from "./draggable-BQNU47zu.js";
|
|
2
|
+
import { At as f } from "./features-ofOGnSC0.js";
|
|
3
|
+
import { n as p, t as m } from "./icons-bIb7PBOE.js";
|
|
4
|
+
import { t as h } from "./readableTextColor-Cd_cgWO_.js";
|
|
5
|
+
//#region src/cloud/components/CollaboratorBar.vue?vue&type=script&setup=true&lang.ts
|
|
6
|
+
var g = { class: "tpl-collaborator-bar tpl:flex tpl:items-center tpl:gap-2" }, _ = ["title"], v = {
|
|
7
|
+
key: 0,
|
|
8
|
+
class: "tpl:flex tpl:items-center tpl:-space-x-1.5"
|
|
9
|
+
}, y = ["title"], b = ["title"], x = 3, S = /* @__PURE__ */ n({
|
|
10
|
+
__name: "CollaboratorBar",
|
|
11
|
+
props: {
|
|
12
|
+
collaborators: {},
|
|
13
|
+
isConnected: { type: Boolean }
|
|
14
|
+
},
|
|
15
|
+
setup(n) {
|
|
16
|
+
let S = n, { t: C } = f(), w = d(() => S.collaborators.slice(0, x)), T = d(() => S.collaborators.slice(x)), E = d(() => T.value.length), D = d(() => T.value.map((e) => e.name).join("\n"));
|
|
17
|
+
function O(e) {
|
|
18
|
+
let t = e.trim().split(/\s+/);
|
|
19
|
+
return t.length >= 2 ? (t[0].charAt(0) + t[t.length - 1].charAt(0)).toUpperCase() : e.charAt(0).toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
return (d, f) => (t(), r("div", g, [u("div", {
|
|
22
|
+
class: "tpl:flex tpl:items-center tpl:gap-1 tpl:text-[11px]",
|
|
23
|
+
style: a({ color: n.isConnected ? "var(--tpl-success)" : "var(--tpl-text-muted)" }),
|
|
24
|
+
title: n.isConnected ? i(C).collaboration.connected : i(C).collaboration.disconnected
|
|
25
|
+
}, [n.isConnected ? (t(), c(i(m), {
|
|
26
|
+
key: 0,
|
|
27
|
+
size: 12,
|
|
28
|
+
"stroke-width": 2
|
|
29
|
+
})) : (t(), c(i(p), {
|
|
30
|
+
key: 1,
|
|
31
|
+
size: 12,
|
|
32
|
+
"stroke-width": 2
|
|
33
|
+
}))], 12, _), n.collaborators.length > 0 ? (t(), r("div", v, [(t(!0), r(o, null, e(w.value, (e) => (t(), r("div", {
|
|
34
|
+
key: e.id,
|
|
35
|
+
class: "tpl-collaborator-avatar tpl:relative tpl:flex tpl:size-6 tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-2 tpl:text-[10px] tpl:font-bold tpl:transition-transform tpl:duration-150 tpl:hover:z-10 tpl:hover:scale-110 tpl:border-[var(--tpl-bg)]",
|
|
36
|
+
style: a({
|
|
37
|
+
backgroundColor: e.color,
|
|
38
|
+
color: i(h)(e.color)
|
|
39
|
+
}),
|
|
40
|
+
title: e.name
|
|
41
|
+
}, l(O(e.name)), 13, y))), 128)), E.value > 0 ? (t(), r("div", {
|
|
42
|
+
key: 0,
|
|
43
|
+
class: "tpl:relative tpl:flex tpl:size-6 tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-2 tpl:text-[9px] tpl:font-bold tpl:border-[var(--tpl-bg)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]",
|
|
44
|
+
title: D.value
|
|
45
|
+
}, " +" + l(E.value), 9, b)) : s("", !0)])) : s("", !0)]));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { S as default };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=CollaboratorBar-D2PKtlOw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CollaboratorBar-D2PKtlOw.js","names":[],"sources":["../../../src/cloud/components/CollaboratorBar.vue","../../../src/cloud/components/CollaboratorBar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { Collaborator } from \"@templatical/types\";\nimport { useI18n } from \"../../composables\";\nimport { Wifi, WifiOff } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { readableTextColor } from \"../../utils/readableTextColor\";\n\nconst props = defineProps<{\n collaborators: Collaborator[];\n isConnected: boolean;\n}>();\n\nconst { t } = useI18n();\n\nconst maxVisible = 3;\n\nconst visibleCollaborators = computed(() =>\n props.collaborators.slice(0, maxVisible),\n);\n\nconst overflowCollaborators = computed(() =>\n props.collaborators.slice(maxVisible),\n);\n\nconst overflowCount = computed(() => overflowCollaborators.value.length);\n\nconst overflowNames = computed(() =>\n overflowCollaborators.value.map((c) => c.name).join(\"\\n\"),\n);\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/);\n if (parts.length >= 2) {\n return (\n parts[0].charAt(0) + parts[parts.length - 1].charAt(0)\n ).toUpperCase();\n }\n return name.charAt(0).toUpperCase();\n}\n</script>\n\n<template>\n <div class=\"tpl-collaborator-bar tpl:flex tpl:items-center tpl:gap-2\">\n <!-- Connection indicator -->\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:text-[11px]\"\n :style=\"{\n color: isConnected ? 'var(--tpl-success)' : 'var(--tpl-text-muted)',\n }\"\n :title=\"\n isConnected ? t.collaboration.connected : t.collaboration.disconnected\n \"\n >\n <Wifi v-if=\"isConnected\" :size=\"12\" :stroke-width=\"2\" />\n <WifiOff v-else :size=\"12\" :stroke-width=\"2\" />\n </div>\n\n <!-- Avatar stack -->\n <div\n v-if=\"collaborators.length > 0\"\n class=\"tpl:flex tpl:items-center tpl:-space-x-1.5\"\n >\n <div\n v-for=\"collaborator in visibleCollaborators\"\n :key=\"collaborator.id\"\n class=\"tpl-collaborator-avatar tpl:relative tpl:flex tpl:size-6 tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-2 tpl:text-[10px] tpl:font-bold tpl:transition-transform tpl:duration-150 tpl:hover:z-10 tpl:hover:scale-110 tpl:border-[var(--tpl-bg)]\"\n :style=\"{\n backgroundColor: collaborator.color,\n color: readableTextColor(collaborator.color),\n }\"\n :title=\"collaborator.name\"\n >\n {{ getInitials(collaborator.name) }}\n </div>\n <div\n v-if=\"overflowCount > 0\"\n class=\"tpl:relative tpl:flex tpl:size-6 tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-2 tpl:text-[9px] tpl:font-bold tpl:border-[var(--tpl-bg)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n :title=\"overflowNames\"\n >\n +{{ overflowCount }}\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { Collaborator } from \"@templatical/types\";\nimport { useI18n } from \"../../composables\";\nimport { Wifi, WifiOff } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { readableTextColor } from \"../../utils/readableTextColor\";\n\nconst props = defineProps<{\n collaborators: Collaborator[];\n isConnected: boolean;\n}>();\n\nconst { t } = useI18n();\n\nconst maxVisible = 3;\n\nconst visibleCollaborators = computed(() =>\n props.collaborators.slice(0, maxVisible),\n);\n\nconst overflowCollaborators = computed(() =>\n props.collaborators.slice(maxVisible),\n);\n\nconst overflowCount = computed(() => overflowCollaborators.value.length);\n\nconst overflowNames = computed(() =>\n overflowCollaborators.value.map((c) => c.name).join(\"\\n\"),\n);\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/);\n if (parts.length >= 2) {\n return (\n parts[0].charAt(0) + parts[parts.length - 1].charAt(0)\n ).toUpperCase();\n }\n return name.charAt(0).toUpperCase();\n}\n</script>\n\n<template>\n <div class=\"tpl-collaborator-bar tpl:flex tpl:items-center tpl:gap-2\">\n <!-- Connection indicator -->\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:text-[11px]\"\n :style=\"{\n color: isConnected ? 'var(--tpl-success)' : 'var(--tpl-text-muted)',\n }\"\n :title=\"\n isConnected ? t.collaboration.connected : t.collaboration.disconnected\n \"\n >\n <Wifi v-if=\"isConnected\" :size=\"12\" :stroke-width=\"2\" />\n <WifiOff v-else :size=\"12\" :stroke-width=\"2\" />\n </div>\n\n <!-- Avatar stack -->\n <div\n v-if=\"collaborators.length > 0\"\n class=\"tpl:flex tpl:items-center tpl:-space-x-1.5\"\n >\n <div\n v-for=\"collaborator in visibleCollaborators\"\n :key=\"collaborator.id\"\n class=\"tpl-collaborator-avatar tpl:relative tpl:flex tpl:size-6 tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-2 tpl:text-[10px] tpl:font-bold tpl:transition-transform tpl:duration-150 tpl:hover:z-10 tpl:hover:scale-110 tpl:border-[var(--tpl-bg)]\"\n :style=\"{\n backgroundColor: collaborator.color,\n color: readableTextColor(collaborator.color),\n }\"\n :title=\"collaborator.name\"\n >\n {{ getInitials(collaborator.name) }}\n </div>\n <div\n v-if=\"overflowCount > 0\"\n class=\"tpl:relative tpl:flex tpl:size-6 tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-2 tpl:text-[9px] tpl:font-bold tpl:border-[var(--tpl-bg)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n :title=\"overflowNames\"\n >\n +{{ overflowCount }}\n </div>\n </div>\n </div>\n</template>\n"],"mappings":";;;;;;;;iCAcM,IAAa;;;;;;;EAPnB,IAAM,IAAQ,GAKR,EAAE,SAAM,GAAS,EAIjB,IAAuB,QAC3B,EAAM,cAAc,MAAM,GAAG,EAAW,CACzC,EAEK,IAAwB,QAC5B,EAAM,cAAc,MAAM,EAAW,CACtC,EAEK,IAAgB,QAAe,EAAsB,MAAM,OAAO,EAElE,IAAgB,QACpB,EAAsB,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK,CAC1D;EAED,SAAS,EAAY,GAAsB;GACzC,IAAM,IAAQ,EAAK,MAAM,CAAC,MAAM,MAAM;AAMtC,UALI,EAAM,UAAU,KAEhB,EAAM,GAAG,OAAO,EAAE,GAAG,EAAM,EAAM,SAAS,GAAG,OAAO,EAAC,EACrD,aAAa,GAEV,EAAK,OAAO,EAAE,CAAC,aAAa;;yBAKnC,EAwCM,OAxCN,GAwCM,CAtCJ,EAWM,OAAA;GAVJ,OAAM;GACL,OAAK,EAAA,EAAA,OAAmB,EAAA,cAAW,uBAAA,yBAAA,CAAA;GAGnC,OAAgB,EAAA,cAAc,EAAA,EAAC,CAAC,cAAc,YAAY,EAAA,EAAC,CAAC,cAAc;MAI/D,EAAA,eAAA,GAAA,EAAZ,EAAwD,EAAA,EAAA,EAAA;;GAA9B,MAAM;GAAK,gBAAc;cACnD,EAA+C,EAAA,EAAA,EAAA;;GAA9B,MAAM;GAAK,gBAAc;gBAKpC,EAAA,cAAc,SAAM,KAAA,GAAA,EAD5B,EAuBM,OAvBN,GAuBM,EAAA,EAAA,GAAA,EAnBJ,EAWM,GAAA,MAAA,EAVmB,EAAA,QAAhB,YADT,EAWM,OAAA;GATH,KAAK,EAAa;GACnB,OAAM;GACL,OAAK,EAAA;qBAA+B,EAAa;WAAwB,EAAA,EAAiB,CAAC,EAAa,MAAK;;GAI7G,OAAO,EAAa;OAElB,EAAY,EAAa,KAAI,CAAA,EAAA,IAAA,EAAA,WAG1B,EAAA,QAAa,KAAA,GAAA,EADrB,EAMM,OAAA;;GAJJ,OAAM;GACL,OAAO,EAAA;KACT,OACE,EAAG,EAAA,MAAa,EAAA,GAAA,EAAA,IAAA,EAAA,IAAA,GAAA,CAAA,CAAA,IAAA,EAAA,IAAA,GAAA,CAAA,CAAA"}
|