@templatical/editor 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/{AiChatSidebar-Nq-jrGaf.js → AiChatSidebar-CIkS8DsO.js} +15 -15
  2. package/dist/{AiFeatureMenu-tg_zMKMK.js → AiFeatureMenu-Cji1LBJJ.js} +7 -7
  3. package/dist/{BlockIssueBadge-Bd94kIiw.js → BlockIssueBadge-BQiT0FML.js} +5 -5
  4. package/dist/{BlockPreviewCanvas-B34aMhHe.js → BlockPreviewCanvas-DAnwZofk.js} +11 -7
  5. package/dist/{CloudEditor-DjN_9Yso.js → CloudEditor-CXaft6Ej.js} +73 -71
  6. package/dist/{CollaboratorBar-Bhazzcg-.js → CollaboratorBar-BR-rL2wG.js} +4 -4
  7. package/dist/{ColorPicker-DXGrcJxJ.js → ColorPicker-B4ArSUhy.js} +7 -9
  8. package/dist/{CommentsSidebar-ku1kt10u.js → CommentsSidebar-Bm5GLDts.js} +16 -16
  9. package/dist/{CountdownBlock-CN9Lc23W.js → CountdownBlock-CIVAQDVu.js} +3 -3
  10. package/dist/{CountdownToolbar-VEsIyKV6.js → CountdownToolbar-DBmb3tlb.js} +4 -4
  11. package/dist/DesignReferenceSidebar-CnyJ8L-j.js +264 -0
  12. package/dist/{IssuesPanel-CEfzOpS_.js → IssuesPanel-D3XgmYCt.js} +8 -8
  13. package/dist/{LoadingTrack-KIcREPlS.js → LoadingTrack-XK_eeUdq.js} +1 -1
  14. package/dist/{MergeTagModeToggle-C3V2xGAg.js → MergeTagModeToggle-BK0fI3P2.js} +6 -6
  15. package/dist/{MergeTagSuggestion-Dn8ww9nV.js → MergeTagSuggestion-D0OHRv7L.js} +8 -12
  16. package/dist/{NumberWithSuffix-BGocZkj9.js → NumberWithSuffix-Cjj9UNIp.js} +2 -2
  17. package/dist/{ParagraphEditor-D-fz0kUb.js → ParagraphEditor-jGnbJEA2.js} +45 -45
  18. package/dist/{RichTextEditorContent-DydrqFts.js → RichTextEditorContent-DaaKdzjb.js} +8 -8
  19. package/dist/{SaveBlockDialog-BDDFbAWF.js → SaveBlockDialog-B3ngEldp.js} +12 -12
  20. package/dist/{SavedBlocksBrowserModal-Djik_fJh.js → SavedBlocksBrowserModal-DawN3XaB.js} +11 -11
  21. package/dist/{SavedBlocksPanels-CxemyPxL.js → SavedBlocksPanels-eDC9Y46N.js} +2 -2
  22. package/dist/{SavedBlocksPickBar-Dlhipmob.js → SavedBlocksPickBar-CQbbXmzj.js} +4 -4
  23. package/dist/{SnapshotHistory-DPbM4ons.js → SnapshotHistory-COxKtHM-.js} +10 -10
  24. package/dist/{TemplateScoringPanel-BFYgweDX.js → TemplateScoringPanel-bUuqOO1z.js} +16 -16
  25. package/dist/{TestEmailModal-IZfwgya-.js → TestEmailModal-CytkbZVQ.js} +89 -58
  26. package/dist/{TestEmailPanel-QEEx_PkN.js → TestEmailPanel-CHUY7VZn.js} +2 -2
  27. package/dist/{TitleEditor-BB4YlIBF.js → TitleEditor-aMR1bOyY.js} +13 -13
  28. package/dist/TplModal-fmYNYdyp.js +49 -0
  29. package/dist/{blockTypeIcons-Wzd7W_2x.js → blockTypeIcons-C6b9rg88.js} +26 -26
  30. package/dist/bundle-stats.json +6 -6
  31. package/dist/{ca-CO4nq6TX.js → ca-8Dvbwo50.js} +5 -0
  32. package/dist/cdn/chunks/{AiChatSidebar-CN-0DIuA.js → AiChatSidebar-bkBKpNgE.js} +9 -9
  33. package/dist/cdn/chunks/{AiChatSidebar-CN-0DIuA.js.map → AiChatSidebar-bkBKpNgE.js.map} +1 -1
  34. package/dist/cdn/chunks/{AiFeatureMenu-B5pV6Iux.js → AiFeatureMenu-CI4SYTq2.js} +6 -6
  35. package/dist/cdn/chunks/{AiFeatureMenu-B5pV6Iux.js.map → AiFeatureMenu-CI4SYTq2.js.map} +1 -1
  36. package/dist/cdn/chunks/{BlockIssueBadge-aanOjeyi.js → BlockIssueBadge-CRnx8Dv9.js} +5 -5
  37. package/dist/cdn/chunks/{BlockIssueBadge-aanOjeyi.js.map → BlockIssueBadge-CRnx8Dv9.js.map} +1 -1
  38. package/dist/cdn/chunks/{BlockPreviewCanvas-Dc4F4HzY.js → BlockPreviewCanvas-CXCWhqbp.js} +13 -9
  39. package/dist/cdn/chunks/BlockPreviewCanvas-CXCWhqbp.js.map +1 -0
  40. package/dist/cdn/chunks/{CloudEditor-CeKaWWO5.js → CloudEditor-D-t7RyDh.js} +67 -65
  41. package/dist/cdn/chunks/CloudEditor-D-t7RyDh.js.map +1 -0
  42. package/dist/cdn/chunks/{CollaboratorBar-CEdKr0A2.js → CollaboratorBar-DcXYo47v.js} +5 -5
  43. package/dist/cdn/chunks/{CollaboratorBar-CEdKr0A2.js.map → CollaboratorBar-DcXYo47v.js.map} +1 -1
  44. package/dist/cdn/chunks/{ColorPicker-BckwEfcN.js → ColorPicker-_1O7YU2z.js} +8 -10
  45. package/dist/cdn/chunks/{ColorPicker-BckwEfcN.js.map → ColorPicker-_1O7YU2z.js.map} +1 -1
  46. package/dist/cdn/chunks/{CommentsSidebar-6PfpJKh-.js → CommentsSidebar-BEgLBYX-.js} +7 -7
  47. package/dist/cdn/chunks/{CommentsSidebar-6PfpJKh-.js.map → CommentsSidebar-BEgLBYX-.js.map} +1 -1
  48. package/dist/cdn/chunks/{CountdownBlock-CoBmvQKv.js → CountdownBlock-CPnQA44g.js} +4 -4
  49. package/dist/cdn/chunks/{CountdownBlock-CoBmvQKv.js.map → CountdownBlock-CPnQA44g.js.map} +1 -1
  50. package/dist/cdn/chunks/{CountdownToolbar-CdV8RTQu.js → CountdownToolbar-Cdo-30U4.js} +5 -5
  51. package/dist/cdn/chunks/{CountdownToolbar-CdV8RTQu.js.map → CountdownToolbar-Cdo-30U4.js.map} +1 -1
  52. package/dist/cdn/chunks/DesignReferenceSidebar-DPqBr-ut.js +223 -0
  53. package/dist/cdn/chunks/{DesignReferenceSidebar-BjlaP69l.js.map → DesignReferenceSidebar-DPqBr-ut.js.map} +1 -1
  54. package/dist/cdn/chunks/{IssuesPanel-CkkEnhxZ.js → IssuesPanel-DoQWb23r.js} +5 -5
  55. package/dist/cdn/chunks/{IssuesPanel-CkkEnhxZ.js.map → IssuesPanel-DoQWb23r.js.map} +1 -1
  56. package/dist/cdn/chunks/{LoadingTrack-D-CW2tUa.js → LoadingTrack-G2tuXqCJ.js} +2 -2
  57. package/dist/cdn/chunks/{LoadingTrack-D-CW2tUa.js.map → LoadingTrack-G2tuXqCJ.js.map} +1 -1
  58. package/dist/cdn/chunks/{MergeTagModeToggle-DmtR300G.js → MergeTagModeToggle-CWuVC3MU.js} +6 -6
  59. package/dist/cdn/chunks/{MergeTagModeToggle-DmtR300G.js.map → MergeTagModeToggle-CWuVC3MU.js.map} +1 -1
  60. package/dist/cdn/chunks/{MergeTagSuggestion-Cvlkw_PM.js → MergeTagSuggestion-Ces5Ua78.js} +3 -3
  61. package/dist/cdn/chunks/{MergeTagSuggestion-Cvlkw_PM.js.map → MergeTagSuggestion-Ces5Ua78.js.map} +1 -1
  62. package/dist/cdn/chunks/{NumberWithSuffix-BOcMN26z.js → NumberWithSuffix-DQ4sgvt5.js} +3 -3
  63. package/dist/cdn/chunks/{NumberWithSuffix-BOcMN26z.js.map → NumberWithSuffix-DQ4sgvt5.js.map} +1 -1
  64. package/dist/cdn/chunks/{ParagraphEditor-CX1yks4N.js → ParagraphEditor-Uv5LPF3W.js} +131 -131
  65. package/dist/cdn/chunks/{ParagraphEditor-CX1yks4N.js.map → ParagraphEditor-Uv5LPF3W.js.map} +1 -1
  66. package/dist/cdn/chunks/{RichTextEditorContent-z9s7qxs3.js → RichTextEditorContent-Bt3s09qY.js} +7 -7
  67. package/dist/cdn/chunks/{RichTextEditorContent-z9s7qxs3.js.map → RichTextEditorContent-Bt3s09qY.js.map} +1 -1
  68. package/dist/cdn/chunks/{SaveBlockDialog-D2CtW4lC.js → SaveBlockDialog-BAew6EFd.js} +10 -10
  69. package/dist/cdn/chunks/{SaveBlockDialog-D2CtW4lC.js.map → SaveBlockDialog-BAew6EFd.js.map} +1 -1
  70. package/dist/cdn/chunks/{SavedBlocksBrowserModal-DdUKJzzF.js → SavedBlocksBrowserModal-Dn4IJDJW.js} +9 -9
  71. package/dist/cdn/chunks/{SavedBlocksBrowserModal-DdUKJzzF.js.map → SavedBlocksBrowserModal-Dn4IJDJW.js.map} +1 -1
  72. package/dist/cdn/chunks/{SavedBlocksPanels-1kVbqdpc.js → SavedBlocksPanels-B62U5DYW.js} +3 -3
  73. package/dist/cdn/chunks/{SavedBlocksPanels-1kVbqdpc.js.map → SavedBlocksPanels-B62U5DYW.js.map} +1 -1
  74. package/dist/cdn/chunks/{SavedBlocksPickBar--noDJIyN.js → SavedBlocksPickBar-CyRKZg2T.js} +5 -5
  75. package/dist/cdn/chunks/{SavedBlocksPickBar--noDJIyN.js.map → SavedBlocksPickBar-CyRKZg2T.js.map} +1 -1
  76. package/dist/cdn/chunks/{SnapshotHistory-DFuNJRc9.js → SnapshotHistory-QQt0DKey.js} +7 -7
  77. package/dist/cdn/chunks/{SnapshotHistory-DFuNJRc9.js.map → SnapshotHistory-QQt0DKey.js.map} +1 -1
  78. package/dist/cdn/chunks/{TemplateScoringPanel-DG1dAjQk.js → TemplateScoringPanel-BwyCMcit.js} +67 -67
  79. package/dist/cdn/chunks/{TemplateScoringPanel-DG1dAjQk.js.map → TemplateScoringPanel-BwyCMcit.js.map} +1 -1
  80. package/dist/cdn/chunks/{TestEmailModal-CiPfOn2v.js → TestEmailModal-B5UQkDJp.js} +89 -58
  81. package/dist/cdn/chunks/TestEmailModal-B5UQkDJp.js.map +1 -0
  82. package/dist/cdn/chunks/{TestEmailPanel-saOfnpjz.js → TestEmailPanel-CvuBq4RA.js} +3 -3
  83. package/dist/cdn/chunks/{TestEmailPanel-saOfnpjz.js.map → TestEmailPanel-CvuBq4RA.js.map} +1 -1
  84. package/dist/cdn/chunks/{TitleEditor-08KM_VzZ.js → TitleEditor-B9W954u9.js} +13 -13
  85. package/dist/cdn/chunks/{TitleEditor-08KM_VzZ.js.map → TitleEditor-B9W954u9.js.map} +1 -1
  86. package/dist/cdn/chunks/TplModal-B8UsvWc8.js +50 -0
  87. package/dist/cdn/chunks/TplModal-B8UsvWc8.js.map +1 -0
  88. package/dist/cdn/chunks/{blockTypeIcons-DvohkXqi.js → blockTypeIcons-B8s3_ahs.js} +2 -2
  89. package/dist/cdn/chunks/{blockTypeIcons-DvohkXqi.js.map → blockTypeIcons-B8s3_ahs.js.map} +1 -1
  90. package/dist/cdn/chunks/{ca-CO4nq6TX.js → ca-8Dvbwo50.js} +6 -1
  91. package/dist/cdn/chunks/{ca-CO4nq6TX.js.map → ca-8Dvbwo50.js.map} +1 -1
  92. package/dist/cdn/chunks/{cloud-C2I0P9qj.js → cloud-DOaGUdR0.js} +13 -17
  93. package/dist/cdn/chunks/{cloud-C2I0P9qj.js.map → cloud-DOaGUdR0.js.map} +1 -1
  94. package/dist/{de-CUeaFJph.js → cdn/chunks/de-CieSMCUp.js} +7 -0
  95. package/dist/cdn/chunks/{de-CUeaFJph.js.map → de-CieSMCUp.js.map} +1 -1
  96. package/dist/cdn/chunks/{dist-CxTt8FRE.js → dist-B8AX9np4.js} +16 -12
  97. package/dist/cdn/chunks/dist-B8AX9np4.js.map +1 -0
  98. package/dist/cdn/chunks/{dist-CiBEt0VR.js → dist-lZgXNk4G.js} +36 -20
  99. package/dist/cdn/chunks/dist-lZgXNk4G.js.map +1 -0
  100. package/dist/cdn/chunks/{draggable-D840DIZB.js → draggable-JcMTNF3U.js} +14 -15
  101. package/dist/cdn/chunks/{draggable-D840DIZB.js.map → draggable-JcMTNF3U.js.map} +1 -1
  102. package/dist/{en-zCYIPAL5.js → cdn/chunks/en-D0t_2Fq_.js} +7 -0
  103. package/dist/cdn/chunks/{en-zCYIPAL5.js.map → en-D0t_2Fq_.js.map} +1 -1
  104. package/dist/cdn/chunks/{es-BSiH7YkP.js → es-DwDoNuLM.js} +6 -1
  105. package/dist/cdn/chunks/{es-BSiH7YkP.js.map → es-DwDoNuLM.js.map} +1 -1
  106. package/dist/cdn/chunks/{extensions-DBWF_lxx.js → extensions-5jgtarD-.js} +30 -26
  107. package/dist/cdn/chunks/{extensions-DBWF_lxx.js.map → extensions-5jgtarD-.js.map} +1 -1
  108. package/dist/{fr-DTVJ3det.js → cdn/chunks/fr-CSEEzv1_.js} +7 -0
  109. package/dist/cdn/chunks/{fr-DTVJ3det.js.map → fr-CSEEzv1_.js.map} +1 -1
  110. package/dist/cdn/chunks/{htmlparser-CZa2V5cF.js → htmlparser-CPhs2cYE.js} +3 -5
  111. package/dist/cdn/chunks/{htmlparser-CZa2V5cF.js.map → htmlparser-CPhs2cYE.js.map} +1 -1
  112. package/dist/cdn/chunks/{icons-Cqxvwgz7.js → icons-Cp4ZIJw-.js} +31 -31
  113. package/dist/cdn/chunks/{icons-Cqxvwgz7.js.map → icons-Cp4ZIJw-.js.map} +1 -1
  114. package/dist/cdn/chunks/keys-BxPCfHBH.js +12 -0
  115. package/dist/cdn/chunks/keys-BxPCfHBH.js.map +1 -0
  116. package/dist/cdn/chunks/{liquid.browser-BJOEu6Cg.js → liquid.browser-CZaQe87t.js} +756 -718
  117. package/dist/cdn/chunks/liquid.browser-CZaQe87t.js.map +1 -0
  118. package/dist/cdn/chunks/{nl-BqSquFS3.js → nl-xyk1T2mA.js} +6 -1
  119. package/dist/cdn/chunks/{nl-BqSquFS3.js.map → nl-xyk1T2mA.js.map} +1 -1
  120. package/dist/{pt-BR-CMljK37h.js → cdn/chunks/pt-BR-yr6uOJgn.js} +7 -0
  121. package/dist/cdn/chunks/{pt-BR-CMljK37h.js.map → pt-BR-yr6uOJgn.js.map} +1 -1
  122. package/dist/cdn/chunks/{pusher-R0v5p99W.js → pusher-kUlkqMvR.js} +21 -28
  123. package/dist/cdn/chunks/{pusher-R0v5p99W.js.map → pusher-kUlkqMvR.js.map} +1 -1
  124. package/dist/cdn/chunks/{rolldown-runtime-Dqa2HsxW.js → rolldown-runtime-B0aSnxlc.js} +2 -2
  125. package/dist/cdn/chunks/{src-YVsPFLNJ.js → src-B8W42vT-.js} +153 -153
  126. package/dist/cdn/chunks/{src-YVsPFLNJ.js.map → src-B8W42vT-.js.map} +1 -1
  127. package/dist/cdn/chunks/{src-C45kfA2t.js → src-C417zeUg.js} +22 -26
  128. package/dist/cdn/chunks/{src-C45kfA2t.js.map → src-C417zeUg.js.map} +1 -1
  129. package/dist/cdn/chunks/{src-BCJna-6p.js → src-CZjSXPYq.js} +9 -4
  130. package/dist/cdn/chunks/src-CZjSXPYq.js.map +1 -0
  131. package/dist/cdn/chunks/{src-XQmI_Xs1.js → src-Dpj0x27M.js} +523 -787
  132. package/dist/cdn/chunks/{src-XQmI_Xs1.js.map → src-Dpj0x27M.js.map} +1 -1
  133. package/dist/cdn/chunks/{styles-BohMfIBd.js → styles-DANwMzW2.js} +770 -751
  134. package/dist/cdn/chunks/styles-DANwMzW2.js.map +1 -0
  135. package/dist/cdn/chunks/timeouts-SsLMC4a3.js +6 -0
  136. package/dist/cdn/chunks/{timeouts-Bmi_yePw.js.map → timeouts-SsLMC4a3.js.map} +1 -1
  137. package/dist/cdn/chunks/{tiptap-DRKu__AX.js → tiptap-rsib432n.js} +132 -119
  138. package/dist/cdn/chunks/tiptap-rsib432n.js.map +1 -0
  139. package/dist/cdn/chunks/{useAliveFlag-aanU8uqB.js → useAliveFlag-CseYNikC.js} +2 -2
  140. package/dist/cdn/chunks/{useAliveFlag-aanU8uqB.js.map → useAliveFlag-CseYNikC.js.map} +1 -1
  141. package/dist/cdn/chunks/{useCloudI18n-Bem8DUjH.js → useCloudI18n-CjVzv11v.js} +3 -3
  142. package/dist/cdn/chunks/{useCloudI18n-Bem8DUjH.js.map → useCloudI18n-CjVzv11v.js.map} +1 -1
  143. package/dist/cdn/chunks/{useEditorCore-B_vwuKdI.js → useEditorCore-DfW-thCr.js} +484 -411
  144. package/dist/cdn/chunks/useEditorCore-DfW-thCr.js.map +1 -0
  145. package/dist/cdn/chunks/{useI18n-graz19nI.js → useI18n-DniEFeIl.js} +3 -3
  146. package/dist/cdn/chunks/{useI18n-graz19nI.js.map → useI18n-DniEFeIl.js.map} +1 -1
  147. package/dist/cdn/chunks/{useMergeTag-rTzAFMlr.js → useMergeTag-Bk9_f5cc.js} +7 -7
  148. package/dist/cdn/chunks/{useMergeTag-rTzAFMlr.js.map → useMergeTag-Bk9_f5cc.js.map} +1 -1
  149. package/dist/cdn/chunks/{usePopoverPosition-BaFxLP3w.js → usePopoverPosition-C4xFJCJ2.js} +3 -3
  150. package/dist/cdn/chunks/{usePopoverPosition-BaFxLP3w.js.map → usePopoverPosition-C4xFJCJ2.js.map} +1 -1
  151. package/dist/cdn/editor.css +1 -1
  152. package/dist/cdn/editor.js +24 -22
  153. package/dist/cdn/editor.js.map +1 -1
  154. package/dist/{check-BpQDKiPz.js → check-CIhIDh4h.js} +1 -1
  155. package/dist/{chevron-down-j8GrdTyz.js → chevron-down-CZ0m6G3s.js} +1 -1
  156. package/dist/{chevron-up-BACyVhyC.js → chevron-up-BH_tJLeb.js} +1 -1
  157. package/dist/{circle-alert-CgtE46ND.js → circle-alert-DkEXnGPV.js} +1 -1
  158. package/dist/{clock-BzKLLW5L.js → clock-BDIJZUET.js} +1 -1
  159. package/dist/{cloud-DsUct4Zq.js → cloud-BZn3jqoL.js} +10 -14
  160. package/dist/{createLucideIcon-JWHwTI6N.js → createLucideIcon-AEltGIse.js} +3 -3
  161. package/dist/{cdn/chunks/de-CUeaFJph.js → de-CieSMCUp.js} +5 -2
  162. package/dist/{dist-BAkGuPYm.js → dist-B6Px6GPj.js} +1 -1
  163. package/dist/{dist-9uhI32Q4.js → dist-BxbTVeY0.js} +1 -1
  164. package/dist/{dist-DWu9QWFl.js → dist-CIqhVQ1O.js} +5 -5
  165. package/dist/{dist-DPHqs0Yh.js → dist-CQ0fVBQ3.js} +8 -3
  166. package/dist/{dist-CkCVlKj_.js → dist-CRH880TV.js} +1 -1
  167. package/dist/{dist-ByJVlKWy.js → dist-CcQFPJMF.js} +116 -100
  168. package/dist/{dist-JXOq1hib.js → dist-CkPvZCld.js} +1 -1
  169. package/dist/{dist-IGetXrbO.js → dist-DSIgxp0v.js} +1 -1
  170. package/dist/{dist-DYv_Pttj.js → dist-DT1XnEnj.js} +33 -17
  171. package/dist/{dist-C_kAE1sC.js → dist-DX4x6mQ1.js} +3 -3
  172. package/dist/{dist-DFO896yT.js → dist-Dx71pluS.js} +16 -11
  173. package/dist/{dist-CQiAZlTt.js → dist-DyCHUyRR.js} +1 -1
  174. package/dist/{dist-BIYm-U9x.js → dist-iCEDH3cC.js} +1 -1
  175. package/dist/{dist-C5tigvDF.js → dist-nR6mxyqI.js} +1 -1
  176. package/dist/{cdn/chunks/en-zCYIPAL5.js → en-D0t_2Fq_.js} +5 -2
  177. package/dist/{es-BSiH7YkP.js → es-DwDoNuLM.js} +5 -0
  178. package/dist/{extensions-C0DXnYdA.js → extensions-BjvyU9Ea.js} +16 -12
  179. package/dist/{cdn/chunks/fr-DTVJ3det.js → fr-CSEEzv1_.js} +5 -2
  180. package/dist/{image-up-CQdtq8ge.js → image-up-8ktdYdaZ.js} +1 -1
  181. package/dist/index.d.ts +26 -0
  182. package/dist/{info-BUJBXoE6.js → info-CvrCgTLH.js} +1 -1
  183. package/dist/keys-BFPiNAIn.js +10 -0
  184. package/dist/{liquid.browser-DZLtv26k.js → liquid.browser-BECWFL5Z.js} +755 -717
  185. package/dist/{list-checks-BGHaCSD5.js → list-checks-yzGT93ZL.js} +1 -1
  186. package/dist/{loader-circle-C-Z6kgI6.js → loader-circle-B6euendB.js} +1 -1
  187. package/dist/{message-circle-CEm5qckV.js → message-circle-BMldLVcA.js} +1 -1
  188. package/dist/{nl-BqSquFS3.js → nl-xyk1T2mA.js} +5 -0
  189. package/dist/{pencil-DdwmkKLb.js → pencil-CkKkSd6N.js} +1 -1
  190. package/dist/{cdn/chunks/pt-BR-CMljK37h.js → pt-BR-yr6uOJgn.js} +5 -2
  191. package/dist/{refresh-cw-maKlg0dl.js → refresh-cw-fBPgB97G.js} +1 -1
  192. package/dist/{runtime-dom.esm-bundler-GdMI8WqG.js → runtime-dom.esm-bundler-CBbDt7sw.js} +2 -2
  193. package/dist/{scan-line-BFgesTOY.js → scan-line-CRoEOUiK.js} +1 -1
  194. package/dist/{send-B70aQDrA.js → send-B9Tt2qBi.js} +1 -1
  195. package/dist/{shield-check-B7x5OKt5.js → shield-check-QfZkYeIo.js} +1 -1
  196. package/dist/{sparkles-DVlm1Ipp.js → sparkles-8UeMmZb8.js} +1 -1
  197. package/dist/style.css +1 -1
  198. package/dist/{styles-BTTD0Je4.js → styles-C9GM-bBC.js} +913 -894
  199. package/dist/templatical-editor.js +96 -94
  200. package/dist/{text-align-start-Vay3Kvo2.js → text-align-start-Dn8uIheW.js} +1 -1
  201. package/dist/{trash-2-DE8CmxAG.js → trash-2-DDyvqj0E.js} +1 -1
  202. package/dist/{triangle-alert-Djg6IG8N.js → triangle-alert-B3PydLzl.js} +1 -1
  203. package/dist/{upload-BwwE0U6G.js → upload-hE8ItfyG.js} +2 -2
  204. package/dist/{useAliveFlag-C43EhZFY.js → useAliveFlag-AfoTE_AY.js} +1 -1
  205. package/dist/{useCloudI18n-DGr3b2SX.js → useCloudI18n-B2lItQ6O.js} +2 -2
  206. package/dist/{useEditorCore-DOrGex_t.js → useEditorCore-Ck_hBiGA.js} +986 -915
  207. package/dist/{useI18n-BhAejA-x.js → useI18n-C1KcdX_K.js} +2 -2
  208. package/dist/{useMergeTag-Ce95YIOO.js → useMergeTag-DHCwRMfH.js} +4 -4
  209. package/dist/{usePopoverPosition-vjKJaqJf.js → usePopoverPosition-ClnEh_Cp.js} +13 -13
  210. package/dist/{vue.runtime.esm-bundler-B0xoGkDv.js → vue.runtime.esm-bundler-Xpz-L9Fn.js} +27 -22
  211. package/dist/{x-CkD6A1iB.js → x-C4lETLDx.js} +1 -1
  212. package/package.json +12 -12
  213. package/dist/DesignReferenceSidebar-C4pTMzQE.js +0 -264
  214. package/dist/TplModal-DPH4vV8M.js +0 -49
  215. package/dist/cdn/chunks/BlockPreviewCanvas-Dc4F4HzY.js.map +0 -1
  216. package/dist/cdn/chunks/CloudEditor-CeKaWWO5.js.map +0 -1
  217. package/dist/cdn/chunks/DesignReferenceSidebar-BjlaP69l.js +0 -223
  218. package/dist/cdn/chunks/TestEmailModal-CiPfOn2v.js.map +0 -1
  219. package/dist/cdn/chunks/TplModal-Cu4tBBnF.js +0 -50
  220. package/dist/cdn/chunks/TplModal-Cu4tBBnF.js.map +0 -1
  221. package/dist/cdn/chunks/dist-CiBEt0VR.js.map +0 -1
  222. package/dist/cdn/chunks/dist-CxTt8FRE.js.map +0 -1
  223. package/dist/cdn/chunks/keys-B5uoP9P7.js +0 -12
  224. package/dist/cdn/chunks/keys-B5uoP9P7.js.map +0 -1
  225. package/dist/cdn/chunks/liquid.browser-BJOEu6Cg.js.map +0 -1
  226. package/dist/cdn/chunks/src-BCJna-6p.js.map +0 -1
  227. package/dist/cdn/chunks/styles-BohMfIBd.js.map +0 -1
  228. package/dist/cdn/chunks/timeouts-Bmi_yePw.js +0 -6
  229. package/dist/cdn/chunks/tiptap-DRKu__AX.js.map +0 -1
  230. package/dist/cdn/chunks/useEditorCore-B_vwuKdI.js.map +0 -1
  231. package/dist/keys-BVlG5Ozc.js +0 -10
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEditorCore-DfW-thCr.js","names":[],"sources":["../../../../core/src/editor.ts","../../../../core/src/history.ts","../../../../core/src/block-actions.ts","../../../../core/src/auto-save.ts","../../../../core/src/condition-preview.ts","../../../../core/src/data-source-fetch.ts","../../../../core/src/history-interceptor.ts","../../../../core/src/saved-blocks.ts","../../../../core/src/saved-blocks-local.ts","../../../src/utils/activeEditorTracker.ts","../../../src/composables/usePreviewResolution.ts","../../../src/composables/useImageUrlResolver.ts","../../../src/composables/useMergeTagPicker.ts","../../../src/composables/useLogicTagPicker.ts","../../../src/composables/useTemplateLint.ts","../../../src/utils/blockTypeLabels.ts","../../../src/composables/useKeyboardReorder.ts","../../../src/composables/useUiTheme.ts","../../../src/composables/useThemeStyles.ts","../../../src/utils/logger.ts","../../../src/composables/useBlockRegistry.ts","../../../src/composables/useCustomBlockStylesheets.ts","../../../src/utils/registerBuiltInBlocks.ts","../../../src/utils/resolveHtmlBlockPreview.ts","../../../src/utils/collectOffPaletteDefaults.ts","../../../src/utils/resolveFieldColors.ts","../../../src/utils/collectColorFieldIssues.ts","../../../src/utils/keyboardShortcuts.ts","../../../src/utils/mergeTagLabelSegments.ts","../../../src/components/MergeTagPreviewText.vue","../../../src/components/MergeTagPreviewText.vue","../../../src/components/blocks/ButtonBlock.vue","../../../src/components/blocks/ButtonBlock.vue","../../../src/components/CustomBlockIcon.vue","../../../src/components/CustomBlockIcon.vue","../../../src/composables/useEditorRoot.ts","../../../src/composables/useFocusTrap.ts","../../../src/composables/useEmoji.ts","../../../src/composables/useDragDrop.ts","../../../src/composables/useFonts.ts","../../../src/composables/useLogicTag.ts","../../../src/utils/mergeTagNodeSpec.ts","../../../src/utils/insertLogicTag.ts","../../../src/utils/linkColorExtension.ts","../../../src/composables/useRichTextLinkDialog.ts","../../../src/composables/useRichTextEditor.ts","../../../src/composables/useSmallScreenNotice.ts","../../../src/components/blocks/CustomBlock.vue","../../../src/components/blocks/CustomBlock.vue","../../../src/components/blocks/DividerBlock.vue","../../../src/components/blocks/DividerBlock.vue","../../../src/components/blocks/HtmlBlock.vue","../../../src/components/blocks/HtmlBlock.vue","../../../src/composables/useImageDrop.ts","../../../src/components/blocks/ImageBlock.vue","../../../src/components/blocks/ImageBlock.vue","../../../src/components/blocks/MenuBlock.vue","../../../src/components/blocks/MenuBlock.vue","../../../src/utils/sanitizeRichTextHtml.ts","../../../src/composables/useEditableTextBlock.ts","../../../src/components/blocks/ParagraphBlock.vue","../../../src/components/blocks/ParagraphBlock.vue","../../../src/utils/blockComponentResolver.ts","../../../src/components/blocks/BlockWrapper.vue","../../../src/components/blocks/BlockWrapper.vue","../../../src/utils/unwrapParagraph.ts","../../../src/components/blocks/TitleBlock.vue","../../../src/components/blocks/TitleBlock.vue","../../../src/utils/sectionColumnDrop.ts","../../../src/components/blocks/SectionBlock.vue","../../../src/components/blocks/SectionBlock.vue","../../../src/constants/socialIcons.ts","../../../src/components/blocks/SocialIconSvg.vue","../../../src/components/blocks/SocialIconSvg.vue","../../../src/components/blocks/SocialIconsBlock.vue","../../../src/components/blocks/SocialIconsBlock.vue","../../../src/components/blocks/SpacerBlock.vue","../../../src/components/blocks/SpacerBlock.vue","../../../src/components/blocks/TableBlock.vue","../../../src/components/blocks/TableBlock.vue","../../../src/components/blocks/VideoPlayButton.vue","../../../src/components/blocks/VideoPlayButton.vue","../../../src/utils/videoThumbnail.ts","../../../src/components/blocks/VideoBlock.vue","../../../src/components/blocks/VideoBlock.vue","../../../src/composables/useEditorCore.ts"],"sourcesContent":["import type {\n Block,\n ColumnLayout,\n TemplateContent,\n TemplateDefaults,\n TemplateSettings,\n UiTheme,\n ViewportSize,\n} from \"@templatical/types\";\nimport { createDefaultTemplateContent } from \"@templatical/types\";\n\nfunction getColumnCount(layout: ColumnLayout): number {\n if (layout === \"1\") return 1;\n if (layout === \"3\") return 3;\n return 2;\n}\nimport {\n computed,\n reactive,\n readonly,\n type DeepReadonly,\n type Ref,\n} from \"@vue/reactivity\";\n\nexport interface EditorState {\n content: TemplateContent;\n selectedBlockId: string | null;\n viewport: ViewportSize;\n darkMode: boolean;\n previewMode: boolean;\n isDirty: boolean;\n uiTheme: UiTheme;\n}\n\nexport interface UseEditorOptions {\n content: TemplateContent;\n defaultFontFamily?: string;\n templateDefaults?: TemplateDefaults;\n lockedBlocks?: Ref<Map<string, unknown>>;\n}\n\nexport interface UseEditorReturn {\n state: DeepReadonly<EditorState>;\n content: Ref<TemplateContent>;\n selectedBlock: Ref<Block | null>;\n setContent: (content: TemplateContent, markDirty?: boolean) => void;\n selectBlock: (blockId: string | null) => void;\n setViewport: (viewport: ViewportSize) => void;\n setDarkMode: (darkMode: boolean) => void;\n setPreviewMode: (previewMode: boolean) => void;\n setUiTheme: (theme: UiTheme) => void;\n updateBlock: (blockId: string, updates: Partial<Block>) => void;\n updateSettings: (updates: Partial<TemplateSettings>) => void;\n addBlock: (\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n index?: number,\n ) => void;\n removeBlock: (blockId: string) => void;\n moveBlock: (\n blockId: string,\n newIndex: number,\n targetSectionId?: string,\n columnIndex?: number,\n ) => void;\n isBlockLocked: (blockId: string) => boolean;\n markDirty: () => void;\n findBlockLocation: (blockId: string) => {\n targetSectionId?: string;\n columnIndex?: number;\n index: number;\n } | null;\n}\n\nexport function useEditor(options: UseEditorOptions): UseEditorReturn {\n const state = reactive<EditorState>({\n content:\n options.content ??\n createDefaultTemplateContent(\n options.defaultFontFamily,\n options.templateDefaults,\n ),\n selectedBlockId: null,\n viewport: \"desktop\",\n darkMode: false,\n previewMode: false,\n isDirty: false,\n uiTheme: \"auto\",\n });\n\n const content = computed({\n get: () => state.content,\n set: (value: TemplateContent) => {\n state.content = value;\n state.isDirty = true;\n },\n });\n\n const selectedBlock = computed(() => {\n if (!state.selectedBlockId) return null;\n return findBlockById(state.content.blocks, state.selectedBlockId);\n });\n\n function findBlockById(blocks: Block[], id: string): Block | null {\n for (const block of blocks) {\n if (block.id === id) return block;\n if (block.type === \"section\") {\n for (const column of block.children) {\n const found = findBlockById(column, id);\n if (found) return found;\n }\n }\n }\n return null;\n }\n\n function collectBlockIds(block: Block, ids: Set<string>): void {\n ids.add(block.id);\n if (block.type === \"section\") {\n for (const column of block.children) {\n for (const child of column) {\n collectBlockIds(child, ids);\n }\n }\n }\n }\n\n function findBlockParent(\n blocks: Block[],\n id: string,\n parent: {\n blocks: Block[];\n sectionId?: string;\n columnIndex?: number;\n } = { blocks },\n ): { blocks: Block[]; sectionId?: string; columnIndex?: number } | null {\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i];\n if (block.id === id) return parent;\n if (block.type === \"section\") {\n for (let colIdx = 0; colIdx < block.children.length; colIdx++) {\n const result = findBlockParent(block.children[colIdx], id, {\n blocks: block.children[colIdx],\n sectionId: block.id,\n columnIndex: colIdx,\n });\n if (result) return result;\n }\n }\n }\n return null;\n }\n\n function isBlockLocked(blockId: string): boolean {\n return options.lockedBlocks?.value.has(blockId) ?? false;\n }\n\n function findBlockLocation(blockId: string): {\n targetSectionId?: string;\n columnIndex?: number;\n index: number;\n } | null {\n const parent = findBlockParent(state.content.blocks, blockId);\n if (!parent) return null;\n const index = parent.blocks.findIndex((b) => b.id === blockId);\n if (index === -1) return null;\n return {\n targetSectionId: parent.sectionId,\n columnIndex: parent.columnIndex,\n index,\n };\n }\n\n // TODO(collab): the lock checks in addBlock/moveBlock/removeBlock/updateBlock\n // are shallow — they only consider the directly-targeted block id. A section\n // can still be removed, moved, or have its `children` array rewritten while a\n // peer is editing one of its descendants, which silently disrupts that peer's\n // edit. Add a `hasLockedDescendant(blockId)` helper and gate section-level\n // operations on it (and on the parent of each affected child) so cascades\n // through the tree are also blocked.\n\n function setContent(newContent: TemplateContent, markDirty = true): void {\n state.content = newContent;\n if (markDirty) {\n state.isDirty = true;\n }\n }\n\n function selectBlock(blockId: string | null): void {\n if (blockId && isBlockLocked(blockId)) {\n return;\n }\n state.selectedBlockId = blockId;\n }\n\n function setViewport(viewport: ViewportSize): void {\n state.viewport = viewport;\n }\n\n function setDarkMode(darkMode: boolean): void {\n state.darkMode = darkMode;\n }\n\n function setUiTheme(theme: UiTheme): void {\n state.uiTheme = theme;\n }\n\n function setPreviewMode(previewMode: boolean): void {\n state.previewMode = previewMode;\n if (previewMode) {\n state.selectedBlockId = null;\n }\n }\n\n function updateBlock(blockId: string, updates: Partial<Block>): void {\n if (isBlockLocked(blockId)) {\n return;\n }\n const block = findBlockById(state.content.blocks, blockId);\n if (block) {\n Object.assign(block, updates);\n state.isDirty = true;\n }\n }\n\n function updateSettings(updates: Partial<TemplateSettings>): void {\n state.content.settings = { ...state.content.settings, ...updates };\n state.isDirty = true;\n }\n\n function addBlock(\n block: Block,\n targetSectionId?: string,\n columnIndex = 0,\n index?: number,\n ): void {\n // Sections cannot be nested inside a column — MJML forbids `mj-section`\n // inside `mj-column`, so the renderer drops them on export (issue #292).\n // Reject the nest up front rather than lose the content silently later.\n if (targetSectionId && block.type === \"section\") {\n return;\n }\n if (targetSectionId) {\n if (isBlockLocked(targetSectionId)) {\n return;\n }\n const section = findBlockById(state.content.blocks, targetSectionId);\n if (section && section.type === \"section\") {\n if (columnIndex < 0 || columnIndex >= getColumnCount(section.columns)) {\n return;\n }\n section.children[columnIndex] = section.children[columnIndex] || [];\n const targetArray = section.children[columnIndex];\n if (index !== undefined && index < targetArray.length) {\n targetArray.splice(index, 0, block);\n } else {\n targetArray.push(block);\n }\n }\n } else {\n if (index !== undefined && index < state.content.blocks.length) {\n state.content.blocks.splice(index, 0, block);\n } else {\n state.content.blocks.push(block);\n }\n }\n state.isDirty = true;\n }\n\n function removeBlock(blockId: string): void {\n if (isBlockLocked(blockId)) {\n return;\n }\n const parent = findBlockParent(state.content.blocks, blockId);\n if (parent) {\n const index = parent.blocks.findIndex((b) => b.id === blockId);\n if (index !== -1) {\n const [removed] = parent.blocks.splice(index, 1);\n if (state.selectedBlockId) {\n const removedIds = new Set<string>();\n collectBlockIds(removed, removedIds);\n if (removedIds.has(state.selectedBlockId)) {\n state.selectedBlockId = null;\n }\n }\n state.isDirty = true;\n }\n }\n }\n\n function moveBlock(\n blockId: string,\n newIndex: number,\n targetSectionId?: string,\n columnIndex = 0,\n ): void {\n if (isBlockLocked(blockId)) {\n return;\n }\n if (targetSectionId && isBlockLocked(targetSectionId)) {\n return;\n }\n\n const parent = findBlockParent(state.content.blocks, blockId);\n if (!parent) return;\n\n const oldIndex = parent.blocks.findIndex((b) => b.id === blockId);\n if (oldIndex === -1) return;\n\n // Sections cannot be nested inside a column (issue #292) — refuse to move\n // a section into a section column; MJML would drop it on export.\n if (targetSectionId && parent.blocks[oldIndex].type === \"section\") {\n return;\n }\n\n // Resolve target before mutating the source — otherwise an invalid\n // targetSectionId leaves the block spliced-out and unrecoverable.\n let targetArray: Block[];\n if (targetSectionId) {\n const section = findBlockById(state.content.blocks, targetSectionId);\n if (!section || section.type !== \"section\") return;\n if (columnIndex < 0 || columnIndex >= getColumnCount(section.columns)) {\n return;\n }\n section.children[columnIndex] = section.children[columnIndex] || [];\n targetArray = section.children[columnIndex];\n } else {\n targetArray = state.content.blocks;\n }\n\n const [block] = parent.blocks.splice(oldIndex, 1);\n targetArray.splice(newIndex, 0, block);\n\n state.isDirty = true;\n }\n\n function markDirty(): void {\n state.isDirty = true;\n }\n\n return {\n state: readonly(state),\n content,\n selectedBlock,\n isBlockLocked,\n setContent,\n selectBlock,\n setViewport,\n setDarkMode,\n setUiTheme,\n setPreviewMode,\n updateBlock,\n updateSettings,\n addBlock,\n removeBlock,\n moveBlock,\n markDirty,\n findBlockLocation,\n };\n}\n","import { safeClone, type TemplateContent } from \"@templatical/types\";\nimport { computed, ref, type ComputedRef, type Ref } from \"@vue/reactivity\";\n\nexport interface UseHistoryOptions {\n content: Ref<TemplateContent>;\n setContent: (content: TemplateContent, markDirty?: boolean) => void;\n isRemoteOperation?: () => boolean;\n maxSize?: number;\n}\n\nexport interface UseHistoryReturn {\n canUndo: ComputedRef<boolean>;\n canRedo: ComputedRef<boolean>;\n isNavigating: Ref<boolean>;\n undo: () => void;\n redo: () => void;\n record: () => void;\n recordDebounced: (blockId: string) => void;\n clear: () => void;\n destroy: () => void;\n}\n\ninterface DebouncedSnapshot {\n blockId: string;\n timeoutId: ReturnType<typeof setTimeout>;\n}\n\nconst MAX_STACK_SIZE = 50;\nconst DEBOUNCE_MS = 300;\nconst NAVIGATE_IDLE_MS = 1500;\n\nexport function useHistory(options: UseHistoryOptions): UseHistoryReturn {\n const {\n content,\n setContent,\n isRemoteOperation,\n maxSize = MAX_STACK_SIZE,\n } = options;\n\n const undoStack = ref<TemplateContent[]>([]);\n const redoStack = ref<TemplateContent[]>([]);\n const isNavigating = ref(false);\n let navigatingTimeoutId: ReturnType<typeof setTimeout> | null = null;\n let pendingDebounce: DebouncedSnapshot | null = null;\n\n const canUndo = computed(() => undoStack.value.length > 0);\n const canRedo = computed(() => redoStack.value.length > 0);\n\n function cloneContent(): TemplateContent {\n // Cycle-safe deep clone (see `safeClone`). A naked JSON.stringify throws\n // `Converting circular structure to JSON` if a DOM element with a Sortable\n // expando back-ref ever sneaks into block data via a drag handler — which\n // would freeze the undo stack, since every subsequent mutation calls\n // `record()`. safeClone drops the back-ref instead of throwing.\n return safeClone(content.value);\n }\n\n function pushToUndoStack(snapshot: TemplateContent): void {\n undoStack.value.push(snapshot);\n if (undoStack.value.length > maxSize) {\n undoStack.value.splice(0, undoStack.value.length - maxSize);\n }\n }\n\n function flushPendingDebounce(): void {\n if (pendingDebounce) {\n clearTimeout(pendingDebounce.timeoutId);\n pendingDebounce = null;\n }\n }\n\n function record(): void {\n if (isRemoteOperation?.()) {\n return;\n }\n\n flushPendingDebounce();\n pushToUndoStack(cloneContent());\n redoStack.value = [];\n }\n\n function recordDebounced(blockId: string): void {\n if (isRemoteOperation?.()) {\n return;\n }\n\n if (pendingDebounce && pendingDebounce.blockId === blockId) {\n clearTimeout(pendingDebounce.timeoutId);\n pendingDebounce.timeoutId = setTimeout(() => {\n pendingDebounce = null;\n }, DEBOUNCE_MS);\n return;\n }\n\n flushPendingDebounce();\n\n pushToUndoStack(cloneContent());\n redoStack.value = [];\n\n pendingDebounce = {\n blockId,\n timeoutId: setTimeout(() => {\n pendingDebounce = null;\n }, DEBOUNCE_MS),\n };\n }\n\n function setNavigating(): void {\n isNavigating.value = true;\n if (navigatingTimeoutId) {\n clearTimeout(navigatingTimeoutId);\n }\n navigatingTimeoutId = setTimeout(() => {\n isNavigating.value = false;\n navigatingTimeoutId = null;\n }, NAVIGATE_IDLE_MS);\n }\n\n function undo(): void {\n if (undoStack.value.length === 0) {\n return;\n }\n\n flushPendingDebounce();\n\n const snapshot = undoStack.value.pop()!;\n redoStack.value.push(cloneContent());\n setContent(snapshot, true);\n setNavigating();\n }\n\n function redo(): void {\n if (redoStack.value.length === 0) {\n return;\n }\n\n flushPendingDebounce();\n\n const snapshot = redoStack.value.pop()!;\n undoStack.value.push(cloneContent());\n setContent(snapshot, true);\n setNavigating();\n }\n\n function clear(): void {\n undoStack.value = [];\n redoStack.value = [];\n flushPendingDebounce();\n }\n\n function destroy(): void {\n clear();\n if (navigatingTimeoutId) {\n clearTimeout(navigatingTimeoutId);\n navigatingTimeoutId = null;\n }\n }\n\n return {\n canUndo,\n canRedo,\n isNavigating,\n undo,\n redo,\n record,\n recordDebounced,\n clear,\n destroy,\n };\n}\n","import type { Block, BlockDefaults, BlockType } from \"@templatical/types\";\nimport { createBlock, generateId } from \"@templatical/types\";\n\nfunction regenerateNestedIds(block: Block): void {\n if (block.type === \"table\") {\n block.rows = block.rows.map((row) => ({\n ...row,\n id: generateId(),\n cells: row.cells.map((cell) => ({ ...cell, id: generateId() })),\n }));\n } else if (block.type === \"social\") {\n block.icons = block.icons.map((icon) => ({ ...icon, id: generateId() }));\n } else if (block.type === \"menu\") {\n block.items = block.items.map((item) => ({ ...item, id: generateId() }));\n }\n}\n\nexport interface UseBlockActionsOptions {\n addBlock: (\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n index?: number,\n ) => void;\n removeBlock: (blockId: string) => void;\n updateBlock: (blockId: string, updates: Partial<Block>) => void;\n selectBlock: (blockId: string | null) => void;\n /** Locate a block in the tree — used by `duplicateBlock` to insert the\n * clone right after the source instead of appending to the end. */\n findBlockLocation?: (blockId: string) => {\n targetSectionId?: string;\n columnIndex?: number;\n index: number;\n } | null;\n blockDefaults?: BlockDefaults;\n}\n\nexport interface UseBlockActionsReturn {\n createAndAddBlock: (\n type: BlockType,\n targetSectionId?: string,\n columnIndex?: number,\n ) => Block;\n duplicateBlock: (\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n ) => Block;\n deleteBlock: (blockId: string) => void;\n updateBlockProperty: <K extends keyof Block>(\n blockId: string,\n key: K,\n value: Block[K],\n ) => void;\n}\n\nexport function useBlockActions(\n options: UseBlockActionsOptions,\n): UseBlockActionsReturn {\n const { addBlock, removeBlock, updateBlock, selectBlock, findBlockLocation } =\n options;\n\n function createAndAddBlock(\n type: BlockType,\n targetSectionId?: string,\n columnIndex?: number,\n ): Block {\n const block = createBlock(type, options.blockDefaults);\n addBlock(block, targetSectionId, columnIndex);\n selectBlock(block.id);\n return block;\n }\n\n function duplicateBlock(\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n ): Block {\n const cloned = JSON.parse(JSON.stringify(block)) as Block;\n cloned.id = generateId();\n regenerateNestedIds(cloned);\n\n if (cloned.type === \"section\") {\n cloned.children = cloned.children.map((column) =>\n column.map((child) => {\n const clonedChild = JSON.parse(JSON.stringify(child)) as Block;\n clonedChild.id = generateId();\n regenerateNestedIds(clonedChild);\n return clonedChild;\n }),\n );\n }\n\n // Insert directly after the source block. Explicit target args win;\n // otherwise, resolve the source's location and bump index by 1. Falls\n // back to appending at the end if location is unknown.\n if (targetSectionId !== undefined || columnIndex !== undefined) {\n addBlock(cloned, targetSectionId, columnIndex);\n } else {\n const sourceLocation = findBlockLocation?.(block.id) ?? null;\n if (sourceLocation) {\n addBlock(\n cloned,\n sourceLocation.targetSectionId,\n sourceLocation.columnIndex,\n sourceLocation.index + 1,\n );\n } else {\n addBlock(cloned, targetSectionId, columnIndex);\n }\n }\n selectBlock(cloned.id);\n return cloned;\n }\n\n function deleteBlock(blockId: string): void {\n removeBlock(blockId);\n }\n\n function updateBlockProperty<K extends keyof Block>(\n blockId: string,\n key: K,\n value: Block[K],\n ): void {\n updateBlock(blockId, { [key]: value } as Partial<Block>);\n }\n\n return {\n createAndAddBlock,\n duplicateBlock,\n deleteBlock,\n updateBlockProperty,\n };\n}\n","import type { TemplateContent } from \"@templatical/types\";\nimport { watch, type Ref } from \"@vue/reactivity\";\n\nexport interface UseAutoSaveOptions {\n content: Ref<TemplateContent>;\n isDirty: () => boolean;\n onChange: (content: TemplateContent) => void;\n debounce?: number;\n enabled?: boolean | (() => boolean);\n}\n\nexport interface UseAutoSaveReturn {\n flush: () => void;\n cancel: () => void;\n pause: () => void;\n resume: () => void;\n destroy: () => void;\n}\n\nexport function useAutoSave(options: UseAutoSaveOptions): UseAutoSaveReturn {\n const {\n content,\n isDirty,\n onChange,\n debounce = 1000,\n enabled = true,\n } = options;\n\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let paused = false;\n\n function isEnabled(): boolean {\n return typeof enabled === \"function\" ? enabled() : enabled;\n }\n\n function pause(): void {\n paused = true;\n cancel();\n }\n\n function resume(): void {\n paused = false;\n }\n\n function cancel(): void {\n if (timeoutId) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n }\n\n function flush(): void {\n cancel();\n if (isDirty()) {\n onChange(JSON.parse(JSON.stringify(content.value)));\n }\n }\n\n function scheduleOnChange(): void {\n if (!isEnabled() || paused) return;\n\n cancel();\n timeoutId = setTimeout(() => {\n timeoutId = null;\n if (isEnabled() && !paused && isDirty()) {\n onChange(JSON.parse(JSON.stringify(content.value)));\n }\n }, debounce);\n }\n\n const stopWatch = watch(\n content,\n () => {\n if (isEnabled() && !paused && isDirty()) {\n scheduleOnChange();\n }\n },\n { deep: true },\n );\n\n function destroy(): void {\n stopWatch();\n cancel();\n }\n\n return {\n flush,\n cancel,\n pause,\n resume,\n destroy,\n };\n}\n","import type { UseEditorReturn } from \"./editor\";\nimport { computed, reactive, type ComputedRef } from \"@vue/reactivity\";\n\nexport interface UseConditionPreviewReturn {\n isHidden: (blockId: string) => boolean;\n toggleBlock: (blockId: string) => void;\n reset: () => void;\n hasHiddenBlocks: ComputedRef<boolean>;\n}\n\nexport function useConditionPreview(\n editor: UseEditorReturn,\n): UseConditionPreviewReturn {\n const hiddenBlockIds = reactive(new Set<string>());\n\n const hasHiddenBlocks = computed(() => hiddenBlockIds.size > 0);\n\n function isHidden(blockId: string): boolean {\n return hiddenBlockIds.has(blockId);\n }\n\n function toggleBlock(blockId: string): void {\n if (hiddenBlockIds.has(blockId)) {\n hiddenBlockIds.delete(blockId);\n } else {\n hiddenBlockIds.add(blockId);\n\n if (editor.state.selectedBlockId === blockId) {\n editor.selectBlock(null);\n }\n }\n }\n\n function reset(): void {\n hiddenBlockIds.clear();\n }\n\n return {\n isHidden,\n toggleBlock,\n reset,\n hasHiddenBlocks,\n };\n}\n","import type { CustomBlock, CustomBlockDefinition } from \"@templatical/types\";\nimport type { ComputedRef, Ref } from \"@vue/reactivity\";\nimport { computed, ref } from \"@vue/reactivity\";\n\nexport function useDataSourceFetch(options: {\n definition: ComputedRef<CustomBlockDefinition | undefined>;\n block: ComputedRef<CustomBlock>;\n onUpdate: (fieldValues: Record<string, unknown>, fetched: boolean) => void;\n}): {\n isFetching: Ref<boolean>;\n fetchError: Ref<boolean>;\n fetch: () => Promise<void>;\n hasDataSource: ComputedRef<boolean>;\n needsFetch: ComputedRef<boolean>;\n} {\n const isFetching = ref(false);\n const fetchError = ref(false);\n\n const hasDataSource = computed(() => !!options.definition.value?.dataSource);\n\n const needsFetch = computed(\n () => hasDataSource.value && !options.block.value.dataSourceFetched,\n );\n\n async function fetch(): Promise<void> {\n const def = options.definition.value;\n if (!def?.dataSource) {\n return;\n }\n\n isFetching.value = true;\n fetchError.value = false;\n\n try {\n const result = await def.dataSource.onFetch({\n fieldValues: { ...options.block.value.fieldValues },\n blockId: options.block.value.id,\n });\n\n if (result == null) {\n return;\n }\n\n const merged = { ...options.block.value.fieldValues };\n for (const key of Object.keys(merged)) {\n if (key in result) {\n merged[key] = result[key];\n }\n }\n\n options.onUpdate(merged, true);\n } catch (error) {\n console.warn(\"[Templatical] Data source fetch error:\", error);\n fetchError.value = true;\n } finally {\n isFetching.value = false;\n }\n }\n\n return {\n isFetching,\n fetchError,\n fetch,\n hasDataSource,\n needsFetch,\n };\n}\n","import type { UseEditorReturn } from \"./editor\";\nimport type { UseHistoryReturn } from \"./history\";\n\n/**\n * Wraps editor mutation methods to record history snapshots before each\n * operation. Mutates the editor object in place.\n *\n * Must be applied **after** any collaboration broadcast wrapping so the\n * call chain is: history.record() → broadcast → original mutation.\n */\nexport function useHistoryInterceptor(\n editor: UseEditorReturn,\n history: UseHistoryReturn,\n): void {\n const originalAddBlock = editor.addBlock;\n const originalRemoveBlock = editor.removeBlock;\n const originalMoveBlock = editor.moveBlock;\n const originalUpdateBlock = editor.updateBlock;\n const originalUpdateSettings = editor.updateSettings;\n\n // Skip recording when the underlying op is a no-op (e.g., a peer-locked\n // block or section), otherwise the undo stack fills with snapshots that\n // are identical to current state and undo silently does nothing.\n editor.addBlock = (block, targetSectionId?, columnIndex?, index?) => {\n if (targetSectionId && editor.isBlockLocked(targetSectionId)) {\n return;\n }\n history.record();\n originalAddBlock(block, targetSectionId, columnIndex, index);\n };\n\n editor.removeBlock = (blockId) => {\n if (editor.isBlockLocked(blockId)) {\n return;\n }\n history.record();\n originalRemoveBlock(blockId);\n };\n\n editor.moveBlock = (blockId, newIndex, targetSectionId?, columnIndex?) => {\n if (editor.isBlockLocked(blockId)) {\n return;\n }\n if (targetSectionId && editor.isBlockLocked(targetSectionId)) {\n return;\n }\n history.record();\n originalMoveBlock(blockId, newIndex, targetSectionId, columnIndex);\n };\n\n editor.updateBlock = (blockId, updates) => {\n if (editor.isBlockLocked(blockId)) {\n return;\n }\n history.recordDebounced(blockId);\n originalUpdateBlock(blockId, updates);\n };\n\n editor.updateSettings = (updates) => {\n history.record();\n originalUpdateSettings(updates);\n };\n}\n","import type {\n Block,\n SavedBlock,\n SavedBlockPatch,\n SavedBlocksListParams,\n SavedBlocksProvider,\n} from \"@templatical/types\";\nimport { SdkError } from \"@templatical/types\";\nimport { computed, ref, type ComputedRef, type Ref } from \"vue\";\n\nexport interface UseSavedBlocksOptions {\n /**\n * Storage backend. Supplied by the consumer via `init({ savedBlocks })`, or\n * by Cloud's own adapter — this composable is transport-agnostic and never\n * talks to a network itself.\n */\n provider: SavedBlocksProvider;\n onError?: (error: Error) => void;\n}\n\nexport interface UseSavedBlocksReturn {\n /** In-memory mirror of the provider's list, kept in sync after each call. */\n savedBlocks: Ref<SavedBlock[]>;\n isLoading: Ref<boolean>;\n /**\n * Distinct categories across the loaded entries, alphabetically sorted.\n *\n * Derived rather than stored: a category exists exactly as long as an entry\n * carries it. Sorting here is not a violation of \"the editor never re-sorts\"\n * — that rule protects the provider's ordering of *entries*, whereas these\n * are strings with no given order.\n */\n categories: ComputedRef<string[]>;\n /**\n * Whether the provider supplied each mutation at all — `false` on the\n * provider means the capability does not exist for this user, and the UI\n * hides the corresponding affordance entirely.\n *\n * Constant for the provider's lifetime, but exposed as computed refs so\n * consumers can bind them the same way as everything else here.\n */\n canCreate: ComputedRef<boolean>;\n canUpdate: ComputedRef<boolean>;\n canDelete: ComputedRef<boolean>;\n /**\n * Whether a *particular* entry may be edited / deleted: the capability must\n * exist AND the entry must not opt out via `canUpdate` / `canDelete`.\n *\n * Use these rather than reading the flags directly, so every surface asks the\n * question the same way and the \"absent means allowed\" default lives in one\n * place.\n */\n canUpdateBlock: (block: SavedBlock) => boolean;\n canDeleteBlock: (block: SavedBlock) => boolean;\n /**\n * Re-read from the provider. The editor always calls this bare and filters\n * in memory; `params` exists for headless callers that want the provider to\n * filter instead (see {@link SavedBlocksListParams}).\n */\n load: (params?: SavedBlocksListParams) => Promise<void>;\n /** Rejects with an {@link SdkError} when the provider disabled `create`. */\n create: (\n name: string,\n content: Block[],\n category?: string,\n ) => Promise<SavedBlock>;\n /** Rejects when `update` is disabled or the entry opts out of editing. */\n update: (id: string, patch: SavedBlockPatch) => Promise<SavedBlock>;\n /** Rejects when `delete` is disabled or the entry opts out of deletion. */\n remove: (id: string) => Promise<void>;\n}\n\n/**\n * Reactive state over a {@link SavedBlocksProvider}.\n *\n * Owns the list, the loading flag, and the local mutations that keep the list\n * consistent after each successful call, so a provider only has to implement\n * four promise-returning methods. Errors are reported through `onError` and\n * re-thrown, leaving the local list untouched on failure.\n */\nexport function useSavedBlocks(\n options: UseSavedBlocksOptions,\n): UseSavedBlocksReturn {\n const { provider } = options;\n\n const savedBlocks = ref<SavedBlock[]>([]);\n const isLoading = ref(false);\n\n const categories = computed(() => {\n const seen = new Set<string>();\n for (const block of savedBlocks.value) {\n const category = block.category?.trim();\n if (category) seen.add(category);\n }\n return [...seen].sort((a, b) => a.localeCompare(b));\n });\n\n const canCreate = computed(() => typeof provider.create === \"function\");\n const canUpdate = computed(() => typeof provider.update === \"function\");\n const canDelete = computed(() => typeof provider.delete === \"function\");\n\n /** Absent flag means allowed — the flags exist only to forbid. */\n function canUpdateBlock(block: SavedBlock): boolean {\n return canUpdate.value && block.canUpdate !== false;\n }\n\n function canDeleteBlock(block: SavedBlock): boolean {\n return canDelete.value && block.canDelete !== false;\n }\n\n /**\n * The UI hides disabled actions, so reaching one of these means a programmatic\n * caller went around it. Fail loudly rather than silently no-op: a resolved\n * promise would read as \"saved\" to whoever awaited it.\n */\n function refuse(action: string, reason: string): never {\n throw new SdkError(\n `[Templatical] Saved blocks: ${action} is ${reason}. Check the capability before calling — the editor's own UI hides the action.`,\n );\n }\n\n async function load(params?: SavedBlocksListParams): Promise<void> {\n isLoading.value = true;\n try {\n savedBlocks.value = await provider.list(params);\n } catch (error) {\n options.onError?.(error as Error);\n throw error;\n } finally {\n isLoading.value = false;\n }\n }\n\n async function create(\n name: string,\n content: Block[],\n category?: string,\n ): Promise<SavedBlock> {\n const { create: providerCreate } = provider;\n if (typeof providerCreate !== \"function\") {\n refuse(\"create\", \"disabled by the provider\");\n }\n try {\n // Omit the key entirely when unset, rather than sending `undefined` —\n // a provider serialising the input to JSON would otherwise be handed an\n // explicit null-ish field it never asked for.\n const created = await providerCreate(\n category ? { name, content, category } : { name, content },\n );\n savedBlocks.value = [created, ...savedBlocks.value];\n return created;\n } catch (error) {\n options.onError?.(error as Error);\n throw error;\n }\n }\n\n async function update(\n id: string,\n patch: SavedBlockPatch,\n ): Promise<SavedBlock> {\n const { update: providerUpdate } = provider;\n if (typeof providerUpdate !== \"function\") {\n refuse(\"update\", \"disabled by the provider\");\n }\n // An id absent from the local list isn't refused — a headless caller may\n // legitimately patch something it never loaded. Only a loaded entry that\n // explicitly opted out is blocked.\n const local = savedBlocks.value.find((b) => b.id === id);\n if (local && local.canUpdate === false) {\n refuse(\"update\", `not permitted for entry \"${id}\"`);\n }\n try {\n const updated = await providerUpdate(id, patch);\n savedBlocks.value = savedBlocks.value.map((b) =>\n b.id === id ? updated : b,\n );\n return updated;\n } catch (error) {\n options.onError?.(error as Error);\n throw error;\n }\n }\n\n async function remove(id: string): Promise<void> {\n const { delete: providerDelete } = provider;\n if (typeof providerDelete !== \"function\") {\n refuse(\"delete\", \"disabled by the provider\");\n }\n const local = savedBlocks.value.find((b) => b.id === id);\n if (local && local.canDelete === false) {\n refuse(\"delete\", `not permitted for entry \"${id}\"`);\n }\n try {\n await providerDelete(id);\n savedBlocks.value = savedBlocks.value.filter((b) => b.id !== id);\n } catch (error) {\n options.onError?.(error as Error);\n throw error;\n }\n }\n\n return {\n savedBlocks,\n isLoading,\n categories,\n canCreate,\n canUpdate,\n canDelete,\n canUpdateBlock,\n canDeleteBlock,\n load,\n create,\n update,\n remove,\n };\n}\n","import type {\n SavedBlock,\n SavedBlockInput,\n SavedBlockPatch,\n SavedBlocksListParams,\n SavedBlocksProvider,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\n\nexport interface LocalStorageSavedBlocksOptions {\n /**\n * `localStorage` key holding the serialized array.\n *\n * @default \"templatical:saved-blocks\"\n */\n key?: string;\n}\n\nconst DEFAULT_KEY = \"templatical:saved-blocks\";\n\n/**\n * Browser-local {@link SavedBlocksProvider} backed by `localStorage`.\n *\n * Opt-in: pass it explicitly as `init({ savedBlocks: createLocalStorageSavedBlocksProvider() })`.\n * The editor never falls back to it, so consumers without a provider keep the\n * feature — and its UI — entirely off.\n *\n * Intended for demos, prototypes, and single-device use. Entries live in one\n * browser profile only: they don't sync across devices or users, and clearing\n * site data removes them. Back saved blocks with your own API for anything\n * that needs to outlive a browser profile.\n */\nexport function createLocalStorageSavedBlocksProvider(\n options: LocalStorageSavedBlocksOptions = {},\n): SavedBlocksProvider {\n const storageKey = options.key ?? DEFAULT_KEY;\n\n function getStorage(): Storage {\n // Core targets a neutral platform, so `localStorage` isn't guaranteed —\n // fail with an actionable message rather than a bare ReferenceError.\n if (typeof localStorage === \"undefined\") {\n throw new Error(\n \"[Templatical] createLocalStorageSavedBlocksProvider requires a browser environment with localStorage. Supply your own SavedBlocksProvider for server-side or non-browser use.\",\n );\n }\n return localStorage;\n }\n\n function readAll(): SavedBlock[] {\n const raw = getStorage().getItem(storageKey);\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n // Tolerate a corrupted or hand-edited value rather than breaking the\n // editor: treat anything that isn't an array as \"no saved blocks\".\n return Array.isArray(parsed) ? (parsed as SavedBlock[]) : [];\n } catch {\n return [];\n }\n }\n\n function writeAll(blocks: SavedBlock[]): void {\n getStorage().setItem(storageKey, JSON.stringify(blocks));\n }\n\n // Every method is `async` so a synchronous failure (a missing\n // `localStorage`, a quota error from `setItem`) surfaces as a rejected\n // promise rather than a sync throw — callers of a `Promise`-returning\n // contract must be able to rely on `.catch()`.\n return {\n // The editor calls this bare and filters in memory; the params are honored\n // for headless callers that would rather the provider did the work.\n async list(params?: SavedBlocksListParams): Promise<SavedBlock[]> {\n const all = readAll();\n const search = params?.search?.trim().toLowerCase();\n const category = params?.category?.trim();\n return all.filter((b) => {\n if (search && !b.name.toLowerCase().includes(search)) return false;\n if (category && b.category !== category) return false;\n return true;\n });\n },\n\n // All three mutations are enabled: a browser-local store has no notion of\n // permissions, so there is nothing to forbid.\n async create(input: SavedBlockInput): Promise<SavedBlock> {\n const now = new Date().toISOString();\n const created: SavedBlock = {\n id: generateId(),\n name: input.name,\n content: input.content,\n ...(input.category ? { category: input.category } : {}),\n createdAt: now,\n updatedAt: now,\n };\n // Newest-first, matching the order the composable applies locally after\n // a create — so a reload preserves what the user just saw.\n writeAll([created, ...readAll()]);\n return created;\n },\n\n async update(id: string, patch: SavedBlockPatch): Promise<SavedBlock> {\n const all = readAll();\n const index = all.findIndex((b) => b.id === id);\n // Mirrors a REST 404 so provider behavior is consistent across adapters.\n if (index === -1) {\n throw new Error(`[Templatical] Saved block not found: ${id}`);\n }\n const updated: SavedBlock = {\n ...all[index],\n ...patch,\n updatedAt: new Date().toISOString(),\n };\n const next = [...all];\n next[index] = updated;\n writeAll(next);\n return updated;\n },\n\n async delete(id: string): Promise<void> {\n const all = readAll();\n if (!all.some((b) => b.id === id)) {\n throw new Error(`[Templatical] Saved block not found: ${id}`);\n }\n writeAll(all.filter((b) => b.id !== id));\n },\n };\n}\n","/**\n * Module-level registry of editor instances mounted on the page. When\n * two or more editors mount concurrently, the global `document` keydown\n * listener registered by each `useEditorCore` would otherwise fire on\n * every instance for a single keystroke (double undo, double save).\n * The tracker routes shortcuts to a single active instance based on the\n * user's most recent pointer interaction.\n *\n * Single-instance pages keep the original behavior (always active).\n */\n\nconst instances = new Set<number>();\nlet lastActiveId = 0;\nlet counter = 0;\n\nexport interface EditorInstanceHandle {\n id: number;\n /** True when this instance should handle global keyboard shortcuts. */\n isActive: () => boolean;\n /** Mark this instance as active (typically on user pointer interaction). */\n claim: () => void;\n /** Remove this instance from the registry on unmount. */\n dispose: () => void;\n}\n\nexport function registerEditorInstance(): EditorInstanceHandle {\n const id = ++counter;\n instances.add(id);\n if (lastActiveId === 0) lastActiveId = id;\n return {\n id,\n isActive: () => instances.size <= 1 || lastActiveId === id,\n claim: () => {\n lastActiveId = id;\n },\n dispose: () => {\n instances.delete(id);\n if (lastActiveId === id) {\n const remaining = Array.from(instances);\n lastActiveId = remaining[remaining.length - 1] ?? 0;\n }\n },\n };\n}\n\n/** Test-only: reset module state between tests. */\nexport function _resetActiveEditorTrackerForTests(): void {\n instances.clear();\n lastActiveId = 0;\n counter = 0;\n}\n","import {\n isRenderableTemplateContent,\n safeClone,\n type PreviewResolveContext,\n type ResolvePreview,\n type TemplateContent,\n} from \"@templatical/types\";\nimport {\n computed,\n onScopeDispose,\n ref,\n watch,\n type ComputedRef,\n type Ref,\n} from \"vue\";\n\n/**\n * Runs the consumer's `resolvePreview` hook and owns everything about its\n * lifecycle: debouncing, discarding stale results, shape-checking what comes\n * back, and degrading when it fails.\n *\n * Every preview surface reads `content` from here rather than calling the hook\n * itself, so the failure and loading behaviour cannot diverge between the\n * editor's preview mode and the test-email dialog.\n *\n * Not to be confused with `MergeTag.sample`, which substitutes value tags at\n * *display* time. This hook exists for **logic** tags: branching cannot be\n * evaluated client-side for every supported syntax, so only a backend can do\n * it. When a resolver is configured it supersedes samples entirely — see\n * `supersedesSamples`.\n */\n\n/** How long to wait before resolving, matching `useTemplateLint`'s debounce. */\nconst DEBOUNCE_MS = 500;\n\nexport interface UsePreviewResolutionOptions {\n /** The consumer's hook. `undefined` disables the whole feature. */\n resolvePreview?: ResolvePreview;\n /** The template to resolve, read live so edits are picked up on re-resolve. */\n getContent: () => TemplateContent;\n /**\n * Whether a preview is currently showing. Resolution only runs while true,\n * so an editor that never enters preview mode never calls the hook.\n */\n isActive: () => boolean;\n /** The recipient to resolve for, when the surface has one. */\n getRecipient?: () => string | undefined;\n}\n\nexport interface UsePreviewResolutionReturn {\n /** True when the consumer configured a hook. */\n isConfigured: boolean;\n /**\n * The content preview surfaces should render: the resolved template, or the\n * unresolved one while resolving has not produced a result (including after\n * a failure).\n */\n content: ComputedRef<TemplateContent>;\n /** True while a resolve call is outstanding. */\n isResolving: Ref<boolean>;\n /**\n * True only when there is nothing to show yet — a *first* resolve. A\n * re-resolve keeps the previous result on screen instead of flashing a\n * skeleton over content that is already correct, the same distinction\n * `SavedBlocksBrowserModal` draws with `isInitialLoad`.\n */\n isInitialResolve: ComputedRef<boolean>;\n /** True when the last attempt failed or returned something unrenderable. */\n hasFailed: Ref<boolean>;\n /**\n * True whenever a resolver is **configured** — deliberately not \"…and has\n * produced content\".\n *\n * A resolver is the consumer's declared intent for how previews should read,\n * so samples are off from the first frame: gating on a landed result made the\n * Sample/Label toggle appear for the debounce plus the resolver's latency and\n * then vanish, which looks like a bug. It also keeps the failure note honest —\n * it says the *unresolved* template is showing, which is only true if samples\n * aren't substituting into the fallback.\n */\n supersedesSamples: ComputedRef<boolean>;\n}\n\nexport function usePreviewResolution(\n options: UsePreviewResolutionOptions,\n): UsePreviewResolutionReturn {\n const isConfigured = typeof options.resolvePreview === \"function\";\n\n const resolved = ref<TemplateContent | null>(null);\n const isResolving = ref(false);\n const hasFailed = ref(false);\n\n /**\n * Bumped on every request. A response is applied only if its token is still\n * current, which is what discards a slow resolve that a newer one has\n * superseded — otherwise switching recipient twice could land the first\n * answer last.\n */\n let token = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function clearTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n async function run(): Promise<void> {\n const hook = options.resolvePreview;\n if (!hook) return;\n\n const mine = ++token;\n isResolving.value = true;\n\n // A copy, so a resolver that mutates what it is given cannot reach editor\n // state. `safeClone` rather than a naked stringify, per the repo's\n // cycle-safety rule.\n const context: PreviewResolveContext = {\n content: safeClone(options.getContent()),\n ...(options.getRecipient?.() !== undefined\n ? { recipient: options.getRecipient() }\n : {}),\n };\n\n try {\n const next = await hook(context);\n if (mine !== token) return; // superseded\n if (!isRenderableTemplateContent(next)) {\n // A consumer bug or a mis-shaped API response degrades to the\n // unresolved template rather than throwing inside the render.\n hasFailed.value = true;\n return;\n }\n resolved.value = next;\n hasFailed.value = false;\n } catch {\n if (mine !== token) return;\n // Deliberately not routed to `config.onError`: a degraded preview is\n // user-visible and non-fatal, and reporting it there reads as more\n // severe than it is.\n hasFailed.value = true;\n } finally {\n if (mine === token) isResolving.value = false;\n }\n }\n\n function schedule(): void {\n if (!isConfigured) return;\n clearTimer();\n\n // Nothing on screen yet — resolve **now**, no debounce. Debouncing the\n // first resolve is pure latency: there is nothing to coalesce at the moment\n // a preview opens, and waiting meant the unresolved template rendered for\n // 500ms before the skeleton even appeared. Showing edit-like content and\n // *then* a skeleton is worse than either on its own.\n //\n // `run()` sets `isResolving` synchronously before its first await, so the\n // skeleton is up in the same frame as the click.\n if (resolved.value === null) {\n void run();\n return;\n }\n\n // Something correct is already showing, so a re-resolve (recipient change)\n // debounces — that is the case the delay exists for.\n timer = setTimeout(() => {\n timer = null;\n void run();\n }, DEBOUNCE_MS);\n }\n\n // Resolve when a preview opens and when the recipient changes. Deliberately\n // *not* watching content: re-resolving on every edit would mean a backend\n // request per keystroke burst.\n watch(\n () => [options.isActive(), options.getRecipient?.()] as const,\n ([active]) => {\n if (!active) {\n // Leaving a preview cancels an in-flight resolve and drops the result,\n // so reopening resolves fresh rather than showing stale data.\n clearTimer();\n token++;\n isResolving.value = false;\n resolved.value = null;\n hasFailed.value = false;\n return;\n }\n schedule();\n },\n { immediate: true },\n );\n\n onScopeDispose(() => {\n clearTimer();\n token++;\n });\n\n return {\n isConfigured,\n content: computed(() => resolved.value ?? options.getContent()),\n isResolving,\n isInitialResolve: computed(\n () => isResolving.value && resolved.value === null,\n ),\n hasFailed,\n supersedesSamples: computed(() => isConfigured),\n };\n}\n","import {\n computed,\n getCurrentScope,\n inject,\n onScopeDispose,\n reactive,\n watch,\n type ComputedRef,\n} from \"vue\";\nimport { IMAGE_URL_RESOLVER_KEY } from \"../keys\";\nimport { RESOLVE_IMAGE_DEBOUNCE_MS } from \"../constants/timeouts\";\n\n/**\n * Display-only resolver for image `src` values (`config.resolveImageUrl`,\n * #415). Maps a canonical src (e.g. a plain file name like `logo.png`) to a\n * URL the canvas can actually display (e.g. an ephemeral `blob:` URL).\n * Returning `null` (or the input value) means \"use the src as-is\". The\n * resolved value never enters the content model or the MJML export.\n */\nexport type ResolveImageUrl = (\n src: string,\n) => string | null | Promise<string | null>;\n\n/**\n * Per-editor cache around the host's `resolveImageUrl` callback. Each src is\n * resolved at most once for the editor's lifetime — repeated lookups (same\n * image in several blocks, re-mounts while scrolling) hit the cache, and a\n * failed lookup caches `null` so the host service is never hammered with\n * retries for the same value.\n */\nexport interface ImageUrlResolver {\n /**\n * Cached result for a src: the resolved URL, `null` for \"use as-is\"\n * (also the failure fallback), or `undefined` when the src was never\n * looked up or the lookup is still in flight.\n */\n get(src: string): string | null | undefined;\n /** True when the src is cached or a lookup for it is in flight. */\n has(src: string): boolean;\n /** Start a lookup unless the src is already cached or in flight. */\n lookup(src: string): void;\n}\n\nexport function createImageUrlResolver(\n resolve: ResolveImageUrl,\n): ImageUrlResolver {\n // Reactive so `useResolvedImageSrc`'s computed re-evaluates when an async\n // lookup settles into the cache.\n const cache = reactive(new Map<string, string | null>());\n const inFlight = new Set<string>();\n\n function get(src: string): string | null | undefined {\n return cache.get(src);\n }\n\n function has(src: string): boolean {\n return cache.has(src) || inFlight.has(src);\n }\n\n function lookup(src: string): void {\n if (has(src)) return;\n\n let result: string | null | Promise<string | null>;\n try {\n result = resolve(src);\n } catch {\n cache.set(src, null);\n return;\n }\n\n if (result === null || typeof result === \"string\") {\n cache.set(src, result);\n return;\n }\n\n inFlight.add(src);\n Promise.resolve(result)\n .then(\n (value) => {\n // Normalize a misbehaving resolver (undefined, non-string) to null.\n cache.set(src, typeof value === \"string\" ? value : null);\n },\n () => {\n cache.set(src, null);\n },\n )\n .finally(() => inFlight.delete(src));\n }\n\n return { get, has, lookup };\n}\n\n/**\n * Component-facing display value for an image src. Passes the src through\n * unchanged when no `resolveImageUrl` is configured. With a resolver, the\n * returned computed shows the resolved preview URL once available and the\n * canonical src otherwise (while pending, or when the resolver opted out\n * with `null`).\n *\n * Lookup cadence: the initial src resolves immediately (template load),\n * subsequent changes are debounced by `RESOLVE_IMAGE_DEBOUNCE_MS` so typing\n * in the src input never sends partial values (`lo`, `logo.p`, …) to the\n * host resolver. Already-cached values apply instantly either way.\n *\n * Pass `undefined` from the getter to opt a value out of resolution\n * entirely (e.g. a merge-tag src, which is never displayable).\n */\nexport function useResolvedImageSrc(\n src: () => string | undefined,\n): ComputedRef<string | undefined> {\n const resolver = inject(IMAGE_URL_RESOLVER_KEY, null);\n const srcRef = computed(src);\n if (!resolver) return srcRef;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n function clearTimer(): void {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n }\n if (getCurrentScope()) {\n onScopeDispose(clearTimer);\n }\n\n let initial = true;\n watch(\n srcRef,\n (value) => {\n clearTimer();\n const immediate = initial;\n initial = false;\n if (!value || resolver.has(value)) return;\n if (immediate) {\n resolver.lookup(value);\n return;\n }\n timer = setTimeout(() => {\n timer = undefined;\n resolver.lookup(value);\n }, RESOLVE_IMAGE_DEBOUNCE_MS);\n },\n { immediate: true },\n );\n\n return computed(() => {\n const value = srcRef.value;\n if (!value) return value;\n const resolved = resolver.get(value);\n return typeof resolved === \"string\" && resolved.length > 0\n ? resolved\n : value;\n });\n}\n","import type { MergeTag } from \"@templatical/types\";\nimport { getCurrentScope, onScopeDispose, ref, type Ref } from \"vue\";\n\nexport interface UseMergeTagPickerReturn {\n /** Whether the picker modal is currently open. */\n isOpen: Ref<boolean>;\n /** The merge tags currently shown in the picker. */\n tags: Ref<MergeTag[]>;\n /**\n * Open the picker with the given tags. Returns a promise that resolves\n * with the selected tag when the user clicks or presses Enter, or `null`\n * when the user cancels (Esc, Cancel button, backdrop click, or modal\n * teardown). If `open()` is called while another promise is pending, the\n * previous promise resolves with `null` (latest-wins) and a fresh one is\n * returned.\n */\n open: (tags: MergeTag[]) => Promise<MergeTag | null>;\n /**\n * Resolve the currently pending promise. Safe no-op when none is pending.\n * Closing the modal must always flow through this — direct `isOpen.value`\n * flips would leak the pending promise.\n */\n resolve: (tag: MergeTag | null) => void;\n}\n\n/**\n * Singleton state for the built-in merge tag picker modal. One instance\n * per editor (`useEditorCore` instantiates and provides it). The composable\n * holds a single pending resolver — concurrent `open()` calls supersede\n * the previous one (latest wins). On scope dispose, any in-flight promise\n * is resolved with `null` so callers awaiting the picker never leak.\n */\nexport function useMergeTagPicker(): UseMergeTagPickerReturn {\n const isOpen = ref(false);\n const tags = ref<MergeTag[]>([]);\n\n let pendingResolver: ((tag: MergeTag | null) => void) | null = null;\n\n function open(nextTags: MergeTag[]): Promise<MergeTag | null> {\n // Latest-wins: a fresh open() call cancels the previous pending promise.\n if (pendingResolver) {\n const previous = pendingResolver;\n pendingResolver = null;\n previous(null);\n }\n tags.value = nextTags;\n isOpen.value = true;\n return new Promise<MergeTag | null>((resolvePromise) => {\n pendingResolver = resolvePromise;\n });\n }\n\n function resolve(tag: MergeTag | null): void {\n const resolver = pendingResolver;\n pendingResolver = null;\n isOpen.value = false;\n tags.value = [];\n if (resolver) {\n resolver(tag);\n }\n }\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n // Dispose-while-open: caller awaiting the picker must not hang.\n if (pendingResolver) {\n const resolver = pendingResolver;\n pendingResolver = null;\n resolver(null);\n }\n isOpen.value = false;\n tags.value = [];\n });\n }\n\n return {\n isOpen,\n tags,\n open,\n resolve,\n };\n}\n","import type { LogicPair, LogicTag } from \"@templatical/types\";\nimport { getCurrentScope, onScopeDispose, ref, type Ref } from \"vue\";\n\n/** A logic picker selection: a standalone tag or a paired construct. */\nexport type LogicTagPickerResult = LogicTag | LogicPair;\n\nexport interface UseLogicTagPickerReturn {\n /** Whether the picker modal is currently open. */\n isOpen: Ref<boolean>;\n /** Standalone logic tags shown in the picker's main list. */\n tags: Ref<LogicTag[]>;\n /** Paired constructs shown in the picker's \"Blocks\" section. */\n pairs: Ref<LogicPair[]>;\n /**\n * Open the picker with the given tags + pairs. Resolves with the selected\n * tag/pair on click or Enter, or `null` on cancel (Esc, backdrop, teardown).\n * A fresh `open()` while one is pending resolves the previous with `null`\n * (latest-wins).\n */\n open: (\n tags: LogicTag[],\n pairs: LogicPair[],\n ) => Promise<LogicTagPickerResult | null>;\n /**\n * Resolve the currently pending promise. Safe no-op when none is pending.\n * Closing the modal must always flow through this so the pending promise\n * never leaks.\n */\n resolve: (item: LogicTagPickerResult | null) => void;\n}\n\n/**\n * Singleton state for the built-in logic picker modal. One instance per editor\n * (`useEditorCore` instantiates and provides it). Holds a single pending\n * resolver — concurrent `open()` calls supersede the previous one (latest\n * wins). On scope dispose, any in-flight promise resolves with `null`.\n */\nexport function useLogicTagPicker(): UseLogicTagPickerReturn {\n const isOpen = ref(false);\n const tags = ref<LogicTag[]>([]);\n const pairs = ref<LogicPair[]>([]);\n\n let pendingResolver: ((item: LogicTagPickerResult | null) => void) | null =\n null;\n\n function open(\n nextTags: LogicTag[],\n nextPairs: LogicPair[],\n ): Promise<LogicTagPickerResult | null> {\n if (pendingResolver) {\n const previous = pendingResolver;\n pendingResolver = null;\n previous(null);\n }\n tags.value = nextTags;\n pairs.value = nextPairs;\n isOpen.value = true;\n return new Promise<LogicTagPickerResult | null>((resolvePromise) => {\n pendingResolver = resolvePromise;\n });\n }\n\n function resolve(item: LogicTagPickerResult | null): void {\n const resolver = pendingResolver;\n pendingResolver = null;\n isOpen.value = false;\n tags.value = [];\n pairs.value = [];\n if (resolver) {\n resolver(item);\n }\n }\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n if (pendingResolver) {\n const resolver = pendingResolver;\n pendingResolver = null;\n resolver(null);\n }\n isOpen.value = false;\n tags.value = [];\n pairs.value = [];\n });\n }\n\n return {\n isOpen,\n tags,\n pairs,\n open,\n resolve,\n };\n}\n","import { ref, shallowRef, watch, type Ref } from \"vue\";\nimport { watchDebounced } from \"@vueuse/core\";\nimport type {\n Block,\n TemplateContent,\n TemplateSettings,\n} from \"@templatical/types\";\n// Type-only import — runtime is dynamically loaded.\nimport type {\n LintIssue,\n LintOptions,\n lintTemplate as LintTemplateFn,\n} from \"@templatical/quality\";\n\nexport type { LintIssue, LintOptions } from \"@templatical/quality\";\n\n/**\n * Mirrors `isLintFullyDisabled` from `@templatical/quality`. Duplicated\n * here to avoid a static runtime import of the optional peer, which would\n * defeat the editor's lazy-load gate. Keep the two in sync.\n */\nexport function isLintFullyDisabled(options: LintOptions | undefined): boolean {\n if (!options) return false;\n if (options.disabled === true) return true;\n return (\n options.accessibility === false &&\n options.structure === false &&\n options.links === false\n );\n}\n\nexport interface UseTemplateLintOptions {\n content: Ref<TemplateContent>;\n options: LintOptions;\n updateBlock: (blockId: string, updates: Partial<Block>) => void;\n updateSettings: (updates: Partial<TemplateSettings>) => void;\n removeBlock: (blockId: string) => void;\n /** Debounce in ms; defaults to 500 to match the plan. */\n debounce?: number;\n}\n\nexport interface UseTemplateLintReturn {\n issues: Ref<LintIssue[]>;\n /** True once the quality package has been successfully imported. */\n ready: Ref<boolean>;\n /** True if the dynamic import failed (e.g. package not installed). */\n unavailable: Ref<boolean>;\n applyFix: (issue: LintIssue) => void;\n destroy: () => void;\n}\n\ninterface Loaded {\n lintTemplate: typeof LintTemplateFn;\n}\n\n/**\n * Live template linting. Runs every linter in `@templatical/quality`\n * (accessibility + structure + links) on every content change,\n * debounced 500ms. Lazy-imports the quality package on first call so the\n * OSS bundle pays nothing for the chunk until linting actually starts.\n *\n * When all linting is turned off — either `options.disabled === true` or\n * every per-tool key (`accessibility`, `structure`, `links`) is set to\n * `false` — the dynamic import is skipped entirely and `issues` stays\n * empty. Saves the chunk download for consumers that turn the linter off.\n */\nexport function useTemplateLint(\n opts: UseTemplateLintOptions,\n): UseTemplateLintReturn {\n const issues = ref<LintIssue[]>([]) as Ref<LintIssue[]>;\n const ready = ref(false);\n const unavailable = ref(false);\n const loaded = shallowRef<Loaded | null>(null);\n\n const disabled = isLintFullyDisabled(opts.options);\n\n let stopWatch: (() => void) | null = null;\n let destroyed = false;\n\n if (!disabled) {\n void load();\n }\n\n async function load(): Promise<void> {\n try {\n const mod = await import(\"@templatical/quality\");\n // Bail if the consumer destroyed us during the dynamic import — both\n // to avoid mutating dead refs and to avoid leaking a watcher that\n // destroy() can no longer reach.\n if (destroyed) return;\n loaded.value = {\n lintTemplate: mod.lintTemplate,\n };\n ready.value = true;\n runLint();\n stopWatch = watchDebounced(opts.content, runLint, {\n debounce: opts.debounce ?? 500,\n deep: true,\n });\n } catch {\n if (destroyed) return;\n // The quality package is an optional peer; surface the unavailable\n // flag so consumers can hide UI rather than crash.\n unavailable.value = true;\n }\n }\n\n function runLint(): void {\n if (!loaded.value) return;\n issues.value = loaded.value.lintTemplate(opts.content.value, opts.options);\n }\n\n // Re-run when severity overrides change at runtime (rare, but useful for\n // cloud plan-config merges).\n const stopOptionsWatch = watch(\n () => opts.options,\n () => {\n if (loaded.value) runLint();\n },\n { deep: true },\n );\n\n function applyFix(issue: LintIssue): void {\n if (!issue.fix) return;\n issue.fix.apply({\n updateBlock: opts.updateBlock,\n updateSettings: opts.updateSettings,\n removeBlock: opts.removeBlock,\n });\n // The content watcher will re-lint via the debounced watcher.\n }\n\n function destroy(): void {\n destroyed = true;\n stopWatch?.();\n stopOptionsWatch();\n }\n\n return { issues, ready, unavailable, applyFix, destroy };\n}\n","import type { Translations } from \"../i18n\";\nimport type {\n Block,\n CustomBlock,\n CustomBlockDefinition,\n} from \"@templatical/types\";\n\n/**\n * Returns the i18n label for a built-in block type.\n * Falls back to the raw type string for unknown types.\n */\nexport function getBlockTypeLabel(\n blockType: string,\n translations: Translations,\n): string {\n const labels = translations.blocks as Record<string, string>;\n return labels[blockType] ?? blockType;\n}\n\n/**\n * Display label for a block *instance*, which is what user-facing copy wants:\n * built-ins resolve to their i18n label, custom blocks to the consumer's own\n * name for them (\"Featured Article\"), not the literal type `\"custom\"`.\n *\n * `blocks` carries no `custom` key on purpose — one generic \"Custom\" would make\n * two different custom blocks indistinguishable in a list. The name comes from\n * the definition instead; it is consumer-supplied and therefore NOT localized,\n * which is already how the toolbar and the sidebar palette present it.\n *\n * Falls back to `customType` (the slug) when no definition matches — an\n * instance whose definition was never registered — mirroring `Toolbar.vue`.\n * Pass the array injected under `CUSTOM_BLOCK_DEFINITIONS_KEY`; omitting it\n * degrades to that same slug fallback rather than throwing.\n */\nexport function getBlockLabel(\n block: Block,\n translations: Translations,\n customBlockDefinitions: readonly CustomBlockDefinition[] = [],\n): string {\n if (block.type !== \"custom\") {\n return getBlockTypeLabel(block.type, translations);\n }\n const { customType } = block as CustomBlock;\n const definition = customBlockDefinitions.find((d) => d.type === customType);\n return definition?.name ?? customType;\n}\n","import { ref, type Ref } from \"vue\";\nimport type {\n Block,\n CustomBlockDefinition,\n SectionBlock,\n} from \"@templatical/types\";\nimport type { BaseEditorReturn } from \"./useEditorCore\";\nimport type { UseI18nReturn } from \"./useI18n\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\n\ninterface BlockLocation {\n index: number;\n total: number;\n sectionId?: string;\n columnIndex?: number;\n}\n\nexport interface UseKeyboardReorderReturn {\n liftedBlockId: Ref<string | null>;\n announcement: Ref<string>;\n isLifted: (blockId: string) => boolean;\n lift: (blockId: string) => void;\n cancel: () => void;\n moveUp: (blockId: string) => void;\n moveDown: (blockId: string) => void;\n drop: (blockId: string) => void;\n}\n\n/**\n * Keyboard-driven reorder for blocks — mirrors the GitHub Projects pattern.\n * Space/Enter lifts, arrow keys move, Space/Enter drops, Escape cancels.\n * Moves within the same container (top-level or a section column).\n */\nexport function useKeyboardReorder(\n editor: BaseEditorReturn,\n i18n: UseI18nReturn,\n /**\n * Consumer-registered custom blocks, so announcements name them the way the\n * user knows them (\"Featured Article\") instead of the type `custom`. Passed\n * in rather than injected: this runs inside `useEditorCore`, which already\n * holds the definitions before it provides them.\n */\n customBlockDefinitions: readonly CustomBlockDefinition[] = [],\n): UseKeyboardReorderReturn {\n const liftedBlockId = ref<string | null>(null);\n const announcement = ref(\"\");\n let originalLocation: BlockLocation | null = null;\n\n function findBlockLocation(blockId: string): BlockLocation | null {\n const topLevel = editor.content.value.blocks;\n const topIndex = topLevel.findIndex((b) => b.id === blockId);\n if (topIndex !== -1) {\n return { index: topIndex, total: topLevel.length };\n }\n for (const block of topLevel) {\n if (block.type !== \"section\") continue;\n const section = block as SectionBlock;\n for (let col = 0; col < section.children.length; col++) {\n const column = section.children[col];\n const childIndex = column.findIndex((b: Block) => b.id === blockId);\n if (childIndex !== -1) {\n return {\n index: childIndex,\n total: column.length,\n sectionId: section.id,\n columnIndex: col,\n };\n }\n }\n }\n return null;\n }\n\n function blockLabel(blockId: string): string {\n const location = findBlockLocation(blockId);\n if (!location) return \"\";\n const blocks = location.sectionId\n ? resolveColumnBlocks(location.sectionId, location.columnIndex ?? 0)\n : editor.content.value.blocks;\n const block = blocks?.[location.index];\n if (!block) return \"\";\n return getBlockLabel(block, i18n.t, customBlockDefinitions);\n }\n\n function resolveColumnBlocks(\n sectionId: string,\n columnIndex: number,\n ): Block[] | null {\n const section = editor.content.value.blocks.find(\n (b) => b.id === sectionId && b.type === \"section\",\n ) as SectionBlock | undefined;\n return section?.children[columnIndex] ?? null;\n }\n\n function announce(message: string): void {\n // Reset to empty first so repeated identical announcements still fire.\n announcement.value = \"\";\n queueMicrotask(() => {\n announcement.value = message;\n });\n }\n\n function isLifted(blockId: string): boolean {\n return liftedBlockId.value === blockId;\n }\n\n function lift(blockId: string): void {\n const location = findBlockLocation(blockId);\n if (!location) return;\n liftedBlockId.value = blockId;\n originalLocation = location;\n announce(\n i18n.format(i18n.t.blockActions.lifted, {\n block: blockLabel(blockId),\n position: String(location.index + 1),\n total: String(location.total),\n }),\n );\n }\n\n function move(blockId: string, delta: number): void {\n const location = findBlockLocation(blockId);\n if (!location) return;\n const target = location.index + delta;\n if (target < 0 || target >= location.total) return;\n editor.moveBlock(blockId, target, location.sectionId, location.columnIndex);\n announce(\n i18n.format(i18n.t.blockActions.moved, {\n block: blockLabel(blockId),\n position: String(target + 1),\n total: String(location.total),\n }),\n );\n }\n\n function moveUp(blockId: string): void {\n move(blockId, -1);\n }\n\n function moveDown(blockId: string): void {\n move(blockId, 1);\n }\n\n function drop(blockId: string): void {\n const location = findBlockLocation(blockId);\n if (location) {\n announce(\n i18n.format(i18n.t.blockActions.dropped, {\n block: blockLabel(blockId),\n position: String(location.index + 1),\n total: String(location.total),\n }),\n );\n }\n liftedBlockId.value = null;\n originalLocation = null;\n }\n\n function cancel(): void {\n const id = liftedBlockId.value;\n if (id && originalLocation) {\n const current = findBlockLocation(id);\n const movedAcrossContainers =\n !!current &&\n (current.sectionId !== originalLocation.sectionId ||\n current.columnIndex !== originalLocation.columnIndex);\n const movedWithinContainer =\n !!current && current.index !== originalLocation.index;\n if (current && (movedAcrossContainers || movedWithinContainer)) {\n editor.moveBlock(\n id,\n originalLocation.index,\n originalLocation.sectionId,\n originalLocation.columnIndex,\n );\n }\n announce(\n i18n.format(i18n.t.blockActions.cancelled, {\n block: blockLabel(id),\n position: String(originalLocation.index + 1),\n }),\n );\n }\n liftedBlockId.value = null;\n originalLocation = null;\n }\n\n return {\n liftedBlockId,\n announcement,\n isLifted,\n lift,\n cancel,\n moveUp,\n moveDown,\n drop,\n };\n}\n","import { useMediaQuery } from \"@vueuse/core\";\nimport { computed, type ComputedRef, type Ref } from \"vue\";\nimport type { UiTheme } from \"@templatical/types\";\n\nexport interface UseUiThemeReturn {\n resolvedTheme: ComputedRef<\"light\" | \"dark\">;\n}\n\nexport function useUiTheme(uiTheme: Ref<UiTheme>): UseUiThemeReturn {\n const systemPrefersDark = useMediaQuery(\"(prefers-color-scheme: dark)\");\n\n const resolvedTheme = computed<\"light\" | \"dark\">(() => {\n if (uiTheme.value === \"auto\") {\n return systemPrefersDark.value ? \"dark\" : \"light\";\n }\n return uiTheme.value;\n });\n\n return { resolvedTheme };\n}\n","import type { ThemeOverrides } from \"@templatical/types\";\nimport { computed, type ComputedRef, type Ref } from \"vue\";\n\nconst THEME_VAR_MAPPING: Record<keyof Omit<ThemeOverrides, \"dark\">, string> = {\n bg: \"--tpl-bg\",\n bgElevated: \"--tpl-bg-elevated\",\n bgHover: \"--tpl-bg-hover\",\n bgActive: \"--tpl-bg-active\",\n border: \"--tpl-border\",\n borderLight: \"--tpl-border-light\",\n text: \"--tpl-text\",\n textMuted: \"--tpl-text-muted\",\n textDim: \"--tpl-text-dim\",\n primary: \"--tpl-primary\",\n primaryHover: \"--tpl-primary-hover\",\n primaryLight: \"--tpl-primary-light\",\n secondary: \"--tpl-secondary\",\n secondaryHover: \"--tpl-secondary-hover\",\n secondaryLight: \"--tpl-secondary-light\",\n success: \"--tpl-success\",\n successLight: \"--tpl-success-light\",\n warning: \"--tpl-warning\",\n warningLight: \"--tpl-warning-light\",\n danger: \"--tpl-danger\",\n dangerLight: \"--tpl-danger-light\",\n canvasBg: \"--tpl-canvas-bg\",\n};\n\nexport interface UseThemeStylesOptions {\n themeOverrides: Ref<ThemeOverrides>;\n resolvedTheme: ComputedRef<string>;\n extraStyles?: () => Record<string, string>;\n}\n\nexport interface UseThemeStylesReturn {\n themeStyles: ComputedRef<Record<string, string>>;\n}\n\nexport function useThemeStyles({\n themeOverrides,\n resolvedTheme,\n extraStyles,\n}: UseThemeStylesOptions): UseThemeStylesReturn {\n const themeStyles = computed(() => {\n const styles: Record<string, string> = {};\n const base = themeOverrides.value;\n const isDark = resolvedTheme.value === \"dark\";\n // In dark mode, only apply consumer's dark overrides as inline styles.\n // Base (light) overrides must NOT be inlined — they'd pin CSS variables to\n // light values and prevent the CSS [data-tpl-theme=\"dark\"] block from working.\n const overrides = isDark ? base.dark : base;\n\n if (overrides) {\n for (const [key, cssVar] of Object.entries(THEME_VAR_MAPPING)) {\n const k = key as keyof Omit<ThemeOverrides, \"dark\">;\n const value = overrides[k];\n if (value) {\n styles[cssVar] = value;\n }\n }\n }\n\n if (extraStyles) {\n Object.assign(styles, extraStyles());\n }\n\n return styles;\n });\n\n return { themeStyles };\n}\n","/**\n * Lightweight logger for the editor package.\n *\n * In production builds, `warn` and `error` still emit so consumers can surface\n * SDK issues, but tagged with `[Templatical]` for filtering. `debug`/`info` are\n * compiled out in production via the bundler's dead-code elimination.\n */\n\nconst TAG = \"[Templatical]\";\n\nfunction isProduction(): boolean {\n // `process.env.NODE_ENV` is replaced statically by Vite/tsup in consumer builds.\n // Guard access so pure browser environments (no process) don't throw.\n return (\n typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\"\n );\n}\n\nexport const logger = {\n warn(...args: unknown[]): void {\n console.warn(TAG, ...args);\n },\n error(...args: unknown[]): void {\n console.error(TAG, ...args);\n },\n debug(...args: unknown[]): void {\n if (!isProduction()) {\n console.debug(TAG, ...args);\n }\n },\n info(...args: unknown[]): void {\n if (!isProduction()) {\n console.info(TAG, ...args);\n }\n },\n};\n","/** @see https://templatical.com/docs/v1/custom-blocks */\n\nimport type {\n Block,\n CustomBlock,\n CustomBlockDefinition,\n} from \"@templatical/types\";\nimport { createCustomBlock } from \"@templatical/types\";\nimport { type Component, shallowRef, triggerRef } from \"vue\";\nimport { logger } from \"../utils/logger\";\n\nexport interface BlockRegistration {\n component: Component;\n createBlock: () => Block;\n definition?: CustomBlockDefinition;\n}\n\nexport interface UseBlockRegistryReturn {\n registerBuiltIn: (type: string, registration: BlockRegistration) => void;\n registerCustom: (\n definition: CustomBlockDefinition,\n component: Component,\n ) => void;\n getComponent: (block: Block) => Component | undefined;\n createBlock: (type: string) => Block | undefined;\n getDefinition: (customType: string) => CustomBlockDefinition | undefined;\n renderCustomBlock: (block: CustomBlock) => Promise<string>;\n isRegistered: (type: string) => boolean;\n}\n\nexport function useBlockRegistry(): UseBlockRegistryReturn {\n const registry = shallowRef(new Map<string, BlockRegistration>());\n\n function registerBuiltIn(\n type: string,\n registration: BlockRegistration,\n ): void {\n registry.value.set(type, registration);\n triggerRef(registry);\n }\n\n function registerCustom(\n definition: CustomBlockDefinition,\n component: Component,\n ): void {\n const registrationKey = `custom:${definition.type}`;\n const registration: BlockRegistration = {\n component,\n createBlock: () => createCustomBlock(definition),\n definition,\n };\n\n registry.value.set(registrationKey, registration);\n triggerRef(registry);\n }\n\n function getComponent(block: Block): Component | undefined {\n if (block.type === \"custom\") {\n const key = `custom:${(block as CustomBlock).customType}`;\n return registry.value.get(key)?.component;\n }\n\n return registry.value.get(block.type)?.component;\n }\n\n function createBlockFromType(type: string): Block | undefined {\n return registry.value.get(type)?.createBlock();\n }\n\n function getDefinition(\n customType: string,\n ): CustomBlockDefinition | undefined {\n const key = `custom:${customType}`;\n return registry.value.get(key)?.definition;\n }\n\n function renderErrorHtml(message: string): string {\n return `<div style=\"color: var(--tpl-text-muted); padding: 16px; text-align: center; border: 1px dashed var(--tpl-border); border-radius: var(--tpl-radius-sm); font-family: var(--tpl-font-family); font-size: 14px;\">${message}</div>`;\n }\n\n async function renderCustomBlockHtml(block: CustomBlock): Promise<string> {\n const definition = getDefinition(block.customType);\n if (!definition) {\n return renderErrorHtml(\"Block definition not found\");\n }\n\n try {\n const { Liquid } = await import(\"liquidjs\");\n const engine = new Liquid({\n strictVariables: false,\n strictFilters: false,\n });\n\n return await engine.parseAndRender(\n definition.template,\n block.fieldValues,\n );\n } catch (error) {\n logger.error(\n `Failed to render custom block \"${block.customType}\":`,\n error,\n );\n return renderErrorHtml(`Render error: ${block.customType}`);\n }\n }\n\n function isRegistered(type: string): boolean {\n return registry.value.has(type);\n }\n\n return {\n registerBuiltIn,\n registerCustom,\n getComponent,\n createBlock: createBlockFromType,\n getDefinition,\n renderCustomBlock: renderCustomBlockHtml,\n isRegistered,\n };\n}\n","import { computed, type ComputedRef, type Ref } from \"vue\";\nimport type { Block, TemplateContent } from \"@templatical/types\";\nimport { isCustomBlock } from \"@templatical/types\";\nimport type { UseBlockRegistryReturn } from \"./useBlockRegistry\";\n\n/**\n * Derive the reactive set of custom-block `stylesheet` strings currently in\n * use by the editor's content. Two layers of dedupe:\n *\n * 1. By `customType` — each definition's stylesheet is resolved once even if\n * dozens of instances of that type exist in the template.\n * 2. By trimmed CSS content — two definitions that ship the same stylesheet\n * string (unlikely but possible) emit it only once. Matches the renderer's\n * `collectCustomBlockStylesheets` behavior so the editor preview and the\n * exported MJML reflect the same rule set.\n *\n * Whitespace-only and missing stylesheets are filtered out. The result is a\n * `ComputedRef<string[]>` in insertion order (i.e., the order custom-block\n * types first appear in the content tree, depth-first through sections).\n *\n * Consumers render one `<style>` element per entry inside the editor's mount\n * root so the rules apply to the canvas. In shadow-DOM mode they scope to the\n * shadow root; in light-DOM mode they share the global stylesheet surface\n * already established by the published editor CSS — no new leak.\n */\nexport function useCustomBlockStylesheets(\n content: Ref<TemplateContent>,\n registry: UseBlockRegistryReturn,\n): ComputedRef<string[]> {\n return computed(() => {\n const customTypes = collectUniqueCustomTypes(content.value.blocks);\n if (customTypes.size === 0) {\n return [];\n }\n\n const seenContent = new Set<string>();\n const out: string[] = [];\n\n for (const customType of customTypes) {\n const css = registry.getDefinition(customType)?.stylesheet;\n if (!css) continue;\n\n const trimmed = css.trim();\n if (trimmed === \"\" || seenContent.has(trimmed)) continue;\n\n seenContent.add(trimmed);\n out.push(trimmed);\n }\n\n return out;\n });\n}\n\nfunction collectUniqueCustomTypes(blocks: Block[]): Set<string> {\n const types = new Set<string>();\n walk(blocks, types);\n return types;\n}\n\nfunction walk(blocks: Block[], out: Set<string>): void {\n for (const block of blocks) {\n if (isCustomBlock(block)) {\n out.add(block.customType);\n continue;\n }\n if (block.type === \"section\") {\n // SectionBlock has `children: Block[][]` (one array per column).\n const children = (block as unknown as { children?: Block[][] }).children;\n if (!children) continue;\n for (const column of children) {\n walk(column, out);\n }\n }\n }\n}\n","import type { Block } from \"@templatical/types\";\nimport {\n createButtonBlock,\n createCountdownBlock,\n createDividerBlock,\n createHtmlBlock,\n createImageBlock,\n createMenuBlock,\n createParagraphBlock,\n createSectionBlock,\n createSocialIconsBlock,\n createSpacerBlock,\n createTableBlock,\n createTitleBlock,\n createVideoBlock,\n} from \"@templatical/types\";\nimport type { Component } from \"vue\";\nimport type { UseBlockRegistryReturn } from \"../composables/useBlockRegistry\";\n\ninterface BuiltInBlockDef {\n type: string;\n createBlock: () => Block;\n}\n\nconst BUILT_IN_BLOCKS: BuiltInBlockDef[] = [\n { type: \"section\", createBlock: createSectionBlock },\n { type: \"title\", createBlock: createTitleBlock },\n { type: \"paragraph\", createBlock: createParagraphBlock },\n { type: \"image\", createBlock: createImageBlock },\n { type: \"button\", createBlock: createButtonBlock },\n { type: \"divider\", createBlock: createDividerBlock },\n { type: \"video\", createBlock: createVideoBlock },\n { type: \"social\", createBlock: createSocialIconsBlock },\n { type: \"menu\", createBlock: createMenuBlock },\n { type: \"table\", createBlock: createTableBlock },\n { type: \"spacer\", createBlock: createSpacerBlock },\n { type: \"html\", createBlock: createHtmlBlock },\n { type: \"countdown\", createBlock: createCountdownBlock },\n];\n\nexport function registerBuiltInBlocks(\n registry: UseBlockRegistryReturn,\n componentMap: Record<string, Component>,\n): void {\n for (const def of BUILT_IN_BLOCKS) {\n const component = componentMap[def.type];\n if (component) {\n registry.registerBuiltIn(def.type, {\n component,\n createBlock: def.createBlock,\n });\n }\n }\n}\n","/**\n * Consumer-facing shape of the `htmlBlockPreview` editor config option.\n *\n * Controls whether an HTML block's raw content renders as a live preview in\n * the editor canvas — inside a sandboxed iframe — instead of the static\n * placeholder card. Off by default. Accepts a bare boolean or an object so the\n * shape can grow (e.g. render modes, resource policies) without a breaking\n * change; today the object carries only `enabled`.\n */\nexport type HtmlBlockPreviewConfig = boolean | { enabled: boolean };\n\n/**\n * Normalize `htmlBlockPreview` to a plain boolean for the provide/inject layer.\n * `undefined` and `false` → `false`; `true` and `{ enabled: true }` → `true`;\n * `{ enabled: false }` → `false`.\n */\nexport function resolveHtmlBlockPreview(\n config: HtmlBlockPreviewConfig | undefined,\n): boolean {\n if (typeof config === \"boolean\") {\n return config;\n }\n return config?.enabled ?? false;\n}\n","import {\n DEFAULT_BLOCK_DEFAULTS,\n DEFAULT_TEMPLATE_DEFAULTS,\n type BlockDefaults,\n type TemplateDefaults,\n} from \"@templatical/types\";\nimport { canonicalizeHexColor } from \"./color\";\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Audit the effective block/template default colours against a preset palette\n * and return the ones that fall OUTSIDE it, as `type.key: value` descriptors —\n * e.g. `button.backgroundColor: #333333`; template-level keys as\n * `template.textColor: #1a1a1a`.\n *\n * \"Effective\" defaults are the factory defaults ({@link DEFAULT_BLOCK_DEFAULTS} /\n * {@link DEFAULT_TEMPLATE_DEFAULTS}) with the consumer's per-block / template\n * overrides spread on top. This is a shallow per-block spread, which for these\n * flat colour keys (`backgroundColor`, `color`, `digitColor`, …) resolves to\n * exactly the value the factory's `deepMergeDefaults` produces when it builds a\n * new block (see `packages/types/src/factory.ts` → `createBlock`) — the two\n * only diverge on nested objects, which carry no colour keys.\n *\n * A key counts as a colour when it matches `/color$/i`; its value is compared\n * through {@link canonicalizeHexColor}, so `#abc`, `#AABBCC`, and an `rgb(...)`\n * round-trip all resolve to one canonical member form. Empty-string values\n * (\"no default\") are skipped.\n *\n * Pure — it never logs. The consumer (`useEditorCore`, when custom colours are\n * locked) owns the warning, mirroring the resolver/consumer split used by\n * `resolveColorsConfig` and `resolvePaletteBlocks`.\n */\nexport function collectOffPaletteDefaults(\n presets: string[],\n blockDefaults?: BlockDefaults,\n templateDefaults?: TemplateDefaults,\n): string[] {\n const palette = new Set(\n presets.map((preset) => canonicalizeHexColor(preset)),\n );\n const offenders: string[] = [];\n\n const collect = (source: Record<string, unknown>, prefix: string): void => {\n for (const [key, value] of Object.entries(source)) {\n if (typeof value === \"string\") {\n if (\n value !== \"\" &&\n /color$/i.test(key) &&\n !palette.has(canonicalizeHexColor(value))\n ) {\n offenders.push(`${prefix}.${key}: ${value}`);\n }\n } else if (isPlainObject(value)) {\n collect(value, `${prefix}.${key}`);\n }\n }\n };\n\n const overrides = blockDefaults as Record<string, unknown> | undefined;\n for (const [type, factory] of Object.entries(DEFAULT_BLOCK_DEFAULTS)) {\n const override = overrides?.[type];\n collect(\n {\n ...(factory as Record<string, unknown>),\n ...(isPlainObject(override) ? override : {}),\n },\n type,\n );\n }\n\n collect(\n { ...DEFAULT_TEMPLATE_DEFAULTS, ...(templateDefaults ?? {}) },\n \"template\",\n );\n\n return offenders;\n}\n","import type { CustomBlockColorField } from \"@templatical/types\";\nimport {\n DEFAULT_RESOLVED_COLORS,\n resolveColorsConfig,\n type ResolvedColors,\n} from \"./resolveColorsConfig\";\n\n/**\n * The palette one custom-block color field renders, plus the audit signals a\n * consumer needs to warn about a field config that could not be honoured.\n *\n * Pure — this util never logs. The warnings live at the definition-registration\n * site (`useEditorCore`, via `collectColorFieldIssues`), the same resolver /\n * consumer split `resolveColorsConfig` and `resolvePaletteBlocks` use.\n */\nexport interface ResolvedFieldColors {\n /** The preset grid this field's picker renders. */\n presets: string[];\n /** Whether the wheel + hex input are offered alongside the grid. */\n allowCustom: boolean;\n /**\n * Entries of the FIELD's own `presets` dropped as invalid hex. Never includes\n * anything inherited from the editor palette — those were already validated\n * (and already warned about) at the editor level, so they are not re-reported.\n */\n invalidPresets: string[];\n /** The field declared `presets: []` — an empty list cannot narrow anything. */\n emptyPresets: boolean;\n /**\n * The field contributed no usable presets (unset, empty, or all-invalid), so\n * {@link ResolvedFieldColors.presets} is the editor's palette.\n */\n presetsInherited: boolean;\n /**\n * The field asked for `allowCustom: true` while the editor is locked. A field\n * may only narrow, so the request is dropped and the field stays locked.\n */\n allowCustomIgnored: boolean;\n}\n\n/**\n * Resolve a custom-block color field's own `presets` / `allowCustom` against\n * the already-resolved editor-wide palette. A field narrows the author's\n * freedom, not the palette: it can lock this field or give it its own presets,\n * but never unlock what the editor locked — and its `presets` *replace* the\n * editor grid rather than intersecting it (a locked field may list colors\n * outside it):\n *\n * - `allowCustom` — locked stays locked. `editorResolved.allowCustom === false`\n * wins outright, so an explicit field `allowCustom: true` under a locked\n * editor is ignored (flagged via `allowCustomIgnored`). Otherwise the field's\n * value applies, falling back to the editor's.\n * - `presets` — the field's entries are validated by {@link resolveColorsConfig}\n * (against the unconfigured baseline, so the hex rule lives in exactly one\n * place and inheritance is decided here rather than by that util's generic\n * per-property precedence). A non-empty valid list replaces the editor's;\n * unset, `[]`, or all-invalid all inherit the editor's palette. This is why\n * `[]` cannot mean \"no chips\" — an empty grid on a locked field would leave\n * the picker with nothing to pick.\n *\n * One residual case is left to `ColorPicker`'s own guard rather than forced\n * here: a field locking itself (`allowCustom: false`) when neither it nor the\n * editor has presets resolves to `{ presets: [], allowCustom: false }`, and the\n * picker's `showFreeform = allowCustom || !hasPresets` still renders the wheel,\n * so the field is never left unable to set a color.\n */\nexport function resolveFieldColors(\n field: Pick<CustomBlockColorField, \"presets\" | \"allowCustom\">,\n editorResolved: ResolvedColors,\n): ResolvedFieldColors {\n // Validate the field's own presets through the shared resolver, against the\n // unconfigured baseline — NOT against `editorResolved`, whose per-property\n // precedence would inherit before we can tell \"field said nothing\" apart\n // from \"field said something unusable\".\n const own = resolveColorsConfig(\n { presets: field.presets },\n DEFAULT_RESOLVED_COLORS,\n );\n\n const presetsInherited = own.presets.length === 0;\n\n const requestedAllowCustom = field.allowCustom ?? editorResolved.allowCustom;\n\n return {\n presets: presetsInherited ? editorResolved.presets : own.presets,\n allowCustom:\n editorResolved.allowCustom === false ? false : requestedAllowCustom,\n invalidPresets: own.invalidPresets,\n emptyPresets: field.presets !== undefined && field.presets.length === 0,\n presetsInherited,\n allowCustomIgnored:\n field.allowCustom === true && editorResolved.allowCustom === false,\n };\n}\n","import type {\n CustomBlockColorField,\n CustomBlockDefinition,\n} from \"@templatical/types\";\nimport { canonicalizeHexColor } from \"./color\";\nimport type { ResolvedColors } from \"./resolveColorsConfig\";\nimport { resolveFieldColors } from \"./resolveFieldColors\";\n\n/**\n * One problem found in a custom-block definition's color fields, ready to log.\n *\n * `id` is the once-guard key: `<block type>:<field path>:<kind>`. The same\n * field renders on every block instance, every toolbar mount, and every\n * repeater item, so the consumer keys a `Set` on this to warn once per\n * definition rather than once per render (the same guard `Sidebar.vue` uses\n * for unknown `paletteBlocks` entries).\n */\nexport interface ColorFieldIssue {\n id: string;\n message: string;\n}\n\n/**\n * Audit every color field of a custom-block definition against the resolved\n * editor-wide palette, returning the field configs that could not be honoured.\n *\n * Covers four cases, all of which resolve to *something* renderable (nothing\n * here throws or blocks registration) but none of which do what the author\n * wrote: invalid preset entries, an empty `presets: []`, an `allowCustom: true`\n * that can't unlock a locked editor, and a `default` colour the field's own\n * locked palette can't reselect. The last mirrors the editor-wide off-palette\n * defaults audit (`collectOffPaletteDefaults`): `createCustomBlock` seeds\n * `fieldValues[key] = field.default ?? \"\"` (and `RepeatableField` does the same\n * per item), so an off-palette default paints new blocks a colour the locked\n * picker offers no chip for.\n *\n * Pure — it never logs; the caller owns the warning and the once-guard.\n */\nexport function collectColorFieldIssues(\n definition: CustomBlockDefinition,\n editorColors: ResolvedColors,\n): ColorFieldIssue[] {\n const issues: ColorFieldIssue[] = [];\n\n const audit = (field: CustomBlockColorField, path: string): void => {\n const resolved = resolveFieldColors(field, editorColors);\n // Both the block type and the field path are named: field keys like\n // `accentColor` repeat across definitions, so the key alone can't tell an\n // author which block to fix.\n const at = `custom block \"${definition.type}\" field \"${path}\"`;\n const id = (kind: string): string => `${definition.type}:${path}:${kind}`;\n\n if (resolved.invalidPresets.length > 0) {\n issues.push({\n id: id(\"invalid-presets\"),\n message:\n `${at}: presets skipped invalid entries: ` +\n `${resolved.invalidPresets.join(\", \")} — presets must be hex ` +\n \"colors (#rgb or #rrggbb).\" +\n (resolved.presetsInherited\n ? \" No valid entries are left, so the field inherits colors.presets.\"\n : \"\"),\n });\n }\n\n if (resolved.emptyPresets) {\n issues.push({\n id: id(\"empty-presets\"),\n message:\n `${at}: presets is empty — a field palette can only narrow the ` +\n \"editor's, so an empty list is ignored and the field inherits \" +\n \"colors.presets.\",\n });\n }\n\n if (resolved.allowCustomIgnored) {\n issues.push({\n id: id(\"allow-custom-ignored\"),\n message:\n `${at}: allowCustom: true is ignored because colors.allowCustom ` +\n \"is false — a field can narrow the editor-wide palette, never \" +\n \"unlock it.\",\n });\n }\n\n // Only meaningful while locked: with the wheel available the author can\n // always get back to an off-palette default. Same gate as the editor-wide\n // audit. A field locked with no palette at all falls back to the wheel\n // (see `resolveFieldColors`), so it is exempt too.\n if (!resolved.allowCustom && resolved.presets.length > 0) {\n const seeded = field.default ?? \"\";\n const palette = new Set(\n resolved.presets.map((preset) => canonicalizeHexColor(preset)),\n );\n // \"\" seeds the unset state, which the locked grid's leading none chip\n // reselects — not an offence.\n if (seeded !== \"\" && !palette.has(canonicalizeHexColor(seeded))) {\n issues.push({\n id: id(\"off-palette-default\"),\n message:\n `${at} locks custom colours, but its default ${seeded} falls ` +\n `outside the field's presets: ${resolved.presets.join(\", \")}. ` +\n \"New blocks start on a colour the picker can't reselect — set \" +\n \"the default from the same palette.\",\n });\n }\n }\n };\n\n for (const field of definition.fields) {\n if (field.type === \"color\") {\n audit(field, field.key);\n } else if (field.type === \"repeatable\") {\n // Repeater sub-fields render one picker per item, so they need the same\n // audit — reported under a `parent[].child` path to stay distinct from a\n // top-level field of the same key.\n for (const subField of field.fields) {\n if (subField.type === \"color\") {\n audit(subField, `${field.key}[].${subField.key}`);\n }\n }\n }\n }\n\n return issues;\n}\n","import type { UseHistoryReturn } from \"@templatical/core\";\n\nexport interface KeyboardShortcutHandlers {\n history: UseHistoryReturn;\n selectBlock: (blockId: string | null) => void;\n getSelectedBlockId: () => string | null;\n removeBlock: (blockId: string) => void;\n onSave?: () => void;\n onBeforeUndo?: () => void;\n /** True while a saved-blocks pick session is running. */\n isPicking?: () => boolean;\n onConfirmPick?: () => void;\n onCancelPick?: () => void;\n}\n\n/**\n * Returns true when the keyboard event originated from a text-editing\n * context (input, textarea, select, or contentEditable element like\n * TipTap). Used to let native/TipTap controls handle their own key\n * events so the editor doesn't, for example, delete the whole block on\n * Backspace mid-typing.\n *\n * Walks `event.composedPath()` rather than reading `event.target`\n * directly. When the editor mounts inside a shadow root and a global\n * keydown listener at `document` level receives the event, the browser\n * retargets `event.target` to the shadow host element — so the actual\n * focused contenteditable inside the shadow tree is invisible via\n * `event.target` alone. The composed path traverses through every\n * shadow boundary the event crossed and exposes the real innermost\n * element, which is what we need to inspect.\n *\n * Light-DOM mode: `composedPath()` returns the same chain as `target`'s\n * ancestors, so behavior is unchanged.\n */\nexport function isEditingText(e: KeyboardEvent): boolean {\n for (const node of e.composedPath()) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.isContentEditable) return true;\n const tag = node.tagName;\n if (tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\") return true;\n }\n return false;\n}\n\n/**\n * Shared keyboard shortcut handler for both OSS and Cloud editors.\n *\n * - Cmd/Ctrl+Z: undo (skipped when editing text — TipTap handles its own undo)\n * - Cmd/Ctrl+Shift+Z: redo (skipped when editing text)\n * - Cmd/Ctrl+S: save\n * - Escape: deselect block\n * - Delete/Backspace: remove selected block (skipped when editing text)\n *\n * While a saved-blocks pick session runs, Escape/Enter drive the session and\n * Delete/Backspace are swallowed — see the block below.\n */\nexport function handleEditorKeydown(\n e: KeyboardEvent,\n handlers: KeyboardShortcutHandlers,\n): void {\n const isCmd = e.metaKey || e.ctrlKey;\n\n // Cmd/Ctrl+S: save\n if (isCmd && e.key.toLowerCase() === \"s\") {\n e.preventDefault();\n handlers.onSave?.();\n return;\n }\n\n // Cmd/Ctrl+Z / Cmd/Ctrl+Shift+Z: undo/redo\n if (isCmd && e.key.toLowerCase() === \"z\") {\n // Let TipTap handle its own undo/redo when editing text\n if (isEditingText(e)) {\n return;\n }\n\n e.preventDefault();\n\n if (e.shiftKey) {\n handlers.history.redo();\n } else {\n handlers.onBeforeUndo?.();\n handlers.history.undo();\n }\n return;\n }\n\n // Pick session owns Escape/Enter, and blocks destructive keys. Placed after\n // save/undo (those stay useful) but before selection handling: Escape must\n // cancel the session rather than deselect, and Delete must not remove a block\n // the user is only trying to pick. Text-editing contexts are excluded so\n // typing a name in a nested input is unaffected.\n if (handlers.isPicking?.() && !isEditingText(e)) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n handlers.onCancelPick?.();\n return;\n }\n if (e.key === \"Enter\") {\n e.preventDefault();\n handlers.onConfirmPick?.();\n return;\n }\n if (e.key === \"Delete\" || e.key === \"Backspace\") {\n e.preventDefault();\n return;\n }\n }\n\n // Escape: deselect\n if (e.key === \"Escape\") {\n handlers.selectBlock(null);\n return;\n }\n\n // Delete/Backspace: remove selected block\n if (\n (e.key === \"Delete\" || e.key === \"Backspace\") &&\n handlers.getSelectedBlockId() &&\n !isEditingText(e)\n ) {\n e.preventDefault();\n handlers.history.record();\n handlers.removeBlock(handlers.getSelectedBlockId()!);\n }\n}\n","import {\n getLogicMergeTagKeyword,\n getMergeTagLabel,\n getMergeTagSample,\n isLogicMergeTagValue,\n isMergeTagValue,\n type MergeTag,\n type SyntaxPreset,\n} from \"@templatical/types\";\n\n/** A run of a field value: plain text, or a resolved merge-tag label. */\nexport interface MergeTagLabelSegment {\n type: \"text\" | \"tag\";\n /** For `tag`, the resolved label/keyword; for `text`, the literal run. */\n value: string;\n}\n\n/**\n * Splits a plain-string field value into text runs and merge-tag runs, with\n * tags resolved to their human-readable label (data tags via `getMergeTagLabel`,\n * unknown tags falling back to their raw value) or keyword (logic tags).\n *\n * Used to render merge tags on the canvas for blocks that show a merge-tag-\n * enabled field directly as a string (button label, menu items, video/image\n * placeholders). Returning segments — rather than a flattened string — lets the\n * renderer mark the tag runs with a distinct cue so they're distinguishable\n * from user-typed text, mirroring the rich-text editor. The plain-text analog\n * of `resolveHtmlMergeTagLabels`, which only rewrites TipTap `<span>` markup.\n */\nexport function splitMergeTagLabelSegments(\n text: string,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n useSamples = false,\n): MergeTagLabelSegment[] {\n if (!text) return [];\n\n const regex = new RegExp(\n `(${syntax.value.source}|${syntax.logic.source})`,\n \"g\",\n );\n const segments: MergeTagLabelSegment[] = [];\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(text)) !== null) {\n if (match.index > lastIndex) {\n segments.push({\n type: \"text\",\n value: text.slice(lastIndex, match.index),\n });\n }\n\n const matched = match[0];\n if (isMergeTagValue(matched, syntax)) {\n const sample = useSamples\n ? getMergeTagSample(matched, mergeTags)\n : undefined;\n // A resolved sample is emitted as `text`, not `tag` — that is what drops\n // the dotted-underline cue, so the value reads as ordinary content. With\n // no sample configured we fall back to the label, still cued.\n segments.push(\n sample !== undefined\n ? { type: \"text\", value: sample }\n : { type: \"tag\", value: getMergeTagLabel(matched, mergeTags) },\n );\n } else if (isLogicMergeTagValue(matched, syntax)) {\n segments.push({\n type: \"tag\",\n value: getLogicMergeTagKeyword(matched, syntax),\n });\n } else {\n segments.push({ type: \"text\", value: matched });\n }\n\n lastIndex = match.index + matched.length;\n }\n\n if (lastIndex < text.length) {\n segments.push({ type: \"text\", value: text.slice(lastIndex) });\n }\n\n return segments;\n}\n","<script lang=\"ts\">\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport { computed, defineComponent, h, inject } from \"vue\";\nimport {\n MERGE_TAGS_KEY,\n MERGE_TAG_SYNTAX_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../keys\";\nimport { splitMergeTagLabelSegments } from \"../utils/mergeTagLabelSegments\";\n\n/**\n * Renders a plain-string field value on the canvas with merge tags resolved to\n * their labels, marking each tag run with a dotted underline (`.tpl-merge-tag-\n * label`, in `currentColor`) so a dynamic value is distinguishable from\n * user-typed text on any background. Display-only — the raw token is unchanged\n * in the stored value and MJML output.\n *\n * A render function (not a template) is used so the text/tag runs render inline\n * with no stray whitespace between them.\n */\nexport default defineComponent({\n name: \"MergeTagPreviewText\",\n props: {\n text: { type: String, required: true },\n },\n setup(props) {\n const mergeTags = inject(MERGE_TAGS_KEY, []);\n const syntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\n // Absent on the editing canvas, so field values keep showing labels there.\n const useSamples = inject(USE_MERGE_TAG_SAMPLES_KEY, null);\n const segments = computed(() =>\n splitMergeTagLabelSegments(\n props.text,\n mergeTags,\n syntax,\n useSamples?.value ?? false,\n ),\n );\n\n return () =>\n segments.value.map((segment, index) =>\n segment.type === \"tag\"\n ? h(\n \"span\",\n { key: index, class: \"tpl-merge-tag-label\" },\n segment.value,\n )\n : segment.value,\n );\n },\n});\n</script>\n","<script lang=\"ts\">\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport { computed, defineComponent, h, inject } from \"vue\";\nimport {\n MERGE_TAGS_KEY,\n MERGE_TAG_SYNTAX_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../keys\";\nimport { splitMergeTagLabelSegments } from \"../utils/mergeTagLabelSegments\";\n\n/**\n * Renders a plain-string field value on the canvas with merge tags resolved to\n * their labels, marking each tag run with a dotted underline (`.tpl-merge-tag-\n * label`, in `currentColor`) so a dynamic value is distinguishable from\n * user-typed text on any background. Display-only — the raw token is unchanged\n * in the stored value and MJML output.\n *\n * A render function (not a template) is used so the text/tag runs render inline\n * with no stray whitespace between them.\n */\nexport default defineComponent({\n name: \"MergeTagPreviewText\",\n props: {\n text: { type: String, required: true },\n },\n setup(props) {\n const mergeTags = inject(MERGE_TAGS_KEY, []);\n const syntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\n // Absent on the editing canvas, so field values keep showing labels there.\n const useSamples = inject(USE_MERGE_TAG_SAMPLES_KEY, null);\n const segments = computed(() =>\n splitMergeTagLabelSegments(\n props.text,\n mergeTags,\n syntax,\n useSamples?.value ?? false,\n ),\n );\n\n return () =>\n segments.value.map((segment, index) =>\n segment.type === \"tag\"\n ? h(\n \"span\",\n { key: index, class: \"tpl-merge-tag-label\" },\n segment.value,\n )\n : segment.value,\n );\n },\n});\n</script>\n","<script setup lang=\"ts\">\nimport type {\n ButtonBlock as ButtonBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\n\nconst props = defineProps<{\n block: ButtonBlockType;\n viewport: ViewportSize;\n}>();\n\nconst buttonStyle = computed(() => {\n const style: Record<string, string> = {\n display: \"inline-block\",\n padding: `${props.block.buttonPadding.top}px ${props.block.buttonPadding.right}px ${props.block.buttonPadding.bottom}px ${props.block.buttonPadding.left}px`,\n backgroundColor: props.block.backgroundColor,\n color: props.block.textColor,\n fontSize: `${props.block.fontSize}px`,\n fontWeight: \"bold\",\n textDecoration: \"none\",\n borderRadius: `${props.block.borderRadius}px`,\n textAlign: \"center\",\n };\n if (props.block.fontFamily) {\n style.fontFamily = props.block.fontFamily;\n }\n if (props.block.width === \"full\") {\n style.display = \"block\";\n style.width = \"100%\";\n style.boxSizing = \"border-box\";\n } else if (typeof props.block.width === \"number\") {\n style.width = `${props.block.width}px`;\n style.boxSizing = \"border-box\";\n }\n return style;\n});\n</script>\n\n<template>\n <div class=\"tpl:text-center\">\n <a\n :href=\"block.url || '#'\"\n :style=\"buttonStyle\"\n class=\"tpl:cursor-default\"\n @click.prevent\n >\n <MergeTagPreviewText :text=\"block.text\" />\n </a>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n ButtonBlock as ButtonBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\n\nconst props = defineProps<{\n block: ButtonBlockType;\n viewport: ViewportSize;\n}>();\n\nconst buttonStyle = computed(() => {\n const style: Record<string, string> = {\n display: \"inline-block\",\n padding: `${props.block.buttonPadding.top}px ${props.block.buttonPadding.right}px ${props.block.buttonPadding.bottom}px ${props.block.buttonPadding.left}px`,\n backgroundColor: props.block.backgroundColor,\n color: props.block.textColor,\n fontSize: `${props.block.fontSize}px`,\n fontWeight: \"bold\",\n textDecoration: \"none\",\n borderRadius: `${props.block.borderRadius}px`,\n textAlign: \"center\",\n };\n if (props.block.fontFamily) {\n style.fontFamily = props.block.fontFamily;\n }\n if (props.block.width === \"full\") {\n style.display = \"block\";\n style.width = \"100%\";\n style.boxSizing = \"border-box\";\n } else if (typeof props.block.width === \"number\") {\n style.width = `${props.block.width}px`;\n style.boxSizing = \"border-box\";\n }\n return style;\n});\n</script>\n\n<template>\n <div class=\"tpl:text-center\">\n <a\n :href=\"block.url || '#'\"\n :style=\"buttonStyle\"\n class=\"tpl:cursor-default\"\n @click.prevent\n >\n <MergeTagPreviewText :text=\"block.text\" />\n </a>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Box } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n icon?: string;\n size?: number;\n}>();\n\nconst iconSize = computed(() => props.size ?? 20);\n\nconst isSvg = computed(\n () =>\n props.icon &&\n (props.icon.trimStart().startsWith(\"<svg\") ||\n props.icon.trimStart().startsWith(\"<SVG\")),\n);\n\nconst isUrl = computed(\n () =>\n props.icon &&\n !isSvg.value &&\n (props.icon.startsWith(\"http\") || props.icon.startsWith(\"/\")),\n);\n</script>\n\n<template>\n <!-- eslint-disable vue/no-v-html -->\n <span\n v-if=\"isSvg\"\n class=\"tpl:inline-flex tpl:items-center tpl:justify-center\"\n :style=\"{ width: `${iconSize}px`, height: `${iconSize}px` }\"\n v-html=\"icon\"\n />\n <!-- eslint-enable vue/no-v-html -->\n <img\n v-else-if=\"isUrl\"\n :src=\"icon\"\n :width=\"iconSize\"\n :height=\"iconSize\"\n class=\"tpl:inline-block\"\n alt=\"\"\n />\n <Box v-else :size=\"iconSize\" :stroke-width=\"1.5\" />\n</template>\n","<script setup lang=\"ts\">\nimport { Box } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n icon?: string;\n size?: number;\n}>();\n\nconst iconSize = computed(() => props.size ?? 20);\n\nconst isSvg = computed(\n () =>\n props.icon &&\n (props.icon.trimStart().startsWith(\"<svg\") ||\n props.icon.trimStart().startsWith(\"<SVG\")),\n);\n\nconst isUrl = computed(\n () =>\n props.icon &&\n !isSvg.value &&\n (props.icon.startsWith(\"http\") || props.icon.startsWith(\"/\")),\n);\n</script>\n\n<template>\n <!-- eslint-disable vue/no-v-html -->\n <span\n v-if=\"isSvg\"\n class=\"tpl:inline-flex tpl:items-center tpl:justify-center\"\n :style=\"{ width: `${iconSize}px`, height: `${iconSize}px` }\"\n v-html=\"icon\"\n />\n <!-- eslint-enable vue/no-v-html -->\n <img\n v-else-if=\"isUrl\"\n :src=\"icon\"\n :width=\"iconSize\"\n :height=\"iconSize\"\n class=\"tpl:inline-block\"\n alt=\"\"\n />\n <Box v-else :size=\"iconSize\" :stroke-width=\"1.5\" />\n</template>\n","import { inject } from \"vue\";\nimport { EDITOR_ROOT_KEY } from \"../keys\";\n\n/**\n * Returns the editor's effective DOM root — `Document` when mounted in light\n * DOM, `ShadowRoot` when mounted with `shadowDom: true`.\n *\n * Use this instead of reaching for `document` directly when the code is\n * shadow-DOM-aware (focus trap, selection, popover mount, etc.). Both\n * `Document` and `ShadowRoot` expose the same subset of APIs the editor\n * needs (`activeElement`, `getSelection?`, `querySelector`, `appendChild`),\n * so call sites usually don't need to branch.\n *\n * Falls back to `document` if no provider is in scope — keeps headless /\n * test-utility usage working without explicit wiring.\n *\n * Internal — not exported from the package entry.\n */\nexport function useEditorRoot(): Document | ShadowRoot {\n // When called outside a Vue setup() context (e.g. direct composable\n // invocation in unit tests), Vue's inject() warns and returns undefined\n // regardless of the default arg. Coalesce explicitly so callers always\n // get a usable root — keeps test ergonomics simple and matches the\n // \"headless fallback to document\" semantics this composable promises.\n const root = inject(EDITOR_ROOT_KEY, document);\n return root ?? document;\n}\n","import { useEventListener } from \"@vueuse/core\";\nimport { onScopeDispose, watch, type Ref } from \"vue\";\nimport { useEditorRoot } from \"./useEditorRoot\";\n\nconst FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Lightweight focus trap for modal dialogs.\n * Traps Tab/Shift+Tab within the container and restores focus on deactivation.\n */\nexport function useFocusTrap(\n containerRef: Ref<HTMLElement | null>,\n active: Ref<boolean>,\n): void {\n // Resolve the editor's effective DOM root once at setup time. In light-DOM\n // mode this is the global `document`; in shadow mode it's the editor's\n // open `ShadowRoot`. Both expose `activeElement` with the same semantics\n // we need (the focused element WITHIN that root). Reading `activeElement`\n // off the shadow root instead of `document` is the difference between\n // \"the focused descendant inside the trap\" and \"the host element\" when\n // focus is inside the shadow tree.\n const editorRoot = useEditorRoot();\n\n let previouslyFocused: HTMLElement | null = null;\n let cleanupListener: (() => void) | null = null;\n let pendingRaf: number | null = null;\n\n function getFocusableElements(): HTMLElement[] {\n if (!containerRef.value) return [];\n return Array.from(\n containerRef.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),\n ).filter((el) => el.offsetParent !== null);\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\") return;\n\n const focusable = getFocusableElements();\n if (focusable.length === 0) return;\n\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n\n if (event.shiftKey) {\n if (editorRoot.activeElement === first) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (editorRoot.activeElement === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n function activate(): void {\n // Re-entry guard: if a previous activate didn't deactivate (e.g.\n // container ref swapped while still active), tear down the prior\n // listener and rAF before registering new ones — otherwise they leak.\n const wasActive = cleanupListener !== null || pendingRaf !== null;\n if (wasActive) {\n deactivate({ restoreFocus: false });\n }\n\n // Only capture previouslyFocused on the FIRST activation. On re-entry,\n // editorRoot.activeElement is whatever the user tabbed to inside the\n // trap; overwriting would lose the original pre-trap focus target.\n if (!wasActive) {\n previouslyFocused = editorRoot.activeElement as HTMLElement | null;\n }\n\n pendingRaf = requestAnimationFrame(() => {\n pendingRaf = null;\n const focusable = getFocusableElements();\n if (focusable.length > 0) {\n const autofocus = containerRef.value?.querySelector<HTMLElement>(\n \"[autofocus], input:not([disabled])\",\n );\n (autofocus ?? focusable[0]).focus();\n }\n });\n\n cleanupListener = useEventListener(containerRef, \"keydown\", handleKeydown);\n }\n\n function deactivate(opts: { restoreFocus?: boolean } = {}): void {\n const restoreFocus = opts.restoreFocus !== false;\n\n if (pendingRaf !== null) {\n if (typeof cancelAnimationFrame !== \"undefined\") {\n cancelAnimationFrame(pendingRaf);\n }\n pendingRaf = null;\n }\n\n cleanupListener?.();\n cleanupListener = null;\n\n if (restoreFocus && previouslyFocused && previouslyFocused.focus) {\n previouslyFocused.focus();\n previouslyFocused = null;\n }\n }\n\n const stopWatch = watch(\n [active, containerRef],\n ([isActive, container]) => {\n if (isActive && container) {\n activate();\n } else {\n deactivate();\n }\n },\n { flush: \"post\" },\n );\n\n onScopeDispose(() => {\n stopWatch();\n deactivate();\n });\n}\n","import { useToggle } from \"@vueuse/core\";\nimport {\n onScopeDispose,\n shallowRef,\n watch,\n type ShallowRef,\n type Ref,\n} from \"vue\";\nimport type { EmojiCategory, EmojiCategoryKey } from \"./emojiData\";\n\nexport type { EmojiCategoryKey, EmojiCategory };\n\nexport interface UseEmojiReturn {\n categories: ShallowRef<EmojiCategory[]>;\n isOpen: Ref<boolean>;\n toggle: () => void;\n close: () => void;\n}\n\nexport function useEmoji(): UseEmojiReturn {\n const [isOpen, toggleValue] = useToggle(false);\n const categories = shallowRef<EmojiCategory[]>([]);\n\n let disposed = false;\n onScopeDispose(() => {\n disposed = true;\n });\n\n // Lazy-load emoji data on first open\n watch(isOpen, async (open) => {\n if (open && categories.value.length === 0) {\n const { emojiCategories } = await import(\"./emojiData\");\n if (disposed) return;\n categories.value = emojiCategories;\n }\n });\n\n function toggle(): void {\n toggleValue();\n }\n\n function close(): void {\n isOpen.value = false;\n }\n\n return {\n categories,\n isOpen,\n toggle,\n close,\n };\n}\n","import type { Block } from \"@templatical/types\";\nimport { ref, type Ref } from \"vue\";\n\nexport interface UseDragDropOptions {\n onBlockMove: (\n blockId: string,\n newIndex: number,\n targetSectionId?: string,\n columnIndex?: number,\n ) => void;\n onBlockAdd: (\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n ) => void;\n}\n\nexport interface UseDragDropReturn {\n isDragging: Ref<boolean>;\n draggedBlock: Ref<Block | null>;\n dropTargetId: Ref<string | null>;\n startDrag: (block: Block) => void;\n endDrag: () => void;\n setDropTarget: (targetId: string | null) => void;\n handleDrop: (\n blocks: Block[],\n newIndex: number,\n targetSectionId?: string,\n columnIndex?: number,\n ) => void;\n getSortableOptions: (\n group: string,\n sectionId?: string,\n columnIndex?: number,\n ) => Record<string, unknown>;\n}\n\nexport function useDragDrop(options: UseDragDropOptions): UseDragDropReturn {\n const { onBlockMove, onBlockAdd } = options;\n\n const isDragging = ref(false);\n const draggedBlock = ref<Block | null>(null);\n const dropTargetId = ref<string | null>(null);\n\n function startDrag(block: Block): void {\n isDragging.value = true;\n draggedBlock.value = block;\n }\n\n function endDrag(): void {\n isDragging.value = false;\n draggedBlock.value = null;\n dropTargetId.value = null;\n }\n\n function setDropTarget(targetId: string | null): void {\n dropTargetId.value = targetId;\n }\n\n function handleDrop(\n blocks: Block[],\n newIndex: number,\n targetSectionId?: string,\n columnIndex?: number,\n ): void {\n if (!draggedBlock.value) return;\n\n const existingIndex = blocks.findIndex(\n (b) => b.id === draggedBlock.value!.id,\n );\n\n if (existingIndex === -1) {\n onBlockAdd(draggedBlock.value, targetSectionId, columnIndex);\n } else {\n onBlockMove(\n draggedBlock.value.id,\n newIndex,\n targetSectionId,\n columnIndex,\n );\n }\n\n endDrag();\n }\n\n function getSortableOptions(\n group: string,\n sectionId?: string,\n columnIndex?: number,\n ): Record<string, unknown> {\n return {\n group,\n animation: 150,\n ghostClass: \"tpl-ghost\",\n dragClass: \"tpl-drag\",\n handle: \".tpl-drag-handle\",\n onStart: (evt: { item: { dataset: { blockId?: string } } }) => {\n const blockId = evt.item.dataset.blockId;\n if (blockId) {\n isDragging.value = true;\n }\n },\n onEnd: () => {\n endDrag();\n },\n onAdd: (evt: {\n item: { dataset: { blockId?: string } };\n newIndex: number;\n }) => {\n const blockId = evt.item.dataset.blockId;\n if (blockId) {\n onBlockMove(blockId, evt.newIndex, sectionId, columnIndex);\n }\n },\n onUpdate: (evt: {\n item: { dataset: { blockId?: string } };\n newIndex: number;\n }) => {\n const blockId = evt.item.dataset.blockId;\n if (blockId) {\n onBlockMove(blockId, evt.newIndex, sectionId, columnIndex);\n }\n },\n };\n }\n\n return {\n isDragging,\n draggedBlock,\n dropTargetId,\n startDrag,\n endDrag,\n setDropTarget,\n handleDrop,\n getSortableOptions,\n };\n}\n","import type { CustomFont, FontsConfig } from \"@templatical/types\";\nimport {\n computed,\n getCurrentScope,\n onScopeDispose,\n ref,\n type ComputedRef,\n type Ref,\n} from \"vue\";\nimport { logger } from \"../utils/logger\";\n\nexport interface FontOption {\n value: string;\n label: string;\n isCustom?: boolean;\n}\n\nexport interface UseFontsReturn {\n fonts: ComputedRef<FontOption[]>;\n defaultFont: ComputedRef<string>;\n defaultFallback: ComputedRef<string>;\n customFonts: Ref<CustomFont[]>;\n customFontsEnabled: Ref<boolean>;\n isLoaded: Ref<boolean>;\n setCustomFontsEnabled: (enabled: boolean) => void;\n loadCustomFonts: () => Promise<void>;\n cleanupFontLinks: () => void;\n getFontWithFallback: (fontName: string) => string;\n getDefaultFont: () => string;\n}\n\nconst BUILT_IN_FONTS: FontOption[] = [\n { value: \"Arial\", label: \"Arial\" },\n { value: \"Helvetica\", label: \"Helvetica\" },\n { value: \"Georgia\", label: \"Georgia\" },\n { value: \"Times New Roman\", label: \"Times New Roman\" },\n { value: \"Verdana\", label: \"Verdana\" },\n { value: \"Trebuchet MS\", label: \"Trebuchet MS\" },\n { value: \"Courier New\", label: \"Courier New\" },\n];\n\nconst BUILT_IN_FONT_STACKS: Record<string, string> = {\n arial: \"Arial, sans-serif\",\n helvetica: \"Helvetica, sans-serif\",\n georgia: \"Georgia, serif\",\n \"times new roman\": \"'Times New Roman', serif\",\n verdana: \"Verdana, sans-serif\",\n \"trebuchet ms\": \"'Trebuchet MS', sans-serif\",\n \"courier new\": \"'Courier New', monospace\",\n};\n\nconst DEFAULT_FALLBACK = \"Arial, sans-serif\";\n\n/**\n * Resolve which built-in fonts the picker offers, honoring `FontsConfig.builtIns`:\n * `true`/`undefined` keeps all seven, `false` drops them all, and a `string[]`\n * keeps only the named families (case-insensitive, exact name). An entry that\n * isn't a built-in is warned and skipped (like an unknown `paletteBlocks` entry)\n * so a typo narrows the list instead of silently reinstating the full set. The\n * warning is emitted here rather than in a separate consumer: `useFonts` builds\n * its font list eagerly at init, so resolution and warning naturally live\n * together (unlike the palette, resolved lazily in `Sidebar`, which warns from a\n * watcher).\n *\n * Only the *picker* list is filtered; `BUILT_IN_FONT_STACKS` (fallback stacks)\n * and `isBuiltInFont` stay complete, so content already using an excluded family\n * still renders with its proper stack.\n */\nfunction resolveBuiltInFonts(builtIns: FontsConfig[\"builtIns\"]): FontOption[] {\n if (builtIns === undefined || builtIns === true) {\n return [...BUILT_IN_FONTS];\n }\n if (builtIns === false) {\n return [];\n }\n\n const allowed: FontOption[] = [];\n const seen = new Set<string>();\n const warned = new Set<string>();\n for (const name of builtIns) {\n const key = name.toLowerCase();\n const match = BUILT_IN_FONTS.find(\n (font) => font.value.toLowerCase() === key,\n );\n if (!match) {\n if (!warned.has(key)) {\n warned.add(key);\n logger.warn(\n `config.fonts.builtIns: \"${name}\" is not a built-in font — skipping it. ` +\n `Built-in fonts are: ${BUILT_IN_FONTS.map((font) => font.value).join(\", \")}.`,\n );\n }\n continue;\n }\n if (seen.has(match.value)) continue;\n seen.add(match.value);\n allowed.push(match);\n }\n return allowed;\n}\n\nexport function useFonts(config?: FontsConfig): UseFontsReturn {\n const customFonts = ref<CustomFont[]>(config?.customFonts ?? []);\n const customFontsEnabled = ref(true);\n const isLoaded = ref(false);\n\n // Built-in fonts the picker offers, after applying the `builtIns` allowlist.\n // Resolved once from config (not reactive): the allowlist can't change after\n // init, so an unknown-name warning fires exactly once per editor.\n const enabledBuiltInFonts = resolveBuiltInFonts(config?.builtIns);\n\n const defaultFallback = computed(\n () => config?.defaultFallback ?? DEFAULT_FALLBACK,\n );\n\n function setCustomFontsEnabled(enabled: boolean): void {\n customFontsEnabled.value = enabled;\n }\n\n const fonts = computed<FontOption[]>(() => {\n const builtInFonts = [...enabledBuiltInFonts];\n\n if (!customFontsEnabled.value) {\n return builtInFonts.sort((a, b) => a.label.localeCompare(b.label));\n }\n\n const customFontOptions: FontOption[] = customFonts.value.map((font) => ({\n value: font.name,\n label: font.name,\n isCustom: true,\n }));\n\n const allFonts = [...builtInFonts, ...customFontOptions];\n\n allFonts.sort((a, b) => a.label.localeCompare(b.label));\n\n return allFonts;\n });\n\n function isFontAvailable(fontName: string): boolean {\n return fonts.value.some(\n (font) =>\n font.label.toLowerCase() === fontName.toLowerCase() ||\n font.value.toLowerCase().startsWith(fontName.toLowerCase()),\n );\n }\n\n // Init-time check, same rationale as `resolveBuiltInFonts` above: `useFonts`\n // resolves everything eagerly at init, so a config-mismatch warning lives\n // here rather than in a separate consumer. A NEW template seeds\n // `config.defaultFont`, or the family from `DEFAULT_FALLBACK` when it's unset\n // (matching `createDefaultTemplateContent`'s \"Arial\" default). If the picker\n // doesn't offer that family — an excluded built-in, or no matching custom\n // font — authors land on a font they can't reselect, so warn once. Uses the\n // same case-insensitive family matching as `isBuiltInFont`, over the offered\n // list.\n const seededFontFamily =\n config?.defaultFont ?? DEFAULT_FALLBACK.split(\",\")[0].trim();\n if (!isFontAvailable(seededFontFamily)) {\n logger.warn(\n `config.fonts: new templates seed \"${seededFontFamily}\", which the font ` +\n `picker doesn't offer — authors can't reselect it. Either add it to ` +\n `fonts.builtIns, or set fonts.defaultFont to an offered font.`,\n );\n }\n\n function isBuiltInFont(fontName: string): boolean {\n return BUILT_IN_FONTS.some(\n (font) =>\n font.label.toLowerCase() === fontName.toLowerCase() ||\n font.value.toLowerCase().startsWith(fontName.toLowerCase()),\n );\n }\n\n function getDefaultFont(): string {\n if (config?.defaultFont) {\n if (!customFontsEnabled.value && !isBuiltInFont(config.defaultFont)) {\n return DEFAULT_FALLBACK;\n }\n\n if (isFontAvailable(config.defaultFont)) {\n const matchedFont = fonts.value.find(\n (font) =>\n font.label.toLowerCase() === config.defaultFont!.toLowerCase() ||\n font.value\n .toLowerCase()\n .startsWith(config.defaultFont!.toLowerCase()),\n );\n if (matchedFont) {\n return matchedFont.value;\n }\n }\n }\n return DEFAULT_FALLBACK;\n }\n\n const defaultFont = computed(() => getDefaultFont());\n\n function getFontWithFallback(fontName: string): string {\n if (!fontName) {\n return defaultFallback.value;\n }\n\n const customFont = customFonts.value.find(\n (font) => font.name.toLowerCase() === fontName.toLowerCase(),\n );\n\n if (customFont) {\n const fallback = customFont.fallback ?? defaultFallback.value;\n return `'${customFont.name}', ${fallback}`;\n }\n\n const builtInStack = BUILT_IN_FONT_STACKS[fontName.toLowerCase()];\n if (builtInStack) {\n return builtInStack;\n }\n\n return fontName.includes(\",\")\n ? fontName\n : `${fontName}, ${defaultFallback.value}`;\n }\n\n const createdLinks: HTMLLinkElement[] = [];\n let disposed = false;\n\n async function loadCustomFonts(): Promise<void> {\n if (customFonts.value.length === 0) {\n if (!disposed) isLoaded.value = true;\n return;\n }\n\n const loadPromises = customFonts.value.map(async (font) => {\n try {\n const existingLink = document.querySelector(\n `link[data-custom-font=\"${CSS.escape(font.name)}\"]`,\n );\n if (existingLink) {\n return;\n }\n\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = font.url;\n link.setAttribute(\"data-custom-font\", font.name);\n\n // Track the link before awaiting so a load failure still leaves it\n // recoverable via cleanupFontLinks — otherwise the <link> orphans\n // in <head> after a network/CSS error.\n createdLinks.push(link);\n\n await new Promise<void>((resolve, reject) => {\n link.onload = () => resolve();\n link.onerror = () =>\n reject(new Error(`Failed to load font: ${font.name}`));\n // shadow-ok: web fonts must load at document level; @font-face from inside a shadow root is unreliable across browsers\n document.head.appendChild(link);\n });\n } catch (error) {\n logger.warn(`Failed to load custom font \"${font.name}\":`, error);\n }\n });\n\n await Promise.allSettled(loadPromises);\n if (disposed) return;\n isLoaded.value = true;\n }\n\n function cleanupFontLinks(): void {\n for (const link of createdLinks) {\n link.remove();\n }\n createdLinks.length = 0;\n }\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n disposed = true;\n cleanupFontLinks();\n });\n }\n\n return {\n fonts,\n defaultFont,\n defaultFallback,\n customFonts,\n customFontsEnabled,\n isLoaded,\n setCustomFontsEnabled,\n loadCustomFonts,\n cleanupFontLinks,\n getFontWithFallback,\n getDefaultFont,\n };\n}\n","import type { LogicPair, LogicTag, SyntaxPreset } from \"@templatical/types\";\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport { inject, ref, type Ref } from \"vue\";\nimport {\n LOGIC_PAIRS_KEY,\n LOGIC_TAG_PICKER_KEY,\n LOGIC_TAGS_KEY,\n MERGE_TAG_SYNTAX_KEY,\n ON_REQUEST_LOGIC_TAG_KEY,\n} from \"../keys\";\nimport type { LogicTagPickerResult } from \"./useLogicTagPicker\";\n\nexport interface UseLogicTagReturn {\n /** Configured standalone logic tags. */\n logicTags: LogicTag[];\n /** Configured open/close logic pairs. */\n logicPairs: LogicPair[];\n /** Resolved syntax preset — shared with merge tags; drives keyword badges. */\n syntax: SyntaxPreset;\n /**\n * Whether the \"Insert logic\" affordance should show. True when at least one\n * logic tag or pair is configured.\n */\n canInsertLogicTag: boolean;\n /** Whether a logic pick is in progress. */\n isRequesting: Ref<boolean>;\n /**\n * Open the built-in logic picker. Resolves with the chosen tag/pair, or null\n * when nothing is configured, the picker is unavailable (headless), or the\n * user cancels.\n */\n requestLogicTag: () => Promise<LogicTagPickerResult | null>;\n}\n\n/**\n * Composable for the standalone logic-tag feature — separate from merge tags.\n * Exposes configured logic tags/pairs and opens the built-in logic picker.\n */\nexport function useLogicTag(): UseLogicTagReturn {\n const logicTags = inject(LOGIC_TAGS_KEY, []);\n const logicPairs = inject(LOGIC_PAIRS_KEY, []);\n const syntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\n const onRequest = inject(ON_REQUEST_LOGIC_TAG_KEY, null);\n // Picker may be null in headless contexts (tests, non-editor consumers).\n const picker = inject(LOGIC_TAG_PICKER_KEY, null);\n\n const isRequesting = ref(false);\n\n // Show the affordance when a consumer callback is set OR static config\n // exists — mirrors useMergeTag's canRequestMergeTag.\n const canInsertLogicTag =\n !!onRequest || logicTags.length > 0 || logicPairs.length > 0;\n\n const hasStaticLogic = logicTags.length > 0 || logicPairs.length > 0;\n\n /**\n * Request a logic tag/pair from the user. Precedence:\n * 1. `logic.onRequest` callback (consumer-owned UX)\n * 2. Built-in picker over `logic.tags` / `logic.pairs`\n * 3. null (nothing configured / headless)\n */\n async function requestLogicTag(): Promise<LogicTagPickerResult | null> {\n if (onRequest) {\n isRequesting.value = true;\n try {\n return await onRequest();\n } finally {\n isRequesting.value = false;\n }\n }\n if (picker && hasStaticLogic) {\n isRequesting.value = true;\n try {\n return await picker.open(logicTags, logicPairs);\n } finally {\n isRequesting.value = false;\n }\n }\n return null;\n }\n\n return {\n logicTags,\n logicPairs,\n syntax,\n canInsertLogicTag,\n isRequesting,\n requestLogicTag,\n };\n}\n","import {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n type SyntaxPreset,\n} from \"@templatical/types\";\n\n/**\n * TipTap content spec for inserting a merge tag as the correct node type.\n * `mergeTagNode` for data tags (`{{ first_name }}`), `logicMergeTagNode` for\n * logic tags (`{% if vip %}`).\n */\nexport type MergeTagNodeSpec =\n | { type: \"mergeTagNode\"; attrs: { label: string; value: string } }\n | { type: \"logicMergeTagNode\"; attrs: { value: string; keyword: string } };\n\n/**\n * Resolve which node a merge tag value should be inserted as, inferring the\n * kind from the value's shape (there is no explicit type discriminator on\n * `MergeTag`). A logic-shaped value yields a `logicMergeTagNode` carrying the\n * derived keyword; everything else yields a data `mergeTagNode`.\n *\n * This gives the selector / autocomplete insertion paths the same branching\n * that manual typing already gets from each node's input rule, so a logic tag\n * picked from the merge tag selector renders as a logic badge instead of a\n * data tag. `label` is only used by data tags — logic nodes display the\n * keyword extracted from the value.\n *\n * Attrs are constructed from `value`/`label`/derived keyword only, so callers\n * that smuggle extra `MergeTag` fields (group, description) via a wider type\n * cannot leak them into the document JSON.\n */\nexport function mergeTagNodeSpec(\n value: string,\n label: string,\n syntax: SyntaxPreset,\n): MergeTagNodeSpec {\n if (isLogicMergeTagValue(value, syntax)) {\n return {\n type: \"logicMergeTagNode\",\n attrs: { value, keyword: getLogicMergeTagKeyword(value, syntax) },\n };\n }\n return { type: \"mergeTagNode\", attrs: { label, value } };\n}\n","import type { LogicPair, SyntaxPreset } from \"@templatical/types\";\nimport type { Editor } from \"@tiptap/core\";\nimport type { LogicTagPickerResult } from \"../composables/useLogicTagPicker\";\nimport { mergeTagNodeSpec } from \"./mergeTagNodeSpec\";\n\n/** A pair carries `before`/`after`; a standalone tag carries `value`. */\nexport function isLogicTagPair(item: LogicTagPickerResult): item is LogicPair {\n return \"before\" in item && \"after\" in item;\n}\n\n/**\n * Insert a chosen logic tag or pair into the editor. Each side becomes a\n * `logicMergeTagNode` (via `mergeTagNodeSpec`), matching typed/pasted logic.\n *\n * - Standalone tag → inserted at the cursor.\n * - Pair with a non-empty selection → wraps it: `[before]{selection}[after]`.\n * - Pair with no selection → both pills adjacent, caret placed between them.\n */\nexport function insertLogicTagSelection(\n editor: Editor,\n item: LogicTagPickerResult,\n syntax: SyntaxPreset,\n): void {\n if (!isLogicTagPair(item)) {\n editor\n .chain()\n .focus()\n .insertContent(mergeTagNodeSpec(item.value, item.label, syntax))\n .run();\n return;\n }\n\n const openSpec = mergeTagNodeSpec(item.before, item.label, syntax);\n const closeSpec = mergeTagNodeSpec(item.after, item.label, syntax);\n const { from, to } = editor.state.selection;\n\n if (from !== to) {\n // Wrap the selection. Insert the closer at `to` first so the opener's\n // insert at `from` (which shifts everything after it) doesn't invalidate\n // `to` mid-chain — `from < to`, so `to`'s insert leaves `from` valid.\n editor\n .chain()\n .focus()\n .insertContentAt(to, closeSpec)\n .insertContentAt(from, openSpec)\n .run();\n return;\n }\n\n // No selection: drop both pills and park the caret between them. Each pill\n // is an inline atom of size 1, so the gap sits at `from + 1`.\n editor\n .chain()\n .focus()\n .insertContentAt(from, [openSpec, closeSpec])\n .setTextSelection(from + 1)\n .run();\n}\n","import type { Mark } from \"@tiptap/core\";\nimport { normalizeColorToHex } from \"./color\";\n\n/**\n * Restrict a per-link color to safe CSS color forms — hex, `rgb()/rgba()`,\n * `hsl()/hsla()`, or a bare keyword. Anything containing characters that could\n * break out of the `style=\"color: …\"` attribute in the exported HTML (`;`,\n * `{`, `}`, quotes, `url(`) is rejected and returns `null`, so no color is\n * emitted rather than an attacker-controlled declaration. The renderer passes\n * link content through verbatim, so this is the sanitization boundary for the\n * exported email as well as the canvas.\n */\nexport function sanitizeLinkColor(value?: string | null): string | null {\n if (!value) return null;\n const v = value.trim();\n if (/^#[0-9a-f]{3,8}$/i.test(v)) return v;\n if (/^(rgb|rgba|hsl|hsla)\\(\\s*[0-9.,%/\\s]+\\)$/i.test(v)) return v;\n if (/^[a-z]+$/i.test(v)) return v;\n return null;\n}\n\n/**\n * Extend the TipTap Link mark with a `color` attribute so a link's color lives\n * on the `<a>` itself (rendered as inline `style=\"color: …\"`), not a separate\n * inner text-color span.\n *\n * Why: a text-color mark only colors the inner text, while the underline is\n * painted by the ancestor `<a>` in the document link color — so an overridden\n * link showed mismatched text and underline (and the same mismatch shipped in\n * the exported MJML/HTML). With the color on the `<a>`, the underline follows\n * `currentColor`, so text and underline stay in sync in the canvas and the\n * exported email. Inline color also beats the global `a { color }` rule on both\n * surfaces, so a per-link color overrides the document link color. (Per-link\n * color, deferred from #352.)\n *\n * The base `LinkExt` is passed in (rather than imported) so this helper carries\n * no static `@tiptap` dependency and rides the editor's lazy-loaded chunk.\n */\nexport function withLinkColor(LinkExt: Mark): Mark {\n return LinkExt.extend({\n addAttributes() {\n return {\n // Preserve href/target/rel and any other base Link attributes.\n ...this.parent?.(),\n color: {\n default: null,\n parseHTML: (element: HTMLElement) =>\n sanitizeLinkColor(normalizeColorToHex(element.style.color)),\n renderHTML: (attributes: Record<string, unknown>) => {\n const color = sanitizeLinkColor(attributes.color as string | null);\n return color ? { style: `color: ${color}` } : {};\n },\n },\n };\n },\n });\n}\n","import type { Editor } from \"@tiptap/core\";\nimport { ref, type Ref, type ShallowRef } from \"vue\";\nimport { useFocusTrap } from \"./useFocusTrap\";\nimport { sanitizeLinkColor } from \"../utils/linkColorExtension\";\n\nexport interface UseRichTextLinkDialogReturn {\n showLinkDialog: Ref<boolean>;\n linkUrl: Ref<string>;\n /** Per-link color (inline on the `<a>`); empty = inherit the document link color. */\n linkColor: Ref<string>;\n linkDialogRef: Ref<HTMLElement | null>;\n openLinkDialog: () => void;\n insertLink: () => void;\n removeLink: () => void;\n closeLinkDialog: () => void;\n handleLinkKeydown: (event: KeyboardEvent) => void;\n}\n\nexport function useRichTextLinkDialog(\n editor: ShallowRef<Editor | null>,\n): UseRichTextLinkDialogReturn {\n const showLinkDialog = ref(false);\n const linkUrl = ref(\"\");\n const linkColor = ref(\"\");\n const linkDialogRef = ref<HTMLElement | null>(null);\n useFocusTrap(linkDialogRef, showLinkDialog);\n\n function openLinkDialog(): void {\n const attrs = editor.value?.getAttributes(\"link\") ?? {};\n linkUrl.value = attrs.href || \"\";\n linkColor.value = attrs.color || \"\";\n showLinkDialog.value = true;\n }\n\n function insertLink(): void {\n if (linkUrl.value) {\n const url = normalizeLinkUrl(linkUrl.value);\n if (url !== null) {\n const color = sanitizeLinkColor(linkColor.value);\n // Set the link, then stamp its color on the `<a>` itself so text and\n // underline stay in sync. Empty/invalid color → null = inherit the\n // document link color.\n const chain = editor.value\n ?.chain()\n .focus()\n .extendMarkRange(\"link\")\n .setLink({ href: url })\n .updateAttributes(\"link\", { color });\n // A per-link color takes absolute priority over any inner text-color:\n // strip the color from any text-style mark on the link range so the\n // `<a>` color alone drives both the glyphs and the underline. Without\n // it, an inner color span keeps painting the text while only the\n // underline (drawn by the `<a>`) picks up the link color — the mismatch\n // this control exists to remove. Guarded on the command existing: Title\n // links share this dialog but register no Color extension.\n if (color && typeof editor.value?.commands.unsetColor === \"function\") {\n chain?.unsetColor();\n }\n chain?.run();\n }\n }\n closeLinkDialog();\n }\n\n // Allowlist mirroring @tiptap/extension-link defaults so URLs persisted\n // via the dialog can't smuggle in javascript:/data:/vbscript: payloads\n // that bypass TipTap's apply-time filter on JSON load.\n const SAFE_SCHEMES = new Set([\n \"http\",\n \"https\",\n \"mailto\",\n \"tel\",\n \"ftp\",\n \"ftps\",\n \"sms\",\n \"xmpp\",\n \"cid\",\n ]);\n\n function normalizeLinkUrl(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n if (trimmed.startsWith(\"#\")) return trimmed;\n const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);\n if (schemeMatch) {\n return SAFE_SCHEMES.has(schemeMatch[1].toLowerCase()) ? trimmed : null;\n }\n return `https://${trimmed}`;\n }\n\n function removeLink(): void {\n editor.value?.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n closeLinkDialog();\n }\n\n function closeLinkDialog(): void {\n showLinkDialog.value = false;\n linkUrl.value = \"\";\n linkColor.value = \"\";\n }\n\n function handleLinkKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\") {\n event.preventDefault();\n insertLink();\n } else if (event.key === \"Escape\") {\n closeLinkDialog();\n }\n }\n\n return {\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n };\n}\n","import type {\n Editor,\n EditorContent as EditorContentComponent,\n} from \"@tiptap/vue-3\";\nimport type { Extension, Mark, Node } from \"@tiptap/core\";\nimport { useEventListener, useTimeoutFn } from \"@vueuse/core\";\nimport {\n inject,\n isRef,\n onBeforeUnmount,\n ref,\n shallowRef,\n watch,\n type Ref,\n type ShallowRef,\n} from \"vue\";\nimport type { Translations } from \"../i18n\";\nimport { getSyntaxTriggerChar } from \"@templatical/types\";\nimport { EDITOR_KEY, TRANSLATIONS_KEY } from \"../keys\";\nimport { useMergeTag } from \"./useMergeTag\";\nimport { useLogicTag } from \"./useLogicTag\";\nimport { insertLogicTagSelection } from \"../utils/insertLogicTag\";\nimport { useRichTextLinkDialog } from \"./useRichTextLinkDialog\";\nimport { logger } from \"../utils/logger\";\n\nexport interface MergeTagContext {\n mergeTags: ReturnType<typeof useMergeTag>[\"mergeTags\"];\n syntax: ReturnType<typeof useMergeTag>[\"syntax\"];\n /** Resolved trigger string for built-in syntaxes; null for custom syntax. */\n triggerChar: string | null;\n /** Whether the consumer has enabled autocomplete (default true). */\n autocompleteEnabled: boolean;\n /** Localized empty-state label for the autocomplete popup. */\n suggestionEmptyText: string;\n}\n\nexport interface UseRichTextEditorOptions {\n blockId: () => string;\n blockContent: () => string;\n loadExtensions: (ctx: MergeTagContext) => Promise<{\n TiptapEditor: typeof Editor;\n EC: typeof EditorContentComponent;\n extensions: (Extension | Mark | Node)[];\n }>;\n onDone: () => void;\n onClickOutsideSideEffect?: (target: HTMLElement) => void;\n editorName?: string;\n}\n\nexport interface UseRichTextEditorReturn {\n editor: ShallowRef<Editor | null>;\n EditorContent: ShallowRef<typeof EditorContentComponent | null>;\n isLoading: Ref<boolean>;\n initError: Ref<string | null>;\n retry: () => void;\n showLinkDialog: Ref<boolean>;\n linkUrl: Ref<string>;\n linkColor: Ref<string>;\n linkDialogRef: Ref<HTMLElement | null>;\n mergeTags: ReturnType<typeof useMergeTag>[\"mergeTags\"];\n canRequestMergeTag: ReturnType<typeof useMergeTag>[\"canRequestMergeTag\"];\n isRequestingMergeTag: ReturnType<typeof useMergeTag>[\"isRequesting\"];\n syntax: ReturnType<typeof useMergeTag>[\"syntax\"];\n canInsertLogicTag: ReturnType<typeof useLogicTag>[\"canInsertLogicTag\"];\n isRequestingLogicTag: ReturnType<typeof useLogicTag>[\"isRequesting\"];\n openLinkDialog: () => void;\n insertLink: () => void;\n removeLink: () => void;\n closeLinkDialog: () => void;\n handleLinkKeydown: (event: KeyboardEvent) => void;\n handleAddMergeTag: () => Promise<void>;\n handleAddLogicTag: () => Promise<void>;\n}\n\nexport function useRichTextEditor(\n options: UseRichTextEditorOptions,\n): UseRichTextEditorReturn {\n const emailEditor = inject(EDITOR_KEY, null);\n\n const {\n mergeTags,\n canRequestMergeTag,\n isRequesting: isRequestingMergeTag,\n requestMergeTag,\n syntax,\n autocomplete: autocompleteEnabled,\n } = useMergeTag();\n\n const {\n canInsertLogicTag,\n isRequesting: isRequestingLogicTag,\n requestLogicTag,\n } = useLogicTag();\n\n const injectedTranslations = inject(TRANSLATIONS_KEY, null) as\n Translations | Ref<Translations> | null;\n const resolvedTranslations: Translations | null = isRef(injectedTranslations)\n ? injectedTranslations.value\n : injectedTranslations;\n const suggestionEmptyText =\n resolvedTranslations?.mergeTag?.suggestionEmpty ?? \"No matching merge tags\";\n const triggerChar = getSyntaxTriggerChar(syntax);\n\n const editor = shallowRef<Editor | null>(null);\n\n const {\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n } = useRichTextLinkDialog(editor);\n\n const { start: startFocusTimeout, stop: stopFocusTimeout } = useTimeoutFn(\n () => editor.value?.commands.focus(\"end\"),\n 0,\n { immediate: false },\n );\n const EditorContent = shallowRef<typeof EditorContentComponent | null>(null);\n const isLoading = ref(true);\n const initError = ref<string | null>(null);\n\n let destroyed = false;\n\n async function initEditor(): Promise<void> {\n initError.value = null;\n isLoading.value = true;\n\n try {\n const { TiptapEditor, EC, extensions } = await options.loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n });\n\n // Component unmounted while we awaited loadExtensions — bail out\n // before constructing the TipTap editor. Otherwise the editor escapes\n // the onBeforeUnmount destroy hook that already ran.\n if (destroyed) {\n return;\n }\n\n EditorContent.value = EC;\n\n const seen = new Map<string, number>();\n extensions.forEach((ext, i) => seen.set(ext.name, i));\n const uniqueExtensions = extensions.filter(\n (ext, i) => seen.get(ext.name) === i,\n );\n\n const instance = new TiptapEditor({\n extensions: uniqueExtensions,\n content: options.blockContent(),\n editable: true,\n onUpdate: ({ editor: e }) => {\n if (destroyed) return;\n if (emailEditor) {\n emailEditor.updateBlock(options.blockId(), {\n content: e.getHTML(),\n });\n }\n },\n });\n\n // A second unmount check covers the gap between `await` resolving and\n // the constructor running.\n if (destroyed) {\n instance.destroy();\n return;\n }\n\n editor.value = instance;\n isLoading.value = false;\n startFocusTimeout();\n } catch (error) {\n if (destroyed) return;\n logger.error(\n `[${options.editorName ?? \"RichTextEditor\"}] Failed to initialize TipTap editor:`,\n error,\n );\n initError.value =\n error instanceof Error ? error.message : \"Failed to load editor\";\n isLoading.value = false;\n }\n }\n\n function retry(): void {\n editor.value?.destroy();\n editor.value = null;\n initEditor();\n }\n\n initEditor();\n\n const stopContentWatch = watch(\n () => options.blockContent(),\n (newContent) => {\n if (editor.value) {\n const currentContent = editor.value.getHTML();\n if (currentContent !== newContent) {\n editor.value.commands.setContent(newContent, { emitUpdate: false });\n }\n }\n },\n );\n\n function handleClickOutside(event: MouseEvent): void {\n // Suppress while either built-in picker is open: its modal lives in the\n // popover root (outside .tpl-text-editor-wrapper), so a click on a picker\n // item would otherwise fall through to onDone() and tear the block out of\n // edit mode before the pending insert runs — the insert then no-ops on the\n // `destroyed` guard and nothing lands.\n if (isRequestingMergeTag.value || isRequestingLogicTag.value) return;\n\n // `event.target` gets retargeted to the shadow host when the editor\n // mounts inside a shadow root and the listener is at `document`\n // level. The host element isn't inside any `.tpl-text-editor-wrapper`,\n // so `target.closest(...)` returns null and we'd fall through to\n // `onDone()` — closing the editor on every click inside the shadow\n // tree (including double-click word-select). Walk the composed path\n // instead: the first HTMLElement is the real innermost clicked\n // element regardless of shadow boundaries.\n const path = event.composedPath();\n const innerTarget = path.find(\n (n): n is HTMLElement => n instanceof HTMLElement,\n );\n if (!innerTarget) return;\n\n options.onClickOutsideSideEffect?.(innerTarget);\n\n // The link dialog's color picker teleports to the popover root (outside\n // .tpl-link-dialog) AND renders its wheel inside vanilla-colorful's own\n // shadow DOM. `closest()` can't cross that inner shadow boundary, so a\n // mousedown on the wheel has an `innerTarget` from which\n // `.closest('.tpl-color-popover')` returns null. Match via the composed\n // path instead — it flattens shadow boundaries. Without this, picking a\n // color falls through to onDone(), tearing the block out of edit mode (and\n // unmounting the dialog) before the color is applied.\n const inColorPopover = path.some(\n (n) =>\n n instanceof HTMLElement && n.classList.contains(\"tpl-color-popover\"),\n );\n\n if (\n innerTarget.closest(\".tpl-text-editor-wrapper\") ||\n innerTarget.closest(\".tpl-text-toolbar\") ||\n innerTarget.closest(\".tpl-link-dialog\") ||\n inColorPopover\n ) {\n return;\n }\n\n options.onDone();\n }\n\n useEventListener(document, \"mousedown\", handleClickOutside);\n\n onBeforeUnmount(() => {\n destroyed = true;\n stopContentWatch();\n stopFocusTimeout();\n editor.value?.destroy();\n });\n\n async function handleAddMergeTag(): Promise<void> {\n const mergeTag = await requestMergeTag();\n if (destroyed) return;\n if (mergeTag && editor.value) {\n editor.value\n .chain()\n .focus()\n .insertMergeTag({\n label: mergeTag.label,\n value: mergeTag.value,\n })\n .run();\n } else {\n editor.value?.commands.focus();\n }\n }\n\n async function handleAddLogicTag(): Promise<void> {\n const item = await requestLogicTag();\n if (destroyed) return;\n if (item && editor.value) {\n insertLogicTagSelection(editor.value, item, syntax);\n } else {\n editor.value?.commands.focus();\n }\n }\n\n return {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n mergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n syntax,\n canInsertLogicTag,\n isRequestingLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n };\n}\n","import {\n computed,\n toValue,\n type ComputedRef,\n type MaybeRefOrGetter,\n type Ref,\n} from \"vue\";\nimport { useMediaQuery } from \"@vueuse/core\";\n\n/**\n * Viewport width below which the editor chrome can't lay out usably and we\n * show `SmallScreenNotice` instead (issue #235). The three-pane layout needs\n * the 48px palette rail + 320px properties panel + a canvas with padding —\n * roughly 768px before the canvas gets workable room. A typical phone\n * (~360–410px CSS) leaves the canvas at zero width.\n *\n * Deliberately viewport-based (not container/element width): it targets\n * actual small *devices*, so embedding the editor in a narrow desktop pane\n * doesn't trip the notice.\n */\nexport const SMALL_SCREEN_QUERY = \"(max-width: 767px)\";\n\nexport interface UseSmallScreenNoticeReturn {\n /** True while the viewport matches `SMALL_SCREEN_QUERY`. */\n isSmallScreen: Ref<boolean>;\n /** True when the notice should replace the editor chrome. */\n showNotice: ComputedRef<boolean>;\n}\n\n/**\n * Reactive gate for the small-screen notice.\n *\n * @param enabled - The consumer's `smallScreenNotice` config flag. Defaults to\n * `true` when `undefined`, so the notice is opt-out, not opt-in. Pass `false`\n * (or a getter returning `false`) to let the editor render its chrome at any\n * width — for consumers handling small screens themselves.\n */\nexport function useSmallScreenNotice(\n enabled?: MaybeRefOrGetter<boolean | undefined>,\n): UseSmallScreenNoticeReturn {\n const isSmallScreen = useMediaQuery(SMALL_SCREEN_QUERY);\n const showNotice = computed(\n () => (toValue(enabled) ?? true) && isSmallScreen.value,\n );\n return { isSmallScreen, showNotice };\n}\n","<script setup lang=\"ts\">\nimport CustomBlockIcon from \"../CustomBlockIcon.vue\";\nimport LoadingTrack from \"../LoadingTrack.vue\";\nimport { useI18n } from \"../../composables\";\nimport { useDataSourceFetch } from \"@templatical/core\";\nimport type {\n CustomBlock as CustomBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { substituteTextMergeTagSamples } from \"@templatical/types\";\nimport { useDebounceFn } from \"@vueuse/core\";\nimport { TriangleAlert, Puzzle } from \"@lucide/vue\";\nimport { computed, inject, onMounted, ref, watch } from \"vue\";\nimport {\n BLOCK_REGISTRY_KEY,\n MERGE_TAGS_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../../keys\";\n\nconst props = defineProps<{\n block: CustomBlockType;\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (\n e: \"fetchData\",\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n ): void;\n}>();\n\nconst { t } = useI18n();\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst renderedHtml = ref(\"\");\nconst hasError = ref(false);\n\nconst definition = computed(() =>\n blockRegistry?.getDefinition(props.block.customType),\n);\n\nconst hasDefinition = computed(() => !!definition.value);\n\nconst blockRef = computed(() => props.block);\n\nconst {\n isFetching,\n fetch: fetchData,\n needsFetch,\n hasDataSource,\n} = useDataSourceFetch({\n definition,\n block: blockRef,\n onUpdate: (fieldValues, fetched) => {\n emit(\"fetchData\", { fieldValues, dataSourceFetched: fetched });\n },\n});\n\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst useSamples = inject(USE_MERGE_TAG_SAMPLES_KEY, null);\n\n/**\n * The block handed to the renderer. In Sample mode its field values have their\n * tokens substituted, so the consumer's own template renders sample data.\n *\n * Top-level strings only: `fieldValues` is `Record<string, unknown>` of\n * consumer-defined shape, and recursing arbitrary nested data on every render\n * is not worth it. Non-string values pass through untouched.\n */\nconst blockForRender = computed<CustomBlockType>(() => {\n if (!useSamples?.value) return props.block;\n\n const fieldValues: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(props.block.fieldValues)) {\n fieldValues[key] =\n typeof value === \"string\"\n ? substituteTextMergeTagSamples(value, mergeTags)\n : value;\n }\n return { ...props.block, fieldValues };\n});\n\nasync function renderBlock(): Promise<void> {\n if (!blockRegistry) {\n return;\n }\n\n hasError.value = false;\n\n try {\n const html = await blockRegistry.renderCustomBlock(blockForRender.value);\n if (html.includes(\"Template render error:\")) {\n hasError.value = true;\n }\n renderedHtml.value = html;\n } catch {\n hasError.value = true;\n renderedHtml.value = \"\";\n }\n}\n\nconst debouncedRender = useDebounceFn(renderBlock, 150);\n\nonMounted(() => {\n renderBlock();\n});\n\nwatch(\n () => props.block.fieldValues,\n () => {\n debouncedRender();\n },\n { deep: true },\n);\n\n// The render is async and cached in `renderedHtml`, so flipping the mode has to\n// re-run it — otherwise the preview keeps showing whichever view rendered first.\nwatch(\n () => useSamples?.value,\n () => {\n debouncedRender();\n },\n);\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div\n v-if=\"!hasDefinition\"\n class=\"tpl:flex tpl:min-h-[80px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:border-[var(--tpl-warning)] tpl:bg-[var(--tpl-warning-light)]\"\n >\n <Puzzle :size=\"24\" class=\"tpl:text-[var(--tpl-warning)]\" />\n <span class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ t.customBlocks.definitionNotFound }}\n </span>\n </div>\n\n <div\n v-else-if=\"hasError\"\n class=\"tpl:flex tpl:min-h-[80px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:border-[var(--tpl-danger)] tpl:bg-[var(--tpl-danger-light)]\"\n >\n <TriangleAlert :size=\"24\" class=\"tpl:text-[var(--tpl-danger)]\" />\n <span class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ t.customBlocks.renderError }}\n </span>\n </div>\n\n <div v-else class=\"tpl:relative\">\n <!-- eslint-disable-next-line vue/no-v-html -->\n <div v-html=\"renderedHtml\" />\n\n <!-- Data source CTA overlay -->\n <div\n v-if=\"hasDataSource && needsFetch\"\n class=\"tpl:absolute tpl:inset-0 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:rounded tpl:backdrop-blur-[2px]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n \"\n >\n <button\n v-if=\"!isFetching\"\n type=\"button\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-2 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-2 tpl:text-sm tpl:font-semibold tpl:shadow-sm tpl:transition-all tpl:duration-150 hover:tpl:border-[var(--tpl-primary)] hover:tpl:shadow-md tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click.stop=\"fetchData\"\n >\n <CustomBlockIcon\n v-if=\"definition?.icon\"\n :icon=\"definition.icon\"\n :size=\"16\"\n />\n {{\n definition?.dataSource?.label ||\n t.customBlocks.dataSource.fetchButton\n }}\n </button>\n\n <LoadingTrack v-else class=\"tpl:w-48\" />\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport CustomBlockIcon from \"../CustomBlockIcon.vue\";\nimport LoadingTrack from \"../LoadingTrack.vue\";\nimport { useI18n } from \"../../composables\";\nimport { useDataSourceFetch } from \"@templatical/core\";\nimport type {\n CustomBlock as CustomBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { substituteTextMergeTagSamples } from \"@templatical/types\";\nimport { useDebounceFn } from \"@vueuse/core\";\nimport { TriangleAlert, Puzzle } from \"@lucide/vue\";\nimport { computed, inject, onMounted, ref, watch } from \"vue\";\nimport {\n BLOCK_REGISTRY_KEY,\n MERGE_TAGS_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../../keys\";\n\nconst props = defineProps<{\n block: CustomBlockType;\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (\n e: \"fetchData\",\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n ): void;\n}>();\n\nconst { t } = useI18n();\n\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst renderedHtml = ref(\"\");\nconst hasError = ref(false);\n\nconst definition = computed(() =>\n blockRegistry?.getDefinition(props.block.customType),\n);\n\nconst hasDefinition = computed(() => !!definition.value);\n\nconst blockRef = computed(() => props.block);\n\nconst {\n isFetching,\n fetch: fetchData,\n needsFetch,\n hasDataSource,\n} = useDataSourceFetch({\n definition,\n block: blockRef,\n onUpdate: (fieldValues, fetched) => {\n emit(\"fetchData\", { fieldValues, dataSourceFetched: fetched });\n },\n});\n\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst useSamples = inject(USE_MERGE_TAG_SAMPLES_KEY, null);\n\n/**\n * The block handed to the renderer. In Sample mode its field values have their\n * tokens substituted, so the consumer's own template renders sample data.\n *\n * Top-level strings only: `fieldValues` is `Record<string, unknown>` of\n * consumer-defined shape, and recursing arbitrary nested data on every render\n * is not worth it. Non-string values pass through untouched.\n */\nconst blockForRender = computed<CustomBlockType>(() => {\n if (!useSamples?.value) return props.block;\n\n const fieldValues: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(props.block.fieldValues)) {\n fieldValues[key] =\n typeof value === \"string\"\n ? substituteTextMergeTagSamples(value, mergeTags)\n : value;\n }\n return { ...props.block, fieldValues };\n});\n\nasync function renderBlock(): Promise<void> {\n if (!blockRegistry) {\n return;\n }\n\n hasError.value = false;\n\n try {\n const html = await blockRegistry.renderCustomBlock(blockForRender.value);\n if (html.includes(\"Template render error:\")) {\n hasError.value = true;\n }\n renderedHtml.value = html;\n } catch {\n hasError.value = true;\n renderedHtml.value = \"\";\n }\n}\n\nconst debouncedRender = useDebounceFn(renderBlock, 150);\n\nonMounted(() => {\n renderBlock();\n});\n\nwatch(\n () => props.block.fieldValues,\n () => {\n debouncedRender();\n },\n { deep: true },\n);\n\n// The render is async and cached in `renderedHtml`, so flipping the mode has to\n// re-run it — otherwise the preview keeps showing whichever view rendered first.\nwatch(\n () => useSamples?.value,\n () => {\n debouncedRender();\n },\n);\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div\n v-if=\"!hasDefinition\"\n class=\"tpl:flex tpl:min-h-[80px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:border-[var(--tpl-warning)] tpl:bg-[var(--tpl-warning-light)]\"\n >\n <Puzzle :size=\"24\" class=\"tpl:text-[var(--tpl-warning)]\" />\n <span class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ t.customBlocks.definitionNotFound }}\n </span>\n </div>\n\n <div\n v-else-if=\"hasError\"\n class=\"tpl:flex tpl:min-h-[80px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:border-[var(--tpl-danger)] tpl:bg-[var(--tpl-danger-light)]\"\n >\n <TriangleAlert :size=\"24\" class=\"tpl:text-[var(--tpl-danger)]\" />\n <span class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\">\n {{ t.customBlocks.renderError }}\n </span>\n </div>\n\n <div v-else class=\"tpl:relative\">\n <!-- eslint-disable-next-line vue/no-v-html -->\n <div v-html=\"renderedHtml\" />\n\n <!-- Data source CTA overlay -->\n <div\n v-if=\"hasDataSource && needsFetch\"\n class=\"tpl:absolute tpl:inset-0 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-3 tpl:rounded tpl:backdrop-blur-[2px]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n \"\n >\n <button\n v-if=\"!isFetching\"\n type=\"button\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-2 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-2 tpl:text-sm tpl:font-semibold tpl:shadow-sm tpl:transition-all tpl:duration-150 hover:tpl:border-[var(--tpl-primary)] hover:tpl:shadow-md tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click.stop=\"fetchData\"\n >\n <CustomBlockIcon\n v-if=\"definition?.icon\"\n :icon=\"definition.icon\"\n :size=\"16\"\n />\n {{\n definition?.dataSource?.label ||\n t.customBlocks.dataSource.fetchButton\n }}\n </button>\n\n <LoadingTrack v-else class=\"tpl:w-48\" />\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n DividerBlock as DividerBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: DividerBlockType;\n viewport: ViewportSize;\n}>();\n\nconst dividerStyle = computed(() => ({\n borderTop: `${props.block.thickness}px ${props.block.lineStyle} ${props.block.color}`,\n width: props.block.width === \"full\" ? \"100%\" : `${props.block.width}px`,\n margin: props.block.width === \"full\" ? \"0\" : \"0 auto\",\n}));\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <hr class=\"tpl:m-0 tpl:border-none\" :style=\"dividerStyle\" />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n DividerBlock as DividerBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: DividerBlockType;\n viewport: ViewportSize;\n}>();\n\nconst dividerStyle = computed(() => ({\n borderTop: `${props.block.thickness}px ${props.block.lineStyle} ${props.block.color}`,\n width: props.block.width === \"full\" ? \"100%\" : `${props.block.width}px`,\n margin: props.block.width === \"full\" ? \"0\" : \"0 auto\",\n}));\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <hr class=\"tpl:m-0 tpl:border-none\" :style=\"dividerStyle\" />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n HtmlBlock as HtmlBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { substituteTextMergeTagSamples } from \"@templatical/types\";\nimport {\n HTML_BLOCK_PREVIEW_KEY,\n MERGE_TAGS_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../../keys\";\nimport { Code } from \"@lucide/vue\";\nimport { computed, inject, onBeforeUnmount, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n block: HtmlBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\n// Opt-in via `config.htmlBlockPreview` (default off → static placeholder).\nconst previewEnabled = inject(HTML_BLOCK_PREVIEW_KEY, false);\n\nconst hasContent = computed(() => props.block.content.trim().length > 0);\nconst showPreview = computed(() => previewEnabled && hasContent.value);\n\n/**\n * An `html` block reaches no label-resolving component, so its tokens are the\n * one place a raw `{{tag}}` really does render verbatim. In Sample mode they get\n * substituted; a tag with no sample stays a visible token, which is what shows\n * an author a sample is still missing.\n */\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst useSamples = inject(USE_MERGE_TAG_SAMPLES_KEY, null);\nconst previewContent = computed(() =>\n useSamples?.value\n ? substituteTextMergeTagSamples(props.block.content, mergeTags)\n : props.block.content,\n);\n\n// --- Sandboxed-iframe live preview ----------------------------------------\n// The fragment renders verbatim with scripting disabled\n// (`sandbox=\"allow-same-origin\"`, no `allow-scripts`), so nothing in it can\n// execute and its styles can't escape the frame. `allow-same-origin` — without\n// `allow-scripts` — is what lets the parent read `contentDocument` to size the\n// frame to its content; there's no script inside to abuse the same origin.\nconst iframeRef = ref<HTMLIFrameElement | null>(null);\nconst height = ref(60);\nlet resizeObserver: ResizeObserver | null = null;\n\nfunction measure(): void {\n const doc = iframeRef.value?.contentDocument;\n if (!doc) return;\n height.value = Math.max(\n doc.body?.scrollHeight ?? 0,\n doc.documentElement?.scrollHeight ?? 0,\n );\n}\n\nfunction onIframeLoad(): void {\n // Each `srcdoc` change loads a fresh document, so re-attach the observer to\n // the new document's root to catch late reflow (e.g. images loading in).\n resizeObserver?.disconnect();\n resizeObserver = null;\n measure();\n const root = iframeRef.value?.contentDocument?.documentElement;\n if (root && typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(() => measure());\n resizeObserver.observe(root);\n }\n}\n\n// Tear the observer down when the preview goes away so a later re-enable\n// doesn't keep measuring a detached document.\nwatch(showPreview, (on) => {\n if (!on) {\n resizeObserver?.disconnect();\n resizeObserver = null;\n }\n});\n\nonBeforeUnmount(() => {\n resizeObserver?.disconnect();\n resizeObserver = null;\n});\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <iframe\n v-if=\"showPreview\"\n ref=\"iframeRef\"\n :srcdoc=\"previewContent\"\n sandbox=\"allow-same-origin\"\n :title=\"t.html.preview\"\n class=\"tpl:block tpl:w-full tpl:border-0\"\n :style=\"{ height: `${height}px` }\"\n @load=\"onIframeLoad\"\n />\n <div\n v-else\n class=\"tpl:flex tpl:min-h-[80px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <Code :size=\"24\" class=\"tpl:text-[var(--tpl-text-dim)]\" />\n <span\n v-if=\"hasContent\"\n class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.html.preview }}\n </span>\n <span v-else class=\"tpl:text-sm tpl:text-[var(--tpl-text-dim)]\">\n {{ t.html.empty }}\n </span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n HtmlBlock as HtmlBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { substituteTextMergeTagSamples } from \"@templatical/types\";\nimport {\n HTML_BLOCK_PREVIEW_KEY,\n MERGE_TAGS_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n} from \"../../keys\";\nimport { Code } from \"@lucide/vue\";\nimport { computed, inject, onBeforeUnmount, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n block: HtmlBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\n// Opt-in via `config.htmlBlockPreview` (default off → static placeholder).\nconst previewEnabled = inject(HTML_BLOCK_PREVIEW_KEY, false);\n\nconst hasContent = computed(() => props.block.content.trim().length > 0);\nconst showPreview = computed(() => previewEnabled && hasContent.value);\n\n/**\n * An `html` block reaches no label-resolving component, so its tokens are the\n * one place a raw `{{tag}}` really does render verbatim. In Sample mode they get\n * substituted; a tag with no sample stays a visible token, which is what shows\n * an author a sample is still missing.\n */\nconst mergeTags = inject(MERGE_TAGS_KEY, []);\nconst useSamples = inject(USE_MERGE_TAG_SAMPLES_KEY, null);\nconst previewContent = computed(() =>\n useSamples?.value\n ? substituteTextMergeTagSamples(props.block.content, mergeTags)\n : props.block.content,\n);\n\n// --- Sandboxed-iframe live preview ----------------------------------------\n// The fragment renders verbatim with scripting disabled\n// (`sandbox=\"allow-same-origin\"`, no `allow-scripts`), so nothing in it can\n// execute and its styles can't escape the frame. `allow-same-origin` — without\n// `allow-scripts` — is what lets the parent read `contentDocument` to size the\n// frame to its content; there's no script inside to abuse the same origin.\nconst iframeRef = ref<HTMLIFrameElement | null>(null);\nconst height = ref(60);\nlet resizeObserver: ResizeObserver | null = null;\n\nfunction measure(): void {\n const doc = iframeRef.value?.contentDocument;\n if (!doc) return;\n height.value = Math.max(\n doc.body?.scrollHeight ?? 0,\n doc.documentElement?.scrollHeight ?? 0,\n );\n}\n\nfunction onIframeLoad(): void {\n // Each `srcdoc` change loads a fresh document, so re-attach the observer to\n // the new document's root to catch late reflow (e.g. images loading in).\n resizeObserver?.disconnect();\n resizeObserver = null;\n measure();\n const root = iframeRef.value?.contentDocument?.documentElement;\n if (root && typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(() => measure());\n resizeObserver.observe(root);\n }\n}\n\n// Tear the observer down when the preview goes away so a later re-enable\n// doesn't keep measuring a detached document.\nwatch(showPreview, (on) => {\n if (!on) {\n resizeObserver?.disconnect();\n resizeObserver = null;\n }\n});\n\nonBeforeUnmount(() => {\n resizeObserver?.disconnect();\n resizeObserver = null;\n});\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <iframe\n v-if=\"showPreview\"\n ref=\"iframeRef\"\n :srcdoc=\"previewContent\"\n sandbox=\"allow-same-origin\"\n :title=\"t.html.preview\"\n class=\"tpl:block tpl:w-full tpl:border-0\"\n :style=\"{ height: `${height}px` }\"\n @load=\"onIframeLoad\"\n />\n <div\n v-else\n class=\"tpl:flex tpl:min-h-[80px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <Code :size=\"24\" class=\"tpl:text-[var(--tpl-text-dim)]\" />\n <span\n v-if=\"hasContent\"\n class=\"tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.html.preview }}\n </span>\n <span v-else class=\"tpl:text-sm tpl:text-[var(--tpl-text-dim)]\">\n {{ t.html.empty }}\n </span>\n </div>\n </div>\n</template>\n","import {\n computed,\n toValue,\n type ComputedRef,\n type MaybeRefOrGetter,\n} from \"vue\";\nimport { useDropZone } from \"@vueuse/core\";\n\nexport interface UseImageDropOptions {\n /** The drop target element (a template ref). */\n target: MaybeRefOrGetter<HTMLElement | null | undefined>;\n /**\n * Called with the dropped image file(s). The editor acts on a single file\n * per drop (`files[0]`), but the array shape is kept for forward-compat.\n */\n onFiles: (files: File[]) => void;\n /**\n * Gate the zone. While falsy the drop is inert: no highlight, dropped files\n * are ignored. Components pass `canBrowseMedia && !isUploading` so a missing\n * media handler renders no affordance and a second drop can't race an\n * in-flight upload.\n */\n enabled?: MaybeRefOrGetter<boolean>;\n}\n\nexport interface UseImageDropReturn {\n /** True while an image drag hovers the (enabled) target — drive the highlight. */\n isOver: ComputedRef<boolean>;\n}\n\nconst IMAGE_MIME_PREFIX = \"image/\";\n\n/**\n * Drag-and-drop image upload onto an image block/field (#229). Wraps\n * `useDropZone`; filters dropped files to image MIME types client-side (the\n * authoritative size/type check stays server/plan-config side) and forwards\n * the first image to `onFiles`. The caller hands the file to `onRequestMedia`,\n * which uploads it and returns a `MediaResult` — identical to the Browse Media\n * path, only the input differs.\n *\n * `preventDefaultForUnhandled` keeps the browser from navigating to the dropped\n * file even for a non-image drop on the zone; the editor-root guard covers\n * drops that land outside any zone.\n */\nexport function useImageDrop(options: UseImageDropOptions): UseImageDropReturn {\n const { target, onFiles, enabled } = options;\n\n const isEnabled = (): boolean =>\n enabled === undefined ? true : toValue(enabled);\n\n const { isOverDropZone } = useDropZone(target, {\n preventDefaultForUnhandled: true,\n onDrop: (files) => {\n if (!isEnabled()) return;\n const images = (files ?? []).filter((file) =>\n file.type.startsWith(IMAGE_MIME_PREFIX),\n );\n if (images.length > 0) onFiles([images[0]]);\n },\n });\n\n const isOver = computed(() => isEnabled() && isOverDropZone.value);\n\n return { isOver };\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useMergeTag } from \"../../composables/useMergeTag\";\nimport type {\n ImageBlock as ImageBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { containsMergeTag } from \"@templatical/types\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../composables/useImageDrop\";\nimport { useResolvedImageSrc } from \"../../composables/useImageUrlResolver\";\n\nconst props = defineProps<{\n block: ImageBlockType;\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ImageBlockType>): void;\n}>();\n\nconst { t } = useI18n();\nconst { syntax } = useMergeTag();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst canBrowseMedia = computed(() => !!onRequestMedia);\nconst aliveFlag = useAliveFlag();\n\nasync function browseMedia(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n const updates: Partial<ImageBlockType> = { src: result.url };\n if (result.alt) updates.alt = result.alt;\n emit(\"update\", updates);\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\n\n// A merge-tag src is a deliberate dynamic value — never clobber it via drop.\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !hasMergeTagSrc.value,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) {\n const updates: Partial<ImageBlockType> = { src: result.url };\n if (result.alt) updates.alt = result.alt;\n emit(\"update\", updates);\n }\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n\nconst containerStyle = computed(() => ({\n textAlign: props.block.align,\n}));\n\nconst imageStyle = computed(() => {\n const align = props.block.align;\n return {\n maxWidth: \"100%\",\n width: props.block.width === \"full\" ? \"100%\" : `${props.block.width}px`,\n display: \"block\",\n marginLeft: align === \"center\" || align === \"right\" ? \"auto\" : undefined,\n marginRight: align === \"center\" ? \"auto\" : undefined,\n };\n});\n\nconst hasMergeTagSrc = computed(() =>\n containsMergeTag(props.block.src, syntax),\n);\n\n// Display-only preview URLs (#415). The canonical values stay in block.src /\n// block.placeholderUrl (and thus in the MJML export) — the resolver only\n// changes what the canvas <img> shows. A merge-tag src is never displayable,\n// so it's opted out of resolution; its placeholder preview resolves instead.\nconst displaySrc = useResolvedImageSrc(() =>\n hasMergeTagSrc.value ? undefined : props.block.src,\n);\nconst displayPlaceholderUrl = useResolvedImageSrc(() =>\n hasMergeTagSrc.value ? props.block.placeholderUrl : undefined,\n);\n</script>\n\n<template>\n <div\n ref=\"dropZoneRef\"\n data-testid=\"image-drop-zone\"\n class=\"tpl:relative tpl:w-full\"\n :style=\"containerStyle\"\n >\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 85%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"20\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"20\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <!-- Placeholder with preview image provided -->\n <template v-if=\"block.src && hasMergeTagSrc && block.placeholderUrl\">\n <a\n v-if=\"block.linkUrl\"\n :href=\"block.linkUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click.prevent\n >\n <img\n class=\"tpl:border-0\"\n loading=\"lazy\"\n :src=\"displayPlaceholderUrl\"\n :alt=\"block.alt || t.image.altTextPlaceholder\"\n :style=\"imageStyle\"\n />\n </a>\n <img\n v-else\n class=\"tpl:border-0\"\n :src=\"displayPlaceholderUrl\"\n :alt=\"block.alt\"\n :style=\"imageStyle\"\n />\n </template>\n <!-- Placeholder visual (no preview image) -->\n <div\n v-else-if=\"block.src && hasMergeTagSrc\"\n class=\"tpl:!flex tpl:min-h-[120px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-center tpl:bg-[var(--tpl-bg-elevated)]\"\n style=\"\n border-color: color-mix(in srgb, var(--tpl-primary) 40%, transparent);\n \"\n :style=\"imageStyle\"\n >\n <Image\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.5\"\n />\n <span\n class=\"tpl:max-w-full tpl:truncate tpl:px-3 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.7\"\n >\n <MergeTagPreviewText :text=\"block.src\" />\n </span>\n </div>\n <!-- Normal image rendering -->\n <template v-else-if=\"block.src\">\n <a\n v-if=\"block.linkUrl\"\n :href=\"block.linkUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click.prevent\n >\n <img\n class=\"tpl:border-0\"\n loading=\"lazy\"\n :src=\"displaySrc\"\n :alt=\"block.alt || t.image.altTextPlaceholder\"\n :style=\"imageStyle\"\n />\n </a>\n <img\n v-else\n class=\"tpl:border-0\"\n :src=\"displaySrc\"\n :alt=\"block.alt\"\n :style=\"imageStyle\"\n />\n </template>\n <!-- Empty state -->\n <div\n v-else\n class=\"tpl:flex tpl:min-h-[100px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-sm tpl:border-[var(--tpl-border-light)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <button\n v-if=\"canBrowseMedia\"\n :aria-label=\"t.image.browseMedia\"\n class=\"tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-150 tpl:cursor-pointer tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-primary)] tpl:bg-[var(--tpl-bg)]\"\n @click.stop=\"browseMedia\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n <span v-else>{{ t.image.clickToAdd }}</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useMergeTag } from \"../../composables/useMergeTag\";\nimport type {\n ImageBlock as ImageBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { containsMergeTag } from \"@templatical/types\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../composables/useImageDrop\";\nimport { useResolvedImageSrc } from \"../../composables/useImageUrlResolver\";\n\nconst props = defineProps<{\n block: ImageBlockType;\n viewport: ViewportSize;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ImageBlockType>): void;\n}>();\n\nconst { t } = useI18n();\nconst { syntax } = useMergeTag();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst canBrowseMedia = computed(() => !!onRequestMedia);\nconst aliveFlag = useAliveFlag();\n\nasync function browseMedia(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n const updates: Partial<ImageBlockType> = { src: result.url };\n if (result.alt) updates.alt = result.alt;\n emit(\"update\", updates);\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\n\n// A merge-tag src is a deliberate dynamic value — never clobber it via drop.\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !hasMergeTagSrc.value,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) {\n const updates: Partial<ImageBlockType> = { src: result.url };\n if (result.alt) updates.alt = result.alt;\n emit(\"update\", updates);\n }\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n\nconst containerStyle = computed(() => ({\n textAlign: props.block.align,\n}));\n\nconst imageStyle = computed(() => {\n const align = props.block.align;\n return {\n maxWidth: \"100%\",\n width: props.block.width === \"full\" ? \"100%\" : `${props.block.width}px`,\n display: \"block\",\n marginLeft: align === \"center\" || align === \"right\" ? \"auto\" : undefined,\n marginRight: align === \"center\" ? \"auto\" : undefined,\n };\n});\n\nconst hasMergeTagSrc = computed(() =>\n containsMergeTag(props.block.src, syntax),\n);\n\n// Display-only preview URLs (#415). The canonical values stay in block.src /\n// block.placeholderUrl (and thus in the MJML export) — the resolver only\n// changes what the canvas <img> shows. A merge-tag src is never displayable,\n// so it's opted out of resolution; its placeholder preview resolves instead.\nconst displaySrc = useResolvedImageSrc(() =>\n hasMergeTagSrc.value ? undefined : props.block.src,\n);\nconst displayPlaceholderUrl = useResolvedImageSrc(() =>\n hasMergeTagSrc.value ? props.block.placeholderUrl : undefined,\n);\n</script>\n\n<template>\n <div\n ref=\"dropZoneRef\"\n data-testid=\"image-drop-zone\"\n class=\"tpl:relative tpl:w-full\"\n :style=\"containerStyle\"\n >\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 85%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"20\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"20\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <!-- Placeholder with preview image provided -->\n <template v-if=\"block.src && hasMergeTagSrc && block.placeholderUrl\">\n <a\n v-if=\"block.linkUrl\"\n :href=\"block.linkUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click.prevent\n >\n <img\n class=\"tpl:border-0\"\n loading=\"lazy\"\n :src=\"displayPlaceholderUrl\"\n :alt=\"block.alt || t.image.altTextPlaceholder\"\n :style=\"imageStyle\"\n />\n </a>\n <img\n v-else\n class=\"tpl:border-0\"\n :src=\"displayPlaceholderUrl\"\n :alt=\"block.alt\"\n :style=\"imageStyle\"\n />\n </template>\n <!-- Placeholder visual (no preview image) -->\n <div\n v-else-if=\"block.src && hasMergeTagSrc\"\n class=\"tpl:!flex tpl:min-h-[120px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-center tpl:bg-[var(--tpl-bg-elevated)]\"\n style=\"\n border-color: color-mix(in srgb, var(--tpl-primary) 40%, transparent);\n \"\n :style=\"imageStyle\"\n >\n <Image\n :size=\"32\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.5\"\n />\n <span\n class=\"tpl:max-w-full tpl:truncate tpl:px-3 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.7\"\n >\n <MergeTagPreviewText :text=\"block.src\" />\n </span>\n </div>\n <!-- Normal image rendering -->\n <template v-else-if=\"block.src\">\n <a\n v-if=\"block.linkUrl\"\n :href=\"block.linkUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click.prevent\n >\n <img\n class=\"tpl:border-0\"\n loading=\"lazy\"\n :src=\"displaySrc\"\n :alt=\"block.alt || t.image.altTextPlaceholder\"\n :style=\"imageStyle\"\n />\n </a>\n <img\n v-else\n class=\"tpl:border-0\"\n :src=\"displaySrc\"\n :alt=\"block.alt\"\n :style=\"imageStyle\"\n />\n </template>\n <!-- Empty state -->\n <div\n v-else\n class=\"tpl:flex tpl:min-h-[100px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-sm tpl:border-[var(--tpl-border-light)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <button\n v-if=\"canBrowseMedia\"\n :aria-label=\"t.image.browseMedia\"\n class=\"tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-150 tpl:cursor-pointer tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-primary)] tpl:bg-[var(--tpl-bg)]\"\n @click.stop=\"browseMedia\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n <span v-else>{{ t.image.clickToAdd }}</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n MenuBlock as MenuBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { Navigation } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\n\nconst props = defineProps<{\n block: MenuBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst containerStyle = computed(() => ({\n display: \"flex\",\n flexWrap: \"wrap\" as const,\n gap: `0 ${props.block.spacing}px`,\n justifyContent:\n props.block.textAlign === \"left\"\n ? \"flex-start\"\n : props.block.textAlign === \"right\"\n ? \"flex-end\"\n : \"center\",\n fontSize: `${props.block.fontSize}px`,\n fontFamily: props.block.fontFamily || \"inherit\",\n alignItems: \"center\",\n}));\n\nconst hasItems = computed(() => props.block.items.length > 0);\n\n// Unset (all three undefined) = inherit the canvas text color (the document\n// `textColor`); the inline style omits an undefined color so it cascades.\nfunction getLinkColor(itemColor?: string): string | undefined {\n return itemColor || props.block.linkColor || props.block.color;\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div v-if=\"hasItems\" :style=\"containerStyle\">\n <template v-for=\"(item, index) in block.items\" :key=\"item.id\">\n <a\n :href=\"item.url || '#'\"\n class=\"tpl:cursor-default tpl:no-underline\"\n :style=\"{\n color: getLinkColor(item.color),\n fontWeight: item.bold ? 'bold' : 'normal',\n textDecoration: item.underline ? 'underline' : 'none',\n }\"\n @click.prevent\n >\n <MergeTagPreviewText :text=\"item.text || '...'\" />\n </a>\n <span\n v-if=\"index < block.items.length - 1\"\n :style=\"{\n color: block.separatorColor,\n padding: `0 ${block.spacing}px`,\n }\"\n >\n {{ block.separator }}\n </span>\n </template>\n </div>\n <div\n v-else\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:text-sm tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Navigation :size=\"16\" />\n <span>{{ t.menu.addLinks }}</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n MenuBlock as MenuBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { Navigation } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\n\nconst props = defineProps<{\n block: MenuBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst containerStyle = computed(() => ({\n display: \"flex\",\n flexWrap: \"wrap\" as const,\n gap: `0 ${props.block.spacing}px`,\n justifyContent:\n props.block.textAlign === \"left\"\n ? \"flex-start\"\n : props.block.textAlign === \"right\"\n ? \"flex-end\"\n : \"center\",\n fontSize: `${props.block.fontSize}px`,\n fontFamily: props.block.fontFamily || \"inherit\",\n alignItems: \"center\",\n}));\n\nconst hasItems = computed(() => props.block.items.length > 0);\n\n// Unset (all three undefined) = inherit the canvas text color (the document\n// `textColor`); the inline style omits an undefined color so it cascades.\nfunction getLinkColor(itemColor?: string): string | undefined {\n return itemColor || props.block.linkColor || props.block.color;\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div v-if=\"hasItems\" :style=\"containerStyle\">\n <template v-for=\"(item, index) in block.items\" :key=\"item.id\">\n <a\n :href=\"item.url || '#'\"\n class=\"tpl:cursor-default tpl:no-underline\"\n :style=\"{\n color: getLinkColor(item.color),\n fontWeight: item.bold ? 'bold' : 'normal',\n textDecoration: item.underline ? 'underline' : 'none',\n }\"\n @click.prevent\n >\n <MergeTagPreviewText :text=\"item.text || '...'\" />\n </a>\n <span\n v-if=\"index < block.items.length - 1\"\n :style=\"{\n color: block.separatorColor,\n padding: `0 ${block.spacing}px`,\n }\"\n >\n {{ block.separator }}\n </span>\n </template>\n </div>\n <div\n v-else\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:text-sm tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Navigation :size=\"16\" />\n <span>{{ t.menu.addLinks }}</span>\n </div>\n </div>\n</template>\n","/**\n * Strip XSS vectors from a rich-text HTML fragment before `v-html`-ing\n * it into the canvas. TipTap-authored content is already safe; the\n * attack surface is template JSON loaded by the consumer, where a\n * malicious or compromised source can inject `<script>`, `<img\n * onerror>`, or `javascript:` anchors.\n *\n * Strips:\n * - dangerous elements (script, style, iframe, object, embed, link, meta, base)\n * - every event handler attribute (`on*`)\n * - `href` / `xlink:href` / `formaction` / `action` values with disallowed schemes\n * - `src` values with disallowed schemes (except `data:image/...`)\n * - non-image `data:` URLs and any `javascript:` / `vbscript:` / `file:` URL\n *\n * Allowed URL schemes mirror the link-dialog allowlist. Uses the platform\n * `DOMParser`; falls back to returning the input unchanged in environments\n * without one (server-side renderers should never reach this code path,\n * but we don't want to crash if they do).\n */\n\nconst SAFE_URL_SCHEMES = new Set([\n \"http\",\n \"https\",\n \"mailto\",\n \"tel\",\n \"ftp\",\n \"ftps\",\n \"sms\",\n \"xmpp\",\n \"cid\",\n]);\n\nconst FORBIDDEN_ELEMENTS = new Set([\n \"SCRIPT\",\n \"STYLE\",\n \"IFRAME\",\n \"OBJECT\",\n \"EMBED\",\n \"LINK\",\n \"META\",\n \"BASE\",\n \"FORM\",\n]);\n\nconst URL_ATTRIBUTES = new Set([\n \"href\",\n \"xlink:href\",\n \"formaction\",\n \"action\",\n \"ping\",\n \"background\",\n \"poster\",\n]);\n\nfunction isSafeUrl(value: string, allowDataImage: boolean): boolean {\n // Normalize the value the same way the WHATWG URL parser does before\n // testing the scheme, so we inspect the string the browser will actually\n // resolve. The parser removes ASCII tab/LF/CR from *anywhere* in a URL and\n // strips leading C0-control + space characters. Without this, payloads like\n // \"java\\tscript:alert(1)\" or \"\\x01javascript:alert(1)\" match no scheme here\n // (and fall through to the safe branch) yet re-form a live `javascript:`\n // URL once rendered into the anchor.\n const normalized = value\n .replace(/[\\t\\n\\r]/g, \"\")\n .replace(/^[\\u0000-\\u0020]+/, \"\")\n .trimEnd();\n if (!normalized) return true;\n if (normalized.startsWith(\"#\")) return true;\n const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(normalized);\n if (!schemeMatch) return true;\n const scheme = schemeMatch[1].toLowerCase();\n if (SAFE_URL_SCHEMES.has(scheme)) return true;\n if (allowDataImage && scheme === \"data\") {\n return /^data:image\\/(png|jpe?g|gif|webp|svg\\+xml);/i.test(normalized);\n }\n return false;\n}\n\nfunction scrubElement(el: Element): void {\n if (FORBIDDEN_ELEMENTS.has(el.tagName)) {\n el.remove();\n return;\n }\n // Snapshot attribute names — removing during iteration breaks the live list.\n const attrNames: string[] = [];\n for (let i = 0; i < el.attributes.length; i++) {\n attrNames.push(el.attributes[i].name);\n }\n for (const name of attrNames) {\n const lower = name.toLowerCase();\n if (lower.startsWith(\"on\")) {\n el.removeAttribute(name);\n continue;\n }\n if (URL_ATTRIBUTES.has(lower)) {\n const value = el.getAttribute(name) ?? \"\";\n if (!isSafeUrl(value, false)) el.removeAttribute(name);\n continue;\n }\n if (lower === \"src\") {\n const value = el.getAttribute(name) ?? \"\";\n if (!isSafeUrl(value, true)) el.removeAttribute(name);\n continue;\n }\n if (lower === \"srcdoc\") {\n el.removeAttribute(name);\n }\n }\n const children = Array.from(el.children);\n for (const child of children) scrubElement(child);\n}\n\nexport function sanitizeRichTextHtml(html: string): string {\n if (typeof DOMParser === \"undefined\") return html;\n const doc = new DOMParser().parseFromString(\n `<!doctype html><body>${html}</body>`,\n \"text/html\",\n );\n const body = doc.body;\n const children = Array.from(body.children);\n for (const child of children) scrubElement(child);\n return body.innerHTML;\n}\n","import {\n resolveHtmlLogicMergeTagLabels,\n resolveHtmlMergeTagLabels,\n substituteHtmlMergeTagSamples,\n} from \"@templatical/types\";\nimport { useElementBounding } from \"@vueuse/core\";\nimport { computed, inject, ref, type ComputedRef, type Ref } from \"vue\";\nimport { MERGE_TAGS_KEY, USE_MERGE_TAG_SAMPLES_KEY } from \"../keys\";\nimport { useMergeTag } from \"./useMergeTag\";\nimport { usePopoverPosition } from \"./usePopoverPosition\";\nimport { sanitizeRichTextHtml } from \"../utils/sanitizeRichTextHtml\";\n\nexport interface UseEditableTextBlockReturn {\n isEditing: Ref<boolean>;\n blockRef: Ref<HTMLElement | null>;\n toolbarPosition: ComputedRef<{ top: number; left: number }>;\n resolvedContent: ComputedRef<string>;\n handleDoubleClick: () => void;\n handleEditorDone: () => void;\n}\n\nexport function useEditableTextBlock(\n blockContent: () => string,\n): UseEditableTextBlockReturn {\n const mergeTags = inject(MERGE_TAGS_KEY, []);\n // Defaults to `false`, so any surface that doesn't opt in — the editing\n // canvas above all — keeps showing labels.\n const useSamples = inject(USE_MERGE_TAG_SAMPLES_KEY, null);\n const { syntax } = useMergeTag();\n\n /**\n * Data merge tags resolve to either their sample or their label. Sample mode\n * drops the `<span>` wrapper too, so the value renders as ordinary text with\n * no highlight — which is the whole point of the mode.\n *\n * Logic tags are resolved to keywords in both modes: substitution can't\n * evaluate a branch, so they stay visible as badges either way.\n */\n const withResolvedDataTags = computed(() =>\n useSamples?.value\n ? substituteHtmlMergeTagSamples(blockContent(), mergeTags)\n : resolveHtmlMergeTagLabels(blockContent(), mergeTags),\n );\n\n // Sanitize before binding to `v-html`. TipTap-authored content is\n // already safe, but template JSON loaded from a consumer can carry\n // arbitrary HTML — including `<script>`, `<img onerror>`, or\n // `javascript:` anchors that would otherwise execute when the block\n // renders in the canvas.\n const resolvedContent = computed(() =>\n sanitizeRichTextHtml(\n resolveHtmlLogicMergeTagLabels(withResolvedDataTags.value, syntax),\n ),\n );\n\n const isEditing = ref(false);\n const blockRef = ref<HTMLElement | null>(null);\n const { top: boundingTop, left: boundingLeft } = useElementBounding(blockRef);\n // The floating toolbar teleports into the popover root and positions itself\n // `absolute`; convert the block's viewport rect to popover-root-local coords\n // so a transformed ancestor of the editor doesn't offset it. Recomputes with\n // `boundingTop/Left` (which track scroll/resize), re-reading the root each\n // time so the toolbar stays glued to the block. See usePopoverPosition.\n const { toLocal } = usePopoverPosition();\n const toolbarPosition = computed(() =>\n toLocal({ top: boundingTop.value - 8, left: boundingLeft.value }),\n );\n\n function handleDoubleClick(): void {\n isEditing.value = true;\n }\n\n function handleEditorDone(): void {\n isEditing.value = false;\n }\n\n return {\n isEditing,\n blockRef,\n toolbarPosition,\n resolvedContent,\n handleDoubleClick,\n handleEditorDone,\n };\n}\n","<script setup lang=\"ts\">\nimport { useEditableTextBlock } from \"../../composables/useEditableTextBlock\";\nimport type {\n ParagraphBlock as ParagraphBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { defineAsyncComponent } from \"vue\";\n\nconst props = defineProps<{\n block: ParagraphBlockType;\n viewport: ViewportSize;\n}>();\n\nconst ParagraphEditor = defineAsyncComponent(\n () => import(\"./ParagraphEditor.vue\"),\n);\n\nconst {\n isEditing,\n blockRef,\n toolbarPosition,\n resolvedContent,\n handleDoubleClick,\n handleEditorDone,\n} = useEditableTextBlock(() => props.block.content);\n</script>\n\n<template>\n <div\n ref=\"blockRef\"\n class=\"tpl:min-h-[1em] tpl:w-full\"\n @dblclick=\"handleDoubleClick\"\n >\n <ParagraphEditor\n v-if=\"isEditing\"\n :block=\"block\"\n :toolbar-position=\"toolbarPosition\"\n @done=\"handleEditorDone\"\n />\n <!-- eslint-disable vue/no-v-html -->\n <div\n v-else\n class=\"tpl-text-content tpl:outline-none [&_a]:tpl:underline [&_li]:tpl:my-1 [&_ol]:tpl:my-2 [&_ol]:tpl:pl-6 [&_p]:tpl:m-0 [&_p]:tpl:mb-2 [&_p:last-child]:tpl:mb-0 [&_s]:tpl:line-through [&_sub]:tpl:align-sub [&_sub]:tpl:text-[0.75em] [&_sup]:tpl:align-super [&_sup]:tpl:text-[0.75em] [&_ul]:tpl:my-2 [&_ul]:tpl:pl-6\"\n v-html=\"resolvedContent\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useEditableTextBlock } from \"../../composables/useEditableTextBlock\";\nimport type {\n ParagraphBlock as ParagraphBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { defineAsyncComponent } from \"vue\";\n\nconst props = defineProps<{\n block: ParagraphBlockType;\n viewport: ViewportSize;\n}>();\n\nconst ParagraphEditor = defineAsyncComponent(\n () => import(\"./ParagraphEditor.vue\"),\n);\n\nconst {\n isEditing,\n blockRef,\n toolbarPosition,\n resolvedContent,\n handleDoubleClick,\n handleEditorDone,\n} = useEditableTextBlock(() => props.block.content);\n</script>\n\n<template>\n <div\n ref=\"blockRef\"\n class=\"tpl:min-h-[1em] tpl:w-full\"\n @dblclick=\"handleDoubleClick\"\n >\n <ParagraphEditor\n v-if=\"isEditing\"\n :block=\"block\"\n :toolbar-position=\"toolbarPosition\"\n @done=\"handleEditorDone\"\n />\n <!-- eslint-disable vue/no-v-html -->\n <div\n v-else\n class=\"tpl-text-content tpl:outline-none [&_a]:tpl:underline [&_li]:tpl:my-1 [&_ol]:tpl:my-2 [&_ol]:tpl:pl-6 [&_p]:tpl:m-0 [&_p]:tpl:mb-2 [&_p:last-child]:tpl:mb-0 [&_s]:tpl:line-through [&_sub]:tpl:align-sub [&_sub]:tpl:text-[0.75em] [&_sup]:tpl:align-super [&_sup]:tpl:text-[0.75em] [&_ul]:tpl:my-2 [&_ul]:tpl:pl-6\"\n v-html=\"resolvedContent\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n</template>\n","import type { Block, TemplateSettings } from \"@templatical/types\";\nimport type { Component } from \"vue\";\nimport type { UseBlockRegistryReturn } from \"../composables/useBlockRegistry\";\n\n/**\n * Document-level style the blocks inherit — the canvas equivalent of the\n * exported `<mj-attributes>` defaults and global `a { … }` rule, so what the\n * editor shows matches what gets sent.\n *\n * Belongs on whatever element wraps a set of rendered blocks. Every surface\n * that renders blocks must apply it, not just the main canvas: without it the\n * link rules in `styles/index.css` fall through to their unset defaults\n * (`text-decoration: none`, `color: inherit`) and the font drops to the editor\n * UI's, so the same paragraph renders differently in a preview than on the\n * canvas. Set only what's configured — an unset value must stay unset so it\n * matches the renderer omitting the attribute.\n */\nexport function getDocumentStyle(\n settings: TemplateSettings,\n): Record<string, string> {\n const style: Record<string, string> = {\n fontFamily: settings.fontFamily,\n };\n // Titles carry their own inline color and override this, same as in export.\n if (settings.textColor) {\n style.color = settings.textColor;\n }\n // Consumed by the `.tpl-text-content a` rules; unset falls back to `inherit`\n // (the surrounding text color), exactly how an unstyled link exports.\n if (settings.linkColor) {\n style[\"--tpl-doc-link-color\"] = settings.linkColor;\n }\n // Only set when true; the CSS fallback (`none`) covers false and the legacy\n // undefined case, matching the export's default.\n if (settings.linkUnderline) {\n style[\"--tpl-doc-link-underline\"] = \"underline\";\n }\n return style;\n}\n\n/**\n * Resolves a block to its Vue component, checking the registry first\n * and falling back to the provided component map.\n */\nexport function resolveBlockComponent(\n block: Block,\n registry: UseBlockRegistryReturn | null | undefined,\n componentMap: Record<string, Component>,\n): Component | null {\n if (registry) {\n const component = registry.getComponent(block);\n if (component) {\n return component;\n }\n }\n\n return componentMap[block.type] ?? null;\n}\n\n/**\n * Computes inline styles for a block wrapper from its styles config.\n */\nexport function getBlockWrapperStyle(block: Block): Record<string, string> {\n const { padding, backgroundColor } = block.styles;\n const style: Record<string, string> = {\n padding: `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`,\n backgroundColor: backgroundColor || \"transparent\",\n };\n // borderRadius is section-specific (it lives on SectionBlock, not `styles`).\n // Mirror it into the editor box style so the canvas/preview match the\n // exported MJML, which renders it as `border-radius` on the `mj-section`.\n if (\n block.type === \"section\" &&\n block.borderRadius &&\n block.borderRadius > 0\n ) {\n style.borderRadius = `${block.borderRadius}px`;\n }\n return style;\n}\n\n/**\n * Outer-frame style for a section's `wrapper` — the canvas equivalent of the\n * exported `mj-wrapper` (a band with its own background + padding + radius that\n * frames the section's box). Returns `null` for non-sections and for sections\n * without a wrapper, so callers can skip the extra element entirely.\n */\nexport function getSectionWrapperStyle(\n block: Block,\n): Record<string, string> | null {\n if (block.type !== \"section\" || !block.wrapper) return null;\n const w = block.wrapper;\n const style: Record<string, string> = {};\n if (w.backgroundColor) style.backgroundColor = w.backgroundColor;\n if (w.padding) {\n style.padding = `${w.padding.top}px ${w.padding.right}px ${w.padding.bottom}px ${w.padding.left}px`;\n }\n if (w.borderRadius && w.borderRadius > 0) {\n style.borderRadius = `${w.borderRadius}px`;\n }\n return style;\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type { Block, ViewportSize } from \"@templatical/types\";\nimport {\n Bookmark,\n Copy,\n EyeOff,\n Filter,\n GripVertical,\n MessageCircle,\n Trash2,\n} from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, inject, nextTick, ref } from \"vue\";\nimport {\n getBlockWrapperStyle,\n getSectionWrapperStyle,\n} from \"../../utils/blockComponentResolver\";\n\nconst BlockIssueBadge = defineAsyncComponent(\n () => import(\"../canvas/BlockIssueBadge.vue\"),\n);\nimport {\n BLOCK_ACTIONS_KEY,\n CONDITION_PREVIEW_KEY,\n CAPABILITIES_KEY,\n KEYBOARD_REORDER_KEY,\n} from \"../../keys\";\n\nconst props = defineProps<{\n block: Block;\n isSelected: boolean;\n viewport?: ViewportSize;\n previewMode?: boolean;\n /**\n * This block sits inside a section column rather than at the top level.\n * Section children can't be saved as saved blocks: a saved block's content is\n * a top-level `Block[]` and insertion always targets the top level, so a\n * nested block could be saved but never re-inserted where it came from.\n * Save the whole section instead — its `children` round-trip with it.\n */\n nested?: boolean;\n /**\n * Chosen during a saved-blocks pick session. Independent of `isSelected` —\n * picking never touches `EditorState.selectedBlockId`.\n */\n picked?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select\"): void;\n}>();\n\nconst { t, format } = useI18n();\n\nconst keyboardReorder = inject(KEYBOARD_REORDER_KEY, null);\nconst dragButtonRef = ref<HTMLButtonElement | null>(null);\n\nconst isLifted = computed(\n () => keyboardReorder?.liftedBlockId.value === props.block.id,\n);\n\nconst dragAriaLabel = computed(() =>\n isLifted.value\n ? format(t.blockActions.dragLifted, {\n block: props.block.type,\n })\n : t.blockActions.drag,\n);\n\nasync function refocusDragButton(): Promise<void> {\n await nextTick();\n dragButtonRef.value?.focus();\n}\n\nfunction handleDragKeydown(event: KeyboardEvent): void {\n if (!keyboardReorder) return;\n\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault();\n event.stopPropagation();\n if (isLifted.value) {\n keyboardReorder.drop(props.block.id);\n } else {\n keyboardReorder.lift(props.block.id);\n }\n return;\n }\n\n if (!isLifted.value) return;\n\n if (event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n keyboardReorder.cancel();\n refocusDragButton();\n return;\n }\n\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n event.stopPropagation();\n keyboardReorder.moveUp(props.block.id);\n refocusDragButton();\n return;\n }\n\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n event.stopPropagation();\n keyboardReorder.moveDown(props.block.id);\n refocusDragButton();\n }\n}\n\nconst isHiddenOnViewport = computed(() => {\n if (!props.viewport || !props.block.visibility) {\n return false;\n }\n return props.block.visibility[props.viewport] === false;\n});\n\n// In edit mode a viewport-hidden block stays rendered (dimmed + badge) so it\n// remains selectable. In preview mode it must actually disappear to match the\n// exported MJML, which drops it via `@media` + `tpl-hide-*` css-classes.\nconst isHiddenInPreview = computed(\n () => props.previewMode === true && isHiddenOnViewport.value,\n);\n\nconst hiddenLabel = computed(() => {\n if (!props.viewport) {\n return \"\";\n }\n const labels: Record<string, string> = {\n desktop: t.viewport.desktop,\n mobile: t.viewport.mobile,\n };\n return labels[props.viewport] ?? props.viewport;\n});\n\nconst hasDisplayCondition = computed(() => !!props.block.displayCondition);\n\nconst blockActions = inject(BLOCK_ACTIONS_KEY, null);\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\nconst isPicking = computed(() => caps.savedBlocks?.isPicking.value === true);\n\n// `canCreate` too: a provider that withheld `create` gets a read-only library,\n// so offering the bookmark would start a pick session that could never save.\nconst canSaveAsBlock = computed(\n () =>\n !props.nested &&\n caps.savedBlocks?.isAvailable.value === true &&\n caps.savedBlocks?.canCreate.value === true,\n);\n\n// The action bar is chrome for editing a single block; during a pick session the\n// canvas is in a different mode and N bars would be noise.\nconst showActions = computed(() => props.isSelected && !isPicking.value);\n\nconst blockCommentCount = computed(\n () => caps.comments?.getBlockCount(props.block.id) ?? 0,\n);\n\n// padding + backgroundColor live on `.tpl-block-content` (not `.tpl-block`).\n// Filter-based dark-mode preview targets `.tpl-block-content` so the bg\n// inverts together with its content.\nconst contentStyle = computed(() => {\n const full = getBlockWrapperStyle(props.block);\n const style: Record<string, string> = {\n padding: full.padding,\n backgroundColor: full.backgroundColor,\n };\n // Section corner radius (when set) rounds the same element that carries the\n // background, matching the exported MJML.\n if (full.borderRadius) {\n style.borderRadius = full.borderRadius;\n }\n return style;\n});\n\n// Section outer frame — the canvas equivalent of the exported `mj-wrapper`:\n// a band with its own background + padding + radius framing the section's box.\n// `null` for non-wrapped blocks keeps the DOM unchanged (no extra element).\nconst sectionWrapperStyle = computed(() => getSectionWrapperStyle(props.block));\n\nfunction handleClick(event: MouseEvent): void {\n if (props.previewMode) {\n return;\n }\n // Rich-text blocks inject their links as raw <a> via v-html, so — unlike the\n // other link-bearing blocks (button, menu, image…) which carry a\n // template-level `@click.prevent` — they can't attach a directive to the\n // anchor. Suppress navigation here so a click selects the block for editing\n // instead of opening the href. Preview mode returns above, leaving real\n // links clickable. `closest` also walks out of nested inline markup (e.g. a\n // <strong> inside the link).\n if ((event.target as HTMLElement | null)?.closest(\"a\")) {\n event.preventDefault();\n }\n event.stopPropagation();\n emit(\"select\");\n}\n\nfunction handleDelete(): void {\n blockActions?.deleteBlock(props.block.id);\n}\n\nfunction handleDuplicate(): void {\n blockActions?.duplicateBlock(props.block);\n}\n\nfunction handleSaveAsBlock(): void {\n // Seeds a canvas pick session with this block rather than opening the dialog\n // straight away — the user picks any others by clicking them.\n caps.savedBlocks?.startPicking(props.block.id);\n}\n\nfunction handleConditionToggle(): void {\n conditionPreview?.toggleBlock(props.block.id);\n}\n</script>\n\n<template>\n <div\n v-if=\"!isHiddenInPreview\"\n class=\"tpl-block tpl:group tpl:relative tpl:cursor-pointer tpl:rounded-sm tpl:transition-shadow tpl:duration-150\"\n :class=\"{\n 'tpl-block--selected': isSelected,\n 'tpl-block--picked': picked,\n 'tpl-block--idle': !isSelected && !picked,\n 'tpl-block--lifted': isLifted,\n }\"\n :data-block-id=\"block.id\"\n :data-block-type=\"block.type\"\n :data-tpl-picked=\"picked || undefined\"\n @click=\"handleClick\"\n >\n <BlockIssueBadge :block-id=\"block.id\" />\n <!-- Floating action bar — positioned to the right of selected block -->\n <div\n v-if=\"showActions\"\n role=\"toolbar\"\n :aria-label=\"t.blockActions.drag\"\n class=\"tpl-block-actions tpl-fade-in tpl:absolute tpl:-right-2 tpl:top-1/2 tpl:z-10 tpl:flex tpl:-translate-y-1/2 tpl:translate-x-full tpl:gap-0.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-chrome-bg-elevated)] tpl:shadow-[var(--tpl-shadow-md)] tpl:border tpl:border-[var(--tpl-border)]\"\n >\n <button\n ref=\"dragButtonRef\"\n class=\"tpl-block-btn tpl-block-action-btn tpl:flex tpl:size-7 tpl:cursor-grab tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150 tpl:active:cursor-grabbing\"\n :aria-label=\"dragAriaLabel\"\n :aria-pressed=\"isLifted\"\n aria-keyshortcuts=\"Space Enter ArrowUp ArrowDown Escape\"\n :title=\"t.blockActions.drag\"\n @keydown=\"handleDragKeydown\"\n >\n <GripVertical :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n <button\n class=\"tpl-block-action-btn tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150\"\n :aria-label=\"t.blockActions.duplicate\"\n :title=\"t.blockActions.duplicate\"\n @click.stop=\"handleDuplicate\"\n >\n <Copy :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n <button\n v-if=\"canSaveAsBlock\"\n class=\"tpl-block-action-btn tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150\"\n :aria-label=\"t.blockActions.saveAsBlock\"\n :title=\"t.blockActions.saveAsBlock\"\n @click.stop=\"handleSaveAsBlock\"\n >\n <Bookmark :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n <button\n class=\"tpl-block-action-btn tpl-block-delete-btn tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150\"\n :aria-label=\"t.blockActions.delete\"\n :title=\"t.blockActions.delete\"\n @click.stop=\"handleDelete\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n </div>\n <div\n v-if=\"isHiddenOnViewport\"\n class=\"tpl-block-hidden-overlay tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-[5] tpl:flex tpl:items-center tpl:justify-center tpl:rounded-sm\"\n >\n <span\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-2 tpl:py-1 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-chrome-bg-elevated)] tpl:text-[var(--tpl-chrome-text-muted)] tpl:shadow-[var(--tpl-shadow-sm)]\"\n >\n <EyeOff :size=\"12\" :stroke-width=\"1.5\" />\n {{\n format(t.blockActions.hiddenOnViewport, {\n viewport: hiddenLabel,\n })\n }}\n </span>\n </div>\n <div\n v-if=\"hasDisplayCondition && !isHiddenOnViewport\"\n class=\"tpl:absolute tpl:-left-1 tpl:top-1/2 tpl:z-[5] tpl:-translate-x-full tpl:-translate-y-1/2\"\n >\n <button\n data-testid=\"condition-toggle\"\n class=\"tpl-condition-toggle tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1 tpl:transition-colors tpl:duration-150 tpl:bg-[var(--tpl-chrome-bg-elevated)] tpl:text-[var(--tpl-primary)] tpl:border tpl:border-[var(--tpl-border)]\"\n :aria-label=\"t.blockActions.conditionToggle\"\n :title=\"block.displayCondition?.label\"\n @click.stop=\"handleConditionToggle\"\n >\n <Filter :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n <!-- Comment count indicator -->\n <div\n v-if=\"blockCommentCount > 0 && !isHiddenOnViewport\"\n class=\"tpl:absolute tpl:-right-1 tpl:-top-1 tpl:z-[5] tpl:translate-x-full\"\n >\n <button\n class=\"tpl-comment-indicator tpl:flex tpl:min-h-6 tpl:min-w-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-0.5 tpl:rounded-full tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-semibold tpl:transition-colors tpl:duration-150 tpl:bg-[var(--tpl-chrome-primary-light)] tpl:text-[var(--tpl-primary)]\"\n :aria-label=\"\n format(t.blockActions.comments, { count: String(blockCommentCount) })\n \"\n @click.stop=\"caps.comments?.openForBlock(block.id)\"\n >\n <MessageCircle :size=\"12\" :stroke-width=\"2.5\" />\n {{ blockCommentCount }}\n </button>\n </div>\n <!-- Email content wrapper — target for the canvas dark-mode preview\n filter. Block chrome (action bar, indicators, overlays) is a\n sibling of this wrapper, so the filter never reaches them — no\n counter-filter needed and no toggle flicker. -->\n <div\n v-if=\"sectionWrapperStyle\"\n class=\"tpl-section-wrapper\"\n :style=\"sectionWrapperStyle\"\n >\n <div class=\"tpl-block-content\" :style=\"contentStyle\">\n <slot />\n </div>\n </div>\n <div v-else class=\"tpl-block-content\" :style=\"contentStyle\">\n <slot />\n </div>\n </div>\n</template>\n\n<style scoped>\n/* Idle — subtle dashed outline so block boundaries are always visible */\n.tpl-block--idle {\n outline: 1.5px dashed color-mix(in srgb, var(--tpl-primary) 30%, transparent);\n outline-offset: -1px;\n transition:\n outline 150ms cubic-bezier(0.16, 1, 0.3, 1),\n box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* Hover — solid outline, more prominent */\n.tpl-block--idle:hover {\n outline: 1.5px solid color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n outline-offset: -1px;\n box-shadow: 0 0 8px color-mix(in srgb, var(--tpl-primary) 8%, transparent);\n}\n\n/* Picked (saved-blocks pick session) — a solid outline plus a crisp outer ring.\n Deliberately NOT a background fill: the block's own background is painted on\n the inner `.tpl-block-content` (`backgroundColor || \"transparent\"`), so a tint\n on `.tpl-block` is covered by any block that sets one and shows through only\n on transparent ones — the picked state would appear on some blocks and not\n others in the same template. `outline` and a non-inset `box-shadow` draw on\n this element's border box, outside the content's paint area, so they read\n identically regardless of what the block itself is filled with.\n\n The differentiator from idle is line style and weight (dashed 1.5px @30% →\n solid 2px @100%) plus the ring — not hue alone — so the state survives without\n colour perception, and the pick bar's live count is the numeric confirmation.\n Distinct from `--selected`'s soft blurred glow; the two never co-occur anyway,\n since Canvas suppresses selection styling while a session runs. */\n.tpl-block--picked {\n outline: 2px solid var(--tpl-primary);\n outline-offset: -1px;\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--tpl-primary) 18%, transparent);\n transition:\n outline 150ms cubic-bezier(0.16, 1, 0.3, 1),\n box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* Selection — solid outline + soft outer glow */\n.tpl-block--selected {\n outline: 2px solid color-mix(in srgb, var(--tpl-primary) 60%, transparent);\n outline-offset: -1px;\n box-shadow: 0 0 12px color-mix(in srgb, var(--tpl-primary) 15%, transparent);\n transition:\n outline 150ms cubic-bezier(0.16, 1, 0.3, 1),\n box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* Action buttons — muted text, warm hover, press feedback */\n.tpl-block-action-btn {\n color: var(--tpl-chrome-text-muted);\n background-color: transparent;\n}\n\n.tpl-block-action-btn:hover {\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-chrome-text);\n}\n\n.tpl-block-action-btn:active {\n transform: scale(0.9);\n}\n\n.tpl-block-delete-btn:hover {\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n}\n\n/* Condition toggle — interactive hover + press */\n.tpl-condition-toggle:hover {\n background-color: var(--tpl-bg-hover) !important;\n}\n\n.tpl-condition-toggle:active {\n transform: scale(0.9);\n}\n\n/* Hidden block overlay — dims the block when hidden on current viewport */\n.tpl-block-hidden-overlay {\n background-color: color-mix(in srgb, var(--tpl-bg) 60%, transparent);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type { Block, ViewportSize } from \"@templatical/types\";\nimport {\n Bookmark,\n Copy,\n EyeOff,\n Filter,\n GripVertical,\n MessageCircle,\n Trash2,\n} from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, inject, nextTick, ref } from \"vue\";\nimport {\n getBlockWrapperStyle,\n getSectionWrapperStyle,\n} from \"../../utils/blockComponentResolver\";\n\nconst BlockIssueBadge = defineAsyncComponent(\n () => import(\"../canvas/BlockIssueBadge.vue\"),\n);\nimport {\n BLOCK_ACTIONS_KEY,\n CONDITION_PREVIEW_KEY,\n CAPABILITIES_KEY,\n KEYBOARD_REORDER_KEY,\n} from \"../../keys\";\n\nconst props = defineProps<{\n block: Block;\n isSelected: boolean;\n viewport?: ViewportSize;\n previewMode?: boolean;\n /**\n * This block sits inside a section column rather than at the top level.\n * Section children can't be saved as saved blocks: a saved block's content is\n * a top-level `Block[]` and insertion always targets the top level, so a\n * nested block could be saved but never re-inserted where it came from.\n * Save the whole section instead — its `children` round-trip with it.\n */\n nested?: boolean;\n /**\n * Chosen during a saved-blocks pick session. Independent of `isSelected` —\n * picking never touches `EditorState.selectedBlockId`.\n */\n picked?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select\"): void;\n}>();\n\nconst { t, format } = useI18n();\n\nconst keyboardReorder = inject(KEYBOARD_REORDER_KEY, null);\nconst dragButtonRef = ref<HTMLButtonElement | null>(null);\n\nconst isLifted = computed(\n () => keyboardReorder?.liftedBlockId.value === props.block.id,\n);\n\nconst dragAriaLabel = computed(() =>\n isLifted.value\n ? format(t.blockActions.dragLifted, {\n block: props.block.type,\n })\n : t.blockActions.drag,\n);\n\nasync function refocusDragButton(): Promise<void> {\n await nextTick();\n dragButtonRef.value?.focus();\n}\n\nfunction handleDragKeydown(event: KeyboardEvent): void {\n if (!keyboardReorder) return;\n\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault();\n event.stopPropagation();\n if (isLifted.value) {\n keyboardReorder.drop(props.block.id);\n } else {\n keyboardReorder.lift(props.block.id);\n }\n return;\n }\n\n if (!isLifted.value) return;\n\n if (event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n keyboardReorder.cancel();\n refocusDragButton();\n return;\n }\n\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n event.stopPropagation();\n keyboardReorder.moveUp(props.block.id);\n refocusDragButton();\n return;\n }\n\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n event.stopPropagation();\n keyboardReorder.moveDown(props.block.id);\n refocusDragButton();\n }\n}\n\nconst isHiddenOnViewport = computed(() => {\n if (!props.viewport || !props.block.visibility) {\n return false;\n }\n return props.block.visibility[props.viewport] === false;\n});\n\n// In edit mode a viewport-hidden block stays rendered (dimmed + badge) so it\n// remains selectable. In preview mode it must actually disappear to match the\n// exported MJML, which drops it via `@media` + `tpl-hide-*` css-classes.\nconst isHiddenInPreview = computed(\n () => props.previewMode === true && isHiddenOnViewport.value,\n);\n\nconst hiddenLabel = computed(() => {\n if (!props.viewport) {\n return \"\";\n }\n const labels: Record<string, string> = {\n desktop: t.viewport.desktop,\n mobile: t.viewport.mobile,\n };\n return labels[props.viewport] ?? props.viewport;\n});\n\nconst hasDisplayCondition = computed(() => !!props.block.displayCondition);\n\nconst blockActions = inject(BLOCK_ACTIONS_KEY, null);\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\nconst isPicking = computed(() => caps.savedBlocks?.isPicking.value === true);\n\n// `canCreate` too: a provider that withheld `create` gets a read-only library,\n// so offering the bookmark would start a pick session that could never save.\nconst canSaveAsBlock = computed(\n () =>\n !props.nested &&\n caps.savedBlocks?.isAvailable.value === true &&\n caps.savedBlocks?.canCreate.value === true,\n);\n\n// The action bar is chrome for editing a single block; during a pick session the\n// canvas is in a different mode and N bars would be noise.\nconst showActions = computed(() => props.isSelected && !isPicking.value);\n\nconst blockCommentCount = computed(\n () => caps.comments?.getBlockCount(props.block.id) ?? 0,\n);\n\n// padding + backgroundColor live on `.tpl-block-content` (not `.tpl-block`).\n// Filter-based dark-mode preview targets `.tpl-block-content` so the bg\n// inverts together with its content.\nconst contentStyle = computed(() => {\n const full = getBlockWrapperStyle(props.block);\n const style: Record<string, string> = {\n padding: full.padding,\n backgroundColor: full.backgroundColor,\n };\n // Section corner radius (when set) rounds the same element that carries the\n // background, matching the exported MJML.\n if (full.borderRadius) {\n style.borderRadius = full.borderRadius;\n }\n return style;\n});\n\n// Section outer frame — the canvas equivalent of the exported `mj-wrapper`:\n// a band with its own background + padding + radius framing the section's box.\n// `null` for non-wrapped blocks keeps the DOM unchanged (no extra element).\nconst sectionWrapperStyle = computed(() => getSectionWrapperStyle(props.block));\n\nfunction handleClick(event: MouseEvent): void {\n if (props.previewMode) {\n return;\n }\n // Rich-text blocks inject their links as raw <a> via v-html, so — unlike the\n // other link-bearing blocks (button, menu, image…) which carry a\n // template-level `@click.prevent` — they can't attach a directive to the\n // anchor. Suppress navigation here so a click selects the block for editing\n // instead of opening the href. Preview mode returns above, leaving real\n // links clickable. `closest` also walks out of nested inline markup (e.g. a\n // <strong> inside the link).\n if ((event.target as HTMLElement | null)?.closest(\"a\")) {\n event.preventDefault();\n }\n event.stopPropagation();\n emit(\"select\");\n}\n\nfunction handleDelete(): void {\n blockActions?.deleteBlock(props.block.id);\n}\n\nfunction handleDuplicate(): void {\n blockActions?.duplicateBlock(props.block);\n}\n\nfunction handleSaveAsBlock(): void {\n // Seeds a canvas pick session with this block rather than opening the dialog\n // straight away — the user picks any others by clicking them.\n caps.savedBlocks?.startPicking(props.block.id);\n}\n\nfunction handleConditionToggle(): void {\n conditionPreview?.toggleBlock(props.block.id);\n}\n</script>\n\n<template>\n <div\n v-if=\"!isHiddenInPreview\"\n class=\"tpl-block tpl:group tpl:relative tpl:cursor-pointer tpl:rounded-sm tpl:transition-shadow tpl:duration-150\"\n :class=\"{\n 'tpl-block--selected': isSelected,\n 'tpl-block--picked': picked,\n 'tpl-block--idle': !isSelected && !picked,\n 'tpl-block--lifted': isLifted,\n }\"\n :data-block-id=\"block.id\"\n :data-block-type=\"block.type\"\n :data-tpl-picked=\"picked || undefined\"\n @click=\"handleClick\"\n >\n <BlockIssueBadge :block-id=\"block.id\" />\n <!-- Floating action bar — positioned to the right of selected block -->\n <div\n v-if=\"showActions\"\n role=\"toolbar\"\n :aria-label=\"t.blockActions.drag\"\n class=\"tpl-block-actions tpl-fade-in tpl:absolute tpl:-right-2 tpl:top-1/2 tpl:z-10 tpl:flex tpl:-translate-y-1/2 tpl:translate-x-full tpl:gap-0.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-chrome-bg-elevated)] tpl:shadow-[var(--tpl-shadow-md)] tpl:border tpl:border-[var(--tpl-border)]\"\n >\n <button\n ref=\"dragButtonRef\"\n class=\"tpl-block-btn tpl-block-action-btn tpl:flex tpl:size-7 tpl:cursor-grab tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150 tpl:active:cursor-grabbing\"\n :aria-label=\"dragAriaLabel\"\n :aria-pressed=\"isLifted\"\n aria-keyshortcuts=\"Space Enter ArrowUp ArrowDown Escape\"\n :title=\"t.blockActions.drag\"\n @keydown=\"handleDragKeydown\"\n >\n <GripVertical :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n <button\n class=\"tpl-block-action-btn tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150\"\n :aria-label=\"t.blockActions.duplicate\"\n :title=\"t.blockActions.duplicate\"\n @click.stop=\"handleDuplicate\"\n >\n <Copy :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n <button\n v-if=\"canSaveAsBlock\"\n class=\"tpl-block-action-btn tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150\"\n :aria-label=\"t.blockActions.saveAsBlock\"\n :title=\"t.blockActions.saveAsBlock\"\n @click.stop=\"handleSaveAsBlock\"\n >\n <Bookmark :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n <button\n class=\"tpl-block-action-btn tpl-block-delete-btn tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-sm tpl:border-none tpl:transition-colors tpl:duration-150\"\n :aria-label=\"t.blockActions.delete\"\n :title=\"t.blockActions.delete\"\n @click.stop=\"handleDelete\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n </div>\n <div\n v-if=\"isHiddenOnViewport\"\n class=\"tpl-block-hidden-overlay tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-[5] tpl:flex tpl:items-center tpl:justify-center tpl:rounded-sm\"\n >\n <span\n class=\"tpl:flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-2 tpl:py-1 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-chrome-bg-elevated)] tpl:text-[var(--tpl-chrome-text-muted)] tpl:shadow-[var(--tpl-shadow-sm)]\"\n >\n <EyeOff :size=\"12\" :stroke-width=\"1.5\" />\n {{\n format(t.blockActions.hiddenOnViewport, {\n viewport: hiddenLabel,\n })\n }}\n </span>\n </div>\n <div\n v-if=\"hasDisplayCondition && !isHiddenOnViewport\"\n class=\"tpl:absolute tpl:-left-1 tpl:top-1/2 tpl:z-[5] tpl:-translate-x-full tpl:-translate-y-1/2\"\n >\n <button\n data-testid=\"condition-toggle\"\n class=\"tpl-condition-toggle tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:p-1 tpl:transition-colors tpl:duration-150 tpl:bg-[var(--tpl-chrome-bg-elevated)] tpl:text-[var(--tpl-primary)] tpl:border tpl:border-[var(--tpl-border)]\"\n :aria-label=\"t.blockActions.conditionToggle\"\n :title=\"block.displayCondition?.label\"\n @click.stop=\"handleConditionToggle\"\n >\n <Filter :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n <!-- Comment count indicator -->\n <div\n v-if=\"blockCommentCount > 0 && !isHiddenOnViewport\"\n class=\"tpl:absolute tpl:-right-1 tpl:-top-1 tpl:z-[5] tpl:translate-x-full\"\n >\n <button\n class=\"tpl-comment-indicator tpl:flex tpl:min-h-6 tpl:min-w-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-0.5 tpl:rounded-full tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-semibold tpl:transition-colors tpl:duration-150 tpl:bg-[var(--tpl-chrome-primary-light)] tpl:text-[var(--tpl-primary)]\"\n :aria-label=\"\n format(t.blockActions.comments, { count: String(blockCommentCount) })\n \"\n @click.stop=\"caps.comments?.openForBlock(block.id)\"\n >\n <MessageCircle :size=\"12\" :stroke-width=\"2.5\" />\n {{ blockCommentCount }}\n </button>\n </div>\n <!-- Email content wrapper — target for the canvas dark-mode preview\n filter. Block chrome (action bar, indicators, overlays) is a\n sibling of this wrapper, so the filter never reaches them — no\n counter-filter needed and no toggle flicker. -->\n <div\n v-if=\"sectionWrapperStyle\"\n class=\"tpl-section-wrapper\"\n :style=\"sectionWrapperStyle\"\n >\n <div class=\"tpl-block-content\" :style=\"contentStyle\">\n <slot />\n </div>\n </div>\n <div v-else class=\"tpl-block-content\" :style=\"contentStyle\">\n <slot />\n </div>\n </div>\n</template>\n\n<style scoped>\n/* Idle — subtle dashed outline so block boundaries are always visible */\n.tpl-block--idle {\n outline: 1.5px dashed color-mix(in srgb, var(--tpl-primary) 30%, transparent);\n outline-offset: -1px;\n transition:\n outline 150ms cubic-bezier(0.16, 1, 0.3, 1),\n box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* Hover — solid outline, more prominent */\n.tpl-block--idle:hover {\n outline: 1.5px solid color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n outline-offset: -1px;\n box-shadow: 0 0 8px color-mix(in srgb, var(--tpl-primary) 8%, transparent);\n}\n\n/* Picked (saved-blocks pick session) — a solid outline plus a crisp outer ring.\n Deliberately NOT a background fill: the block's own background is painted on\n the inner `.tpl-block-content` (`backgroundColor || \"transparent\"`), so a tint\n on `.tpl-block` is covered by any block that sets one and shows through only\n on transparent ones — the picked state would appear on some blocks and not\n others in the same template. `outline` and a non-inset `box-shadow` draw on\n this element's border box, outside the content's paint area, so they read\n identically regardless of what the block itself is filled with.\n\n The differentiator from idle is line style and weight (dashed 1.5px @30% →\n solid 2px @100%) plus the ring — not hue alone — so the state survives without\n colour perception, and the pick bar's live count is the numeric confirmation.\n Distinct from `--selected`'s soft blurred glow; the two never co-occur anyway,\n since Canvas suppresses selection styling while a session runs. */\n.tpl-block--picked {\n outline: 2px solid var(--tpl-primary);\n outline-offset: -1px;\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--tpl-primary) 18%, transparent);\n transition:\n outline 150ms cubic-bezier(0.16, 1, 0.3, 1),\n box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* Selection — solid outline + soft outer glow */\n.tpl-block--selected {\n outline: 2px solid color-mix(in srgb, var(--tpl-primary) 60%, transparent);\n outline-offset: -1px;\n box-shadow: 0 0 12px color-mix(in srgb, var(--tpl-primary) 15%, transparent);\n transition:\n outline 150ms cubic-bezier(0.16, 1, 0.3, 1),\n box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* Action buttons — muted text, warm hover, press feedback */\n.tpl-block-action-btn {\n color: var(--tpl-chrome-text-muted);\n background-color: transparent;\n}\n\n.tpl-block-action-btn:hover {\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-chrome-text);\n}\n\n.tpl-block-action-btn:active {\n transform: scale(0.9);\n}\n\n.tpl-block-delete-btn:hover {\n background-color: var(--tpl-danger-light);\n color: var(--tpl-danger);\n}\n\n/* Condition toggle — interactive hover + press */\n.tpl-condition-toggle:hover {\n background-color: var(--tpl-bg-hover) !important;\n}\n\n.tpl-condition-toggle:active {\n transform: scale(0.9);\n}\n\n/* Hidden block overlay — dims the block when hidden on current viewport */\n.tpl-block-hidden-overlay {\n background-color: color-mix(in srgb, var(--tpl-bg) 60%, transparent);\n}\n</style>\n","/**\n * Strip a single outer `<p>` wrapper from a TipTap-stored HTML string so the\n * canvas can render the inner inline content directly inside a heading\n * element. Mirrors the renderer's `unwrapParagraph` so the editor canvas and\n * the exported MJML/HTML agree on the title's element structure.\n */\nexport function unwrapParagraph(html: string): string {\n const match = html.match(/^\\s*<p\\b[^>]*>([\\s\\S]*)<\\/p>\\s*$/);\n if (!match) return html;\n // Only unwrap when there is exactly one top-level <p>. If the content has\n // multiple paragraphs, leave it alone — same rule the renderer applies.\n if (/<\\/p>\\s*<p\\b/i.test(match[1])) return html;\n return match[1];\n}\n","<script setup lang=\"ts\">\nimport { useEditableTextBlock } from \"../../composables/useEditableTextBlock\";\nimport type {\n TitleBlock as TitleBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\nimport { computed, defineAsyncComponent } from \"vue\";\nimport { unwrapParagraph } from \"../../utils/unwrapParagraph\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n viewport: ViewportSize;\n}>();\n\nconst TitleEditor = defineAsyncComponent(() => import(\"./TitleEditor.vue\"));\n\nconst {\n isEditing,\n blockRef,\n toolbarPosition,\n resolvedContent,\n handleDoubleClick,\n handleEditorDone,\n} = useEditableTextBlock(() => props.block.content);\n\nconst titleStyle = computed(() => {\n const fontSize = HEADING_LEVEL_FONT_SIZE[props.block.level];\n const style: Record<string, string> = {\n fontSize: `${fontSize}px`,\n textAlign: props.block.textAlign,\n };\n // Unset color = inherit the canvas text color (the document `textColor`).\n if (props.block.color) {\n style.color = props.block.color;\n }\n if (props.block.fontFamily) {\n style.fontFamily = props.block.fontFamily;\n }\n return style;\n});\n\nconst headingTag = computed(() => `h${props.block.level}`);\nconst headingHtml = computed(() => unwrapParagraph(resolvedContent.value));\n</script>\n\n<template>\n <div\n ref=\"blockRef\"\n class=\"tpl:min-h-[1em] tpl:w-full\"\n :style=\"titleStyle\"\n @dblclick=\"handleDoubleClick\"\n >\n <TitleEditor\n v-if=\"isEditing\"\n :block=\"block\"\n :toolbar-position=\"toolbarPosition\"\n @done=\"handleEditorDone\"\n />\n <!-- eslint-disable vue/no-v-html, vue/no-v-text-v-html-on-component -->\n <component\n :is=\"headingTag\"\n v-else\n class=\"tpl-text-content tpl:m-0 tpl:font-[inherit] tpl:text-[length:inherit] tpl:leading-tight tpl:outline-none [&_a]:tpl:underline [&_p]:tpl:m-0 [&_p]:tpl:mb-2 [&_p:last-child]:tpl:mb-0\"\n :style=\"{ color: 'inherit' }\"\n v-html=\"headingHtml\"\n />\n <!-- eslint-enable vue/no-v-html, vue/no-v-text-v-html-on-component -->\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useEditableTextBlock } from \"../../composables/useEditableTextBlock\";\nimport type {\n TitleBlock as TitleBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\nimport { computed, defineAsyncComponent } from \"vue\";\nimport { unwrapParagraph } from \"../../utils/unwrapParagraph\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n viewport: ViewportSize;\n}>();\n\nconst TitleEditor = defineAsyncComponent(() => import(\"./TitleEditor.vue\"));\n\nconst {\n isEditing,\n blockRef,\n toolbarPosition,\n resolvedContent,\n handleDoubleClick,\n handleEditorDone,\n} = useEditableTextBlock(() => props.block.content);\n\nconst titleStyle = computed(() => {\n const fontSize = HEADING_LEVEL_FONT_SIZE[props.block.level];\n const style: Record<string, string> = {\n fontSize: `${fontSize}px`,\n textAlign: props.block.textAlign,\n };\n // Unset color = inherit the canvas text color (the document `textColor`).\n if (props.block.color) {\n style.color = props.block.color;\n }\n if (props.block.fontFamily) {\n style.fontFamily = props.block.fontFamily;\n }\n return style;\n});\n\nconst headingTag = computed(() => `h${props.block.level}`);\nconst headingHtml = computed(() => unwrapParagraph(resolvedContent.value));\n</script>\n\n<template>\n <div\n ref=\"blockRef\"\n class=\"tpl:min-h-[1em] tpl:w-full\"\n :style=\"titleStyle\"\n @dblclick=\"handleDoubleClick\"\n >\n <TitleEditor\n v-if=\"isEditing\"\n :block=\"block\"\n :toolbar-position=\"toolbarPosition\"\n @done=\"handleEditorDone\"\n />\n <!-- eslint-disable vue/no-v-html, vue/no-v-text-v-html-on-component -->\n <component\n :is=\"headingTag\"\n v-else\n class=\"tpl-text-content tpl:m-0 tpl:font-[inherit] tpl:text-[length:inherit] tpl:leading-tight tpl:outline-none [&_a]:tpl:underline [&_p]:tpl:m-0 [&_p]:tpl:mb-2 [&_p:last-child]:tpl:mb-0\"\n :style=\"{ color: 'inherit' }\"\n v-html=\"headingHtml\"\n />\n <!-- eslint-enable vue/no-v-html, vue/no-v-text-v-html-on-component -->\n </div>\n</template>\n","/**\n * Drop-guard for a section column's VueDraggable `put` predicate.\n *\n * MJML forbids `<mj-section>` inside `<mj-column>`, so a section nested in a\n * column is silently dropped from the exported MJML (issue #292). The editor\n * must reject a section being dropped into a column up front.\n *\n * A dragged element identifies its block type differently depending on source:\n * canvas blocks carry `data-block-type` (set by `BlockWrapper`), palette items\n * carry `data-palette-type` (set by `Sidebar`). The guard must check BOTH —\n * checking only `data-block-type` lets a section dragged from the palette slip\n * through, which was the exact hole behind #292.\n */\nexport function canDropInSectionColumn(el: HTMLElement): boolean {\n return (\n el.dataset.blockType !== \"section\" && el.dataset.paletteType !== \"section\"\n );\n}\n","<script setup lang=\"ts\">\nimport BlockWrapper from \"./BlockWrapper.vue\";\nimport ButtonBlock from \"./ButtonBlock.vue\";\nimport CustomBlock from \"./CustomBlock.vue\";\nimport DividerBlock from \"./DividerBlock.vue\";\nimport ImageBlock from \"./ImageBlock.vue\";\nimport TitleBlock from \"./TitleBlock.vue\";\nimport ParagraphBlock from \"./ParagraphBlock.vue\";\nimport { useI18n } from \"../../composables\";\nimport { resolveBlockComponent } from \"../../utils/blockComponentResolver\";\nimport { canDropInSectionColumn } from \"../../utils/sectionColumnDrop\";\nimport type {\n Block,\n CustomBlock as CustomBlockType,\n SectionBlock as SectionBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed, inject, type Component } from \"vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport {\n EDITOR_KEY,\n APPLIES_CONDITION_FILTER_KEY,\n CONDITION_PREVIEW_KEY,\n BLOCK_REGISTRY_KEY,\n CAPABILITIES_KEY,\n requireInject,\n} from \"../../keys\";\n\nconst sectionBlockComponentMap: Record<string, Component> = {\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n block: SectionBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"SectionBlock\");\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\nconst caps = inject(CAPABILITIES_KEY, {});\n// Provided by the surface, never re-derived here — see the key's comment.\nconst appliesConditionFilter = inject(APPLIES_CONDITION_FILTER_KEY, null);\n\n/**\n * A click on a column child either picks the whole section (during a\n * saved-blocks pick session) or selects the child.\n *\n * Section children are never independently savable — a saved block's content is\n * a top-level `Block[]` — so the section is what a click resolves to. This has\n * to be explicit: `BlockWrapper.handleClick` calls `stopPropagation()`, so the\n * click never reaches the section's own wrapper on its own.\n */\nfunction handleChildSelect(childBlockId: string): void {\n const savedBlocks = caps.savedBlocks;\n if (savedBlocks?.isPicking.value) {\n savedBlocks.togglePick(props.block.id);\n return;\n }\n editor.selectBlock(childBlockId);\n}\n\nconst columnWidths = computed(() => {\n switch (props.block.columns) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n return [\"33.33%\", \"33.33%\", \"33.33%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n});\n\nconst columns = computed(() => {\n const count = columnWidths.value.length;\n const children = [...props.block.children];\n while (children.length < count) {\n children.push([]);\n }\n return children.slice(0, count);\n});\n\n// Mirror MJML's responsive behavior in the canvas: on the mobile viewport,\n// columns stack full-width unless the section opts out via `stackOnMobile:\n// false` (which renders as `<mj-group>` and keeps them side-by-side).\nconst isMobileStacked = computed(\n () => props.viewport === \"mobile\" && props.block.stackOnMobile !== false,\n);\n\nfunction getColumnBlocks(colIndex: number): Block[] {\n return columns.value[colIndex] || [];\n}\n\nfunction setColumnBlocks(colIndex: number, blocks: Block[]): void {\n // Strip non-Block fields (e.g., a DOM `.el` back-reference Sortable\n // attaches to its list root expando) before the array lands in state.\n // The block factories produce flat, JSON-clean shapes; rebuilding each\n // entry as a plain object keeps the contract intact without a full\n // deep clone and prevents `history.cloneContent` from later choking on\n // a cyclic structure.\n const safeBlocks = blocks.map((b) => JSON.parse(JSON.stringify(b)) as Block);\n const newChildren = [...props.block.children];\n while (newChildren.length <= colIndex) {\n newChildren.push([]);\n }\n newChildren[colIndex] = safeBlocks;\n editor.updateBlock(props.block.id, { children: newChildren });\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, sectionBlockComponentMap);\n}\n\nfunction handleFetchData(\n block: Block,\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n): void {\n if (block.type !== \"custom\") {\n return;\n }\n\n editor.updateBlock(block.id, {\n fieldValues: payload.fieldValues,\n dataSourceFetched: payload.dataSourceFetched,\n } as Partial<CustomBlockType>);\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div\n class=\"tpl:flex tpl:gap-0\"\n :class=\"{ 'tpl:flex-col': isMobileStacked }\"\n >\n <div\n v-for=\"(_, colIndex) in columns\"\n :key=\"colIndex\"\n class=\"tpl:relative tpl:min-h-[60px] tpl:rounded\"\n :class=\"\n getColumnBlocks(colIndex).length === 0\n ? 'tpl:border tpl:border-dashed tpl:border-[var(--tpl-border)]'\n : ''\n \"\n :style=\"{ width: isMobileStacked ? '100%' : columnWidths[colIndex] }\"\n >\n <VueDraggable\n :model-value=\"getColumnBlocks(colIndex)\"\n :group=\"{\n name: 'blocks',\n pull: true,\n put: (_to: unknown, _from: unknown, el: HTMLElement) =>\n canDropInSectionColumn(el),\n }\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-block-btn\"\n :invert-swap=\"true\"\n :inverted-swap-threshold=\"0.65\"\n :empty-insert-threshold=\"20\"\n :force-fallback=\"true\"\n class=\"tpl:min-h-[60px]\"\n @update:model-value=\"(val: Block[]) => setColumnBlocks(colIndex, val)\"\n >\n <div\n v-for=\"childBlock in getColumnBlocks(colIndex)\"\n :key=\"childBlock.id\"\n v-show=\"\n appliesConditionFilter === false ||\n !conditionPreview?.isHidden(childBlock.id)\n \"\n >\n <BlockWrapper\n :block=\"childBlock\"\n :is-selected=\"editor.state.selectedBlockId === childBlock.id\"\n :viewport=\"viewport\"\n :preview-mode=\"editor.state.previewMode\"\n nested\n @select=\"handleChildSelect(childBlock.id)\"\n >\n <component\n :is=\"getBlockComponent(childBlock)\"\n :block=\"childBlock\"\n :viewport=\"viewport\"\n @fetch-data=\"handleFetchData(childBlock, $event)\"\n @update=\"\n (updates: Partial<Block>) =>\n editor.updateBlock(childBlock.id, updates)\n \"\n />\n </BlockWrapper>\n </div>\n </VueDraggable>\n <div\n v-if=\"getColumnBlocks(colIndex).length === 0\"\n data-testid=\"section-drop-hint\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:flex tpl:items-center tpl:justify-center tpl:text-xs tpl:text-[var(--tpl-chrome-text-dim)]\"\n >\n <span>{{ t.section.dropHere }}</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport BlockWrapper from \"./BlockWrapper.vue\";\nimport ButtonBlock from \"./ButtonBlock.vue\";\nimport CustomBlock from \"./CustomBlock.vue\";\nimport DividerBlock from \"./DividerBlock.vue\";\nimport ImageBlock from \"./ImageBlock.vue\";\nimport TitleBlock from \"./TitleBlock.vue\";\nimport ParagraphBlock from \"./ParagraphBlock.vue\";\nimport { useI18n } from \"../../composables\";\nimport { resolveBlockComponent } from \"../../utils/blockComponentResolver\";\nimport { canDropInSectionColumn } from \"../../utils/sectionColumnDrop\";\nimport type {\n Block,\n CustomBlock as CustomBlockType,\n SectionBlock as SectionBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed, inject, type Component } from \"vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport {\n EDITOR_KEY,\n APPLIES_CONDITION_FILTER_KEY,\n CONDITION_PREVIEW_KEY,\n BLOCK_REGISTRY_KEY,\n CAPABILITIES_KEY,\n requireInject,\n} from \"../../keys\";\n\nconst sectionBlockComponentMap: Record<string, Component> = {\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n block: SectionBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"SectionBlock\");\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\nconst caps = inject(CAPABILITIES_KEY, {});\n// Provided by the surface, never re-derived here — see the key's comment.\nconst appliesConditionFilter = inject(APPLIES_CONDITION_FILTER_KEY, null);\n\n/**\n * A click on a column child either picks the whole section (during a\n * saved-blocks pick session) or selects the child.\n *\n * Section children are never independently savable — a saved block's content is\n * a top-level `Block[]` — so the section is what a click resolves to. This has\n * to be explicit: `BlockWrapper.handleClick` calls `stopPropagation()`, so the\n * click never reaches the section's own wrapper on its own.\n */\nfunction handleChildSelect(childBlockId: string): void {\n const savedBlocks = caps.savedBlocks;\n if (savedBlocks?.isPicking.value) {\n savedBlocks.togglePick(props.block.id);\n return;\n }\n editor.selectBlock(childBlockId);\n}\n\nconst columnWidths = computed(() => {\n switch (props.block.columns) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n return [\"33.33%\", \"33.33%\", \"33.33%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n});\n\nconst columns = computed(() => {\n const count = columnWidths.value.length;\n const children = [...props.block.children];\n while (children.length < count) {\n children.push([]);\n }\n return children.slice(0, count);\n});\n\n// Mirror MJML's responsive behavior in the canvas: on the mobile viewport,\n// columns stack full-width unless the section opts out via `stackOnMobile:\n// false` (which renders as `<mj-group>` and keeps them side-by-side).\nconst isMobileStacked = computed(\n () => props.viewport === \"mobile\" && props.block.stackOnMobile !== false,\n);\n\nfunction getColumnBlocks(colIndex: number): Block[] {\n return columns.value[colIndex] || [];\n}\n\nfunction setColumnBlocks(colIndex: number, blocks: Block[]): void {\n // Strip non-Block fields (e.g., a DOM `.el` back-reference Sortable\n // attaches to its list root expando) before the array lands in state.\n // The block factories produce flat, JSON-clean shapes; rebuilding each\n // entry as a plain object keeps the contract intact without a full\n // deep clone and prevents `history.cloneContent` from later choking on\n // a cyclic structure.\n const safeBlocks = blocks.map((b) => JSON.parse(JSON.stringify(b)) as Block);\n const newChildren = [...props.block.children];\n while (newChildren.length <= colIndex) {\n newChildren.push([]);\n }\n newChildren[colIndex] = safeBlocks;\n editor.updateBlock(props.block.id, { children: newChildren });\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, sectionBlockComponentMap);\n}\n\nfunction handleFetchData(\n block: Block,\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n): void {\n if (block.type !== \"custom\") {\n return;\n }\n\n editor.updateBlock(block.id, {\n fieldValues: payload.fieldValues,\n dataSourceFetched: payload.dataSourceFetched,\n } as Partial<CustomBlockType>);\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div\n class=\"tpl:flex tpl:gap-0\"\n :class=\"{ 'tpl:flex-col': isMobileStacked }\"\n >\n <div\n v-for=\"(_, colIndex) in columns\"\n :key=\"colIndex\"\n class=\"tpl:relative tpl:min-h-[60px] tpl:rounded\"\n :class=\"\n getColumnBlocks(colIndex).length === 0\n ? 'tpl:border tpl:border-dashed tpl:border-[var(--tpl-border)]'\n : ''\n \"\n :style=\"{ width: isMobileStacked ? '100%' : columnWidths[colIndex] }\"\n >\n <VueDraggable\n :model-value=\"getColumnBlocks(colIndex)\"\n :group=\"{\n name: 'blocks',\n pull: true,\n put: (_to: unknown, _from: unknown, el: HTMLElement) =>\n canDropInSectionColumn(el),\n }\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-block-btn\"\n :invert-swap=\"true\"\n :inverted-swap-threshold=\"0.65\"\n :empty-insert-threshold=\"20\"\n :force-fallback=\"true\"\n class=\"tpl:min-h-[60px]\"\n @update:model-value=\"(val: Block[]) => setColumnBlocks(colIndex, val)\"\n >\n <div\n v-for=\"childBlock in getColumnBlocks(colIndex)\"\n :key=\"childBlock.id\"\n v-show=\"\n appliesConditionFilter === false ||\n !conditionPreview?.isHidden(childBlock.id)\n \"\n >\n <BlockWrapper\n :block=\"childBlock\"\n :is-selected=\"editor.state.selectedBlockId === childBlock.id\"\n :viewport=\"viewport\"\n :preview-mode=\"editor.state.previewMode\"\n nested\n @select=\"handleChildSelect(childBlock.id)\"\n >\n <component\n :is=\"getBlockComponent(childBlock)\"\n :block=\"childBlock\"\n :viewport=\"viewport\"\n @fetch-data=\"handleFetchData(childBlock, $event)\"\n @update=\"\n (updates: Partial<Block>) =>\n editor.updateBlock(childBlock.id, updates)\n \"\n />\n </BlockWrapper>\n </div>\n </VueDraggable>\n <div\n v-if=\"getColumnBlocks(colIndex).length === 0\"\n data-testid=\"section-drop-hint\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:flex tpl:items-center tpl:justify-center tpl:text-xs tpl:text-[var(--tpl-chrome-text-dim)]\"\n >\n <span>{{ t.section.dropHere }}</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n","import { SOCIAL_ICON_GLYPHS, type SocialPlatform } from \"@templatical/types\";\n\n/**\n * Icon glyph (SVG path + brand color) per platform. Re-exported from the\n * shared kernel in `@templatical/types` so the editor's inline SVG and the\n * renderer's PNG rasterizer never drift. Platform display labels are NOT here\n * — they come from i18n (`t.social.platforms[platform]`).\n */\nexport const socialIcons = SOCIAL_ICON_GLYPHS;\n\nexport const socialIconSizeMap: Record<\"small\" | \"medium\" | \"large\", number> = {\n small: 24,\n medium: 32,\n large: 48,\n};\n\nexport const socialPlatformOptions: SocialPlatform[] = [\n \"facebook\",\n \"twitter\",\n \"instagram\",\n \"linkedin\",\n \"youtube\",\n \"tiktok\",\n \"pinterest\",\n \"email\",\n \"website\",\n \"whatsapp\",\n \"telegram\",\n \"discord\",\n \"snapchat\",\n \"reddit\",\n \"github\",\n \"dribbble\",\n \"behance\",\n];\n","<script setup lang=\"ts\">\nimport { socialIcons, socialIconSizeMap } from \"../../constants/socialIcons\";\nimport type {\n SocialIconSize,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n platform: SocialPlatform;\n iconStyle: SocialIconStyle;\n iconSize: SocialIconSize;\n}>();\n\nconst iconDef = computed(() => socialIcons[props.platform]);\nconst size = computed(() => socialIconSizeMap[props.iconSize]);\n\nconst containerStyle = computed(() => {\n const baseStyles: Record<string, string> = {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: `${size.value}px`,\n height: `${size.value}px`,\n };\n\n switch (props.iconStyle) {\n case \"solid\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"4px\",\n };\n case \"outlined\":\n return {\n ...baseStyles,\n backgroundColor: \"transparent\",\n border: `2px solid ${iconDef.value.color}`,\n borderRadius: \"4px\",\n };\n case \"rounded\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"8px\",\n };\n case \"square\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"0\",\n };\n case \"circle\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"50%\",\n };\n default:\n return baseStyles;\n }\n});\n\nconst svgSize = computed(() => Math.floor(size.value * 0.6));\n\nconst svgColor = computed(() => {\n if (props.iconStyle === \"outlined\") {\n return iconDef.value.color;\n }\n return \"#ffffff\";\n});\n</script>\n\n<template>\n <span :style=\"containerStyle\">\n <svg\n :width=\"svgSize\"\n :height=\"svgSize\"\n viewBox=\"0 0 24 24\"\n :fill=\"svgColor\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path :d=\"iconDef.path\" />\n </svg>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { socialIcons, socialIconSizeMap } from \"../../constants/socialIcons\";\nimport type {\n SocialIconSize,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n platform: SocialPlatform;\n iconStyle: SocialIconStyle;\n iconSize: SocialIconSize;\n}>();\n\nconst iconDef = computed(() => socialIcons[props.platform]);\nconst size = computed(() => socialIconSizeMap[props.iconSize]);\n\nconst containerStyle = computed(() => {\n const baseStyles: Record<string, string> = {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: `${size.value}px`,\n height: `${size.value}px`,\n };\n\n switch (props.iconStyle) {\n case \"solid\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"4px\",\n };\n case \"outlined\":\n return {\n ...baseStyles,\n backgroundColor: \"transparent\",\n border: `2px solid ${iconDef.value.color}`,\n borderRadius: \"4px\",\n };\n case \"rounded\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"8px\",\n };\n case \"square\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"0\",\n };\n case \"circle\":\n return {\n ...baseStyles,\n backgroundColor: iconDef.value.color,\n borderRadius: \"50%\",\n };\n default:\n return baseStyles;\n }\n});\n\nconst svgSize = computed(() => Math.floor(size.value * 0.6));\n\nconst svgColor = computed(() => {\n if (props.iconStyle === \"outlined\") {\n return iconDef.value.color;\n }\n return \"#ffffff\";\n});\n</script>\n\n<template>\n <span :style=\"containerStyle\">\n <svg\n :width=\"svgSize\"\n :height=\"svgSize\"\n viewBox=\"0 0 24 24\"\n :fill=\"svgColor\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path :d=\"iconDef.path\" />\n </svg>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport SocialIconSvg from \"./SocialIconSvg.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n SocialIconsBlock as SocialIconsBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { Share2 } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: SocialIconsBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst containerStyle = computed(() => ({\n display: \"flex\",\n flexWrap: \"wrap\" as const,\n gap: `${props.block.spacing}px`,\n justifyContent:\n props.block.align === \"left\"\n ? \"flex-start\"\n : props.block.align === \"right\"\n ? \"flex-end\"\n : \"center\",\n}));\n\nconst hasIcons = computed(() => props.block.icons.length > 0);\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div v-if=\"hasIcons\" :style=\"containerStyle\">\n <a\n v-for=\"icon in block.icons\"\n :key=\"icon.id\"\n :href=\"icon.url || '#'\"\n class=\"tpl:cursor-default\"\n @click.prevent\n >\n <SocialIconSvg\n :platform=\"icon.platform\"\n :icon-style=\"block.iconStyle\"\n :icon-size=\"block.iconSize\"\n />\n </a>\n </div>\n <div\n v-else\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:text-sm tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Share2 :size=\"16\" />\n <span>{{ t.social.addIcons }}</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport SocialIconSvg from \"./SocialIconSvg.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n SocialIconsBlock as SocialIconsBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { Share2 } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: SocialIconsBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\n\nconst containerStyle = computed(() => ({\n display: \"flex\",\n flexWrap: \"wrap\" as const,\n gap: `${props.block.spacing}px`,\n justifyContent:\n props.block.align === \"left\"\n ? \"flex-start\"\n : props.block.align === \"right\"\n ? \"flex-end\"\n : \"center\",\n}));\n\nconst hasIcons = computed(() => props.block.icons.length > 0);\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div v-if=\"hasIcons\" :style=\"containerStyle\">\n <a\n v-for=\"icon in block.icons\"\n :key=\"icon.id\"\n :href=\"icon.url || '#'\"\n class=\"tpl:cursor-default\"\n @click.prevent\n >\n <SocialIconSvg\n :platform=\"icon.platform\"\n :icon-style=\"block.iconStyle\"\n :icon-size=\"block.iconSize\"\n />\n </a>\n </div>\n <div\n v-else\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:text-sm tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Share2 :size=\"16\" />\n <span>{{ t.social.addIcons }}</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n SpacerBlock as SpacerBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport { EDITOR_KEY, requireInject } from \"../../keys\";\n\nconst props = defineProps<{\n block: SpacerBlockType;\n viewport: ViewportSize;\n}>();\n\nconst editor = requireInject(EDITOR_KEY, \"SpacerBlock\");\n\nconst spacerStyle = computed(() => ({\n height: `${props.block.height}px`,\n minHeight: `${props.block.height}px`,\n ...(editor.state.previewMode ? {} : { borderColor: \"var(--tpl-border)\" }),\n}));\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div\n :style=\"spacerStyle\"\n class=\"tpl:relative tpl:flex tpl:items-center tpl:justify-center\"\n :class=\"{\n 'tpl:border-y tpl:border-dashed': !editor.state.previewMode,\n }\"\n >\n <span\n v-if=\"!editor.state.previewMode\"\n class=\"tpl:absolute tpl:rounded tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium\"\n style=\"\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-text-dim);\n \"\n >\n {{ block.height }}px\n </span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type {\n SpacerBlock as SpacerBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport { EDITOR_KEY, requireInject } from \"../../keys\";\n\nconst props = defineProps<{\n block: SpacerBlockType;\n viewport: ViewportSize;\n}>();\n\nconst editor = requireInject(EDITOR_KEY, \"SpacerBlock\");\n\nconst spacerStyle = computed(() => ({\n height: `${props.block.height}px`,\n minHeight: `${props.block.height}px`,\n ...(editor.state.previewMode ? {} : { borderColor: \"var(--tpl-border)\" }),\n}));\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <div\n :style=\"spacerStyle\"\n class=\"tpl:relative tpl:flex tpl:items-center tpl:justify-center\"\n :class=\"{\n 'tpl:border-y tpl:border-dashed': !editor.state.previewMode,\n }\"\n >\n <span\n v-if=\"!editor.state.previewMode\"\n class=\"tpl:absolute tpl:rounded tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium\"\n style=\"\n background-color: var(--tpl-bg-hover);\n color: var(--tpl-text-dim);\n \"\n >\n {{ block.height }}px\n </span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n TableBlock as TableBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { Table } from \"@lucide/vue\";\nimport { computed, type Directive } from \"vue\";\nimport { EDITOR_KEY, requireInject } from \"../../keys\";\n\n// Hydrate a contenteditable cell's text imperatively. v-text would re-apply\n// on every prop update and overwrite the user's keystrokes mid-edit when\n// external content lands (collab event, undo replay, autosave round-trip),\n// so we skip the update while the cell is focused — DOM is the source of\n// truth during edit, and the prop syncs back via @blur.\nconst vCellContent: Directive<HTMLElement, string> = {\n mounted(el, binding) {\n el.textContent = binding.value ?? \"\";\n },\n updated(el, binding) {\n if (binding.value === binding.oldValue) return;\n // Resolve the focused element from the cell's own root. In the default\n // shadow-DOM mount, `el.ownerDocument.activeElement` returns the shadow\n // host (never the inner <td>), so that guard never fires and the\n // directive overwrites the user's in-progress keystrokes. `getRootNode()`\n // returns the ShadowRoot in shadow mode and the Document in light mode —\n // both expose `activeElement` pointing at the truly-focused cell.\n const root = el.getRootNode() as Document | ShadowRoot;\n if (root.activeElement === el) return;\n el.textContent = binding.value ?? \"\";\n },\n};\n\nconst props = defineProps<{\n block: TableBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"TableBlock\");\n\nconst hasRows = computed(() => props.block.rows.length > 0);\n\nconst tableStyle = computed((): Record<string, string> => {\n const style: Record<string, string> = {\n width: \"100%\",\n borderCollapse: \"collapse\",\n fontSize: `${props.block.fontSize}px`,\n textAlign: props.block.textAlign,\n fontFamily: props.block.fontFamily || \"inherit\",\n };\n // Unset color = inherit the canvas text color (the document `textColor`).\n if (props.block.color) {\n style.color = props.block.color;\n }\n return style;\n});\n\nconst cellStyle = computed((): Record<string, string> => ({\n border: `${props.block.borderWidth}px solid ${props.block.borderColor}`,\n padding: `${props.block.cellPadding}px`,\n textAlign: props.block.textAlign,\n}));\n\nconst headerCellStyle = computed((): Record<string, string> => ({\n ...cellStyle.value,\n fontWeight: \"bold\",\n backgroundColor: props.block.headerBackgroundColor || \"transparent\",\n}));\n\nconst headerRow = computed(() =>\n props.block.hasHeaderRow && props.block.rows.length > 0\n ? props.block.rows[0]\n : null,\n);\n\nconst dataRows = computed(() =>\n props.block.hasHeaderRow ? props.block.rows.slice(1) : props.block.rows,\n);\n\nfunction onCellClick(): void {\n editor.selectBlock(props.block.id);\n}\n\nfunction onCellBlur(rowId: string, cellId: string, event: FocusEvent): void {\n const target = event.target as HTMLElement;\n const content = target.innerText.trim();\n\n const updatedRows = props.block.rows.map((row) => {\n if (row.id !== rowId) return row;\n return {\n ...row,\n cells: row.cells.map((cell) =>\n cell.id === cellId ? { ...cell, content } : cell,\n ),\n };\n });\n\n editor.updateBlock(props.block.id, { rows: updatedRows });\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <table v-if=\"hasRows\" :style=\"tableStyle\" class=\"tpl-table-editable\">\n <thead v-if=\"headerRow\">\n <tr>\n <th\n v-for=\"cell in headerRow.cells\"\n :key=\"cell.id\"\n :style=\"headerCellStyle\"\n :aria-label=\"t.table.cellPlaceholder\"\n contenteditable=\"true\"\n :data-placeholder=\"t.table.cellPlaceholder\"\n @blur=\"onCellBlur(headerRow!.id, cell.id, $event)\"\n @keydown.enter.prevent=\"($event.target as HTMLElement).blur()\"\n @click.stop=\"onCellClick\"\n v-cell-content=\"cell.content\"\n />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"row in dataRows\" :key=\"row.id\">\n <td\n v-for=\"cell in row.cells\"\n :key=\"cell.id\"\n :style=\"cellStyle\"\n :aria-label=\"t.table.cellPlaceholder\"\n contenteditable=\"true\"\n :data-placeholder=\"t.table.cellPlaceholder\"\n @blur=\"onCellBlur(row.id, cell.id, $event)\"\n @keydown.enter.prevent=\"($event.target as HTMLElement).blur()\"\n @click.stop=\"onCellClick\"\n v-cell-content=\"cell.content\"\n />\n </tr>\n </tbody>\n </table>\n <div\n v-else\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:text-sm tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Table :size=\"16\" />\n <span>{{ t.table.empty }}</span>\n </div>\n </div>\n</template>\n\n<style scoped>\n.tpl-table-editable :deep(th[contenteditable]),\n.tpl-table-editable :deep(td[contenteditable]) {\n outline: none;\n cursor: text;\n /* base-size-relative (issue #209) — not `rem`, which couples to host html font-size */\n min-width: calc(2 * var(--tpl-base-size));\n}\n\n.tpl-table-editable :deep(th[contenteditable]:empty::before),\n.tpl-table-editable :deep(td[contenteditable]:empty::before) {\n content: attr(data-placeholder);\n color: var(--tpl-text-dim);\n pointer-events: none;\n}\n\n.tpl-table-editable :deep(th[contenteditable]:focus),\n.tpl-table-editable :deep(td[contenteditable]:focus) {\n outline: 2px solid var(--tpl-primary);\n outline-offset: -2px;\n border-radius: 1px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport type {\n TableBlock as TableBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { Table } from \"@lucide/vue\";\nimport { computed, type Directive } from \"vue\";\nimport { EDITOR_KEY, requireInject } from \"../../keys\";\n\n// Hydrate a contenteditable cell's text imperatively. v-text would re-apply\n// on every prop update and overwrite the user's keystrokes mid-edit when\n// external content lands (collab event, undo replay, autosave round-trip),\n// so we skip the update while the cell is focused — DOM is the source of\n// truth during edit, and the prop syncs back via @blur.\nconst vCellContent: Directive<HTMLElement, string> = {\n mounted(el, binding) {\n el.textContent = binding.value ?? \"\";\n },\n updated(el, binding) {\n if (binding.value === binding.oldValue) return;\n // Resolve the focused element from the cell's own root. In the default\n // shadow-DOM mount, `el.ownerDocument.activeElement` returns the shadow\n // host (never the inner <td>), so that guard never fires and the\n // directive overwrites the user's in-progress keystrokes. `getRootNode()`\n // returns the ShadowRoot in shadow mode and the Document in light mode —\n // both expose `activeElement` pointing at the truly-focused cell.\n const root = el.getRootNode() as Document | ShadowRoot;\n if (root.activeElement === el) return;\n el.textContent = binding.value ?? \"\";\n },\n};\n\nconst props = defineProps<{\n block: TableBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"TableBlock\");\n\nconst hasRows = computed(() => props.block.rows.length > 0);\n\nconst tableStyle = computed((): Record<string, string> => {\n const style: Record<string, string> = {\n width: \"100%\",\n borderCollapse: \"collapse\",\n fontSize: `${props.block.fontSize}px`,\n textAlign: props.block.textAlign,\n fontFamily: props.block.fontFamily || \"inherit\",\n };\n // Unset color = inherit the canvas text color (the document `textColor`).\n if (props.block.color) {\n style.color = props.block.color;\n }\n return style;\n});\n\nconst cellStyle = computed((): Record<string, string> => ({\n border: `${props.block.borderWidth}px solid ${props.block.borderColor}`,\n padding: `${props.block.cellPadding}px`,\n textAlign: props.block.textAlign,\n}));\n\nconst headerCellStyle = computed((): Record<string, string> => ({\n ...cellStyle.value,\n fontWeight: \"bold\",\n backgroundColor: props.block.headerBackgroundColor || \"transparent\",\n}));\n\nconst headerRow = computed(() =>\n props.block.hasHeaderRow && props.block.rows.length > 0\n ? props.block.rows[0]\n : null,\n);\n\nconst dataRows = computed(() =>\n props.block.hasHeaderRow ? props.block.rows.slice(1) : props.block.rows,\n);\n\nfunction onCellClick(): void {\n editor.selectBlock(props.block.id);\n}\n\nfunction onCellBlur(rowId: string, cellId: string, event: FocusEvent): void {\n const target = event.target as HTMLElement;\n const content = target.innerText.trim();\n\n const updatedRows = props.block.rows.map((row) => {\n if (row.id !== rowId) return row;\n return {\n ...row,\n cells: row.cells.map((cell) =>\n cell.id === cellId ? { ...cell, content } : cell,\n ),\n };\n });\n\n editor.updateBlock(props.block.id, { rows: updatedRows });\n}\n</script>\n\n<template>\n <div class=\"tpl:w-full\">\n <table v-if=\"hasRows\" :style=\"tableStyle\" class=\"tpl-table-editable\">\n <thead v-if=\"headerRow\">\n <tr>\n <th\n v-for=\"cell in headerRow.cells\"\n :key=\"cell.id\"\n :style=\"headerCellStyle\"\n :aria-label=\"t.table.cellPlaceholder\"\n contenteditable=\"true\"\n :data-placeholder=\"t.table.cellPlaceholder\"\n @blur=\"onCellBlur(headerRow!.id, cell.id, $event)\"\n @keydown.enter.prevent=\"($event.target as HTMLElement).blur()\"\n @click.stop=\"onCellClick\"\n v-cell-content=\"cell.content\"\n />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"row in dataRows\" :key=\"row.id\">\n <td\n v-for=\"cell in row.cells\"\n :key=\"cell.id\"\n :style=\"cellStyle\"\n :aria-label=\"t.table.cellPlaceholder\"\n contenteditable=\"true\"\n :data-placeholder=\"t.table.cellPlaceholder\"\n @blur=\"onCellBlur(row.id, cell.id, $event)\"\n @keydown.enter.prevent=\"($event.target as HTMLElement).blur()\"\n @click.stop=\"onCellClick\"\n v-cell-content=\"cell.content\"\n />\n </tr>\n </tbody>\n </table>\n <div\n v-else\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border tpl:border-dashed tpl:py-4 tpl:text-sm tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Table :size=\"16\" />\n <span>{{ t.table.empty }}</span>\n </div>\n </div>\n</template>\n\n<style scoped>\n.tpl-table-editable :deep(th[contenteditable]),\n.tpl-table-editable :deep(td[contenteditable]) {\n outline: none;\n cursor: text;\n /* base-size-relative (issue #209) — not `rem`, which couples to host html font-size */\n min-width: calc(2 * var(--tpl-base-size));\n}\n\n.tpl-table-editable :deep(th[contenteditable]:empty::before),\n.tpl-table-editable :deep(td[contenteditable]:empty::before) {\n content: attr(data-placeholder);\n color: var(--tpl-text-dim);\n pointer-events: none;\n}\n\n.tpl-table-editable :deep(th[contenteditable]:focus),\n.tpl-table-editable :deep(td[contenteditable]:focus) {\n outline: 2px solid var(--tpl-primary);\n outline-offset: -2px;\n border-radius: 1px;\n}\n</style>\n","<template>\n <div\n class=\"tpl:absolute tpl:inset-0 tpl:flex tpl:items-center tpl:justify-center tpl:bg-black/30\"\n :class=\"hoverEffect && 'tpl:transition-opacity tpl:group-hover:bg-black/40'\"\n >\n <div\n class=\"tpl:flex tpl:size-16 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-white/90 tpl:shadow-lg\"\n >\n <svg\n class=\"tpl:ml-1 tpl:text-[var(--tpl-danger)]\"\n width=\"28\"\n height=\"28\"\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n >\n <polygon points=\"5,3 19,12 5,21\" />\n </svg>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nwithDefaults(\n defineProps<{\n hoverEffect?: boolean;\n }>(),\n { hoverEffect: false },\n);\n</script>\n","<template>\n <div\n class=\"tpl:absolute tpl:inset-0 tpl:flex tpl:items-center tpl:justify-center tpl:bg-black/30\"\n :class=\"hoverEffect && 'tpl:transition-opacity tpl:group-hover:bg-black/40'\"\n >\n <div\n class=\"tpl:flex tpl:size-16 tpl:items-center tpl:justify-center tpl:rounded-full tpl:bg-white/90 tpl:shadow-lg\"\n >\n <svg\n class=\"tpl:ml-1 tpl:text-[var(--tpl-danger)]\"\n width=\"28\"\n height=\"28\"\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n >\n <polygon points=\"5,3 19,12 5,21\" />\n </svg>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nwithDefaults(\n defineProps<{\n hoverEffect?: boolean;\n }>(),\n { hoverEffect: false },\n);\n</script>\n","/**\n * Extract video ID and thumbnail URL from common video platforms\n */\n\ninterface VideoInfo {\n platform: \"youtube\" | \"vimeo\" | \"unknown\";\n videoId: string | null;\n thumbnailUrl: string | null;\n}\n\nexport function parseVideoUrl(url: string): VideoInfo {\n if (!url) {\n return { platform: \"unknown\", videoId: null, thumbnailUrl: null };\n }\n\n // YouTube patterns. Host is matched case-insensitively because URLs pasted\n // from the address bar can have an uppercase scheme/host.\n const youtubePatterns = [\n /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|youtube\\.com\\/embed\\/)([a-zA-Z0-9_-]{11})/i,\n /youtube\\.com\\/shorts\\/([a-zA-Z0-9_-]{11})/i,\n ];\n\n for (const pattern of youtubePatterns) {\n const match = url.match(pattern);\n if (match) {\n const videoId = match[1];\n return {\n platform: \"youtube\",\n videoId,\n thumbnailUrl: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,\n };\n }\n }\n\n // Vimeo patterns\n const vimeoPattern = /vimeo\\.com\\/(?:video\\/)?(\\d+)/i;\n const vimeoMatch = url.match(vimeoPattern);\n if (vimeoMatch) {\n const videoId = vimeoMatch[1];\n return {\n platform: \"vimeo\",\n videoId,\n // Vimeo requires API call for thumbnail, use placeholder\n thumbnailUrl: `https://vumbnail.com/${videoId}.jpg`,\n };\n }\n\n return { platform: \"unknown\", videoId: null, thumbnailUrl: null };\n}\n\nexport function getVideoThumbnail(\n url: string,\n customThumbnail?: string,\n): string | null {\n // Custom thumbnail takes priority\n if (customThumbnail) {\n return customThumbnail;\n }\n\n const info = parseVideoUrl(url);\n return info.thumbnailUrl;\n}\n","<script setup lang=\"ts\">\nimport VideoPlayButton from \"./VideoPlayButton.vue\";\nimport { useI18n, useMergeTag } from \"../../composables\";\nimport type {\n VideoBlock as VideoBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { getVideoThumbnail } from \"../../utils/videoThumbnail\";\nimport { useResolvedImageSrc } from \"../../composables/useImageUrlResolver\";\nimport { containsMergeTag } from \"@templatical/types\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\nimport { Video } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: VideoBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\nconst { syntax } = useMergeTag();\n\nconst hasMergeTagUrl = computed(\n () =>\n containsMergeTag(props.block.url, syntax) ||\n containsMergeTag(props.block.thumbnailUrl, syntax),\n);\n\n// Display-only preview URLs (#415). Only the explicit, user-entered\n// thumbnailUrl / placeholderUrl go through resolveImageUrl — an auto-derived\n// provider thumbnail (getVideoThumbnail) is already a real URL. The canonical\n// values stay on the block; the resolver only changes what the canvas shows.\nconst displayThumbnailUrl = useResolvedImageSrc(() =>\n hasMergeTagUrl.value ? undefined : props.block.thumbnailUrl || undefined,\n);\nconst displayPlaceholderUrl = useResolvedImageSrc(() =>\n hasMergeTagUrl.value ? props.block.placeholderUrl : undefined,\n);\n\nconst effectiveThumbnail = computed(() => {\n if (hasMergeTagUrl.value) return null;\n if (props.block.thumbnailUrl) return displayThumbnailUrl.value ?? null;\n return getVideoThumbnail(props.block.url);\n});\n\nconst containerStyle = computed(() => ({\n textAlign: props.block.align,\n}));\n\nconst thumbnailStyle = computed(() => {\n const align = props.block.align;\n return {\n maxWidth: \"100%\",\n width: props.block.width === \"full\" ? \"100%\" : `${props.block.width}px`,\n display: \"block\",\n marginLeft: align === \"center\" || align === \"right\" ? \"auto\" : undefined,\n marginRight: align === \"center\" ? \"auto\" : undefined,\n };\n});\n\nconst mergeTagValue = computed(() =>\n containsMergeTag(props.block.url, syntax)\n ? props.block.url\n : props.block.thumbnailUrl,\n);\n</script>\n\n<template>\n <div class=\"tpl:w-full\" :style=\"containerStyle\">\n <!-- Placeholder with preview image provided -->\n <div\n v-if=\"hasMergeTagUrl && block.placeholderUrl\"\n class=\"tpl:relative tpl:inline-block\"\n :style=\"thumbnailStyle\"\n >\n <img\n class=\"tpl:w-full tpl:border-0\"\n :src=\"displayPlaceholderUrl\"\n :alt=\"block.alt\"\n />\n <VideoPlayButton />\n </div>\n <!-- Placeholder visual (no preview image) -->\n <div\n v-else-if=\"hasMergeTagUrl\"\n class=\"tpl:relative tpl:!flex tpl:min-h-[150px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-center tpl:bg-[var(--tpl-bg-elevated)]\"\n style=\"\n border-color: color-mix(in srgb, var(--tpl-primary) 40%, transparent);\n \"\n :style=\"thumbnailStyle\"\n >\n <Video\n :size=\"36\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.5\"\n />\n <span\n class=\"tpl:max-w-full tpl:truncate tpl:px-3 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.7\"\n >\n <MergeTagPreviewText :text=\"mergeTagValue\" />\n </span>\n </div>\n <!-- Normal video thumbnail -->\n <template v-else-if=\"effectiveThumbnail\">\n <a\n v-if=\"block.url\"\n :href=\"block.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:group tpl:relative tpl:inline-block\"\n :style=\"thumbnailStyle\"\n @click.prevent\n >\n <img\n class=\"tpl:w-full tpl:border-0\"\n :src=\"effectiveThumbnail\"\n :alt=\"block.alt\"\n />\n <VideoPlayButton hover-effect />\n </a>\n <div v-else class=\"tpl:relative tpl:inline-block\" :style=\"thumbnailStyle\">\n <img\n class=\"tpl:w-full tpl:border-0\"\n :src=\"effectiveThumbnail\"\n :alt=\"block.alt\"\n />\n <VideoPlayButton />\n </div>\n </template>\n <!-- Empty state -->\n <div\n v-else\n class=\"tpl:flex tpl:min-h-[150px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-sm tpl:border-[var(--tpl-border-light)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Video\n :size=\"40\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-border-light)]\"\n />\n <span>{{ t.video.addVideo }}</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport VideoPlayButton from \"./VideoPlayButton.vue\";\nimport { useI18n, useMergeTag } from \"../../composables\";\nimport type {\n VideoBlock as VideoBlockType,\n ViewportSize,\n} from \"@templatical/types\";\nimport { getVideoThumbnail } from \"../../utils/videoThumbnail\";\nimport { useResolvedImageSrc } from \"../../composables/useImageUrlResolver\";\nimport { containsMergeTag } from \"@templatical/types\";\nimport MergeTagPreviewText from \"../MergeTagPreviewText.vue\";\nimport { Video } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: VideoBlockType;\n viewport: ViewportSize;\n}>();\n\nconst { t } = useI18n();\nconst { syntax } = useMergeTag();\n\nconst hasMergeTagUrl = computed(\n () =>\n containsMergeTag(props.block.url, syntax) ||\n containsMergeTag(props.block.thumbnailUrl, syntax),\n);\n\n// Display-only preview URLs (#415). Only the explicit, user-entered\n// thumbnailUrl / placeholderUrl go through resolveImageUrl — an auto-derived\n// provider thumbnail (getVideoThumbnail) is already a real URL. The canonical\n// values stay on the block; the resolver only changes what the canvas shows.\nconst displayThumbnailUrl = useResolvedImageSrc(() =>\n hasMergeTagUrl.value ? undefined : props.block.thumbnailUrl || undefined,\n);\nconst displayPlaceholderUrl = useResolvedImageSrc(() =>\n hasMergeTagUrl.value ? props.block.placeholderUrl : undefined,\n);\n\nconst effectiveThumbnail = computed(() => {\n if (hasMergeTagUrl.value) return null;\n if (props.block.thumbnailUrl) return displayThumbnailUrl.value ?? null;\n return getVideoThumbnail(props.block.url);\n});\n\nconst containerStyle = computed(() => ({\n textAlign: props.block.align,\n}));\n\nconst thumbnailStyle = computed(() => {\n const align = props.block.align;\n return {\n maxWidth: \"100%\",\n width: props.block.width === \"full\" ? \"100%\" : `${props.block.width}px`,\n display: \"block\",\n marginLeft: align === \"center\" || align === \"right\" ? \"auto\" : undefined,\n marginRight: align === \"center\" ? \"auto\" : undefined,\n };\n});\n\nconst mergeTagValue = computed(() =>\n containsMergeTag(props.block.url, syntax)\n ? props.block.url\n : props.block.thumbnailUrl,\n);\n</script>\n\n<template>\n <div class=\"tpl:w-full\" :style=\"containerStyle\">\n <!-- Placeholder with preview image provided -->\n <div\n v-if=\"hasMergeTagUrl && block.placeholderUrl\"\n class=\"tpl:relative tpl:inline-block\"\n :style=\"thumbnailStyle\"\n >\n <img\n class=\"tpl:w-full tpl:border-0\"\n :src=\"displayPlaceholderUrl\"\n :alt=\"block.alt\"\n />\n <VideoPlayButton />\n </div>\n <!-- Placeholder visual (no preview image) -->\n <div\n v-else-if=\"hasMergeTagUrl\"\n class=\"tpl:relative tpl:!flex tpl:min-h-[150px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-center tpl:bg-[var(--tpl-bg-elevated)]\"\n style=\"\n border-color: color-mix(in srgb, var(--tpl-primary) 40%, transparent);\n \"\n :style=\"thumbnailStyle\"\n >\n <Video\n :size=\"36\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.5\"\n />\n <span\n class=\"tpl:max-w-full tpl:truncate tpl:px-3 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-primary)]\"\n style=\"opacity: 0.7\"\n >\n <MergeTagPreviewText :text=\"mergeTagValue\" />\n </span>\n </div>\n <!-- Normal video thumbnail -->\n <template v-else-if=\"effectiveThumbnail\">\n <a\n v-if=\"block.url\"\n :href=\"block.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:group tpl:relative tpl:inline-block\"\n :style=\"thumbnailStyle\"\n @click.prevent\n >\n <img\n class=\"tpl:w-full tpl:border-0\"\n :src=\"effectiveThumbnail\"\n :alt=\"block.alt\"\n />\n <VideoPlayButton hover-effect />\n </a>\n <div v-else class=\"tpl:relative tpl:inline-block\" :style=\"thumbnailStyle\">\n <img\n class=\"tpl:w-full tpl:border-0\"\n :src=\"effectiveThumbnail\"\n :alt=\"block.alt\"\n />\n <VideoPlayButton />\n </div>\n </template>\n <!-- Empty state -->\n <div\n v-else\n class=\"tpl:flex tpl:min-h-[150px] tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-sm tpl:border-[var(--tpl-border-light)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Video\n :size=\"40\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-border-light)]\"\n />\n <span>{{ t.video.addVideo }}</span>\n </div>\n </div>\n</template>\n","import {\n computed,\n getCurrentScope,\n onScopeDispose,\n provide,\n ref,\n watch,\n type Component,\n type ComputedRef,\n type DeepReadonly,\n type Ref,\n} from \"vue\";\nimport { useEventListener } from \"@vueuse/core\";\nimport { registerEditorInstance } from \"../utils/activeEditorTracker\";\nimport {\n useHistory,\n useHistoryInterceptor,\n useBlockActions,\n useAutoSave,\n useConditionPreview,\n} from \"@templatical/core\";\nimport type {\n UseHistoryReturn,\n UseBlockActionsReturn,\n UseConditionPreviewReturn,\n UseAutoSaveReturn,\n} from \"@templatical/core\";\nimport type {\n Block,\n BlockDefaults,\n ColorsConfig,\n CustomBlockDefinition,\n DisplayConditionsConfig,\n LogicTagsConfig,\n MergeTagsConfig,\n TemplateContent,\n TemplateDefaults,\n TemplateSettings,\n ThemeOverrides,\n UiTheme,\n ResolvePreview,\n ViewportSize,\n} from \"@templatical/types\";\nimport { hasMergeTagSamples, resolveSyntax } from \"@templatical/types\";\nimport {\n usePreviewResolution,\n type UsePreviewResolutionReturn,\n} from \"./usePreviewResolution\";\nimport type { Translations } from \"../i18n\";\nimport type { OnRequestMedia } from \"../index\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\nimport {\n TRANSLATIONS_KEY,\n EDITOR_KEY,\n HISTORY_KEY,\n BLOCK_ACTIONS_KEY,\n APPLIES_CONDITION_FILTER_KEY,\n CONDITION_PREVIEW_KEY,\n FONTS_MANAGER_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n BLOCK_DEFAULTS_KEY,\n BLOCK_REGISTRY_KEY,\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n PALETTE_BLOCKS_KEY,\n HTML_BLOCK_PREVIEW_KEY,\n COLORS_KEY,\n CUSTOM_BLOCK_STYLESHEETS_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SYNTAX_KEY,\n MERGE_TAG_AUTOCOMPLETE_KEY,\n MERGE_TAG_PICKER_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n PREVIEW_RESOLUTION_KEY,\n RESOLVE_PREVIEW_KEY,\n ON_REQUEST_MERGE_TAG_KEY,\n LOGIC_TAGS_KEY,\n LOGIC_PAIRS_KEY,\n LOGIC_TAG_PICKER_KEY,\n ON_REQUEST_LOGIC_TAG_KEY,\n ON_REQUEST_MEDIA_KEY,\n IMAGE_URL_RESOLVER_KEY,\n DISPLAY_CONDITIONS_KEY,\n ALLOW_CUSTOM_CONDITIONS_KEY,\n CAPABILITIES_KEY,\n KEYBOARD_REORDER_KEY,\n TEMPLATE_LINT_KEY,\n EDITOR_ROOT_KEY,\n POPOVER_ROOT_KEY,\n} from \"../keys\";\nimport {\n createImageUrlResolver,\n type ResolveImageUrl,\n} from \"./useImageUrlResolver\";\nimport { useMergeTagPicker } from \"./useMergeTagPicker\";\nimport { useLogicTagPicker } from \"./useLogicTagPicker\";\nimport {\n isLintFullyDisabled,\n useTemplateLint,\n type UseTemplateLintReturn,\n} from \"./useTemplateLint\";\nimport type { LintOptions } from \"@templatical/quality\";\nimport type { UseFontsReturn } from \"./useFonts\";\nimport { useI18n, type UseI18nReturn } from \"./useI18n\";\nimport {\n useKeyboardReorder,\n type UseKeyboardReorderReturn,\n} from \"./useKeyboardReorder\";\nimport { useUiTheme } from \"./useUiTheme\";\nimport { useThemeStyles } from \"./useThemeStyles\";\nimport {\n useBlockRegistry,\n type UseBlockRegistryReturn,\n} from \"./useBlockRegistry\";\nimport { useCustomBlockStylesheets } from \"./useCustomBlockStylesheets\";\nimport { registerBuiltInBlocks } from \"../utils/registerBuiltInBlocks\";\nimport {\n resolveHtmlBlockPreview,\n type HtmlBlockPreviewConfig,\n} from \"../utils/resolveHtmlBlockPreview\";\nimport { resolveColorsConfig } from \"../utils/resolveColorsConfig\";\nimport { collectOffPaletteDefaults } from \"../utils/collectOffPaletteDefaults\";\nimport { collectColorFieldIssues } from \"../utils/collectColorFieldIssues\";\nimport { logger } from \"../utils/logger\";\nimport { handleEditorKeydown } from \"../utils/keyboardShortcuts\";\n\n// Block components — shared between OSS and Cloud editors\nimport { defineAsyncComponent } from \"vue\";\nimport ButtonBlock from \"../components/blocks/ButtonBlock.vue\";\n// Cloud-only at insertion; lazy so OSS users without countdown content pay nothing.\nconst CountdownBlockComponent = defineAsyncComponent(\n () => import(\"../components/blocks/CountdownBlock.vue\"),\n);\nimport CustomBlockComponent from \"../components/blocks/CustomBlock.vue\";\nimport DividerBlock from \"../components/blocks/DividerBlock.vue\";\nimport HtmlBlock from \"../components/blocks/HtmlBlock.vue\";\nimport ImageBlock from \"../components/blocks/ImageBlock.vue\";\nimport MenuBlock from \"../components/blocks/MenuBlock.vue\";\nimport ParagraphBlock from \"../components/blocks/ParagraphBlock.vue\";\nimport SectionBlock from \"../components/blocks/SectionBlock.vue\";\nimport SocialIconsBlock from \"../components/blocks/SocialIconsBlock.vue\";\nimport SpacerBlock from \"../components/blocks/SpacerBlock.vue\";\nimport TableBlock from \"../components/blocks/TableBlock.vue\";\nimport TitleBlock from \"../components/blocks/TitleBlock.vue\";\nimport VideoBlock from \"../components/blocks/VideoBlock.vue\";\n\nconst BLOCK_COMPONENT_MAP: Record<string, Component> = {\n section: SectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n video: VideoBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n countdown: CountdownBlockComponent,\n};\n\n/**\n * Minimal editor interface shared by both OSS and Cloud UseEditorReturn.\n * Cloud adds extra methods (create, load, save, etc.) that useEditorCore doesn't need.\n */\nexport interface BaseEditorReturn {\n state: DeepReadonly<{\n content: TemplateContent;\n selectedBlockId: string | null;\n viewport: ViewportSize;\n darkMode: boolean;\n previewMode: boolean;\n uiTheme: UiTheme;\n isDirty: boolean;\n template?: { id: string } | null;\n isLoading?: boolean;\n isSaving?: boolean;\n }>;\n content: Ref<TemplateContent>;\n selectedBlock: Ref<Block | null>;\n savedBlockIds?: Ref<Set<string>>;\n setContent: (content: TemplateContent, markDirty?: boolean) => void;\n selectBlock: (blockId: string | null) => void;\n setViewport: (viewport: ViewportSize) => void;\n setDarkMode: (darkMode: boolean) => void;\n setPreviewMode: (previewMode: boolean) => void;\n setUiTheme: (theme: UiTheme) => void;\n updateBlock: (blockId: string, updates: Partial<Block>) => void;\n updateSettings: (updates: Partial<TemplateSettings>) => void;\n addBlock: (\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n index?: number,\n ) => void;\n removeBlock: (blockId: string) => void;\n moveBlock: (\n blockId: string,\n newIndex: number,\n targetSectionId?: string,\n columnIndex?: number,\n ) => void;\n isBlockLocked: (blockId: string) => boolean;\n markDirty: () => void;\n findBlockLocation: (blockId: string) => {\n targetSectionId?: string;\n columnIndex?: number;\n index: number;\n } | null;\n}\n\nexport interface UseEditorCoreOptions {\n editor: BaseEditorReturn;\n\n config: {\n uiTheme?: UiTheme;\n theme?: ThemeOverrides;\n blockDefaults?: BlockDefaults;\n templateDefaults?: TemplateDefaults;\n customBlocks?: CustomBlockDefinition[];\n paletteBlocks?: string[];\n htmlBlockPreview?: HtmlBlockPreviewConfig;\n colors?: ColorsConfig;\n mergeTags?: MergeTagsConfig;\n logicTags?: LogicTagsConfig;\n displayConditions?: DisplayConditionsConfig;\n onRequestMedia?: OnRequestMedia | null;\n resolvePreview?: ResolvePreview;\n resolveImageUrl?: ResolveImageUrl | null;\n onSave?: () => void;\n lint?: LintOptions;\n };\n\n translations: Translations;\n fontsManager: UseFontsReturn;\n\n /** Extra options passed to useHistory (Cloud passes isRemoteOperation) */\n historyOptions?: { isRemoteOperation?: () => boolean };\n\n /** Auto-save configuration. Pass null to skip auto-save. */\n autoSaveOptions?: {\n onChange: () => void | Promise<void>;\n debounce?: number;\n enabled?: () => boolean;\n } | null;\n\n /** Extra CSS custom properties merged into theme styles (Cloud uses for drop-zone text) */\n themeExtraStyles?: () => Record<string, string>;\n\n /** Extra keyboard shortcut hooks (Cloud passes onBeforeUndo for collab warning) */\n keyboardOptions?: { onBeforeUndo?: () => void };\n\n /** Cloud capabilities exposed to OSS components. Empty in OSS mode. */\n capabilities?: EditorCapabilities;\n\n /**\n * Effective DOM root — `Document` in light-DOM mode, `ShadowRoot` when\n * mounted with `shadowDom: true`. Provided via `EDITOR_ROOT_KEY` so\n * shadow-DOM-aware composables (focus trap, popover mount targets, etc.)\n * can read it without reaching for the global `document`. Defaults to\n * `document` if omitted — preserves current light-DOM behavior.\n */\n editorRoot?: Document | ShadowRoot;\n\n /**\n * Ref pointing at this editor's outer `.tpl` container. When two editors\n * mount on the same page, the document-level keydown listener installed\n * by each `useEditorCore` would otherwise fire on every instance. Passing\n * the container ref lets the active-editor tracker route a keystroke to\n * the instance the user most recently interacted with. Omit for\n * single-editor pages — single-instance mode skips the routing check.\n */\n containerEl?: Ref<HTMLElement | null>;\n}\n\nexport interface UseEditorCoreReturn {\n t: Translations;\n format: UseI18nReturn[\"format\"];\n history: UseHistoryReturn;\n blockActions: UseBlockActionsReturn;\n conditionPreview: UseConditionPreviewReturn;\n autoSave: UseAutoSaveReturn | null;\n resolvedTheme: ComputedRef<string>;\n themeStyles: ComputedRef<Record<string, string>>;\n themeOverrides: Ref<ThemeOverrides>;\n registry: UseBlockRegistryReturn;\n keyboardReorder: UseKeyboardReorderReturn;\n templateLint: UseTemplateLintReturn | null;\n /**\n * Ref bound to the `<div class=\"tpl-popover-root\" />` that the editor's\n * top-level template must render. Provided via `POPOVER_ROOT_KEY` so\n * popovers/toolbars/modals teleport inside the editor's effective DOM\n * root (shadow-aware) instead of escaping to `document.body`.\n */\n popoverRoot: Ref<HTMLElement | null>;\n /**\n * Which merge-tag view the previews use: `true` for Sample, `false` for\n * Label. Returned so the editors can bind the toggle they render in preview\n * mode; block components must read `USE_MERGE_TAG_SAMPLES_KEY` instead, which\n * folds in the surface's editing guard.\n */\n mergeTagSampleMode: Ref<boolean>;\n /** The `resolvePreview` seam, shared by every preview surface. */\n previewResolution: UsePreviewResolutionReturn;\n /**\n * Whether the hand-toggled display-condition filter applies. False only while\n * previewing with a configured resolver, which owns what the preview shows.\n * The editors gate the restore button on it so it can't outlive the filter.\n */\n appliesConditionFilter: ComputedRef<boolean>;\n registerCustomBlocks: (definitions: CustomBlockDefinition[]) => void;\n destroy: () => void;\n}\n\nexport function useEditorCore(\n options: UseEditorCoreOptions,\n): UseEditorCoreReturn {\n const { editor, config, translations, fontsManager } = options;\n\n // --- i18n ---\n const { t, format } = useI18n(translations);\n\n // --- UI Theme ---\n editor.setUiTheme(config.uiTheme ?? \"auto\");\n const uiThemeRef = computed(() => editor.state.uiTheme);\n const { resolvedTheme } = useUiTheme(uiThemeRef);\n\n // --- Theme styles ---\n const themeOverrides = ref<ThemeOverrides>(config.theme ?? {});\n const { themeStyles } = useThemeStyles({\n themeOverrides,\n resolvedTheme,\n extraStyles: options.themeExtraStyles,\n });\n\n // --- History ---\n const history = useHistory({\n content: editor.content,\n setContent: (content: TemplateContent, markDirty?: boolean) =>\n editor.setContent(content, markDirty),\n ...options.historyOptions,\n });\n useHistoryInterceptor(editor, history);\n\n // --- Block actions ---\n const blockActions = useBlockActions({\n addBlock: editor.addBlock,\n removeBlock: editor.removeBlock,\n updateBlock: editor.updateBlock,\n selectBlock: editor.selectBlock,\n findBlockLocation: editor.findBlockLocation,\n blockDefaults: config.blockDefaults,\n });\n\n // --- Condition preview ---\n const conditionPreview = useConditionPreview(editor);\n\n // --- Auto-save ---\n const autoSave =\n options.autoSaveOptions !== null && options.autoSaveOptions !== undefined\n ? useAutoSave({\n content: editor.content,\n isDirty: () => editor.state.isDirty,\n ...options.autoSaveOptions,\n })\n : null;\n\n // Pause/resume auto-save during history navigation (undo/redo)\n let stopNavigatingWatch: (() => void) | null = null;\n if (autoSave) {\n stopNavigatingWatch = watch(history.isNavigating, (navigating) => {\n if (navigating) {\n autoSave.pause();\n } else {\n autoSave.resume();\n }\n });\n }\n\n // --- Keyboard reorder (GitHub-style lift/move/drop via keyboard) ---\n const keyboardReorder = useKeyboardReorder(\n editor,\n { t, format },\n config.customBlocks ?? [],\n );\n\n // --- Color palette ---\n // Editor-wide color-picker palette. Resolved here, ahead of the block\n // registry, because custom-block color fields are audited against it at\n // definition-registration time (below); it is provided to the component tree\n // further down. Custom-block color fields layer their own presets over this.\n // The resolver is pure, so the warning for a nonsensical config\n // (`allowCustom: false` with no presets) is emitted here — once, at the\n // config surface that owns it.\n const resolvedColors = resolveColorsConfig(config.colors);\n if (resolvedColors.allowCustomIgnored) {\n logger.warn(\n \"config.colors.allowCustom: false is ignored without presets — \" +\n \"keeping the color wheel and hex input so a color can still be chosen.\",\n );\n }\n if (resolvedColors.invalidPresets.length > 0) {\n logger.warn(\n \"config.colors.presets skipped invalid entries: \" +\n `${resolvedColors.invalidPresets.join(\", \")} — presets must be hex ` +\n \"colors (#rgb or #rrggbb).\",\n );\n }\n // Off-palette default-colour audit. Only meaningful when custom colours are\n // locked (`allowCustom: false`): a block/template default outside the palette\n // then paints new blocks a colour no picker can reselect. Warn — do NOT\n // auto-seed the defaults from the palette; the maintainer flagged that as a\n // separate discussion. Pure util, consumer-owned warning — same split as the\n // resolver above.\n if (resolvedColors.allowCustom === false) {\n const offPaletteDefaults = collectOffPaletteDefaults(\n resolvedColors.presets,\n config.blockDefaults,\n config.templateDefaults,\n );\n if (offPaletteDefaults.length > 0) {\n logger.warn(\n \"config.colors locks custom colours, but these block/template \" +\n \"default colours fall outside colors.presets: \" +\n `${offPaletteDefaults.join(\", \")}. New blocks start on a colour the ` +\n \"palette can't reselect — set blockDefaults / templateDefaults from \" +\n \"the same palette.\",\n );\n }\n }\n\n // --- Block registry ---\n const registry = useBlockRegistry();\n registerBuiltInBlocks(registry, BLOCK_COMPONENT_MAP);\n\n // Per-field color configs are audited ONCE per definition, here at\n // registration — not in `ColorField.vue`, which remounts on every block\n // selection and renders once per repeater item. Keyed by\n // `<block type>:<field path>:<kind>` so a re-registration (Cloud defers\n // registration to `initialize()`) can't repeat a message either — the same\n // once-guard `Sidebar.vue` uses for unknown `paletteBlocks` entries.\n const warnedColorFieldIssues = new Set<string>();\n\n function registerCustomBlocks(definitions: CustomBlockDefinition[]): void {\n for (const definition of definitions) {\n registry.registerCustom(definition, CustomBlockComponent);\n for (const issue of collectColorFieldIssues(definition, resolvedColors)) {\n if (warnedColorFieldIssues.has(issue.id)) continue;\n warnedColorFieldIssues.add(issue.id);\n logger.warn(issue.message);\n }\n }\n }\n\n // Register custom blocks provided at init time (Cloud may defer this to initialize())\n if (config.customBlocks?.length) {\n registerCustomBlocks(config.customBlocks);\n }\n\n // --- Keyboard shortcuts ---\n // Register this editor with the page-wide active-instance tracker. On\n // single-editor pages `isActive()` is always true. With two editors\n // mounted, `claim()` runs on pointerdown inside this editor's\n // container, and `handleKeyboard` only forwards when this is the\n // active instance — otherwise a single Cmd+Z would fire both editors'\n // undo handlers.\n const instanceHandle = registerEditorInstance();\n if (getCurrentScope()) {\n onScopeDispose(instanceHandle.dispose);\n }\n\n if (options.containerEl) {\n const containerEl = options.containerEl;\n useEventListener(\n document,\n \"pointerdown\",\n (e: PointerEvent) => {\n const target = containerEl.value;\n if (!target) return;\n const path = e.composedPath?.() ?? [];\n if (path.includes(target)) instanceHandle.claim();\n },\n { capture: true },\n );\n }\n\n function handleKeyboard(e: KeyboardEvent): void {\n if (!instanceHandle.isActive()) return;\n handleEditorKeydown(e, {\n history,\n selectBlock: (id) => editor.selectBlock(id),\n getSelectedBlockId: () => editor.state.selectedBlockId,\n removeBlock: (id) => editor.removeBlock(id),\n onSave: config.onSave,\n onBeforeUndo: options.keyboardOptions?.onBeforeUndo,\n // Read off the capability rather than a new option, so neither editor has\n // to know about the pick session to get Escape/Enter working.\n isPicking: () =>\n options.capabilities?.savedBlocks?.isPicking.value === true,\n onConfirmPick: () => options.capabilities?.savedBlocks?.confirmPicking(),\n onCancelPick: () => options.capabilities?.savedBlocks?.cancelPicking(),\n });\n }\n\n // Attach the global keydown listener at the `document` level even in\n // shadow mode. Non-focusable elements (most blocks are bare `<div>`)\n // don't capture focus on click, so after a user clicks a block their\n // active element stays on `document.body` — a subsequent keystroke\n // never reaches the shadow root, only `document`. Listening at\n // `document` keeps Escape / Cmd+Z / Delete shortcuts working in both\n // modes; multi-editor disambiguation is handled inside the listener\n // via `activeEditorTracker`.\n useEventListener(document, \"keydown\", handleKeyboard);\n\n // --- Popover mount ---\n // Ref bound by the editor template to `<div class=\"tpl-popover-root\" />`.\n // Null until the template mounts; consumers guard with `v-if=\"popoverRoot\"`.\n const popoverRoot = ref<HTMLElement | null>(null);\n\n // --- Provides (19 shared keys) ---\n provide(EDITOR_ROOT_KEY, options.editorRoot ?? document);\n provide(POPOVER_ROOT_KEY, popoverRoot);\n provide(TRANSLATIONS_KEY, translations);\n provide(EDITOR_KEY, editor);\n provide(HISTORY_KEY, history);\n provide(BLOCK_ACTIONS_KEY, blockActions);\n provide(CONDITION_PREVIEW_KEY, conditionPreview);\n provide(FONTS_MANAGER_KEY, fontsManager);\n provide(THEME_STYLES_KEY, themeStyles);\n provide(UI_THEME_KEY, resolvedTheme);\n provide(BLOCK_DEFAULTS_KEY, config.blockDefaults);\n provide(BLOCK_REGISTRY_KEY, registry);\n provide(CUSTOM_BLOCK_DEFINITIONS_KEY, config.customBlocks ?? []);\n provide(PALETTE_BLOCKS_KEY, config.paletteBlocks);\n provide(\n HTML_BLOCK_PREVIEW_KEY,\n resolveHtmlBlockPreview(config.htmlBlockPreview),\n );\n // Editor-wide color-picker palette (resolved + audited above, ahead of the\n // block registry).\n provide(COLORS_KEY, resolvedColors);\n // Reactive deduped list of custom-block stylesheets currently in use. The\n // `<CustomBlockStylesheets>` component reads this and renders `<style>` tags\n // into the editor root so authored CSS previews live in the canvas. The\n // renderer's `getCustomBlockStylesheet` resolver covers the export path.\n provide(\n CUSTOM_BLOCK_STYLESHEETS_KEY,\n useCustomBlockStylesheets(editor.content, registry),\n );\n\n const mergeTagSyntax = resolveSyntax(config.mergeTags?.syntax);\n provide(MERGE_TAGS_KEY, config.mergeTags?.tags ?? []);\n provide(MERGE_TAG_SYNTAX_KEY, mergeTagSyntax);\n provide(ON_REQUEST_MERGE_TAG_KEY, config.mergeTags?.onRequest ?? null);\n provide(MERGE_TAG_AUTOCOMPLETE_KEY, config.mergeTags?.autocomplete !== false);\n\n // Which merge-tag view the previews use. Session state only — deliberately\n // not persisted and not a config option: it is a per-look choice.\n //\n // Defaults to Sample **only when a sample exists to show**. With none\n // configured the two views would render identically, so defaulting to Sample\n // would mean an inert toggle and — worse — labels shown without their usual\n // highlight. Every consumer who never sets `sample` therefore sees exactly\n // the previous behaviour, and no toggle at all.\n //\n // Only preview surfaces act on it; `Canvas.vue` gates it behind its own\n // `previewMode`, so editing never sees substituted values. See\n // USE_MERGE_TAG_SAMPLES_KEY.\n const mergeTagSampleMode = ref(\n hasMergeTagSamples(config.mergeTags?.tags ?? []),\n );\n provide(MERGE_TAG_SAMPLE_MODE_KEY, mergeTagSampleMode);\n\n // The `resolvePreview` seam. Instantiated unconditionally so the injection is\n // always present, but it never calls the hook unless one is configured *and* a\n // preview is showing — `isActive` is the editor's own preview mode.\n //\n // The test-email dialog has its own recipient, so it re-runs resolution\n // through its own instance rather than this one; see `TestEmailModal`.\n const previewResolution = usePreviewResolution({\n resolvePreview: config.resolvePreview,\n getContent: () => editor.content.value,\n isActive: () => editor.state.previewMode,\n });\n provide(PREVIEW_RESOLUTION_KEY, previewResolution);\n provide(RESOLVE_PREVIEW_KEY, config.resolvePreview);\n\n // Whether the hand-toggled display-condition filter applies. It always does\n // while editing — that is the feature — and while previewing *without* a\n // resolver, which is the simulate-then-preview flow.\n //\n // A configured resolver owns the preview, exactly as it does for merge-tag\n // samples: it has evaluated every condition against real data, so a manual\n // hide layered on top would veto the answer that was asked for, silently and\n // with the restore button still sitting there claiming blocks are hidden.\n //\n // Suppressed rather than `reset()` so the simulation survives leaving the\n // preview: clicking a view toggle must not discard work, least of all when\n // the resolve then fails and the *unresolved* template is what renders.\n //\n // Gated on `isConfigured` rather than a landed result, for the same reason\n // `supersedesSamples` is — keying off `resolved !== null` would flicker the\n // filter and the restore button back on for the duration of every resolve.\n const appliesConditionFilter = computed(\n () => !(editor.state.previewMode && previewResolution.isConfigured),\n );\n provide(APPLIES_CONDITION_FILTER_KEY, appliesConditionFilter);\n\n // Built-in merge tag picker singleton. Always instantiated so\n // `useMergeTag.requestMergeTag()` can fall through to it whenever\n // static `tags` are configured without an `onRequest` callback. Cost\n // is a ref + ref + closure per editor — negligible.\n const mergeTagPicker = useMergeTagPicker();\n provide(MERGE_TAG_PICKER_KEY, mergeTagPicker);\n\n // Standalone logic tags — separate from merge tags. Native highlighting\n // (LogicMergeTagNode) is always on; these power the dedicated logic picker.\n provide(LOGIC_TAGS_KEY, config.logicTags?.tags ?? []);\n provide(LOGIC_PAIRS_KEY, config.logicTags?.pairs ?? []);\n provide(ON_REQUEST_LOGIC_TAG_KEY, config.logicTags?.onRequest ?? null);\n provide(LOGIC_TAG_PICKER_KEY, useLogicTagPicker());\n\n provide(ON_REQUEST_MEDIA_KEY, config.onRequestMedia ?? null);\n\n // Display-only image src resolver (#415). Created once per editor so the\n // per-src cache spans all image-displaying blocks of the instance.\n provide(\n IMAGE_URL_RESOLVER_KEY,\n config.resolveImageUrl\n ? createImageUrlResolver(config.resolveImageUrl)\n : null,\n );\n\n provide(DISPLAY_CONDITIONS_KEY, config.displayConditions?.conditions ?? []);\n provide(\n ALLOW_CUSTOM_CONDITIONS_KEY,\n config.displayConditions?.allowCustom ?? false,\n );\n\n // Default empty capabilities for OSS mode.\n // CloudEditor overrides this via provide() after cloud composables are ready.\n provide(CAPABILITIES_KEY, options.capabilities ?? {});\n\n provide(KEYBOARD_REORDER_KEY, keyboardReorder);\n\n // --- Template lint (accessibility + structure) ---\n // editor.updateBlock / updateSettings / removeBlock are wrapped by\n // useHistoryInterceptor, so each fix patch lands as its own undo entry.\n const templateLint: UseTemplateLintReturn | null = isLintFullyDisabled(\n config.lint,\n )\n ? null\n : useTemplateLint({\n content: editor.content,\n options: config.lint ?? {},\n updateBlock: editor.updateBlock,\n updateSettings: editor.updateSettings,\n removeBlock: editor.removeBlock,\n });\n provide(TEMPLATE_LINT_KEY, templateLint);\n\n // --- Cleanup ---\n function destroy(): void {\n stopNavigatingWatch?.();\n templateLint?.destroy();\n autoSave?.destroy();\n history.destroy();\n }\n\n return {\n t,\n format,\n history,\n blockActions,\n conditionPreview,\n autoSave,\n resolvedTheme,\n themeStyles,\n themeOverrides,\n registry,\n keyboardReorder,\n templateLint,\n popoverRoot,\n mergeTagSampleMode,\n previewResolution,\n appliesConditionFilter,\n registerCustomBlocks,\n destroy,\n };\n}\n"],"mappings":"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"}