@templatical/editor 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/dist/{AiChatSidebar-DYDtzTDd.js → AiChatSidebar-ftdKHSR-.js} +15 -15
  2. package/dist/{AiFeatureMenu-BJ882XCi.js → AiFeatureMenu-aNeCRlil.js} +7 -7
  3. package/dist/{BlockIssueBadge-D6y8vQpL.js → BlockIssueBadge-bTDu6W_2.js} +5 -5
  4. package/dist/{BlockPreviewCanvas-DQNxLy7u.js → BlockPreviewCanvas-BsOuOfzB.js} +20 -19
  5. package/dist/{CloudHeaderExtras-CqG3RaKw.js → CloudHeaderExtras-BNpFohqt.js} +5 -5
  6. package/dist/{CloudPanels-B6xrk7kN.js → CloudPanels-xoGWwqSH.js} +7 -7
  7. package/dist/{CollaboratorBar-by16gXQH.js → CollaboratorBar-mEqXkdEi.js} +4 -4
  8. package/dist/{ColorPicker-E5gqlCZE.js → ColorPicker-FL5Vb9bE.js} +7 -7
  9. package/dist/{CommentsPanel-B_kzvTHR.js → CommentsPanel-slfi74ng.js} +2 -2
  10. package/dist/{CommentsSidebar-gfzUX7cd.js → CommentsSidebar-Dea4spMl.js} +14 -14
  11. package/dist/{CountdownBlock-BgNK7fZY.js → CountdownBlock-DQTwaRxP.js} +3 -3
  12. package/dist/{CountdownToolbar-DU8UHx9A.js → CountdownToolbar-C7xwJoqh.js} +4 -4
  13. package/dist/{DesignReferenceSidebar-DgndMB4H.js → DesignReferenceSidebar-BtBd7bbz.js} +11 -11
  14. package/dist/{FieldRow-0-UbQR4a.js → FieldRow-BWt7ECqo.js} +1 -1
  15. package/dist/{IssuesPanel-DmJTb_SZ.js → IssuesPanel-Ci2H36L2.js} +8 -8
  16. package/dist/{LoadingTrack-5KBr5cE8.js → LoadingTrack-SQXoZVf2.js} +1 -1
  17. package/dist/{MergeTagModeToggle-CZnJ2Gz_.js → MergeTagModeToggle-Cdi_DmVw.js} +6 -6
  18. package/dist/{MergeTagSuggestion-DyUcY_9e.js → MergeTagSuggestion-CoCuzEmW.js} +4 -4
  19. package/dist/{ParagraphEditor-CnB4kXle.js → ParagraphEditor-DQEOFc0K.js} +26 -26
  20. package/dist/{RestoreVersionDialog-BpOphPiE.js → RestoreVersionDialog-CHwngBae.js} +5 -5
  21. package/dist/{RichTextEditorContent-ChPNgHQ_.js → RichTextEditorContent-C_9xi7dD.js} +8 -8
  22. package/dist/{SaveBlockDialog-CKw4Va6j.js → SaveBlockDialog-tZqZzPjW.js} +21 -21
  23. package/dist/SavedBlocksBrowserModal-Bdj3GOD_.js +322 -0
  24. package/dist/{SavedBlocksPanels-ChV8bOgM.js → SavedBlocksPanels-2sBxAH9w.js} +2 -2
  25. package/dist/{SavedBlocksPickBar-BbEeD5RB.js → SavedBlocksPickBar-DTjQBR00.js} +4 -4
  26. package/dist/{TemplateScoringPanel-De8BZpRU.js → TemplateScoringPanel-B6nHSrBD.js} +15 -15
  27. package/dist/{TestEmailModal-4d2pXeHU.js → TestEmailModal-DuHbzO37.js} +11 -11
  28. package/dist/{TestEmailPanel-DEgCesNP.js → TestEmailPanel-TpwP-eK_.js} +2 -2
  29. package/dist/{TitleEditor-89pmKLVY.js → TitleEditor-C0JQJu38.js} +12 -12
  30. package/dist/{TplModal-DswccJRJ.js → TplModal-BMGmhquu.js} +5 -5
  31. package/dist/VersionHistoryMenu-D2x6MMYx.js +157 -0
  32. package/dist/{VersionHistoryPanels-CFLHDH9P.js → VersionHistoryPanels-aGlUVEGF.js} +2 -2
  33. package/dist/{VersionPreviewBanner-DhG5RcRt.js → VersionPreviewBanner-61t3_Z-a.js} +3 -3
  34. package/dist/{blockTypeIcons-dpKs5GQc.js → blockTypeIcons-BsNup009.js} +2 -2
  35. package/dist/bundle-stats.json +7 -7
  36. package/dist/{ca-DZSrLNXK.js → ca-C0hfqt4U.js} +5 -0
  37. package/dist/cdn/chunks/{AiChatSidebar-DmYDRmvX.js → AiChatSidebar-CqWvtRwB.js} +9 -9
  38. package/dist/cdn/chunks/{AiChatSidebar-DmYDRmvX.js.map → AiChatSidebar-CqWvtRwB.js.map} +1 -1
  39. package/dist/cdn/chunks/{AiFeatureMenu-C0Rt08qb.js → AiFeatureMenu-DoFrLcXJ.js} +6 -6
  40. package/dist/cdn/chunks/{AiFeatureMenu-C0Rt08qb.js.map → AiFeatureMenu-DoFrLcXJ.js.map} +1 -1
  41. package/dist/cdn/chunks/{BlockIssueBadge-BBog2CMy.js → BlockIssueBadge-B_mUH69a.js} +5 -5
  42. package/dist/cdn/chunks/{BlockIssueBadge-BBog2CMy.js.map → BlockIssueBadge-B_mUH69a.js.map} +1 -1
  43. package/dist/cdn/chunks/{BlockPreviewCanvas-DNBjBKBI.js → BlockPreviewCanvas-CSIxs7hr.js} +21 -20
  44. package/dist/cdn/chunks/BlockPreviewCanvas-CSIxs7hr.js.map +1 -0
  45. package/dist/cdn/chunks/{CloudHeaderExtras-Ditfv7y9.js → CloudHeaderExtras-Dgtr7WaE.js} +5 -5
  46. package/dist/cdn/chunks/{CloudHeaderExtras-Ditfv7y9.js.map → CloudHeaderExtras-Dgtr7WaE.js.map} +1 -1
  47. package/dist/cdn/chunks/{CloudPanels-C4U5bJO6.js → CloudPanels-B91lUmof.js} +7 -7
  48. package/dist/cdn/chunks/{CloudPanels-C4U5bJO6.js.map → CloudPanels-B91lUmof.js.map} +1 -1
  49. package/dist/cdn/chunks/{CollaboratorBar-B79JR9NH.js → CollaboratorBar-CQwPWoFu.js} +5 -5
  50. package/dist/cdn/chunks/{CollaboratorBar-B79JR9NH.js.map → CollaboratorBar-CQwPWoFu.js.map} +1 -1
  51. package/dist/cdn/chunks/{ColorPicker-C-ZiwUV3.js → ColorPicker-CdrKX48J.js} +7 -7
  52. package/dist/cdn/chunks/{ColorPicker-C-ZiwUV3.js.map → ColorPicker-CdrKX48J.js.map} +1 -1
  53. package/dist/cdn/chunks/{CommentsPanel-D1Ke8PbO.js → CommentsPanel-BJNgTmAA.js} +3 -3
  54. package/dist/cdn/chunks/{CommentsPanel-D1Ke8PbO.js.map → CommentsPanel-BJNgTmAA.js.map} +1 -1
  55. package/dist/cdn/chunks/{CommentsSidebar-B-UyhIZN.js → CommentsSidebar-D6VIIPhB.js} +5 -5
  56. package/dist/cdn/chunks/{CommentsSidebar-B-UyhIZN.js.map → CommentsSidebar-D6VIIPhB.js.map} +1 -1
  57. package/dist/cdn/chunks/{CountdownBlock-CWeJBC0L.js → CountdownBlock-BRwXUvGU.js} +4 -4
  58. package/dist/cdn/chunks/{CountdownBlock-CWeJBC0L.js.map → CountdownBlock-BRwXUvGU.js.map} +1 -1
  59. package/dist/cdn/chunks/{CountdownToolbar-DyOHOu44.js → CountdownToolbar-CVaU5WG6.js} +5 -5
  60. package/dist/cdn/chunks/{CountdownToolbar-DyOHOu44.js.map → CountdownToolbar-CVaU5WG6.js.map} +1 -1
  61. package/dist/cdn/chunks/{DesignReferenceSidebar-BCCX1FtP.js → DesignReferenceSidebar-BvOwE1kg.js} +7 -7
  62. package/dist/cdn/chunks/{DesignReferenceSidebar-BCCX1FtP.js.map → DesignReferenceSidebar-BvOwE1kg.js.map} +1 -1
  63. package/dist/cdn/chunks/{FieldRow-CbcpsxZ4.js → FieldRow-D9OJhYeb.js} +2 -2
  64. package/dist/cdn/chunks/{FieldRow-CbcpsxZ4.js.map → FieldRow-D9OJhYeb.js.map} +1 -1
  65. package/dist/cdn/chunks/{IssuesPanel-3vD3ImHF.js → IssuesPanel-MJZxkScz.js} +6 -6
  66. package/dist/cdn/chunks/{IssuesPanel-3vD3ImHF.js.map → IssuesPanel-MJZxkScz.js.map} +1 -1
  67. package/dist/cdn/chunks/{LoadingTrack-Cz1sT0wP.js → LoadingTrack-Cm3TDKb3.js} +2 -2
  68. package/dist/cdn/chunks/{LoadingTrack-Cz1sT0wP.js.map → LoadingTrack-Cm3TDKb3.js.map} +1 -1
  69. package/dist/cdn/chunks/{MergeTagModeToggle-BU0SDRow.js → MergeTagModeToggle-bNuN6PTV.js} +6 -6
  70. package/dist/cdn/chunks/{MergeTagModeToggle-BU0SDRow.js.map → MergeTagModeToggle-bNuN6PTV.js.map} +1 -1
  71. package/dist/cdn/chunks/{MergeTagSuggestion-B9vL0VaG.js → MergeTagSuggestion-t8Uj00Er.js} +3 -3
  72. package/dist/cdn/chunks/{MergeTagSuggestion-B9vL0VaG.js.map → MergeTagSuggestion-t8Uj00Er.js.map} +1 -1
  73. package/dist/cdn/chunks/{ParagraphEditor-CSwIuvoF.js → ParagraphEditor-BU8tlFbL.js} +23 -23
  74. package/dist/cdn/chunks/{ParagraphEditor-CSwIuvoF.js.map → ParagraphEditor-BU8tlFbL.js.map} +1 -1
  75. package/dist/cdn/chunks/{RestoreVersionDialog-Bvg2WvJN.js → RestoreVersionDialog-vdRFfyy0.js} +5 -5
  76. package/dist/cdn/chunks/{RestoreVersionDialog-Bvg2WvJN.js.map → RestoreVersionDialog-vdRFfyy0.js.map} +1 -1
  77. package/dist/cdn/chunks/{RichTextEditorContent-DdkmEEpK.js → RichTextEditorContent-C1-5I8Df.js} +7 -7
  78. package/dist/cdn/chunks/{RichTextEditorContent-DdkmEEpK.js.map → RichTextEditorContent-C1-5I8Df.js.map} +1 -1
  79. package/dist/cdn/chunks/{SaveBlockDialog-D-QO3LuR.js → SaveBlockDialog-DWaeqcM8.js} +9 -9
  80. package/dist/cdn/chunks/{SaveBlockDialog-D-QO3LuR.js.map → SaveBlockDialog-DWaeqcM8.js.map} +1 -1
  81. package/dist/cdn/chunks/{SavedBlocksBrowserModal-BRdN56r7.js → SavedBlocksBrowserModal-qC7UhW74.js} +110 -111
  82. package/dist/cdn/chunks/SavedBlocksBrowserModal-qC7UhW74.js.map +1 -0
  83. package/dist/cdn/chunks/{SavedBlocksPanels-D9GTlLgb.js → SavedBlocksPanels-BvtjtdsY.js} +3 -3
  84. package/dist/cdn/chunks/{SavedBlocksPanels-D9GTlLgb.js.map → SavedBlocksPanels-BvtjtdsY.js.map} +1 -1
  85. package/dist/cdn/chunks/{SavedBlocksPickBar-DSRcUeIo.js → SavedBlocksPickBar-CxXjF3RN.js} +5 -5
  86. package/dist/cdn/chunks/{SavedBlocksPickBar-DSRcUeIo.js.map → SavedBlocksPickBar-CxXjF3RN.js.map} +1 -1
  87. package/dist/cdn/chunks/{TemplateScoringPanel-Dy5oOVhB.js → TemplateScoringPanel-CroWaWqy.js} +50 -50
  88. package/dist/cdn/chunks/{TemplateScoringPanel-Dy5oOVhB.js.map → TemplateScoringPanel-CroWaWqy.js.map} +1 -1
  89. package/dist/cdn/chunks/{TestEmailModal-B4L2lmIV.js → TestEmailModal-iWxAZRRq.js} +10 -10
  90. package/dist/cdn/chunks/{TestEmailModal-B4L2lmIV.js.map → TestEmailModal-iWxAZRRq.js.map} +1 -1
  91. package/dist/cdn/chunks/{TestEmailPanel-cTPgEf-D.js → TestEmailPanel-CGUtEuwp.js} +3 -3
  92. package/dist/cdn/chunks/{TestEmailPanel-cTPgEf-D.js.map → TestEmailPanel-CGUtEuwp.js.map} +1 -1
  93. package/dist/cdn/chunks/{TitleEditor-BKjL2Zs1.js → TitleEditor-DvXUW2j-.js} +12 -12
  94. package/dist/cdn/chunks/{TitleEditor-BKjL2Zs1.js.map → TitleEditor-DvXUW2j-.js.map} +1 -1
  95. package/dist/cdn/chunks/{TplModal-DxrgNmt9.js → TplModal-X-qNoLGZ.js} +5 -5
  96. package/dist/cdn/chunks/{TplModal-DxrgNmt9.js.map → TplModal-X-qNoLGZ.js.map} +1 -1
  97. package/dist/cdn/chunks/VersionHistoryMenu-yKzwxf7n.js +150 -0
  98. package/dist/cdn/chunks/VersionHistoryMenu-yKzwxf7n.js.map +1 -0
  99. package/dist/cdn/chunks/{VersionHistoryPanels-CxIFBJsz.js → VersionHistoryPanels--hQaanjH.js} +3 -3
  100. package/dist/cdn/chunks/{VersionHistoryPanels-CxIFBJsz.js.map → VersionHistoryPanels--hQaanjH.js.map} +1 -1
  101. package/dist/cdn/chunks/{VersionPreviewBanner-CdsZvVYF.js → VersionPreviewBanner-DWU_SqMd.js} +4 -4
  102. package/dist/cdn/chunks/{VersionPreviewBanner-CdsZvVYF.js.map → VersionPreviewBanner-DWU_SqMd.js.map} +1 -1
  103. package/dist/cdn/chunks/{blockTypeIcons-DXPd_hd1.js → blockTypeIcons-ZEzEpCti.js} +2 -2
  104. package/dist/cdn/chunks/{blockTypeIcons-DXPd_hd1.js.map → blockTypeIcons-ZEzEpCti.js.map} +1 -1
  105. package/dist/cdn/chunks/{ca-DZSrLNXK.js → ca-C0hfqt4U.js} +6 -1
  106. package/dist/cdn/chunks/ca-C0hfqt4U.js.map +1 -0
  107. package/dist/cdn/chunks/{cloud-CITqEHEc.js → cloud-BP5-6__d.js} +4 -4
  108. package/dist/cdn/chunks/{cloud-CITqEHEc.js.map → cloud-BP5-6__d.js.map} +1 -1
  109. package/dist/cdn/chunks/{createCloudRuntime-DwIXvrb0.js → createCloudRuntime-DD6ppb71.js} +10 -10
  110. package/dist/cdn/chunks/{createCloudRuntime-DwIXvrb0.js.map → createCloudRuntime-DD6ppb71.js.map} +1 -1
  111. package/dist/{de-DbayqpK-.js → cdn/chunks/de-C6J2Iae8.js} +7 -0
  112. package/dist/cdn/chunks/de-C6J2Iae8.js.map +1 -0
  113. package/dist/cdn/chunks/{dist-CgR3inPR.js → dist-BW6Y49zE.js} +2 -2
  114. package/dist/cdn/chunks/{dist-CgR3inPR.js.map → dist-BW6Y49zE.js.map} +1 -1
  115. package/dist/cdn/chunks/{dist-El3fQnfk.js → dist-CAFTQ6Y2.js} +5 -5
  116. package/dist/cdn/chunks/{dist-El3fQnfk.js.map → dist-CAFTQ6Y2.js.map} +1 -1
  117. package/dist/cdn/chunks/{draggable-D2vo-aQI.js → draggable-kbOhCgnk.js} +12 -13
  118. package/dist/cdn/chunks/{draggable-D2vo-aQI.js.map → draggable-kbOhCgnk.js.map} +1 -1
  119. package/dist/cdn/chunks/{en-l0ib5X0f.js → en-BaUYOQi8.js} +6 -1
  120. package/dist/cdn/chunks/en-BaUYOQi8.js.map +1 -0
  121. package/dist/cdn/chunks/en-mEKGL1PX.js +2 -0
  122. package/dist/cdn/chunks/{es-CMQ-5PJP.js → es-CKDYfJYn.js} +6 -1
  123. package/dist/cdn/chunks/es-CKDYfJYn.js.map +1 -0
  124. package/dist/cdn/chunks/{extensions-C87Z6_23.js → extensions-BUPHNpKD.js} +7 -7
  125. package/dist/cdn/chunks/{extensions-C87Z6_23.js.map → extensions-BUPHNpKD.js.map} +1 -1
  126. package/dist/cdn/chunks/formatAbsoluteDateTime-DMmop3C-.js +15 -0
  127. package/dist/cdn/chunks/formatAbsoluteDateTime-DMmop3C-.js.map +1 -0
  128. package/dist/{fr-DNXmZWAq.js → cdn/chunks/fr-L8DR73mK.js} +7 -0
  129. package/dist/cdn/chunks/fr-L8DR73mK.js.map +1 -0
  130. package/dist/cdn/chunks/{icons-ciivb5BS.js → icons-SP5Itd-A.js} +9 -9
  131. package/dist/cdn/chunks/icons-SP5Itd-A.js.map +1 -0
  132. package/dist/cdn/chunks/keys-BOn5i9jV.js +12 -0
  133. package/dist/cdn/chunks/keys-BOn5i9jV.js.map +1 -0
  134. package/dist/cdn/chunks/{liquid.browser-BPvKXNfO.js → liquid.browser-BypTT2oc.js} +4 -4
  135. package/dist/cdn/chunks/{liquid.browser-BPvKXNfO.js.map → liquid.browser-BypTT2oc.js.map} +1 -1
  136. package/dist/{nl-BylRRHYJ.js → cdn/chunks/nl-GDWeuX18.js} +7 -0
  137. package/dist/cdn/chunks/nl-GDWeuX18.js.map +1 -0
  138. package/dist/cdn/chunks/{preRenderCustomBlocks-DyCIgmqJ.js → preRenderCustomBlocks-LFB-e-0m.js} +2 -2
  139. package/dist/cdn/chunks/{preRenderCustomBlocks-DyCIgmqJ.js.map → preRenderCustomBlocks-LFB-e-0m.js.map} +1 -1
  140. package/dist/cdn/chunks/{pt-BR-BVoJhpie.js → pt-BR-B1AVVhWj.js} +6 -1
  141. package/dist/cdn/chunks/pt-BR-B1AVVhWj.js.map +1 -0
  142. package/dist/cdn/chunks/{src-6J-yyZoD.js → src-BM3f9I7t.js} +268 -268
  143. package/dist/cdn/chunks/{src-6J-yyZoD.js.map → src-BM3f9I7t.js.map} +1 -1
  144. package/dist/cdn/chunks/{src-BD60gqPW.js → src-CLoCCDfs.js} +26 -26
  145. package/dist/cdn/chunks/{src-BD60gqPW.js.map → src-CLoCCDfs.js.map} +1 -1
  146. package/dist/cdn/chunks/{src-Bid3cmHa.js → src-Cxl-9O-L.js} +699 -684
  147. package/dist/cdn/chunks/src-Cxl-9O-L.js.map +1 -0
  148. package/dist/cdn/chunks/{src-Cf0ohOog.js → src-DtnvR_Hl.js} +19 -19
  149. package/dist/cdn/chunks/src-DtnvR_Hl.js.map +1 -0
  150. package/dist/cdn/chunks/{tiptap-Ey0FpaX7.js → tiptap-Epe8m3Vo.js} +2636 -2629
  151. package/dist/cdn/chunks/tiptap-Epe8m3Vo.js.map +1 -0
  152. package/dist/cdn/chunks/{useAliveFlag-DrXxPV8V.js → useAliveFlag-CT9gjvO_.js} +2 -2
  153. package/dist/cdn/chunks/{useAliveFlag-DrXxPV8V.js.map → useAliveFlag-CT9gjvO_.js.map} +1 -1
  154. package/dist/cdn/chunks/{useCloudI18n-D0AoLV0a.js → useCloudI18n-DbsLlCbZ.js} +3 -3
  155. package/dist/cdn/chunks/{useCloudI18n-D0AoLV0a.js.map → useCloudI18n-DbsLlCbZ.js.map} +1 -1
  156. package/dist/cdn/chunks/{useEditorCore-CTNB2Mkl.js → useEditorCore-ChlgO2GN.js} +556 -412
  157. package/dist/cdn/chunks/useEditorCore-ChlgO2GN.js.map +1 -0
  158. package/dist/cdn/chunks/useEditorRoot-BlF1WPeu.js +10 -0
  159. package/dist/cdn/chunks/{useEditorRoot-BFGRBGts.js.map → useEditorRoot-BlF1WPeu.js.map} +1 -1
  160. package/dist/cdn/chunks/{useI18n-DIhawILp.js → useI18n-BdxNNw6j.js} +3 -3
  161. package/dist/cdn/chunks/{useI18n-DIhawILp.js.map → useI18n-BdxNNw6j.js.map} +1 -1
  162. package/dist/cdn/chunks/{useMergeTag-lWo7So18.js → useMergeTag-CI2WUNrR.js} +4 -4
  163. package/dist/cdn/chunks/{useMergeTag-lWo7So18.js.map → useMergeTag-CI2WUNrR.js.map} +1 -1
  164. package/dist/cdn/chunks/{usePopoverPosition-DkiTLnBm.js → usePopoverPosition-Dw1nTSNx.js} +3 -3
  165. package/dist/cdn/chunks/{usePopoverPosition-DkiTLnBm.js.map → usePopoverPosition-Dw1nTSNx.js.map} +1 -1
  166. package/dist/cdn/chunks/{useScrollToBlock-KcSAuXKz.js → useScrollToBlock-BMis3-PR.js} +3 -3
  167. package/dist/cdn/chunks/{useScrollToBlock-KcSAuXKz.js.map → useScrollToBlock-BMis3-PR.js.map} +1 -1
  168. package/dist/cdn/editor.css +1 -1
  169. package/dist/cdn/editor.js +1099 -1143
  170. package/dist/cdn/editor.js.map +1 -1
  171. package/dist/{check-80E6ZGin.js → check-JK8Uacq0.js} +1 -1
  172. package/dist/{chevron-down-BmkLsn2c.js → chevron-down-a-fb_Ijn.js} +1 -1
  173. package/dist/{chevron-up-DJSAzlUU.js → chevron-up-x4l0et_Y.js} +1 -1
  174. package/dist/{circle-alert-BsCfZryY.js → circle-alert-CJDW5LZo.js} +1 -1
  175. package/dist/{clock-BcLIngSK.js → clock-BW6rXk2f.js} +1 -1
  176. package/dist/{cloud-DwJ8ebNl.js → cloud-BMzRKCbk.js} +3 -3
  177. package/dist/{createCloudRuntime-CdXQdl34.js → createCloudRuntime-zNcMjRgb.js} +8 -8
  178. package/dist/{createLucideIcon-eeetEgBb.js → createLucideIcon-Bvw1Zx56.js} +3 -3
  179. package/dist/{cdn/chunks/de-DbayqpK-.js → de-C6J2Iae8.js} +5 -2
  180. package/dist/{dist-BFcAowKZ.js → dist-BxFbxpgW.js} +5 -5
  181. package/dist/{dist-Do83S47T.js → dist-ByT5fgKq.js} +2 -2
  182. package/dist/{dist-CQ9T1yi-.js → dist-C1tsW_99.js} +2 -2
  183. package/dist/{dist-Cd0LvUlh.js → dist-CtF9STPS.js} +2 -2
  184. package/dist/{dist-CvW1nNzy.js → dist-CtMXAH7a.js} +2 -2
  185. package/dist/dist-D08oVYhr.js +5 -0
  186. package/dist/{dist-prgdllHG.js → dist-D7WY8VPq.js} +2 -2
  187. package/dist/{dist-D8MekYFl.js → dist-DC-3T0ED.js} +2 -2
  188. package/dist/{dist-BC1y_U2m.js → dist-DL9334yz.js} +1418 -1411
  189. package/dist/{dist-BkAVGSRr.js → dist-DkcA6epL.js} +7 -7
  190. package/dist/dist-DzdeVn-W.js +5 -0
  191. package/dist/{dist-CzVIVbSa.js → dist-PL6vP4SC.js} +61 -61
  192. package/dist/{dist-CuuPnIMy.js → dist-dNiknO7j.js} +4 -4
  193. package/dist/{dist-CWC5t5J3.js → dist-f7HXo6j6.js} +2 -2
  194. package/dist/{dist-BUpBA58N.js → dist-heatwAdo.js} +1 -1
  195. package/dist/{en-l0ib5X0f.js → en-BaUYOQi8.js} +5 -0
  196. package/dist/{es-CMQ-5PJP.js → es-CKDYfJYn.js} +5 -0
  197. package/dist/{extensions-BYImwYeO.js → extensions-kOYwOh72.js} +21 -21
  198. package/dist/formatAbsoluteDateTime-DMmop3C-.js +13 -0
  199. package/dist/{cdn/chunks/fr-DNXmZWAq.js → fr-L8DR73mK.js} +5 -2
  200. package/dist/{image-up-BfxY85ar.js → image-up-CuzFc1mu.js} +1 -1
  201. package/dist/{info-LX4QFBr6.js → info-DybAwcA3.js} +1 -1
  202. package/dist/keys-BOhFlHsv.js +10 -0
  203. package/dist/{liquid.browser-CJgDDvQz.js → liquid.browser-wPG0n4Cs.js} +3 -3
  204. package/dist/{loader-circle-Bwima0v8.js → loader-circle-t-SVgmJ3.js} +1 -1
  205. package/dist/{message-circle-Box-mf2k.js → message-circle-DtwKrlhN.js} +1 -1
  206. package/dist/{cdn/chunks/nl-BylRRHYJ.js → nl-GDWeuX18.js} +5 -2
  207. package/dist/{pencil-BM9gSq_J.js → pencil-2qhC-Y4q.js} +1 -1
  208. package/dist/{preRenderCustomBlocks-2KyBJocY.js → preRenderCustomBlocks-CSIF-HyJ.js} +1 -1
  209. package/dist/{pt-BR-BVoJhpie.js → pt-BR-B1AVVhWj.js} +5 -0
  210. package/dist/{refresh-cw-BuzFX0wH.js → refresh-cw-C8SBuPdg.js} +1 -1
  211. package/dist/{runtime-dom.esm-bundler-Co_SPqAz.js → runtime-dom.esm-bundler-BuG03jBM.js} +3 -3
  212. package/dist/{scan-line-Duq1WXoG.js → scan-line-E7w4yQyv.js} +1 -1
  213. package/dist/{send-DZ4nOnIA.js → send-BoqJliF6.js} +1 -1
  214. package/dist/{shield-check-Cz4BRYdv.js → shield-check-D0dMaHtE.js} +1 -1
  215. package/dist/{sparkles-DP8RqOeL.js → sparkles-Dy2BJjiX.js} +1 -1
  216. package/dist/style.css +1 -1
  217. package/dist/templatical-editor.js +1236 -1281
  218. package/dist/{text-align-start-D7tXhjUP.js → text-align-start-DrxjxvIy.js} +1 -1
  219. package/dist/{trash-2-BS7lZeyb.js → trash-rYMzzHZd.js} +2 -2
  220. package/dist/{triangle-alert-Cx4mQGjx.js → triangle-alert-DxnFj4-B.js} +1 -1
  221. package/dist/{upload-L8tLVM10.js → upload-DZu1adQz.js} +1 -1
  222. package/dist/{useAliveFlag-AFbwfHlJ.js → useAliveFlag-PVmp95MT.js} +1 -1
  223. package/dist/{useCloudI18n-BA69BpQ_.js → useCloudI18n-BlMajbKp.js} +2 -2
  224. package/dist/{useEditorCore-CtR46sbJ.js → useEditorCore-CwQYCSXO.js} +1212 -1067
  225. package/dist/{useEditorRoot-Cdo8Vhxa.js → useEditorRoot-C92q8uV-.js} +2 -2
  226. package/dist/{useI18n-B135ASaG.js → useI18n-Ce_BRzYv.js} +2 -2
  227. package/dist/{useMergeTag-Cx36rtrt.js → useMergeTag-CrXJ49lr.js} +3 -3
  228. package/dist/{usePopoverPosition-DAOHr_iM.js → usePopoverPosition-CPk7Twye.js} +2 -2
  229. package/dist/{useScrollToBlock-tndC8vKo.js → useScrollToBlock-DvpMfWs2.js} +3 -3
  230. package/dist/{vue.runtime.esm-bundler-BhOUdXMi.js → vue.runtime.esm-bundler-D9JUcKrV.js} +4 -4
  231. package/dist/{x-ChIQ7HwK.js → x-DJOd1hS4.js} +1 -1
  232. package/package.json +23 -23
  233. package/dist/SavedBlocksBrowserModal-SST-7EMK.js +0 -323
  234. package/dist/VersionHistoryMenu-DBxSJR1k.js +0 -155
  235. package/dist/cdn/chunks/BlockPreviewCanvas-DNBjBKBI.js.map +0 -1
  236. package/dist/cdn/chunks/SavedBlocksBrowserModal-BRdN56r7.js.map +0 -1
  237. package/dist/cdn/chunks/VersionHistoryMenu-CMqZFUHE.js +0 -148
  238. package/dist/cdn/chunks/VersionHistoryMenu-CMqZFUHE.js.map +0 -1
  239. package/dist/cdn/chunks/ca-DZSrLNXK.js.map +0 -1
  240. package/dist/cdn/chunks/de-DbayqpK-.js.map +0 -1
  241. package/dist/cdn/chunks/en-BzFt4b80.js +0 -2
  242. package/dist/cdn/chunks/en-l0ib5X0f.js.map +0 -1
  243. package/dist/cdn/chunks/es-CMQ-5PJP.js.map +0 -1
  244. package/dist/cdn/chunks/fr-DNXmZWAq.js.map +0 -1
  245. package/dist/cdn/chunks/icons-ciivb5BS.js.map +0 -1
  246. package/dist/cdn/chunks/keys-BJ770Mwm.js +0 -12
  247. package/dist/cdn/chunks/keys-BJ770Mwm.js.map +0 -1
  248. package/dist/cdn/chunks/nl-BylRRHYJ.js.map +0 -1
  249. package/dist/cdn/chunks/pt-BR-BVoJhpie.js.map +0 -1
  250. package/dist/cdn/chunks/src-Bid3cmHa.js.map +0 -1
  251. package/dist/cdn/chunks/src-Cf0ohOog.js.map +0 -1
  252. package/dist/cdn/chunks/tiptap-Ey0FpaX7.js.map +0 -1
  253. package/dist/cdn/chunks/useEditorCore-CTNB2Mkl.js.map +0 -1
  254. package/dist/cdn/chunks/useEditorRoot-BFGRBGts.js +0 -10
  255. package/dist/dist-BQqIUwvB.js +0 -5
  256. package/dist/dist-waDcdb1l.js +0 -5
  257. package/dist/keys-BJtoezGU.js +0 -10
@@ -1 +1 @@
1
- {"version":3,"file":"TestEmailModal-B4L2lmIV.js","names":[],"sources":["../../../src/utils/validateEmailShape.ts","../../../src/components/TestEmailModal.vue","../../../src/components/TestEmailModal.vue"],"sourcesContent":["/**\n * Characters that cannot appear in a domain but routinely arrive attached to one.\n *\n * A **denylist**, not an allowlist, so unicode domains keep working — an\n * allowlist of `[a-z0-9.-]` would be the stricter, more obviously \"correct\"\n * choice and would reject them. What this actually targets is the punctuation\n * that comes from pasting out of a list or a mail client: `user@example.com,`\n * and `<user@example.com>`.\n */\nconst IMPOSSIBLE_DOMAIN_CHARS = /[,;<>()[\\]\\\\\"'`|/?#@!$%^&*+=~{}]/;\n\n/**\n * Does this string look like an email address?\n *\n * Deliberately loose: non-empty, no whitespace, exactly one `@`, a non-empty\n * local part, and a domain containing a dot with something either side and no\n * impossible characters. That is enough to catch the mistakes a typo actually\n * produces — a missing `@`, a trailing comma, a half-typed domain — before the\n * user pays for a round-trip.\n *\n * **Do not \"improve\" this into an RFC-ish regex.** Those reliably reject valid\n * addresses (plus-addressing, long new gTLDs, unicode domains) and the resulting\n * bug reports cost far more than the malformed addresses they catch. Only the\n * receiving mail server can really tell, and the sending backend has to validate\n * anyway — this is a fast-feedback affordance, not a gate. `validateEmailShape`\n * has its own test file pinning the addresses that must keep passing.\n */\nexport function looksLikeEmail(value: string): boolean {\n const trimmed = value.trim();\n if (trimmed.length === 0) return false;\n if (/\\s/.test(trimmed)) return false;\n\n const parts = trimmed.split(\"@\");\n if (parts.length !== 2) return false;\n\n const [local, domain] = parts;\n if (local.length === 0) return false;\n if (IMPOSSIBLE_DOMAIN_CHARS.test(domain)) return false;\n\n // A dot with at least one character either side, so `a@b` and `a@b.` fail but\n // `a@b.c` and `a@sub.example.museum` pass.\n const dot = domain.indexOf(\".\");\n return dot > 0 && dot < domain.length - 1;\n}\n","<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\n/**\n * Send-a-test dialog, shared by the OSS and Cloud editors.\n *\n * The recipient control has three shapes, driven entirely by whether the\n * provider configured an allowlist:\n *\n * - **no allowlist** — free text, validated for shape only. This branch exists\n * for BYO senders; Cloud never takes it, because its list comes from a signed\n * JWT.\n * - **exactly one** — a read-only field. Preserved verbatim from the Cloud-only\n * version; there is nothing to choose, so offering an editable field would\n * imply otherwise.\n * - **several** — a picker of exactly those addresses.\n *\n * An empty allowlist never reaches here: the feature reports itself unavailable,\n * so no trigger renders. (The Cloud-only version fell through to `v-else` and\n * rendered a select with no options and a permanently disabled Send.)\n */\nimport TplModal from \"./TplModal.vue\";\n// Statically imported: this modal is already lazy-loaded as a whole, so the\n// preview rides its chunk rather than costing a second round-trip and flashing\n// an empty frame. Still shared with the saved-blocks surfaces, which lazy-load\n// it themselves.\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\n// The editor header's own control, reused verbatim so the two viewport switches\n// are the same component rather than two things that resemble each other.\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./MergeTagModeToggle.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { looksLikeEmail } from \"../utils/validateEmailShape\";\nimport { Check, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { hasMergeTagSamples, type ViewportSize } from \"@templatical/types\";\nimport {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\nimport type { TestEmailError } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n visible: boolean;\n /** Omitted / undefined means unrestricted, so the field accepts free text. */\n allowedRecipients?: string[];\n defaultRecipient?: string;\n isSending: boolean;\n justSent: boolean;\n error: TestEmailError | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send\", recipient: string): void;\n (e: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\nconst editor = inject(EDITOR_KEY, null);\n\nconst recipient = ref(\"\");\n\nconst previewViewport = ref<ViewportSize>(\"desktop\");\n\n/**\n * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\n\n/**\n * Shared with the editor's own preview, so switching here and switching there\n * are the same choice — it is a property of how the user wants to read a\n * preview, not of one dialog. Falls back to a local ref for headless mounts.\n */\nconst sharedSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Seeded from availability, matching how `useEditorCore` seeds the shared ref.\n// A blanket `true` would make the dialog claim tags \"show example values\" for a\n// consumer who configured none — false, and with no toggle rendered to correct\n// it, since the toggle hides itself when nothing has a sample.\nconst localSampleMode = ref(hasMergeTagSamples(inject(MERGE_TAGS_KEY, [])));\nconst sampleMode = computed({\n get: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\n\nconst hasAllowlist = computed(\n () =>\n props.allowedRecipients !== undefined && props.allowedRecipients.length > 0,\n);\nconst isSingleRecipient = computed(() => props.allowedRecipients?.length === 1);\n\n// Mounted behind a `v-if` on the same state that drives `visible`, so it can\n// mount with `visible` already true — without `immediate` the field would never\n// be seeded.\nwatch(\n () => props.visible,\n (visible) => {\n if (!visible) return;\n recipient.value =\n props.defaultRecipient ?? props.allowedRecipients?.[0] ?? \"\";\n },\n { immediate: true },\n);\n\n/** Shape-check free text only; a picker can't produce a malformed value. */\nconst isRecipientValid = computed(() =>\n hasAllowlist.value\n ? recipient.value.length > 0\n : looksLikeEmail(recipient.value),\n);\n\nconst canSend = computed(\n () => isRecipientValid.value && !props.isSending && !props.justSent,\n);\n\n/** Free text that's been typed into but isn't yet a plausible address. */\nconst showShapeHint = computed(\n () =>\n !hasAllowlist.value &&\n recipient.value.trim().length > 0 &&\n !isRecipientValid.value,\n);\n\nconst errorText = computed(() => {\n if (!props.error) return null;\n return props.error.kind === \"provider\"\n ? props.error.message\n : t.testEmail.recipientNotAllowed;\n});\n\nfunction handleSend(): void {\n if (!canSend.value) return;\n emit(\"send\", recipient.value.trim());\n}\n\nfunction handleClose(): void {\n // Sending is the one state that blocks dismissal; the success state closes\n // immediately rather than making the user wait out the timer.\n if (props.isSending) return;\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Grows to `max-w-3xl` only while the preview is open. A 600px email has\n to render at true size AND leave slack on both sides for the band of\n body background the preview draws beside it: at `max-w-2xl` the\n preview region's content box is exactly 600px, so the band would have\n nowhere to go and `settings.backgroundColor` would look unset for any\n template whose sections span the full width. Collapsed, it stays the\n compact form it was. `w-full` keeps it shrinking on narrow screens.\n `max-h-[90%]`\n with a `min-h-0 overflow-y-auto` preview region is the pattern the\n saved-blocks dialogs use: the cap bounds the panel to TplModal's\n backdrop (never to `vh` — see the note there), and `min-h-0` is what\n lets a flex child shrink below its content and actually scroll. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSending\"\n aria-labelledby=\"tpl-test-email-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-full tpl:max-w-3xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-test-email-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.testEmail.title }}\n </h3>\n\n <!-- Recipient -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-test-email-recipient\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.recipientLabel }}\n </label>\n\n <!-- Exactly one allowed address: nothing to choose. -->\n <input\n v-if=\"isSingleRecipient\"\n id=\"tpl-test-email-recipient\"\n type=\"text\"\n :value=\"recipient\"\n disabled\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:opacity-70 tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <!-- Several allowed addresses: pick one. -->\n <select\n v-else-if=\"hasAllowlist\"\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"email in allowedRecipients\"\n :key=\"email\"\n :value=\"email\"\n >\n {{ email }}\n </option>\n </select>\n\n <!-- No allowlist: free text. -->\n <input\n v-else\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n type=\"email\"\n autocomplete=\"email\"\n spellcheck=\"false\"\n :placeholder=\"t.testEmail.recipientPlaceholder\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <p\n v-if=\"showShapeHint\"\n class=\"tpl:mt-1.5 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.testEmail.invalidAddress }}\n </p>\n </div>\n\n <!-- Preview header: a label plus the viewport switch. Always shown — the\n preview is part of the dialog rather than something to opt into. -->\n <div class=\"tpl:mb-2 tpl:shrink-0\">\n <!-- Two segmented controls: `flex-wrap` so they drop to their own row\n rather than crushing the label on a narrow viewport. -->\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.preview }}\n </span>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <MergeTagModeToggle\n v-if=\"!previewResolution.supersedesSamples.value\"\n :sample-mode=\"sampleMode\"\n @change=\"sampleMode = $event\"\n />\n\n <!-- Without this the preview would silently show only the desktop\n variant of any responsive block. -->\n <ViewportToggle\n :viewport=\"previewViewport\"\n @change=\"previewViewport = $event\"\n />\n </div>\n </div>\n\n <!-- The hint has to follow the mode: \"shown unresolved\" is simply false\n once every tag renders a realistic value. -->\n <p\n data-testid=\"test-email-preview-hint\"\n class=\"tpl:mt-1 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : t.testEmail.previewHint\n }}\n </p>\n </div>\n\n <!-- The one region allowed to grow: `flex-1 min-h-0` so it absorbs the\n remaining height and scrolls, leaving the form and actions fixed. -->\n <div\n id=\"tpl-test-email-preview\"\n data-testid=\"test-email-preview\"\n class=\"tpl:mb-3 tpl:min-h-0 tpl:flex-1 tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius-sm)] tpl:p-3 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\n />\n </div>\n\n <!-- Success. Announced politely; the dialog closes itself shortly after. -->\n <p\n v-if=\"justSent\"\n role=\"status\"\n data-testid=\"test-email-success\"\n class=\"tpl:mb-3 tpl:flex tpl:shrink-0 tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success,var(--tpl-primary))]\"\n >\n <Check :size=\"13\" :stroke-width=\"2.5\" />\n {{ t.testEmail.success }}\n </p>\n\n <!-- Failure. Stays open so the user can retry. -->\n <p\n v-else-if=\"errorText\"\n role=\"alert\"\n data-testid=\"test-email-error\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ errorText }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"test-email-cancel\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-[var(--tpl-shadow-sm)] tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSending\"\n :class=\"{ 'tpl:cursor-not-allowed tpl:opacity-50': isSending }\"\n @click=\"handleClose\"\n >\n {{ t.testEmail.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"test-email-send\"\n :class=\"primaryBtnClass\"\n :disabled=\"!canSend\"\n @click=\"handleSend\"\n >\n <span v-if=\"isSending\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.testEmail.sending }}\n </span>\n <span v-else>\n {{ t.testEmail.send }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\n/**\n * Send-a-test dialog, shared by the OSS and Cloud editors.\n *\n * The recipient control has three shapes, driven entirely by whether the\n * provider configured an allowlist:\n *\n * - **no allowlist** — free text, validated for shape only. This branch exists\n * for BYO senders; Cloud never takes it, because its list comes from a signed\n * JWT.\n * - **exactly one** — a read-only field. Preserved verbatim from the Cloud-only\n * version; there is nothing to choose, so offering an editable field would\n * imply otherwise.\n * - **several** — a picker of exactly those addresses.\n *\n * An empty allowlist never reaches here: the feature reports itself unavailable,\n * so no trigger renders. (The Cloud-only version fell through to `v-else` and\n * rendered a select with no options and a permanently disabled Send.)\n */\nimport TplModal from \"./TplModal.vue\";\n// Statically imported: this modal is already lazy-loaded as a whole, so the\n// preview rides its chunk rather than costing a second round-trip and flashing\n// an empty frame. Still shared with the saved-blocks surfaces, which lazy-load\n// it themselves.\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\n// The editor header's own control, reused verbatim so the two viewport switches\n// are the same component rather than two things that resemble each other.\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./MergeTagModeToggle.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { looksLikeEmail } from \"../utils/validateEmailShape\";\nimport { Check, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { hasMergeTagSamples, type ViewportSize } from \"@templatical/types\";\nimport {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\nimport type { TestEmailError } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n visible: boolean;\n /** Omitted / undefined means unrestricted, so the field accepts free text. */\n allowedRecipients?: string[];\n defaultRecipient?: string;\n isSending: boolean;\n justSent: boolean;\n error: TestEmailError | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send\", recipient: string): void;\n (e: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\nconst editor = inject(EDITOR_KEY, null);\n\nconst recipient = ref(\"\");\n\nconst previewViewport = ref<ViewportSize>(\"desktop\");\n\n/**\n * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\n\n/**\n * Shared with the editor's own preview, so switching here and switching there\n * are the same choice — it is a property of how the user wants to read a\n * preview, not of one dialog. Falls back to a local ref for headless mounts.\n */\nconst sharedSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Seeded from availability, matching how `useEditorCore` seeds the shared ref.\n// A blanket `true` would make the dialog claim tags \"show example values\" for a\n// consumer who configured none — false, and with no toggle rendered to correct\n// it, since the toggle hides itself when nothing has a sample.\nconst localSampleMode = ref(hasMergeTagSamples(inject(MERGE_TAGS_KEY, [])));\nconst sampleMode = computed({\n get: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\n\nconst hasAllowlist = computed(\n () =>\n props.allowedRecipients !== undefined && props.allowedRecipients.length > 0,\n);\nconst isSingleRecipient = computed(() => props.allowedRecipients?.length === 1);\n\n// Mounted behind a `v-if` on the same state that drives `visible`, so it can\n// mount with `visible` already true — without `immediate` the field would never\n// be seeded.\nwatch(\n () => props.visible,\n (visible) => {\n if (!visible) return;\n recipient.value =\n props.defaultRecipient ?? props.allowedRecipients?.[0] ?? \"\";\n },\n { immediate: true },\n);\n\n/** Shape-check free text only; a picker can't produce a malformed value. */\nconst isRecipientValid = computed(() =>\n hasAllowlist.value\n ? recipient.value.length > 0\n : looksLikeEmail(recipient.value),\n);\n\nconst canSend = computed(\n () => isRecipientValid.value && !props.isSending && !props.justSent,\n);\n\n/** Free text that's been typed into but isn't yet a plausible address. */\nconst showShapeHint = computed(\n () =>\n !hasAllowlist.value &&\n recipient.value.trim().length > 0 &&\n !isRecipientValid.value,\n);\n\nconst errorText = computed(() => {\n if (!props.error) return null;\n return props.error.kind === \"provider\"\n ? props.error.message\n : t.testEmail.recipientNotAllowed;\n});\n\nfunction handleSend(): void {\n if (!canSend.value) return;\n emit(\"send\", recipient.value.trim());\n}\n\nfunction handleClose(): void {\n // Sending is the one state that blocks dismissal; the success state closes\n // immediately rather than making the user wait out the timer.\n if (props.isSending) return;\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Grows to `max-w-3xl` only while the preview is open. A 600px email has\n to render at true size AND leave slack on both sides for the band of\n body background the preview draws beside it: at `max-w-2xl` the\n preview region's content box is exactly 600px, so the band would have\n nowhere to go and `settings.backgroundColor` would look unset for any\n template whose sections span the full width. Collapsed, it stays the\n compact form it was. `w-full` keeps it shrinking on narrow screens.\n `max-h-[90%]`\n with a `min-h-0 overflow-y-auto` preview region is the pattern the\n saved-blocks dialogs use: the cap bounds the panel to TplModal's\n backdrop (never to `vh` — see the note there), and `min-h-0` is what\n lets a flex child shrink below its content and actually scroll. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSending\"\n aria-labelledby=\"tpl-test-email-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-full tpl:max-w-3xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-test-email-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.testEmail.title }}\n </h3>\n\n <!-- Recipient -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-test-email-recipient\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.recipientLabel }}\n </label>\n\n <!-- Exactly one allowed address: nothing to choose. -->\n <input\n v-if=\"isSingleRecipient\"\n id=\"tpl-test-email-recipient\"\n type=\"text\"\n :value=\"recipient\"\n disabled\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:opacity-70 tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <!-- Several allowed addresses: pick one. -->\n <select\n v-else-if=\"hasAllowlist\"\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"email in allowedRecipients\"\n :key=\"email\"\n :value=\"email\"\n >\n {{ email }}\n </option>\n </select>\n\n <!-- No allowlist: free text. -->\n <input\n v-else\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n type=\"email\"\n autocomplete=\"email\"\n spellcheck=\"false\"\n :placeholder=\"t.testEmail.recipientPlaceholder\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <p\n v-if=\"showShapeHint\"\n class=\"tpl:mt-1.5 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.testEmail.invalidAddress }}\n </p>\n </div>\n\n <!-- Preview header: a label plus the viewport switch. Always shown — the\n preview is part of the dialog rather than something to opt into. -->\n <div class=\"tpl:mb-2 tpl:shrink-0\">\n <!-- Two segmented controls: `flex-wrap` so they drop to their own row\n rather than crushing the label on a narrow viewport. -->\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.preview }}\n </span>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <MergeTagModeToggle\n v-if=\"!previewResolution.supersedesSamples.value\"\n :sample-mode=\"sampleMode\"\n @change=\"sampleMode = $event\"\n />\n\n <!-- Without this the preview would silently show only the desktop\n variant of any responsive block. -->\n <ViewportToggle\n :viewport=\"previewViewport\"\n @change=\"previewViewport = $event\"\n />\n </div>\n </div>\n\n <!-- The hint has to follow the mode: \"shown unresolved\" is simply false\n once every tag renders a realistic value. -->\n <p\n data-testid=\"test-email-preview-hint\"\n class=\"tpl:mt-1 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : t.testEmail.previewHint\n }}\n </p>\n </div>\n\n <!-- The one region allowed to grow: `flex-1 min-h-0` so it absorbs the\n remaining height and scrolls, leaving the form and actions fixed. -->\n <div\n id=\"tpl-test-email-preview\"\n data-testid=\"test-email-preview\"\n class=\"tpl:mb-3 tpl:min-h-0 tpl:flex-1 tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius-sm)] tpl:p-3 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\n />\n </div>\n\n <!-- Success. Announced politely; the dialog closes itself shortly after. -->\n <p\n v-if=\"justSent\"\n role=\"status\"\n data-testid=\"test-email-success\"\n class=\"tpl:mb-3 tpl:flex tpl:shrink-0 tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success,var(--tpl-primary))]\"\n >\n <Check :size=\"13\" :stroke-width=\"2.5\" />\n {{ t.testEmail.success }}\n </p>\n\n <!-- Failure. Stays open so the user can retry. -->\n <p\n v-else-if=\"errorText\"\n role=\"alert\"\n data-testid=\"test-email-error\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ errorText }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"test-email-cancel\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-[var(--tpl-shadow-sm)] tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSending\"\n :class=\"{ 'tpl:cursor-not-allowed tpl:opacity-50': isSending }\"\n @click=\"handleClose\"\n >\n {{ t.testEmail.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"test-email-send\"\n :class=\"primaryBtnClass\"\n :disabled=\"!canSend\"\n @click=\"handleSend\"\n >\n <span v-if=\"isSending\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.testEmail.sending }}\n </span>\n <span v-else>\n {{ t.testEmail.send }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"TestEmailModal-iWxAZRRq.js","names":[],"sources":["../../../src/utils/validateEmailShape.ts","../../../src/components/TestEmailModal.vue","../../../src/components/TestEmailModal.vue"],"sourcesContent":["/**\n * Characters that cannot appear in a domain but routinely arrive attached to one.\n *\n * A **denylist**, not an allowlist, so unicode domains keep working — an\n * allowlist of `[a-z0-9.-]` would be the stricter, more obviously \"correct\"\n * choice and would reject them. What this actually targets is the punctuation\n * that comes from pasting out of a list or a mail client: `user@example.com,`\n * and `<user@example.com>`.\n */\nconst IMPOSSIBLE_DOMAIN_CHARS = /[,;<>()[\\]\\\\\"'`|/?#@!$%^&*+=~{}]/;\n\n/**\n * Does this string look like an email address?\n *\n * Deliberately loose: non-empty, no whitespace, exactly one `@`, a non-empty\n * local part, and a domain containing a dot with something either side and no\n * impossible characters. That is enough to catch the mistakes a typo actually\n * produces — a missing `@`, a trailing comma, a half-typed domain — before the\n * user pays for a round-trip.\n *\n * **Do not \"improve\" this into an RFC-ish regex.** Those reliably reject valid\n * addresses (plus-addressing, long new gTLDs, unicode domains) and the resulting\n * bug reports cost far more than the malformed addresses they catch. Only the\n * receiving mail server can really tell, and the sending backend has to validate\n * anyway — this is a fast-feedback affordance, not a gate. `validateEmailShape`\n * has its own test file pinning the addresses that must keep passing.\n */\nexport function looksLikeEmail(value: string): boolean {\n const trimmed = value.trim();\n if (trimmed.length === 0) return false;\n if (/\\s/.test(trimmed)) return false;\n\n const parts = trimmed.split(\"@\");\n if (parts.length !== 2) return false;\n\n const [local, domain] = parts;\n if (local.length === 0) return false;\n if (IMPOSSIBLE_DOMAIN_CHARS.test(domain)) return false;\n\n // A dot with at least one character either side, so `a@b` and `a@b.` fail but\n // `a@b.c` and `a@sub.example.museum` pass.\n const dot = domain.indexOf(\".\");\n return dot > 0 && dot < domain.length - 1;\n}\n","<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\n/**\n * Send-a-test dialog, shared by the OSS and Cloud editors.\n *\n * The recipient control has three shapes, driven entirely by whether the\n * provider configured an allowlist:\n *\n * - **no allowlist** — free text, validated for shape only. This branch exists\n * for BYO senders; Cloud never takes it, because its list comes from a signed\n * JWT.\n * - **exactly one** — a read-only field. Preserved verbatim from the Cloud-only\n * version; there is nothing to choose, so offering an editable field would\n * imply otherwise.\n * - **several** — a picker of exactly those addresses.\n *\n * An empty allowlist never reaches here: the feature reports itself unavailable,\n * so no trigger renders. (The Cloud-only version fell through to `v-else` and\n * rendered a select with no options and a permanently disabled Send.)\n */\nimport TplModal from \"./TplModal.vue\";\n// Statically imported: this modal is already lazy-loaded as a whole, so the\n// preview rides its chunk rather than costing a second round-trip and flashing\n// an empty frame. Still shared with the saved-blocks surfaces, which lazy-load\n// it themselves.\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\n// The editor header's own control, reused verbatim so the two viewport switches\n// are the same component rather than two things that resemble each other.\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./MergeTagModeToggle.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { looksLikeEmail } from \"../utils/validateEmailShape\";\nimport { Check, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { hasMergeTagSamples, type ViewportSize } from \"@templatical/types\";\nimport {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\nimport type { TestEmailError } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n visible: boolean;\n /** Omitted / undefined means unrestricted, so the field accepts free text. */\n allowedRecipients?: string[];\n defaultRecipient?: string;\n isSending: boolean;\n justSent: boolean;\n error: TestEmailError | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send\", recipient: string): void;\n (e: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\nconst editor = inject(EDITOR_KEY, null);\n\nconst recipient = ref(\"\");\n\nconst previewViewport = ref<ViewportSize>(\"desktop\");\n\n/**\n * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\n\n/**\n * Shared with the editor's own preview, so switching here and switching there\n * are the same choice — it is a property of how the user wants to read a\n * preview, not of one dialog. Falls back to a local ref for headless mounts.\n */\nconst sharedSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Seeded from availability, matching how `useEditorCore` seeds the shared ref.\n// A blanket `true` would make the dialog claim tags \"show example values\" for a\n// consumer who configured none — false, and with no toggle rendered to correct\n// it, since the toggle hides itself when nothing has a sample.\nconst localSampleMode = ref(hasMergeTagSamples(inject(MERGE_TAGS_KEY, [])));\nconst sampleMode = computed({\n get: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\n\nconst hasAllowlist = computed(\n () =>\n props.allowedRecipients !== undefined && props.allowedRecipients.length > 0,\n);\nconst isSingleRecipient = computed(() => props.allowedRecipients?.length === 1);\n\n// Mounted behind a `v-if` on the same state that drives `visible`, so it can\n// mount with `visible` already true — without `immediate` the field would never\n// be seeded.\nwatch(\n () => props.visible,\n (visible) => {\n if (!visible) return;\n recipient.value =\n props.defaultRecipient ?? props.allowedRecipients?.[0] ?? \"\";\n },\n { immediate: true },\n);\n\n/** Shape-check free text only; a picker can't produce a malformed value. */\nconst isRecipientValid = computed(() =>\n hasAllowlist.value\n ? recipient.value.length > 0\n : looksLikeEmail(recipient.value),\n);\n\nconst canSend = computed(\n () => isRecipientValid.value && !props.isSending && !props.justSent,\n);\n\n/** Free text that's been typed into but isn't yet a plausible address. */\nconst showShapeHint = computed(\n () =>\n !hasAllowlist.value &&\n recipient.value.trim().length > 0 &&\n !isRecipientValid.value,\n);\n\nconst errorText = computed(() => {\n if (!props.error) return null;\n return props.error.kind === \"provider\"\n ? props.error.message\n : t.testEmail.recipientNotAllowed;\n});\n\nfunction handleSend(): void {\n if (!canSend.value) return;\n emit(\"send\", recipient.value.trim());\n}\n\nfunction handleClose(): void {\n // Sending is the one state that blocks dismissal; the success state closes\n // immediately rather than making the user wait out the timer.\n if (props.isSending) return;\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Grows to `max-w-3xl` only while the preview is open. A 600px email has\n to render at true size AND leave slack on both sides for the band of\n body background the preview draws beside it: at `max-w-2xl` the\n preview region's content box is exactly 600px, so the band would have\n nowhere to go and `settings.backgroundColor` would look unset for any\n template whose sections span the full width. Collapsed, it stays the\n compact form it was. `w-full` keeps it shrinking on narrow screens.\n `max-h-[90%]`\n with a `min-h-0 overflow-y-auto` preview region is the pattern the\n saved-blocks dialogs use: the cap bounds the panel to TplModal's\n backdrop (never to `vh` — see the note there), and `min-h-0` is what\n lets a flex child shrink below its content and actually scroll. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSending\"\n aria-labelledby=\"tpl-test-email-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-full tpl:max-w-3xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-test-email-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.testEmail.title }}\n </h3>\n\n <!-- Recipient -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-test-email-recipient\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.recipientLabel }}\n </label>\n\n <!-- Exactly one allowed address: nothing to choose. -->\n <input\n v-if=\"isSingleRecipient\"\n id=\"tpl-test-email-recipient\"\n type=\"text\"\n :value=\"recipient\"\n disabled\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:opacity-70 tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <!-- Several allowed addresses: pick one. -->\n <select\n v-else-if=\"hasAllowlist\"\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"email in allowedRecipients\"\n :key=\"email\"\n :value=\"email\"\n >\n {{ email }}\n </option>\n </select>\n\n <!-- No allowlist: free text. -->\n <input\n v-else\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n type=\"email\"\n autocomplete=\"email\"\n spellcheck=\"false\"\n :placeholder=\"t.testEmail.recipientPlaceholder\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <p\n v-if=\"showShapeHint\"\n class=\"tpl:mt-1.5 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.testEmail.invalidAddress }}\n </p>\n </div>\n\n <!-- Preview header: a label plus the viewport switch. Always shown — the\n preview is part of the dialog rather than something to opt into. -->\n <div class=\"tpl:mb-2 tpl:shrink-0\">\n <!-- Two segmented controls: `flex-wrap` so they drop to their own row\n rather than crushing the label on a narrow viewport. -->\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.preview }}\n </span>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <MergeTagModeToggle\n v-if=\"!previewResolution.supersedesSamples.value\"\n :sample-mode=\"sampleMode\"\n @change=\"sampleMode = $event\"\n />\n\n <!-- Without this the preview would silently show only the desktop\n variant of any responsive block. -->\n <ViewportToggle\n :viewport=\"previewViewport\"\n @change=\"previewViewport = $event\"\n />\n </div>\n </div>\n\n <!-- The hint has to follow the mode: \"shown unresolved\" is simply false\n once every tag renders a realistic value. -->\n <p\n data-testid=\"test-email-preview-hint\"\n class=\"tpl:mt-1 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : t.testEmail.previewHint\n }}\n </p>\n </div>\n\n <!-- The one region allowed to grow: `flex-1 min-h-0` so it absorbs the\n remaining height and scrolls, leaving the form and actions fixed. -->\n <div\n id=\"tpl-test-email-preview\"\n data-testid=\"test-email-preview\"\n class=\"tpl:mb-3 tpl:min-h-0 tpl:flex-1 tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius-sm)] tpl:p-3 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\n />\n </div>\n\n <!-- Success. Announced politely; the dialog closes itself shortly after. -->\n <p\n v-if=\"justSent\"\n role=\"status\"\n data-testid=\"test-email-success\"\n class=\"tpl:mb-3 tpl:flex tpl:shrink-0 tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success,var(--tpl-primary))]\"\n >\n <Check :size=\"13\" :stroke-width=\"2.5\" />\n {{ t.testEmail.success }}\n </p>\n\n <!-- Failure. Stays open so the user can retry. -->\n <p\n v-else-if=\"errorText\"\n role=\"alert\"\n data-testid=\"test-email-error\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ errorText }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"test-email-cancel\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-[var(--tpl-shadow-sm)] tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSending\"\n :class=\"{ 'tpl:cursor-not-allowed tpl:opacity-50': isSending }\"\n @click=\"handleClose\"\n >\n {{ t.testEmail.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"test-email-send\"\n :class=\"primaryBtnClass\"\n :disabled=\"!canSend\"\n @click=\"handleSend\"\n >\n <span v-if=\"isSending\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.testEmail.sending }}\n </span>\n <span v-else>\n {{ t.testEmail.send }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\n/**\n * Send-a-test dialog, shared by the OSS and Cloud editors.\n *\n * The recipient control has three shapes, driven entirely by whether the\n * provider configured an allowlist:\n *\n * - **no allowlist** — free text, validated for shape only. This branch exists\n * for BYO senders; Cloud never takes it, because its list comes from a signed\n * JWT.\n * - **exactly one** — a read-only field. Preserved verbatim from the Cloud-only\n * version; there is nothing to choose, so offering an editable field would\n * imply otherwise.\n * - **several** — a picker of exactly those addresses.\n *\n * An empty allowlist never reaches here: the feature reports itself unavailable,\n * so no trigger renders. (The Cloud-only version fell through to `v-else` and\n * rendered a select with no options and a permanently disabled Send.)\n */\nimport TplModal from \"./TplModal.vue\";\n// Statically imported: this modal is already lazy-loaded as a whole, so the\n// preview rides its chunk rather than costing a second round-trip and flashing\n// an empty frame. Still shared with the saved-blocks surfaces, which lazy-load\n// it themselves.\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\n// The editor header's own control, reused verbatim so the two viewport switches\n// are the same component rather than two things that resemble each other.\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./MergeTagModeToggle.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { looksLikeEmail } from \"../utils/validateEmailShape\";\nimport { Check, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { hasMergeTagSamples, type ViewportSize } from \"@templatical/types\";\nimport {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\nimport type { TestEmailError } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n visible: boolean;\n /** Omitted / undefined means unrestricted, so the field accepts free text. */\n allowedRecipients?: string[];\n defaultRecipient?: string;\n isSending: boolean;\n justSent: boolean;\n error: TestEmailError | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send\", recipient: string): void;\n (e: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\nconst editor = inject(EDITOR_KEY, null);\n\nconst recipient = ref(\"\");\n\nconst previewViewport = ref<ViewportSize>(\"desktop\");\n\n/**\n * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\n\n/**\n * Shared with the editor's own preview, so switching here and switching there\n * are the same choice — it is a property of how the user wants to read a\n * preview, not of one dialog. Falls back to a local ref for headless mounts.\n */\nconst sharedSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Seeded from availability, matching how `useEditorCore` seeds the shared ref.\n// A blanket `true` would make the dialog claim tags \"show example values\" for a\n// consumer who configured none — false, and with no toggle rendered to correct\n// it, since the toggle hides itself when nothing has a sample.\nconst localSampleMode = ref(hasMergeTagSamples(inject(MERGE_TAGS_KEY, [])));\nconst sampleMode = computed({\n get: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\n\nconst hasAllowlist = computed(\n () =>\n props.allowedRecipients !== undefined && props.allowedRecipients.length > 0,\n);\nconst isSingleRecipient = computed(() => props.allowedRecipients?.length === 1);\n\n// Mounted behind a `v-if` on the same state that drives `visible`, so it can\n// mount with `visible` already true — without `immediate` the field would never\n// be seeded.\nwatch(\n () => props.visible,\n (visible) => {\n if (!visible) return;\n recipient.value =\n props.defaultRecipient ?? props.allowedRecipients?.[0] ?? \"\";\n },\n { immediate: true },\n);\n\n/** Shape-check free text only; a picker can't produce a malformed value. */\nconst isRecipientValid = computed(() =>\n hasAllowlist.value\n ? recipient.value.length > 0\n : looksLikeEmail(recipient.value),\n);\n\nconst canSend = computed(\n () => isRecipientValid.value && !props.isSending && !props.justSent,\n);\n\n/** Free text that's been typed into but isn't yet a plausible address. */\nconst showShapeHint = computed(\n () =>\n !hasAllowlist.value &&\n recipient.value.trim().length > 0 &&\n !isRecipientValid.value,\n);\n\nconst errorText = computed(() => {\n if (!props.error) return null;\n return props.error.kind === \"provider\"\n ? props.error.message\n : t.testEmail.recipientNotAllowed;\n});\n\nfunction handleSend(): void {\n if (!canSend.value) return;\n emit(\"send\", recipient.value.trim());\n}\n\nfunction handleClose(): void {\n // Sending is the one state that blocks dismissal; the success state closes\n // immediately rather than making the user wait out the timer.\n if (props.isSending) return;\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Grows to `max-w-3xl` only while the preview is open. A 600px email has\n to render at true size AND leave slack on both sides for the band of\n body background the preview draws beside it: at `max-w-2xl` the\n preview region's content box is exactly 600px, so the band would have\n nowhere to go and `settings.backgroundColor` would look unset for any\n template whose sections span the full width. Collapsed, it stays the\n compact form it was. `w-full` keeps it shrinking on narrow screens.\n `max-h-[90%]`\n with a `min-h-0 overflow-y-auto` preview region is the pattern the\n saved-blocks dialogs use: the cap bounds the panel to TplModal's\n backdrop (never to `vh` — see the note there), and `min-h-0` is what\n lets a flex child shrink below its content and actually scroll. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSending\"\n aria-labelledby=\"tpl-test-email-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-full tpl:max-w-3xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-test-email-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.testEmail.title }}\n </h3>\n\n <!-- Recipient -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-test-email-recipient\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.recipientLabel }}\n </label>\n\n <!-- Exactly one allowed address: nothing to choose. -->\n <input\n v-if=\"isSingleRecipient\"\n id=\"tpl-test-email-recipient\"\n type=\"text\"\n :value=\"recipient\"\n disabled\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:opacity-70 tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <!-- Several allowed addresses: pick one. -->\n <select\n v-else-if=\"hasAllowlist\"\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"email in allowedRecipients\"\n :key=\"email\"\n :value=\"email\"\n >\n {{ email }}\n </option>\n </select>\n\n <!-- No allowlist: free text. -->\n <input\n v-else\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n type=\"email\"\n autocomplete=\"email\"\n spellcheck=\"false\"\n :placeholder=\"t.testEmail.recipientPlaceholder\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <p\n v-if=\"showShapeHint\"\n class=\"tpl:mt-1.5 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.testEmail.invalidAddress }}\n </p>\n </div>\n\n <!-- Preview header: a label plus the viewport switch. Always shown — the\n preview is part of the dialog rather than something to opt into. -->\n <div class=\"tpl:mb-2 tpl:shrink-0\">\n <!-- Two segmented controls: `flex-wrap` so they drop to their own row\n rather than crushing the label on a narrow viewport. -->\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.preview }}\n </span>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <MergeTagModeToggle\n v-if=\"!previewResolution.supersedesSamples.value\"\n :sample-mode=\"sampleMode\"\n @change=\"sampleMode = $event\"\n />\n\n <!-- Without this the preview would silently show only the desktop\n variant of any responsive block. -->\n <ViewportToggle\n :viewport=\"previewViewport\"\n @change=\"previewViewport = $event\"\n />\n </div>\n </div>\n\n <!-- The hint has to follow the mode: \"shown unresolved\" is simply false\n once every tag renders a realistic value. -->\n <p\n data-testid=\"test-email-preview-hint\"\n class=\"tpl:mt-1 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : t.testEmail.previewHint\n }}\n </p>\n </div>\n\n <!-- The one region allowed to grow: `flex-1 min-h-0` so it absorbs the\n remaining height and scrolls, leaving the form and actions fixed. -->\n <div\n id=\"tpl-test-email-preview\"\n data-testid=\"test-email-preview\"\n class=\"tpl:mb-3 tpl:min-h-0 tpl:flex-1 tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius-sm)] tpl:p-3 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\n />\n </div>\n\n <!-- Success. Announced politely; the dialog closes itself shortly after. -->\n <p\n v-if=\"justSent\"\n role=\"status\"\n data-testid=\"test-email-success\"\n class=\"tpl:mb-3 tpl:flex tpl:shrink-0 tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success,var(--tpl-primary))]\"\n >\n <Check :size=\"13\" :stroke-width=\"2.5\" />\n {{ t.testEmail.success }}\n </p>\n\n <!-- Failure. Stays open so the user can retry. -->\n <p\n v-else-if=\"errorText\"\n role=\"alert\"\n data-testid=\"test-email-error\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ errorText }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"test-email-cancel\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-[var(--tpl-shadow-sm)] tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSending\"\n :class=\"{ 'tpl:cursor-not-allowed tpl:opacity-50': isSending }\"\n @click=\"handleClose\"\n >\n {{ t.testEmail.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"test-email-send\"\n :class=\"primaryBtnClass\"\n :disabled=\"!canSend\"\n @click=\"handleSend\"\n >\n <span v-if=\"isSending\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.testEmail.sending }}\n </span>\n <span v-else>\n {{ t.testEmail.send }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
- import { N as e, S as t, at as n, h as r, m as i, x as a } from "./draggable-D2vo-aQI.js";
1
+ import { N as e, S as t, at as n, h as r, m as i, x as a } from "./draggable-kbOhCgnk.js";
2
2
  //#endregion
3
3
  //#region src/components/TestEmailPanel.vue
4
4
  var o = /* @__PURE__ */ t({
5
5
  __name: "TestEmailPanel",
6
6
  props: { feature: {} },
7
7
  setup(t) {
8
- let o = t, s = a(() => import("./TestEmailModal-B4L2lmIV.js"));
8
+ let o = t, s = a(() => import("./TestEmailModal-iWxAZRRq.js"));
9
9
  function c(e) {
10
10
  o.feature.send(e);
11
11
  }
@@ -32,4 +32,4 @@ var o = /* @__PURE__ */ t({
32
32
  //#endregion
33
33
  export { o as default };
34
34
 
35
- //# sourceMappingURL=TestEmailPanel-cTPgEf-D.js.map
35
+ //# sourceMappingURL=TestEmailPanel-CGUtEuwp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TestEmailPanel-cTPgEf-D.js","names":[],"sources":["../../../src/components/TestEmailPanel.vue","../../../src/components/TestEmailPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Mount point for the test-email dialog, shared by the OSS and Cloud editors.\n *\n * The dialog is `defineAsyncComponent` behind a `v-if` on its open state, so its\n * chunk is fetched only when the user actually opens it — `defineAsyncComponent`\n * triggers its `import()` on first render, not at definition time. `Editor.vue`\n * in turn lazy-loads *this* wrapper and renders it only when a\n * `TestEmailProvider` is configured, so a consumer without one downloads none\n * of it.\n *\n * Never static-import `TestEmailModal` into `Editor.vue`:\n * that collapses it into the main entry for every consumer, including those who\n * never configure a sender. Guarded by `tests/cdn-chunk-granularity.test.ts`.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n feature: UseTestEmailFeatureReturn;\n}>();\n\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\n\nfunction handleSend(recipient: string): void {\n void props.feature.send(recipient);\n}\n</script>\n\n<template>\n <TestEmailModal\n v-if=\"feature.isModalOpen.value\"\n :visible=\"feature.isModalOpen.value\"\n :allowed-recipients=\"feature.allowedRecipients.value\"\n :default-recipient=\"feature.defaultRecipient.value\"\n :is-sending=\"feature.isSending.value\"\n :just-sent=\"feature.justSent.value\"\n :error=\"feature.error.value\"\n @send=\"handleSend\"\n @close=\"feature.close()\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * Mount point for the test-email dialog, shared by the OSS and Cloud editors.\n *\n * The dialog is `defineAsyncComponent` behind a `v-if` on its open state, so its\n * chunk is fetched only when the user actually opens it — `defineAsyncComponent`\n * triggers its `import()` on first render, not at definition time. `Editor.vue`\n * in turn lazy-loads *this* wrapper and renders it only when a\n * `TestEmailProvider` is configured, so a consumer without one downloads none\n * of it.\n *\n * Never static-import `TestEmailModal` into `Editor.vue`:\n * that collapses it into the main entry for every consumer, including those who\n * never configure a sender. Guarded by `tests/cdn-chunk-granularity.test.ts`.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n feature: UseTestEmailFeatureReturn;\n}>();\n\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\n\nfunction handleSend(recipient: string): void {\n void props.feature.send(recipient);\n}\n</script>\n\n<template>\n <TestEmailModal\n v-if=\"feature.isModalOpen.value\"\n :visible=\"feature.isModalOpen.value\"\n :allowed-recipients=\"feature.allowedRecipients.value\"\n :default-recipient=\"feature.defaultRecipient.value\"\n :is-sending=\"feature.isSending.value\"\n :just-sent=\"feature.justSent.value\"\n :error=\"feature.error.value\"\n @send=\"handleSend\"\n @close=\"feature.close()\"\n />\n</template>\n"],"mappings":";;;;;;;EAkBA,IAAM,IAAQ,GAIR,IAAiB,QACf,OAAO,+BACf;EAEA,SAAS,EAAW,GAAyB;GAC3C,EAAW,QAAQ,KAAK,CAAS;EACnC;mBAKU,EAAA,QAAQ,YAAY,SAD5B,EAAA,GAAA,EAUE,EAAA,CAAA,GAAA;;GARC,SAAS,EAAA,QAAQ,YAAY;GAC7B,sBAAoB,EAAA,QAAQ,kBAAkB;GAC9C,qBAAmB,EAAA,QAAQ,iBAAiB;GAC5C,cAAY,EAAA,QAAQ,UAAU;GAC9B,aAAW,EAAA,QAAQ,SAAS;GAC5B,OAAO,EAAA,QAAQ,MAAM;GACrB,QAAM;GACN,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,QAAQ,MAAK"}
1
+ {"version":3,"file":"TestEmailPanel-CGUtEuwp.js","names":[],"sources":["../../../src/components/TestEmailPanel.vue","../../../src/components/TestEmailPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Mount point for the test-email dialog, shared by the OSS and Cloud editors.\n *\n * The dialog is `defineAsyncComponent` behind a `v-if` on its open state, so its\n * chunk is fetched only when the user actually opens it — `defineAsyncComponent`\n * triggers its `import()` on first render, not at definition time. `Editor.vue`\n * in turn lazy-loads *this* wrapper and renders it only when a\n * `TestEmailProvider` is configured, so a consumer without one downloads none\n * of it.\n *\n * Never static-import `TestEmailModal` into `Editor.vue`:\n * that collapses it into the main entry for every consumer, including those who\n * never configure a sender. Guarded by `tests/cdn-chunk-granularity.test.ts`.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n feature: UseTestEmailFeatureReturn;\n}>();\n\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\n\nfunction handleSend(recipient: string): void {\n void props.feature.send(recipient);\n}\n</script>\n\n<template>\n <TestEmailModal\n v-if=\"feature.isModalOpen.value\"\n :visible=\"feature.isModalOpen.value\"\n :allowed-recipients=\"feature.allowedRecipients.value\"\n :default-recipient=\"feature.defaultRecipient.value\"\n :is-sending=\"feature.isSending.value\"\n :just-sent=\"feature.justSent.value\"\n :error=\"feature.error.value\"\n @send=\"handleSend\"\n @close=\"feature.close()\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * Mount point for the test-email dialog, shared by the OSS and Cloud editors.\n *\n * The dialog is `defineAsyncComponent` behind a `v-if` on its open state, so its\n * chunk is fetched only when the user actually opens it — `defineAsyncComponent`\n * triggers its `import()` on first render, not at definition time. `Editor.vue`\n * in turn lazy-loads *this* wrapper and renders it only when a\n * `TestEmailProvider` is configured, so a consumer without one downloads none\n * of it.\n *\n * Never static-import `TestEmailModal` into `Editor.vue`:\n * that collapses it into the main entry for every consumer, including those who\n * never configure a sender. Guarded by `tests/cdn-chunk-granularity.test.ts`.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n feature: UseTestEmailFeatureReturn;\n}>();\n\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\n\nfunction handleSend(recipient: string): void {\n void props.feature.send(recipient);\n}\n</script>\n\n<template>\n <TestEmailModal\n v-if=\"feature.isModalOpen.value\"\n :visible=\"feature.isModalOpen.value\"\n :allowed-recipients=\"feature.allowedRecipients.value\"\n :default-recipient=\"feature.defaultRecipient.value\"\n :is-sending=\"feature.isSending.value\"\n :just-sent=\"feature.justSent.value\"\n :error=\"feature.error.value\"\n @send=\"handleSend\"\n @close=\"feature.close()\"\n />\n</template>\n"],"mappings":";;;;;;;EAkBA,IAAM,IAAQ,GAIR,IAAiB,QACf,OAAO,+BACf;EAEA,SAAS,EAAW,GAAyB;GAC3C,EAAW,QAAQ,KAAK,CAAS;EACnC;mBAKU,EAAA,QAAQ,YAAY,SAD5B,EAAA,GAAA,EAUE,EAAA,CAAA,GAAA;;GARC,SAAS,EAAA,QAAQ,YAAY;GAC7B,sBAAoB,EAAA,QAAQ,kBAAkB;GAC9C,qBAAmB,EAAA,QAAQ,iBAAiB;GAC5C,cAAY,EAAA,QAAQ,UAAU;GAC9B,aAAW,EAAA,QAAQ,SAAS;GAC5B,OAAO,EAAA,QAAQ,MAAM;GACrB,QAAM;GACN,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,QAAQ,MAAK"}
@@ -1,10 +1,10 @@
1
- import { E as e, N as t, S as n, at as r, b as i, ct as a, d as o, f as s, g as c, h as l, lt as u, m as d, p as f, q as p, u as m, ut as h, y as g } from "./draggable-D2vo-aQI.js";
2
- import { S as ee, x as te } from "./useEditorCore-CTNB2Mkl.js";
3
- import { B as ne, D as _, H as v, w as y } from "./keys-BJ770Mwm.js";
4
- import { t as b } from "./useI18n-DIhawILp.js";
5
- import { n as x } from "./usePopoverPosition-DkiTLnBm.js";
6
- import { P as re, Rt as ie, Vt as ae, ct as oe, it as se, ut as ce } from "./icons-ciivb5BS.js";
7
- import { n as S, t as C } from "./RichTextEditorContent-DdkmEEpK.js";
1
+ import { E as e, N as t, S as n, at as r, b as i, ct as a, d as o, f as s, g as c, h as l, lt as u, m as d, p as f, q as p, u as m, ut as h, y as g } from "./draggable-kbOhCgnk.js";
2
+ import { S as ee, x as te } from "./useEditorCore-ChlgO2GN.js";
3
+ import { B as ne, D as _, U as v, w as y } from "./keys-BOn5i9jV.js";
4
+ import { t as b } from "./useI18n-BdxNNw6j.js";
5
+ import { n as x } from "./usePopoverPosition-Dw1nTSNx.js";
6
+ import { P as re, Rt as ie, Vt as ae, ct as oe, it as se, ut as ce } from "./icons-SP5Itd-A.js";
7
+ import { n as S, t as C } from "./RichTextEditorContent-C1-5I8Df.js";
8
8
  //#region src/components/blocks/TitleEditor.vue?vue&type=script&setup=true&lang.ts
9
9
  var w = { class: "tpl-text-editor-wrapper tpl:relative" }, T = ["data-tpl-theme", "aria-label"], E = ["aria-label", "title"], D = ["aria-label", "title"], O = ["aria-label", "title"], k = {
10
10
  key: 0,
@@ -30,10 +30,10 @@ var w = { class: "tpl-text-editor-wrapper tpl:relative" }, T = ["data-tpl-theme"
30
30
  editorName: "TitleEditor",
31
31
  async loadExtensions({ mergeTags: e, syntax: t, triggerChar: n, autocompleteEnabled: r, suggestionEmptyText: i }) {
32
32
  let [{ Editor: a, EditorContent: o }, { default: s }, { default: c }, { MergeTagNode: l, MergeTagSuggestion: u, LogicMergeTagNode: d, LineBoundaryKeys: f }] = await Promise.all([
33
- import("./tiptap-Ey0FpaX7.js").then((e) => e.r),
34
- import("./tiptap-Ey0FpaX7.js").then((e) => e.i),
35
- import("./tiptap-Ey0FpaX7.js").then((e) => e.l),
36
- import("./extensions-C87Z6_23.js")
33
+ import("./tiptap-Epe8m3Vo.js").then((e) => e.r),
34
+ import("./tiptap-Epe8m3Vo.js").then((e) => e.i),
35
+ import("./tiptap-Epe8m3Vo.js").then((e) => e.l),
36
+ import("./extensions-BUPHNpKD.js")
37
37
  ]);
38
38
  return {
39
39
  TiptapEditor: a,
@@ -196,4 +196,4 @@ var w = { class: "tpl-text-editor-wrapper tpl:relative" }, T = ["data-tpl-theme"
196
196
  //#endregion
197
197
  export { P as default };
198
198
 
199
- //# sourceMappingURL=TitleEditor-BKjL2Zs1.js.map
199
+ //# sourceMappingURL=TitleEditor-DvXUW2j-.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TitleEditor-BKjL2Zs1.js","names":[],"sources":["../../../src/components/blocks/TitleEditor.vue","../../../src/components/blocks/TitleEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables\";\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { TitleBlock as TitleBlockType } from \"@templatical/types\";\nimport {\n Bold,\n Braces,\n Italic,\n Link,\n LoaderCircle,\n ScanLine,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n} from \"../../keys\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst popoverRoot = usePopoverRoot();\n// Hide the floating toolbar while either built-in picker modal is open — see\n// ParagraphToolbar.vue for the same rationale.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"TitleEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { MergeTagNode, MergeTagSuggestion, LogicMergeTagNode, LineBoundaryKeys },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n bulletList: false,\n orderedList: false,\n listItem: false,\n strike: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n }),\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.titleEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Bold -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('bold'),\n }\"\n :aria-label=\"t.titleEditor.bold\"\n :title=\"t.titleEditor.bold\"\n @click=\"editor?.chain().focus().toggleBold().run()\"\n >\n <Bold :size=\"16\" :stroke-width=\"2.5\" />\n </button>\n <!-- Italic -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('italic'),\n }\"\n :aria-label=\"t.titleEditor.italic\"\n :title=\"t.titleEditor.italic\"\n @click=\"editor?.chain().focus().toggleItalic().run()\"\n >\n <Italic :size=\"16\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n aria-hidden=\"true\"\n ></span>\n <!-- Link -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('link'),\n }\"\n :aria-label=\"t.titleEditor.addLink\"\n :title=\"t.titleEditor.addLink\"\n @click=\"openLinkDialog\"\n >\n <Link :size=\"16\" :stroke-width=\"2\" />\n </button>\n <!-- Add Merge Tag -->\n <span\n v-if=\"canRequestMergeTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canRequestMergeTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"handleAddMergeTag\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n <!-- Insert Logic -->\n <span\n v-if=\"canInsertLogicTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canInsertLogicTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"handleAddLogicTag\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables\";\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { TitleBlock as TitleBlockType } from \"@templatical/types\";\nimport {\n Bold,\n Braces,\n Italic,\n Link,\n LoaderCircle,\n ScanLine,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n} from \"../../keys\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst popoverRoot = usePopoverRoot();\n// Hide the floating toolbar while either built-in picker modal is open — see\n// ParagraphToolbar.vue for the same rationale.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"TitleEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { MergeTagNode, MergeTagSuggestion, LogicMergeTagNode, LineBoundaryKeys },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n bulletList: false,\n orderedList: false,\n listItem: false,\n strike: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n }),\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.titleEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Bold -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('bold'),\n }\"\n :aria-label=\"t.titleEditor.bold\"\n :title=\"t.titleEditor.bold\"\n @click=\"editor?.chain().focus().toggleBold().run()\"\n >\n <Bold :size=\"16\" :stroke-width=\"2.5\" />\n </button>\n <!-- Italic -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('italic'),\n }\"\n :aria-label=\"t.titleEditor.italic\"\n :title=\"t.titleEditor.italic\"\n @click=\"editor?.chain().focus().toggleItalic().run()\"\n >\n <Italic :size=\"16\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n aria-hidden=\"true\"\n ></span>\n <!-- Link -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('link'),\n }\"\n :aria-label=\"t.titleEditor.addLink\"\n :title=\"t.titleEditor.addLink\"\n @click=\"openLinkDialog\"\n >\n <Link :size=\"16\" :stroke-width=\"2\" />\n </button>\n <!-- Add Merge Tag -->\n <span\n v-if=\"canRequestMergeTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canRequestMergeTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"handleAddMergeTag\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n <!-- Insert Logic -->\n <span\n v-if=\"canInsertLogicTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canInsertLogicTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"handleAddLogicTag\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"TitleEditor-DvXUW2j-.js","names":[],"sources":["../../../src/components/blocks/TitleEditor.vue","../../../src/components/blocks/TitleEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables\";\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { TitleBlock as TitleBlockType } from \"@templatical/types\";\nimport {\n Bold,\n Braces,\n Italic,\n Link,\n LoaderCircle,\n ScanLine,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n} from \"../../keys\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst popoverRoot = usePopoverRoot();\n// Hide the floating toolbar while either built-in picker modal is open — see\n// ParagraphToolbar.vue for the same rationale.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"TitleEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { MergeTagNode, MergeTagSuggestion, LogicMergeTagNode, LineBoundaryKeys },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n bulletList: false,\n orderedList: false,\n listItem: false,\n strike: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n }),\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.titleEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Bold -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('bold'),\n }\"\n :aria-label=\"t.titleEditor.bold\"\n :title=\"t.titleEditor.bold\"\n @click=\"editor?.chain().focus().toggleBold().run()\"\n >\n <Bold :size=\"16\" :stroke-width=\"2.5\" />\n </button>\n <!-- Italic -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('italic'),\n }\"\n :aria-label=\"t.titleEditor.italic\"\n :title=\"t.titleEditor.italic\"\n @click=\"editor?.chain().focus().toggleItalic().run()\"\n >\n <Italic :size=\"16\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n aria-hidden=\"true\"\n ></span>\n <!-- Link -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('link'),\n }\"\n :aria-label=\"t.titleEditor.addLink\"\n :title=\"t.titleEditor.addLink\"\n @click=\"openLinkDialog\"\n >\n <Link :size=\"16\" :stroke-width=\"2\" />\n </button>\n <!-- Add Merge Tag -->\n <span\n v-if=\"canRequestMergeTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canRequestMergeTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"handleAddMergeTag\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n <!-- Insert Logic -->\n <span\n v-if=\"canInsertLogicTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canInsertLogicTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"handleAddLogicTag\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables\";\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { TitleBlock as TitleBlockType } from \"@templatical/types\";\nimport {\n Bold,\n Braces,\n Italic,\n Link,\n LoaderCircle,\n ScanLine,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n} from \"../../keys\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst popoverRoot = usePopoverRoot();\n// Hide the floating toolbar while either built-in picker modal is open — see\n// ParagraphToolbar.vue for the same rationale.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"TitleEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { MergeTagNode, MergeTagSuggestion, LogicMergeTagNode, LineBoundaryKeys },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n bulletList: false,\n orderedList: false,\n listItem: false,\n strike: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n }),\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.titleEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Bold -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('bold'),\n }\"\n :aria-label=\"t.titleEditor.bold\"\n :title=\"t.titleEditor.bold\"\n @click=\"editor?.chain().focus().toggleBold().run()\"\n >\n <Bold :size=\"16\" :stroke-width=\"2.5\" />\n </button>\n <!-- Italic -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('italic'),\n }\"\n :aria-label=\"t.titleEditor.italic\"\n :title=\"t.titleEditor.italic\"\n @click=\"editor?.chain().focus().toggleItalic().run()\"\n >\n <Italic :size=\"16\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n aria-hidden=\"true\"\n ></span>\n <!-- Link -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('link'),\n }\"\n :aria-label=\"t.titleEditor.addLink\"\n :title=\"t.titleEditor.addLink\"\n @click=\"openLinkDialog\"\n >\n <Link :size=\"16\" :stroke-width=\"2\" />\n </button>\n <!-- Add Merge Tag -->\n <span\n v-if=\"canRequestMergeTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canRequestMergeTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"handleAddMergeTag\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n <!-- Insert Logic -->\n <span\n v-if=\"canInsertLogicTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canInsertLogicTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"handleAddLogicTag\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { E as e, H as t, I as n, N as r, Q as i, S as a, at as o, b as s, d as c, f as l, g as u, h as d, l as f, lt as p, m, n as h, p as g } from "./draggable-D2vo-aQI.js";
2
- import { E as _ } from "./useEditorCore-CTNB2Mkl.js";
3
- import { B as v, H as y } from "./keys-BJ770Mwm.js";
4
- import { n as b } from "./usePopoverPosition-DkiTLnBm.js";
1
+ import { E as e, H as t, I as n, N as r, Q as i, S as a, at as o, b as s, d as c, f as l, g as u, h as d, l as f, lt as p, m, n as h, p as g } from "./draggable-kbOhCgnk.js";
2
+ import { E as _ } from "./useEditorCore-ChlgO2GN.js";
3
+ import { B as v, U as y } from "./keys-BOn5i9jV.js";
4
+ import { n as b } from "./usePopoverPosition-Dw1nTSNx.js";
5
5
  //#region src/components/TplModal.vue?vue&type=script&setup=true&lang.ts
6
6
  var x = ["data-tpl-theme"], S = /* @__PURE__ */ a({
7
7
  __name: "TplModal",
@@ -49,4 +49,4 @@ var x = ["data-tpl-theme"], S = /* @__PURE__ */ a({
49
49
  //#endregion
50
50
  export { S as t };
51
51
 
52
- //# sourceMappingURL=TplModal-DxrgNmt9.js.map
52
+ //# sourceMappingURL=TplModal-X-qNoLGZ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TplModal-DxrgNmt9.js","names":[],"sources":["../../../src/components/TplModal.vue","../../../src/components/TplModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useFocusTrap } from \"../composables\";\nimport { usePopoverRoot } from \"../composables/usePopoverRoot\";\nimport { THEME_STYLES_KEY, UI_THEME_KEY } from \"../keys\";\nimport { computed, inject, ref } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"keydown\", event: KeyboardEvent): void;\n}>();\n\nconst dialogRef = ref<HTMLElement | null>(null);\nconst isVisible = computed(() => props.visible);\nuseFocusTrap(dialogRef, isVisible);\n\nconst tplUiTheme = inject(UI_THEME_KEY);\n\n// The backdrop below carries the bare `tpl` class, which re-declares every\n// `--tpl-*` token from the base `.tpl` rule. A custom property declared on an\n// element beats one inherited from an ancestor, so that re-declaration shadows\n// the consumer's `theme` config — which lives as inline styles on the editor\n// root — for this whole teleported subtree. Re-applying `themeStyles` here is\n// what restores it, and doing it on the backdrop covers every dialog rendered\n// through this wrapper rather than each one remembering to. `data-tpl-theme`\n// does the same job for the dark token block. See the invariant in\n// `tests/theme-token-scope.test.ts` (issue #487).\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst popoverRoot = usePopoverRoot();\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n emit(\"close\");\n }\n emit(\"keydown\", event);\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot\" :to=\"popoverRoot\">\n <Transition\n enter-active-class=\"tpl:transition\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition tpl:duration-100\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <!-- The backdrop is the reference box every slotted panel caps against,\n which is why it owns the gutter (`p-4`) rather than each panel\n carrying its own margin. `inset-0` gives it a definite height; the\n wrapper below spans that height so a panel's percentage cap\n (`max-h-[90%]` / `max-h-[80%]`) has an unbroken chain to resolve\n through. Those numbers are the `vh` values they replaced, so an\n untrapped editor renders exactly as before.\n\n Panels must never cap in `vh`. `fixed` covers the viewport only while\n nothing traps it, and an ancestor with `transform`, `filter`,\n `backdrop-filter`, `will-change: transform`, `contain: paint`,\n `container-type` or a transform animation becomes the containing\n block for fixed descendants. The editor is embedded in someone\n else's page, so any of those can sit above it: `inset-0` then\n resolves to that ancestor's box while a `vh` cap still resolves to\n the viewport, and the panel overflows a container that usually also\n has `overflow: hidden`. That is issue #575 — in a 420px host inside a\n 720px viewport, a 648px panel clipped ~113px off both the top and the\n bottom, with no scrollbar to reach the buttons. Capping against the\n backdrop is self-correcting whether or not the trap is present.\n Locked by `tests/overlay-height-scope.test.ts`.\n\n The padding belongs here and not on the wrapper: preflight is\n omitted, so non-form elements keep `content-box` and padding on an\n `h-full` element would add to 100% and overflow by exactly the\n padding (the trap behind issue #115). Insets size the backdrop's\n border box, so padding here safely shrinks the content box instead. -->\n <div\n v-if=\"visible\"\n :data-tpl-theme=\"tplUiTheme\"\n class=\"tpl tpl:fixed tpl:inset-0 tpl:z-modal tpl:flex tpl:items-center tpl:justify-center tpl:p-4\"\n style=\"\n background-color: var(--tpl-overlay);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n :style=\"themeStyles\"\n @click.self=\"emit('close')\"\n @keydown=\"handleKeydown\"\n >\n <!-- `h-full` only, deliberately no `w-full`: height is what has to\n become definite, and widening this element would change what\n `w-full` on a panel resolves against. Panels are shrink-to-fit\n horizontally — the test-email dialog stays compact until its\n preview opens, and the saved-blocks browser pins an explicit width\n precisely because this wrapper is shrink-to-fit. Constraining one\n axis leaves all of that intact.\n\n It repeats `@click.self` because it now covers the backdrop's full\n height: clicks in the gap above or below the panel land here, so\n without it click-outside-to-close would only work on the padding\n ring and the columns either side. -->\n <div\n ref=\"dialogRef\"\n class=\"tpl:flex tpl:h-full tpl:items-center tpl:justify-center\"\n @click.self=\"emit('close')\"\n >\n <slot />\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\nimport { useFocusTrap } from \"../composables\";\nimport { usePopoverRoot } from \"../composables/usePopoverRoot\";\nimport { THEME_STYLES_KEY, UI_THEME_KEY } from \"../keys\";\nimport { computed, inject, ref } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"keydown\", event: KeyboardEvent): void;\n}>();\n\nconst dialogRef = ref<HTMLElement | null>(null);\nconst isVisible = computed(() => props.visible);\nuseFocusTrap(dialogRef, isVisible);\n\nconst tplUiTheme = inject(UI_THEME_KEY);\n\n// The backdrop below carries the bare `tpl` class, which re-declares every\n// `--tpl-*` token from the base `.tpl` rule. A custom property declared on an\n// element beats one inherited from an ancestor, so that re-declaration shadows\n// the consumer's `theme` config — which lives as inline styles on the editor\n// root — for this whole teleported subtree. Re-applying `themeStyles` here is\n// what restores it, and doing it on the backdrop covers every dialog rendered\n// through this wrapper rather than each one remembering to. `data-tpl-theme`\n// does the same job for the dark token block. See the invariant in\n// `tests/theme-token-scope.test.ts` (issue #487).\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst popoverRoot = usePopoverRoot();\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n emit(\"close\");\n }\n emit(\"keydown\", event);\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot\" :to=\"popoverRoot\">\n <Transition\n enter-active-class=\"tpl:transition\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition tpl:duration-100\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <!-- The backdrop is the reference box every slotted panel caps against,\n which is why it owns the gutter (`p-4`) rather than each panel\n carrying its own margin. `inset-0` gives it a definite height; the\n wrapper below spans that height so a panel's percentage cap\n (`max-h-[90%]` / `max-h-[80%]`) has an unbroken chain to resolve\n through. Those numbers are the `vh` values they replaced, so an\n untrapped editor renders exactly as before.\n\n Panels must never cap in `vh`. `fixed` covers the viewport only while\n nothing traps it, and an ancestor with `transform`, `filter`,\n `backdrop-filter`, `will-change: transform`, `contain: paint`,\n `container-type` or a transform animation becomes the containing\n block for fixed descendants. The editor is embedded in someone\n else's page, so any of those can sit above it: `inset-0` then\n resolves to that ancestor's box while a `vh` cap still resolves to\n the viewport, and the panel overflows a container that usually also\n has `overflow: hidden`. That is issue #575 — in a 420px host inside a\n 720px viewport, a 648px panel clipped ~113px off both the top and the\n bottom, with no scrollbar to reach the buttons. Capping against the\n backdrop is self-correcting whether or not the trap is present.\n Locked by `tests/overlay-height-scope.test.ts`.\n\n The padding belongs here and not on the wrapper: preflight is\n omitted, so non-form elements keep `content-box` and padding on an\n `h-full` element would add to 100% and overflow by exactly the\n padding (the trap behind issue #115). Insets size the backdrop's\n border box, so padding here safely shrinks the content box instead. -->\n <div\n v-if=\"visible\"\n :data-tpl-theme=\"tplUiTheme\"\n class=\"tpl tpl:fixed tpl:inset-0 tpl:z-modal tpl:flex tpl:items-center tpl:justify-center tpl:p-4\"\n style=\"\n background-color: var(--tpl-overlay);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n :style=\"themeStyles\"\n @click.self=\"emit('close')\"\n @keydown=\"handleKeydown\"\n >\n <!-- `h-full` only, deliberately no `w-full`: height is what has to\n become definite, and widening this element would change what\n `w-full` on a panel resolves against. Panels are shrink-to-fit\n horizontally — the test-email dialog stays compact until its\n preview opens, and the saved-blocks browser pins an explicit width\n precisely because this wrapper is shrink-to-fit. Constraining one\n axis leaves all of that intact.\n\n It repeats `@click.self` because it now covers the backdrop's full\n height: clicks in the gap above or below the panel land here, so\n without it click-outside-to-close would only work on the padding\n ring and the columns either side. -->\n <div\n ref=\"dialogRef\"\n class=\"tpl:flex tpl:h-full tpl:items-center tpl:justify-center\"\n @click.self=\"emit('close')\"\n >\n <slot />\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n"],"mappings":";;;;;;;;;;EAMA,IAAM,IAAQ,GAIR,IAAO,GAKP,IAAY,EAAwB,IAAI,GACxC,IAAY,QAAe,EAAM,OAAO;EAC9C,EAAa,GAAW,CAAS;EAEjC,IAAM,IAAa,EAAO,CAAY,GAWhC,IAAc,EAAO,GAAkB,IAAI,GAC3C,IAAc,EAAe;EAEnC,SAAS,EAAc,GAA4B;GAIjD,AAHI,EAAM,QAAQ,YAChB,EAAK,OAAO,GAEd,EAAK,WAAW,CAAK;EACvB;mBAIkB,EAAA,CAAA,KAAhB,EAAA,GAAA,EAsEW,GAAA;;GAtEmB,IAAI,EAAA,CAAA;EAChC,GAAA,CAAA,EAoEa,GAAA;GAnEX,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;GACf,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;;GA6Bf,SAAA,QAgCM,CA/BE,EAAA,WADR,EAAA,GAAA,EAgCM,OAAA;;IA9BH,kBAAgB,EAAA,CAAA;IACjB,OAAM;IACN,OAIC,EAAA,CAJD;KAAA,oBAAA;KAAA,mBAAA;KAAA,2BAAA;IAAA,GAKQ,EAAA,CAAA,CAAW,CAAA;IAClB,SAAK,AAAA,EAAA,OAAA,GAAA,MAAO,EAAI,OAAA,GAAA,CAAA,MAAA,CAAA;IAChB,WAAS;GAcV,GAAA,CAAA,EAMM,OAAA;IALA,SAAA;IAAJ,KAAI;IACJ,OAAM;IACL,SAAK,AAAA,EAAA,OAAA,GAAA,MAAO,EAAI,OAAA,GAAA,CAAA,MAAA,CAAA;GAEjB,GAAA,CAAA,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,GAAA,CAAA,GAAA,IAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"TplModal-X-qNoLGZ.js","names":[],"sources":["../../../src/components/TplModal.vue","../../../src/components/TplModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useFocusTrap } from \"../composables\";\nimport { usePopoverRoot } from \"../composables/usePopoverRoot\";\nimport { THEME_STYLES_KEY, UI_THEME_KEY } from \"../keys\";\nimport { computed, inject, ref } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"keydown\", event: KeyboardEvent): void;\n}>();\n\nconst dialogRef = ref<HTMLElement | null>(null);\nconst isVisible = computed(() => props.visible);\nuseFocusTrap(dialogRef, isVisible);\n\nconst tplUiTheme = inject(UI_THEME_KEY);\n\n// The backdrop below carries the bare `tpl` class, which re-declares every\n// `--tpl-*` token from the base `.tpl` rule. A custom property declared on an\n// element beats one inherited from an ancestor, so that re-declaration shadows\n// the consumer's `theme` config — which lives as inline styles on the editor\n// root — for this whole teleported subtree. Re-applying `themeStyles` here is\n// what restores it, and doing it on the backdrop covers every dialog rendered\n// through this wrapper rather than each one remembering to. `data-tpl-theme`\n// does the same job for the dark token block. See the invariant in\n// `tests/theme-token-scope.test.ts` (issue #487).\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst popoverRoot = usePopoverRoot();\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n emit(\"close\");\n }\n emit(\"keydown\", event);\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot\" :to=\"popoverRoot\">\n <Transition\n enter-active-class=\"tpl:transition\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition tpl:duration-100\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <!-- The backdrop is the reference box every slotted panel caps against,\n which is why it owns the gutter (`p-4`) rather than each panel\n carrying its own margin. `inset-0` gives it a definite height; the\n wrapper below spans that height so a panel's percentage cap\n (`max-h-[90%]` / `max-h-[80%]`) has an unbroken chain to resolve\n through. Those numbers are the `vh` values they replaced, so an\n untrapped editor renders exactly as before.\n\n Panels must never cap in `vh`. `fixed` covers the viewport only while\n nothing traps it, and an ancestor with `transform`, `filter`,\n `backdrop-filter`, `will-change: transform`, `contain: paint`,\n `container-type` or a transform animation becomes the containing\n block for fixed descendants. The editor is embedded in someone\n else's page, so any of those can sit above it: `inset-0` then\n resolves to that ancestor's box while a `vh` cap still resolves to\n the viewport, and the panel overflows a container that usually also\n has `overflow: hidden`. That is issue #575 — in a 420px host inside a\n 720px viewport, a 648px panel clipped ~113px off both the top and the\n bottom, with no scrollbar to reach the buttons. Capping against the\n backdrop is self-correcting whether or not the trap is present.\n Locked by `tests/overlay-height-scope.test.ts`.\n\n The padding belongs here and not on the wrapper: preflight is\n omitted, so non-form elements keep `content-box` and padding on an\n `h-full` element would add to 100% and overflow by exactly the\n padding (the trap behind issue #115). Insets size the backdrop's\n border box, so padding here safely shrinks the content box instead. -->\n <div\n v-if=\"visible\"\n :data-tpl-theme=\"tplUiTheme\"\n class=\"tpl tpl:fixed tpl:inset-0 tpl:z-modal tpl:flex tpl:items-center tpl:justify-center tpl:p-4\"\n style=\"\n background-color: var(--tpl-overlay);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n :style=\"themeStyles\"\n @click.self=\"emit('close')\"\n @keydown=\"handleKeydown\"\n >\n <!-- `h-full` only, deliberately no `w-full`: height is what has to\n become definite, and widening this element would change what\n `w-full` on a panel resolves against. Panels are shrink-to-fit\n horizontally — the test-email dialog stays compact until its\n preview opens, and the saved-blocks browser pins an explicit width\n precisely because this wrapper is shrink-to-fit. Constraining one\n axis leaves all of that intact.\n\n It repeats `@click.self` because it now covers the backdrop's full\n height: clicks in the gap above or below the panel land here, so\n without it click-outside-to-close would only work on the padding\n ring and the columns either side. -->\n <div\n ref=\"dialogRef\"\n class=\"tpl:flex tpl:h-full tpl:items-center tpl:justify-center\"\n @click.self=\"emit('close')\"\n >\n <slot />\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\nimport { useFocusTrap } from \"../composables\";\nimport { usePopoverRoot } from \"../composables/usePopoverRoot\";\nimport { THEME_STYLES_KEY, UI_THEME_KEY } from \"../keys\";\nimport { computed, inject, ref } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"keydown\", event: KeyboardEvent): void;\n}>();\n\nconst dialogRef = ref<HTMLElement | null>(null);\nconst isVisible = computed(() => props.visible);\nuseFocusTrap(dialogRef, isVisible);\n\nconst tplUiTheme = inject(UI_THEME_KEY);\n\n// The backdrop below carries the bare `tpl` class, which re-declares every\n// `--tpl-*` token from the base `.tpl` rule. A custom property declared on an\n// element beats one inherited from an ancestor, so that re-declaration shadows\n// the consumer's `theme` config — which lives as inline styles on the editor\n// root — for this whole teleported subtree. Re-applying `themeStyles` here is\n// what restores it, and doing it on the backdrop covers every dialog rendered\n// through this wrapper rather than each one remembering to. `data-tpl-theme`\n// does the same job for the dark token block. See the invariant in\n// `tests/theme-token-scope.test.ts` (issue #487).\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst popoverRoot = usePopoverRoot();\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n emit(\"close\");\n }\n emit(\"keydown\", event);\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot\" :to=\"popoverRoot\">\n <Transition\n enter-active-class=\"tpl:transition\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition tpl:duration-100\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <!-- The backdrop is the reference box every slotted panel caps against,\n which is why it owns the gutter (`p-4`) rather than each panel\n carrying its own margin. `inset-0` gives it a definite height; the\n wrapper below spans that height so a panel's percentage cap\n (`max-h-[90%]` / `max-h-[80%]`) has an unbroken chain to resolve\n through. Those numbers are the `vh` values they replaced, so an\n untrapped editor renders exactly as before.\n\n Panels must never cap in `vh`. `fixed` covers the viewport only while\n nothing traps it, and an ancestor with `transform`, `filter`,\n `backdrop-filter`, `will-change: transform`, `contain: paint`,\n `container-type` or a transform animation becomes the containing\n block for fixed descendants. The editor is embedded in someone\n else's page, so any of those can sit above it: `inset-0` then\n resolves to that ancestor's box while a `vh` cap still resolves to\n the viewport, and the panel overflows a container that usually also\n has `overflow: hidden`. That is issue #575 — in a 420px host inside a\n 720px viewport, a 648px panel clipped ~113px off both the top and the\n bottom, with no scrollbar to reach the buttons. Capping against the\n backdrop is self-correcting whether or not the trap is present.\n Locked by `tests/overlay-height-scope.test.ts`.\n\n The padding belongs here and not on the wrapper: preflight is\n omitted, so non-form elements keep `content-box` and padding on an\n `h-full` element would add to 100% and overflow by exactly the\n padding (the trap behind issue #115). Insets size the backdrop's\n border box, so padding here safely shrinks the content box instead. -->\n <div\n v-if=\"visible\"\n :data-tpl-theme=\"tplUiTheme\"\n class=\"tpl tpl:fixed tpl:inset-0 tpl:z-modal tpl:flex tpl:items-center tpl:justify-center tpl:p-4\"\n style=\"\n background-color: var(--tpl-overlay);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n :style=\"themeStyles\"\n @click.self=\"emit('close')\"\n @keydown=\"handleKeydown\"\n >\n <!-- `h-full` only, deliberately no `w-full`: height is what has to\n become definite, and widening this element would change what\n `w-full` on a panel resolves against. Panels are shrink-to-fit\n horizontally — the test-email dialog stays compact until its\n preview opens, and the saved-blocks browser pins an explicit width\n precisely because this wrapper is shrink-to-fit. Constraining one\n axis leaves all of that intact.\n\n It repeats `@click.self` because it now covers the backdrop's full\n height: clicks in the gap above or below the panel land here, so\n without it click-outside-to-close would only work on the padding\n ring and the columns either side. -->\n <div\n ref=\"dialogRef\"\n class=\"tpl:flex tpl:h-full tpl:items-center tpl:justify-center\"\n @click.self=\"emit('close')\"\n >\n <slot />\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n"],"mappings":";;;;;;;;;;EAMA,IAAM,IAAQ,GAIR,IAAO,GAKP,IAAY,EAAwB,IAAI,GACxC,IAAY,QAAe,EAAM,OAAO;EAC9C,EAAa,GAAW,CAAS;EAEjC,IAAM,IAAa,EAAO,CAAY,GAWhC,IAAc,EAAO,GAAkB,IAAI,GAC3C,IAAc,EAAe;EAEnC,SAAS,EAAc,GAA4B;GAIjD,AAHI,EAAM,QAAQ,YAChB,EAAK,OAAO,GAEd,EAAK,WAAW,CAAK;EACvB;mBAIkB,EAAA,CAAA,KAAhB,EAAA,GAAA,EAsEW,GAAA;;GAtEmB,IAAI,EAAA,CAAA;EAChC,GAAA,CAAA,EAoEa,GAAA;GAnEX,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;GACf,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;;GA6Bf,SAAA,QAgCM,CA/BE,EAAA,WADR,EAAA,GAAA,EAgCM,OAAA;;IA9BH,kBAAgB,EAAA,CAAA;IACjB,OAAM;IACN,OAIC,EAAA,CAJD;KAAA,oBAAA;KAAA,mBAAA;KAAA,2BAAA;IAAA,GAKQ,EAAA,CAAA,CAAW,CAAA;IAClB,SAAK,AAAA,EAAA,OAAA,GAAA,MAAO,EAAI,OAAA,GAAA,CAAA,MAAA,CAAA;IAChB,WAAS;GAcV,GAAA,CAAA,EAMM,OAAA;IALA,SAAA;IAAJ,KAAI;IACJ,OAAM;IACL,SAAK,AAAA,EAAA,OAAA,GAAA,MAAO,EAAI,OAAA,GAAA,CAAA,MAAA,CAAA;GAEjB,GAAA,CAAA,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,GAAA,CAAA,GAAA,IAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA"}