@templatical/editor 0.30.0 → 0.31.0
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-Bq99wU01.js → AiChatSidebar-DYDtzTDd.js} +7 -7
- package/dist/{AiFeatureMenu-F-C59LAV.js → AiFeatureMenu-BJ882XCi.js} +4 -4
- package/dist/{BlockIssueBadge-Dhk7Hv4m.js → BlockIssueBadge-D6y8vQpL.js} +2 -2
- package/dist/{BlockPreviewCanvas-CqU1kwmG.js → BlockPreviewCanvas-DQNxLy7u.js} +1 -1
- package/dist/{CloudHeaderExtras-vnGV_mLD.js → CloudHeaderExtras-CqG3RaKw.js} +2 -2
- package/dist/{CloudPanels-Dv-MD1Tc.js → CloudPanels-B6xrk7kN.js} +3 -3
- package/dist/{CollaboratorBar-DmssyJoB.js → CollaboratorBar-by16gXQH.js} +2 -2
- package/dist/{ColorPicker-CXPSbnRD.js → ColorPicker-E5gqlCZE.js} +1 -1
- package/dist/{CommentsPanel-DnWdgQHg.js → CommentsPanel-B_kzvTHR.js} +1 -1
- package/dist/{CommentsSidebar-ZOqmuo4D.js → CommentsSidebar-gfzUX7cd.js} +10 -10
- package/dist/{CountdownToolbar-SwmE7CIk.js → CountdownToolbar-DU8UHx9A.js} +1 -1
- package/dist/{DesignReferenceSidebar-9cLTzOqP.js → DesignReferenceSidebar-DgndMB4H.js} +5 -5
- package/dist/{IssuesPanel-CDk7lUna.js → IssuesPanel-DmJTb_SZ.js} +5 -5
- package/dist/{MergeTagModeToggle-DtD_Xl9w.js → MergeTagModeToggle-CZnJ2Gz_.js} +2 -2
- package/dist/{MergeTagSuggestion-DPfXtDzP.js → MergeTagSuggestion-DyUcY_9e.js} +2 -2
- package/dist/{ParagraphEditor-C0NbqDmV.js → ParagraphEditor-CnB4kXle.js} +19 -19
- package/dist/{RestoreVersionDialog-DCX0rJiV.js → RestoreVersionDialog-BpOphPiE.js} +3 -3
- package/dist/{RichTextEditorContent-DZ6GiAUi.js → RichTextEditorContent-ChPNgHQ_.js} +3 -3
- package/dist/{SaveBlockDialog-e5LtQRVk.js → SaveBlockDialog-CKw4Va6j.js} +6 -6
- package/dist/{SavedBlocksBrowserModal-ZQIQoARO.js → SavedBlocksBrowserModal-SST-7EMK.js} +7 -7
- package/dist/{SavedBlocksPanels-pGZWjSmp.js → SavedBlocksPanels-ChV8bOgM.js} +1 -1
- package/dist/{SavedBlocksPickBar-OZaOG1Si.js → SavedBlocksPickBar-BbEeD5RB.js} +2 -2
- package/dist/{TemplateScoringPanel-CDwlSFEe.js → TemplateScoringPanel-De8BZpRU.js} +10 -10
- package/dist/{TestEmailModal-PVa1VL2p.js → TestEmailModal-4d2pXeHU.js} +6 -6
- package/dist/{TestEmailPanel-RekBdAtO.js → TestEmailPanel-DEgCesNP.js} +1 -1
- package/dist/{TitleEditor-Cs2tELCL.js → TitleEditor-89pmKLVY.js} +8 -8
- package/dist/{TplModal-Bw1i8_eE.js → TplModal-DswccJRJ.js} +1 -1
- package/dist/{VersionHistoryMenu-B6zAcmAC.js → VersionHistoryMenu-DBxSJR1k.js} +4 -4
- package/dist/{VersionHistoryPanels-TqTC4ofA.js → VersionHistoryPanels-CFLHDH9P.js} +1 -1
- package/dist/{VersionPreviewBanner-CQlSzQ2B.js → VersionPreviewBanner-DhG5RcRt.js} +1 -1
- package/dist/{blockTypeIcons-C7WedEOG.js → blockTypeIcons-dpKs5GQc.js} +2 -2
- package/dist/bundle-stats.json +6 -6
- package/dist/cdn/chunks/{AiChatSidebar-DmZ1Nvxo.js → AiChatSidebar-DmYDRmvX.js} +5 -5
- package/dist/cdn/chunks/{AiChatSidebar-DmZ1Nvxo.js.map → AiChatSidebar-DmYDRmvX.js.map} +1 -1
- package/dist/cdn/chunks/{AiFeatureMenu-D4tax9j3.js → AiFeatureMenu-C0Rt08qb.js} +3 -3
- package/dist/cdn/chunks/{AiFeatureMenu-D4tax9j3.js.map → AiFeatureMenu-C0Rt08qb.js.map} +1 -1
- package/dist/cdn/chunks/{BlockIssueBadge-Bry71Wjz.js → BlockIssueBadge-BBog2CMy.js} +2 -2
- package/dist/cdn/chunks/{BlockIssueBadge-Bry71Wjz.js.map → BlockIssueBadge-BBog2CMy.js.map} +1 -1
- package/dist/cdn/chunks/{BlockPreviewCanvas-DDOqFJlT.js → BlockPreviewCanvas-DNBjBKBI.js} +2 -2
- package/dist/cdn/chunks/{BlockPreviewCanvas-DDOqFJlT.js.map → BlockPreviewCanvas-DNBjBKBI.js.map} +1 -1
- package/dist/cdn/chunks/{CloudHeaderExtras-g-_SFPna.js → CloudHeaderExtras-Ditfv7y9.js} +3 -3
- package/dist/cdn/chunks/{CloudHeaderExtras-g-_SFPna.js.map → CloudHeaderExtras-Ditfv7y9.js.map} +1 -1
- package/dist/cdn/chunks/{CloudPanels-Cnh07_2G.js → CloudPanels-C4U5bJO6.js} +4 -4
- package/dist/cdn/chunks/{CloudPanels-Cnh07_2G.js.map → CloudPanels-C4U5bJO6.js.map} +1 -1
- package/dist/cdn/chunks/{CollaboratorBar-CLGB0prG.js → CollaboratorBar-B79JR9NH.js} +3 -3
- package/dist/cdn/chunks/{CollaboratorBar-CLGB0prG.js.map → CollaboratorBar-B79JR9NH.js.map} +1 -1
- package/dist/cdn/chunks/{ColorPicker-Ww4xHHMI.js → ColorPicker-C-ZiwUV3.js} +2 -2
- package/dist/cdn/chunks/{ColorPicker-Ww4xHHMI.js.map → ColorPicker-C-ZiwUV3.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsPanel-DjId-XH-.js → CommentsPanel-D1Ke8PbO.js} +2 -2
- package/dist/cdn/chunks/{CommentsPanel-DjId-XH-.js.map → CommentsPanel-D1Ke8PbO.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsSidebar-DXXGWSr4.js → CommentsSidebar-B-UyhIZN.js} +2 -2
- package/dist/cdn/chunks/{CommentsSidebar-DXXGWSr4.js.map → CommentsSidebar-B-UyhIZN.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownToolbar-CO6xLgYc.js → CountdownToolbar-DyOHOu44.js} +2 -2
- package/dist/cdn/chunks/{CountdownToolbar-CO6xLgYc.js.map → CountdownToolbar-DyOHOu44.js.map} +1 -1
- package/dist/cdn/chunks/{DesignReferenceSidebar-DRGqm33h.js → DesignReferenceSidebar-BCCX1FtP.js} +2 -2
- package/dist/cdn/chunks/{DesignReferenceSidebar-DRGqm33h.js.map → DesignReferenceSidebar-BCCX1FtP.js.map} +1 -1
- package/dist/cdn/chunks/{IssuesPanel-iC2hgrXx.js → IssuesPanel-3vD3ImHF.js} +2 -2
- package/dist/cdn/chunks/{IssuesPanel-iC2hgrXx.js.map → IssuesPanel-3vD3ImHF.js.map} +1 -1
- package/dist/cdn/chunks/{MergeTagModeToggle-DZtnCj0F.js → MergeTagModeToggle-BU0SDRow.js} +2 -2
- package/dist/cdn/chunks/{MergeTagModeToggle-DZtnCj0F.js.map → MergeTagModeToggle-BU0SDRow.js.map} +1 -1
- package/dist/cdn/chunks/{MergeTagSuggestion-VP_nC-8W.js → MergeTagSuggestion-B9vL0VaG.js} +2 -2
- package/dist/cdn/chunks/{MergeTagSuggestion-VP_nC-8W.js.map → MergeTagSuggestion-B9vL0VaG.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-BalgLjBt.js → ParagraphEditor-CSwIuvoF.js} +22 -22
- package/dist/cdn/chunks/{ParagraphEditor-BalgLjBt.js.map → ParagraphEditor-CSwIuvoF.js.map} +1 -1
- package/dist/cdn/chunks/{RestoreVersionDialog-DGOXQ0ou.js → RestoreVersionDialog-Bvg2WvJN.js} +3 -3
- package/dist/cdn/chunks/{RestoreVersionDialog-DGOXQ0ou.js.map → RestoreVersionDialog-Bvg2WvJN.js.map} +1 -1
- package/dist/cdn/chunks/{RichTextEditorContent-DpvOKKH-.js → RichTextEditorContent-DdkmEEpK.js} +3 -3
- package/dist/cdn/chunks/{RichTextEditorContent-DpvOKKH-.js.map → RichTextEditorContent-DdkmEEpK.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-Dod3jX9X.js → SaveBlockDialog-D-QO3LuR.js} +5 -5
- package/dist/cdn/chunks/{SaveBlockDialog-Dod3jX9X.js.map → SaveBlockDialog-D-QO3LuR.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-BysGB8zb.js → SavedBlocksBrowserModal-BRdN56r7.js} +6 -6
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-BysGB8zb.js.map → SavedBlocksBrowserModal-BRdN56r7.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPanels-DJ4fceEM.js → SavedBlocksPanels-D9GTlLgb.js} +2 -2
- package/dist/cdn/chunks/{SavedBlocksPanels-DJ4fceEM.js.map → SavedBlocksPanels-D9GTlLgb.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPickBar-DGgbqAsb.js → SavedBlocksPickBar-DSRcUeIo.js} +3 -3
- package/dist/cdn/chunks/{SavedBlocksPickBar-DGgbqAsb.js.map → SavedBlocksPickBar-DSRcUeIo.js.map} +1 -1
- package/dist/cdn/chunks/{TemplateScoringPanel-B0YcaJsR.js → TemplateScoringPanel-Dy5oOVhB.js} +2 -2
- package/dist/cdn/chunks/{TemplateScoringPanel-B0YcaJsR.js.map → TemplateScoringPanel-Dy5oOVhB.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailModal-DvZFnhWa.js → TestEmailModal-B4L2lmIV.js} +6 -6
- package/dist/cdn/chunks/{TestEmailModal-DvZFnhWa.js.map → TestEmailModal-B4L2lmIV.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailPanel-BdxpMiWc.js → TestEmailPanel-cTPgEf-D.js} +2 -2
- package/dist/cdn/chunks/{TestEmailPanel-BdxpMiWc.js.map → TestEmailPanel-cTPgEf-D.js.map} +1 -1
- package/dist/cdn/chunks/{TitleEditor-CfHWnThg.js → TitleEditor-BKjL2Zs1.js} +8 -8
- package/dist/cdn/chunks/{TitleEditor-CfHWnThg.js.map → TitleEditor-BKjL2Zs1.js.map} +1 -1
- package/dist/cdn/chunks/{TplModal-BMTW5bEN.js → TplModal-DxrgNmt9.js} +2 -2
- package/dist/cdn/chunks/{TplModal-BMTW5bEN.js.map → TplModal-DxrgNmt9.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryMenu-DJGcA_RX.js → VersionHistoryMenu-CMqZFUHE.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryMenu-DJGcA_RX.js.map → VersionHistoryMenu-CMqZFUHE.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryPanels-2l_FLtix.js → VersionHistoryPanels-CxIFBJsz.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryPanels-2l_FLtix.js.map → VersionHistoryPanels-CxIFBJsz.js.map} +1 -1
- package/dist/cdn/chunks/{VersionPreviewBanner-C8pfES5A.js → VersionPreviewBanner-CdsZvVYF.js} +2 -2
- package/dist/cdn/chunks/{VersionPreviewBanner-C8pfES5A.js.map → VersionPreviewBanner-CdsZvVYF.js.map} +1 -1
- package/dist/cdn/chunks/{blockTypeIcons-JUwZtKaq.js → blockTypeIcons-DXPd_hd1.js} +2 -2
- package/dist/cdn/chunks/{blockTypeIcons-JUwZtKaq.js.map → blockTypeIcons-DXPd_hd1.js.map} +1 -1
- package/dist/cdn/chunks/{createCloudRuntime-jLhuyKds.js → createCloudRuntime-DwIXvrb0.js} +2 -2
- package/dist/cdn/chunks/{createCloudRuntime-jLhuyKds.js.map → createCloudRuntime-DwIXvrb0.js.map} +1 -1
- package/dist/cdn/chunks/en-BzFt4b80.js +2 -0
- package/dist/cdn/chunks/{extensions-D30V0hzj.js → extensions-C87Z6_23.js} +3 -3
- package/dist/cdn/chunks/{extensions-D30V0hzj.js.map → extensions-C87Z6_23.js.map} +1 -1
- package/dist/cdn/chunks/{icons-BTdLXRXn.js → icons-ciivb5BS.js} +10 -10
- package/dist/cdn/chunks/{icons-BTdLXRXn.js.map → icons-ciivb5BS.js.map} +1 -1
- package/dist/cdn/chunks/{src-65H3RNcb.js → src-Bid3cmHa.js} +3 -3
- package/dist/cdn/chunks/{src-65H3RNcb.js.map → src-Bid3cmHa.js.map} +1 -1
- package/dist/cdn/chunks/{src-CLTXM4PM.js → src-Cf0ohOog.js} +2 -2
- package/dist/cdn/chunks/{src-CLTXM4PM.js.map → src-Cf0ohOog.js.map} +1 -1
- package/dist/cdn/chunks/{tiptap-Bqy-H6CT.js → tiptap-Ey0FpaX7.js} +1710 -1700
- package/dist/cdn/chunks/tiptap-Ey0FpaX7.js.map +1 -0
- package/dist/cdn/chunks/{useEditorCore-BQeEwEQ9.js → useEditorCore-CTNB2Mkl.js} +5 -5
- package/dist/cdn/chunks/{useEditorCore-BQeEwEQ9.js.map → useEditorCore-CTNB2Mkl.js.map} +1 -1
- package/dist/cdn/editor.js +15 -15
- package/dist/{check-CTe6VejT.js → check-80E6ZGin.js} +1 -1
- package/dist/{chevron-down-BBThVMu-.js → chevron-down-BmkLsn2c.js} +1 -1
- package/dist/{chevron-up-iECwf-Bc.js → chevron-up-DJSAzlUU.js} +1 -1
- package/dist/{circle-alert-Z_eeykrO.js → circle-alert-BsCfZryY.js} +1 -1
- package/dist/{clock-CEj1c29j.js → clock-BcLIngSK.js} +1 -1
- package/dist/{createLucideIcon-DphAT3tw.js → createLucideIcon-eeetEgBb.js} +2 -2
- package/dist/{dist-nmzPlqlz.js → dist-BC1y_U2m.js} +151 -141
- package/dist/{dist-gWM8H2hz.js → dist-BFcAowKZ.js} +2 -2
- package/dist/dist-BQqIUwvB.js +5 -0
- package/dist/{dist-DLaIQ5H5.js → dist-BkAVGSRr.js} +6 -6
- package/dist/{dist-BfcqryVg.js → dist-CQ9T1yi-.js} +2 -2
- package/dist/{dist-CaR5TLiM.js → dist-CWC5t5J3.js} +2 -2
- package/dist/{dist-D2P8U3p6.js → dist-Cd0LvUlh.js} +2 -2
- package/dist/{dist-DSgmnQ-f.js → dist-CvW1nNzy.js} +2 -2
- package/dist/{dist-9fjN9cuD.js → dist-D8MekYFl.js} +2 -2
- package/dist/{dist-DifptC-U.js → dist-Do83S47T.js} +2 -2
- package/dist/{dist-BdIwIlKz.js → dist-prgdllHG.js} +2 -2
- package/dist/dist-waDcdb1l.js +5 -0
- package/dist/{extensions-Bjsdx6Vm.js → extensions-BYImwYeO.js} +3 -3
- package/dist/{image-up-W1INxCu8.js → image-up-BfxY85ar.js} +1 -1
- package/dist/{info-5t4WYKfh.js → info-LX4QFBr6.js} +1 -1
- package/dist/{loader-circle-DJCZ_JMY.js → loader-circle-Bwima0v8.js} +1 -1
- package/dist/{message-circle-Biu6lHh_.js → message-circle-Box-mf2k.js} +1 -1
- package/dist/{pencil-DkPlMca7.js → pencil-BM9gSq_J.js} +1 -1
- package/dist/{refresh-cw-DHbpkkol.js → refresh-cw-BuzFX0wH.js} +1 -1
- package/dist/{scan-line-DojwGzjU.js → scan-line-Duq1WXoG.js} +1 -1
- package/dist/{send-c6ppcE9I.js → send-DZ4nOnIA.js} +1 -1
- package/dist/{shield-check-F9T4ylNK.js → shield-check-Cz4BRYdv.js} +1 -1
- package/dist/{sparkles-D-0OiTZD.js → sparkles-DP8RqOeL.js} +1 -1
- package/dist/templatical-editor.js +29 -29
- package/dist/{text-align-start-kEEtoRrD.js → text-align-start-D7tXhjUP.js} +1 -1
- package/dist/{trash-2-DRQzhY9R.js → trash-2-BS7lZeyb.js} +1 -1
- package/dist/{triangle-alert-Bt4-GU3t.js → triangle-alert-Cx4mQGjx.js} +1 -1
- package/dist/{upload-D2wtjvH8.js → upload-L8tLVM10.js} +1 -1
- package/dist/{useEditorCore-Du9Vx-qr.js → useEditorCore-CtR46sbJ.js} +9 -9
- package/dist/{useScrollToBlock-B0Qpjfa4.js → useScrollToBlock-tndC8vKo.js} +1 -1
- package/dist/{x-YiG-TUGN.js → x-ChIQ7HwK.js} +1 -1
- package/package.json +22 -22
- package/dist/cdn/chunks/en-CwXyUb6-.js +0 -2
- package/dist/cdn/chunks/tiptap-Bqy-H6CT.js.map +0 -1
- package/dist/dist-CzMzshfS.js +0 -5
- package/dist/dist-DTVuNn8M.js +0 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DesignReferenceSidebar-DRGqm33h.js","names":[],"sources":["../../../src/cloud/components/DesignReferenceSidebar.vue","../../../src/cloud/components/DesignReferenceSidebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { MAX_UPLOAD_SIZE_BYTES } from \"../../constants/timeouts\";\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useDesignReference } from \"@templatical/core/cloud\";\nimport { EDITOR_KEY, AUTH_MANAGER_KEY, requireInject } from \"../../keys\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n FileImage,\n FileText,\n ImageUp,\n Upload,\n X,\n} from \"@lucide/vue\";\nimport { computed, onBeforeUnmount, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n hasExistingBlocks: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"apply\", content: TemplateContent): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"DesignReferenceSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"DesignReferenceSidebar\");\n\nconst designReference = useDesignReference({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: (content) => emit(\"apply\", content),\n});\n\nconst fileInput = ref<HTMLInputElement | null>(null);\n\ntype SourceTab = \"image\" | \"pdf\";\n\nconst activeTab = ref<SourceTab>(\"image\");\nconst selectedFile = ref<File | null>(null);\nconst prompt = ref(\"\");\nconst filePreviewUrl = ref<string | null>(null);\nconst showConfirmation = ref(false);\nconst isDragging = ref(false);\n\nconst canGenerate = computed(() => {\n if (designReference.isGenerating.value) {\n return false;\n }\n\n return selectedFile.value !== null;\n});\n\nfunction selectTab(tab: SourceTab): void {\n activeTab.value = tab;\n clearFile();\n}\n\nfunction handleFileSelect(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n if (file) {\n setFile(file);\n }\n // Reset input so same file can be selected again\n input.value = \"\";\n}\n\nfunction setFile(file: File): void {\n // Validate file size (10MB)\n if (file.size > MAX_UPLOAD_SIZE_BYTES) {\n designReference.error.value = cloudT.designReference.fileTooLarge;\n return;\n }\n\n // Validate file type\n if (activeTab.value === \"image\") {\n const validTypes = [\"image/png\", \"image/jpeg\", \"image/jpg\", \"image/webp\"];\n if (!validTypes.includes(file.type)) {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n } else if (activeTab.value === \"pdf\") {\n if (file.type !== \"application/pdf\") {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n }\n\n selectedFile.value = file;\n designReference.error.value = null;\n\n // Create preview URL for images\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n }\n if (file.type.startsWith(\"image/\")) {\n filePreviewUrl.value = URL.createObjectURL(file);\n } else {\n filePreviewUrl.value = null;\n }\n}\n\nfunction clearFile(): void {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n selectedFile.value = null;\n}\n\nfunction handleDragOver(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = true;\n}\n\nfunction handleDragLeave(): void {\n isDragging.value = false;\n}\n\nfunction handleDrop(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = false;\n\n const file = event.dataTransfer?.files?.[0];\n if (file) {\n setFile(file);\n }\n}\n\nfunction handleGenerate(): void {\n if (!canGenerate.value) {\n return;\n }\n\n // Show confirmation if there are existing blocks\n if (props.hasExistingBlocks && !showConfirmation.value) {\n showConfirmation.value = true;\n return;\n }\n\n showConfirmation.value = false;\n\n const input: {\n prompt?: string;\n imageUpload?: File;\n pdfUpload?: File;\n } = {};\n\n if (prompt.value.trim()) {\n input.prompt = prompt.value.trim();\n }\n\n if (activeTab.value === \"image\" && selectedFile.value) {\n input.imageUpload = selectedFile.value;\n } else if (activeTab.value === \"pdf\" && selectedFile.value) {\n input.pdfUpload = selectedFile.value;\n }\n\n designReference.generate(input);\n}\n\nfunction cancelConfirmation(): void {\n showConfirmation.value = false;\n}\n\n// Reset state when panel closes\nwatch(\n () => props.visible,\n (isVisible) => {\n if (!isVisible) {\n showConfirmation.value = false;\n }\n },\n);\n\nonBeforeUnmount(() => {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n});\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-design-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-design-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-design-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 <ImageUp :size=\"13\" :stroke-width=\"2\" />\n <span>{{ cloudT.designReference.title }}</span>\n </div>\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\n <!-- Content -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <!-- Loading state -->\n <div\n v-if=\"designReference.isGenerating.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 <div\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-center tpl:gap-3\"\n >\n <LoadingTrack />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudT.designReference.generating }}\n </p>\n </div>\n </div>\n\n <!-- Input form -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <!-- Source tabs -->\n <div\n class=\"tpl:flex tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'image' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'image'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'image' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('image')\"\n >\n <FileImage :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadImage }}\n </button>\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'pdf' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'pdf'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'pdf' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('pdf')\"\n >\n <FileText :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadPdf }}\n </button>\n </div>\n\n <!-- File upload area (image or pdf) -->\n <div>\n <!-- Preview -->\n <div v-if=\"selectedFile\" class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n class=\"tpl:relative tpl:overflow-hidden tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Image preview -->\n <img\n v-if=\"filePreviewUrl\"\n :src=\"filePreviewUrl\"\n :alt=\"selectedFile.name\"\n class=\"tpl:h-auto tpl:max-h-48 tpl:w-full tpl:object-contain\"\n />\n <!-- PDF preview (icon-based) -->\n <div\n v-else\n class=\"tpl:flex tpl:h-32 tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2\"\n >\n <FileText\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">\n {{ selectedFile.name }}\n </span>\n </div>\n <!-- Remove button -->\n <button\n class=\"tpl:absolute tpl:top-2 tpl:right-2 tpl:rounded-full tpl:p-1 tpl:transition-colors tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:shadow-[var(--tpl-shadow)]\"\n @click=\"clearFile\"\n >\n <X :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Drop zone -->\n <div\n v-else\n class=\"tpl-design-dropzone tpl:flex tpl:cursor-pointer tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border-2 tpl:border-dashed tpl:px-4 tpl:py-8 tpl:transition-colors\"\n :style=\"{\n borderColor: isDragging\n ? 'var(--tpl-primary)'\n : 'var(--tpl-border-light)',\n backgroundColor: isDragging\n ? 'var(--tpl-primary-light)'\n : 'var(--tpl-bg)',\n }\"\n @click=\"fileInput?.click()\"\n @dragover=\"handleDragOver\"\n @dragleave=\"handleDragLeave\"\n @drop=\"handleDrop\"\n >\n <Upload\n :size=\"24\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span\n class=\"tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.dropHint }}\n </span>\n <span\n class=\"tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n activeTab === \"image\"\n ? cloudT.designReference.acceptedImages\n : cloudT.designReference.acceptedPdf\n }}\n </span>\n </div>\n <input\n ref=\"fileInput\"\n type=\"file\"\n class=\"tpl:hidden\"\n :accept=\"\n activeTab === 'image'\n ? 'image/png,image/jpeg,image/webp'\n : 'application/pdf'\n \"\n @change=\"handleFileSelect\"\n />\n </div>\n\n <!-- Prompt textarea -->\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <label\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.promptLabel }}\n </label>\n <textarea\n v-model=\"prompt\"\n class=\"tpl:min-h-[72px] tpl:w-full tpl:resize-none tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:font-sans tpl:text-sm tpl:outline-none tpl:transition-colors tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :class=\"['tpl-design-prompt-input']\"\n :placeholder=\"cloudT.designReference.promptPlaceholder\"\n rows=\"3\"\n />\n </div>\n\n <!-- Confirmation overlay -->\n <div\n v-if=\"showConfirmation\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:px-3 tpl:py-3 tpl:bg-[var(--tpl-warning-light)] tpl:border tpl:border-[var(--tpl-warning)]\"\n >\n <p class=\"tpl:text-xs tpl:leading-snug tpl:text-[var(--tpl-text)]\">\n {{ cloudT.designReference.replaceWarning }}\n </p>\n <div class=\"tpl:flex tpl:gap-2\">\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"cancelConfirmation\"\n >\n {{ cloudT.designReference.replaceCancel }}\n </button>\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n @click=\"handleGenerate\"\n >\n {{ cloudT.designReference.replaceConfirm }}\n </button>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"designReference.error.value\"\n class=\"tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{ cloudT.designReference.error }}</span>\n </div>\n\n <!-- Generate button -->\n <button\n v-if=\"!showConfirmation\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"!canGenerate\"\n @click=\"handleGenerate\"\n >\n <ImageUp :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.designReference.generate }}\n </button>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:pt-1 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudT.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-design-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-prompt-input:focus {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-design-dropzone: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 { MAX_UPLOAD_SIZE_BYTES } from \"../../constants/timeouts\";\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useDesignReference } from \"@templatical/core/cloud\";\nimport { EDITOR_KEY, AUTH_MANAGER_KEY, requireInject } from \"../../keys\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n FileImage,\n FileText,\n ImageUp,\n Upload,\n X,\n} from \"@lucide/vue\";\nimport { computed, onBeforeUnmount, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n hasExistingBlocks: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"apply\", content: TemplateContent): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"DesignReferenceSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"DesignReferenceSidebar\");\n\nconst designReference = useDesignReference({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: (content) => emit(\"apply\", content),\n});\n\nconst fileInput = ref<HTMLInputElement | null>(null);\n\ntype SourceTab = \"image\" | \"pdf\";\n\nconst activeTab = ref<SourceTab>(\"image\");\nconst selectedFile = ref<File | null>(null);\nconst prompt = ref(\"\");\nconst filePreviewUrl = ref<string | null>(null);\nconst showConfirmation = ref(false);\nconst isDragging = ref(false);\n\nconst canGenerate = computed(() => {\n if (designReference.isGenerating.value) {\n return false;\n }\n\n return selectedFile.value !== null;\n});\n\nfunction selectTab(tab: SourceTab): void {\n activeTab.value = tab;\n clearFile();\n}\n\nfunction handleFileSelect(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n if (file) {\n setFile(file);\n }\n // Reset input so same file can be selected again\n input.value = \"\";\n}\n\nfunction setFile(file: File): void {\n // Validate file size (10MB)\n if (file.size > MAX_UPLOAD_SIZE_BYTES) {\n designReference.error.value = cloudT.designReference.fileTooLarge;\n return;\n }\n\n // Validate file type\n if (activeTab.value === \"image\") {\n const validTypes = [\"image/png\", \"image/jpeg\", \"image/jpg\", \"image/webp\"];\n if (!validTypes.includes(file.type)) {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n } else if (activeTab.value === \"pdf\") {\n if (file.type !== \"application/pdf\") {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n }\n\n selectedFile.value = file;\n designReference.error.value = null;\n\n // Create preview URL for images\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n }\n if (file.type.startsWith(\"image/\")) {\n filePreviewUrl.value = URL.createObjectURL(file);\n } else {\n filePreviewUrl.value = null;\n }\n}\n\nfunction clearFile(): void {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n selectedFile.value = null;\n}\n\nfunction handleDragOver(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = true;\n}\n\nfunction handleDragLeave(): void {\n isDragging.value = false;\n}\n\nfunction handleDrop(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = false;\n\n const file = event.dataTransfer?.files?.[0];\n if (file) {\n setFile(file);\n }\n}\n\nfunction handleGenerate(): void {\n if (!canGenerate.value) {\n return;\n }\n\n // Show confirmation if there are existing blocks\n if (props.hasExistingBlocks && !showConfirmation.value) {\n showConfirmation.value = true;\n return;\n }\n\n showConfirmation.value = false;\n\n const input: {\n prompt?: string;\n imageUpload?: File;\n pdfUpload?: File;\n } = {};\n\n if (prompt.value.trim()) {\n input.prompt = prompt.value.trim();\n }\n\n if (activeTab.value === \"image\" && selectedFile.value) {\n input.imageUpload = selectedFile.value;\n } else if (activeTab.value === \"pdf\" && selectedFile.value) {\n input.pdfUpload = selectedFile.value;\n }\n\n designReference.generate(input);\n}\n\nfunction cancelConfirmation(): void {\n showConfirmation.value = false;\n}\n\n// Reset state when panel closes\nwatch(\n () => props.visible,\n (isVisible) => {\n if (!isVisible) {\n showConfirmation.value = false;\n }\n },\n);\n\nonBeforeUnmount(() => {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n});\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-design-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-design-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-design-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 <ImageUp :size=\"13\" :stroke-width=\"2\" />\n <span>{{ cloudT.designReference.title }}</span>\n </div>\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\n <!-- Content -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <!-- Loading state -->\n <div\n v-if=\"designReference.isGenerating.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 <div\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-center tpl:gap-3\"\n >\n <LoadingTrack />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudT.designReference.generating }}\n </p>\n </div>\n </div>\n\n <!-- Input form -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <!-- Source tabs -->\n <div\n class=\"tpl:flex tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'image' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'image'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'image' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('image')\"\n >\n <FileImage :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadImage }}\n </button>\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'pdf' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'pdf'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'pdf' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('pdf')\"\n >\n <FileText :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadPdf }}\n </button>\n </div>\n\n <!-- File upload area (image or pdf) -->\n <div>\n <!-- Preview -->\n <div v-if=\"selectedFile\" class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n class=\"tpl:relative tpl:overflow-hidden tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Image preview -->\n <img\n v-if=\"filePreviewUrl\"\n :src=\"filePreviewUrl\"\n :alt=\"selectedFile.name\"\n class=\"tpl:h-auto tpl:max-h-48 tpl:w-full tpl:object-contain\"\n />\n <!-- PDF preview (icon-based) -->\n <div\n v-else\n class=\"tpl:flex tpl:h-32 tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2\"\n >\n <FileText\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">\n {{ selectedFile.name }}\n </span>\n </div>\n <!-- Remove button -->\n <button\n class=\"tpl:absolute tpl:top-2 tpl:right-2 tpl:rounded-full tpl:p-1 tpl:transition-colors tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:shadow-[var(--tpl-shadow)]\"\n @click=\"clearFile\"\n >\n <X :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Drop zone -->\n <div\n v-else\n class=\"tpl-design-dropzone tpl:flex tpl:cursor-pointer tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border-2 tpl:border-dashed tpl:px-4 tpl:py-8 tpl:transition-colors\"\n :style=\"{\n borderColor: isDragging\n ? 'var(--tpl-primary)'\n : 'var(--tpl-border-light)',\n backgroundColor: isDragging\n ? 'var(--tpl-primary-light)'\n : 'var(--tpl-bg)',\n }\"\n @click=\"fileInput?.click()\"\n @dragover=\"handleDragOver\"\n @dragleave=\"handleDragLeave\"\n @drop=\"handleDrop\"\n >\n <Upload\n :size=\"24\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span\n class=\"tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.dropHint }}\n </span>\n <span\n class=\"tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n activeTab === \"image\"\n ? cloudT.designReference.acceptedImages\n : cloudT.designReference.acceptedPdf\n }}\n </span>\n </div>\n <input\n ref=\"fileInput\"\n type=\"file\"\n class=\"tpl:hidden\"\n :accept=\"\n activeTab === 'image'\n ? 'image/png,image/jpeg,image/webp'\n : 'application/pdf'\n \"\n @change=\"handleFileSelect\"\n />\n </div>\n\n <!-- Prompt textarea -->\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <label\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.promptLabel }}\n </label>\n <textarea\n v-model=\"prompt\"\n class=\"tpl:min-h-[72px] tpl:w-full tpl:resize-none tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:font-sans tpl:text-sm tpl:outline-none tpl:transition-colors tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :class=\"['tpl-design-prompt-input']\"\n :placeholder=\"cloudT.designReference.promptPlaceholder\"\n rows=\"3\"\n />\n </div>\n\n <!-- Confirmation overlay -->\n <div\n v-if=\"showConfirmation\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:px-3 tpl:py-3 tpl:bg-[var(--tpl-warning-light)] tpl:border tpl:border-[var(--tpl-warning)]\"\n >\n <p class=\"tpl:text-xs tpl:leading-snug tpl:text-[var(--tpl-text)]\">\n {{ cloudT.designReference.replaceWarning }}\n </p>\n <div class=\"tpl:flex tpl:gap-2\">\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"cancelConfirmation\"\n >\n {{ cloudT.designReference.replaceCancel }}\n </button>\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n @click=\"handleGenerate\"\n >\n {{ cloudT.designReference.replaceConfirm }}\n </button>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"designReference.error.value\"\n class=\"tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{ cloudT.designReference.error }}</span>\n </div>\n\n <!-- Generate button -->\n <button\n v-if=\"!showConfirmation\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"!canGenerate\"\n @click=\"handleGenerate\"\n >\n <ImageUp :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.designReference.generate }}\n </button>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:pt-1 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudT.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-design-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-prompt-input:focus {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-design-dropzone: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
|
+
{"version":3,"file":"DesignReferenceSidebar-BCCX1FtP.js","names":[],"sources":["../../../src/cloud/components/DesignReferenceSidebar.vue","../../../src/cloud/components/DesignReferenceSidebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { MAX_UPLOAD_SIZE_BYTES } from \"../../constants/timeouts\";\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useDesignReference } from \"@templatical/core/cloud\";\nimport { EDITOR_KEY, AUTH_MANAGER_KEY, requireInject } from \"../../keys\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n FileImage,\n FileText,\n ImageUp,\n Upload,\n X,\n} from \"@lucide/vue\";\nimport { computed, onBeforeUnmount, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n hasExistingBlocks: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"apply\", content: TemplateContent): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"DesignReferenceSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"DesignReferenceSidebar\");\n\nconst designReference = useDesignReference({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: (content) => emit(\"apply\", content),\n});\n\nconst fileInput = ref<HTMLInputElement | null>(null);\n\ntype SourceTab = \"image\" | \"pdf\";\n\nconst activeTab = ref<SourceTab>(\"image\");\nconst selectedFile = ref<File | null>(null);\nconst prompt = ref(\"\");\nconst filePreviewUrl = ref<string | null>(null);\nconst showConfirmation = ref(false);\nconst isDragging = ref(false);\n\nconst canGenerate = computed(() => {\n if (designReference.isGenerating.value) {\n return false;\n }\n\n return selectedFile.value !== null;\n});\n\nfunction selectTab(tab: SourceTab): void {\n activeTab.value = tab;\n clearFile();\n}\n\nfunction handleFileSelect(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n if (file) {\n setFile(file);\n }\n // Reset input so same file can be selected again\n input.value = \"\";\n}\n\nfunction setFile(file: File): void {\n // Validate file size (10MB)\n if (file.size > MAX_UPLOAD_SIZE_BYTES) {\n designReference.error.value = cloudT.designReference.fileTooLarge;\n return;\n }\n\n // Validate file type\n if (activeTab.value === \"image\") {\n const validTypes = [\"image/png\", \"image/jpeg\", \"image/jpg\", \"image/webp\"];\n if (!validTypes.includes(file.type)) {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n } else if (activeTab.value === \"pdf\") {\n if (file.type !== \"application/pdf\") {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n }\n\n selectedFile.value = file;\n designReference.error.value = null;\n\n // Create preview URL for images\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n }\n if (file.type.startsWith(\"image/\")) {\n filePreviewUrl.value = URL.createObjectURL(file);\n } else {\n filePreviewUrl.value = null;\n }\n}\n\nfunction clearFile(): void {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n selectedFile.value = null;\n}\n\nfunction handleDragOver(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = true;\n}\n\nfunction handleDragLeave(): void {\n isDragging.value = false;\n}\n\nfunction handleDrop(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = false;\n\n const file = event.dataTransfer?.files?.[0];\n if (file) {\n setFile(file);\n }\n}\n\nfunction handleGenerate(): void {\n if (!canGenerate.value) {\n return;\n }\n\n // Show confirmation if there are existing blocks\n if (props.hasExistingBlocks && !showConfirmation.value) {\n showConfirmation.value = true;\n return;\n }\n\n showConfirmation.value = false;\n\n const input: {\n prompt?: string;\n imageUpload?: File;\n pdfUpload?: File;\n } = {};\n\n if (prompt.value.trim()) {\n input.prompt = prompt.value.trim();\n }\n\n if (activeTab.value === \"image\" && selectedFile.value) {\n input.imageUpload = selectedFile.value;\n } else if (activeTab.value === \"pdf\" && selectedFile.value) {\n input.pdfUpload = selectedFile.value;\n }\n\n designReference.generate(input);\n}\n\nfunction cancelConfirmation(): void {\n showConfirmation.value = false;\n}\n\n// Reset state when panel closes\nwatch(\n () => props.visible,\n (isVisible) => {\n if (!isVisible) {\n showConfirmation.value = false;\n }\n },\n);\n\nonBeforeUnmount(() => {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n});\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-design-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-design-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-design-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 <ImageUp :size=\"13\" :stroke-width=\"2\" />\n <span>{{ cloudT.designReference.title }}</span>\n </div>\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\n <!-- Content -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <!-- Loading state -->\n <div\n v-if=\"designReference.isGenerating.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 <div\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-center tpl:gap-3\"\n >\n <LoadingTrack />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudT.designReference.generating }}\n </p>\n </div>\n </div>\n\n <!-- Input form -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <!-- Source tabs -->\n <div\n class=\"tpl:flex tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'image' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'image'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'image' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('image')\"\n >\n <FileImage :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadImage }}\n </button>\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'pdf' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'pdf'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'pdf' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('pdf')\"\n >\n <FileText :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadPdf }}\n </button>\n </div>\n\n <!-- File upload area (image or pdf) -->\n <div>\n <!-- Preview -->\n <div v-if=\"selectedFile\" class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n class=\"tpl:relative tpl:overflow-hidden tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Image preview -->\n <img\n v-if=\"filePreviewUrl\"\n :src=\"filePreviewUrl\"\n :alt=\"selectedFile.name\"\n class=\"tpl:h-auto tpl:max-h-48 tpl:w-full tpl:object-contain\"\n />\n <!-- PDF preview (icon-based) -->\n <div\n v-else\n class=\"tpl:flex tpl:h-32 tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2\"\n >\n <FileText\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">\n {{ selectedFile.name }}\n </span>\n </div>\n <!-- Remove button -->\n <button\n class=\"tpl:absolute tpl:top-2 tpl:right-2 tpl:rounded-full tpl:p-1 tpl:transition-colors tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:shadow-[var(--tpl-shadow)]\"\n @click=\"clearFile\"\n >\n <X :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Drop zone -->\n <div\n v-else\n class=\"tpl-design-dropzone tpl:flex tpl:cursor-pointer tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border-2 tpl:border-dashed tpl:px-4 tpl:py-8 tpl:transition-colors\"\n :style=\"{\n borderColor: isDragging\n ? 'var(--tpl-primary)'\n : 'var(--tpl-border-light)',\n backgroundColor: isDragging\n ? 'var(--tpl-primary-light)'\n : 'var(--tpl-bg)',\n }\"\n @click=\"fileInput?.click()\"\n @dragover=\"handleDragOver\"\n @dragleave=\"handleDragLeave\"\n @drop=\"handleDrop\"\n >\n <Upload\n :size=\"24\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span\n class=\"tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.dropHint }}\n </span>\n <span\n class=\"tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n activeTab === \"image\"\n ? cloudT.designReference.acceptedImages\n : cloudT.designReference.acceptedPdf\n }}\n </span>\n </div>\n <input\n ref=\"fileInput\"\n type=\"file\"\n class=\"tpl:hidden\"\n :accept=\"\n activeTab === 'image'\n ? 'image/png,image/jpeg,image/webp'\n : 'application/pdf'\n \"\n @change=\"handleFileSelect\"\n />\n </div>\n\n <!-- Prompt textarea -->\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <label\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.promptLabel }}\n </label>\n <textarea\n v-model=\"prompt\"\n class=\"tpl:min-h-[72px] tpl:w-full tpl:resize-none tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:font-sans tpl:text-sm tpl:outline-none tpl:transition-colors tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :class=\"['tpl-design-prompt-input']\"\n :placeholder=\"cloudT.designReference.promptPlaceholder\"\n rows=\"3\"\n />\n </div>\n\n <!-- Confirmation overlay -->\n <div\n v-if=\"showConfirmation\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:px-3 tpl:py-3 tpl:bg-[var(--tpl-warning-light)] tpl:border tpl:border-[var(--tpl-warning)]\"\n >\n <p class=\"tpl:text-xs tpl:leading-snug tpl:text-[var(--tpl-text)]\">\n {{ cloudT.designReference.replaceWarning }}\n </p>\n <div class=\"tpl:flex tpl:gap-2\">\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"cancelConfirmation\"\n >\n {{ cloudT.designReference.replaceCancel }}\n </button>\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n @click=\"handleGenerate\"\n >\n {{ cloudT.designReference.replaceConfirm }}\n </button>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"designReference.error.value\"\n class=\"tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{ cloudT.designReference.error }}</span>\n </div>\n\n <!-- Generate button -->\n <button\n v-if=\"!showConfirmation\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"!canGenerate\"\n @click=\"handleGenerate\"\n >\n <ImageUp :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.designReference.generate }}\n </button>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:pt-1 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudT.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-design-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-prompt-input:focus {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-design-dropzone: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 { MAX_UPLOAD_SIZE_BYTES } from \"../../constants/timeouts\";\nimport LoadingTrack from \"../../components/LoadingTrack.vue\";\nimport { useDesignReference } from \"@templatical/core/cloud\";\nimport { EDITOR_KEY, AUTH_MANAGER_KEY, requireInject } from \"../../keys\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport {\n CircleAlert,\n FileImage,\n FileText,\n ImageUp,\n Upload,\n X,\n} from \"@lucide/vue\";\nimport { computed, onBeforeUnmount, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n hasExistingBlocks: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"apply\", content: TemplateContent): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"DesignReferenceSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"DesignReferenceSidebar\");\n\nconst designReference = useDesignReference({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n onApply: (content) => emit(\"apply\", content),\n});\n\nconst fileInput = ref<HTMLInputElement | null>(null);\n\ntype SourceTab = \"image\" | \"pdf\";\n\nconst activeTab = ref<SourceTab>(\"image\");\nconst selectedFile = ref<File | null>(null);\nconst prompt = ref(\"\");\nconst filePreviewUrl = ref<string | null>(null);\nconst showConfirmation = ref(false);\nconst isDragging = ref(false);\n\nconst canGenerate = computed(() => {\n if (designReference.isGenerating.value) {\n return false;\n }\n\n return selectedFile.value !== null;\n});\n\nfunction selectTab(tab: SourceTab): void {\n activeTab.value = tab;\n clearFile();\n}\n\nfunction handleFileSelect(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n if (file) {\n setFile(file);\n }\n // Reset input so same file can be selected again\n input.value = \"\";\n}\n\nfunction setFile(file: File): void {\n // Validate file size (10MB)\n if (file.size > MAX_UPLOAD_SIZE_BYTES) {\n designReference.error.value = cloudT.designReference.fileTooLarge;\n return;\n }\n\n // Validate file type\n if (activeTab.value === \"image\") {\n const validTypes = [\"image/png\", \"image/jpeg\", \"image/jpg\", \"image/webp\"];\n if (!validTypes.includes(file.type)) {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n } else if (activeTab.value === \"pdf\") {\n if (file.type !== \"application/pdf\") {\n designReference.error.value = cloudT.designReference.invalidFileType;\n return;\n }\n }\n\n selectedFile.value = file;\n designReference.error.value = null;\n\n // Create preview URL for images\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n }\n if (file.type.startsWith(\"image/\")) {\n filePreviewUrl.value = URL.createObjectURL(file);\n } else {\n filePreviewUrl.value = null;\n }\n}\n\nfunction clearFile(): void {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n selectedFile.value = null;\n}\n\nfunction handleDragOver(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = true;\n}\n\nfunction handleDragLeave(): void {\n isDragging.value = false;\n}\n\nfunction handleDrop(event: DragEvent): void {\n event.preventDefault();\n isDragging.value = false;\n\n const file = event.dataTransfer?.files?.[0];\n if (file) {\n setFile(file);\n }\n}\n\nfunction handleGenerate(): void {\n if (!canGenerate.value) {\n return;\n }\n\n // Show confirmation if there are existing blocks\n if (props.hasExistingBlocks && !showConfirmation.value) {\n showConfirmation.value = true;\n return;\n }\n\n showConfirmation.value = false;\n\n const input: {\n prompt?: string;\n imageUpload?: File;\n pdfUpload?: File;\n } = {};\n\n if (prompt.value.trim()) {\n input.prompt = prompt.value.trim();\n }\n\n if (activeTab.value === \"image\" && selectedFile.value) {\n input.imageUpload = selectedFile.value;\n } else if (activeTab.value === \"pdf\" && selectedFile.value) {\n input.pdfUpload = selectedFile.value;\n }\n\n designReference.generate(input);\n}\n\nfunction cancelConfirmation(): void {\n showConfirmation.value = false;\n}\n\n// Reset state when panel closes\nwatch(\n () => props.visible,\n (isVisible) => {\n if (!isVisible) {\n showConfirmation.value = false;\n }\n },\n);\n\nonBeforeUnmount(() => {\n if (filePreviewUrl.value) {\n URL.revokeObjectURL(filePreviewUrl.value);\n filePreviewUrl.value = null;\n }\n});\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-design-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-design-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-design-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 <ImageUp :size=\"13\" :stroke-width=\"2\" />\n <span>{{ cloudT.designReference.title }}</span>\n </div>\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\n <!-- Content -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <!-- Loading state -->\n <div\n v-if=\"designReference.isGenerating.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 <div\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-center tpl:gap-3\"\n >\n <LoadingTrack />\n <p class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudT.designReference.generating }}\n </p>\n </div>\n </div>\n\n <!-- Input form -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-4\">\n <!-- Source tabs -->\n <div\n class=\"tpl:flex tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'image' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'image'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'image' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('image')\"\n >\n <FileImage :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadImage }}\n </button>\n <button\n class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-2 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all\"\n :style=\"{\n backgroundColor:\n activeTab === 'pdf' ? 'var(--tpl-bg)' : 'transparent',\n color:\n activeTab === 'pdf'\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow: activeTab === 'pdf' ? 'var(--tpl-shadow)' : 'none',\n }\"\n @click=\"selectTab('pdf')\"\n >\n <FileText :size=\"12\" :stroke-width=\"2\" />\n {{ cloudT.designReference.uploadPdf }}\n </button>\n </div>\n\n <!-- File upload area (image or pdf) -->\n <div>\n <!-- Preview -->\n <div v-if=\"selectedFile\" class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n class=\"tpl:relative tpl:overflow-hidden tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Image preview -->\n <img\n v-if=\"filePreviewUrl\"\n :src=\"filePreviewUrl\"\n :alt=\"selectedFile.name\"\n class=\"tpl:h-auto tpl:max-h-48 tpl:w-full tpl:object-contain\"\n />\n <!-- PDF preview (icon-based) -->\n <div\n v-else\n class=\"tpl:flex tpl:h-32 tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2\"\n >\n <FileText\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">\n {{ selectedFile.name }}\n </span>\n </div>\n <!-- Remove button -->\n <button\n class=\"tpl:absolute tpl:top-2 tpl:right-2 tpl:rounded-full tpl:p-1 tpl:transition-colors tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:shadow-[var(--tpl-shadow)]\"\n @click=\"clearFile\"\n >\n <X :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Drop zone -->\n <div\n v-else\n class=\"tpl-design-dropzone tpl:flex tpl:cursor-pointer tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border-2 tpl:border-dashed tpl:px-4 tpl:py-8 tpl:transition-colors\"\n :style=\"{\n borderColor: isDragging\n ? 'var(--tpl-primary)'\n : 'var(--tpl-border-light)',\n backgroundColor: isDragging\n ? 'var(--tpl-primary-light)'\n : 'var(--tpl-bg)',\n }\"\n @click=\"fileInput?.click()\"\n @dragover=\"handleDragOver\"\n @dragleave=\"handleDragLeave\"\n @drop=\"handleDrop\"\n >\n <Upload\n :size=\"24\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span\n class=\"tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.dropHint }}\n </span>\n <span\n class=\"tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n activeTab === \"image\"\n ? cloudT.designReference.acceptedImages\n : cloudT.designReference.acceptedPdf\n }}\n </span>\n </div>\n <input\n ref=\"fileInput\"\n type=\"file\"\n class=\"tpl:hidden\"\n :accept=\"\n activeTab === 'image'\n ? 'image/png,image/jpeg,image/webp'\n : 'application/pdf'\n \"\n @change=\"handleFileSelect\"\n />\n </div>\n\n <!-- Prompt textarea -->\n <div class=\"tpl:flex tpl:flex-col tpl:gap-1.5\">\n <label\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ cloudT.designReference.promptLabel }}\n </label>\n <textarea\n v-model=\"prompt\"\n class=\"tpl:min-h-[72px] tpl:w-full tpl:resize-none tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-2 tpl:font-sans tpl:text-sm tpl:outline-none tpl:transition-colors tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :class=\"['tpl-design-prompt-input']\"\n :placeholder=\"cloudT.designReference.promptPlaceholder\"\n rows=\"3\"\n />\n </div>\n\n <!-- Confirmation overlay -->\n <div\n v-if=\"showConfirmation\"\n class=\"tpl:flex tpl:flex-col tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:px-3 tpl:py-3 tpl:bg-[var(--tpl-warning-light)] tpl:border tpl:border-[var(--tpl-warning)]\"\n >\n <p class=\"tpl:text-xs tpl:leading-snug tpl:text-[var(--tpl-text)]\">\n {{ cloudT.designReference.replaceWarning }}\n </p>\n <div class=\"tpl:flex tpl:gap-2\">\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"cancelConfirmation\"\n >\n {{ cloudT.designReference.replaceCancel }}\n </button>\n <button\n class=\"tpl:flex-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n @click=\"handleGenerate\"\n >\n {{ cloudT.designReference.replaceConfirm }}\n </button>\n </div>\n </div>\n\n <!-- Error message -->\n <div\n v-if=\"designReference.error.value\"\n class=\"tpl:flex tpl:items-start tpl:gap-2 tpl:rounded-lg tpl:px-3 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:mt-0.5 tpl:shrink-0\"\n />\n <span>{{ cloudT.designReference.error }}</span>\n </div>\n\n <!-- Generate button -->\n <button\n v-if=\"!showConfirmation\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-[var(--tpl-radius-sm)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"!canGenerate\"\n @click=\"handleGenerate\"\n >\n <ImageUp :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.designReference.generate }}\n </button>\n\n <!-- AI disclaimer -->\n <p\n class=\"tpl:m-0 tpl:pt-1 tpl:text-center tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ cloudT.aiMenu.disclaimer }}\n </p>\n </div>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-design-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-design-prompt-input:focus {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-design-dropzone: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 { E as e, F as t, N as n, R as r, S as i, at as a, b as o, ct as s, f as c, g as l, h as u, m as d, p as f, u as p, ut as m, y as h } from "./draggable-D2vo-aQI.js";
|
|
2
2
|
import { h as g, z as _ } from "./keys-BJ770Mwm.js";
|
|
3
3
|
import { t as v } from "./useI18n-DIhawILp.js";
|
|
4
|
-
import { Ht as y, Mt as b, dt as x, r as S, st as C, u as w } from "./icons-
|
|
4
|
+
import { Ht as y, Mt as b, dt as x, r as S, st as C, u as w } from "./icons-ciivb5BS.js";
|
|
5
5
|
import { t as T } from "./useScrollToBlock-KcSAuXKz.js";
|
|
6
6
|
//#region src/components/sidebar/IssuesPanel.vue?vue&type=script&setup=true&lang.ts
|
|
7
7
|
var E = { class: "tpl:flex tpl:flex-col tpl:gap-4 tpl:p-4" }, D = { class: "tpl:flex tpl:items-center tpl:gap-2" }, O = { class: "tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]" }, k = ["title"], A = {
|
|
@@ -97,4 +97,4 @@ var E = { class: "tpl:flex tpl:flex-col tpl:gap-4 tpl:p-4" }, D = { class: "tpl:
|
|
|
97
97
|
//#endregion
|
|
98
98
|
export { z as default };
|
|
99
99
|
|
|
100
|
-
//# sourceMappingURL=IssuesPanel-
|
|
100
|
+
//# sourceMappingURL=IssuesPanel-3vD3ImHF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IssuesPanel-iC2hgrXx.js","names":[],"sources":["../../../src/components/sidebar/IssuesPanel.vue","../../../src/components/sidebar/IssuesPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport {\n ListChecks,\n AlertCircle,\n AlertTriangle,\n Info,\n ArrowRight,\n Wrench,\n} from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useScrollToBlock } from \"../../composables/useScrollToBlock\";\nimport { TEMPLATE_LINT_KEY, EDITOR_KEY } from \"../../keys\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\nconst { t, format } = useI18n();\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst errors = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"error\"),\n);\nconst warnings = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"warning\"),\n);\nconst infos = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"info\"),\n);\n\nconst totalCount = computed(\n () => errors.value.length + warnings.value.length + infos.value.length,\n);\n\nfunction jumpTo(issue: LintIssue): void {\n if (!editor) return;\n if (!issue.blockId) return;\n editor.selectBlock(issue.blockId);\n // Selecting is invisible on its own — on a long template the block sits\n // below the fold and the canvas stays put, so \"Jump\" appears to do nothing.\n scrollToBlock(issue.blockId);\n}\n\nfunction applyFix(issue: LintIssue): void {\n lint?.applyFix(issue);\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-4 tpl:p-4\">\n <header class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <ListChecks :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.issues.panelTitle }}\n </h3>\n <span\n v-if=\"totalCount > 0\"\n class=\"tpl:ml-auto tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-0.5 tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n :title=\"format(t.issues.issueCountTooltip, { count: totalCount })\"\n >\n {{ totalCount }}\n </span>\n </header>\n\n <div\n v-if=\"!lint || lint.unavailable.value\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <!-- Quality package not available; render nothing visible beyond a hint. -->\n </div>\n\n <div\n v-else-if=\"totalCount === 0\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.issues.emptyState }}\n </div>\n\n <template v-else>\n <section\n v-for=\"group in [\n {\n key: 'errors',\n title: t.issues.groupErrors,\n icon: AlertCircle,\n items: errors,\n tone: 'tpl:text-[var(--tpl-danger)]',\n },\n {\n key: 'warnings',\n title: t.issues.groupWarnings,\n icon: AlertTriangle,\n items: warnings,\n tone: 'tpl:text-[var(--tpl-warning)]',\n },\n {\n key: 'info',\n title: t.issues.groupInfo,\n icon: Info,\n items: infos,\n tone: 'tpl:text-[var(--tpl-text-muted)]',\n },\n ]\"\n :key=\"group.key\"\n >\n <header\n v-if=\"group.items.length > 0\"\n class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-[11px] tpl:font-semibold tpl:uppercase tpl:tracking-wide\"\n :class=\"group.tone\"\n >\n <component :is=\"group.icon\" :size=\"12\" :stroke-width=\"2\" />\n {{ group.title }}\n <span class=\"tpl:ml-auto tpl:font-normal tpl:opacity-70\">\n {{ group.items.length }}\n </span>\n </header>\n <ul\n v-if=\"group.items.length > 0\"\n class=\"tpl:m-0 tpl:mb-3 tpl:flex tpl:list-none tpl:flex-col tpl:gap-2 tpl:p-0\"\n >\n <li\n v-for=\"issue in group.items\"\n :key=\"`${issue.ruleId}-${issue.blockId ?? 'template'}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <p class=\"tpl:m-0 tpl:mb-2 tpl:text-xs tpl:text-[var(--tpl-text)]\">\n {{ issue.message }}\n </p>\n <p\n class=\"tpl:m-0 tpl:mb-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ issue.ruleId }}\n </p>\n <div class=\"tpl:flex tpl:gap-1.5\">\n <button\n v-if=\"issue.blockId\"\n type=\"button\"\n data-testid=\"issue-jump\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text)]\"\n @click=\"jumpTo(issue)\"\n >\n <ArrowRight :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.jump }}\n </button>\n <button\n v-if=\"issue.fix\"\n type=\"button\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:bg-[var(--tpl-primary)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-on-primary)]\"\n :title=\"issue.fix.description\"\n @click=\"applyFix(issue)\"\n >\n <Wrench :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.fix }}\n </button>\n </div>\n </li>\n </ul>\n </section>\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport {\n ListChecks,\n AlertCircle,\n AlertTriangle,\n Info,\n ArrowRight,\n Wrench,\n} from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useScrollToBlock } from \"../../composables/useScrollToBlock\";\nimport { TEMPLATE_LINT_KEY, EDITOR_KEY } from \"../../keys\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\nconst { t, format } = useI18n();\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst errors = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"error\"),\n);\nconst warnings = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"warning\"),\n);\nconst infos = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"info\"),\n);\n\nconst totalCount = computed(\n () => errors.value.length + warnings.value.length + infos.value.length,\n);\n\nfunction jumpTo(issue: LintIssue): void {\n if (!editor) return;\n if (!issue.blockId) return;\n editor.selectBlock(issue.blockId);\n // Selecting is invisible on its own — on a long template the block sits\n // below the fold and the canvas stays put, so \"Jump\" appears to do nothing.\n scrollToBlock(issue.blockId);\n}\n\nfunction applyFix(issue: LintIssue): void {\n lint?.applyFix(issue);\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-4 tpl:p-4\">\n <header class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <ListChecks :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.issues.panelTitle }}\n </h3>\n <span\n v-if=\"totalCount > 0\"\n class=\"tpl:ml-auto tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-0.5 tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n :title=\"format(t.issues.issueCountTooltip, { count: totalCount })\"\n >\n {{ totalCount }}\n </span>\n </header>\n\n <div\n v-if=\"!lint || lint.unavailable.value\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <!-- Quality package not available; render nothing visible beyond a hint. -->\n </div>\n\n <div\n v-else-if=\"totalCount === 0\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.issues.emptyState }}\n </div>\n\n <template v-else>\n <section\n v-for=\"group in [\n {\n key: 'errors',\n title: t.issues.groupErrors,\n icon: AlertCircle,\n items: errors,\n tone: 'tpl:text-[var(--tpl-danger)]',\n },\n {\n key: 'warnings',\n title: t.issues.groupWarnings,\n icon: AlertTriangle,\n items: warnings,\n tone: 'tpl:text-[var(--tpl-warning)]',\n },\n {\n key: 'info',\n title: t.issues.groupInfo,\n icon: Info,\n items: infos,\n tone: 'tpl:text-[var(--tpl-text-muted)]',\n },\n ]\"\n :key=\"group.key\"\n >\n <header\n v-if=\"group.items.length > 0\"\n class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-[11px] tpl:font-semibold tpl:uppercase tpl:tracking-wide\"\n :class=\"group.tone\"\n >\n <component :is=\"group.icon\" :size=\"12\" :stroke-width=\"2\" />\n {{ group.title }}\n <span class=\"tpl:ml-auto tpl:font-normal tpl:opacity-70\">\n {{ group.items.length }}\n </span>\n </header>\n <ul\n v-if=\"group.items.length > 0\"\n class=\"tpl:m-0 tpl:mb-3 tpl:flex tpl:list-none tpl:flex-col tpl:gap-2 tpl:p-0\"\n >\n <li\n v-for=\"issue in group.items\"\n :key=\"`${issue.ruleId}-${issue.blockId ?? 'template'}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <p class=\"tpl:m-0 tpl:mb-2 tpl:text-xs tpl:text-[var(--tpl-text)]\">\n {{ issue.message }}\n </p>\n <p\n class=\"tpl:m-0 tpl:mb-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ issue.ruleId }}\n </p>\n <div class=\"tpl:flex tpl:gap-1.5\">\n <button\n v-if=\"issue.blockId\"\n type=\"button\"\n data-testid=\"issue-jump\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text)]\"\n @click=\"jumpTo(issue)\"\n >\n <ArrowRight :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.jump }}\n </button>\n <button\n v-if=\"issue.fix\"\n type=\"button\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:bg-[var(--tpl-primary)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-on-primary)]\"\n :title=\"issue.fix.description\"\n @click=\"applyFix(issue)\"\n >\n <Wrench :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.fix }}\n </button>\n </div>\n </li>\n </ul>\n </section>\n </template>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;EAeA,IAAM,EAAE,MAAG,cAAW,EAAQ,GACxB,IAAO,EAAO,GAAmB,IAAI,GACrC,IAAS,EAAO,GAAY,IAAI,GAChC,IAAgB,EAAiB,GAEjC,IAAS,SACZ,GAAM,OAAO,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,EAAE,aAAa,OAAO,CACjE,GACM,IAAW,SACd,GAAM,OAAO,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,EAAE,aAAa,SAAS,CACnE,GACM,IAAQ,SACX,GAAM,OAAO,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,EAAE,aAAa,MAAM,CAChE,GAEM,IAAa,QACX,EAAO,MAAM,SAAS,EAAS,MAAM,SAAS,EAAM,MAAM,MAClE;EAEA,SAAS,EAAO,GAAwB;GACjC,KACA,EAAM,YACX,EAAO,YAAY,EAAM,OAAO,GAGhC,EAAc,EAAM,OAAO;EAC7B;EAEA,SAAS,EAAS,GAAwB;GACxC,GAAM,SAAS,CAAK;EACtB;oBAIE,EAAA,GAAA,EAgHM,OAhHN,GAgHM,CA/GJ,EAcS,UAdT,GAcS;GAbP,EAA6C,EAAA,CAAA,GAAA;IAAhC,MAAM;IAAK,gBAAc;;GACtC,EAIK,MAJL,GAIK,EADA,EAAA,CAAA,CAAC,CAAC,OAAO,UAAU,GAAA,CAAA;GAGhB,EAAA,QAAU,KADlB,EAAA,GAAA,EAMO,QAAA;;IAJL,OAAM;IACL,OAAO,EAAA,CAAA,CAAM,CAAC,EAAA,CAAA,CAAC,CAAC,OAAO,mBAAiB,EAAA,OAAW,EAAA,MAAU,CAAA;GAE3D,GAAA,EAAA,EAAA,KAAU,GAAA,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA;EAKR,CAAA,GAAA,CAAA,EAAA,CAAA,KAAQ,EAAA,CAAA,CAAI,CAAC,YAAY,SADlC,EAAA,GAAA,EAKM,OALN,CAKM,KAGO,EAAA,UAAU,KADvB,EAAA,GAAA,EAKM,OALN,GAKM,EADD,EAAA,CAAA,CAAC,CAAC,OAAO,UAAU,GAAA,CAAA,MAItB,EAAA,EAAA,GAAA,EA8EU,GAAA,EAAA,KAAA,EAAA,GAAA,EAAA;;;IA7EoE,OAAA,EAAA,CAAA,CAAC,CAAC,OAAO;IAA+B,MAAA,EAAA,CAAA;IAAgC,OAAA,EAAA;;;;;IAAmI,OAAA,EAAA,CAAA,CAAC,CAAC,OAAO;IAAiC,MAAA,EAAA,CAAA;IAAkC,OAAA,EAAA;;;;;IAAkI,OAAA,EAAA,CAAA,CAAC,CAAC,OAAO;IAA6B,MAAA,EAAA,CAAA;IAAyB,OAAA,EAAA;;;EAA7hB,IAAA,OADT,EAAA,GAAA,EA8EU,WAAA,EAtDP,KAAK,EAAM,IAAA,GAAA,CAGJ,EAAM,MAAM,SAAM,KAD1B,EAAA,GAAA,EAUS,UAAA;;GARP,OAAK,EAAA,CAAC,oHACE,EAAM,IAAI,CAAA;;IAElB,EAAA,GAAA,EAA2D,EAA3C,EAAM,IAAI,GAAA;IAAG,MAAM;IAAK,gBAAc;;GAAK,EAAA,MAC3D,EAAG,EAAM,KAAK,IAAG,KACjB,CAAA;GAAA,EAEO,QAFP,GAEO,EADF,EAAM,MAAM,MAAM,GAAA,CAAA;EAIjB,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAAA,EAAM,MAAM,SAAM,KAD1B,EAAA,GAAA,EAwCK,MAxCL,GAwCK,EApCH,EAAA,EAAA,GAAA,EAmCK,GAAA,MAAA,EAlCa,EAAM,QAAf,OADT,EAAA,GAAA,EAmCK,MAAA;GAjCF,KAAG,GAAK,EAAM,OAAM,GAAI,EAAM,WAAO;GACtC,OAAM;;GAEN,EAEI,KAFJ,GAEI,EADC,EAAM,OAAO,GAAA,CAAA;GAElB,EAII,KAJJ,GAII,EADC,EAAM,MAAM,GAAA,CAAA;GAEjB,EAqBM,OArBN,GAqBM,CAnBI,EAAM,WADd,EAAA,GAAA,EASS,UAAA;;IAPP,MAAK;IACL,eAAY;IACZ,OAAM;IACL,UAAK,MAAE,EAAO,CAAK;GAEpB,GAAA,CAAA,EAA2C,EAAA,CAAA,GAAA;IAA9B,MAAM;IAAK,gBAAc;GAAK,CAAA,GAAA,EAAA,MAC3C,EAAG,EAAA,CAAA,CAAC,CAAC,OAAO,IAAI,GAAA,CAAA,CAAA,GAAA,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAGV,EAAM,OADd,EAAA,GAAA,EASS,UAAA;;IAPP,MAAK;IACL,OAAM;IACL,OAAO,EAAM,IAAI;IACjB,UAAK,MAAE,EAAS,CAAK;GAEtB,GAAA,CAAA,EAAuC,EAAA,CAAA,GAAA;IAA9B,MAAM;IAAK,gBAAc;GAAK,CAAA,GAAA,EAAA,MACvC,EAAG,EAAA,CAAA,CAAC,CAAC,OAAO,GAAG,GAAA,CAAA,CAAA,GAAA,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"IssuesPanel-3vD3ImHF.js","names":[],"sources":["../../../src/components/sidebar/IssuesPanel.vue","../../../src/components/sidebar/IssuesPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport {\n ListChecks,\n AlertCircle,\n AlertTriangle,\n Info,\n ArrowRight,\n Wrench,\n} from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useScrollToBlock } from \"../../composables/useScrollToBlock\";\nimport { TEMPLATE_LINT_KEY, EDITOR_KEY } from \"../../keys\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\nconst { t, format } = useI18n();\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst errors = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"error\"),\n);\nconst warnings = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"warning\"),\n);\nconst infos = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"info\"),\n);\n\nconst totalCount = computed(\n () => errors.value.length + warnings.value.length + infos.value.length,\n);\n\nfunction jumpTo(issue: LintIssue): void {\n if (!editor) return;\n if (!issue.blockId) return;\n editor.selectBlock(issue.blockId);\n // Selecting is invisible on its own — on a long template the block sits\n // below the fold and the canvas stays put, so \"Jump\" appears to do nothing.\n scrollToBlock(issue.blockId);\n}\n\nfunction applyFix(issue: LintIssue): void {\n lint?.applyFix(issue);\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-4 tpl:p-4\">\n <header class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <ListChecks :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.issues.panelTitle }}\n </h3>\n <span\n v-if=\"totalCount > 0\"\n class=\"tpl:ml-auto tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-0.5 tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n :title=\"format(t.issues.issueCountTooltip, { count: totalCount })\"\n >\n {{ totalCount }}\n </span>\n </header>\n\n <div\n v-if=\"!lint || lint.unavailable.value\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <!-- Quality package not available; render nothing visible beyond a hint. -->\n </div>\n\n <div\n v-else-if=\"totalCount === 0\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.issues.emptyState }}\n </div>\n\n <template v-else>\n <section\n v-for=\"group in [\n {\n key: 'errors',\n title: t.issues.groupErrors,\n icon: AlertCircle,\n items: errors,\n tone: 'tpl:text-[var(--tpl-danger)]',\n },\n {\n key: 'warnings',\n title: t.issues.groupWarnings,\n icon: AlertTriangle,\n items: warnings,\n tone: 'tpl:text-[var(--tpl-warning)]',\n },\n {\n key: 'info',\n title: t.issues.groupInfo,\n icon: Info,\n items: infos,\n tone: 'tpl:text-[var(--tpl-text-muted)]',\n },\n ]\"\n :key=\"group.key\"\n >\n <header\n v-if=\"group.items.length > 0\"\n class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-[11px] tpl:font-semibold tpl:uppercase tpl:tracking-wide\"\n :class=\"group.tone\"\n >\n <component :is=\"group.icon\" :size=\"12\" :stroke-width=\"2\" />\n {{ group.title }}\n <span class=\"tpl:ml-auto tpl:font-normal tpl:opacity-70\">\n {{ group.items.length }}\n </span>\n </header>\n <ul\n v-if=\"group.items.length > 0\"\n class=\"tpl:m-0 tpl:mb-3 tpl:flex tpl:list-none tpl:flex-col tpl:gap-2 tpl:p-0\"\n >\n <li\n v-for=\"issue in group.items\"\n :key=\"`${issue.ruleId}-${issue.blockId ?? 'template'}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <p class=\"tpl:m-0 tpl:mb-2 tpl:text-xs tpl:text-[var(--tpl-text)]\">\n {{ issue.message }}\n </p>\n <p\n class=\"tpl:m-0 tpl:mb-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ issue.ruleId }}\n </p>\n <div class=\"tpl:flex tpl:gap-1.5\">\n <button\n v-if=\"issue.blockId\"\n type=\"button\"\n data-testid=\"issue-jump\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text)]\"\n @click=\"jumpTo(issue)\"\n >\n <ArrowRight :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.jump }}\n </button>\n <button\n v-if=\"issue.fix\"\n type=\"button\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:bg-[var(--tpl-primary)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-on-primary)]\"\n :title=\"issue.fix.description\"\n @click=\"applyFix(issue)\"\n >\n <Wrench :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.fix }}\n </button>\n </div>\n </li>\n </ul>\n </section>\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport {\n ListChecks,\n AlertCircle,\n AlertTriangle,\n Info,\n ArrowRight,\n Wrench,\n} from \"@lucide/vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useScrollToBlock } from \"../../composables/useScrollToBlock\";\nimport { TEMPLATE_LINT_KEY, EDITOR_KEY } from \"../../keys\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\nconst { t, format } = useI18n();\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst errors = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"error\"),\n);\nconst warnings = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"warning\"),\n);\nconst infos = computed(() =>\n (lint?.issues.value ?? []).filter((i) => i.severity === \"info\"),\n);\n\nconst totalCount = computed(\n () => errors.value.length + warnings.value.length + infos.value.length,\n);\n\nfunction jumpTo(issue: LintIssue): void {\n if (!editor) return;\n if (!issue.blockId) return;\n editor.selectBlock(issue.blockId);\n // Selecting is invisible on its own — on a long template the block sits\n // below the fold and the canvas stays put, so \"Jump\" appears to do nothing.\n scrollToBlock(issue.blockId);\n}\n\nfunction applyFix(issue: LintIssue): void {\n lint?.applyFix(issue);\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-4 tpl:p-4\">\n <header class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <ListChecks :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.issues.panelTitle }}\n </h3>\n <span\n v-if=\"totalCount > 0\"\n class=\"tpl:ml-auto tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-0.5 tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n :title=\"format(t.issues.issueCountTooltip, { count: totalCount })\"\n >\n {{ totalCount }}\n </span>\n </header>\n\n <div\n v-if=\"!lint || lint.unavailable.value\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <!-- Quality package not available; render nothing visible beyond a hint. -->\n </div>\n\n <div\n v-else-if=\"totalCount === 0\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.issues.emptyState }}\n </div>\n\n <template v-else>\n <section\n v-for=\"group in [\n {\n key: 'errors',\n title: t.issues.groupErrors,\n icon: AlertCircle,\n items: errors,\n tone: 'tpl:text-[var(--tpl-danger)]',\n },\n {\n key: 'warnings',\n title: t.issues.groupWarnings,\n icon: AlertTriangle,\n items: warnings,\n tone: 'tpl:text-[var(--tpl-warning)]',\n },\n {\n key: 'info',\n title: t.issues.groupInfo,\n icon: Info,\n items: infos,\n tone: 'tpl:text-[var(--tpl-text-muted)]',\n },\n ]\"\n :key=\"group.key\"\n >\n <header\n v-if=\"group.items.length > 0\"\n class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-[11px] tpl:font-semibold tpl:uppercase tpl:tracking-wide\"\n :class=\"group.tone\"\n >\n <component :is=\"group.icon\" :size=\"12\" :stroke-width=\"2\" />\n {{ group.title }}\n <span class=\"tpl:ml-auto tpl:font-normal tpl:opacity-70\">\n {{ group.items.length }}\n </span>\n </header>\n <ul\n v-if=\"group.items.length > 0\"\n class=\"tpl:m-0 tpl:mb-3 tpl:flex tpl:list-none tpl:flex-col tpl:gap-2 tpl:p-0\"\n >\n <li\n v-for=\"issue in group.items\"\n :key=\"`${issue.ruleId}-${issue.blockId ?? 'template'}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <p class=\"tpl:m-0 tpl:mb-2 tpl:text-xs tpl:text-[var(--tpl-text)]\">\n {{ issue.message }}\n </p>\n <p\n class=\"tpl:m-0 tpl:mb-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ issue.ruleId }}\n </p>\n <div class=\"tpl:flex tpl:gap-1.5\">\n <button\n v-if=\"issue.blockId\"\n type=\"button\"\n data-testid=\"issue-jump\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text)]\"\n @click=\"jumpTo(issue)\"\n >\n <ArrowRight :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.jump }}\n </button>\n <button\n v-if=\"issue.fix\"\n type=\"button\"\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-md tpl:bg-[var(--tpl-primary)] tpl:px-2 tpl:py-1 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-on-primary)]\"\n :title=\"issue.fix.description\"\n @click=\"applyFix(issue)\"\n >\n <Wrench :size=\"10\" :stroke-width=\"2\" />\n {{ t.issues.fix }}\n </button>\n </div>\n </li>\n </ul>\n </section>\n </template>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { E as e, F as t, N as n, S as r, at as i, f as a, g as o, h as s, lt as
|
|
|
2
2
|
import { u as p } from "./src-BD60gqPW.js";
|
|
3
3
|
import { T as m } from "./keys-BJ770Mwm.js";
|
|
4
4
|
import { t as h } from "./useI18n-DIhawILp.js";
|
|
5
|
-
import { $ as g, D as _, E as v, p as y } from "./icons-
|
|
5
|
+
import { $ as g, D as _, E as v, p as y } from "./icons-ciivb5BS.js";
|
|
6
6
|
//#region src/components/ViewportToggle.vue?vue&type=script&setup=true&lang.ts
|
|
7
7
|
var b = ["aria-label"], x = [
|
|
8
8
|
"aria-checked",
|
|
@@ -124,4 +124,4 @@ var b = ["aria-label"], x = [
|
|
|
124
124
|
//#endregion
|
|
125
125
|
export { S as n, T as t };
|
|
126
126
|
|
|
127
|
-
//# sourceMappingURL=MergeTagModeToggle-
|
|
127
|
+
//# sourceMappingURL=MergeTagModeToggle-BU0SDRow.js.map
|
package/dist/cdn/chunks/{MergeTagModeToggle-DZtnCj0F.js.map → MergeTagModeToggle-BU0SDRow.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MergeTagModeToggle-DZtnCj0F.js","names":[],"sources":["../../../src/components/ViewportToggle.vue","../../../src/components/ViewportToggle.vue","../../../src/components/MergeTagModeToggle.vue","../../../src/components/MergeTagModeToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { ViewportSize } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", viewport: ViewportSize): void;\n}>();\n\nconst { t } = useI18n();\n\nconst viewports = computed(() => [\n { value: \"desktop\" as ViewportSize, label: t.viewport.desktop },\n { value: \"mobile\" as ViewportSize, label: t.viewport.mobile },\n]);\n\nconst pillOffset = computed(() => {\n const index = viewports.value.findIndex((vp) => vp.value === props.viewport);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n role=\"radiogroup\"\n :aria-label=\"t.viewport.label\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${viewports.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${viewports.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"vp in viewports\"\n :key=\"vp.value\"\n role=\"radio\"\n :aria-checked=\"viewport === vp.value\"\n :aria-label=\"vp.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n viewport === vp.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"vp.label\"\n @click=\"emit('change', vp.value)\"\n >\n <Monitor v-if=\"vp.value === 'desktop'\" :size=\"18\" :stroke-width=\"1.5\" />\n <Smartphone v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ vp.label }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { ViewportSize } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", viewport: ViewportSize): void;\n}>();\n\nconst { t } = useI18n();\n\nconst viewports = computed(() => [\n { value: \"desktop\" as ViewportSize, label: t.viewport.desktop },\n { value: \"mobile\" as ViewportSize, label: t.viewport.mobile },\n]);\n\nconst pillOffset = computed(() => {\n const index = viewports.value.findIndex((vp) => vp.value === props.viewport);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n role=\"radiogroup\"\n :aria-label=\"t.viewport.label\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${viewports.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${viewports.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"vp in viewports\"\n :key=\"vp.value\"\n role=\"radio\"\n :aria-checked=\"viewport === vp.value\"\n :aria-label=\"vp.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n viewport === vp.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"vp.label\"\n @click=\"emit('change', vp.value)\"\n >\n <Monitor v-if=\"vp.value === 'desktop'\" :size=\"18\" :stroke-width=\"1.5\" />\n <Smartphone v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ vp.label }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Switches the previews between the two merge-tag views: **Sample** (tags\n * replaced by their `MergeTag.sample`, rendered as ordinary text) and **Label**\n * (tags shown as their label with the usual cue).\n *\n * Deliberately the same segmented control as `ViewportToggle.vue` — the two sit\n * one click apart on the same preview surfaces, so they have to read as one\n * family rather than two similar things. Kept as a separate component rather\n * than generalising the viewport one: that would mean a generic segmented\n * control with an icon map, and two call sites don't justify the indirection.\n *\n * Renders only where a preview is showing. It must never appear over the\n * editing canvas, because substitution never happens there.\n *\n * **Renders nothing at all unless some configured tag declares a `sample`.**\n * With none, the two views are identical, so the control would be inert. The\n * check lives here rather than at each call site so the guarantee holds for\n * every surface — present and future — without three `v-if`s to keep in step.\n */\nimport { hasMergeTagSamples } from \"@templatical/types\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { MERGE_TAGS_KEY } from \"../keys\";\nimport { TextCursorInput, Sparkles } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\n\nconst props = defineProps<{\n /** `true` for Sample view, `false` for Label view. */\n sampleMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", sampleMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst isAvailable = computed(() => hasMergeTagSamples(mergeTags));\n\nconst modes = computed(() => [\n { value: true, label: t.mergeTagPreview.sample },\n { value: false, label: t.mergeTagPreview.labelView },\n]);\n\nconst pillOffset = computed(() => {\n const index = modes.value.findIndex((m) => m.value === props.sampleMode);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n v-if=\"isAvailable\"\n role=\"radiogroup\"\n :aria-label=\"t.mergeTagPreview.label\"\n data-testid=\"merge-tag-mode-toggle\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${modes.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${modes.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"mode in modes\"\n :key=\"String(mode.value)\"\n role=\"radio\"\n :aria-checked=\"sampleMode === mode.value\"\n :aria-label=\"mode.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n sampleMode === mode.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"mode.label\"\n @click=\"emit('change', mode.value)\"\n >\n <Sparkles v-if=\"mode.value\" :size=\"18\" :stroke-width=\"1.5\" />\n <TextCursorInput v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ mode.label }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Switches the previews between the two merge-tag views: **Sample** (tags\n * replaced by their `MergeTag.sample`, rendered as ordinary text) and **Label**\n * (tags shown as their label with the usual cue).\n *\n * Deliberately the same segmented control as `ViewportToggle.vue` — the two sit\n * one click apart on the same preview surfaces, so they have to read as one\n * family rather than two similar things. Kept as a separate component rather\n * than generalising the viewport one: that would mean a generic segmented\n * control with an icon map, and two call sites don't justify the indirection.\n *\n * Renders only where a preview is showing. It must never appear over the\n * editing canvas, because substitution never happens there.\n *\n * **Renders nothing at all unless some configured tag declares a `sample`.**\n * With none, the two views are identical, so the control would be inert. The\n * check lives here rather than at each call site so the guarantee holds for\n * every surface — present and future — without three `v-if`s to keep in step.\n */\nimport { hasMergeTagSamples } from \"@templatical/types\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { MERGE_TAGS_KEY } from \"../keys\";\nimport { TextCursorInput, Sparkles } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\n\nconst props = defineProps<{\n /** `true` for Sample view, `false` for Label view. */\n sampleMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", sampleMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst isAvailable = computed(() => hasMergeTagSamples(mergeTags));\n\nconst modes = computed(() => [\n { value: true, label: t.mergeTagPreview.sample },\n { value: false, label: t.mergeTagPreview.labelView },\n]);\n\nconst pillOffset = computed(() => {\n const index = modes.value.findIndex((m) => m.value === props.sampleMode);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n v-if=\"isAvailable\"\n role=\"radiogroup\"\n :aria-label=\"t.mergeTagPreview.label\"\n data-testid=\"merge-tag-mode-toggle\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${modes.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${modes.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"mode in modes\"\n :key=\"String(mode.value)\"\n role=\"radio\"\n :aria-checked=\"sampleMode === mode.value\"\n :aria-label=\"mode.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n sampleMode === mode.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"mode.label\"\n @click=\"emit('change', mode.value)\"\n >\n <Sparkles v-if=\"mode.value\" :size=\"18\" :stroke-width=\"1.5\" />\n <TextCursorInput v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ mode.label }}</span>\n </button>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;EAMA,IAAM,IAAQ,GAIR,IAAO,GAIP,EAAE,SAAM,EAAQ,GAEhB,IAAY,QAAe,CAC/B;GAAE,OAAO;GAA2B,OAAO,EAAE,SAAS;EAAQ,GAC9D;GAAE,OAAO;GAA0B,OAAO,EAAE,SAAS;EAAO,CAC9D,CAAC,GAEK,IAAa,QAEV,cADO,EAAU,MAAM,WAAW,MAAO,EAAG,UAAU,EAAM,QAC9C,IAAQ,IAAI,GAClC;oBAIC,EAAA,GAAA,EA2CM,OAAA;GA1CJ,MAAK;GACJ,cAAY,EAAA,CAAA,CAAC,CAAC,SAAS;GACxB,OAAM;GACL,OAAK,EAAA;IAAyC,qBAAA,UAAA,EAAA,MAAU,OAAM;;;EAM/D,GAAA,CAAA,EAUO,OAAA;GATL,OAAM;GACL,OAAK,EAAA;;IAA+D,OAAA,uBAAA,EAAA,MAAU,OAAM;IAAwB,WAAA,EAAA;;;;;EAU/G,GAAA,MAAA,CAAA,IAAA,EAAA,EAAA,GAAA,EAoBS,GAAA,MAAA,EAnBM,EAAA,QAAN,OADT,EAAA,GAAA,EAoBS,UAAA;GAlBN,KAAK,EAAG;GACT,MAAK;GACJ,gBAAc,EAAA,aAAa,EAAG;GAC9B,cAAY,EAAG;GAChB,OAAM;GACL,OAAK,EAAA;IAA6B,OAAA,EAAA,aAAa,EAAG,QAAA,uBAAA;;;GAOlD,OAAO,EAAG;GACV,UAAK,MAAE,EAAI,UAAW,EAAG,KAAK;EAEhB,GAAA,CAAA,EAAG,UAAK,aAAvB,EAAA,GAAA,EAAwE,EAAA,CAAA,GAAA;;GAAhC,MAAM;GAAK,gBAAc;EACjE,CAAA,MAAA,EAAA,GAAA,EAAoD,EAAA,CAAA,GAAA;;GAAhC,MAAM;GAAK,gBAAc;EAC7C,CAAA,IAAA,EAA2B,QAAA,MAAA,EAAlB,EAAG,KAAK,GAAA,CAAA,CAAA,GAAA,IAAA,CAAA;;;;;;;;;;;;EE3CvB,IAAM,IAAQ,GAKR,IAAO,GAIP,EAAE,SAAM,EAAQ,GAEhB,IAAY,EAAO,GAAgB,CAAC,CAAC,GACrC,IAAc,QAAe,EAAmB,CAAS,CAAC,GAE1D,IAAQ,QAAe,CAC3B;GAAE,OAAO;GAAM,OAAO,EAAE,gBAAgB;EAAO,GAC/C;GAAE,OAAO;GAAO,OAAO,EAAE,gBAAgB;EAAU,CACrD,CAAC,GAEK,IAAa,QAEV,cADO,EAAM,MAAM,WAAW,MAAM,EAAE,UAAU,EAAM,UACxC,IAAQ,IAAI,GAClC;mBAKS,EAAA,SADR,EAAA,GAAA,EA6CM,OAAA;;GA3CJ,MAAK;GACJ,cAAY,EAAA,CAAA,CAAC,CAAC,gBAAgB;GAC/B,eAAY;GACZ,OAAM;GACL,OAAK,EAAA;IAAyC,qBAAA,UAAA,EAAA,MAAM,OAAM;;;EAM3D,GAAA,CAAA,EAUO,OAAA;GATL,OAAM;GACL,OAAK,EAAA;;IAA+D,OAAA,uBAAA,EAAA,MAAM,OAAM;IAAwB,WAAA,EAAA;;;;;EAU3G,GAAA,MAAA,CAAA,IAAA,EAAA,EAAA,GAAA,EAoBS,GAAA,MAAA,EAnBQ,EAAA,QAAR,OADT,EAAA,GAAA,EAoBS,UAAA;GAlBN,KAAK,OAAO,EAAK,KAAK;GACvB,MAAK;GACJ,gBAAc,EAAA,eAAe,EAAK;GAClC,cAAY,EAAK;GAClB,OAAM;GACL,OAAK,EAAA;IAA6B,OAAA,EAAA,eAAe,EAAK,QAAA,uBAAA;;;GAOtD,OAAO,EAAK;GACZ,UAAK,MAAE,EAAI,UAAW,EAAK,KAAK;EAEjB,GAAA,CAAA,EAAK,SAArB,EAAA,GAAA,EAA6D,EAAA,CAAA,GAAA;;GAAhC,MAAM;GAAK,gBAAc;EACtD,CAAA,MAAA,EAAA,GAAA,EAAyD,EAAA,CAAA,GAAA;;GAAhC,MAAM;GAAK,gBAAc;EAClD,CAAA,IAAA,EAA6B,QAAA,MAAA,EAApB,EAAK,KAAK,GAAA,CAAA,CAAA,GAAA,IAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"MergeTagModeToggle-BU0SDRow.js","names":[],"sources":["../../../src/components/ViewportToggle.vue","../../../src/components/ViewportToggle.vue","../../../src/components/MergeTagModeToggle.vue","../../../src/components/MergeTagModeToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { ViewportSize } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", viewport: ViewportSize): void;\n}>();\n\nconst { t } = useI18n();\n\nconst viewports = computed(() => [\n { value: \"desktop\" as ViewportSize, label: t.viewport.desktop },\n { value: \"mobile\" as ViewportSize, label: t.viewport.mobile },\n]);\n\nconst pillOffset = computed(() => {\n const index = viewports.value.findIndex((vp) => vp.value === props.viewport);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n role=\"radiogroup\"\n :aria-label=\"t.viewport.label\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${viewports.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${viewports.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"vp in viewports\"\n :key=\"vp.value\"\n role=\"radio\"\n :aria-checked=\"viewport === vp.value\"\n :aria-label=\"vp.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n viewport === vp.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"vp.label\"\n @click=\"emit('change', vp.value)\"\n >\n <Monitor v-if=\"vp.value === 'desktop'\" :size=\"18\" :stroke-width=\"1.5\" />\n <Smartphone v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ vp.label }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { ViewportSize } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", viewport: ViewportSize): void;\n}>();\n\nconst { t } = useI18n();\n\nconst viewports = computed(() => [\n { value: \"desktop\" as ViewportSize, label: t.viewport.desktop },\n { value: \"mobile\" as ViewportSize, label: t.viewport.mobile },\n]);\n\nconst pillOffset = computed(() => {\n const index = viewports.value.findIndex((vp) => vp.value === props.viewport);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n role=\"radiogroup\"\n :aria-label=\"t.viewport.label\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${viewports.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${viewports.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"vp in viewports\"\n :key=\"vp.value\"\n role=\"radio\"\n :aria-checked=\"viewport === vp.value\"\n :aria-label=\"vp.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n viewport === vp.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"vp.label\"\n @click=\"emit('change', vp.value)\"\n >\n <Monitor v-if=\"vp.value === 'desktop'\" :size=\"18\" :stroke-width=\"1.5\" />\n <Smartphone v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ vp.label }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Switches the previews between the two merge-tag views: **Sample** (tags\n * replaced by their `MergeTag.sample`, rendered as ordinary text) and **Label**\n * (tags shown as their label with the usual cue).\n *\n * Deliberately the same segmented control as `ViewportToggle.vue` — the two sit\n * one click apart on the same preview surfaces, so they have to read as one\n * family rather than two similar things. Kept as a separate component rather\n * than generalising the viewport one: that would mean a generic segmented\n * control with an icon map, and two call sites don't justify the indirection.\n *\n * Renders only where a preview is showing. It must never appear over the\n * editing canvas, because substitution never happens there.\n *\n * **Renders nothing at all unless some configured tag declares a `sample`.**\n * With none, the two views are identical, so the control would be inert. The\n * check lives here rather than at each call site so the guarantee holds for\n * every surface — present and future — without three `v-if`s to keep in step.\n */\nimport { hasMergeTagSamples } from \"@templatical/types\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { MERGE_TAGS_KEY } from \"../keys\";\nimport { TextCursorInput, Sparkles } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\n\nconst props = defineProps<{\n /** `true` for Sample view, `false` for Label view. */\n sampleMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", sampleMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst isAvailable = computed(() => hasMergeTagSamples(mergeTags));\n\nconst modes = computed(() => [\n { value: true, label: t.mergeTagPreview.sample },\n { value: false, label: t.mergeTagPreview.labelView },\n]);\n\nconst pillOffset = computed(() => {\n const index = modes.value.findIndex((m) => m.value === props.sampleMode);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n v-if=\"isAvailable\"\n role=\"radiogroup\"\n :aria-label=\"t.mergeTagPreview.label\"\n data-testid=\"merge-tag-mode-toggle\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${modes.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${modes.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"mode in modes\"\n :key=\"String(mode.value)\"\n role=\"radio\"\n :aria-checked=\"sampleMode === mode.value\"\n :aria-label=\"mode.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n sampleMode === mode.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"mode.label\"\n @click=\"emit('change', mode.value)\"\n >\n <Sparkles v-if=\"mode.value\" :size=\"18\" :stroke-width=\"1.5\" />\n <TextCursorInput v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ mode.label }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Switches the previews between the two merge-tag views: **Sample** (tags\n * replaced by their `MergeTag.sample`, rendered as ordinary text) and **Label**\n * (tags shown as their label with the usual cue).\n *\n * Deliberately the same segmented control as `ViewportToggle.vue` — the two sit\n * one click apart on the same preview surfaces, so they have to read as one\n * family rather than two similar things. Kept as a separate component rather\n * than generalising the viewport one: that would mean a generic segmented\n * control with an icon map, and two call sites don't justify the indirection.\n *\n * Renders only where a preview is showing. It must never appear over the\n * editing canvas, because substitution never happens there.\n *\n * **Renders nothing at all unless some configured tag declares a `sample`.**\n * With none, the two views are identical, so the control would be inert. The\n * check lives here rather than at each call site so the guarantee holds for\n * every surface — present and future — without three `v-if`s to keep in step.\n */\nimport { hasMergeTagSamples } from \"@templatical/types\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { MERGE_TAGS_KEY } from \"../keys\";\nimport { TextCursorInput, Sparkles } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\n\nconst props = defineProps<{\n /** `true` for Sample view, `false` for Label view. */\n sampleMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", sampleMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst isAvailable = computed(() => hasMergeTagSamples(mergeTags));\n\nconst modes = computed(() => [\n { value: true, label: t.mergeTagPreview.sample },\n { value: false, label: t.mergeTagPreview.labelView },\n]);\n\nconst pillOffset = computed(() => {\n const index = modes.value.findIndex((m) => m.value === props.sampleMode);\n return `translateX(${index * 100}%)`;\n});\n</script>\n\n<template>\n <div\n v-if=\"isAvailable\"\n role=\"radiogroup\"\n :aria-label=\"t.mergeTagPreview.label\"\n data-testid=\"merge-tag-mode-toggle\"\n class=\"tpl:relative tpl:grid tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1\"\n :style=\"{\n gridTemplateColumns: `repeat(${modes.length}, 1fr)`,\n backgroundColor: 'var(--tpl-bg-hover)',\n }\"\n >\n <!-- Sliding pill -->\n <div\n class=\"tpl:absolute tpl:inset-y-1 tpl:rounded-[var(--tpl-radius-sm)]\"\n :style=\"{\n left: '4px',\n width: `calc((100% - 8px) / ${modes.length})`,\n transform: pillOffset,\n backgroundColor: 'var(--tpl-bg)',\n boxShadow: 'var(--tpl-shadow)',\n transition: 'transform 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n ></div>\n\n <button\n v-for=\"mode in modes\"\n :key=\"String(mode.value)\"\n role=\"radio\"\n :aria-checked=\"sampleMode === mode.value\"\n :aria-label=\"mode.label\"\n class=\"tpl:relative tpl:z-10 tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium\"\n :style=\"{\n color:\n sampleMode === mode.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n transition: 'color 120ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n :title=\"mode.label\"\n @click=\"emit('change', mode.value)\"\n >\n <Sparkles v-if=\"mode.value\" :size=\"18\" :stroke-width=\"1.5\" />\n <TextCursorInput v-else :size=\"18\" :stroke-width=\"1.5\" />\n <span>{{ mode.label }}</span>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { F as e, N as t, Q as n, S as r, ct as i, g as a, h as o, l as s, p as c, r as l, u, ut as d, w as f } from "./draggable-D2vo-aQI.js";
|
|
2
|
-
import { h as p, m } from "./tiptap-
|
|
2
|
+
import { h as p, m } from "./tiptap-Ey0FpaX7.js";
|
|
3
3
|
//#region src/components/MergeTagSuggestionList.vue?vue&type=script&setup=true&lang.ts
|
|
4
4
|
var h = ["id"], g = {
|
|
5
5
|
key: 0,
|
|
@@ -212,4 +212,4 @@ var D = p.create({
|
|
|
212
212
|
//#endregion
|
|
213
213
|
export { w as i, T as n, C as r, D as t };
|
|
214
214
|
|
|
215
|
-
//# sourceMappingURL=MergeTagSuggestion-
|
|
215
|
+
//# sourceMappingURL=MergeTagSuggestion-B9vL0VaG.js.map
|
package/dist/cdn/chunks/{MergeTagSuggestion-VP_nC-8W.js.map → MergeTagSuggestion-B9vL0VaG.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MergeTagSuggestion-VP_nC-8W.js","names":["$emit"],"sources":["../../../src/components/MergeTagSuggestionList.vue","../../../src/components/MergeTagSuggestionList.vue","../../../src/extensions/MergeTagSuggestion.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { MergeTag } from \"@templatical/types\";\n\nconst props = defineProps<{\n items: MergeTag[];\n selectedIndex: number;\n emptyText: string;\n /** Stable id used for aria-controls + per-option id derivation. */\n listId?: string;\n}>();\n\ndefineEmits<{\n (e: \"select\", item: MergeTag): void;\n (e: \"hover\", index: number): void;\n}>();\n\nfunction optionId(index: number): string | undefined {\n return props.listId ? `${props.listId}-opt-${index}` : undefined;\n}\n</script>\n\n<template>\n <div\n :id=\"listId\"\n class=\"tpl:min-w-[200px] tpl:max-w-[320px] tpl:max-h-[50vh] tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1 tpl:shadow-[var(--tpl-shadow-lg)]\"\n role=\"listbox\"\n data-testid=\"merge-tag-suggestion-list\"\n >\n <div\n v-if=\"items.length === 0\"\n class=\"tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-suggestion-empty\"\n >\n {{ emptyText }}\n </div>\n <button\n v-for=\"(item, index) in items\"\n :key=\"item.value\"\n :id=\"optionId(index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"index === selectedIndex\"\n :data-selected=\"index === selectedIndex ? 'true' : 'false'\"\n :data-merge-tag-value=\"item.value\"\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:px-3 tpl:py-1.5 tpl:text-left tpl:text-xs tpl:transition-colors\"\n :class=\"\n index === selectedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n @mousedown.prevent.stop=\"$emit('select', item)\"\n @mousemove=\"index !== selectedIndex && $emit('hover', index)\"\n >\n <span class=\"tpl:font-medium\">{{ item.label }}</span>\n <span class=\"tpl:text-[var(--tpl-text-dim)] tpl:font-mono\">{{\n item.value\n }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { MergeTag } from \"@templatical/types\";\n\nconst props = defineProps<{\n items: MergeTag[];\n selectedIndex: number;\n emptyText: string;\n /** Stable id used for aria-controls + per-option id derivation. */\n listId?: string;\n}>();\n\ndefineEmits<{\n (e: \"select\", item: MergeTag): void;\n (e: \"hover\", index: number): void;\n}>();\n\nfunction optionId(index: number): string | undefined {\n return props.listId ? `${props.listId}-opt-${index}` : undefined;\n}\n</script>\n\n<template>\n <div\n :id=\"listId\"\n class=\"tpl:min-w-[200px] tpl:max-w-[320px] tpl:max-h-[50vh] tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1 tpl:shadow-[var(--tpl-shadow-lg)]\"\n role=\"listbox\"\n data-testid=\"merge-tag-suggestion-list\"\n >\n <div\n v-if=\"items.length === 0\"\n class=\"tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-suggestion-empty\"\n >\n {{ emptyText }}\n </div>\n <button\n v-for=\"(item, index) in items\"\n :key=\"item.value\"\n :id=\"optionId(index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"index === selectedIndex\"\n :data-selected=\"index === selectedIndex ? 'true' : 'false'\"\n :data-merge-tag-value=\"item.value\"\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:px-3 tpl:py-1.5 tpl:text-left tpl:text-xs tpl:transition-colors\"\n :class=\"\n index === selectedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n @mousedown.prevent.stop=\"$emit('select', item)\"\n @mousemove=\"index !== selectedIndex && $emit('hover', index)\"\n >\n <span class=\"tpl:font-medium\">{{ item.label }}</span>\n <span class=\"tpl:text-[var(--tpl-text-dim)] tpl:font-mono\">{{\n item.value\n }}</span>\n </button>\n </div>\n</template>\n","import type { MergeTag } from \"@templatical/types\";\nimport { Extension } from \"@tiptap/core\";\nimport Suggestion, {\n type SuggestionOptions,\n type SuggestionProps,\n type SuggestionKeyDownProps,\n} from \"@tiptap/suggestion\";\nimport { type App, createApp, h, ref, type Ref } from \"vue\";\nimport MergeTagSuggestionList from \"../components/MergeTagSuggestionList.vue\";\n\nconst MAX_RESULTS = 10;\n\n// Monotonic counter for unique listbox IDs across multiple popup instances.\nlet POPUP_ID_SEQ = 0;\n\nexport interface MergeTagSuggestionOptions {\n /** Available merge tags */\n mergeTags: MergeTag[];\n /** Trigger string (e.g. \"{{\", \"*|\", \"%%=\") */\n char: string;\n /** Localized empty-state label */\n emptyText: string;\n /**\n * Mount target for the suggestion popup. When provided with a non-null\n * `.value`, the popup attaches into that element instead of\n * `document.body` — keeping it inside the editor's effective DOM root\n * (shadow-aware). Pass the ref returned by `usePopoverRoot()`.\n *\n * Falls back to `document.body` when omitted or when the ref's value is\n * null at popup-open time (e.g. headless/test editors without an editor\n * shell). Preserves pre-Phase-3 behavior for callers that don't migrate.\n */\n popoverRoot?: Ref<HTMLElement | null> | null;\n}\n\n/**\n * Filter merge tags by query against label and value (case-insensitive).\n * Capped at MAX_RESULTS. Exported for testing.\n */\nexport function filterMergeTags(tags: MergeTag[], query: string): MergeTag[] {\n const trimmed = query.trim().toLowerCase();\n if (trimmed === \"\") {\n return tags.slice(0, MAX_RESULTS);\n }\n return tags\n .filter((tag) => {\n const label = tag.label.toLowerCase();\n const value = tag.value.toLowerCase();\n return label.includes(trimmed) || value.includes(trimmed);\n })\n .slice(0, MAX_RESULTS);\n}\n\n/**\n * Handle a keydown event against a list of items. Returns whether the\n * event was handled (so the suggestion plugin can stop propagation).\n * Exported for testing.\n */\nexport function handleSuggestionKeyDown(\n event: KeyboardEvent,\n items: MergeTag[],\n selectedIndex: Ref<number>,\n onSelect: (item: MergeTag) => void,\n): boolean {\n if (items.length === 0) {\n if (event.key === \"Enter\" || event.key === \"Tab\") return true;\n return false;\n }\n\n if (event.key === \"ArrowDown\") {\n selectedIndex.value = (selectedIndex.value + 1) % items.length;\n return true;\n }\n if (event.key === \"ArrowUp\") {\n selectedIndex.value =\n (selectedIndex.value - 1 + items.length) % items.length;\n return true;\n }\n if (event.key === \"Enter\" || event.key === \"Tab\") {\n onSelect(items[selectedIndex.value]);\n return true;\n }\n return false;\n}\n\n/** Options for opening the merge-tag popup. */\nexport interface MergeTagPopupOpenOptions {\n /** Filtered tags to render. */\n items: MergeTag[];\n /**\n * Anchor rect getter (the caret rect). Same contract as\n * `@tiptap/suggestion`'s `clientRect`: viewport coordinates, re-read on\n * every reposition so the popup tracks the caret through scroll.\n */\n getRect: (() => DOMRect | null) | null;\n /**\n * Element that owns the ARIA combobox state and serves as the theme +\n * scroll-ancestor anchor — the ProseMirror `view.dom` in rich text, the\n * `<input>`/`<textarea>` in a field.\n */\n anchorEl: HTMLElement | null;\n /** Called with the item the user picks. */\n onCommand: (item: MergeTag) => void;\n}\n\n/** Options for updating the popup after the query changed. */\nexport interface MergeTagPopupUpdateOptions {\n items: MergeTag[];\n getRect: (() => DOMRect | null) | null;\n onCommand: (item: MergeTag) => void;\n}\n\n/**\n * Imperative handle over a single merge-tag suggestion popup.\n * Both the TipTap suggestion renderer and the input/textarea autocomplete\n * driver drive the SAME controller, so the popup's rendering, positioning,\n * scroll-tracking, ARIA wiring, and keyboard behavior are identical across\n * rich-text and plain-field surfaces by construction.\n */\nexport interface MergeTagPopupController {\n /** Mount the popup and start tracking the caret. */\n open(options: MergeTagPopupOpenOptions): void;\n /** Refresh items + anchor after the query changed. */\n update(options: MergeTagPopupUpdateOptions): void;\n /**\n * Handle a navigation/selection key (Arrow/Enter/Tab). Returns whether it\n * was handled. Does NOT handle Escape — each caller owns Escape (TipTap's\n * plugin closes via `onExit`; the field driver calls `close()`).\n */\n handleKeyDown(event: KeyboardEvent): boolean;\n /** Unmount the popup, clear ARIA, and detach listeners. Idempotent. */\n close(): void;\n /** Whether the popup is currently mounted. */\n isOpen(): boolean;\n}\n\n/**\n * Creates a merge-tag suggestion popup controller: mount, ARIA wiring,\n * caret-tracking position (flip-above + off-screen freeze), scroll/resize\n * repositioning, and cleanup. Framework-agnostic — it takes a plain caret\n * rect getter and an anchor element, so it serves both the ProseMirror\n * suggestion plugin and native text fields.\n */\nexport function createMergeTagPopup(\n emptyText: string,\n popoverRootRef?: Ref<HTMLElement | null> | null,\n): MergeTagPopupController {\n let app: App | null = null;\n let container: HTMLElement | null = null;\n let editableEl: HTMLElement | null = null;\n const itemsRef = ref<MergeTag[]>([]);\n const selectedIndex = ref(0);\n let currentCommand: ((item: MergeTag) => void) | null = null;\n const listId = `tpl-merge-tag-suggestion-${++POPUP_ID_SEQ}`;\n let latestClientRect: (() => DOMRect | null) | null = null;\n let scrollTargets: Array<EventTarget> = [];\n let pendingRaf: number | null = null;\n\n function reposition(): void {\n position(latestClientRect?.() ?? null);\n }\n\n /**\n * Reposition immediately and on the next animation frame.\n * After a keystroke triggers the suggestion, TipTap may run its\n * own `scrollIntoView` to keep the caret visible. That scroll\n * lands AFTER the current task's `position()` call but BEFORE\n * the browser's next paint, so we re-measure on rAF to catch\n * the post-scroll caret rect. Without this, the popup pins to\n * the pre-scroll caret position and ends up offset on slower\n * runners.\n */\n function repositionAfterPaint(): void {\n reposition();\n if (pendingRaf !== null) cancelAnimationFrame(pendingRaf);\n pendingRaf = requestAnimationFrame(() => {\n pendingRaf = null;\n reposition();\n });\n }\n\n function collectScrollAncestors(el: HTMLElement | null): HTMLElement[] {\n // Walk up the DOM finding scrollable ancestors. ProseMirror's\n // scrollIntoView fires on whichever ancestor scrolls — listening\n // to all of them ensures we reposition regardless of which one\n // moves.\n const result: HTMLElement[] = [];\n let node: HTMLElement | null = el?.parentElement ?? null;\n while (\n node &&\n // shadow-ok: ancestor walk terminator; not a mount target\n node !== document.body &&\n node !== document.documentElement\n ) {\n const style = window.getComputedStyle(node);\n const overflow = style.overflow + style.overflowX + style.overflowY;\n if (/(auto|scroll|overlay)/.test(overflow)) {\n result.push(node);\n }\n node = node.parentElement;\n }\n return result;\n }\n\n function attachScrollListeners(viewDom: HTMLElement | null): void {\n scrollTargets = [window, ...collectScrollAncestors(viewDom)];\n for (const target of scrollTargets) {\n target.addEventListener(\"scroll\", reposition, {\n passive: true,\n capture: true,\n });\n }\n window.addEventListener(\"resize\", reposition, { passive: true });\n }\n\n function detachScrollListeners(): void {\n for (const target of scrollTargets) {\n target.removeEventListener(\"scroll\", reposition, {\n capture: true,\n } as EventListenerOptions);\n }\n window.removeEventListener(\"resize\", reposition);\n scrollTargets = [];\n }\n\n function position(rect: DOMRect | null): void {\n if (!container || !rect) return;\n // If the caret has scrolled out of the viewport, freeze the\n // popup at its last on-screen position. Following the caret\n // off-screen produces an invisible popup the user can't reach,\n // and lets pathological scroll loops drag the popup further\n // each tick.\n if (rect.bottom < 0 || rect.top > window.innerHeight) return;\n // Position `absolute` relative to the mount target — the popover root\n // (`position: relative`) or the <body> fallback. The caret rect is in\n // viewport space, so subtract the offset parent's rect to get local\n // coords: a transformed ancestor of the editor becomes the containing\n // block for a `fixed` popup and would offset it, but not for an\n // `absolute` popup anchored inside the positioned popover root. The\n // off-screen freeze and flip-above decisions still use viewport metrics.\n container.style.position = \"absolute\";\n container.style.zIndex = \"9999\";\n const origin = container.offsetParent?.getBoundingClientRect();\n const originTop = origin?.top ?? 0;\n const originLeft = origin?.left ?? 0;\n container.style.left = `${rect.left - originLeft}px`;\n // Place below caret first; offsetHeight is sync-readable after\n // the Vue app has mounted (or after onUpdate's reactive flush).\n container.style.top = `${rect.bottom - originTop}px`;\n const popupHeight = container.offsetHeight;\n if (popupHeight === 0) return;\n const spaceBelow = window.innerHeight - rect.bottom;\n if (spaceBelow < popupHeight) {\n // Not enough room below — flip above. Clamp to 0 so the\n // popup never positions off the top of the viewport.\n const flippedTop = Math.max(0, rect.top - popupHeight);\n container.style.top = `${flippedTop - originTop}px`;\n }\n }\n\n function applyThemeContext(\n target: HTMLElement,\n editorEl: HTMLElement | null | undefined,\n ): void {\n // When the popup mounts to document.body its `position: fixed`\n // resolves against the viewport — any transform/filter on a\n // consumer-page ancestor (route transitions, reveal animations,\n // dark canvas inversion) creates a containing block and moves\n // fixed descendants with it. Body ancestors don't transform.\n //\n // When mounting inside the editor's popover root, the popup is a\n // descendant of the `.tpl[data-tpl-theme]` root so CSS vars + font\n // would inherit — but the snapshot below is still emitted inline\n // because inline declarations win and the cost is negligible. This\n // keeps a single behavior across both mount paths.\n //\n // CSS vars (--tpl-bg-elevated, --tpl-border, etc.) are scoped to\n // `.tpl` and `.tpl[data-tpl-theme=\"dark\"]` in the editor's\n // stylesheet, so the popup wouldn't inherit them at body root.\n // Adding `class=\"tpl\"` would also pull base rules (min-height,\n // flex, full-page bg) we don't want on a popup. Instead, snapshot\n // every --tpl-* custom property from the editor's theme root and\n // re-emit them inline on the popup wrapper.\n const themeRoot = editorEl?.closest<HTMLElement>(\"[data-tpl-theme]\");\n if (!themeRoot) return;\n const themeValue = themeRoot.getAttribute(\"data-tpl-theme\");\n if (themeValue) target.setAttribute(\"data-tpl-theme\", themeValue);\n const computed = window.getComputedStyle(themeRoot);\n for (let i = 0; i < computed.length; i++) {\n const prop = computed[i];\n if (prop.startsWith(\"--tpl-\")) {\n target.style.setProperty(prop, computed.getPropertyValue(prop));\n }\n }\n // The popup is teleported out of the editor wrapper, so it inherits no\n // font from it: without this its content renders in the page's default font\n // and ends up at a different height — which changes the flip-above decision\n // and shifts the popup off the caret. Copy typography too.\n target.style.fontFamily = computed.fontFamily;\n target.style.fontSize = computed.fontSize;\n target.style.lineHeight = computed.lineHeight;\n }\n\n function setEditableAria(active: boolean): void {\n if (!editableEl) return;\n if (active) {\n editableEl.setAttribute(\"role\", \"combobox\");\n editableEl.setAttribute(\"aria-haspopup\", \"listbox\");\n editableEl.setAttribute(\"aria-expanded\", \"true\");\n editableEl.setAttribute(\"aria-controls\", listId);\n } else {\n editableEl.removeAttribute(\"aria-expanded\");\n editableEl.removeAttribute(\"aria-controls\");\n editableEl.removeAttribute(\"aria-activedescendant\");\n editableEl.removeAttribute(\"aria-haspopup\");\n editableEl.removeAttribute(\"role\");\n }\n }\n\n function setActiveDescendant(): void {\n if (!editableEl) return;\n if (itemsRef.value.length === 0) {\n editableEl.removeAttribute(\"aria-activedescendant\");\n return;\n }\n editableEl.setAttribute(\n \"aria-activedescendant\",\n `${listId}-opt-${selectedIndex.value}`,\n );\n }\n\n function select(item: MergeTag): void {\n currentCommand?.(item);\n }\n\n return {\n open: ({ items, getRect, anchorEl, onCommand }) => {\n itemsRef.value = items;\n selectedIndex.value = 0;\n currentCommand = onCommand;\n\n container = document.createElement(\"div\");\n container.setAttribute(\"data-testid\", \"merge-tag-suggestion-popup\");\n editableEl = anchorEl;\n applyThemeContext(container, anchorEl);\n // Prefer the editor's popover root (shadow-aware) when wired by\n // the consumer. Falls back to document.body for headless callers\n // that don't pass `popoverRoot` to configure().\n // shadow-ok: fallback when popoverRoot wasn't provided (e.g., headless caller); editor mounts pass the shadow-aware root\n const mountTarget = popoverRootRef?.value ?? document.body;\n mountTarget.appendChild(container);\n\n app = createApp({\n render() {\n return h(MergeTagSuggestionList, {\n items: itemsRef.value,\n selectedIndex: selectedIndex.value,\n emptyText,\n listId,\n onSelect: (item: MergeTag) => select(item),\n onHover: (index: number) => {\n selectedIndex.value = index;\n setActiveDescendant();\n },\n });\n },\n });\n app.mount(container);\n setEditableAria(true);\n setActiveDescendant();\n latestClientRect = getRect;\n repositionAfterPaint();\n attachScrollListeners(anchorEl);\n },\n update: ({ items, getRect, onCommand }) => {\n itemsRef.value = items;\n // Reset selection when item set changes (query changed).\n if (selectedIndex.value >= items.length) {\n selectedIndex.value = 0;\n }\n currentCommand = onCommand;\n setActiveDescendant();\n latestClientRect = getRect;\n repositionAfterPaint();\n },\n handleKeyDown: (event: KeyboardEvent): boolean => {\n const handled = handleSuggestionKeyDown(\n event,\n itemsRef.value,\n selectedIndex,\n select,\n );\n if (handled) setActiveDescendant();\n return handled;\n },\n close: () => {\n // Idempotent: nothing to tear down (and no window listeners to detach)\n // when the popup was never opened. Keeps repeated close() / close-before-\n // open cheap and side-effect-free.\n if (container === null) return;\n if (pendingRaf !== null) {\n cancelAnimationFrame(pendingRaf);\n pendingRaf = null;\n }\n detachScrollListeners();\n setEditableAria(false);\n app?.unmount();\n container.remove();\n app = null;\n container = null;\n editableEl = null;\n currentCommand = null;\n latestClientRect = null;\n },\n isOpen: () => container !== null,\n };\n}\n\n/**\n * Builds the `@tiptap/suggestion` `render` factory for the merge-tag popup.\n * A thin adapter over {@link createMergeTagPopup}: it maps ProseMirror's\n * `SuggestionProps` (items, `clientRect`, `view.dom`, `command`) onto the\n * controller. Exported as a standalone function so that mapping can be\n * unit-tested with mocked `SuggestionProps` instead of a full editor.\n */\nexport function createMergeTagSuggestionRenderer(\n emptyText: string,\n popoverRootRef?: Ref<HTMLElement | null> | null,\n): NonNullable<SuggestionOptions<MergeTag>[\"render\"]> {\n return () => {\n const popup = createMergeTagPopup(emptyText, popoverRootRef);\n\n return {\n onStart: (props: SuggestionProps<MergeTag>) => {\n // Use view.dom (ProseMirror contenteditable, actually\n // attached to the DOM) rather than options.element, which may\n // be a detached div when no `element` is passed to the editor\n // constructor (as is the case with @tiptap/vue-3 EditorContent).\n const viewDom = props.editor.view?.dom as HTMLElement | undefined;\n popup.open({\n items: props.items,\n getRect: props.clientRect ?? null,\n anchorEl: viewDom ?? null,\n onCommand: (item) => props.command(item),\n });\n },\n onUpdate: (props: SuggestionProps<MergeTag>) => {\n popup.update({\n items: props.items,\n getRect: props.clientRect ?? null,\n onCommand: (item) => props.command(item),\n });\n },\n onKeyDown: (props: SuggestionKeyDownProps): boolean => {\n // Escape closes via TipTap's suggestion plugin (→ onExit); report it\n // handled without selecting.\n if (props.event.key === \"Escape\") {\n return true;\n }\n return popup.handleKeyDown(props.event);\n },\n onExit: () => popup.close(),\n };\n };\n}\n\nexport const MergeTagSuggestion = Extension.create<MergeTagSuggestionOptions>({\n name: \"mergeTagSuggestion\",\n\n addOptions() {\n return {\n mergeTags: [],\n char: \"{{\",\n emptyText: \"No matching merge tags\",\n popoverRoot: null,\n };\n },\n\n addProseMirrorPlugins() {\n const tags = this.options.mergeTags;\n const emptyText = this.options.emptyText;\n const popoverRootRef = this.options.popoverRoot;\n\n const config: Omit<SuggestionOptions<MergeTag>, \"editor\"> = {\n char: this.options.char,\n allowSpaces: false,\n startOfLine: false,\n // Default is [\" \"] which requires whitespace/line-start before the\n // trigger char — so `.{{` would not fire. Allow any preceding char.\n allowedPrefixes: null,\n items: ({ query }: { query: string }) => filterMergeTags(tags, query),\n command: ({\n editor,\n range,\n props,\n }: {\n editor: SuggestionProps<MergeTag>[\"editor\"];\n range: { from: number; to: number };\n props: MergeTag;\n }) => {\n // Use insertContentAt for atomic replace (matches the canonical\n // @tiptap/suggestion + Mention pattern). Avoids edge cases where\n // chained deleteRange + insertMergeTag fails to insert when the\n // selection state shifts mid-chain.\n editor\n .chain()\n .focus()\n .insertContentAt(range, {\n type: \"mergeTagNode\",\n attrs: { label: props.label, value: props.value },\n })\n .run();\n },\n render: createMergeTagSuggestionRenderer(emptyText, popoverRootRef),\n };\n\n return [\n Suggestion({\n editor: this.editor,\n ...config,\n }),\n ];\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MergeTagSuggestion-B9vL0VaG.js","names":["$emit"],"sources":["../../../src/components/MergeTagSuggestionList.vue","../../../src/components/MergeTagSuggestionList.vue","../../../src/extensions/MergeTagSuggestion.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { MergeTag } from \"@templatical/types\";\n\nconst props = defineProps<{\n items: MergeTag[];\n selectedIndex: number;\n emptyText: string;\n /** Stable id used for aria-controls + per-option id derivation. */\n listId?: string;\n}>();\n\ndefineEmits<{\n (e: \"select\", item: MergeTag): void;\n (e: \"hover\", index: number): void;\n}>();\n\nfunction optionId(index: number): string | undefined {\n return props.listId ? `${props.listId}-opt-${index}` : undefined;\n}\n</script>\n\n<template>\n <div\n :id=\"listId\"\n class=\"tpl:min-w-[200px] tpl:max-w-[320px] tpl:max-h-[50vh] tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1 tpl:shadow-[var(--tpl-shadow-lg)]\"\n role=\"listbox\"\n data-testid=\"merge-tag-suggestion-list\"\n >\n <div\n v-if=\"items.length === 0\"\n class=\"tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-suggestion-empty\"\n >\n {{ emptyText }}\n </div>\n <button\n v-for=\"(item, index) in items\"\n :key=\"item.value\"\n :id=\"optionId(index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"index === selectedIndex\"\n :data-selected=\"index === selectedIndex ? 'true' : 'false'\"\n :data-merge-tag-value=\"item.value\"\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:px-3 tpl:py-1.5 tpl:text-left tpl:text-xs tpl:transition-colors\"\n :class=\"\n index === selectedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n @mousedown.prevent.stop=\"$emit('select', item)\"\n @mousemove=\"index !== selectedIndex && $emit('hover', index)\"\n >\n <span class=\"tpl:font-medium\">{{ item.label }}</span>\n <span class=\"tpl:text-[var(--tpl-text-dim)] tpl:font-mono\">{{\n item.value\n }}</span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { MergeTag } from \"@templatical/types\";\n\nconst props = defineProps<{\n items: MergeTag[];\n selectedIndex: number;\n emptyText: string;\n /** Stable id used for aria-controls + per-option id derivation. */\n listId?: string;\n}>();\n\ndefineEmits<{\n (e: \"select\", item: MergeTag): void;\n (e: \"hover\", index: number): void;\n}>();\n\nfunction optionId(index: number): string | undefined {\n return props.listId ? `${props.listId}-opt-${index}` : undefined;\n}\n</script>\n\n<template>\n <div\n :id=\"listId\"\n class=\"tpl:min-w-[200px] tpl:max-w-[320px] tpl:max-h-[50vh] tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1 tpl:shadow-[var(--tpl-shadow-lg)]\"\n role=\"listbox\"\n data-testid=\"merge-tag-suggestion-list\"\n >\n <div\n v-if=\"items.length === 0\"\n class=\"tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-suggestion-empty\"\n >\n {{ emptyText }}\n </div>\n <button\n v-for=\"(item, index) in items\"\n :key=\"item.value\"\n :id=\"optionId(index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"index === selectedIndex\"\n :data-selected=\"index === selectedIndex ? 'true' : 'false'\"\n :data-merge-tag-value=\"item.value\"\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:px-3 tpl:py-1.5 tpl:text-left tpl:text-xs tpl:transition-colors\"\n :class=\"\n index === selectedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n @mousedown.prevent.stop=\"$emit('select', item)\"\n @mousemove=\"index !== selectedIndex && $emit('hover', index)\"\n >\n <span class=\"tpl:font-medium\">{{ item.label }}</span>\n <span class=\"tpl:text-[var(--tpl-text-dim)] tpl:font-mono\">{{\n item.value\n }}</span>\n </button>\n </div>\n</template>\n","import type { MergeTag } from \"@templatical/types\";\nimport { Extension } from \"@tiptap/core\";\nimport Suggestion, {\n type SuggestionOptions,\n type SuggestionProps,\n type SuggestionKeyDownProps,\n} from \"@tiptap/suggestion\";\nimport { type App, createApp, h, ref, type Ref } from \"vue\";\nimport MergeTagSuggestionList from \"../components/MergeTagSuggestionList.vue\";\n\nconst MAX_RESULTS = 10;\n\n// Monotonic counter for unique listbox IDs across multiple popup instances.\nlet POPUP_ID_SEQ = 0;\n\nexport interface MergeTagSuggestionOptions {\n /** Available merge tags */\n mergeTags: MergeTag[];\n /** Trigger string (e.g. \"{{\", \"*|\", \"%%=\") */\n char: string;\n /** Localized empty-state label */\n emptyText: string;\n /**\n * Mount target for the suggestion popup. When provided with a non-null\n * `.value`, the popup attaches into that element instead of\n * `document.body` — keeping it inside the editor's effective DOM root\n * (shadow-aware). Pass the ref returned by `usePopoverRoot()`.\n *\n * Falls back to `document.body` when omitted or when the ref's value is\n * null at popup-open time (e.g. headless/test editors without an editor\n * shell). Preserves pre-Phase-3 behavior for callers that don't migrate.\n */\n popoverRoot?: Ref<HTMLElement | null> | null;\n}\n\n/**\n * Filter merge tags by query against label and value (case-insensitive).\n * Capped at MAX_RESULTS. Exported for testing.\n */\nexport function filterMergeTags(tags: MergeTag[], query: string): MergeTag[] {\n const trimmed = query.trim().toLowerCase();\n if (trimmed === \"\") {\n return tags.slice(0, MAX_RESULTS);\n }\n return tags\n .filter((tag) => {\n const label = tag.label.toLowerCase();\n const value = tag.value.toLowerCase();\n return label.includes(trimmed) || value.includes(trimmed);\n })\n .slice(0, MAX_RESULTS);\n}\n\n/**\n * Handle a keydown event against a list of items. Returns whether the\n * event was handled (so the suggestion plugin can stop propagation).\n * Exported for testing.\n */\nexport function handleSuggestionKeyDown(\n event: KeyboardEvent,\n items: MergeTag[],\n selectedIndex: Ref<number>,\n onSelect: (item: MergeTag) => void,\n): boolean {\n if (items.length === 0) {\n if (event.key === \"Enter\" || event.key === \"Tab\") return true;\n return false;\n }\n\n if (event.key === \"ArrowDown\") {\n selectedIndex.value = (selectedIndex.value + 1) % items.length;\n return true;\n }\n if (event.key === \"ArrowUp\") {\n selectedIndex.value =\n (selectedIndex.value - 1 + items.length) % items.length;\n return true;\n }\n if (event.key === \"Enter\" || event.key === \"Tab\") {\n onSelect(items[selectedIndex.value]);\n return true;\n }\n return false;\n}\n\n/** Options for opening the merge-tag popup. */\nexport interface MergeTagPopupOpenOptions {\n /** Filtered tags to render. */\n items: MergeTag[];\n /**\n * Anchor rect getter (the caret rect). Same contract as\n * `@tiptap/suggestion`'s `clientRect`: viewport coordinates, re-read on\n * every reposition so the popup tracks the caret through scroll.\n */\n getRect: (() => DOMRect | null) | null;\n /**\n * Element that owns the ARIA combobox state and serves as the theme +\n * scroll-ancestor anchor — the ProseMirror `view.dom` in rich text, the\n * `<input>`/`<textarea>` in a field.\n */\n anchorEl: HTMLElement | null;\n /** Called with the item the user picks. */\n onCommand: (item: MergeTag) => void;\n}\n\n/** Options for updating the popup after the query changed. */\nexport interface MergeTagPopupUpdateOptions {\n items: MergeTag[];\n getRect: (() => DOMRect | null) | null;\n onCommand: (item: MergeTag) => void;\n}\n\n/**\n * Imperative handle over a single merge-tag suggestion popup.\n * Both the TipTap suggestion renderer and the input/textarea autocomplete\n * driver drive the SAME controller, so the popup's rendering, positioning,\n * scroll-tracking, ARIA wiring, and keyboard behavior are identical across\n * rich-text and plain-field surfaces by construction.\n */\nexport interface MergeTagPopupController {\n /** Mount the popup and start tracking the caret. */\n open(options: MergeTagPopupOpenOptions): void;\n /** Refresh items + anchor after the query changed. */\n update(options: MergeTagPopupUpdateOptions): void;\n /**\n * Handle a navigation/selection key (Arrow/Enter/Tab). Returns whether it\n * was handled. Does NOT handle Escape — each caller owns Escape (TipTap's\n * plugin closes via `onExit`; the field driver calls `close()`).\n */\n handleKeyDown(event: KeyboardEvent): boolean;\n /** Unmount the popup, clear ARIA, and detach listeners. Idempotent. */\n close(): void;\n /** Whether the popup is currently mounted. */\n isOpen(): boolean;\n}\n\n/**\n * Creates a merge-tag suggestion popup controller: mount, ARIA wiring,\n * caret-tracking position (flip-above + off-screen freeze), scroll/resize\n * repositioning, and cleanup. Framework-agnostic — it takes a plain caret\n * rect getter and an anchor element, so it serves both the ProseMirror\n * suggestion plugin and native text fields.\n */\nexport function createMergeTagPopup(\n emptyText: string,\n popoverRootRef?: Ref<HTMLElement | null> | null,\n): MergeTagPopupController {\n let app: App | null = null;\n let container: HTMLElement | null = null;\n let editableEl: HTMLElement | null = null;\n const itemsRef = ref<MergeTag[]>([]);\n const selectedIndex = ref(0);\n let currentCommand: ((item: MergeTag) => void) | null = null;\n const listId = `tpl-merge-tag-suggestion-${++POPUP_ID_SEQ}`;\n let latestClientRect: (() => DOMRect | null) | null = null;\n let scrollTargets: Array<EventTarget> = [];\n let pendingRaf: number | null = null;\n\n function reposition(): void {\n position(latestClientRect?.() ?? null);\n }\n\n /**\n * Reposition immediately and on the next animation frame.\n * After a keystroke triggers the suggestion, TipTap may run its\n * own `scrollIntoView` to keep the caret visible. That scroll\n * lands AFTER the current task's `position()` call but BEFORE\n * the browser's next paint, so we re-measure on rAF to catch\n * the post-scroll caret rect. Without this, the popup pins to\n * the pre-scroll caret position and ends up offset on slower\n * runners.\n */\n function repositionAfterPaint(): void {\n reposition();\n if (pendingRaf !== null) cancelAnimationFrame(pendingRaf);\n pendingRaf = requestAnimationFrame(() => {\n pendingRaf = null;\n reposition();\n });\n }\n\n function collectScrollAncestors(el: HTMLElement | null): HTMLElement[] {\n // Walk up the DOM finding scrollable ancestors. ProseMirror's\n // scrollIntoView fires on whichever ancestor scrolls — listening\n // to all of them ensures we reposition regardless of which one\n // moves.\n const result: HTMLElement[] = [];\n let node: HTMLElement | null = el?.parentElement ?? null;\n while (\n node &&\n // shadow-ok: ancestor walk terminator; not a mount target\n node !== document.body &&\n node !== document.documentElement\n ) {\n const style = window.getComputedStyle(node);\n const overflow = style.overflow + style.overflowX + style.overflowY;\n if (/(auto|scroll|overlay)/.test(overflow)) {\n result.push(node);\n }\n node = node.parentElement;\n }\n return result;\n }\n\n function attachScrollListeners(viewDom: HTMLElement | null): void {\n scrollTargets = [window, ...collectScrollAncestors(viewDom)];\n for (const target of scrollTargets) {\n target.addEventListener(\"scroll\", reposition, {\n passive: true,\n capture: true,\n });\n }\n window.addEventListener(\"resize\", reposition, { passive: true });\n }\n\n function detachScrollListeners(): void {\n for (const target of scrollTargets) {\n target.removeEventListener(\"scroll\", reposition, {\n capture: true,\n } as EventListenerOptions);\n }\n window.removeEventListener(\"resize\", reposition);\n scrollTargets = [];\n }\n\n function position(rect: DOMRect | null): void {\n if (!container || !rect) return;\n // If the caret has scrolled out of the viewport, freeze the\n // popup at its last on-screen position. Following the caret\n // off-screen produces an invisible popup the user can't reach,\n // and lets pathological scroll loops drag the popup further\n // each tick.\n if (rect.bottom < 0 || rect.top > window.innerHeight) return;\n // Position `absolute` relative to the mount target — the popover root\n // (`position: relative`) or the <body> fallback. The caret rect is in\n // viewport space, so subtract the offset parent's rect to get local\n // coords: a transformed ancestor of the editor becomes the containing\n // block for a `fixed` popup and would offset it, but not for an\n // `absolute` popup anchored inside the positioned popover root. The\n // off-screen freeze and flip-above decisions still use viewport metrics.\n container.style.position = \"absolute\";\n container.style.zIndex = \"9999\";\n const origin = container.offsetParent?.getBoundingClientRect();\n const originTop = origin?.top ?? 0;\n const originLeft = origin?.left ?? 0;\n container.style.left = `${rect.left - originLeft}px`;\n // Place below caret first; offsetHeight is sync-readable after\n // the Vue app has mounted (or after onUpdate's reactive flush).\n container.style.top = `${rect.bottom - originTop}px`;\n const popupHeight = container.offsetHeight;\n if (popupHeight === 0) return;\n const spaceBelow = window.innerHeight - rect.bottom;\n if (spaceBelow < popupHeight) {\n // Not enough room below — flip above. Clamp to 0 so the\n // popup never positions off the top of the viewport.\n const flippedTop = Math.max(0, rect.top - popupHeight);\n container.style.top = `${flippedTop - originTop}px`;\n }\n }\n\n function applyThemeContext(\n target: HTMLElement,\n editorEl: HTMLElement | null | undefined,\n ): void {\n // When the popup mounts to document.body its `position: fixed`\n // resolves against the viewport — any transform/filter on a\n // consumer-page ancestor (route transitions, reveal animations,\n // dark canvas inversion) creates a containing block and moves\n // fixed descendants with it. Body ancestors don't transform.\n //\n // When mounting inside the editor's popover root, the popup is a\n // descendant of the `.tpl[data-tpl-theme]` root so CSS vars + font\n // would inherit — but the snapshot below is still emitted inline\n // because inline declarations win and the cost is negligible. This\n // keeps a single behavior across both mount paths.\n //\n // CSS vars (--tpl-bg-elevated, --tpl-border, etc.) are scoped to\n // `.tpl` and `.tpl[data-tpl-theme=\"dark\"]` in the editor's\n // stylesheet, so the popup wouldn't inherit them at body root.\n // Adding `class=\"tpl\"` would also pull base rules (min-height,\n // flex, full-page bg) we don't want on a popup. Instead, snapshot\n // every --tpl-* custom property from the editor's theme root and\n // re-emit them inline on the popup wrapper.\n const themeRoot = editorEl?.closest<HTMLElement>(\"[data-tpl-theme]\");\n if (!themeRoot) return;\n const themeValue = themeRoot.getAttribute(\"data-tpl-theme\");\n if (themeValue) target.setAttribute(\"data-tpl-theme\", themeValue);\n const computed = window.getComputedStyle(themeRoot);\n for (let i = 0; i < computed.length; i++) {\n const prop = computed[i];\n if (prop.startsWith(\"--tpl-\")) {\n target.style.setProperty(prop, computed.getPropertyValue(prop));\n }\n }\n // The popup is teleported out of the editor wrapper, so it inherits no\n // font from it: without this its content renders in the page's default font\n // and ends up at a different height — which changes the flip-above decision\n // and shifts the popup off the caret. Copy typography too.\n target.style.fontFamily = computed.fontFamily;\n target.style.fontSize = computed.fontSize;\n target.style.lineHeight = computed.lineHeight;\n }\n\n function setEditableAria(active: boolean): void {\n if (!editableEl) return;\n if (active) {\n editableEl.setAttribute(\"role\", \"combobox\");\n editableEl.setAttribute(\"aria-haspopup\", \"listbox\");\n editableEl.setAttribute(\"aria-expanded\", \"true\");\n editableEl.setAttribute(\"aria-controls\", listId);\n } else {\n editableEl.removeAttribute(\"aria-expanded\");\n editableEl.removeAttribute(\"aria-controls\");\n editableEl.removeAttribute(\"aria-activedescendant\");\n editableEl.removeAttribute(\"aria-haspopup\");\n editableEl.removeAttribute(\"role\");\n }\n }\n\n function setActiveDescendant(): void {\n if (!editableEl) return;\n if (itemsRef.value.length === 0) {\n editableEl.removeAttribute(\"aria-activedescendant\");\n return;\n }\n editableEl.setAttribute(\n \"aria-activedescendant\",\n `${listId}-opt-${selectedIndex.value}`,\n );\n }\n\n function select(item: MergeTag): void {\n currentCommand?.(item);\n }\n\n return {\n open: ({ items, getRect, anchorEl, onCommand }) => {\n itemsRef.value = items;\n selectedIndex.value = 0;\n currentCommand = onCommand;\n\n container = document.createElement(\"div\");\n container.setAttribute(\"data-testid\", \"merge-tag-suggestion-popup\");\n editableEl = anchorEl;\n applyThemeContext(container, anchorEl);\n // Prefer the editor's popover root (shadow-aware) when wired by\n // the consumer. Falls back to document.body for headless callers\n // that don't pass `popoverRoot` to configure().\n // shadow-ok: fallback when popoverRoot wasn't provided (e.g., headless caller); editor mounts pass the shadow-aware root\n const mountTarget = popoverRootRef?.value ?? document.body;\n mountTarget.appendChild(container);\n\n app = createApp({\n render() {\n return h(MergeTagSuggestionList, {\n items: itemsRef.value,\n selectedIndex: selectedIndex.value,\n emptyText,\n listId,\n onSelect: (item: MergeTag) => select(item),\n onHover: (index: number) => {\n selectedIndex.value = index;\n setActiveDescendant();\n },\n });\n },\n });\n app.mount(container);\n setEditableAria(true);\n setActiveDescendant();\n latestClientRect = getRect;\n repositionAfterPaint();\n attachScrollListeners(anchorEl);\n },\n update: ({ items, getRect, onCommand }) => {\n itemsRef.value = items;\n // Reset selection when item set changes (query changed).\n if (selectedIndex.value >= items.length) {\n selectedIndex.value = 0;\n }\n currentCommand = onCommand;\n setActiveDescendant();\n latestClientRect = getRect;\n repositionAfterPaint();\n },\n handleKeyDown: (event: KeyboardEvent): boolean => {\n const handled = handleSuggestionKeyDown(\n event,\n itemsRef.value,\n selectedIndex,\n select,\n );\n if (handled) setActiveDescendant();\n return handled;\n },\n close: () => {\n // Idempotent: nothing to tear down (and no window listeners to detach)\n // when the popup was never opened. Keeps repeated close() / close-before-\n // open cheap and side-effect-free.\n if (container === null) return;\n if (pendingRaf !== null) {\n cancelAnimationFrame(pendingRaf);\n pendingRaf = null;\n }\n detachScrollListeners();\n setEditableAria(false);\n app?.unmount();\n container.remove();\n app = null;\n container = null;\n editableEl = null;\n currentCommand = null;\n latestClientRect = null;\n },\n isOpen: () => container !== null,\n };\n}\n\n/**\n * Builds the `@tiptap/suggestion` `render` factory for the merge-tag popup.\n * A thin adapter over {@link createMergeTagPopup}: it maps ProseMirror's\n * `SuggestionProps` (items, `clientRect`, `view.dom`, `command`) onto the\n * controller. Exported as a standalone function so that mapping can be\n * unit-tested with mocked `SuggestionProps` instead of a full editor.\n */\nexport function createMergeTagSuggestionRenderer(\n emptyText: string,\n popoverRootRef?: Ref<HTMLElement | null> | null,\n): NonNullable<SuggestionOptions<MergeTag>[\"render\"]> {\n return () => {\n const popup = createMergeTagPopup(emptyText, popoverRootRef);\n\n return {\n onStart: (props: SuggestionProps<MergeTag>) => {\n // Use view.dom (ProseMirror contenteditable, actually\n // attached to the DOM) rather than options.element, which may\n // be a detached div when no `element` is passed to the editor\n // constructor (as is the case with @tiptap/vue-3 EditorContent).\n const viewDom = props.editor.view?.dom as HTMLElement | undefined;\n popup.open({\n items: props.items,\n getRect: props.clientRect ?? null,\n anchorEl: viewDom ?? null,\n onCommand: (item) => props.command(item),\n });\n },\n onUpdate: (props: SuggestionProps<MergeTag>) => {\n popup.update({\n items: props.items,\n getRect: props.clientRect ?? null,\n onCommand: (item) => props.command(item),\n });\n },\n onKeyDown: (props: SuggestionKeyDownProps): boolean => {\n // Escape closes via TipTap's suggestion plugin (→ onExit); report it\n // handled without selecting.\n if (props.event.key === \"Escape\") {\n return true;\n }\n return popup.handleKeyDown(props.event);\n },\n onExit: () => popup.close(),\n };\n };\n}\n\nexport const MergeTagSuggestion = Extension.create<MergeTagSuggestionOptions>({\n name: \"mergeTagSuggestion\",\n\n addOptions() {\n return {\n mergeTags: [],\n char: \"{{\",\n emptyText: \"No matching merge tags\",\n popoverRoot: null,\n };\n },\n\n addProseMirrorPlugins() {\n const tags = this.options.mergeTags;\n const emptyText = this.options.emptyText;\n const popoverRootRef = this.options.popoverRoot;\n\n const config: Omit<SuggestionOptions<MergeTag>, \"editor\"> = {\n char: this.options.char,\n allowSpaces: false,\n startOfLine: false,\n // Default is [\" \"] which requires whitespace/line-start before the\n // trigger char — so `.{{` would not fire. Allow any preceding char.\n allowedPrefixes: null,\n items: ({ query }: { query: string }) => filterMergeTags(tags, query),\n command: ({\n editor,\n range,\n props,\n }: {\n editor: SuggestionProps<MergeTag>[\"editor\"];\n range: { from: number; to: number };\n props: MergeTag;\n }) => {\n // Use insertContentAt for atomic replace (matches the canonical\n // @tiptap/suggestion + Mention pattern). Avoids edge cases where\n // chained deleteRange + insertMergeTag fails to insert when the\n // selection state shifts mid-chain.\n editor\n .chain()\n .focus()\n .insertContentAt(range, {\n type: \"mergeTagNode\",\n attrs: { label: props.label, value: props.value },\n })\n .run();\n },\n render: createMergeTagSuggestionRenderer(emptyText, popoverRootRef),\n };\n\n return [\n Suggestion({\n editor: this.editor,\n ...config,\n }),\n ];\n },\n});\n"],"mappings":"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"}
|