@templatical/editor 0.22.0 → 0.24.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-K_AehWc-.js → AiChatSidebar-Nq-jrGaf.js} +2 -2
- package/dist/{AiFeatureMenu-CcqjRMFW.js → AiFeatureMenu-tg_zMKMK.js} +3 -3
- package/dist/{BlockIssueBadge-Dmj9rVra.js → BlockIssueBadge-Bd94kIiw.js} +2 -2
- package/dist/BlockPreviewCanvas-B34aMhHe.js +117 -0
- package/dist/{CloudEditor-BVH7D1Ka.js → CloudEditor-DjN_9Yso.js} +285 -284
- package/dist/{CollaboratorBar-C_1SF7Xf.js → CollaboratorBar-Bhazzcg-.js} +2 -2
- package/dist/{ColorPicker-s9GHXjZs.js → ColorPicker-DXGrcJxJ.js} +5 -5
- package/dist/{CommentsSidebar-DvNEdugN.js → CommentsSidebar-ku1kt10u.js} +3 -3
- package/dist/{CountdownBlock-J_Q9JRQM.js → CountdownBlock-CN9Lc23W.js} +1 -1
- package/dist/{CountdownToolbar-DPRcv3Fu.js → CountdownToolbar-VEsIyKV6.js} +3 -3
- package/dist/{DesignReferenceSidebar-DNmK3dxp.js → DesignReferenceSidebar-C4pTMzQE.js} +3 -3
- package/dist/{IssuesPanel-CEnEGFtd.js → IssuesPanel-CEfzOpS_.js} +2 -2
- package/dist/MergeTagModeToggle-C3V2xGAg.js +180 -0
- package/dist/{MergeTagSuggestion-D9eCryMC.js → MergeTagSuggestion-Dn8ww9nV.js} +2 -2
- package/dist/{NumberWithSuffix-CCPN_oCh.js → NumberWithSuffix-BGocZkj9.js} +1 -1
- package/dist/{ParagraphEditor-MKJYyWN9.js → ParagraphEditor-D-fz0kUb.js} +19 -19
- package/dist/{RichTextEditorContent-FbnWsh46.js → RichTextEditorContent-DydrqFts.js} +5 -5
- package/dist/{SaveBlockDialog-B_5KBaXW.js → SaveBlockDialog-BDDFbAWF.js} +100 -99
- package/dist/{SavedBlocksBrowserModal-DoJBoYPk.js → SavedBlocksBrowserModal-Djik_fJh.js} +6 -6
- package/dist/{SavedBlocksPanels-3Af5dMaf.js → SavedBlocksPanels-CxemyPxL.js} +1 -1
- package/dist/{SavedBlocksPickBar-C1FTJP-I.js → SavedBlocksPickBar-Dlhipmob.js} +2 -2
- package/dist/{SnapshotHistory-DWlQ3Eh0.js → SnapshotHistory-DPbM4ons.js} +4 -4
- package/dist/{TemplateScoringPanel-ifGGGWs5.js → TemplateScoringPanel-BFYgweDX.js} +2 -2
- package/dist/TestEmailModal-IZfwgya-.js +174 -0
- package/dist/TestEmailPanel-QEEx_PkN.js +33 -0
- package/dist/{TitleEditor-DzsPxKv4.js → TitleEditor-BB4YlIBF.js} +10 -10
- package/dist/{TplModal-CykwWFhF.js → TplModal-DPH4vV8M.js} +3 -3
- package/dist/{blockTypeIcons-CpKkI47V.js → blockTypeIcons-Wzd7W_2x.js} +1 -1
- package/dist/bundle-stats.json +7 -7
- package/dist/{ca-BBgad6y5.js → ca-CO4nq6TX.js} +22 -1
- package/dist/cdn/chunks/{AiChatSidebar-A1CuWGY0.js → AiChatSidebar-CN-0DIuA.js} +11 -11
- package/dist/cdn/chunks/{AiChatSidebar-A1CuWGY0.js.map → AiChatSidebar-CN-0DIuA.js.map} +1 -1
- package/dist/cdn/chunks/{AiFeatureMenu-hWgWQg11.js → AiFeatureMenu-B5pV6Iux.js} +8 -8
- package/dist/cdn/chunks/{AiFeatureMenu-hWgWQg11.js.map → AiFeatureMenu-B5pV6Iux.js.map} +1 -1
- package/dist/cdn/chunks/{BlockIssueBadge-Da4yR-mh.js → BlockIssueBadge-aanOjeyi.js} +4 -4
- package/dist/cdn/chunks/{BlockIssueBadge-Da4yR-mh.js.map → BlockIssueBadge-aanOjeyi.js.map} +1 -1
- package/dist/cdn/chunks/BlockPreviewCanvas-Dc4F4HzY.js +119 -0
- package/dist/cdn/chunks/BlockPreviewCanvas-Dc4F4HzY.js.map +1 -0
- package/dist/cdn/chunks/{CloudEditor-FCS53UcZ.js → CloudEditor-CeKaWWO5.js} +264 -263
- package/dist/cdn/chunks/CloudEditor-CeKaWWO5.js.map +1 -0
- package/dist/cdn/chunks/{CollaboratorBar-mkJVAkSh.js → CollaboratorBar-CEdKr0A2.js} +4 -4
- package/dist/cdn/chunks/{CollaboratorBar-mkJVAkSh.js.map → CollaboratorBar-CEdKr0A2.js.map} +1 -1
- package/dist/cdn/chunks/{ColorPicker-DZSzZ5ZP.js → ColorPicker-BckwEfcN.js} +7 -7
- package/dist/cdn/chunks/{ColorPicker-DZSzZ5ZP.js.map → ColorPicker-BckwEfcN.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsSidebar-DHdRiGPX.js → CommentsSidebar-6PfpJKh-.js} +18 -18
- package/dist/cdn/chunks/{CommentsSidebar-DHdRiGPX.js.map → CommentsSidebar-6PfpJKh-.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownBlock-Bffpoppw.js → CountdownBlock-CoBmvQKv.js} +2 -2
- package/dist/cdn/chunks/{CountdownBlock-Bffpoppw.js.map → CountdownBlock-CoBmvQKv.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownToolbar-DAnmIZjq.js → CountdownToolbar-CdV8RTQu.js} +4 -4
- package/dist/cdn/chunks/{CountdownToolbar-DAnmIZjq.js.map → CountdownToolbar-CdV8RTQu.js.map} +1 -1
- package/dist/cdn/chunks/{DesignReferenceSidebar-Dre99DTH.js → DesignReferenceSidebar-BjlaP69l.js} +90 -90
- package/dist/cdn/chunks/{DesignReferenceSidebar-Dre99DTH.js.map → DesignReferenceSidebar-BjlaP69l.js.map} +1 -1
- package/dist/cdn/chunks/{IssuesPanel-D80JedOj.js → IssuesPanel-CkkEnhxZ.js} +9 -9
- package/dist/cdn/chunks/{IssuesPanel-D80JedOj.js.map → IssuesPanel-CkkEnhxZ.js.map} +1 -1
- package/dist/cdn/chunks/MergeTagModeToggle-DmtR300G.js +127 -0
- package/dist/cdn/chunks/MergeTagModeToggle-DmtR300G.js.map +1 -0
- package/dist/cdn/chunks/{MergeTagSuggestion-BM349_QH.js → MergeTagSuggestion-Cvlkw_PM.js} +2 -2
- package/dist/cdn/chunks/{MergeTagSuggestion-BM349_QH.js.map → MergeTagSuggestion-Cvlkw_PM.js.map} +1 -1
- package/dist/cdn/chunks/{NumberWithSuffix-CjlVNIEe.js → NumberWithSuffix-BOcMN26z.js} +2 -2
- package/dist/cdn/chunks/{NumberWithSuffix-CjlVNIEe.js.map → NumberWithSuffix-BOcMN26z.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-J-1RLyi7.js → ParagraphEditor-CX1yks4N.js} +35 -35
- package/dist/cdn/chunks/{ParagraphEditor-J-1RLyi7.js.map → ParagraphEditor-CX1yks4N.js.map} +1 -1
- package/dist/cdn/chunks/{RichTextEditorContent-BbdgJqWM.js → RichTextEditorContent-z9s7qxs3.js} +7 -7
- package/dist/cdn/chunks/{RichTextEditorContent-BbdgJqWM.js.map → RichTextEditorContent-z9s7qxs3.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-CSeD3gn9.js → SaveBlockDialog-D2CtW4lC.js} +100 -99
- package/dist/cdn/chunks/SaveBlockDialog-D2CtW4lC.js.map +1 -0
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-B2x5WRPN.js → SavedBlocksBrowserModal-DdUKJzzF.js} +8 -8
- package/dist/cdn/chunks/SavedBlocksBrowserModal-DdUKJzzF.js.map +1 -0
- package/dist/cdn/chunks/{SavedBlocksPanels-BzMTZxf2.js → SavedBlocksPanels-1kVbqdpc.js} +2 -2
- package/dist/cdn/chunks/{SavedBlocksPanels-BzMTZxf2.js.map → SavedBlocksPanels-1kVbqdpc.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPickBar-CduR1Cpb.js → SavedBlocksPickBar--noDJIyN.js} +6 -6
- package/dist/cdn/chunks/{SavedBlocksPickBar-CduR1Cpb.js.map → SavedBlocksPickBar--noDJIyN.js.map} +1 -1
- package/dist/cdn/chunks/{SnapshotHistory-DO-gzr3i.js → SnapshotHistory-DFuNJRc9.js} +8 -8
- package/dist/cdn/chunks/{SnapshotHistory-DO-gzr3i.js.map → SnapshotHistory-DFuNJRc9.js.map} +1 -1
- package/dist/cdn/chunks/{TemplateScoringPanel-CTk8WwxU.js → TemplateScoringPanel-DG1dAjQk.js} +52 -52
- package/dist/cdn/chunks/{TemplateScoringPanel-CTk8WwxU.js.map → TemplateScoringPanel-DG1dAjQk.js.map} +1 -1
- package/dist/cdn/chunks/TestEmailModal-CiPfOn2v.js +174 -0
- package/dist/cdn/chunks/TestEmailModal-CiPfOn2v.js.map +1 -0
- package/dist/cdn/chunks/TestEmailPanel-saOfnpjz.js +35 -0
- package/dist/cdn/chunks/TestEmailPanel-saOfnpjz.js.map +1 -0
- package/dist/cdn/chunks/{TitleEditor-J1vgU_J6.js → TitleEditor-08KM_VzZ.js} +18 -18
- package/dist/cdn/chunks/{TitleEditor-J1vgU_J6.js.map → TitleEditor-08KM_VzZ.js.map} +1 -1
- package/dist/cdn/chunks/{TplModal-BN53d9Se.js → TplModal-Cu4tBBnF.js} +4 -4
- package/dist/cdn/chunks/{TplModal-BN53d9Se.js.map → TplModal-Cu4tBBnF.js.map} +1 -1
- package/dist/cdn/chunks/blockTypeIcons-DvohkXqi.js +21 -0
- package/dist/cdn/chunks/{blockTypeIcons-etMMQoVv.js.map → blockTypeIcons-DvohkXqi.js.map} +1 -1
- package/dist/cdn/chunks/{ca-BBgad6y5.js → ca-CO4nq6TX.js} +23 -2
- package/dist/cdn/chunks/{ca-BBgad6y5.js.map → ca-CO4nq6TX.js.map} +1 -1
- package/dist/cdn/chunks/{cloud-BJlbXGTJ.js → cloud-C2I0P9qj.js} +46 -42
- package/dist/cdn/chunks/cloud-C2I0P9qj.js.map +1 -0
- package/dist/{de-D7WbpFBR.js → cdn/chunks/de-CUeaFJph.js} +24 -1
- package/dist/cdn/chunks/{de-D7WbpFBR.js.map → de-CUeaFJph.js.map} +1 -1
- package/dist/{de-Bw6fB5JU.js → cdn/chunks/de-uVoSP1xF.js} +2 -9
- package/dist/cdn/chunks/de-uVoSP1xF.js.map +1 -0
- package/dist/cdn/chunks/emailFrameWidth-BmFCN2dp.js +8 -0
- package/dist/cdn/chunks/emailFrameWidth-BmFCN2dp.js.map +1 -0
- package/dist/{en-DAdHJz7h.js → cdn/chunks/en-CjhsJxb-.js} +2 -9
- package/dist/cdn/chunks/en-CjhsJxb-.js.map +1 -0
- package/dist/{en-BrZ0uGK2.js → cdn/chunks/en-zCYIPAL5.js} +24 -1
- package/dist/cdn/chunks/{en-BrZ0uGK2.js.map → en-zCYIPAL5.js.map} +1 -1
- package/dist/{es-tLSLysPs.js → cdn/chunks/es-BSiH7YkP.js} +24 -1
- package/dist/cdn/chunks/{es-tLSLysPs.js.map → es-BSiH7YkP.js.map} +1 -1
- package/dist/cdn/chunks/{extensions-C49c51P1.js → extensions-DBWF_lxx.js} +16 -16
- package/dist/cdn/chunks/{extensions-C49c51P1.js.map → extensions-DBWF_lxx.js.map} +1 -1
- package/dist/{fr-BMWOPztQ.js → cdn/chunks/fr-DTVJ3det.js} +24 -1
- package/dist/cdn/chunks/fr-DTVJ3det.js.map +1 -0
- package/dist/cdn/chunks/{icons-CYWw8Xfp.js → icons-Cqxvwgz7.js} +35 -14
- package/dist/cdn/chunks/icons-Cqxvwgz7.js.map +1 -0
- package/dist/cdn/chunks/keys-B5uoP9P7.js +12 -0
- package/dist/cdn/chunks/keys-B5uoP9P7.js.map +1 -0
- package/dist/{nl-Dhctu7JL.js → cdn/chunks/nl-BqSquFS3.js} +24 -1
- package/dist/cdn/chunks/nl-BqSquFS3.js.map +1 -0
- package/dist/cdn/chunks/{pt-BR-BWFokdTU.js → pt-BR-CMljK37h.js} +23 -2
- package/dist/cdn/chunks/{pt-BR-BWFokdTU.js.map → pt-BR-CMljK37h.js.map} +1 -1
- package/dist/{pt-BR-w2sGzHNX.js → cdn/chunks/pt-BR-CTzHTqJD.js} +2 -9
- package/dist/cdn/chunks/pt-BR-CTzHTqJD.js.map +1 -0
- package/dist/cdn/chunks/{src-BKI1x1k7.js → src-BCJna-6p.js} +183 -165
- package/dist/cdn/chunks/{src-BKI1x1k7.js.map → src-BCJna-6p.js.map} +1 -1
- package/dist/cdn/chunks/{src-MA8DWI8O.js → src-C45kfA2t.js} +17 -17
- package/dist/cdn/chunks/{src-MA8DWI8O.js.map → src-C45kfA2t.js.map} +1 -1
- package/dist/cdn/chunks/{src-ByWL5K1f.js → src-XQmI_Xs1.js} +48 -48
- package/dist/cdn/chunks/{src-ByWL5K1f.js.map → src-XQmI_Xs1.js.map} +1 -1
- package/dist/cdn/chunks/{src-CM0gCvAS.js → src-YVsPFLNJ.js} +251 -251
- package/dist/cdn/chunks/{src-CM0gCvAS.js.map → src-YVsPFLNJ.js.map} +1 -1
- package/dist/cdn/chunks/{styles-BkXzQ4-n.js → styles-BohMfIBd.js} +785 -746
- package/dist/cdn/chunks/styles-BohMfIBd.js.map +1 -0
- package/dist/cdn/chunks/{tiptap-DUTbZTeR.js → tiptap-DRKu__AX.js} +5 -5
- package/dist/cdn/chunks/{tiptap-DUTbZTeR.js.map → tiptap-DRKu__AX.js.map} +1 -1
- package/dist/cdn/chunks/{useCloudI18n-BjNENqkM.js → useCloudI18n-Bem8DUjH.js} +2 -2
- package/dist/cdn/chunks/{useCloudI18n-BjNENqkM.js.map → useCloudI18n-Bem8DUjH.js.map} +1 -1
- package/dist/cdn/chunks/{useEditorCore-CzMhD6Gd.js → useEditorCore-B_vwuKdI.js} +434 -414
- package/dist/cdn/chunks/useEditorCore-B_vwuKdI.js.map +1 -0
- package/dist/cdn/chunks/{useI18n-8Wd7G-cq.js → useI18n-graz19nI.js} +2 -2
- package/dist/cdn/chunks/{useI18n-8Wd7G-cq.js.map → useI18n-graz19nI.js.map} +1 -1
- package/dist/cdn/chunks/{useMergeTag-C7G31KV-.js → useMergeTag-rTzAFMlr.js} +4 -4
- package/dist/cdn/chunks/{useMergeTag-C7G31KV-.js.map → useMergeTag-rTzAFMlr.js.map} +1 -1
- package/dist/cdn/chunks/{usePopoverPosition-Bx0QewTv.js → usePopoverPosition-BaFxLP3w.js} +2 -2
- package/dist/cdn/chunks/{usePopoverPosition-Bx0QewTv.js.map → usePopoverPosition-BaFxLP3w.js.map} +1 -1
- package/dist/cdn/editor.css +1 -1
- package/dist/cdn/editor.js +172 -150
- package/dist/cdn/editor.js.map +1 -1
- package/dist/{cloud-Du7mnkz2.js → cloud-DsUct4Zq.js} +42 -40
- package/dist/{cdn/chunks/de-D7WbpFBR.js → de-CUeaFJph.js} +22 -3
- package/dist/{cdn/chunks/de-Bw6fB5JU.js → de-uVoSP1xF.js} +0 -11
- package/dist/{dist-CL_CfQ3Q.js → dist-9uhI32Q4.js} +2 -2
- package/dist/{dist-3DHaYEiT.js → dist-BAkGuPYm.js} +2 -2
- package/dist/dist-BIYm-U9x.js +5 -0
- package/dist/{dist-3HenmYIg.js → dist-ByJVlKWy.js} +1 -1
- package/dist/{dist-DKWoZJAV.js → dist-C5tigvDF.js} +2 -2
- package/dist/{dist-bivrM-nS.js → dist-CQiAZlTt.js} +2 -2
- package/dist/{dist-Ih7ejWzn.js → dist-C_kAE1sC.js} +2 -2
- package/dist/{dist-CclnU_v0.js → dist-CkCVlKj_.js} +2 -2
- package/dist/{dist-B_etKYyh.js → dist-DFO896yT.js} +5 -5
- package/dist/{dist-BJbqgyPn.js → dist-DPHqs0Yh.js} +105 -87
- package/dist/{dist-DOtLGnLr.js → dist-DWu9QWFl.js} +2 -2
- package/dist/{dist-BCnJcAQc.js → dist-IGetXrbO.js} +2 -2
- package/dist/dist-JXOq1hib.js +5 -0
- package/dist/emailFrameWidth-BmFCN2dp.js +6 -0
- package/dist/{cdn/chunks/en-DAdHJz7h.js → en-CjhsJxb-.js} +0 -11
- package/dist/{cdn/chunks/en-BrZ0uGK2.js → en-zCYIPAL5.js} +22 -3
- package/dist/{cdn/chunks/es-tLSLysPs.js → es-BSiH7YkP.js} +22 -3
- package/dist/{extensions-3870a_3R.js → extensions-C0DXnYdA.js} +13 -13
- package/dist/{cdn/chunks/fr-BMWOPztQ.js → fr-DTVJ3det.js} +22 -3
- package/dist/index.d.ts +74 -0
- package/dist/keys-BVlG5Ozc.js +10 -0
- package/dist/{cdn/chunks/nl-Dhctu7JL.js → nl-BqSquFS3.js} +22 -3
- package/dist/{pt-BR-BWFokdTU.js → pt-BR-CMljK37h.js} +22 -1
- package/dist/{cdn/chunks/pt-BR-w2sGzHNX.js → pt-BR-CTzHTqJD.js} +0 -11
- package/dist/style.css +1 -1
- package/dist/{styles-AxBd7CgB.js → styles-BTTD0Je4.js} +941 -935
- package/dist/templatical-editor.js +112 -89
- package/dist/{useCloudI18n-BmiygEM4.js → useCloudI18n-DGr3b2SX.js} +1 -1
- package/dist/{useEditorCore-CuMMIfiS.js → useEditorCore-DOrGex_t.js} +917 -897
- package/dist/{useI18n-D2L9Mkik.js → useI18n-BhAejA-x.js} +1 -1
- package/dist/{useMergeTag-BTdDq5DC.js → useMergeTag-Ce95YIOO.js} +3 -3
- package/dist/{usePopoverPosition-BO2LNy5n.js → usePopoverPosition-vjKJaqJf.js} +1 -1
- package/package.json +20 -20
- package/dist/SavedBlockPreviewCanvas-DXkGDYph.js +0 -109
- package/dist/TestEmailModal-DComAFpR.js +0 -95
- package/dist/cdn/chunks/CloudEditor-FCS53UcZ.js.map +0 -1
- package/dist/cdn/chunks/SaveBlockDialog-CSeD3gn9.js.map +0 -1
- package/dist/cdn/chunks/SavedBlockPreviewCanvas-DgLOKtR_.js +0 -111
- package/dist/cdn/chunks/SavedBlockPreviewCanvas-DgLOKtR_.js.map +0 -1
- package/dist/cdn/chunks/SavedBlocksBrowserModal-B2x5WRPN.js.map +0 -1
- package/dist/cdn/chunks/TestEmailModal-DT37w7i_.js +0 -96
- package/dist/cdn/chunks/TestEmailModal-DT37w7i_.js.map +0 -1
- package/dist/cdn/chunks/blockTypeIcons-etMMQoVv.js +0 -21
- package/dist/cdn/chunks/cloud-BJlbXGTJ.js.map +0 -1
- package/dist/cdn/chunks/de-Bw6fB5JU.js.map +0 -1
- package/dist/cdn/chunks/en-DAdHJz7h.js.map +0 -1
- package/dist/cdn/chunks/fr-BMWOPztQ.js.map +0 -1
- package/dist/cdn/chunks/icons-CYWw8Xfp.js.map +0 -1
- package/dist/cdn/chunks/keys-D7kUmfpE.js +0 -12
- package/dist/cdn/chunks/keys-D7kUmfpE.js.map +0 -1
- package/dist/cdn/chunks/nl-Dhctu7JL.js.map +0 -1
- package/dist/cdn/chunks/pt-BR-w2sGzHNX.js.map +0 -1
- package/dist/cdn/chunks/styles-BkXzQ4-n.js.map +0 -1
- package/dist/cdn/chunks/useEditorCore-CzMhD6Gd.js.map +0 -1
- package/dist/dist-CcQLJeWV.js +0 -5
- package/dist/dist-DerdmERj.js +0 -5
- package/dist/keys-CHPcp-Nz.js +0 -10
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CloudEditor-FCS53UcZ.js","names":["$emit"],"sources":["../../../src/cloud/composables/useSnapshotPreview.ts","../../../src/cloud/composables/useCloudPanelState.ts","../../../src/cloud/composables/useCollabUndoWarning.ts","../../../src/cloud/composables/useCloudFeatureFlags.ts","../../../src/cloud/composables/useCloudMediaLibrary.ts","../../../src/cloud/composables/useCloudInitialization.ts","../../../src/utils/preRenderCustomBlocks.ts","../../../src/cloud/composables/useCloudLifecycle.ts","../../../src/cloud/composables/useCloudSaveGate.ts","../../../src/cloud/components/CloudSaveGateModal.vue","../../../src/cloud/components/CloudSaveGateModal.vue","../../../src/cloud/components/CloudHeader.vue","../../../src/cloud/components/CloudHeader.vue","../../../src/cloud/components/CloudPanels.vue","../../../src/cloud/components/CloudPanels.vue","../../../src/cloud/components/CloudLoadingOverlay.vue","../../../src/cloud/components/CloudLoadingOverlay.vue","../../../src/cloud/components/CloudErrorOverlay.vue","../../../src/cloud/components/CloudErrorOverlay.vue","../../../src/cloud/components/SnapshotPreviewBanner.vue","../../../src/cloud/components/SnapshotPreviewBanner.vue","../../../src/cloud/components/CollabUndoToast.vue","../../../src/cloud/components/CollabUndoToast.vue","../../../src/cloud/CloudEditor.vue","../../../src/cloud/CloudEditor.vue"],"sourcesContent":["import {\n computed,\n onScopeDispose,\n shallowRef,\n ref,\n type ComputedRef,\n type Ref,\n} from \"vue\";\nimport type { TemplateContent, TemplateSnapshot } from \"@templatical/types\";\nimport type {\n UseHistoryReturn,\n UseConditionPreviewReturn,\n UseAutoSaveReturn,\n} from \"@templatical/core\";\nimport {\n useSnapshotHistory,\n type AuthManager,\n type UseSnapshotHistoryReturn,\n} from \"@templatical/core/cloud\";\nimport type { BaseEditorReturn } from \"../../composables/useEditorCore\";\n\nexport interface UseSnapshotPreviewOptions {\n authManager: AuthManager;\n editor: BaseEditorReturn & {\n hasTemplate: () => boolean;\n createSnapshot: () => Promise<void>;\n };\n history: UseHistoryReturn;\n conditionPreview: UseConditionPreviewReturn;\n autoSave: UseAutoSaveReturn | null;\n onError?: (error: Error) => void;\n}\n\nexport interface UseSnapshotPreviewReturn {\n snapshotHistoryInstance: Ref<UseSnapshotHistoryReturn | null>;\n previewingSnapshot: Ref<TemplateSnapshot | null>;\n contentBeforePreview: Ref<TemplateContent | null>;\n isPreviewingSnapshot: ComputedRef<boolean>;\n snapshotHistorySnapshots: ComputedRef<TemplateSnapshot[]>;\n snapshotHistoryIsLoading: ComputedRef<boolean>;\n snapshotHistoryIsRestoring: ComputedRef<boolean>;\n initSnapshotHistory: () => void;\n handleRestore: (template: { content: TemplateContent }) => void;\n handleSnapshotNavigate: (snapshot: TemplateSnapshot) => Promise<void>;\n confirmRestoreSnapshot: () => Promise<void>;\n cancelPreview: () => void;\n loadSnapshotHistory: () => Promise<void>;\n}\n\nexport function useSnapshotPreview(\n options: UseSnapshotPreviewOptions,\n): UseSnapshotPreviewReturn {\n const { authManager, editor, history, conditionPreview, autoSave, onError } =\n options;\n\n const snapshotHistoryInstance = shallowRef<UseSnapshotHistoryReturn | null>(\n null,\n );\n const previewingSnapshot = ref<TemplateSnapshot | null>(null);\n const contentBeforePreview = ref<TemplateContent | null>(null);\n\n // Per CLAUDE.md, async functions must guard against post-unmount execution\n // after every `await` so we don't write to dead refs or fire side effects\n // (autoSave.pause, setContent) on a torn-down component.\n let destroyed = false;\n onScopeDispose(() => {\n destroyed = true;\n });\n\n const isPreviewingSnapshot = computed(\n () => previewingSnapshot.value !== null,\n );\n const snapshotHistorySnapshots = computed(\n () => snapshotHistoryInstance.value?.snapshots.value ?? [],\n );\n const snapshotHistoryIsLoading = computed(\n () => snapshotHistoryInstance.value?.isLoading.value ?? false,\n );\n const snapshotHistoryIsRestoring = computed(\n () => snapshotHistoryInstance.value?.isRestoring.value ?? false,\n );\n\n function initSnapshotHistory(): void {\n if (editor.state.template?.id && !snapshotHistoryInstance.value) {\n snapshotHistoryInstance.value = useSnapshotHistory({\n authManager,\n templateId: editor.state.template.id,\n onRestore: handleRestore,\n onError,\n });\n snapshotHistoryInstance.value.loadSnapshots();\n }\n }\n\n function handleRestore(template: { content: TemplateContent }): void {\n editor.setContent(template.content, false);\n history.clear();\n conditionPreview.reset();\n }\n\n async function handleSnapshotNavigate(\n snapshot: TemplateSnapshot,\n ): Promise<void> {\n if (destroyed) return;\n\n if (previewingSnapshot.value) {\n previewingSnapshot.value = snapshot;\n editor.setContent(snapshot.content, false);\n return;\n }\n\n if (editor.state.isDirty && editor.hasTemplate()) {\n await editor.createSnapshot();\n if (destroyed) return;\n }\n\n contentBeforePreview.value = structuredClone(editor.content.value);\n\n autoSave?.pause();\n previewingSnapshot.value = snapshot;\n editor.setContent(snapshot.content, false);\n }\n\n async function confirmRestoreSnapshot(): Promise<void> {\n if (!previewingSnapshot.value || !snapshotHistoryInstance.value) return;\n\n try {\n await snapshotHistoryInstance.value.restoreSnapshot(\n previewingSnapshot.value.id,\n );\n if (destroyed) return;\n await snapshotHistoryInstance.value.loadSnapshots();\n if (destroyed) return;\n } catch (error) {\n // Restore failed. The editor is still showing the previewed snapshot\n // content (set in handleSnapshotNavigate), so roll back to the\n // pre-preview content before the finally discards the backup — otherwise\n // the user is silently left editing the un-restored snapshot with the\n // banner gone, and the next autosave persists the wrong content.\n if (!destroyed && contentBeforePreview.value) {\n editor.setContent(contentBeforePreview.value, false);\n }\n throw error;\n } finally {\n if (!destroyed) {\n previewingSnapshot.value = null;\n contentBeforePreview.value = null;\n autoSave?.resume();\n }\n }\n }\n\n function cancelPreview(): void {\n if (!previewingSnapshot.value || !contentBeforePreview.value) return;\n\n editor.setContent(contentBeforePreview.value, false);\n\n previewingSnapshot.value = null;\n contentBeforePreview.value = null;\n\n autoSave?.resume();\n }\n\n async function loadSnapshotHistory(): Promise<void> {\n if (destroyed) return;\n if (snapshotHistoryInstance.value) {\n await snapshotHistoryInstance.value.loadSnapshots();\n }\n }\n\n return {\n snapshotHistoryInstance,\n previewingSnapshot,\n contentBeforePreview,\n isPreviewingSnapshot,\n snapshotHistorySnapshots,\n snapshotHistoryIsLoading,\n snapshotHistoryIsRestoring,\n initSnapshotHistory,\n handleRestore,\n handleSnapshotNavigate,\n confirmRestoreSnapshot,\n cancelPreview,\n loadSnapshotHistory,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { onClickOutside } from \"@vueuse/core\";\nimport type { AiFeature } from \"../components/AiFeatureMenu.vue\";\nimport type { MediaCategory } from \"@templatical/media-library\";\n\nexport interface UseCloudPanelStateReturn {\n activePanel: Ref<RightPanel | null>;\n aiChatOpen: ComputedRef<boolean> & { value: boolean };\n scoringPanelOpen: ComputedRef<boolean> & { value: boolean };\n designReferenceOpen: ComputedRef<boolean> & { value: boolean };\n commentsOpen: ComputedRef<boolean> & { value: boolean };\n testEmailModalOpen: Ref<boolean>;\n mediaLibraryOpen: Ref<boolean>;\n mediaLibraryAccept: Ref<MediaCategory[] | undefined>;\n aiMenuOpen: Ref<boolean>;\n aiMenuRef: Ref<HTMLElement | null>;\n rightPanelOpen: ComputedRef<boolean>;\n activeAiFeature: ComputedRef<AiFeature | null>;\n aiButtonActive: ComputedRef<boolean>;\n toggleAiMenu: () => void;\n handleAiFeatureSelect: (feature: AiFeature) => void;\n}\n\ntype RightPanel = \"ai-chat\" | \"scoring\" | \"design-reference\" | \"comments\";\n\nexport function useCloudPanelState(): UseCloudPanelStateReturn {\n const activePanel = ref<RightPanel | null>(null);\n\n const aiChatOpen = computed({\n get: () => activePanel.value === \"ai-chat\",\n set: (v) => (activePanel.value = v ? \"ai-chat\" : null),\n });\n const scoringPanelOpen = computed({\n get: () => activePanel.value === \"scoring\",\n set: (v) => (activePanel.value = v ? \"scoring\" : null),\n });\n const designReferenceOpen = computed({\n get: () => activePanel.value === \"design-reference\",\n set: (v) => (activePanel.value = v ? \"design-reference\" : null),\n });\n const commentsOpen = computed({\n get: () => activePanel.value === \"comments\",\n set: (v) => (activePanel.value = v ? \"comments\" : null),\n });\n\n const testEmailModalOpen = ref(false);\n const mediaLibraryOpen = ref(false);\n const mediaLibraryAccept = ref<MediaCategory[] | undefined>(undefined);\n const aiMenuOpen = ref(false);\n const aiMenuRef = ref<HTMLElement | null>(null);\n\n const rightPanelOpen = computed(() => activePanel.value !== null);\n\n const activeAiFeature = computed<AiFeature | null>(() => {\n const p = activePanel.value;\n if (p === \"ai-chat\" || p === \"design-reference\" || p === \"scoring\")\n return p;\n return null;\n });\n\n const aiButtonActive = computed(\n () =>\n aiMenuOpen.value ||\n activePanel.value === \"ai-chat\" ||\n activePanel.value === \"design-reference\" ||\n activePanel.value === \"scoring\",\n );\n\n function toggleAiMenu(): void {\n aiMenuOpen.value = !aiMenuOpen.value;\n }\n\n function handleAiFeatureSelect(feature: AiFeature): void {\n aiMenuOpen.value = false;\n activePanel.value = activePanel.value === feature ? null : feature;\n }\n\n onClickOutside(aiMenuRef, () => {\n aiMenuOpen.value = false;\n });\n\n return {\n activePanel,\n aiChatOpen,\n scoringPanelOpen,\n designReferenceOpen,\n commentsOpen,\n testEmailModalOpen,\n mediaLibraryOpen,\n mediaLibraryAccept,\n aiMenuOpen,\n aiMenuRef,\n rightPanelOpen,\n activeAiFeature,\n aiButtonActive,\n toggleAiMenu,\n handleAiFeatureSelect,\n };\n}\n","import { type ComputedRef, type Ref, ref } from \"vue\";\nimport { useTimeoutFn } from \"@vueuse/core\";\nimport { COLLAB_UNDO_WARNING_MS } from \"../../constants/timeouts\";\n\nexport interface UseCollabUndoWarningOptions {\n /** Whether collaboration is currently enabled (reactive). */\n isCollaborationEnabled: ComputedRef<boolean>;\n /** Returns the current list of collaborators. */\n getCollaboratorCount: () => number;\n /** Whether the history stack has entries to undo. */\n canUndo: ComputedRef<boolean>;\n}\n\nexport interface UseCollabUndoWarningReturn {\n collabUndoWarningVisible: Ref<boolean>;\n showCollabUndoWarning: () => void;\n}\n\nexport function useCollabUndoWarning(\n options: UseCollabUndoWarningOptions,\n): UseCollabUndoWarningReturn {\n const { isCollaborationEnabled, getCollaboratorCount, canUndo } = options;\n\n const collabUndoWarningFired = ref(false);\n const collabUndoWarningVisible = ref(false);\n\n const { start: startCollabUndoWarningTimeout } = useTimeoutFn(\n () => {\n collabUndoWarningVisible.value = false;\n },\n COLLAB_UNDO_WARNING_MS,\n { immediate: false },\n );\n\n function showCollabUndoWarning(): void {\n if (\n collabUndoWarningFired.value ||\n !isCollaborationEnabled.value ||\n getCollaboratorCount() === 0 ||\n !canUndo.value\n ) {\n return;\n }\n\n collabUndoWarningFired.value = true;\n collabUndoWarningVisible.value = true;\n startCollabUndoWarningTimeout();\n }\n\n return {\n collabUndoWarningVisible,\n showCollabUndoWarning,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { useTimeoutFn } from \"@vueuse/core\";\nimport type {\n UsePlanConfigReturn,\n UseAiConfigReturn,\n} from \"@templatical/core/cloud\";\n\nexport interface UseCloudFeatureFlagsOptions {\n planConfigInstance: UsePlanConfigReturn;\n aiConfig: UseAiConfigReturn;\n editor: {\n state: {\n readonly template?: { id: string } | null;\n };\n };\n}\n\nexport interface UseCloudFeatureFlagsReturn {\n canUseAiGeneration: ComputedRef<boolean>;\n canSendTestEmail: ComputedRef<boolean>;\n hasTemplateSaved: ComputedRef<boolean>;\n isWhiteLabeled: ComputedRef<boolean>;\n templateLimit: ComputedRef<number | null>;\n templateCount: ComputedRef<number>;\n isSaveExporting: Ref<boolean>;\n saveStatus: Ref<\"idle\" | \"saved\" | \"error\">;\n saveErrorMessage: Ref<string>;\n startSaveStatusClear: () => void;\n}\n\nexport function useCloudFeatureFlags(\n options: UseCloudFeatureFlagsOptions,\n): UseCloudFeatureFlagsReturn {\n const { planConfigInstance, aiConfig, editor } = options;\n\n const canUseAiGeneration = computed(\n () =>\n planConfigInstance.hasFeature(\"ai_generation\") &&\n aiConfig.hasAnyMenuFeature.value,\n );\n const canSendTestEmail = computed(() =>\n planConfigInstance.hasFeature(\"test_email\"),\n );\n const hasTemplateSaved = computed(() => !!editor.state.template?.id);\n const isWhiteLabeled = computed(() =>\n planConfigInstance.hasFeature(\"white_label\"),\n );\n const templateLimit = computed(\n () => planConfigInstance.config.value?.limits.max_templates ?? null,\n );\n const templateCount = computed(\n () => planConfigInstance.config.value?.template_count ?? 0,\n );\n\n const isSaveExporting = ref(false);\n const saveStatus = ref<\"idle\" | \"saved\" | \"error\">(\"idle\");\n const saveErrorMessage = ref(\"\");\n\n const { start: startSaveStatusClear } = useTimeoutFn(\n () => {\n saveStatus.value = \"idle\";\n },\n 3000,\n { immediate: false },\n );\n\n return {\n canUseAiGeneration,\n canSendTestEmail,\n hasTemplateSaved,\n isWhiteLabeled,\n templateLimit,\n templateCount,\n isSaveExporting,\n saveStatus,\n saveErrorMessage,\n startSaveStatusClear,\n };\n}\n","import { onScopeDispose, type Ref } from \"vue\";\nimport type {\n MediaCategory,\n MediaConfig,\n MediaItem,\n MediaRequestContext,\n} from \"@templatical/media-library\";\nimport type { AuthManager } from \"@templatical/core/cloud\";\nimport type { MediaResult } from \"@templatical/types\";\n\nexport interface UseCloudMediaLibraryOptions {\n onRequestMedia?: (context: MediaRequestContext) => Promise<MediaItem | null>;\n mediaLibraryOpen: Ref<boolean>;\n mediaLibraryAccept: Ref<MediaCategory[] | undefined>;\n /** Auth used by the built-in drag-and-drop upload (#229). */\n authManager: AuthManager;\n /**\n * Active plan's media config, read at upload time for a client-side\n * pre-check (MIME + size) before hitting the network. Returns `null` until\n * the plan config has loaded; the server stays the authoritative validator.\n */\n getMediaConfig?: () => MediaConfig | null;\n /** Surface upload/validation failures to the consumer's error handler. */\n onError?: (error: Error) => void;\n}\n\nexport interface UseCloudMediaLibraryReturn {\n handleRequestMedia: (\n context?: MediaRequestContext,\n ) => Promise<MediaResult | null>;\n handleMediaSelect: (item: MediaItem) => void;\n handleMediaLibraryClose: () => void;\n}\n\nexport function useCloudMediaLibrary(\n options: UseCloudMediaLibraryOptions,\n): UseCloudMediaLibraryReturn {\n const {\n onRequestMedia,\n mediaLibraryOpen,\n mediaLibraryAccept,\n authManager,\n getMediaConfig,\n onError,\n } = options;\n\n let mediaResolve: ((result: MediaResult | null) => void) | null = null;\n\n function toMediaResult(item: MediaItem): MediaResult {\n return { url: item.url, alt: item.alt_text || undefined };\n }\n\n async function handleRequestMedia(\n context?: MediaRequestContext,\n ): Promise<MediaResult | null> {\n const accept = context?.accept ?? [\"images\"];\n\n // Drag-and-drop upload path (#229). A dropped file is forwarded to a\n // consumer handler if one is configured; otherwise it's uploaded directly\n // to the cloud and the resulting media item becomes the block's image.\n if (context?.files?.length) {\n if (onRequestMedia) {\n const item = await onRequestMedia({ accept, files: context.files });\n return item ? toMediaResult(item) : null;\n }\n return uploadDroppedFile(context.files[0]);\n }\n\n // If consumer provides a custom media handler, use it.\n if (onRequestMedia) {\n const item = await onRequestMedia({ accept });\n return item ? toMediaResult(item) : null;\n }\n\n // Otherwise open the built-in media library. If a previous request\n // is still pending (e.g. consumer fired two requests back-to-back),\n // settle it with null so its caller doesn't hang forever.\n if (mediaResolve) {\n mediaResolve(null);\n mediaResolve = null;\n }\n mediaLibraryAccept.value = accept;\n mediaLibraryOpen.value = true;\n return new Promise<MediaResult | null>((resolve) => {\n mediaResolve = (result) => {\n resolve(result);\n };\n });\n }\n\n async function uploadDroppedFile(file: File): Promise<MediaResult | null> {\n const validationError = validateFile(file);\n if (validationError) {\n onError?.(validationError);\n return null;\n }\n try {\n // media-library is an optional, cloud-only peer — import lazily so OSS\n // bundles never pull it in. Cloud consumers always have it installed.\n const { MediaApiClient } = await import(\"@templatical/media-library\");\n const item = await new MediaApiClient(authManager).uploadMedia(file);\n return toMediaResult(item);\n } catch (error) {\n onError?.(error as Error);\n return null;\n }\n }\n\n function validateFile(file: File): Error | null {\n const media = getMediaConfig?.() ?? null;\n if (!media) return null; // config not loaded yet → defer to server.\n const acceptedTypes = media.categories?.images?.mime_types ?? [];\n if (acceptedTypes.length > 0 && !acceptedTypes.includes(file.type)) {\n return new Error(\n `Unsupported image type: ${file.type || \"unknown\"}. Accepted: ${acceptedTypes.join(\", \")}.`,\n );\n }\n if (media.max_file_size > 0 && file.size > media.max_file_size) {\n return new Error(\n `Image is too large (${file.size} bytes). Maximum allowed is ${media.max_file_size} bytes.`,\n );\n }\n return null;\n }\n\n function handleMediaSelect(item: MediaItem): void {\n mediaLibraryOpen.value = false;\n mediaResolve?.(toMediaResult(item));\n mediaResolve = null;\n }\n\n function handleMediaLibraryClose(): void {\n mediaLibraryOpen.value = false;\n mediaResolve?.(null);\n mediaResolve = null;\n }\n\n onScopeDispose(() => {\n if (mediaResolve) {\n mediaResolve(null);\n mediaResolve = null;\n }\n });\n\n return {\n handleRequestMedia,\n handleMediaSelect,\n handleMediaLibraryClose,\n };\n}\n","import { computed, provide, ref, watch, type ComputedRef, type Ref } from \"vue\";\nimport {\n AuthManager,\n performHealthCheck,\n useAiConfig,\n useCollaboration,\n useCollaborationBroadcast,\n useCommentListener,\n useComments,\n useEditor,\n useExport,\n useMcpListener,\n usePlanConfig,\n createCloudSavedBlocksProvider,\n useTemplateScoring,\n useTestEmail,\n useWebSocket,\n type UseAiConfigReturn,\n type UseCollaborationReturn,\n type UseCommentsReturn,\n type UseExportReturn,\n type UsePlanConfigReturn,\n type UseTemplateScoringReturn,\n type UseTestEmailReturn,\n type UseWebSocketReturn,\n type UseEditorReturn as CloudUseEditorReturn,\n} from \"@templatical/core/cloud\";\nimport {\n useSavedBlocksFeature,\n type UseSavedBlocksFeatureReturn,\n} from \"../../composables/useSavedBlocksFeature\";\nimport type {\n McpOperationPayload,\n ThemeOverrides,\n UiTheme,\n} from \"@templatical/types\";\n\nimport {\n useEditorCore,\n type UseEditorCoreReturn,\n} from \"../../composables/useEditorCore\";\nimport { resolveLintOptions } from \"../../utils/resolveLintOptions\";\nimport { useDragDrop } from \"../../composables/useDragDrop\";\nimport type { UseFontsReturn } from \"../../composables/useFonts\";\nimport type { Translations } from \"../../i18n\";\nimport type { EditorCapabilities } from \"../../types/editor-capabilities\";\nimport {\n ON_REQUEST_MEDIA_KEY,\n AUTH_MANAGER_KEY,\n AI_CONFIG_KEY,\n COMMENTS_KEY,\n SCORING_KEY,\n CAPABILITIES_KEY,\n} from \"../../keys\";\nimport { DEFAULT_AUTO_SAVE_DEBOUNCE_MS } from \"../../constants/timeouts\";\nimport { logger } from \"../../utils/logger\";\n\nimport {\n useSnapshotPreview,\n type UseSnapshotPreviewReturn,\n} from \"./useSnapshotPreview\";\nimport {\n useCloudPanelState,\n type UseCloudPanelStateReturn,\n} from \"./useCloudPanelState\";\nimport { useCollabUndoWarning } from \"./useCollabUndoWarning\";\nimport {\n useCloudFeatureFlags,\n type UseCloudFeatureFlagsReturn,\n} from \"./useCloudFeatureFlags\";\nimport {\n useCloudMediaLibrary,\n type UseCloudMediaLibraryReturn,\n} from \"./useCloudMediaLibrary\";\n\nimport type { TemplaticalCloudEditorConfig } from \"../cloudConfig\";\n\n/** Minimal interface of the CommentsSidebar-filter target. */\ninterface CommentsSidebarInstance {\n filterByBlock: (blockId: string) => void;\n}\n\n/** Getter for the target — evaluated lazily so the sidebar can mount later. */\ntype CommentsSidebarGetter = () => CommentsSidebarInstance | null;\n\ntype CollaborationInstance = UseCollaborationReturn & {\n _broadcastOperation: (payload: McpOperationPayload) => void;\n _isProcessingRemoteOperation: () => boolean;\n};\n\nexport interface UseCloudInitializationOptions {\n config: TemplaticalCloudEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n emit: (event: \"ready\") => void;\n /** Lazy getter for the CommentsSidebar-filter target (for block-filter). */\n getCommentsSidebar: CommentsSidebarGetter;\n /**\n * Effective DOM root passed through to `useEditorCore`. Set when the\n * cloud editor is mounted inside a shadow root (`shadowDom: true`).\n */\n editorRoot?: Document | ShadowRoot;\n /**\n * Outer `.tpl` container ref forwarded to `useEditorCore` for\n * multi-editor keyboard routing.\n */\n containerEl?: Ref<HTMLElement | null>;\n}\n\nexport interface UseCloudInitializationReturn {\n // Init state\n isInitializing: Ref<boolean>;\n isAuthReady: Ref<boolean>;\n initError: Ref<Error | null>;\n isDestroyed: () => boolean;\n\n // Infrastructure\n authManager: AuthManager;\n planConfigInstance: UsePlanConfigReturn;\n websocket: UseWebSocketReturn;\n collaboration: CollaborationInstance | null;\n isCollaborationEnabled: ComputedRef<boolean>;\n\n // Editor + core\n editor: CloudUseEditorReturn;\n core: UseEditorCoreReturn;\n\n // Cloud composables\n aiConfig: UseAiConfigReturn;\n featureFlags: UseCloudFeatureFlagsReturn;\n mediaLib: UseCloudMediaLibraryReturn;\n exporter: UseExportReturn;\n testEmail: UseTestEmailReturn;\n commentsInstance: UseCommentsReturn;\n savedBlocks: UseSavedBlocksFeatureReturn;\n scoringInstance: UseTemplateScoringReturn;\n panelState: UseCloudPanelStateReturn;\n snapshotPreview: UseSnapshotPreviewReturn;\n collabWarning: ReturnType<typeof useCollabUndoWarning>;\n\n // Late-bound save hook — set by `useCloudLifecycle` after it wires saveTemplate.\n // The `onSave` keyboard shortcut + `useEditorCore` autoSave both route here.\n onSaveHook: { value: (() => Promise<unknown>) | null };\n\n // Methods\n initialize: () => Promise<void>;\n destroy: () => void;\n setThemeOverrides: (overrides: ThemeOverrides) => void;\n setUiTheme: (theme: UiTheme) => void;\n openCommentsForBlock: (blockId: string) => void;\n}\n\n/**\n * Owns the entire CloudEditor.vue initialization dance:\n * 1. AuthManager + PlanConfig\n * 2. Collaboration locked-blocks forward-ref\n * 3. Cloud editor\n * 4. WebSocket + MCP listener\n * 5. Collaboration (wraps editor methods — MUST precede useEditorCore)\n * 6. useEditorCore (provides all shared keys)\n * 7. Collab undo warning (needs core.history.canUndo)\n * 8. Snapshot preview (needs core.autoSave)\n * 9. Remaining cloud composables (aiConfig, exporter, testEmail, etc.)\n * 10. Cloud-only provides (including CAPABILITIES_KEY override)\n *\n * Forward-ref pattern for `snapshotPreview` / `collabWarning` is contained\n * here instead of leaking to the parent.\n */\nexport function useCloudInitialization(\n options: UseCloudInitializationOptions,\n): UseCloudInitializationReturn {\n const { config, translations, fontsManager, emit, getCommentsSidebar } =\n options;\n\n // --- Init state ---\n const isInitializing = ref(true);\n const isAuthReady = ref(false);\n const initError = ref<Error | null>(null);\n let _destroyed = false;\n\n // --- Late-bound save hook (filled in by useCloudLifecycle) ---\n const onSaveHook: { value: (() => Promise<unknown>) | null } = {\n value: null,\n };\n\n // --- Forward refs for circular composable dependencies ---\n let snapshotPreviewRef: UseSnapshotPreviewReturn | null = null;\n let collabWarningRef: ReturnType<typeof useCollabUndoWarning> | null = null;\n\n // --- 1. AuthManager + PlanConfig ---\n const authManager = new AuthManager({\n ...config.auth,\n onError: config.onError,\n });\n\n const planConfigInstance = usePlanConfig({\n authManager,\n onError: config.onError,\n });\n\n // --- 2. Collaboration locked blocks ref (forward-declared for step 3) ---\n const collaborationLockedBlocks = ref<Map<string, unknown>>(new Map());\n\n // --- 3. Cloud editor ---\n const editor = useEditor({\n authManager,\n defaultFontFamily: config.fonts?.defaultFont,\n templateDefaults: config.templateDefaults,\n onError: config.onError,\n lockedBlocks: collaborationLockedBlocks,\n });\n\n // --- 4. WebSocket + MCP listener ---\n const websocket = useWebSocket({\n authManager,\n onError: config.onError,\n });\n\n if (config.mcp?.enabled) {\n useMcpListener({\n editor,\n channel: websocket.channel,\n onOperation: config.mcp.onOperation,\n });\n }\n\n // --- 5. Collaboration (MUST precede useEditorCore) ---\n //\n // Order: `useCollaborationBroadcast` wraps editor mutation methods so they\n // broadcast to peers. Step 6's `useHistoryInterceptor` then wraps them\n // AGAIN to push history entries. Reversing would push history for remote\n // operations, causing local/remote state drift.\n let collaboration: CollaborationInstance | null = null;\n\n if (config.collaboration?.enabled) {\n collaboration = useCollaboration({\n authManager,\n editor,\n channel: websocket.channel,\n onError: config.onError,\n onCollaboratorJoined: config.collaboration.onCollaboratorJoined,\n onCollaboratorLeft: config.collaboration.onCollaboratorLeft,\n onBlockLocked: config.collaboration.onBlockLocked,\n onBlockUnlocked: config.collaboration.onBlockUnlocked,\n }) as CollaborationInstance;\n\n watch(\n () => collaboration!.lockedBlocks.value,\n (newLockedBlocks) => {\n collaborationLockedBlocks.value = newLockedBlocks;\n },\n { immediate: true },\n );\n\n useCollaborationBroadcast(editor, collaboration);\n }\n\n const isCollaborationEnabled = computed(\n () =>\n !!config.collaboration?.enabled &&\n planConfigInstance.hasFeature(\"collaboration\"),\n );\n\n // --- 6. useEditorCore ---\n const core = useEditorCore({\n editor,\n config: {\n uiTheme: config.uiTheme,\n theme: undefined, // applied in initialize() after plan check\n blockDefaults: config.blockDefaults,\n templateDefaults: config.templateDefaults,\n customBlocks: [], // deferred to initialize()\n paletteBlocks: config.paletteBlocks,\n htmlBlockPreview: config.htmlBlockPreview,\n colors: config.colors,\n mergeTags: config.mergeTags,\n logicTags: config.logicTags,\n displayConditions: config.displayConditions,\n onRequestMedia: null, // cloud handles via mediaLib.handleRequestMedia\n lint: resolveLintOptions(config),\n onSave: () => {\n onSaveHook.value?.().catch((err) => {\n config.onError?.(err as Error);\n });\n },\n },\n translations,\n fontsManager,\n historyOptions: collaboration\n ? {\n isRemoteOperation: () =>\n collaboration!._isProcessingRemoteOperation(),\n }\n : undefined,\n autoSaveOptions: {\n onChange: async () => {\n if (editor.hasTemplate()) {\n await editor.createSnapshot();\n snapshotPreviewRef?.snapshotHistoryInstance.value?.loadSnapshots();\n }\n },\n debounce: config.autoSaveDebounce ?? DEFAULT_AUTO_SAVE_DEBOUNCE_MS,\n enabled: () =>\n config.autoSave !== false && planConfigInstance.hasFeature(\"auto_save\"),\n },\n themeExtraStyles: () => ({\n \"--tpl-drop-text\": `\"${translations.canvas.dropHere}\"`,\n }),\n keyboardOptions: {\n onBeforeUndo: () => collabWarningRef?.showCollabUndoWarning(),\n },\n editorRoot: options.editorRoot,\n containerEl: options.containerEl,\n });\n\n // --- 7. Collab undo warning ---\n const collabWarning = useCollabUndoWarning({\n isCollaborationEnabled,\n getCollaboratorCount: () => collaboration?.collaborators.value.length ?? 0,\n canUndo: core.history.canUndo,\n });\n collabWarningRef = collabWarning;\n\n // --- 8. Snapshot preview ---\n const snapshotPreview = useSnapshotPreview({\n authManager,\n editor,\n history: core.history,\n conditionPreview: core.conditionPreview,\n autoSave: core.autoSave,\n onError: config.onError,\n });\n snapshotPreviewRef = snapshotPreview;\n\n // --- 9. Remaining cloud composables ---\n const panelState = useCloudPanelState();\n const aiConfig = useAiConfig(config.ai);\n\n const featureFlags = useCloudFeatureFlags({\n planConfigInstance,\n aiConfig,\n editor,\n });\n\n const mediaLib = useCloudMediaLibrary({\n onRequestMedia: config.onRequestMedia,\n mediaLibraryOpen: panelState.mediaLibraryOpen,\n mediaLibraryAccept: panelState.mediaLibraryAccept,\n authManager,\n getMediaConfig: () => planConfigInstance.config.value?.media ?? null,\n onError: config.onError,\n });\n\n // Install drag-drop listeners (no return value needed).\n useDragDrop({\n onBlockMove: editor.moveBlock,\n onBlockAdd: editor.addBlock,\n });\n\n const exporter = useExport({\n authManager,\n getFontsConfig: () => config.fonts,\n canUseCustomFonts: () => planConfigInstance.hasFeature(\"custom_fonts\"),\n });\n\n const testEmail = useTestEmail({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n save: () => editor.save(),\n exportHtml: (templateId: string) => exporter.exportHtml(templateId),\n onError: config.onError,\n isAuthReady,\n onBeforeTestEmail: config.onBeforeTestEmail,\n });\n\n const commentsInstance = useComments({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n getSocketId: () => websocket.getSocketId(),\n onComment: config.onComment,\n onError: config.onError,\n isAuthReady,\n hasCommentingFeature: () =>\n config.commenting !== false &&\n planConfigInstance.hasFeature(\"commenting\"),\n });\n\n useCommentListener({\n comments: commentsInstance,\n channel: websocket.channel,\n });\n\n // Saved blocks run on the same shared composable as OSS; only the storage\n // adapter differs. Availability is read reactively from the plan, which\n // resolves after `fetchConfig()` — so the capability never advertises a\n // feature the plan doesn't grant.\n //\n // A consumer may pass their own provider instead of a boolean, in which case\n // it replaces Cloud's store. That path is deliberately NOT plan-gated: the\n // `saved_modules` feature licenses Cloud's *storage*, and someone else's\n // backend isn't Cloud's to sell. It keeps the config key type-identical to\n // `init()`'s, so an OSS integration upgrades by deleting the key or leaving it\n // alone rather than rewriting it.\n const consumerSavedBlocksProvider =\n typeof config.savedBlocks === \"object\" && config.savedBlocks !== null\n ? config.savedBlocks\n : null;\n\n const savedBlocks = useSavedBlocksFeature({\n // `??` short-circuits, so Cloud's adapter isn't constructed at all when the\n // consumer supplied one.\n provider:\n consumerSavedBlocksProvider ??\n createCloudSavedBlocksProvider(authManager),\n editor,\n onError: config.onError,\n isAvailable: consumerSavedBlocksProvider\n ? () => true\n : () =>\n config.savedBlocks !== false &&\n planConfigInstance.hasFeature(\"saved_modules\"),\n });\n\n const scoringInstance = useTemplateScoring({\n authManager,\n getTemplateId: () => editor.state.template?.id ?? null,\n });\n\n // --- Comments block-filter bridge ---\n function openCommentsForBlock(blockId: string): void {\n panelState.commentsOpen.value = true;\n // Sidebar may not be mounted yet (async component); defer look-up.\n queueMicrotask(() => {\n getCommentsSidebar()?.filterByBlock(blockId);\n });\n }\n\n // --- 10. Cloud-only provides ---\n provide(ON_REQUEST_MEDIA_KEY, mediaLib.handleRequestMedia);\n provide(AUTH_MANAGER_KEY, authManager);\n provide(AI_CONFIG_KEY, aiConfig);\n provide(COMMENTS_KEY, commentsInstance);\n provide(SCORING_KEY, scoringInstance);\n\n // Override default capabilities from useEditorCore with cloud capabilities.\n provide(CAPABILITIES_KEY, {\n plan: planConfigInstance,\n ai: aiConfig,\n comments: {\n getBlockCount: (blockId: string) =>\n commentsInstance.commentCountByBlock.value.get(blockId) ?? 0,\n openForBlock: openCommentsForBlock,\n },\n savedBlocks: savedBlocks.capability,\n } satisfies EditorCapabilities);\n\n // --- Theme setters (plan-gated) ---\n function setThemeOverrides(overrides: ThemeOverrides): void {\n if (!planConfigInstance.hasFeature(\"theme_customization\")) return;\n core.themeOverrides.value = overrides;\n }\n\n function setUiTheme(theme: UiTheme): void {\n editor.setUiTheme(theme);\n }\n\n // --- Initialize (async bootstrap) ---\n async function initialize(): Promise<void> {\n isInitializing.value = true;\n initError.value = null;\n\n try {\n await authManager.initialize();\n if (_destroyed) return;\n isAuthReady.value = true;\n\n const healthResult = await performHealthCheck({ authManager });\n if (_destroyed) return;\n\n if (!healthResult.api.ok) {\n throw new Error(\"Health check failed: API is not reachable\");\n }\n\n if (!healthResult.auth.ok) {\n throw new Error(\n `Health check failed: authentication error${healthResult.auth.error ? ` - ${healthResult.auth.error}` : \"\"}`,\n );\n }\n\n if (!healthResult.websocket.ok) {\n logger.warn(\n \"WebSocket health check failed:\",\n healthResult.websocket.error ?? \"unknown error\",\n \"-- real-time features will be disabled.\",\n );\n }\n\n await planConfigInstance.fetchConfig();\n if (_destroyed) return;\n\n fontsManager.setCustomFontsEnabled(\n planConfigInstance.hasFeature(\"custom_fonts\"),\n );\n\n if (\n config.customBlocks?.length &&\n planConfigInstance.hasFeature(\"custom_blocks\")\n ) {\n core.registerCustomBlocks(config.customBlocks);\n }\n\n if (\n config.theme &&\n planConfigInstance.hasFeature(\"theme_customization\")\n ) {\n core.themeOverrides.value = config.theme;\n }\n\n emit(\"ready\");\n } catch (error) {\n if (_destroyed) return;\n const wrappedError =\n error instanceof Error\n ? error\n : new Error(\"Initialization failed\", { cause: error });\n initError.value = wrappedError;\n config.onError?.(wrappedError);\n } finally {\n if (!_destroyed) {\n isInitializing.value = false;\n }\n }\n }\n\n function destroy(): void {\n _destroyed = true;\n fontsManager.cleanupFontLinks();\n websocket.disconnect();\n core.destroy();\n config.onUnmount?.();\n }\n\n return {\n isInitializing,\n isAuthReady,\n initError,\n isDestroyed: () => _destroyed,\n\n authManager,\n planConfigInstance,\n websocket,\n collaboration,\n isCollaborationEnabled,\n\n editor,\n core,\n\n aiConfig,\n featureFlags,\n mediaLib,\n exporter,\n testEmail,\n commentsInstance,\n savedBlocks,\n scoringInstance,\n panelState,\n snapshotPreview,\n collabWarning,\n\n onSaveHook,\n\n initialize,\n destroy,\n setThemeOverrides,\n setUiTheme,\n openCommentsForBlock,\n };\n}\n","import type { Block, CustomBlock, TemplateContent } from \"@templatical/types\";\nimport { isCustomBlock } from \"@templatical/types\";\nimport type { UseBlockRegistryReturn } from \"../composables/useBlockRegistry\";\n\n/**\n * Renders every custom block in the content tree to its HTML representation\n * and stores the result on `block.renderedHtml`. Called before save so the\n * backend can include custom-block output in its MJML export.\n *\n * Failures per-block are swallowed (and replaced with a comment placeholder)\n * so one broken block doesn't block the save of the rest.\n */\nexport async function preRenderCustomBlocks(\n content: TemplateContent,\n registry: UseBlockRegistryReturn,\n): Promise<void> {\n const renderBlock = async (block: Block): Promise<void> => {\n if (isCustomBlock(block)) {\n const customBlock = block as CustomBlock;\n try {\n customBlock.renderedHtml =\n await registry.renderCustomBlock(customBlock);\n } catch {\n customBlock.renderedHtml = `<!-- Custom block render error: ${customBlock.customType} -->`;\n }\n }\n\n if (block.type === \"section\" && \"children\" in block) {\n const sectionBlock = block as { children: Block[][] };\n for (const column of sectionBlock.children) {\n for (const child of column) {\n await renderBlock(child);\n }\n }\n }\n };\n\n for (const block of content.blocks) {\n await renderBlock(block);\n }\n}\n","import {\n resolveWebSocketConfig,\n type UseExportReturn,\n type UsePlanConfigReturn,\n type UseWebSocketReturn,\n type UseEditorReturn as CloudUseEditorReturn,\n} from \"@templatical/core/cloud\";\nimport type { SaveResult, Template, TemplateContent } from \"@templatical/types\";\n\nimport type { UseEditorCoreReturn } from \"../../composables/useEditorCore\";\nimport { preRenderCustomBlocks } from \"../../utils/preRenderCustomBlocks\";\n\nimport type { UseSnapshotPreviewReturn } from \"./useSnapshotPreview\";\nimport type { UseCloudFeatureFlagsReturn } from \"./useCloudFeatureFlags\";\nimport type { TemplaticalCloudEditorConfig } from \"../cloudConfig\";\n\nexport interface UseCloudLifecycleOptions {\n config: TemplaticalCloudEditorConfig;\n editor: CloudUseEditorReturn;\n websocket: UseWebSocketReturn;\n planConfigInstance: UsePlanConfigReturn;\n snapshotPreview: UseSnapshotPreviewReturn;\n core: UseEditorCoreReturn;\n exporter: UseExportReturn;\n featureFlags: UseCloudFeatureFlagsReturn;\n isDestroyed: () => boolean;\n}\n\nexport interface UseCloudLifecycleReturn {\n createTemplate: (content?: TemplateContent) => Promise<Template>;\n loadTemplate: (templateId: string) => Promise<Template>;\n saveTemplate: () => Promise<SaveResult>;\n}\n\n/**\n * Template-level operations on a cloud editor: create/load/save.\n * Each guards against post-unmount execution via `isDestroyed()`.\n */\nexport function useCloudLifecycle(\n options: UseCloudLifecycleOptions,\n): UseCloudLifecycleReturn {\n const {\n config,\n editor,\n websocket,\n planConfigInstance,\n snapshotPreview,\n core,\n exporter,\n featureFlags,\n isDestroyed,\n } = options;\n\n function getWebSocketConfig() {\n return resolveWebSocketConfig(planConfigInstance.config.value!.websocket);\n }\n\n async function createTemplate(content?: TemplateContent): Promise<Template> {\n const template = await editor.create(content);\n if (isDestroyed()) return template;\n config.onCreate?.(template);\n snapshotPreview.initSnapshotHistory();\n websocket.connect(template.id, getWebSocketConfig());\n return template;\n }\n\n async function loadTemplate(templateId: string): Promise<Template> {\n const template = await editor.load(templateId);\n if (isDestroyed()) return template;\n config.onLoad?.(template);\n snapshotPreview.initSnapshotHistory();\n websocket.connect(template.id, getWebSocketConfig());\n return template;\n }\n\n async function saveTemplate(): Promise<SaveResult> {\n featureFlags.isSaveExporting.value = true;\n featureFlags.saveStatus.value = \"idle\";\n try {\n // Pre-render custom blocks so backend can include them in MJML export.\n await preRenderCustomBlocks(editor.content.value, core.registry);\n if (isDestroyed()) throw new Error(\"Component unmounted during save\");\n\n const template = await editor.save();\n if (isDestroyed()) throw new Error(\"Component unmounted during save\");\n\n snapshotPreview.initSnapshotHistory();\n snapshotPreview.snapshotHistoryInstance.value?.loadSnapshots();\n\n const exportResult = await exporter.exportHtml(template.id);\n if (isDestroyed()) throw new Error(\"Component unmounted during save\");\n\n const saveResult: SaveResult = {\n templateId: template.id,\n html: exportResult.html,\n mjml: exportResult.mjml,\n content: template.content,\n };\n\n config.onSave?.(saveResult);\n\n featureFlags.saveStatus.value = \"saved\";\n featureFlags.startSaveStatusClear();\n\n return saveResult;\n } catch (error) {\n if (!isDestroyed()) {\n featureFlags.saveStatus.value = \"error\";\n featureFlags.saveErrorMessage.value =\n error instanceof Error ? error.message : \"Save failed\";\n }\n throw error;\n } finally {\n if (!isDestroyed()) {\n featureFlags.isSaveExporting.value = false;\n }\n }\n }\n\n return { createTemplate, loadTemplate, saveTemplate };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport type { PlanConfig } from \"@templatical/types\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\nexport interface UseCloudSaveGateOptions {\n /** Reactive accessibility issues from `useTemplateLint`. */\n issues: Ref<LintIssue[]>;\n /** Reactive plan config from the server. */\n planConfig: Ref<PlanConfig | null>;\n}\n\nexport interface UseCloudSaveGateReturn {\n /** True when the gate would block a save click given current state. */\n shouldBlock: ComputedRef<boolean>;\n /** Errors that would block the save. Empty when `shouldBlock` is false. */\n blockingIssues: ComputedRef<LintIssue[]>;\n /** Modal visibility — toggled by `tryRunSave` and `cancel`. */\n modalOpen: Ref<boolean>;\n /**\n * Wraps a save callback. Returns `true` when the save was invoked\n * (blocking disabled or no errors), `false` when the modal opened\n * instead. Caller can `await` the returned promise to know when the\n * save settled.\n */\n tryRunSave: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n /** Force-run the pending save, bypassing the gate. */\n confirmAndSave: () => Promise<void>;\n /** Dismiss the modal without saving. */\n cancel: () => void;\n}\n\n/**\n * Cloud-only save-gate. When `planConfig.accessibility.blockOnError === true`\n * and the linter reports any error-severity issues, intercept the save\n * click and surface a confirmation modal listing the blockers. The user\n * can dismiss to fix issues, or click \"Save anyway\" to bypass — the gate\n * does NOT enforce a hard block, it surfaces a deliberate choice.\n *\n * Server policy is the source of truth for `blockOnError`; consumer-side\n * `accessibility` options can't override it.\n */\nexport function useCloudSaveGate(\n opts: UseCloudSaveGateOptions,\n): UseCloudSaveGateReturn {\n const modalOpen = ref(false);\n let pending: (() => Promise<unknown> | unknown) | null = null;\n\n const blockOnError = computed(\n () => opts.planConfig.value?.accessibility?.blockOnError === true,\n );\n\n const blockingIssues = computed(() =>\n blockOnError.value\n ? opts.issues.value.filter((i) => i.severity === \"error\")\n : [],\n );\n\n const shouldBlock = computed(() => blockingIssues.value.length > 0);\n\n async function tryRunSave(\n run: () => Promise<unknown> | unknown,\n ): Promise<boolean> {\n if (!shouldBlock.value) {\n await run();\n return true;\n }\n pending = run;\n modalOpen.value = true;\n return false;\n }\n\n async function confirmAndSave(): Promise<void> {\n const run = pending;\n pending = null;\n modalOpen.value = false;\n if (run) {\n await run();\n }\n }\n\n function cancel(): void {\n pending = null;\n modalOpen.value = false;\n }\n\n return {\n shouldBlock,\n blockingIssues,\n modalOpen,\n tryRunSave,\n confirmAndSave,\n cancel,\n };\n}\n","<script setup lang=\"ts\">\nimport { AlertTriangle } from \"@lucide/vue\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\ndefineProps<{\n open: boolean;\n issues: LintIssue[];\n}>();\n\nconst emit = defineEmits<{\n (e: \"cancel\"): void;\n (e: \"confirm\"): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-150\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-150\"\n enter-from-class=\"tpl:opacity-0\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <div\n v-if=\"open\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"cloudT.saveGate.title\"\n class=\"tpl:fixed tpl:inset-0 tpl:z-50 tpl:flex tpl:items-center tpl:justify-center tpl:bg-black/40 tpl:p-6\"\n @click.self=\"emit('cancel')\"\n >\n <div\n class=\"tpl:flex tpl:max-h-[80vh] tpl:w-full tpl:max-w-md tpl:flex-col tpl:gap-4 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)] tpl:p-5 tpl:shadow-[var(--tpl-shadow-md)]\"\n >\n <header class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <AlertTriangle\n :size=\"18\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-warning)]\"\n />\n <h2\n class=\"tpl:m-0 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ cloudT.saveGate.title }}\n </h2>\n </header>\n\n <p class=\"tpl:m-0 tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudT.saveGate.body }}\n </p>\n\n <ul\n class=\"tpl:m-0 tpl:flex tpl:max-h-64 tpl:list-none tpl:flex-col tpl:gap-1.5 tpl:overflow-y-auto tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-2\"\n >\n <li\n v-for=\"issue in issues\"\n :key=\"`${issue.ruleId}-${issue.blockId ?? 'template'}`\"\n class=\"tpl:flex tpl:flex-col tpl:gap-0.5 tpl:rounded tpl:px-2 tpl:py-1.5\"\n >\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\">\n {{ issue.message }}\n </span>\n <span\n class=\"tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ issue.ruleId }}\n </span>\n </li>\n </ul>\n\n <footer class=\"tpl:flex tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n @click=\"emit('cancel')\"\n >\n {{ cloudT.saveGate.cancel }}\n </button>\n <button\n type=\"button\"\n class=\"tpl:rounded-md tpl:bg-[var(--tpl-danger)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-white\"\n @click=\"emit('confirm')\"\n >\n {{ cloudT.saveGate.confirm }}\n </button>\n </footer>\n </div>\n </div>\n </Transition>\n</template>\n","<script setup lang=\"ts\">\nimport { AlertTriangle } from \"@lucide/vue\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport type { LintIssue } from \"../../composables/useTemplateLint\";\n\ndefineProps<{\n open: boolean;\n issues: LintIssue[];\n}>();\n\nconst emit = defineEmits<{\n (e: \"cancel\"): void;\n (e: \"confirm\"): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n</script>\n\n<template>\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-150\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-150\"\n enter-from-class=\"tpl:opacity-0\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <div\n v-if=\"open\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"cloudT.saveGate.title\"\n class=\"tpl:fixed tpl:inset-0 tpl:z-50 tpl:flex tpl:items-center tpl:justify-center tpl:bg-black/40 tpl:p-6\"\n @click.self=\"emit('cancel')\"\n >\n <div\n class=\"tpl:flex tpl:max-h-[80vh] tpl:w-full tpl:max-w-md tpl:flex-col tpl:gap-4 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)] tpl:p-5 tpl:shadow-[var(--tpl-shadow-md)]\"\n >\n <header class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <AlertTriangle\n :size=\"18\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-warning)]\"\n />\n <h2\n class=\"tpl:m-0 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ cloudT.saveGate.title }}\n </h2>\n </header>\n\n <p class=\"tpl:m-0 tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ cloudT.saveGate.body }}\n </p>\n\n <ul\n class=\"tpl:m-0 tpl:flex tpl:max-h-64 tpl:list-none tpl:flex-col tpl:gap-1.5 tpl:overflow-y-auto tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-2\"\n >\n <li\n v-for=\"issue in issues\"\n :key=\"`${issue.ruleId}-${issue.blockId ?? 'template'}`\"\n class=\"tpl:flex tpl:flex-col tpl:gap-0.5 tpl:rounded tpl:px-2 tpl:py-1.5\"\n >\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\">\n {{ issue.message }}\n </span>\n <span\n class=\"tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ issue.ruleId }}\n </span>\n </li>\n </ul>\n\n <footer class=\"tpl:flex tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n @click=\"emit('cancel')\"\n >\n {{ cloudT.saveGate.cancel }}\n </button>\n <button\n type=\"button\"\n class=\"tpl:rounded-md tpl:bg-[var(--tpl-danger)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-white\"\n @click=\"emit('confirm')\"\n >\n {{ cloudT.saveGate.confirm }}\n </button>\n </footer>\n </div>\n </div>\n </Transition>\n</template>\n","<script setup lang=\"ts\">\nimport {\n Check,\n CircleAlert,\n LoaderCircle,\n MessageCircle,\n Save,\n Send,\n Sparkles,\n} from \"@lucide/vue\";\nimport { defineAsyncComponent } from \"vue\";\nimport type {\n UseCommentsReturn,\n UseTestEmailReturn,\n UseWebSocketReturn,\n UseEditorReturn as CloudUseEditorReturn,\n} from \"@templatical/core/cloud\";\n\nimport type { UseEditorCoreReturn } from \"../../composables/useEditorCore\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport { headerBtnClass } from \"../../constants/styleConstants\";\nimport ViewportToggle from \"../../components/ViewportToggle.vue\";\nimport PreviewToggle from \"../../components/PreviewToggle.vue\";\nimport DarkModeToggle from \"../../components/DarkModeToggle.vue\";\nimport type { UseCloudFeatureFlagsReturn } from \"../composables/useCloudFeatureFlags\";\nimport type { UseCloudPanelStateReturn } from \"../composables/useCloudPanelState\";\nimport type { UseSnapshotPreviewReturn } from \"../composables/useSnapshotPreview\";\nimport type { UseCloudInitializationReturn } from \"../composables/useCloudInitialization\";\n\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./CollaboratorBar.vue\"),\n);\nconst SnapshotHistory = defineAsyncComponent(\n () => import(\"./SnapshotHistory.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(() => import(\"./AiFeatureMenu.vue\"));\n\ndefineProps<{\n editor: CloudUseEditorReturn;\n core: UseEditorCoreReturn;\n featureFlags: UseCloudFeatureFlagsReturn;\n panelState: UseCloudPanelStateReturn;\n snapshotPreview: UseSnapshotPreviewReturn;\n commentsInstance: UseCommentsReturn;\n testEmail: UseTestEmailReturn;\n websocket: UseWebSocketReturn;\n collaboration: UseCloudInitializationReturn[\"collaboration\"];\n isCollaborationEnabled: boolean;\n isSaveDisabled: boolean;\n isSaving: boolean;\n}>();\n\ndefineEmits<{\n (e: \"save\"): void;\n}>();\n\nconst { t: cloudT, format: cloudFormat } = useCloudI18nStrict();\n</script>\n\n<template>\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n box-shadow: var(--tpl-shadow-md);\n border-bottom: 1px solid var(--tpl-border);\n \"\n >\n <!-- Left: Logo + template count -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-3\"\n >\n <span\n v-if=\"featureFlags.templateLimit.value !== null\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n cloudFormat(cloudT.header.templatesUsed, {\n used: featureFlags.templateCount.value,\n max: featureFlags.templateLimit.value,\n })\n }}\n </span>\n </div>\n\n <!-- Center: viewport + preview + dark mode + collaboration + snapshots -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <CollaboratorBar\n v-if=\"collaboration && isCollaborationEnabled\"\n :collaborators=\"collaboration.collaborators.value\"\n :is-connected=\"websocket.isConnected.value\"\n />\n <SnapshotHistory\n v-if=\"snapshotPreview.snapshotHistoryInstance.value\"\n :snapshots=\"snapshotPreview.snapshotHistorySnapshots.value\"\n :is-loading=\"snapshotPreview.snapshotHistoryIsLoading.value\"\n :is-restoring=\"snapshotPreview.snapshotHistoryIsRestoring.value\"\n @load=\"snapshotPreview.loadSnapshotHistory\"\n @navigate=\"snapshotPreview.handleSnapshotNavigate\"\n />\n </div>\n\n <!-- Right: Cloud actions -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Save status indicator -->\n <div\n v-if=\"featureFlags.saveStatus.value === 'error'\"\n aria-live=\"assertive\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"featureFlags.saveErrorMessage.value\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ cloudT.header.saveFailed }}\n </div>\n <div\n v-else-if=\"featureFlags.saveStatus.value === 'saved'\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ cloudT.header.saved }}\n </div>\n <div\n v-else-if=\"editor.state.isDirty\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ cloudT.header.unsaved }}\n </div>\n\n <!-- Comments button -->\n <button\n v-if=\"\n commentsInstance.isEnabled.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :aria-label=\"\n commentsInstance.unresolvedCount.value > 0\n ? `${cloudT.comments.button} (${commentsInstance.unresolvedCount.value})`\n : cloudT.comments.button\n \"\n :aria-expanded=\"panelState.commentsOpen.value\"\n :class=\"headerBtnClass\"\n :style=\"{\n backgroundColor: panelState.commentsOpen.value\n ? 'var(--tpl-primary)'\n : 'transparent',\n color: panelState.commentsOpen.value\n ? 'var(--tpl-bg)'\n : 'var(--tpl-primary)',\n borderColor: 'var(--tpl-primary)',\n }\"\n @click=\"panelState.commentsOpen.value = !panelState.commentsOpen.value\"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.comments.button }}\n <span\n v-if=\"\n commentsInstance.unresolvedCount.value > 0 &&\n !panelState.commentsOpen.value\n \"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n >\n {{ commentsInstance.unresolvedCount.value }}\n </span>\n </button>\n\n <!-- AI button + menu -->\n <div\n v-if=\"\n featureFlags.canUseAiGeneration.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :ref=\"(el) => (panelState.aiMenuRef.value = el as HTMLElement | null)\"\n class=\"tpl:relative\"\n >\n <button\n :aria-expanded=\"panelState.aiMenuOpen.value\"\n class=\"tpl-ai-btn tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-4 tpl:py-2 tpl:text-sm tpl:font-semibold tpl:whitespace-nowrap tpl:transition-all tpl:duration-200\"\n :class=\"\n panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ cloudT.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-150 tpl:ease-out\"\n enter-from-class=\"tpl:scale-95 tpl:opacity-0\"\n enter-to-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-100 tpl:ease-in\"\n leave-from-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-to-class=\"tpl:scale-95 tpl:opacity-0\"\n >\n <div\n v-if=\"panelState.aiMenuOpen.value\"\n class=\"tpl:absolute tpl:right-0 tpl:top-full tpl:z-50 tpl:mt-1 tpl:origin-top-right\"\n >\n <AiFeatureMenu\n :active-feature=\"panelState.activeAiFeature.value\"\n @select=\"panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n\n <!-- Test email button -->\n <button\n v-if=\"testEmail.isEnabled.value && featureFlags.canSendTestEmail.value\"\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"\n testEmail.isSending.value || !featureFlags.hasTemplateSaved.value\n \"\n @click=\"panelState.testEmailModalOpen.value = true\"\n >\n <Send v-if=\"!testEmail.isSending.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.testEmail.button }}\n </button>\n\n <!-- Save button -->\n <button\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"isSaveDisabled\"\n @click=\"$emit('save')\"\n >\n <Save v-if=\"!isSaving\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ isSaving ? cloudT.header.saving : cloudT.header.save }}\n </button>\n </div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport {\n Check,\n CircleAlert,\n LoaderCircle,\n MessageCircle,\n Save,\n Send,\n Sparkles,\n} from \"@lucide/vue\";\nimport { defineAsyncComponent } from \"vue\";\nimport type {\n UseCommentsReturn,\n UseTestEmailReturn,\n UseWebSocketReturn,\n UseEditorReturn as CloudUseEditorReturn,\n} from \"@templatical/core/cloud\";\n\nimport type { UseEditorCoreReturn } from \"../../composables/useEditorCore\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\nimport { headerBtnClass } from \"../../constants/styleConstants\";\nimport ViewportToggle from \"../../components/ViewportToggle.vue\";\nimport PreviewToggle from \"../../components/PreviewToggle.vue\";\nimport DarkModeToggle from \"../../components/DarkModeToggle.vue\";\nimport type { UseCloudFeatureFlagsReturn } from \"../composables/useCloudFeatureFlags\";\nimport type { UseCloudPanelStateReturn } from \"../composables/useCloudPanelState\";\nimport type { UseSnapshotPreviewReturn } from \"../composables/useSnapshotPreview\";\nimport type { UseCloudInitializationReturn } from \"../composables/useCloudInitialization\";\n\nconst CollaboratorBar = defineAsyncComponent(\n () => import(\"./CollaboratorBar.vue\"),\n);\nconst SnapshotHistory = defineAsyncComponent(\n () => import(\"./SnapshotHistory.vue\"),\n);\nconst AiFeatureMenu = defineAsyncComponent(() => import(\"./AiFeatureMenu.vue\"));\n\ndefineProps<{\n editor: CloudUseEditorReturn;\n core: UseEditorCoreReturn;\n featureFlags: UseCloudFeatureFlagsReturn;\n panelState: UseCloudPanelStateReturn;\n snapshotPreview: UseSnapshotPreviewReturn;\n commentsInstance: UseCommentsReturn;\n testEmail: UseTestEmailReturn;\n websocket: UseWebSocketReturn;\n collaboration: UseCloudInitializationReturn[\"collaboration\"];\n isCollaborationEnabled: boolean;\n isSaveDisabled: boolean;\n isSaving: boolean;\n}>();\n\ndefineEmits<{\n (e: \"save\"): void;\n}>();\n\nconst { t: cloudT, format: cloudFormat } = useCloudI18nStrict();\n</script>\n\n<template>\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n box-shadow: var(--tpl-shadow-md);\n border-bottom: 1px solid var(--tpl-border);\n \"\n >\n <!-- Left: Logo + template count -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-3\"\n >\n <span\n v-if=\"featureFlags.templateLimit.value !== null\"\n class=\"tpl:text-xs tpl:opacity-60 tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n cloudFormat(cloudT.header.templatesUsed, {\n used: featureFlags.templateCount.value,\n max: featureFlags.templateLimit.value,\n })\n }}\n </span>\n </div>\n\n <!-- Center: viewport + preview + dark mode + collaboration + snapshots -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <CollaboratorBar\n v-if=\"collaboration && isCollaborationEnabled\"\n :collaborators=\"collaboration.collaborators.value\"\n :is-connected=\"websocket.isConnected.value\"\n />\n <SnapshotHistory\n v-if=\"snapshotPreview.snapshotHistoryInstance.value\"\n :snapshots=\"snapshotPreview.snapshotHistorySnapshots.value\"\n :is-loading=\"snapshotPreview.snapshotHistoryIsLoading.value\"\n :is-restoring=\"snapshotPreview.snapshotHistoryIsRestoring.value\"\n @load=\"snapshotPreview.loadSnapshotHistory\"\n @navigate=\"snapshotPreview.handleSnapshotNavigate\"\n />\n </div>\n\n <!-- Right: Cloud actions -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Save status indicator -->\n <div\n v-if=\"featureFlags.saveStatus.value === 'error'\"\n aria-live=\"assertive\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"featureFlags.saveErrorMessage.value\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ cloudT.header.saveFailed }}\n </div>\n <div\n v-else-if=\"featureFlags.saveStatus.value === 'saved'\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ cloudT.header.saved }}\n </div>\n <div\n v-else-if=\"editor.state.isDirty\"\n aria-live=\"polite\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ cloudT.header.unsaved }}\n </div>\n\n <!-- Comments button -->\n <button\n v-if=\"\n commentsInstance.isEnabled.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :aria-label=\"\n commentsInstance.unresolvedCount.value > 0\n ? `${cloudT.comments.button} (${commentsInstance.unresolvedCount.value})`\n : cloudT.comments.button\n \"\n :aria-expanded=\"panelState.commentsOpen.value\"\n :class=\"headerBtnClass\"\n :style=\"{\n backgroundColor: panelState.commentsOpen.value\n ? 'var(--tpl-primary)'\n : 'transparent',\n color: panelState.commentsOpen.value\n ? 'var(--tpl-bg)'\n : 'var(--tpl-primary)',\n borderColor: 'var(--tpl-primary)',\n }\"\n @click=\"panelState.commentsOpen.value = !panelState.commentsOpen.value\"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.comments.button }}\n <span\n v-if=\"\n commentsInstance.unresolvedCount.value > 0 &&\n !panelState.commentsOpen.value\n \"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n >\n {{ commentsInstance.unresolvedCount.value }}\n </span>\n </button>\n\n <!-- AI button + menu -->\n <div\n v-if=\"\n featureFlags.canUseAiGeneration.value &&\n featureFlags.hasTemplateSaved.value\n \"\n :ref=\"(el) => (panelState.aiMenuRef.value = el as HTMLElement | null)\"\n class=\"tpl:relative\"\n >\n <button\n :aria-expanded=\"panelState.aiMenuOpen.value\"\n class=\"tpl-ai-btn tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-4 tpl:py-2 tpl:text-sm tpl:font-semibold tpl:whitespace-nowrap tpl:transition-all tpl:duration-200\"\n :class=\"\n panelState.aiButtonActive.value\n ? 'tpl-ai-btn--active'\n : 'tpl-ai-btn--idle'\n \"\n @click.stop=\"panelState.toggleAiMenu\"\n >\n <Sparkles :size=\"16\" :stroke-width=\"2\" class=\"tpl-ai-btn-icon\" />\n {{ cloudT.aiChat.button }}\n </button>\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-150 tpl:ease-out\"\n enter-from-class=\"tpl:scale-95 tpl:opacity-0\"\n enter-to-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-100 tpl:ease-in\"\n leave-from-class=\"tpl:scale-100 tpl:opacity-100\"\n leave-to-class=\"tpl:scale-95 tpl:opacity-0\"\n >\n <div\n v-if=\"panelState.aiMenuOpen.value\"\n class=\"tpl:absolute tpl:right-0 tpl:top-full tpl:z-50 tpl:mt-1 tpl:origin-top-right\"\n >\n <AiFeatureMenu\n :active-feature=\"panelState.activeAiFeature.value\"\n @select=\"panelState.handleAiFeatureSelect\"\n />\n </div>\n </Transition>\n </div>\n\n <!-- Test email button -->\n <button\n v-if=\"testEmail.isEnabled.value && featureFlags.canSendTestEmail.value\"\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"\n testEmail.isSending.value || !featureFlags.hasTemplateSaved.value\n \"\n @click=\"panelState.testEmailModalOpen.value = true\"\n >\n <Send v-if=\"!testEmail.isSending.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ cloudT.testEmail.button }}\n </button>\n\n <!-- Save button -->\n <button\n :class=\"headerBtnClass\"\n style=\"\n background-color: transparent;\n color: var(--tpl-primary);\n border-color: var(--tpl-primary);\n \"\n :disabled=\"isSaveDisabled\"\n @click=\"$emit('save')\"\n >\n <Save v-if=\"!isSaving\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ isSaving ? cloudT.header.saving : cloudT.header.save }}\n </button>\n </div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent, ref } from \"vue\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport type {\n UsePlanConfigReturn,\n UseTestEmailReturn,\n UseEditorReturn as CloudUseEditorReturn,\n} from \"@templatical/core/cloud\";\n\nimport type { UseEditorCoreReturn } from \"../../composables/useEditorCore\";\nimport type { UseCloudPanelStateReturn } from \"../composables/useCloudPanelState\";\nimport type { UseCloudMediaLibraryReturn } from \"../composables/useCloudMediaLibrary\";\nimport type { TemplaticalCloudEditorConfig } from \"../cloudConfig\";\n\nconst AiChatSidebar = defineAsyncComponent(() => import(\"./AiChatSidebar.vue\"));\nconst CommentsSidebar = defineAsyncComponent(\n () => import(\"./CommentsSidebar.vue\"),\n);\nconst DesignReferenceSidebar = defineAsyncComponent(\n () => import(\"./DesignReferenceSidebar.vue\"),\n);\nconst TemplateScoringPanel = defineAsyncComponent(\n () => import(\"./TemplateScoringPanel.vue\"),\n);\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\nconst MediaLibraryModal = defineAsyncComponent(async () => {\n // try/catch downgrades Webpack's \"Module not found\" from error to warning\n // when the optional peer isn't installed. Cloud consumers always install it.\n try {\n const m = await import(\"@templatical/media-library\");\n return m.MediaLibraryModal;\n } catch {\n throw new Error(\n \"[Templatical] Cloud media library requires the optional peer dependency '@templatical/media-library'. Please install it.\",\n );\n }\n});\n\ndefineProps<{\n config: TemplaticalCloudEditorConfig;\n editor: CloudUseEditorReturn;\n core: UseEditorCoreReturn;\n panelState: UseCloudPanelStateReturn;\n planConfigInstance: UsePlanConfigReturn;\n testEmail: UseTestEmailReturn;\n mediaLib: UseCloudMediaLibraryReturn;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send-test-email\", recipient: string): void;\n}>();\n\nfunction applyContent(\n content: TemplateContent,\n core: UseEditorCoreReturn,\n editor: CloudUseEditorReturn,\n): void {\n core.history.record();\n editor.setContent(content);\n core.conditionPreview.reset();\n}\n\ninterface CommentsSidebarInstance {\n filterByBlock: (blockId: string) => void;\n}\n\nconst commentsSidebar = ref<CommentsSidebarInstance | null>(null);\n\n/** Delegates to the CommentsSidebar's filterByBlock method once it's mounted. */\nfunction filterCommentsByBlock(blockId: string): void {\n commentsSidebar.value?.filterByBlock(blockId);\n}\n\ndefineExpose({ filterCommentsByBlock });\n</script>\n\n<template>\n <AiChatSidebar\n :visible=\"panelState.aiChatOpen.value\"\n :on-apply=\"(c: TemplateContent) => applyContent(c, core, editor)\"\n @close=\"panelState.aiChatOpen.value = false\"\n />\n\n <TemplateScoringPanel\n :visible=\"panelState.scoringPanelOpen.value\"\n @close=\"panelState.scoringPanelOpen.value = false\"\n />\n\n <DesignReferenceSidebar\n :visible=\"panelState.designReferenceOpen.value\"\n :has-existing-blocks=\"editor.content.value.blocks.length > 0\"\n @close=\"panelState.designReferenceOpen.value = false\"\n @apply=\"(c: TemplateContent) => applyContent(c, core, editor)\"\n />\n\n <CommentsSidebar\n ref=\"commentsSidebar\"\n :visible=\"panelState.commentsOpen.value\"\n @close=\"panelState.commentsOpen.value = false\"\n />\n\n <TestEmailModal\n :visible=\"panelState.testEmailModalOpen.value\"\n :allowed-emails=\"testEmail.allowedEmails.value\"\n :is-sending=\"testEmail.isSending.value\"\n :error=\"testEmail.error.value\"\n @send=\"(recipient: string) => emit('send-test-email', recipient)\"\n @close=\"panelState.testEmailModalOpen.value = false\"\n />\n\n <MediaLibraryModal\n :visible=\"panelState.mediaLibraryOpen.value\"\n :accept=\"panelState.mediaLibraryAccept.value\"\n :popover-target=\"core.popoverRoot.value\"\n @select=\"mediaLib.handleMediaSelect\"\n @close=\"mediaLib.handleMediaLibraryClose\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent, ref } from \"vue\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport type {\n UsePlanConfigReturn,\n UseTestEmailReturn,\n UseEditorReturn as CloudUseEditorReturn,\n} from \"@templatical/core/cloud\";\n\nimport type { UseEditorCoreReturn } from \"../../composables/useEditorCore\";\nimport type { UseCloudPanelStateReturn } from \"../composables/useCloudPanelState\";\nimport type { UseCloudMediaLibraryReturn } from \"../composables/useCloudMediaLibrary\";\nimport type { TemplaticalCloudEditorConfig } from \"../cloudConfig\";\n\nconst AiChatSidebar = defineAsyncComponent(() => import(\"./AiChatSidebar.vue\"));\nconst CommentsSidebar = defineAsyncComponent(\n () => import(\"./CommentsSidebar.vue\"),\n);\nconst DesignReferenceSidebar = defineAsyncComponent(\n () => import(\"./DesignReferenceSidebar.vue\"),\n);\nconst TemplateScoringPanel = defineAsyncComponent(\n () => import(\"./TemplateScoringPanel.vue\"),\n);\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\nconst MediaLibraryModal = defineAsyncComponent(async () => {\n // try/catch downgrades Webpack's \"Module not found\" from error to warning\n // when the optional peer isn't installed. Cloud consumers always install it.\n try {\n const m = await import(\"@templatical/media-library\");\n return m.MediaLibraryModal;\n } catch {\n throw new Error(\n \"[Templatical] Cloud media library requires the optional peer dependency '@templatical/media-library'. Please install it.\",\n );\n }\n});\n\ndefineProps<{\n config: TemplaticalCloudEditorConfig;\n editor: CloudUseEditorReturn;\n core: UseEditorCoreReturn;\n panelState: UseCloudPanelStateReturn;\n planConfigInstance: UsePlanConfigReturn;\n testEmail: UseTestEmailReturn;\n mediaLib: UseCloudMediaLibraryReturn;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send-test-email\", recipient: string): void;\n}>();\n\nfunction applyContent(\n content: TemplateContent,\n core: UseEditorCoreReturn,\n editor: CloudUseEditorReturn,\n): void {\n core.history.record();\n editor.setContent(content);\n core.conditionPreview.reset();\n}\n\ninterface CommentsSidebarInstance {\n filterByBlock: (blockId: string) => void;\n}\n\nconst commentsSidebar = ref<CommentsSidebarInstance | null>(null);\n\n/** Delegates to the CommentsSidebar's filterByBlock method once it's mounted. */\nfunction filterCommentsByBlock(blockId: string): void {\n commentsSidebar.value?.filterByBlock(blockId);\n}\n\ndefineExpose({ filterCommentsByBlock });\n</script>\n\n<template>\n <AiChatSidebar\n :visible=\"panelState.aiChatOpen.value\"\n :on-apply=\"(c: TemplateContent) => applyContent(c, core, editor)\"\n @close=\"panelState.aiChatOpen.value = false\"\n />\n\n <TemplateScoringPanel\n :visible=\"panelState.scoringPanelOpen.value\"\n @close=\"panelState.scoringPanelOpen.value = false\"\n />\n\n <DesignReferenceSidebar\n :visible=\"panelState.designReferenceOpen.value\"\n :has-existing-blocks=\"editor.content.value.blocks.length > 0\"\n @close=\"panelState.designReferenceOpen.value = false\"\n @apply=\"(c: TemplateContent) => applyContent(c, core, editor)\"\n />\n\n <CommentsSidebar\n ref=\"commentsSidebar\"\n :visible=\"panelState.commentsOpen.value\"\n @close=\"panelState.commentsOpen.value = false\"\n />\n\n <TestEmailModal\n :visible=\"panelState.testEmailModalOpen.value\"\n :allowed-emails=\"testEmail.allowedEmails.value\"\n :is-sending=\"testEmail.isSending.value\"\n :error=\"testEmail.error.value\"\n @send=\"(recipient: string) => emit('send-test-email', recipient)\"\n @close=\"panelState.testEmailModalOpen.value = false\"\n />\n\n <MediaLibraryModal\n :visible=\"panelState.mediaLibraryOpen.value\"\n :accept=\"panelState.mediaLibraryAccept.value\"\n :popover-target=\"core.popoverRoot.value\"\n @select=\"mediaLib.handleMediaSelect\"\n @close=\"mediaLib.handleMediaLibraryClose\"\n />\n</template>\n","<script setup lang=\"ts\">\ndefineProps<{\n visible: boolean;\n}>();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-loading tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Skeleton header -->\n <div\n class=\"tpl:flex tpl:h-14 tpl:shrink-0 tpl:items-center tpl:justify-between tpl:px-4 tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-5 tpl:w-28 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl:flex tpl:gap-3\">\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n </div>\n <!-- Skeleton body -->\n <div class=\"tpl:flex tpl:flex-1 tpl:overflow-hidden\">\n <!-- Left sidebar rail -->\n <div\n class=\"tpl:flex tpl:w-12 tpl:shrink-0 tpl:flex-col tpl:items-center tpl:gap-4 tpl:py-5 tpl:border-r tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-for=\"n in 5\"\n :key=\"n\"\n class=\"tpl-shimmer tpl:size-7 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <!-- Canvas area -->\n <div\n class=\"tpl:flex tpl:flex-1 tpl:items-start tpl:justify-center tpl:overflow-auto tpl:p-8 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <div\n class=\"tpl:w-full tpl:max-w-[600px] tpl:rounded-[var(--tpl-radius)] tpl:p-6 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)]\"\n >\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-3/4 tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-5/6 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-44 tpl:w-full tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-2/3 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-10 tpl:w-36 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/2 tpl:rounded\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/3 tpl:rounded\"\n ></div>\n </div>\n </div>\n </div>\n <!-- Right panel -->\n <div\n class=\"tpl:flex tpl:w-[320px] tpl:shrink-0 tpl:flex-col tpl:gap-4 tpl:p-4 tpl:border-l tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\ndefineProps<{\n visible: boolean;\n}>();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-loading tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:bg-[var(--tpl-bg)]\"\n >\n <!-- Skeleton header -->\n <div\n class=\"tpl:flex tpl:h-14 tpl:shrink-0 tpl:items-center tpl:justify-between tpl:px-4 tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-5 tpl:w-28 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl:flex tpl:gap-3\">\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:w-20 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n </div>\n <!-- Skeleton body -->\n <div class=\"tpl:flex tpl:flex-1 tpl:overflow-hidden\">\n <!-- Left sidebar rail -->\n <div\n class=\"tpl:flex tpl:w-12 tpl:shrink-0 tpl:flex-col tpl:items-center tpl:gap-4 tpl:py-5 tpl:border-r tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-for=\"n in 5\"\n :key=\"n\"\n class=\"tpl-shimmer tpl:size-7 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <!-- Canvas area -->\n <div\n class=\"tpl:flex tpl:flex-1 tpl:items-start tpl:justify-center tpl:overflow-auto tpl:p-8 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <div\n class=\"tpl:w-full tpl:max-w-[600px] tpl:rounded-[var(--tpl-radius)] tpl:p-6 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)]\"\n >\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-3/4 tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-5/6 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-44 tpl:w-full tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-full tpl:rounded\"></div>\n <div class=\"tpl-shimmer tpl:h-3 tpl:w-2/3 tpl:rounded\"></div>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:h-10 tpl:w-36 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n </div>\n <div class=\"tpl:space-y-2 tpl:py-4\">\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/2 tpl:rounded\"\n ></div>\n <div\n class=\"tpl-shimmer tpl:mx-auto tpl:h-2.5 tpl:w-1/3 tpl:rounded\"\n ></div>\n </div>\n </div>\n </div>\n <!-- Right panel -->\n <div\n class=\"tpl:flex tpl:w-[320px] tpl:shrink-0 tpl:flex-col tpl:gap-4 tpl:p-4 tpl:border-l tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl-shimmer tpl:h-8 tpl:rounded-[var(--tpl-radius-sm)]\"\n ></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n <div class=\"tpl-shimmer tpl:h-32 tpl:rounded-[var(--tpl-radius)]\"></div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { CircleAlert } from \"@lucide/vue\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\n\ndefineProps<{\n error: Error | null;\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"retry\"): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n\nfunction getErrorMessage(error: Error): string {\n if (\n \"isUnauthorized\" in error &&\n (error as { isUnauthorized: boolean }).isUnauthorized\n ) {\n return cloudT.error.authFailed;\n }\n if (\"isNotFound\" in error && (error as { isNotFound: boolean }).isNotFound) {\n return cloudT.error.templateNotFound;\n }\n return cloudT.error.defaultMessage;\n}\n\nfunction isNotFoundError(error: Error): boolean {\n return (\n \"isNotFound\" in error && !!(error as { isNotFound: boolean }).isNotFound\n );\n}\n</script>\n\n<template>\n <div\n v-if=\"visible && error\"\n role=\"alert\"\n class=\"tpl-error tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-6 tpl:px-8 tpl:bg-[var(--tpl-bg)]\"\n >\n <div\n class=\"tpl:flex tpl:size-16 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-[var(--tpl-danger-light)]\"\n >\n <CircleAlert\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n />\n </div>\n <div\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-2 tpl:text-center\"\n >\n <h2 class=\"tpl:text-lg tpl:font-semibold tpl:text-[var(--tpl-text)]\">\n {{ cloudT.error.title }}\n </h2>\n <p class=\"tpl:max-w-md tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ getErrorMessage(error) }}\n </p>\n </div>\n <button\n v-if=\"!isNotFoundError(error)\"\n class=\"tpl-btn tpl-btn-primary tpl:inline-flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('retry')\"\n >\n {{ cloudT.error.retry }}\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { CircleAlert } from \"@lucide/vue\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\n\ndefineProps<{\n error: Error | null;\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"retry\"): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n\nfunction getErrorMessage(error: Error): string {\n if (\n \"isUnauthorized\" in error &&\n (error as { isUnauthorized: boolean }).isUnauthorized\n ) {\n return cloudT.error.authFailed;\n }\n if (\"isNotFound\" in error && (error as { isNotFound: boolean }).isNotFound) {\n return cloudT.error.templateNotFound;\n }\n return cloudT.error.defaultMessage;\n}\n\nfunction isNotFoundError(error: Error): boolean {\n return (\n \"isNotFound\" in error && !!(error as { isNotFound: boolean }).isNotFound\n );\n}\n</script>\n\n<template>\n <div\n v-if=\"visible && error\"\n role=\"alert\"\n class=\"tpl-error tpl:absolute tpl:inset-0 tpl:z-overlay tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-6 tpl:px-8 tpl:bg-[var(--tpl-bg)]\"\n >\n <div\n class=\"tpl:flex tpl:size-16 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-[var(--tpl-danger-light)]\"\n >\n <CircleAlert\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n />\n </div>\n <div\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-2 tpl:text-center\"\n >\n <h2 class=\"tpl:text-lg tpl:font-semibold tpl:text-[var(--tpl-text)]\">\n {{ cloudT.error.title }}\n </h2>\n <p class=\"tpl:max-w-md tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ getErrorMessage(error) }}\n </p>\n </div>\n <button\n v-if=\"!isNotFoundError(error)\"\n class=\"tpl-btn tpl-btn-primary tpl:inline-flex tpl:items-center tpl:gap-2 tpl:rounded-md tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('retry')\"\n >\n {{ cloudT.error.retry }}\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Clock } from \"@lucide/vue\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"cancel\"): void;\n (e: \"confirm\"): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-preview-banner tpl:absolute tpl:top-14 tpl:right-0 tpl:left-0 tpl:z-40 tpl:flex tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-4 tpl:py-3 tpl:bg-[var(--tpl-primary-light)] tpl:border-b tpl:border-[var(--tpl-primary)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:text-[var(--tpl-text)]\"\n >\n <Clock\n :size=\"18\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n />\n <span>{{ cloudT.snapshotPreview.message }}</span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"emit('cancel')\"\n >\n {{ cloudT.snapshotPreview.cancel }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('confirm')\"\n >\n {{ cloudT.snapshotPreview.restore }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Clock } from \"@lucide/vue\";\nimport { useCloudI18nStrict } from \"../../composables/useCloudI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"cancel\"): void;\n (e: \"confirm\"): void;\n}>();\n\nconst { t: cloudT } = useCloudI18nStrict();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n class=\"tpl-preview-banner tpl:absolute tpl:top-14 tpl:right-0 tpl:left-0 tpl:z-40 tpl:flex tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-4 tpl:py-3 tpl:bg-[var(--tpl-primary-light)] tpl:border-b tpl:border-[var(--tpl-primary)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:text-[var(--tpl-text)]\"\n >\n <Clock\n :size=\"18\"\n :stroke-width=\"2\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n />\n <span>{{ cloudT.snapshotPreview.message }}</span>\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:text-[var(--tpl-text-muted)] tpl:border tpl:border-[var(--tpl-border)]\"\n style=\"background-color: transparent\"\n @click=\"emit('cancel')\"\n >\n {{ cloudT.snapshotPreview.cancel }}\n </button>\n <button\n class=\"tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n @click=\"emit('confirm')\"\n >\n {{ cloudT.snapshotPreview.restore }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n role=\"status\"\n aria-live=\"polite\"\n class=\"tpl:absolute tpl:top-16 tpl:left-1/2 tpl:z-toast tpl:-translate-x-1/2 tpl:rounded-[var(--tpl-radius)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:shadow-lg\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-text);\n border: 1px solid var(--tpl-warning);\n \"\n >\n {{ t.history.collabWarning }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\n\ndefineProps<{\n visible: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n role=\"status\"\n aria-live=\"polite\"\n class=\"tpl:absolute tpl:top-16 tpl:left-1/2 tpl:z-toast tpl:-translate-x-1/2 tpl:rounded-[var(--tpl-radius)] tpl:px-4 tpl:py-2.5 tpl:text-sm tpl:shadow-lg\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-text);\n border: 1px solid var(--tpl-warning);\n \"\n >\n {{ t.history.collabWarning }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { TemplateContent } from \"@templatical/types\";\n\nimport type { UseFontsReturn } from \"../composables/useFonts\";\nimport { defineAsyncComponent, onMounted, onUnmounted, ref } from \"vue\";\nimport { RotateCcw } from \"@lucide/vue\";\nimport type { Translations, CloudTranslations } from \"../i18n\";\nimport { provide } from \"vue\";\nimport { CLOUD_TRANSLATIONS_KEY } from \"../keys\";\n\nimport { useSmallScreenNotice } from \"../composables/useSmallScreenNotice\";\nimport { useCloudInitialization } from \"./composables/useCloudInitialization\";\nimport { useCloudLifecycle } from \"./composables/useCloudLifecycle\";\nimport { useCloudSaveGate } from \"./composables/useCloudSaveGate\";\nimport CloudSaveGateModal from \"./components/CloudSaveGateModal.vue\";\nimport CloudHeader from \"./components/CloudHeader.vue\";\nimport CloudPanels from \"./components/CloudPanels.vue\";\nimport EditorFooter from \"../components/EditorFooter.vue\";\n\nimport Canvas from \"../components/Canvas.vue\";\nimport CustomBlockStylesheets from \"../components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"../components/Sidebar.vue\";\nimport RightSidebar from \"../components/RightSidebar.vue\";\nimport SmallScreenNotice from \"../components/SmallScreenNotice.vue\";\nimport CloudLoadingOverlay from \"./components/CloudLoadingOverlay.vue\";\nimport CloudErrorOverlay from \"./components/CloudErrorOverlay.vue\";\nimport SnapshotPreviewBanner from \"./components/SnapshotPreviewBanner.vue\";\nimport CollabUndoToast from \"./components/CollabUndoToast.vue\";\nimport MergeTagPickerModal from \"../components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"../components/LogicTagPickerModal.vue\";\nimport \"../styles/index.css\";\n\n// Lazy so the saved-blocks UI stays out of the initial cloud chunk; the inner\n// dialogs are lazy in turn and load only when opened.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"../components/SavedBlocksPanels.vue\"),\n);\n\nexport type { TemplaticalCloudEditorConfig } from \"./cloudConfig\";\nimport type { TemplaticalCloudEditorConfig } from \"./cloudConfig\";\n\nconst props = defineProps<{\n config: TemplaticalCloudEditorConfig;\n translations: Translations;\n cloudTranslations: CloudTranslations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the cloud editor is mounted into. Supplied by `initCloud()`\n * when `shadowDom: true`; undefined in light-DOM mode. Plumbed through\n * `useCloudInitialization` → `useEditorCore`.\n */\n shadowRoot?: ShadowRoot;\n}>();\n\nprovide(CLOUD_TRANSLATIONS_KEY, props.cloudTranslations);\nconst emit = defineEmits<{\n (e: \"ready\"): void;\n}>();\n\n// Template ref for CloudPanels. Its `filterCommentsByBlock()` method is what\n// init.openCommentsForBlock() bridges to once the panel mounts.\nconst cloudPanelsRef = ref<{\n filterCommentsByBlock: (blockId: string) => void;\n} | null>(null);\n\n// Outer `.tpl` ref forwarded to `useEditorCore` (via `useCloudInitialization`)\n// for multi-editor keyboard routing.\nconst rootEl = ref<HTMLElement | null>(null);\n\nconst init = useCloudInitialization({\n config: props.config,\n translations: props.translations,\n fontsManager: props.fontsManager,\n emit,\n getCommentsSidebar: () =>\n cloudPanelsRef.value\n ? { filterByBlock: cloudPanelsRef.value.filterCommentsByBlock }\n : null,\n editorRoot: props.shadowRoot,\n containerEl: rootEl,\n});\n\n// Destructure heavily-used members for template readability.\nconst {\n isInitializing,\n isAuthReady,\n initError,\n planConfigInstance,\n websocket,\n collaboration,\n isCollaborationEnabled,\n editor,\n core,\n featureFlags,\n mediaLib,\n exporter,\n testEmail,\n commentsInstance,\n savedBlocks,\n panelState,\n snapshotPreview,\n collabWarning,\n setThemeOverrides,\n setUiTheme,\n} = init;\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`. The cloud session\n// (auth/websocket) stays connected behind the notice, so rotating back to a\n// wide viewport restores the live editor without re-initializing.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// ---------------------------------------------------------------------------\n// Test email handler\n// ---------------------------------------------------------------------------\n\nasync function handleSendTestEmail(recipient: string): Promise<void> {\n try {\n await testEmail.sendTestEmail(recipient);\n panelState.testEmailModalOpen.value = false;\n } catch {\n // Error is already handled in the composable\n }\n}\n\nasync function handleConfirmRestoreSnapshot(): Promise<void> {\n try {\n await snapshotPreview.confirmRestoreSnapshot();\n } catch {\n // Restore failure is already surfaced via onError, and the composable\n // rolls the editor content back to the pre-preview state. Swallow here so\n // the rejected promise from the event binding isn't an unhandled rejection.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Template lifecycle (create/load/save)\n// ---------------------------------------------------------------------------\n\nconst lifecycle = useCloudLifecycle({\n config: props.config,\n editor,\n websocket,\n planConfigInstance,\n snapshotPreview,\n core,\n exporter,\n featureFlags,\n isDestroyed: init.isDestroyed,\n});\n\n// --- Lint save-gate ---\nconst saveGate = useCloudSaveGate({\n issues: core.templateLint ? core.templateLint.issues : ref([]),\n planConfig: planConfigInstance.config,\n});\n\nasync function gatedSave(): Promise<void> {\n await saveGate.tryRunSave(() =>\n lifecycle.saveTemplate().catch((err: Error) => props.config.onError?.(err)),\n );\n}\n\n// Route the keyboard Save hook through the gate so Cmd+S also triggers the\n// modal when the plan policy says so.\ninit.onSaveHook.value = gatedSave;\n\n// ---------------------------------------------------------------------------\n// Lifecycle\n// ---------------------------------------------------------------------------\n\nonMounted(() => {\n init.initialize();\n});\n\nonUnmounted(() => {\n init.destroy();\n});\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: setUiTheme,\n setThemeOverrides: setThemeOverrides,\n create: lifecycle.createTemplate,\n load: lifecycle.loadTemplate,\n save: lifecycle.saveTemplate,\n sendTestEmail: testEmail.sendTestEmail,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Loading overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-100\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudLoadingOverlay\n :visible=\"isInitializing || editor.state.isLoading\"\n />\n </Transition>\n\n <!-- Error overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudErrorOverlay\n :error=\"initError\"\n :visible=\"!!initError && !isInitializing\"\n @retry=\"init.initialize\"\n />\n </Transition>\n\n <CloudHeader\n :editor=\"editor\"\n :core=\"core\"\n :feature-flags=\"featureFlags\"\n :panel-state=\"panelState\"\n :snapshot-preview=\"snapshotPreview\"\n :comments-instance=\"commentsInstance\"\n :test-email=\"testEmail\"\n :websocket=\"websocket\"\n :collaboration=\"collaboration\"\n :is-collaboration-enabled=\"isCollaborationEnabled\"\n :is-saving=\"editor.state.isSaving || featureFlags.isSaveExporting.value\"\n :is-save-disabled=\"\n editor.state.isSaving ||\n featureFlags.isSaveExporting.value ||\n !editor.state.isDirty\n \"\n @save=\"gatedSave\"\n />\n\n <CloudSaveGateModal\n :open=\"saveGate.modalOpen.value\"\n :issues=\"saveGate.blockingIssues.value\"\n @cancel=\"saveGate.cancel\"\n @confirm=\"saveGate.confirmAndSave\"\n />\n\n <!-- Snapshot preview banner -->\n <SnapshotPreviewBanner\n :visible=\"snapshotPreview.isPreviewingSnapshot.value\"\n @cancel=\"snapshotPreview.cancelPreview\"\n @confirm=\"handleConfirmRestoreSnapshot\"\n />\n\n <!-- Collaboration undo warning toast -->\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-200 tpl:ease-out\"\n enter-from-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n enter-to-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-300 tpl:ease-in\"\n leave-from-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-to-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n >\n <CollabUndoToast\n :visible=\"collabWarning.collabUndoWarningVisible.value\"\n />\n </Transition>\n\n <!-- Left sidebar -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas body -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto\"\n style=\"\n transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);\n background-color: var(--tpl-canvas-bg);\n \"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n snapshotPreview.isPreviewingSnapshot.value\n ? 'tpl:top-[104px]'\n : 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-warning);\n border-color: var(--tpl-warning);\n backdrop-filter: blur(8px);\n \"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"collaboration?.lockedBlocks.value ?? undefined\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"panelState.aiChatOpen.value = true\"\n @open-design-reference=\"panelState.designReferenceOpen.value = true\"\n />\n </main>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false && !featureFlags.isWhiteLabeled.value\"\n :position-class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"panelState.rightPanelOpen.value\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.selectedBlock.value!.id, updates)\n \"\n @delete-block=\"\n core.blockActions.deleteBlock(editor.selectedBlock.value!.id)\n \"\n @duplicate-block=\"\n core.blockActions.duplicateBlock(editor.selectedBlock.value!)\n \"\n @update-settings=\"editor.updateSettings\"\n />\n\n <!-- Cloud sidebars + modals — only mount after cloud init completes -->\n <CloudPanels\n v-if=\"!isInitializing && isAuthReady\"\n ref=\"cloudPanelsRef\"\n :config=\"props.config\"\n :editor=\"editor\"\n :core=\"core\"\n :panel-state=\"panelState\"\n :plan-config-instance=\"planConfigInstance\"\n :test-email=\"testEmail\"\n :media-lib=\"mediaLib\"\n @send-test-email=\"handleSendTestEmail\"\n />\n\n <!-- Saved blocks dialogs. Shared with the OSS editor; each dialog's chunk\n loads on first open, and nothing mounts unless the plan allows it. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modal.\n Replaces the historical body-level teleport pattern so popups\n render inside the editor's effective DOM root (shadow-aware). -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","<script setup lang=\"ts\">\nimport type { TemplateContent } from \"@templatical/types\";\n\nimport type { UseFontsReturn } from \"../composables/useFonts\";\nimport { defineAsyncComponent, onMounted, onUnmounted, ref } from \"vue\";\nimport { RotateCcw } from \"@lucide/vue\";\nimport type { Translations, CloudTranslations } from \"../i18n\";\nimport { provide } from \"vue\";\nimport { CLOUD_TRANSLATIONS_KEY } from \"../keys\";\n\nimport { useSmallScreenNotice } from \"../composables/useSmallScreenNotice\";\nimport { useCloudInitialization } from \"./composables/useCloudInitialization\";\nimport { useCloudLifecycle } from \"./composables/useCloudLifecycle\";\nimport { useCloudSaveGate } from \"./composables/useCloudSaveGate\";\nimport CloudSaveGateModal from \"./components/CloudSaveGateModal.vue\";\nimport CloudHeader from \"./components/CloudHeader.vue\";\nimport CloudPanels from \"./components/CloudPanels.vue\";\nimport EditorFooter from \"../components/EditorFooter.vue\";\n\nimport Canvas from \"../components/Canvas.vue\";\nimport CustomBlockStylesheets from \"../components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"../components/Sidebar.vue\";\nimport RightSidebar from \"../components/RightSidebar.vue\";\nimport SmallScreenNotice from \"../components/SmallScreenNotice.vue\";\nimport CloudLoadingOverlay from \"./components/CloudLoadingOverlay.vue\";\nimport CloudErrorOverlay from \"./components/CloudErrorOverlay.vue\";\nimport SnapshotPreviewBanner from \"./components/SnapshotPreviewBanner.vue\";\nimport CollabUndoToast from \"./components/CollabUndoToast.vue\";\nimport MergeTagPickerModal from \"../components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"../components/LogicTagPickerModal.vue\";\nimport \"../styles/index.css\";\n\n// Lazy so the saved-blocks UI stays out of the initial cloud chunk; the inner\n// dialogs are lazy in turn and load only when opened.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"../components/SavedBlocksPanels.vue\"),\n);\n\nexport type { TemplaticalCloudEditorConfig } from \"./cloudConfig\";\nimport type { TemplaticalCloudEditorConfig } from \"./cloudConfig\";\n\nconst props = defineProps<{\n config: TemplaticalCloudEditorConfig;\n translations: Translations;\n cloudTranslations: CloudTranslations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the cloud editor is mounted into. Supplied by `initCloud()`\n * when `shadowDom: true`; undefined in light-DOM mode. Plumbed through\n * `useCloudInitialization` → `useEditorCore`.\n */\n shadowRoot?: ShadowRoot;\n}>();\n\nprovide(CLOUD_TRANSLATIONS_KEY, props.cloudTranslations);\nconst emit = defineEmits<{\n (e: \"ready\"): void;\n}>();\n\n// Template ref for CloudPanels. Its `filterCommentsByBlock()` method is what\n// init.openCommentsForBlock() bridges to once the panel mounts.\nconst cloudPanelsRef = ref<{\n filterCommentsByBlock: (blockId: string) => void;\n} | null>(null);\n\n// Outer `.tpl` ref forwarded to `useEditorCore` (via `useCloudInitialization`)\n// for multi-editor keyboard routing.\nconst rootEl = ref<HTMLElement | null>(null);\n\nconst init = useCloudInitialization({\n config: props.config,\n translations: props.translations,\n fontsManager: props.fontsManager,\n emit,\n getCommentsSidebar: () =>\n cloudPanelsRef.value\n ? { filterByBlock: cloudPanelsRef.value.filterCommentsByBlock }\n : null,\n editorRoot: props.shadowRoot,\n containerEl: rootEl,\n});\n\n// Destructure heavily-used members for template readability.\nconst {\n isInitializing,\n isAuthReady,\n initError,\n planConfigInstance,\n websocket,\n collaboration,\n isCollaborationEnabled,\n editor,\n core,\n featureFlags,\n mediaLib,\n exporter,\n testEmail,\n commentsInstance,\n savedBlocks,\n panelState,\n snapshotPreview,\n collabWarning,\n setThemeOverrides,\n setUiTheme,\n} = init;\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`. The cloud session\n// (auth/websocket) stays connected behind the notice, so rotating back to a\n// wide viewport restores the live editor without re-initializing.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// ---------------------------------------------------------------------------\n// Test email handler\n// ---------------------------------------------------------------------------\n\nasync function handleSendTestEmail(recipient: string): Promise<void> {\n try {\n await testEmail.sendTestEmail(recipient);\n panelState.testEmailModalOpen.value = false;\n } catch {\n // Error is already handled in the composable\n }\n}\n\nasync function handleConfirmRestoreSnapshot(): Promise<void> {\n try {\n await snapshotPreview.confirmRestoreSnapshot();\n } catch {\n // Restore failure is already surfaced via onError, and the composable\n // rolls the editor content back to the pre-preview state. Swallow here so\n // the rejected promise from the event binding isn't an unhandled rejection.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Template lifecycle (create/load/save)\n// ---------------------------------------------------------------------------\n\nconst lifecycle = useCloudLifecycle({\n config: props.config,\n editor,\n websocket,\n planConfigInstance,\n snapshotPreview,\n core,\n exporter,\n featureFlags,\n isDestroyed: init.isDestroyed,\n});\n\n// --- Lint save-gate ---\nconst saveGate = useCloudSaveGate({\n issues: core.templateLint ? core.templateLint.issues : ref([]),\n planConfig: planConfigInstance.config,\n});\n\nasync function gatedSave(): Promise<void> {\n await saveGate.tryRunSave(() =>\n lifecycle.saveTemplate().catch((err: Error) => props.config.onError?.(err)),\n );\n}\n\n// Route the keyboard Save hook through the gate so Cmd+S also triggers the\n// modal when the plan policy says so.\ninit.onSaveHook.value = gatedSave;\n\n// ---------------------------------------------------------------------------\n// Lifecycle\n// ---------------------------------------------------------------------------\n\nonMounted(() => {\n init.initialize();\n});\n\nonUnmounted(() => {\n init.destroy();\n});\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: setUiTheme,\n setThemeOverrides: setThemeOverrides,\n create: lifecycle.createTemplate,\n load: lifecycle.loadTemplate,\n save: lifecycle.saveTemplate,\n sendTestEmail: testEmail.sendTestEmail,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Loading overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-100\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudLoadingOverlay\n :visible=\"isInitializing || editor.state.isLoading\"\n />\n </Transition>\n\n <!-- Error overlay -->\n <Transition\n enter-active-class=\"tpl:transition-opacity tpl:duration-200\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition-opacity tpl:duration-300\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <CloudErrorOverlay\n :error=\"initError\"\n :visible=\"!!initError && !isInitializing\"\n @retry=\"init.initialize\"\n />\n </Transition>\n\n <CloudHeader\n :editor=\"editor\"\n :core=\"core\"\n :feature-flags=\"featureFlags\"\n :panel-state=\"panelState\"\n :snapshot-preview=\"snapshotPreview\"\n :comments-instance=\"commentsInstance\"\n :test-email=\"testEmail\"\n :websocket=\"websocket\"\n :collaboration=\"collaboration\"\n :is-collaboration-enabled=\"isCollaborationEnabled\"\n :is-saving=\"editor.state.isSaving || featureFlags.isSaveExporting.value\"\n :is-save-disabled=\"\n editor.state.isSaving ||\n featureFlags.isSaveExporting.value ||\n !editor.state.isDirty\n \"\n @save=\"gatedSave\"\n />\n\n <CloudSaveGateModal\n :open=\"saveGate.modalOpen.value\"\n :issues=\"saveGate.blockingIssues.value\"\n @cancel=\"saveGate.cancel\"\n @confirm=\"saveGate.confirmAndSave\"\n />\n\n <!-- Snapshot preview banner -->\n <SnapshotPreviewBanner\n :visible=\"snapshotPreview.isPreviewingSnapshot.value\"\n @cancel=\"snapshotPreview.cancelPreview\"\n @confirm=\"handleConfirmRestoreSnapshot\"\n />\n\n <!-- Collaboration undo warning toast -->\n <Transition\n enter-active-class=\"tpl:transition-all tpl:duration-200 tpl:ease-out\"\n enter-from-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n enter-to-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-active-class=\"tpl:transition-all tpl:duration-300 tpl:ease-in\"\n leave-from-class=\"tpl:translate-y-0 tpl:opacity-100\"\n leave-to-class=\"tpl:translate-y-[-8px] tpl:opacity-0\"\n >\n <CollabUndoToast\n :visible=\"collabWarning.collabUndoWarningVisible.value\"\n />\n </Transition>\n\n <!-- Left sidebar -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas body -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto\"\n style=\"\n transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);\n background-color: var(--tpl-canvas-bg);\n \"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n snapshotPreview.isPreviewingSnapshot.value\n ? 'tpl:top-[104px]'\n : 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80\"\n style=\"\n background-color: var(--tpl-warning-light);\n color: var(--tpl-warning);\n border-color: var(--tpl-warning);\n backdrop-filter: blur(8px);\n \"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"collaboration?.lockedBlocks.value ?? undefined\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"panelState.aiChatOpen.value = true\"\n @open-design-reference=\"panelState.designReferenceOpen.value = true\"\n />\n </main>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false && !featureFlags.isWhiteLabeled.value\"\n :position-class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : panelState.rightPanelOpen.value\n ? 'tpl:left-12 tpl:right-[680px]'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"panelState.rightPanelOpen.value\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.selectedBlock.value!.id, updates)\n \"\n @delete-block=\"\n core.blockActions.deleteBlock(editor.selectedBlock.value!.id)\n \"\n @duplicate-block=\"\n core.blockActions.duplicateBlock(editor.selectedBlock.value!)\n \"\n @update-settings=\"editor.updateSettings\"\n />\n\n <!-- Cloud sidebars + modals — only mount after cloud init completes -->\n <CloudPanels\n v-if=\"!isInitializing && isAuthReady\"\n ref=\"cloudPanelsRef\"\n :config=\"props.config\"\n :editor=\"editor\"\n :core=\"core\"\n :panel-state=\"panelState\"\n :plan-config-instance=\"planConfigInstance\"\n :test-email=\"testEmail\"\n :media-lib=\"mediaLib\"\n @send-test-email=\"handleSendTestEmail\"\n />\n\n <!-- Saved blocks dialogs. Shared with the OSS editor; each dialog's chunk\n loads on first open, and nothing mounts unless the plan allows it. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modal.\n Replaces the historical body-level teleport pattern so popups\n render inside the editor's effective DOM root (shadow-aware). -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SaveBlockDialog-CSeD3gn9.js","names":[],"sources":["../../../src/components/SavedBlockPreviewRow.vue","../../../src/components/SavedBlockPreviewRow.vue","../../../src/components/SaveBlockDialog.vue","../../../src/components/SaveBlockDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * One reorderable row in the save dialog's preview list: a grip handle beside a\n * scaled-down render of the real block.\n *\n * The preview is the same `SavedBlockPreviewCanvas` the browser modal uses, so\n * a row shows what the block actually looks like rather than a description of\n * it. That canvas is fixed at the 600px email width every block component\n * renders against, so the row shrinks it with `transform: scale()` instead of\n * re-flowing it at dialog width — a re-flow would show a layout the recipient\n * will never see.\n *\n * `transform` doesn't participate in layout, so the frame would keep reserving\n * the full unscaled height; a `ResizeObserver` mirrors the scaled height back\n * onto it. The handle is a sibling of the scaled subtree, so it stays full size\n * and keeps its real hit area.\n *\n * Row chrome uses the raw theme tokens, not the `--tpl-chrome-*` aliases: the\n * chrome is an ancestor and a sibling of the rendered email content, never a\n * descendant, so the `.tpl-block-content` token override can't reach it.\n */\nimport SavedBlockPreviewCanvas from \"./SavedBlockPreviewCanvas.vue\";\nimport { useI18n } from \"../composables\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY } from \"../keys\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { ChevronDown, ChevronUp, GripVertical } from \"@lucide/vue\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, onBeforeUnmount, onMounted, ref } from \"vue\";\n\nconst props = defineProps<{\n block: Block;\n /** 1-based position in the list — spoken by the handle's label. */\n position: number;\n total: number;\n}>();\n\nconst emit = defineEmits<{\n (e: \"move\", delta: -1 | 1): void;\n}>();\n\nconst { t, format } = useI18n();\n// Optional: absent when the consumer registers no custom blocks.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\n/** The email width every block component lays itself out against. */\nconst PREVIEW_WIDTH = 600;\n/**\n * Collapsed height for a tall block. A hero card or a multi-row section would\n * otherwise be taller than the whole list, leaving one row on screen — and\n * seeing the rows together is what makes them orderable. Anything taller\n * collapses to this, fades out at the cut, and can be expanded in place.\n */\nconst COLLAPSED_HEIGHT = 240;\n\nconst frameEl = ref<HTMLElement | null>(null);\nconst contentEl = ref<HTMLElement | null>(null);\nconst scale = ref(1);\n/** Scaled height of the whole block; `null` until first measured. */\nconst fullHeight = ref<number | null>(null);\nconst expanded = ref(false);\n\nlet observer: ResizeObserver | null = null;\n\n/** Only a block taller than the collapsed cap needs fading or expanding. */\nconst isClipped = computed(\n () => fullHeight.value !== null && fullHeight.value > COLLAPSED_HEIGHT,\n);\n\nconst frameHeight = computed(() => {\n if (fullHeight.value === null) return null;\n if (expanded.value || !isClipped.value) return fullHeight.value;\n return COLLAPSED_HEIGHT;\n});\n\nfunction measure(): void {\n const frame = frameEl.value;\n const content = contentEl.value;\n if (!frame || !content) return;\n const width = frame.clientWidth;\n if (width <= 0) return;\n // Never scale up: a dialog wider than the email would blow the preview past\n // its real proportions.\n const next = Math.min(1, width / PREVIEW_WIDTH);\n scale.value = next;\n fullHeight.value = content.offsetHeight * next;\n}\n\nonMounted(() => {\n measure();\n // Absent in non-DOM test environments. Without it the frame simply falls back\n // to the untransformed height and clips — degraded, never broken.\n if (typeof ResizeObserver === \"undefined\") return;\n observer = new ResizeObserver(() => measure());\n if (frameEl.value) observer.observe(frameEl.value);\n if (contentEl.value) observer.observe(contentEl.value);\n});\n\nonBeforeUnmount(() => {\n observer?.disconnect();\n observer = null;\n});\n\nconst blockLabel = computed(() =>\n getBlockLabel(props.block, t, customBlockDefinitions),\n);\n\nconst handleLabel = computed(() =>\n format(t.savedBlocks.reorderHandle, {\n block: blockLabel.value,\n position: props.position,\n total: props.total,\n }),\n);\n\nconst toggleLabel = computed(() =>\n format(\n expanded.value\n ? t.savedBlocks.collapsePreview\n : t.savedBlocks.expandPreview,\n { block: blockLabel.value },\n ),\n);\n</script>\n\n<template>\n <div\n data-testid=\"saved-blocks-reorder-row\"\n :data-block-id=\"block.id\"\n class=\"tpl:flex tpl:items-start tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:p-1.5 tpl:border-[var(--tpl-border)]\"\n style=\"background-color: var(--tpl-bg)\"\n >\n <!-- Sortable's `handle` selector points here, so a pointer-drag can only\n start from the grip — never from the preview itself. -->\n <button\n type=\"button\"\n data-testid=\"saved-blocks-reorder-handle\"\n class=\"tpl-saved-block-reorder-handle tpl:mt-0.5 tpl:shrink-0 tpl:cursor-grab tpl:rounded tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:text-[var(--tpl-text-dim)]\"\n :aria-label=\"handleLabel\"\n :title=\"t.savedBlocks.reorderHint\"\n @keydown.up.prevent=\"emit('move', -1)\"\n @keydown.down.prevent=\"emit('move', 1)\"\n >\n <GripVertical :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n\n <div class=\"tpl:relative tpl:min-w-0 tpl:flex-1\">\n <div\n ref=\"frameEl\"\n class=\"tpl:overflow-hidden\"\n :style=\"{\n height: frameHeight !== null ? `${frameHeight}px` : undefined,\n }\"\n >\n <div\n ref=\"contentEl\"\n :style=\"{\n width: `${PREVIEW_WIDTH}px`,\n transform: `scale(${scale})`,\n transformOrigin: 'top left',\n }\"\n >\n <SavedBlockPreviewCanvas :blocks=\"[block]\" />\n </div>\n </div>\n\n <!-- Fade over the cut, so a clipped block reads as truncated rather than\n as a block that simply ends there. Pointer-transparent: it sits over\n the preview, and swallowing clicks would break the row. -->\n <div\n v-if=\"isClipped && !expanded\"\n data-testid=\"saved-blocks-preview-fade\"\n aria-hidden=\"true\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-x-0 tpl:bottom-0 tpl:h-10\"\n style=\"\n background: linear-gradient(\n to bottom,\n transparent,\n var(--tpl-bg) 90%\n );\n \"\n />\n\n <button\n v-if=\"isClipped\"\n type=\"button\"\n data-testid=\"saved-blocks-preview-toggle\"\n class=\"tpl:absolute tpl:bottom-1 tpl:right-1 tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-0.5 tpl:rounded tpl:border tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)] tpl:text-[var(--tpl-text-muted)]\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"expanded\"\n @click=\"expanded = !expanded\"\n >\n <component\n :is=\"expanded ? ChevronUp : ChevronDown\"\n :size=\"11\"\n :stroke-width=\"2\"\n />\n {{ expanded ? t.savedBlocks.collapse : t.savedBlocks.expand }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * One reorderable row in the save dialog's preview list: a grip handle beside a\n * scaled-down render of the real block.\n *\n * The preview is the same `SavedBlockPreviewCanvas` the browser modal uses, so\n * a row shows what the block actually looks like rather than a description of\n * it. That canvas is fixed at the 600px email width every block component\n * renders against, so the row shrinks it with `transform: scale()` instead of\n * re-flowing it at dialog width — a re-flow would show a layout the recipient\n * will never see.\n *\n * `transform` doesn't participate in layout, so the frame would keep reserving\n * the full unscaled height; a `ResizeObserver` mirrors the scaled height back\n * onto it. The handle is a sibling of the scaled subtree, so it stays full size\n * and keeps its real hit area.\n *\n * Row chrome uses the raw theme tokens, not the `--tpl-chrome-*` aliases: the\n * chrome is an ancestor and a sibling of the rendered email content, never a\n * descendant, so the `.tpl-block-content` token override can't reach it.\n */\nimport SavedBlockPreviewCanvas from \"./SavedBlockPreviewCanvas.vue\";\nimport { useI18n } from \"../composables\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY } from \"../keys\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { ChevronDown, ChevronUp, GripVertical } from \"@lucide/vue\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, onBeforeUnmount, onMounted, ref } from \"vue\";\n\nconst props = defineProps<{\n block: Block;\n /** 1-based position in the list — spoken by the handle's label. */\n position: number;\n total: number;\n}>();\n\nconst emit = defineEmits<{\n (e: \"move\", delta: -1 | 1): void;\n}>();\n\nconst { t, format } = useI18n();\n// Optional: absent when the consumer registers no custom blocks.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\n/** The email width every block component lays itself out against. */\nconst PREVIEW_WIDTH = 600;\n/**\n * Collapsed height for a tall block. A hero card or a multi-row section would\n * otherwise be taller than the whole list, leaving one row on screen — and\n * seeing the rows together is what makes them orderable. Anything taller\n * collapses to this, fades out at the cut, and can be expanded in place.\n */\nconst COLLAPSED_HEIGHT = 240;\n\nconst frameEl = ref<HTMLElement | null>(null);\nconst contentEl = ref<HTMLElement | null>(null);\nconst scale = ref(1);\n/** Scaled height of the whole block; `null` until first measured. */\nconst fullHeight = ref<number | null>(null);\nconst expanded = ref(false);\n\nlet observer: ResizeObserver | null = null;\n\n/** Only a block taller than the collapsed cap needs fading or expanding. */\nconst isClipped = computed(\n () => fullHeight.value !== null && fullHeight.value > COLLAPSED_HEIGHT,\n);\n\nconst frameHeight = computed(() => {\n if (fullHeight.value === null) return null;\n if (expanded.value || !isClipped.value) return fullHeight.value;\n return COLLAPSED_HEIGHT;\n});\n\nfunction measure(): void {\n const frame = frameEl.value;\n const content = contentEl.value;\n if (!frame || !content) return;\n const width = frame.clientWidth;\n if (width <= 0) return;\n // Never scale up: a dialog wider than the email would blow the preview past\n // its real proportions.\n const next = Math.min(1, width / PREVIEW_WIDTH);\n scale.value = next;\n fullHeight.value = content.offsetHeight * next;\n}\n\nonMounted(() => {\n measure();\n // Absent in non-DOM test environments. Without it the frame simply falls back\n // to the untransformed height and clips — degraded, never broken.\n if (typeof ResizeObserver === \"undefined\") return;\n observer = new ResizeObserver(() => measure());\n if (frameEl.value) observer.observe(frameEl.value);\n if (contentEl.value) observer.observe(contentEl.value);\n});\n\nonBeforeUnmount(() => {\n observer?.disconnect();\n observer = null;\n});\n\nconst blockLabel = computed(() =>\n getBlockLabel(props.block, t, customBlockDefinitions),\n);\n\nconst handleLabel = computed(() =>\n format(t.savedBlocks.reorderHandle, {\n block: blockLabel.value,\n position: props.position,\n total: props.total,\n }),\n);\n\nconst toggleLabel = computed(() =>\n format(\n expanded.value\n ? t.savedBlocks.collapsePreview\n : t.savedBlocks.expandPreview,\n { block: blockLabel.value },\n ),\n);\n</script>\n\n<template>\n <div\n data-testid=\"saved-blocks-reorder-row\"\n :data-block-id=\"block.id\"\n class=\"tpl:flex tpl:items-start tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:p-1.5 tpl:border-[var(--tpl-border)]\"\n style=\"background-color: var(--tpl-bg)\"\n >\n <!-- Sortable's `handle` selector points here, so a pointer-drag can only\n start from the grip — never from the preview itself. -->\n <button\n type=\"button\"\n data-testid=\"saved-blocks-reorder-handle\"\n class=\"tpl-saved-block-reorder-handle tpl:mt-0.5 tpl:shrink-0 tpl:cursor-grab tpl:rounded tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:text-[var(--tpl-text-dim)]\"\n :aria-label=\"handleLabel\"\n :title=\"t.savedBlocks.reorderHint\"\n @keydown.up.prevent=\"emit('move', -1)\"\n @keydown.down.prevent=\"emit('move', 1)\"\n >\n <GripVertical :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n\n <div class=\"tpl:relative tpl:min-w-0 tpl:flex-1\">\n <div\n ref=\"frameEl\"\n class=\"tpl:overflow-hidden\"\n :style=\"{\n height: frameHeight !== null ? `${frameHeight}px` : undefined,\n }\"\n >\n <div\n ref=\"contentEl\"\n :style=\"{\n width: `${PREVIEW_WIDTH}px`,\n transform: `scale(${scale})`,\n transformOrigin: 'top left',\n }\"\n >\n <SavedBlockPreviewCanvas :blocks=\"[block]\" />\n </div>\n </div>\n\n <!-- Fade over the cut, so a clipped block reads as truncated rather than\n as a block that simply ends there. Pointer-transparent: it sits over\n the preview, and swallowing clicks would break the row. -->\n <div\n v-if=\"isClipped && !expanded\"\n data-testid=\"saved-blocks-preview-fade\"\n aria-hidden=\"true\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-x-0 tpl:bottom-0 tpl:h-10\"\n style=\"\n background: linear-gradient(\n to bottom,\n transparent,\n var(--tpl-bg) 90%\n );\n \"\n />\n\n <button\n v-if=\"isClipped\"\n type=\"button\"\n data-testid=\"saved-blocks-preview-toggle\"\n class=\"tpl:absolute tpl:bottom-1 tpl:right-1 tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-0.5 tpl:rounded tpl:border tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)] tpl:text-[var(--tpl-text-muted)]\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"expanded\"\n @click=\"expanded = !expanded\"\n >\n <component\n :is=\"expanded ? ChevronUp : ChevronDown\"\n :size=\"11\"\n :stroke-width=\"2\"\n />\n {{ expanded ? t.savedBlocks.collapse : t.savedBlocks.expand }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport SavedBlockPreviewRow from \"./SavedBlockPreviewRow.vue\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n EDITOR_KEY,\n SAVED_BLOCKS_KEY,\n requireInject,\n} from \"../keys\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { LoaderCircle } from \"@lucide/vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n /**\n * Blocks chosen during the canvas pick session, in the order they were\n * picked — `pickedIds` is built from a `Set`, which iterates in insertion\n * order. That order seeds the reorderable list below. Every id must be a\n * top-level block, which is what the session guarantees.\n */\n pickedIds: readonly string[];\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"saved\"): void;\n}>();\n\nconst { t, format } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"SaveBlockDialog\");\nconst savedBlocks = requireInject(SAVED_BLOCKS_KEY, \"SaveBlockDialog\");\n// Optional: a consumer with no custom blocks never provides definitions, and\n// the label helper degrades to the type slug rather than throwing.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst name = ref(\"\");\nconst category = ref(\"\");\nconst isSaving = ref(false);\nconst error = ref<string | null>(null);\nconst announcement = ref(\"\");\n\n/**\n * The order the blocks will be saved in: seeded from the pick order, then owned\n * by this dialog once the user drags a row.\n *\n * Ids rather than blocks, deliberately. Resolving late means a block edited\n * while the dialog is open (a collaborator in Cloud) is persisted as it stands\n * at save time, not as it looked when the dialog opened, and an id that stops\n * resolving drops out on its own instead of leaving a stale snapshot behind.\n */\nconst orderedIds = ref<string[]>([]);\n\n/**\n * The picked blocks in the list's current order. Ids that no longer resolve (a\n * block deleted mid-session) simply drop out.\n */\nconst pickedBlocks = computed(() => {\n const byId = new Map(editor.content.value.blocks.map((b) => [b.id, b]));\n return orderedIds.value\n .map((id) => byId.get(id))\n .filter((b): b is Block => b !== undefined);\n});\n\n/**\n * Sortable's model. The setter reads ids straight back off the emitted entries\n * rather than storing them, so a Sortable `el` expando can never reach the\n * payload `create()` sends to the provider — and because the getter is already\n * filtered, writing through it prunes unresolvable ids too.\n */\nconst orderedBlocks = computed<Block[]>({\n get: () => pickedBlocks.value,\n set: (blocks) => {\n orderedIds.value = blocks.map((b) => b.id);\n },\n});\n\n/** Keyboard reorder from a focused grip: swap with the neighbouring row. */\nfunction moveBlock(blockId: string, delta: -1 | 1): void {\n const order = pickedBlocks.value.map((b) => b.id);\n const from = order.indexOf(blockId);\n const to = from + delta;\n if (from === -1 || to < 0 || to >= order.length) return;\n [order[from], order[to]] = [order[to], order[from]];\n orderedIds.value = order;\n\n const moved = pickedBlocks.value[to];\n announcement.value = moved\n ? format(t.savedBlocks.reorderAnnouncement, {\n block: label(moved),\n position: to + 1,\n total: order.length,\n })\n : \"\";\n}\n\n/** Custom blocks read as the consumer's name for them, not the type `custom`. */\nfunction label(block: Block): string {\n return getBlockLabel(block, t, customBlockDefinitions);\n}\n\n/** e.g. \"Title, Paragraph, Button\" — what the user is about to save. */\nconst summary = computed(() => pickedBlocks.value.map(label).join(\", \"));\n\n// `immediate` matters: the dialog is mounted lazily behind a `v-if` on the\n// same state that drives `visible`, so it can mount with `visible` already\n// true and would otherwise never reset the name field or seed the order.\nwatch(\n () => props.visible,\n (visible) => {\n if (visible) {\n name.value = \"\";\n category.value = \"\";\n error.value = null;\n announcement.value = \"\";\n orderedIds.value = [...props.pickedIds];\n }\n },\n { immediate: true },\n);\n\nconst canSave = computed(\n () =>\n name.value.trim().length > 0 &&\n pickedBlocks.value.length > 0 &&\n !isSaving.value,\n);\n\nasync function handleSave(): Promise<void> {\n if (!canSave.value) return;\n\n const blocks = pickedBlocks.value;\n // Defence in depth: never persist an empty saved block (it would list as\n // \"0 block(s)\" and insert nothing). Guards a pick set whose ids no longer\n // resolve to anything on the canvas.\n if (blocks.length === 0) return;\n\n isSaving.value = true;\n error.value = null;\n\n try {\n // Empty stays undefined rather than \"\": an entry is either categorised or\n // it isn't, and a blank string would show up as a nameless filter option.\n await savedBlocks.create(\n name.value.trim(),\n blocks,\n category.value.trim() || undefined,\n );\n emit(\"saved\");\n emit(\"close\");\n } catch (err) {\n error.value = (err as Error).message;\n } finally {\n isSaving.value = false;\n }\n}\n\nfunction handleClose(): void {\n if (!isSaving.value) {\n emit(\"close\");\n }\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSave();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSaving\"\n aria-labelledby=\"tpl-save-block-title\"\n class=\"tpl-scale-in tpl:mx-4 tpl:flex tpl:w-full tpl:max-w-2xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n max-height: 90vh;\n \"\n >\n <h3\n id=\"tpl-save-block-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.savedBlocks.saveAsBlock }}\n </h3>\n\n <!-- Saved block name -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.name }}\n </label>\n <input\n v-model=\"name\"\n type=\"text\"\n data-testid=\"saved-blocks-name-input\"\n :placeholder=\"t.savedBlocks.namePlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n </div>\n\n <!-- Category: free text, with the already-used categories offered as\n suggestions. A datalist rather than a <select> because the set is\n derived from existing entries — there is no registry to pick from,\n and the first entry in a category has to be able to name it. -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-save-block-category\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.category }}\n </label>\n <input\n id=\"tpl-save-block-category\"\n v-model=\"category\"\n type=\"text\"\n data-testid=\"saved-blocks-category-input\"\n list=\"tpl-saved-block-categories\"\n :placeholder=\"t.savedBlocks.categoryPlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n <datalist id=\"tpl-saved-block-categories\">\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n />\n </datalist>\n </div>\n\n <!-- Summary of what the pick session chose. Which blocks were picked is\n settled on the canvas — the dialog re-presents them only so the user\n can see and order what they're about to save. -->\n <p\n data-testid=\"saved-blocks-save-summary\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ format(t.savedBlocks.savingCount, { count: pickedBlocks.length }) }}\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">{{ summary }}</span>\n </p>\n\n <!-- Reorderable preview. Seeded in pick order and saved in whatever order\n the list ends up in. Its own Sortable instance with no `group`, so it\n can never exchange items with the canvas or a section column. -->\n <p\n v-if=\"pickedBlocks.length > 1\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.reorderHint }}\n </p>\n <!-- The only growing region: takes whatever the dialog's 90vh cap leaves\n after the fixed chrome, rather than a hand-tuned vh guess that a row\n expanding to full height would blow past. `min-h-0` is what lets a\n flex child actually shrink below its content and scroll. -->\n <VueDraggable\n v-model=\"orderedBlocks\"\n data-testid=\"saved-blocks-reorder-list\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-saved-block-reorder-handle\"\n :force-fallback=\"true\"\n class=\"tpl:mb-4 tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-1.5 tpl:overflow-y-auto\"\n >\n <SavedBlockPreviewRow\n v-for=\"(block, index) in orderedBlocks\"\n :key=\"block.id\"\n :block=\"block\"\n :position=\"index + 1\"\n :total=\"orderedBlocks.length\"\n @move=\"(delta) => moveBlock(block.id, delta)\"\n />\n </VueDraggable>\n\n <!-- Keyboard reorder announcement region (visually hidden, live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n {{ announcement }}\n </div>\n\n <!-- Error message -->\n <p\n v-if=\"error\"\n role=\"alert\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ error }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSaving\"\n :class=\"{\n 'tpl:cursor-not-allowed tpl:opacity-50': isSaving,\n }\"\n @click=\"handleClose\"\n >\n {{ t.savedBlocks.cancel }}\n </button>\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!canSave\"\n @click=\"handleSave\"\n >\n <span v-if=\"isSaving\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.savedBlocks.saving }}\n </span>\n <span v-else>\n {{ t.savedBlocks.save }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport SavedBlockPreviewRow from \"./SavedBlockPreviewRow.vue\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n EDITOR_KEY,\n SAVED_BLOCKS_KEY,\n requireInject,\n} from \"../keys\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { LoaderCircle } from \"@lucide/vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n /**\n * Blocks chosen during the canvas pick session, in the order they were\n * picked — `pickedIds` is built from a `Set`, which iterates in insertion\n * order. That order seeds the reorderable list below. Every id must be a\n * top-level block, which is what the session guarantees.\n */\n pickedIds: readonly string[];\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"saved\"): void;\n}>();\n\nconst { t, format } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"SaveBlockDialog\");\nconst savedBlocks = requireInject(SAVED_BLOCKS_KEY, \"SaveBlockDialog\");\n// Optional: a consumer with no custom blocks never provides definitions, and\n// the label helper degrades to the type slug rather than throwing.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst name = ref(\"\");\nconst category = ref(\"\");\nconst isSaving = ref(false);\nconst error = ref<string | null>(null);\nconst announcement = ref(\"\");\n\n/**\n * The order the blocks will be saved in: seeded from the pick order, then owned\n * by this dialog once the user drags a row.\n *\n * Ids rather than blocks, deliberately. Resolving late means a block edited\n * while the dialog is open (a collaborator in Cloud) is persisted as it stands\n * at save time, not as it looked when the dialog opened, and an id that stops\n * resolving drops out on its own instead of leaving a stale snapshot behind.\n */\nconst orderedIds = ref<string[]>([]);\n\n/**\n * The picked blocks in the list's current order. Ids that no longer resolve (a\n * block deleted mid-session) simply drop out.\n */\nconst pickedBlocks = computed(() => {\n const byId = new Map(editor.content.value.blocks.map((b) => [b.id, b]));\n return orderedIds.value\n .map((id) => byId.get(id))\n .filter((b): b is Block => b !== undefined);\n});\n\n/**\n * Sortable's model. The setter reads ids straight back off the emitted entries\n * rather than storing them, so a Sortable `el` expando can never reach the\n * payload `create()` sends to the provider — and because the getter is already\n * filtered, writing through it prunes unresolvable ids too.\n */\nconst orderedBlocks = computed<Block[]>({\n get: () => pickedBlocks.value,\n set: (blocks) => {\n orderedIds.value = blocks.map((b) => b.id);\n },\n});\n\n/** Keyboard reorder from a focused grip: swap with the neighbouring row. */\nfunction moveBlock(blockId: string, delta: -1 | 1): void {\n const order = pickedBlocks.value.map((b) => b.id);\n const from = order.indexOf(blockId);\n const to = from + delta;\n if (from === -1 || to < 0 || to >= order.length) return;\n [order[from], order[to]] = [order[to], order[from]];\n orderedIds.value = order;\n\n const moved = pickedBlocks.value[to];\n announcement.value = moved\n ? format(t.savedBlocks.reorderAnnouncement, {\n block: label(moved),\n position: to + 1,\n total: order.length,\n })\n : \"\";\n}\n\n/** Custom blocks read as the consumer's name for them, not the type `custom`. */\nfunction label(block: Block): string {\n return getBlockLabel(block, t, customBlockDefinitions);\n}\n\n/** e.g. \"Title, Paragraph, Button\" — what the user is about to save. */\nconst summary = computed(() => pickedBlocks.value.map(label).join(\", \"));\n\n// `immediate` matters: the dialog is mounted lazily behind a `v-if` on the\n// same state that drives `visible`, so it can mount with `visible` already\n// true and would otherwise never reset the name field or seed the order.\nwatch(\n () => props.visible,\n (visible) => {\n if (visible) {\n name.value = \"\";\n category.value = \"\";\n error.value = null;\n announcement.value = \"\";\n orderedIds.value = [...props.pickedIds];\n }\n },\n { immediate: true },\n);\n\nconst canSave = computed(\n () =>\n name.value.trim().length > 0 &&\n pickedBlocks.value.length > 0 &&\n !isSaving.value,\n);\n\nasync function handleSave(): Promise<void> {\n if (!canSave.value) return;\n\n const blocks = pickedBlocks.value;\n // Defence in depth: never persist an empty saved block (it would list as\n // \"0 block(s)\" and insert nothing). Guards a pick set whose ids no longer\n // resolve to anything on the canvas.\n if (blocks.length === 0) return;\n\n isSaving.value = true;\n error.value = null;\n\n try {\n // Empty stays undefined rather than \"\": an entry is either categorised or\n // it isn't, and a blank string would show up as a nameless filter option.\n await savedBlocks.create(\n name.value.trim(),\n blocks,\n category.value.trim() || undefined,\n );\n emit(\"saved\");\n emit(\"close\");\n } catch (err) {\n error.value = (err as Error).message;\n } finally {\n isSaving.value = false;\n }\n}\n\nfunction handleClose(): void {\n if (!isSaving.value) {\n emit(\"close\");\n }\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSave();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSaving\"\n aria-labelledby=\"tpl-save-block-title\"\n class=\"tpl-scale-in tpl:mx-4 tpl:flex tpl:w-full tpl:max-w-2xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n max-height: 90vh;\n \"\n >\n <h3\n id=\"tpl-save-block-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.savedBlocks.saveAsBlock }}\n </h3>\n\n <!-- Saved block name -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.name }}\n </label>\n <input\n v-model=\"name\"\n type=\"text\"\n data-testid=\"saved-blocks-name-input\"\n :placeholder=\"t.savedBlocks.namePlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n </div>\n\n <!-- Category: free text, with the already-used categories offered as\n suggestions. A datalist rather than a <select> because the set is\n derived from existing entries — there is no registry to pick from,\n and the first entry in a category has to be able to name it. -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-save-block-category\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.category }}\n </label>\n <input\n id=\"tpl-save-block-category\"\n v-model=\"category\"\n type=\"text\"\n data-testid=\"saved-blocks-category-input\"\n list=\"tpl-saved-block-categories\"\n :placeholder=\"t.savedBlocks.categoryPlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n <datalist id=\"tpl-saved-block-categories\">\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n />\n </datalist>\n </div>\n\n <!-- Summary of what the pick session chose. Which blocks were picked is\n settled on the canvas — the dialog re-presents them only so the user\n can see and order what they're about to save. -->\n <p\n data-testid=\"saved-blocks-save-summary\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ format(t.savedBlocks.savingCount, { count: pickedBlocks.length }) }}\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">{{ summary }}</span>\n </p>\n\n <!-- Reorderable preview. Seeded in pick order and saved in whatever order\n the list ends up in. Its own Sortable instance with no `group`, so it\n can never exchange items with the canvas or a section column. -->\n <p\n v-if=\"pickedBlocks.length > 1\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.reorderHint }}\n </p>\n <!-- The only growing region: takes whatever the dialog's 90vh cap leaves\n after the fixed chrome, rather than a hand-tuned vh guess that a row\n expanding to full height would blow past. `min-h-0` is what lets a\n flex child actually shrink below its content and scroll. -->\n <VueDraggable\n v-model=\"orderedBlocks\"\n data-testid=\"saved-blocks-reorder-list\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-saved-block-reorder-handle\"\n :force-fallback=\"true\"\n class=\"tpl:mb-4 tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-1.5 tpl:overflow-y-auto\"\n >\n <SavedBlockPreviewRow\n v-for=\"(block, index) in orderedBlocks\"\n :key=\"block.id\"\n :block=\"block\"\n :position=\"index + 1\"\n :total=\"orderedBlocks.length\"\n @move=\"(delta) => moveBlock(block.id, delta)\"\n />\n </VueDraggable>\n\n <!-- Keyboard reorder announcement region (visually hidden, live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n {{ announcement }}\n </div>\n\n <!-- Error message -->\n <p\n v-if=\"error\"\n role=\"alert\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ error }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSaving\"\n :class=\"{\n 'tpl:cursor-not-allowed tpl:opacity-50': isSaving,\n }\"\n @click=\"handleClose\"\n >\n {{ t.savedBlocks.cancel }}\n </button>\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!canSave\"\n @click=\"handleSave\"\n >\n <span v-if=\"isSaving\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.savedBlocks.saving }}\n </span>\n <span v-else>\n {{ t.savedBlocks.save }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n"],"mappings":"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"}
|