@templatical/editor 0.24.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AiChatSidebar-X4viKtvs.js → AiChatSidebar-CIkS8DsO.js} +3 -3
- package/dist/{AiFeatureMenu-DDAamRaA.js → AiFeatureMenu-Cji1LBJJ.js} +3 -3
- package/dist/{BlockIssueBadge-Qxrl7Ujl.js → BlockIssueBadge-BQiT0FML.js} +2 -2
- package/dist/{BlockPreviewCanvas-Cf-kxbEr.js → BlockPreviewCanvas-DAnwZofk.js} +10 -6
- package/dist/{CloudEditor-JIzhj1Xq.js → CloudEditor-CXaft6Ej.js} +60 -58
- package/dist/{CollaboratorBar-tC7AACTb.js → CollaboratorBar-BR-rL2wG.js} +2 -2
- package/dist/{ColorPicker--_2PbzFk.js → ColorPicker-B4ArSUhy.js} +3 -3
- package/dist/{CommentsSidebar-Ci2Ov8FG.js → CommentsSidebar-Bm5GLDts.js} +4 -4
- package/dist/{CountdownBlock-aZIKfBWY.js → CountdownBlock-CIVAQDVu.js} +1 -1
- package/dist/{CountdownToolbar-DHp6T0Q_.js → CountdownToolbar-DBmb3tlb.js} +3 -3
- package/dist/{DesignReferenceSidebar-DQsC-3hB.js → DesignReferenceSidebar-CnyJ8L-j.js} +3 -3
- package/dist/{IssuesPanel-CoOfib44.js → IssuesPanel-D3XgmYCt.js} +2 -2
- package/dist/{MergeTagModeToggle-CiFiSV0C.js → MergeTagModeToggle-BK0fI3P2.js} +3 -3
- package/dist/{NumberWithSuffix-CXFbUqsx.js → NumberWithSuffix-Cjj9UNIp.js} +1 -1
- package/dist/{ParagraphEditor-Cy-bpEjI.js → ParagraphEditor-jGnbJEA2.js} +8 -8
- package/dist/{RichTextEditorContent-CYJko6id.js → RichTextEditorContent-DaaKdzjb.js} +4 -4
- package/dist/{SaveBlockDialog-CNTSaMEa.js → SaveBlockDialog-B3ngEldp.js} +7 -7
- package/dist/{SavedBlocksBrowserModal-CvxS5-YX.js → SavedBlocksBrowserModal-DawN3XaB.js} +6 -6
- package/dist/{SavedBlocksPanels-CNdUxS89.js → SavedBlocksPanels-eDC9Y46N.js} +1 -1
- package/dist/{SavedBlocksPickBar-BvXKhblL.js → SavedBlocksPickBar-CQbbXmzj.js} +2 -2
- package/dist/{SnapshotHistory-BqPSIV9L.js → SnapshotHistory-COxKtHM-.js} +4 -4
- package/dist/{TemplateScoringPanel-CMEuz9yF.js → TemplateScoringPanel-bUuqOO1z.js} +3 -3
- package/dist/{TestEmailModal-BDhnnCBx.js → TestEmailModal-CytkbZVQ.js} +87 -56
- package/dist/{TestEmailPanel--RwnwLn2.js → TestEmailPanel-CHUY7VZn.js} +1 -1
- package/dist/{TitleEditor-F7E_pU9H.js → TitleEditor-aMR1bOyY.js} +7 -7
- package/dist/{TplModal-BIiS24Of.js → TplModal-fmYNYdyp.js} +3 -3
- package/dist/{blockTypeIcons-C6PnLMp7.js → blockTypeIcons-C6b9rg88.js} +1 -1
- package/dist/bundle-stats.json +6 -6
- package/dist/{cdn/chunks/ca-CO4nq6TX.js → ca-8Dvbwo50.js} +5 -2
- package/dist/cdn/chunks/{AiChatSidebar-UbF3ePzR.js → AiChatSidebar-bkBKpNgE.js} +4 -4
- package/dist/cdn/chunks/{AiChatSidebar-UbF3ePzR.js.map → AiChatSidebar-bkBKpNgE.js.map} +1 -1
- package/dist/cdn/chunks/{AiFeatureMenu-OIwoJzHG.js → AiFeatureMenu-CI4SYTq2.js} +4 -4
- package/dist/cdn/chunks/{AiFeatureMenu-OIwoJzHG.js.map → AiFeatureMenu-CI4SYTq2.js.map} +1 -1
- package/dist/cdn/chunks/{BlockIssueBadge-Bd2EuT1C.js → BlockIssueBadge-CRnx8Dv9.js} +3 -3
- package/dist/cdn/chunks/{BlockIssueBadge-Bd2EuT1C.js.map → BlockIssueBadge-CRnx8Dv9.js.map} +1 -1
- package/dist/cdn/chunks/{BlockPreviewCanvas-C-ut8GkJ.js → BlockPreviewCanvas-CXCWhqbp.js} +11 -7
- package/dist/cdn/chunks/BlockPreviewCanvas-CXCWhqbp.js.map +1 -0
- package/dist/cdn/chunks/{CloudEditor-D87qsqbZ.js → CloudEditor-D-t7RyDh.js} +62 -60
- package/dist/cdn/chunks/CloudEditor-D-t7RyDh.js.map +1 -0
- package/dist/cdn/chunks/{CollaboratorBar-Br4RvNrA.js → CollaboratorBar-DcXYo47v.js} +3 -3
- package/dist/cdn/chunks/{CollaboratorBar-Br4RvNrA.js.map → CollaboratorBar-DcXYo47v.js.map} +1 -1
- package/dist/cdn/chunks/{ColorPicker-B-ymPPtU.js → ColorPicker-_1O7YU2z.js} +4 -4
- package/dist/cdn/chunks/{ColorPicker-B-ymPPtU.js.map → ColorPicker-_1O7YU2z.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsSidebar-Tv_WTcJv.js → CommentsSidebar-BEgLBYX-.js} +5 -5
- package/dist/cdn/chunks/{CommentsSidebar-Tv_WTcJv.js.map → CommentsSidebar-BEgLBYX-.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownBlock-cEcXSK91.js → CountdownBlock-CPnQA44g.js} +2 -2
- package/dist/cdn/chunks/{CountdownBlock-cEcXSK91.js.map → CountdownBlock-CPnQA44g.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownToolbar-CLp3_ebk.js → CountdownToolbar-Cdo-30U4.js} +4 -4
- package/dist/cdn/chunks/{CountdownToolbar-CLp3_ebk.js.map → CountdownToolbar-Cdo-30U4.js.map} +1 -1
- package/dist/cdn/chunks/{DesignReferenceSidebar-YeqxSsxO.js → DesignReferenceSidebar-DPqBr-ut.js} +4 -4
- package/dist/cdn/chunks/{DesignReferenceSidebar-YeqxSsxO.js.map → DesignReferenceSidebar-DPqBr-ut.js.map} +1 -1
- package/dist/cdn/chunks/{IssuesPanel-BhsvC3XQ.js → IssuesPanel-DoQWb23r.js} +3 -3
- package/dist/cdn/chunks/{IssuesPanel-BhsvC3XQ.js.map → IssuesPanel-DoQWb23r.js.map} +1 -1
- package/dist/cdn/chunks/{MergeTagModeToggle-DrdxQA0K.js → MergeTagModeToggle-CWuVC3MU.js} +4 -4
- package/dist/cdn/chunks/{MergeTagModeToggle-DrdxQA0K.js.map → MergeTagModeToggle-CWuVC3MU.js.map} +1 -1
- package/dist/cdn/chunks/{NumberWithSuffix-BK_Xh4w4.js → NumberWithSuffix-DQ4sgvt5.js} +2 -2
- package/dist/cdn/chunks/{NumberWithSuffix-BK_Xh4w4.js.map → NumberWithSuffix-DQ4sgvt5.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-DKz4vTWB.js → ParagraphEditor-Uv5LPF3W.js} +9 -9
- package/dist/cdn/chunks/{ParagraphEditor-DKz4vTWB.js.map → ParagraphEditor-Uv5LPF3W.js.map} +1 -1
- package/dist/cdn/chunks/{RichTextEditorContent-CJ2WVicQ.js → RichTextEditorContent-Bt3s09qY.js} +5 -5
- package/dist/cdn/chunks/{RichTextEditorContent-CJ2WVicQ.js.map → RichTextEditorContent-Bt3s09qY.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-DTemkKPX.js → SaveBlockDialog-BAew6EFd.js} +8 -8
- package/dist/cdn/chunks/{SaveBlockDialog-DTemkKPX.js.map → SaveBlockDialog-BAew6EFd.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-DbMaIbeH.js → SavedBlocksBrowserModal-Dn4IJDJW.js} +6 -6
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-DbMaIbeH.js.map → SavedBlocksBrowserModal-Dn4IJDJW.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPanels-CUffuDAC.js → SavedBlocksPanels-B62U5DYW.js} +2 -2
- package/dist/cdn/chunks/{SavedBlocksPanels-CUffuDAC.js.map → SavedBlocksPanels-B62U5DYW.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPickBar-D7SmNz2n.js → SavedBlocksPickBar-CyRKZg2T.js} +3 -3
- package/dist/cdn/chunks/{SavedBlocksPickBar-D7SmNz2n.js.map → SavedBlocksPickBar-CyRKZg2T.js.map} +1 -1
- package/dist/cdn/chunks/{SnapshotHistory-m5Z8bTej.js → SnapshotHistory-QQt0DKey.js} +4 -4
- package/dist/cdn/chunks/{SnapshotHistory-m5Z8bTej.js.map → SnapshotHistory-QQt0DKey.js.map} +1 -1
- package/dist/cdn/chunks/{TemplateScoringPanel-DgRU5fty.js → TemplateScoringPanel-BwyCMcit.js} +65 -65
- package/dist/cdn/chunks/{TemplateScoringPanel-DgRU5fty.js.map → TemplateScoringPanel-BwyCMcit.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailModal-Ce8-vzGh.js → TestEmailModal-B5UQkDJp.js} +89 -58
- package/dist/cdn/chunks/TestEmailModal-B5UQkDJp.js.map +1 -0
- package/dist/cdn/chunks/{TestEmailPanel-BlmIKKSL.js → TestEmailPanel-CvuBq4RA.js} +2 -2
- package/dist/cdn/chunks/{TestEmailPanel-BlmIKKSL.js.map → TestEmailPanel-CvuBq4RA.js.map} +1 -1
- package/dist/cdn/chunks/{TitleEditor-C6_bQxYR.js → TitleEditor-B9W954u9.js} +8 -8
- package/dist/cdn/chunks/{TitleEditor-C6_bQxYR.js.map → TitleEditor-B9W954u9.js.map} +1 -1
- package/dist/cdn/chunks/{TplModal-D6bFON2K.js → TplModal-B8UsvWc8.js} +4 -4
- package/dist/cdn/chunks/{TplModal-D6bFON2K.js.map → TplModal-B8UsvWc8.js.map} +1 -1
- package/dist/{ca-CO4nq6TX.js → cdn/chunks/ca-8Dvbwo50.js} +7 -0
- package/dist/cdn/chunks/{ca-CO4nq6TX.js.map → ca-8Dvbwo50.js.map} +1 -1
- package/dist/cdn/chunks/{cloud-CRlDc0Ev.js → cloud-DOaGUdR0.js} +2 -2
- package/dist/cdn/chunks/{cloud-CRlDc0Ev.js.map → cloud-DOaGUdR0.js.map} +1 -1
- package/dist/{de-CUeaFJph.js → cdn/chunks/de-CieSMCUp.js} +7 -0
- package/dist/cdn/chunks/{de-CUeaFJph.js.map → de-CieSMCUp.js.map} +1 -1
- package/dist/{en-zCYIPAL5.js → cdn/chunks/en-D0t_2Fq_.js} +7 -0
- package/dist/cdn/chunks/{en-zCYIPAL5.js.map → en-D0t_2Fq_.js.map} +1 -1
- package/dist/cdn/chunks/{es-BSiH7YkP.js → es-DwDoNuLM.js} +6 -1
- package/dist/cdn/chunks/{es-BSiH7YkP.js.map → es-DwDoNuLM.js.map} +1 -1
- package/dist/cdn/chunks/{extensions-CKMD_er_.js → extensions-5jgtarD-.js} +19 -19
- package/dist/cdn/chunks/{extensions-CKMD_er_.js.map → extensions-5jgtarD-.js.map} +1 -1
- package/dist/{fr-DTVJ3det.js → cdn/chunks/fr-CSEEzv1_.js} +7 -0
- package/dist/cdn/chunks/{fr-DTVJ3det.js.map → fr-CSEEzv1_.js.map} +1 -1
- package/dist/cdn/chunks/keys-BxPCfHBH.js +12 -0
- package/dist/cdn/chunks/keys-BxPCfHBH.js.map +1 -0
- package/dist/{nl-BqSquFS3.js → cdn/chunks/nl-xyk1T2mA.js} +7 -0
- package/dist/cdn/chunks/{nl-BqSquFS3.js.map → nl-xyk1T2mA.js.map} +1 -1
- package/dist/{pt-BR-CMljK37h.js → cdn/chunks/pt-BR-yr6uOJgn.js} +7 -0
- package/dist/cdn/chunks/{pt-BR-CMljK37h.js.map → pt-BR-yr6uOJgn.js.map} +1 -1
- package/dist/cdn/chunks/{src-B4UhYbI8.js → src-B8W42vT-.js} +152 -152
- package/dist/cdn/chunks/{src-B4UhYbI8.js.map → src-B8W42vT-.js.map} +1 -1
- package/dist/cdn/chunks/{src-B3HKHglu.js → src-C417zeUg.js} +20 -20
- package/dist/cdn/chunks/{src-B3HKHglu.js.map → src-C417zeUg.js.map} +1 -1
- package/dist/cdn/chunks/{src-D5mfLR4V.js → src-CZjSXPYq.js} +8 -3
- package/dist/cdn/chunks/src-CZjSXPYq.js.map +1 -0
- package/dist/cdn/chunks/{src-CumZZNXZ.js → src-Dpj0x27M.js} +2 -2
- package/dist/cdn/chunks/{src-CumZZNXZ.js.map → src-Dpj0x27M.js.map} +1 -1
- package/dist/cdn/chunks/{styles--m9OCb_Z.js → styles-DANwMzW2.js} +743 -724
- package/dist/cdn/chunks/styles-DANwMzW2.js.map +1 -0
- package/dist/cdn/chunks/{useCloudI18n-sx1pqEbY.js → useCloudI18n-CjVzv11v.js} +2 -2
- package/dist/cdn/chunks/{useCloudI18n-sx1pqEbY.js.map → useCloudI18n-CjVzv11v.js.map} +1 -1
- package/dist/cdn/chunks/{useEditorCore-ZqF7P99K.js → useEditorCore-DfW-thCr.js} +472 -401
- package/dist/cdn/chunks/useEditorCore-DfW-thCr.js.map +1 -0
- package/dist/cdn/chunks/{useI18n-vD57BfVR.js → useI18n-DniEFeIl.js} +2 -2
- package/dist/cdn/chunks/{useI18n-vD57BfVR.js.map → useI18n-DniEFeIl.js.map} +1 -1
- package/dist/cdn/chunks/{useMergeTag-HatzNGgc.js → useMergeTag-Bk9_f5cc.js} +6 -6
- package/dist/cdn/chunks/{useMergeTag-HatzNGgc.js.map → useMergeTag-Bk9_f5cc.js.map} +1 -1
- package/dist/cdn/chunks/{usePopoverPosition-BdQ3Sv4B.js → usePopoverPosition-C4xFJCJ2.js} +2 -2
- package/dist/cdn/chunks/{usePopoverPosition-BdQ3Sv4B.js.map → usePopoverPosition-C4xFJCJ2.js.map} +1 -1
- package/dist/cdn/editor.css +1 -1
- package/dist/cdn/editor.js +21 -19
- package/dist/cdn/editor.js.map +1 -1
- package/dist/{cloud-R8ci6yR1.js → cloud-BZn3jqoL.js} +1 -1
- package/dist/{cdn/chunks/de-CUeaFJph.js → de-CieSMCUp.js} +5 -2
- package/dist/{dist-Ba5Q-UX1.js → dist-CQ0fVBQ3.js} +7 -2
- package/dist/{cdn/chunks/en-zCYIPAL5.js → en-D0t_2Fq_.js} +5 -2
- package/dist/{es-BSiH7YkP.js → es-DwDoNuLM.js} +5 -0
- package/dist/{extensions-CGxj6-2o.js → extensions-BjvyU9Ea.js} +3 -3
- package/dist/{cdn/chunks/fr-DTVJ3det.js → fr-CSEEzv1_.js} +5 -2
- package/dist/index.d.ts +26 -0
- package/dist/keys-BFPiNAIn.js +10 -0
- package/dist/{cdn/chunks/nl-BqSquFS3.js → nl-xyk1T2mA.js} +5 -2
- package/dist/{cdn/chunks/pt-BR-CMljK37h.js → pt-BR-yr6uOJgn.js} +5 -2
- package/dist/style.css +1 -1
- package/dist/{styles-C9AMvkJk.js → styles-C9GM-bBC.js} +885 -866
- package/dist/templatical-editor.js +93 -91
- package/dist/{useCloudI18n-BC_QV2Z7.js → useCloudI18n-B2lItQ6O.js} +1 -1
- package/dist/{useEditorCore-Dojb-P2f.js → useEditorCore-Ck_hBiGA.js} +908 -837
- package/dist/{useI18n-DzOcl2wl.js → useI18n-C1KcdX_K.js} +1 -1
- package/dist/{useMergeTag-BFxXCv3p.js → useMergeTag-DHCwRMfH.js} +3 -3
- package/dist/{usePopoverPosition-Dtr1Mp1x.js → usePopoverPosition-ClnEh_Cp.js} +1 -1
- package/package.json +7 -7
- package/dist/cdn/chunks/BlockPreviewCanvas-C-ut8GkJ.js.map +0 -1
- package/dist/cdn/chunks/CloudEditor-D87qsqbZ.js.map +0 -1
- package/dist/cdn/chunks/TestEmailModal-Ce8-vzGh.js.map +0 -1
- package/dist/cdn/chunks/keys-CswSK7eb.js +0 -12
- package/dist/cdn/chunks/keys-CswSK7eb.js.map +0 -1
- package/dist/cdn/chunks/src-D5mfLR4V.js.map +0 -1
- package/dist/cdn/chunks/styles--m9OCb_Z.js.map +0 -1
- package/dist/cdn/chunks/useEditorCore-ZqF7P99K.js.map +0 -1
- package/dist/keys-uWpGv4Pq.js +0 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentsSidebar-Tv_WTcJv.js","names":[],"sources":["../../../src/cloud/components/CommentsSidebar.vue","../../../src/cloud/components/CommentsSidebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { formatRelativeTime } from \"../../utils/formatRelativeTime\";\nimport {\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n COMMENTS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\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\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"filterBlock\", blockId: string | null): void;\n}>();\n\nconst { format } = useI18n();\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"CommentsSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"CommentsSidebar\");\nconst comments = requireInject(COMMENTS_KEY, \"CommentsSidebar\");\n\ntype FilterMode = \"all\" | \"unresolved\" | \"block\";\nconst filterMode = ref<FilterMode>(\"unresolved\");\nconst filterBlockId = ref<string | null>(null);\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\nconst currentUserId = computed(() => authManager.userConfig?.id ?? null);\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): 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 return !(editor.savedBlockIds?.value.has(filterBlockId.value) ?? true);\n});\n\nconst filteredComments = computed(() => {\n let items = comments.comments.value;\n\n if (filterMode.value === \"unresolved\") {\n items = items.filter((c) => !c.resolved_at);\n } else if (filterMode.value === \"block\" && filterBlockId.value) {\n items = items.filter((c) => c.block_id === filterBlockId.value);\n }\n\n return items;\n});\n\nwatch(\n () => props.visible,\n (isVisible) => {\n if (isVisible) {\n comments.loadComments();\n }\n },\n);\n\nwatch(\n () => editor.state.selectedBlockId,\n (newBlockId) => {\n if (filterMode.value === \"block\" && newBlockId) {\n filterBlockId.value = newBlockId;\n }\n },\n);\n\nfunction setFilter(mode: FilterMode, blockId?: string): void {\n filterMode.value = mode;\n filterBlockId.value = mode === \"block\" ? (blockId ?? null) : null;\n}\n\nfunction filterByBlock(blockId: string): void {\n setFilter(\"block\", blockId);\n emit(\"filterBlock\", 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\nasync function handleAddComment(): Promise<void> {\n const body = newCommentBody.value.trim();\n if (!body) {\n return;\n }\n\n const blockId =\n filterMode.value === \"block\" ? filterBlockId.value : undefined;\n await comments.addComment(body, blockId ?? undefined);\n newCommentBody.value = \"\";\n}\n\nasync function handleReply(parentId: string): Promise<void> {\n const body = replyBody.value.trim();\n if (!body) {\n return;\n }\n\n const parent = comments.comments.value.find((c) => c.id === parentId);\n await comments.addComment(body, parent?.block_id ?? undefined, parentId);\n replyingTo.value = null;\n replyBody.value = \"\";\n}\n\nasync function handleEdit(commentId: string): Promise<void> {\n const body = editBody.value.trim();\n if (!body) {\n return;\n }\n\n await comments.editComment(commentId, body);\n editingId.value = null;\n editBody.value = \"\";\n}\n\nasync function handleDelete(commentId: string): Promise<void> {\n await comments.removeComment(commentId);\n deletingId.value = null;\n}\n\nasync function handleResolve(commentId: string): Promise<void> {\n await comments.toggleResolve(commentId);\n}\n\nfunction isOwnComment(comment: Comment): boolean {\n return comment.author_identifier === currentUserId.value;\n}\n\nfunction formatTime(dateString: string): string {\n return formatRelativeTime(dateString, cloudT.snapshotHistory) ?? 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 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>{{ cloudT.comments.title }}</span>\n <span\n v-if=\"comments.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-bg)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </div>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:duration-150 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 tpl:duration-150\"\n :class=\"\n filterMode === 'unresolved' ? 'tpl-comment-filter--active' : ''\n \"\n @click=\"setFilter('unresolved')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'all' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('all')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'block' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('block', editor.state.selectedBlockId ?? undefined)\"\n >\n {{ cloudT.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=\"comments.isLoading.value\"\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 ? cloudT.comments.noCommentsHint\n : cloudT.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 >\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 isOwnComment(thread)\n ? cloudT.comments.ownedByYou\n : thread.author_name\n }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ formatTime(thread.created_at) }}\n </span>\n <span\n v-if=\"thread.updated_at !== thread.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <!-- Resolve toggle -->\n <button\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"\n thread.resolved_at\n ? cloudT.comments.unresolve\n : cloudT.comments.resolve\n \"\n @click=\"handleResolve(thread.id)\"\n >\n <CircleCheck\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl-resolve-icon\"\n :style=\"{\n color: thread.resolved_at\n ? 'var(--tpl-primary)'\n : undefined,\n }\"\n />\n </button>\n <!-- Edit (own only) -->\n <button\n v-if=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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.resolved_at\"\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(cloudT.comments.resolvedBy, {\n name: thread.resolved_by_name ?? \"\",\n })\n }}\n </span>\n </div>\n </Transition>\n\n <!-- Block anchor indicator -->\n <span\n v-if=\"thread.block_id && isBlockMissing(thread.block_id)\"\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 {{ cloudT.comments.missingBlock }}\n </span>\n <button\n v-else-if=\"thread.block_id\"\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:duration-150 tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n @click=\"editor.selectBlock(thread.block_id ?? '')\"\n >\n Block\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:duration-150 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!editBody.trim() || comments.isSubmitting.value\"\n @click=\"handleEdit(thread.id)\"\n >\n {{ cloudT.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:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(thread.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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:duration-150 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(cloudT.comments.replyOne, {\n count: String(thread.replies?.length ?? 0),\n })\n : format(cloudT.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 isOwnComment(reply)\n ? cloudT.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.created_at) }}\n </span>\n <span\n v-if=\"reply.updated_at !== reply.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.comments.edit\"\n @click=\"startEdit(reply)\"\n >\n <Pencil :size=\"11\" :stroke-width=\"2\" />\n </button>\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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-bg)]\"\n :disabled=\"\n !editBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleEdit(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 <div class=\"tpl:flex tpl:items-end tpl:gap-2\">\n <textarea\n v-model=\"replyBody\"\n class=\"tpl:flex-1 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 :placeholder=\"cloudT.comments.replyPlaceholder\"\n rows=\"2\"\n @keydown=\"handleReplyKeydown($event, thread.id)\"\n />\n <div class=\"tpl:flex tpl:shrink-0 tpl:flex-col tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"\n !replyBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleReply(thread.id)\"\n >\n <Send :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelReply()\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n\n <!-- New comment input -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\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 {{ cloudT.comments.saveTemplateFirst }}\n </div>\n <div\n v-else\n class=\"tpl-comments-input-wrapper 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 :placeholder=\"cloudT.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:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\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 {\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n COMMENTS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\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\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"filterBlock\", blockId: string | null): void;\n}>();\n\nconst { format } = useI18n();\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"CommentsSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"CommentsSidebar\");\nconst comments = requireInject(COMMENTS_KEY, \"CommentsSidebar\");\n\ntype FilterMode = \"all\" | \"unresolved\" | \"block\";\nconst filterMode = ref<FilterMode>(\"unresolved\");\nconst filterBlockId = ref<string | null>(null);\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\nconst currentUserId = computed(() => authManager.userConfig?.id ?? null);\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): 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 return !(editor.savedBlockIds?.value.has(filterBlockId.value) ?? true);\n});\n\nconst filteredComments = computed(() => {\n let items = comments.comments.value;\n\n if (filterMode.value === \"unresolved\") {\n items = items.filter((c) => !c.resolved_at);\n } else if (filterMode.value === \"block\" && filterBlockId.value) {\n items = items.filter((c) => c.block_id === filterBlockId.value);\n }\n\n return items;\n});\n\nwatch(\n () => props.visible,\n (isVisible) => {\n if (isVisible) {\n comments.loadComments();\n }\n },\n);\n\nwatch(\n () => editor.state.selectedBlockId,\n (newBlockId) => {\n if (filterMode.value === \"block\" && newBlockId) {\n filterBlockId.value = newBlockId;\n }\n },\n);\n\nfunction setFilter(mode: FilterMode, blockId?: string): void {\n filterMode.value = mode;\n filterBlockId.value = mode === \"block\" ? (blockId ?? null) : null;\n}\n\nfunction filterByBlock(blockId: string): void {\n setFilter(\"block\", blockId);\n emit(\"filterBlock\", 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\nasync function handleAddComment(): Promise<void> {\n const body = newCommentBody.value.trim();\n if (!body) {\n return;\n }\n\n const blockId =\n filterMode.value === \"block\" ? filterBlockId.value : undefined;\n await comments.addComment(body, blockId ?? undefined);\n newCommentBody.value = \"\";\n}\n\nasync function handleReply(parentId: string): Promise<void> {\n const body = replyBody.value.trim();\n if (!body) {\n return;\n }\n\n const parent = comments.comments.value.find((c) => c.id === parentId);\n await comments.addComment(body, parent?.block_id ?? undefined, parentId);\n replyingTo.value = null;\n replyBody.value = \"\";\n}\n\nasync function handleEdit(commentId: string): Promise<void> {\n const body = editBody.value.trim();\n if (!body) {\n return;\n }\n\n await comments.editComment(commentId, body);\n editingId.value = null;\n editBody.value = \"\";\n}\n\nasync function handleDelete(commentId: string): Promise<void> {\n await comments.removeComment(commentId);\n deletingId.value = null;\n}\n\nasync function handleResolve(commentId: string): Promise<void> {\n await comments.toggleResolve(commentId);\n}\n\nfunction isOwnComment(comment: Comment): boolean {\n return comment.author_identifier === currentUserId.value;\n}\n\nfunction formatTime(dateString: string): string {\n return formatRelativeTime(dateString, cloudT.snapshotHistory) ?? 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 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>{{ cloudT.comments.title }}</span>\n <span\n v-if=\"comments.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-bg)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </div>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:duration-150 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 tpl:duration-150\"\n :class=\"\n filterMode === 'unresolved' ? 'tpl-comment-filter--active' : ''\n \"\n @click=\"setFilter('unresolved')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'all' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('all')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'block' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('block', editor.state.selectedBlockId ?? undefined)\"\n >\n {{ cloudT.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=\"comments.isLoading.value\"\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 ? cloudT.comments.noCommentsHint\n : cloudT.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 >\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 isOwnComment(thread)\n ? cloudT.comments.ownedByYou\n : thread.author_name\n }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ formatTime(thread.created_at) }}\n </span>\n <span\n v-if=\"thread.updated_at !== thread.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <!-- Resolve toggle -->\n <button\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"\n thread.resolved_at\n ? cloudT.comments.unresolve\n : cloudT.comments.resolve\n \"\n @click=\"handleResolve(thread.id)\"\n >\n <CircleCheck\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl-resolve-icon\"\n :style=\"{\n color: thread.resolved_at\n ? 'var(--tpl-primary)'\n : undefined,\n }\"\n />\n </button>\n <!-- Edit (own only) -->\n <button\n v-if=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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.resolved_at\"\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(cloudT.comments.resolvedBy, {\n name: thread.resolved_by_name ?? \"\",\n })\n }}\n </span>\n </div>\n </Transition>\n\n <!-- Block anchor indicator -->\n <span\n v-if=\"thread.block_id && isBlockMissing(thread.block_id)\"\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 {{ cloudT.comments.missingBlock }}\n </span>\n <button\n v-else-if=\"thread.block_id\"\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:duration-150 tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n @click=\"editor.selectBlock(thread.block_id ?? '')\"\n >\n Block\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:duration-150 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!editBody.trim() || comments.isSubmitting.value\"\n @click=\"handleEdit(thread.id)\"\n >\n {{ cloudT.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:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(thread.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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:duration-150 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(cloudT.comments.replyOne, {\n count: String(thread.replies?.length ?? 0),\n })\n : format(cloudT.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 isOwnComment(reply)\n ? cloudT.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.created_at) }}\n </span>\n <span\n v-if=\"reply.updated_at !== reply.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.comments.edit\"\n @click=\"startEdit(reply)\"\n >\n <Pencil :size=\"11\" :stroke-width=\"2\" />\n </button>\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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-bg)]\"\n :disabled=\"\n !editBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleEdit(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 <div class=\"tpl:flex tpl:items-end tpl:gap-2\">\n <textarea\n v-model=\"replyBody\"\n class=\"tpl:flex-1 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 :placeholder=\"cloudT.comments.replyPlaceholder\"\n rows=\"2\"\n @keydown=\"handleReplyKeydown($event, thread.id)\"\n />\n <div class=\"tpl:flex tpl:shrink-0 tpl:flex-col tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"\n !replyBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleReply(thread.id)\"\n >\n <Send :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelReply()\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n\n <!-- New comment input -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\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 {{ cloudT.comments.saveTemplateFirst }}\n </div>\n <div\n v-else\n class=\"tpl-comments-input-wrapper 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 :placeholder=\"cloudT.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:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\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
|
+
{"version":3,"file":"CommentsSidebar-BEgLBYX-.js","names":[],"sources":["../../../src/cloud/components/CommentsSidebar.vue","../../../src/cloud/components/CommentsSidebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { formatRelativeTime } from \"../../utils/formatRelativeTime\";\nimport {\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n COMMENTS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\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\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"filterBlock\", blockId: string | null): void;\n}>();\n\nconst { format } = useI18n();\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"CommentsSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"CommentsSidebar\");\nconst comments = requireInject(COMMENTS_KEY, \"CommentsSidebar\");\n\ntype FilterMode = \"all\" | \"unresolved\" | \"block\";\nconst filterMode = ref<FilterMode>(\"unresolved\");\nconst filterBlockId = ref<string | null>(null);\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\nconst currentUserId = computed(() => authManager.userConfig?.id ?? null);\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): 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 return !(editor.savedBlockIds?.value.has(filterBlockId.value) ?? true);\n});\n\nconst filteredComments = computed(() => {\n let items = comments.comments.value;\n\n if (filterMode.value === \"unresolved\") {\n items = items.filter((c) => !c.resolved_at);\n } else if (filterMode.value === \"block\" && filterBlockId.value) {\n items = items.filter((c) => c.block_id === filterBlockId.value);\n }\n\n return items;\n});\n\nwatch(\n () => props.visible,\n (isVisible) => {\n if (isVisible) {\n comments.loadComments();\n }\n },\n);\n\nwatch(\n () => editor.state.selectedBlockId,\n (newBlockId) => {\n if (filterMode.value === \"block\" && newBlockId) {\n filterBlockId.value = newBlockId;\n }\n },\n);\n\nfunction setFilter(mode: FilterMode, blockId?: string): void {\n filterMode.value = mode;\n filterBlockId.value = mode === \"block\" ? (blockId ?? null) : null;\n}\n\nfunction filterByBlock(blockId: string): void {\n setFilter(\"block\", blockId);\n emit(\"filterBlock\", 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\nasync function handleAddComment(): Promise<void> {\n const body = newCommentBody.value.trim();\n if (!body) {\n return;\n }\n\n const blockId =\n filterMode.value === \"block\" ? filterBlockId.value : undefined;\n await comments.addComment(body, blockId ?? undefined);\n newCommentBody.value = \"\";\n}\n\nasync function handleReply(parentId: string): Promise<void> {\n const body = replyBody.value.trim();\n if (!body) {\n return;\n }\n\n const parent = comments.comments.value.find((c) => c.id === parentId);\n await comments.addComment(body, parent?.block_id ?? undefined, parentId);\n replyingTo.value = null;\n replyBody.value = \"\";\n}\n\nasync function handleEdit(commentId: string): Promise<void> {\n const body = editBody.value.trim();\n if (!body) {\n return;\n }\n\n await comments.editComment(commentId, body);\n editingId.value = null;\n editBody.value = \"\";\n}\n\nasync function handleDelete(commentId: string): Promise<void> {\n await comments.removeComment(commentId);\n deletingId.value = null;\n}\n\nasync function handleResolve(commentId: string): Promise<void> {\n await comments.toggleResolve(commentId);\n}\n\nfunction isOwnComment(comment: Comment): boolean {\n return comment.author_identifier === currentUserId.value;\n}\n\nfunction formatTime(dateString: string): string {\n return formatRelativeTime(dateString, cloudT.snapshotHistory) ?? 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 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>{{ cloudT.comments.title }}</span>\n <span\n v-if=\"comments.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-bg)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </div>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:duration-150 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 tpl:duration-150\"\n :class=\"\n filterMode === 'unresolved' ? 'tpl-comment-filter--active' : ''\n \"\n @click=\"setFilter('unresolved')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'all' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('all')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'block' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('block', editor.state.selectedBlockId ?? undefined)\"\n >\n {{ cloudT.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=\"comments.isLoading.value\"\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 ? cloudT.comments.noCommentsHint\n : cloudT.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 >\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 isOwnComment(thread)\n ? cloudT.comments.ownedByYou\n : thread.author_name\n }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ formatTime(thread.created_at) }}\n </span>\n <span\n v-if=\"thread.updated_at !== thread.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <!-- Resolve toggle -->\n <button\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"\n thread.resolved_at\n ? cloudT.comments.unresolve\n : cloudT.comments.resolve\n \"\n @click=\"handleResolve(thread.id)\"\n >\n <CircleCheck\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl-resolve-icon\"\n :style=\"{\n color: thread.resolved_at\n ? 'var(--tpl-primary)'\n : undefined,\n }\"\n />\n </button>\n <!-- Edit (own only) -->\n <button\n v-if=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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.resolved_at\"\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(cloudT.comments.resolvedBy, {\n name: thread.resolved_by_name ?? \"\",\n })\n }}\n </span>\n </div>\n </Transition>\n\n <!-- Block anchor indicator -->\n <span\n v-if=\"thread.block_id && isBlockMissing(thread.block_id)\"\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 {{ cloudT.comments.missingBlock }}\n </span>\n <button\n v-else-if=\"thread.block_id\"\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:duration-150 tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n @click=\"editor.selectBlock(thread.block_id ?? '')\"\n >\n Block\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:duration-150 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!editBody.trim() || comments.isSubmitting.value\"\n @click=\"handleEdit(thread.id)\"\n >\n {{ cloudT.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:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(thread.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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:duration-150 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(cloudT.comments.replyOne, {\n count: String(thread.replies?.length ?? 0),\n })\n : format(cloudT.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 isOwnComment(reply)\n ? cloudT.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.created_at) }}\n </span>\n <span\n v-if=\"reply.updated_at !== reply.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.comments.edit\"\n @click=\"startEdit(reply)\"\n >\n <Pencil :size=\"11\" :stroke-width=\"2\" />\n </button>\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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-bg)]\"\n :disabled=\"\n !editBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleEdit(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 <div class=\"tpl:flex tpl:items-end tpl:gap-2\">\n <textarea\n v-model=\"replyBody\"\n class=\"tpl:flex-1 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 :placeholder=\"cloudT.comments.replyPlaceholder\"\n rows=\"2\"\n @keydown=\"handleReplyKeydown($event, thread.id)\"\n />\n <div class=\"tpl:flex tpl:shrink-0 tpl:flex-col tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"\n !replyBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleReply(thread.id)\"\n >\n <Send :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelReply()\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n\n <!-- New comment input -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\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 {{ cloudT.comments.saveTemplateFirst }}\n </div>\n <div\n v-else\n class=\"tpl-comments-input-wrapper 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 :placeholder=\"cloudT.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:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\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 {\n EDITOR_KEY,\n AUTH_MANAGER_KEY,\n COMMENTS_KEY,\n requireInject,\n} from \"../../keys\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\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\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"filterBlock\", blockId: string | null): void;\n}>();\n\nconst { format } = useI18n();\nconst { t: cloudT } = useCloudI18nStrict();\nconst editor = requireInject(EDITOR_KEY, \"CommentsSidebar\");\nconst authManager = requireInject(AUTH_MANAGER_KEY, \"CommentsSidebar\");\nconst comments = requireInject(COMMENTS_KEY, \"CommentsSidebar\");\n\ntype FilterMode = \"all\" | \"unresolved\" | \"block\";\nconst filterMode = ref<FilterMode>(\"unresolved\");\nconst filterBlockId = ref<string | null>(null);\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\nconst currentUserId = computed(() => authManager.userConfig?.id ?? null);\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): 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 return !(editor.savedBlockIds?.value.has(filterBlockId.value) ?? true);\n});\n\nconst filteredComments = computed(() => {\n let items = comments.comments.value;\n\n if (filterMode.value === \"unresolved\") {\n items = items.filter((c) => !c.resolved_at);\n } else if (filterMode.value === \"block\" && filterBlockId.value) {\n items = items.filter((c) => c.block_id === filterBlockId.value);\n }\n\n return items;\n});\n\nwatch(\n () => props.visible,\n (isVisible) => {\n if (isVisible) {\n comments.loadComments();\n }\n },\n);\n\nwatch(\n () => editor.state.selectedBlockId,\n (newBlockId) => {\n if (filterMode.value === \"block\" && newBlockId) {\n filterBlockId.value = newBlockId;\n }\n },\n);\n\nfunction setFilter(mode: FilterMode, blockId?: string): void {\n filterMode.value = mode;\n filterBlockId.value = mode === \"block\" ? (blockId ?? null) : null;\n}\n\nfunction filterByBlock(blockId: string): void {\n setFilter(\"block\", blockId);\n emit(\"filterBlock\", 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\nasync function handleAddComment(): Promise<void> {\n const body = newCommentBody.value.trim();\n if (!body) {\n return;\n }\n\n const blockId =\n filterMode.value === \"block\" ? filterBlockId.value : undefined;\n await comments.addComment(body, blockId ?? undefined);\n newCommentBody.value = \"\";\n}\n\nasync function handleReply(parentId: string): Promise<void> {\n const body = replyBody.value.trim();\n if (!body) {\n return;\n }\n\n const parent = comments.comments.value.find((c) => c.id === parentId);\n await comments.addComment(body, parent?.block_id ?? undefined, parentId);\n replyingTo.value = null;\n replyBody.value = \"\";\n}\n\nasync function handleEdit(commentId: string): Promise<void> {\n const body = editBody.value.trim();\n if (!body) {\n return;\n }\n\n await comments.editComment(commentId, body);\n editingId.value = null;\n editBody.value = \"\";\n}\n\nasync function handleDelete(commentId: string): Promise<void> {\n await comments.removeComment(commentId);\n deletingId.value = null;\n}\n\nasync function handleResolve(commentId: string): Promise<void> {\n await comments.toggleResolve(commentId);\n}\n\nfunction isOwnComment(comment: Comment): boolean {\n return comment.author_identifier === currentUserId.value;\n}\n\nfunction formatTime(dateString: string): string {\n return formatRelativeTime(dateString, cloudT.snapshotHistory) ?? 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 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>{{ cloudT.comments.title }}</span>\n <span\n v-if=\"comments.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-bg)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </div>\n <button\n class=\"tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:duration-150 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 tpl:duration-150\"\n :class=\"\n filterMode === 'unresolved' ? 'tpl-comment-filter--active' : ''\n \"\n @click=\"setFilter('unresolved')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'all' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('all')\"\n >\n {{ cloudT.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 tpl:duration-150\"\n :class=\"filterMode === 'block' ? 'tpl-comment-filter--active' : ''\"\n @click=\"setFilter('block', editor.state.selectedBlockId ?? undefined)\"\n >\n {{ cloudT.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=\"comments.isLoading.value\"\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 ? cloudT.comments.noCommentsHint\n : cloudT.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 >\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 isOwnComment(thread)\n ? cloudT.comments.ownedByYou\n : thread.author_name\n }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ formatTime(thread.created_at) }}\n </span>\n <span\n v-if=\"thread.updated_at !== thread.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <!-- Resolve toggle -->\n <button\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"\n thread.resolved_at\n ? cloudT.comments.unresolve\n : cloudT.comments.resolve\n \"\n @click=\"handleResolve(thread.id)\"\n >\n <CircleCheck\n :size=\"13\"\n :stroke-width=\"2\"\n class=\"tpl-resolve-icon\"\n :style=\"{\n color: thread.resolved_at\n ? 'var(--tpl-primary)'\n : undefined,\n }\"\n />\n </button>\n <!-- Edit (own only) -->\n <button\n v-if=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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=\"isOwnComment(thread)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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.resolved_at\"\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(cloudT.comments.resolvedBy, {\n name: thread.resolved_by_name ?? \"\",\n })\n }}\n </span>\n </div>\n </Transition>\n\n <!-- Block anchor indicator -->\n <span\n v-if=\"thread.block_id && isBlockMissing(thread.block_id)\"\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 {{ cloudT.comments.missingBlock }}\n </span>\n <button\n v-else-if=\"thread.block_id\"\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:duration-150 tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n @click=\"editor.selectBlock(thread.block_id ?? '')\"\n >\n Block\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:duration-150 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!editBody.trim() || comments.isSubmitting.value\"\n @click=\"handleEdit(thread.id)\"\n >\n {{ cloudT.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:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelEdit()\"\n >\n {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(thread.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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:duration-150 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(cloudT.comments.replyOne, {\n count: String(thread.replies?.length ?? 0),\n })\n : format(cloudT.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 isOwnComment(reply)\n ? cloudT.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.created_at) }}\n </span>\n <span\n v-if=\"reply.updated_at !== reply.created_at\"\n class=\"tpl:text-[10px] tpl:italic tpl:text-[var(--tpl-text-dim)]\"\n >\n ({{ cloudT.comments.edited }})\n </span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-0.5\">\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.comments.edit\"\n @click=\"startEdit(reply)\"\n >\n <Pencil :size=\"11\" :stroke-width=\"2\" />\n </button>\n <button\n v-if=\"isOwnComment(reply)\"\n class=\"tpl-comment-action tpl-comment-delete tpl:rounded tpl:p-1 tpl:transition-colors tpl:duration-150\"\n :title=\"cloudT.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-bg)]\"\n :disabled=\"\n !editBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleEdit(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 {{ cloudT.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 @click=\"handleDelete(reply.id)\"\n >\n {{ cloudT.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 {{ cloudT.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 <div class=\"tpl:flex tpl:items-end tpl:gap-2\">\n <textarea\n v-model=\"replyBody\"\n class=\"tpl:flex-1 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 :placeholder=\"cloudT.comments.replyPlaceholder\"\n rows=\"2\"\n @keydown=\"handleReplyKeydown($event, thread.id)\"\n />\n <div class=\"tpl:flex tpl:shrink-0 tpl:flex-col tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\n :disabled=\"\n !replyBody.trim() || comments.isSubmitting.value\n \"\n @click=\"handleReply(thread.id)\"\n >\n <Send :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1.5 tpl:transition-colors tpl:duration-150 tpl:text-[var(--tpl-text-muted)]\"\n @click=\"cancelReply()\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n\n <!-- New comment input -->\n <div class=\"tpl:border-t tpl:p-3 tpl:border-[var(--tpl-border)]\">\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 {{ cloudT.comments.saveTemplateFirst }}\n </div>\n <div\n v-else\n class=\"tpl-comments-input-wrapper 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 :placeholder=\"cloudT.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:duration-150 tpl:disabled:opacity-40 tpl:text-[var(--tpl-primary)]\"\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,6 +1,6 @@
|
|
|
1
1
|
import { M as e, P as t, Z as n, ct as r, f as i, g as a, h as o, it as s, p as c, st as l, u, x as d } from "./draggable-JcMTNF3U.js";
|
|
2
2
|
import { g as f } from "./dist-lZgXNk4G.js";
|
|
3
|
-
import { t as p } from "./useI18n-
|
|
3
|
+
import { t as p } from "./useI18n-DniEFeIl.js";
|
|
4
4
|
//#region src/components/blocks/CountdownBlock.vue?vue&type=script&setup=true&lang.ts
|
|
5
5
|
var m = {
|
|
6
6
|
key: 0,
|
|
@@ -90,4 +90,4 @@ var m = {
|
|
|
90
90
|
//#endregion
|
|
91
91
|
export { _ as default };
|
|
92
92
|
|
|
93
|
-
//# sourceMappingURL=CountdownBlock-
|
|
93
|
+
//# sourceMappingURL=CountdownBlock-CPnQA44g.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CountdownBlock-cEcXSK91.js","names":[],"sources":["../../../src/components/blocks/CountdownBlock.vue","../../../src/components/blocks/CountdownBlock.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type {\n CountdownBlock as CountdownBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\nconst props = defineProps<{\n block: CountdownBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst now = ref(Date.now());\nuseIntervalFn(() => {\n now.value = Date.now();\n}, 1000);\n\nconst targetTime = computed(() => {\n if (!props.block.targetDate) return null;\n const date = new Date(props.block.targetDate);\n return isNaN(date.getTime()) ? null : date.getTime();\n});\n\nconst isExpired = computed(() => {\n if (!targetTime.value) return false;\n return now.value >= targetTime.value;\n});\n\nconst remaining = computed(() => {\n if (!targetTime.value || isExpired.value) {\n return { days: 0, hours: 0, minutes: 0, seconds: 0 };\n }\n const diff = targetTime.value - now.value;\n const totalSeconds = Math.floor(diff / 1000);\n return {\n days: Math.floor(totalSeconds / 86400),\n hours: Math.floor((totalSeconds % 86400) / 3600),\n minutes: Math.floor((totalSeconds % 3600) / 60),\n seconds: totalSeconds % 60,\n };\n});\n\nconst segments = computed(() => {\n const result: Array<{ value: string; label: string }> = [];\n if (props.block.showDays) {\n result.push({\n value: String(remaining.value.days).padStart(2, \"0\"),\n label: props.block.labelDays,\n });\n }\n if (props.block.showHours) {\n result.push({\n value: String(remaining.value.hours).padStart(2, \"0\"),\n label: props.block.labelHours,\n });\n }\n if (props.block.showMinutes) {\n result.push({\n value: String(remaining.value.minutes).padStart(2, \"0\"),\n label: props.block.labelMinutes,\n });\n }\n if (props.block.showSeconds) {\n result.push({\n value: String(remaining.value.seconds).padStart(2, \"0\"),\n label: props.block.labelSeconds,\n });\n }\n return result;\n});\n\nconst containerStyle = computed(() => ({\n backgroundColor: props.block.backgroundColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n\nconst digitStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n fontWeight: \"bold\" as const,\n color: props.block.digitColor,\n lineHeight: \"1.2\",\n}));\n\nconst labelStyle = computed(() => ({\n fontSize: `${props.block.labelFontSize}px`,\n color: props.block.labelColor,\n marginTop: \"4px\",\n}));\n\nconst separatorStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n color: props.block.digitColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n</script>\n\n<template>\n <!-- Empty state: no target date -->\n <div\n v-if=\"!block.targetDate\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-8 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.countdown.setDate }}\n </div>\n\n <!-- Expired + hidden -->\n <div\n v-else-if=\"isExpired && block.hideOnExpiry\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-4 tpl:text-xs tpl:text-[var(--tpl-text-dim)] tpl:opacity-50\"\n >\n {{ t.countdown.hidden }}\n </div>\n\n <!-- Expired with message -->\n <div\n v-else-if=\"isExpired\"\n :style=\"containerStyle\"\n class=\"tpl:py-4 tpl:text-center\"\n >\n <span\n :style=\"{\n fontSize: `${block.digitFontSize}px`,\n color: block.digitColor,\n }\"\n >\n {{ block.expiredMessage }}\n </span>\n </div>\n\n <!-- Active countdown -->\n <div\n v-else\n :style=\"containerStyle\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:py-4\"\n >\n <template v-for=\"(segment, index) in segments\" :key=\"segment.label\">\n <span v-if=\"index > 0\" :style=\"separatorStyle\" class=\"tpl:self-start\">\n {{ block.separator }}\n </span>\n <div class=\"tpl:text-center\">\n <div :style=\"digitStyle\">{{ segment.value }}</div>\n <div :style=\"labelStyle\">{{ segment.label }}</div>\n </div>\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n CountdownBlock as CountdownBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\nconst props = defineProps<{\n block: CountdownBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst now = ref(Date.now());\nuseIntervalFn(() => {\n now.value = Date.now();\n}, 1000);\n\nconst targetTime = computed(() => {\n if (!props.block.targetDate) return null;\n const date = new Date(props.block.targetDate);\n return isNaN(date.getTime()) ? null : date.getTime();\n});\n\nconst isExpired = computed(() => {\n if (!targetTime.value) return false;\n return now.value >= targetTime.value;\n});\n\nconst remaining = computed(() => {\n if (!targetTime.value || isExpired.value) {\n return { days: 0, hours: 0, minutes: 0, seconds: 0 };\n }\n const diff = targetTime.value - now.value;\n const totalSeconds = Math.floor(diff / 1000);\n return {\n days: Math.floor(totalSeconds / 86400),\n hours: Math.floor((totalSeconds % 86400) / 3600),\n minutes: Math.floor((totalSeconds % 3600) / 60),\n seconds: totalSeconds % 60,\n };\n});\n\nconst segments = computed(() => {\n const result: Array<{ value: string; label: string }> = [];\n if (props.block.showDays) {\n result.push({\n value: String(remaining.value.days).padStart(2, \"0\"),\n label: props.block.labelDays,\n });\n }\n if (props.block.showHours) {\n result.push({\n value: String(remaining.value.hours).padStart(2, \"0\"),\n label: props.block.labelHours,\n });\n }\n if (props.block.showMinutes) {\n result.push({\n value: String(remaining.value.minutes).padStart(2, \"0\"),\n label: props.block.labelMinutes,\n });\n }\n if (props.block.showSeconds) {\n result.push({\n value: String(remaining.value.seconds).padStart(2, \"0\"),\n label: props.block.labelSeconds,\n });\n }\n return result;\n});\n\nconst containerStyle = computed(() => ({\n backgroundColor: props.block.backgroundColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n\nconst digitStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n fontWeight: \"bold\" as const,\n color: props.block.digitColor,\n lineHeight: \"1.2\",\n}));\n\nconst labelStyle = computed(() => ({\n fontSize: `${props.block.labelFontSize}px`,\n color: props.block.labelColor,\n marginTop: \"4px\",\n}));\n\nconst separatorStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n color: props.block.digitColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n</script>\n\n<template>\n <!-- Empty state: no target date -->\n <div\n v-if=\"!block.targetDate\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-8 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.countdown.setDate }}\n </div>\n\n <!-- Expired + hidden -->\n <div\n v-else-if=\"isExpired && block.hideOnExpiry\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-4 tpl:text-xs tpl:text-[var(--tpl-text-dim)] tpl:opacity-50\"\n >\n {{ t.countdown.hidden }}\n </div>\n\n <!-- Expired with message -->\n <div\n v-else-if=\"isExpired\"\n :style=\"containerStyle\"\n class=\"tpl:py-4 tpl:text-center\"\n >\n <span\n :style=\"{\n fontSize: `${block.digitFontSize}px`,\n color: block.digitColor,\n }\"\n >\n {{ block.expiredMessage }}\n </span>\n </div>\n\n <!-- Active countdown -->\n <div\n v-else\n :style=\"containerStyle\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:py-4\"\n >\n <template v-for=\"(segment, index) in segments\" :key=\"segment.label\">\n <span v-if=\"index > 0\" :style=\"separatorStyle\" class=\"tpl:self-start\">\n {{ block.separator }}\n </span>\n <div class=\"tpl:text-center\">\n <div :style=\"digitStyle\">{{ segment.value }}</div>\n <div :style=\"labelStyle\">{{ segment.label }}</div>\n </div>\n </template>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CountdownBlock-CPnQA44g.js","names":[],"sources":["../../../src/components/blocks/CountdownBlock.vue","../../../src/components/blocks/CountdownBlock.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type {\n CountdownBlock as CountdownBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\nconst props = defineProps<{\n block: CountdownBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst now = ref(Date.now());\nuseIntervalFn(() => {\n now.value = Date.now();\n}, 1000);\n\nconst targetTime = computed(() => {\n if (!props.block.targetDate) return null;\n const date = new Date(props.block.targetDate);\n return isNaN(date.getTime()) ? null : date.getTime();\n});\n\nconst isExpired = computed(() => {\n if (!targetTime.value) return false;\n return now.value >= targetTime.value;\n});\n\nconst remaining = computed(() => {\n if (!targetTime.value || isExpired.value) {\n return { days: 0, hours: 0, minutes: 0, seconds: 0 };\n }\n const diff = targetTime.value - now.value;\n const totalSeconds = Math.floor(diff / 1000);\n return {\n days: Math.floor(totalSeconds / 86400),\n hours: Math.floor((totalSeconds % 86400) / 3600),\n minutes: Math.floor((totalSeconds % 3600) / 60),\n seconds: totalSeconds % 60,\n };\n});\n\nconst segments = computed(() => {\n const result: Array<{ value: string; label: string }> = [];\n if (props.block.showDays) {\n result.push({\n value: String(remaining.value.days).padStart(2, \"0\"),\n label: props.block.labelDays,\n });\n }\n if (props.block.showHours) {\n result.push({\n value: String(remaining.value.hours).padStart(2, \"0\"),\n label: props.block.labelHours,\n });\n }\n if (props.block.showMinutes) {\n result.push({\n value: String(remaining.value.minutes).padStart(2, \"0\"),\n label: props.block.labelMinutes,\n });\n }\n if (props.block.showSeconds) {\n result.push({\n value: String(remaining.value.seconds).padStart(2, \"0\"),\n label: props.block.labelSeconds,\n });\n }\n return result;\n});\n\nconst containerStyle = computed(() => ({\n backgroundColor: props.block.backgroundColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n\nconst digitStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n fontWeight: \"bold\" as const,\n color: props.block.digitColor,\n lineHeight: \"1.2\",\n}));\n\nconst labelStyle = computed(() => ({\n fontSize: `${props.block.labelFontSize}px`,\n color: props.block.labelColor,\n marginTop: \"4px\",\n}));\n\nconst separatorStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n color: props.block.digitColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n</script>\n\n<template>\n <!-- Empty state: no target date -->\n <div\n v-if=\"!block.targetDate\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-8 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.countdown.setDate }}\n </div>\n\n <!-- Expired + hidden -->\n <div\n v-else-if=\"isExpired && block.hideOnExpiry\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-4 tpl:text-xs tpl:text-[var(--tpl-text-dim)] tpl:opacity-50\"\n >\n {{ t.countdown.hidden }}\n </div>\n\n <!-- Expired with message -->\n <div\n v-else-if=\"isExpired\"\n :style=\"containerStyle\"\n class=\"tpl:py-4 tpl:text-center\"\n >\n <span\n :style=\"{\n fontSize: `${block.digitFontSize}px`,\n color: block.digitColor,\n }\"\n >\n {{ block.expiredMessage }}\n </span>\n </div>\n\n <!-- Active countdown -->\n <div\n v-else\n :style=\"containerStyle\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:py-4\"\n >\n <template v-for=\"(segment, index) in segments\" :key=\"segment.label\">\n <span v-if=\"index > 0\" :style=\"separatorStyle\" class=\"tpl:self-start\">\n {{ block.separator }}\n </span>\n <div class=\"tpl:text-center\">\n <div :style=\"digitStyle\">{{ segment.value }}</div>\n <div :style=\"labelStyle\">{{ segment.label }}</div>\n </div>\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n CountdownBlock as CountdownBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"../../composables/useI18n\";\n\nconst props = defineProps<{\n block: CountdownBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst now = ref(Date.now());\nuseIntervalFn(() => {\n now.value = Date.now();\n}, 1000);\n\nconst targetTime = computed(() => {\n if (!props.block.targetDate) return null;\n const date = new Date(props.block.targetDate);\n return isNaN(date.getTime()) ? null : date.getTime();\n});\n\nconst isExpired = computed(() => {\n if (!targetTime.value) return false;\n return now.value >= targetTime.value;\n});\n\nconst remaining = computed(() => {\n if (!targetTime.value || isExpired.value) {\n return { days: 0, hours: 0, minutes: 0, seconds: 0 };\n }\n const diff = targetTime.value - now.value;\n const totalSeconds = Math.floor(diff / 1000);\n return {\n days: Math.floor(totalSeconds / 86400),\n hours: Math.floor((totalSeconds % 86400) / 3600),\n minutes: Math.floor((totalSeconds % 3600) / 60),\n seconds: totalSeconds % 60,\n };\n});\n\nconst segments = computed(() => {\n const result: Array<{ value: string; label: string }> = [];\n if (props.block.showDays) {\n result.push({\n value: String(remaining.value.days).padStart(2, \"0\"),\n label: props.block.labelDays,\n });\n }\n if (props.block.showHours) {\n result.push({\n value: String(remaining.value.hours).padStart(2, \"0\"),\n label: props.block.labelHours,\n });\n }\n if (props.block.showMinutes) {\n result.push({\n value: String(remaining.value.minutes).padStart(2, \"0\"),\n label: props.block.labelMinutes,\n });\n }\n if (props.block.showSeconds) {\n result.push({\n value: String(remaining.value.seconds).padStart(2, \"0\"),\n label: props.block.labelSeconds,\n });\n }\n return result;\n});\n\nconst containerStyle = computed(() => ({\n backgroundColor: props.block.backgroundColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n\nconst digitStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n fontWeight: \"bold\" as const,\n color: props.block.digitColor,\n lineHeight: \"1.2\",\n}));\n\nconst labelStyle = computed(() => ({\n fontSize: `${props.block.labelFontSize}px`,\n color: props.block.labelColor,\n marginTop: \"4px\",\n}));\n\nconst separatorStyle = computed(() => ({\n fontSize: `${props.block.digitFontSize}px`,\n color: props.block.digitColor,\n fontFamily: props.block.fontFamily || \"inherit\",\n}));\n</script>\n\n<template>\n <!-- Empty state: no target date -->\n <div\n v-if=\"!block.targetDate\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-8 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.countdown.setDate }}\n </div>\n\n <!-- Expired + hidden -->\n <div\n v-else-if=\"isExpired && block.hideOnExpiry\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:py-4 tpl:text-xs tpl:text-[var(--tpl-text-dim)] tpl:opacity-50\"\n >\n {{ t.countdown.hidden }}\n </div>\n\n <!-- Expired with message -->\n <div\n v-else-if=\"isExpired\"\n :style=\"containerStyle\"\n class=\"tpl:py-4 tpl:text-center\"\n >\n <span\n :style=\"{\n fontSize: `${block.digitFontSize}px`,\n color: block.digitColor,\n }\"\n >\n {{ block.expiredMessage }}\n </span>\n </div>\n\n <!-- Active countdown -->\n <div\n v-else\n :style=\"containerStyle\"\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:py-4\"\n >\n <template v-for=\"(segment, index) in segments\" :key=\"segment.label\">\n <span v-if=\"index > 0\" :style=\"separatorStyle\" class=\"tpl:self-start\">\n {{ block.separator }}\n </span>\n <div class=\"tpl:text-center\">\n <div :style=\"digitStyle\">{{ segment.value }}</div>\n <div :style=\"labelStyle\">{{ segment.label }}</div>\n </div>\n </template>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { M as e, P as t, V as n, ct as r, f as i, g as a, it as o, m as s, ot as c, p as l, u, x as d, y as f } from "./draggable-JcMTNF3U.js";
|
|
2
|
-
import { t as p } from "./useI18n-
|
|
3
|
-
import { f as m, t as h } from "./ColorPicker-
|
|
4
|
-
import { i as g, n as _, r as v, t as y } from "./NumberWithSuffix-
|
|
2
|
+
import { t as p } from "./useI18n-DniEFeIl.js";
|
|
3
|
+
import { f as m, t as h } from "./ColorPicker-_1O7YU2z.js";
|
|
4
|
+
import { i as g, n as _, r as v, t as y } from "./NumberWithSuffix-DQ4sgvt5.js";
|
|
5
5
|
//#region src/components/toolbar/CountdownToolbar.vue?vue&type=script&setup=true&lang.ts
|
|
6
6
|
var b = ["value"], x = ["value"], S = ["value"], C = { class: "tpl:grid tpl:grid-cols-2 tpl:gap-2" }, w = ["value"], T = { value: "" }, E = ["value"], D = { class: "tpl:grid tpl:grid-cols-2 tpl:gap-3" }, O = { class: "tpl:grid tpl:grid-cols-2 tpl:gap-3" }, k = { class: "tpl:grid tpl:grid-cols-2 tpl:gap-2" }, A = [
|
|
7
7
|
"value",
|
|
@@ -194,4 +194,4 @@ var b = ["value"], x = ["value"], S = ["value"], C = { class: "tpl:grid tpl:grid
|
|
|
194
194
|
//#endregion
|
|
195
195
|
export { N as default };
|
|
196
196
|
|
|
197
|
-
//# sourceMappingURL=CountdownToolbar-
|
|
197
|
+
//# sourceMappingURL=CountdownToolbar-Cdo-30U4.js.map
|
package/dist/cdn/chunks/{CountdownToolbar-CLp3_ebk.js.map → CountdownToolbar-Cdo-30U4.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CountdownToolbar-CLp3_ebk.js","names":[],"sources":["../../../src/components/toolbar/CountdownToolbar.vue","../../../src/components/toolbar/CountdownToolbar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass } from \"../../constants/styleConstants\";\nimport type { CountdownBlock } from \"@templatical/types\";\nimport { computed } from \"vue\";\n\ndefineProps<{\n block: CountdownBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<CountdownBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst TIMEZONES = [\n \"UTC\",\n \"America/New_York\",\n \"America/Chicago\",\n \"America/Denver\",\n \"America/Los_Angeles\",\n \"Europe/London\",\n \"Europe/Berlin\",\n \"Europe/Paris\",\n \"Europe/Moscow\",\n \"Asia/Dubai\",\n \"Asia/Kolkata\",\n \"Asia/Shanghai\",\n \"Asia/Tokyo\",\n \"Australia/Sydney\",\n \"Pacific/Auckland\",\n];\n\nconst SEPARATORS = [\n { value: \":\", label: \":\" },\n { value: \"-\", label: \"-\" },\n { value: \" \", label: \"␣\" },\n];\n\ntype UnitKey = \"Days\" | \"Hours\" | \"Minutes\" | \"Seconds\";\nconst UNITS: UnitKey[] = [\"Days\", \"Hours\", \"Minutes\", \"Seconds\"];\n\nconst unitItems = computed(() =>\n UNITS.map((unit) => ({\n unit,\n showKey: `show${unit}` as const,\n labelKey: `label${unit}` as const,\n translationKey: unit.toLowerCase() as\n \"days\" | \"hours\" | \"minutes\" | \"seconds\",\n })),\n);\n\nfunction updateField(field: keyof CountdownBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<CountdownBlock>);\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.countdown.targetDate\">\n <input\n type=\"datetime-local\"\n :class=\"inputClass\"\n :value=\"block.targetDate\"\n @input=\"\n updateField('targetDate', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.timezone\">\n <select\n :class=\"inputClass\"\n :value=\"block.timezone\"\n @change=\"\n updateField('timezone', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option v-for=\"tz in TIMEZONES\" :key=\"tz\" :value=\"tz\">{{ tz }}</option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.display\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <ToggleSwitch\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n class=\"tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block[item.showKey]\"\n :label=\"t.countdown[item.translationKey]\"\n @update:model-value=\"updateField(item.showKey, $event)\"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.separator\">\n <SlidingPillSelect\n :options=\"SEPARATORS\"\n :model-value=\"block.separator\"\n @update:model-value=\"updateField('separator', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.countdown.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitFontSize\">\n <NumberWithSuffix\n :model-value=\"block.digitFontSize\"\n :min=\"12\"\n :max=\"72\"\n suffix=\"px\"\n @update:model-value=\"updateField('digitFontSize', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelFontSize\">\n <NumberWithSuffix\n :model-value=\"block.labelFontSize\"\n :min=\"8\"\n :max=\"24\"\n suffix=\"px\"\n @update:model-value=\"updateField('labelFontSize', $event)\"\n />\n </FieldRow>\n </div>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitColor\">\n <ColorPicker\n :model-value=\"block.digitColor\"\n @update:model-value=\"updateField('digitColor', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelColor\">\n <ColorPicker\n :model-value=\"block.labelColor\"\n @update:model-value=\"updateField('labelColor', $event)\"\n />\n </FieldRow>\n </div>\n\n <FieldRow :label=\"t.countdown.background\">\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.labels\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <input\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block[item.labelKey]\"\n :placeholder=\"t.countdown[item.translationKey]\"\n @input=\"\n updateField(item.labelKey, ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiry\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.expiredMessage\"\n :placeholder=\"t.countdown.expiredMessagePlaceholder\"\n @input=\"\n updateField('expiredMessage', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiredImageUrl\">\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.expiredImageUrl\"\n placeholder=\"https://...\"\n @input=\"\n updateField(\n 'expiredImageUrl',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n </FieldRow>\n\n <ToggleSwitch\n class=\"tpl:mb-3.5 tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.hideOnExpiry\"\n :label=\"t.countdown.hideOnExpiry\"\n @update:model-value=\"updateField('hideOnExpiry', $event)\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass } from \"../../constants/styleConstants\";\nimport type { CountdownBlock } from \"@templatical/types\";\nimport { computed } from \"vue\";\n\ndefineProps<{\n block: CountdownBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<CountdownBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst TIMEZONES = [\n \"UTC\",\n \"America/New_York\",\n \"America/Chicago\",\n \"America/Denver\",\n \"America/Los_Angeles\",\n \"Europe/London\",\n \"Europe/Berlin\",\n \"Europe/Paris\",\n \"Europe/Moscow\",\n \"Asia/Dubai\",\n \"Asia/Kolkata\",\n \"Asia/Shanghai\",\n \"Asia/Tokyo\",\n \"Australia/Sydney\",\n \"Pacific/Auckland\",\n];\n\nconst SEPARATORS = [\n { value: \":\", label: \":\" },\n { value: \"-\", label: \"-\" },\n { value: \" \", label: \"␣\" },\n];\n\ntype UnitKey = \"Days\" | \"Hours\" | \"Minutes\" | \"Seconds\";\nconst UNITS: UnitKey[] = [\"Days\", \"Hours\", \"Minutes\", \"Seconds\"];\n\nconst unitItems = computed(() =>\n UNITS.map((unit) => ({\n unit,\n showKey: `show${unit}` as const,\n labelKey: `label${unit}` as const,\n translationKey: unit.toLowerCase() as\n \"days\" | \"hours\" | \"minutes\" | \"seconds\",\n })),\n);\n\nfunction updateField(field: keyof CountdownBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<CountdownBlock>);\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.countdown.targetDate\">\n <input\n type=\"datetime-local\"\n :class=\"inputClass\"\n :value=\"block.targetDate\"\n @input=\"\n updateField('targetDate', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.timezone\">\n <select\n :class=\"inputClass\"\n :value=\"block.timezone\"\n @change=\"\n updateField('timezone', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option v-for=\"tz in TIMEZONES\" :key=\"tz\" :value=\"tz\">{{ tz }}</option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.display\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <ToggleSwitch\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n class=\"tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block[item.showKey]\"\n :label=\"t.countdown[item.translationKey]\"\n @update:model-value=\"updateField(item.showKey, $event)\"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.separator\">\n <SlidingPillSelect\n :options=\"SEPARATORS\"\n :model-value=\"block.separator\"\n @update:model-value=\"updateField('separator', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.countdown.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitFontSize\">\n <NumberWithSuffix\n :model-value=\"block.digitFontSize\"\n :min=\"12\"\n :max=\"72\"\n suffix=\"px\"\n @update:model-value=\"updateField('digitFontSize', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelFontSize\">\n <NumberWithSuffix\n :model-value=\"block.labelFontSize\"\n :min=\"8\"\n :max=\"24\"\n suffix=\"px\"\n @update:model-value=\"updateField('labelFontSize', $event)\"\n />\n </FieldRow>\n </div>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitColor\">\n <ColorPicker\n :model-value=\"block.digitColor\"\n @update:model-value=\"updateField('digitColor', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelColor\">\n <ColorPicker\n :model-value=\"block.labelColor\"\n @update:model-value=\"updateField('labelColor', $event)\"\n />\n </FieldRow>\n </div>\n\n <FieldRow :label=\"t.countdown.background\">\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.labels\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <input\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block[item.labelKey]\"\n :placeholder=\"t.countdown[item.translationKey]\"\n @input=\"\n updateField(item.labelKey, ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiry\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.expiredMessage\"\n :placeholder=\"t.countdown.expiredMessagePlaceholder\"\n @input=\"\n updateField('expiredMessage', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiredImageUrl\">\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.expiredImageUrl\"\n placeholder=\"https://...\"\n @input=\"\n updateField(\n 'expiredImageUrl',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n </FieldRow>\n\n <ToggleSwitch\n class=\"tpl:mb-3.5 tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.hideOnExpiry\"\n :label=\"t.countdown.hideOnExpiry\"\n @update:model-value=\"updateField('hideOnExpiry', $event)\"\n />\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CountdownToolbar-Cdo-30U4.js","names":[],"sources":["../../../src/components/toolbar/CountdownToolbar.vue","../../../src/components/toolbar/CountdownToolbar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass } from \"../../constants/styleConstants\";\nimport type { CountdownBlock } from \"@templatical/types\";\nimport { computed } from \"vue\";\n\ndefineProps<{\n block: CountdownBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<CountdownBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst TIMEZONES = [\n \"UTC\",\n \"America/New_York\",\n \"America/Chicago\",\n \"America/Denver\",\n \"America/Los_Angeles\",\n \"Europe/London\",\n \"Europe/Berlin\",\n \"Europe/Paris\",\n \"Europe/Moscow\",\n \"Asia/Dubai\",\n \"Asia/Kolkata\",\n \"Asia/Shanghai\",\n \"Asia/Tokyo\",\n \"Australia/Sydney\",\n \"Pacific/Auckland\",\n];\n\nconst SEPARATORS = [\n { value: \":\", label: \":\" },\n { value: \"-\", label: \"-\" },\n { value: \" \", label: \"␣\" },\n];\n\ntype UnitKey = \"Days\" | \"Hours\" | \"Minutes\" | \"Seconds\";\nconst UNITS: UnitKey[] = [\"Days\", \"Hours\", \"Minutes\", \"Seconds\"];\n\nconst unitItems = computed(() =>\n UNITS.map((unit) => ({\n unit,\n showKey: `show${unit}` as const,\n labelKey: `label${unit}` as const,\n translationKey: unit.toLowerCase() as\n \"days\" | \"hours\" | \"minutes\" | \"seconds\",\n })),\n);\n\nfunction updateField(field: keyof CountdownBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<CountdownBlock>);\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.countdown.targetDate\">\n <input\n type=\"datetime-local\"\n :class=\"inputClass\"\n :value=\"block.targetDate\"\n @input=\"\n updateField('targetDate', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.timezone\">\n <select\n :class=\"inputClass\"\n :value=\"block.timezone\"\n @change=\"\n updateField('timezone', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option v-for=\"tz in TIMEZONES\" :key=\"tz\" :value=\"tz\">{{ tz }}</option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.display\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <ToggleSwitch\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n class=\"tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block[item.showKey]\"\n :label=\"t.countdown[item.translationKey]\"\n @update:model-value=\"updateField(item.showKey, $event)\"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.separator\">\n <SlidingPillSelect\n :options=\"SEPARATORS\"\n :model-value=\"block.separator\"\n @update:model-value=\"updateField('separator', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.countdown.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitFontSize\">\n <NumberWithSuffix\n :model-value=\"block.digitFontSize\"\n :min=\"12\"\n :max=\"72\"\n suffix=\"px\"\n @update:model-value=\"updateField('digitFontSize', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelFontSize\">\n <NumberWithSuffix\n :model-value=\"block.labelFontSize\"\n :min=\"8\"\n :max=\"24\"\n suffix=\"px\"\n @update:model-value=\"updateField('labelFontSize', $event)\"\n />\n </FieldRow>\n </div>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitColor\">\n <ColorPicker\n :model-value=\"block.digitColor\"\n @update:model-value=\"updateField('digitColor', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelColor\">\n <ColorPicker\n :model-value=\"block.labelColor\"\n @update:model-value=\"updateField('labelColor', $event)\"\n />\n </FieldRow>\n </div>\n\n <FieldRow :label=\"t.countdown.background\">\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.labels\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <input\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block[item.labelKey]\"\n :placeholder=\"t.countdown[item.translationKey]\"\n @input=\"\n updateField(item.labelKey, ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiry\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.expiredMessage\"\n :placeholder=\"t.countdown.expiredMessagePlaceholder\"\n @input=\"\n updateField('expiredMessage', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiredImageUrl\">\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.expiredImageUrl\"\n placeholder=\"https://...\"\n @input=\"\n updateField(\n 'expiredImageUrl',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n </FieldRow>\n\n <ToggleSwitch\n class=\"tpl:mb-3.5 tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.hideOnExpiry\"\n :label=\"t.countdown.hideOnExpiry\"\n @update:model-value=\"updateField('hideOnExpiry', $event)\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass } from \"../../constants/styleConstants\";\nimport type { CountdownBlock } from \"@templatical/types\";\nimport { computed } from \"vue\";\n\ndefineProps<{\n block: CountdownBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<CountdownBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst TIMEZONES = [\n \"UTC\",\n \"America/New_York\",\n \"America/Chicago\",\n \"America/Denver\",\n \"America/Los_Angeles\",\n \"Europe/London\",\n \"Europe/Berlin\",\n \"Europe/Paris\",\n \"Europe/Moscow\",\n \"Asia/Dubai\",\n \"Asia/Kolkata\",\n \"Asia/Shanghai\",\n \"Asia/Tokyo\",\n \"Australia/Sydney\",\n \"Pacific/Auckland\",\n];\n\nconst SEPARATORS = [\n { value: \":\", label: \":\" },\n { value: \"-\", label: \"-\" },\n { value: \" \", label: \"␣\" },\n];\n\ntype UnitKey = \"Days\" | \"Hours\" | \"Minutes\" | \"Seconds\";\nconst UNITS: UnitKey[] = [\"Days\", \"Hours\", \"Minutes\", \"Seconds\"];\n\nconst unitItems = computed(() =>\n UNITS.map((unit) => ({\n unit,\n showKey: `show${unit}` as const,\n labelKey: `label${unit}` as const,\n translationKey: unit.toLowerCase() as\n \"days\" | \"hours\" | \"minutes\" | \"seconds\",\n })),\n);\n\nfunction updateField(field: keyof CountdownBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<CountdownBlock>);\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.countdown.targetDate\">\n <input\n type=\"datetime-local\"\n :class=\"inputClass\"\n :value=\"block.targetDate\"\n @input=\"\n updateField('targetDate', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.timezone\">\n <select\n :class=\"inputClass\"\n :value=\"block.timezone\"\n @change=\"\n updateField('timezone', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option v-for=\"tz in TIMEZONES\" :key=\"tz\" :value=\"tz\">{{ tz }}</option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.display\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <ToggleSwitch\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n class=\"tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block[item.showKey]\"\n :label=\"t.countdown[item.translationKey]\"\n @update:model-value=\"updateField(item.showKey, $event)\"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.separator\">\n <SlidingPillSelect\n :options=\"SEPARATORS\"\n :model-value=\"block.separator\"\n @update:model-value=\"updateField('separator', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.countdown.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitFontSize\">\n <NumberWithSuffix\n :model-value=\"block.digitFontSize\"\n :min=\"12\"\n :max=\"72\"\n suffix=\"px\"\n @update:model-value=\"updateField('digitFontSize', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelFontSize\">\n <NumberWithSuffix\n :model-value=\"block.labelFontSize\"\n :min=\"8\"\n :max=\"24\"\n suffix=\"px\"\n @update:model-value=\"updateField('labelFontSize', $event)\"\n />\n </FieldRow>\n </div>\n\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <FieldRow :label=\"t.countdown.digitColor\">\n <ColorPicker\n :model-value=\"block.digitColor\"\n @update:model-value=\"updateField('digitColor', $event)\"\n />\n </FieldRow>\n <FieldRow :label=\"t.countdown.labelColor\">\n <ColorPicker\n :model-value=\"block.labelColor\"\n @update:model-value=\"updateField('labelColor', $event)\"\n />\n </FieldRow>\n </div>\n\n <FieldRow :label=\"t.countdown.background\">\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.labels\">\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-2\">\n <input\n v-for=\"item in unitItems\"\n :key=\"item.unit\"\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block[item.labelKey]\"\n :placeholder=\"t.countdown[item.translationKey]\"\n @input=\"\n updateField(item.labelKey, ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiry\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.expiredMessage\"\n :placeholder=\"t.countdown.expiredMessagePlaceholder\"\n @input=\"\n updateField('expiredMessage', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.countdown.expiredImageUrl\">\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.expiredImageUrl\"\n placeholder=\"https://...\"\n @input=\"\n updateField(\n 'expiredImageUrl',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n </FieldRow>\n\n <ToggleSwitch\n class=\"tpl:mb-3.5 tpl:text-[12px] tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.hideOnExpiry\"\n :label=\"t.countdown.hideOnExpiry\"\n @update:model-value=\"updateField('hideOnExpiry', $event)\"\n />\n</template>\n"],"mappings":"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"}
|