@templatical/editor 0.27.3 → 0.27.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-inzMb2Vi.js → AiChatSidebar-LstHjwBU.js} +7 -7
- package/dist/{AiFeatureMenu-BTdgjwdx.js → AiFeatureMenu-DVQrznsN.js} +4 -4
- package/dist/{BlockIssueBadge-a7jENlL4.js → BlockIssueBadge-DKaFQNxW.js} +2 -2
- package/dist/{BlockPreviewCanvas-CMuRmhQg.js → BlockPreviewCanvas-D9eC04OA.js} +1 -1
- package/dist/{CloudHeaderExtras-Bgr-Ox9M.js → CloudHeaderExtras-DX0j3CJ3.js} +2 -2
- package/dist/{CloudPanels-oFNlTOO5.js → CloudPanels-DCeNSwXy.js} +3 -3
- package/dist/{CollaboratorBar-BHja900c.js → CollaboratorBar-BZ-JCjdM.js} +2 -2
- package/dist/{ColorPicker-ClUa_hMj.js → ColorPicker-j0TQu5lE.js} +1 -1
- package/dist/{CommentsPanel-CB1-poNb.js → CommentsPanel-c5LmzlNN.js} +1 -1
- package/dist/{CommentsSidebar-B6mV8ft4.js → CommentsSidebar-Bm1DxFb5.js} +10 -10
- package/dist/{CountdownToolbar-BpnKUXN_.js → CountdownToolbar-UPdwNvVI.js} +1 -1
- package/dist/{DesignReferenceSidebar-BC5UxrJl.js → DesignReferenceSidebar-C8WTApc5.js} +5 -5
- package/dist/{IssuesPanel-BFczwZkL.js → IssuesPanel-D49aJjLS.js} +5 -5
- package/dist/{MergeTagModeToggle-koHNtEJe.js → MergeTagModeToggle-CVzJY9b2.js} +2 -2
- package/dist/{ParagraphEditor-cKosDi_l.js → ParagraphEditor-C9-jQ5zP.js} +7 -7
- package/dist/{RestoreVersionDialog-DpZmcFuf.js → RestoreVersionDialog-Ds6gEF5u.js} +3 -3
- package/dist/{RichTextEditorContent-Br3WBNOs.js → RichTextEditorContent-CO1se6ln.js} +3 -3
- package/dist/{SaveBlockDialog-C7mSGScN.js → SaveBlockDialog-DJIcncsU.js} +6 -6
- package/dist/{SavedBlocksBrowserModal-D7MYzuVP.js → SavedBlocksBrowserModal-BUhhkGEb.js} +7 -7
- package/dist/{SavedBlocksPanels-D5T63WyR.js → SavedBlocksPanels-DBfPz4iB.js} +1 -1
- package/dist/{SavedBlocksPickBar-BluJS58M.js → SavedBlocksPickBar-D9NsfzCB.js} +2 -2
- package/dist/{TemplateScoringPanel-C0z27t2p.js → TemplateScoringPanel-BFsUqnXR.js} +10 -10
- package/dist/{TestEmailModal-DoSIuHXe.js → TestEmailModal-DARwBqpV.js} +6 -6
- package/dist/{TestEmailPanel-3Y6uUX3Z.js → TestEmailPanel-BkgAew1S.js} +1 -1
- package/dist/{TitleEditor-D9IWtoH3.js → TitleEditor-BWLwZh84.js} +4 -4
- package/dist/{TplModal-CyBCFn5s.js → TplModal-CJLs8as_.js} +1 -1
- package/dist/{VersionHistoryMenu-8nd_aOnh.js → VersionHistoryMenu-CrIVG_SN.js} +10 -10
- package/dist/{VersionHistoryPanels-Cm7MxfrI.js → VersionHistoryPanels-s45U7XY-.js} +1 -1
- package/dist/{VersionPreviewBanner-B9w773jz.js → VersionPreviewBanner-BIy95B34.js} +1 -1
- package/dist/{blockTypeIcons-BRCuWioG.js → blockTypeIcons-BjUkQsAe.js} +2 -2
- package/dist/bundle-stats.json +5 -5
- package/dist/cdn/chunks/{AiChatSidebar-BIvQg9Q5.js → AiChatSidebar-tZqbhmuJ.js} +2 -2
- package/dist/cdn/chunks/{AiChatSidebar-BIvQg9Q5.js.map → AiChatSidebar-tZqbhmuJ.js.map} +1 -1
- package/dist/cdn/chunks/{AiFeatureMenu-BMvcmvDZ.js → AiFeatureMenu-BhtNkpIq.js} +3 -3
- package/dist/cdn/chunks/{AiFeatureMenu-BMvcmvDZ.js.map → AiFeatureMenu-BhtNkpIq.js.map} +1 -1
- package/dist/cdn/chunks/{BlockIssueBadge-Drrmjqd7.js → BlockIssueBadge-D9L8j7Db.js} +2 -2
- package/dist/cdn/chunks/{BlockIssueBadge-Drrmjqd7.js.map → BlockIssueBadge-D9L8j7Db.js.map} +1 -1
- package/dist/cdn/chunks/{BlockPreviewCanvas-CuWJLv8h.js → BlockPreviewCanvas-Bt7fFzIQ.js} +2 -2
- package/dist/cdn/chunks/{BlockPreviewCanvas-CuWJLv8h.js.map → BlockPreviewCanvas-Bt7fFzIQ.js.map} +1 -1
- package/dist/cdn/chunks/{CloudHeaderExtras-Ctat5U0M.js → CloudHeaderExtras-CScGpeLl.js} +3 -3
- package/dist/cdn/chunks/{CloudHeaderExtras-Ctat5U0M.js.map → CloudHeaderExtras-CScGpeLl.js.map} +1 -1
- package/dist/cdn/chunks/{CloudPanels-YRYmXE9O.js → CloudPanels-DVtJkWsj.js} +4 -4
- package/dist/cdn/chunks/{CloudPanels-YRYmXE9O.js.map → CloudPanels-DVtJkWsj.js.map} +1 -1
- package/dist/cdn/chunks/{CollaboratorBar-DgdxBCkN.js → CollaboratorBar-BH1lM_Ta.js} +3 -3
- package/dist/cdn/chunks/{CollaboratorBar-DgdxBCkN.js.map → CollaboratorBar-BH1lM_Ta.js.map} +1 -1
- package/dist/cdn/chunks/{ColorPicker-B-we7_bC.js → ColorPicker-d4kY7WWd.js} +2 -2
- package/dist/cdn/chunks/{ColorPicker-B-we7_bC.js.map → ColorPicker-d4kY7WWd.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsPanel-D7Y-74Nj.js → CommentsPanel-COhQMagD.js} +2 -2
- package/dist/cdn/chunks/{CommentsPanel-D7Y-74Nj.js.map → CommentsPanel-COhQMagD.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsSidebar-DxTobvYS.js → CommentsSidebar-vzLX-_h3.js} +2 -2
- package/dist/cdn/chunks/{CommentsSidebar-DxTobvYS.js.map → CommentsSidebar-vzLX-_h3.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownToolbar-D8QWL-jK.js → CountdownToolbar-BRDbI1mM.js} +2 -2
- package/dist/cdn/chunks/{CountdownToolbar-D8QWL-jK.js.map → CountdownToolbar-BRDbI1mM.js.map} +1 -1
- package/dist/cdn/chunks/{DesignReferenceSidebar-D9Lm4Al0.js → DesignReferenceSidebar-npLxDRJI.js} +2 -2
- package/dist/cdn/chunks/{DesignReferenceSidebar-D9Lm4Al0.js.map → DesignReferenceSidebar-npLxDRJI.js.map} +1 -1
- package/dist/cdn/chunks/{IssuesPanel-Dz90c1C6.js → IssuesPanel-CTGlv7HF.js} +2 -2
- package/dist/cdn/chunks/{IssuesPanel-Dz90c1C6.js.map → IssuesPanel-CTGlv7HF.js.map} +1 -1
- package/dist/cdn/chunks/{MergeTagModeToggle-CfFrfYNC.js → MergeTagModeToggle-CkpKFx9N.js} +2 -2
- package/dist/cdn/chunks/{MergeTagModeToggle-CfFrfYNC.js.map → MergeTagModeToggle-CkpKFx9N.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-Be-BO5hD.js → ParagraphEditor-BgeeE0Gj.js} +5 -5
- package/dist/cdn/chunks/{ParagraphEditor-Be-BO5hD.js.map → ParagraphEditor-BgeeE0Gj.js.map} +1 -1
- package/dist/cdn/chunks/{RestoreVersionDialog-CKtz_FEz.js → RestoreVersionDialog-tfoAQAeP.js} +3 -3
- package/dist/cdn/chunks/{RestoreVersionDialog-CKtz_FEz.js.map → RestoreVersionDialog-tfoAQAeP.js.map} +1 -1
- package/dist/cdn/chunks/{RichTextEditorContent-D3eWZ793.js → RichTextEditorContent-DvLBtUcW.js} +3 -3
- package/dist/cdn/chunks/{RichTextEditorContent-D3eWZ793.js.map → RichTextEditorContent-DvLBtUcW.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-C5nT7Fxi.js → SaveBlockDialog-D67K2lHn.js} +5 -5
- package/dist/cdn/chunks/{SaveBlockDialog-C5nT7Fxi.js.map → SaveBlockDialog-D67K2lHn.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-Cvyewimb.js → SavedBlocksBrowserModal-BxVR-yDt.js} +6 -6
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-Cvyewimb.js.map → SavedBlocksBrowserModal-BxVR-yDt.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPanels-B7aBlxKW.js → SavedBlocksPanels-VkKCO0uY.js} +2 -2
- package/dist/cdn/chunks/{SavedBlocksPanels-B7aBlxKW.js.map → SavedBlocksPanels-VkKCO0uY.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPickBar-D0opM9P9.js → SavedBlocksPickBar-BJHQDgUT.js} +3 -3
- package/dist/cdn/chunks/{SavedBlocksPickBar-D0opM9P9.js.map → SavedBlocksPickBar-BJHQDgUT.js.map} +1 -1
- package/dist/cdn/chunks/{TemplateScoringPanel-DEsNZlHe.js → TemplateScoringPanel-MvdrkgqL.js} +2 -2
- package/dist/cdn/chunks/{TemplateScoringPanel-DEsNZlHe.js.map → TemplateScoringPanel-MvdrkgqL.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailModal-BANpGbEP.js → TestEmailModal-CEBFCevy.js} +6 -6
- package/dist/cdn/chunks/{TestEmailModal-BANpGbEP.js.map → TestEmailModal-CEBFCevy.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailPanel-DItZMP3B.js → TestEmailPanel-C35nqIIl.js} +2 -2
- package/dist/cdn/chunks/{TestEmailPanel-DItZMP3B.js.map → TestEmailPanel-C35nqIIl.js.map} +1 -1
- package/dist/cdn/chunks/{TitleEditor-D_NJ4Ww8.js → TitleEditor-c2aQCyds.js} +4 -4
- package/dist/cdn/chunks/{TitleEditor-D_NJ4Ww8.js.map → TitleEditor-c2aQCyds.js.map} +1 -1
- package/dist/cdn/chunks/{TplModal-C51W8LTn.js → TplModal-DSTVZKuI.js} +2 -2
- package/dist/cdn/chunks/{TplModal-C51W8LTn.js.map → TplModal-DSTVZKuI.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryMenu-Cv23A0uu.js → VersionHistoryMenu-BNYSIjVj.js} +4 -4
- package/dist/cdn/chunks/{VersionHistoryMenu-Cv23A0uu.js.map → VersionHistoryMenu-BNYSIjVj.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryPanels-DHDN1owI.js → VersionHistoryPanels-CvWySHUE.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryPanels-DHDN1owI.js.map → VersionHistoryPanels-CvWySHUE.js.map} +1 -1
- package/dist/cdn/chunks/{VersionPreviewBanner-Dw4kFkrh.js → VersionPreviewBanner-CBVDWnhq.js} +2 -2
- package/dist/cdn/chunks/{VersionPreviewBanner-Dw4kFkrh.js.map → VersionPreviewBanner-CBVDWnhq.js.map} +1 -1
- package/dist/cdn/chunks/{blockTypeIcons-hX_W4dqo.js → blockTypeIcons-A9qFb1cR.js} +2 -2
- package/dist/cdn/chunks/{blockTypeIcons-hX_W4dqo.js.map → blockTypeIcons-A9qFb1cR.js.map} +1 -1
- package/dist/cdn/chunks/{createCloudRuntime-D75oJ9j-.js → createCloudRuntime-DYDf_-rN.js} +2 -2
- package/dist/cdn/chunks/{createCloudRuntime-D75oJ9j-.js.map → createCloudRuntime-DYDf_-rN.js.map} +1 -1
- package/dist/cdn/chunks/{icons-C36Ha5VU.js → icons-Dl5bcXmT.js} +7 -7
- package/dist/cdn/chunks/{icons-C36Ha5VU.js.map → icons-Dl5bcXmT.js.map} +1 -1
- package/dist/cdn/chunks/{src-B_pCqXyM.js → src-BAWlZu4B.js} +3 -3
- package/dist/cdn/chunks/{src-B_pCqXyM.js.map → src-BAWlZu4B.js.map} +1 -1
- package/dist/cdn/chunks/{src-CoHDh8Ua.js → src-BDUi-9js.js} +15 -15
- package/dist/cdn/chunks/{src-CoHDh8Ua.js.map → src-BDUi-9js.js.map} +1 -1
- package/dist/cdn/chunks/{useEditorCore-BtEO1qms.js → useEditorCore-D84laTMj.js} +5 -5
- package/dist/cdn/chunks/{useEditorCore-BtEO1qms.js.map → useEditorCore-D84laTMj.js.map} +1 -1
- package/dist/cdn/editor.js +638 -625
- package/dist/cdn/editor.js.map +1 -1
- package/dist/{check-DZbmkMQe.js → check-GRcTaHlZ.js} +1 -1
- package/dist/{chevron-down-nYScOLeZ.js → chevron-down-NUIAa2Qf.js} +1 -1
- package/dist/{chevron-up-CpYpDLnc.js → chevron-up-Bjf2BQtL.js} +1 -1
- package/dist/{circle-alert-CuuK6thq.js → circle-alert-FpQ8ykQL.js} +1 -1
- package/dist/{clock-DxkrtqOr.js → clock-CaOM1DC-.js} +1 -1
- package/dist/{createLucideIcon-L-wQXW_R.js → createLucideIcon-CAmDKr5Z.js} +2 -2
- package/dist/{image-up-DouZRGVt.js → image-up-CIGT4cSb.js} +1 -1
- package/dist/{info-puX5V6Iq.js → info-9YwKinXj.js} +1 -1
- package/dist/{loader-circle-DqTxB7Kr.js → loader-circle-QyURl47g.js} +1 -1
- package/dist/{message-circle-BZ7YpB22.js → message-circle-C9pycEzV.js} +1 -1
- package/dist/{pencil-DmbRhTJu.js → pencil-BvW86jVn.js} +1 -1
- package/dist/{refresh-cw-OK_O-cL0.js → refresh-cw-BDAUomac.js} +1 -1
- package/dist/{scan-line-DyaSTsJ5.js → scan-line-B5EvAPSG.js} +1 -1
- package/dist/{send-B-wasJJC.js → send-BAUn7DLp.js} +1 -1
- package/dist/{shield-check-DUIyRrTo.js → shield-check-CLGywlg0.js} +1 -1
- package/dist/{sparkles-DYEMATWC.js → sparkles-X6C0spo6.js} +1 -1
- package/dist/templatical-editor.js +131 -118
- package/dist/{text-align-start-CzjLeBLe.js → text-align-start-C6FXOoEj.js} +1 -1
- package/dist/{trash-2-C0shEPry.js → trash-2-balHkQP4.js} +1 -1
- package/dist/{triangle-alert-Fw0ce8oQ.js → triangle-alert-D2JBEr6k.js} +1 -1
- package/dist/{upload-D69C2CBx.js → upload-D70OYprI.js} +1 -1
- package/dist/{useEditorCore-DoKDh27o.js → useEditorCore-D7Gz7_CW.js} +9 -9
- package/dist/{useScrollToBlock-KA65jhQ_.js → useScrollToBlock-DcImB73T.js} +1 -1
- package/dist/{x-BjTlFqRv.js → x-DTUPbuwx.js} +1 -1
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiChatSidebar-BIvQg9Q5.js","names":[],"sources":["../../../src/cloud/components/AiChatSidebar.vue","../../../src/cloud/components/AiChatSidebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useAiChat } from \"@templatical/core/cloud\";\nimport {\n CLOUD_TRANSLATIONS_KEY,\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n MERGE_TAGS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n LoaderCircle,\n Redo2,\n Send,\n Sparkles,\n Trash2,\n Undo2,\n X,\n} from \"@lucide/vue\";\nimport { inject, nextTick, ref, watch } from \"vue\";\nimport { useIntervalFn, useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n visible: boolean;\n onApply?: (content: TemplateContent) => void;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n}>();\n\nconst cloudTranslations = requireInject(\n CLOUD_TRANSLATIONS_KEY,\n \"AiChatSidebar\",\n);\nconst editor = requireInject(EDITOR_KEY, \"AiChatSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"AiChatSidebar\");\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst aliveFlag = useAliveFlag();\n\nconst aiChat = useAiChat({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: props.onApply,\n onError: undefined,\n});\n\nconst promptInput = ref(\"\");\nconst messagesContainer = ref<HTMLElement | null>(null);\nconst historyLoaded = ref(false);\nconst visibleSuggestionCount = ref(0);\n\nconst { pause: pauseReveal, resume: resumeReveal } = useIntervalFn(\n () => {\n const count = aiChat.suggestions.value?.length ?? 0;\n if (visibleSuggestionCount.value < count) {\n visibleSuggestionCount.value++;\n } else {\n pauseReveal();\n }\n },\n 150,\n { immediate: false },\n);\n\nconst { start: startRevealTimeout } = useTimeoutFn(() => resumeReveal(), 100, {\n immediate: false,\n});\n\nwatch(\n () => aiChat.suggestions.value?.length ?? 0,\n (count) => {\n pauseReveal();\n if (count === 0) {\n visibleSuggestionCount.value = 0;\n return;\n }\n\n visibleSuggestionCount.value = 0;\n startRevealTimeout();\n },\n);\n\nfunction scrollToBottom(): void {\n nextTick(() => {\n if (messagesContainer.value) {\n messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;\n }\n });\n}\n\nwatch(\n () => aiChat.messages.value?.length ?? 0,\n () => scrollToBottom(),\n);\n\nwatch(\n () => props.visible,\n async (isVisible) => {\n if (isVisible && !historyLoaded.value) {\n historyLoaded.value = true;\n await aiChat.loadConversation();\n if (!aliveFlag.alive) return;\n\n if (\n (aiChat.messages.value?.length ?? 0) === 0 &&\n editor.content.value.blocks.length === 0\n ) {\n aiChat.loadSuggestions(editor.content.value, mergeTags);\n }\n }\n },\n);\n\nasync function handleSend(): Promise<void> {\n const prompt = promptInput.value.trim();\n if (!prompt || aiChat.isGenerating.value) {\n return;\n }\n\n promptInput.value = \"\";\n aiChat.error.value = null;\n aiChat.failedPrompt.value = null;\n scrollToBottom();\n\n await aiChat.sendPrompt(prompt, editor.content.value, mergeTags);\n if (!aliveFlag.alive) return;\n\n if (aiChat.failedPrompt.value) {\n promptInput.value = aiChat.failedPrompt.value;\n }\n\n scrollToBottom();\n}\n\nfunction stripJsonBlock(text: string): string {\n return text\n .replace(/```json[\\s\\S]*?```/g, \"\")\n .replace(/```json[\\s\\S]*/g, \"\")\n .trim();\n}\n\nfunction handleSuggestionClick(suggestion: string): void {\n pauseReveal();\n promptInput.value = suggestion;\n handleSend();\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n}\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-ai-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-ai-slide-leave-active\"\n leave-from-class=\"tpl:translate-x-0\"\n leave-to-class=\"tpl:translate-x-full\"\n >\n <div\n v-if=\"visible\"\n class=\"tpl-ai-sidebar tpl:absolute tpl:top-14 tpl:right-0 tpl:bottom-0 tpl:z-panel tpl:flex tpl:w-[360px] tpl:flex-col tpl:border-l tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <!-- Header -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n >\n <Sparkles :size=\"13\" :stroke-width=\"2\" />\n <span>\n {{ cloudTranslations.aiChat.title }}\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <button\n v-if=\"(aiChat.messages.value?.length ?? 0) > 0\"\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n :title=\"cloudTranslations.aiChat.clear\"\n @click=\"aiChat.clearChat()\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"emit('close')\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Messages wrapper -->\n <div class=\"tpl:relative tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col\">\n <!-- Messages -->\n <div\n ref=\"messagesContainer\"\n class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Loading history state -->\n <div\n v-if=\"aiChat.isLoadingHistory.value\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <LoaderCircle\n class=\"tpl-spinner tpl:text-[var(--tpl-text-muted)]\"\n :size=\"24\"\n :stroke-width=\"2\"\n />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudTranslations.aiChat.loadingHistory }}\n </p>\n </div>\n\n <!-- Empty state -->\n <div\n v-else-if=\"(aiChat.messages.value?.length ?? 0) === 0\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <Sparkles\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:max-w-[240px] tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudTranslations.aiChat.placeholder }}\n </p>\n </div>\n\n <!-- Message list -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <div\n v-for=\"(message, index) in aiChat.messages.value\"\n :key=\"message.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2\"\n >\n <!-- User message -->\n <div\n v-if=\"message.role === 'user'\"\n class=\"tpl:self-end tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm\"\n style=\"\n background-color: var(--tpl-primary-light);\n color: var(--tpl-text);\n max-width: 85%;\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{ message.content }}\n </div>\n\n <!-- Assistant message -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <LoadingTrack\n v-if=\"\n !stripJsonBlock(message.content) &&\n aiChat.isGenerating.value &&\n index === (aiChat.messages.value?.length ?? 0) - 1\n \"\n />\n <div\n v-else\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm tpl:whitespace-pre-wrap\"\n style=\"\n max-width: 85%;\n background-color: var(--tpl-bg);\n color: var(--tpl-text);\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{\n stripJsonBlock(message.content) ||\n cloudTranslations.aiChat.applied\n }}\n </div>\n <button\n v-if=\"\n message.id === aiChat.lastApplyMessageId.value &&\n !aiChat.isGenerating.value\n \"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:self-start tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-text-muted);\n background-color: transparent;\n \"\n @click=\"aiChat.toggleLastRevert()\"\n >\n <template v-if=\"aiChat.isLastChangeReverted.value\">\n <Redo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.reapply }}\n </template>\n <template v-else>\n <Undo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.revert }}\n </template>\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"aiChat.error.value\"\n class=\"tpl:mx-3 tpl:mb-2 tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs\"\n style=\"\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n \"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{\n aiChat.error.value === \"ai_apply_failed\"\n ? cloudTranslations.aiChat.applyFailed\n : cloudTranslations.aiChat.error\n }}</span>\n </div>\n\n <!-- Suggestions (overlay) -->\n <div\n v-if=\"(aiChat.suggestions.value?.length ?? 0) > 0\"\n class=\"tpl:absolute tpl:right-0 tpl:bottom-0 tpl:left-0 tpl:z-10 tpl:px-3 tpl:pb-3\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 50%,\n transparent\n );\n backdrop-filter: blur(2px);\n \"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <button\n v-for=\"(suggestion, index) in aiChat.suggestions.value ?? []\"\n :key=\"index\"\n class=\"tpl-suggestion-btn tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:text-left tpl:text-xs tpl:leading-snug tpl:transition-all tpl:duration-300 tpl:ease-out\"\n :class=\"\n (aiChat.suggestions.value?.length ?? 0) - 1 - index <\n visibleSuggestionCount\n ? 'tpl:translate-y-0 tpl:opacity-100'\n : 'tpl:pointer-events-none tpl:-translate-y-2 tpl:opacity-0'\n \"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n box-shadow: var(--tpl-shadow);\n \"\n @click=\"handleSuggestionClick(suggestion)\"\n >\n {{ suggestion }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Input area -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\n <div\n class=\"tpl-ai-input-wrapper tpl-focus-ring-host tpl:flex tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n v-model=\"promptInput\"\n class=\"tpl:max-h-32 tpl:min-h-[64px] tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-sm tpl:outline-none tpl:text-[var(--tpl-text)]\"\n :placeholder=\"cloudTranslations.aiChat.inputPlaceholder\"\n :disabled=\"aiChat.isGenerating.value\"\n rows=\"3\"\n @keydown=\"handleKeydown\"\n />\n <button\n class=\"tpl-ai-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"!promptInput.trim() || aiChat.isGenerating.value\"\n @click=\"handleSend\"\n >\n <Send :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:px-1 tpl:pt-2 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudTranslations.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-ai-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-input-wrapper:focus-within {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-ai-send-btn:not(:disabled):hover {\n transform: scale(1.05);\n}\n\n.tpl-suggestion-btn:hover {\n border-color: var(--tpl-primary) !important;\n background-color: var(--tpl-primary-light) !important;\n}\n</style>\n","<script setup lang=\"ts\">\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useAiChat } from \"@templatical/core/cloud\";\nimport {\n CLOUD_TRANSLATIONS_KEY,\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n MERGE_TAGS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n LoaderCircle,\n Redo2,\n Send,\n Sparkles,\n Trash2,\n Undo2,\n X,\n} from \"@lucide/vue\";\nimport { inject, nextTick, ref, watch } from \"vue\";\nimport { useIntervalFn, useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n visible: boolean;\n onApply?: (content: TemplateContent) => void;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n}>();\n\nconst cloudTranslations = requireInject(\n CLOUD_TRANSLATIONS_KEY,\n \"AiChatSidebar\",\n);\nconst editor = requireInject(EDITOR_KEY, \"AiChatSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"AiChatSidebar\");\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst aliveFlag = useAliveFlag();\n\nconst aiChat = useAiChat({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: props.onApply,\n onError: undefined,\n});\n\nconst promptInput = ref(\"\");\nconst messagesContainer = ref<HTMLElement | null>(null);\nconst historyLoaded = ref(false);\nconst visibleSuggestionCount = ref(0);\n\nconst { pause: pauseReveal, resume: resumeReveal } = useIntervalFn(\n () => {\n const count = aiChat.suggestions.value?.length ?? 0;\n if (visibleSuggestionCount.value < count) {\n visibleSuggestionCount.value++;\n } else {\n pauseReveal();\n }\n },\n 150,\n { immediate: false },\n);\n\nconst { start: startRevealTimeout } = useTimeoutFn(() => resumeReveal(), 100, {\n immediate: false,\n});\n\nwatch(\n () => aiChat.suggestions.value?.length ?? 0,\n (count) => {\n pauseReveal();\n if (count === 0) {\n visibleSuggestionCount.value = 0;\n return;\n }\n\n visibleSuggestionCount.value = 0;\n startRevealTimeout();\n },\n);\n\nfunction scrollToBottom(): void {\n nextTick(() => {\n if (messagesContainer.value) {\n messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;\n }\n });\n}\n\nwatch(\n () => aiChat.messages.value?.length ?? 0,\n () => scrollToBottom(),\n);\n\nwatch(\n () => props.visible,\n async (isVisible) => {\n if (isVisible && !historyLoaded.value) {\n historyLoaded.value = true;\n await aiChat.loadConversation();\n if (!aliveFlag.alive) return;\n\n if (\n (aiChat.messages.value?.length ?? 0) === 0 &&\n editor.content.value.blocks.length === 0\n ) {\n aiChat.loadSuggestions(editor.content.value, mergeTags);\n }\n }\n },\n);\n\nasync function handleSend(): Promise<void> {\n const prompt = promptInput.value.trim();\n if (!prompt || aiChat.isGenerating.value) {\n return;\n }\n\n promptInput.value = \"\";\n aiChat.error.value = null;\n aiChat.failedPrompt.value = null;\n scrollToBottom();\n\n await aiChat.sendPrompt(prompt, editor.content.value, mergeTags);\n if (!aliveFlag.alive) return;\n\n if (aiChat.failedPrompt.value) {\n promptInput.value = aiChat.failedPrompt.value;\n }\n\n scrollToBottom();\n}\n\nfunction stripJsonBlock(text: string): string {\n return text\n .replace(/```json[\\s\\S]*?```/g, \"\")\n .replace(/```json[\\s\\S]*/g, \"\")\n .trim();\n}\n\nfunction handleSuggestionClick(suggestion: string): void {\n pauseReveal();\n promptInput.value = suggestion;\n handleSend();\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n}\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-ai-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-ai-slide-leave-active\"\n leave-from-class=\"tpl:translate-x-0\"\n leave-to-class=\"tpl:translate-x-full\"\n >\n <div\n v-if=\"visible\"\n class=\"tpl-ai-sidebar tpl:absolute tpl:top-14 tpl:right-0 tpl:bottom-0 tpl:z-panel tpl:flex tpl:w-[360px] tpl:flex-col tpl:border-l tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <!-- Header -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n >\n <Sparkles :size=\"13\" :stroke-width=\"2\" />\n <span>\n {{ cloudTranslations.aiChat.title }}\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <button\n v-if=\"(aiChat.messages.value?.length ?? 0) > 0\"\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n :title=\"cloudTranslations.aiChat.clear\"\n @click=\"aiChat.clearChat()\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"emit('close')\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Messages wrapper -->\n <div class=\"tpl:relative tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col\">\n <!-- Messages -->\n <div\n ref=\"messagesContainer\"\n class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Loading history state -->\n <div\n v-if=\"aiChat.isLoadingHistory.value\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <LoaderCircle\n class=\"tpl-spinner tpl:text-[var(--tpl-text-muted)]\"\n :size=\"24\"\n :stroke-width=\"2\"\n />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudTranslations.aiChat.loadingHistory }}\n </p>\n </div>\n\n <!-- Empty state -->\n <div\n v-else-if=\"(aiChat.messages.value?.length ?? 0) === 0\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <Sparkles\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:max-w-[240px] tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudTranslations.aiChat.placeholder }}\n </p>\n </div>\n\n <!-- Message list -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <div\n v-for=\"(message, index) in aiChat.messages.value\"\n :key=\"message.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2\"\n >\n <!-- User message -->\n <div\n v-if=\"message.role === 'user'\"\n class=\"tpl:self-end tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm\"\n style=\"\n background-color: var(--tpl-primary-light);\n color: var(--tpl-text);\n max-width: 85%;\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{ message.content }}\n </div>\n\n <!-- Assistant message -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <LoadingTrack\n v-if=\"\n !stripJsonBlock(message.content) &&\n aiChat.isGenerating.value &&\n index === (aiChat.messages.value?.length ?? 0) - 1\n \"\n />\n <div\n v-else\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm tpl:whitespace-pre-wrap\"\n style=\"\n max-width: 85%;\n background-color: var(--tpl-bg);\n color: var(--tpl-text);\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{\n stripJsonBlock(message.content) ||\n cloudTranslations.aiChat.applied\n }}\n </div>\n <button\n v-if=\"\n message.id === aiChat.lastApplyMessageId.value &&\n !aiChat.isGenerating.value\n \"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:self-start tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-text-muted);\n background-color: transparent;\n \"\n @click=\"aiChat.toggleLastRevert()\"\n >\n <template v-if=\"aiChat.isLastChangeReverted.value\">\n <Redo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.reapply }}\n </template>\n <template v-else>\n <Undo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.revert }}\n </template>\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"aiChat.error.value\"\n class=\"tpl:mx-3 tpl:mb-2 tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs\"\n style=\"\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n \"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{\n aiChat.error.value === \"ai_apply_failed\"\n ? cloudTranslations.aiChat.applyFailed\n : cloudTranslations.aiChat.error\n }}</span>\n </div>\n\n <!-- Suggestions (overlay) -->\n <div\n v-if=\"(aiChat.suggestions.value?.length ?? 0) > 0\"\n class=\"tpl:absolute tpl:right-0 tpl:bottom-0 tpl:left-0 tpl:z-10 tpl:px-3 tpl:pb-3\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 50%,\n transparent\n );\n backdrop-filter: blur(2px);\n \"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <button\n v-for=\"(suggestion, index) in aiChat.suggestions.value ?? []\"\n :key=\"index\"\n class=\"tpl-suggestion-btn tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:text-left tpl:text-xs tpl:leading-snug tpl:transition-all tpl:duration-300 tpl:ease-out\"\n :class=\"\n (aiChat.suggestions.value?.length ?? 0) - 1 - index <\n visibleSuggestionCount\n ? 'tpl:translate-y-0 tpl:opacity-100'\n : 'tpl:pointer-events-none tpl:-translate-y-2 tpl:opacity-0'\n \"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n box-shadow: var(--tpl-shadow);\n \"\n @click=\"handleSuggestionClick(suggestion)\"\n >\n {{ suggestion }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Input area -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\n <div\n class=\"tpl-ai-input-wrapper tpl-focus-ring-host tpl:flex tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n v-model=\"promptInput\"\n class=\"tpl:max-h-32 tpl:min-h-[64px] tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-sm tpl:outline-none tpl:text-[var(--tpl-text)]\"\n :placeholder=\"cloudTranslations.aiChat.inputPlaceholder\"\n :disabled=\"aiChat.isGenerating.value\"\n rows=\"3\"\n @keydown=\"handleKeydown\"\n />\n <button\n class=\"tpl-ai-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"!promptInput.trim() || aiChat.isGenerating.value\"\n @click=\"handleSend\"\n >\n <Send :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:px-1 tpl:pt-2 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudTranslations.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-ai-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-input-wrapper:focus-within {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-ai-send-btn:not(:disabled):hover {\n transform: scale(1.05);\n}\n\n.tpl-suggestion-btn:hover {\n border-color: var(--tpl-primary) !important;\n background-color: var(--tpl-primary-light) !important;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBA,IAAM,IAAQ,GAKR,KAAO,GAIP,IAAoB,EACxB,IACA,eACF,GACM,IAAS,EAAc,IAAY,eAAe,GAClD,KAAc,EAAc,IAAkB,eAAe,GAC7D,IAAY,EAAO,IAAgB,CAAC,CAAC,GACrC,IAAY,EAAa,GAEzB,IAAS,EAAU;GACvB;GACA,qBAAqB,EAAO,MAAM,UAAU,MAAM;GAClD,SAAS,EAAM;GACf,SAAS,KAAA;EACX,CAAC,GAEK,IAAc,EAAI,EAAE,GACpB,IAAoB,EAAwB,IAAI,GAChD,IAAgB,EAAI,EAAK,GACzB,IAAyB,EAAI,CAAC,GAE9B,EAAE,OAAO,GAAa,QAAQ,OAAiB,SAC7C;GAEJ,AAAI,EAAuB,SADb,EAAO,YAAY,OAAO,UAAU,KAEhD,EAAuB,UAEvB,EAAY;EAEhB,GACA,KACA,EAAE,WAAW,GAAM,CACrB,GAEM,EAAE,OAAO,OAAuB,QAAmB,GAAa,GAAG,KAAK,EAC5E,WAAW,GACb,CAAC;EAED,QACQ,EAAO,YAAY,OAAO,UAAU,IACzC,MAAU;GAET,IADA,EAAY,GACR,MAAU,GAAG;IACf,EAAuB,QAAQ;IAC/B;GACF;GAGA,AADA,EAAuB,QAAQ,GAC/B,GAAmB;EACrB,CACF;EAEA,SAAS,IAAuB;GAC9B,SAAe;IACb,AAAI,EAAkB,UACpB,EAAkB,MAAM,YAAY,EAAkB,MAAM;GAEhE,CAAC;EACH;EAOA,AALA,QACQ,EAAO,SAAS,OAAO,UAAU,SACjC,EAAe,CACvB,GAEA,QACQ,EAAM,SACZ,OAAO,MAAc;GACnB,IAAI,KAAa,CAAC,EAAc,OAAO;IAGrC,IAFA,EAAc,QAAQ,IACtB,MAAM,EAAO,iBAAiB,GAC1B,CAAC,EAAU,OAAO;IAEtB,CACG,EAAO,SAAS,OAAO,UAAU,OAAO,KACzC,EAAO,QAAQ,MAAM,OAAO,WAAW,KAEvC,EAAO,gBAAgB,EAAO,QAAQ,OAAO,CAAS;GAE1D;EACF,CACF;EAEA,eAAe,IAA4B;GACzC,IAAM,IAAS,EAAY,MAAM,KAAK;GAClC,CAAC,KAAU,EAAO,aAAa,UAInC,EAAY,QAAQ,IACpB,EAAO,MAAM,QAAQ,MACrB,EAAO,aAAa,QAAQ,MAC5B,EAAe,GAEf,MAAM,EAAO,WAAW,GAAQ,EAAO,QAAQ,OAAO,CAAS,GAC1D,EAAU,UAEX,EAAO,aAAa,UACtB,EAAY,QAAQ,EAAO,aAAa,QAG1C,EAAe;EACjB;EAEA,SAAS,EAAe,GAAsB;GAC5C,OAAO,EACJ,QAAQ,uBAAuB,EAAE,CAAA,CACjC,QAAQ,mBAAmB,EAAE,CAAA,CAC7B,KAAK;EACV;EAEA,SAAS,GAAsB,GAA0B;GAGvD,AAFA,EAAY,GACZ,EAAY,QAAQ,GACpB,EAAW;EACb;EAEA,SAAS,GAAc,GAA4B;GACjD,AAAI,EAAM,QAAQ,WAAW,CAAC,EAAM,aAClC,EAAM,eAAe,GACrB,EAAW;EAEf;oBAIE,EAAA,GAAA,EAkPa,GAAA;GAjPX,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;GACf,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;;GAEf,SAAA,SAyOM,CAxOE,EAAA,WADR,EAAA,GAAA,EAyOM,OAzON,GAyOM;IApOJ,EA2BM,OA3BN,GA2BM,CAxBJ,EAOM,OAPN,GAOM,CAJJ,EAAyC,EAAA,CAAA,GAAA;KAA9B,MAAM;KAAK,gBAAc;IACpC,CAAA,GAAA,EAEO,QAAA,MAAA,EADF,EAAA,CAAA,CAAiB,CAAC,OAAO,KAAK,GAAA,CAAA,CAAA,CAAA,GAGrC,EAeM,OAfN,GAeM,EAbK,EAAA,CAAA,CAAM,CAAC,SAAS,OAAO,UAAM,KAAA,KADtC,EAAA,GAAA,EAOS,UAAA;;KALP,OAAM;KACL,OAAO,EAAA,CAAA,CAAiB,CAAC,OAAO;KAChC,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,CAAA,CAAM,CAAC,UAAS;IAExB,GAAA,CAAA,EAAuC,EAAA,EAAA,GAAA;KAA9B,MAAM;KAAK,gBAAc;IAEpC,CAAA,CAAA,GAAA,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAAA,EAKS,UAAA;KAJP,OAAM;KACL,SAAK,AAAA,EAAA,QAAA,MAAE,GAAI,OAAA;IAEZ,GAAA,CAAA,EAAkC,EAAA,CAAA,GAAA;KAA9B,MAAM;KAAK,gBAAc;;IAMnC,EAuKM,OAvKN,GAuKM;KArKJ,EA0GM,OAAA;MAzGA,SAAA;MAAJ,KAAI;MACJ,OAAM;KAIE,GAAA,CAAA,EAAA,CAAA,CAAM,CAAC,iBAAiB,SADhC,EAAA,GAAA,EAYM,OAZN,GAYM,CARJ,EAIE,EAAA,CAAA,GAAA;MAHA,OAAM;MACL,MAAM;MACN,gBAAc;KAEjB,CAAA,GAAA,EAEI,KAFJ,GAEI,EADC,EAAA,CAAA,CAAiB,CAAC,OAAO,cAAc,GAAA,CAAA,CAAA,CAAA,MAMhC,EAAA,CAAA,CAAM,CAAC,SAAS,OAAO,UAAM,OAAA,KAD3C,EAAA,GAAA,EAcM,OAdN,GAcM,CAVJ,EAIE,EAAA,CAAA,GAAA;MAHC,MAAM;MACN,gBAAc;MACf,OAAM;KAER,CAAA,GAAA,EAII,KAJJ,GAII,EADC,EAAA,CAAA,CAAiB,CAAC,OAAO,WAAW,GAAA,CAAA,CAAA,CAAA,MAK3C,EAAA,GAAA,EAoEM,OApEN,GAoEM,EAnEJ,EAAA,EAAA,GAAA,EAkEM,GAAA,MAAA,EAjEuB,EAAA,CAAA,CAAM,CAAC,SAAS,QAAnC,GAAS,OADnB,EAAA,GAAA,EAkEM,OAAA;MAhEH,KAAK,EAAQ;MACd,OAAM;KAIE,GAAA,CAAA,EAAQ,SAAI,UADpB,EAAA,GAAA,EAWM,OAXN,GAWM,EADD,EAAQ,OAAO,GAAA,CAAA,MAIpB,EAAA,GAAA,EA6CM,OA7CN,GA6CM,CA3C0B,CAAA,EAAe,EAAQ,OAAO,KAAyB,EAAA,CAAA,CAAM,CAAC,aAAa,SAA6B,OAAW,EAAA,CAAA,CAAM,CAAC,SAAS,OAAO,UAAM,KAAA,KAD9K,EAAA,GAAA,EAME,GAAA,EAAA,KAAA,EAAA,CAAA,MACF,EAAA,GAAA,EAcM,OAdN,GAcM,EAHF,EAAe,EAAQ,OAAO,KAAyB,EAAA,CAAA,CAAiB,CAAC,OAAO,OAAO,GAAA,CAAA,IAK9D,EAAQ,OAAO,EAAA,CAAA,CAAM,CAAC,mBAAmB,SAA8B,CAAA,EAAA,CAAA,CAAM,CAAC,aAAa,SADxH,EAAA,GAAA,EAqBS,UAAA;;MAhBP,OAAM;MACN,OAAA;OAAA,gBAAA;OAAA,OAAA;OAAA,oBAAA;MAAA;MAKC,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,CAAA,CAAM,CAAC,iBAAgB;KAEf,GAAA,CAAA,EAAA,CAAA,CAAM,CAAC,qBAAqB,SAA5C,EAAA,GAAA,EAGW,GAAA,EAAA,KAAA,EAAA,GAAA,CAFT,EAAsC,EAAA,EAAA,GAAA;MAA9B,MAAM;MAAK,gBAAc;KAAK,CAAA,GAAA,EAAA,MACtC,EAAG,EAAA,CAAA,CAAiB,CAAC,OAAO,OAAO,GAAA,CAAA,CAAA,GAAA,EAAA,MAErC,EAAA,GAAA,EAGW,GAAA,EAAA,KAAA,EAAA,GAAA,CAFT,EAAsC,EAAA,EAAA,GAAA;MAA9B,MAAM;MAAK,gBAAc;KAAK,CAAA,GAAA,EAAA,MACtC,EAAG,EAAA,CAAA,CAAiB,CAAC,OAAO,MAAM,GAAA,CAAA,CAAA,GAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA,EAAA,CAAA;KAUtC,EAAA,CAAA,CAAM,CAAC,MAAM,SADrB,EAAA,GAAA,EAkBM,OAlBN,GAkBM,CAVJ,EAIE,EAAA,EAAA,GAAA;MAHC,MAAM;MACN,gBAAc;MACf,OAAM;KAER,CAAA,GAAA,EAIS,QAAA,MAAA,EAHP,EAAA,CAAA,CAAM,CAAC,MAAM,UAAK,oBAAuC,EAAA,CAAA,CAAiB,CAAC,OAAO,cAA4B,EAAA,CAAA,CAAiB,CAAC,OAAO,KAAK,GAAA,CAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA;MAQvI,EAAA,CAAA,CAAM,CAAC,YAAY,OAAO,UAAM,KAAA,KADzC,EAAA,GAAA,EAkCM,OAlCN,IAkCM,CAtBJ,EAqBM,OArBN,IAqBM,EApBJ,EAAA,EAAA,GAAA,EAmBS,GAAA,MAAA,EAlBuB,EAAA,CAAA,CAAM,CAAC,YAAY,SAAK,CAAA,IAA9C,GAAY,OADtB,EAAA,GAAA,EAmBS,UAAA;MAjBN,KAAK;MACN,OAAK,GAAA,CAAC,sMACoB,EAAA,CAAA,CAAM,CAAC,YAAY,OAAO,UAAM,KAAA,IAAa,IAAwB,EAAA,QAAA,sCAAA,0DAAA,CAAA;MAM/F,OAAA;OAAA,gBAAA;OAAA,OAAA;OAAA,oBAAA;OAAA,cAAA;MAAA;MAMC,UAAK,MAAE,GAAsB,CAAU;KAErC,GAAA,EAAA,CAAU,GAAA,IAAA,EAAA;;IAOrB,EA2BM,OA3BN,IA2BM,CA1BJ,EAkBM,OAlBN,IAkBM,CAfJ,GAAA,EAOE,YAAA;KANS,uBAAA,AAAA,EAAA,QAAA,MAAA,EAAW,QAAA;KACpB,OAAM;KACL,aAAa,EAAA,CAAA,CAAiB,CAAC,OAAO;KACtC,UAAU,EAAA,CAAA,CAAM,CAAC,aAAa;KAC/B,MAAK;KACJ,WAAS;IALD,GAAA,MAAA,IAAA,EAAA,GAAA,CAAA,CAAA,IAAA,EAAA,KAAW,CAAA,CAAA,GAOtB,EAMS,UAAA;KALP,OAAM;KACL,UAAQ,CAAG,EAAA,MAAY,KAAI,KAAM,EAAA,CAAA,CAAM,CAAC,aAAa;KACrD,SAAO;IAER,GAAA,CAAA,EAAqC,EAAA,EAAA,GAAA;KAA9B,MAAM;KAAK,gBAAc;IAKpC,CAAA,CAAA,GAAA,GAAA,EAAA,CAAA,CAAA,GAAA,EAII,KAJJ,IAII,EADC,EAAA,CAAA,CAAiB,CAAC,OAAO,UAAU,GAAA,CAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"AiChatSidebar-tZqbhmuJ.js","names":[],"sources":["../../../src/cloud/components/AiChatSidebar.vue","../../../src/cloud/components/AiChatSidebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useAiChat } from \"@templatical/core/cloud\";\nimport {\n CLOUD_TRANSLATIONS_KEY,\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n MERGE_TAGS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n LoaderCircle,\n Redo2,\n Send,\n Sparkles,\n Trash2,\n Undo2,\n X,\n} from \"@lucide/vue\";\nimport { inject, nextTick, ref, watch } from \"vue\";\nimport { useIntervalFn, useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n visible: boolean;\n onApply?: (content: TemplateContent) => void;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n}>();\n\nconst cloudTranslations = requireInject(\n CLOUD_TRANSLATIONS_KEY,\n \"AiChatSidebar\",\n);\nconst editor = requireInject(EDITOR_KEY, \"AiChatSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"AiChatSidebar\");\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst aliveFlag = useAliveFlag();\n\nconst aiChat = useAiChat({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: props.onApply,\n onError: undefined,\n});\n\nconst promptInput = ref(\"\");\nconst messagesContainer = ref<HTMLElement | null>(null);\nconst historyLoaded = ref(false);\nconst visibleSuggestionCount = ref(0);\n\nconst { pause: pauseReveal, resume: resumeReveal } = useIntervalFn(\n () => {\n const count = aiChat.suggestions.value?.length ?? 0;\n if (visibleSuggestionCount.value < count) {\n visibleSuggestionCount.value++;\n } else {\n pauseReveal();\n }\n },\n 150,\n { immediate: false },\n);\n\nconst { start: startRevealTimeout } = useTimeoutFn(() => resumeReveal(), 100, {\n immediate: false,\n});\n\nwatch(\n () => aiChat.suggestions.value?.length ?? 0,\n (count) => {\n pauseReveal();\n if (count === 0) {\n visibleSuggestionCount.value = 0;\n return;\n }\n\n visibleSuggestionCount.value = 0;\n startRevealTimeout();\n },\n);\n\nfunction scrollToBottom(): void {\n nextTick(() => {\n if (messagesContainer.value) {\n messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;\n }\n });\n}\n\nwatch(\n () => aiChat.messages.value?.length ?? 0,\n () => scrollToBottom(),\n);\n\nwatch(\n () => props.visible,\n async (isVisible) => {\n if (isVisible && !historyLoaded.value) {\n historyLoaded.value = true;\n await aiChat.loadConversation();\n if (!aliveFlag.alive) return;\n\n if (\n (aiChat.messages.value?.length ?? 0) === 0 &&\n editor.content.value.blocks.length === 0\n ) {\n aiChat.loadSuggestions(editor.content.value, mergeTags);\n }\n }\n },\n);\n\nasync function handleSend(): Promise<void> {\n const prompt = promptInput.value.trim();\n if (!prompt || aiChat.isGenerating.value) {\n return;\n }\n\n promptInput.value = \"\";\n aiChat.error.value = null;\n aiChat.failedPrompt.value = null;\n scrollToBottom();\n\n await aiChat.sendPrompt(prompt, editor.content.value, mergeTags);\n if (!aliveFlag.alive) return;\n\n if (aiChat.failedPrompt.value) {\n promptInput.value = aiChat.failedPrompt.value;\n }\n\n scrollToBottom();\n}\n\nfunction stripJsonBlock(text: string): string {\n return text\n .replace(/```json[\\s\\S]*?```/g, \"\")\n .replace(/```json[\\s\\S]*/g, \"\")\n .trim();\n}\n\nfunction handleSuggestionClick(suggestion: string): void {\n pauseReveal();\n promptInput.value = suggestion;\n handleSend();\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n}\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-ai-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-ai-slide-leave-active\"\n leave-from-class=\"tpl:translate-x-0\"\n leave-to-class=\"tpl:translate-x-full\"\n >\n <div\n v-if=\"visible\"\n class=\"tpl-ai-sidebar tpl:absolute tpl:top-14 tpl:right-0 tpl:bottom-0 tpl:z-panel tpl:flex tpl:w-[360px] tpl:flex-col tpl:border-l tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <!-- Header -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n >\n <Sparkles :size=\"13\" :stroke-width=\"2\" />\n <span>\n {{ cloudTranslations.aiChat.title }}\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <button\n v-if=\"(aiChat.messages.value?.length ?? 0) > 0\"\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n :title=\"cloudTranslations.aiChat.clear\"\n @click=\"aiChat.clearChat()\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"emit('close')\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Messages wrapper -->\n <div class=\"tpl:relative tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col\">\n <!-- Messages -->\n <div\n ref=\"messagesContainer\"\n class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Loading history state -->\n <div\n v-if=\"aiChat.isLoadingHistory.value\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <LoaderCircle\n class=\"tpl-spinner tpl:text-[var(--tpl-text-muted)]\"\n :size=\"24\"\n :stroke-width=\"2\"\n />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudTranslations.aiChat.loadingHistory }}\n </p>\n </div>\n\n <!-- Empty state -->\n <div\n v-else-if=\"(aiChat.messages.value?.length ?? 0) === 0\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <Sparkles\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:max-w-[240px] tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudTranslations.aiChat.placeholder }}\n </p>\n </div>\n\n <!-- Message list -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <div\n v-for=\"(message, index) in aiChat.messages.value\"\n :key=\"message.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2\"\n >\n <!-- User message -->\n <div\n v-if=\"message.role === 'user'\"\n class=\"tpl:self-end tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm\"\n style=\"\n background-color: var(--tpl-primary-light);\n color: var(--tpl-text);\n max-width: 85%;\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{ message.content }}\n </div>\n\n <!-- Assistant message -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <LoadingTrack\n v-if=\"\n !stripJsonBlock(message.content) &&\n aiChat.isGenerating.value &&\n index === (aiChat.messages.value?.length ?? 0) - 1\n \"\n />\n <div\n v-else\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm tpl:whitespace-pre-wrap\"\n style=\"\n max-width: 85%;\n background-color: var(--tpl-bg);\n color: var(--tpl-text);\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{\n stripJsonBlock(message.content) ||\n cloudTranslations.aiChat.applied\n }}\n </div>\n <button\n v-if=\"\n message.id === aiChat.lastApplyMessageId.value &&\n !aiChat.isGenerating.value\n \"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:self-start tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-text-muted);\n background-color: transparent;\n \"\n @click=\"aiChat.toggleLastRevert()\"\n >\n <template v-if=\"aiChat.isLastChangeReverted.value\">\n <Redo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.reapply }}\n </template>\n <template v-else>\n <Undo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.revert }}\n </template>\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"aiChat.error.value\"\n class=\"tpl:mx-3 tpl:mb-2 tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs\"\n style=\"\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n \"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{\n aiChat.error.value === \"ai_apply_failed\"\n ? cloudTranslations.aiChat.applyFailed\n : cloudTranslations.aiChat.error\n }}</span>\n </div>\n\n <!-- Suggestions (overlay) -->\n <div\n v-if=\"(aiChat.suggestions.value?.length ?? 0) > 0\"\n class=\"tpl:absolute tpl:right-0 tpl:bottom-0 tpl:left-0 tpl:z-10 tpl:px-3 tpl:pb-3\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 50%,\n transparent\n );\n backdrop-filter: blur(2px);\n \"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <button\n v-for=\"(suggestion, index) in aiChat.suggestions.value ?? []\"\n :key=\"index\"\n class=\"tpl-suggestion-btn tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:text-left tpl:text-xs tpl:leading-snug tpl:transition-all tpl:duration-300 tpl:ease-out\"\n :class=\"\n (aiChat.suggestions.value?.length ?? 0) - 1 - index <\n visibleSuggestionCount\n ? 'tpl:translate-y-0 tpl:opacity-100'\n : 'tpl:pointer-events-none tpl:-translate-y-2 tpl:opacity-0'\n \"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n box-shadow: var(--tpl-shadow);\n \"\n @click=\"handleSuggestionClick(suggestion)\"\n >\n {{ suggestion }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Input area -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\n <div\n class=\"tpl-ai-input-wrapper tpl-focus-ring-host tpl:flex tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n v-model=\"promptInput\"\n class=\"tpl:max-h-32 tpl:min-h-[64px] tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-sm tpl:outline-none tpl:text-[var(--tpl-text)]\"\n :placeholder=\"cloudTranslations.aiChat.inputPlaceholder\"\n :disabled=\"aiChat.isGenerating.value\"\n rows=\"3\"\n @keydown=\"handleKeydown\"\n />\n <button\n class=\"tpl-ai-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"!promptInput.trim() || aiChat.isGenerating.value\"\n @click=\"handleSend\"\n >\n <Send :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:px-1 tpl:pt-2 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudTranslations.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-ai-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-input-wrapper:focus-within {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-ai-send-btn:not(:disabled):hover {\n transform: scale(1.05);\n}\n\n.tpl-suggestion-btn:hover {\n border-color: var(--tpl-primary) !important;\n background-color: var(--tpl-primary-light) !important;\n}\n</style>\n","<script setup lang=\"ts\">\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useAiChat } from \"@templatical/core/cloud\";\nimport {\n CLOUD_TRANSLATIONS_KEY,\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n MERGE_TAGS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n LoaderCircle,\n Redo2,\n Send,\n Sparkles,\n Trash2,\n Undo2,\n X,\n} from \"@lucide/vue\";\nimport { inject, nextTick, ref, watch } from \"vue\";\nimport { useIntervalFn, useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n visible: boolean;\n onApply?: (content: TemplateContent) => void;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n}>();\n\nconst cloudTranslations = requireInject(\n CLOUD_TRANSLATIONS_KEY,\n \"AiChatSidebar\",\n);\nconst editor = requireInject(EDITOR_KEY, \"AiChatSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"AiChatSidebar\");\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst aliveFlag = useAliveFlag();\n\nconst aiChat = useAiChat({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: props.onApply,\n onError: undefined,\n});\n\nconst promptInput = ref(\"\");\nconst messagesContainer = ref<HTMLElement | null>(null);\nconst historyLoaded = ref(false);\nconst visibleSuggestionCount = ref(0);\n\nconst { pause: pauseReveal, resume: resumeReveal } = useIntervalFn(\n () => {\n const count = aiChat.suggestions.value?.length ?? 0;\n if (visibleSuggestionCount.value < count) {\n visibleSuggestionCount.value++;\n } else {\n pauseReveal();\n }\n },\n 150,\n { immediate: false },\n);\n\nconst { start: startRevealTimeout } = useTimeoutFn(() => resumeReveal(), 100, {\n immediate: false,\n});\n\nwatch(\n () => aiChat.suggestions.value?.length ?? 0,\n (count) => {\n pauseReveal();\n if (count === 0) {\n visibleSuggestionCount.value = 0;\n return;\n }\n\n visibleSuggestionCount.value = 0;\n startRevealTimeout();\n },\n);\n\nfunction scrollToBottom(): void {\n nextTick(() => {\n if (messagesContainer.value) {\n messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;\n }\n });\n}\n\nwatch(\n () => aiChat.messages.value?.length ?? 0,\n () => scrollToBottom(),\n);\n\nwatch(\n () => props.visible,\n async (isVisible) => {\n if (isVisible && !historyLoaded.value) {\n historyLoaded.value = true;\n await aiChat.loadConversation();\n if (!aliveFlag.alive) return;\n\n if (\n (aiChat.messages.value?.length ?? 0) === 0 &&\n editor.content.value.blocks.length === 0\n ) {\n aiChat.loadSuggestions(editor.content.value, mergeTags);\n }\n }\n },\n);\n\nasync function handleSend(): Promise<void> {\n const prompt = promptInput.value.trim();\n if (!prompt || aiChat.isGenerating.value) {\n return;\n }\n\n promptInput.value = \"\";\n aiChat.error.value = null;\n aiChat.failedPrompt.value = null;\n scrollToBottom();\n\n await aiChat.sendPrompt(prompt, editor.content.value, mergeTags);\n if (!aliveFlag.alive) return;\n\n if (aiChat.failedPrompt.value) {\n promptInput.value = aiChat.failedPrompt.value;\n }\n\n scrollToBottom();\n}\n\nfunction stripJsonBlock(text: string): string {\n return text\n .replace(/```json[\\s\\S]*?```/g, \"\")\n .replace(/```json[\\s\\S]*/g, \"\")\n .trim();\n}\n\nfunction handleSuggestionClick(suggestion: string): void {\n pauseReveal();\n promptInput.value = suggestion;\n handleSend();\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n}\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-ai-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-ai-slide-leave-active\"\n leave-from-class=\"tpl:translate-x-0\"\n leave-to-class=\"tpl:translate-x-full\"\n >\n <div\n v-if=\"visible\"\n class=\"tpl-ai-sidebar tpl:absolute tpl:top-14 tpl:right-0 tpl:bottom-0 tpl:z-panel tpl:flex tpl:w-[360px] tpl:flex-col tpl:border-l tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <!-- Header -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n >\n <Sparkles :size=\"13\" :stroke-width=\"2\" />\n <span>\n {{ cloudTranslations.aiChat.title }}\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <button\n v-if=\"(aiChat.messages.value?.length ?? 0) > 0\"\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n :title=\"cloudTranslations.aiChat.clear\"\n @click=\"aiChat.clearChat()\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"emit('close')\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Messages wrapper -->\n <div class=\"tpl:relative tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col\">\n <!-- Messages -->\n <div\n ref=\"messagesContainer\"\n class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Loading history state -->\n <div\n v-if=\"aiChat.isLoadingHistory.value\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <LoaderCircle\n class=\"tpl-spinner tpl:text-[var(--tpl-text-muted)]\"\n :size=\"24\"\n :stroke-width=\"2\"\n />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudTranslations.aiChat.loadingHistory }}\n </p>\n </div>\n\n <!-- Empty state -->\n <div\n v-else-if=\"(aiChat.messages.value?.length ?? 0) === 0\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:text-center\"\n >\n <Sparkles\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:max-w-[240px] tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudTranslations.aiChat.placeholder }}\n </p>\n </div>\n\n <!-- Message list -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <div\n v-for=\"(message, index) in aiChat.messages.value\"\n :key=\"message.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2\"\n >\n <!-- User message -->\n <div\n v-if=\"message.role === 'user'\"\n class=\"tpl:self-end tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm\"\n style=\"\n background-color: var(--tpl-primary-light);\n color: var(--tpl-text);\n max-width: 85%;\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{ message.content }}\n </div>\n\n <!-- Assistant message -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <LoadingTrack\n v-if=\"\n !stripJsonBlock(message.content) &&\n aiChat.isGenerating.value &&\n index === (aiChat.messages.value?.length ?? 0) - 1\n \"\n />\n <div\n v-else\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3.5 tpl:py-2.5 tpl:text-sm tpl:whitespace-pre-wrap\"\n style=\"\n max-width: 85%;\n background-color: var(--tpl-bg);\n color: var(--tpl-text);\n box-shadow: var(--tpl-shadow);\n \"\n >\n {{\n stripJsonBlock(message.content) ||\n cloudTranslations.aiChat.applied\n }}\n </div>\n <button\n v-if=\"\n message.id === aiChat.lastApplyMessageId.value &&\n !aiChat.isGenerating.value\n \"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:self-start tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-text-muted);\n background-color: transparent;\n \"\n @click=\"aiChat.toggleLastRevert()\"\n >\n <template v-if=\"aiChat.isLastChangeReverted.value\">\n <Redo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.reapply }}\n </template>\n <template v-else>\n <Undo2 :size=\"12\" :stroke-width=\"2\" />\n {{ cloudTranslations.aiChat.revert }}\n </template>\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"aiChat.error.value\"\n class=\"tpl:mx-3 tpl:mb-2 tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs\"\n style=\"\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n \"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{\n aiChat.error.value === \"ai_apply_failed\"\n ? cloudTranslations.aiChat.applyFailed\n : cloudTranslations.aiChat.error\n }}</span>\n </div>\n\n <!-- Suggestions (overlay) -->\n <div\n v-if=\"(aiChat.suggestions.value?.length ?? 0) > 0\"\n class=\"tpl:absolute tpl:right-0 tpl:bottom-0 tpl:left-0 tpl:z-10 tpl:px-3 tpl:pb-3\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 50%,\n transparent\n );\n backdrop-filter: blur(2px);\n \"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <button\n v-for=\"(suggestion, index) in aiChat.suggestions.value ?? []\"\n :key=\"index\"\n class=\"tpl-suggestion-btn tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:text-left tpl:text-xs tpl:leading-snug tpl:transition-all tpl:duration-300 tpl:ease-out\"\n :class=\"\n (aiChat.suggestions.value?.length ?? 0) - 1 - index <\n visibleSuggestionCount\n ? 'tpl:translate-y-0 tpl:opacity-100'\n : 'tpl:pointer-events-none tpl:-translate-y-2 tpl:opacity-0'\n \"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n box-shadow: var(--tpl-shadow);\n \"\n @click=\"handleSuggestionClick(suggestion)\"\n >\n {{ suggestion }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Input area -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\n <div\n class=\"tpl-ai-input-wrapper tpl-focus-ring-host tpl:flex tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n v-model=\"promptInput\"\n class=\"tpl:max-h-32 tpl:min-h-[64px] tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-sm tpl:outline-none tpl:text-[var(--tpl-text)]\"\n :placeholder=\"cloudTranslations.aiChat.inputPlaceholder\"\n :disabled=\"aiChat.isGenerating.value\"\n rows=\"3\"\n @keydown=\"handleKeydown\"\n />\n <button\n class=\"tpl-ai-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"!promptInput.trim() || aiChat.isGenerating.value\"\n @click=\"handleSend\"\n >\n <Send :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:px-1 tpl:pt-2 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudTranslations.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-ai-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-ai-input-wrapper:focus-within {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-ai-send-btn:not(:disabled):hover {\n transform: scale(1.05);\n}\n\n.tpl-suggestion-btn:hover {\n border-color: var(--tpl-primary) !important;\n background-color: var(--tpl-primary-light) !important;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { F as e, N as t, R as n, S as r, f as i, g as a, lt as o, m as s, p as c, u as l, ut as u } from "./draggable-BfpsXFg7.js";
|
|
2
|
-
import "./useEditorCore-
|
|
2
|
+
import "./useEditorCore-D84laTMj.js";
|
|
3
3
|
import { W as d, t as f } from "./keys-oJdkdtPn.js";
|
|
4
|
-
import { E as p, k as m, pt as h } from "./icons-
|
|
4
|
+
import { E as p, k as m, pt as h } from "./icons-Dl5bcXmT.js";
|
|
5
5
|
import { t as g } from "./_plugin-vue_export-helper-B3ysoDQm.js";
|
|
6
6
|
import { n as _ } from "./useCloudI18n-Ch-SiRjU.js";
|
|
7
7
|
//#region src/cloud/components/AiFeatureMenu.vue?vue&type=script&setup=true&lang.ts
|
|
@@ -54,4 +54,4 @@ var v = { class: "tpl-ai-feature-menu tpl:w-[280px] tpl:overflow-hidden tpl:roun
|
|
|
54
54
|
//#endregion
|
|
55
55
|
export { C as default };
|
|
56
56
|
|
|
57
|
-
//# sourceMappingURL=AiFeatureMenu-
|
|
57
|
+
//# sourceMappingURL=AiFeatureMenu-BhtNkpIq.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiFeatureMenu-
|
|
1
|
+
{"version":3,"file":"AiFeatureMenu-BhtNkpIq.js","names":[],"sources":["../../../src/cloud/components/AiFeatureMenu.vue","../../../src/cloud/components/AiFeatureMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useCloudI18nStrict } from \"../../composables\";\nimport { AI_CONFIG_KEY, requireInject } from \"../../keys\";\nimport type { AiConfig } from \"@templatical/types\";\nimport { ImageUp, ShieldCheck, Sparkles } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nexport type AiFeature = \"ai-chat\" | \"design-reference\" | \"scoring\";\n\ndefineProps<{\n activeFeature: AiFeature | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select\", feature: AiFeature): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n\nconst aiConfig = requireInject(AI_CONFIG_KEY, \"AiFeatureMenu\");\n\nconst configKeyMap: Record<AiFeature, keyof AiConfig> = {\n \"ai-chat\": \"chat\",\n \"design-reference\": \"designToTemplate\",\n scoring: \"scoring\",\n};\n\nconst allFeatures: { key: AiFeature; icon: typeof Sparkles }[] = [\n { key: \"ai-chat\", icon: Sparkles },\n { key: \"design-reference\", icon: ImageUp },\n { key: \"scoring\", icon: ShieldCheck },\n];\n\nconst features = computed(() =>\n allFeatures.filter((f) => aiConfig.isFeatureEnabled(configKeyMap[f.key])),\n);\n\nfunction getTitle(key: AiFeature): string {\n if (key === \"ai-chat\") return cloudT.aiMenu.aiAssistant;\n if (key === \"design-reference\") return cloudT.aiMenu.designToTemplate;\n return cloudT.aiMenu.templateScore;\n}\n\nfunction getDescription(key: AiFeature): string {\n if (key === \"ai-chat\") return cloudT.aiMenu.aiAssistantDesc;\n if (key === \"design-reference\") return cloudT.aiMenu.designToTemplateDesc;\n return cloudT.aiMenu.templateScoreDesc;\n}\n</script>\n\n<template>\n <div\n class=\"tpl-ai-feature-menu tpl:w-[280px] tpl:overflow-hidden tpl:rounded-[var(--tpl-radius)] tpl:py-1 tpl:bg-[var(--tpl-bg)] tpl:border tpl:border-[var(--tpl-border)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n >\n <button\n v-for=\"feature in features\"\n :key=\"feature.key\"\n class=\"tpl-ai-feature-menu-item tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-start tpl:gap-3 tpl:border-none tpl:px-3 tpl:py-2.5 tpl:text-left tpl:transition-colors\"\n :style=\"{\n backgroundColor:\n activeFeature === feature.key\n ? 'var(--tpl-primary-light)'\n : 'transparent',\n }\"\n @click=\"emit('select', feature.key)\"\n >\n <div\n class=\"tpl:mt-0.5 tpl:flex tpl:size-7 tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n backgroundColor:\n activeFeature === feature.key\n ? 'var(--tpl-primary)'\n : 'var(--tpl-bg-active)',\n color:\n activeFeature === feature.key\n ? 'var(--tpl-bg)'\n : 'var(--tpl-text-muted)',\n }\"\n >\n <component :is=\"feature.icon\" :size=\"15\" :stroke-width=\"2\" />\n </div>\n <div class=\"tpl:flex tpl:min-w-0 tpl:flex-col tpl:gap-0.5\">\n <span class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\">\n {{ getTitle(feature.key) }}\n </span>\n <span\n class=\"tpl:text-xs tpl:leading-snug tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ getDescription(feature.key) }}\n </span>\n </div>\n </button>\n </div>\n</template>\n\n<style scoped>\n.tpl-ai-feature-menu-item:hover {\n background-color: var(--tpl-bg-hover) !important;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useCloudI18nStrict } from \"../../composables\";\nimport { AI_CONFIG_KEY, requireInject } from \"../../keys\";\nimport type { AiConfig } from \"@templatical/types\";\nimport { ImageUp, ShieldCheck, Sparkles } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nexport type AiFeature = \"ai-chat\" | \"design-reference\" | \"scoring\";\n\ndefineProps<{\n activeFeature: AiFeature | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select\", feature: AiFeature): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n\nconst aiConfig = requireInject(AI_CONFIG_KEY, \"AiFeatureMenu\");\n\nconst configKeyMap: Record<AiFeature, keyof AiConfig> = {\n \"ai-chat\": \"chat\",\n \"design-reference\": \"designToTemplate\",\n scoring: \"scoring\",\n};\n\nconst allFeatures: { key: AiFeature; icon: typeof Sparkles }[] = [\n { key: \"ai-chat\", icon: Sparkles },\n { key: \"design-reference\", icon: ImageUp },\n { key: \"scoring\", icon: ShieldCheck },\n];\n\nconst features = computed(() =>\n allFeatures.filter((f) => aiConfig.isFeatureEnabled(configKeyMap[f.key])),\n);\n\nfunction getTitle(key: AiFeature): string {\n if (key === \"ai-chat\") return cloudT.aiMenu.aiAssistant;\n if (key === \"design-reference\") return cloudT.aiMenu.designToTemplate;\n return cloudT.aiMenu.templateScore;\n}\n\nfunction getDescription(key: AiFeature): string {\n if (key === \"ai-chat\") return cloudT.aiMenu.aiAssistantDesc;\n if (key === \"design-reference\") return cloudT.aiMenu.designToTemplateDesc;\n return cloudT.aiMenu.templateScoreDesc;\n}\n</script>\n\n<template>\n <div\n class=\"tpl-ai-feature-menu tpl:w-[280px] tpl:overflow-hidden tpl:rounded-[var(--tpl-radius)] tpl:py-1 tpl:bg-[var(--tpl-bg)] tpl:border tpl:border-[var(--tpl-border)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n >\n <button\n v-for=\"feature in features\"\n :key=\"feature.key\"\n class=\"tpl-ai-feature-menu-item tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-start tpl:gap-3 tpl:border-none tpl:px-3 tpl:py-2.5 tpl:text-left tpl:transition-colors\"\n :style=\"{\n backgroundColor:\n activeFeature === feature.key\n ? 'var(--tpl-primary-light)'\n : 'transparent',\n }\"\n @click=\"emit('select', feature.key)\"\n >\n <div\n class=\"tpl:mt-0.5 tpl:flex tpl:size-7 tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n backgroundColor:\n activeFeature === feature.key\n ? 'var(--tpl-primary)'\n : 'var(--tpl-bg-active)',\n color:\n activeFeature === feature.key\n ? 'var(--tpl-bg)'\n : 'var(--tpl-text-muted)',\n }\"\n >\n <component :is=\"feature.icon\" :size=\"15\" :stroke-width=\"2\" />\n </div>\n <div class=\"tpl:flex tpl:min-w-0 tpl:flex-col tpl:gap-0.5\">\n <span class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\">\n {{ getTitle(feature.key) }}\n </span>\n <span\n class=\"tpl:text-xs tpl:leading-snug tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ getDescription(feature.key) }}\n </span>\n </div>\n </button>\n </div>\n</template>\n\n<style scoped>\n.tpl-ai-feature-menu-item:hover {\n background-color: var(--tpl-bg-hover) !important;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;EAaA,IAAM,IAAO,GAIP,EAAE,GAAG,MAAW,EAAmB,GAEnC,IAAW,EAAc,GAAe,eAAe,GAEvD,IAAkD;GACtD,WAAW;GACX,oBAAoB;GACpB,SAAS;EACX,GAEM,IAA2D;GAC/D;IAAE,KAAK;IAAW,MAAM;GAAS;GACjC;IAAE,KAAK;IAAoB,MAAM;GAAQ;GACzC;IAAE,KAAK;IAAW,MAAM;GAAY;EACtC,GAEM,IAAW,QACf,EAAY,QAAQ,MAAM,EAAS,iBAAiB,EAAa,EAAE,IAAI,CAAC,CAC1E;EAEA,SAAS,EAAS,GAAwB;GAGxC,OAFI,MAAQ,YAAkB,EAAO,OAAO,cACxC,MAAQ,qBAA2B,EAAO,OAAO,mBAC9C,EAAO,OAAO;EACvB;EAEA,SAAS,EAAe,GAAwB;GAG9C,OAFI,MAAQ,YAAkB,EAAO,OAAO,kBACxC,MAAQ,qBAA2B,EAAO,OAAO,uBAC9C,EAAO,OAAO;EACvB;oBAIE,EAAA,GAAA,EAyCM,OAzCN,GAyCM,EAtCJ,EAAA,EAAA,GAAA,EAqCS,GAAA,MAAA,EApCW,EAAA,QAAX,OADT,EAAA,GAAA,EAqCS,UAAA;GAnCN,KAAK,EAAQ;GACd,OAAM;GACL,OAAK,EAAA,EAAA,iBAAuC,EAAA,kBAAkB,EAAQ,MAAA,6BAAA,cAAA,CAAA;GAMtE,UAAK,MAAE,EAAI,UAAW,EAAQ,GAAG;EAElC,GAAA,CAAA,EAcM,OAAA;GAbJ,OAAM;GACL,OAAK,EAAA;IAA2C,iBAAA,EAAA,kBAAkB,EAAQ,MAAA,uBAAA;IAA8G,OAAA,EAAA,kBAAkB,EAAQ,MAAA,kBAAA;;EAWnN,GAAA,EAAA,EAAA,GAAA,EAA6D,EAA7C,EAAQ,IAAI,GAAA;GAAG,MAAM;GAAK,gBAAc;EAE1D,CAAA,EAAA,GAAA,CAAA,GAAA,EASM,OATN,GASM,CARJ,EAEO,QAFP,GAEO,EADF,EAAS,EAAQ,GAAG,CAAA,GAAA,CAAA,GAEzB,EAIO,QAJP,GAIO,EADF,EAAe,EAAQ,GAAG,CAAA,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,IAAA,CAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { E as e, N as t, S as n, at as r, ct as i, f as a, g as o, h as s, m as c } from "./draggable-BfpsXFg7.js";
|
|
2
2
|
import { z as l } from "./keys-oJdkdtPn.js";
|
|
3
3
|
import { t as u } from "./useI18n-4sUbTM3Y.js";
|
|
4
|
-
import { Mt as d, u as f } from "./icons-
|
|
4
|
+
import { Mt as d, u as f } from "./icons-Dl5bcXmT.js";
|
|
5
5
|
//#region src/components/canvas/BlockIssueBadge.vue?vue&type=script&setup=true&lang.ts
|
|
6
6
|
var p = ["title", "aria-label"], m = /* @__PURE__ */ n({
|
|
7
7
|
__name: "BlockIssueBadge",
|
|
@@ -32,4 +32,4 @@ var p = ["title", "aria-label"], m = /* @__PURE__ */ n({
|
|
|
32
32
|
//#endregion
|
|
33
33
|
export { m as default };
|
|
34
34
|
|
|
35
|
-
//# sourceMappingURL=BlockIssueBadge-
|
|
35
|
+
//# sourceMappingURL=BlockIssueBadge-D9L8j7Db.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockIssueBadge-
|
|
1
|
+
{"version":3,"file":"BlockIssueBadge-D9L8j7Db.js","names":[],"sources":["../../../src/components/canvas/BlockIssueBadge.vue","../../../src/components/canvas/BlockIssueBadge.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport { CircleAlert, TriangleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { TEMPLATE_LINT_KEY } from \"../../keys\";\n\nconst props = defineProps<{ blockId: string }>();\nconst { t } = useI18n();\nconst lint = inject(TEMPLATE_LINT_KEY, null);\n\nconst blockIssues = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.blockId === props.blockId),\n);\n\nconst severity = computed<\"error\" | \"warning\" | null>(() => {\n if (blockIssues.value.some((i) => i.severity === \"error\")) return \"error\";\n if (blockIssues.value.some((i) => i.severity === \"warning\")) return \"warning\";\n return null;\n});\n\nconst tooltip = computed(() =>\n severity.value === \"error\" ? t.issues.badgeError : t.issues.badgeWarning,\n);\n</script>\n\n<template>\n <span\n v-if=\"severity\"\n class=\"tpl:absolute tpl:top-1 tpl:right-1 tpl:z-10 tpl:flex tpl:size-5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)] tpl:ring-1 tpl:ring-[var(--tpl-border)]\"\n :class=\"\n severity === 'error'\n ? 'tpl:text-[var(--tpl-danger)]'\n : 'tpl:text-[var(--tpl-warning)]'\n \"\n :title=\"tooltip\"\n :aria-label=\"tooltip\"\n role=\"img\"\n >\n <CircleAlert\n v-if=\"severity === 'error'\"\n :size=\"14\"\n :stroke-width=\"2.25\"\n fill=\"currentColor\"\n stroke=\"var(--tpl-bg)\"\n />\n <TriangleAlert\n v-else\n :size=\"14\"\n :stroke-width=\"2.25\"\n fill=\"currentColor\"\n stroke=\"var(--tpl-bg)\"\n />\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport { CircleAlert, TriangleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { TEMPLATE_LINT_KEY } from \"../../keys\";\n\nconst props = defineProps<{ blockId: string }>();\nconst { t } = useI18n();\nconst lint = inject(TEMPLATE_LINT_KEY, null);\n\nconst blockIssues = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.blockId === props.blockId),\n);\n\nconst severity = computed<\"error\" | \"warning\" | null>(() => {\n if (blockIssues.value.some((i) => i.severity === \"error\")) return \"error\";\n if (blockIssues.value.some((i) => i.severity === \"warning\")) return \"warning\";\n return null;\n});\n\nconst tooltip = computed(() =>\n severity.value === \"error\" ? t.issues.badgeError : t.issues.badgeWarning,\n);\n</script>\n\n<template>\n <span\n v-if=\"severity\"\n class=\"tpl:absolute tpl:top-1 tpl:right-1 tpl:z-10 tpl:flex tpl:size-5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)] tpl:ring-1 tpl:ring-[var(--tpl-border)]\"\n :class=\"\n severity === 'error'\n ? 'tpl:text-[var(--tpl-danger)]'\n : 'tpl:text-[var(--tpl-warning)]'\n \"\n :title=\"tooltip\"\n :aria-label=\"tooltip\"\n role=\"img\"\n >\n <CircleAlert\n v-if=\"severity === 'error'\"\n :size=\"14\"\n :stroke-width=\"2.25\"\n fill=\"currentColor\"\n stroke=\"var(--tpl-bg)\"\n />\n <TriangleAlert\n v-else\n :size=\"14\"\n :stroke-width=\"2.25\"\n fill=\"currentColor\"\n stroke=\"var(--tpl-bg)\"\n />\n </span>\n</template>\n"],"mappings":";;;;;;;;;EAMA,IAAM,IAAQ,GACR,EAAE,SAAM,EAAQ,GAChB,IAAO,EAAO,GAAmB,IAAI,GAErC,IAAc,SACjB,GAAM,OAAO,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,EAAE,YAAY,EAAM,OAAO,CACtE,GAEM,IAAW,QACX,EAAY,MAAM,MAAM,MAAM,EAAE,aAAa,OAAO,IAAU,UAC9D,EAAY,MAAM,MAAM,MAAM,EAAE,aAAa,SAAS,IAAU,YAC7D,IACR,GAEK,IAAU,QACd,EAAS,UAAU,UAAU,EAAE,OAAO,aAAa,EAAE,OAAO,YAC9D;mBAKU,EAAA,SADR,EAAA,GAAA,EA0BO,QAAA;;GAxBL,OAAK,EAAA,CAAC,yNACS,EAAA,UAAQ,UAAA,iCAAA,+BAAA,CAAA;GAKtB,OAAO,EAAA;GACP,cAAY,EAAA;GACb,MAAK;EAGG,GAAA,CAAA,EAAA,UAAQ,WADhB,EAAA,GAAA,EAME,EAAA,CAAA,GAAA;;GAJC,MAAM;GACN,gBAAc;GACf,MAAK;GACL,QAAO;EAET,CAAA,MAAA,EAAA,GAAA,EAME,EAAA,CAAA,GAAA;;GAJC,MAAM;GACN,gBAAc;GACf,MAAK;GACL,QAAO"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as e } from "./rolldown-runtime-B0aSnxlc.js";
|
|
2
2
|
import { E as t, F as n, L as r, N as i, P as a, R as o, S as s, at as c, f as l, g as u, lt as d, m as f, p, u as m } from "./draggable-BfpsXFg7.js";
|
|
3
|
-
import { O as h, _ as g, a as _, c as v, d as y, f as b, h as x, i as S, m as C, n as w, p as T, r as E, u as D, v as O, y as k } from "./useEditorCore-
|
|
3
|
+
import { O as h, _ as g, a as _, c as v, d as y, f as b, h as x, i as S, m as C, n as w, p as T, r as E, u as D, v as O, y as k } from "./useEditorCore-D84laTMj.js";
|
|
4
4
|
import { O as A, U as j, d as M, h as N, s as P } from "./keys-oJdkdtPn.js";
|
|
5
5
|
import { n as F, t as I } from "./emailFrameWidth-BmFCN2dp.js";
|
|
6
6
|
//#region src/components/blocks/PreviewSectionBlock.vue?vue&type=script&setup=true&lang.ts
|
|
@@ -120,4 +120,4 @@ var L = { class: "tpl:w-full" }, R = { class: "tpl:flex tpl:gap-0" }, z = /* @__
|
|
|
120
120
|
//#endregion
|
|
121
121
|
export { V as n, H as t };
|
|
122
122
|
|
|
123
|
-
//# sourceMappingURL=BlockPreviewCanvas-
|
|
123
|
+
//# sourceMappingURL=BlockPreviewCanvas-Bt7fFzIQ.js.map
|
package/dist/cdn/chunks/{BlockPreviewCanvas-CuWJLv8h.js.map → BlockPreviewCanvas-Bt7fFzIQ.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockPreviewCanvas-CuWJLv8h.js","names":[],"sources":["../../../src/components/blocks/PreviewSectionBlock.vue","../../../src/components/blocks/PreviewSectionBlock.vue","../../../src/components/BlockPreviewCanvas.vue","../../../src/components/BlockPreviewCanvas.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport ButtonBlock from \"./ButtonBlock.vue\";\nimport CustomBlock from \"./CustomBlock.vue\";\nimport DividerBlock from \"./DividerBlock.vue\";\nimport HtmlBlock from \"./HtmlBlock.vue\";\nimport ImageBlock from \"./ImageBlock.vue\";\nimport MenuBlock from \"./MenuBlock.vue\";\nimport SocialIconsBlock from \"./SocialIconsBlock.vue\";\nimport SpacerBlock from \"./SpacerBlock.vue\";\nimport TableBlock from \"./TableBlock.vue\";\nimport TitleBlock from \"./TitleBlock.vue\";\nimport ParagraphBlock from \"./ParagraphBlock.vue\";\nimport VideoBlock from \"./VideoBlock.vue\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n} from \"../../utils/blockComponentResolver\";\nimport type {\n Block,\n SectionBlock as SectionBlockType,\n} from \"@templatical/types\";\nimport { computed, inject, type Component } from \"vue\";\nimport { BLOCK_REGISTRY_KEY } from \"../../keys\";\n\nconst previewBlockComponentMap: Record<string, Component> = {\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n block: SectionBlockType;\n viewport: \"desktop\";\n}>();\n\ndefineOptions({ name: \"PreviewSectionBlock\" });\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst columnWidths = computed(() => {\n switch (props.block.columns) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n return [\"33.33%\", \"33.33%\", \"33.33%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n});\n\nconst columns = computed(() => {\n const count = columnWidths.value.length;\n const children = [...props.block.children];\n while (children.length < count) {\n children.push([]);\n }\n return children.slice(0, count);\n});\n\nfunction getColumnBlocks(colIndex: number): Block[] {\n return columns.value[colIndex] || [];\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewBlockComponentMap);\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div class=\"tpl:flex tpl:gap-0\">\n <div\n v-for=\"(_, colIndex) in columns\"\n :key=\"colIndex\"\n :style=\"{ width: columnWidths[colIndex] }\"\n >\n <div\n v-for=\"childBlock in getColumnBlocks(colIndex)\"\n :key=\"childBlock.id\"\n :style=\"getBlockWrapperStyle(childBlock)\"\n >\n <!-- Recursive self-reference for nested sections -->\n <PreviewSectionBlock\n v-if=\"childBlock.type === 'section'\"\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n <component\n :is=\"getBlockComponent(childBlock)\"\n v-else\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ButtonBlock from \"./ButtonBlock.vue\";\nimport CustomBlock from \"./CustomBlock.vue\";\nimport DividerBlock from \"./DividerBlock.vue\";\nimport HtmlBlock from \"./HtmlBlock.vue\";\nimport ImageBlock from \"./ImageBlock.vue\";\nimport MenuBlock from \"./MenuBlock.vue\";\nimport SocialIconsBlock from \"./SocialIconsBlock.vue\";\nimport SpacerBlock from \"./SpacerBlock.vue\";\nimport TableBlock from \"./TableBlock.vue\";\nimport TitleBlock from \"./TitleBlock.vue\";\nimport ParagraphBlock from \"./ParagraphBlock.vue\";\nimport VideoBlock from \"./VideoBlock.vue\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n} from \"../../utils/blockComponentResolver\";\nimport type {\n Block,\n SectionBlock as SectionBlockType,\n} from \"@templatical/types\";\nimport { computed, inject, type Component } from \"vue\";\nimport { BLOCK_REGISTRY_KEY } from \"../../keys\";\n\nconst previewBlockComponentMap: Record<string, Component> = {\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n block: SectionBlockType;\n viewport: \"desktop\";\n}>();\n\ndefineOptions({ name: \"PreviewSectionBlock\" });\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst columnWidths = computed(() => {\n switch (props.block.columns) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n return [\"33.33%\", \"33.33%\", \"33.33%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n});\n\nconst columns = computed(() => {\n const count = columnWidths.value.length;\n const children = [...props.block.children];\n while (children.length < count) {\n children.push([]);\n }\n return children.slice(0, count);\n});\n\nfunction getColumnBlocks(colIndex: number): Block[] {\n return columns.value[colIndex] || [];\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewBlockComponentMap);\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div class=\"tpl:flex tpl:gap-0\">\n <div\n v-for=\"(_, colIndex) in columns\"\n :key=\"colIndex\"\n :style=\"{ width: columnWidths[colIndex] }\"\n >\n <div\n v-for=\"childBlock in getColumnBlocks(colIndex)\"\n :key=\"childBlock.id\"\n :style=\"getBlockWrapperStyle(childBlock)\"\n >\n <!-- Recursive self-reference for nested sections -->\n <PreviewSectionBlock\n v-if=\"childBlock.type === 'section'\"\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n <component\n :is=\"getBlockComponent(childBlock)\"\n v-else\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport PreviewSectionBlock from \"./blocks/PreviewSectionBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\nimport {\n BLOCK_REGISTRY_KEY,\n CONDITION_PREVIEW_KEY,\n EDITOR_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../keys\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n getDocumentStyle,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport type { Block, ViewportSize } from \"@templatical/types\";\nimport { computed, inject, provide, type Component } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n blocks: Block[];\n /**\n * Which viewport to render. Defaults to `desktop`, so the saved-blocks\n * surfaces — which have no viewport control — behave exactly as before.\n * The test-email dialog passes the user's choice so responsive blocks show\n * the variant a recipient on that device would receive.\n */\n viewport?: ViewportSize;\n /**\n * Whether the hand-toggled display-condition filter still applies here.\n * `false` where a resolver owns the preview: it has already evaluated every\n * condition against real data, so layering a manual hide on top would veto\n * the answer that was asked for.\n *\n * A prop rather than an injected `PREVIEW_RESOLUTION_KEY` read, because the\n * test-email dialog builds its *own* resolution instance (keyed to the\n * selected recipient) — injecting would consult the shared canvas instance\n * and get the wrong answer.\n */\n applyConditionFilter?: boolean;\n }>(),\n { viewport: \"desktop\", applyConditionFilter: true },\n);\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY);\nconst editor = inject(EDITOR_KEY, null);\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\n\n/**\n * This canvas is only ever a preview — nothing here is editable — so it honours\n * the user's mode directly, with no `previewMode` gate of the kind `Canvas.vue`\n * needs. Re-provided rather than left to inherit, because a `BlockPreviewCanvas`\n * inside a dialog may sit under an editing `Canvas` that provides `false`.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(() => mergeTagSampleMode?.value ?? false),\n);\n\n/**\n * Blocks a display condition currently excludes, so a preview never shows\n * content the recipient won't get. The canvas does the same via\n * `v-show=\"!conditionPreview?.isHidden(block.id)\"`.\n *\n * A no-op for the saved-blocks browser: entries there carry their *stored* ids,\n * which aren't in the current template, so nothing is tracked as hidden. It\n * bites where it should — the save dialog and the test-email preview, both of\n * which show blocks that are live on the canvas.\n *\n * Skipped entirely when `applyConditionFilter` is false — see the prop.\n */\nconst visibleBlocks = computed(() =>\n props.applyConditionFilter\n ? props.blocks.filter((block) => !conditionPreview?.isHidden(block.id))\n : props.blocks,\n);\n\n/**\n * Frame width, from the one helper the canvas and the scaled preview rows also\n * use — so all three agree by construction rather than by three constants that\n * happen to match. A template with a custom body width now previews at that\n * width instead of a flat 600.\n */\nconst frameWidth = computed(() =>\n getEmailFrameWidth(editor?.content.value.settings, props.viewport),\n);\n\n/**\n * The same document-level style the canvas applies, so a previewed block\n * renders identically to the canvas one: without it the font falls back to the\n * editor UI's and the link rules hit their unset defaults, dropping the\n * underline and the link colour.\n *\n * Read from the *current* template even in the saved-blocks browser, where the\n * previewed content came from elsewhere — a saved block stores only `Block[]`,\n * and the current settings are what it will actually look like once inserted.\n */\nconst documentStyle = computed(() =>\n editor ? getDocumentStyle(editor.content.value.settings) : {},\n);\n\nconst previewComponentMap: Record<string, Component> = {\n section: PreviewSectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewComponentMap);\n}\n</script>\n\n<template>\n <div\n data-testid=\"block-preview-canvas\"\n class=\"tpl:pointer-events-none tpl:mx-auto tpl:select-none tpl:rounded-lg\"\n :style=\"{\n width: `${frameWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n backgroundColor: 'var(--tpl-canvas-bg)',\n boxShadow: 'var(--tpl-shadow-sm)',\n ...documentStyle,\n }\"\n >\n <div\n v-for=\"block in visibleBlocks\"\n :key=\"block.id\"\n :style=\"getBlockWrapperStyle(block)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport PreviewSectionBlock from \"./blocks/PreviewSectionBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\nimport {\n BLOCK_REGISTRY_KEY,\n CONDITION_PREVIEW_KEY,\n EDITOR_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../keys\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n getDocumentStyle,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport type { Block, ViewportSize } from \"@templatical/types\";\nimport { computed, inject, provide, type Component } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n blocks: Block[];\n /**\n * Which viewport to render. Defaults to `desktop`, so the saved-blocks\n * surfaces — which have no viewport control — behave exactly as before.\n * The test-email dialog passes the user's choice so responsive blocks show\n * the variant a recipient on that device would receive.\n */\n viewport?: ViewportSize;\n /**\n * Whether the hand-toggled display-condition filter still applies here.\n * `false` where a resolver owns the preview: it has already evaluated every\n * condition against real data, so layering a manual hide on top would veto\n * the answer that was asked for.\n *\n * A prop rather than an injected `PREVIEW_RESOLUTION_KEY` read, because the\n * test-email dialog builds its *own* resolution instance (keyed to the\n * selected recipient) — injecting would consult the shared canvas instance\n * and get the wrong answer.\n */\n applyConditionFilter?: boolean;\n }>(),\n { viewport: \"desktop\", applyConditionFilter: true },\n);\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY);\nconst editor = inject(EDITOR_KEY, null);\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\n\n/**\n * This canvas is only ever a preview — nothing here is editable — so it honours\n * the user's mode directly, with no `previewMode` gate of the kind `Canvas.vue`\n * needs. Re-provided rather than left to inherit, because a `BlockPreviewCanvas`\n * inside a dialog may sit under an editing `Canvas` that provides `false`.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(() => mergeTagSampleMode?.value ?? false),\n);\n\n/**\n * Blocks a display condition currently excludes, so a preview never shows\n * content the recipient won't get. The canvas does the same via\n * `v-show=\"!conditionPreview?.isHidden(block.id)\"`.\n *\n * A no-op for the saved-blocks browser: entries there carry their *stored* ids,\n * which aren't in the current template, so nothing is tracked as hidden. It\n * bites where it should — the save dialog and the test-email preview, both of\n * which show blocks that are live on the canvas.\n *\n * Skipped entirely when `applyConditionFilter` is false — see the prop.\n */\nconst visibleBlocks = computed(() =>\n props.applyConditionFilter\n ? props.blocks.filter((block) => !conditionPreview?.isHidden(block.id))\n : props.blocks,\n);\n\n/**\n * Frame width, from the one helper the canvas and the scaled preview rows also\n * use — so all three agree by construction rather than by three constants that\n * happen to match. A template with a custom body width now previews at that\n * width instead of a flat 600.\n */\nconst frameWidth = computed(() =>\n getEmailFrameWidth(editor?.content.value.settings, props.viewport),\n);\n\n/**\n * The same document-level style the canvas applies, so a previewed block\n * renders identically to the canvas one: without it the font falls back to the\n * editor UI's and the link rules hit their unset defaults, dropping the\n * underline and the link colour.\n *\n * Read from the *current* template even in the saved-blocks browser, where the\n * previewed content came from elsewhere — a saved block stores only `Block[]`,\n * and the current settings are what it will actually look like once inserted.\n */\nconst documentStyle = computed(() =>\n editor ? getDocumentStyle(editor.content.value.settings) : {},\n);\n\nconst previewComponentMap: Record<string, Component> = {\n section: PreviewSectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewComponentMap);\n}\n</script>\n\n<template>\n <div\n data-testid=\"block-preview-canvas\"\n class=\"tpl:pointer-events-none tpl:mx-auto tpl:select-none tpl:rounded-lg\"\n :style=\"{\n width: `${frameWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n backgroundColor: 'var(--tpl-canvas-bg)',\n boxShadow: 'var(--tpl-shadow-sm)',\n ...documentStyle,\n }\"\n >\n <div\n v-for=\"block in visibleBlocks\"\n :key=\"block.id\"\n :style=\"getBlockWrapperStyle(block)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n />\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BlockPreviewCanvas-Bt7fFzIQ.js","names":[],"sources":["../../../src/components/blocks/PreviewSectionBlock.vue","../../../src/components/blocks/PreviewSectionBlock.vue","../../../src/components/BlockPreviewCanvas.vue","../../../src/components/BlockPreviewCanvas.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport ButtonBlock from \"./ButtonBlock.vue\";\nimport CustomBlock from \"./CustomBlock.vue\";\nimport DividerBlock from \"./DividerBlock.vue\";\nimport HtmlBlock from \"./HtmlBlock.vue\";\nimport ImageBlock from \"./ImageBlock.vue\";\nimport MenuBlock from \"./MenuBlock.vue\";\nimport SocialIconsBlock from \"./SocialIconsBlock.vue\";\nimport SpacerBlock from \"./SpacerBlock.vue\";\nimport TableBlock from \"./TableBlock.vue\";\nimport TitleBlock from \"./TitleBlock.vue\";\nimport ParagraphBlock from \"./ParagraphBlock.vue\";\nimport VideoBlock from \"./VideoBlock.vue\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n} from \"../../utils/blockComponentResolver\";\nimport type {\n Block,\n SectionBlock as SectionBlockType,\n} from \"@templatical/types\";\nimport { computed, inject, type Component } from \"vue\";\nimport { BLOCK_REGISTRY_KEY } from \"../../keys\";\n\nconst previewBlockComponentMap: Record<string, Component> = {\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n block: SectionBlockType;\n viewport: \"desktop\";\n}>();\n\ndefineOptions({ name: \"PreviewSectionBlock\" });\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst columnWidths = computed(() => {\n switch (props.block.columns) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n return [\"33.33%\", \"33.33%\", \"33.33%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n});\n\nconst columns = computed(() => {\n const count = columnWidths.value.length;\n const children = [...props.block.children];\n while (children.length < count) {\n children.push([]);\n }\n return children.slice(0, count);\n});\n\nfunction getColumnBlocks(colIndex: number): Block[] {\n return columns.value[colIndex] || [];\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewBlockComponentMap);\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div class=\"tpl:flex tpl:gap-0\">\n <div\n v-for=\"(_, colIndex) in columns\"\n :key=\"colIndex\"\n :style=\"{ width: columnWidths[colIndex] }\"\n >\n <div\n v-for=\"childBlock in getColumnBlocks(colIndex)\"\n :key=\"childBlock.id\"\n :style=\"getBlockWrapperStyle(childBlock)\"\n >\n <!-- Recursive self-reference for nested sections -->\n <PreviewSectionBlock\n v-if=\"childBlock.type === 'section'\"\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n <component\n :is=\"getBlockComponent(childBlock)\"\n v-else\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ButtonBlock from \"./ButtonBlock.vue\";\nimport CustomBlock from \"./CustomBlock.vue\";\nimport DividerBlock from \"./DividerBlock.vue\";\nimport HtmlBlock from \"./HtmlBlock.vue\";\nimport ImageBlock from \"./ImageBlock.vue\";\nimport MenuBlock from \"./MenuBlock.vue\";\nimport SocialIconsBlock from \"./SocialIconsBlock.vue\";\nimport SpacerBlock from \"./SpacerBlock.vue\";\nimport TableBlock from \"./TableBlock.vue\";\nimport TitleBlock from \"./TitleBlock.vue\";\nimport ParagraphBlock from \"./ParagraphBlock.vue\";\nimport VideoBlock from \"./VideoBlock.vue\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n} from \"../../utils/blockComponentResolver\";\nimport type {\n Block,\n SectionBlock as SectionBlockType,\n} from \"@templatical/types\";\nimport { computed, inject, type Component } from \"vue\";\nimport { BLOCK_REGISTRY_KEY } from \"../../keys\";\n\nconst previewBlockComponentMap: Record<string, Component> = {\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n block: SectionBlockType;\n viewport: \"desktop\";\n}>();\n\ndefineOptions({ name: \"PreviewSectionBlock\" });\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst columnWidths = computed(() => {\n switch (props.block.columns) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n return [\"33.33%\", \"33.33%\", \"33.33%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n});\n\nconst columns = computed(() => {\n const count = columnWidths.value.length;\n const children = [...props.block.children];\n while (children.length < count) {\n children.push([]);\n }\n return children.slice(0, count);\n});\n\nfunction getColumnBlocks(colIndex: number): Block[] {\n return columns.value[colIndex] || [];\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewBlockComponentMap);\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div class=\"tpl:flex tpl:gap-0\">\n <div\n v-for=\"(_, colIndex) in columns\"\n :key=\"colIndex\"\n :style=\"{ width: columnWidths[colIndex] }\"\n >\n <div\n v-for=\"childBlock in getColumnBlocks(colIndex)\"\n :key=\"childBlock.id\"\n :style=\"getBlockWrapperStyle(childBlock)\"\n >\n <!-- Recursive self-reference for nested sections -->\n <PreviewSectionBlock\n v-if=\"childBlock.type === 'section'\"\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n <component\n :is=\"getBlockComponent(childBlock)\"\n v-else\n :block=\"childBlock\"\n viewport=\"desktop\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport PreviewSectionBlock from \"./blocks/PreviewSectionBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\nimport {\n BLOCK_REGISTRY_KEY,\n CONDITION_PREVIEW_KEY,\n EDITOR_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../keys\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n getDocumentStyle,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport type { Block, ViewportSize } from \"@templatical/types\";\nimport { computed, inject, provide, type Component } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n blocks: Block[];\n /**\n * Which viewport to render. Defaults to `desktop`, so the saved-blocks\n * surfaces — which have no viewport control — behave exactly as before.\n * The test-email dialog passes the user's choice so responsive blocks show\n * the variant a recipient on that device would receive.\n */\n viewport?: ViewportSize;\n /**\n * Whether the hand-toggled display-condition filter still applies here.\n * `false` where a resolver owns the preview: it has already evaluated every\n * condition against real data, so layering a manual hide on top would veto\n * the answer that was asked for.\n *\n * A prop rather than an injected `PREVIEW_RESOLUTION_KEY` read, because the\n * test-email dialog builds its *own* resolution instance (keyed to the\n * selected recipient) — injecting would consult the shared canvas instance\n * and get the wrong answer.\n */\n applyConditionFilter?: boolean;\n }>(),\n { viewport: \"desktop\", applyConditionFilter: true },\n);\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY);\nconst editor = inject(EDITOR_KEY, null);\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\n\n/**\n * This canvas is only ever a preview — nothing here is editable — so it honours\n * the user's mode directly, with no `previewMode` gate of the kind `Canvas.vue`\n * needs. Re-provided rather than left to inherit, because a `BlockPreviewCanvas`\n * inside a dialog may sit under an editing `Canvas` that provides `false`.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(() => mergeTagSampleMode?.value ?? false),\n);\n\n/**\n * Blocks a display condition currently excludes, so a preview never shows\n * content the recipient won't get. The canvas does the same via\n * `v-show=\"!conditionPreview?.isHidden(block.id)\"`.\n *\n * A no-op for the saved-blocks browser: entries there carry their *stored* ids,\n * which aren't in the current template, so nothing is tracked as hidden. It\n * bites where it should — the save dialog and the test-email preview, both of\n * which show blocks that are live on the canvas.\n *\n * Skipped entirely when `applyConditionFilter` is false — see the prop.\n */\nconst visibleBlocks = computed(() =>\n props.applyConditionFilter\n ? props.blocks.filter((block) => !conditionPreview?.isHidden(block.id))\n : props.blocks,\n);\n\n/**\n * Frame width, from the one helper the canvas and the scaled preview rows also\n * use — so all three agree by construction rather than by three constants that\n * happen to match. A template with a custom body width now previews at that\n * width instead of a flat 600.\n */\nconst frameWidth = computed(() =>\n getEmailFrameWidth(editor?.content.value.settings, props.viewport),\n);\n\n/**\n * The same document-level style the canvas applies, so a previewed block\n * renders identically to the canvas one: without it the font falls back to the\n * editor UI's and the link rules hit their unset defaults, dropping the\n * underline and the link colour.\n *\n * Read from the *current* template even in the saved-blocks browser, where the\n * previewed content came from elsewhere — a saved block stores only `Block[]`,\n * and the current settings are what it will actually look like once inserted.\n */\nconst documentStyle = computed(() =>\n editor ? getDocumentStyle(editor.content.value.settings) : {},\n);\n\nconst previewComponentMap: Record<string, Component> = {\n section: PreviewSectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewComponentMap);\n}\n</script>\n\n<template>\n <div\n data-testid=\"block-preview-canvas\"\n class=\"tpl:pointer-events-none tpl:mx-auto tpl:select-none tpl:rounded-lg\"\n :style=\"{\n width: `${frameWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n backgroundColor: 'var(--tpl-canvas-bg)',\n boxShadow: 'var(--tpl-shadow-sm)',\n ...documentStyle,\n }\"\n >\n <div\n v-for=\"block in visibleBlocks\"\n :key=\"block.id\"\n :style=\"getBlockWrapperStyle(block)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport PreviewSectionBlock from \"./blocks/PreviewSectionBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\nimport {\n BLOCK_REGISTRY_KEY,\n CONDITION_PREVIEW_KEY,\n EDITOR_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../keys\";\nimport {\n resolveBlockComponent,\n getBlockWrapperStyle,\n getDocumentStyle,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport type { Block, ViewportSize } from \"@templatical/types\";\nimport { computed, inject, provide, type Component } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n blocks: Block[];\n /**\n * Which viewport to render. Defaults to `desktop`, so the saved-blocks\n * surfaces — which have no viewport control — behave exactly as before.\n * The test-email dialog passes the user's choice so responsive blocks show\n * the variant a recipient on that device would receive.\n */\n viewport?: ViewportSize;\n /**\n * Whether the hand-toggled display-condition filter still applies here.\n * `false` where a resolver owns the preview: it has already evaluated every\n * condition against real data, so layering a manual hide on top would veto\n * the answer that was asked for.\n *\n * A prop rather than an injected `PREVIEW_RESOLUTION_KEY` read, because the\n * test-email dialog builds its *own* resolution instance (keyed to the\n * selected recipient) — injecting would consult the shared canvas instance\n * and get the wrong answer.\n */\n applyConditionFilter?: boolean;\n }>(),\n { viewport: \"desktop\", applyConditionFilter: true },\n);\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY);\nconst editor = inject(EDITOR_KEY, null);\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\n\n/**\n * This canvas is only ever a preview — nothing here is editable — so it honours\n * the user's mode directly, with no `previewMode` gate of the kind `Canvas.vue`\n * needs. Re-provided rather than left to inherit, because a `BlockPreviewCanvas`\n * inside a dialog may sit under an editing `Canvas` that provides `false`.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(() => mergeTagSampleMode?.value ?? false),\n);\n\n/**\n * Blocks a display condition currently excludes, so a preview never shows\n * content the recipient won't get. The canvas does the same via\n * `v-show=\"!conditionPreview?.isHidden(block.id)\"`.\n *\n * A no-op for the saved-blocks browser: entries there carry their *stored* ids,\n * which aren't in the current template, so nothing is tracked as hidden. It\n * bites where it should — the save dialog and the test-email preview, both of\n * which show blocks that are live on the canvas.\n *\n * Skipped entirely when `applyConditionFilter` is false — see the prop.\n */\nconst visibleBlocks = computed(() =>\n props.applyConditionFilter\n ? props.blocks.filter((block) => !conditionPreview?.isHidden(block.id))\n : props.blocks,\n);\n\n/**\n * Frame width, from the one helper the canvas and the scaled preview rows also\n * use — so all three agree by construction rather than by three constants that\n * happen to match. A template with a custom body width now previews at that\n * width instead of a flat 600.\n */\nconst frameWidth = computed(() =>\n getEmailFrameWidth(editor?.content.value.settings, props.viewport),\n);\n\n/**\n * The same document-level style the canvas applies, so a previewed block\n * renders identically to the canvas one: without it the font falls back to the\n * editor UI's and the link rules hit their unset defaults, dropping the\n * underline and the link colour.\n *\n * Read from the *current* template even in the saved-blocks browser, where the\n * previewed content came from elsewhere — a saved block stores only `Block[]`,\n * and the current settings are what it will actually look like once inserted.\n */\nconst documentStyle = computed(() =>\n editor ? getDocumentStyle(editor.content.value.settings) : {},\n);\n\nconst previewComponentMap: Record<string, Component> = {\n section: PreviewSectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n video: VideoBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n custom: CustomBlock,\n};\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, previewComponentMap);\n}\n</script>\n\n<template>\n <div\n data-testid=\"block-preview-canvas\"\n class=\"tpl:pointer-events-none tpl:mx-auto tpl:select-none tpl:rounded-lg\"\n :style=\"{\n width: `${frameWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n backgroundColor: 'var(--tpl-canvas-bg)',\n boxShadow: 'var(--tpl-shadow-sm)',\n ...documentStyle,\n }\"\n >\n <div\n v-for=\"block in visibleBlocks\"\n :key=\"block.id\"\n :style=\"getBlockWrapperStyle(block)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n />\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { H as e, N as t, S as n, at as r, b as i, ct as a, g as o, h as s, l as c, m as l, n as u, p as d, u as f, ut as p, x as m, y as h } from "./draggable-BfpsXFg7.js";
|
|
2
|
-
import { E as g } from "./icons-
|
|
2
|
+
import { E as g } from "./icons-Dl5bcXmT.js";
|
|
3
3
|
import { n as _ } from "./useCloudI18n-Ch-SiRjU.js";
|
|
4
4
|
//#region src/cloud/components/CloudHeaderExtras.vue?vue&type=script&setup=true&lang.ts
|
|
5
5
|
var v = {
|
|
@@ -16,7 +16,7 @@ var v = {
|
|
|
16
16
|
cloud: {}
|
|
17
17
|
},
|
|
18
18
|
setup(n) {
|
|
19
|
-
let x = m(() => import("./CollaboratorBar-
|
|
19
|
+
let x = m(() => import("./CollaboratorBar-BH1lM_Ta.js")), S = m(() => import("./AiFeatureMenu-BhtNkpIq.js")), { t: C, format: w } = _();
|
|
20
20
|
return (m, _) => n.part === "left" && n.cloud.featureFlags.templateLimit.value !== null ? (t(), o("span", v, p(r(w)(r(C).header.templatesUsed, {
|
|
21
21
|
used: n.cloud.featureFlags.templateCount.value,
|
|
22
22
|
max: n.cloud.featureFlags.templateLimit.value
|
|
@@ -57,4 +57,4 @@ var v = {
|
|
|
57
57
|
//#endregion
|
|
58
58
|
export { x as default };
|
|
59
59
|
|
|
60
|
-
//# sourceMappingURL=CloudHeaderExtras-
|
|
60
|
+
//# sourceMappingURL=CloudHeaderExtras-CScGpeLl.js.map
|
package/dist/cdn/chunks/{CloudHeaderExtras-Ctat5U0M.js.map → CloudHeaderExtras-CScGpeLl.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CloudHeaderExtras-Ctat5U0M.js","names":[],"sources":["../../../src/cloud/components/CloudHeaderExtras.vue","../../../src/cloud/components/CloudHeaderExtras.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { Sparkles } from \"@lucide/vue\";\n\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { CloudAttachment } from \"../runtime\";\n\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./CollaboratorBar.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(() => import(\"./AiFeatureMenu.vue\"));\n\n/**\n * The Cloud-only header controls, filled into `EditorHeader`'s three\n * `*-extras` slots.\n *\n * One component with a `part` prop rather than three: the three regions share\n * this file's lazy chunk, so a Cloud session pays one round-trip for all of them\n * and an OSS session pays none.\n *\n * The plan-usage readout and the AI button live here rather than in the shared\n * header because they render **cloud-chunk** strings; OSS translators never see\n * them. Comments are deliberately not here: that feature is shared and\n * provider-backed, so its trigger and its strings belong to the shared header and\n * the OSS chunk.\n */\ndefineProps<{\n part: \"left\" | \"center\" | \"right\";\n cloud: CloudAttachment;\n}>();\n\nconst { t: cloudT, format: cloudFormat } = useCloudI18nStrict();\n</script>\n\n<template>\n <!-- Left: templates used / allowed. A plan *limit*, not a templates concern,\n which is why it sits here and its string stays in the cloud chunk. -->\n <span\n v-if=\"part === 'left' && cloud.featureFlags.templateLimit.value !== null\"\n data-testid=\"cloud-template-usage\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n cloudFormat(cloudT.header.templatesUsed, {\n used: cloud.featureFlags.templateCount.value,\n max: cloud.featureFlags.templateLimit.value,\n })\n }}\n </span>\n\n <!-- Center: who else is in the template right now. -->\n <CollaboratorBar\n v-else-if=\"\n part === 'center' &&\n cloud.collaboration &&\n cloud.isCollaborationEnabled.value\n \"\n :collaborators=\"cloud.collaboration.collaborators.value\"\n :is-connected=\"cloud.websocket.isConnected.value\"\n />\n\n <!-- Right: AI — after the shared header's Comments button and before the\n test-email button, so the shared header is a superset of the OSS one rather\n than a rearrangement. -->\n <template v-else-if=\"part === 'right'\">\n <div\n v-if=\"\n cloud.featureFlags.canUseAiGeneration.value &&\n cloud.featureFlags.hasTemplateSaved.value\n \"\n :ref=\"\n (el) => (cloud.panelState.aiMenuRef.value = el as HTMLElement | null)\n \"\n class=\"tpl:relative\"\n >\n <button\n type=\"button\"\n data-testid=\"ai-menu-trigger\"\n :aria-expanded=\"cloud.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 cloud.panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"cloud.panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ cloudT.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all 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=\"cloud.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=\"cloud.panelState.activeAiFeature.value\"\n @select=\"cloud.panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n </template>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { Sparkles } from \"@lucide/vue\";\n\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { CloudAttachment } from \"../runtime\";\n\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./CollaboratorBar.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(() => import(\"./AiFeatureMenu.vue\"));\n\n/**\n * The Cloud-only header controls, filled into `EditorHeader`'s three\n * `*-extras` slots.\n *\n * One component with a `part` prop rather than three: the three regions share\n * this file's lazy chunk, so a Cloud session pays one round-trip for all of them\n * and an OSS session pays none.\n *\n * The plan-usage readout and the AI button live here rather than in the shared\n * header because they render **cloud-chunk** strings; OSS translators never see\n * them. Comments are deliberately not here: that feature is shared and\n * provider-backed, so its trigger and its strings belong to the shared header and\n * the OSS chunk.\n */\ndefineProps<{\n part: \"left\" | \"center\" | \"right\";\n cloud: CloudAttachment;\n}>();\n\nconst { t: cloudT, format: cloudFormat } = useCloudI18nStrict();\n</script>\n\n<template>\n <!-- Left: templates used / allowed. A plan *limit*, not a templates concern,\n which is why it sits here and its string stays in the cloud chunk. -->\n <span\n v-if=\"part === 'left' && cloud.featureFlags.templateLimit.value !== null\"\n data-testid=\"cloud-template-usage\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n cloudFormat(cloudT.header.templatesUsed, {\n used: cloud.featureFlags.templateCount.value,\n max: cloud.featureFlags.templateLimit.value,\n })\n }}\n </span>\n\n <!-- Center: who else is in the template right now. -->\n <CollaboratorBar\n v-else-if=\"\n part === 'center' &&\n cloud.collaboration &&\n cloud.isCollaborationEnabled.value\n \"\n :collaborators=\"cloud.collaboration.collaborators.value\"\n :is-connected=\"cloud.websocket.isConnected.value\"\n />\n\n <!-- Right: AI — after the shared header's Comments button and before the\n test-email button, so the shared header is a superset of the OSS one rather\n than a rearrangement. -->\n <template v-else-if=\"part === 'right'\">\n <div\n v-if=\"\n cloud.featureFlags.canUseAiGeneration.value &&\n cloud.featureFlags.hasTemplateSaved.value\n \"\n :ref=\"\n (el) => (cloud.panelState.aiMenuRef.value = el as HTMLElement | null)\n \"\n class=\"tpl:relative\"\n >\n <button\n type=\"button\"\n data-testid=\"ai-menu-trigger\"\n :aria-expanded=\"cloud.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 cloud.panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"cloud.panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ cloudT.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all 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=\"cloud.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=\"cloud.panelState.activeAiFeature.value\"\n @select=\"cloud.panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n </template>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;EAOA,IAAM,IAAkB,QAChB,OAAO,gCACf,GACM,IAAgB,QAA2B,OAAO,8BAAsB,GAqBxE,EAAE,GAAG,GAAQ,QAAQ,MAAgB,EAAmB;mBAOpD,EAAA,SAAI,UAAe,EAAA,MAAM,aAAa,cAAc,UAAK,QADjE,EAAA,GAAA,EAWO,QAXP,GAWO,EALH,EAAA,CAAA,CAAW,CAAC,EAAA,CAAA,CAAM,CAAC,OAAO,eAAa;GAAkB,MAAA,EAAA,MAAM,aAAa,cAAc;GAAoB,KAAA,EAAA,MAAM,aAAa,cAAc;EAS/H,CAAA,CAAA,GAAA,CAAA,KAAA,EAAA,SAAI,YAAuB,EAAA,MAAM,iBAAuB,EAAA,MAAM,uBAAuB,SADzG,EAAA,GAAA,EAQE,EAAA,CAAA,GAAA;;GAFC,eAAe,EAAA,MAAM,cAAc,cAAc;GACjD,gBAAc,EAAA,MAAM,UAAU,YAAY;EAMxB,GAAA,MAAA,GAAA,CAAA,iBAAA,cAAA,CAAA,KAAA,EAAA,SAAI,WAAzB,EAAA,GAAA,EA6CW,GAAA,EAAA,KAAA,EAAA,GAAA,CA3CQ,EAAA,MAAM,aAAa,mBAAmB,SAAiB,EAAA,MAAM,aAAa,iBAAiB,SAD5G,EAAA,GAAA,EA2CM,OAAA;;GAtCH,MAAe,MAAQ,EAAA,MAAM,WAAW,UAAU,QAAQ;GAG3D,OAAM;EAEN,GAAA,CAAA,EAcS,UAAA;GAbP,MAAK;GACL,eAAY;GACX,iBAAe,EAAA,MAAM,WAAW,WAAW;GAC5C,OAAK,EAAA,CAAC,wNACa,EAAA,MAAM,WAAW,eAAe,QAAA,uBAAA,kBAAA,CAAA;GAKlD,SAAK,AAAA,EAAA,OAAA,GAAO,GAAA,MAAA,EAAA,MAAM,WAAW,gBAAjB,EAAA,MAAM,WAAW,aAAY,GAAA,CAAA,GAAA,CAAA,MAAA,CAAA;EAE1C,GAAA,CAAA,EAAiE,EAAA,CAAA,GAAA;GAAtD,MAAM;GAAK,gBAAc;GAAG,OAAM;EAAoB,CAAA,GAAA,EAAA,MACjE,EAAG,EAAA,CAAA,CAAM,CAAC,OAAO,MAAM,GAAA,CAAA,CAAA,GAAA,IAAA,CAAA,GAEzB,EAiBa,GAAA;GAhBX,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;GACf,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;;GAEf,SAAA,QAQM,CAPE,EAAA,MAAM,WAAW,WAAW,SADpC,EAAA,GAAA,EAQM,OARN,GAQM,CAJJ,EAGE,EAAA,CAAA,GAAA;IAFC,kBAAgB,EAAA,MAAM,WAAW,gBAAgB;IACjD,UAAQ,EAAA,MAAM,WAAW"}
|
|
1
|
+
{"version":3,"file":"CloudHeaderExtras-CScGpeLl.js","names":[],"sources":["../../../src/cloud/components/CloudHeaderExtras.vue","../../../src/cloud/components/CloudHeaderExtras.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { Sparkles } from \"@lucide/vue\";\n\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { CloudAttachment } from \"../runtime\";\n\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./CollaboratorBar.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(() => import(\"./AiFeatureMenu.vue\"));\n\n/**\n * The Cloud-only header controls, filled into `EditorHeader`'s three\n * `*-extras` slots.\n *\n * One component with a `part` prop rather than three: the three regions share\n * this file's lazy chunk, so a Cloud session pays one round-trip for all of them\n * and an OSS session pays none.\n *\n * The plan-usage readout and the AI button live here rather than in the shared\n * header because they render **cloud-chunk** strings; OSS translators never see\n * them. Comments are deliberately not here: that feature is shared and\n * provider-backed, so its trigger and its strings belong to the shared header and\n * the OSS chunk.\n */\ndefineProps<{\n part: \"left\" | \"center\" | \"right\";\n cloud: CloudAttachment;\n}>();\n\nconst { t: cloudT, format: cloudFormat } = useCloudI18nStrict();\n</script>\n\n<template>\n <!-- Left: templates used / allowed. A plan *limit*, not a templates concern,\n which is why it sits here and its string stays in the cloud chunk. -->\n <span\n v-if=\"part === 'left' && cloud.featureFlags.templateLimit.value !== null\"\n data-testid=\"cloud-template-usage\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n cloudFormat(cloudT.header.templatesUsed, {\n used: cloud.featureFlags.templateCount.value,\n max: cloud.featureFlags.templateLimit.value,\n })\n }}\n </span>\n\n <!-- Center: who else is in the template right now. -->\n <CollaboratorBar\n v-else-if=\"\n part === 'center' &&\n cloud.collaboration &&\n cloud.isCollaborationEnabled.value\n \"\n :collaborators=\"cloud.collaboration.collaborators.value\"\n :is-connected=\"cloud.websocket.isConnected.value\"\n />\n\n <!-- Right: AI — after the shared header's Comments button and before the\n test-email button, so the shared header is a superset of the OSS one rather\n than a rearrangement. -->\n <template v-else-if=\"part === 'right'\">\n <div\n v-if=\"\n cloud.featureFlags.canUseAiGeneration.value &&\n cloud.featureFlags.hasTemplateSaved.value\n \"\n :ref=\"\n (el) => (cloud.panelState.aiMenuRef.value = el as HTMLElement | null)\n \"\n class=\"tpl:relative\"\n >\n <button\n type=\"button\"\n data-testid=\"ai-menu-trigger\"\n :aria-expanded=\"cloud.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 cloud.panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"cloud.panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ cloudT.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all 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=\"cloud.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=\"cloud.panelState.activeAiFeature.value\"\n @select=\"cloud.panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n </template>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { Sparkles } from \"@lucide/vue\";\n\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { CloudAttachment } from \"../runtime\";\n\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./CollaboratorBar.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(() => import(\"./AiFeatureMenu.vue\"));\n\n/**\n * The Cloud-only header controls, filled into `EditorHeader`'s three\n * `*-extras` slots.\n *\n * One component with a `part` prop rather than three: the three regions share\n * this file's lazy chunk, so a Cloud session pays one round-trip for all of them\n * and an OSS session pays none.\n *\n * The plan-usage readout and the AI button live here rather than in the shared\n * header because they render **cloud-chunk** strings; OSS translators never see\n * them. Comments are deliberately not here: that feature is shared and\n * provider-backed, so its trigger and its strings belong to the shared header and\n * the OSS chunk.\n */\ndefineProps<{\n part: \"left\" | \"center\" | \"right\";\n cloud: CloudAttachment;\n}>();\n\nconst { t: cloudT, format: cloudFormat } = useCloudI18nStrict();\n</script>\n\n<template>\n <!-- Left: templates used / allowed. A plan *limit*, not a templates concern,\n which is why it sits here and its string stays in the cloud chunk. -->\n <span\n v-if=\"part === 'left' && cloud.featureFlags.templateLimit.value !== null\"\n data-testid=\"cloud-template-usage\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n cloudFormat(cloudT.header.templatesUsed, {\n used: cloud.featureFlags.templateCount.value,\n max: cloud.featureFlags.templateLimit.value,\n })\n }}\n </span>\n\n <!-- Center: who else is in the template right now. -->\n <CollaboratorBar\n v-else-if=\"\n part === 'center' &&\n cloud.collaboration &&\n cloud.isCollaborationEnabled.value\n \"\n :collaborators=\"cloud.collaboration.collaborators.value\"\n :is-connected=\"cloud.websocket.isConnected.value\"\n />\n\n <!-- Right: AI — after the shared header's Comments button and before the\n test-email button, so the shared header is a superset of the OSS one rather\n than a rearrangement. -->\n <template v-else-if=\"part === 'right'\">\n <div\n v-if=\"\n cloud.featureFlags.canUseAiGeneration.value &&\n cloud.featureFlags.hasTemplateSaved.value\n \"\n :ref=\"\n (el) => (cloud.panelState.aiMenuRef.value = el as HTMLElement | null)\n \"\n class=\"tpl:relative\"\n >\n <button\n type=\"button\"\n data-testid=\"ai-menu-trigger\"\n :aria-expanded=\"cloud.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 cloud.panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"cloud.panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ cloudT.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all 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=\"cloud.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=\"cloud.panelState.activeAiFeature.value\"\n @select=\"cloud.panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n </template>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;EAOA,IAAM,IAAkB,QAChB,OAAO,gCACf,GACM,IAAgB,QAA2B,OAAO,8BAAsB,GAqBxE,EAAE,GAAG,GAAQ,QAAQ,MAAgB,EAAmB;mBAOpD,EAAA,SAAI,UAAe,EAAA,MAAM,aAAa,cAAc,UAAK,QADjE,EAAA,GAAA,EAWO,QAXP,GAWO,EALH,EAAA,CAAA,CAAW,CAAC,EAAA,CAAA,CAAM,CAAC,OAAO,eAAa;GAAkB,MAAA,EAAA,MAAM,aAAa,cAAc;GAAoB,KAAA,EAAA,MAAM,aAAa,cAAc;EAS/H,CAAA,CAAA,GAAA,CAAA,KAAA,EAAA,SAAI,YAAuB,EAAA,MAAM,iBAAuB,EAAA,MAAM,uBAAuB,SADzG,EAAA,GAAA,EAQE,EAAA,CAAA,GAAA;;GAFC,eAAe,EAAA,MAAM,cAAc,cAAc;GACjD,gBAAc,EAAA,MAAM,UAAU,YAAY;EAMxB,GAAA,MAAA,GAAA,CAAA,iBAAA,cAAA,CAAA,KAAA,EAAA,SAAI,WAAzB,EAAA,GAAA,EA6CW,GAAA,EAAA,KAAA,EAAA,GAAA,CA3CQ,EAAA,MAAM,aAAa,mBAAmB,SAAiB,EAAA,MAAM,aAAa,iBAAiB,SAD5G,EAAA,GAAA,EA2CM,OAAA;;GAtCH,MAAe,MAAQ,EAAA,MAAM,WAAW,UAAU,QAAQ;GAG3D,OAAM;EAEN,GAAA,CAAA,EAcS,UAAA;GAbP,MAAK;GACL,eAAY;GACX,iBAAe,EAAA,MAAM,WAAW,WAAW;GAC5C,OAAK,EAAA,CAAC,wNACa,EAAA,MAAM,WAAW,eAAe,QAAA,uBAAA,kBAAA,CAAA;GAKlD,SAAK,AAAA,EAAA,OAAA,GAAO,GAAA,MAAA,EAAA,MAAM,WAAW,gBAAjB,EAAA,MAAM,WAAW,aAAY,GAAA,CAAA,GAAA,CAAA,MAAA,CAAA;EAE1C,GAAA,CAAA,EAAiE,EAAA,CAAA,GAAA;GAAtD,MAAM;GAAK,gBAAc;GAAG,OAAM;EAAoB,CAAA,GAAA,EAAA,MACjE,EAAG,EAAA,CAAA,CAAM,CAAC,OAAO,MAAM,GAAA,CAAA,CAAA,GAAA,IAAA,CAAA,GAEzB,EAiBa,GAAA;GAhBX,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;GACf,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;;GAEf,SAAA,QAQM,CAPE,EAAA,MAAM,WAAW,WAAW,SADpC,EAAA,GAAA,EAQM,OARN,GAQM,CAJJ,EAGE,EAAA,CAAA,GAAA;IAFC,kBAAgB,EAAA,MAAM,WAAW,gBAAgB;IACjD,UAAQ,EAAA,MAAM,WAAW"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { F as e, H as t, N as n, S as r, at as i, b as a, g as o, h as s, l as c, m as l, n as u, p as d, u as f, ut as p, v as m, x as h } from "./draggable-BfpsXFg7.js";
|
|
2
2
|
import { t as g } from "./useI18n-4sUbTM3Y.js";
|
|
3
|
-
import { Mt as _, u as v } from "./icons-
|
|
3
|
+
import { Mt as _, u as v } from "./icons-Dl5bcXmT.js";
|
|
4
4
|
import { n as y } from "./useCloudI18n-Ch-SiRjU.js";
|
|
5
5
|
//#region src/cloud/components/CloudErrorOverlay.vue?vue&type=script&setup=true&lang.ts
|
|
6
6
|
var b = {
|
|
@@ -122,9 +122,9 @@ var b = {
|
|
|
122
122
|
locale: {}
|
|
123
123
|
},
|
|
124
124
|
setup(e) {
|
|
125
|
-
let r = h(() => import("./AiChatSidebar-
|
|
125
|
+
let r = h(() => import("./AiChatSidebar-tZqbhmuJ.js")), s = h(() => import("./DesignReferenceSidebar-npLxDRJI.js")), c = h(() => import("./TemplateScoringPanel-MvdrkgqL.js")), l = h(async () => {
|
|
126
126
|
try {
|
|
127
|
-
return (await import("./src-
|
|
127
|
+
return (await import("./src-BDUi-9js.js")).MediaLibraryModal;
|
|
128
128
|
} catch {
|
|
129
129
|
throw Error("[Templatical] Cloud media library requires the optional peer dependency '@templatical/media-library'. Please install it.");
|
|
130
130
|
}
|
|
@@ -229,4 +229,4 @@ var b = {
|
|
|
229
229
|
//#endregion
|
|
230
230
|
export { H as default };
|
|
231
231
|
|
|
232
|
-
//# sourceMappingURL=CloudPanels-
|
|
232
|
+
//# sourceMappingURL=CloudPanels-DVtJkWsj.js.map
|