@templatical/editor 0.41.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/dist/{AiChatSidebar-a8TPBAl2.js → AiChatSidebar-DOCvzGBe.js} +8 -8
  2. package/dist/{AiFeatureMenu-CroDMqRG.js → AiFeatureMenu-CfijAx4G.js} +4 -4
  3. package/dist/{BlockIssueBadge-D8KsGWY6.js → BlockIssueBadge-BJ3A6mqh.js} +2 -2
  4. package/dist/{BlockPreviewCanvas-Z9SZIgfA.js → BlockPreviewCanvas-DggfBaoJ.js} +33 -33
  5. package/dist/{CloudHeaderExtras-CVqHeqc6.js → CloudHeaderExtras-dFbdqBr5.js} +2 -2
  6. package/dist/{CloudPanels-CmEpQUJB.js → CloudPanels-DmMxhzzZ.js} +4 -4
  7. package/dist/{CollaboratorBar-AIQC9fEF.js → CollaboratorBar-XXk1tOC5.js} +2 -2
  8. package/dist/{CommentsPanel-9k1q-79y.js → CommentsPanel-CGEhbjx0.js} +1 -1
  9. package/dist/{CommentsSidebar-Bc7nORmD.js → CommentsSidebar-CUpst9ou.js} +10 -10
  10. package/dist/{CountdownToolbar-DkC-qSgq.js → CountdownToolbar-B1a_UIuB.js} +3 -3
  11. package/dist/{DesignReferenceSidebar-OoiYJAKS.js → DesignReferenceSidebar-DkAmsBgM.js} +6 -6
  12. package/dist/{IssuesPanel-Dnns3O9z.js → IssuesPanel-lTpsCV4a.js} +5 -5
  13. package/dist/{LogicTagInsertButton-BimKobaG.js → LogicTagInsertButton-B5YymR4U.js} +243 -219
  14. package/dist/{MediaEditModal-C-InB-H2.js → MediaEditModal-B1X8qtJb.js} +1 -1
  15. package/dist/{MediaPanels-CTosUpeo.js → MediaPanels-DO30L1KS.js} +1 -1
  16. package/dist/{MergeTagInput-DqpVzhy7.js → MergeTagInput-B0M_-X0K.js} +1 -1
  17. package/dist/{MergeTagModeToggle-BkIiAgpQ.js → MergeTagModeToggle-B-8W1tK7.js} +4 -4
  18. package/dist/{ParagraphEditor-DaZ03d1U.js → ParagraphEditor-BhhpXgoO.js} +35 -35
  19. package/dist/{RestoreVersionDialog-DHb1mPoF.js → RestoreVersionDialog-DE8aqmdi.js} +3 -3
  20. package/dist/{RichTextEditorContent-AEtfKvLz.js → RichTextEditorContent-AaLAArA0.js} +5 -5
  21. package/dist/{SaveBlockDialog-hGwLDWfE.js → SaveBlockDialog-BJ-hEwLv.js} +8 -8
  22. package/dist/{SavedBlocksBrowserModal-C0lYVBFi.js → SavedBlocksBrowserModal-nLI8aAQ5.js} +9 -9
  23. package/dist/{SavedBlocksPanels-CaTbeVDv.js → SavedBlocksPanels-RnzUMUJI.js} +1 -1
  24. package/dist/{SavedBlocksPickBar-DR2afid2.js → SavedBlocksPickBar-D3QA3Tgy.js} +2 -2
  25. package/dist/{TemplateScoringPanel-Cs0TGtRF.js → TemplateScoringPanel-C6ellAcs.js} +11 -11
  26. package/dist/{TemplateSettings-BD1WNP4C.js → TemplateSettings-C_0qK-zs.js} +6 -6
  27. package/dist/{TestEmailModal-DqoosG1u.js → TestEmailModal-CnyxLhWJ.js} +7 -7
  28. package/dist/{TestEmailPanel-DpozqyHF.js → TestEmailPanel-BRisIL4G.js} +1 -1
  29. package/dist/{TitleEditor-BCBdZLt_.js → TitleEditor-lPITPzw6.js} +12 -12
  30. package/dist/{ToggleSwitch-CJfQ8u57.js → ToggleSwitch-CtZ6adlX.js} +1 -1
  31. package/dist/{Toolbar-C7SjilzH.js → Toolbar-B6dG5_IP.js} +847 -554
  32. package/dist/{TplModal-CVO-2KrU.js → TplModal-BiAf7tWe.js} +2 -2
  33. package/dist/{VersionHistoryMenu-b0A8mS2_.js → VersionHistoryMenu-DdspCRSA.js} +5 -5
  34. package/dist/{VersionHistoryPanels-BzF7dmwa.js → VersionHistoryPanels-C589Jm_B.js} +1 -1
  35. package/dist/{VersionPreviewBanner-xB9r83r0.js → VersionPreviewBanner-XUuLe36h.js} +1 -1
  36. package/dist/{WrapperBlock-BDjOn3fY.js → WrapperBlock-CVQ4t1tY.js} +4 -4
  37. package/dist/{blockTypeIcons-B0Bq1WT1.js → blockTypeIcons-Sg8SY1NQ.js} +2 -2
  38. package/dist/bundle-stats.json +6 -6
  39. package/dist/{cdn/chunks/ca-Ckgw9yd0.js → ca--zGWn9AU.js} +14 -2
  40. package/dist/cdn/chunks/{AiChatSidebar-mKImUXi6.js → AiChatSidebar-DiG-EQ-E.js} +8 -8
  41. package/dist/cdn/chunks/AiChatSidebar-DiG-EQ-E.js.map +1 -0
  42. package/dist/cdn/chunks/{AiFeatureMenu-Cx3qdStQ.js → AiFeatureMenu-BXSPgOzA.js} +5 -5
  43. package/dist/cdn/chunks/AiFeatureMenu-BXSPgOzA.js.map +1 -0
  44. package/dist/cdn/chunks/{BlockIssueBadge-DS1pUf5g.js → BlockIssueBadge-ChQ0mPf5.js} +2 -2
  45. package/dist/cdn/chunks/BlockIssueBadge-ChQ0mPf5.js.map +1 -0
  46. package/dist/cdn/chunks/{BlockPreviewCanvas-D31TNib3.js → BlockPreviewCanvas-CMmv1FTA.js} +28 -28
  47. package/dist/cdn/chunks/BlockPreviewCanvas-CMmv1FTA.js.map +1 -0
  48. package/dist/cdn/chunks/{CloudHeaderExtras-0zdlKiq7.js → CloudHeaderExtras-yvFz2_e-.js} +3 -3
  49. package/dist/cdn/chunks/CloudHeaderExtras-yvFz2_e-.js.map +1 -0
  50. package/dist/cdn/chunks/{CloudPanels-DPh71yOO.js → CloudPanels-DpKck9Na.js} +4 -4
  51. package/dist/cdn/chunks/CloudPanels-DpKck9Na.js.map +1 -0
  52. package/dist/cdn/chunks/{CollaboratorBar-lCZU8qkg.js → CollaboratorBar-Clm1ZVXQ.js} +3 -3
  53. package/dist/cdn/chunks/CollaboratorBar-Clm1ZVXQ.js.map +1 -0
  54. package/dist/cdn/chunks/{CommentsPanel-BtAGZfF6.js → CommentsPanel-CjhxGMjL.js} +2 -2
  55. package/dist/cdn/chunks/CommentsPanel-CjhxGMjL.js.map +1 -0
  56. package/dist/cdn/chunks/{CommentsSidebar-Ws07MXYj.js → CommentsSidebar-D9bqMnlt.js} +16 -16
  57. package/dist/cdn/chunks/CommentsSidebar-D9bqMnlt.js.map +1 -0
  58. package/dist/cdn/chunks/CountdownBlock-sVDjKkRw.js.map +1 -1
  59. package/dist/cdn/chunks/{CountdownToolbar-Bu93WiS8.js → CountdownToolbar-CIRx5w1U.js} +4 -4
  60. package/dist/cdn/chunks/CountdownToolbar-CIRx5w1U.js.map +1 -0
  61. package/dist/cdn/chunks/{DesignReferenceSidebar-BlSdh5B7.js → DesignReferenceSidebar-C1-SbE_R.js} +9 -9
  62. package/dist/cdn/chunks/DesignReferenceSidebar-C1-SbE_R.js.map +1 -0
  63. package/dist/cdn/chunks/{IssuesPanel-CKkv9z9g.js → IssuesPanel-qKY6UDd-.js} +8 -8
  64. package/dist/cdn/chunks/IssuesPanel-qKY6UDd-.js.map +1 -0
  65. package/dist/cdn/chunks/LoadingTrack-_7DH_5aw.js.map +1 -1
  66. package/dist/cdn/chunks/{LogicTagInsertButton-CHc1yZ9v.js → LogicTagInsertButton-CItDS_U1.js} +250 -226
  67. package/dist/cdn/chunks/LogicTagInsertButton-CItDS_U1.js.map +1 -0
  68. package/dist/cdn/chunks/{MediaEditModal-Cw9MhjVz.js → MediaEditModal-CzUtXkpk.js} +2 -2
  69. package/dist/cdn/chunks/MediaEditModal-CzUtXkpk.js.map +1 -0
  70. package/dist/cdn/chunks/{MediaPanels-FD_aX3H5.js → MediaPanels-DOLsh7WC.js} +2 -2
  71. package/dist/cdn/chunks/MediaPanels-DOLsh7WC.js.map +1 -0
  72. package/dist/cdn/chunks/{MergeTagInput--qTYfCWk.js → MergeTagInput-B33u3nxj.js} +2 -2
  73. package/dist/cdn/chunks/MergeTagInput-B33u3nxj.js.map +1 -0
  74. package/dist/cdn/chunks/{MergeTagModeToggle-CdWIGo3Q.js → MergeTagModeToggle-Bz1CH3NT.js} +10 -10
  75. package/dist/cdn/chunks/MergeTagModeToggle-Bz1CH3NT.js.map +1 -0
  76. package/dist/cdn/chunks/{ParagraphEditor-w2qEZTkM.js → ParagraphEditor-J74SIn-m.js} +51 -51
  77. package/dist/cdn/chunks/ParagraphEditor-J74SIn-m.js.map +1 -0
  78. package/dist/cdn/chunks/{RestoreVersionDialog-cnWVN3nX.js → RestoreVersionDialog-CFE7jqDB.js} +3 -3
  79. package/dist/cdn/chunks/RestoreVersionDialog-CFE7jqDB.js.map +1 -0
  80. package/dist/cdn/chunks/{RichTextEditorContent-Fzuu9pLP.js → RichTextEditorContent-dOg1xFqv.js} +5 -5
  81. package/dist/cdn/chunks/RichTextEditorContent-dOg1xFqv.js.map +1 -0
  82. package/dist/cdn/chunks/{SaveBlockDialog-jxQtKnVs.js → SaveBlockDialog-DZOmDCC8.js} +38 -38
  83. package/dist/cdn/chunks/SaveBlockDialog-DZOmDCC8.js.map +1 -0
  84. package/dist/cdn/chunks/{SavedBlocksBrowserModal-DpXWf4Pi.js → SavedBlocksBrowserModal-B43H5zKA.js} +10 -10
  85. package/dist/cdn/chunks/SavedBlocksBrowserModal-B43H5zKA.js.map +1 -0
  86. package/dist/cdn/chunks/{SavedBlocksPanels-B-ASmf1P.js → SavedBlocksPanels-uTSb2xO_.js} +2 -2
  87. package/dist/cdn/chunks/SavedBlocksPanels-uTSb2xO_.js.map +1 -0
  88. package/dist/cdn/chunks/{SavedBlocksPickBar-CEFIQ8Nd.js → SavedBlocksPickBar-BUvmGqHK.js} +3 -3
  89. package/dist/cdn/chunks/SavedBlocksPickBar-BUvmGqHK.js.map +1 -0
  90. package/dist/cdn/chunks/{TemplateScoringPanel-d_Ppowsq.js → TemplateScoringPanel-ebsrVge3.js} +19 -19
  91. package/dist/cdn/chunks/TemplateScoringPanel-ebsrVge3.js.map +1 -0
  92. package/dist/cdn/chunks/{TemplateSettings-Dr5OJ0Gi.js → TemplateSettings-BaEVdUTM.js} +9 -9
  93. package/dist/cdn/chunks/TemplateSettings-BaEVdUTM.js.map +1 -0
  94. package/dist/cdn/chunks/{TestEmailModal-CrjzVw8e.js → TestEmailModal-ide7UP_H.js} +7 -7
  95. package/dist/cdn/chunks/TestEmailModal-ide7UP_H.js.map +1 -0
  96. package/dist/cdn/chunks/{TestEmailPanel-k9vdulTu.js → TestEmailPanel-Cqdsnufx.js} +2 -2
  97. package/dist/cdn/chunks/TestEmailPanel-Cqdsnufx.js.map +1 -0
  98. package/dist/cdn/chunks/{TitleEditor-laILWfKh.js → TitleEditor-CoFzxLPA.js} +16 -16
  99. package/dist/cdn/chunks/TitleEditor-CoFzxLPA.js.map +1 -0
  100. package/dist/cdn/chunks/{ToggleSwitch-0dGCYnRY.js → ToggleSwitch-D-8dCEbI.js} +2 -2
  101. package/dist/cdn/chunks/ToggleSwitch-D-8dCEbI.js.map +1 -0
  102. package/dist/cdn/chunks/{Toolbar-BsiHW9gY.js → Toolbar-BDzhrmGV.js} +814 -566
  103. package/dist/cdn/chunks/Toolbar-BDzhrmGV.js.map +1 -0
  104. package/dist/cdn/chunks/{TplModal-D8DoPbFa.js → TplModal-mLMmolQp.js} +3 -3
  105. package/dist/cdn/chunks/TplModal-mLMmolQp.js.map +1 -0
  106. package/dist/cdn/chunks/{VersionHistoryMenu-B3-hNu9c.js → VersionHistoryMenu-C_bBKOYU.js} +7 -7
  107. package/dist/cdn/chunks/VersionHistoryMenu-C_bBKOYU.js.map +1 -0
  108. package/dist/cdn/chunks/{VersionHistoryPanels-zw1AtzGN.js → VersionHistoryPanels-B9LwOKxm.js} +2 -2
  109. package/dist/cdn/chunks/VersionHistoryPanels-B9LwOKxm.js.map +1 -0
  110. package/dist/cdn/chunks/{VersionPreviewBanner-PzVQSBJE.js → VersionPreviewBanner-CKyrcgxJ.js} +2 -2
  111. package/dist/cdn/chunks/VersionPreviewBanner-CKyrcgxJ.js.map +1 -0
  112. package/dist/cdn/chunks/{WrapperBlock-CpRvfAV0.js → WrapperBlock-DI8vSvTO.js} +4 -4
  113. package/dist/cdn/chunks/WrapperBlock-DI8vSvTO.js.map +1 -0
  114. package/dist/cdn/chunks/blockTypeIcons-CdfCfqHE.js +21 -0
  115. package/dist/cdn/chunks/blockTypeIcons-CdfCfqHE.js.map +1 -0
  116. package/dist/{ca-Ckgw9yd0.js → cdn/chunks/ca--zGWn9AU.js} +16 -0
  117. package/dist/cdn/chunks/ca--zGWn9AU.js.map +1 -0
  118. package/dist/cdn/chunks/ca-BTf1Zbqb.js.map +1 -1
  119. package/dist/cdn/chunks/{cloud-CIT4yPp4.js → cloud-De-eh1u6.js} +2 -2
  120. package/dist/cdn/chunks/cloud-De-eh1u6.js.map +1 -0
  121. package/dist/cdn/chunks/{createCloudRuntime-COUxbPvd.js → createCloudRuntime-DsaUaWvr.js} +3 -3
  122. package/dist/cdn/chunks/createCloudRuntime-DsaUaWvr.js.map +1 -0
  123. package/dist/cdn/chunks/{de-D4dweQw8.js → de-BRNLKcDl.js} +15 -1
  124. package/dist/cdn/chunks/de-BRNLKcDl.js.map +1 -0
  125. package/dist/cdn/chunks/de-mMY5yQJu.js.map +1 -1
  126. package/dist/cdn/chunks/de-xzrh9PA3.js.map +1 -1
  127. package/dist/cdn/chunks/dist-BRZhN8qJ.js.map +1 -1
  128. package/dist/cdn/chunks/dist-CXB2_kBa.js.map +1 -1
  129. package/dist/cdn/chunks/draggable-IB4x6w2O.js.map +1 -1
  130. package/dist/cdn/chunks/{editor-modal-CKu8HhiB.js → editor-modal-CZQArGNj.js} +88 -88
  131. package/dist/cdn/chunks/editor-modal-CZQArGNj.js.map +1 -0
  132. package/dist/cdn/chunks/emojiData-6m0DBh7O.js.map +1 -1
  133. package/dist/cdn/chunks/en-9unxBjV8.js +2 -0
  134. package/dist/cdn/chunks/en-ChVeGQBY.js.map +1 -1
  135. package/dist/{en-VE7R7NyL.js → cdn/chunks/en-JJjqHMjy.js} +16 -0
  136. package/dist/cdn/chunks/en-JJjqHMjy.js.map +1 -0
  137. package/dist/cdn/chunks/{es-B9OhP8H3.js → es-BIh8BxV9.js} +15 -1
  138. package/dist/cdn/chunks/es-BIh8BxV9.js.map +1 -0
  139. package/dist/cdn/chunks/es-BT4LSpE2.js.map +1 -1
  140. package/dist/cdn/chunks/{extensions-CafoGF65.js → extensions-Dy251lPd.js} +5 -5
  141. package/dist/cdn/chunks/extensions-Dy251lPd.js.map +1 -0
  142. package/dist/cdn/chunks/formatAbsoluteDateTime-DMmop3C-.js.map +1 -1
  143. package/dist/cdn/chunks/formatRelativeTime-CtUU-QZ8.js.map +1 -1
  144. package/dist/{fr-wrXhIi00.js → cdn/chunks/fr-C5VUZogP.js} +16 -0
  145. package/dist/cdn/chunks/fr-C5VUZogP.js.map +1 -0
  146. package/dist/cdn/chunks/fr-CI7OJoql.js.map +1 -1
  147. package/dist/cdn/chunks/htmlparser-ClUZ6waR.js.map +1 -1
  148. package/dist/cdn/chunks/{icons-BKkJ-5xF.js → icons-CQnjugjy.js} +86 -45
  149. package/dist/cdn/chunks/icons-CQnjugjy.js.map +1 -0
  150. package/dist/cdn/chunks/ja-DhlWUfke.js.map +1 -1
  151. package/dist/cdn/chunks/{ja-fKnVWXHC.js → ja-x5JYxaCW.js} +15 -1
  152. package/dist/cdn/chunks/ja-x5JYxaCW.js.map +1 -0
  153. package/dist/cdn/chunks/keys-DCWeHkSF.js.map +1 -1
  154. package/dist/cdn/chunks/liquid.browser-BypTT2oc.js.map +1 -1
  155. package/dist/cdn/chunks/logger-ByvdoAwq.js.map +1 -1
  156. package/dist/cdn/chunks/nl-D8y5OWGB.js.map +1 -1
  157. package/dist/cdn/chunks/{nl-DeFatKJe.js → nl-UXxFyXV6.js} +15 -1
  158. package/dist/cdn/chunks/nl-UXxFyXV6.js.map +1 -0
  159. package/dist/cdn/chunks/{preRenderCustomBlocks-Dpj-_cOj.js → preRenderCustomBlocks-h4ZFBKbD.js} +2 -2
  160. package/dist/cdn/chunks/preRenderCustomBlocks-h4ZFBKbD.js.map +1 -0
  161. package/dist/cdn/chunks/pt-BR-CoCwRUi0.js.map +1 -1
  162. package/dist/{pt-BR-Bh1J8ey6.js → cdn/chunks/pt-BR-DHm3SP6W.js} +16 -0
  163. package/dist/cdn/chunks/pt-BR-DHm3SP6W.js.map +1 -0
  164. package/dist/cdn/chunks/pt-BR-DMCbUBvJ.js.map +1 -1
  165. package/dist/cdn/chunks/pusher-BbIHJKf3.js.map +1 -1
  166. package/dist/cdn/chunks/readableTextColor-f8Kykfnh.js.map +1 -1
  167. package/dist/cdn/chunks/{socialIcons-BvYNMn1G.js → socialIcons-ZOCwCCyn.js} +3 -3
  168. package/dist/cdn/chunks/socialIcons-ZOCwCCyn.js.map +1 -0
  169. package/dist/cdn/chunks/{src-DveVYoWS.js → src-BR3I3gOW.js} +265 -224
  170. package/dist/cdn/chunks/src-BR3I3gOW.js.map +1 -0
  171. package/dist/cdn/chunks/{src-Cnc5E71L.js → src-fZSLjhjy.js} +12 -12
  172. package/dist/cdn/chunks/src-fZSLjhjy.js.map +1 -0
  173. package/dist/cdn/chunks/{src-L6HEdwFn.js → src-nhIF5-mf.js} +179 -166
  174. package/dist/cdn/chunks/src-nhIF5-mf.js.map +1 -0
  175. package/dist/cdn/chunks/styleConstants-C_YAPdQS.js.map +1 -1
  176. package/dist/cdn/chunks/templateSettingsFields-YCFVBVYh.js.map +1 -1
  177. package/dist/cdn/chunks/timeouts-DHPZQ0G3.js.map +1 -1
  178. package/dist/cdn/chunks/{tiptap-DhLuKyDc.js → tiptap-D1u8JIUW.js} +1916 -1917
  179. package/dist/cdn/chunks/tiptap-D1u8JIUW.js.map +1 -0
  180. package/dist/cdn/chunks/useAliveFlag-DAlQLbM2.js.map +1 -1
  181. package/dist/cdn/chunks/useCloudI18n-CrjWbpbu.js.map +1 -1
  182. package/dist/cdn/chunks/{useEditorCore-DZmtKwJh.js → useEditorCore-D4kzTMtQ.js} +458 -446
  183. package/dist/cdn/chunks/useEditorCore-D4kzTMtQ.js.map +1 -0
  184. package/dist/cdn/chunks/useEditorRoot-CshefZLP.js.map +1 -1
  185. package/dist/cdn/chunks/useI18n-CyO_Nmca.js.map +1 -1
  186. package/dist/cdn/chunks/{useLogicTag-DF7-l66G.js → useLogicTag-J_i_hvZg.js} +2 -2
  187. package/dist/cdn/chunks/useLogicTag-J_i_hvZg.js.map +1 -0
  188. package/dist/cdn/chunks/{useMergeTag-DbIHw4NG.js → useMergeTag-C5MyHYxJ.js} +2 -2
  189. package/dist/cdn/chunks/useMergeTag-C5MyHYxJ.js.map +1 -0
  190. package/dist/cdn/chunks/{usePopoverPosition-BJRvAb-h.js → usePopoverPosition-DGXjXKjM.js} +25 -12
  191. package/dist/cdn/chunks/usePopoverPosition-DGXjXKjM.js.map +1 -0
  192. package/dist/cdn/chunks/useScrollToBlock-DgHZJvID.js.map +1 -1
  193. package/dist/cdn/editor.css +1 -1
  194. package/dist/cdn/editor.js +493 -464
  195. package/dist/cdn/editor.js.map +1 -1
  196. package/dist/{check-eqb2JhqU.js → check-BMx_TLAS.js} +1 -1
  197. package/dist/{chevron-down-Dkr-7Ywl.js → chevron-down-KZf5mh9_.js} +1 -1
  198. package/dist/{chevron-right-Bge4XF_B.js → chevron-right-BLZ60Vxw.js} +1 -1
  199. package/dist/{chevron-up-CQAyl0kC.js → chevron-up-B9ynB8eU.js} +1 -1
  200. package/dist/{circle-alert-CYgTHKWr.js → circle-alert-pjvFFDrF.js} +1 -1
  201. package/dist/{clock-BgSJZJP5.js → clock-CiiTBfF3.js} +1 -1
  202. package/dist/{cloud-DsqqUfzh.js → cloud-AO_X8T9g.js} +1 -1
  203. package/dist/{copy-mw6PVnOJ.js → copy-BF0_pQjH.js} +1 -1
  204. package/dist/{createCloudRuntime-iLflkYzP.js → createCloudRuntime-D641nhri.js} +2 -2
  205. package/dist/{createLucideIcon-BxY5yz5u.js → createLucideIcon-C2YOVVWx.js} +8 -8
  206. package/dist/{de-D4dweQw8.js → de-BRNLKcDl.js} +14 -0
  207. package/dist/{dist-CQSVro7b.js → dist-B0dkvy7z.js} +1 -1
  208. package/dist/{dist-BJM3eFyT.js → dist-BPWuzECC.js} +1 -1
  209. package/dist/{dist-EDkj3Ub1.js → dist-BUHovM4k.js} +1 -1
  210. package/dist/{dist-BmsLcnrO.js → dist-BwoEME8p.js} +1 -1
  211. package/dist/{dist-DerFNpDY.js → dist-C2Ge44fD.js} +760 -761
  212. package/dist/{dist-BdHF7nyq.js → dist-CIxmOOSG.js} +5 -5
  213. package/dist/{dist-wyXxj3R1.js → dist-CJ7RZARo.js} +1 -1
  214. package/dist/{dist-Bl4rxpX9.js → dist-CxmLZQso.js} +1 -1
  215. package/dist/{dist-DyrEOKEJ.js → dist-DRmafAUz.js} +1 -1
  216. package/dist/{dist-Djmzp6wi.js → dist-DdOAotfz.js} +1 -1
  217. package/dist/{dist-BxVnMgXZ.js → dist-W_ERo6c_.js} +1 -1
  218. package/dist/{dist-DbUZvK04.js → dist-iLUVPysa.js} +263 -222
  219. package/dist/{dist-DQbOebGW.js → dist-p-LkiWIQ.js} +1 -1
  220. package/dist/{editor-modal-qzmEVcIk.js → editor-modal-99FB40dl.js} +16 -16
  221. package/dist/en-BvOjRLiw.js +2 -0
  222. package/dist/{cdn/chunks/en-VE7R7NyL.js → en-JJjqHMjy.js} +14 -2
  223. package/dist/{es-B9OhP8H3.js → es-BIh8BxV9.js} +14 -0
  224. package/dist/{extensions-DDhjuVKy.js → extensions-DqQXxrvP.js} +13 -13
  225. package/dist/{eye-DphkRtAD.js → eye-RdXYgrQZ.js} +1 -1
  226. package/dist/{cdn/chunks/fr-wrXhIi00.js → fr-C5VUZogP.js} +14 -2
  227. package/dist/{image-up-Qi2BXD7T.js → image-up-BwdoReQt.js} +1 -1
  228. package/dist/{info-VSyAs02q.js → info-zFMSGrL1.js} +1 -1
  229. package/dist/{ja-fKnVWXHC.js → ja-x5JYxaCW.js} +14 -0
  230. package/dist/{link-CNuozGqD.js → link-D7orqFBT.js} +1 -1
  231. package/dist/{list-BA0I5Wmu.js → list-BGt4Pgxc.js} +1 -1
  232. package/dist/{loader-circle-z5eeqasv.js → loader-circle-BVBgkRGz.js} +1 -1
  233. package/dist/{message-circle-BsQ9hEup.js → message-circle-D8YLsarv.js} +1 -1
  234. package/dist/{nl-DeFatKJe.js → nl-UXxFyXV6.js} +14 -0
  235. package/dist/{package-OkXfuRoO.js → package-CuFvk0L5.js} +1 -1
  236. package/dist/{pencil-DFVFA7Dj.js → pencil-DRYbmZW7.js} +1 -1
  237. package/dist/{plus-D8bkVjZ7.js → plus-Dv0b3kh0.js} +1 -1
  238. package/dist/{preRenderCustomBlocks-BBSqQgke.js → preRenderCustomBlocks-CTstI3Q-.js} +4 -4
  239. package/dist/{cdn/chunks/pt-BR-Bh1J8ey6.js → pt-BR-DHm3SP6W.js} +14 -2
  240. package/dist/{refresh-cw-DPB7YFuz.js → refresh-cw-gYTIS5bJ.js} +1 -1
  241. package/dist/{search-C-dCgqIa.js → search-D84_Ki_a.js} +1 -1
  242. package/dist/{send-Cm2fy2zm.js → send-Cn-8_H_U.js} +1 -1
  243. package/dist/{shield-check-COgBYFDF.js → shield-check-BOoVWJtD.js} +1 -1
  244. package/dist/{smartphone-v0u_MOEV.js → smartphone-4Ej3PBw5.js} +1 -1
  245. package/dist/{socialIcons-O7PZCW2P.js → socialIcons-CzRJskEk.js} +47 -47
  246. package/dist/{sparkles-Ds0RF8Iu.js → sparkles-D3vnNe7O.js} +1 -1
  247. package/dist/style.css +1 -1
  248. package/dist/templatical-editor.js +432 -403
  249. package/dist/{text-align-start-DiavaIm6.js → text-align-start-Ci2h_Xly.js} +1 -1
  250. package/dist/{trash-DnOkfu2V.js → trash-wmi4UKvv.js} +1 -1
  251. package/dist/{triangle-alert-DGLAyXnh.js → triangle-alert-DJ0r2wmQ.js} +1 -1
  252. package/dist/{upload-BBDme2cy.js → upload-XMde-TXf.js} +1 -1
  253. package/dist/{useEditorCore-dcdHB_7H.js → useEditorCore-Dhit8Ygg.js} +894 -882
  254. package/dist/{useLogicTag-D9pvgxQR.js → useLogicTag-BaH8gey0.js} +1 -1
  255. package/dist/{useMergeTag-BaVFPdlq.js → useMergeTag-Cyad1yBE.js} +1 -1
  256. package/dist/{usePopoverPosition-DH7z3Ghm.js → usePopoverPosition-BYmd-lhR.js} +24 -11
  257. package/dist/{useScrollToBlock-Dj4v3rxr.js → useScrollToBlock-B4F7KCrf.js} +1 -1
  258. package/dist/{x-3BHV3Q6K.js → x-BWrtpVl-.js} +1 -1
  259. package/package.json +10 -10
  260. package/dist/cdn/chunks/AiChatSidebar-mKImUXi6.js.map +0 -1
  261. package/dist/cdn/chunks/AiFeatureMenu-Cx3qdStQ.js.map +0 -1
  262. package/dist/cdn/chunks/BlockIssueBadge-DS1pUf5g.js.map +0 -1
  263. package/dist/cdn/chunks/BlockPreviewCanvas-D31TNib3.js.map +0 -1
  264. package/dist/cdn/chunks/CloudHeaderExtras-0zdlKiq7.js.map +0 -1
  265. package/dist/cdn/chunks/CloudPanels-DPh71yOO.js.map +0 -1
  266. package/dist/cdn/chunks/CollaboratorBar-lCZU8qkg.js.map +0 -1
  267. package/dist/cdn/chunks/CommentsPanel-BtAGZfF6.js.map +0 -1
  268. package/dist/cdn/chunks/CommentsSidebar-Ws07MXYj.js.map +0 -1
  269. package/dist/cdn/chunks/CountdownToolbar-Bu93WiS8.js.map +0 -1
  270. package/dist/cdn/chunks/DesignReferenceSidebar-BlSdh5B7.js.map +0 -1
  271. package/dist/cdn/chunks/IssuesPanel-CKkv9z9g.js.map +0 -1
  272. package/dist/cdn/chunks/LogicTagInsertButton-CHc1yZ9v.js.map +0 -1
  273. package/dist/cdn/chunks/MediaEditModal-Cw9MhjVz.js.map +0 -1
  274. package/dist/cdn/chunks/MediaPanels-FD_aX3H5.js.map +0 -1
  275. package/dist/cdn/chunks/MergeTagInput--qTYfCWk.js.map +0 -1
  276. package/dist/cdn/chunks/MergeTagModeToggle-CdWIGo3Q.js.map +0 -1
  277. package/dist/cdn/chunks/ParagraphEditor-w2qEZTkM.js.map +0 -1
  278. package/dist/cdn/chunks/RestoreVersionDialog-cnWVN3nX.js.map +0 -1
  279. package/dist/cdn/chunks/RichTextEditorContent-Fzuu9pLP.js.map +0 -1
  280. package/dist/cdn/chunks/SaveBlockDialog-jxQtKnVs.js.map +0 -1
  281. package/dist/cdn/chunks/SavedBlocksBrowserModal-DpXWf4Pi.js.map +0 -1
  282. package/dist/cdn/chunks/SavedBlocksPanels-B-ASmf1P.js.map +0 -1
  283. package/dist/cdn/chunks/SavedBlocksPickBar-CEFIQ8Nd.js.map +0 -1
  284. package/dist/cdn/chunks/TemplateScoringPanel-d_Ppowsq.js.map +0 -1
  285. package/dist/cdn/chunks/TemplateSettings-Dr5OJ0Gi.js.map +0 -1
  286. package/dist/cdn/chunks/TestEmailModal-CrjzVw8e.js.map +0 -1
  287. package/dist/cdn/chunks/TestEmailPanel-k9vdulTu.js.map +0 -1
  288. package/dist/cdn/chunks/TitleEditor-laILWfKh.js.map +0 -1
  289. package/dist/cdn/chunks/ToggleSwitch-0dGCYnRY.js.map +0 -1
  290. package/dist/cdn/chunks/Toolbar-BsiHW9gY.js.map +0 -1
  291. package/dist/cdn/chunks/TplModal-D8DoPbFa.js.map +0 -1
  292. package/dist/cdn/chunks/VersionHistoryMenu-B3-hNu9c.js.map +0 -1
  293. package/dist/cdn/chunks/VersionHistoryPanels-zw1AtzGN.js.map +0 -1
  294. package/dist/cdn/chunks/VersionPreviewBanner-PzVQSBJE.js.map +0 -1
  295. package/dist/cdn/chunks/WrapperBlock-CpRvfAV0.js.map +0 -1
  296. package/dist/cdn/chunks/blockTypeIcons-raBjStuv.js +0 -21
  297. package/dist/cdn/chunks/blockTypeIcons-raBjStuv.js.map +0 -1
  298. package/dist/cdn/chunks/ca-Ckgw9yd0.js.map +0 -1
  299. package/dist/cdn/chunks/cloud-CIT4yPp4.js.map +0 -1
  300. package/dist/cdn/chunks/createCloudRuntime-COUxbPvd.js.map +0 -1
  301. package/dist/cdn/chunks/de-D4dweQw8.js.map +0 -1
  302. package/dist/cdn/chunks/editor-modal-CKu8HhiB.js.map +0 -1
  303. package/dist/cdn/chunks/en-DqzAdO8F.js +0 -2
  304. package/dist/cdn/chunks/en-VE7R7NyL.js.map +0 -1
  305. package/dist/cdn/chunks/es-B9OhP8H3.js.map +0 -1
  306. package/dist/cdn/chunks/extensions-CafoGF65.js.map +0 -1
  307. package/dist/cdn/chunks/fr-wrXhIi00.js.map +0 -1
  308. package/dist/cdn/chunks/icons-BKkJ-5xF.js.map +0 -1
  309. package/dist/cdn/chunks/ja-fKnVWXHC.js.map +0 -1
  310. package/dist/cdn/chunks/nl-DeFatKJe.js.map +0 -1
  311. package/dist/cdn/chunks/preRenderCustomBlocks-Dpj-_cOj.js.map +0 -1
  312. package/dist/cdn/chunks/pt-BR-Bh1J8ey6.js.map +0 -1
  313. package/dist/cdn/chunks/socialIcons-BvYNMn1G.js.map +0 -1
  314. package/dist/cdn/chunks/src-Cnc5E71L.js.map +0 -1
  315. package/dist/cdn/chunks/src-DveVYoWS.js.map +0 -1
  316. package/dist/cdn/chunks/src-L6HEdwFn.js.map +0 -1
  317. package/dist/cdn/chunks/tiptap-DhLuKyDc.js.map +0 -1
  318. package/dist/cdn/chunks/useEditorCore-DZmtKwJh.js.map +0 -1
  319. package/dist/cdn/chunks/useLogicTag-DF7-l66G.js.map +0 -1
  320. package/dist/cdn/chunks/useMergeTag-DbIHw4NG.js.map +0 -1
  321. package/dist/cdn/chunks/usePopoverPosition-BJRvAb-h.js.map +0 -1
  322. package/dist/en-1tgLzB8q.js +0 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SaveBlockDialog-DZOmDCC8.js","names":["_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_openBlock","_createElementBlock","_createElementVNode","_unref","_withKeys","_withModifiers","_createVNode","_normalizeStyle","BlockPreviewCanvas","_createCommentVNode","_createBlock","_resolveDynamicComponent","_createTextVNode","_toDisplayString","_sfc_main","_openBlock","_createBlock","TplModal","_withCtx","_createElementVNode","_toDisplayString","_unref","_withDirectives","_vModelText","_createElementBlock","_Fragment","_renderList","_createTextVNode","_createCommentVNode","_createVNode","VueDraggable","SavedBlockPreviewRow","_normalizeClass","_sfc_main","onBeforeUnmount","watch","inject","renderList","withCtx","openBlock","ref","resolveDynamicComponent","defineComponent","withDirectives","unref","createVNode","withKeys","normalizeClass","computed","createElementBlock","createCommentVNode","onMounted","withModifiers","normalizeStyle","createBlock","vModelText","createBaseVNode","lo","Fragment","toDisplayString","createTextVNode","getBlockLabel","requireInject","SAVED_BLOCKS_KEY","EDITOR_KEY","CUSTOM_BLOCK_DEFINITIONS_KEY","useI18n","ChevronDown","ChevronUp","LoaderCircle","GripVertical","primaryBtnClass","getEmailFrameWidth","TplModal_default","BlockPreviewCanvas_default","_hoisted_1$1","_hoisted_2$1","_hoisted_3$1","_hoisted_4$1","\"background\"","_hoisted_5$1","COLLAPSED_HEIGHT","SavedBlockPreviewRow_default","__emit","props","__props","emit","customBlockDefinitions","editor","previewWidth","frameEl","contentEl","scale","fullHeight","expanded","observer","isClipped","frameHeight","measure","frame","content","width","next","blockLabel","t","handleLabel","format","toggleLabel","_cache","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","SaveBlockDialog_default","savedBlocks","name","category","isSaving","error","announcement","orderedIds","pickedBlocks","byId","b","id","orderedBlocks","blocks","moveBlock","order","from","blockId","to","delta","moved","label","block","summary","visible","canSave","handleSave","err","handleClose","handleKeydown","event","$event","option","index"],"sources":["../../../src/components/SavedBlockPreviewRow.vue","../../../src/components/SavedBlockPreviewRow.vue","../../../src/components/SaveBlockDialog.vue","../../../src/components/SaveBlockDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * One reorderable row in the save dialog's preview list: a grip handle beside a\n * scaled-down render of the real block.\n *\n * The preview is the same `BlockPreviewCanvas` the browser modal uses, so\n * a row shows what the block actually looks like rather than a description of\n * it. That canvas is fixed at the 600px email width every block component\n * renders against, so the row shrinks it with `transform: scale()` instead of\n * re-flowing it at dialog width — a re-flow would show a layout the recipient\n * will never see.\n *\n * `transform` doesn't participate in layout, so the frame would keep reserving\n * the full unscaled height; a `ResizeObserver` mirrors the scaled height back\n * onto it. The handle is a sibling of the scaled subtree, so it stays full size\n * and keeps its real hit area.\n *\n * Row chrome uses the raw theme tokens, not the `--tpl-chrome-*` aliases: the\n * chrome is an ancestor and a sibling of the rendered email content, never a\n * descendant, so the `.tpl-block-content` token override can't reach it.\n */\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\nimport { useI18n } from \"../composables\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY, EDITOR_KEY } from \"../keys\";\nimport { getEmailFrameWidth } from \"../utils/emailFrameWidth\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { ChevronDown, ChevronUp, GripVertical } from \"@lucide/vue\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, onBeforeUnmount, onMounted, ref } from \"vue\";\n\nconst props = defineProps<{\n block: Block;\n /** 1-based position in the list — spoken by the handle's label. */\n position: number;\n total: number;\n}>();\n\nconst emit = defineEmits<{\n (e: \"move\", delta: -1 | 1): void;\n}>();\n\nconst { t, format } = useI18n();\n// Optional: absent when the consumer registers no custom blocks.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst editor = inject(EDITOR_KEY, null);\n\n/**\n * The width the nested `BlockPreviewCanvas` actually renders at.\n *\n * MUST come from the same helper the canvas uses, because the scale factor below\n * divides by it: a mismatch scales every row by the wrong amount, so a template\n * with a custom body width would overflow its frame or under-fill it. Rows are\n * always the desktop width — the save dialog has no viewport control.\n */\nconst previewWidth = computed(() =>\n getEmailFrameWidth(editor?.content.value.settings),\n);\n/**\n * Collapsed height for a tall block. A hero card or a multi-row section would\n * otherwise be taller than the whole list, leaving one row on screen — and\n * seeing the rows together is what makes them orderable. Anything taller\n * collapses to this, fades out at the cut, and can be expanded in place.\n */\nconst COLLAPSED_HEIGHT = 240;\n\nconst frameEl = ref<HTMLElement | null>(null);\nconst contentEl = ref<HTMLElement | null>(null);\nconst scale = ref(1);\n/** Scaled height of the whole block; `null` until first measured. */\nconst fullHeight = ref<number | null>(null);\nconst expanded = ref(false);\n\nlet observer: ResizeObserver | null = null;\n\n/** Only a block taller than the collapsed cap needs fading or expanding. */\nconst isClipped = computed(\n () => fullHeight.value !== null && fullHeight.value > COLLAPSED_HEIGHT,\n);\n\nconst frameHeight = computed(() => {\n if (fullHeight.value === null) return null;\n if (expanded.value || !isClipped.value) return fullHeight.value;\n return COLLAPSED_HEIGHT;\n});\n\nfunction measure(): void {\n const frame = frameEl.value;\n const content = contentEl.value;\n if (!frame || !content) return;\n const width = frame.clientWidth;\n if (width <= 0) return;\n // Never scale up: a dialog wider than the email would blow the preview past\n // its real proportions.\n const next = Math.min(1, width / previewWidth.value);\n scale.value = next;\n fullHeight.value = content.offsetHeight * next;\n}\n\nonMounted(() => {\n measure();\n // Absent in non-DOM test environments. Without it the frame simply falls back\n // to the untransformed height and clips — degraded, never broken.\n if (typeof ResizeObserver === \"undefined\") return;\n observer = new ResizeObserver(() => measure());\n if (frameEl.value) observer.observe(frameEl.value);\n if (contentEl.value) observer.observe(contentEl.value);\n});\n\nonBeforeUnmount(() => {\n observer?.disconnect();\n observer = null;\n});\n\nconst blockLabel = computed(() =>\n getBlockLabel(props.block, t, customBlockDefinitions),\n);\n\nconst handleLabel = computed(() =>\n format(t.savedBlocks.reorderHandle, {\n block: blockLabel.value,\n position: props.position,\n total: props.total,\n }),\n);\n\nconst toggleLabel = computed(() =>\n format(\n expanded.value\n ? t.savedBlocks.collapsePreview\n : t.savedBlocks.expandPreview,\n { block: blockLabel.value },\n ),\n);\n</script>\n\n<template>\n <div\n data-testid=\"saved-blocks-reorder-row\"\n :data-block-id=\"block.id\"\n class=\"tpl:flex tpl:items-start tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:p-1.5 tpl:border-[var(--tpl-border)]\"\n style=\"background-color: var(--tpl-bg)\"\n >\n <!-- Sortable's `handle` selector points here, so a pointer-drag can only\n start from the grip — never from the preview itself. -->\n <button\n type=\"button\"\n data-testid=\"saved-blocks-reorder-handle\"\n class=\"tpl-saved-block-reorder-handle tpl:mt-0.5 tpl:shrink-0 tpl:cursor-grab tpl:rounded tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:text-[var(--tpl-text-dim)]\"\n :aria-label=\"handleLabel\"\n :title=\"t.savedBlocks.reorderHint\"\n @keydown.up.prevent=\"emit('move', -1)\"\n @keydown.down.prevent=\"emit('move', 1)\"\n >\n <GripVertical :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n\n <div class=\"tpl:relative tpl:min-w-0 tpl:flex-1\">\n <div\n ref=\"frameEl\"\n class=\"tpl:overflow-hidden\"\n :style=\"{\n height: frameHeight !== null ? `${frameHeight}px` : undefined,\n }\"\n >\n <div\n ref=\"contentEl\"\n :style=\"{\n width: `${previewWidth}px`,\n transform: `scale(${scale})`,\n transformOrigin: 'top left',\n }\"\n >\n <BlockPreviewCanvas :blocks=\"[block]\" />\n </div>\n </div>\n\n <!-- Fade over the cut, so a clipped block reads as truncated rather than\n as a block that simply ends there. Pointer-transparent: it sits over\n the preview, and swallowing clicks would break the row. -->\n <div\n v-if=\"isClipped && !expanded\"\n data-testid=\"saved-blocks-preview-fade\"\n aria-hidden=\"true\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-x-0 tpl:bottom-0 tpl:h-10\"\n style=\"\n background: linear-gradient(\n to bottom,\n transparent,\n var(--tpl-bg) 90%\n );\n \"\n />\n\n <button\n v-if=\"isClipped\"\n type=\"button\"\n data-testid=\"saved-blocks-preview-toggle\"\n class=\"tpl:absolute tpl:bottom-1 tpl:right-1 tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-0.5 tpl:rounded tpl:border tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)] tpl:text-[var(--tpl-text-muted)]\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"expanded\"\n @click=\"expanded = !expanded\"\n >\n <component\n :is=\"expanded ? ChevronUp : ChevronDown\"\n :size=\"11\"\n :stroke-width=\"2\"\n />\n {{ expanded ? t.savedBlocks.collapse : t.savedBlocks.expand }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * One reorderable row in the save dialog's preview list: a grip handle beside a\n * scaled-down render of the real block.\n *\n * The preview is the same `BlockPreviewCanvas` the browser modal uses, so\n * a row shows what the block actually looks like rather than a description of\n * it. That canvas is fixed at the 600px email width every block component\n * renders against, so the row shrinks it with `transform: scale()` instead of\n * re-flowing it at dialog width — a re-flow would show a layout the recipient\n * will never see.\n *\n * `transform` doesn't participate in layout, so the frame would keep reserving\n * the full unscaled height; a `ResizeObserver` mirrors the scaled height back\n * onto it. The handle is a sibling of the scaled subtree, so it stays full size\n * and keeps its real hit area.\n *\n * Row chrome uses the raw theme tokens, not the `--tpl-chrome-*` aliases: the\n * chrome is an ancestor and a sibling of the rendered email content, never a\n * descendant, so the `.tpl-block-content` token override can't reach it.\n */\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\nimport { useI18n } from \"../composables\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY, EDITOR_KEY } from \"../keys\";\nimport { getEmailFrameWidth } from \"../utils/emailFrameWidth\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { ChevronDown, ChevronUp, GripVertical } from \"@lucide/vue\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, onBeforeUnmount, onMounted, ref } from \"vue\";\n\nconst props = defineProps<{\n block: Block;\n /** 1-based position in the list — spoken by the handle's label. */\n position: number;\n total: number;\n}>();\n\nconst emit = defineEmits<{\n (e: \"move\", delta: -1 | 1): void;\n}>();\n\nconst { t, format } = useI18n();\n// Optional: absent when the consumer registers no custom blocks.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst editor = inject(EDITOR_KEY, null);\n\n/**\n * The width the nested `BlockPreviewCanvas` actually renders at.\n *\n * MUST come from the same helper the canvas uses, because the scale factor below\n * divides by it: a mismatch scales every row by the wrong amount, so a template\n * with a custom body width would overflow its frame or under-fill it. Rows are\n * always the desktop width — the save dialog has no viewport control.\n */\nconst previewWidth = computed(() =>\n getEmailFrameWidth(editor?.content.value.settings),\n);\n/**\n * Collapsed height for a tall block. A hero card or a multi-row section would\n * otherwise be taller than the whole list, leaving one row on screen — and\n * seeing the rows together is what makes them orderable. Anything taller\n * collapses to this, fades out at the cut, and can be expanded in place.\n */\nconst COLLAPSED_HEIGHT = 240;\n\nconst frameEl = ref<HTMLElement | null>(null);\nconst contentEl = ref<HTMLElement | null>(null);\nconst scale = ref(1);\n/** Scaled height of the whole block; `null` until first measured. */\nconst fullHeight = ref<number | null>(null);\nconst expanded = ref(false);\n\nlet observer: ResizeObserver | null = null;\n\n/** Only a block taller than the collapsed cap needs fading or expanding. */\nconst isClipped = computed(\n () => fullHeight.value !== null && fullHeight.value > COLLAPSED_HEIGHT,\n);\n\nconst frameHeight = computed(() => {\n if (fullHeight.value === null) return null;\n if (expanded.value || !isClipped.value) return fullHeight.value;\n return COLLAPSED_HEIGHT;\n});\n\nfunction measure(): void {\n const frame = frameEl.value;\n const content = contentEl.value;\n if (!frame || !content) return;\n const width = frame.clientWidth;\n if (width <= 0) return;\n // Never scale up: a dialog wider than the email would blow the preview past\n // its real proportions.\n const next = Math.min(1, width / previewWidth.value);\n scale.value = next;\n fullHeight.value = content.offsetHeight * next;\n}\n\nonMounted(() => {\n measure();\n // Absent in non-DOM test environments. Without it the frame simply falls back\n // to the untransformed height and clips — degraded, never broken.\n if (typeof ResizeObserver === \"undefined\") return;\n observer = new ResizeObserver(() => measure());\n if (frameEl.value) observer.observe(frameEl.value);\n if (contentEl.value) observer.observe(contentEl.value);\n});\n\nonBeforeUnmount(() => {\n observer?.disconnect();\n observer = null;\n});\n\nconst blockLabel = computed(() =>\n getBlockLabel(props.block, t, customBlockDefinitions),\n);\n\nconst handleLabel = computed(() =>\n format(t.savedBlocks.reorderHandle, {\n block: blockLabel.value,\n position: props.position,\n total: props.total,\n }),\n);\n\nconst toggleLabel = computed(() =>\n format(\n expanded.value\n ? t.savedBlocks.collapsePreview\n : t.savedBlocks.expandPreview,\n { block: blockLabel.value },\n ),\n);\n</script>\n\n<template>\n <div\n data-testid=\"saved-blocks-reorder-row\"\n :data-block-id=\"block.id\"\n class=\"tpl:flex tpl:items-start tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:p-1.5 tpl:border-[var(--tpl-border)]\"\n style=\"background-color: var(--tpl-bg)\"\n >\n <!-- Sortable's `handle` selector points here, so a pointer-drag can only\n start from the grip — never from the preview itself. -->\n <button\n type=\"button\"\n data-testid=\"saved-blocks-reorder-handle\"\n class=\"tpl-saved-block-reorder-handle tpl:mt-0.5 tpl:shrink-0 tpl:cursor-grab tpl:rounded tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:text-[var(--tpl-text-dim)]\"\n :aria-label=\"handleLabel\"\n :title=\"t.savedBlocks.reorderHint\"\n @keydown.up.prevent=\"emit('move', -1)\"\n @keydown.down.prevent=\"emit('move', 1)\"\n >\n <GripVertical :size=\"14\" :stroke-width=\"1.5\" />\n </button>\n\n <div class=\"tpl:relative tpl:min-w-0 tpl:flex-1\">\n <div\n ref=\"frameEl\"\n class=\"tpl:overflow-hidden\"\n :style=\"{\n height: frameHeight !== null ? `${frameHeight}px` : undefined,\n }\"\n >\n <div\n ref=\"contentEl\"\n :style=\"{\n width: `${previewWidth}px`,\n transform: `scale(${scale})`,\n transformOrigin: 'top left',\n }\"\n >\n <BlockPreviewCanvas :blocks=\"[block]\" />\n </div>\n </div>\n\n <!-- Fade over the cut, so a clipped block reads as truncated rather than\n as a block that simply ends there. Pointer-transparent: it sits over\n the preview, and swallowing clicks would break the row. -->\n <div\n v-if=\"isClipped && !expanded\"\n data-testid=\"saved-blocks-preview-fade\"\n aria-hidden=\"true\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-x-0 tpl:bottom-0 tpl:h-10\"\n style=\"\n background: linear-gradient(\n to bottom,\n transparent,\n var(--tpl-bg) 90%\n );\n \"\n />\n\n <button\n v-if=\"isClipped\"\n type=\"button\"\n data-testid=\"saved-blocks-preview-toggle\"\n class=\"tpl:absolute tpl:bottom-1 tpl:right-1 tpl:flex tpl:cursor-pointer tpl:items-center tpl:gap-0.5 tpl:rounded tpl:border tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)] tpl:text-[var(--tpl-text-muted)]\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"expanded\"\n @click=\"expanded = !expanded\"\n >\n <component\n :is=\"expanded ? ChevronUp : ChevronDown\"\n :size=\"11\"\n :stroke-width=\"2\"\n />\n {{ expanded ? t.savedBlocks.collapse : t.savedBlocks.expand }}\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\nimport SavedBlockPreviewRow from \"./SavedBlockPreviewRow.vue\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n EDITOR_KEY,\n SAVED_BLOCKS_KEY,\n requireInject,\n} from \"../keys\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { LoaderCircle } from \"@lucide/vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n /**\n * Blocks chosen during the canvas pick session, in the order they were\n * picked — `pickedIds` is built from a `Set`, which iterates in insertion\n * order. That order seeds the reorderable list below. Every id must be a\n * top-level block, which is what the session guarantees.\n */\n pickedIds: readonly string[];\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"saved\"): void;\n}>();\n\nconst { t, format } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"SaveBlockDialog\");\nconst savedBlocks = requireInject(SAVED_BLOCKS_KEY, \"SaveBlockDialog\");\n// Optional: a consumer with no custom blocks never provides definitions, and\n// the label helper degrades to the type slug rather than throwing.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst name = ref(\"\");\nconst category = ref(\"\");\nconst isSaving = ref(false);\nconst error = ref<string | null>(null);\nconst announcement = ref(\"\");\n\n/**\n * The order the blocks will be saved in: seeded from the pick order, then owned\n * by this dialog once the user drags a row.\n *\n * Ids rather than blocks, deliberately. Resolving late means a block edited\n * while the dialog is open (a collaborator in Cloud) is persisted as it stands\n * at save time, not as it looked when the dialog opened, and an id that stops\n * resolving drops out on its own instead of leaving a stale snapshot behind.\n */\nconst orderedIds = ref<string[]>([]);\n\n/**\n * The picked blocks in the list's current order. Ids that no longer resolve (a\n * block deleted mid-session) simply drop out.\n */\nconst pickedBlocks = computed(() => {\n const byId = new Map(editor.content.value.blocks.map((b) => [b.id, b]));\n return orderedIds.value\n .map((id) => byId.get(id))\n .filter((b): b is Block => b !== undefined);\n});\n\n/**\n * Sortable's model. The setter reads ids straight back off the emitted entries\n * rather than storing them, so a Sortable `el` expando can never reach the\n * payload `create()` sends to the provider — and because the getter is already\n * filtered, writing through it prunes unresolvable ids too.\n */\nconst orderedBlocks = computed<Block[]>({\n get: () => pickedBlocks.value,\n set: (blocks) => {\n orderedIds.value = blocks.map((b) => b.id);\n },\n});\n\n/** Keyboard reorder from a focused grip: swap with the neighbouring row. */\nfunction moveBlock(blockId: string, delta: -1 | 1): void {\n const order = pickedBlocks.value.map((b) => b.id);\n const from = order.indexOf(blockId);\n const to = from + delta;\n if (from === -1 || to < 0 || to >= order.length) return;\n [order[from], order[to]] = [order[to], order[from]];\n orderedIds.value = order;\n\n const moved = pickedBlocks.value[to];\n announcement.value = moved\n ? format(t.savedBlocks.reorderAnnouncement, {\n block: label(moved),\n position: to + 1,\n total: order.length,\n })\n : \"\";\n}\n\n/** Custom blocks read as the consumer's name for them, not the type `custom`. */\nfunction label(block: Block): string {\n return getBlockLabel(block, t, customBlockDefinitions);\n}\n\n/** e.g. \"Title, Paragraph, Button\" — what the user is about to save. */\nconst summary = computed(() => pickedBlocks.value.map(label).join(\", \"));\n\n// `immediate` matters: the dialog is mounted lazily behind a `v-if` on the\n// same state that drives `visible`, so it can mount with `visible` already\n// true and would otherwise never reset the name field or seed the order.\nwatch(\n () => props.visible,\n (visible) => {\n if (visible) {\n name.value = \"\";\n category.value = \"\";\n error.value = null;\n announcement.value = \"\";\n orderedIds.value = [...props.pickedIds];\n }\n },\n { immediate: true },\n);\n\nconst canSave = computed(\n () =>\n name.value.trim().length > 0 &&\n pickedBlocks.value.length > 0 &&\n !isSaving.value,\n);\n\nasync function handleSave(): Promise<void> {\n if (!canSave.value) return;\n\n const blocks = pickedBlocks.value;\n // Defence in depth: never persist an empty saved block (it would list as\n // \"0 block(s)\" and insert nothing). Guards a pick set whose ids no longer\n // resolve to anything on the canvas.\n if (blocks.length === 0) return;\n\n isSaving.value = true;\n error.value = null;\n\n try {\n // Empty stays undefined rather than \"\": an entry is either categorised or\n // it isn't, and a blank string would show up as a nameless filter option.\n await savedBlocks.create(\n name.value.trim(),\n blocks,\n category.value.trim() || undefined,\n );\n emit(\"saved\");\n emit(\"close\");\n } catch (err) {\n error.value = (err as Error).message;\n } finally {\n isSaving.value = false;\n }\n}\n\nfunction handleClose(): void {\n if (!isSaving.value) {\n emit(\"close\");\n }\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSave();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSaving\"\n aria-labelledby=\"tpl-save-block-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-full tpl:max-w-2xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-save-block-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.savedBlocks.saveAsBlock }}\n </h3>\n\n <!-- Saved block name -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.name }}\n </label>\n <input\n v-model=\"name\"\n type=\"text\"\n data-testid=\"saved-blocks-name-input\"\n :placeholder=\"t.savedBlocks.namePlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n </div>\n\n <!-- Category: free text, with the already-used categories offered as\n suggestions. A datalist rather than a <select> because the set is\n derived from existing entries — there is no registry to pick from,\n and the first entry in a category has to be able to name it. -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-save-block-category\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.category }}\n </label>\n <input\n id=\"tpl-save-block-category\"\n v-model=\"category\"\n type=\"text\"\n data-testid=\"saved-blocks-category-input\"\n list=\"tpl-saved-block-categories\"\n :placeholder=\"t.savedBlocks.categoryPlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n <datalist id=\"tpl-saved-block-categories\">\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n />\n </datalist>\n </div>\n\n <!-- Summary of what the pick session chose. Which blocks were picked is\n settled on the canvas — the dialog re-presents them only so the user\n can see and order what they're about to save. -->\n <p\n data-testid=\"saved-blocks-save-summary\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ format(t.savedBlocks.savingCount, { count: pickedBlocks.length }) }}\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">{{ summary }}</span>\n </p>\n\n <!-- Reorderable preview. Seeded in pick order and saved in whatever order\n the list ends up in. Its own Sortable instance with no `group`, so it\n can never exchange items with the canvas or a section column. -->\n <p\n v-if=\"pickedBlocks.length > 1\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.reorderHint }}\n </p>\n <!-- The only growing region: takes whatever the dialog's backdrop cap\n leaves after the fixed chrome, rather than a hand-tuned vh guess that\n a row expanding to full height would blow past. `min-h-0` is what\n lets a flex child actually shrink below its content and scroll. -->\n <VueDraggable\n v-model=\"orderedBlocks\"\n data-testid=\"saved-blocks-reorder-list\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-saved-block-reorder-handle\"\n :force-fallback=\"true\"\n class=\"tpl:mb-4 tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-1.5 tpl:overflow-y-auto\"\n >\n <SavedBlockPreviewRow\n v-for=\"(block, index) in orderedBlocks\"\n :key=\"block.id\"\n :block=\"block\"\n :position=\"index + 1\"\n :total=\"orderedBlocks.length\"\n @move=\"(delta) => moveBlock(block.id, delta)\"\n />\n </VueDraggable>\n\n <!-- Keyboard reorder announcement region (visually hidden, live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n {{ announcement }}\n </div>\n\n <!-- Error message -->\n <p\n v-if=\"error\"\n role=\"alert\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ error }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-[var(--tpl-shadow-sm)] tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSaving\"\n :class=\"{\n 'tpl:cursor-not-allowed tpl:opacity-50': isSaving,\n }\"\n @click=\"handleClose\"\n >\n {{ t.savedBlocks.cancel }}\n </button>\n <button\n type=\"button\"\n :class=\"primaryBtnClass\"\n :disabled=\"!canSave\"\n @click=\"handleSave\"\n >\n <span v-if=\"isSaving\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.savedBlocks.saving }}\n </span>\n <span v-else>\n {{ t.savedBlocks.save }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\nimport SavedBlockPreviewRow from \"./SavedBlockPreviewRow.vue\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n EDITOR_KEY,\n SAVED_BLOCKS_KEY,\n requireInject,\n} from \"../keys\";\nimport { getBlockLabel } from \"../utils/blockTypeLabels\";\nimport { LoaderCircle } from \"@lucide/vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport type { Block } from \"@templatical/types\";\nimport { computed, inject, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n /**\n * Blocks chosen during the canvas pick session, in the order they were\n * picked — `pickedIds` is built from a `Set`, which iterates in insertion\n * order. That order seeds the reorderable list below. Every id must be a\n * top-level block, which is what the session guarantees.\n */\n pickedIds: readonly string[];\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"saved\"): void;\n}>();\n\nconst { t, format } = useI18n();\nconst editor = requireInject(EDITOR_KEY, \"SaveBlockDialog\");\nconst savedBlocks = requireInject(SAVED_BLOCKS_KEY, \"SaveBlockDialog\");\n// Optional: a consumer with no custom blocks never provides definitions, and\n// the label helper degrades to the type slug rather than throwing.\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst name = ref(\"\");\nconst category = ref(\"\");\nconst isSaving = ref(false);\nconst error = ref<string | null>(null);\nconst announcement = ref(\"\");\n\n/**\n * The order the blocks will be saved in: seeded from the pick order, then owned\n * by this dialog once the user drags a row.\n *\n * Ids rather than blocks, deliberately. Resolving late means a block edited\n * while the dialog is open (a collaborator in Cloud) is persisted as it stands\n * at save time, not as it looked when the dialog opened, and an id that stops\n * resolving drops out on its own instead of leaving a stale snapshot behind.\n */\nconst orderedIds = ref<string[]>([]);\n\n/**\n * The picked blocks in the list's current order. Ids that no longer resolve (a\n * block deleted mid-session) simply drop out.\n */\nconst pickedBlocks = computed(() => {\n const byId = new Map(editor.content.value.blocks.map((b) => [b.id, b]));\n return orderedIds.value\n .map((id) => byId.get(id))\n .filter((b): b is Block => b !== undefined);\n});\n\n/**\n * Sortable's model. The setter reads ids straight back off the emitted entries\n * rather than storing them, so a Sortable `el` expando can never reach the\n * payload `create()` sends to the provider — and because the getter is already\n * filtered, writing through it prunes unresolvable ids too.\n */\nconst orderedBlocks = computed<Block[]>({\n get: () => pickedBlocks.value,\n set: (blocks) => {\n orderedIds.value = blocks.map((b) => b.id);\n },\n});\n\n/** Keyboard reorder from a focused grip: swap with the neighbouring row. */\nfunction moveBlock(blockId: string, delta: -1 | 1): void {\n const order = pickedBlocks.value.map((b) => b.id);\n const from = order.indexOf(blockId);\n const to = from + delta;\n if (from === -1 || to < 0 || to >= order.length) return;\n [order[from], order[to]] = [order[to], order[from]];\n orderedIds.value = order;\n\n const moved = pickedBlocks.value[to];\n announcement.value = moved\n ? format(t.savedBlocks.reorderAnnouncement, {\n block: label(moved),\n position: to + 1,\n total: order.length,\n })\n : \"\";\n}\n\n/** Custom blocks read as the consumer's name for them, not the type `custom`. */\nfunction label(block: Block): string {\n return getBlockLabel(block, t, customBlockDefinitions);\n}\n\n/** e.g. \"Title, Paragraph, Button\" — what the user is about to save. */\nconst summary = computed(() => pickedBlocks.value.map(label).join(\", \"));\n\n// `immediate` matters: the dialog is mounted lazily behind a `v-if` on the\n// same state that drives `visible`, so it can mount with `visible` already\n// true and would otherwise never reset the name field or seed the order.\nwatch(\n () => props.visible,\n (visible) => {\n if (visible) {\n name.value = \"\";\n category.value = \"\";\n error.value = null;\n announcement.value = \"\";\n orderedIds.value = [...props.pickedIds];\n }\n },\n { immediate: true },\n);\n\nconst canSave = computed(\n () =>\n name.value.trim().length > 0 &&\n pickedBlocks.value.length > 0 &&\n !isSaving.value,\n);\n\nasync function handleSave(): Promise<void> {\n if (!canSave.value) return;\n\n const blocks = pickedBlocks.value;\n // Defence in depth: never persist an empty saved block (it would list as\n // \"0 block(s)\" and insert nothing). Guards a pick set whose ids no longer\n // resolve to anything on the canvas.\n if (blocks.length === 0) return;\n\n isSaving.value = true;\n error.value = null;\n\n try {\n // Empty stays undefined rather than \"\": an entry is either categorised or\n // it isn't, and a blank string would show up as a nameless filter option.\n await savedBlocks.create(\n name.value.trim(),\n blocks,\n category.value.trim() || undefined,\n );\n emit(\"saved\");\n emit(\"close\");\n } catch (err) {\n error.value = (err as Error).message;\n } finally {\n isSaving.value = false;\n }\n}\n\nfunction handleClose(): void {\n if (!isSaving.value) {\n emit(\"close\");\n }\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSave();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSaving\"\n aria-labelledby=\"tpl-save-block-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-full tpl:max-w-2xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-save-block-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.savedBlocks.saveAsBlock }}\n </h3>\n\n <!-- Saved block name -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.name }}\n </label>\n <input\n v-model=\"name\"\n type=\"text\"\n data-testid=\"saved-blocks-name-input\"\n :placeholder=\"t.savedBlocks.namePlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n </div>\n\n <!-- Category: free text, with the already-used categories offered as\n suggestions. A datalist rather than a <select> because the set is\n derived from existing entries — there is no registry to pick from,\n and the first entry in a category has to be able to name it. -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-save-block-category\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.savedBlocks.category }}\n </label>\n <input\n id=\"tpl-save-block-category\"\n v-model=\"category\"\n type=\"text\"\n data-testid=\"saved-blocks-category-input\"\n list=\"tpl-saved-block-categories\"\n :placeholder=\"t.savedBlocks.categoryPlaceholder\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-[var(--tpl-shadow-sm)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n :disabled=\"isSaving\"\n />\n <datalist id=\"tpl-saved-block-categories\">\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n />\n </datalist>\n </div>\n\n <!-- Summary of what the pick session chose. Which blocks were picked is\n settled on the canvas — the dialog re-presents them only so the user\n can see and order what they're about to save. -->\n <p\n data-testid=\"saved-blocks-save-summary\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ format(t.savedBlocks.savingCount, { count: pickedBlocks.length }) }}\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">{{ summary }}</span>\n </p>\n\n <!-- Reorderable preview. Seeded in pick order and saved in whatever order\n the list ends up in. Its own Sortable instance with no `group`, so it\n can never exchange items with the canvas or a section column. -->\n <p\n v-if=\"pickedBlocks.length > 1\"\n class=\"tpl:mb-1.5 tpl:shrink-0 tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.reorderHint }}\n </p>\n <!-- The only growing region: takes whatever the dialog's backdrop cap\n leaves after the fixed chrome, rather than a hand-tuned vh guess that\n a row expanding to full height would blow past. `min-h-0` is what\n lets a flex child actually shrink below its content and scroll. -->\n <VueDraggable\n v-model=\"orderedBlocks\"\n data-testid=\"saved-blocks-reorder-list\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-saved-block-reorder-handle\"\n :force-fallback=\"true\"\n class=\"tpl:mb-4 tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-1.5 tpl:overflow-y-auto\"\n >\n <SavedBlockPreviewRow\n v-for=\"(block, index) in orderedBlocks\"\n :key=\"block.id\"\n :block=\"block\"\n :position=\"index + 1\"\n :total=\"orderedBlocks.length\"\n @move=\"(delta) => moveBlock(block.id, delta)\"\n />\n </VueDraggable>\n\n <!-- Keyboard reorder announcement region (visually hidden, live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n {{ announcement }}\n </div>\n\n <!-- Error message -->\n <p\n v-if=\"error\"\n role=\"alert\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ error }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-[var(--tpl-shadow-sm)] tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSaving\"\n :class=\"{\n 'tpl:cursor-not-allowed tpl:opacity-50': isSaving,\n }\"\n @click=\"handleClose\"\n >\n {{ t.savedBlocks.cancel }}\n </button>\n <button\n type=\"button\"\n :class=\"primaryBtnClass\"\n :disabled=\"!canSave\"\n @click=\"handleSave\"\n >\n <span v-if=\"isSaving\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.savedBlocks.saving }}\n </span>\n <span v-else>\n {{ t.savedBlocks.save }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  import { B as e, E as t, F as n, H as r, N as i, Q as a, R as ee, S as o, U as s, a as c, at as l, b as u, c as d, ct as f, f as p, g as m, h, k as te, l as g, lt as ne, m as _, o as v, p as y, u as b, ut as x, x as re } from "./draggable-IB4x6w2O.js";
2
- import "./useEditorCore-DZmtKwJh.js";
2
+ import "./useEditorCore-D4kzTMtQ.js";
3
3
  import { $ as S, U as ie, X as ae, g as oe } from "./keys-DCWeHkSF.js";
4
4
  import { t as se } from "./useI18n-CyO_Nmca.js";
5
- import { K as ce, N as le, Y as C, d as ue, n as de } from "./icons-BKkJ-5xF.js";
5
+ import { P as ce, X as C, f as le, n as ue, q as de } from "./icons-CQnjugjy.js";
6
6
  import { v as fe } from "./styleConstants-C_YAPdQS.js";
7
- import { t as w } from "./blockTypeIcons-raBjStuv.js";
7
+ import { t as w } from "./blockTypeIcons-CdfCfqHE.js";
8
8
  import { t as pe } from "./formatAbsoluteDateTime-DMmop3C-.js";
9
9
  import { t as me } from "./formatRelativeTime-CtUU-QZ8.js";
10
- import { t as he } from "./TplModal-D8DoPbFa.js";
10
+ import { t as he } from "./TplModal-mLMmolQp.js";
11
11
  //#region src/components/SavedBlocksBrowserModal.vue?vue&type=script&setup=true&lang.ts
12
12
  var ge = {
13
13
  role: "dialog",
@@ -63,7 +63,7 @@ var ge = {
63
63
  props: { visible: { type: Boolean } },
64
64
  emits: ["close", "insert"],
65
65
  setup(o, { emit: A }) {
66
- let it = o, j = A, at = re(() => import("./BlockPreviewCanvas-D31TNib3.js").then((e) => e.n)), { t: M, format: N } = se(), P = S(ie, "SavedBlocksBrowserModal"), F = S(oe, "SavedBlocksBrowserModal"), ot = t(ae, void 0), I = a(""), L = a(""), R = a(null), z = a(null), B = a(null), V = a(""), H = a(""), U = a(null), W = a("end"), G = p(() => {
66
+ let it = o, j = A, at = re(() => import("./BlockPreviewCanvas-CMmv1FTA.js").then((e) => e.n)), { t: M, format: N } = se(), P = S(ie, "SavedBlocksBrowserModal"), F = S(oe, "SavedBlocksBrowserModal"), ot = t(ae, void 0), I = a(""), L = a(""), R = a(null), z = a(null), B = a(null), V = a(""), H = a(""), U = a(null), W = a("end"), G = p(() => {
67
67
  let e = I.value.trim().toLowerCase(), t = L.value;
68
68
  return P.savedBlocks.value.filter((n) => !(e && !n.name.toLowerCase().includes(e) || t && n.category !== t));
69
69
  }), K = p(() => P.isLoading.value && P.savedBlocks.value.length === 0), st = p(() => I.value.trim().length > 0 || L.value !== ""), q = p(() => R.value ? P.savedBlocks.value.find((e) => e.id === R.value) ?? null : null), ct = p(() => {
@@ -167,12 +167,12 @@ var ge = {
167
167
  "aria-label": l(M).savedBlocks.close,
168
168
  class: "tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-1 tpl:transition-colors tpl:text-[var(--tpl-text-dim)]",
169
169
  onClick: $
170
- }, [u(l(de), {
170
+ }, [u(l(ue), {
171
171
  size: 16,
172
172
  "stroke-width": 2
173
173
  })], 8, ye)]),
174
174
  y("div", be, [y("div", xe, [
175
- y("div", Se, [y("div", Ce, [u(l(le), {
175
+ y("div", Se, [y("div", Ce, [u(l(ce), {
176
176
  size: 14,
177
177
  "stroke-width": 2,
178
178
  class: "tpl:pointer-events-none tpl:absolute tpl:left-3 tpl:top-1/2 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]"
@@ -269,7 +269,7 @@ var ge = {
269
269
  "aria-label": l(M).savedBlocks.rename,
270
270
  title: l(M).savedBlocks.rename,
271
271
  onClick: g((t) => pt(e), ["stop"])
272
- }, [u(l(ce), {
272
+ }, [u(l(de), {
273
273
  size: 12,
274
274
  "stroke-width": 1.5
275
275
  })], 8, He)) : h("", !0), l(P).canDeleteBlock(e) ? (i(), m("button", {
@@ -278,7 +278,7 @@ var ge = {
278
278
  "aria-label": l(M).savedBlocks.delete,
279
279
  title: l(M).savedBlocks.delete,
280
280
  onClick: g((t) => z.value = e.id, ["stop"])
281
- }, [u(l(ue), {
281
+ }, [u(l(le), {
282
282
  size: 12,
283
283
  "stroke-width": 1.5
284
284
  })], 10, Ue)) : h("", !0)], 64))
@@ -318,4 +318,4 @@ var ge = {
318
318
  //#endregion
319
319
  export { A as default };
320
320
 
321
- //# sourceMappingURL=SavedBlocksBrowserModal-DpXWf4Pi.js.map
321
+ //# sourceMappingURL=SavedBlocksBrowserModal-B43H5zKA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SavedBlocksBrowserModal-B43H5zKA.js","names":["_openBlock","_createBlock","TplModal","_withCtx","_createElementVNode","_toDisplayString","_unref","_createVNode","_withDirectives","_vModelText","_createElementBlock","_Fragment","_renderList","_vModelSelect","_createCommentVNode","_withKeys","_withModifiers","_normalizeStyle","_resolveDynamicComponent","_normalizeClass","Trash2","_sfc_main","watch","inject","renderList","withCtx","openBlock","ref","resolveDynamicComponent","defineComponent","withDirectives","vModelSelect","unref","createVNode","withKeys","normalizeClass","computed","createElementBlock","nextTick","withModifiers","normalizeStyle","createBlock","vModelText","createBaseVNode","Fragment","toDisplayString","defineAsyncComponent","requireInject","SAVED_BLOCKS_KEY","UI_LOCALE_KEY","EDITOR_KEY","useI18n","Search","Package","Trash","X","Pencil","primaryBtnClass","blockTypeIcons","formatAbsoluteDateTime","formatRelativeTime","TplModal_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","_hoisted_31","_hoisted_32","_hoisted_33","_hoisted_34","_hoisted_35","_hoisted_36","_hoisted_37","_hoisted_38","_hoisted_39","_hoisted_40","_hoisted_41","_hoisted_42","_hoisted_43","_hoisted_44","_hoisted_45","_hoisted_46","_hoisted_47","SavedBlocksBrowserModal_default","__emit","props","__props","emit","BlockPreviewCanvas","n","savedBlocks","editor","uiLocale","searchQuery","categoryFilter","selectedId","confirmDeleteId","renamingId","renameDraft","renameCategoryDraft","renameInput","insertPosition","filtered","query","category","b","isInitialLoad","isFiltering","selected","positionOptions","options","t","blocks","i","block","format","resolvedInsertIndex","idx","visible","selectedBlockId","getBlockTypeIcons","icons","seen","saved","getRemainingTypeCount","types","relativeLabel","raw","absoluteLabel","handleDelete","id","startRename","cancelRename","commitRename","nextName","nextCategory","current","patch","onEditFocusOut","row","event","next","handleInsert","handleClose","handleKeydown","_cache","$event","option","createCommentVNode","item","el","icon","opt"],"sources":["../../../src/components/SavedBlocksBrowserModal.vue","../../../src/components/SavedBlocksBrowserModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { formatAbsoluteDateTime } from \"../utils/formatAbsoluteDateTime\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport {\n SAVED_BLOCKS_KEY,\n EDITOR_KEY,\n UI_LOCALE_KEY,\n requireInject,\n} from \"../keys\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport { Package, Pencil, Search, Trash2, X } from \"@lucide/vue\";\nimport {\n computed,\n defineAsyncComponent,\n inject,\n nextTick,\n ref,\n watch,\n} from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"insert\", saved: SavedBlock, insertIndex: number | undefined): void;\n}>();\n\nconst BlockPreviewCanvas = defineAsyncComponent(\n () => import(\"./BlockPreviewCanvas.vue\"),\n);\n\nconst { t, format } = useI18n();\nconst savedBlocks = requireInject(SAVED_BLOCKS_KEY, \"SavedBlocksBrowserModal\");\nconst editor = requireInject(EDITOR_KEY, \"SavedBlocksBrowserModal\");\nconst uiLocale = inject(UI_LOCALE_KEY, undefined);\n\nconst searchQuery = ref(\"\");\n/** `\"\"` = no category filter. Matched exactly against `SavedBlock.category`. */\nconst categoryFilter = ref(\"\");\nconst selectedId = ref<string | null>(null);\nconst confirmDeleteId = ref<string | null>(null);\nconst renamingId = ref<string | null>(null);\nconst renameDraft = ref(\"\");\nconst renameCategoryDraft = ref(\"\");\nconst renameInput = ref<HTMLInputElement | null>(null);\n// 'end' = append, 'beginning' = index 0, or block id = after that block\nconst insertPosition = ref<string>(\"end\");\n\n// Order is the provider's to decide — the editor never re-sorts. Whatever\n// `list()` returns is what the user sees (the bundled localStorage adapter, for\n// instance, returns newest-first). Filters only narrow; they never reorder.\n//\n// Both filters run in memory over the loaded entries rather than round-tripping\n// to the provider: that keeps a BYO provider at four dumb methods, and it is\n// what lets the category options below be derived at all — a provider-filtered\n// response could not tell us which other categories exist.\nconst filtered = computed(() => {\n const query = searchQuery.value.trim().toLowerCase();\n const category = categoryFilter.value;\n return savedBlocks.savedBlocks.value.filter((b) => {\n if (query && !b.name.toLowerCase().includes(query)) return false;\n if (category && b.category !== category) return false;\n return true;\n });\n});\n\n/**\n * The list is fetched when this modal opens, not at editor mount, so a first\n * open can arrive before the provider answers. Show the skeleton only while\n * there is *nothing* to display: on a reopen the previous entries render\n * immediately and the refetch lands underneath them.\n *\n * Without this the empty state would claim \"No saved blocks yet\" for the whole\n * duration of the request — false, not merely unhelpful.\n */\nconst isInitialLoad = computed(\n () =>\n savedBlocks.isLoading.value && savedBlocks.savedBlocks.value.length === 0,\n);\n\n/** Whether any filter is narrowing the list — drives the empty-state copy. */\nconst isFiltering = computed(\n () => searchQuery.value.trim().length > 0 || categoryFilter.value !== \"\",\n);\n\nconst selected = computed(() => {\n if (!selectedId.value) return null;\n return (\n savedBlocks.savedBlocks.value.find((b) => b.id === selectedId.value) ?? null\n );\n});\n\ninterface PositionOption {\n value: string;\n label: string;\n}\n\nconst positionOptions = computed<PositionOption[]>(() => {\n const options: PositionOption[] = [\n { value: \"beginning\", label: t.savedBlocks.insertAtBeginning },\n ];\n const blocks = editor.content.value.blocks;\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i];\n const typeKey = block.type as keyof typeof t.blocks;\n const label = t.blocks[typeKey] ?? block.type;\n options.push({\n value: block.id,\n label: format(t.savedBlocks.insertAfterBlock, {\n block: `${label} ${i + 1}`,\n }),\n });\n }\n options.push({ value: \"end\", label: t.savedBlocks.insertAtEnd });\n return options;\n});\n\nconst resolvedInsertIndex = computed<number | undefined>(() => {\n if (insertPosition.value === \"end\") return undefined;\n if (insertPosition.value === \"beginning\") return 0;\n const blocks = editor.content.value.blocks;\n const idx = blocks.findIndex((b) => b.id === insertPosition.value);\n if (idx !== -1) return idx + 1;\n return undefined;\n});\n\n// `immediate` matters: the modal is mounted lazily behind a `v-if` on the same\n// state that drives `visible`, so it can mount with `visible` already true and\n// would otherwise never resolve the default insert position.\nwatch(\n () => props.visible,\n (visible) => {\n if (visible) {\n searchQuery.value = \"\";\n categoryFilter.value = \"\";\n selectedId.value = null;\n confirmDeleteId.value = null;\n renamingId.value = null;\n // Default to after selected block, or end\n const selectedBlockId = editor.state.selectedBlockId;\n if (selectedBlockId) {\n const idx = editor.content.value.blocks.findIndex(\n (b) => b.id === selectedBlockId,\n );\n insertPosition.value = idx !== -1 ? selectedBlockId : \"end\";\n } else {\n insertPosition.value = \"end\";\n }\n }\n },\n { immediate: true },\n);\n\nfunction getBlockTypeIcons(\n saved: SavedBlock,\n): { type: string; icon: unknown }[] {\n const icons: { type: string; icon: unknown }[] = [];\n const seen = new Set<string>();\n for (const block of saved.content) {\n if (!seen.has(block.type) && blockTypeIcons[block.type]) {\n seen.add(block.type);\n icons.push({ type: block.type, icon: blockTypeIcons[block.type] });\n }\n if (icons.length >= 5) break;\n }\n return icons;\n}\n\nfunction getRemainingTypeCount(saved: SavedBlock): number {\n const types = new Set(saved.content.map((b) => b.type));\n return Math.max(0, types.size - 5);\n}\n\n/** Compact \"5m ago\" label, or \"\" when the provider supplies no usable timestamp. */\nfunction relativeLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n return formatRelativeTime(raw, t.time, format) ?? \"\";\n}\n\n/**\n * Absolute timestamp for the row's tooltip. Built by `Intl`, so it needs no\n * i18n keys — but it does need the editor's locale, or it formats in the\n * browser's and disagrees with the translated row beside it.\n */\nfunction absoluteLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n return formatAbsoluteDateTime(raw, uiLocale);\n}\n\nasync function handleDelete(id: string): Promise<void> {\n try {\n await savedBlocks.remove(id);\n if (selectedId.value === id) {\n selectedId.value = null;\n }\n } finally {\n confirmDeleteId.value = null;\n }\n}\n\nasync function startRename(saved: SavedBlock): Promise<void> {\n // The pencil is already hidden when editing isn't permitted; this stops a\n // programmatic call opening an editor whose commit could only fail.\n if (!savedBlocks.canUpdateBlock(saved)) return;\n renamingId.value = saved.id;\n renameDraft.value = saved.name;\n renameCategoryDraft.value = saved.category ?? \"\";\n confirmDeleteId.value = null;\n await nextTick();\n renameInput.value?.focus();\n renameInput.value?.select();\n}\n\nfunction cancelRename(): void {\n renamingId.value = null;\n renameDraft.value = \"\";\n renameCategoryDraft.value = \"\";\n}\n\nasync function commitRename(id: string): Promise<void> {\n // Guards two races: Escape sets `renamingId` to null and the unmounting\n // input can still emit `blur`, and Enter commits then blurs. Either would\n // otherwise fire a second, unwanted update.\n if (renamingId.value !== id) return;\n const nextName = renameDraft.value.trim();\n const nextCategory = renameCategoryDraft.value.trim();\n const current = savedBlocks.savedBlocks.value.find((b) => b.id === id);\n\n // An empty name is not a rename — fall back to what's stored, so clearing\n // the field and blurring can't wipe the entry's name.\n const patch: Partial<{ name: string; category: string }> = {};\n if (nextName && nextName !== current?.name) patch.name = nextName;\n // \"\" is meaningful here: it clears the category. Only skip when unchanged.\n if (nextCategory !== (current?.category ?? \"\")) patch.category = nextCategory;\n\n // Skip the round-trip when nothing actually changed.\n if (Object.keys(patch).length === 0) {\n cancelRename();\n return;\n }\n try {\n await savedBlocks.update(id, patch);\n } finally {\n cancelRename();\n }\n}\n\n/**\n * Commit when focus leaves the edit row entirely — not when it moves between\n * the name and category inputs. A plain `@blur` on each input would commit\n * (and unmount the row) the moment the user tabbed from one to the other.\n */\nfunction onEditFocusOut(event: FocusEvent, id: string): void {\n const row = event.currentTarget as HTMLElement;\n const next = event.relatedTarget as Node | null;\n if (next && row.contains(next)) return;\n void commitRename(id);\n}\n\nfunction handleInsert(): void {\n if (selected.value) {\n emit(\"insert\", selected.value, resolvedInsertIndex.value);\n }\n}\n\nfunction handleClose(): void {\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n // While renaming, Enter/Escape belong to the inline input, not the dialog.\n if (renamingId.value) return;\n if (event.key === \"Escape\") {\n handleClose();\n }\n if (event.key === \"Enter\" && selected.value) {\n event.preventDefault();\n handleInsert();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Explicit width, not `w-full`: the modal's parent is shrink-to-fit, so\n `width: 100%` resolved against its own contents and the 600px preview\n canvas drove the dialog's size — it measured 523px with nothing selected\n and 965px with a block selected, nearly doubling on click. A pinned\n width makes every state identical and stops any future right-pane\n content from moving it. `max-w-full` keeps it inside the backdrop on\n narrow hosts — a percentage, so it neither reintroduces the `rem`\n sensitivity of issue #209 nor the viewport mismatch of issue #575; the\n preview scrolls within. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n data-testid=\"saved-blocks-browser\"\n aria-labelledby=\"tpl-saved-blocks-browser-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-[1000px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)]\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <!-- Header -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:px-5 tpl:py-4 tpl:border-[var(--tpl-border)]\"\n >\n <h3\n id=\"tpl-saved-blocks-browser-title\"\n class=\"tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.savedBlocks.browse }}\n </h3>\n <button\n :aria-label=\"t.savedBlocks.close\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-1 tpl:transition-colors tpl:text-[var(--tpl-text-dim)]\"\n @click=\"handleClose\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Body -->\n <div class=\"tpl:flex tpl:min-h-0 tpl:flex-1 tpl:overflow-hidden\">\n <!-- Left panel: saved block list -->\n <div\n class=\"tpl:flex tpl:w-[300px] tpl:shrink-0 tpl:flex-col tpl:overflow-hidden\"\n >\n <!-- Search + category filter -->\n <div\n class=\"tpl:flex tpl:flex-col tpl:gap-2 tpl:px-4 tpl:pt-4 tpl:pb-3\"\n >\n <div class=\"tpl:relative\">\n <Search\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:left-3 tpl:top-1/2 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n />\n <input\n v-model=\"searchQuery\"\n type=\"text\"\n :placeholder=\"t.savedBlocks.search\"\n :disabled=\"isInitialLoad\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:pl-9 tpl:pr-3 tpl:text-sm tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:disabled:opacity-50 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n </div>\n <!-- Only worth showing once something is actually categorised. -->\n <select\n v-if=\"savedBlocks.categories.value.length > 0\"\n v-model=\"categoryFilter\"\n data-testid=\"saved-blocks-category-filter\"\n :aria-label=\"t.savedBlocks.filterByCategory\"\n class=\"tpl:h-8 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option value=\"\">{{ t.savedBlocks.allCategories }}</option>\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n >\n {{ option }}\n </option>\n </select>\n </div>\n\n <!-- Suggestions for the inline category editor, shared by every row -->\n <datalist id=\"tpl-saved-block-browser-categories\">\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n />\n </datalist>\n\n <!-- List -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:px-4 tpl:pb-4\">\n <!-- Skeleton rows: same height and gap as a real card, so the pane\n doesn't resize when the entries land. `aria-busy` on the region\n is what a screen reader announces; the bars are decorative. -->\n <div\n v-if=\"isInitialLoad\"\n data-testid=\"saved-blocks-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1\"\n role=\"status\"\n aria-busy=\"true\"\n :aria-label=\"t.savedBlocks.loading\"\n >\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-saved-block-skeleton tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl:h-3 tpl:w-1/2 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]\"\n />\n <div\n class=\"tpl:mt-2 tpl:h-2.5 tpl:w-1/4 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]\"\n />\n </div>\n </div>\n\n <div\n v-else-if=\"filtered.length > 0\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1\"\n >\n <template v-for=\"item in filtered\" :key=\"item.id\">\n <!-- Inline rename replaces the whole row: an <input> nested\n inside the card <button> would be invalid markup and\n wouldn't focus reliably. -->\n <div\n v-if=\"renamingId === item.id\"\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:gap-1 tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-primary)]\"\n style=\"background-color: var(--tpl-primary-light)\"\n @focusout=\"onEditFocusOut($event, item.id)\"\n >\n <input\n :ref=\"(el) => (renameInput = el as HTMLInputElement | null)\"\n v-model=\"renameDraft\"\n type=\"text\"\n :aria-label=\"t.savedBlocks.rename\"\n class=\"tpl:h-7 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n @keydown.enter.prevent.stop=\"commitRename(item.id)\"\n @keydown.esc.prevent.stop=\"cancelRename()\"\n />\n <input\n v-model=\"renameCategoryDraft\"\n type=\"text\"\n data-testid=\"saved-blocks-edit-category\"\n :aria-label=\"t.savedBlocks.category\"\n :placeholder=\"t.savedBlocks.categoryPlaceholder\"\n list=\"tpl-saved-block-browser-categories\"\n class=\"tpl:h-7 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n @keydown.enter.prevent.stop=\"commitRename(item.id)\"\n @keydown.esc.prevent.stop=\"cancelRename()\"\n />\n </div>\n\n <button\n v-else\n type=\"button\"\n data-testid=\"saved-block-card\"\n :aria-pressed=\"selectedId === item.id\"\n class=\"tpl:group/card tpl:w-full tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:bg-transparent tpl:px-3 tpl:py-2 tpl:text-left tpl:transition-all tpl:duration-[120ms]\"\n :style=\"{\n borderColor:\n selectedId === item.id\n ? 'var(--tpl-primary)'\n : 'var(--tpl-border)',\n backgroundColor:\n selectedId === item.id\n ? 'var(--tpl-primary-light)'\n : 'transparent',\n }\"\n @click=\"selectedId = item.id\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <span\n class=\"tpl:flex-1 tpl:truncate tpl:text-xs tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ item.name }}\n </span>\n <span\n class=\"tpl:shrink-0 tpl:rounded-full tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n format(t.savedBlocks.blockCount, {\n count: item.content.length,\n })\n }}\n </span>\n </div>\n <div\n v-if=\"item.category\"\n class=\"tpl:mt-1 tpl:flex tpl:items-center\"\n >\n <span\n data-testid=\"saved-block-category\"\n class=\"tpl:max-w-full tpl:truncate tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]\"\n >\n {{ item.category }}\n </span>\n </div>\n <div class=\"tpl:mt-1 tpl:flex tpl:items-center tpl:gap-1\">\n <component\n :is=\"icon.icon\"\n v-for=\"icon in getBlockTypeIcons(item)\"\n :key=\"icon.type\"\n :size=\"14\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span\n v-if=\"getRemainingTypeCount(item) > 0\"\n class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n +{{ getRemainingTypeCount(item) }}\n </span>\n <span\n v-if=\"relativeLabel(item)\"\n data-testid=\"saved-block-updated\"\n class=\"tpl:ml-1 tpl:truncate tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n :title=\"absoluteLabel(item)\"\n >\n {{ relativeLabel(item) }}\n </span>\n <button\n v-if=\"confirmDeleteId === item.id\"\n :aria-label=\"t.savedBlocks.deleteConfirm\"\n class=\"tpl:ml-auto tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:transition-colors tpl:border-[var(--tpl-danger)] tpl:text-[var(--tpl-danger)]\"\n style=\"background-color: transparent\"\n @click.stop=\"handleDelete(item.id)\"\n >\n {{ t.savedBlocks.deleteConfirm }}\n </button>\n <!-- Hidden, not disabled: an action the user can't perform\n is better absent than greyed out. `ml-auto` moves to\n whichever button survives so the row stays aligned. -->\n <template v-else>\n <button\n v-if=\"savedBlocks.canUpdateBlock(item)\"\n class=\"tpl-saved-block-rename-btn tpl:ml-auto tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-dim)]\"\n :aria-label=\"t.savedBlocks.rename\"\n :title=\"t.savedBlocks.rename\"\n @click.stop=\"startRename(item)\"\n >\n <Pencil :size=\"12\" :stroke-width=\"1.5\" />\n </button>\n <button\n v-if=\"savedBlocks.canDeleteBlock(item)\"\n class=\"tpl-saved-block-delete-btn tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-dim)]\"\n :class=\"{\n 'tpl:ml-auto': !savedBlocks.canUpdateBlock(item),\n }\"\n :aria-label=\"t.savedBlocks.delete\"\n :title=\"t.savedBlocks.delete\"\n @click.stop=\"confirmDeleteId = item.id\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"1.5\" />\n </button>\n </template>\n </div>\n </button>\n </template>\n </div>\n\n <!-- Empty state — only once we actually know the library is empty -->\n <div\n v-else\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:py-12\"\n >\n <Package\n :size=\"32\"\n :stroke-width=\"1\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n v-if=\"isFiltering\"\n class=\"tpl:mt-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.noResults }}\n </p>\n <template v-else>\n <p class=\"tpl:mt-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\">\n {{ t.savedBlocks.empty }}\n </p>\n <p\n class=\"tpl:mt-1 tpl:max-w-[220px] tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.emptyHint }}\n </p>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Right panel: preview -->\n <div\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-hidden tpl:border-l tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-if=\"selected\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-hidden\"\n >\n <!-- Visual preview -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <BlockPreviewCanvas :blocks=\"selected.content\" />\n </div>\n </div>\n\n <!-- Empty preview state -->\n <div\n v-else\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:items-center tpl:justify-center tpl:px-4\"\n >\n <Package\n :size=\"32\"\n :stroke-width=\"1\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:mt-2 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.selectToPreview }}\n </p>\n </div>\n </div>\n </div>\n\n <!-- Footer -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-t tpl:px-5 tpl:py-3 tpl:border-[var(--tpl-border)]\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <label\n class=\"tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.insertPosition }}\n </label>\n <select\n v-model=\"insertPosition\"\n class=\"tpl:h-7 tpl:max-w-[220px] tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"opt in positionOptions\"\n :key=\"opt.value\"\n :value=\"opt.value\"\n >\n {{ opt.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:flex tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"saved-blocks-browser-close\"\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 @click=\"handleClose\"\n >\n {{ t.savedBlocks.close }}\n </button>\n <button\n type=\"button\"\n :class=\"primaryBtnClass\"\n :disabled=\"!selected\"\n @click=\"handleInsert\"\n >\n {{ t.savedBlocks.insert }}\n </button>\n </div>\n </div>\n </div>\n </TplModal>\n</template>\n\n<style>\n/* Subtle breathing so the placeholders read as \"loading\" rather than as empty\n rows. `prefers-reduced-motion` holds them still at full opacity — the bars\n still communicate shape without animating. */\n.tpl-saved-block-skeleton {\n animation: tpl-saved-block-pulse 1.4s ease-in-out infinite;\n}\n\n@keyframes tpl-saved-block-pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tpl-saved-block-skeleton {\n animation: none;\n }\n}\n\n.tpl-saved-block-delete-btn:hover {\n color: var(--tpl-danger) !important;\n}\n\n.tpl-saved-block-rename-btn:hover {\n color: var(--tpl-primary) !important;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { primaryBtnClass } from \"../constants/styleConstants\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { formatAbsoluteDateTime } from \"../utils/formatAbsoluteDateTime\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport {\n SAVED_BLOCKS_KEY,\n EDITOR_KEY,\n UI_LOCALE_KEY,\n requireInject,\n} from \"../keys\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport { Package, Pencil, Search, Trash2, X } from \"@lucide/vue\";\nimport {\n computed,\n defineAsyncComponent,\n inject,\n nextTick,\n ref,\n watch,\n} from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"insert\", saved: SavedBlock, insertIndex: number | undefined): void;\n}>();\n\nconst BlockPreviewCanvas = defineAsyncComponent(\n () => import(\"./BlockPreviewCanvas.vue\"),\n);\n\nconst { t, format } = useI18n();\nconst savedBlocks = requireInject(SAVED_BLOCKS_KEY, \"SavedBlocksBrowserModal\");\nconst editor = requireInject(EDITOR_KEY, \"SavedBlocksBrowserModal\");\nconst uiLocale = inject(UI_LOCALE_KEY, undefined);\n\nconst searchQuery = ref(\"\");\n/** `\"\"` = no category filter. Matched exactly against `SavedBlock.category`. */\nconst categoryFilter = ref(\"\");\nconst selectedId = ref<string | null>(null);\nconst confirmDeleteId = ref<string | null>(null);\nconst renamingId = ref<string | null>(null);\nconst renameDraft = ref(\"\");\nconst renameCategoryDraft = ref(\"\");\nconst renameInput = ref<HTMLInputElement | null>(null);\n// 'end' = append, 'beginning' = index 0, or block id = after that block\nconst insertPosition = ref<string>(\"end\");\n\n// Order is the provider's to decide — the editor never re-sorts. Whatever\n// `list()` returns is what the user sees (the bundled localStorage adapter, for\n// instance, returns newest-first). Filters only narrow; they never reorder.\n//\n// Both filters run in memory over the loaded entries rather than round-tripping\n// to the provider: that keeps a BYO provider at four dumb methods, and it is\n// what lets the category options below be derived at all — a provider-filtered\n// response could not tell us which other categories exist.\nconst filtered = computed(() => {\n const query = searchQuery.value.trim().toLowerCase();\n const category = categoryFilter.value;\n return savedBlocks.savedBlocks.value.filter((b) => {\n if (query && !b.name.toLowerCase().includes(query)) return false;\n if (category && b.category !== category) return false;\n return true;\n });\n});\n\n/**\n * The list is fetched when this modal opens, not at editor mount, so a first\n * open can arrive before the provider answers. Show the skeleton only while\n * there is *nothing* to display: on a reopen the previous entries render\n * immediately and the refetch lands underneath them.\n *\n * Without this the empty state would claim \"No saved blocks yet\" for the whole\n * duration of the request — false, not merely unhelpful.\n */\nconst isInitialLoad = computed(\n () =>\n savedBlocks.isLoading.value && savedBlocks.savedBlocks.value.length === 0,\n);\n\n/** Whether any filter is narrowing the list — drives the empty-state copy. */\nconst isFiltering = computed(\n () => searchQuery.value.trim().length > 0 || categoryFilter.value !== \"\",\n);\n\nconst selected = computed(() => {\n if (!selectedId.value) return null;\n return (\n savedBlocks.savedBlocks.value.find((b) => b.id === selectedId.value) ?? null\n );\n});\n\ninterface PositionOption {\n value: string;\n label: string;\n}\n\nconst positionOptions = computed<PositionOption[]>(() => {\n const options: PositionOption[] = [\n { value: \"beginning\", label: t.savedBlocks.insertAtBeginning },\n ];\n const blocks = editor.content.value.blocks;\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i];\n const typeKey = block.type as keyof typeof t.blocks;\n const label = t.blocks[typeKey] ?? block.type;\n options.push({\n value: block.id,\n label: format(t.savedBlocks.insertAfterBlock, {\n block: `${label} ${i + 1}`,\n }),\n });\n }\n options.push({ value: \"end\", label: t.savedBlocks.insertAtEnd });\n return options;\n});\n\nconst resolvedInsertIndex = computed<number | undefined>(() => {\n if (insertPosition.value === \"end\") return undefined;\n if (insertPosition.value === \"beginning\") return 0;\n const blocks = editor.content.value.blocks;\n const idx = blocks.findIndex((b) => b.id === insertPosition.value);\n if (idx !== -1) return idx + 1;\n return undefined;\n});\n\n// `immediate` matters: the modal is mounted lazily behind a `v-if` on the same\n// state that drives `visible`, so it can mount with `visible` already true and\n// would otherwise never resolve the default insert position.\nwatch(\n () => props.visible,\n (visible) => {\n if (visible) {\n searchQuery.value = \"\";\n categoryFilter.value = \"\";\n selectedId.value = null;\n confirmDeleteId.value = null;\n renamingId.value = null;\n // Default to after selected block, or end\n const selectedBlockId = editor.state.selectedBlockId;\n if (selectedBlockId) {\n const idx = editor.content.value.blocks.findIndex(\n (b) => b.id === selectedBlockId,\n );\n insertPosition.value = idx !== -1 ? selectedBlockId : \"end\";\n } else {\n insertPosition.value = \"end\";\n }\n }\n },\n { immediate: true },\n);\n\nfunction getBlockTypeIcons(\n saved: SavedBlock,\n): { type: string; icon: unknown }[] {\n const icons: { type: string; icon: unknown }[] = [];\n const seen = new Set<string>();\n for (const block of saved.content) {\n if (!seen.has(block.type) && blockTypeIcons[block.type]) {\n seen.add(block.type);\n icons.push({ type: block.type, icon: blockTypeIcons[block.type] });\n }\n if (icons.length >= 5) break;\n }\n return icons;\n}\n\nfunction getRemainingTypeCount(saved: SavedBlock): number {\n const types = new Set(saved.content.map((b) => b.type));\n return Math.max(0, types.size - 5);\n}\n\n/** Compact \"5m ago\" label, or \"\" when the provider supplies no usable timestamp. */\nfunction relativeLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n return formatRelativeTime(raw, t.time, format) ?? \"\";\n}\n\n/**\n * Absolute timestamp for the row's tooltip. Built by `Intl`, so it needs no\n * i18n keys — but it does need the editor's locale, or it formats in the\n * browser's and disagrees with the translated row beside it.\n */\nfunction absoluteLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n return formatAbsoluteDateTime(raw, uiLocale);\n}\n\nasync function handleDelete(id: string): Promise<void> {\n try {\n await savedBlocks.remove(id);\n if (selectedId.value === id) {\n selectedId.value = null;\n }\n } finally {\n confirmDeleteId.value = null;\n }\n}\n\nasync function startRename(saved: SavedBlock): Promise<void> {\n // The pencil is already hidden when editing isn't permitted; this stops a\n // programmatic call opening an editor whose commit could only fail.\n if (!savedBlocks.canUpdateBlock(saved)) return;\n renamingId.value = saved.id;\n renameDraft.value = saved.name;\n renameCategoryDraft.value = saved.category ?? \"\";\n confirmDeleteId.value = null;\n await nextTick();\n renameInput.value?.focus();\n renameInput.value?.select();\n}\n\nfunction cancelRename(): void {\n renamingId.value = null;\n renameDraft.value = \"\";\n renameCategoryDraft.value = \"\";\n}\n\nasync function commitRename(id: string): Promise<void> {\n // Guards two races: Escape sets `renamingId` to null and the unmounting\n // input can still emit `blur`, and Enter commits then blurs. Either would\n // otherwise fire a second, unwanted update.\n if (renamingId.value !== id) return;\n const nextName = renameDraft.value.trim();\n const nextCategory = renameCategoryDraft.value.trim();\n const current = savedBlocks.savedBlocks.value.find((b) => b.id === id);\n\n // An empty name is not a rename — fall back to what's stored, so clearing\n // the field and blurring can't wipe the entry's name.\n const patch: Partial<{ name: string; category: string }> = {};\n if (nextName && nextName !== current?.name) patch.name = nextName;\n // \"\" is meaningful here: it clears the category. Only skip when unchanged.\n if (nextCategory !== (current?.category ?? \"\")) patch.category = nextCategory;\n\n // Skip the round-trip when nothing actually changed.\n if (Object.keys(patch).length === 0) {\n cancelRename();\n return;\n }\n try {\n await savedBlocks.update(id, patch);\n } finally {\n cancelRename();\n }\n}\n\n/**\n * Commit when focus leaves the edit row entirely — not when it moves between\n * the name and category inputs. A plain `@blur` on each input would commit\n * (and unmount the row) the moment the user tabbed from one to the other.\n */\nfunction onEditFocusOut(event: FocusEvent, id: string): void {\n const row = event.currentTarget as HTMLElement;\n const next = event.relatedTarget as Node | null;\n if (next && row.contains(next)) return;\n void commitRename(id);\n}\n\nfunction handleInsert(): void {\n if (selected.value) {\n emit(\"insert\", selected.value, resolvedInsertIndex.value);\n }\n}\n\nfunction handleClose(): void {\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n // While renaming, Enter/Escape belong to the inline input, not the dialog.\n if (renamingId.value) return;\n if (event.key === \"Escape\") {\n handleClose();\n }\n if (event.key === \"Enter\" && selected.value) {\n event.preventDefault();\n handleInsert();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Explicit width, not `w-full`: the modal's parent is shrink-to-fit, so\n `width: 100%` resolved against its own contents and the 600px preview\n canvas drove the dialog's size — it measured 523px with nothing selected\n and 965px with a block selected, nearly doubling on click. A pinned\n width makes every state identical and stops any future right-pane\n content from moving it. `max-w-full` keeps it inside the backdrop on\n narrow hosts — a percentage, so it neither reintroduces the `rem`\n sensitivity of issue #209 nor the viewport mismatch of issue #575; the\n preview scrolls within. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n data-testid=\"saved-blocks-browser\"\n aria-labelledby=\"tpl-saved-blocks-browser-title\"\n class=\"tpl-scale-in tpl:flex tpl:max-h-[90%] tpl:w-[1000px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)]\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <!-- Header -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:px-5 tpl:py-4 tpl:border-[var(--tpl-border)]\"\n >\n <h3\n id=\"tpl-saved-blocks-browser-title\"\n class=\"tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.savedBlocks.browse }}\n </h3>\n <button\n :aria-label=\"t.savedBlocks.close\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-1 tpl:transition-colors tpl:text-[var(--tpl-text-dim)]\"\n @click=\"handleClose\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Body -->\n <div class=\"tpl:flex tpl:min-h-0 tpl:flex-1 tpl:overflow-hidden\">\n <!-- Left panel: saved block list -->\n <div\n class=\"tpl:flex tpl:w-[300px] tpl:shrink-0 tpl:flex-col tpl:overflow-hidden\"\n >\n <!-- Search + category filter -->\n <div\n class=\"tpl:flex tpl:flex-col tpl:gap-2 tpl:px-4 tpl:pt-4 tpl:pb-3\"\n >\n <div class=\"tpl:relative\">\n <Search\n :size=\"14\"\n :stroke-width=\"2\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:left-3 tpl:top-1/2 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n />\n <input\n v-model=\"searchQuery\"\n type=\"text\"\n :placeholder=\"t.savedBlocks.search\"\n :disabled=\"isInitialLoad\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:pl-9 tpl:pr-3 tpl:text-sm tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:disabled:opacity-50 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n </div>\n <!-- Only worth showing once something is actually categorised. -->\n <select\n v-if=\"savedBlocks.categories.value.length > 0\"\n v-model=\"categoryFilter\"\n data-testid=\"saved-blocks-category-filter\"\n :aria-label=\"t.savedBlocks.filterByCategory\"\n class=\"tpl:h-8 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option value=\"\">{{ t.savedBlocks.allCategories }}</option>\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n >\n {{ option }}\n </option>\n </select>\n </div>\n\n <!-- Suggestions for the inline category editor, shared by every row -->\n <datalist id=\"tpl-saved-block-browser-categories\">\n <option\n v-for=\"option in savedBlocks.categories.value\"\n :key=\"option\"\n :value=\"option\"\n />\n </datalist>\n\n <!-- List -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:px-4 tpl:pb-4\">\n <!-- Skeleton rows: same height and gap as a real card, so the pane\n doesn't resize when the entries land. `aria-busy` on the region\n is what a screen reader announces; the bars are decorative. -->\n <div\n v-if=\"isInitialLoad\"\n data-testid=\"saved-blocks-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1\"\n role=\"status\"\n aria-busy=\"true\"\n :aria-label=\"t.savedBlocks.loading\"\n >\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-saved-block-skeleton tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)]\"\n >\n <div\n class=\"tpl:h-3 tpl:w-1/2 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]\"\n />\n <div\n class=\"tpl:mt-2 tpl:h-2.5 tpl:w-1/4 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]\"\n />\n </div>\n </div>\n\n <div\n v-else-if=\"filtered.length > 0\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1\"\n >\n <template v-for=\"item in filtered\" :key=\"item.id\">\n <!-- Inline rename replaces the whole row: an <input> nested\n inside the card <button> would be invalid markup and\n wouldn't focus reliably. -->\n <div\n v-if=\"renamingId === item.id\"\n class=\"tpl:flex tpl:w-full tpl:flex-col tpl:gap-1 tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-primary)]\"\n style=\"background-color: var(--tpl-primary-light)\"\n @focusout=\"onEditFocusOut($event, item.id)\"\n >\n <input\n :ref=\"(el) => (renameInput = el as HTMLInputElement | null)\"\n v-model=\"renameDraft\"\n type=\"text\"\n :aria-label=\"t.savedBlocks.rename\"\n class=\"tpl:h-7 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n @keydown.enter.prevent.stop=\"commitRename(item.id)\"\n @keydown.esc.prevent.stop=\"cancelRename()\"\n />\n <input\n v-model=\"renameCategoryDraft\"\n type=\"text\"\n data-testid=\"saved-blocks-edit-category\"\n :aria-label=\"t.savedBlocks.category\"\n :placeholder=\"t.savedBlocks.categoryPlaceholder\"\n list=\"tpl-saved-block-browser-categories\"\n class=\"tpl:h-7 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n @keydown.enter.prevent.stop=\"commitRename(item.id)\"\n @keydown.esc.prevent.stop=\"cancelRename()\"\n />\n </div>\n\n <button\n v-else\n type=\"button\"\n data-testid=\"saved-block-card\"\n :aria-pressed=\"selectedId === item.id\"\n class=\"tpl:group/card tpl:w-full tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:bg-transparent tpl:px-3 tpl:py-2 tpl:text-left tpl:transition-all tpl:duration-[120ms]\"\n :style=\"{\n borderColor:\n selectedId === item.id\n ? 'var(--tpl-primary)'\n : 'var(--tpl-border)',\n backgroundColor:\n selectedId === item.id\n ? 'var(--tpl-primary-light)'\n : 'transparent',\n }\"\n @click=\"selectedId = item.id\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <span\n class=\"tpl:flex-1 tpl:truncate tpl:text-xs tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ item.name }}\n </span>\n <span\n class=\"tpl:shrink-0 tpl:rounded-full tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)]\"\n >\n {{\n format(t.savedBlocks.blockCount, {\n count: item.content.length,\n })\n }}\n </span>\n </div>\n <div\n v-if=\"item.category\"\n class=\"tpl:mt-1 tpl:flex tpl:items-center\"\n >\n <span\n data-testid=\"saved-block-category\"\n class=\"tpl:max-w-full tpl:truncate tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]\"\n >\n {{ item.category }}\n </span>\n </div>\n <div class=\"tpl:mt-1 tpl:flex tpl:items-center tpl:gap-1\">\n <component\n :is=\"icon.icon\"\n v-for=\"icon in getBlockTypeIcons(item)\"\n :key=\"icon.type\"\n :size=\"14\"\n :stroke-width=\"1.5\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <span\n v-if=\"getRemainingTypeCount(item) > 0\"\n class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n >\n +{{ getRemainingTypeCount(item) }}\n </span>\n <span\n v-if=\"relativeLabel(item)\"\n data-testid=\"saved-block-updated\"\n class=\"tpl:ml-1 tpl:truncate tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\"\n :title=\"absoluteLabel(item)\"\n >\n {{ relativeLabel(item) }}\n </span>\n <button\n v-if=\"confirmDeleteId === item.id\"\n :aria-label=\"t.savedBlocks.deleteConfirm\"\n class=\"tpl:ml-auto tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:transition-colors tpl:border-[var(--tpl-danger)] tpl:text-[var(--tpl-danger)]\"\n style=\"background-color: transparent\"\n @click.stop=\"handleDelete(item.id)\"\n >\n {{ t.savedBlocks.deleteConfirm }}\n </button>\n <!-- Hidden, not disabled: an action the user can't perform\n is better absent than greyed out. `ml-auto` moves to\n whichever button survives so the row stays aligned. -->\n <template v-else>\n <button\n v-if=\"savedBlocks.canUpdateBlock(item)\"\n class=\"tpl-saved-block-rename-btn tpl:ml-auto tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-dim)]\"\n :aria-label=\"t.savedBlocks.rename\"\n :title=\"t.savedBlocks.rename\"\n @click.stop=\"startRename(item)\"\n >\n <Pencil :size=\"12\" :stroke-width=\"1.5\" />\n </button>\n <button\n v-if=\"savedBlocks.canDeleteBlock(item)\"\n class=\"tpl-saved-block-delete-btn tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-dim)]\"\n :class=\"{\n 'tpl:ml-auto': !savedBlocks.canUpdateBlock(item),\n }\"\n :aria-label=\"t.savedBlocks.delete\"\n :title=\"t.savedBlocks.delete\"\n @click.stop=\"confirmDeleteId = item.id\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"1.5\" />\n </button>\n </template>\n </div>\n </button>\n </template>\n </div>\n\n <!-- Empty state — only once we actually know the library is empty -->\n <div\n v-else\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:py-12\"\n >\n <Package\n :size=\"32\"\n :stroke-width=\"1\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n v-if=\"isFiltering\"\n class=\"tpl:mt-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.noResults }}\n </p>\n <template v-else>\n <p class=\"tpl:mt-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\">\n {{ t.savedBlocks.empty }}\n </p>\n <p\n class=\"tpl:mt-1 tpl:max-w-[220px] tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.emptyHint }}\n </p>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Right panel: preview -->\n <div\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-hidden tpl:border-l tpl:border-[var(--tpl-border)]\"\n >\n <div\n v-if=\"selected\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-hidden\"\n >\n <!-- Visual preview -->\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <BlockPreviewCanvas :blocks=\"selected.content\" />\n </div>\n </div>\n\n <!-- Empty preview state -->\n <div\n v-else\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:items-center tpl:justify-center tpl:px-4\"\n >\n <Package\n :size=\"32\"\n :stroke-width=\"1\"\n class=\"tpl:text-[var(--tpl-text-dim)]\"\n />\n <p\n class=\"tpl:mt-2 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.selectToPreview }}\n </p>\n </div>\n </div>\n </div>\n\n <!-- Footer -->\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-t tpl:px-5 tpl:py-3 tpl:border-[var(--tpl-border)]\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <label\n class=\"tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.savedBlocks.insertPosition }}\n </label>\n <select\n v-model=\"insertPosition\"\n class=\"tpl:h-7 tpl:max-w-[220px] tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"opt in positionOptions\"\n :key=\"opt.value\"\n :value=\"opt.value\"\n >\n {{ opt.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:flex tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"saved-blocks-browser-close\"\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 @click=\"handleClose\"\n >\n {{ t.savedBlocks.close }}\n </button>\n <button\n type=\"button\"\n :class=\"primaryBtnClass\"\n :disabled=\"!selected\"\n @click=\"handleInsert\"\n >\n {{ t.savedBlocks.insert }}\n </button>\n </div>\n </div>\n </div>\n </TplModal>\n</template>\n\n<style>\n/* Subtle breathing so the placeholders read as \"loading\" rather than as empty\n rows. `prefers-reduced-motion` holds them still at full opacity — the bars\n still communicate shape without animating. */\n.tpl-saved-block-skeleton {\n animation: tpl-saved-block-pulse 1.4s ease-in-out infinite;\n}\n\n@keyframes tpl-saved-block-pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tpl-saved-block-skeleton {\n animation: none;\n }\n}\n\n.tpl-saved-block-delete-btn:hover {\n color: var(--tpl-danger) !important;\n}\n\n.tpl-saved-block-rename-btn:hover {\n color: var(--tpl-primary) !important;\n}\n</style>\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ var c = /* @__PURE__ */ t({
5
5
  __name: "SavedBlocksPanels",
6
6
  props: { feature: {} },
7
7
  setup(t) {
8
- let c = t, l = s(() => import("./SavedBlocksPickBar-CEFIQ8Nd.js")), u = s(() => import("./SaveBlockDialog-jxQtKnVs.js")), d = s(() => import("./SavedBlocksBrowserModal-DpXWf4Pi.js"));
8
+ let c = t, l = s(() => import("./SavedBlocksPickBar-BUvmGqHK.js")), u = s(() => import("./SaveBlockDialog-DZOmDCC8.js")), d = s(() => import("./SavedBlocksBrowserModal-B43H5zKA.js"));
9
9
  function f(e, t) {
10
10
  c.feature.insert(e, t);
11
11
  }
@@ -35,4 +35,4 @@ var c = /* @__PURE__ */ t({
35
35
  //#endregion
36
36
  export { c as default };
37
37
 
38
- //# sourceMappingURL=SavedBlocksPanels-B-ASmf1P.js.map
38
+ //# sourceMappingURL=SavedBlocksPanels-uTSb2xO_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SavedBlocksPanels-uTSb2xO_.js","names":["_openBlock","_createElementBlock","_Fragment","_createBlock","_unref","_createCommentVNode","_sfc_main","openBlock","defineComponent","unref","createElementBlock","createCommentVNode","createBlock","Fragment","defineAsyncComponent","SavedBlocksPanels_default","props","__props","SavedBlocksPickBar","SaveBlockDialog","SavedBlocksBrowserModal","handleInsert","saved","insertIndex","_cache"],"sources":["../../../src/components/SavedBlocksPanels.vue","../../../src/components/SavedBlocksPanels.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Mount point for the saved-blocks UI, shared by the OSS and Cloud editors.\n *\n * Everything here is `defineAsyncComponent` rendered behind `v-if` on its own\n * state, so each chunk is fetched only when actually needed —\n * `defineAsyncComponent` triggers its `import()` on first render, not at\n * definition time. `Editor.vue` in turn lazy-loads *this* wrapper and renders\n * it only when a `SavedBlocksProvider` is configured, so a consumer without one\n * downloads none of it.\n *\n * The pick bar lives here rather than in `Editor.vue` so the editor shell needs\n * no knowledge of saved blocks, and the lazy-load guarantee covers the bar too.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport type { UseSavedBlocksFeatureReturn } from \"../composables/useSavedBlocksFeature\";\n\nconst props = defineProps<{\n feature: UseSavedBlocksFeatureReturn;\n}>();\n\nconst SavedBlocksPickBar = defineAsyncComponent(\n () => import(\"./SavedBlocksPickBar.vue\"),\n);\nconst SaveBlockDialog = defineAsyncComponent(\n () => import(\"./SaveBlockDialog.vue\"),\n);\nconst SavedBlocksBrowserModal = defineAsyncComponent(\n () => import(\"./SavedBlocksBrowserModal.vue\"),\n);\n\nfunction handleInsert(\n saved: SavedBlock,\n insertIndex: number | undefined,\n): void {\n props.feature.insert(saved, insertIndex);\n}\n</script>\n\n<template>\n <SavedBlocksPickBar\n v-if=\"feature.isPicking.value\"\n :count=\"feature.pickedCount.value\"\n @confirm=\"feature.confirmPicking()\"\n @cancel=\"feature.cancelPicking()\"\n />\n\n <SaveBlockDialog\n v-if=\"feature.isSaveDialogOpen.value\"\n :visible=\"feature.isSaveDialogOpen.value\"\n :picked-ids=\"[...feature.pickedIds.value]\"\n @close=\"feature.closeSaveDialog()\"\n @saved=\"feature.refresh()\"\n />\n\n <SavedBlocksBrowserModal\n v-if=\"feature.isBrowserOpen.value\"\n :visible=\"feature.isBrowserOpen.value\"\n @close=\"feature.closeBrowser()\"\n @insert=\"handleInsert\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * Mount point for the saved-blocks UI, shared by the OSS and Cloud editors.\n *\n * Everything here is `defineAsyncComponent` rendered behind `v-if` on its own\n * state, so each chunk is fetched only when actually needed —\n * `defineAsyncComponent` triggers its `import()` on first render, not at\n * definition time. `Editor.vue` in turn lazy-loads *this* wrapper and renders\n * it only when a `SavedBlocksProvider` is configured, so a consumer without one\n * downloads none of it.\n *\n * The pick bar lives here rather than in `Editor.vue` so the editor shell needs\n * no knowledge of saved blocks, and the lazy-load guarantee covers the bar too.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport type { UseSavedBlocksFeatureReturn } from \"../composables/useSavedBlocksFeature\";\n\nconst props = defineProps<{\n feature: UseSavedBlocksFeatureReturn;\n}>();\n\nconst SavedBlocksPickBar = defineAsyncComponent(\n () => import(\"./SavedBlocksPickBar.vue\"),\n);\nconst SaveBlockDialog = defineAsyncComponent(\n () => import(\"./SaveBlockDialog.vue\"),\n);\nconst SavedBlocksBrowserModal = defineAsyncComponent(\n () => import(\"./SavedBlocksBrowserModal.vue\"),\n);\n\nfunction handleInsert(\n saved: SavedBlock,\n insertIndex: number | undefined,\n): void {\n props.feature.insert(saved, insertIndex);\n}\n</script>\n\n<template>\n <SavedBlocksPickBar\n v-if=\"feature.isPicking.value\"\n :count=\"feature.pickedCount.value\"\n @confirm=\"feature.confirmPicking()\"\n @cancel=\"feature.cancelPicking()\"\n />\n\n <SaveBlockDialog\n v-if=\"feature.isSaveDialogOpen.value\"\n :visible=\"feature.isSaveDialogOpen.value\"\n :picked-ids=\"[...feature.pickedIds.value]\"\n @close=\"feature.closeSaveDialog()\"\n @saved=\"feature.refresh()\"\n />\n\n <SavedBlocksBrowserModal\n v-if=\"feature.isBrowserOpen.value\"\n :visible=\"feature.isBrowserOpen.value\"\n @close=\"feature.closeBrowser()\"\n @insert=\"handleInsert\"\n />\n</template>\n"],"mappings":";;;;;;;EAkBA,IAAMgB,IAAQC,GAIRC,IAAqBJ,QACnB,OAAO,mCACf,GACMK,IAAkBL,QAChB,OAAO,gCACf,GACMM,IAA0BN,QACxB,OAAO,wCACf;EAEA,SAASO,EACP,GACA,GACM;GACN,EAAM,QAAQ,OAAOC,GAAOC,CAAW;EACzC;;GAKUN,EAAA,QAAQ,UAAU,SAD1BjB,EAAA,GAAAG,EAKEC,EAAAc,CAAA,GAAA;;IAHC,OAAOD,EAAA,QAAQ,YAAY;IAC3B,WAAO,AAAAO,EAAA,QAAA,MAAEP,EAAA,QAAQ,eAAc;IAC/B,UAAM,AAAAO,EAAA,QAAA,MAAEP,EAAA,QAAQ,cAAa;;GAIxBA,EAAA,QAAQ,iBAAiB,SADjCjB,EAAA,GAAAG,EAMEC,EAAAe,CAAA,GAAA;;IAJC,SAASF,EAAA,QAAQ,iBAAiB;IAClC,cAAU,CAAA,GAAMA,EAAA,QAAQ,UAAU,KAAK;IACvC,SAAK,AAAAO,EAAA,QAAA,MAAEP,EAAA,QAAQ,gBAAe;IAC9B,SAAK,AAAAO,EAAA,QAAA,MAAEP,EAAA,QAAQ,QAAO;;GAIjBA,EAAA,QAAQ,cAAc,SAD9BjB,EAAA,GAAAG,EAKEC,EAAAgB,CAAA,GAAA;;IAHC,SAASH,EAAA,QAAQ,cAAc;IAC/B,SAAK,AAAAO,EAAA,QAAA,MAAEP,EAAA,QAAQ,aAAY;IAC3B,UAAQI"}
@@ -1,7 +1,7 @@
1
1
  import { N as e, S as t, at as n, b as r, ct as i, g as a, p as o, ut as s, y as c } from "./draggable-IB4x6w2O.js";
2
- import "./useEditorCore-DZmtKwJh.js";
2
+ import "./useEditorCore-D4kzTMtQ.js";
3
3
  import { t as l } from "./useI18n-CyO_Nmca.js";
4
- import { Bt as u, n as d } from "./icons-BKkJ-5xF.js";
4
+ import { Vt as u, n as d } from "./icons-CQnjugjy.js";
5
5
  import { v as f, x as p } from "./styleConstants-C_YAPdQS.js";
6
6
  //#region src/components/SavedBlocksPickBar.vue?vue&type=script&setup=true&lang.ts
7
7
  var m = {
@@ -52,4 +52,4 @@ var m = {
52
52
  //#endregion
53
53
  export { x as default };
54
54
 
55
- //# sourceMappingURL=SavedBlocksPickBar-CEFIQ8Nd.js.map
55
+ //# sourceMappingURL=SavedBlocksPickBar-BUvmGqHK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SavedBlocksPickBar-BUvmGqHK.js","names":["_openBlock","_createElementBlock","_createElementVNode","_unref","_toDisplayString","_normalizeClass","_createVNode","_createTextVNode","_sfc_main","openBlock","defineComponent","unref","createVNode","normalizeClass","createElementBlock","createBaseVNode","toDisplayString","createTextVNode","useI18n","Bookmark","X","primaryBtnClass","secondaryBtnClass","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","SavedBlocksPickBar_default","__emit","props","__props","emit","handleConfirm","t","format","_cache"],"sources":["../../../src/components/SavedBlocksPickBar.vue","../../../src/components/SavedBlocksPickBar.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Floating bar for an active pick session: how many blocks are picked, plus\n * Save and Cancel.\n *\n * Positioned like `EditorFooter` — absolute inside `.tpl`, NOT inside the\n * canvas body, whose `overflow: auto` would clip it. The rail offsets are the\n * non-preview ones because a session can't be active in preview mode\n * (`useSavedBlocksFeature` cancels it if preview turns on).\n */\nimport { useI18n } from \"../composables\";\nimport {\n primaryBtnClass,\n secondaryBtnClass,\n} from \"../constants/styleConstants\";\nimport { Bookmark, X } from \"@lucide/vue\";\n\nconst props = defineProps<{\n count: number;\n}>();\n\nconst emit = defineEmits<{\n (e: \"confirm\"): void;\n (e: \"cancel\"): void;\n}>();\n\nconst { t, format } = useI18n();\n\nfunction handleConfirm(): void {\n if (props.count > 0) emit(\"confirm\");\n}\n</script>\n\n<template>\n <div\n data-testid=\"saved-blocks-pick-bar\"\n class=\"tpl:absolute tpl:bottom-10 tpl:left-12 tpl:right-[320px] tpl:z-50 tpl:flex tpl:justify-center tpl:px-4\"\n style=\"pointer-events: none\"\n >\n <div\n role=\"toolbar\"\n :aria-label=\"t.savedBlocks.pickToolbar\"\n class=\"tpl-scale-in tpl:flex tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-lg)] tpl:border tpl:py-2 tpl:pl-4 tpl:pr-2 tpl:border-[var(--tpl-border)]\"\n style=\"\n pointer-events: auto;\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <div class=\"tpl:flex tpl:flex-col\">\n <span\n data-testid=\"saved-blocks-pick-count\"\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >\n {{ format(t.savedBlocks.pickCount, { count }) }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ t.savedBlocks.pickHint }}\n </span>\n </div>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <button\n type=\"button\"\n data-testid=\"saved-blocks-pick-cancel\"\n :class=\"secondaryBtnClass\"\n @click=\"emit('cancel')\"\n >\n <X :size=\"13\" :stroke-width=\"2\" />\n {{ t.savedBlocks.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"saved-blocks-pick-confirm\"\n :class=\"primaryBtnClass\"\n :disabled=\"count === 0\"\n @click=\"handleConfirm\"\n >\n <Bookmark :size=\"13\" :stroke-width=\"2\" />\n {{ t.savedBlocks.save }}\n </button>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Floating bar for an active pick session: how many blocks are picked, plus\n * Save and Cancel.\n *\n * Positioned like `EditorFooter` — absolute inside `.tpl`, NOT inside the\n * canvas body, whose `overflow: auto` would clip it. The rail offsets are the\n * non-preview ones because a session can't be active in preview mode\n * (`useSavedBlocksFeature` cancels it if preview turns on).\n */\nimport { useI18n } from \"../composables\";\nimport {\n primaryBtnClass,\n secondaryBtnClass,\n} from \"../constants/styleConstants\";\nimport { Bookmark, X } from \"@lucide/vue\";\n\nconst props = defineProps<{\n count: number;\n}>();\n\nconst emit = defineEmits<{\n (e: \"confirm\"): void;\n (e: \"cancel\"): void;\n}>();\n\nconst { t, format } = useI18n();\n\nfunction handleConfirm(): void {\n if (props.count > 0) emit(\"confirm\");\n}\n</script>\n\n<template>\n <div\n data-testid=\"saved-blocks-pick-bar\"\n class=\"tpl:absolute tpl:bottom-10 tpl:left-12 tpl:right-[320px] tpl:z-50 tpl:flex tpl:justify-center tpl:px-4\"\n style=\"pointer-events: none\"\n >\n <div\n role=\"toolbar\"\n :aria-label=\"t.savedBlocks.pickToolbar\"\n class=\"tpl-scale-in tpl:flex tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-lg)] tpl:border tpl:py-2 tpl:pl-4 tpl:pr-2 tpl:border-[var(--tpl-border)]\"\n style=\"\n pointer-events: auto;\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <div class=\"tpl:flex tpl:flex-col\">\n <span\n data-testid=\"saved-blocks-pick-count\"\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >\n {{ format(t.savedBlocks.pickCount, { count }) }}\n </span>\n <span class=\"tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]\">\n {{ t.savedBlocks.pickHint }}\n </span>\n </div>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <button\n type=\"button\"\n data-testid=\"saved-blocks-pick-cancel\"\n :class=\"secondaryBtnClass\"\n @click=\"emit('cancel')\"\n >\n <X :size=\"13\" :stroke-width=\"2\" />\n {{ t.savedBlocks.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"saved-blocks-pick-confirm\"\n :class=\"primaryBtnClass\"\n :disabled=\"count === 0\"\n @click=\"handleConfirm\"\n >\n <Bookmark :size=\"13\" :stroke-width=\"2\" />\n {{ t.savedBlocks.save }}\n </button>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;EAiBA,IAAMgC,IAAQC,GAIRC,IAAOH,GAKP,EAAE,MAAG,cAAWb,EAAQ;EAE9B,SAASiB,IAAsB;GAC7B,AAAIH,EAAM,QAAQ,KAAGE,EAAK,SAAS;EACrC;oBAIElC,EAAA,GAAAC,EAiDM,OAjDNsB,GAiDM,CA5CJrB,EA2CM,OAAA;GA1CJ,MAAK;GACJ,cAAYC,EAAAiC,CAAA,CAAC,CAAC,YAAY;GAC3B,OAAM;GACN,OAAA;IAAA,kBAAA;IAAA,oBAAA;IAAA,cAAA;GAAA;EAMAlC,GAAAA,CAAAA,EAUM,OAVNuB,GAUM,CATJvB,EAKO,QALPwB,GAKOtB,EADFD,EAAAkC,CAAA,CAAM,CAAClC,EAAAiC,CAAA,CAAC,CAAC,YAAY,WAAS,EAAA,OAAIH,EAAA,MAAK,CAAA,CAAA,GAAA,CAAA,GAE5C/B,EAEO,QAFPyB,GAEOvB,EADFD,EAAAiC,CAAA,CAAC,CAAC,YAAY,QAAQ,GAAA,CAAA,CAAA,CAAA,GAI7BlC,EAoBM,OApBN0B,GAoBM,CAnBJ1B,EAQS,UAAA;GAPP,MAAK;GACL,eAAY;GACX,OAAKG,EAAEF,EAAAmB,CAAA,CAAiB;GACxB,SAAK,AAAAgB,EAAA,QAAA,MAAEJ,EAAI,QAAA;EAEZ5B,GAAAA,CAAAA,EAAkCH,EAAAiB,CAAA,GAAA;GAA9B,MAAM;GAAK,gBAAc;EAAKb,CAAAA,GAAAA,EAAA,MAClCH,EAAGD,EAAAiC,CAAA,CAAC,CAAC,YAAY,MAAM,GAAA,CAAA,CAAA,GAAA,CAAA,GAEzBlC,EASS,UAAA;GARP,MAAK;GACL,eAAY;GACX,OAAKG,EAAEF,EAAAkB,CAAA,CAAe;GACtB,UAAUY,EAAA,UAAK;GACf,SAAOE;EAER7B,GAAAA,CAAAA,EAAyCH,EAAAgB,CAAA,GAAA;GAA9B,MAAM;GAAK,gBAAc;EAAKZ,CAAAA,GAAAA,EAAA,MACzCH,EAAGD,EAAAiC,CAAA,CAAC,CAAC,YAAY,IAAI,GAAA,CAAA,CAAA,GAAA,IAAAP,CAAA,CAAA,CAAA,CAAA,GAAA,GAAAL,CAAA,CAAA,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import { B as e, E as t, F as n, H as r, N as i, Q as ee, R as a, S as o, at as s, b as c, ct as l, g as u, h as d, lt as f, m as p, n as te, p as m, u as h, ut as g, y as _ } from "./draggable-IB4x6w2O.js";
2
- import { $ as v, F as y, O as ne, W as re, g as b } from "./keys-DCWeHkSF.js";
3
- import { E as x, It as S, Mt as C, O as w, dt as ie, it as ae, k as T, n as oe, t as se, u as ce, wt as le, z as E } from "./icons-BKkJ-5xF.js";
2
+ import { $ as v, F as y, O as b, W as ne, g as x } from "./keys-DCWeHkSF.js";
3
+ import { A as S, B as C, D as w, Lt as T, Nt as E, Tt as re, at as ie, d as ae, ft as oe, k as se, n as ce, t as le } from "./icons-CQnjugjy.js";
4
4
  import { t as D } from "./LoadingTrack-_7DH_5aw.js";
5
5
  import { t as O } from "./_plugin-vue_export-helper-B3ysoDQm.js";
6
6
  import { n as k } from "./useCloudI18n-CrjWbpbu.js";
@@ -60,7 +60,7 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
60
60
  props: { visible: { type: Boolean } },
61
61
  emits: ["close"],
62
62
  setup(o, { emit: O }) {
63
- let G = o, Ae = O, { t: K } = k(), q = v(b, "TemplateScoringPanel"), J = v(re, "TemplateScoringPanel"), Y = t(ne, y), X = ee({
63
+ let G = o, Ae = O, { t: K } = k(), q = v(x, "TemplateScoringPanel"), J = v(ne, "TemplateScoringPanel"), Y = t(b, y), X = ee({
64
64
  spam: !0,
65
65
  readability: !0,
66
66
  accessibility: !0,
@@ -75,10 +75,10 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
75
75
  "accessibility",
76
76
  "bestPractices"
77
77
  ], Me = {
78
- spam: w,
79
- readability: le,
80
- accessibility: x,
81
- bestPractices: se
78
+ spam: se,
79
+ readability: re,
80
+ accessibility: w,
81
+ bestPractices: le
82
82
  };
83
83
  function Q() {
84
84
  J.score(q.content.value, Y.value);
@@ -111,7 +111,7 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
111
111
  role: "complementary",
112
112
  "aria-label": s(K).scoring.title,
113
113
  class: "tpl-scoring-panel tpl:absolute tpl:top-14 tpl:right-0 tpl:bottom-0 tpl:flex tpl:w-[360px] tpl:flex-col tpl:border-l tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-elevated)]"
114
- }, [m("div", F, [m("div", I, [c(s(T), {
114
+ }, [m("div", F, [m("div", I, [c(s(S), {
115
115
  size: 13,
116
116
  "stroke-width": 2
117
117
  }), m("span", null, g(s(K).scoring.title), 1)]), m("div", L, [s(J).scoringResult.value && !s(J).isScoring.value ? (i(), u("button", {
@@ -121,7 +121,7 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
121
121
  "aria-label": s(K).scoring.rescore,
122
122
  title: s(K).scoring.rescore,
123
123
  onClick: t[0] ||= (e) => Q()
124
- }, [c(s(E), {
124
+ }, [c(s(C), {
125
125
  size: 14,
126
126
  "stroke-width": 2
127
127
  })], 8, R)) : d("", !0), m("button", {
@@ -129,11 +129,11 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
129
129
  class: "tpl:rounded-md tpl:p-0.5 tpl:transition-colors tpl:text-[var(--tpl-text-muted)]",
130
130
  "aria-label": s(K).scoring.close,
131
131
  onClick: t[1] ||= (e) => Ae("close")
132
- }, [c(s(oe), {
132
+ }, [c(s(ce), {
133
133
  size: 14,
134
134
  "stroke-width": 2
135
135
  })], 8, z)])]), m("div", B, [s(J).isScoring.value ? (i(), u("div", V, [m("p", H, g(s(K).scoring.scoring), 1), c(D, { class: "tpl:w-3/4" })])) : s(J).error.value && !s(J).scoringResult.value ? (i(), u("div", U, [
136
- c(s(C), {
136
+ c(s(E), {
137
137
  size: 32,
138
138
  "stroke-width": 1.5,
139
139
  class: "tpl:text-[var(--tpl-danger)]"
@@ -142,7 +142,7 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
142
142
  m("button", {
143
143
  class: "tpl:mt-2 tpl:inline-flex tpl:items-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text-muted)]",
144
144
  onClick: t[2] ||= (e) => Q()
145
- }, [c(s(E), {
145
+ }, [c(s(C), {
146
146
  size: 12,
147
147
  "stroke-width": 2
148
148
  }), _(" " + g(s(K).scoring.rescore), 1)])
@@ -158,7 +158,7 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
158
158
  m("span", de, g(s(K).scoring.overallScore), 1),
159
159
  $() > 0 ? (i(), u("span", fe, g($()) + " " + g(s(K).scoring.findings), 1)) : d("", !0)
160
160
  ], 4),
161
- s(J).fixError.value ? (i(), u("div", pe, [c(s(C), {
161
+ s(J).fixError.value ? (i(), u("div", pe, [c(s(E), {
162
162
  size: 14,
163
163
  "stroke-width": 2,
164
164
  class: "tpl:mt-0.5 tpl:shrink-0"
@@ -184,7 +184,7 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
184
184
  })
185
185
  }, g(s(J).scoringResult.value.categories[e].score), 5),
186
186
  s(J).scoringResult.value.categories[e].findings.length > 0 ? (i(), u("span", ge, g(s(J).scoringResult.value.categories[e].findings.length), 1)) : d("", !0),
187
- c(s(S), {
187
+ c(s(T), {
188
188
  size: 12,
189
189
  "stroke-width": 2,
190
190
  class: l(["tpl:transition-transform tpl:duration-200 tpl:text-[var(--tpl-text-dim)]", X.value[e] ? "tpl:rotate-0" : "tpl:-rotate-90"])
@@ -192,7 +192,7 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
192
192
  ], 8, me), X.value[e] ? (i(), u("div", _e, [s(J).scoringResult.value.categories[e].findings.length === 0 ? (i(), u("div", ve, g(s(K).scoring.noFindings), 1)) : d("", !0), (i(!0), u(h, null, n(s(J).scoringResult.value.categories[e].findings, (e) => (i(), u("div", {
193
193
  key: e.id,
194
194
  class: "tpl:border-t tpl:px-3 tpl:py-2.5 tpl:first:border-t-0 tpl:border-[var(--tpl-border-light)]"
195
- }, [m("div", ye, [(i(), p(a(s(e.severity === "high" ? C : e.severity === "medium" ? ce : ie)), {
195
+ }, [m("div", ye, [(i(), p(a(s(e.severity === "high" ? E : e.severity === "medium" ? ae : oe)), {
196
196
  size: 13,
197
197
  "stroke-width": 2,
198
198
  class: "tpl:mt-0.5 tpl:shrink-0",
@@ -211,18 +211,18 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
211
211
  style: { "background-color": "transparent" },
212
212
  disabled: s(J).fixingFindingId.value !== null,
213
213
  onClick: (t) => Ne(e)
214
- }, [s(J).fixingFindingId.value === e.id ? (i(), p(s(ae), {
214
+ }, [s(J).fixingFindingId.value === e.id ? (i(), p(s(ie), {
215
215
  key: 0,
216
216
  class: "tpl-spinner",
217
217
  size: 11,
218
218
  "stroke-width": 2
219
- })) : (i(), p(s(x), {
219
+ })) : (i(), p(s(w), {
220
220
  key: 1,
221
221
  size: 11,
222
222
  "stroke-width": 2
223
223
  })), _(" " + g(s(J).fixingFindingId.value === e.id ? s(K).scoring.fixing : s(K).scoring.fix), 1)], 8, Te), s(J).fixError.value ? (i(), u("p", Ee, g(s(J).fixError.value), 1)) : d("", !0)])) : d("", !0)
224
224
  ])])]))), 128))])) : d("", !0)])), 64))
225
- ])) : (i(), u("div", De, [c(s(T), {
225
+ ])) : (i(), u("div", De, [c(s(S), {
226
226
  size: 32,
227
227
  "stroke-width": 1.5,
228
228
  class: "tpl:text-[var(--tpl-text-dim)]"
@@ -234,4 +234,4 @@ var P = ["aria-label"], F = { class: "tpl:flex tpl:items-center tpl:justify-betw
234
234
  //#endregion
235
235
  export { G as default };
236
236
 
237
- //# sourceMappingURL=TemplateScoringPanel-d_Ppowsq.js.map
237
+ //# sourceMappingURL=TemplateScoringPanel-ebsrVge3.js.map