@templatical/editor 0.32.0 → 0.34.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.
Files changed (154) hide show
  1. package/dist/{AiChatSidebar-ftdKHSR-.js → AiChatSidebar-CkHJ28sf.js} +3 -3
  2. package/dist/{AiFeatureMenu-aNeCRlil.js → AiFeatureMenu-DWdKguDN.js} +3 -3
  3. package/dist/{BlockIssueBadge-bTDu6W_2.js → BlockIssueBadge-DcyHTAqw.js} +2 -2
  4. package/dist/{BlockPreviewCanvas-BsOuOfzB.js → BlockPreviewCanvas-B621ANn9.js} +4 -4
  5. package/dist/{CloudHeaderExtras-BNpFohqt.js → CloudHeaderExtras-BJ3Cebhv.js} +2 -2
  6. package/dist/{CloudPanels-xoGWwqSH.js → CloudPanels-BGz9ygo6.js} +6 -6
  7. package/dist/{CollaboratorBar-mEqXkdEi.js → CollaboratorBar-KphmSQeZ.js} +2 -2
  8. package/dist/{ColorPicker-FL5Vb9bE.js → ColorPicker-C6EOwtHC.js} +5 -5
  9. package/dist/{CommentsPanel-slfi74ng.js → CommentsPanel-DmWh8f08.js} +1 -1
  10. package/dist/{CommentsSidebar-Dea4spMl.js → CommentsSidebar-CL_tffzT.js} +4 -4
  11. package/dist/{CountdownBlock-DQTwaRxP.js → CountdownBlock-D1fsNsyS.js} +1 -1
  12. package/dist/{CountdownToolbar-C7xwJoqh.js → CountdownToolbar-B8J4i5Xp.js} +2 -2
  13. package/dist/{DesignReferenceSidebar-BtBd7bbz.js → DesignReferenceSidebar-BDhEeaAh.js} +4 -4
  14. package/dist/{IssuesPanel-Ci2H36L2.js → IssuesPanel-CiOjeRFI.js} +4 -4
  15. package/dist/MergeTagInput-RytOlh1A.js +448 -0
  16. package/dist/{MergeTagModeToggle-Cdi_DmVw.js → MergeTagModeToggle-Bi-FydXj.js} +2 -2
  17. package/dist/{ParagraphEditor-DQEOFc0K.js → ParagraphEditor-C87tkmtE.js} +12 -12
  18. package/dist/{RestoreVersionDialog-CHwngBae.js → RestoreVersionDialog-Db5rZRMA.js} +2 -2
  19. package/dist/RichTextEditorContent-BmTcV2-a.js +152 -0
  20. package/dist/{SaveBlockDialog-tZqZzPjW.js → SaveBlockDialog-CDCzx6Bo.js} +15 -15
  21. package/dist/{SavedBlocksBrowserModal-Bdj3GOD_.js → SavedBlocksBrowserModal-DQR6kJB1.js} +101 -101
  22. package/dist/{SavedBlocksPanels-2sBxAH9w.js → SavedBlocksPanels-DFzV0CRD.js} +1 -1
  23. package/dist/{SavedBlocksPickBar-DTjQBR00.js → SavedBlocksPickBar-XyXmnSnO.js} +2 -2
  24. package/dist/{TemplateScoringPanel-B6nHSrBD.js → TemplateScoringPanel-CpyEKMzC.js} +4 -4
  25. package/dist/{TestEmailModal-DuHbzO37.js → TestEmailModal-DIfJy9eE.js} +8 -8
  26. package/dist/{TestEmailPanel-TpwP-eK_.js → TestEmailPanel-C8Wx5yPS.js} +1 -1
  27. package/dist/{TitleEditor-C0JQJu38.js → TitleEditor-iNFOisr4.js} +11 -11
  28. package/dist/{TplModal-BMGmhquu.js → TplModal-CowM3o_F.js} +5 -5
  29. package/dist/{VersionHistoryMenu-D2x6MMYx.js → VersionHistoryMenu-CV90OhXs.js} +2 -2
  30. package/dist/{VersionHistoryPanels-aGlUVEGF.js → VersionHistoryPanels-BE3pplJG.js} +1 -1
  31. package/dist/{VersionPreviewBanner-61t3_Z-a.js → VersionPreviewBanner-BwUWBtlH.js} +1 -1
  32. package/dist/{blockTypeIcons-BsNup009.js → blockTypeIcons-DQsKtHik.js} +1 -1
  33. package/dist/bundle-stats.json +6 -6
  34. package/dist/cdn/chunks/{AiChatSidebar-CqWvtRwB.js → AiChatSidebar-BEnIWn6s.js} +4 -4
  35. package/dist/cdn/chunks/AiChatSidebar-BEnIWn6s.js.map +1 -0
  36. package/dist/cdn/chunks/{AiFeatureMenu-DoFrLcXJ.js → AiFeatureMenu-DMl23akc.js} +4 -4
  37. package/dist/cdn/chunks/{AiFeatureMenu-DoFrLcXJ.js.map → AiFeatureMenu-DMl23akc.js.map} +1 -1
  38. package/dist/cdn/chunks/{BlockIssueBadge-B_mUH69a.js → BlockIssueBadge-CloK4k3M.js} +3 -3
  39. package/dist/cdn/chunks/{BlockIssueBadge-B_mUH69a.js.map → BlockIssueBadge-CloK4k3M.js.map} +1 -1
  40. package/dist/cdn/chunks/{BlockPreviewCanvas-CSIxs7hr.js → BlockPreviewCanvas-DSiJFJa0.js} +5 -5
  41. package/dist/cdn/chunks/{BlockPreviewCanvas-CSIxs7hr.js.map → BlockPreviewCanvas-DSiJFJa0.js.map} +1 -1
  42. package/dist/cdn/chunks/{CloudHeaderExtras-Dgtr7WaE.js → CloudHeaderExtras-b4KpVeUN.js} +3 -3
  43. package/dist/cdn/chunks/{CloudHeaderExtras-Dgtr7WaE.js.map → CloudHeaderExtras-b4KpVeUN.js.map} +1 -1
  44. package/dist/cdn/chunks/{CloudPanels-B91lUmof.js → CloudPanels-DzwOq9lp.js} +7 -7
  45. package/dist/cdn/chunks/CloudPanels-DzwOq9lp.js.map +1 -0
  46. package/dist/cdn/chunks/{CollaboratorBar-CQwPWoFu.js → CollaboratorBar-WXKvpoGK.js} +3 -3
  47. package/dist/cdn/chunks/{CollaboratorBar-CQwPWoFu.js.map → CollaboratorBar-WXKvpoGK.js.map} +1 -1
  48. package/dist/cdn/chunks/{ColorPicker-CdrKX48J.js → ColorPicker-BubTDiS1.js} +6 -6
  49. package/dist/cdn/chunks/{ColorPicker-CdrKX48J.js.map → ColorPicker-BubTDiS1.js.map} +1 -1
  50. package/dist/cdn/chunks/{CommentsPanel-BJNgTmAA.js → CommentsPanel-DAA2VqX4.js} +2 -2
  51. package/dist/cdn/chunks/{CommentsPanel-BJNgTmAA.js.map → CommentsPanel-DAA2VqX4.js.map} +1 -1
  52. package/dist/cdn/chunks/{CommentsSidebar-D6VIIPhB.js → CommentsSidebar-Ci0M2ePp.js} +5 -5
  53. package/dist/cdn/chunks/CommentsSidebar-Ci0M2ePp.js.map +1 -0
  54. package/dist/cdn/chunks/{CountdownBlock-BRwXUvGU.js → CountdownBlock-rK055jpy.js} +2 -2
  55. package/dist/cdn/chunks/{CountdownBlock-BRwXUvGU.js.map → CountdownBlock-rK055jpy.js.map} +1 -1
  56. package/dist/cdn/chunks/{CountdownToolbar-CVaU5WG6.js → CountdownToolbar-CzZq26Vp.js} +3 -3
  57. package/dist/cdn/chunks/{CountdownToolbar-CVaU5WG6.js.map → CountdownToolbar-CzZq26Vp.js.map} +1 -1
  58. package/dist/cdn/chunks/{DesignReferenceSidebar-BvOwE1kg.js → DesignReferenceSidebar-C_ehdX5C.js} +5 -5
  59. package/dist/cdn/chunks/DesignReferenceSidebar-C_ehdX5C.js.map +1 -0
  60. package/dist/cdn/chunks/{IssuesPanel-MJZxkScz.js → IssuesPanel-BXAN2_al.js} +5 -5
  61. package/dist/cdn/chunks/{IssuesPanel-MJZxkScz.js.map → IssuesPanel-BXAN2_al.js.map} +1 -1
  62. package/dist/cdn/chunks/MergeTagInput-1vcI9uQH.js +419 -0
  63. package/dist/cdn/chunks/MergeTagInput-1vcI9uQH.js.map +1 -0
  64. package/dist/cdn/chunks/{MergeTagModeToggle-bNuN6PTV.js → MergeTagModeToggle-B_mUYJo4.js} +3 -3
  65. package/dist/cdn/chunks/{MergeTagModeToggle-bNuN6PTV.js.map → MergeTagModeToggle-B_mUYJo4.js.map} +1 -1
  66. package/dist/cdn/chunks/{ParagraphEditor-BU8tlFbL.js → ParagraphEditor-DCDHMcmx.js} +10 -10
  67. package/dist/cdn/chunks/ParagraphEditor-DCDHMcmx.js.map +1 -0
  68. package/dist/cdn/chunks/{RestoreVersionDialog-vdRFfyy0.js → RestoreVersionDialog-BkSSwf-Q.js} +3 -3
  69. package/dist/cdn/chunks/{RestoreVersionDialog-vdRFfyy0.js.map → RestoreVersionDialog-BkSSwf-Q.js.map} +1 -1
  70. package/dist/cdn/chunks/RichTextEditorContent-S4KogjIj.js +122 -0
  71. package/dist/cdn/chunks/RichTextEditorContent-S4KogjIj.js.map +1 -0
  72. package/dist/cdn/chunks/{SaveBlockDialog-DWaeqcM8.js → SaveBlockDialog-BH774Nt0.js} +14 -14
  73. package/dist/cdn/chunks/{SaveBlockDialog-DWaeqcM8.js.map → SaveBlockDialog-BH774Nt0.js.map} +1 -1
  74. package/dist/cdn/chunks/{SavedBlocksBrowserModal-qC7UhW74.js → SavedBlocksBrowserModal-CSUqw_dD.js} +6 -6
  75. package/dist/cdn/chunks/{SavedBlocksBrowserModal-qC7UhW74.js.map → SavedBlocksBrowserModal-CSUqw_dD.js.map} +1 -1
  76. package/dist/cdn/chunks/{SavedBlocksPanels-BvtjtdsY.js → SavedBlocksPanels-B6ApbCjG.js} +2 -2
  77. package/dist/cdn/chunks/{SavedBlocksPanels-BvtjtdsY.js.map → SavedBlocksPanels-B6ApbCjG.js.map} +1 -1
  78. package/dist/cdn/chunks/{SavedBlocksPickBar-CxXjF3RN.js → SavedBlocksPickBar-C9MXSRcR.js} +3 -3
  79. package/dist/cdn/chunks/{SavedBlocksPickBar-CxXjF3RN.js.map → SavedBlocksPickBar-C9MXSRcR.js.map} +1 -1
  80. package/dist/cdn/chunks/{TemplateScoringPanel-CroWaWqy.js → TemplateScoringPanel-QVOxc9TZ.js} +50 -50
  81. package/dist/cdn/chunks/TemplateScoringPanel-QVOxc9TZ.js.map +1 -0
  82. package/dist/cdn/chunks/{TestEmailModal-iWxAZRRq.js → TestEmailModal-_CZvaO58.js} +9 -9
  83. package/dist/cdn/chunks/{TestEmailModal-iWxAZRRq.js.map → TestEmailModal-_CZvaO58.js.map} +1 -1
  84. package/dist/cdn/chunks/{TestEmailPanel-CGUtEuwp.js → TestEmailPanel-DXLxvyrD.js} +2 -2
  85. package/dist/cdn/chunks/{TestEmailPanel-CGUtEuwp.js.map → TestEmailPanel-DXLxvyrD.js.map} +1 -1
  86. package/dist/cdn/chunks/{TitleEditor-DvXUW2j-.js → TitleEditor-BuCOahW3.js} +9 -9
  87. package/dist/cdn/chunks/TitleEditor-BuCOahW3.js.map +1 -0
  88. package/dist/cdn/chunks/{TplModal-X-qNoLGZ.js → TplModal-CqY8R3hn.js} +6 -6
  89. package/dist/cdn/chunks/TplModal-CqY8R3hn.js.map +1 -0
  90. package/dist/cdn/chunks/{VersionHistoryMenu-yKzwxf7n.js → VersionHistoryMenu-DDjIBvad.js} +3 -3
  91. package/dist/cdn/chunks/{VersionHistoryMenu-yKzwxf7n.js.map → VersionHistoryMenu-DDjIBvad.js.map} +1 -1
  92. package/dist/cdn/chunks/{VersionHistoryPanels--hQaanjH.js → VersionHistoryPanels-BuTw04T5.js} +2 -2
  93. package/dist/cdn/chunks/{VersionHistoryPanels--hQaanjH.js.map → VersionHistoryPanels-BuTw04T5.js.map} +1 -1
  94. package/dist/cdn/chunks/{VersionPreviewBanner-DWU_SqMd.js → VersionPreviewBanner-fGAv20qI.js} +2 -2
  95. package/dist/cdn/chunks/{VersionPreviewBanner-DWU_SqMd.js.map → VersionPreviewBanner-fGAv20qI.js.map} +1 -1
  96. package/dist/cdn/chunks/{createCloudRuntime-DD6ppb71.js → createCloudRuntime-JFE-sxbT.js} +2 -2
  97. package/dist/cdn/chunks/{createCloudRuntime-DD6ppb71.js.map → createCloudRuntime-JFE-sxbT.js.map} +1 -1
  98. package/dist/cdn/chunks/{extensions-BUPHNpKD.js → extensions-7z04EIGg.js} +3 -3
  99. package/dist/cdn/chunks/{extensions-BUPHNpKD.js.map → extensions-7z04EIGg.js.map} +1 -1
  100. package/dist/cdn/chunks/keys-DQp8x7Nx.js +12 -0
  101. package/dist/cdn/chunks/keys-DQp8x7Nx.js.map +1 -0
  102. package/dist/cdn/chunks/src-CLoCCDfs.js.map +1 -1
  103. package/dist/cdn/chunks/{src-DtnvR_Hl.js → src-CiAi5oRe.js} +2 -2
  104. package/dist/cdn/chunks/{src-DtnvR_Hl.js.map → src-CiAi5oRe.js.map} +1 -1
  105. package/dist/cdn/chunks/{useCloudI18n-DbsLlCbZ.js → useCloudI18n-MxgHWrV6.js} +2 -2
  106. package/dist/cdn/chunks/{useCloudI18n-DbsLlCbZ.js.map → useCloudI18n-MxgHWrV6.js.map} +1 -1
  107. package/dist/cdn/chunks/{useEditorCore-ChlgO2GN.js → useEditorCore-ehUSEsbw.js} +548 -511
  108. package/dist/cdn/chunks/useEditorCore-ehUSEsbw.js.map +1 -0
  109. package/dist/cdn/chunks/{useEditorRoot-BlF1WPeu.js → useEditorRoot-CteADR3y.js} +2 -2
  110. package/dist/cdn/chunks/{useEditorRoot-BlF1WPeu.js.map → useEditorRoot-CteADR3y.js.map} +1 -1
  111. package/dist/cdn/chunks/{useI18n-BdxNNw6j.js → useI18n-Ct1VIFcf.js} +2 -2
  112. package/dist/cdn/chunks/{useI18n-BdxNNw6j.js.map → useI18n-Ct1VIFcf.js.map} +1 -1
  113. package/dist/cdn/chunks/useMergeTag-TLhF2fGK.js +46 -0
  114. package/dist/cdn/chunks/useMergeTag-TLhF2fGK.js.map +1 -0
  115. package/dist/cdn/chunks/{usePopoverPosition-Dw1nTSNx.js → usePopoverPosition-BxVWE5_0.js} +2 -2
  116. package/dist/cdn/chunks/{usePopoverPosition-Dw1nTSNx.js.map → usePopoverPosition-BxVWE5_0.js.map} +1 -1
  117. package/dist/cdn/chunks/{useScrollToBlock-BMis3-PR.js → useScrollToBlock-Cz3fpgTK.js} +2 -2
  118. package/dist/cdn/chunks/{useScrollToBlock-BMis3-PR.js.map → useScrollToBlock-Cz3fpgTK.js.map} +1 -1
  119. package/dist/cdn/editor.css +1 -1
  120. package/dist/cdn/editor.js +1722 -2071
  121. package/dist/cdn/editor.js.map +1 -1
  122. package/dist/{createCloudRuntime-zNcMjRgb.js → createCloudRuntime-DbTFm1ES.js} +1 -1
  123. package/dist/{extensions-kOYwOh72.js → extensions-Mf8iHey9.js} +2 -2
  124. package/dist/index.d.ts +41 -0
  125. package/dist/keys-C3P1yxnr.js +10 -0
  126. package/dist/style.css +1 -1
  127. package/dist/templatical-editor.js +1890 -2240
  128. package/dist/{useCloudI18n-BlMajbKp.js → useCloudI18n-BKoiYsWZ.js} +1 -1
  129. package/dist/{useEditorCore-CwQYCSXO.js → useEditorCore-DdkQuTHl.js} +1101 -1064
  130. package/dist/{useEditorRoot-C92q8uV-.js → useEditorRoot-DkRGTQRs.js} +1 -1
  131. package/dist/{useI18n-Ce_BRzYv.js → useI18n-CL6RfPzK.js} +1 -1
  132. package/dist/useMergeTag-gSuYus-M.js +44 -0
  133. package/dist/{usePopoverPosition-CPk7Twye.js → usePopoverPosition-CWxaIWd3.js} +1 -1
  134. package/dist/{useScrollToBlock-DvpMfWs2.js → useScrollToBlock-CdI8fGqt.js} +1 -1
  135. package/package.json +7 -7
  136. package/dist/RichTextEditorContent-C_9xi7dD.js +0 -153
  137. package/dist/cdn/chunks/AiChatSidebar-CqWvtRwB.js.map +0 -1
  138. package/dist/cdn/chunks/CloudPanels-B91lUmof.js.map +0 -1
  139. package/dist/cdn/chunks/CommentsSidebar-D6VIIPhB.js.map +0 -1
  140. package/dist/cdn/chunks/DesignReferenceSidebar-BvOwE1kg.js.map +0 -1
  141. package/dist/cdn/chunks/ParagraphEditor-BU8tlFbL.js.map +0 -1
  142. package/dist/cdn/chunks/RichTextEditorContent-C1-5I8Df.js +0 -123
  143. package/dist/cdn/chunks/RichTextEditorContent-C1-5I8Df.js.map +0 -1
  144. package/dist/cdn/chunks/TemplateScoringPanel-CroWaWqy.js.map +0 -1
  145. package/dist/cdn/chunks/TitleEditor-DvXUW2j-.js.map +0 -1
  146. package/dist/cdn/chunks/TplModal-X-qNoLGZ.js.map +0 -1
  147. package/dist/cdn/chunks/keys-BOn5i9jV.js +0 -12
  148. package/dist/cdn/chunks/keys-BOn5i9jV.js.map +0 -1
  149. package/dist/cdn/chunks/useEditorCore-ChlgO2GN.js.map +0 -1
  150. package/dist/cdn/chunks/useMergeTag-CI2WUNrR.js +0 -46
  151. package/dist/cdn/chunks/useMergeTag-CI2WUNrR.js.map +0 -1
  152. package/dist/keys-BOhFlHsv.js +0 -10
  153. package/dist/scan-line-E7w4yQyv.js +0 -31
  154. package/dist/useMergeTag-CrXJ49lr.js +0 -44
@@ -1 +0,0 @@
1
- {"version":3,"file":"CommentsSidebar-D6VIIPhB.js","names":[],"sources":["../../../src/components/CommentsSidebar.vue","../../../src/components/CommentsSidebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport { EDITOR_KEY, requireInject } from \"../keys\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type { UseCommentsFeatureReturn } from \"../composables/useCommentsFeature\";\nimport type { Comment } from \"@templatical/types\";\nimport {\n Check,\n CircleCheck,\n ChevronDown,\n ChevronUp,\n LoaderCircle,\n MessageCircle,\n Pencil,\n Reply,\n Send,\n Trash2,\n X,\n} from \"@lucide/vue\";\nimport { computed, nextTick, ref, watch } from \"vue\";\n\n/**\n * The review panel, **shared by both entry points**.\n *\n * It takes the shared feature as a prop — the `TestEmailPanel` /\n * `SavedBlocksPanels` shape — so the same panel drives a consumer's own store and\n * Cloud's, and its strings live in the OSS chunk.\n *\n * Every write action is gated on the provider having supplied that mutation, and\n * **hidden** rather than disabled when it hasn't: a provider that withholds all\n * four yields a readable review with no way to change anything.\n */\nconst props = defineProps<{\n visible: boolean;\n feature: UseCommentsFeatureReturn;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n}>();\n\nconst { t, format } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"CommentsSidebar\");\n\nconst comments = computed(() => props.feature.headless);\n\ntype FilterMode = \"all\" | \"unresolved\" | \"block\";\nconst filterMode = ref<FilterMode>(\"unresolved\");\n\nconst newCommentBody = ref(\"\");\nconst replyingTo = ref<string | null>(null);\nconst replyBody = ref(\"\");\nconst editingId = ref<string | null>(null);\nconst editBody = ref(\"\");\nconst expandedThreads = ref<Set<string>>(new Set());\nconst deletingId = ref<string | null>(null);\nconst newCommentInput = ref<HTMLTextAreaElement | null>(null);\n\n/**\n * The block filter lives on the feature, not here: a canvas comment indicator sets\n * it *before* this panel exists (it is lazy), so the target has to survive the gap.\n */\nconst filterBlockId = computed(() => props.feature.filterBlockId.value);\n\n// An indicator click lands a block id on the feature; switching to that filter is\n// this panel's half of the handshake. `immediate` covers the case that opened the\n// panel in the first place, where the id is already set at mount.\nwatch(\n filterBlockId,\n (blockId) => {\n if (blockId) filterMode.value = \"block\";\n },\n { immediate: true },\n);\n\n// `isLoading` alone would flash the skeleton over a perfectly good list every\n// re-open; the pair distinguishes \"fetching, nothing yet\" from \"refreshing\".\nconst isInitialLoad = computed(\n () =>\n comments.value.isLoading.value &&\n comments.value.comments.value.length === 0,\n);\n\nconst allBlockIds = computed<Set<string>>(() => {\n const ids = new Set<string>();\n for (const block of editor.content.value.blocks) {\n ids.add(block.id);\n if (block.type === \"section\") {\n for (const column of block.children) {\n for (const child of column) {\n ids.add(child.id);\n }\n }\n }\n }\n return ids;\n});\n\nfunction isBlockMissing(blockId: string | null | undefined): boolean {\n if (!blockId) {\n return false;\n }\n return !allBlockIds.value.has(blockId);\n}\n\nconst isBlockFilterUnsaved = computed(() => {\n if (filterMode.value !== \"block\" || !filterBlockId.value) {\n return false;\n }\n // Asked through the capability rather than off the editor: a comment anchors to\n // a block in the *saved* template, and only the provider's side knows what that\n // contains. A store that accepts any anchor answers `true` and this never fires.\n return props.feature.capability.isBlockSaved(filterBlockId.value) === false;\n});\n\nconst filteredComments = computed(() => {\n let items = comments.value.comments.value;\n\n if (filterMode.value === \"unresolved\") {\n items = items.filter((c) => !c.resolvedAt);\n } else if (filterMode.value === \"block\" && filterBlockId.value) {\n items = items.filter((c) => c.blockId === filterBlockId.value);\n }\n\n return items;\n});\n\nwatch(\n () => editor.state.selectedBlockId,\n (newBlockId) => {\n if (filterMode.value === \"block\" && newBlockId) {\n props.feature.filterBlockId.value = newBlockId;\n }\n },\n);\n\nfunction setFilter(mode: FilterMode, blockId?: string): void {\n filterMode.value = mode;\n props.feature.filterBlockId.value =\n mode === \"block\" ? (blockId ?? null) : null;\n}\n\nfunction filterByBlock(blockId: string): void {\n setFilter(\"block\", blockId);\n}\n\nfunction toggleThread(threadId: string): void {\n if (expandedThreads.value.has(threadId)) {\n expandedThreads.value.delete(threadId);\n } else {\n expandedThreads.value.add(threadId);\n }\n}\n\nfunction startReply(threadId: string): void {\n replyingTo.value = threadId;\n replyBody.value = \"\";\n editingId.value = null;\n\n if (!expandedThreads.value.has(threadId)) {\n expandedThreads.value.add(threadId);\n }\n}\n\nfunction cancelReply(): void {\n replyingTo.value = null;\n replyBody.value = \"\";\n}\n\nfunction startEdit(comment: Comment): void {\n editingId.value = comment.id;\n editBody.value = comment.body;\n replyingTo.value = null;\n}\n\nfunction cancelEdit(): void {\n editingId.value = null;\n editBody.value = \"\";\n}\n\nfunction confirmDelete(commentId: string): void {\n deletingId.value = commentId;\n}\n\nfunction cancelDelete(): void {\n deletingId.value = null;\n}\n\n/**\n * Every write swallows its rejection. The feature already routed it to `onError`\n * and left the list untouched, and an unhandled rejection out of a DOM event\n * binding helps nobody — same shape as `VersionHistoryPanels`.\n */\nasync function handleAddComment(): Promise<void> {\n const body = newCommentBody.value.trim();\n if (!body || !comments.value.canCreate.value) {\n return;\n }\n\n const blockId =\n filterMode.value === \"block\" ? filterBlockId.value : undefined;\n try {\n await comments.value.create({ body, blockId: blockId ?? undefined });\n newCommentBody.value = \"\";\n } catch {\n /* reported through onError */\n }\n}\n\nasync function handleReply(parentId: string): Promise<void> {\n const body = replyBody.value.trim();\n if (!body || !comments.value.canCreate.value) {\n return;\n }\n\n const parent = comments.value.find(parentId);\n try {\n await comments.value.create({\n body,\n blockId: parent?.blockId ?? undefined,\n parentId,\n });\n replyingTo.value = null;\n replyBody.value = \"\";\n } catch {\n /* reported through onError */\n }\n}\n\nasync function handleEdit(commentId: string): Promise<void> {\n const body = editBody.value.trim();\n if (!body) {\n return;\n }\n\n try {\n await comments.value.update(commentId, { body });\n editingId.value = null;\n editBody.value = \"\";\n } catch {\n /* reported through onError */\n }\n}\n\nasync function handleDelete(commentId: string): Promise<void> {\n try {\n await comments.value.remove(commentId);\n } catch {\n /* reported through onError */\n }\n deletingId.value = null;\n}\n\nasync function handleResolve(comment: Comment): Promise<void> {\n try {\n // The target state, not a toggle: two clicks in flight can't invert.\n await comments.value.setResolved(comment.id, !comment.resolvedAt);\n } catch {\n /* reported through onError */\n }\n}\n\n/** Editing and deleting are the author's own, and only when the store allows it. */\nfunction canEdit(comment: Comment): boolean {\n return comments.value.canUpdate.value && comments.value.isOwn(comment);\n}\n\nfunction canDelete(comment: Comment): boolean {\n return comments.value.canDelete.value && comments.value.isOwn(comment);\n}\n\nfunction formatTime(dateString: string): string {\n return formatRelativeTime(dateString, t.time) ?? dateString;\n}\n\nfunction handleNewCommentKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleAddComment();\n }\n}\n\nfunction handleReplyKeydown(event: KeyboardEvent, parentId: string): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleReply(parentId);\n }\n}\n\nfunction handleEditKeydown(event: KeyboardEvent, commentId: string): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleEdit(commentId);\n }\n if (event.key === \"Escape\") {\n cancelEdit();\n }\n}\n\nfunction focusNewComment(): void {\n nextTick(() => {\n newCommentInput.value?.focus();\n });\n}\n\ndefineExpose({ filterByBlock, focusNewComment });\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-comments-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-comments-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 data-testid=\"comments-sidebar\"\n class=\"tpl-comments-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-text)]\"\n >\n <MessageCircle :size=\"13\" :stroke-width=\"2\" />\n <span>{{ t.comments.title }}</span>\n <span\n v-if=\"feature.unresolvedCount.value > 0\"\n class=\"tpl:ml-1 tpl:inline-flex tpl:size-5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n >\n {{ feature.unresolvedCount.value }}\n </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 <!-- Filter bar -->\n <div\n class=\"tpl:flex tpl:gap-1 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-2\"\n >\n <button\n class=\"tpl-comment-filter tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors\"\n :class=\"\n filterMode === 'unresolved' ? 'tpl-comment-filter--active' : ''\n \"\n data-testid=\"comments-filter-unresolved\"\n @click=\"setFilter('unresolved')\"\n >\n {{ t.comments.filterUnresolved }}\n </button>\n <button\n class=\"tpl-comment-filter tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors\"\n :class=\"filterMode === 'all' ? 'tpl-comment-filter--active' : ''\"\n data-testid=\"comments-filter-all\"\n @click=\"setFilter('all')\"\n >\n {{ t.comments.filterAll }}\n </button>\n <button\n v-if=\"editor.state.selectedBlockId\"\n class=\"tpl-comment-filter tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors\"\n :class=\"filterMode === 'block' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('block', editor.state.selectedBlockId ?? undefined)\"\n >\n {{ t.comments.filterBlock }}\n </button>\n </div>\n\n <!-- Thread list -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto\">\n <!-- Loading state -->\n <div\n v-if=\"isInitialLoad\"\n class=\"tpl:flex tpl:h-full tpl:items-center tpl:justify-center\"\n >\n <LoaderCircle\n class=\"tpl-spinner tpl:text-[var(--tpl-text-muted)]\"\n :size=\"24\"\n :stroke-width=\"2\"\n />\n </div>\n\n <!-- Empty state -->\n <div\n v-else-if=\"filteredComments.length === 0\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:px-6 tpl:text-center\"\n >\n <MessageCircle\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:max-w-[240px] tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n filterMode === \"all\"\n ? t.comments.noCommentsHint\n : t.comments.noComments\n }}\n </p>\n </div>\n\n <!-- Comment threads -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-3 tpl:p-3\">\n <div\n v-for=\"thread in filteredComments\"\n :key=\"thread.id\"\n class=\"tpl-comment-thread\"\n data-testid=\"comment-thread\"\n >\n <!-- Root comment card -->\n <div\n class=\"tpl-comment-card tpl:rounded-lg tpl:border tpl:px-3.5 tpl:py-3\"\n >\n <!-- Comment header -->\n <div\n class=\"tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <span\n class=\"tpl:text-xs tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{\n comments.isOwn(thread)\n ? t.comments.ownedByYou\n : thread.author.name\n }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ formatTime(thread.createdAt) }}\n </span>\n <span\n v-if=\"thread.updatedAt\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ t.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <!-- Resolve toggle -->\n <button\n v-if=\"comments.canResolve.value\"\n data-testid=\"comment-resolve\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"\n thread.resolvedAt\n ? t.comments.unresolve\n : t.comments.resolve\n \"\n @click=\"handleResolve(thread)\"\n >\n <CircleCheck\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl-resolve-icon\"\n :style=\"{\n color: thread.resolvedAt\n ? 'var(--tpl-primary)'\n : undefined,\n }\"\n />\n </button>\n <!-- Edit (own only) -->\n <button\n v-if=\"canEdit(thread)\"\n data-testid=\"comment-edit\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.edit\"\n @click=\"startEdit(thread)\"\n >\n <Pencil :size=\"12\" :stroke-width=\"2\" />\n </button>\n <!-- Delete (own only) -->\n <button\n v-if=\"canDelete(thread)\"\n data-testid=\"comment-delete\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.delete\"\n @click=\"confirmDelete(thread.id)\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Resolved badge -->\n <Transition name=\"tpl-resolve\">\n <div\n v-if=\"thread.resolvedAt\"\n class=\"tpl:mt-1 tpl:flex tpl:items-center tpl:gap-1 tpl:text-[10px] tpl:text-[var(--tpl-primary)]\"\n >\n <Check :size=\"10\" :stroke-width=\"2.5\" />\n <span>\n {{\n format(t.comments.resolvedBy, {\n name: thread.resolvedBy?.name ?? \"\",\n })\n }}\n </span>\n </div>\n </Transition>\n\n <!-- Block anchor indicator -->\n <span\n v-if=\"thread.blockId && isBlockMissing(thread.blockId)\"\n class=\"tpl:mt-1 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)]\"\n >\n {{ t.comments.missingBlock }}\n </span>\n <button\n v-else-if=\"thread.blockId\"\n class=\"tpl:mt-1 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:transition-colors tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n @click=\"editor.selectBlock(thread.blockId ?? '')\"\n >\n {{ t.comments.jumpToBlock }}\n </button>\n\n <!-- Comment body (or edit mode) -->\n <div v-if=\"editingId === thread.id\" class=\"tpl:mt-2\">\n <textarea\n v-model=\"editBody\"\n class=\"tpl:w-full tpl:resize-none tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:font-sans tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n rows=\"3\"\n @keydown=\"handleEditKeydown($event, thread.id)\"\n />\n <div class=\"tpl:mt-1.5 tpl:flex tpl:gap-1.5\">\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"!editBody.trim() || comments.isSubmitting.value\"\n @click=\"handleEdit(thread.id)\"\n >\n {{ t.comments.save }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n </div>\n <p\n v-else\n class=\"tpl:mt-1.5 tpl:text-xs tpl:leading-relaxed tpl:whitespace-pre-wrap tpl:text-[var(--tpl-text)]\"\n >\n {{ thread.body }}\n </p>\n\n <!-- Delete confirmation -->\n <div\n v-if=\"deletingId === thread.id\"\n class=\"tpl:mt-2 tpl:flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <span class=\"tpl:flex-1\">\n {{ t.comments.deleteConfirm }}\n </span>\n <button\n class=\"tpl:rounded tpl:px-2 tpl:py-0.5 tpl:text-xs tpl:font-medium tpl:bg-[var(--tpl-danger)] tpl:text-[var(--tpl-bg)]\"\n data-testid=\"comment-delete-confirm\"\n @click=\"handleDelete(thread.id)\"\n >\n {{ t.comments.delete }}\n </button>\n <button\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelDelete()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n\n <!-- Thread actions -->\n <div\n v-if=\"editingId !== thread.id && deletingId !== thread.id\"\n class=\"tpl:mt-2 tpl:flex tpl:items-center tpl:gap-2\"\n >\n <button\n v-if=\"comments.canCreate.value\"\n data-testid=\"comment-reply\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.reply\"\n @click=\"startReply(thread.id)\"\n >\n <Reply\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n />\n </button>\n <button\n v-if=\"(thread.replies?.length ?? 0) > 0\"\n class=\"tpl:flex tpl:items-center tpl:gap-0.5 tpl:text-[11px] tpl:font-medium tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"toggleThread(thread.id)\"\n >\n <template v-if=\"expandedThreads.has(thread.id)\">\n <ChevronUp :size=\"11\" :stroke-width=\"2\" />\n </template>\n <template v-else>\n <ChevronDown :size=\"11\" :stroke-width=\"2\" />\n </template>\n {{\n (thread.replies?.length ?? 0) === 1\n ? format(t.comments.replyOne, {\n count: String(thread.replies?.length ?? 0),\n })\n : format(t.comments.replyMany, {\n count: String(thread.replies?.length ?? 0),\n })\n }}\n </button>\n </div>\n </div>\n\n <!-- Replies -->\n <Transition name=\"tpl-replies\">\n <div\n v-if=\"\n expandedThreads.has(thread.id) &&\n (thread.replies?.length ?? 0) > 0\n \"\n class=\"tpl-comment-replies tpl:ml-5 tpl:pl-3 tpl:pt-2\"\n >\n <div\n v-for=\"(reply, index) in thread.replies\"\n :key=\"reply.id\"\n class=\"tpl-comment-reply-card tpl:relative tpl:rounded-lg tpl:border tpl:px-3.5 tpl:py-2.5\"\n :class=\"\n index < (thread.replies?.length ?? 0) - 1 ? 'tpl:mb-2' : ''\n \"\n >\n <div\n class=\"tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <span\n class=\"tpl:text-xs tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{\n comments.isOwn(reply)\n ? t.comments.ownedByYou\n : reply.author.name\n }}\n </span>\n <span\n class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ formatTime(reply.createdAt) }}\n </span>\n <span\n v-if=\"reply.updatedAt\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ t.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <button\n v-if=\"canEdit(reply)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.edit\"\n @click=\"startEdit(reply)\"\n >\n <Pencil :size=\"11\" :stroke-width=\"2\" />\n </button>\n <button\n v-if=\"canDelete(reply)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.delete\"\n @click=\"confirmDelete(reply.id)\"\n >\n <Trash2 :size=\"11\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Reply body (or edit mode) -->\n <div v-if=\"editingId === reply.id\" class=\"tpl:mt-1.5\">\n <textarea\n v-model=\"editBody\"\n class=\"tpl:w-full tpl:resize-none tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:font-sans tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n rows=\"2\"\n @keydown=\"handleEditKeydown($event, reply.id)\"\n />\n <div class=\"tpl:mt-1.5 tpl:flex tpl:gap-1.5\">\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"\n !editBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleEdit(reply.id)\"\n >\n {{ t.comments.save }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n </div>\n <p\n v-else\n class=\"tpl:mt-1 tpl:text-xs tpl:leading-relaxed tpl:whitespace-pre-wrap tpl:text-[var(--tpl-text)]\"\n >\n {{ reply.body }}\n </p>\n\n <!-- Delete confirmation for reply -->\n <div\n v-if=\"deletingId === reply.id\"\n class=\"tpl:mt-2 tpl:flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <span class=\"tpl:flex-1\">\n {{ t.comments.deleteConfirm }}\n </span>\n <button\n class=\"tpl:rounded tpl:px-2 tpl:py-0.5 tpl:text-xs tpl:font-medium tpl:bg-[var(--tpl-danger)] tpl:text-[var(--tpl-bg)]\"\n data-testid=\"comment-delete-confirm\"\n @click=\"handleDelete(reply.id)\"\n >\n {{ t.comments.delete }}\n </button>\n <button\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelDelete()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n </div>\n </div>\n </Transition>\n\n <!-- Reply input -->\n <Transition name=\"tpl-replies\">\n <div\n v-if=\"replyingTo === thread.id\"\n class=\"tpl-comment-replies tpl:ml-5 tpl:pl-3 tpl:pt-2\"\n >\n <!-- Same shape as the new-comment composer below: the border and\n the focus ring belong to the wrapper, the field is bare\n inside it, and the actions sit within the bounds rather than\n floating beside them. The two composers do the same job, so a\n reader should not have to notice they are different\n components. Only the height differs — a reply is nested and\n indented, so it starts at two rows without the composer's\n `min-h`. -->\n <div\n class=\"tpl-comments-input-wrapper tpl-focus-ring-host tpl:flex tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n v-model=\"replyBody\"\n class=\"tpl:max-h-24 tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-xs tpl:outline-none tpl:text-[var(--tpl-text)]\"\n :placeholder=\"t.comments.replyPlaceholder\"\n rows=\"2\"\n @keydown=\"handleReplyKeydown($event, thread.id)\"\n />\n <button\n class=\"tpl-comments-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"!replyBody.trim() || comments.isSubmitting.value\"\n @click=\"handleReply(thread.id)\"\n >\n <Send :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl-comments-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelReply()\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n\n <!-- New comment input. Absent entirely when the store withheld `create` —\n hidden rather than disabled, so a read-only review reads as one rather\n than as a broken composer. -->\n <div\n v-if=\"comments.canCreate.value\"\n class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-if=\"isBlockFilterUnsaved\"\n class=\"tpl:flex tpl:min-h-[68px] tpl:items-center tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)]\"\n >\n {{ t.comments.saveTemplateFirst }}\n </div>\n <div\n v-else\n class=\"tpl-comments-input-wrapper tpl-focus-ring-host tpl:flex tpl:min-h-[68px] tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n ref=\"newCommentInput\"\n v-model=\"newCommentBody\"\n class=\"tpl:max-h-24 tpl:min-h-[48px] tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-xs tpl:outline-none tpl:text-[var(--tpl-text)]\"\n data-testid=\"comments-input\"\n :placeholder=\"t.comments.placeholder\"\n :disabled=\"comments.isSubmitting.value\"\n rows=\"2\"\n @keydown=\"handleNewCommentKeydown\"\n />\n <button\n class=\"tpl-comments-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n data-testid=\"comments-send\"\n :disabled=\"!newCommentBody.trim() || comments.isSubmitting.value\"\n @click=\"handleAddComment\"\n >\n <LoaderCircle\n v-if=\"comments.isSubmitting.value\"\n class=\"tpl-spinner\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n <Send v-else :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-comments-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-comments-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-comments-input-wrapper:focus-within {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-comments-send-btn:not(:disabled):hover {\n transform: scale(1.05);\n}\n\n.tpl-comment-filter {\n color: var(--tpl-text-muted);\n background-color: transparent;\n}\n\n.tpl-comment-filter:hover {\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-text);\n}\n\n.tpl-comment-filter--active {\n background-color: var(--tpl-primary-light);\n color: var(--tpl-primary);\n}\n\n.tpl-comment-action {\n color: var(--tpl-text-dim);\n background-color: transparent;\n}\n\n.tpl-comment-action:hover {\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-text);\n}\n\n.tpl-comment-delete:hover {\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n}\n\n.tpl-comment-card {\n border-color: var(--tpl-border);\n background-color: var(--tpl-bg);\n}\n\n.tpl-comment-reply-card {\n border-color: var(--tpl-border);\n background-color: var(--tpl-bg);\n}\n\n.tpl-resolve-enter-active,\n.tpl-resolve-leave-active {\n transition:\n opacity 0.3s ease,\n transform 0.3s ease;\n}\n\n.tpl-resolve-enter-from,\n.tpl-resolve-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n.tpl-resolve-icon {\n transition:\n color 0.3s ease,\n transform 0.2s ease;\n}\n\n.tpl-resolve-icon:active {\n transform: scale(1.3);\n}\n\n.tpl-replies-enter-active,\n.tpl-replies-leave-active {\n transition:\n opacity 0.25s ease,\n transform 0.25s ease;\n}\n\n.tpl-replies-enter-from,\n.tpl-replies-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport { EDITOR_KEY, requireInject } from \"../keys\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type { UseCommentsFeatureReturn } from \"../composables/useCommentsFeature\";\nimport type { Comment } from \"@templatical/types\";\nimport {\n Check,\n CircleCheck,\n ChevronDown,\n ChevronUp,\n LoaderCircle,\n MessageCircle,\n Pencil,\n Reply,\n Send,\n Trash2,\n X,\n} from \"@lucide/vue\";\nimport { computed, nextTick, ref, watch } from \"vue\";\n\n/**\n * The review panel, **shared by both entry points**.\n *\n * It takes the shared feature as a prop — the `TestEmailPanel` /\n * `SavedBlocksPanels` shape — so the same panel drives a consumer's own store and\n * Cloud's, and its strings live in the OSS chunk.\n *\n * Every write action is gated on the provider having supplied that mutation, and\n * **hidden** rather than disabled when it hasn't: a provider that withholds all\n * four yields a readable review with no way to change anything.\n */\nconst props = defineProps<{\n visible: boolean;\n feature: UseCommentsFeatureReturn;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n}>();\n\nconst { t, format } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"CommentsSidebar\");\n\nconst comments = computed(() => props.feature.headless);\n\ntype FilterMode = \"all\" | \"unresolved\" | \"block\";\nconst filterMode = ref<FilterMode>(\"unresolved\");\n\nconst newCommentBody = ref(\"\");\nconst replyingTo = ref<string | null>(null);\nconst replyBody = ref(\"\");\nconst editingId = ref<string | null>(null);\nconst editBody = ref(\"\");\nconst expandedThreads = ref<Set<string>>(new Set());\nconst deletingId = ref<string | null>(null);\nconst newCommentInput = ref<HTMLTextAreaElement | null>(null);\n\n/**\n * The block filter lives on the feature, not here: a canvas comment indicator sets\n * it *before* this panel exists (it is lazy), so the target has to survive the gap.\n */\nconst filterBlockId = computed(() => props.feature.filterBlockId.value);\n\n// An indicator click lands a block id on the feature; switching to that filter is\n// this panel's half of the handshake. `immediate` covers the case that opened the\n// panel in the first place, where the id is already set at mount.\nwatch(\n filterBlockId,\n (blockId) => {\n if (blockId) filterMode.value = \"block\";\n },\n { immediate: true },\n);\n\n// `isLoading` alone would flash the skeleton over a perfectly good list every\n// re-open; the pair distinguishes \"fetching, nothing yet\" from \"refreshing\".\nconst isInitialLoad = computed(\n () =>\n comments.value.isLoading.value &&\n comments.value.comments.value.length === 0,\n);\n\nconst allBlockIds = computed<Set<string>>(() => {\n const ids = new Set<string>();\n for (const block of editor.content.value.blocks) {\n ids.add(block.id);\n if (block.type === \"section\") {\n for (const column of block.children) {\n for (const child of column) {\n ids.add(child.id);\n }\n }\n }\n }\n return ids;\n});\n\nfunction isBlockMissing(blockId: string | null | undefined): boolean {\n if (!blockId) {\n return false;\n }\n return !allBlockIds.value.has(blockId);\n}\n\nconst isBlockFilterUnsaved = computed(() => {\n if (filterMode.value !== \"block\" || !filterBlockId.value) {\n return false;\n }\n // Asked through the capability rather than off the editor: a comment anchors to\n // a block in the *saved* template, and only the provider's side knows what that\n // contains. A store that accepts any anchor answers `true` and this never fires.\n return props.feature.capability.isBlockSaved(filterBlockId.value) === false;\n});\n\nconst filteredComments = computed(() => {\n let items = comments.value.comments.value;\n\n if (filterMode.value === \"unresolved\") {\n items = items.filter((c) => !c.resolvedAt);\n } else if (filterMode.value === \"block\" && filterBlockId.value) {\n items = items.filter((c) => c.blockId === filterBlockId.value);\n }\n\n return items;\n});\n\nwatch(\n () => editor.state.selectedBlockId,\n (newBlockId) => {\n if (filterMode.value === \"block\" && newBlockId) {\n props.feature.filterBlockId.value = newBlockId;\n }\n },\n);\n\nfunction setFilter(mode: FilterMode, blockId?: string): void {\n filterMode.value = mode;\n props.feature.filterBlockId.value =\n mode === \"block\" ? (blockId ?? null) : null;\n}\n\nfunction filterByBlock(blockId: string): void {\n setFilter(\"block\", blockId);\n}\n\nfunction toggleThread(threadId: string): void {\n if (expandedThreads.value.has(threadId)) {\n expandedThreads.value.delete(threadId);\n } else {\n expandedThreads.value.add(threadId);\n }\n}\n\nfunction startReply(threadId: string): void {\n replyingTo.value = threadId;\n replyBody.value = \"\";\n editingId.value = null;\n\n if (!expandedThreads.value.has(threadId)) {\n expandedThreads.value.add(threadId);\n }\n}\n\nfunction cancelReply(): void {\n replyingTo.value = null;\n replyBody.value = \"\";\n}\n\nfunction startEdit(comment: Comment): void {\n editingId.value = comment.id;\n editBody.value = comment.body;\n replyingTo.value = null;\n}\n\nfunction cancelEdit(): void {\n editingId.value = null;\n editBody.value = \"\";\n}\n\nfunction confirmDelete(commentId: string): void {\n deletingId.value = commentId;\n}\n\nfunction cancelDelete(): void {\n deletingId.value = null;\n}\n\n/**\n * Every write swallows its rejection. The feature already routed it to `onError`\n * and left the list untouched, and an unhandled rejection out of a DOM event\n * binding helps nobody — same shape as `VersionHistoryPanels`.\n */\nasync function handleAddComment(): Promise<void> {\n const body = newCommentBody.value.trim();\n if (!body || !comments.value.canCreate.value) {\n return;\n }\n\n const blockId =\n filterMode.value === \"block\" ? filterBlockId.value : undefined;\n try {\n await comments.value.create({ body, blockId: blockId ?? undefined });\n newCommentBody.value = \"\";\n } catch {\n /* reported through onError */\n }\n}\n\nasync function handleReply(parentId: string): Promise<void> {\n const body = replyBody.value.trim();\n if (!body || !comments.value.canCreate.value) {\n return;\n }\n\n const parent = comments.value.find(parentId);\n try {\n await comments.value.create({\n body,\n blockId: parent?.blockId ?? undefined,\n parentId,\n });\n replyingTo.value = null;\n replyBody.value = \"\";\n } catch {\n /* reported through onError */\n }\n}\n\nasync function handleEdit(commentId: string): Promise<void> {\n const body = editBody.value.trim();\n if (!body) {\n return;\n }\n\n try {\n await comments.value.update(commentId, { body });\n editingId.value = null;\n editBody.value = \"\";\n } catch {\n /* reported through onError */\n }\n}\n\nasync function handleDelete(commentId: string): Promise<void> {\n try {\n await comments.value.remove(commentId);\n } catch {\n /* reported through onError */\n }\n deletingId.value = null;\n}\n\nasync function handleResolve(comment: Comment): Promise<void> {\n try {\n // The target state, not a toggle: two clicks in flight can't invert.\n await comments.value.setResolved(comment.id, !comment.resolvedAt);\n } catch {\n /* reported through onError */\n }\n}\n\n/** Editing and deleting are the author's own, and only when the store allows it. */\nfunction canEdit(comment: Comment): boolean {\n return comments.value.canUpdate.value && comments.value.isOwn(comment);\n}\n\nfunction canDelete(comment: Comment): boolean {\n return comments.value.canDelete.value && comments.value.isOwn(comment);\n}\n\nfunction formatTime(dateString: string): string {\n return formatRelativeTime(dateString, t.time) ?? dateString;\n}\n\nfunction handleNewCommentKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleAddComment();\n }\n}\n\nfunction handleReplyKeydown(event: KeyboardEvent, parentId: string): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleReply(parentId);\n }\n}\n\nfunction handleEditKeydown(event: KeyboardEvent, commentId: string): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleEdit(commentId);\n }\n if (event.key === \"Escape\") {\n cancelEdit();\n }\n}\n\nfunction focusNewComment(): void {\n nextTick(() => {\n newCommentInput.value?.focus();\n });\n}\n\ndefineExpose({ filterByBlock, focusNewComment });\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl-comments-slide-enter-active\"\n enter-from-class=\"tpl:translate-x-full\"\n enter-to-class=\"tpl:translate-x-0\"\n leave-active-class=\"tpl-comments-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 data-testid=\"comments-sidebar\"\n class=\"tpl-comments-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-text)]\"\n >\n <MessageCircle :size=\"13\" :stroke-width=\"2\" />\n <span>{{ t.comments.title }}</span>\n <span\n v-if=\"feature.unresolvedCount.value > 0\"\n class=\"tpl:ml-1 tpl:inline-flex tpl:size-5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n >\n {{ feature.unresolvedCount.value }}\n </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 <!-- Filter bar -->\n <div\n class=\"tpl:flex tpl:gap-1 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-2\"\n >\n <button\n class=\"tpl-comment-filter tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors\"\n :class=\"\n filterMode === 'unresolved' ? 'tpl-comment-filter--active' : ''\n \"\n data-testid=\"comments-filter-unresolved\"\n @click=\"setFilter('unresolved')\"\n >\n {{ t.comments.filterUnresolved }}\n </button>\n <button\n class=\"tpl-comment-filter tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors\"\n :class=\"filterMode === 'all' ? 'tpl-comment-filter--active' : ''\"\n data-testid=\"comments-filter-all\"\n @click=\"setFilter('all')\"\n >\n {{ t.comments.filterAll }}\n </button>\n <button\n v-if=\"editor.state.selectedBlockId\"\n class=\"tpl-comment-filter tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors\"\n :class=\"filterMode === 'block' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('block', editor.state.selectedBlockId ?? undefined)\"\n >\n {{ t.comments.filterBlock }}\n </button>\n </div>\n\n <!-- Thread list -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto\">\n <!-- Loading state -->\n <div\n v-if=\"isInitialLoad\"\n class=\"tpl:flex tpl:h-full tpl:items-center tpl:justify-center\"\n >\n <LoaderCircle\n class=\"tpl-spinner tpl:text-[var(--tpl-text-muted)]\"\n :size=\"24\"\n :stroke-width=\"2\"\n />\n </div>\n\n <!-- Empty state -->\n <div\n v-else-if=\"filteredComments.length === 0\"\n class=\"tpl:flex tpl:h-full tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:px-6 tpl:text-center\"\n >\n <MessageCircle\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:max-w-[240px] tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n filterMode === \"all\"\n ? t.comments.noCommentsHint\n : t.comments.noComments\n }}\n </p>\n </div>\n\n <!-- Comment threads -->\n <div v-else class=\"tpl:flex tpl:flex-col tpl:gap-3 tpl:p-3\">\n <div\n v-for=\"thread in filteredComments\"\n :key=\"thread.id\"\n class=\"tpl-comment-thread\"\n data-testid=\"comment-thread\"\n >\n <!-- Root comment card -->\n <div\n class=\"tpl-comment-card tpl:rounded-lg tpl:border tpl:px-3.5 tpl:py-3\"\n >\n <!-- Comment header -->\n <div\n class=\"tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <span\n class=\"tpl:text-xs tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{\n comments.isOwn(thread)\n ? t.comments.ownedByYou\n : thread.author.name\n }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ formatTime(thread.createdAt) }}\n </span>\n <span\n v-if=\"thread.updatedAt\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ t.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <!-- Resolve toggle -->\n <button\n v-if=\"comments.canResolve.value\"\n data-testid=\"comment-resolve\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"\n thread.resolvedAt\n ? t.comments.unresolve\n : t.comments.resolve\n \"\n @click=\"handleResolve(thread)\"\n >\n <CircleCheck\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl-resolve-icon\"\n :style=\"{\n color: thread.resolvedAt\n ? 'var(--tpl-primary)'\n : undefined,\n }\"\n />\n </button>\n <!-- Edit (own only) -->\n <button\n v-if=\"canEdit(thread)\"\n data-testid=\"comment-edit\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.edit\"\n @click=\"startEdit(thread)\"\n >\n <Pencil :size=\"12\" :stroke-width=\"2\" />\n </button>\n <!-- Delete (own only) -->\n <button\n v-if=\"canDelete(thread)\"\n data-testid=\"comment-delete\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.delete\"\n @click=\"confirmDelete(thread.id)\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Resolved badge -->\n <Transition name=\"tpl-resolve\">\n <div\n v-if=\"thread.resolvedAt\"\n class=\"tpl:mt-1 tpl:flex tpl:items-center tpl:gap-1 tpl:text-[10px] tpl:text-[var(--tpl-primary)]\"\n >\n <Check :size=\"10\" :stroke-width=\"2.5\" />\n <span>\n {{\n format(t.comments.resolvedBy, {\n name: thread.resolvedBy?.name ?? \"\",\n })\n }}\n </span>\n </div>\n </Transition>\n\n <!-- Block anchor indicator -->\n <span\n v-if=\"thread.blockId && isBlockMissing(thread.blockId)\"\n class=\"tpl:mt-1 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)]\"\n >\n {{ t.comments.missingBlock }}\n </span>\n <button\n v-else-if=\"thread.blockId\"\n class=\"tpl:mt-1 tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:transition-colors tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n @click=\"editor.selectBlock(thread.blockId ?? '')\"\n >\n {{ t.comments.jumpToBlock }}\n </button>\n\n <!-- Comment body (or edit mode) -->\n <div v-if=\"editingId === thread.id\" class=\"tpl:mt-2\">\n <textarea\n v-model=\"editBody\"\n class=\"tpl:w-full tpl:resize-none tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:font-sans tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n rows=\"3\"\n @keydown=\"handleEditKeydown($event, thread.id)\"\n />\n <div class=\"tpl:mt-1.5 tpl:flex tpl:gap-1.5\">\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"!editBody.trim() || comments.isSubmitting.value\"\n @click=\"handleEdit(thread.id)\"\n >\n {{ t.comments.save }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n </div>\n <p\n v-else\n class=\"tpl:mt-1.5 tpl:text-xs tpl:leading-relaxed tpl:whitespace-pre-wrap tpl:text-[var(--tpl-text)]\"\n >\n {{ thread.body }}\n </p>\n\n <!-- Delete confirmation -->\n <div\n v-if=\"deletingId === thread.id\"\n class=\"tpl:mt-2 tpl:flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <span class=\"tpl:flex-1\">\n {{ t.comments.deleteConfirm }}\n </span>\n <button\n class=\"tpl:rounded tpl:px-2 tpl:py-0.5 tpl:text-xs tpl:font-medium tpl:bg-[var(--tpl-danger)] tpl:text-[var(--tpl-bg)]\"\n data-testid=\"comment-delete-confirm\"\n @click=\"handleDelete(thread.id)\"\n >\n {{ t.comments.delete }}\n </button>\n <button\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelDelete()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n\n <!-- Thread actions -->\n <div\n v-if=\"editingId !== thread.id && deletingId !== thread.id\"\n class=\"tpl:mt-2 tpl:flex tpl:items-center tpl:gap-2\"\n >\n <button\n v-if=\"comments.canCreate.value\"\n data-testid=\"comment-reply\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.reply\"\n @click=\"startReply(thread.id)\"\n >\n <Reply\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n />\n </button>\n <button\n v-if=\"(thread.replies?.length ?? 0) > 0\"\n class=\"tpl:flex tpl:items-center tpl:gap-0.5 tpl:text-[11px] tpl:font-medium tpl:transition-colors tpl:text-[var(--tpl-text-muted)]\"\n @click=\"toggleThread(thread.id)\"\n >\n <template v-if=\"expandedThreads.has(thread.id)\">\n <ChevronUp :size=\"11\" :stroke-width=\"2\" />\n </template>\n <template v-else>\n <ChevronDown :size=\"11\" :stroke-width=\"2\" />\n </template>\n {{\n (thread.replies?.length ?? 0) === 1\n ? format(t.comments.replyOne, {\n count: String(thread.replies?.length ?? 0),\n })\n : format(t.comments.replyMany, {\n count: String(thread.replies?.length ?? 0),\n })\n }}\n </button>\n </div>\n </div>\n\n <!-- Replies -->\n <Transition name=\"tpl-replies\">\n <div\n v-if=\"\n expandedThreads.has(thread.id) &&\n (thread.replies?.length ?? 0) > 0\n \"\n class=\"tpl-comment-replies tpl:ml-5 tpl:pl-3 tpl:pt-2\"\n >\n <div\n v-for=\"(reply, index) in thread.replies\"\n :key=\"reply.id\"\n class=\"tpl-comment-reply-card tpl:relative tpl:rounded-lg tpl:border tpl:px-3.5 tpl:py-2.5\"\n :class=\"\n index < (thread.replies?.length ?? 0) - 1 ? 'tpl:mb-2' : ''\n \"\n >\n <div\n class=\"tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <span\n class=\"tpl:text-xs tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{\n comments.isOwn(reply)\n ? t.comments.ownedByYou\n : reply.author.name\n }}\n </span>\n <span\n class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ formatTime(reply.createdAt) }}\n </span>\n <span\n v-if=\"reply.updatedAt\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ t.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <button\n v-if=\"canEdit(reply)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.edit\"\n @click=\"startEdit(reply)\"\n >\n <Pencil :size=\"11\" :stroke-width=\"2\" />\n </button>\n <button\n v-if=\"canDelete(reply)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors\"\n :title=\"t.comments.delete\"\n @click=\"confirmDelete(reply.id)\"\n >\n <Trash2 :size=\"11\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Reply body (or edit mode) -->\n <div v-if=\"editingId === reply.id\" class=\"tpl:mt-1.5\">\n <textarea\n v-model=\"editBody\"\n class=\"tpl:w-full tpl:resize-none tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:font-sans tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n rows=\"2\"\n @keydown=\"handleEditKeydown($event, reply.id)\"\n />\n <div class=\"tpl:mt-1.5 tpl:flex tpl:gap-1.5\">\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n :disabled=\"\n !editBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleEdit(reply.id)\"\n >\n {{ t.comments.save }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-2.5 tpl:py-1 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n </div>\n <p\n v-else\n class=\"tpl:mt-1 tpl:text-xs tpl:leading-relaxed tpl:whitespace-pre-wrap tpl:text-[var(--tpl-text)]\"\n >\n {{ reply.body }}\n </p>\n\n <!-- Delete confirmation for reply -->\n <div\n v-if=\"deletingId === reply.id\"\n class=\"tpl:mt-2 tpl:flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-danger-light)] tpl:text-[var(--tpl-danger)]\"\n >\n <span class=\"tpl:flex-1\">\n {{ t.comments.deleteConfirm }}\n </span>\n <button\n class=\"tpl:rounded tpl:px-2 tpl:py-0.5 tpl:text-xs tpl:font-medium tpl:bg-[var(--tpl-danger)] tpl:text-[var(--tpl-bg)]\"\n data-testid=\"comment-delete-confirm\"\n @click=\"handleDelete(reply.id)\"\n >\n {{ t.comments.delete }}\n </button>\n <button\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelDelete()\"\n >\n {{ t.comments.cancel }}\n </button>\n </div>\n </div>\n </div>\n </Transition>\n\n <!-- Reply input -->\n <Transition name=\"tpl-replies\">\n <div\n v-if=\"replyingTo === thread.id\"\n class=\"tpl-comment-replies tpl:ml-5 tpl:pl-3 tpl:pt-2\"\n >\n <!-- Same shape as the new-comment composer below: the border and\n the focus ring belong to the wrapper, the field is bare\n inside it, and the actions sit within the bounds rather than\n floating beside them. The two composers do the same job, so a\n reader should not have to notice they are different\n components. Only the height differs — a reply is nested and\n indented, so it starts at two rows without the composer's\n `min-h`. -->\n <div\n class=\"tpl-comments-input-wrapper tpl-focus-ring-host tpl:flex tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n v-model=\"replyBody\"\n class=\"tpl:max-h-24 tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-xs tpl:outline-none tpl:text-[var(--tpl-text)]\"\n :placeholder=\"t.comments.replyPlaceholder\"\n rows=\"2\"\n @keydown=\"handleReplyKeydown($event, thread.id)\"\n />\n <button\n class=\"tpl-comments-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"!replyBody.trim() || comments.isSubmitting.value\"\n @click=\"handleReply(thread.id)\"\n >\n <Send :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl-comments-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelReply()\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n\n <!-- New comment input. Absent entirely when the store withheld `create` —\n hidden rather than disabled, so a read-only review reads as one rather\n than as a broken composer. -->\n <div\n v-if=\"comments.canCreate.value\"\n class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-if=\"isBlockFilterUnsaved\"\n class=\"tpl:flex tpl:min-h-[68px] tpl:items-center tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)]\"\n >\n {{ t.comments.saveTemplateFirst }}\n </div>\n <div\n v-else\n class=\"tpl-comments-input-wrapper tpl-focus-ring-host tpl:flex tpl:min-h-[68px] tpl:items-end tpl:gap-2 tpl:rounded-[var(--tpl-radius)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <textarea\n ref=\"newCommentInput\"\n v-model=\"newCommentBody\"\n class=\"tpl:max-h-24 tpl:min-h-[48px] tpl:flex-1 tpl:resize-none tpl:border-none tpl:bg-transparent tpl:font-sans tpl:text-xs tpl:outline-none tpl:text-[var(--tpl-text)]\"\n data-testid=\"comments-input\"\n :placeholder=\"t.comments.placeholder\"\n :disabled=\"comments.isSubmitting.value\"\n rows=\"2\"\n @keydown=\"handleNewCommentKeydown\"\n />\n <button\n class=\"tpl-comments-send-btn tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-all tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n data-testid=\"comments-send\"\n :disabled=\"!newCommentBody.trim() || comments.isSubmitting.value\"\n @click=\"handleAddComment\"\n >\n <LoaderCircle\n v-if=\"comments.isSubmitting.value\"\n class=\"tpl-spinner\"\n :size=\"16\"\n :stroke-width=\"2\"\n />\n <Send v-else :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </Transition>\n</template>\n\n<style scoped>\n.tpl-comments-slide-enter-active {\n transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-comments-slide-leave-active {\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-comments-input-wrapper:focus-within {\n border-color: var(--tpl-primary);\n box-shadow: var(--tpl-ring);\n}\n\n.tpl-comments-send-btn:not(:disabled):hover {\n transform: scale(1.05);\n}\n\n.tpl-comment-filter {\n color: var(--tpl-text-muted);\n background-color: transparent;\n}\n\n.tpl-comment-filter:hover {\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-text);\n}\n\n.tpl-comment-filter--active {\n background-color: var(--tpl-primary-light);\n color: var(--tpl-primary);\n}\n\n.tpl-comment-action {\n color: var(--tpl-text-dim);\n background-color: transparent;\n}\n\n.tpl-comment-action:hover {\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-text);\n}\n\n.tpl-comment-delete:hover {\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n}\n\n.tpl-comment-card {\n border-color: var(--tpl-border);\n background-color: var(--tpl-bg);\n}\n\n.tpl-comment-reply-card {\n border-color: var(--tpl-border);\n background-color: var(--tpl-bg);\n}\n\n.tpl-resolve-enter-active,\n.tpl-resolve-leave-active {\n transition:\n opacity 0.3s ease,\n transform 0.3s ease;\n}\n\n.tpl-resolve-enter-from,\n.tpl-resolve-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n.tpl-resolve-icon {\n transition:\n color 0.3s ease,\n transform 0.2s ease;\n}\n\n.tpl-resolve-icon:active {\n transform: scale(1.3);\n}\n\n.tpl-replies-enter-active,\n.tpl-replies-leave-active {\n transition:\n opacity 0.25s ease,\n transform 0.25s ease;\n}\n\n.tpl-replies-enter-from,\n.tpl-replies-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n</style>\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"DesignReferenceSidebar-BvOwE1kg.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 +0,0 @@
1
- {"version":3,"file":"ParagraphEditor-BU8tlFbL.js","names":[],"sources":["../../../src/components/blocks/EmojiPickerDropdown.vue","../../../src/components/blocks/EmojiPickerDropdown.vue","../../../src/components/toolbar/ToolbarIconButton.vue","../../../src/components/toolbar/ToolbarIconButton.vue","../../../src/components/toolbar/ToolbarSeparator.vue","../../../src/components/toolbar/ToolbarSelect.vue","../../../src/components/toolbar/ToolbarSelect.vue","../../../src/utils/richTextColor.ts","../../../src/components/blocks/ParagraphToolbar.vue","../../../src/components/blocks/ParagraphToolbar.vue","../../../src/components/blocks/ParagraphEditor.vue","../../../src/components/blocks/ParagraphEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useEmoji, useI18n } from \"../../composables\";\nimport { useFocusTrap } from \"../../composables/useFocusTrap\";\nimport { onClickOutside } from \"@vueuse/core\";\nimport { Smile } from \"@lucide/vue\";\nimport { computed, ref } from \"vue\";\n\nconst emit = defineEmits<{\n (e: \"insert\", emoji: string): void;\n}>();\n\nconst {\n categories: emojiCategories,\n isOpen: showEmojiPicker,\n toggle: toggleEmojiPicker,\n close: closeEmojiPicker,\n} = useEmoji();\n\nconst { t, format } = useI18n();\n\nconst pickerRef = ref<HTMLElement | null>(null);\nconst rootRef = ref<HTMLElement | null>(null);\n\nconst isOpenRef = computed(() => showEmojiPicker.value);\nuseFocusTrap(pickerRef, isOpenRef);\n\nonClickOutside(rootRef, () => {\n if (showEmojiPicker.value) {\n closeEmojiPicker();\n }\n});\n\nfunction handleInsert(emoji: string): void {\n emit(\"insert\", emoji);\n closeEmojiPicker();\n}\n</script>\n\n<template>\n <div ref=\"rootRef\" class=\"tpl:relative\">\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': showEmojiPicker,\n }\"\n :aria-label=\"t.paragraphEditor.insertEmoji\"\n :title=\"t.paragraphEditor.insertEmoji\"\n :aria-expanded=\"showEmojiPicker\"\n aria-haspopup=\"dialog\"\n aria-controls=\"tpl-emoji-picker\"\n @click=\"toggleEmojiPicker\"\n >\n <Smile :size=\"16\" :stroke-width=\"2\" />\n </button>\n <div\n v-if=\"showEmojiPicker\"\n id=\"tpl-emoji-picker\"\n ref=\"pickerRef\"\n role=\"dialog\"\n aria-modal=\"false\"\n :aria-label=\"t.paragraphEditor.insertEmoji\"\n tabindex=\"-1\"\n class=\"tpl-emoji-picker tpl:absolute tpl:top-full tpl:left-0 tpl:z-10 tpl:mt-2 tpl:w-72 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n @keydown.esc.stop.prevent=\"closeEmojiPicker\"\n >\n <div\n v-for=\"category in emojiCategories\"\n :key=\"category.key\"\n class=\"tpl:mb-2 tpl:last:mb-0\"\n >\n <div\n class=\"tpl:mb-1.5 tpl:text-[10px] tpl:font-medium tpl:tracking-wide tpl:text-[var(--tpl-text-muted)] tpl:uppercase\"\n >\n {{ t.emoji[category.key] }}\n </div>\n <div class=\"tpl:grid tpl:grid-cols-10 tpl:gap-0.5\">\n <button\n v-for=\"emoji in category.emojis\"\n :key=\"emoji\"\n type=\"button\"\n :aria-label=\"format(t.paragraphEditor.emojiItemLabel, { emoji })\"\n class=\"tpl:flex tpl:size-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded tpl:border-none tpl:bg-transparent tpl:text-base tpl:transition-all tpl:hover:scale-125 tpl:hover:bg-[var(--tpl-bg-active)]\"\n @click=\"handleInsert(emoji)\"\n >\n {{ emoji }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useEmoji, useI18n } from \"../../composables\";\nimport { useFocusTrap } from \"../../composables/useFocusTrap\";\nimport { onClickOutside } from \"@vueuse/core\";\nimport { Smile } from \"@lucide/vue\";\nimport { computed, ref } from \"vue\";\n\nconst emit = defineEmits<{\n (e: \"insert\", emoji: string): void;\n}>();\n\nconst {\n categories: emojiCategories,\n isOpen: showEmojiPicker,\n toggle: toggleEmojiPicker,\n close: closeEmojiPicker,\n} = useEmoji();\n\nconst { t, format } = useI18n();\n\nconst pickerRef = ref<HTMLElement | null>(null);\nconst rootRef = ref<HTMLElement | null>(null);\n\nconst isOpenRef = computed(() => showEmojiPicker.value);\nuseFocusTrap(pickerRef, isOpenRef);\n\nonClickOutside(rootRef, () => {\n if (showEmojiPicker.value) {\n closeEmojiPicker();\n }\n});\n\nfunction handleInsert(emoji: string): void {\n emit(\"insert\", emoji);\n closeEmojiPicker();\n}\n</script>\n\n<template>\n <div ref=\"rootRef\" class=\"tpl:relative\">\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': showEmojiPicker,\n }\"\n :aria-label=\"t.paragraphEditor.insertEmoji\"\n :title=\"t.paragraphEditor.insertEmoji\"\n :aria-expanded=\"showEmojiPicker\"\n aria-haspopup=\"dialog\"\n aria-controls=\"tpl-emoji-picker\"\n @click=\"toggleEmojiPicker\"\n >\n <Smile :size=\"16\" :stroke-width=\"2\" />\n </button>\n <div\n v-if=\"showEmojiPicker\"\n id=\"tpl-emoji-picker\"\n ref=\"pickerRef\"\n role=\"dialog\"\n aria-modal=\"false\"\n :aria-label=\"t.paragraphEditor.insertEmoji\"\n tabindex=\"-1\"\n class=\"tpl-emoji-picker tpl:absolute tpl:top-full tpl:left-0 tpl:z-10 tpl:mt-2 tpl:w-72 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n @keydown.esc.stop.prevent=\"closeEmojiPicker\"\n >\n <div\n v-for=\"category in emojiCategories\"\n :key=\"category.key\"\n class=\"tpl:mb-2 tpl:last:mb-0\"\n >\n <div\n class=\"tpl:mb-1.5 tpl:text-[10px] tpl:font-medium tpl:tracking-wide tpl:text-[var(--tpl-text-muted)] tpl:uppercase\"\n >\n {{ t.emoji[category.key] }}\n </div>\n <div class=\"tpl:grid tpl:grid-cols-10 tpl:gap-0.5\">\n <button\n v-for=\"emoji in category.emojis\"\n :key=\"emoji\"\n type=\"button\"\n :aria-label=\"format(t.paragraphEditor.emojiItemLabel, { emoji })\"\n class=\"tpl:flex tpl:size-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded tpl:border-none tpl:bg-transparent tpl:text-base tpl:transition-all tpl:hover:scale-125 tpl:hover:bg-[var(--tpl-bg-active)]\"\n @click=\"handleInsert(emoji)\"\n >\n {{ emoji }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { Component } from \"vue\";\n\ndefineProps<{\n icon: Component;\n label: string;\n active?: boolean;\n strokeWidth?: number;\n size?: number;\n}>();\n</script>\n\n<template>\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{ 'tpl-text-toolbar-btn--active': active }\"\n :aria-label=\"label\"\n :title=\"label\"\n :aria-pressed=\"active ? 'true' : 'false'\"\n >\n <component :is=\"icon\" :size=\"size ?? 16\" :stroke-width=\"strokeWidth ?? 2\" />\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport type { Component } from \"vue\";\n\ndefineProps<{\n icon: Component;\n label: string;\n active?: boolean;\n strokeWidth?: number;\n size?: number;\n}>();\n</script>\n\n<template>\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{ 'tpl-text-toolbar-btn--active': active }\"\n :aria-label=\"label\"\n :title=\"label\"\n :aria-pressed=\"active ? 'true' : 'false'\"\n >\n <component :is=\"icon\" :size=\"size ?? 16\" :stroke-width=\"strokeWidth ?? 2\" />\n </button>\n</template>\n","<template>\n <span\n class=\"tpl:mx-1 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n aria-hidden=\"true\"\n ></span>\n</template>\n","<script setup lang=\"ts\">\ninterface OptionItem {\n value: string;\n label: string;\n}\n\ndefineProps<{\n modelValue: string;\n options: readonly (string | OptionItem)[];\n label: string;\n placeholder?: string;\n widthClass?: string;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nfunction onChange(e: Event): void {\n emit(\"update:modelValue\", (e.target as HTMLSelectElement).value);\n}\n\nfunction optionValue(opt: string | OptionItem): string {\n return typeof opt === \"string\" ? opt : opt.value;\n}\n\nfunction optionLabel(opt: string | OptionItem): string {\n return typeof opt === \"string\" ? opt : opt.label;\n}\n</script>\n\n<template>\n <select\n :class=\"[\n 'tpl:h-8 tpl:cursor-pointer tpl:rounded tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text)] tpl:outline-none',\n widthClass ?? 'tpl:w-20',\n ]\"\n :value=\"modelValue\"\n :aria-label=\"label\"\n :title=\"label\"\n @change=\"onChange\"\n >\n <option value=\"\">{{ placeholder ?? \"\" }}</option>\n <option\n v-for=\"opt in options\"\n :key=\"optionValue(opt)\"\n :value=\"optionValue(opt)\"\n >\n {{ optionLabel(opt) }}\n </option>\n </select>\n</template>\n","<script setup lang=\"ts\">\ninterface OptionItem {\n value: string;\n label: string;\n}\n\ndefineProps<{\n modelValue: string;\n options: readonly (string | OptionItem)[];\n label: string;\n placeholder?: string;\n widthClass?: string;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nfunction onChange(e: Event): void {\n emit(\"update:modelValue\", (e.target as HTMLSelectElement).value);\n}\n\nfunction optionValue(opt: string | OptionItem): string {\n return typeof opt === \"string\" ? opt : opt.value;\n}\n\nfunction optionLabel(opt: string | OptionItem): string {\n return typeof opt === \"string\" ? opt : opt.label;\n}\n</script>\n\n<template>\n <select\n :class=\"[\n 'tpl:h-8 tpl:cursor-pointer tpl:rounded tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text)] tpl:outline-none',\n widthClass ?? 'tpl:w-20',\n ]\"\n :value=\"modelValue\"\n :aria-label=\"label\"\n :title=\"label\"\n @change=\"onChange\"\n >\n <option value=\"\">{{ placeholder ?? \"\" }}</option>\n <option\n v-for=\"opt in options\"\n :key=\"optionValue(opt)\"\n :value=\"optionValue(opt)\"\n >\n {{ optionLabel(opt) }}\n </option>\n </select>\n</template>\n","import { DEFAULT_TEXT_COLOR } from \"../constants/styleConstants\";\n\n/**\n * The color a rich-text selection actually renders as, for display in the\n * text-color control (issue #373).\n *\n * Priority: an explicit inline text-color mark on the selection wins; otherwise\n * the text inherits the document-level `textColor`; if that too is unset (older\n * content predating document text color), fall back to the built-in default.\n *\n * A native `<input type=\"color\">` can't represent \"no color\", so the control\n * must be handed a concrete value. Resolving the effective color is what keeps\n * that value truthful — the swatch shows the color the text actually renders in,\n * whatever level it comes from. Defaulting to a hard-coded `#000000` instead\n * would both read as an explicit choice and not even match the real inherited\n * color (`#1a1a1a`).\n */\nexport function resolveEffectiveTextColor(\n explicitColor: string,\n documentTextColor: string | undefined,\n): string {\n return explicitColor || documentTextColor || DEFAULT_TEXT_COLOR;\n}\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport EmojiPickerDropdown from \"./EmojiPickerDropdown.vue\";\nimport ToolbarIconButton from \"../toolbar/ToolbarIconButton.vue\";\nimport ToolbarSeparator from \"../toolbar/ToolbarSeparator.vue\";\nimport ToolbarSelect from \"../toolbar/ToolbarSelect.vue\";\nimport { useI18n } from \"../../composables\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { Editor } from \"@tiptap/core\";\nimport {\n AlignCenter,\n AlignJustify,\n AlignLeft,\n AlignRight,\n Bold,\n Italic,\n Braces,\n Link,\n List,\n ListOrdered,\n LoaderCircle,\n RemoveFormatting,\n ScanLine,\n Strikethrough,\n Subscript,\n Superscript,\n Underline,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n EDITOR_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n FONTS_MANAGER_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n requireInject,\n} from \"../../keys\";\nimport {\n DEFAULT_HIGHLIGHT_COLOR,\n FONT_SIZE_OPTIONS,\n LINE_HEIGHT_OPTIONS,\n LETTER_SPACING_OPTIONS,\n} from \"../../constants/styleConstants\";\nimport { resolveEffectiveTextColor } from \"../../utils/richTextColor\";\n\nconst props = defineProps<{\n editor: Editor | null;\n toolbarPosition: { top: number; left: number };\n isLoading: boolean;\n canRequestMergeTag: boolean;\n canInsertLogicTag: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"open-link-dialog\"): void;\n (e: \"add-merge-tag\"): void;\n (e: \"add-logic-tag\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"ParagraphToolbar\");\nconst popoverRoot = usePopoverRoot();\n// Picker may be null in non-editor contexts (e.g. isolated component tests).\n// When it's provided AND open, hide the floating toolbar — leaving it\n// visible behind the modal is visually noisy and the toolbar's Tailwind\n// z-index utility doesn't compile reliably, so it can sometimes paint\n// over the modal backdrop.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\n// Editor may be absent in isolated tests; used only to resolve the inherited\n// document text color for the text-color swatch.\nconst editorReturn = inject(EDITOR_KEY, null);\n// Hide the floating toolbar while either built-in picker modal is open, so it\n// doesn't overlap the dialog.\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst fontFamilies = fontsManager.fonts;\n\nfunction insertEmoji(emoji: string): void {\n props.editor?.chain().focus().insertContent(emoji).run();\n}\n\nfunction textStyleAttr(attr: string): string {\n return (props.editor?.getAttributes(\"textStyle\")[attr] as string) || \"\";\n}\n\nfunction setFontFamily(family: string): void {\n const chain = props.editor?.chain().focus();\n if (family) chain?.setFontFamily(family).run();\n else chain?.unsetFontFamily().run();\n}\n\nfunction setFontSize(size: string): void {\n const chain = props.editor?.chain().focus();\n if (size) chain?.setFontSize(size).run();\n else chain?.unsetFontSize().run();\n}\n\n// When the selection is a link, the color control operates on the link itself\n// (its `color` attribute → inline on the `<a>`) so the link's text and underline\n// stay in sync; otherwise it applies a plain inline text-color mark.\nfunction isLinkSelection(): boolean {\n return props.editor?.isActive(\"link\") ?? false;\n}\n\nfunction linkColorAttr(): string {\n return (props.editor?.getAttributes(\"link\").color as string) || \"\";\n}\n\n// Set (or clear, when empty) the color of the current selection. On a link the\n// whole link is recolored (extendMarkRange) so it doesn't split into segments.\nfunction setColor(color: string): void {\n const chain = props.editor?.chain().focus();\n if (!chain) return;\n if (isLinkSelection()) {\n chain.extendMarkRange(\"link\").updateAttributes(\"link\", {\n color: color || null,\n });\n // A per-link color takes absolute priority over any inner text-color: strip\n // the inline text color on the link range so the `<a>` color alone drives\n // the glyphs and the underline (mirrors useRichTextLinkDialog.insertLink).\n // Only when setting a color — clearing leaves the text color untouched.\n if (color) chain.unsetColor();\n chain.run();\n } else if (color) {\n // A text color applied over a selection that includes a link unifies the\n // link with it: update the link's own color so the `<a>` (which paints the\n // underline and is what the dialog/toolbar read) matches the recolored\n // glyphs. A no-op on a selection with no link. Mirror of the link branch\n // above — either way a link never ends up with a text color fighting its\n // own color.\n chain.setColor(color).updateAttributes(\"link\", { color }).run();\n } else {\n chain.unsetColor().run();\n }\n}\n\n// The color the current selection actually renders in — a link's own color (else\n// the effective document link color: link color, then text color), or for plain\n// text the inline mark else the document text color. Keeps the swatch truthful\n// rather than showing a hard-coded default (issue #373 / per-link color).\nfunction effectiveTextColor(): string {\n const settings = editorReturn?.content.value.settings;\n if (isLinkSelection()) {\n return resolveEffectiveTextColor(\n linkColorAttr(),\n settings?.linkColor || settings?.textColor,\n );\n }\n return resolveEffectiveTextColor(textStyleAttr(\"color\"), settings?.textColor);\n}\n\n// The selection's own explicit color: a link's color when on a link, else the\n// inline text-color mark. May be \"\" when the selection only inherits its color\n// — that drives the swatch's set/unset state, while `effectiveTextColor()`\n// seeds the wheel so it still opens on the real rendered color.\nfunction explicitTextColor(): string {\n return isLinkSelection() ? linkColorAttr() : textStyleAttr(\"color\");\n}\n\nfunction getCurrentLineHeight(): string {\n return (props.editor?.getAttributes(\"paragraph\").lineHeight as string) || \"\";\n}\n\nfunction setLineHeight(value: string): void {\n const chain = props.editor?.chain().focus();\n if (value) chain?.setLineHeight(value).run();\n else chain?.unsetLineHeight().run();\n}\n\nfunction setLetterSpacing(value: string): void {\n const chain = props.editor?.chain().focus();\n if (value && value !== \"normal\") chain?.setLetterSpacing(value).run();\n else chain?.unsetLetterSpacing().run();\n}\n\nfunction getCurrentHighlight(): string {\n return (props.editor?.getAttributes(\"highlight\").color as string) || \"\";\n}\n\nfunction setHighlight(color: string): void {\n const chain = props.editor?.chain().focus();\n if (color) chain?.setHighlight({ color }).run();\n else chain?.unsetHighlight().run();\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.paragraphEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n flexDirection: 'column',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Row 1: Font family, Font size, Text color, Bold/Italic/Underline/Strikethrough -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <ToolbarSelect\n :model-value=\"textStyleAttr('fontFamily')\"\n :options=\"fontFamilies\"\n :label=\"t.paragraphEditor.fontFamily\"\n :placeholder=\"t.paragraphEditor.defaultFont\"\n width-class=\"tpl:w-32\"\n @update:model-value=\"setFontFamily\"\n />\n <ToolbarSelect\n :model-value=\"textStyleAttr('fontSize')\"\n :options=\"FONT_SIZE_OPTIONS\"\n :label=\"t.paragraphEditor.fontSize\"\n :placeholder=\"t.paragraphEditor.defaultSize\"\n width-class=\"tpl:w-20\"\n @update:model-value=\"setFontSize\"\n />\n <ToolbarSeparator />\n <ColorPicker\n swatch-only\n size=\"sm\"\n data-testid=\"text-color-picker\"\n :model-value=\"explicitTextColor()\"\n :seed-color=\"effectiveTextColor()\"\n :aria-label=\"t.paragraphEditor.textColor\"\n @update:model-value=\"setColor\"\n />\n <ColorPicker\n swatch-only\n size=\"sm\"\n data-testid=\"highlight-color-picker\"\n :model-value=\"getCurrentHighlight()\"\n :seed-color=\"DEFAULT_HIGHLIGHT_COLOR\"\n :aria-label=\"t.paragraphEditor.highlightColor\"\n @update:model-value=\"setHighlight\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"Bold\"\n :label=\"t.paragraphEditor.bold\"\n :active=\"editor.isActive('bold')\"\n :stroke-width=\"2.5\"\n @click=\"editor.chain().focus().toggleBold().run()\"\n />\n <ToolbarIconButton\n :icon=\"Italic\"\n :label=\"t.paragraphEditor.italic\"\n :active=\"editor.isActive('italic')\"\n @click=\"editor.chain().focus().toggleItalic().run()\"\n />\n <ToolbarIconButton\n :icon=\"Underline\"\n :label=\"t.paragraphEditor.underline\"\n :active=\"editor.isActive('underline')\"\n @click=\"editor.chain().focus().toggleUnderline().run()\"\n />\n <ToolbarIconButton\n :icon=\"Strikethrough\"\n :label=\"t.paragraphEditor.strikethrough\"\n :active=\"editor.isActive('strike')\"\n @click=\"editor.chain().focus().toggleStrike().run()\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"Subscript\"\n :label=\"t.paragraphEditor.subscript\"\n :active=\"editor.isActive('subscript')\"\n @click=\"editor.chain().focus().toggleSubscript().run()\"\n />\n <ToolbarIconButton\n :icon=\"Superscript\"\n :label=\"t.paragraphEditor.superscript\"\n :active=\"editor.isActive('superscript')\"\n @click=\"editor.chain().focus().toggleSuperscript().run()\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"Link\"\n :label=\"t.paragraphEditor.addLink\"\n :active=\"editor.isActive('link')\"\n @click=\"emit('open-link-dialog')\"\n />\n </div>\n <!-- Row 2: Lists, Alignment, LH, LS, Clear, Emoji, Merge tags -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <ToolbarIconButton\n :icon=\"List\"\n :label=\"t.paragraphEditor.bulletList\"\n :active=\"editor.isActive('bulletList')\"\n @click=\"editor.chain().focus().toggleBulletList().run()\"\n />\n <ToolbarIconButton\n :icon=\"ListOrdered\"\n :label=\"t.paragraphEditor.numberedList\"\n :active=\"editor.isActive('orderedList')\"\n @click=\"editor.chain().focus().toggleOrderedList().run()\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"AlignLeft\"\n :label=\"t.paragraphEditor.alignLeft\"\n :active=\"editor.isActive({ textAlign: 'left' })\"\n @click=\"editor.chain().focus().setTextAlign('left').run()\"\n />\n <ToolbarIconButton\n :icon=\"AlignCenter\"\n :label=\"t.paragraphEditor.alignCenter\"\n :active=\"editor.isActive({ textAlign: 'center' })\"\n @click=\"editor.chain().focus().setTextAlign('center').run()\"\n />\n <ToolbarIconButton\n :icon=\"AlignRight\"\n :label=\"t.paragraphEditor.alignRight\"\n :active=\"editor.isActive({ textAlign: 'right' })\"\n @click=\"editor.chain().focus().setTextAlign('right').run()\"\n />\n <ToolbarIconButton\n :icon=\"AlignJustify\"\n :label=\"t.paragraphEditor.alignJustify\"\n :active=\"editor.isActive({ textAlign: 'justify' })\"\n @click=\"editor.chain().focus().setTextAlign('justify').run()\"\n />\n <ToolbarSeparator />\n <ToolbarSelect\n :model-value=\"getCurrentLineHeight()\"\n :options=\"LINE_HEIGHT_OPTIONS\"\n :label=\"t.paragraphEditor.lineHeight\"\n placeholder=\"LH\"\n width-class=\"tpl:w-16\"\n @update:model-value=\"setLineHeight\"\n />\n <ToolbarSelect\n :model-value=\"textStyleAttr('letterSpacing')\"\n :options=\"LETTER_SPACING_OPTIONS\"\n :label=\"t.paragraphEditor.letterSpacing\"\n placeholder=\"LS\"\n width-class=\"tpl:w-20\"\n @update:model-value=\"setLetterSpacing\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"RemoveFormatting\"\n :label=\"t.paragraphEditor.clearFormatting\"\n @click=\"editor.chain().focus().clearNodes().unsetAllMarks().run()\"\n />\n <ToolbarSeparator />\n <EmojiPickerDropdown @insert=\"insertEmoji\" />\n <template v-if=\"canRequestMergeTag\">\n <ToolbarSeparator />\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"emit('add-merge-tag')\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n </template>\n <template v-if=\"canInsertLogicTag\">\n <ToolbarSeparator />\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"emit('add-logic-tag')\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n </div>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport EmojiPickerDropdown from \"./EmojiPickerDropdown.vue\";\nimport ToolbarIconButton from \"../toolbar/ToolbarIconButton.vue\";\nimport ToolbarSeparator from \"../toolbar/ToolbarSeparator.vue\";\nimport ToolbarSelect from \"../toolbar/ToolbarSelect.vue\";\nimport { useI18n } from \"../../composables\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { Editor } from \"@tiptap/core\";\nimport {\n AlignCenter,\n AlignJustify,\n AlignLeft,\n AlignRight,\n Bold,\n Italic,\n Braces,\n Link,\n List,\n ListOrdered,\n LoaderCircle,\n RemoveFormatting,\n ScanLine,\n Strikethrough,\n Subscript,\n Superscript,\n Underline,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n EDITOR_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n FONTS_MANAGER_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n requireInject,\n} from \"../../keys\";\nimport {\n DEFAULT_HIGHLIGHT_COLOR,\n FONT_SIZE_OPTIONS,\n LINE_HEIGHT_OPTIONS,\n LETTER_SPACING_OPTIONS,\n} from \"../../constants/styleConstants\";\nimport { resolveEffectiveTextColor } from \"../../utils/richTextColor\";\n\nconst props = defineProps<{\n editor: Editor | null;\n toolbarPosition: { top: number; left: number };\n isLoading: boolean;\n canRequestMergeTag: boolean;\n canInsertLogicTag: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"open-link-dialog\"): void;\n (e: \"add-merge-tag\"): void;\n (e: \"add-logic-tag\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"ParagraphToolbar\");\nconst popoverRoot = usePopoverRoot();\n// Picker may be null in non-editor contexts (e.g. isolated component tests).\n// When it's provided AND open, hide the floating toolbar — leaving it\n// visible behind the modal is visually noisy and the toolbar's Tailwind\n// z-index utility doesn't compile reliably, so it can sometimes paint\n// over the modal backdrop.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\n// Editor may be absent in isolated tests; used only to resolve the inherited\n// document text color for the text-color swatch.\nconst editorReturn = inject(EDITOR_KEY, null);\n// Hide the floating toolbar while either built-in picker modal is open, so it\n// doesn't overlap the dialog.\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst fontFamilies = fontsManager.fonts;\n\nfunction insertEmoji(emoji: string): void {\n props.editor?.chain().focus().insertContent(emoji).run();\n}\n\nfunction textStyleAttr(attr: string): string {\n return (props.editor?.getAttributes(\"textStyle\")[attr] as string) || \"\";\n}\n\nfunction setFontFamily(family: string): void {\n const chain = props.editor?.chain().focus();\n if (family) chain?.setFontFamily(family).run();\n else chain?.unsetFontFamily().run();\n}\n\nfunction setFontSize(size: string): void {\n const chain = props.editor?.chain().focus();\n if (size) chain?.setFontSize(size).run();\n else chain?.unsetFontSize().run();\n}\n\n// When the selection is a link, the color control operates on the link itself\n// (its `color` attribute → inline on the `<a>`) so the link's text and underline\n// stay in sync; otherwise it applies a plain inline text-color mark.\nfunction isLinkSelection(): boolean {\n return props.editor?.isActive(\"link\") ?? false;\n}\n\nfunction linkColorAttr(): string {\n return (props.editor?.getAttributes(\"link\").color as string) || \"\";\n}\n\n// Set (or clear, when empty) the color of the current selection. On a link the\n// whole link is recolored (extendMarkRange) so it doesn't split into segments.\nfunction setColor(color: string): void {\n const chain = props.editor?.chain().focus();\n if (!chain) return;\n if (isLinkSelection()) {\n chain.extendMarkRange(\"link\").updateAttributes(\"link\", {\n color: color || null,\n });\n // A per-link color takes absolute priority over any inner text-color: strip\n // the inline text color on the link range so the `<a>` color alone drives\n // the glyphs and the underline (mirrors useRichTextLinkDialog.insertLink).\n // Only when setting a color — clearing leaves the text color untouched.\n if (color) chain.unsetColor();\n chain.run();\n } else if (color) {\n // A text color applied over a selection that includes a link unifies the\n // link with it: update the link's own color so the `<a>` (which paints the\n // underline and is what the dialog/toolbar read) matches the recolored\n // glyphs. A no-op on a selection with no link. Mirror of the link branch\n // above — either way a link never ends up with a text color fighting its\n // own color.\n chain.setColor(color).updateAttributes(\"link\", { color }).run();\n } else {\n chain.unsetColor().run();\n }\n}\n\n// The color the current selection actually renders in — a link's own color (else\n// the effective document link color: link color, then text color), or for plain\n// text the inline mark else the document text color. Keeps the swatch truthful\n// rather than showing a hard-coded default (issue #373 / per-link color).\nfunction effectiveTextColor(): string {\n const settings = editorReturn?.content.value.settings;\n if (isLinkSelection()) {\n return resolveEffectiveTextColor(\n linkColorAttr(),\n settings?.linkColor || settings?.textColor,\n );\n }\n return resolveEffectiveTextColor(textStyleAttr(\"color\"), settings?.textColor);\n}\n\n// The selection's own explicit color: a link's color when on a link, else the\n// inline text-color mark. May be \"\" when the selection only inherits its color\n// — that drives the swatch's set/unset state, while `effectiveTextColor()`\n// seeds the wheel so it still opens on the real rendered color.\nfunction explicitTextColor(): string {\n return isLinkSelection() ? linkColorAttr() : textStyleAttr(\"color\");\n}\n\nfunction getCurrentLineHeight(): string {\n return (props.editor?.getAttributes(\"paragraph\").lineHeight as string) || \"\";\n}\n\nfunction setLineHeight(value: string): void {\n const chain = props.editor?.chain().focus();\n if (value) chain?.setLineHeight(value).run();\n else chain?.unsetLineHeight().run();\n}\n\nfunction setLetterSpacing(value: string): void {\n const chain = props.editor?.chain().focus();\n if (value && value !== \"normal\") chain?.setLetterSpacing(value).run();\n else chain?.unsetLetterSpacing().run();\n}\n\nfunction getCurrentHighlight(): string {\n return (props.editor?.getAttributes(\"highlight\").color as string) || \"\";\n}\n\nfunction setHighlight(color: string): void {\n const chain = props.editor?.chain().focus();\n if (color) chain?.setHighlight({ color }).run();\n else chain?.unsetHighlight().run();\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.paragraphEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n flexDirection: 'column',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Row 1: Font family, Font size, Text color, Bold/Italic/Underline/Strikethrough -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <ToolbarSelect\n :model-value=\"textStyleAttr('fontFamily')\"\n :options=\"fontFamilies\"\n :label=\"t.paragraphEditor.fontFamily\"\n :placeholder=\"t.paragraphEditor.defaultFont\"\n width-class=\"tpl:w-32\"\n @update:model-value=\"setFontFamily\"\n />\n <ToolbarSelect\n :model-value=\"textStyleAttr('fontSize')\"\n :options=\"FONT_SIZE_OPTIONS\"\n :label=\"t.paragraphEditor.fontSize\"\n :placeholder=\"t.paragraphEditor.defaultSize\"\n width-class=\"tpl:w-20\"\n @update:model-value=\"setFontSize\"\n />\n <ToolbarSeparator />\n <ColorPicker\n swatch-only\n size=\"sm\"\n data-testid=\"text-color-picker\"\n :model-value=\"explicitTextColor()\"\n :seed-color=\"effectiveTextColor()\"\n :aria-label=\"t.paragraphEditor.textColor\"\n @update:model-value=\"setColor\"\n />\n <ColorPicker\n swatch-only\n size=\"sm\"\n data-testid=\"highlight-color-picker\"\n :model-value=\"getCurrentHighlight()\"\n :seed-color=\"DEFAULT_HIGHLIGHT_COLOR\"\n :aria-label=\"t.paragraphEditor.highlightColor\"\n @update:model-value=\"setHighlight\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"Bold\"\n :label=\"t.paragraphEditor.bold\"\n :active=\"editor.isActive('bold')\"\n :stroke-width=\"2.5\"\n @click=\"editor.chain().focus().toggleBold().run()\"\n />\n <ToolbarIconButton\n :icon=\"Italic\"\n :label=\"t.paragraphEditor.italic\"\n :active=\"editor.isActive('italic')\"\n @click=\"editor.chain().focus().toggleItalic().run()\"\n />\n <ToolbarIconButton\n :icon=\"Underline\"\n :label=\"t.paragraphEditor.underline\"\n :active=\"editor.isActive('underline')\"\n @click=\"editor.chain().focus().toggleUnderline().run()\"\n />\n <ToolbarIconButton\n :icon=\"Strikethrough\"\n :label=\"t.paragraphEditor.strikethrough\"\n :active=\"editor.isActive('strike')\"\n @click=\"editor.chain().focus().toggleStrike().run()\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"Subscript\"\n :label=\"t.paragraphEditor.subscript\"\n :active=\"editor.isActive('subscript')\"\n @click=\"editor.chain().focus().toggleSubscript().run()\"\n />\n <ToolbarIconButton\n :icon=\"Superscript\"\n :label=\"t.paragraphEditor.superscript\"\n :active=\"editor.isActive('superscript')\"\n @click=\"editor.chain().focus().toggleSuperscript().run()\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"Link\"\n :label=\"t.paragraphEditor.addLink\"\n :active=\"editor.isActive('link')\"\n @click=\"emit('open-link-dialog')\"\n />\n </div>\n <!-- Row 2: Lists, Alignment, LH, LS, Clear, Emoji, Merge tags -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <ToolbarIconButton\n :icon=\"List\"\n :label=\"t.paragraphEditor.bulletList\"\n :active=\"editor.isActive('bulletList')\"\n @click=\"editor.chain().focus().toggleBulletList().run()\"\n />\n <ToolbarIconButton\n :icon=\"ListOrdered\"\n :label=\"t.paragraphEditor.numberedList\"\n :active=\"editor.isActive('orderedList')\"\n @click=\"editor.chain().focus().toggleOrderedList().run()\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"AlignLeft\"\n :label=\"t.paragraphEditor.alignLeft\"\n :active=\"editor.isActive({ textAlign: 'left' })\"\n @click=\"editor.chain().focus().setTextAlign('left').run()\"\n />\n <ToolbarIconButton\n :icon=\"AlignCenter\"\n :label=\"t.paragraphEditor.alignCenter\"\n :active=\"editor.isActive({ textAlign: 'center' })\"\n @click=\"editor.chain().focus().setTextAlign('center').run()\"\n />\n <ToolbarIconButton\n :icon=\"AlignRight\"\n :label=\"t.paragraphEditor.alignRight\"\n :active=\"editor.isActive({ textAlign: 'right' })\"\n @click=\"editor.chain().focus().setTextAlign('right').run()\"\n />\n <ToolbarIconButton\n :icon=\"AlignJustify\"\n :label=\"t.paragraphEditor.alignJustify\"\n :active=\"editor.isActive({ textAlign: 'justify' })\"\n @click=\"editor.chain().focus().setTextAlign('justify').run()\"\n />\n <ToolbarSeparator />\n <ToolbarSelect\n :model-value=\"getCurrentLineHeight()\"\n :options=\"LINE_HEIGHT_OPTIONS\"\n :label=\"t.paragraphEditor.lineHeight\"\n placeholder=\"LH\"\n width-class=\"tpl:w-16\"\n @update:model-value=\"setLineHeight\"\n />\n <ToolbarSelect\n :model-value=\"textStyleAttr('letterSpacing')\"\n :options=\"LETTER_SPACING_OPTIONS\"\n :label=\"t.paragraphEditor.letterSpacing\"\n placeholder=\"LS\"\n width-class=\"tpl:w-20\"\n @update:model-value=\"setLetterSpacing\"\n />\n <ToolbarSeparator />\n <ToolbarIconButton\n :icon=\"RemoveFormatting\"\n :label=\"t.paragraphEditor.clearFormatting\"\n @click=\"editor.chain().focus().clearNodes().unsetAllMarks().run()\"\n />\n <ToolbarSeparator />\n <EmojiPickerDropdown @insert=\"insertEmoji\" />\n <template v-if=\"canRequestMergeTag\">\n <ToolbarSeparator />\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"emit('add-merge-tag')\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n </template>\n <template v-if=\"canInsertLogicTag\">\n <ToolbarSeparator />\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"emit('add-logic-tag')\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n </div>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { ParagraphBlock as ParagraphBlockType } from \"@templatical/types\";\nimport ParagraphToolbar from \"./ParagraphToolbar.vue\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: ParagraphBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst popoverRoot = usePopoverRoot();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"ParagraphEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { default: UnderlineExt },\n { default: SubscriptExt },\n { default: SuperscriptExt },\n { default: TextAlign },\n { TextStyle },\n { default: Color },\n { default: FontFamily },\n { default: Highlight },\n {\n MergeTagNode,\n MergeTagSuggestion,\n LogicMergeTagNode,\n FontSize,\n LineHeight,\n LetterSpacing,\n LineBoundaryKeys,\n },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"@tiptap/extension-underline\"),\n import(\"@tiptap/extension-subscript\"),\n import(\"@tiptap/extension-superscript\"),\n import(\"@tiptap/extension-text-align\"),\n import(\"@tiptap/extension-text-style\"),\n import(\"@tiptap/extension-color\"),\n import(\"@tiptap/extension-font-family\"),\n import(\"@tiptap/extension-highlight\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n // StarterKit also bundles @tiptap/extension-underline; the editor\n // registers its own UnderlineExt below, so disable StarterKit's copy\n // to avoid a duplicate `underline` extension (and its console warning).\n underline: false,\n }),\n UnderlineExt,\n SubscriptExt,\n SuperscriptExt,\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n TextAlign.configure({ types: [\"paragraph\"] }),\n TextStyle,\n Color,\n FontFamily,\n Highlight.configure({ multicolor: true }),\n FontSize,\n LineHeight,\n LetterSpacing,\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <ParagraphToolbar\n :editor=\"editor\"\n :toolbar-position=\"toolbarPosition\"\n :is-loading=\"isLoading\"\n :can-request-merge-tag=\"canRequestMergeTag\"\n :can-insert-logic-tag=\"canInsertLogicTag\"\n @open-link-dialog=\"openLinkDialog\"\n @add-merge-tag=\"handleAddMergeTag\"\n @add-logic-tag=\"handleAddLogicTag\"\n />\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { ParagraphBlock as ParagraphBlockType } from \"@templatical/types\";\nimport ParagraphToolbar from \"./ParagraphToolbar.vue\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: ParagraphBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst popoverRoot = usePopoverRoot();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"ParagraphEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { default: UnderlineExt },\n { default: SubscriptExt },\n { default: SuperscriptExt },\n { default: TextAlign },\n { TextStyle },\n { default: Color },\n { default: FontFamily },\n { default: Highlight },\n {\n MergeTagNode,\n MergeTagSuggestion,\n LogicMergeTagNode,\n FontSize,\n LineHeight,\n LetterSpacing,\n LineBoundaryKeys,\n },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"@tiptap/extension-underline\"),\n import(\"@tiptap/extension-subscript\"),\n import(\"@tiptap/extension-superscript\"),\n import(\"@tiptap/extension-text-align\"),\n import(\"@tiptap/extension-text-style\"),\n import(\"@tiptap/extension-color\"),\n import(\"@tiptap/extension-font-family\"),\n import(\"@tiptap/extension-highlight\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n // StarterKit also bundles @tiptap/extension-underline; the editor\n // registers its own UnderlineExt below, so disable StarterKit's copy\n // to avoid a duplicate `underline` extension (and its console warning).\n underline: false,\n }),\n UnderlineExt,\n SubscriptExt,\n SuperscriptExt,\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n TextAlign.configure({ types: [\"paragraph\"] }),\n TextStyle,\n Color,\n FontFamily,\n Highlight.configure({ multicolor: true }),\n FontSize,\n LineHeight,\n LetterSpacing,\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <ParagraphToolbar\n :editor=\"editor\"\n :toolbar-position=\"toolbarPosition\"\n :is-loading=\"isLoading\"\n :can-request-merge-tag=\"canRequestMergeTag\"\n :can-insert-logic-tag=\"canInsertLogicTag\"\n @open-link-dialog=\"openLinkDialog\"\n @add-merge-tag=\"handleAddMergeTag\"\n @add-logic-tag=\"handleAddLogicTag\"\n />\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;EAOA,IAAM,IAAO,GAIP,EACJ,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,OAAO,MACL,EAAS,GAEP,EAAE,MAAG,cAAW,EAAQ,GAExB,IAAY,EAAwB,IAAI,GACxC,IAAU,EAAwB,IAAI,GAEtC,IAAY,QAAe,EAAgB,KAAK;EAGtD,AAFA,EAAa,GAAW,CAAS,GAEjC,EAAe,SAAe;GAC5B,AAAI,EAAgB,SAClB,EAAiB;EAErB,CAAC;EAED,SAAS,EAAa,GAAqB;GAEzC,AADA,EAAK,UAAU,CAAK,GACpB,EAAiB;EACnB;oBAIE,EAAA,GAAA,EAmDM,OAAA;GAnDG,SAAA;GAAJ,KAAI;GAAU,OAAM;EACvB,GAAA,CAAA,EAcS,UAAA;GAbP,MAAK;GACL,OAAK,EAAA,CAAC,wBAAsB,EACsB,gCAAA,EAAA,CAAA,EAAA,CAAA,CAAA;GAGjD,cAAY,EAAA,CAAA,CAAC,CAAC,gBAAgB;GAC9B,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;GACzB,iBAAe,EAAA,CAAA;GAChB,iBAAc;GACd,iBAAc;GACb,SAAK,AAAA,EAAA,QAAE,GAAA,MAAA,EAAA,CAAA,KAAA,EAAA,CAAA,CAAA,CAAA,GAAA,CAAA;EAER,GAAA,CAAA,EAAsC,EAAA,CAAA,GAAA;GAA9B,MAAM;GAAK,gBAAc;EAG3B,CAAA,CAAA,GAAA,IAAA,CAAA,GAAA,EAAA,CAAA,KADR,EAAA,GAAA,EAkCM,OAAA;;GAhCJ,IAAG;GACC,SAAA;GAAJ,KAAI;GACJ,MAAK;GACL,cAAW;GACV,cAAY,EAAA,CAAA,CAAC,CAAC,gBAAgB;GAC/B,UAAS;GACT,OAAM;GACL,WAAO,AAAA,EAAA,OAAA,EAAA,GAAmB,GAAA,MAAA,EAAA,CAAA,KAAA,EAAA,CAAA,CAAA,CAAA,GAAA,CAAA,GAAgB,CAAA,QAAA,SAAA,CAAA,GAAA,CAAA,KAAA,CAAA;EAE3C,GAAA,EAAA,EAAA,EAAA,GAAA,EAsBM,GAAA,MAAA,EArBe,EAAA,CAAA,IAAZ,OADT,EAAA,GAAA,EAsBM,OAAA;GApBH,KAAK,EAAS;GACf,OAAM;EAEN,GAAA,CAAA,EAIM,OAJN,GAIM,EADD,EAAA,CAAA,CAAC,CAAC,MAAM,EAAS,IAAG,GAAA,CAAA,GAEzB,EAWM,OAXN,GAWM,EAVJ,EAAA,EAAA,GAAA,EASS,GAAA,MAAA,EARS,EAAS,SAAlB,OADT,EAAA,GAAA,EASS,UAAA;GAPN,KAAK;GACN,MAAK;GACJ,cAAY,EAAA,CAAA,CAAM,CAAC,EAAA,CAAA,CAAC,CAAC,gBAAgB,gBAAc,EAAI,SAAK,CAAA;GAC7D,OAAM;GACL,UAAK,MAAE,EAAa,CAAK;EAEvB,GAAA,EAAA,CAAK,GAAA,GAAA,CAAA;;;;;;;;;;;;;;;;oBExElB,EAAA,GAAA,EASS,UAAA;GARP,MAAK;GACL,OAAK,EAAA,CAAC,wBAAsB,EAAA,gCACc,EAAA,OAAM,CAAA,CAAA;GAC/C,cAAY,EAAA;GACZ,OAAO,EAAA;GACP,gBAAc,EAAA,SAAM,SAAA;EAErB,GAAA,EAAA,EAAA,GAAA,EAA4E,EAA5D,EAAA,IAAI,GAAA;GAAG,MAAM,EAAA,QAAI;GAAS,gBAAc,EAAA,eAAW;;;;CEnBnE,OAAM;CACN,eAAY;;;CAFd,OAAA,EAAA,GAAA,EAGQ,QAHR,CAGQ;;;;;;;;;;;;;;;;;ECUV,IAAM,IAAO;EAIb,SAAS,EAAS,GAAgB;GAChC,EAAK,qBAAsB,EAAE,OAA6B,KAAK;EACjE;EAEA,SAAS,EAAY,GAAkC;GACrD,OAAO,OAAO,KAAQ,WAAW,IAAM,EAAI;EAC7C;EAEA,SAAS,EAAY,GAAkC;GACrD,OAAO,OAAO,KAAQ,WAAW,IAAM,EAAI;EAC7C;oBAIE,EAAA,GAAA,EAkBS,UAAA;GAjBN,OAAK,EAAA,CAAA,4KAA4L,EAAA,cAAU,UAAA,CAAA;GAI3M,OAAO,EAAA;GACP,cAAY,EAAA;GACZ,OAAO,EAAA;GACC;EAET,GAAA,CAAA,EAAiD,UAAjD,GAAiD,EAA7B,EAAA,eAAW,EAAA,GAAA,CAAA,IAC/B,EAAA,EAAA,GAAA,EAMS,GAAA,MAAA,EALO,EAAA,UAAP,OADT,EAAA,GAAA,EAMS,UAAA;GAJN,KAAK,EAAY,CAAG;GACpB,OAAO,EAAY,CAAG;EAEpB,GAAA,EAAA,EAAY,CAAG,CAAA,GAAA,GAAA,EAAA;;;;;AE/BxB,SAAgB,EACd,GACA,GACQ;CACR,OAAO,KAAiB,KAAA;AAC1B;;;;;;;;;;;;;;;;;;;;;ECwBA,IAAM,IAAQ,GAQR,IAAO,GAMP,IAAc,EAAO,GAAkB,IAAI,GAC3C,IAAa,EAAO,IAAc,IAAI,GACtC,IAAe,EAAc,IAAmB,kBAAkB,GAClE,IAAc,EAAe,GAM7B,IAAS,EAAO,GAAsB,IAAI,GAC1C,IAAc,EAAO,GAAsB,IAAI,GAG/C,IAAe,EAAO,IAAY,IAAI,GAGtC,IAAe,SACZ,GAAQ,OAAO,SAAS,QAAW,GAAa,OAAO,SAAS,GACzE,GAEM,EAAE,SAAM,EAAQ,GAEhB,IAAe,EAAa;EAElC,SAAS,EAAY,GAAqB;GACxC,EAAM,QAAQ,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,cAAc,CAAK,CAAC,CAAC,IAAI;EACzD;EAEA,SAAS,EAAc,GAAsB;GAC3C,OAAQ,EAAM,QAAQ,cAAc,WAAW,CAAC,CAAC,MAAoB;EACvE;EAEA,SAAS,EAAc,GAAsB;GAC3C,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,CAAC,MAAM;GAC1C,AAAI,IAAQ,GAAO,cAAc,CAAM,CAAC,CAAC,IAAI,IACxC,GAAO,gBAAgB,CAAC,CAAC,IAAI;EACpC;EAEA,SAAS,EAAY,GAAoB;GACvC,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,CAAC,MAAM;GAC1C,AAAI,IAAM,GAAO,YAAY,CAAI,CAAC,CAAC,IAAI,IAClC,GAAO,cAAc,CAAC,CAAC,IAAI;EAClC;EAKA,SAAS,IAA2B;GAClC,OAAO,EAAM,QAAQ,SAAS,MAAM,KAAK;EAC3C;EAEA,SAAS,IAAwB;GAC/B,OAAQ,EAAM,QAAQ,cAAc,MAAM,CAAC,CAAC,SAAoB;EAClE;EAIA,SAAS,EAAS,GAAqB;GACrC,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,CAAC,MAAM;GACrC,MACD,EAAgB,KAClB,EAAM,gBAAgB,MAAM,CAAC,CAAC,iBAAiB,QAAQ,EACrD,OAAO,KAAS,KAClB,CAAC,GAKG,KAAO,EAAM,WAAW,GAC5B,EAAM,IAAI,KACD,IAOT,EAAM,SAAS,CAAK,CAAC,CAAC,iBAAiB,QAAQ,EAAE,SAAM,CAAC,CAAC,CAAC,IAAI,IAE9D,EAAM,WAAW,CAAC,CAAC,IAAI;EAE3B;EAMA,SAAS,IAA6B;GACpC,IAAM,IAAW,GAAc,QAAQ,MAAM;GAO7C,OANI,EAAgB,IACX,EACL,EAAc,GACd,GAAU,aAAa,GAAU,SACnC,IAEK,EAA0B,EAAc,OAAO,GAAG,GAAU,SAAS;EAC9E;EAMA,SAAS,IAA4B;GACnC,OAAO,EAAgB,IAAI,EAAc,IAAI,EAAc,OAAO;EACpE;EAEA,SAAS,IAA+B;GACtC,OAAQ,EAAM,QAAQ,cAAc,WAAW,CAAC,CAAC,cAAyB;EAC5E;EAEA,SAAS,EAAc,GAAqB;GAC1C,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,CAAC,MAAM;GAC1C,AAAI,IAAO,GAAO,cAAc,CAAK,CAAC,CAAC,IAAI,IACtC,GAAO,gBAAgB,CAAC,CAAC,IAAI;EACpC;EAEA,SAAS,EAAiB,GAAqB;GAC7C,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,CAAC,MAAM;GAC1C,AAAI,KAAS,MAAU,WAAU,GAAO,iBAAiB,CAAK,CAAC,CAAC,IAAI,IAC/D,GAAO,mBAAmB,CAAC,CAAC,IAAI;EACvC;EAEA,SAAS,IAA8B;GACrC,OAAQ,EAAM,QAAQ,cAAc,WAAW,CAAC,CAAC,SAAoB;EACvE;EAEA,SAAS,EAAa,GAAqB;GACzC,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,CAAC,MAAM;GAC1C,AAAI,IAAO,GAAO,aAAa,EAAE,SAAM,CAAC,CAAC,CAAC,IAAI,IACzC,GAAO,eAAe,CAAC,CAAC,IAAI;EACnC;mBAIkB,EAAA,CAAA,KAAW,CAAK,EAAA,SAAhC,EAAA,GAAA,EAyMW,GAAA;;GAzMoC,IAAI,EAAA,CAAA;EACjD,GAAA,CAAA,EAuMM,OAAA;GAtMH,kBAAgB,EAAA,CAAA;GACjB,MAAK;GACJ,cAAY,EAAA,CAAA,CAAC,CAAC,gBAAgB;GAC/B,OAAM;GACL,OAAK,EAAA;IAAe,GAAA,EAAA,CAAA;IAA6B,KAAA,GAAA,EAAA,gBAAgB,IAAG;IAAuB,MAAA,GAAA,EAAA,gBAAgB,KAAI;;;;EAQ/F,GAAA,CAAA,CAAA,EAAA,aAAa,EAAA,UAA9B,EAAA,GAAA,EAiLW,GAAA,EAAA,KAAA,EAAA,GAAA,CA/KT,EAkFM,OAlFN,IAkFM;GAjFJ,EAOE,GAAA;IANC,eAAa,EAAa,YAAA;IAC1B,SAAS,EAAA,CAAA;IACT,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,aAAa,EAAA,CAAA,CAAC,CAAC,gBAAgB;IAChC,eAAY;IACX,uBAAoB;;;;;;;GAEvB,EAOE,GAAA;IANC,eAAa,EAAa,UAAA;IAC1B,SAAS,EAAA,EAAA;IACT,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,aAAa,EAAA,CAAA,CAAC,CAAC,gBAAgB;IAChC,eAAY;IACX,uBAAoB;;;;;;;GAEvB,EAAoB,CAAA;GACpB,EAQE,GAAA;IAPA,eAAA;IACA,MAAK;IACL,eAAY;IACX,eAAa,EAAiB;IAC9B,cAAY,EAAkB;IAC9B,cAAY,EAAA,CAAA,CAAC,CAAC,gBAAgB;IAC9B,uBAAoB;;;;;;GAEvB,EAQE,GAAA;IAPA,eAAA;IACA,MAAK;IACL,eAAY;IACX,eAAa,EAAmB;IAChC,cAAY,EAAA,EAAA;IACZ,cAAY,EAAA,CAAA,CAAC,CAAC,gBAAgB;IAC9B,uBAAoB;;;;;;GAEvB,EAAoB,CAAA;GACpB,EAME,GAAA;IALC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,MAAA;IACvB,gBAAc;IACd,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,WAAU,CAAA,CAAG,IAAG;;;;;;GAEjD,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,QAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,aAAY,CAAA,CAAG,IAAG;;;;;;GAEnD,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,WAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,gBAAe,CAAA,CAAG,IAAG;;;;;;GAEtD,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,QAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,aAAY,CAAA,CAAG,IAAG;;;;;;GAEnD,EAAoB,CAAA;GACpB,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,WAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,gBAAe,CAAA,CAAG,IAAG;;;;;;GAEtD,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,aAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,kBAAiB,CAAA,CAAG,IAAG;;;;;;GAExD,EAAoB,CAAA;GACpB,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,MAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAI,kBAAA;;;;;;EAIhB,CAAA,GAAA,EA0FM,OA1FN,IA0FM;GAzFJ,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,YAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,iBAAgB,CAAA,CAAG,IAAG;;;;;;GAEvD,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,aAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,kBAAiB,CAAA,CAAG,IAAG;;;;;;GAExD,EAAoB,CAAA;GACpB,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,EAAA,WAAA,OAAA,CAAA;IACvB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,aAAY,MAAA,CAAA,CAAS,IAAG;;;;;;GAEzD,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,EAAA,WAAA,SAAA,CAAA;IACvB,SAAK,AAAA,EAAA,SAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,aAAY,QAAA,CAAA,CAAW,IAAG;;;;;;GAE3D,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,EAAA,WAAA,QAAA,CAAA;IACvB,SAAK,AAAA,EAAA,SAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,aAAY,OAAA,CAAA,CAAU,IAAG;;;;;;GAE1D,EAKE,GAAA;IAJC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,QAAQ,EAAA,OAAO,SAAQ,EAAA,WAAA,UAAA,CAAA;IACvB,SAAK,AAAA,EAAA,SAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,aAAY,SAAA,CAAA,CAAY,IAAG;;;;;;GAE5D,EAAoB,CAAA;GACpB,EAOE,GAAA;IANC,eAAa,EAAoB;IACjC,SAAS,EAAA,EAAA;IACT,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IAC1B,aAAY;IACZ,eAAY;IACX,uBAAoB;;;;;;GAEvB,EAOE,GAAA;IANC,eAAa,EAAa,eAAA;IAC1B,SAAS,EAAA,EAAA;IACT,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IAC1B,aAAY;IACZ,eAAY;IACX,uBAAoB;;;;;;GAEvB,EAAoB,CAAA;GACpB,EAIE,GAAA;IAHC,MAAM,EAAA,EAAA;IACN,OAAO,EAAA,CAAA,CAAC,CAAC,gBAAgB;IACzB,SAAK,AAAA,EAAA,SAAA,MAAE,EAAA,OAAO,MAAK,CAAA,CAAG,MAAK,CAAA,CAAG,WAAU,CAAA,CAAG,cAAa,CAAA,CAAG,IAAG;;GAEjE,EAAoB,CAAA;GACpB,EAA6C,IAAA,EAAvB,UAAQ,EAAW,CAAA;GACzB,EAAA,sBAAhB,EAAA,GAAA,EAYW,GAAA,EAAA,KAAA,EAAA,GAAA,CAXT,EAAoB,CAAA,GACpB,EASS,UAAA;IARP,MAAK;IACL,OAAM;IACL,cAAY,EAAA,CAAA,CAAC,CAAC,SAAS;IACvB,OAAO,EAAA,CAAA,CAAC,CAAC,SAAS;IAClB,SAAK,AAAA,EAAA,SAAA,MAAE,EAAI,eAAA;GAEZ,GAAA,CAAA,EAAyC,EAAA,EAAA,GAAA;IAA9B,MAAM;IAAK,gBAAc;GAAK,CAAA,GAAA,EAAA,MACzC,EAAG,EAAA,CAAA,CAAC,CAAC,SAAS,WAAW,GAAA,CAAA,CAAA,GAAA,GAAA,EAAA,CAAA,GAAA,EAAA,KAAA,EAAA,IAAA,EAAA;GAGb,EAAA,qBAAhB,EAAA,GAAA,EAaW,GAAA,EAAA,KAAA,EAAA,GAAA,CAZT,EAAoB,CAAA,GACpB,EAUS,UAAA;IATP,MAAK;IACL,OAAM;IACL,cAAY,EAAA,CAAA,CAAC,CAAC,SAAS;IACvB,OAAO,EAAA,CAAA,CAAC,CAAC,SAAS;IACnB,eAAY;IACX,SAAK,AAAA,EAAA,SAAA,MAAE,EAAI,eAAA;GAEZ,GAAA,CAAA,EAAuC,EAAA,EAAA,GAAA;IAA9B,MAAM;IAAK,gBAAc;GAAK,CAAA,GAAA,EAAA,MACvC,EAAG,EAAA,CAAA,CAAC,CAAC,SAAS,WAAW,GAAA,CAAA,CAAA,GAAA,GAAA,EAAA,CAAA,GAAA,EAAA,KAAA,EAAA,IAAA,EAAA;EAM/B,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,EAKM,OALN,GAKM,CAFJ,EAAiE,EAAA,EAAA,GAAA;GAAnD,OAAM;GAAe,MAAM;GAAK,gBAAc;EAAK,CAAA,GAAA,EAAA,MACjE,EAAG,EAAA,CAAA,CAAC,CAAC,OAAO,aAAa,GAAA,CAAA,CAAA,CAAA,EAAA,GAAA,IAAA,EAAA,CAAA,GAAA,GAAA,CAAA,IAAA,CAAA,KAAA,EAAA,IAAA,EAAA;;;;;;;;;;EE9XnC,IAAM,IAAQ,GAKR,IAAO,GAIP,IAAc,EAAe,GAE7B,EACJ,WACA,kBACA,cACA,cACA,UACA,mBACA,YACA,cACA,kBACA,uBACA,sBACA,mBACA,eACA,eACA,oBACA,sBACA,sBACA,yBACE,EAAkB;GACpB,eAAe,EAAM,MAAM;GAC3B,oBAAoB,EAAM,MAAM;GAChC,cAAc,EAAK,MAAM;GACzB,YAAY;GACZ,MAAM,eAAe,EACnB,cACA,WACA,gBACA,wBACA,0BACC;IACD,IAAM,CACJ,EAAE,QAAQ,GAAc,eAAe,KACvC,EAAE,SAAS,KACX,EAAE,SAAS,KACX,EAAE,SAAS,KACX,EAAE,SAAS,KACX,EAAE,SAAS,KACX,EAAE,SAAS,KACX,EAAE,gBACF,EAAE,SAAS,KACX,EAAE,SAAS,KACX,EAAE,SAAS,KACX,EACE,iBACA,uBACA,sBACA,aACA,eACA,kBACA,yBAEA,MAAM,QAAQ,IAAI;KACpB,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;KACP,OAAO;IACT,CAAC;IAED,OAAO;KACL;KACA;KACA,YAAY;MACV,EAAW,UAAU;OACnB,SAAS;OACT,WAAW;OACX,YAAY;OACZ,gBAAgB;OAIhB,MAAM;OAIN,WAAW;MACb,CAAC;MACD;MACA;MACA;MACA,EAAc,CAAO,CAAC,CAAC,UAAU;OAC/B,aAAa;OACb,gBAAgB;QACd,QAAQ;QACR,KAAK;OACP;MACF,CAAC;MACD,EAAU,UAAU,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;MAC5C;MACA;MACA;MACA,EAAU,UAAU,EAAE,YAAY,GAAK,CAAC;MACxC;MACA;MACA;MACA,EAAa,UAAU;OAAE;OAAW;MAAO,CAAC;MAC5C,EAAkB,UAAU,EAAE,UAAO,CAAC;MACtC;MACA,GAAI,KAAuB,KAAe,EAAU,SAAS,IACzD,CACE,EAAmB,UAAU;OAC3B;OACA,MAAM;OACN,WAAW;OACX;MACF,CAAC,CACH,IACA,CAAC;KACP;IACF;GACF;EACF,CAAC;oBAIC,EAAA,GAAA,EA+BM,OA/BN,IA+BM;GA9BJ,EASE,IAAA;IARC,QAAQ,EAAA,CAAA;IACR,oBAAkB,EAAA;IAClB,cAAY,EAAA,CAAA;IACZ,yBAAuB,EAAA,CAAA;IACvB,wBAAsB,EAAA,CAAA;IACtB,kBAAkB,EAAA,CAAA;IAClB,eAAe,EAAA,CAAA;IACf,eAAe,EAAA,CAAA;;;;;;;;;;;GAGlB,EAME,GAAA;IALC,QAAQ,EAAA,CAAA;IACR,kBAAgB,EAAA,CAAA;IAChB,cAAY,EAAA,CAAA;IACZ,cAAY,EAAA,CAAA;IACZ,SAAO,EAAA,CAAA;;;;;;;;GAGV,EAUE,GAAA;IATC,SAAS,EAAA,CAAA;IACT,mBAAiB,EAAA,CAAA,CAAM,EAAE,SAAQ,MAAA,KAAA;IAC1B,cAAY,EAAA,CAAA;IAAA,sBAAA,AAAA,EAAA,QAAA,MAAA,EAAA,CAAA,IAAA,EAAa,QAAA,IAAA;IACzB,YAAU,EAAA,CAAA;IAAA,oBAAA,AAAA,EAAA,QAAA,MAAA,EAAA,CAAA,IAAA,EAAO,QAAA,IAAA;IACjB,cAAY,EAAA,CAAA;IAAA,sBAAA,AAAA,EAAA,QAAA,MAAA,EAAA,CAAA,IAAA,EAAS,QAAA,IAAA;IAC5B,SAAO,EAAA,CAAA;IACP,UAAQ,EAAA,CAAA;IACR,UAAQ,EAAA,CAAA;IACR,WAAS,EAAA,CAAA"}