@templatical/editor 0.24.0 → 0.24.1

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 (198) hide show
  1. package/dist/{AiChatSidebar-Nq-jrGaf.js → AiChatSidebar-X4viKtvs.js} +14 -14
  2. package/dist/{AiFeatureMenu-tg_zMKMK.js → AiFeatureMenu-DDAamRaA.js} +7 -7
  3. package/dist/{BlockIssueBadge-Bd94kIiw.js → BlockIssueBadge-Qxrl7Ujl.js} +5 -5
  4. package/dist/{BlockPreviewCanvas-B34aMhHe.js → BlockPreviewCanvas-Cf-kxbEr.js} +3 -3
  5. package/dist/{CloudEditor-DjN_9Yso.js → CloudEditor-JIzhj1Xq.js} +26 -26
  6. package/dist/{CollaboratorBar-Bhazzcg-.js → CollaboratorBar-tC7AACTb.js} +4 -4
  7. package/dist/{ColorPicker-DXGrcJxJ.js → ColorPicker--_2PbzFk.js} +7 -9
  8. package/dist/{CommentsSidebar-ku1kt10u.js → CommentsSidebar-Ci2Ov8FG.js} +15 -15
  9. package/dist/{CountdownBlock-CN9Lc23W.js → CountdownBlock-aZIKfBWY.js} +3 -3
  10. package/dist/{CountdownToolbar-VEsIyKV6.js → CountdownToolbar-DHp6T0Q_.js} +4 -4
  11. package/dist/DesignReferenceSidebar-DQsC-3hB.js +264 -0
  12. package/dist/{IssuesPanel-CEfzOpS_.js → IssuesPanel-CoOfib44.js} +8 -8
  13. package/dist/{LoadingTrack-KIcREPlS.js → LoadingTrack-XK_eeUdq.js} +1 -1
  14. package/dist/{MergeTagModeToggle-C3V2xGAg.js → MergeTagModeToggle-CiFiSV0C.js} +6 -6
  15. package/dist/{MergeTagSuggestion-Dn8ww9nV.js → MergeTagSuggestion-D0OHRv7L.js} +8 -12
  16. package/dist/{NumberWithSuffix-BGocZkj9.js → NumberWithSuffix-CXFbUqsx.js} +2 -2
  17. package/dist/{ParagraphEditor-D-fz0kUb.js → ParagraphEditor-Cy-bpEjI.js} +44 -44
  18. package/dist/{RichTextEditorContent-DydrqFts.js → RichTextEditorContent-CYJko6id.js} +8 -8
  19. package/dist/{SaveBlockDialog-BDDFbAWF.js → SaveBlockDialog-CNTSaMEa.js} +10 -10
  20. package/dist/{SavedBlocksBrowserModal-Djik_fJh.js → SavedBlocksBrowserModal-CvxS5-YX.js} +11 -11
  21. package/dist/{SavedBlocksPanels-CxemyPxL.js → SavedBlocksPanels-CNdUxS89.js} +2 -2
  22. package/dist/{SavedBlocksPickBar-Dlhipmob.js → SavedBlocksPickBar-BvXKhblL.js} +4 -4
  23. package/dist/{SnapshotHistory-DPbM4ons.js → SnapshotHistory-BqPSIV9L.js} +10 -10
  24. package/dist/{TemplateScoringPanel-BFYgweDX.js → TemplateScoringPanel-CMEuz9yF.js} +15 -15
  25. package/dist/{TestEmailModal-IZfwgya-.js → TestEmailModal-BDhnnCBx.js} +10 -10
  26. package/dist/{TestEmailPanel-QEEx_PkN.js → TestEmailPanel--RwnwLn2.js} +2 -2
  27. package/dist/{TitleEditor-BB4YlIBF.js → TitleEditor-F7E_pU9H.js} +12 -12
  28. package/dist/TplModal-BIiS24Of.js +49 -0
  29. package/dist/{blockTypeIcons-Wzd7W_2x.js → blockTypeIcons-C6PnLMp7.js} +26 -26
  30. package/dist/bundle-stats.json +6 -6
  31. package/dist/cdn/chunks/{AiChatSidebar-CN-0DIuA.js → AiChatSidebar-UbF3ePzR.js} +8 -8
  32. package/dist/cdn/chunks/{AiChatSidebar-CN-0DIuA.js.map → AiChatSidebar-UbF3ePzR.js.map} +1 -1
  33. package/dist/cdn/chunks/{AiFeatureMenu-B5pV6Iux.js → AiFeatureMenu-OIwoJzHG.js} +6 -6
  34. package/dist/cdn/chunks/{AiFeatureMenu-B5pV6Iux.js.map → AiFeatureMenu-OIwoJzHG.js.map} +1 -1
  35. package/dist/cdn/chunks/{BlockIssueBadge-aanOjeyi.js → BlockIssueBadge-Bd2EuT1C.js} +5 -5
  36. package/dist/cdn/chunks/{BlockIssueBadge-aanOjeyi.js.map → BlockIssueBadge-Bd2EuT1C.js.map} +1 -1
  37. package/dist/cdn/chunks/{BlockPreviewCanvas-Dc4F4HzY.js → BlockPreviewCanvas-C-ut8GkJ.js} +5 -5
  38. package/dist/cdn/chunks/{BlockPreviewCanvas-Dc4F4HzY.js.map → BlockPreviewCanvas-C-ut8GkJ.js.map} +1 -1
  39. package/dist/cdn/chunks/{CloudEditor-CeKaWWO5.js → CloudEditor-D87qsqbZ.js} +20 -20
  40. package/dist/cdn/chunks/{CloudEditor-CeKaWWO5.js.map → CloudEditor-D87qsqbZ.js.map} +1 -1
  41. package/dist/cdn/chunks/{CollaboratorBar-CEdKr0A2.js → CollaboratorBar-Br4RvNrA.js} +5 -5
  42. package/dist/cdn/chunks/{CollaboratorBar-CEdKr0A2.js.map → CollaboratorBar-Br4RvNrA.js.map} +1 -1
  43. package/dist/cdn/chunks/{ColorPicker-BckwEfcN.js → ColorPicker-B-ymPPtU.js} +8 -10
  44. package/dist/cdn/chunks/{ColorPicker-BckwEfcN.js.map → ColorPicker-B-ymPPtU.js.map} +1 -1
  45. package/dist/cdn/chunks/{CommentsSidebar-6PfpJKh-.js → CommentsSidebar-Tv_WTcJv.js} +6 -6
  46. package/dist/cdn/chunks/{CommentsSidebar-6PfpJKh-.js.map → CommentsSidebar-Tv_WTcJv.js.map} +1 -1
  47. package/dist/cdn/chunks/{CountdownBlock-CoBmvQKv.js → CountdownBlock-cEcXSK91.js} +4 -4
  48. package/dist/cdn/chunks/{CountdownBlock-CoBmvQKv.js.map → CountdownBlock-cEcXSK91.js.map} +1 -1
  49. package/dist/cdn/chunks/{CountdownToolbar-CdV8RTQu.js → CountdownToolbar-CLp3_ebk.js} +5 -5
  50. package/dist/cdn/chunks/{CountdownToolbar-CdV8RTQu.js.map → CountdownToolbar-CLp3_ebk.js.map} +1 -1
  51. package/dist/cdn/chunks/DesignReferenceSidebar-YeqxSsxO.js +223 -0
  52. package/dist/cdn/chunks/{DesignReferenceSidebar-BjlaP69l.js.map → DesignReferenceSidebar-YeqxSsxO.js.map} +1 -1
  53. package/dist/cdn/chunks/{IssuesPanel-CkkEnhxZ.js → IssuesPanel-BhsvC3XQ.js} +5 -5
  54. package/dist/cdn/chunks/{IssuesPanel-CkkEnhxZ.js.map → IssuesPanel-BhsvC3XQ.js.map} +1 -1
  55. package/dist/cdn/chunks/{LoadingTrack-D-CW2tUa.js → LoadingTrack-G2tuXqCJ.js} +2 -2
  56. package/dist/cdn/chunks/{LoadingTrack-D-CW2tUa.js.map → LoadingTrack-G2tuXqCJ.js.map} +1 -1
  57. package/dist/cdn/chunks/{MergeTagModeToggle-DmtR300G.js → MergeTagModeToggle-DrdxQA0K.js} +6 -6
  58. package/dist/cdn/chunks/{MergeTagModeToggle-DmtR300G.js.map → MergeTagModeToggle-DrdxQA0K.js.map} +1 -1
  59. package/dist/cdn/chunks/{MergeTagSuggestion-Cvlkw_PM.js → MergeTagSuggestion-Ces5Ua78.js} +3 -3
  60. package/dist/cdn/chunks/{MergeTagSuggestion-Cvlkw_PM.js.map → MergeTagSuggestion-Ces5Ua78.js.map} +1 -1
  61. package/dist/cdn/chunks/{NumberWithSuffix-BOcMN26z.js → NumberWithSuffix-BK_Xh4w4.js} +3 -3
  62. package/dist/cdn/chunks/{NumberWithSuffix-BOcMN26z.js.map → NumberWithSuffix-BK_Xh4w4.js.map} +1 -1
  63. package/dist/cdn/chunks/{ParagraphEditor-CX1yks4N.js → ParagraphEditor-DKz4vTWB.js} +131 -131
  64. package/dist/cdn/chunks/{ParagraphEditor-CX1yks4N.js.map → ParagraphEditor-DKz4vTWB.js.map} +1 -1
  65. package/dist/cdn/chunks/{RichTextEditorContent-z9s7qxs3.js → RichTextEditorContent-CJ2WVicQ.js} +7 -7
  66. package/dist/cdn/chunks/{RichTextEditorContent-z9s7qxs3.js.map → RichTextEditorContent-CJ2WVicQ.js.map} +1 -1
  67. package/dist/cdn/chunks/{SaveBlockDialog-D2CtW4lC.js → SaveBlockDialog-DTemkKPX.js} +8 -8
  68. package/dist/cdn/chunks/{SaveBlockDialog-D2CtW4lC.js.map → SaveBlockDialog-DTemkKPX.js.map} +1 -1
  69. package/dist/cdn/chunks/{SavedBlocksBrowserModal-DdUKJzzF.js → SavedBlocksBrowserModal-DbMaIbeH.js} +9 -9
  70. package/dist/cdn/chunks/{SavedBlocksBrowserModal-DdUKJzzF.js.map → SavedBlocksBrowserModal-DbMaIbeH.js.map} +1 -1
  71. package/dist/cdn/chunks/{SavedBlocksPanels-1kVbqdpc.js → SavedBlocksPanels-CUffuDAC.js} +3 -3
  72. package/dist/cdn/chunks/{SavedBlocksPanels-1kVbqdpc.js.map → SavedBlocksPanels-CUffuDAC.js.map} +1 -1
  73. package/dist/cdn/chunks/{SavedBlocksPickBar--noDJIyN.js → SavedBlocksPickBar-D7SmNz2n.js} +5 -5
  74. package/dist/cdn/chunks/{SavedBlocksPickBar--noDJIyN.js.map → SavedBlocksPickBar-D7SmNz2n.js.map} +1 -1
  75. package/dist/cdn/chunks/{SnapshotHistory-DFuNJRc9.js → SnapshotHistory-m5Z8bTej.js} +7 -7
  76. package/dist/cdn/chunks/{SnapshotHistory-DFuNJRc9.js.map → SnapshotHistory-m5Z8bTej.js.map} +1 -1
  77. package/dist/cdn/chunks/{TemplateScoringPanel-DG1dAjQk.js → TemplateScoringPanel-DgRU5fty.js} +6 -6
  78. package/dist/cdn/chunks/{TemplateScoringPanel-DG1dAjQk.js.map → TemplateScoringPanel-DgRU5fty.js.map} +1 -1
  79. package/dist/cdn/chunks/{TestEmailModal-CiPfOn2v.js → TestEmailModal-Ce8-vzGh.js} +9 -9
  80. package/dist/cdn/chunks/{TestEmailModal-CiPfOn2v.js.map → TestEmailModal-Ce8-vzGh.js.map} +1 -1
  81. package/dist/cdn/chunks/{TestEmailPanel-saOfnpjz.js → TestEmailPanel-BlmIKKSL.js} +3 -3
  82. package/dist/cdn/chunks/{TestEmailPanel-saOfnpjz.js.map → TestEmailPanel-BlmIKKSL.js.map} +1 -1
  83. package/dist/cdn/chunks/{TitleEditor-08KM_VzZ.js → TitleEditor-C6_bQxYR.js} +12 -12
  84. package/dist/cdn/chunks/{TitleEditor-08KM_VzZ.js.map → TitleEditor-C6_bQxYR.js.map} +1 -1
  85. package/dist/cdn/chunks/TplModal-D6bFON2K.js +50 -0
  86. package/dist/cdn/chunks/TplModal-D6bFON2K.js.map +1 -0
  87. package/dist/cdn/chunks/{blockTypeIcons-DvohkXqi.js → blockTypeIcons-B8s3_ahs.js} +2 -2
  88. package/dist/cdn/chunks/{blockTypeIcons-DvohkXqi.js.map → blockTypeIcons-B8s3_ahs.js.map} +1 -1
  89. package/dist/cdn/chunks/{cloud-C2I0P9qj.js → cloud-CRlDc0Ev.js} +13 -17
  90. package/dist/cdn/chunks/{cloud-C2I0P9qj.js.map → cloud-CRlDc0Ev.js.map} +1 -1
  91. package/dist/cdn/chunks/{dist-CxTt8FRE.js → dist-B8AX9np4.js} +16 -12
  92. package/dist/cdn/chunks/dist-B8AX9np4.js.map +1 -0
  93. package/dist/cdn/chunks/{dist-CiBEt0VR.js → dist-lZgXNk4G.js} +36 -20
  94. package/dist/cdn/chunks/dist-lZgXNk4G.js.map +1 -0
  95. package/dist/cdn/chunks/{draggable-D840DIZB.js → draggable-JcMTNF3U.js} +14 -15
  96. package/dist/cdn/chunks/{draggable-D840DIZB.js.map → draggable-JcMTNF3U.js.map} +1 -1
  97. package/dist/cdn/chunks/{extensions-DBWF_lxx.js → extensions-CKMD_er_.js} +15 -11
  98. package/dist/cdn/chunks/{extensions-DBWF_lxx.js.map → extensions-CKMD_er_.js.map} +1 -1
  99. package/dist/cdn/chunks/{htmlparser-CZa2V5cF.js → htmlparser-CPhs2cYE.js} +3 -5
  100. package/dist/cdn/chunks/{htmlparser-CZa2V5cF.js.map → htmlparser-CPhs2cYE.js.map} +1 -1
  101. package/dist/cdn/chunks/{icons-Cqxvwgz7.js → icons-Cp4ZIJw-.js} +31 -31
  102. package/dist/cdn/chunks/{icons-Cqxvwgz7.js.map → icons-Cp4ZIJw-.js.map} +1 -1
  103. package/dist/cdn/chunks/{keys-B5uoP9P7.js → keys-CswSK7eb.js} +2 -2
  104. package/dist/cdn/chunks/{keys-B5uoP9P7.js.map → keys-CswSK7eb.js.map} +1 -1
  105. package/dist/cdn/chunks/{liquid.browser-BJOEu6Cg.js → liquid.browser-CZaQe87t.js} +756 -718
  106. package/dist/cdn/chunks/liquid.browser-CZaQe87t.js.map +1 -0
  107. package/dist/cdn/chunks/{pusher-R0v5p99W.js → pusher-kUlkqMvR.js} +21 -28
  108. package/dist/cdn/chunks/{pusher-R0v5p99W.js.map → pusher-kUlkqMvR.js.map} +1 -1
  109. package/dist/cdn/chunks/{rolldown-runtime-Dqa2HsxW.js → rolldown-runtime-B0aSnxlc.js} +2 -2
  110. package/dist/cdn/chunks/{src-C45kfA2t.js → src-B3HKHglu.js} +5 -9
  111. package/dist/cdn/chunks/{src-C45kfA2t.js.map → src-B3HKHglu.js.map} +1 -1
  112. package/dist/cdn/chunks/{src-YVsPFLNJ.js → src-B4UhYbI8.js} +4 -4
  113. package/dist/cdn/chunks/{src-YVsPFLNJ.js.map → src-B4UhYbI8.js.map} +1 -1
  114. package/dist/cdn/chunks/{src-XQmI_Xs1.js → src-CumZZNXZ.js} +523 -787
  115. package/dist/cdn/chunks/{src-XQmI_Xs1.js.map → src-CumZZNXZ.js.map} +1 -1
  116. package/dist/cdn/chunks/{src-BCJna-6p.js → src-D5mfLR4V.js} +2 -2
  117. package/dist/cdn/chunks/{src-BCJna-6p.js.map → src-D5mfLR4V.js.map} +1 -1
  118. package/dist/cdn/chunks/{styles-BohMfIBd.js → styles--m9OCb_Z.js} +530 -530
  119. package/dist/cdn/chunks/{styles-BohMfIBd.js.map → styles--m9OCb_Z.js.map} +1 -1
  120. package/dist/cdn/chunks/timeouts-SsLMC4a3.js +6 -0
  121. package/dist/cdn/chunks/{timeouts-Bmi_yePw.js.map → timeouts-SsLMC4a3.js.map} +1 -1
  122. package/dist/cdn/chunks/{tiptap-DRKu__AX.js → tiptap-rsib432n.js} +132 -119
  123. package/dist/cdn/chunks/tiptap-rsib432n.js.map +1 -0
  124. package/dist/cdn/chunks/{useAliveFlag-aanU8uqB.js → useAliveFlag-CseYNikC.js} +2 -2
  125. package/dist/cdn/chunks/{useAliveFlag-aanU8uqB.js.map → useAliveFlag-CseYNikC.js.map} +1 -1
  126. package/dist/cdn/chunks/{useCloudI18n-Bem8DUjH.js → useCloudI18n-sx1pqEbY.js} +3 -3
  127. package/dist/cdn/chunks/{useCloudI18n-Bem8DUjH.js.map → useCloudI18n-sx1pqEbY.js.map} +1 -1
  128. package/dist/cdn/chunks/{useEditorCore-B_vwuKdI.js → useEditorCore-ZqF7P99K.js} +201 -199
  129. package/dist/cdn/chunks/{useEditorCore-B_vwuKdI.js.map → useEditorCore-ZqF7P99K.js.map} +1 -1
  130. package/dist/cdn/chunks/{useI18n-graz19nI.js → useI18n-vD57BfVR.js} +3 -3
  131. package/dist/cdn/chunks/{useI18n-graz19nI.js.map → useI18n-vD57BfVR.js.map} +1 -1
  132. package/dist/cdn/chunks/{useMergeTag-rTzAFMlr.js → useMergeTag-HatzNGgc.js} +4 -4
  133. package/dist/cdn/chunks/{useMergeTag-rTzAFMlr.js.map → useMergeTag-HatzNGgc.js.map} +1 -1
  134. package/dist/cdn/chunks/{usePopoverPosition-BaFxLP3w.js → usePopoverPosition-BdQ3Sv4B.js} +3 -3
  135. package/dist/cdn/chunks/{usePopoverPosition-BaFxLP3w.js.map → usePopoverPosition-BdQ3Sv4B.js.map} +1 -1
  136. package/dist/cdn/editor.js +10 -10
  137. package/dist/cdn/editor.js.map +1 -1
  138. package/dist/{check-BpQDKiPz.js → check-CIhIDh4h.js} +1 -1
  139. package/dist/{chevron-down-j8GrdTyz.js → chevron-down-CZ0m6G3s.js} +1 -1
  140. package/dist/{chevron-up-BACyVhyC.js → chevron-up-BH_tJLeb.js} +1 -1
  141. package/dist/{circle-alert-CgtE46ND.js → circle-alert-DkEXnGPV.js} +1 -1
  142. package/dist/{clock-BzKLLW5L.js → clock-BDIJZUET.js} +1 -1
  143. package/dist/{cloud-DsUct4Zq.js → cloud-R8ci6yR1.js} +10 -14
  144. package/dist/{createLucideIcon-JWHwTI6N.js → createLucideIcon-AEltGIse.js} +3 -3
  145. package/dist/{dist-BAkGuPYm.js → dist-B6Px6GPj.js} +1 -1
  146. package/dist/{dist-DPHqs0Yh.js → dist-Ba5Q-UX1.js} +1 -1
  147. package/dist/{dist-9uhI32Q4.js → dist-BxbTVeY0.js} +1 -1
  148. package/dist/{dist-DWu9QWFl.js → dist-CIqhVQ1O.js} +5 -5
  149. package/dist/{dist-CkCVlKj_.js → dist-CRH880TV.js} +1 -1
  150. package/dist/{dist-ByJVlKWy.js → dist-CcQFPJMF.js} +116 -100
  151. package/dist/{dist-JXOq1hib.js → dist-CkPvZCld.js} +1 -1
  152. package/dist/{dist-IGetXrbO.js → dist-DSIgxp0v.js} +1 -1
  153. package/dist/{dist-DYv_Pttj.js → dist-DT1XnEnj.js} +33 -17
  154. package/dist/{dist-C_kAE1sC.js → dist-DX4x6mQ1.js} +3 -3
  155. package/dist/{dist-DFO896yT.js → dist-Dx71pluS.js} +16 -11
  156. package/dist/{dist-CQiAZlTt.js → dist-DyCHUyRR.js} +1 -1
  157. package/dist/{dist-BIYm-U9x.js → dist-iCEDH3cC.js} +1 -1
  158. package/dist/{dist-C5tigvDF.js → dist-nR6mxyqI.js} +1 -1
  159. package/dist/{extensions-C0DXnYdA.js → extensions-CGxj6-2o.js} +16 -12
  160. package/dist/{image-up-CQdtq8ge.js → image-up-8ktdYdaZ.js} +1 -1
  161. package/dist/{info-BUJBXoE6.js → info-CvrCgTLH.js} +1 -1
  162. package/dist/{keys-BVlG5Ozc.js → keys-uWpGv4Pq.js} +1 -1
  163. package/dist/{liquid.browser-DZLtv26k.js → liquid.browser-BECWFL5Z.js} +755 -717
  164. package/dist/{list-checks-BGHaCSD5.js → list-checks-yzGT93ZL.js} +1 -1
  165. package/dist/{loader-circle-C-Z6kgI6.js → loader-circle-B6euendB.js} +1 -1
  166. package/dist/{message-circle-CEm5qckV.js → message-circle-BMldLVcA.js} +1 -1
  167. package/dist/{pencil-DdwmkKLb.js → pencil-CkKkSd6N.js} +1 -1
  168. package/dist/{refresh-cw-maKlg0dl.js → refresh-cw-fBPgB97G.js} +1 -1
  169. package/dist/{runtime-dom.esm-bundler-GdMI8WqG.js → runtime-dom.esm-bundler-CBbDt7sw.js} +2 -2
  170. package/dist/{scan-line-BFgesTOY.js → scan-line-CRoEOUiK.js} +1 -1
  171. package/dist/{send-B70aQDrA.js → send-B9Tt2qBi.js} +1 -1
  172. package/dist/{shield-check-B7x5OKt5.js → shield-check-QfZkYeIo.js} +1 -1
  173. package/dist/{sparkles-DVlm1Ipp.js → sparkles-8UeMmZb8.js} +1 -1
  174. package/dist/{styles-BTTD0Je4.js → styles-C9AMvkJk.js} +541 -541
  175. package/dist/templatical-editor.js +11 -11
  176. package/dist/{text-align-start-Vay3Kvo2.js → text-align-start-Dn8uIheW.js} +1 -1
  177. package/dist/{trash-2-DE8CmxAG.js → trash-2-DDyvqj0E.js} +1 -1
  178. package/dist/{triangle-alert-Djg6IG8N.js → triangle-alert-B3PydLzl.js} +1 -1
  179. package/dist/{upload-BwwE0U6G.js → upload-hE8ItfyG.js} +2 -2
  180. package/dist/{useAliveFlag-C43EhZFY.js → useAliveFlag-AfoTE_AY.js} +1 -1
  181. package/dist/{useCloudI18n-DGr3b2SX.js → useCloudI18n-BC_QV2Z7.js} +2 -2
  182. package/dist/{useEditorCore-DOrGex_t.js → useEditorCore-Dojb-P2f.js} +655 -655
  183. package/dist/{useI18n-BhAejA-x.js → useI18n-DzOcl2wl.js} +2 -2
  184. package/dist/{useMergeTag-Ce95YIOO.js → useMergeTag-BFxXCv3p.js} +3 -3
  185. package/dist/{usePopoverPosition-vjKJaqJf.js → usePopoverPosition-Dtr1Mp1x.js} +13 -13
  186. package/dist/{vue.runtime.esm-bundler-B0xoGkDv.js → vue.runtime.esm-bundler-Xpz-L9Fn.js} +27 -22
  187. package/dist/{x-CkD6A1iB.js → x-C4lETLDx.js} +1 -1
  188. package/package.json +12 -12
  189. package/dist/DesignReferenceSidebar-C4pTMzQE.js +0 -264
  190. package/dist/TplModal-DPH4vV8M.js +0 -49
  191. package/dist/cdn/chunks/DesignReferenceSidebar-BjlaP69l.js +0 -223
  192. package/dist/cdn/chunks/TplModal-Cu4tBBnF.js +0 -50
  193. package/dist/cdn/chunks/TplModal-Cu4tBBnF.js.map +0 -1
  194. package/dist/cdn/chunks/dist-CiBEt0VR.js.map +0 -1
  195. package/dist/cdn/chunks/dist-CxTt8FRE.js.map +0 -1
  196. package/dist/cdn/chunks/liquid.browser-BJOEu6Cg.js.map +0 -1
  197. package/dist/cdn/chunks/timeouts-Bmi_yePw.js +0 -6
  198. package/dist/cdn/chunks/tiptap-DRKu__AX.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { K as e, T as t } from "./draggable-D840DIZB.js";
2
- import { z as n } from "./keys-B5uoP9P7.js";
1
+ import { K as e, T as t } from "./draggable-JcMTNF3U.js";
2
+ import { z as n } from "./keys-CswSK7eb.js";
3
3
  //#region src/composables/useI18n.ts
4
4
  function r(r) {
5
5
  let i = r ?? t(n, null);
@@ -16,4 +16,4 @@ function r(r) {
16
16
  //#endregion
17
17
  export { r as t };
18
18
 
19
- //# sourceMappingURL=useI18n-graz19nI.js.map
19
+ //# sourceMappingURL=useI18n-vD57BfVR.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useI18n-graz19nI.js","names":[],"sources":["../../../src/composables/useI18n.ts"],"sourcesContent":["import type { Translations } from \"../i18n\";\nimport { type Ref, inject, isRef } from \"vue\";\nimport { TRANSLATIONS_KEY } from \"../keys\";\n\nexport interface UseI18nReturn {\n /** Current translations object */\n t: Translations;\n /** Format a string with placeholders */\n format: (template: string, values: Record<string, string | number>) => string;\n}\n\n/**\n * Composable for internationalization.\n * Provides access to the current locale's translations.\n *\n * @param translationsOverride - Optional translations to use instead of injected value\n */\nexport function useI18n(translationsOverride?: Translations): UseI18nReturn {\n const injected =\n translationsOverride ??\n (inject(TRANSLATIONS_KEY, null) as Translations | Ref<Translations> | null);\n\n if (!injected) {\n throw new Error(\n \"useI18n() requires a translations provider. Ensure the component is a descendant of Editor or CloudEditor.\",\n );\n }\n\n const t: Translations = isRef(injected) ? injected.value : injected;\n\n /**\n * Format a string with placeholders.\n * e.g., format('{minutes}m ago', { minutes: 5 }) -> '5m ago'\n */\n function format(\n template: string,\n values: Record<string, string | number>,\n ): string {\n return template.replace(/\\{(\\w+)\\}/g, (_, key) => {\n return key in values ? String(values[key]) : `{${key}}`;\n });\n }\n\n return {\n t,\n format,\n };\n}\n"],"mappings":";;;AAiBA,SAAgB,EAAQ,GAAoD;CAC1E,IAAM,IACJ,KACC,EAAO,GAAkB,IAAI;CAEhC,IAAI,CAAC,GACH,MAAU,MACR,4GACF;CAGF,IAAM,IAAkB,EAAM,CAAQ,IAAI,EAAS,QAAQ;CAM3D,SAAS,EACP,GACA,GACQ;EACR,OAAO,EAAS,QAAQ,eAAe,GAAG,MACjC,KAAO,IAAS,OAAO,EAAO,EAAI,IAAI,IAAI,EAAI,EACtD;CACH;CAEA,OAAO;EACL;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useI18n-vD57BfVR.js","names":[],"sources":["../../../src/composables/useI18n.ts"],"sourcesContent":["import type { Translations } from \"../i18n\";\nimport { type Ref, inject, isRef } from \"vue\";\nimport { TRANSLATIONS_KEY } from \"../keys\";\n\nexport interface UseI18nReturn {\n /** Current translations object */\n t: Translations;\n /** Format a string with placeholders */\n format: (template: string, values: Record<string, string | number>) => string;\n}\n\n/**\n * Composable for internationalization.\n * Provides access to the current locale's translations.\n *\n * @param translationsOverride - Optional translations to use instead of injected value\n */\nexport function useI18n(translationsOverride?: Translations): UseI18nReturn {\n const injected =\n translationsOverride ??\n (inject(TRANSLATIONS_KEY, null) as Translations | Ref<Translations> | null);\n\n if (!injected) {\n throw new Error(\n \"useI18n() requires a translations provider. Ensure the component is a descendant of Editor or CloudEditor.\",\n );\n }\n\n const t: Translations = isRef(injected) ? injected.value : injected;\n\n /**\n * Format a string with placeholders.\n * e.g., format('{minutes}m ago', { minutes: 5 }) -> '5m ago'\n */\n function format(\n template: string,\n values: Record<string, string | number>,\n ): string {\n return template.replace(/\\{(\\w+)\\}/g, (_, key) => {\n return key in values ? String(values[key]) : `{${key}}`;\n });\n }\n\n return {\n t,\n format,\n };\n}\n"],"mappings":";;;AAiBA,SAAgB,EAAQ,GAAoD;CAC1E,IAAM,IACJ,KACC,EAAO,GAAkB,IAAI;CAEhC,IAAI,CAAC,GACH,MAAU,MACR,4GACF;CAGF,IAAM,IAAkB,EAAM,CAAQ,IAAI,EAAS,QAAQ;CAM3D,SAAS,EACP,GACA,GACQ;EACR,OAAO,EAAS,QAAQ,eAAe,GAAG,MACjC,KAAO,IAAS,OAAO,EAAO,EAAI,IAAI,IAAI,EAAI,EACtD;CACH;CAEA,OAAO;EACL;EACA;CACF;AACF"}
@@ -1,6 +1,6 @@
1
- import { T as e, Z as t } from "./draggable-D840DIZB.js";
2
- import { a as n, d as r, n as i } from "./src-BCJna-6p.js";
3
- import { D as a, E as o, M as s, T as c, k as l } from "./keys-B5uoP9P7.js";
1
+ import { T as e, Z as t } from "./draggable-JcMTNF3U.js";
2
+ import { a as n, d as r, n as i } from "./src-D5mfLR4V.js";
3
+ import { D as a, E as o, M as s, T as c, k as l } from "./keys-CswSK7eb.js";
4
4
  //#region src/composables/useMergeTag.ts
5
5
  function u() {
6
6
  let u = e(c, []), d = e(l, i.liquid), f = e(s, null), p = e(o, !0), m = e(a, null), h = t(!1);
@@ -43,4 +43,4 @@ function u() {
43
43
  //#endregion
44
44
  export { u as t };
45
45
 
46
- //# sourceMappingURL=useMergeTag-rTzAFMlr.js.map
46
+ //# sourceMappingURL=useMergeTag-HatzNGgc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useMergeTag-rTzAFMlr.js","names":[],"sources":["../../../src/composables/useMergeTag.ts"],"sourcesContent":["import type { MergeTag, SyntaxPreset } from \"@templatical/types\";\nimport {\n isMergeTagValue as checkIsMergeTagValue,\n getMergeTagLabel as resolveMergeTagLabel,\n SYNTAX_PRESETS,\n} from \"@templatical/types\";\nimport { inject, ref, type Ref } from \"vue\";\nimport {\n MERGE_TAGS_KEY,\n MERGE_TAG_SYNTAX_KEY,\n MERGE_TAG_AUTOCOMPLETE_KEY,\n MERGE_TAG_PICKER_KEY,\n ON_REQUEST_MERGE_TAG_KEY,\n} from \"../keys\";\n\nexport interface UseMergeTagReturn {\n /** Available merge tags from config */\n mergeTags: MergeTag[];\n /** Whether a merge tag request is in progress */\n isRequesting: Ref<boolean>;\n /**\n * Whether the \"Insert merge tag\" button should be shown. True when\n * either `onRequestMergeTag` is provided or `mergeTags.tags` is\n * non-empty (the built-in picker then handles the click).\n */\n canRequestMergeTag: boolean;\n /** Whether typing-based autocomplete is enabled by configuration */\n autocomplete: boolean;\n /** The resolved syntax preset for merge tags */\n syntax: SyntaxPreset;\n /** Check if a value matches the configured merge tag syntax */\n isMergeTagValue: (value: string) => boolean;\n /** Get the human-readable label for a merge tag value */\n getMergeTagLabel: (value: string) => string;\n /**\n * Request a merge tag from the user. Precedence: when\n * `onRequestMergeTag` is set it owns the UX; otherwise the built-in\n * picker opens with the configured `mergeTags.tags`; otherwise null.\n */\n requestMergeTag: () => Promise<MergeTag | null>;\n}\n\n/**\n * Composable for merge tag functionality.\n * Provides utilities for detecting, displaying, and requesting merge tags.\n */\nexport function useMergeTag(): UseMergeTagReturn {\n const mergeTags = inject(MERGE_TAGS_KEY, []);\n const syntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\n const onRequestMergeTag = inject(ON_REQUEST_MERGE_TAG_KEY, null);\n const autocomplete = inject(MERGE_TAG_AUTOCOMPLETE_KEY, true);\n // Picker may be null in headless contexts (tests, non-editor consumers).\n // requestMergeTag() returns null in that case rather than throwing.\n const picker = inject(MERGE_TAG_PICKER_KEY, null);\n\n const isRequesting = ref(false);\n\n function isMergeTagValue(value: string): boolean {\n return checkIsMergeTagValue(value, syntax);\n }\n\n function getMergeTagLabel(value: string): string {\n return resolveMergeTagLabel(value, mergeTags);\n }\n\n /**\n * Request a merge tag from the user. Precedence:\n * 1. `onRequestMergeTag` callback (consumer-owned UX)\n * 2. Built-in picker over `mergeTags.tags`\n * 3. null (nothing configured)\n */\n async function requestMergeTag(): Promise<MergeTag | null> {\n if (onRequestMergeTag) {\n isRequesting.value = true;\n try {\n return await onRequestMergeTag();\n } finally {\n isRequesting.value = false;\n }\n }\n if (mergeTags.length > 0 && picker) {\n isRequesting.value = true;\n try {\n return await picker.open(mergeTags);\n } finally {\n isRequesting.value = false;\n }\n }\n return null;\n }\n\n const canRequestMergeTag = !!onRequestMergeTag || mergeTags.length > 0;\n\n return {\n mergeTags,\n isRequesting,\n canRequestMergeTag,\n autocomplete,\n syntax,\n isMergeTagValue,\n getMergeTagLabel,\n requestMergeTag,\n };\n}\n"],"mappings":";;;;AA8CA,SAAgB,IAAiC;CAC/C,IAAM,IAAY,EAAO,GAAgB,CAAC,CAAC,GACrC,IAAS,EAAO,GAAsB,EAAe,MAAM,GAC3D,IAAoB,EAAO,GAA0B,IAAI,GACzD,IAAe,EAAO,GAA4B,EAAI,GAGtD,IAAS,EAAO,GAAsB,IAAI,GAE1C,IAAe,EAAI,EAAK;CAE9B,SAAS,EAAgB,GAAwB;EAC/C,OAAO,EAAqB,GAAO,CAAM;CAC3C;CAEA,SAAS,EAAiB,GAAuB;EAC/C,OAAO,EAAqB,GAAO,CAAS;CAC9C;CAQA,eAAe,IAA4C;EACzD,IAAI,GAAmB;GACrB,EAAa,QAAQ;GACrB,IAAI;IACF,OAAO,MAAM,EAAkB;GACjC,UAAU;IACR,EAAa,QAAQ;GACvB;EACF;EACA,IAAI,EAAU,SAAS,KAAK,GAAQ;GAClC,EAAa,QAAQ;GACrB,IAAI;IACF,OAAO,MAAM,EAAO,KAAK,CAAS;GACpC,UAAU;IACR,EAAa,QAAQ;GACvB;EACF;EACA,OAAO;CACT;CAIA,OAAO;EACL;EACA;EACA,oBALyB,CAAC,CAAC,KAAqB,EAAU,SAAS;EAMnE;EACA;EACA,iBAAA;EACA,kBAAA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useMergeTag-HatzNGgc.js","names":[],"sources":["../../../src/composables/useMergeTag.ts"],"sourcesContent":["import type { MergeTag, SyntaxPreset } from \"@templatical/types\";\nimport {\n isMergeTagValue as checkIsMergeTagValue,\n getMergeTagLabel as resolveMergeTagLabel,\n SYNTAX_PRESETS,\n} from \"@templatical/types\";\nimport { inject, ref, type Ref } from \"vue\";\nimport {\n MERGE_TAGS_KEY,\n MERGE_TAG_SYNTAX_KEY,\n MERGE_TAG_AUTOCOMPLETE_KEY,\n MERGE_TAG_PICKER_KEY,\n ON_REQUEST_MERGE_TAG_KEY,\n} from \"../keys\";\n\nexport interface UseMergeTagReturn {\n /** Available merge tags from config */\n mergeTags: MergeTag[];\n /** Whether a merge tag request is in progress */\n isRequesting: Ref<boolean>;\n /**\n * Whether the \"Insert merge tag\" button should be shown. True when\n * either `onRequestMergeTag` is provided or `mergeTags.tags` is\n * non-empty (the built-in picker then handles the click).\n */\n canRequestMergeTag: boolean;\n /** Whether typing-based autocomplete is enabled by configuration */\n autocomplete: boolean;\n /** The resolved syntax preset for merge tags */\n syntax: SyntaxPreset;\n /** Check if a value matches the configured merge tag syntax */\n isMergeTagValue: (value: string) => boolean;\n /** Get the human-readable label for a merge tag value */\n getMergeTagLabel: (value: string) => string;\n /**\n * Request a merge tag from the user. Precedence: when\n * `onRequestMergeTag` is set it owns the UX; otherwise the built-in\n * picker opens with the configured `mergeTags.tags`; otherwise null.\n */\n requestMergeTag: () => Promise<MergeTag | null>;\n}\n\n/**\n * Composable for merge tag functionality.\n * Provides utilities for detecting, displaying, and requesting merge tags.\n */\nexport function useMergeTag(): UseMergeTagReturn {\n const mergeTags = inject(MERGE_TAGS_KEY, []);\n const syntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\n const onRequestMergeTag = inject(ON_REQUEST_MERGE_TAG_KEY, null);\n const autocomplete = inject(MERGE_TAG_AUTOCOMPLETE_KEY, true);\n // Picker may be null in headless contexts (tests, non-editor consumers).\n // requestMergeTag() returns null in that case rather than throwing.\n const picker = inject(MERGE_TAG_PICKER_KEY, null);\n\n const isRequesting = ref(false);\n\n function isMergeTagValue(value: string): boolean {\n return checkIsMergeTagValue(value, syntax);\n }\n\n function getMergeTagLabel(value: string): string {\n return resolveMergeTagLabel(value, mergeTags);\n }\n\n /**\n * Request a merge tag from the user. Precedence:\n * 1. `onRequestMergeTag` callback (consumer-owned UX)\n * 2. Built-in picker over `mergeTags.tags`\n * 3. null (nothing configured)\n */\n async function requestMergeTag(): Promise<MergeTag | null> {\n if (onRequestMergeTag) {\n isRequesting.value = true;\n try {\n return await onRequestMergeTag();\n } finally {\n isRequesting.value = false;\n }\n }\n if (mergeTags.length > 0 && picker) {\n isRequesting.value = true;\n try {\n return await picker.open(mergeTags);\n } finally {\n isRequesting.value = false;\n }\n }\n return null;\n }\n\n const canRequestMergeTag = !!onRequestMergeTag || mergeTags.length > 0;\n\n return {\n mergeTags,\n isRequesting,\n canRequestMergeTag,\n autocomplete,\n syntax,\n isMergeTagValue,\n getMergeTagLabel,\n requestMergeTag,\n };\n}\n"],"mappings":";;;;AA8CA,SAAgB,IAAiC;CAC/C,IAAM,IAAY,EAAO,GAAgB,CAAC,CAAC,GACrC,IAAS,EAAO,GAAsB,EAAe,MAAM,GAC3D,IAAoB,EAAO,GAA0B,IAAI,GACzD,IAAe,EAAO,GAA4B,EAAI,GAGtD,IAAS,EAAO,GAAsB,IAAI,GAE1C,IAAe,EAAI,EAAK;CAE9B,SAAS,EAAgB,GAAwB;EAC/C,OAAO,EAAqB,GAAO,CAAM;CAC3C;CAEA,SAAS,EAAiB,GAAuB;EAC/C,OAAO,EAAqB,GAAO,CAAS;CAC9C;CAQA,eAAe,IAA4C;EACzD,IAAI,GAAmB;GACrB,EAAa,QAAQ;GACrB,IAAI;IACF,OAAO,MAAM,EAAkB;GACjC,UAAU;IACR,EAAa,QAAQ;GACvB;EACF;EACA,IAAI,EAAU,SAAS,KAAK,GAAQ;GAClC,EAAa,QAAQ;GACrB,IAAI;IACF,OAAO,MAAM,EAAO,KAAK,CAAS;GACpC,UAAU;IACR,EAAa,QAAQ;GACvB;EACF;EACA,OAAO;CACT;CAIA,OAAO;EACL;EACA;EACA,oBALyB,CAAC,CAAC,KAAqB,EAAU,SAAS;EAMnE;EACA;EACA,iBAAA;EACA,kBAAA;EACA;CACF;AACF"}
@@ -1,5 +1,5 @@
1
- import { T as e, Z as t } from "./draggable-D840DIZB.js";
2
- import { P as n } from "./keys-B5uoP9P7.js";
1
+ import { T as e, Z as t } from "./draggable-JcMTNF3U.js";
2
+ import { P as n } from "./keys-CswSK7eb.js";
3
3
  //#region src/utils/resolveColorsConfig.ts
4
4
  var r = {
5
5
  presets: [],
@@ -58,4 +58,4 @@ function l() {
58
58
  //#endregion
59
59
  export { r as a, o as i, c as n, a as o, s as r, l as t };
60
60
 
61
- //# sourceMappingURL=usePopoverPosition-BaFxLP3w.js.map
61
+ //# sourceMappingURL=usePopoverPosition-BdQ3Sv4B.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePopoverPosition-BaFxLP3w.js","names":[],"sources":["../../../src/utils/resolveColorsConfig.ts","../../../src/utils/color.ts","../../../src/composables/usePopoverRoot.ts","../../../src/composables/usePopoverPosition.ts"],"sourcesContent":["import type { ColorsConfig } from \"@templatical/types\";\n\n/**\n * Normalized color-picker configuration handed to the provide/inject layer and\n * to `ColorPicker`. `presets` defaults to an empty array and `allowCustom` to\n * `true`, so consumers never branch on `undefined`.\n *\n * `allowCustomIgnored` is a validation signal, not a rendered value: it is\n * `true` when `allowCustom: false` was requested but forced back to `true`\n * because there were no presets to fall back on. This util stays pure — it\n * never logs; the consumer that owns the config surface emits the warning\n * (today that is `useEditorCore` for the editor-level config), the same split\n * `resolvePaletteBlocks` / `Sidebar.vue` use.\n */\nexport interface ResolvedColors {\n presets: string[];\n allowCustom: boolean;\n allowCustomIgnored: boolean;\n /**\n * Preset entries dropped because they are not a valid `#rgb` / `#rrggbb` hex\n * string (see the validity rule on {@link resolveColorsConfig}). Collected —\n * not logged — so the config-owning consumer can warn once listing them,\n * exactly as `allowCustomIgnored` is surfaced. Reflects THIS resolution's\n * `colors.presets` input only: when a `fallback`'s already-resolved (already\n * filtered) presets are inherited, its invalids are never re-reported here.\n */\n invalidPresets: string[];\n}\n\n/**\n * The unconfigured baseline: no presets, free-form entry allowed — i.e. the\n * picker behaves exactly as it did before the `colors` option existed. Used as\n * the inject default and as the fallback for the top-level resolution.\n */\nexport const DEFAULT_RESOLVED_COLORS: ResolvedColors = {\n presets: [],\n allowCustom: true,\n allowCustomIgnored: false,\n invalidPresets: [],\n};\n\n/**\n * A valid preset is a `#rgb` or `#rrggbb` hex string. 4-/8-digit alpha hex is\n * deliberately rejected: the wheel (vanilla-colorful) is RGB-only,\n * `normalizeColorToHex` drops alpha on the DOM round-trip so a selected alpha\n * preset would never read back as selected, and alpha degrades unpredictably\n * across email clients.\n */\nconst VALID_PRESET_HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\n\n/**\n * Resolve a `ColorsConfig` against an already-resolved `fallback`, applying\n * precedence per property (`input` → `fallback` → default) plus the shared\n * validation rule.\n *\n * The editor-level call passes no `fallback` (it defaults to\n * {@link DEFAULT_RESOLVED_COLORS}) and its result is provided via `COLORS_KEY`.\n * The `fallback` parameter is the nested-resolution seam: a narrower config can\n * be resolved against a broader already-resolved one, inheriting whichever\n * property it does not set.\n *\n * Validation: entries in `colors.presets` that aren't valid hex\n * ({@link VALID_PRESET_HEX}) are dropped from `presets` and collected into\n * `invalidPresets` for the caller to warn about. Separately, `allowCustom:\n * false` with no presets anywhere in the chain is ignored — forced back to\n * `true`, with `allowCustomIgnored` set so the caller can warn — because\n * hiding the wheel and hex input without a preset grid would leave the picker\n * unable to set a color. Filtering runs BEFORE that guard, so an all-invalid\n * list with `allowCustom: false` cascades into the forced-true path. This\n * mirrors how `paletteBlocks` is resolved: `resolvePaletteBlocks` collects the\n * offending entries and the consumer warns; this util stays pure the same way.\n */\nexport function resolveColorsConfig(\n colors: ColorsConfig | undefined,\n fallback: ResolvedColors = DEFAULT_RESOLVED_COLORS,\n): ResolvedColors {\n // Validate only presets this resolution actually supplies. When it inherits\n // `fallback.presets`, those were already resolved (already filtered), so\n // re-validating would double-report an upstream invalid entry.\n let presets: string[];\n let invalidPresets: string[];\n if (colors?.presets === undefined) {\n presets = fallback.presets;\n invalidPresets = [];\n } else {\n presets = [];\n invalidPresets = [];\n for (const entry of colors.presets) {\n if (VALID_PRESET_HEX.test(entry)) {\n presets.push(entry);\n } else {\n invalidPresets.push(entry);\n }\n }\n }\n\n const requestedAllowCustom = colors?.allowCustom ?? fallback.allowCustom;\n\n const allowCustomIgnored = !requestedAllowCustom && presets.length === 0;\n const allowCustom = allowCustomIgnored ? true : requestedAllowCustom;\n\n return { presets, allowCustom, allowCustomIgnored, invalidPresets };\n}\n","/**\n * Normalize a browser-serialized `rgb()/rgba()` color to `#rrggbb`. The browser\n * serializes an inline `style=\"color:#hex\"` to `rgb(...)`, so reading a stored\n * color back via `element.style.color` (e.g. a TipTap textStyle/highlight mark,\n * or a saved link color) surfaces `rgb(...)` — mismatching the hex used\n * everywhere else, and unparseable by the hex-only picker (`vanilla-colorful`)\n * and the native `<input type=\"color\">`. Non-rgb input (already hex, a keyword,\n * or empty) is returned unchanged.\n */\nexport function normalizeColorToHex(value: string): string {\n const m = /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i.exec(value.trim());\n if (!m) return value;\n const toHex = (n: string) => Number(n).toString(16).padStart(2, \"0\");\n return `#${toHex(m[1])}${toHex(m[2])}${toHex(m[3])}`;\n}\n\n/**\n * Canonicalize a hex color to the lowercase 6-digit form used for preset\n * membership tests. Runs {@link normalizeColorToHex} first (so a\n * browser-serialized `rgb(...)` round-trip collapses to hex), then expands a\n * 3-digit shorthand (`#abc` → `#aabbcc`) and lowercases it. Non-hex input (a\n * keyword, or empty) is returned unchanged after that rgb normalization —\n * the caller decides whether that counts as a member.\n *\n * Kept separate from `normalizeColorToHex` (which is left untouched) because a\n * configured preset may be `#abc` while the browser round-trips the selected\n * value to `#aabbcc`, and a factory default may be `#FFFFFF` while the preset\n * is `#ffffff` — bare equality misses both. Preset-selection display\n * (`ColorPicker`) and the off-palette defaults audit (`collectOffPaletteDefaults`)\n * both compare through this one function so the two membership tests can't drift.\n */\nexport function canonicalizeHexColor(value: string): string {\n const hex = normalizeColorToHex(value);\n const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(hex);\n if (short) {\n return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`.toLowerCase();\n }\n if (/^#[0-9a-f]{6}$/i.test(hex)) {\n return hex.toLowerCase();\n }\n return hex;\n}\n","import { inject, ref, type Ref } from \"vue\";\nimport { POPOVER_ROOT_KEY } from \"../keys\";\n\n/**\n * Returns the editor's shared popover mount ref — the `<div\n * class=\"tpl-popover-root\" />` rendered at the top level of `Editor.vue` /\n * `CloudEditor.vue`. Teleport targets that previously hard-coded\n * `to=\"body\"` (RichTextLinkDialog, TitleEditor toolbar, ParagraphToolbar,\n * TplModal) inject this and teleport to `popoverRoot.value` instead, so\n * popups stay inside the editor's effective DOM root (shadow-aware).\n *\n * Falls back to a never-resolving `ref(null)` when no provider is in scope.\n * Components must guard with `v-if=\"popoverRoot\"` before teleporting —\n * with the fallback, isolated component tests that don't mount the editor\n * shell simply render nothing rather than blow up.\n *\n * Internal — not exported from the package entry.\n */\nexport function usePopoverRoot(): Ref<HTMLElement | null> {\n return inject(POPOVER_ROOT_KEY, ref<HTMLElement | null>(null));\n}\n","import { usePopoverRoot } from \"./usePopoverRoot\";\n\n/**\n * Coordinate helper for popovers that teleport into the shared popover root\n * (`.tpl-popover-root`, `position: relative`) and position themselves with\n * `position: absolute`.\n *\n * Callers compute a target point in **viewport** space — from\n * `getBoundingClientRect()`, `useElementBounding`, or a TipTap caret rect —\n * and `toLocal` converts it to coordinates **local to the popover root** by\n * subtracting the root's own viewport rect.\n *\n * Why this exists: a `position: fixed` popover resolves its coordinates\n * against the nearest ancestor that establishes a containing block — and any\n * `transform` / `filter` / `will-change` on an ancestor of the editor (a\n * consumer's scroll-parallax wrapper, route-transition, reveal animation…)\n * does exactly that, even while its computed `transform` reads `none` (a\n * running/animated transform still promotes the element). The fixed popover\n * would then be offset by that ancestor's position. Anchoring `absolute`\n * inside the (positioned) popover root instead makes the placement immune:\n * both points are read in viewport space, so the subtraction cancels the\n * ancestor offset, and the popover tracks the editor if the ancestor moves.\n *\n * Read `toLocal` inside a `computed` for a reactive result (recomputes with\n * its viewport-coordinate dependencies), or call it once when a popover opens.\n * Falls back to the viewport coordinates unchanged when no popover root is in\n * scope (isolated component tests / headless callers).\n *\n * Internal — not exported from the package entry.\n */\nexport function usePopoverPosition(): {\n toLocal: (viewport: { top: number; left: number }) => {\n top: number;\n left: number;\n };\n} {\n const popoverRoot = usePopoverRoot();\n function toLocal(viewport: { top: number; left: number }): {\n top: number;\n left: number;\n } {\n const rect = popoverRoot.value?.getBoundingClientRect();\n if (!rect) return { top: viewport.top, left: viewport.left };\n return { top: viewport.top - rect.top, left: viewport.left - rect.left };\n }\n return { toLocal };\n}\n"],"mappings":";;;AAkCA,IAAa,IAA0C;CACrD,SAAS,CAAC;CACV,aAAa;CACb,oBAAoB;CACpB,gBAAgB,CAAC;AACnB,GASM,IAAmB;AAwBzB,SAAgB,EACd,GACA,IAA2B,GACX;CAIhB,IAAI,GACA;CACJ,IAAI,GAAQ,YAAY,KAAA,GAEtB,AADA,IAAU,EAAS,SACnB,IAAiB,CAAC;MACb;EAEL,AADA,IAAU,CAAC,GACX,IAAiB,CAAC;EAClB,KAAK,IAAM,KAAS,EAAO,SACzB,AAAI,EAAiB,KAAK,CAAK,IAC7B,EAAQ,KAAK,CAAK,IAElB,EAAe,KAAK,CAAK;CAG/B;CAEA,IAAM,IAAuB,GAAQ,eAAe,EAAS,aAEvD,IAAqB,CAAC,KAAwB,EAAQ,WAAW;CAGvE,OAAO;EAAE;EAAS,aAFE,IAAqB,KAAO;EAEjB;EAAoB;CAAe;AACpE;;;AC7FA,SAAgB,EAAoB,GAAuB;CACzD,IAAM,IAAI,4CAA4C,KAAK,EAAM,KAAK,CAAC;CACvE,IAAI,CAAC,GAAG,OAAO;CACf,IAAM,KAAS,MAAc,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CACnE,OAAO,IAAI,EAAM,EAAE,EAAE,IAAI,EAAM,EAAE,EAAE,IAAI,EAAM,EAAE,EAAE;AACnD;AAiBA,SAAgB,EAAqB,GAAuB;CAC1D,IAAM,IAAM,EAAoB,CAAK,GAC/B,IAAQ,qCAAqC,KAAK,CAAG;CAO3D,OANI,IACK,IAAI,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,YAAY,IAEvF,kBAAkB,KAAK,CAAG,IACrB,EAAI,YAAY,IAElB;AACT;;;ACvBA,SAAgB,IAA0C;CACxD,OAAO,EAAO,GAAkB,EAAwB,IAAI,CAAC;AAC/D;;;ACUA,SAAgB,IAKd;CACA,IAAM,IAAc,EAAe;CACnC,SAAS,EAAQ,GAGf;EACA,IAAM,IAAO,EAAY,OAAO,sBAAsB;EAEtD,OADK,IACE;GAAE,KAAK,EAAS,MAAM,EAAK;GAAK,MAAM,EAAS,OAAO,EAAK;EAAK,IADrD;GAAE,KAAK,EAAS;GAAK,MAAM,EAAS;EAAK;CAE7D;CACA,OAAO,EAAE,WAAQ;AACnB"}
1
+ {"version":3,"file":"usePopoverPosition-BdQ3Sv4B.js","names":[],"sources":["../../../src/utils/resolveColorsConfig.ts","../../../src/utils/color.ts","../../../src/composables/usePopoverRoot.ts","../../../src/composables/usePopoverPosition.ts"],"sourcesContent":["import type { ColorsConfig } from \"@templatical/types\";\n\n/**\n * Normalized color-picker configuration handed to the provide/inject layer and\n * to `ColorPicker`. `presets` defaults to an empty array and `allowCustom` to\n * `true`, so consumers never branch on `undefined`.\n *\n * `allowCustomIgnored` is a validation signal, not a rendered value: it is\n * `true` when `allowCustom: false` was requested but forced back to `true`\n * because there were no presets to fall back on. This util stays pure — it\n * never logs; the consumer that owns the config surface emits the warning\n * (today that is `useEditorCore` for the editor-level config), the same split\n * `resolvePaletteBlocks` / `Sidebar.vue` use.\n */\nexport interface ResolvedColors {\n presets: string[];\n allowCustom: boolean;\n allowCustomIgnored: boolean;\n /**\n * Preset entries dropped because they are not a valid `#rgb` / `#rrggbb` hex\n * string (see the validity rule on {@link resolveColorsConfig}). Collected —\n * not logged — so the config-owning consumer can warn once listing them,\n * exactly as `allowCustomIgnored` is surfaced. Reflects THIS resolution's\n * `colors.presets` input only: when a `fallback`'s already-resolved (already\n * filtered) presets are inherited, its invalids are never re-reported here.\n */\n invalidPresets: string[];\n}\n\n/**\n * The unconfigured baseline: no presets, free-form entry allowed — i.e. the\n * picker behaves exactly as it did before the `colors` option existed. Used as\n * the inject default and as the fallback for the top-level resolution.\n */\nexport const DEFAULT_RESOLVED_COLORS: ResolvedColors = {\n presets: [],\n allowCustom: true,\n allowCustomIgnored: false,\n invalidPresets: [],\n};\n\n/**\n * A valid preset is a `#rgb` or `#rrggbb` hex string. 4-/8-digit alpha hex is\n * deliberately rejected: the wheel (vanilla-colorful) is RGB-only,\n * `normalizeColorToHex` drops alpha on the DOM round-trip so a selected alpha\n * preset would never read back as selected, and alpha degrades unpredictably\n * across email clients.\n */\nconst VALID_PRESET_HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\n\n/**\n * Resolve a `ColorsConfig` against an already-resolved `fallback`, applying\n * precedence per property (`input` → `fallback` → default) plus the shared\n * validation rule.\n *\n * The editor-level call passes no `fallback` (it defaults to\n * {@link DEFAULT_RESOLVED_COLORS}) and its result is provided via `COLORS_KEY`.\n * The `fallback` parameter is the nested-resolution seam: a narrower config can\n * be resolved against a broader already-resolved one, inheriting whichever\n * property it does not set.\n *\n * Validation: entries in `colors.presets` that aren't valid hex\n * ({@link VALID_PRESET_HEX}) are dropped from `presets` and collected into\n * `invalidPresets` for the caller to warn about. Separately, `allowCustom:\n * false` with no presets anywhere in the chain is ignored — forced back to\n * `true`, with `allowCustomIgnored` set so the caller can warn — because\n * hiding the wheel and hex input without a preset grid would leave the picker\n * unable to set a color. Filtering runs BEFORE that guard, so an all-invalid\n * list with `allowCustom: false` cascades into the forced-true path. This\n * mirrors how `paletteBlocks` is resolved: `resolvePaletteBlocks` collects the\n * offending entries and the consumer warns; this util stays pure the same way.\n */\nexport function resolveColorsConfig(\n colors: ColorsConfig | undefined,\n fallback: ResolvedColors = DEFAULT_RESOLVED_COLORS,\n): ResolvedColors {\n // Validate only presets this resolution actually supplies. When it inherits\n // `fallback.presets`, those were already resolved (already filtered), so\n // re-validating would double-report an upstream invalid entry.\n let presets: string[];\n let invalidPresets: string[];\n if (colors?.presets === undefined) {\n presets = fallback.presets;\n invalidPresets = [];\n } else {\n presets = [];\n invalidPresets = [];\n for (const entry of colors.presets) {\n if (VALID_PRESET_HEX.test(entry)) {\n presets.push(entry);\n } else {\n invalidPresets.push(entry);\n }\n }\n }\n\n const requestedAllowCustom = colors?.allowCustom ?? fallback.allowCustom;\n\n const allowCustomIgnored = !requestedAllowCustom && presets.length === 0;\n const allowCustom = allowCustomIgnored ? true : requestedAllowCustom;\n\n return { presets, allowCustom, allowCustomIgnored, invalidPresets };\n}\n","/**\n * Normalize a browser-serialized `rgb()/rgba()` color to `#rrggbb`. The browser\n * serializes an inline `style=\"color:#hex\"` to `rgb(...)`, so reading a stored\n * color back via `element.style.color` (e.g. a TipTap textStyle/highlight mark,\n * or a saved link color) surfaces `rgb(...)` — mismatching the hex used\n * everywhere else, and unparseable by the hex-only picker (`vanilla-colorful`)\n * and the native `<input type=\"color\">`. Non-rgb input (already hex, a keyword,\n * or empty) is returned unchanged.\n */\nexport function normalizeColorToHex(value: string): string {\n const m = /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i.exec(value.trim());\n if (!m) return value;\n const toHex = (n: string) => Number(n).toString(16).padStart(2, \"0\");\n return `#${toHex(m[1])}${toHex(m[2])}${toHex(m[3])}`;\n}\n\n/**\n * Canonicalize a hex color to the lowercase 6-digit form used for preset\n * membership tests. Runs {@link normalizeColorToHex} first (so a\n * browser-serialized `rgb(...)` round-trip collapses to hex), then expands a\n * 3-digit shorthand (`#abc` → `#aabbcc`) and lowercases it. Non-hex input (a\n * keyword, or empty) is returned unchanged after that rgb normalization —\n * the caller decides whether that counts as a member.\n *\n * Kept separate from `normalizeColorToHex` (which is left untouched) because a\n * configured preset may be `#abc` while the browser round-trips the selected\n * value to `#aabbcc`, and a factory default may be `#FFFFFF` while the preset\n * is `#ffffff` — bare equality misses both. Preset-selection display\n * (`ColorPicker`) and the off-palette defaults audit (`collectOffPaletteDefaults`)\n * both compare through this one function so the two membership tests can't drift.\n */\nexport function canonicalizeHexColor(value: string): string {\n const hex = normalizeColorToHex(value);\n const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(hex);\n if (short) {\n return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`.toLowerCase();\n }\n if (/^#[0-9a-f]{6}$/i.test(hex)) {\n return hex.toLowerCase();\n }\n return hex;\n}\n","import { inject, ref, type Ref } from \"vue\";\nimport { POPOVER_ROOT_KEY } from \"../keys\";\n\n/**\n * Returns the editor's shared popover mount ref — the `<div\n * class=\"tpl-popover-root\" />` rendered at the top level of `Editor.vue` /\n * `CloudEditor.vue`. Teleport targets that previously hard-coded\n * `to=\"body\"` (RichTextLinkDialog, TitleEditor toolbar, ParagraphToolbar,\n * TplModal) inject this and teleport to `popoverRoot.value` instead, so\n * popups stay inside the editor's effective DOM root (shadow-aware).\n *\n * Falls back to a never-resolving `ref(null)` when no provider is in scope.\n * Components must guard with `v-if=\"popoverRoot\"` before teleporting —\n * with the fallback, isolated component tests that don't mount the editor\n * shell simply render nothing rather than blow up.\n *\n * Internal — not exported from the package entry.\n */\nexport function usePopoverRoot(): Ref<HTMLElement | null> {\n return inject(POPOVER_ROOT_KEY, ref<HTMLElement | null>(null));\n}\n","import { usePopoverRoot } from \"./usePopoverRoot\";\n\n/**\n * Coordinate helper for popovers that teleport into the shared popover root\n * (`.tpl-popover-root`, `position: relative`) and position themselves with\n * `position: absolute`.\n *\n * Callers compute a target point in **viewport** space — from\n * `getBoundingClientRect()`, `useElementBounding`, or a TipTap caret rect —\n * and `toLocal` converts it to coordinates **local to the popover root** by\n * subtracting the root's own viewport rect.\n *\n * Why this exists: a `position: fixed` popover resolves its coordinates\n * against the nearest ancestor that establishes a containing block — and any\n * `transform` / `filter` / `will-change` on an ancestor of the editor (a\n * consumer's scroll-parallax wrapper, route-transition, reveal animation…)\n * does exactly that, even while its computed `transform` reads `none` (a\n * running/animated transform still promotes the element). The fixed popover\n * would then be offset by that ancestor's position. Anchoring `absolute`\n * inside the (positioned) popover root instead makes the placement immune:\n * both points are read in viewport space, so the subtraction cancels the\n * ancestor offset, and the popover tracks the editor if the ancestor moves.\n *\n * Read `toLocal` inside a `computed` for a reactive result (recomputes with\n * its viewport-coordinate dependencies), or call it once when a popover opens.\n * Falls back to the viewport coordinates unchanged when no popover root is in\n * scope (isolated component tests / headless callers).\n *\n * Internal — not exported from the package entry.\n */\nexport function usePopoverPosition(): {\n toLocal: (viewport: { top: number; left: number }) => {\n top: number;\n left: number;\n };\n} {\n const popoverRoot = usePopoverRoot();\n function toLocal(viewport: { top: number; left: number }): {\n top: number;\n left: number;\n } {\n const rect = popoverRoot.value?.getBoundingClientRect();\n if (!rect) return { top: viewport.top, left: viewport.left };\n return { top: viewport.top - rect.top, left: viewport.left - rect.left };\n }\n return { toLocal };\n}\n"],"mappings":";;;AAkCA,IAAa,IAA0C;CACrD,SAAS,CAAC;CACV,aAAa;CACb,oBAAoB;CACpB,gBAAgB,CAAC;AACnB,GASM,IAAmB;AAwBzB,SAAgB,EACd,GACA,IAA2B,GACX;CAIhB,IAAI,GACA;CACJ,IAAI,GAAQ,YAAY,KAAA,GAEtB,AADA,IAAU,EAAS,SACnB,IAAiB,CAAC;MACb;EAEL,AADA,IAAU,CAAC,GACX,IAAiB,CAAC;EAClB,KAAK,IAAM,KAAS,EAAO,SACzB,AAAI,EAAiB,KAAK,CAAK,IAC7B,EAAQ,KAAK,CAAK,IAElB,EAAe,KAAK,CAAK;CAG/B;CAEA,IAAM,IAAuB,GAAQ,eAAe,EAAS,aAEvD,IAAqB,CAAC,KAAwB,EAAQ,WAAW;CAGvE,OAAO;EAAE;EAAS,aAFE,IAAqB,KAAO;EAEjB;EAAoB;CAAe;AACpE;;;AC7FA,SAAgB,EAAoB,GAAuB;CACzD,IAAM,IAAI,4CAA4C,KAAK,EAAM,KAAK,CAAC;CACvE,IAAI,CAAC,GAAG,OAAO;CACf,IAAM,KAAS,MAAc,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CACnE,OAAO,IAAI,EAAM,EAAE,EAAE,IAAI,EAAM,EAAE,EAAE,IAAI,EAAM,EAAE,EAAE;AACnD;AAiBA,SAAgB,EAAqB,GAAuB;CAC1D,IAAM,IAAM,EAAoB,CAAK,GAC/B,IAAQ,qCAAqC,KAAK,CAAG;CAO3D,OANI,IACK,IAAI,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,EAAM,KAAK,YAAY,IAEvF,kBAAkB,KAAK,CAAG,IACrB,EAAI,YAAY,IAElB;AACT;;;ACvBA,SAAgB,IAA0C;CACxD,OAAO,EAAO,GAAkB,EAAwB,IAAI,CAAC;AAC/D;;;ACUA,SAAgB,IAKd;CACA,IAAM,IAAc,EAAe;CACnC,SAAS,EAAQ,GAGf;EACA,IAAM,IAAO,EAAY,OAAO,sBAAsB;EAEtD,OADK,IACE;GAAE,KAAK,EAAS,MAAM,EAAK;GAAK,MAAM,EAAS,OAAO,EAAK;EAAK,IADrD;GAAE,KAAK,EAAS;GAAK,MAAM,EAAS;EAAK;CAE7D;CACA,OAAO,EAAE,WAAQ;AACnB"}
@@ -1,11 +1,11 @@
1
- import { A as e, C as t, H as n, M as r, V as i, Z as a, b as o, ct as s, g as c, h as l, it as u, j as d, l as f, m as p, n as ee, ot as m, p as h, r as g, s as _, st as v, v as y, x as b, y as x } from "./chunks/draggable-D840DIZB.js";
2
- import { r as S } from "./chunks/timeouts-Bmi_yePw.js";
3
- import { F as C, _ as w } from "./chunks/src-BCJna-6p.js";
4
- import { L as te, P as T, b as ne, t as re, w as E } from "./chunks/useEditorCore-B_vwuKdI.js";
5
- import { I as ie, M as ae } from "./chunks/icons-Cqxvwgz7.js";
1
+ import { A as e, C as t, H as n, M as r, V as i, Z as a, b as o, ct as s, g as c, h as l, it as u, j as d, l as f, m as p, n as ee, ot as m, p as h, r as g, s as _, st as v, v as y, x as b, y as x } from "./chunks/draggable-JcMTNF3U.js";
2
+ import { r as S } from "./chunks/timeouts-SsLMC4a3.js";
3
+ import { F as C, _ as w } from "./chunks/src-D5mfLR4V.js";
4
+ import { L as te, P as T, b as ne, t as re, w as E } from "./chunks/useEditorCore-ZqF7P99K.js";
5
+ import { I as ie, M as ae } from "./chunks/icons-Cp4ZIJw-.js";
6
6
  import { t as D } from "./chunks/_plugin-vue_export-helper-B3ysoDQm.js";
7
- import { a as oe, c as se, d as ce, f as le, i as ue, l as de, m as fe, n as pe, o as me, p as O, r as he, s as ge, t as _e, u as ve } from "./chunks/styles-BohMfIBd.js";
8
- import { n as ye, t as be } from "./chunks/MergeTagModeToggle-DmtR300G.js";
7
+ import { a as oe, c as se, d as ce, f as le, i as ue, l as de, m as fe, n as pe, o as me, p as O, r as he, s as ge, t as _e, u as ve } from "./chunks/styles--m9OCb_Z.js";
8
+ import { n as ye, t as be } from "./chunks/MergeTagModeToggle-DrdxQA0K.js";
9
9
  //#region src/Editor.vue?vue&type=script&setup=true&lang.ts
10
10
  var k = ["data-tpl-theme"], A = {
11
11
  class: "tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:shadow-[var(--tpl-shadow-md)] tpl:border-b tpl:border-[var(--tpl-border)]",
@@ -30,12 +30,12 @@ var k = ["data-tpl-theme"], A = {
30
30
  provider: b.config.savedBlocks,
31
31
  editor: S,
32
32
  onError: b.config.onError
33
- }) : null, T = o(() => import("./chunks/SavedBlocksPanels-1kVbqdpc.js")), E = b.config.testEmail ? le({
33
+ }) : null, T = o(() => import("./chunks/SavedBlocksPanels-CUffuDAC.js")), E = b.config.testEmail ? le({
34
34
  provider: b.config.testEmail,
35
35
  getContent: () => S.content.value,
36
36
  renderMjml: F,
37
37
  onError: b.config.onError
38
- }) : null, D = o(() => import("./chunks/TestEmailPanel-saOfnpjz.js")), P = re({
38
+ }) : null, D = o(() => import("./chunks/TestEmailPanel-BlmIKKSL.js")), P = re({
39
39
  editor: S,
40
40
  containerEl: C,
41
41
  config: {
@@ -330,7 +330,7 @@ function $(e) {
330
330
  async function Ne(e) {
331
331
  let n = typeof e.container == "string" ? document.querySelector(e.container) : e.container;
332
332
  if (!n) throw Error(`[Templatical] Container element not found: ${e.container}`);
333
- let { default: r } = await import("./chunks/CloudEditor-CeKaWWO5.js"), [i, o] = await Promise.all([G(e.locale ?? "en"), we(e.locale ?? "en")]), s = E(e.fonts);
333
+ let { default: r } = await import("./chunks/CloudEditor-D87qsqbZ.js"), [i, o] = await Promise.all([G(e.locale ?? "en"), we(e.locale ?? "en")]), s = E(e.fonts);
334
334
  $(n);
335
335
  let c = q(n, e.shadowDom ?? !0), l = a(null);
336
336
  return await new Promise((a, u) => {
@@ -1 +1 @@
1
- {"version":3,"file":"editor.js","names":[],"sources":["../../src/Editor.vue","../../src/Editor.vue","../../src/i18n/index.ts","../../src/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent, onMounted, onUnmounted, ref } from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport type { Translations } from \"./i18n\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw, Send } from \"@lucide/vue\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport ViewportToggle from \"./components/ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport PreviewToggle from \"./components/PreviewToggle.vue\";\nimport DarkModeToggle from \"./components/DarkModeToggle.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n}>();\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n templateDefaults: props.config.templateDefaults,\n});\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n onRequestMedia: props.config.onRequestMedia,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n onSave: props.config.onSave\n ? () =>\n props.config.onSave!(JSON.parse(JSON.stringify(editor.state.content)))\n : undefined,\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions: props.config.onChange\n ? {\n onChange: () =>\n props.config.onChange!(\n JSON.parse(JSON.stringify(editor.state.content)),\n ),\n }\n : null,\n capabilities: {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n },\n editorRoot: props.shadowRoot,\n});\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n });\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Header — absolute, full width, above everything -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:shadow-[var(--tpl-shadow-md)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n \"\n >\n <!-- Left: empty (reserved for embedder customization) -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2.5\"></div>\n\n <!-- Center: viewport + preview + dark mode -->\n <div class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\">\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <!-- Preview mode only. Merge tags are never substituted on the editing\n canvas, so offering the choice there would be a control with no\n effect. -->\n <MergeTagModeToggle\n v-if=\"editor.state.previewMode\"\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n\n <!-- Right: the test-email trigger when a sender is configured, else empty.\n Same slot CloudHeader puts its Test button in, so placement matches\n between the two editors. -->\n <div\n class=\"tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- A real button — border, surface fill and `shadow-xs`, the same subtle\n elevation `inputClass` uses — but coloured down: muted text rather\n than full-strength `--tpl-text`, and no primary tint until hover. It\n reads as a raised control against the header's translucent backdrop\n while staying clearly secondary; sending a test is not the page's\n primary action. Recipe follows `removeItemBtnClass` (border + surface\n + muted text) with elevation added. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] hover:tpl:bg-[var(--tpl-bg-hover)] hover:tpl:text-[var(--tpl-text)]\"\n @click=\"testEmail.open()\"\n >\n <Send :size=\"14\" :stroke-width=\"1.75\" />\n {{ core.t.testEmail.button }}\n </button>\n </div>\n </header>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80 tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)] tpl:border-[var(--tpl-warning)]\"\n style=\"backdrop-filter: blur(8px)\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n @select-block=\"editor.selectBlock\"\n />\n </div>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modal.\n Replaces the historical body-level teleport pattern so popups\n render inside the editor's effective DOM root (shadow-aware). -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent, onMounted, onUnmounted, ref } from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport type { Translations } from \"./i18n\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw, Send } from \"@lucide/vue\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport ViewportToggle from \"./components/ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport PreviewToggle from \"./components/PreviewToggle.vue\";\nimport DarkModeToggle from \"./components/DarkModeToggle.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n}>();\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n templateDefaults: props.config.templateDefaults,\n});\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n onRequestMedia: props.config.onRequestMedia,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n onSave: props.config.onSave\n ? () =>\n props.config.onSave!(JSON.parse(JSON.stringify(editor.state.content)))\n : undefined,\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions: props.config.onChange\n ? {\n onChange: () =>\n props.config.onChange!(\n JSON.parse(JSON.stringify(editor.state.content)),\n ),\n }\n : null,\n capabilities: {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n },\n editorRoot: props.shadowRoot,\n});\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n });\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Header — absolute, full width, above everything -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:shadow-[var(--tpl-shadow-md)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n \"\n >\n <!-- Left: empty (reserved for embedder customization) -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2.5\"></div>\n\n <!-- Center: viewport + preview + dark mode -->\n <div class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\">\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <!-- Preview mode only. Merge tags are never substituted on the editing\n canvas, so offering the choice there would be a control with no\n effect. -->\n <MergeTagModeToggle\n v-if=\"editor.state.previewMode\"\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n\n <!-- Right: the test-email trigger when a sender is configured, else empty.\n Same slot CloudHeader puts its Test button in, so placement matches\n between the two editors. -->\n <div\n class=\"tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- A real button — border, surface fill and `shadow-xs`, the same subtle\n elevation `inputClass` uses — but coloured down: muted text rather\n than full-strength `--tpl-text`, and no primary tint until hover. It\n reads as a raised control against the header's translucent backdrop\n while staying clearly secondary; sending a test is not the page's\n primary action. Recipe follows `removeItemBtnClass` (border + surface\n + muted text) with elevation added. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] hover:tpl:bg-[var(--tpl-bg-hover)] hover:tpl:text-[var(--tpl-text)]\"\n @click=\"testEmail.open()\"\n >\n <Send :size=\"14\" :stroke-width=\"1.75\" />\n {{ core.t.testEmail.button }}\n </button>\n </div>\n </header>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80 tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)] tpl:border-[var(--tpl-warning)]\"\n style=\"backdrop-filter: blur(8px)\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n @select-block=\"editor.selectBlock\"\n />\n </div>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modal.\n Replaces the historical body-level teleport pattern so popups\n render inside the editor's effective DOM root (shadow-aware). -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","/// <reference types=\"vite/client\" />\n\nimport type en from \"./locales/en\";\nimport type cloudEn from \"./locales/cloud/en\";\n\nexport type Translations = typeof en;\nexport type CloudTranslations = typeof cloudEn;\nexport type TranslationKey = keyof Translations;\n\n// Vite resolves these globs at build time. Adding a new locale file\n// automatically registers it — no array to keep in sync.\nconst ossModules = import.meta.glob<{ default: Translations }>(\n \"./locales/*.ts\",\n);\nconst cloudModules = import.meta.glob<{ default: CloudTranslations }>(\n \"./locales/cloud/*.ts\",\n);\n\nfunction localesFromGlob(modules: Record<string, unknown>): string[] {\n return Object.keys(modules)\n .map((path) => path.match(/\\/([^/]+)\\.ts$/)?.[1])\n .filter((locale): locale is string => Boolean(locale));\n}\n\nconst supportedLocales = localesFromGlob(ossModules);\nconst supportedCloudLocales = localesFromGlob(cloudModules);\n\nfunction canonicalize(locale: string): string {\n return locale.trim().replace(/_/g, \"-\").toLowerCase();\n}\n\nfunction findSupportedLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n const canonical = canonicalize(locale);\n return supported.find((s) => canonicalize(s) === canonical);\n}\n\n/**\n * Get the base language code from a locale string.\n * e.g., 'en-GB' -> 'en', 'de_DE' -> 'de'\n */\nexport function getBaseLocale(locale: string): string {\n return canonicalize(locale).split(\"-\")[0];\n}\n\nfunction tryResolveLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n return (\n findSupportedLocale(locale, supported) ??\n findSupportedLocale(getBaseLocale(locale), supported)\n );\n}\n\nfunction resolveLocale(locale: string, supported: string[]): string {\n return tryResolveLocale(locale, supported) ?? \"en\";\n}\n\n/**\n * Load OSS translations for a given locale.\n * Falls back to English if the locale is not supported.\n */\nexport async function loadTranslations(locale: string): Promise<Translations> {\n const target = resolveLocale(locale, supportedLocales);\n const loader = ossModules[`./locales/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/**\n * Load cloud translations for a given locale.\n * Cloud translations cover features available only via initCloud() —\n * AI, comments, collaboration, scoring, snapshots, plan limits, etc.\n * OSS contributors don't need to translate these; cloud locales fall back\n * to English independently of the OSS locale.\n */\nexport async function loadCloudTranslations(\n locale: string,\n): Promise<CloudTranslations> {\n const target = resolveLocale(locale, supportedCloudLocales);\n const loader = cloudModules[`./locales/cloud/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/** Check if a locale has OSS translations (matched by exact locale, then base). */\nexport function isLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedLocales) !== undefined;\n}\n\n/** Check if a locale has cloud translations (matched by exact locale, then base). */\nexport function isCloudLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedCloudLocales) !== undefined;\n}\n\n/** List of OSS-supported locales. */\nexport function getSupportedLocales(): string[] {\n return [...supportedLocales];\n}\n\n/** List of cloud-supported locales. May be a subset of OSS locales. */\nexport function getSupportedCloudLocales(): string[] {\n return [...supportedCloudLocales];\n}\n","// eslint-disable-next-line @typescript-eslint/triple-slash-reference -- ambient-module declaration must be loaded for cross-package typecheck (workspace path alias resolves to source)\n/// <reference path=\"./virtual-modules.d.ts\" />\nimport { createApp, h, ref, type App, type Ref } from \"vue\";\nimport { INIT_TIMEOUT_MS } from \"./constants/timeouts\";\nimport type {\n BlockDefaults,\n ColorsConfig,\n CustomBlock,\n CustomBlockDefinition,\n DisplayConditionsConfig,\n FontsConfig,\n LogicTagsConfig,\n MediaResult,\n MergeTagsConfig,\n SavedBlocksProvider,\n TestEmailProvider,\n SaveResult,\n Template,\n TemplateContent,\n TemplateDefaults,\n ThemeOverrides,\n UiTheme,\n} from \"@templatical/types\";\nimport { createDefaultTemplateContent, safeClone } from \"@templatical/types\";\nimport type { MediaRequestContext } from \"@templatical/media-library\";\n\nimport Editor from \"./Editor.vue\";\nimport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nimport { loadTranslations, loadCloudTranslations } from \"./i18n\";\nimport { useFonts } from \"./composables\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n// Compiled-CSS-as-string for shadow root adoption. The `virtual:editor-css`\n// module is owned by `scripts/inline-style-css-plugin.ts` — at build time it\n// captures every emitted CSS asset (Tailwind utilities + every `.vue` SFC\n// `<style>` block + `styles/index.css` rules) and replaces this import's\n// runtime value with the full library CSS string. In dev/test the plugin\n// returns `styles/index.css` source as a fallback.\n//\n// Separate concern from the side-effecting `import \"./styles/index.css\"` in\n// `Editor.vue` / `CloudEditor.vue`, which injects styles into `document.head`\n// for light-DOM mode and survives untouched.\n//\n// Ambient declaration for `virtual:editor-css` lives in `virtual-modules.d.ts`\n// referenced at the top of this file via triple-slash so it's visible to any\n// consumer typechecking through the workspace path alias.\nimport editorStylesInline from \"virtual:editor-css\";\n\n// ---------------------------------------------------------------------------\n// OSS config + return types\n// ---------------------------------------------------------------------------\n\nexport interface TemplaticalEditorConfig {\n /**\n * Where to mount the editor — a CSS selector or an `HTMLElement`.\n *\n * Layout caveat: do NOT apply `transform`, `filter`, `perspective`, or\n * `will-change` to an ancestor of this element. Each establishes a CSS\n * containing block for `position: fixed`, which offsets the editor's\n * floating UI (color pickers, rich-text toolbars) and its drag ghost away\n * from their anchor — even when the transform's computed value is `none`\n * (an active/animated transform still promotes the element). For a\n * scroll/entrance effect on a wrapper, animate `opacity` instead of\n * `transform`.\n */\n container: string | HTMLElement;\n content?: TemplateContent;\n\n /**\n * Mount the editor inside a Shadow DOM (open mode) for CSS isolation\n * from the host page. Defaults to `true` — host stylesheets cannot\n * cascade past the shadow boundary into editor elements (`p`, `a`,\n * `input`, etc.), and editor utility classes never collide with host\n * class names.\n *\n * Set to `false` to mount in light DOM. Opt out when:\n * - Your host integration uses `document.querySelector` to reach\n * editor internals (with shadow DOM, use `container.shadowRoot\n * .querySelector(...)` instead).\n * - You need to support Firefox <101 or Safari <16.4, which lack the\n * `adoptedStyleSheets` API the shadow path relies on.\n *\n * Light-mode consumers should keep this set to `false` explicitly so\n * future SDK changes don't silently flip the default again.\n *\n * @default true\n */\n shadowDom?: boolean;\n\n onChange?: (content: TemplateContent) => void;\n onSave?: (content: TemplateContent) => void;\n onError?: (error: Error) => void;\n\n onRequestMedia?: OnRequestMedia;\n\n /**\n * Display-only resolver for image `src` values. The canvas calls this to\n * obtain a preview URL for a src the user entered; the content model (and\n * `toMjml()` output) always keeps the canonical value. Return `null` — or\n * the input value — to use the src as-is.\n *\n * Use this when templates reference images by a value that isn't directly\n * displayable, e.g. plain file names resolved to ephemeral `blob:` URLs\n * from local storage:\n *\n * ```ts\n * resolveImageUrl: async (src) => {\n * const file = await fileStore.lookup(src);\n * return file ? URL.createObjectURL(file) : null;\n * }\n * ```\n *\n * The resolver is called once per committed src value (typing in the src\n * input is debounced, so partial values never reach it) and results are\n * cached per src for the editor's lifetime — including failures, which\n * fall back to displaying the src verbatim. Note the caching applies to\n * transient failures too: a src that failed to resolve stays unresolved\n * until the editor is re-initialized (a re-resolve hook may be added\n * later). Applies to image srcs, design-time placeholder previews, and\n * explicit video thumbnails; auto-derived provider thumbnails (YouTube/\n * Vimeo) are already real URLs and are never resolved.\n */\n resolveImageUrl?: ResolveImageUrl;\n\n mergeTags?: MergeTagsConfig;\n /**\n * Standalone logic tags — control-flow tokens (`{% else %}`) and open/close\n * pairs (`{% if %}` … `{% endif %}`) inserted from a dedicated \"Insert logic\"\n * button. Separate from `mergeTags`; typed/pasted logic is highlighted\n * regardless of this config.\n */\n logicTags?: LogicTagsConfig;\n displayConditions?: DisplayConditionsConfig;\n customBlocks?: CustomBlockDefinition[];\n\n /**\n * Allowlist + order for the block palette. When set, only the listed block\n * types appear in the sidebar palette, in exactly this order — unlisted\n * built-ins (e.g. `video`, `table`) are hidden. When omitted, the full\n * default palette is shown.\n *\n * Reference built-in blocks by their bare type (`'section'`, `'image'`, …)\n * and custom blocks by their `custom:`-prefixed type (`'custom:qrcode'`),\n * so they can be interleaved freely:\n *\n * ```ts\n * paletteBlocks: ['section', 'title', 'image', 'custom:qrcode', 'button']\n * ```\n *\n * Unknown entries — a typo, an unregistered custom block, or `countdown`\n * outside a Cloud plan — are logged with a warning and skipped. Filtering\n * the palette never affects rendering: existing content using a hidden\n * block type still renders correctly.\n */\n paletteBlocks?: string[];\n\n /**\n * Render each HTML block's raw content as a live preview in the editor\n * canvas instead of the static placeholder card. **Off by default.**\n *\n * ```ts\n * htmlBlockPreview: true // shorthand for { enabled: true }\n * htmlBlockPreview: { enabled: true }\n * ```\n *\n * The content is rendered verbatim inside a sandboxed `<iframe>`\n * (`sandbox=\"allow-same-origin\"`, **no** `allow-scripts`): scripts and\n * inline event handlers never execute and styles can't bleed into the rest\n * of the editor. This is a preview-only setting — the MJML/HTML export path\n * renders HTML blocks regardless of it.\n *\n * @default false\n */\n htmlBlockPreview?: HtmlBlockPreviewConfig;\n\n fonts?: FontsConfig;\n\n /**\n * Editor-wide color-picker palette. `presets` appear as a clickable grid in\n * every color picker popover (block toolbars, template settings, rich-text\n * color, custom-block color fields) — clicking one applies it, and the preset\n * matching the current value is marked selected.\n *\n * ```ts\n * colors: {\n * presets: ['#0b5cff', '#111827', '#6b7280', '#ffffff'],\n * allowCustom: false, // lock authors to the presets above\n * }\n * ```\n *\n * `allowCustom` defaults to `true`. Set it to `false` (together with\n * `presets`) to hide the wheel and hex input so authors can only pick from the\n * palette — useful when embedding the editor as a white-label / brand-kit\n * tool. `allowCustom: false` is ignored, with a warning, when no `presets` are\n * configured, since that would leave the picker with no way to set a color.\n */\n colors?: ColorsConfig;\n\n /**\n * Storage backend for **saved blocks** — reusable groups of blocks a user\n * saves from the canvas and re-inserts into other templates.\n *\n * The editor owns the UI (a save action on each block, a searchable browser\n * with preview, insert-at-position, rename, delete); you own persistence.\n * Implement the four methods of `SavedBlocksProvider` against your own API,\n * or use the bundled browser-local provider for demos and prototypes:\n *\n * ```ts\n * import { init, createLocalStorageSavedBlocksProvider } from \"@templatical/editor\";\n *\n * init({ container, savedBlocks: createLocalStorageSavedBlocksProvider() });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * save action, no sidebar entry, and none of its UI code is downloaded.\n *\n * Not to be confused with `customBlocks`, which registers developer-defined\n * block *types* with their own templates and field schemas.\n */\n savedBlocks?: SavedBlocksProvider;\n\n /**\n * Sending backend for **test emails** — letting a user mail themselves the\n * template they're editing.\n *\n * The editor owns the trigger, the dialog, recipient validation and the\n * sending/success/error states; you own delivery. One method is enough:\n *\n * ```ts\n * init({\n * container,\n * testEmail: {\n * send: ({ recipient, content }) =>\n * fetch(\"/api/test-email\", {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify({ recipient, content }),\n * }).then((r) => {\n * if (!r.ok) throw new Error(\"Could not send the test email\");\n * }),\n * },\n * });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * button, and none of its UI code is downloaded.\n *\n * `allowedRecipients` restricts the picker but is **not** a security boundary:\n * it lives in the user's browser. Validate the recipient on your server.\n */\n testEmail?: TestEmailProvider;\n\n blockDefaults?: BlockDefaults;\n templateDefaults?: TemplateDefaults;\n\n theme?: ThemeOverrides;\n uiTheme?: UiTheme;\n locale?: string;\n\n /**\n * Show the \"Powered by Templatical\" footer. Defaults to `true`.\n * Set to `false` to hide the footer (no attribution required by the license).\n */\n branding?: boolean;\n\n /**\n * Show a \"use a larger screen\" notice instead of the editor chrome on\n * viewports narrower than ~768px. Defaults to `true`.\n *\n * The drag-and-drop editor is a desktop-class tool — the block palette,\n * canvas, and properties panel can't lay out usably on a phone, and touch\n * dragging is impractical. Rather than render a broken, cramped layout, the\n * editor shows a clear message below the breakpoint.\n *\n * Set to `false` if you handle small screens yourself (e.g. you embed the\n * editor in a deliberately narrow desktop pane). The check is viewport-based,\n * so it targets actual small devices, not narrow containers on a wide screen.\n *\n * @default true\n */\n smallScreenNotice?: boolean;\n\n /**\n * Template linter (`@templatical/quality`) configuration. Runs every\n * linter exported by the package (accessibility + structure).\n *\n * - When unset, the linter loads on demand once the user opens the panel.\n * - When `disabled: true`, the optional peer is never imported (saves the\n * chunk download) and the sidebar tab + inline badges are suppressed.\n * - `rules`/`thresholds` follow the shape exported by `@templatical/quality`.\n */\n lint?: import(\"@templatical/quality\").LintOptions;\n}\n\n/** Function type for media browser requests, used by both OSS and Cloud editors. */\nexport type OnRequestMedia = (\n context?: MediaRequestContext,\n) => Promise<MediaResult | null>;\n\ninterface TemplaticalEditorBase {\n getContent(): TemplateContent;\n setContent(content: TemplateContent): void;\n setTheme(theme: UiTheme): void;\n unmount(): void;\n}\n\nexport interface TemplaticalEditor extends TemplaticalEditorBase {\n /**\n * Render the current template to an MJML string. Resolves custom blocks\n * via the editor's internal block registry. Throws if the optional\n * `@templatical/renderer` package is not installed.\n */\n toMjml(): Promise<string>;\n /**\n * Render a single custom block to its HTML representation, using the\n * registered custom block definition's template and the block's current\n * field values. Exposed for headless callers that want to reuse the\n * editor's renderer (e.g., to drive `@templatical/renderer`'s\n * `renderCustomBlock` option from outside the editor instance).\n */\n renderCustomBlock(block: CustomBlock): Promise<string>;\n /**\n * Look up the definition-level `stylesheet` for a registered custom block\n * type. Returns the raw CSS string, or `undefined` when the type is unknown\n * or the definition has no stylesheet. Exposed for headless callers that\n * want to drive `@templatical/renderer`'s `getCustomBlockStylesheet` option\n * from outside the editor instance.\n */\n getCustomBlockStylesheet(customType: string): string | undefined;\n}\n\n/**\n * Cloud editor does not expose `toMjml` or `renderCustomBlock`: the cloud\n * backend performs MJML conversion server-side with additional processing\n * (e.g., signed image URLs, attachment handling) that isn't available client\n * side. Use the cloud `save()` flow to persist content; the backend handles\n * MJML/HTML export from there.\n */\nexport interface TemplaticalCloudEditor extends TemplaticalEditorBase {\n create(content?: TemplateContent): Promise<Template>;\n load(templateId: string): Promise<Template>;\n save(): Promise<SaveResult>;\n}\n\n// ---------------------------------------------------------------------------\n// Shadow root helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Module-cached `CSSStyleSheet` built once from the inline editor CSS string.\n * `adoptedStyleSheets` accepts the same sheet object across multiple shadow\n * roots — sharing one sheet costs zero per-instance memory, regardless of\n * how many editors mount.\n */\nlet cachedEditorStyleSheet: CSSStyleSheet | null = null;\nfunction getEditorStyleSheet(): CSSStyleSheet {\n if (cachedEditorStyleSheet === null) {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(editorStylesInline);\n cachedEditorStyleSheet = sheet;\n }\n return cachedEditorStyleSheet;\n}\n\ninterface MountTarget {\n target: Element;\n shadowRoot: ShadowRoot | null;\n /**\n * Disposer for any dev-mode side effects attached to the shadow root\n * (currently: the document.head `<style>` mirror's MutationObserver).\n * Always safe to call — no-op when no side effects were registered.\n */\n cleanup: () => void;\n}\n\n/**\n * Dev-only: mirror every `<style>` tag in `document.head` into the\n * shadow root's `adoptedStyleSheets`, and observe `document.head` so\n * Vite's HMR-injected style updates flow through to the shadow root\n * automatically.\n *\n * Background: Vite dev injects each `.vue` `<style scoped>` block as a\n * separate `<style>` element in `document.head` via HMR. Those don't\n * cross the shadow boundary, so a shadow-mounted editor in dev would\n * render with only the bundled `styles/index.css` rules — every SFC\n * scoped style (block selection outlines, sidebar layout, etc.) missing.\n *\n * In production builds, `inline-style-css-plugin.ts` captures every\n * emitted CSS asset at `generateBundle` time and inlines the full\n * library CSS as a single string adopted by the shadow root. This\n * dev-only mirror does the same thing at runtime by observing whatever\n * Vite ends up injecting.\n *\n * The dead-code-elimination on `import.meta.env.DEV` ensures the\n * observer + filter logic is stripped from production bundles entirely.\n *\n * Caveats:\n * - In a real consumer's Vite-dev environment, this would also adopt\n * the consumer's page-level styles (whatever they put in\n * `document.head`). That's harmless when consumers install the\n * editor from npm dist (the dev branch is dead-coded out). It only\n * matters when a consumer source-resolves this package, which is\n * unusual outside this repo's own playground.\n * - `replaceSync` strips `@import` rules per the CSSOM spec. Styles\n * containing `@import` are skipped silently (the catch below). The\n * primary bundled sheet covers Tailwind imports already, so this\n * should never matter in practice.\n */\nfunction attachDevStyleMirror(shadowRoot: ShadowRoot): () => void {\n if (!import.meta.env?.DEV) return () => {};\n\n function buildSheets(): CSSStyleSheet[] {\n const sheets: CSSStyleSheet[] = [];\n // shadow-ok: dev-only HMR style mirror (production path is plugin-driven via adoptedStyleSheets)\n document.head.querySelectorAll(\"style\").forEach((el) => {\n const text = el.textContent;\n if (!text) return;\n try {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(text);\n sheets.push(sheet);\n } catch {\n // Skip styles that contain disallowed constructs (e.g. `@import`).\n }\n });\n return sheets;\n }\n\n function refresh(): void {\n // Keep the editor's primary (bundled) sheet first so its declarations\n // are the cascade fallback; dev sheets adopted after may override them\n // — same precedence model as production where everything ends up in\n // one stylesheet anyway.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet(), ...buildSheets()];\n }\n\n refresh();\n\n const observer = new MutationObserver(() => refresh());\n // childList catches new/removed <style> tags; characterData + subtree\n // catches Vite HMR mutating an existing style's textContent in place.\n // shadow-ok: dev-only HMR style mirror observer\n observer.observe(document.head, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n\n return () => observer.disconnect();\n}\n\n/**\n * Resolve where Vue should mount: directly on the consumer's container\n * (light-DOM mode) or on a fresh `<div>` inside a newly-attached open\n * shadow root (shadow mode). In shadow mode, also attaches the editor's\n * cached `CSSStyleSheet` to the root so chrome renders with the right\n * styles inside the boundary, plus a dev-only mirror that observes\n * `document.head` (see `attachDevStyleMirror`).\n *\n * Idempotent: a second call on the same container reuses any existing\n * shadow root, clearing its contents so a prior mount's stale DOM doesn't\n * accumulate.\n */\nfunction resolveMountTarget(\n container: Element,\n shadowDom: boolean | undefined,\n): MountTarget {\n if (!shadowDom) {\n return { target: container, shadowRoot: null, cleanup: () => {} };\n }\n\n const shadowRoot =\n container.shadowRoot ?? container.attachShadow({ mode: \"open\" });\n\n // Adopt the editor stylesheet. Idempotent — repeated assignment of the\n // same sheet is fine. Dev mirror (below) will overwrite this with the\n // primary sheet + mirrored sheets, then keep them in sync.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet()];\n\n // Clear stale content from a prior mount (re-init on same container).\n while (shadowRoot.firstChild) {\n shadowRoot.removeChild(shadowRoot.firstChild);\n }\n\n const host = document.createElement(\"div\");\n host.className = \"tpl-editor-host\";\n // Match the container's layout. Without an explicit size the host is a\n // default block element (height: auto) and the editor's `tpl:h-full`\n // template root collapses to content height because the height-100% chain\n // breaks at the shadow boundary. `width: 100%` is redundant for block\n // elements but harmless and explicit.\n host.style.cssText = \"display:block;height:100%;width:100%;\";\n shadowRoot.appendChild(host);\n\n const cleanup = attachDevStyleMirror(shadowRoot);\n\n return { target: host, shadowRoot, cleanup };\n}\n\n// ---------------------------------------------------------------------------\n// OSS init — sync\n// ---------------------------------------------------------------------------\n\ninterface OssEntry {\n app: App;\n editorRef: Ref<InstanceType<typeof Editor> | null>;\n /** Tear down dev-mode side effects (style mirror observer, etc.). */\n cleanup: () => void;\n}\n\n// Per-container registry so two `init()` calls with different containers\n// produce independent editor instances (multi-instance support). Re-init\n// on the same container still auto-unmounts the previous instance.\nconst ossEntries = new Map<Element, OssEntry>();\n\n// \"Last init'd\" container preserves the legacy single-instance behavior\n// of the top-level `unmount()` export — the playground (and other one-\n// editor consumers) calls bare `unmount()` and expects it to tear down\n// whatever was most recently mounted.\nlet lastOssContainer: Element | null = null;\n\nfunction unmountOssContainer(container: Element): void {\n const entry = ossEntries.get(container);\n if (!entry) return;\n entry.cleanup();\n entry.app.unmount();\n ossEntries.delete(container);\n if (lastOssContainer === container) {\n lastOssContainer = null;\n }\n}\n\nexport async function init(\n config: TemplaticalEditorConfig,\n): Promise<TemplaticalEditor> {\n const container =\n typeof config.container === \"string\"\n ? document.querySelector(config.container)\n : config.container;\n\n if (!container) {\n throw new Error(\n `[Templatical] Container element not found: ${config.container}`,\n );\n }\n\n // Load translations before mounting so child components can use useI18n synchronously\n const translations = await loadTranslations(config.locale ?? \"en\");\n\n // Create fonts manager to pass to Editor\n const fontsManager = useFonts(config.fonts);\n\n // Auto-unmount any prior instance on the SAME container *after* awaits\n // — checking before the await would let two concurrent init() calls\n // both pass the guard and orphan the first-mounted app on this\n // container.\n unmountOssContainer(container);\n\n const mount = resolveMountTarget(container, config.shadowDom ?? true);\n const editorRef: Ref<InstanceType<typeof Editor> | null> = ref(null);\n\n const app = createApp({\n setup() {\n return () =>\n h(Editor, {\n config,\n translations,\n fontsManager,\n shadowRoot: mount.shadowRoot ?? undefined,\n ref: editorRef,\n });\n },\n });\n\n app.mount(mount.target);\n\n ossEntries.set(container, { app, editorRef, cleanup: mount.cleanup });\n lastOssContainer = container;\n\n const instance: TemplaticalEditor = {\n getContent() {\n // safeClone (not a naked JSON.stringify): a drag inside a section can\n // leave a Sortable expando cycle reachable from live content, which a\n // plain stringify chokes on (#203). The clone drops the back-ref.\n if (editorRef.value) {\n return safeClone(editorRef.value.getContent());\n }\n return safeClone(config.content ?? createDefaultTemplateContent());\n },\n setContent(content: TemplateContent) {\n if (editorRef.value) {\n editorRef.value.setContent(content);\n }\n config.content = content;\n },\n setTheme(theme: UiTheme) {\n if (editorRef.value) {\n editorRef.value.setTheme(theme);\n }\n },\n unmount: () => unmountOssContainer(container),\n renderCustomBlock(block: CustomBlock) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.renderCustomBlock(block);\n },\n getCustomBlockStylesheet(customType: string) {\n // Pre-mount: registry is empty, so no definition has a stylesheet to\n // resolve. Returning undefined here matches the \"unknown type\" branch\n // and keeps the renderer's `getCustomBlockStylesheet` resolver total.\n if (!editorRef.value) {\n return undefined;\n }\n return editorRef.value.getCustomBlockStylesheet(customType);\n },\n toMjml: () => toMjmlForInstance(instance),\n };\n\n return instance;\n}\n\n// ---------------------------------------------------------------------------\n// Cloud init — async, flat config, tree-shaken when not called\n// ---------------------------------------------------------------------------\n\ninterface CloudEntry {\n app: App;\n editorRef: Ref<InstanceType<\n typeof import(\"./cloud/CloudEditor.vue\").default\n > | null>;\n cleanup: () => void;\n}\n\nconst cloudEntries = new Map<Element, CloudEntry>();\nlet lastCloudContainer: Element | null = null;\n\nfunction unmountCloudContainer(container: Element): void {\n const entry = cloudEntries.get(container);\n if (!entry) return;\n entry.cleanup();\n entry.app.unmount();\n cloudEntries.delete(container);\n if (lastCloudContainer === container) {\n lastCloudContainer = null;\n }\n}\n\nexport async function initCloud(\n config: import(\"./cloud/CloudEditor.vue\").TemplaticalCloudEditorConfig,\n): Promise<TemplaticalCloudEditor> {\n const container =\n typeof config.container === \"string\"\n ? document.querySelector(config.container)\n : config.container;\n\n if (!container) {\n throw new Error(\n `[Templatical] Container element not found: ${config.container}`,\n );\n }\n\n // Dynamic import — CloudEditor.vue is tree-shaken from the OSS bundle\n const { default: CloudEditor } = await import(\"./cloud/CloudEditor.vue\");\n\n // Load OSS + cloud translations in parallel so child components can use\n // useI18n / useCloudI18n synchronously\n const [translations, cloudTranslations] = await Promise.all([\n loadTranslations(config.locale ?? \"en\"),\n loadCloudTranslations(config.locale ?? \"en\"),\n ]);\n\n // Create fonts manager to pass to CloudEditor\n const fontsManager = useFonts(config.fonts);\n\n // Auto-unmount any prior instance on the SAME container *after* awaits\n // — checking before the await would let two concurrent initCloud()\n // calls both pass the guard and orphan the first-mounted app on this\n // container.\n unmountCloudContainer(container);\n\n const mount = resolveMountTarget(container, config.shadowDom ?? true);\n const cloudEditorRef: CloudEntry[\"editorRef\"] = ref(null);\n\n // Promise that resolves when CloudEditor emits 'ready'\n const readyPromise = new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"[Templatical] Cloud editor initialization timed out\"));\n }, INIT_TIMEOUT_MS);\n\n const app = createApp({\n setup() {\n return () =>\n h(CloudEditor, {\n config,\n translations,\n cloudTranslations,\n fontsManager,\n shadowRoot: mount.shadowRoot ?? undefined,\n ref: cloudEditorRef,\n onReady: () => {\n clearTimeout(timeout);\n resolve();\n },\n });\n },\n });\n\n app.mount(mount.target);\n\n cloudEntries.set(container, {\n app,\n editorRef: cloudEditorRef,\n cleanup: mount.cleanup,\n });\n lastCloudContainer = container;\n });\n\n await readyPromise;\n\n const instance: TemplaticalCloudEditor = {\n getContent() {\n // safeClone (not a naked JSON.stringify): a drag inside a section can\n // leave a Sortable expando cycle reachable from live content, which a\n // plain stringify chokes on (#203). The clone drops the back-ref.\n if (cloudEditorRef.value) {\n return safeClone(cloudEditorRef.value.getContent());\n }\n return safeClone(config.content ?? createDefaultTemplateContent());\n },\n setContent(content: TemplateContent) {\n if (cloudEditorRef.value) {\n cloudEditorRef.value.setContent(content);\n }\n },\n setTheme(theme: UiTheme) {\n if (cloudEditorRef.value) {\n cloudEditorRef.value.setTheme(theme);\n }\n },\n unmount: () => unmountCloudContainer(container),\n create(content?: TemplateContent) {\n if (!cloudEditorRef.value) {\n return Promise.reject(\n new Error(\"[Templatical] Cloud editor not ready\"),\n );\n }\n return cloudEditorRef.value.create(content);\n },\n load(templateId: string) {\n if (!cloudEditorRef.value) {\n return Promise.reject(\n new Error(\"[Templatical] Cloud editor not ready\"),\n );\n }\n return cloudEditorRef.value.load(templateId);\n },\n save() {\n if (!cloudEditorRef.value) {\n return Promise.reject(\n new Error(\"[Templatical] Cloud editor not ready\"),\n );\n }\n return cloudEditorRef.value.save();\n },\n };\n\n return instance;\n}\n\n// ---------------------------------------------------------------------------\n// Unmount helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Unmount the most-recently-created OSS editor. Single-instance legacy\n * API — callers managing multiple editors should use `instance.unmount()`\n * from each returned object, which targets the specific container.\n */\nexport function unmount(): void {\n if (lastOssContainer) {\n unmountOssContainer(lastOssContainer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TemplaticalCloudEditorConfig } from \"./cloud/CloudEditor.vue\";\nexport type {\n BlockDefaults,\n TemplateContent,\n TemplateDefaults,\n ThemeOverrides,\n UiTheme,\n MergeTagsConfig,\n LogicTagsConfig,\n LogicTag,\n LogicPair,\n DisplayConditionsConfig,\n CustomBlockDefinition,\n ViewportSize,\n ColorsConfig,\n CustomFont,\n FontsConfig,\n SavedBlock,\n SavedBlocksListParams,\n SavedBlocksProvider,\n SaveResult,\n Template,\n TestEmailPayload,\n TestEmailProvider,\n} from \"@templatical/types\";\n\n// Bundled browser-local saved-blocks provider. Re-exported here (rather than\n// leaving it to `@templatical/core`) because consumers install only this\n// package — core is bundled inline and isn't resolvable on their side.\nexport { createLocalStorageSavedBlocksProvider } from \"@templatical/core\";\nexport type { LocalStorageSavedBlocksOptions } from \"@templatical/core\";\n\nexport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nexport type { UseFontsReturn, FontOption } from \"./composables/useFonts\";\nexport { useFonts } from \"./composables/useFonts\";\nexport type { EditorCapabilities } from \"./types/editor-capabilities\";\nexport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n\nexport {\n getSupportedLocales,\n getSupportedCloudLocales,\n isLocaleSupported,\n isCloudLocaleSupported,\n getBaseLocale,\n} from \"./i18n\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"editor.js","names":[],"sources":["../../src/Editor.vue","../../src/Editor.vue","../../src/i18n/index.ts","../../src/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent, onMounted, onUnmounted, ref } from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport type { Translations } from \"./i18n\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw, Send } from \"@lucide/vue\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport ViewportToggle from \"./components/ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport PreviewToggle from \"./components/PreviewToggle.vue\";\nimport DarkModeToggle from \"./components/DarkModeToggle.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n}>();\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n templateDefaults: props.config.templateDefaults,\n});\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n onRequestMedia: props.config.onRequestMedia,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n onSave: props.config.onSave\n ? () =>\n props.config.onSave!(JSON.parse(JSON.stringify(editor.state.content)))\n : undefined,\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions: props.config.onChange\n ? {\n onChange: () =>\n props.config.onChange!(\n JSON.parse(JSON.stringify(editor.state.content)),\n ),\n }\n : null,\n capabilities: {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n },\n editorRoot: props.shadowRoot,\n});\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n });\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Header — absolute, full width, above everything -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:shadow-[var(--tpl-shadow-md)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n \"\n >\n <!-- Left: empty (reserved for embedder customization) -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2.5\"></div>\n\n <!-- Center: viewport + preview + dark mode -->\n <div class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\">\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <!-- Preview mode only. Merge tags are never substituted on the editing\n canvas, so offering the choice there would be a control with no\n effect. -->\n <MergeTagModeToggle\n v-if=\"editor.state.previewMode\"\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n\n <!-- Right: the test-email trigger when a sender is configured, else empty.\n Same slot CloudHeader puts its Test button in, so placement matches\n between the two editors. -->\n <div\n class=\"tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- A real button — border, surface fill and `shadow-xs`, the same subtle\n elevation `inputClass` uses — but coloured down: muted text rather\n than full-strength `--tpl-text`, and no primary tint until hover. It\n reads as a raised control against the header's translucent backdrop\n while staying clearly secondary; sending a test is not the page's\n primary action. Recipe follows `removeItemBtnClass` (border + surface\n + muted text) with elevation added. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] hover:tpl:bg-[var(--tpl-bg-hover)] hover:tpl:text-[var(--tpl-text)]\"\n @click=\"testEmail.open()\"\n >\n <Send :size=\"14\" :stroke-width=\"1.75\" />\n {{ core.t.testEmail.button }}\n </button>\n </div>\n </header>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80 tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)] tpl:border-[var(--tpl-warning)]\"\n style=\"backdrop-filter: blur(8px)\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n @select-block=\"editor.selectBlock\"\n />\n </div>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modal.\n Replaces the historical body-level teleport pattern so popups\n render inside the editor's effective DOM root (shadow-aware). -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent, onMounted, onUnmounted, ref } from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport type { Translations } from \"./i18n\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw, Send } from \"@lucide/vue\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport ViewportToggle from \"./components/ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport PreviewToggle from \"./components/PreviewToggle.vue\";\nimport DarkModeToggle from \"./components/DarkModeToggle.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n}>();\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n templateDefaults: props.config.templateDefaults,\n});\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n onRequestMedia: props.config.onRequestMedia,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n onSave: props.config.onSave\n ? () =>\n props.config.onSave!(JSON.parse(JSON.stringify(editor.state.content)))\n : undefined,\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions: props.config.onChange\n ? {\n onChange: () =>\n props.config.onChange!(\n JSON.parse(JSON.stringify(editor.state.content)),\n ),\n }\n : null,\n capabilities: {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n },\n editorRoot: props.shadowRoot,\n});\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n });\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full tpl:overflow-hidden\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Header — absolute, full width, above everything -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:shadow-[var(--tpl-shadow-md)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 80%, transparent);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n \"\n >\n <!-- Left: empty (reserved for embedder customization) -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2.5\"></div>\n\n <!-- Center: viewport + preview + dark mode -->\n <div class=\"tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\">\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n <!-- Preview mode only. Merge tags are never substituted on the editing\n canvas, so offering the choice there would be a control with no\n effect. -->\n <MergeTagModeToggle\n v-if=\"editor.state.previewMode\"\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n\n <!-- Right: the test-email trigger when a sender is configured, else empty.\n Same slot CloudHeader puts its Test button in, so placement matches\n between the two editors. -->\n <div\n class=\"tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- A real button — border, surface fill and `shadow-xs`, the same subtle\n elevation `inputClass` uses — but coloured down: muted text rather\n than full-strength `--tpl-text`, and no primary tint until hover. It\n reads as a raised control against the header's translucent backdrop\n while staying clearly secondary; sending a test is not the page's\n primary action. Recipe follows `removeItemBtnClass` (border + surface\n + muted text) with elevation added. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] hover:tpl:bg-[var(--tpl-bg-hover)] hover:tpl:text-[var(--tpl-text)]\"\n @click=\"testEmail.open()\"\n >\n <Send :size=\"14\" :stroke-width=\"1.75\" />\n {{ core.t.testEmail.button }}\n </button>\n </div>\n </header>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n 'tpl:top-14',\n ]\"\n >\n <!-- Restore hidden blocks button -->\n <div class=\"tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <Transition name=\"tpl-restore-btn\">\n <button\n v-if=\"core.conditionPreview.hasHiddenBlocks.value\"\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:-translate-x-1/2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-full tpl:border tpl:px-3.5 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:whitespace-nowrap tpl:shadow-md tpl:hover:opacity-80 tpl:bg-[var(--tpl-warning-light)] tpl:text-[var(--tpl-warning)] tpl:border-[var(--tpl-warning)]\"\n style=\"backdrop-filter: blur(8px)\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"13\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n <div class=\"tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"editor.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n @select-block=\"editor.selectBlock\"\n />\n </div>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[\n editor.state.previewMode\n ? 'tpl:left-0 tpl:right-0'\n : 'tpl:left-12 tpl:right-[320px]',\n ]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n @update-block=\"\n (updates) => editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modal.\n Replaces the historical body-level teleport pattern so popups\n render inside the editor's effective DOM root (shadow-aware). -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-restore-btn-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-restore-btn-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-restore-btn-enter-from,\n.tpl-restore-btn-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-restore-btn-enter-to,\n.tpl-restore-btn-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","/// <reference types=\"vite/client\" />\n\nimport type en from \"./locales/en\";\nimport type cloudEn from \"./locales/cloud/en\";\n\nexport type Translations = typeof en;\nexport type CloudTranslations = typeof cloudEn;\nexport type TranslationKey = keyof Translations;\n\n// Vite resolves these globs at build time. Adding a new locale file\n// automatically registers it — no array to keep in sync.\nconst ossModules = import.meta.glob<{ default: Translations }>(\n \"./locales/*.ts\",\n);\nconst cloudModules = import.meta.glob<{ default: CloudTranslations }>(\n \"./locales/cloud/*.ts\",\n);\n\nfunction localesFromGlob(modules: Record<string, unknown>): string[] {\n return Object.keys(modules)\n .map((path) => path.match(/\\/([^/]+)\\.ts$/)?.[1])\n .filter((locale): locale is string => Boolean(locale));\n}\n\nconst supportedLocales = localesFromGlob(ossModules);\nconst supportedCloudLocales = localesFromGlob(cloudModules);\n\nfunction canonicalize(locale: string): string {\n return locale.trim().replace(/_/g, \"-\").toLowerCase();\n}\n\nfunction findSupportedLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n const canonical = canonicalize(locale);\n return supported.find((s) => canonicalize(s) === canonical);\n}\n\n/**\n * Get the base language code from a locale string.\n * e.g., 'en-GB' -> 'en', 'de_DE' -> 'de'\n */\nexport function getBaseLocale(locale: string): string {\n return canonicalize(locale).split(\"-\")[0];\n}\n\nfunction tryResolveLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n return (\n findSupportedLocale(locale, supported) ??\n findSupportedLocale(getBaseLocale(locale), supported)\n );\n}\n\nfunction resolveLocale(locale: string, supported: string[]): string {\n return tryResolveLocale(locale, supported) ?? \"en\";\n}\n\n/**\n * Load OSS translations for a given locale.\n * Falls back to English if the locale is not supported.\n */\nexport async function loadTranslations(locale: string): Promise<Translations> {\n const target = resolveLocale(locale, supportedLocales);\n const loader = ossModules[`./locales/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/**\n * Load cloud translations for a given locale.\n * Cloud translations cover features available only via initCloud() —\n * AI, comments, collaboration, scoring, snapshots, plan limits, etc.\n * OSS contributors don't need to translate these; cloud locales fall back\n * to English independently of the OSS locale.\n */\nexport async function loadCloudTranslations(\n locale: string,\n): Promise<CloudTranslations> {\n const target = resolveLocale(locale, supportedCloudLocales);\n const loader = cloudModules[`./locales/cloud/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/** Check if a locale has OSS translations (matched by exact locale, then base). */\nexport function isLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedLocales) !== undefined;\n}\n\n/** Check if a locale has cloud translations (matched by exact locale, then base). */\nexport function isCloudLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedCloudLocales) !== undefined;\n}\n\n/** List of OSS-supported locales. */\nexport function getSupportedLocales(): string[] {\n return [...supportedLocales];\n}\n\n/** List of cloud-supported locales. May be a subset of OSS locales. */\nexport function getSupportedCloudLocales(): string[] {\n return [...supportedCloudLocales];\n}\n","// eslint-disable-next-line @typescript-eslint/triple-slash-reference -- ambient-module declaration must be loaded for cross-package typecheck (workspace path alias resolves to source)\n/// <reference path=\"./virtual-modules.d.ts\" />\nimport { createApp, h, ref, type App, type Ref } from \"vue\";\nimport { INIT_TIMEOUT_MS } from \"./constants/timeouts\";\nimport type {\n BlockDefaults,\n ColorsConfig,\n CustomBlock,\n CustomBlockDefinition,\n DisplayConditionsConfig,\n FontsConfig,\n LogicTagsConfig,\n MediaResult,\n MergeTagsConfig,\n SavedBlocksProvider,\n TestEmailProvider,\n SaveResult,\n Template,\n TemplateContent,\n TemplateDefaults,\n ThemeOverrides,\n UiTheme,\n} from \"@templatical/types\";\nimport { createDefaultTemplateContent, safeClone } from \"@templatical/types\";\nimport type { MediaRequestContext } from \"@templatical/media-library\";\n\nimport Editor from \"./Editor.vue\";\nimport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nimport { loadTranslations, loadCloudTranslations } from \"./i18n\";\nimport { useFonts } from \"./composables\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n// Compiled-CSS-as-string for shadow root adoption. The `virtual:editor-css`\n// module is owned by `scripts/inline-style-css-plugin.ts` — at build time it\n// captures every emitted CSS asset (Tailwind utilities + every `.vue` SFC\n// `<style>` block + `styles/index.css` rules) and replaces this import's\n// runtime value with the full library CSS string. In dev/test the plugin\n// returns `styles/index.css` source as a fallback.\n//\n// Separate concern from the side-effecting `import \"./styles/index.css\"` in\n// `Editor.vue` / `CloudEditor.vue`, which injects styles into `document.head`\n// for light-DOM mode and survives untouched.\n//\n// Ambient declaration for `virtual:editor-css` lives in `virtual-modules.d.ts`\n// referenced at the top of this file via triple-slash so it's visible to any\n// consumer typechecking through the workspace path alias.\nimport editorStylesInline from \"virtual:editor-css\";\n\n// ---------------------------------------------------------------------------\n// OSS config + return types\n// ---------------------------------------------------------------------------\n\nexport interface TemplaticalEditorConfig {\n /**\n * Where to mount the editor — a CSS selector or an `HTMLElement`.\n *\n * Layout caveat: do NOT apply `transform`, `filter`, `perspective`, or\n * `will-change` to an ancestor of this element. Each establishes a CSS\n * containing block for `position: fixed`, which offsets the editor's\n * floating UI (color pickers, rich-text toolbars) and its drag ghost away\n * from their anchor — even when the transform's computed value is `none`\n * (an active/animated transform still promotes the element). For a\n * scroll/entrance effect on a wrapper, animate `opacity` instead of\n * `transform`.\n */\n container: string | HTMLElement;\n content?: TemplateContent;\n\n /**\n * Mount the editor inside a Shadow DOM (open mode) for CSS isolation\n * from the host page. Defaults to `true` — host stylesheets cannot\n * cascade past the shadow boundary into editor elements (`p`, `a`,\n * `input`, etc.), and editor utility classes never collide with host\n * class names.\n *\n * Set to `false` to mount in light DOM. Opt out when:\n * - Your host integration uses `document.querySelector` to reach\n * editor internals (with shadow DOM, use `container.shadowRoot\n * .querySelector(...)` instead).\n * - You need to support Firefox <101 or Safari <16.4, which lack the\n * `adoptedStyleSheets` API the shadow path relies on.\n *\n * Light-mode consumers should keep this set to `false` explicitly so\n * future SDK changes don't silently flip the default again.\n *\n * @default true\n */\n shadowDom?: boolean;\n\n onChange?: (content: TemplateContent) => void;\n onSave?: (content: TemplateContent) => void;\n onError?: (error: Error) => void;\n\n onRequestMedia?: OnRequestMedia;\n\n /**\n * Display-only resolver for image `src` values. The canvas calls this to\n * obtain a preview URL for a src the user entered; the content model (and\n * `toMjml()` output) always keeps the canonical value. Return `null` — or\n * the input value — to use the src as-is.\n *\n * Use this when templates reference images by a value that isn't directly\n * displayable, e.g. plain file names resolved to ephemeral `blob:` URLs\n * from local storage:\n *\n * ```ts\n * resolveImageUrl: async (src) => {\n * const file = await fileStore.lookup(src);\n * return file ? URL.createObjectURL(file) : null;\n * }\n * ```\n *\n * The resolver is called once per committed src value (typing in the src\n * input is debounced, so partial values never reach it) and results are\n * cached per src for the editor's lifetime — including failures, which\n * fall back to displaying the src verbatim. Note the caching applies to\n * transient failures too: a src that failed to resolve stays unresolved\n * until the editor is re-initialized (a re-resolve hook may be added\n * later). Applies to image srcs, design-time placeholder previews, and\n * explicit video thumbnails; auto-derived provider thumbnails (YouTube/\n * Vimeo) are already real URLs and are never resolved.\n */\n resolveImageUrl?: ResolveImageUrl;\n\n mergeTags?: MergeTagsConfig;\n /**\n * Standalone logic tags — control-flow tokens (`{% else %}`) and open/close\n * pairs (`{% if %}` … `{% endif %}`) inserted from a dedicated \"Insert logic\"\n * button. Separate from `mergeTags`; typed/pasted logic is highlighted\n * regardless of this config.\n */\n logicTags?: LogicTagsConfig;\n displayConditions?: DisplayConditionsConfig;\n customBlocks?: CustomBlockDefinition[];\n\n /**\n * Allowlist + order for the block palette. When set, only the listed block\n * types appear in the sidebar palette, in exactly this order — unlisted\n * built-ins (e.g. `video`, `table`) are hidden. When omitted, the full\n * default palette is shown.\n *\n * Reference built-in blocks by their bare type (`'section'`, `'image'`, …)\n * and custom blocks by their `custom:`-prefixed type (`'custom:qrcode'`),\n * so they can be interleaved freely:\n *\n * ```ts\n * paletteBlocks: ['section', 'title', 'image', 'custom:qrcode', 'button']\n * ```\n *\n * Unknown entries — a typo, an unregistered custom block, or `countdown`\n * outside a Cloud plan — are logged with a warning and skipped. Filtering\n * the palette never affects rendering: existing content using a hidden\n * block type still renders correctly.\n */\n paletteBlocks?: string[];\n\n /**\n * Render each HTML block's raw content as a live preview in the editor\n * canvas instead of the static placeholder card. **Off by default.**\n *\n * ```ts\n * htmlBlockPreview: true // shorthand for { enabled: true }\n * htmlBlockPreview: { enabled: true }\n * ```\n *\n * The content is rendered verbatim inside a sandboxed `<iframe>`\n * (`sandbox=\"allow-same-origin\"`, **no** `allow-scripts`): scripts and\n * inline event handlers never execute and styles can't bleed into the rest\n * of the editor. This is a preview-only setting — the MJML/HTML export path\n * renders HTML blocks regardless of it.\n *\n * @default false\n */\n htmlBlockPreview?: HtmlBlockPreviewConfig;\n\n fonts?: FontsConfig;\n\n /**\n * Editor-wide color-picker palette. `presets` appear as a clickable grid in\n * every color picker popover (block toolbars, template settings, rich-text\n * color, custom-block color fields) — clicking one applies it, and the preset\n * matching the current value is marked selected.\n *\n * ```ts\n * colors: {\n * presets: ['#0b5cff', '#111827', '#6b7280', '#ffffff'],\n * allowCustom: false, // lock authors to the presets above\n * }\n * ```\n *\n * `allowCustom` defaults to `true`. Set it to `false` (together with\n * `presets`) to hide the wheel and hex input so authors can only pick from the\n * palette — useful when embedding the editor as a white-label / brand-kit\n * tool. `allowCustom: false` is ignored, with a warning, when no `presets` are\n * configured, since that would leave the picker with no way to set a color.\n */\n colors?: ColorsConfig;\n\n /**\n * Storage backend for **saved blocks** — reusable groups of blocks a user\n * saves from the canvas and re-inserts into other templates.\n *\n * The editor owns the UI (a save action on each block, a searchable browser\n * with preview, insert-at-position, rename, delete); you own persistence.\n * Implement the four methods of `SavedBlocksProvider` against your own API,\n * or use the bundled browser-local provider for demos and prototypes:\n *\n * ```ts\n * import { init, createLocalStorageSavedBlocksProvider } from \"@templatical/editor\";\n *\n * init({ container, savedBlocks: createLocalStorageSavedBlocksProvider() });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * save action, no sidebar entry, and none of its UI code is downloaded.\n *\n * Not to be confused with `customBlocks`, which registers developer-defined\n * block *types* with their own templates and field schemas.\n */\n savedBlocks?: SavedBlocksProvider;\n\n /**\n * Sending backend for **test emails** — letting a user mail themselves the\n * template they're editing.\n *\n * The editor owns the trigger, the dialog, recipient validation and the\n * sending/success/error states; you own delivery. One method is enough:\n *\n * ```ts\n * init({\n * container,\n * testEmail: {\n * send: ({ recipient, content }) =>\n * fetch(\"/api/test-email\", {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify({ recipient, content }),\n * }).then((r) => {\n * if (!r.ok) throw new Error(\"Could not send the test email\");\n * }),\n * },\n * });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * button, and none of its UI code is downloaded.\n *\n * `allowedRecipients` restricts the picker but is **not** a security boundary:\n * it lives in the user's browser. Validate the recipient on your server.\n */\n testEmail?: TestEmailProvider;\n\n blockDefaults?: BlockDefaults;\n templateDefaults?: TemplateDefaults;\n\n theme?: ThemeOverrides;\n uiTheme?: UiTheme;\n locale?: string;\n\n /**\n * Show the \"Powered by Templatical\" footer. Defaults to `true`.\n * Set to `false` to hide the footer (no attribution required by the license).\n */\n branding?: boolean;\n\n /**\n * Show a \"use a larger screen\" notice instead of the editor chrome on\n * viewports narrower than ~768px. Defaults to `true`.\n *\n * The drag-and-drop editor is a desktop-class tool — the block palette,\n * canvas, and properties panel can't lay out usably on a phone, and touch\n * dragging is impractical. Rather than render a broken, cramped layout, the\n * editor shows a clear message below the breakpoint.\n *\n * Set to `false` if you handle small screens yourself (e.g. you embed the\n * editor in a deliberately narrow desktop pane). The check is viewport-based,\n * so it targets actual small devices, not narrow containers on a wide screen.\n *\n * @default true\n */\n smallScreenNotice?: boolean;\n\n /**\n * Template linter (`@templatical/quality`) configuration. Runs every\n * linter exported by the package (accessibility + structure).\n *\n * - When unset, the linter loads on demand once the user opens the panel.\n * - When `disabled: true`, the optional peer is never imported (saves the\n * chunk download) and the sidebar tab + inline badges are suppressed.\n * - `rules`/`thresholds` follow the shape exported by `@templatical/quality`.\n */\n lint?: import(\"@templatical/quality\").LintOptions;\n}\n\n/** Function type for media browser requests, used by both OSS and Cloud editors. */\nexport type OnRequestMedia = (\n context?: MediaRequestContext,\n) => Promise<MediaResult | null>;\n\ninterface TemplaticalEditorBase {\n getContent(): TemplateContent;\n setContent(content: TemplateContent): void;\n setTheme(theme: UiTheme): void;\n unmount(): void;\n}\n\nexport interface TemplaticalEditor extends TemplaticalEditorBase {\n /**\n * Render the current template to an MJML string. Resolves custom blocks\n * via the editor's internal block registry. Throws if the optional\n * `@templatical/renderer` package is not installed.\n */\n toMjml(): Promise<string>;\n /**\n * Render a single custom block to its HTML representation, using the\n * registered custom block definition's template and the block's current\n * field values. Exposed for headless callers that want to reuse the\n * editor's renderer (e.g., to drive `@templatical/renderer`'s\n * `renderCustomBlock` option from outside the editor instance).\n */\n renderCustomBlock(block: CustomBlock): Promise<string>;\n /**\n * Look up the definition-level `stylesheet` for a registered custom block\n * type. Returns the raw CSS string, or `undefined` when the type is unknown\n * or the definition has no stylesheet. Exposed for headless callers that\n * want to drive `@templatical/renderer`'s `getCustomBlockStylesheet` option\n * from outside the editor instance.\n */\n getCustomBlockStylesheet(customType: string): string | undefined;\n}\n\n/**\n * Cloud editor does not expose `toMjml` or `renderCustomBlock`: the cloud\n * backend performs MJML conversion server-side with additional processing\n * (e.g., signed image URLs, attachment handling) that isn't available client\n * side. Use the cloud `save()` flow to persist content; the backend handles\n * MJML/HTML export from there.\n */\nexport interface TemplaticalCloudEditor extends TemplaticalEditorBase {\n create(content?: TemplateContent): Promise<Template>;\n load(templateId: string): Promise<Template>;\n save(): Promise<SaveResult>;\n}\n\n// ---------------------------------------------------------------------------\n// Shadow root helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Module-cached `CSSStyleSheet` built once from the inline editor CSS string.\n * `adoptedStyleSheets` accepts the same sheet object across multiple shadow\n * roots — sharing one sheet costs zero per-instance memory, regardless of\n * how many editors mount.\n */\nlet cachedEditorStyleSheet: CSSStyleSheet | null = null;\nfunction getEditorStyleSheet(): CSSStyleSheet {\n if (cachedEditorStyleSheet === null) {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(editorStylesInline);\n cachedEditorStyleSheet = sheet;\n }\n return cachedEditorStyleSheet;\n}\n\ninterface MountTarget {\n target: Element;\n shadowRoot: ShadowRoot | null;\n /**\n * Disposer for any dev-mode side effects attached to the shadow root\n * (currently: the document.head `<style>` mirror's MutationObserver).\n * Always safe to call — no-op when no side effects were registered.\n */\n cleanup: () => void;\n}\n\n/**\n * Dev-only: mirror every `<style>` tag in `document.head` into the\n * shadow root's `adoptedStyleSheets`, and observe `document.head` so\n * Vite's HMR-injected style updates flow through to the shadow root\n * automatically.\n *\n * Background: Vite dev injects each `.vue` `<style scoped>` block as a\n * separate `<style>` element in `document.head` via HMR. Those don't\n * cross the shadow boundary, so a shadow-mounted editor in dev would\n * render with only the bundled `styles/index.css` rules — every SFC\n * scoped style (block selection outlines, sidebar layout, etc.) missing.\n *\n * In production builds, `inline-style-css-plugin.ts` captures every\n * emitted CSS asset at `generateBundle` time and inlines the full\n * library CSS as a single string adopted by the shadow root. This\n * dev-only mirror does the same thing at runtime by observing whatever\n * Vite ends up injecting.\n *\n * The dead-code-elimination on `import.meta.env.DEV` ensures the\n * observer + filter logic is stripped from production bundles entirely.\n *\n * Caveats:\n * - In a real consumer's Vite-dev environment, this would also adopt\n * the consumer's page-level styles (whatever they put in\n * `document.head`). That's harmless when consumers install the\n * editor from npm dist (the dev branch is dead-coded out). It only\n * matters when a consumer source-resolves this package, which is\n * unusual outside this repo's own playground.\n * - `replaceSync` strips `@import` rules per the CSSOM spec. Styles\n * containing `@import` are skipped silently (the catch below). The\n * primary bundled sheet covers Tailwind imports already, so this\n * should never matter in practice.\n */\nfunction attachDevStyleMirror(shadowRoot: ShadowRoot): () => void {\n if (!import.meta.env?.DEV) return () => {};\n\n function buildSheets(): CSSStyleSheet[] {\n const sheets: CSSStyleSheet[] = [];\n // shadow-ok: dev-only HMR style mirror (production path is plugin-driven via adoptedStyleSheets)\n document.head.querySelectorAll(\"style\").forEach((el) => {\n const text = el.textContent;\n if (!text) return;\n try {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(text);\n sheets.push(sheet);\n } catch {\n // Skip styles that contain disallowed constructs (e.g. `@import`).\n }\n });\n return sheets;\n }\n\n function refresh(): void {\n // Keep the editor's primary (bundled) sheet first so its declarations\n // are the cascade fallback; dev sheets adopted after may override them\n // — same precedence model as production where everything ends up in\n // one stylesheet anyway.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet(), ...buildSheets()];\n }\n\n refresh();\n\n const observer = new MutationObserver(() => refresh());\n // childList catches new/removed <style> tags; characterData + subtree\n // catches Vite HMR mutating an existing style's textContent in place.\n // shadow-ok: dev-only HMR style mirror observer\n observer.observe(document.head, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n\n return () => observer.disconnect();\n}\n\n/**\n * Resolve where Vue should mount: directly on the consumer's container\n * (light-DOM mode) or on a fresh `<div>` inside a newly-attached open\n * shadow root (shadow mode). In shadow mode, also attaches the editor's\n * cached `CSSStyleSheet` to the root so chrome renders with the right\n * styles inside the boundary, plus a dev-only mirror that observes\n * `document.head` (see `attachDevStyleMirror`).\n *\n * Idempotent: a second call on the same container reuses any existing\n * shadow root, clearing its contents so a prior mount's stale DOM doesn't\n * accumulate.\n */\nfunction resolveMountTarget(\n container: Element,\n shadowDom: boolean | undefined,\n): MountTarget {\n if (!shadowDom) {\n return { target: container, shadowRoot: null, cleanup: () => {} };\n }\n\n const shadowRoot =\n container.shadowRoot ?? container.attachShadow({ mode: \"open\" });\n\n // Adopt the editor stylesheet. Idempotent — repeated assignment of the\n // same sheet is fine. Dev mirror (below) will overwrite this with the\n // primary sheet + mirrored sheets, then keep them in sync.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet()];\n\n // Clear stale content from a prior mount (re-init on same container).\n while (shadowRoot.firstChild) {\n shadowRoot.removeChild(shadowRoot.firstChild);\n }\n\n const host = document.createElement(\"div\");\n host.className = \"tpl-editor-host\";\n // Match the container's layout. Without an explicit size the host is a\n // default block element (height: auto) and the editor's `tpl:h-full`\n // template root collapses to content height because the height-100% chain\n // breaks at the shadow boundary. `width: 100%` is redundant for block\n // elements but harmless and explicit.\n host.style.cssText = \"display:block;height:100%;width:100%;\";\n shadowRoot.appendChild(host);\n\n const cleanup = attachDevStyleMirror(shadowRoot);\n\n return { target: host, shadowRoot, cleanup };\n}\n\n// ---------------------------------------------------------------------------\n// OSS init — sync\n// ---------------------------------------------------------------------------\n\ninterface OssEntry {\n app: App;\n editorRef: Ref<InstanceType<typeof Editor> | null>;\n /** Tear down dev-mode side effects (style mirror observer, etc.). */\n cleanup: () => void;\n}\n\n// Per-container registry so two `init()` calls with different containers\n// produce independent editor instances (multi-instance support). Re-init\n// on the same container still auto-unmounts the previous instance.\nconst ossEntries = new Map<Element, OssEntry>();\n\n// \"Last init'd\" container preserves the legacy single-instance behavior\n// of the top-level `unmount()` export — the playground (and other one-\n// editor consumers) calls bare `unmount()` and expects it to tear down\n// whatever was most recently mounted.\nlet lastOssContainer: Element | null = null;\n\nfunction unmountOssContainer(container: Element): void {\n const entry = ossEntries.get(container);\n if (!entry) return;\n entry.cleanup();\n entry.app.unmount();\n ossEntries.delete(container);\n if (lastOssContainer === container) {\n lastOssContainer = null;\n }\n}\n\nexport async function init(\n config: TemplaticalEditorConfig,\n): Promise<TemplaticalEditor> {\n const container =\n typeof config.container === \"string\"\n ? document.querySelector(config.container)\n : config.container;\n\n if (!container) {\n throw new Error(\n `[Templatical] Container element not found: ${config.container}`,\n );\n }\n\n // Load translations before mounting so child components can use useI18n synchronously\n const translations = await loadTranslations(config.locale ?? \"en\");\n\n // Create fonts manager to pass to Editor\n const fontsManager = useFonts(config.fonts);\n\n // Auto-unmount any prior instance on the SAME container *after* awaits\n // — checking before the await would let two concurrent init() calls\n // both pass the guard and orphan the first-mounted app on this\n // container.\n unmountOssContainer(container);\n\n const mount = resolveMountTarget(container, config.shadowDom ?? true);\n const editorRef: Ref<InstanceType<typeof Editor> | null> = ref(null);\n\n const app = createApp({\n setup() {\n return () =>\n h(Editor, {\n config,\n translations,\n fontsManager,\n shadowRoot: mount.shadowRoot ?? undefined,\n ref: editorRef,\n });\n },\n });\n\n app.mount(mount.target);\n\n ossEntries.set(container, { app, editorRef, cleanup: mount.cleanup });\n lastOssContainer = container;\n\n const instance: TemplaticalEditor = {\n getContent() {\n // safeClone (not a naked JSON.stringify): a drag inside a section can\n // leave a Sortable expando cycle reachable from live content, which a\n // plain stringify chokes on (#203). The clone drops the back-ref.\n if (editorRef.value) {\n return safeClone(editorRef.value.getContent());\n }\n return safeClone(config.content ?? createDefaultTemplateContent());\n },\n setContent(content: TemplateContent) {\n if (editorRef.value) {\n editorRef.value.setContent(content);\n }\n config.content = content;\n },\n setTheme(theme: UiTheme) {\n if (editorRef.value) {\n editorRef.value.setTheme(theme);\n }\n },\n unmount: () => unmountOssContainer(container),\n renderCustomBlock(block: CustomBlock) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.renderCustomBlock(block);\n },\n getCustomBlockStylesheet(customType: string) {\n // Pre-mount: registry is empty, so no definition has a stylesheet to\n // resolve. Returning undefined here matches the \"unknown type\" branch\n // and keeps the renderer's `getCustomBlockStylesheet` resolver total.\n if (!editorRef.value) {\n return undefined;\n }\n return editorRef.value.getCustomBlockStylesheet(customType);\n },\n toMjml: () => toMjmlForInstance(instance),\n };\n\n return instance;\n}\n\n// ---------------------------------------------------------------------------\n// Cloud init — async, flat config, tree-shaken when not called\n// ---------------------------------------------------------------------------\n\ninterface CloudEntry {\n app: App;\n editorRef: Ref<InstanceType<\n typeof import(\"./cloud/CloudEditor.vue\").default\n > | null>;\n cleanup: () => void;\n}\n\nconst cloudEntries = new Map<Element, CloudEntry>();\nlet lastCloudContainer: Element | null = null;\n\nfunction unmountCloudContainer(container: Element): void {\n const entry = cloudEntries.get(container);\n if (!entry) return;\n entry.cleanup();\n entry.app.unmount();\n cloudEntries.delete(container);\n if (lastCloudContainer === container) {\n lastCloudContainer = null;\n }\n}\n\nexport async function initCloud(\n config: import(\"./cloud/CloudEditor.vue\").TemplaticalCloudEditorConfig,\n): Promise<TemplaticalCloudEditor> {\n const container =\n typeof config.container === \"string\"\n ? document.querySelector(config.container)\n : config.container;\n\n if (!container) {\n throw new Error(\n `[Templatical] Container element not found: ${config.container}`,\n );\n }\n\n // Dynamic import — CloudEditor.vue is tree-shaken from the OSS bundle\n const { default: CloudEditor } = await import(\"./cloud/CloudEditor.vue\");\n\n // Load OSS + cloud translations in parallel so child components can use\n // useI18n / useCloudI18n synchronously\n const [translations, cloudTranslations] = await Promise.all([\n loadTranslations(config.locale ?? \"en\"),\n loadCloudTranslations(config.locale ?? \"en\"),\n ]);\n\n // Create fonts manager to pass to CloudEditor\n const fontsManager = useFonts(config.fonts);\n\n // Auto-unmount any prior instance on the SAME container *after* awaits\n // — checking before the await would let two concurrent initCloud()\n // calls both pass the guard and orphan the first-mounted app on this\n // container.\n unmountCloudContainer(container);\n\n const mount = resolveMountTarget(container, config.shadowDom ?? true);\n const cloudEditorRef: CloudEntry[\"editorRef\"] = ref(null);\n\n // Promise that resolves when CloudEditor emits 'ready'\n const readyPromise = new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"[Templatical] Cloud editor initialization timed out\"));\n }, INIT_TIMEOUT_MS);\n\n const app = createApp({\n setup() {\n return () =>\n h(CloudEditor, {\n config,\n translations,\n cloudTranslations,\n fontsManager,\n shadowRoot: mount.shadowRoot ?? undefined,\n ref: cloudEditorRef,\n onReady: () => {\n clearTimeout(timeout);\n resolve();\n },\n });\n },\n });\n\n app.mount(mount.target);\n\n cloudEntries.set(container, {\n app,\n editorRef: cloudEditorRef,\n cleanup: mount.cleanup,\n });\n lastCloudContainer = container;\n });\n\n await readyPromise;\n\n const instance: TemplaticalCloudEditor = {\n getContent() {\n // safeClone (not a naked JSON.stringify): a drag inside a section can\n // leave a Sortable expando cycle reachable from live content, which a\n // plain stringify chokes on (#203). The clone drops the back-ref.\n if (cloudEditorRef.value) {\n return safeClone(cloudEditorRef.value.getContent());\n }\n return safeClone(config.content ?? createDefaultTemplateContent());\n },\n setContent(content: TemplateContent) {\n if (cloudEditorRef.value) {\n cloudEditorRef.value.setContent(content);\n }\n },\n setTheme(theme: UiTheme) {\n if (cloudEditorRef.value) {\n cloudEditorRef.value.setTheme(theme);\n }\n },\n unmount: () => unmountCloudContainer(container),\n create(content?: TemplateContent) {\n if (!cloudEditorRef.value) {\n return Promise.reject(\n new Error(\"[Templatical] Cloud editor not ready\"),\n );\n }\n return cloudEditorRef.value.create(content);\n },\n load(templateId: string) {\n if (!cloudEditorRef.value) {\n return Promise.reject(\n new Error(\"[Templatical] Cloud editor not ready\"),\n );\n }\n return cloudEditorRef.value.load(templateId);\n },\n save() {\n if (!cloudEditorRef.value) {\n return Promise.reject(\n new Error(\"[Templatical] Cloud editor not ready\"),\n );\n }\n return cloudEditorRef.value.save();\n },\n };\n\n return instance;\n}\n\n// ---------------------------------------------------------------------------\n// Unmount helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Unmount the most-recently-created OSS editor. Single-instance legacy\n * API — callers managing multiple editors should use `instance.unmount()`\n * from each returned object, which targets the specific container.\n */\nexport function unmount(): void {\n if (lastOssContainer) {\n unmountOssContainer(lastOssContainer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TemplaticalCloudEditorConfig } from \"./cloud/CloudEditor.vue\";\nexport type {\n BlockDefaults,\n TemplateContent,\n TemplateDefaults,\n ThemeOverrides,\n UiTheme,\n MergeTagsConfig,\n LogicTagsConfig,\n LogicTag,\n LogicPair,\n DisplayConditionsConfig,\n CustomBlockDefinition,\n ViewportSize,\n ColorsConfig,\n CustomFont,\n FontsConfig,\n SavedBlock,\n SavedBlocksListParams,\n SavedBlocksProvider,\n SaveResult,\n Template,\n TestEmailPayload,\n TestEmailProvider,\n} from \"@templatical/types\";\n\n// Bundled browser-local saved-blocks provider. Re-exported here (rather than\n// leaving it to `@templatical/core`) because consumers install only this\n// package — core is bundled inline and isn't resolvable on their side.\nexport { createLocalStorageSavedBlocksProvider } from \"@templatical/core\";\nexport type { LocalStorageSavedBlocksOptions } from \"@templatical/core\";\n\nexport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nexport type { UseFontsReturn, FontOption } from \"./composables/useFonts\";\nexport { useFonts } from \"./composables/useFonts\";\nexport type { EditorCapabilities } from \"./types/editor-capabilities\";\nexport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n\nexport {\n getSupportedLocales,\n getSupportedCloudLocales,\n isLocaleSupported,\n isCloudLocaleSupported,\n getBaseLocale,\n} from \"./i18n\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;EA6BA,IAAM,IAAQ,GAcR,IAAS,GAAU;GACvB,SAAS,EAAM,OAAO;GACtB,kBAAkB,EAAM,OAAO;EACjC,CAAC,GAIK,IAAS,EAAwB,IAAI,GAKrC,IAAc,EAAM,OAAO,cAC7B,GAAsB;GACpB,UAAU,EAAM,OAAO;GACvB;GACA,SAAS,EAAM,OAAO;EACxB,CAAC,IACD,MAIE,IAAoB,QAClB,OAAO,yCACf,GAOM,IAAY,EAAM,OAAO,YAC3B,GAAoB;GAClB,UAAU,EAAM,OAAO;GACvB,kBAAkB,EAAO,QAAQ;GACjC,YAAY;GACZ,SAAS,EAAM,OAAO;EACxB,CAAC,IACD,MAEE,IAAiB,QACf,OAAO,sCACf,GAGM,IAAO,GAAc;GACzB;GACA,aAAa;GACb,QAAQ;IACN,SAAS,EAAM,OAAO;IACtB,OAAO,EAAM,OAAO;IACpB,eAAe,EAAM,OAAO;IAC5B,kBAAkB,EAAM,OAAO;IAC/B,cAAc,EAAM,OAAO;IAC3B,eAAe,EAAM,OAAO;IAC5B,kBAAkB,EAAM,OAAO;IAC/B,QAAQ,EAAM,OAAO;IACrB,WAAW,EAAM,OAAO;IACxB,WAAW,EAAM,OAAO;IACxB,mBAAmB,EAAM,OAAO;IAChC,gBAAgB,EAAM,OAAO;IAC7B,iBAAiB,EAAM,OAAO;IAC9B,MAAM,GAAmB,EAAM,MAAM;IACrC,QAAQ,EAAM,OAAO,eAEf,EAAM,OAAO,OAAQ,KAAK,MAAM,KAAK,UAAU,EAAO,MAAM,OAAO,CAAC,CAAC,IACvE,KAAA;GACN;GACA,cAAc,EAAM;GACpB,cAAc,EAAM;GACpB,iBAAiB,EAAM,OAAO,WAC1B,EACE,gBACE,EAAM,OAAO,SACX,KAAK,MAAM,KAAK,UAAU,EAAO,MAAM,OAAO,CAAC,CACjD,EACJ,IACA;GACJ,cAAc;IACZ,GAAI,IAAc,EAAE,aAAa,EAAY,WAAW,IAAI,CAAC;IAC7D,GAAI,IAAY,EAAE,WAAW,EAAU,WAAW,IAAI,CAAC;GACzD;GACA,YAAY,EAAM;EACpB,CAAC;EAUD,SAAS,IAAqC;GAC5C,OAAO,EAAkB;IACvB,kBAAkB,EAAO,QAAQ;IACjC,mBAAmB,EAAK,SAAS;IACjC,2BAA2B,MACzB,EAAK,SAAS,cAAc,CAAU,CAAC,EAAE;GAC7C,CAAC;EACH;EAKA,IAAM,EAAE,YAAY,MAA0B,SACtC,EAAM,OAAO,iBACrB;SAGA,EAAU,YAAY;GACpB,MAAM,EAAM,aAAa,gBAAgB;EAC3C,CAAC,GAED,QAAkB;GAEhB,AADA,EAAM,aAAa,iBAAiB,GACpC,EAAK,QAAQ;EACf,CAAC,GAGD,EAAa;GACX,kBAAkB,EAAO,QAAQ;GACjC,aAAa,MAA6B,EAAO,WAAW,CAAO;GACnE,WAAW,MAAmB,EAAO,WAAW,CAAK;GACrD,mBAAmB,EAAK,SAAS;GACjC,2BAA2B,MACzB,EAAK,SAAS,cAAc,CAAU,CAAC,EAAE;EAC7C,CAAC,cAQC,EAAA,GAAA,EA6LM,OAAA;GA5LA,SAAA;GAAJ,KAAI;GACJ,OAAK,EAAA,CAAC,mDAAiD,EAAA,YACjC,EAAA,CAAA,CAAM,CAAC,MAAM,SAAQ,CAAA,CAAA;GAC1C,kBAAgB,EAAA,CAAA,CAAI,CAAC,cAAc;GACnC,OAAK,EAAE,EAAA,CAAA,CAAI,CAAC,YAAY,KAAK;GAC7B,YAAQ,AAAA,EAAA,OAAA,QAAT,CAAA,GAAiB,CAAA,SAAA,CAAA;GAChB,QAAI,AAAA,EAAA,OAAA,QAAL,CAAA,GAAa,CAAA,SAAA,CAAA;;GAIb,EAA0B,EAAA;GAE1B,EA6DS,UA7DT,GA6DS;IApDP,AAAA,EAAA,OAAA,EAAyD,OAAA,EAApD,OAAM,wCAAuC,GAAA,MAAA,EAAA;IAGlD,EAqBM,OArBN,GAqBM;KApBJ,EAGE,IAAA;MAFC,UAAU,EAAA,CAAA,CAAM,CAAC,MAAM;MACvB,UAAQ,EAAA,CAAA,CAAM,CAAC;;KAElB,EAGE,IAAA;MAFC,aAAW,EAAA,CAAA,CAAM,CAAC,MAAM;MACxB,UAAQ,EAAA,CAAA,CAAM,CAAC;;KAElB,EAGE,IAAA;MAFC,gBAAc,EAAA,CAAA,CAAM,CAAC,MAAM;MAC3B,UAAQ,EAAA,CAAA,CAAM,CAAC;;KAMV,EAAA,CAAA,CAAM,CAAC,MAAM,eADrB,EAAA,GAAA,EAIE,IAAA;;MAFC,eAAa,EAAA,CAAA,CAAI,CAAC,mBAAmB;MACrC,UAAM,AAAA,EAAA,QAAA,MAAE,EAAA,CAAA,CAAI,CAAC,mBAAmB,QAAQ;;;IAO7C,EAsBM,OAtBN,GAsBM,CAXI,EAAA,CAAA,CAAS,EAAE,YAAY,SAD/B,EAAA,GAAA,EAWS,UAAA;;KATP,MAAK;KACL,eAAY;KACX,cAAY,EAAA,CAAA,CAAI,CAAC,EAAE,UAAU;KAC7B,OAAO,EAAA,CAAA,CAAI,CAAC,EAAE,UAAU;KACzB,OAAM;KACL,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,CAAA,CAAS,CAAC,KAAI;IAEtB,GAAA,CAAA,EAAwC,EAAA,EAAA,GAAA;KAAjC,MAAM;KAAK,gBAAc;IAAQ,CAAA,GAAA,EAAA,MACxC,EAAG,EAAA,CAAA,CAAI,CAAC,EAAE,UAAU,MAAM,GAAA,CAAA,CAAA,GAAA,GAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA;;GAMhC,EAAA,EAA8C,IAAA,MAAA,MAAA,GAAA,GAAA,CAA5B,CAAA,GAAA,CAAA,EAAA,CAAA,CAAM,CAAC,MAAM,WAAW,CAAA,CAAA;GAG1C,EAkCM,OAAA;IAjCJ,OAAK,EAAA,CAAC,sFAAoF,CAExE,EAAA,CAAA,CAAM,CAAC,MAAM,cAAA,2BAAA,iCAAA,YAAA,CAAA,CAAA;IAD/B,OAAA,EAAA,YAAA,8CAAA;GASA,GAAA,CAAA,EAYM,OAZN,IAYM,CAXJ,EAUa,IAAA,EAVD,MAAK,kBAAiB,GAAA;IAChC,SAAA,QAQS,CAPD,EAAA,CAAA,CAAI,CAAC,iBAAiB,gBAAgB,SAD9C,EAAA,GAAA,EAQS,UAAA;;KANP,OAAM;KACN,OAAA,EAAA,mBAAA,YAAA;KACC,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,CAAA,CAAI,CAAC,iBAAiB,MAAK;IAEnC,GAAA,CAAA,EAA0C,EAAA,EAAA,GAAA;KAA9B,MAAM;KAAK,gBAAc;IAAK,CAAA,GAAA,EAAA,MAC1C,EAAG,EAAA,CAAA,CAAI,CAAC,EAAE,cAAc,mBAAmB,GAAA,CAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA;;GAIjD,CAAA,CAAA,CAAA,GAAA,EASM,OATN,IASM,CARJ,EAOE,IAAA;IANC,UAAU,EAAA,CAAA,CAAM,CAAC,MAAM;IACvB,SAAS,EAAA,CAAA,CAAM,CAAC,QAAQ;IACxB,qBAAmB,EAAA,CAAA,CAAM,CAAC,MAAM;IAChC,aAAW,EAAA,CAAA,CAAM,CAAC,MAAM;IACxB,gBAAc,EAAA,CAAA,CAAM,CAAC,MAAM;IAC3B,eAAc,EAAA,CAAA,CAAM,CAAC;;;;;;;;;GAMpB,EAAA,OAAO,aAAQ,kBADvB,EAAA,GAAA,EAOE,IAAA;;IALC,kBAAc,CAAY,EAAA,CAAA,CAAM,CAAC,MAAM,cAAA,2BAAA,+BAAA;;GAQ1C,EAQM,OAAA;IAPJ,OAAM;IACN,MAAK;IACL,aAAU;IACV,eAAY;IACX,cAAY,EAAA,CAAA,CAAI,CAAC,EAAE,UAAU;GAE3B,GAAA,EAAA,EAAA,CAAA,CAAI,CAAC,gBAAgB,aAAa,KAAK,GAAA,GAAA,EAAA;GAI5C,EAAA,EAsBE,IAAA;IApBC,kBAAgB,EAAA,CAAA,CAAM,CAAC,cAAc;IACrC,UAAU,EAAA,CAAA,CAAM,CAAC,QAAQ,MAAM;IAC/B,eAAY,AAAA,EAAA,QAAY,MAAY,EAAA,CAAA,CAAM,CAAC,YAAY,EAAA,CAAA,CAAM,CAAC,MAAM,iBAAkB,CAAO;IAG7F,eAAY,AAAA,EAAA,aAAA;KAAiC,AAAA,EAAA,CAAA,CAAM,CAAC,MAAM,mBAA+B,EAAA,CAAA,CAAI,CAAC,aAAa,YAAY,EAAA,CAAA,CAAM,CAAC,MAAM,eAAe;;IAOnJ,kBAAe,AAAA,EAAA,aAAA;KAAiC,AAAA,EAAA,CAAA,CAAM,CAAC,cAAc,SAAqB,EAAA,CAAA,CAAI,CAAC,aAAa,eAAe,EAAA,CAAA,CAAM,CAAC,cAAc,KAAK;;IAOrJ,kBAAe,AAAA,EAAA,QAAG,MAAY,EAAA,CAAA,CAAM,CAAC,eAAe,CAAO;GApBnD,GAAA,MAAA,GAAA,CAAA,kBAAA,UAAA,CAAA,GAAA,CAAA,CAAA,GAAA,CAAA,EAAA,CAAA,CAAM,CAAC,MAAM,WAAW,CAAA,CAAA;GA0BnC,EAGE,OAAA;IAFC,MAAM,MAAQ,EAAA,CAAA,CAAI,CAAC,YAAY,QAAQ;IACxC,OAAM;;GAKR,EAAuB,EAAA;GAGvB,EAAuB,EAAA;GAKf,EAAA,CAAA,CAAW,EAAE,YAAY,SADjC,EAAA,GAAA,EAGE,EAAA,CAAA,GAAA;;IADC,SAAS,EAAA,CAAA;;GAGU,EAAA,CAAA,CAAS,EAAE,YAAY,SAA7C,EAAA,GAAA,EAA2E,EAAA,CAAA,GAAA;;IAAtB,SAAS,EAAA,CAAA;;GAKrC,EAAA,CAAA,KAAzB,EAAA,GAAA,EAAkD,IAAA,EAAA,KAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA;;;yCEnWhD,IAAa,uBAAA,OAAA;CAAA,yBAAA,OAAA;CAAA,yBAAA,OAAA;CAAA,yBAAA,OAAA;CAAA,yBAAA,OAAA;CAAA,yBAAA,OAAA;CAAA,yBAAA,OAAA;CAAA,4BAAA,OAAA;AAAA,CAAA,GAGb,IAAe,uBAAA,OAAA;CAAA,+BAAA,OAAA;CAAA,+BAAA,OAAA;CAAA,kCAAA,OAAA;AAAA,CAAA;AAIrB,SAAS,EAAgB,GAA4C;CACnE,OAAO,OAAO,KAAK,CAAO,CAAC,CACxB,KAAK,MAAS,EAAK,MAAM,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAChD,QAAQ,MAA6B,EAAQ,CAAO;AACzD;AAEA,IAAM,IAAmB,EAAgB,CAAU,GAC7C,IAAwB,EAAgB,CAAY;AAE1D,SAAS,EAAa,GAAwB;CAC5C,OAAO,EAAO,KAAK,CAAC,CAAC,QAAQ,MAAM,GAAG,CAAC,CAAC,YAAY;AACtD;AAEA,SAAS,EACP,GACA,GACoB;CACpB,IAAM,IAAY,EAAa,CAAM;CACrC,OAAO,EAAU,MAAM,MAAM,EAAa,CAAC,MAAM,CAAS;AAC5D;AAMA,SAAgB,EAAc,GAAwB;CACpD,OAAO,EAAa,CAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AACzC;AAEA,SAAS,EACP,GACA,GACoB;CACpB,OACE,EAAoB,GAAQ,CAAS,KACrC,EAAoB,EAAc,CAAM,GAAG,CAAS;AAExD;AAEA,SAAS,EAAc,GAAgB,GAA6B;CAClE,OAAO,EAAiB,GAAQ,CAAS,KAAK;AAChD;AAMA,eAAsB,EAAiB,GAAuC;CAE5E,IAAM,IAAS,EAAW,aADX,EAAc,GAAQ,CACE,EAAO;CAE9C,QAAO,MADW,EAAO,EAAA,CACd;AACb;AASA,eAAsB,GACpB,GAC4B;CAE5B,IAAM,IAAS,EAAa,mBADb,EAAc,GAAQ,CACU,EAAO;CAEtD,QAAO,MADW,EAAO,EAAA,CACd;AACb;AAGA,SAAgB,GAAkB,GAAyB;CACzD,OAAO,EAAiB,GAAQ,CAAgB,MAAM,KAAA;AACxD;AAGA,SAAgB,GAAuB,GAAyB;CAC9D,OAAO,EAAiB,GAAQ,CAAqB,MAAM,KAAA;AAC7D;AAGA,SAAgB,KAAgC;CAC9C,OAAO,CAAC,GAAG,CAAgB;AAC7B;AAGA,SAAgB,KAAqC;CACnD,OAAO,CAAC,GAAG,CAAqB;AAClC;;;sCCwPI,IAA+C;AACnD,SAAS,KAAqC;CAC5C,IAAI,MAA2B,MAAM;EACnC,IAAM,IAAQ,IAAI,cAAc;EAEhC,AADA,EAAM,YAAY,EAAkB,GACpC,IAAyB;CAC3B;CACA,OAAO;AACT;AA8CA,SAAS,GAAqB,GAAoC;CACrC,aAAa,CAAC;AAwC3C;AAcA,SAAS,EACP,GACA,GACa;CACb,IAAI,CAAC,GACH,OAAO;EAAE,QAAQ;EAAW,YAAY;EAAM,eAAe,CAAC;CAAE;CAGlE,IAAM,IACJ,EAAU,cAAc,EAAU,aAAa,EAAE,MAAM,OAAO,CAAC;CAQjE,KAHA,EAAW,qBAAqB,CAAC,GAAoB,CAAC,GAG/C,EAAW,aAChB,EAAW,YAAY,EAAW,UAAU;CAG9C,IAAM,IAAO,SAAS,cAAc,KAAK;CAYzC,OAXA,EAAK,YAAY,mBAMjB,EAAK,MAAM,UAAU,yCACrB,EAAW,YAAY,CAAI,GAIpB;EAAE,QAAQ;EAAM;EAAY,SAFnB,GAAqB,CAEF;CAAQ;AAC7C;AAgBA,IAAM,oBAAa,IAAI,IAAuB,GAM1C,IAAmC;AAEvC,SAAS,EAAoB,GAA0B;CACrD,IAAM,IAAQ,EAAW,IAAI,CAAS;CACjC,MACL,EAAM,QAAQ,GACd,EAAM,IAAI,QAAQ,GAClB,EAAW,OAAO,CAAS,GACvB,MAAqB,MACvB,IAAmB;AAEvB;AAEA,eAAsB,GACpB,GAC4B;CAC5B,IAAM,IACJ,OAAO,EAAO,aAAc,WACxB,SAAS,cAAc,EAAO,SAAS,IACvC,EAAO;CAEb,IAAI,CAAC,GACH,MAAU,MACR,8CAA8C,EAAO,WACvD;CAIF,IAAM,IAAe,MAAM,EAAiB,EAAO,UAAU,IAAI,GAG3D,IAAe,EAAS,EAAO,KAAK;CAM1C,EAAoB,CAAS;CAE7B,IAAM,IAAQ,EAAmB,GAAW,EAAO,aAAa,EAAI,GAC9D,IAAqD,EAAI,IAAI,GAE7D,IAAM,EAAU,EACpB,QAAQ;EACN,aACE,EAAE,GAAQ;GACR;GACA;GACA;GACA,YAAY,EAAM,cAAc,KAAA;GAChC,KAAK;EACP,CAAC;CACL,EACF,CAAC;CAKD,AAHA,EAAI,MAAM,EAAM,MAAM,GAEtB,EAAW,IAAI,GAAW;EAAE;EAAK;EAAW,SAAS,EAAM;CAAQ,CAAC,GACpE,IAAmB;CAEnB,IAAM,IAA8B;EAClC,aAAa;GAOX,OAFS,EADL,EAAU,QACK,EAAU,MAAM,WAAW,IAE7B,EAAO,WAAW,EAA6B,CAFjB;EAGjD;EACA,WAAW,GAA0B;GAInC,AAHI,EAAU,SACZ,EAAU,MAAM,WAAW,CAAO,GAEpC,EAAO,UAAU;EACnB;EACA,SAAS,GAAgB;GACvB,AAAI,EAAU,SACZ,EAAU,MAAM,SAAS,CAAK;EAElC;EACA,eAAe,EAAoB,CAAS;EAC5C,kBAAkB,GAAoB;GAIpC,OAHK,EAAU,QAGR,EAAU,MAAM,kBAAkB,CAAK,IAFrC,QAAQ,OAAO,gBAAI,MAAM,gCAAgC,CAAC;EAGrE;EACA,yBAAyB,GAAoB;GAItC,MAAU,OAGf,OAAO,EAAU,MAAM,yBAAyB,CAAU;EAC5D;EACA,cAAc,EAAkB,CAAQ;CAC1C;CAEA,OAAO;AACT;AAcA,IAAM,oBAAe,IAAI,IAAyB,GAC9C,IAAqC;AAEzC,SAAS,EAAsB,GAA0B;CACvD,IAAM,IAAQ,EAAa,IAAI,CAAS;CACnC,MACL,EAAM,QAAQ,GACd,EAAM,IAAI,QAAQ,GAClB,EAAa,OAAO,CAAS,GACzB,MAAuB,MACzB,IAAqB;AAEzB;AAEA,eAAsB,GACpB,GACiC;CACjC,IAAM,IACJ,OAAO,EAAO,aAAc,WACxB,SAAS,cAAc,EAAO,SAAS,IACvC,EAAO;CAEb,IAAI,CAAC,GACH,MAAU,MACR,8CAA8C,EAAO,WACvD;CAIF,IAAM,EAAE,SAAS,MAAgB,MAAM,OAAO,qCAIxC,CAAC,GAAc,KAAqB,MAAM,QAAQ,IAAI,CAC1D,EAAiB,EAAO,UAAU,IAAI,GACtC,GAAsB,EAAO,UAAU,IAAI,CAC7C,CAAC,GAGK,IAAe,EAAS,EAAO,KAAK;CAM1C,EAAsB,CAAS;CAE/B,IAAM,IAAQ,EAAmB,GAAW,EAAO,aAAa,EAAI,GAC9D,IAA0C,EAAI,IAAI;CAqFxD,OAjDA,MAAM,IAjCmB,SAAe,GAAS,MAAW;EAC1D,IAAM,IAAU,iBAAiB;GAC/B,EAAO,gBAAI,MAAM,qDAAqD,CAAC;EACzE,GAAG,CAAe,GAEZ,IAAM,EAAU,EACpB,QAAQ;GACN,aACE,EAAE,GAAa;IACb;IACA;IACA;IACA;IACA,YAAY,EAAM,cAAc,KAAA;IAChC,KAAK;IACL,eAAe;KAEb,AADA,aAAa,CAAO,GACpB,EAAQ;IACV;GACF,CAAC;EACL,EACF,CAAC;EASD,AAPA,EAAI,MAAM,EAAM,MAAM,GAEtB,EAAa,IAAI,GAAW;GAC1B;GACA,WAAW;GACX,SAAS,EAAM;EACjB,CAAC,GACD,IAAqB;CACvB,CAEM,GAiDC;EA9CL,aAAa;GAOX,OAFS,EADL,EAAe,QACA,EAAe,MAAM,WAAW,IAElC,EAAO,WAAW,EAA6B,CAFZ;EAGtD;EACA,WAAW,GAA0B;GACnC,AAAI,EAAe,SACjB,EAAe,MAAM,WAAW,CAAO;EAE3C;EACA,SAAS,GAAgB;GACvB,AAAI,EAAe,SACjB,EAAe,MAAM,SAAS,CAAK;EAEvC;EACA,eAAe,EAAsB,CAAS;EAC9C,OAAO,GAA2B;GAMhC,OALK,EAAe,QAKb,EAAe,MAAM,OAAO,CAAO,IAJjC,QAAQ,OACb,gBAAI,MAAM,sCAAsC,CAClD;EAGJ;EACA,KAAK,GAAoB;GAMvB,OALK,EAAe,QAKb,EAAe,MAAM,KAAK,CAAU,IAJlC,QAAQ,OACb,gBAAI,MAAM,sCAAsC,CAClD;EAGJ;EACA,OAAO;GAML,OALK,EAAe,QAKb,EAAe,MAAM,KAAK,IAJxB,QAAQ,OACb,gBAAI,MAAM,sCAAsC,CAClD;EAGJ;CAGK;AACT;AAWA,SAAgB,KAAgB;CAC9B,AAAI,KACF,EAAoB,CAAgB;AAExC"}
@@ -1,4 +1,4 @@
1
- import { t as e } from "./createLucideIcon-JWHwTI6N.js";
1
+ import { t as e } from "./createLucideIcon-AEltGIse.js";
2
2
  var t = e("check", [["path", {
3
3
  d: "M20 6 9 17l-5-5",
4
4
  key: "1gmf2c"
@@ -1,4 +1,4 @@
1
- import { t as e } from "./createLucideIcon-JWHwTI6N.js";
1
+ import { t as e } from "./createLucideIcon-AEltGIse.js";
2
2
  var t = e("chevron-down", [["path", {
3
3
  d: "m6 9 6 6 6-6",
4
4
  key: "qrunsl"