@templatical/editor 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/{AiChatSidebar-BYMdVldY.js → AiChatSidebar-c-3yyjN1.js} +14 -14
  2. package/dist/{AiFeatureMenu-BhHhnu9G.js → AiFeatureMenu-B-COyG7k.js} +7 -7
  3. package/dist/{BlockIssueBadge-De36PJC4.js → BlockIssueBadge-Buk3_6QF.js} +5 -5
  4. package/dist/{BlockPreviewCanvas-BxSoA1QV.js → BlockPreviewCanvas-CJPHGI93.js} +3 -3
  5. package/dist/{CloudEditor-BXfuytQj.js → CloudEditor-BdSQ8hfv.js} +26 -26
  6. package/dist/{CollaboratorBar-D9AfWRTH.js → CollaboratorBar-mGoi6yye.js} +4 -4
  7. package/dist/{ColorPicker-VPq4ASC8.js → ColorPicker-ClETsjA3.js} +6 -6
  8. package/dist/{CommentsSidebar-C8BLy3Pr.js → CommentsSidebar-CdRCCSDD.js} +15 -15
  9. package/dist/{CountdownBlock-Bs2Q_bFX.js → CountdownBlock-DuyvGcr8.js} +3 -3
  10. package/dist/{CountdownToolbar-Cic6cdOw.js → CountdownToolbar-CBE9wRaV.js} +4 -4
  11. package/dist/{DesignReferenceSidebar-k-26nkJI.js → DesignReferenceSidebar-CgYkySre.js} +11 -11
  12. package/dist/{IssuesPanel-NPzwTPYW.js → IssuesPanel-Do7RLXsP.js} +8 -8
  13. package/dist/{LoadingTrack-DH3OEM3z.js → LoadingTrack-BexpNuTV.js} +1 -1
  14. package/dist/{MergeTagModeToggle-DEGyx8tw.js → MergeTagModeToggle-hAJgIKhw.js} +6 -6
  15. package/dist/{MergeTagSuggestion-DvisJ9im.js → MergeTagSuggestion-BJ43ihWR.js} +34 -32
  16. package/dist/{NumberWithSuffix-CuHCIwSn.js → NumberWithSuffix-D37ZWiZh.js} +2 -2
  17. package/dist/{ParagraphEditor-DK_IPi9H.js → ParagraphEditor-DBG63S70.js} +74 -80
  18. package/dist/{RichTextEditorContent-DznIQcdY.js → RichTextEditorContent-fbYJXAio.js} +8 -8
  19. package/dist/{SaveBlockDialog-R9PbMsy-.js → SaveBlockDialog-Dq9UVihi.js} +10 -10
  20. package/dist/{SavedBlocksBrowserModal-ttPAklPE.js → SavedBlocksBrowserModal-CtFG4zeV.js} +11 -11
  21. package/dist/{SavedBlocksPanels-BCwQgKLF.js → SavedBlocksPanels-DNVdso_W.js} +2 -2
  22. package/dist/{SavedBlocksPickBar-DD5Ddv9t.js → SavedBlocksPickBar-Cv4dIpP-.js} +4 -4
  23. package/dist/{SnapshotHistory-BXSTGgBh.js → SnapshotHistory-DsOtMHOV.js} +10 -10
  24. package/dist/{TemplateScoringPanel-Ue6Dfnjb.js → TemplateScoringPanel-BZUsb3FD.js} +15 -15
  25. package/dist/{TestEmailModal-KPFc5oSv.js → TestEmailModal-CIwqtGfr.js} +11 -11
  26. package/dist/{TestEmailPanel-b8GHTrnE.js → TestEmailPanel-l6LDmBGM.js} +2 -2
  27. package/dist/{TitleEditor-8ln7UBr_.js → TitleEditor-imoBUynh.js} +12 -12
  28. package/dist/{TplModal-D_FA3Wm3.js → TplModal-DDDHcoir.js} +5 -5
  29. package/dist/{blockTypeIcons-BoZeK6l7.js → blockTypeIcons-CfLN4A6t.js} +2 -2
  30. package/dist/bundle-stats.json +6 -6
  31. package/dist/cdn/chunks/{AiChatSidebar-D6jGwb4y.js → AiChatSidebar-BHqNxNcX.js} +35 -35
  32. package/dist/cdn/chunks/{AiChatSidebar-D6jGwb4y.js.map → AiChatSidebar-BHqNxNcX.js.map} +1 -1
  33. package/dist/cdn/chunks/{AiFeatureMenu-BtEcjxLL.js → AiFeatureMenu-D2AIDwEG.js} +13 -13
  34. package/dist/cdn/chunks/{AiFeatureMenu-BtEcjxLL.js.map → AiFeatureMenu-D2AIDwEG.js.map} +1 -1
  35. package/dist/cdn/chunks/{BlockIssueBadge-dnqZk6pl.js → BlockIssueBadge-BEEY9o2o.js} +5 -5
  36. package/dist/cdn/chunks/{BlockIssueBadge-dnqZk6pl.js.map → BlockIssueBadge-BEEY9o2o.js.map} +1 -1
  37. package/dist/cdn/chunks/{BlockPreviewCanvas-D8pfTFWi.js → BlockPreviewCanvas-DH5ULGkw.js} +22 -22
  38. package/dist/cdn/chunks/{BlockPreviewCanvas-D8pfTFWi.js.map → BlockPreviewCanvas-DH5ULGkw.js.map} +1 -1
  39. package/dist/cdn/chunks/{CloudEditor-BJ4-pPAl.js → CloudEditor-C0nB1JfD.js} +67 -67
  40. package/dist/cdn/chunks/{CloudEditor-BJ4-pPAl.js.map → CloudEditor-C0nB1JfD.js.map} +1 -1
  41. package/dist/cdn/chunks/{CollaboratorBar-SeUOuLeJ.js → CollaboratorBar-f6Ni7KNH.js} +12 -12
  42. package/dist/cdn/chunks/{CollaboratorBar-SeUOuLeJ.js.map → CollaboratorBar-f6Ni7KNH.js.map} +1 -1
  43. package/dist/cdn/chunks/{ColorPicker-yxvrro60.js → ColorPicker-W_UyiVGc.js} +131 -131
  44. package/dist/cdn/chunks/{ColorPicker-yxvrro60.js.map → ColorPicker-W_UyiVGc.js.map} +1 -1
  45. package/dist/cdn/chunks/{CommentsSidebar-FPbg-FuU.js → CommentsSidebar-BLRZRSSC.js} +112 -112
  46. package/dist/cdn/chunks/{CommentsSidebar-FPbg-FuU.js.map → CommentsSidebar-BLRZRSSC.js.map} +1 -1
  47. package/dist/cdn/chunks/{CountdownBlock-D2Oth7YY.js → CountdownBlock-vLTkTI9e.js} +11 -11
  48. package/dist/cdn/chunks/{CountdownBlock-D2Oth7YY.js.map → CountdownBlock-vLTkTI9e.js.map} +1 -1
  49. package/dist/cdn/chunks/{CountdownToolbar-BNgTBQGB.js → CountdownToolbar-DPjcgcVR.js} +41 -41
  50. package/dist/cdn/chunks/{CountdownToolbar-BNgTBQGB.js.map → CountdownToolbar-DPjcgcVR.js.map} +1 -1
  51. package/dist/cdn/chunks/{DesignReferenceSidebar-ChB_HBK1.js → DesignReferenceSidebar-DBoEBKxQ.js} +34 -34
  52. package/dist/cdn/chunks/{DesignReferenceSidebar-ChB_HBK1.js.map → DesignReferenceSidebar-DBoEBKxQ.js.map} +1 -1
  53. package/dist/cdn/chunks/{IssuesPanel-CjiZ8cLW.js → IssuesPanel-DKTS4QVb.js} +32 -32
  54. package/dist/cdn/chunks/{IssuesPanel-CjiZ8cLW.js.map → IssuesPanel-DKTS4QVb.js.map} +1 -1
  55. package/dist/cdn/chunks/{LoadingTrack-CiNg9s16.js → LoadingTrack-C52ZGL5q.js} +2 -2
  56. package/dist/cdn/chunks/{LoadingTrack-CiNg9s16.js.map → LoadingTrack-C52ZGL5q.js.map} +1 -1
  57. package/dist/cdn/chunks/{MergeTagModeToggle-CiYbOKzd.js → MergeTagModeToggle-D8iKvz7v.js} +28 -28
  58. package/dist/cdn/chunks/{MergeTagModeToggle-CiYbOKzd.js.map → MergeTagModeToggle-D8iKvz7v.js.map} +1 -1
  59. package/dist/cdn/chunks/{MergeTagSuggestion-CvZLI6Np.js → MergeTagSuggestion-Dn8dExNa.js} +18 -18
  60. package/dist/cdn/chunks/{MergeTagSuggestion-CvZLI6Np.js.map → MergeTagSuggestion-Dn8dExNa.js.map} +1 -1
  61. package/dist/cdn/chunks/{NumberWithSuffix-vVwCWrd_.js → NumberWithSuffix-BXrRdfq1.js} +22 -22
  62. package/dist/cdn/chunks/{NumberWithSuffix-vVwCWrd_.js.map → NumberWithSuffix-BXrRdfq1.js.map} +1 -1
  63. package/dist/cdn/chunks/{ParagraphEditor-o4hs9mgy.js → ParagraphEditor-Cuc83Blj.js} +58 -58
  64. package/dist/cdn/chunks/{ParagraphEditor-o4hs9mgy.js.map → ParagraphEditor-Cuc83Blj.js.map} +1 -1
  65. package/dist/cdn/chunks/{RichTextEditorContent-BAriGldd.js → RichTextEditorContent-DtL49Jpo.js} +20 -20
  66. package/dist/cdn/chunks/{RichTextEditorContent-BAriGldd.js.map → RichTextEditorContent-DtL49Jpo.js.map} +1 -1
  67. package/dist/cdn/chunks/{SaveBlockDialog-D3-oILb5.js → SaveBlockDialog-B8KHhVkI.js} +50 -50
  68. package/dist/cdn/chunks/{SaveBlockDialog-D3-oILb5.js.map → SaveBlockDialog-B8KHhVkI.js.map} +1 -1
  69. package/dist/cdn/chunks/{SavedBlocksBrowserModal-jMnNd4wn.js → SavedBlocksBrowserModal-B-sjsIJv.js} +62 -62
  70. package/dist/cdn/chunks/{SavedBlocksBrowserModal-jMnNd4wn.js.map → SavedBlocksBrowserModal-B-sjsIJv.js.map} +1 -1
  71. package/dist/cdn/chunks/{SavedBlocksPanels-BDd8FH6l.js → SavedBlocksPanels-Bb-eDMAh.js} +3 -3
  72. package/dist/cdn/chunks/{SavedBlocksPanels-BDd8FH6l.js.map → SavedBlocksPanels-Bb-eDMAh.js.map} +1 -1
  73. package/dist/cdn/chunks/{SavedBlocksPickBar-BNkLe8In.js → SavedBlocksPickBar-GudFfXa4.js} +12 -12
  74. package/dist/cdn/chunks/{SavedBlocksPickBar-BNkLe8In.js.map → SavedBlocksPickBar-GudFfXa4.js.map} +1 -1
  75. package/dist/cdn/chunks/{SnapshotHistory-CYVi7fvQ.js → SnapshotHistory-Srpzt2na.js} +26 -26
  76. package/dist/cdn/chunks/{SnapshotHistory-CYVi7fvQ.js.map → SnapshotHistory-Srpzt2na.js.map} +1 -1
  77. package/dist/cdn/chunks/{TemplateScoringPanel-D0KrxVnr.js → TemplateScoringPanel-NucRtAP0.js} +42 -42
  78. package/dist/cdn/chunks/{TemplateScoringPanel-D0KrxVnr.js.map → TemplateScoringPanel-NucRtAP0.js.map} +1 -1
  79. package/dist/cdn/chunks/{TestEmailModal-_RbTD787.js → TestEmailModal-DLeoJqaJ.js} +31 -31
  80. package/dist/cdn/chunks/{TestEmailModal-_RbTD787.js.map → TestEmailModal-DLeoJqaJ.js.map} +1 -1
  81. package/dist/cdn/chunks/{TestEmailPanel-BAnKtbSu.js → TestEmailPanel-Dr7Dkq2P.js} +3 -3
  82. package/dist/cdn/chunks/{TestEmailPanel-BAnKtbSu.js.map → TestEmailPanel-Dr7Dkq2P.js.map} +1 -1
  83. package/dist/cdn/chunks/{TitleEditor-LiUvXGPw.js → TitleEditor-DzYRr18C.js} +35 -35
  84. package/dist/cdn/chunks/{TitleEditor-LiUvXGPw.js.map → TitleEditor-DzYRr18C.js.map} +1 -1
  85. package/dist/cdn/chunks/{TplModal-C1iguuZz.js → TplModal-byw7-3wg.js} +17 -17
  86. package/dist/cdn/chunks/{TplModal-C1iguuZz.js.map → TplModal-byw7-3wg.js.map} +1 -1
  87. package/dist/cdn/chunks/blockTypeIcons-FXlMM3PL.js +21 -0
  88. package/dist/cdn/chunks/{blockTypeIcons-CrPzyP_k.js.map → blockTypeIcons-FXlMM3PL.js.map} +1 -1
  89. package/dist/cdn/chunks/{cloud-CxNsW3hp.js → cloud-CuItCnN_.js} +4 -4
  90. package/dist/cdn/chunks/{cloud-CxNsW3hp.js.map → cloud-CuItCnN_.js.map} +1 -1
  91. package/dist/cdn/chunks/{dist-BLF-S9_A.js → dist-B2HKGHnc.js} +9 -7
  92. package/dist/cdn/chunks/{dist-BLF-S9_A.js.map → dist-B2HKGHnc.js.map} +1 -1
  93. package/dist/cdn/chunks/{dist-Dp46rwVY.js → dist-C6z8AjBC.js} +2 -2
  94. package/dist/cdn/chunks/{dist-Dp46rwVY.js.map → dist-C6z8AjBC.js.map} +1 -1
  95. package/dist/cdn/chunks/{draggable-BRF_Q_jB.js → draggable-DCs9qL-b.js} +114 -92
  96. package/dist/cdn/chunks/{draggable-BRF_Q_jB.js.map → draggable-DCs9qL-b.js.map} +1 -1
  97. package/dist/cdn/chunks/{extensions-yNQlgLNE.js → extensions-B7laxHjs.js} +22 -22
  98. package/dist/cdn/chunks/{extensions-yNQlgLNE.js.map → extensions-B7laxHjs.js.map} +1 -1
  99. package/dist/cdn/chunks/{htmlparser-CPhs2cYE.js → htmlparser-DTML8Gvz.js} +8 -7
  100. package/dist/cdn/chunks/{htmlparser-CPhs2cYE.js.map → htmlparser-DTML8Gvz.js.map} +1 -1
  101. package/dist/cdn/chunks/{icons-DN008liP.js → icons-CvYN6zrM.js} +74 -80
  102. package/dist/cdn/chunks/icons-CvYN6zrM.js.map +1 -0
  103. package/dist/cdn/chunks/{keys-CZOBuCQd.js → keys-Cmc2Yl3D.js} +2 -2
  104. package/dist/cdn/chunks/{keys-CZOBuCQd.js.map → keys-Cmc2Yl3D.js.map} +1 -1
  105. package/dist/cdn/chunks/{liquid.browser-CZaQe87t.js → liquid.browser-UlAOKBjI.js} +120 -110
  106. package/dist/cdn/chunks/liquid.browser-UlAOKBjI.js.map +1 -0
  107. package/dist/cdn/chunks/{pusher-kUlkqMvR.js → pusher-BbIHJKf3.js} +29 -25
  108. package/dist/cdn/chunks/{pusher-kUlkqMvR.js.map → pusher-BbIHJKf3.js.map} +1 -1
  109. package/dist/cdn/chunks/{src-uc_Mb_yH.js → src-BYKYmEOD.js} +7 -6
  110. package/dist/cdn/chunks/src-BYKYmEOD.js.map +1 -0
  111. package/dist/cdn/chunks/{src-D_3bXt7-.js → src-Bi-Qhtfs.js} +3 -3
  112. package/dist/cdn/chunks/{src-D_3bXt7-.js.map → src-Bi-Qhtfs.js.map} +1 -1
  113. package/dist/cdn/chunks/{src-B2LpXMIw.js → src-C0N74uvF.js} +280 -277
  114. package/dist/cdn/chunks/{src-B2LpXMIw.js.map → src-C0N74uvF.js.map} +1 -1
  115. package/dist/cdn/chunks/{src-CZjSXPYq.js → src-EKTTBQ0y.js} +3 -2
  116. package/dist/cdn/chunks/src-EKTTBQ0y.js.map +1 -0
  117. package/dist/cdn/chunks/{styles-BzP13r-I.js → styles-C7s-VtJi.js} +1112 -1094
  118. package/dist/cdn/chunks/styles-C7s-VtJi.js.map +1 -0
  119. package/dist/cdn/chunks/{tiptap-CgwK_fKJ.js → tiptap-C44S1Cmh.js} +3681 -2965
  120. package/dist/cdn/chunks/tiptap-C44S1Cmh.js.map +1 -0
  121. package/dist/cdn/chunks/{useAliveFlag-Ctyt3GgH.js → useAliveFlag-Dxwgo6P4.js} +2 -2
  122. package/dist/cdn/chunks/{useAliveFlag-Ctyt3GgH.js.map → useAliveFlag-Dxwgo6P4.js.map} +1 -1
  123. package/dist/cdn/chunks/{useCloudI18n-CML0BxqX.js → useCloudI18n-8wkJtuJD.js} +3 -3
  124. package/dist/cdn/chunks/{useCloudI18n-CML0BxqX.js.map → useCloudI18n-8wkJtuJD.js.map} +1 -1
  125. package/dist/cdn/chunks/{useEditorCore-CTYH6u4r.js → useEditorCore-C_nUXiph.js} +275 -275
  126. package/dist/cdn/chunks/useEditorCore-C_nUXiph.js.map +1 -0
  127. package/dist/cdn/chunks/{useI18n-aRMtgYRj.js → useI18n-UMs0F71x.js} +3 -3
  128. package/dist/cdn/chunks/{useI18n-aRMtgYRj.js.map → useI18n-UMs0F71x.js.map} +1 -1
  129. package/dist/cdn/chunks/{useMergeTag-8a8BnIZp.js → useMergeTag-CPa_9VOq.js} +4 -4
  130. package/dist/cdn/chunks/{useMergeTag-8a8BnIZp.js.map → useMergeTag-CPa_9VOq.js.map} +1 -1
  131. package/dist/cdn/chunks/{usePopoverPosition-Dm1jv3y5.js → usePopoverPosition-cf8Y-ynK.js} +3 -3
  132. package/dist/cdn/chunks/{usePopoverPosition-Dm1jv3y5.js.map → usePopoverPosition-cf8Y-ynK.js.map} +1 -1
  133. package/dist/cdn/editor.js +47 -47
  134. package/dist/cdn/editor.js.map +1 -1
  135. package/dist/{check-Bqimqf7l.js → check-On4TSac-.js} +1 -1
  136. package/dist/{chevron-down-CYOoeGBd.js → chevron-down-69EiBGoB.js} +1 -1
  137. package/dist/{chevron-up-DYknsfm0.js → chevron-up-Dh3MG1Dw.js} +1 -1
  138. package/dist/{circle-alert-6q_jgtv_.js → circle-alert-cilwzafX.js} +1 -1
  139. package/dist/{clock-CnadSSTD.js → clock-Dt0APH92.js} +1 -1
  140. package/dist/{cloud-BJLg_SlM.js → cloud-C7-FhzMc.js} +2 -2
  141. package/dist/{createLucideIcon-D7GKhya2.js → createLucideIcon-C8WzBYCv.js} +3 -3
  142. package/dist/dist-6YJAWMuf.js +5 -0
  143. package/dist/dist-8vlm9kfO.js +5 -0
  144. package/dist/{dist-DSIgxp0v.js → dist-BImfm-46.js} +2 -2
  145. package/dist/{dist-DDfXShPv.js → dist-BpKxrUi2.js} +1 -1
  146. package/dist/{dist-B6Px6GPj.js → dist-C08jiemd.js} +7 -7
  147. package/dist/{dist-BxbTVeY0.js → dist-C27SeFL5.js} +15 -15
  148. package/dist/{dist-nR6mxyqI.js → dist-CYx-E82k.js} +2 -2
  149. package/dist/{dist-CcQFPJMF.js → dist-ChYLhoJ4.js} +2300 -1704
  150. package/dist/{dist-C31bMR4x.js → dist-Cl8J0IES.js} +270 -182
  151. package/dist/{dist-CQ0fVBQ3.js → dist-D5zCZFdU.js} +2 -1
  152. package/dist/{dist-Dx71pluS.js → dist-DMJRSouD.js} +470 -440
  153. package/dist/{dist-DyCHUyRR.js → dist-DfpeSdmE.js} +2 -2
  154. package/dist/{dist-DX4x6mQ1.js → dist-M7m1lYRe.js} +47 -47
  155. package/dist/{dist-CRH880TV.js → dist-g6cbzhZW.js} +2 -2
  156. package/dist/{extensions-CbiWtzS6.js → extensions-CxwhoG8v.js} +19 -19
  157. package/dist/{image-up-BfukboIn.js → image-up-Ceggh_R3.js} +1 -1
  158. package/dist/{info-DSYRzSZo.js → info-DO5odBcZ.js} +1 -1
  159. package/dist/{keys-BI6VSUh4.js → keys-fwF3qZax.js} +1 -1
  160. package/dist/{liquid.browser-BECWFL5Z.js → liquid.browser-B-rGrnrQ.js} +119 -109
  161. package/dist/{list-checks-URZOyLhr.js → list-checks-BSkPKbuI.js} +1 -1
  162. package/dist/{loader-circle-GADaYcyQ.js → loader-circle-bO7o7_HD.js} +1 -1
  163. package/dist/{message-circle-B62fdo0b.js → message-circle-BEkAk65X.js} +1 -1
  164. package/dist/{pencil-BGwkOpD_.js → pencil-t6m0EtKL.js} +1 -1
  165. package/dist/{refresh-cw-Z4wXfduO.js → refresh-cw-CkOYVzK5.js} +1 -1
  166. package/dist/{runtime-dom.esm-bundler-BkOsapVg.js → runtime-dom.esm-bundler-CepEmvwF.js} +13 -10
  167. package/dist/{scan-line-D8Oh5xos.js → scan-line-DwSVj25O.js} +1 -1
  168. package/dist/{send-BYp4k38T.js → send-CWsWN8PQ.js} +1 -1
  169. package/dist/{shield-check-Cm0RePnW.js → shield-check-qje8Kufw.js} +1 -1
  170. package/dist/{sparkles-BHEE-vRD.js → sparkles-DumaWRW-.js} +1 -1
  171. package/dist/{styles-B0MEBpOK.js → styles-C_slTGwp.js} +454 -436
  172. package/dist/templatical-editor.js +11 -11
  173. package/dist/{text-align-start-CHrg3vK8.js → text-align-start-Ir8Ah4mT.js} +1 -1
  174. package/dist/{trash-2-BoNT8wbq.js → trash-2-CklAFCmO.js} +1 -1
  175. package/dist/{triangle-alert-CiN0ssB3.js → triangle-alert-BBi4n93g.js} +1 -1
  176. package/dist/{upload-WvZGBySw.js → upload-sVqqppuv.js} +1 -1
  177. package/dist/{useAliveFlag-eT67QCMf.js → useAliveFlag-cvyBxHp7.js} +1 -1
  178. package/dist/{useCloudI18n-KgIWl-IE.js → useCloudI18n-DKVm6PCa.js} +2 -2
  179. package/dist/{useEditorCore-BMbxdUbY.js → useEditorCore-r4gWzCBA.js} +893 -893
  180. package/dist/{useI18n-BkHfCWC6.js → useI18n-DoVKdCTH.js} +2 -2
  181. package/dist/{useMergeTag-CZ4XYAxu.js → useMergeTag-C-_WxrgL.js} +3 -3
  182. package/dist/{usePopoverPosition-D93u-EZm.js → usePopoverPosition-C0dPEF4X.js} +3 -3
  183. package/dist/{vue.runtime.esm-bundler-0R1UhmZB.js → vue.runtime.esm-bundler-D1diBMsm.js} +119 -95
  184. package/dist/{x-B4WnJVRx.js → x-Mby7KlhS.js} +1 -1
  185. package/package.json +23 -23
  186. package/dist/cdn/chunks/blockTypeIcons-CrPzyP_k.js +0 -21
  187. package/dist/cdn/chunks/icons-DN008liP.js.map +0 -1
  188. package/dist/cdn/chunks/liquid.browser-CZaQe87t.js.map +0 -1
  189. package/dist/cdn/chunks/src-CZjSXPYq.js.map +0 -1
  190. package/dist/cdn/chunks/src-uc_Mb_yH.js.map +0 -1
  191. package/dist/cdn/chunks/styles-BzP13r-I.js.map +0 -1
  192. package/dist/cdn/chunks/tiptap-CgwK_fKJ.js.map +0 -1
  193. package/dist/cdn/chunks/useEditorCore-CTYH6u4r.js.map +0 -1
  194. package/dist/dist-CkPvZCld.js +0 -5
  195. package/dist/dist-iCEDH3cC.js +0 -5
@@ -1,10 +1,10 @@
1
- import { H as e, L as t, M as n, O as r, P as i, V as ee, Z as a, a as o, b as te, c as s, ct as c, f as l, g as u, h as d, it as f, l as p, m, o as h, ot as ne, p as g, st as re, u as _, x as v, y, z as ie } from "./draggable-BRF_Q_jB.js";
2
- import "./useEditorCore-CTYH6u4r.js";
3
- import { G as b, R as ae, g as oe } from "./keys-CZOBuCQd.js";
4
- import { t as se } from "./useI18n-aRMtgYRj.js";
5
- import { P as ce, X as x, d as le, n as ue, q as de } from "./icons-DN008liP.js";
6
- import { t as S } from "./blockTypeIcons-CrPzyP_k.js";
7
- import { t as fe } from "./TplModal-C1iguuZz.js";
1
+ import { H as e, L as t, M as n, O as r, P as i, V as ee, Z as a, a as o, b as te, c as s, ct as ne, f as c, g as l, h as u, it as d, l as f, lt as p, m, o as h, p as g, st as re, u as _, x as v, y, z as ie } from "./draggable-DCs9qL-b.js";
2
+ import "./useEditorCore-C_nUXiph.js";
3
+ import { G as b, R as ae, g as oe } from "./keys-Cmc2Yl3D.js";
4
+ import { t as se } from "./useI18n-UMs0F71x.js";
5
+ import { K as ce, N as le, Y as x, d as ue, n as de } from "./icons-CvYN6zrM.js";
6
+ import { t as S } from "./blockTypeIcons-FXlMM3PL.js";
7
+ import { t as fe } from "./TplModal-byw7-3wg.js";
8
8
  import { t as pe } from "./formatRelativeTime-CtUU-QZ8.js";
9
9
  //#region src/components/SavedBlocksBrowserModal.vue?vue&type=script&setup=true&lang.ts
10
10
  var me = {
@@ -62,10 +62,10 @@ var me = {
62
62
  props: { visible: { type: Boolean } },
63
63
  emits: ["close", "insert"],
64
64
  setup(v, { emit: A }) {
65
- let et = v, j = A, tt = te(() => import("./BlockPreviewCanvas-D8pfTFWi.js").then((e) => e.n)), { t: M, format: N } = se(), P = b(ae, "SavedBlocksBrowserModal"), F = b(oe, "SavedBlocksBrowserModal"), I = a(""), L = a(""), R = a(null), z = a(null), B = a(null), V = a(""), H = a(""), U = a(null), W = a("end"), G = l(() => {
65
+ let et = v, j = A, tt = te(() => import("./BlockPreviewCanvas-DH5ULGkw.js").then((e) => e.n)), { t: M, format: N } = se(), P = b(ae, "SavedBlocksBrowserModal"), F = b(oe, "SavedBlocksBrowserModal"), I = a(""), L = a(""), R = a(null), z = a(null), B = a(null), V = a(""), H = a(""), U = a(null), W = a("end"), G = c(() => {
66
66
  let e = I.value.trim().toLowerCase(), t = L.value;
67
67
  return P.savedBlocks.value.filter((n) => !(e && !n.name.toLowerCase().includes(e) || t && n.category !== t));
68
- }), K = l(() => P.isLoading.value && P.savedBlocks.value.length === 0), nt = l(() => I.value.trim().length > 0 || L.value !== ""), q = l(() => R.value ? P.savedBlocks.value.find((e) => e.id === R.value) ?? null : null), rt = l(() => {
68
+ }), K = c(() => P.isLoading.value && P.savedBlocks.value.length === 0), nt = c(() => I.value.trim().length > 0 || L.value !== ""), q = c(() => R.value ? P.savedBlocks.value.find((e) => e.id === R.value) ?? null : null), rt = c(() => {
69
69
  let e = [{
70
70
  value: "beginning",
71
71
  label: M.savedBlocks.insertAtBeginning
@@ -81,7 +81,7 @@ var me = {
81
81
  value: "end",
82
82
  label: M.savedBlocks.insertAtEnd
83
83
  }), e;
84
- }), it = l(() => {
84
+ }), it = c(() => {
85
85
  if (W.value === "end") return;
86
86
  if (W.value === "beginning") return 0;
87
87
  let e = F.content.value.blocks.findIndex((e) => e.id === W.value);
@@ -164,51 +164,51 @@ var me = {
164
164
  onKeydown: ut
165
165
  }, {
166
166
  default: ee(() => [g("div", me, [
167
- g("div", he, [g("h3", ge, c(f(M).savedBlocks.browse), 1), g("button", {
168
- "aria-label": f(M).savedBlocks.close,
167
+ g("div", he, [g("h3", ge, p(d(M).savedBlocks.browse), 1), g("button", {
168
+ "aria-label": d(M).savedBlocks.close,
169
169
  class: "tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-transparent tpl:p-1 tpl:transition-colors tpl:duration-100 tpl:text-[var(--tpl-text-dim)]",
170
170
  onClick: $
171
- }, [y(f(ue), {
171
+ }, [y(d(de), {
172
172
  size: 16,
173
173
  "stroke-width": 2
174
174
  })], 8, _e)]),
175
175
  g("div", ve, [g("div", ye, [
176
- g("div", be, [g("div", xe, [y(f(ce), {
176
+ g("div", be, [g("div", xe, [y(d(le), {
177
177
  size: 14,
178
178
  "stroke-width": 2,
179
179
  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)]"
180
180
  }), e(g("input", {
181
181
  "onUpdate:modelValue": a[0] ||= (e) => I.value = e,
182
182
  type: "text",
183
- placeholder: f(M).savedBlocks.search,
183
+ placeholder: d(M).savedBlocks.search,
184
184
  disabled: K.value,
185
185
  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:disabled:opacity-50 tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]"
186
- }, null, 8, Se), [[h, I.value]])]), f(P).categories.value.length > 0 ? e((n(), u("select", {
186
+ }, null, 8, Se), [[h, I.value]])]), d(P).categories.value.length > 0 ? e((n(), l("select", {
187
187
  key: 0,
188
188
  "onUpdate:modelValue": a[1] ||= (e) => L.value = e,
189
189
  "data-testid": "saved-blocks-category-filter",
190
- "aria-label": f(M).savedBlocks.filterByCategory,
190
+ "aria-label": d(M).savedBlocks.filterByCategory,
191
191
  class: "tpl:h-8 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]"
192
- }, [g("option", we, c(f(M).savedBlocks.allCategories), 1), (n(!0), u(_, null, i(f(P).categories.value, (e) => (n(), u("option", {
192
+ }, [g("option", we, p(d(M).savedBlocks.allCategories), 1), (n(!0), l(_, null, i(d(P).categories.value, (e) => (n(), l("option", {
193
193
  key: e,
194
194
  value: e
195
- }, c(e), 9, Te))), 128))], 8, Ce)), [[o, L.value]]) : d("", !0)]),
196
- g("datalist", C, [(n(!0), u(_, null, i(f(P).categories.value, (e) => (n(), u("option", {
195
+ }, p(e), 9, Te))), 128))], 8, Ce)), [[o, L.value]]) : u("", !0)]),
196
+ g("datalist", C, [(n(!0), l(_, null, i(d(P).categories.value, (e) => (n(), l("option", {
197
197
  key: e,
198
198
  value: e
199
199
  }, null, 8, w))), 128))]),
200
- g("div", T, [K.value ? (n(), u("div", {
200
+ g("div", T, [K.value ? (n(), l("div", {
201
201
  key: 0,
202
202
  "data-testid": "saved-blocks-loading",
203
203
  class: "tpl:flex tpl:flex-col tpl:gap-1",
204
204
  role: "status",
205
205
  "aria-busy": "true",
206
- "aria-label": f(M).savedBlocks.loading
207
- }, [(n(), u(_, null, i(3, (e) => g("div", {
206
+ "aria-label": d(M).savedBlocks.loading
207
+ }, [(n(), l(_, null, i(3, (e) => g("div", {
208
208
  key: e,
209
209
  "aria-hidden": "true",
210
210
  class: "tpl-saved-block-skeleton tpl:rounded-[var(--tpl-radius-md)] tpl:border tpl:px-3 tpl:py-2 tpl:border-[var(--tpl-border)]"
211
- }, [...a[7] ||= [g("div", { class: "tpl:h-3 tpl:w-1/2 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]" }, null, -1), g("div", { class: "tpl:mt-2 tpl:h-2.5 tpl:w-1/4 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]" }, null, -1)]])), 64))], 8, E)) : G.value.length > 0 ? (n(), u("div", D, [(n(!0), u(_, null, i(G.value, (r) => (n(), u(_, { key: r.id }, [B.value === r.id ? (n(), u("div", {
211
+ }, [...a[7] ||= [g("div", { class: "tpl:h-3 tpl:w-1/2 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]" }, null, -1), g("div", { class: "tpl:mt-2 tpl:h-2.5 tpl:w-1/4 tpl:rounded tpl:bg-[var(--tpl-bg-hover)]" }, null, -1)]])), 64))], 8, E)) : G.value.length > 0 ? (n(), l("div", D, [(n(!0), l(_, null, i(G.value, (r) => (n(), l(_, { key: r.id }, [B.value === r.id ? (n(), l("div", {
212
212
  key: 0,
213
213
  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)]",
214
214
  style: { "background-color": "var(--tpl-primary-light)" },
@@ -218,99 +218,99 @@ var me = {
218
218
  ref: (e) => U.value = e,
219
219
  "onUpdate:modelValue": a[2] ||= (e) => V.value = e,
220
220
  type: "text",
221
- "aria-label": f(M).savedBlocks.rename,
221
+ "aria-label": d(M).savedBlocks.rename,
222
222
  class: "tpl:h-7 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]",
223
- onKeydown: [s(p((e) => Z(r.id), ["prevent", "stop"]), ["enter"]), a[3] ||= s(p((e) => X(), ["prevent", "stop"]), ["esc"])]
223
+ onKeydown: [s(f((e) => Z(r.id), ["prevent", "stop"]), ["enter"]), a[3] ||= s(f((e) => X(), ["prevent", "stop"]), ["esc"])]
224
224
  }, null, 40, Ee), [[h, V.value]]), e(g("input", {
225
225
  "onUpdate:modelValue": a[4] ||= (e) => H.value = e,
226
226
  type: "text",
227
227
  "data-testid": "saved-blocks-edit-category",
228
- "aria-label": f(M).savedBlocks.category,
229
- placeholder: f(M).savedBlocks.categoryPlaceholder,
228
+ "aria-label": d(M).savedBlocks.category,
229
+ placeholder: d(M).savedBlocks.categoryPlaceholder,
230
230
  list: "tpl-saved-block-browser-categories",
231
231
  class: "tpl:h-7 tpl:w-full tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]",
232
- onKeydown: [s(p((e) => Z(r.id), ["prevent", "stop"]), ["enter"]), a[5] ||= s(p((e) => X(), ["prevent", "stop"]), ["esc"])]
233
- }, null, 40, De), [[h, H.value]])], 40, O)) : (n(), u("button", {
232
+ onKeydown: [s(f((e) => Z(r.id), ["prevent", "stop"]), ["enter"]), a[5] ||= s(f((e) => X(), ["prevent", "stop"]), ["esc"])]
233
+ }, null, 40, De), [[h, H.value]])], 40, O)) : (n(), l("button", {
234
234
  key: 1,
235
235
  type: "button",
236
236
  "data-testid": "saved-block-card",
237
237
  "aria-pressed": R.value === r.id,
238
238
  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]",
239
- style: re({
239
+ style: ne({
240
240
  borderColor: R.value === r.id ? "var(--tpl-primary)" : "var(--tpl-border)",
241
241
  backgroundColor: R.value === r.id ? "var(--tpl-primary-light)" : "transparent"
242
242
  }),
243
243
  onClick: (e) => R.value = r.id
244
244
  }, [
245
- g("div", ke, [g("span", Ae, c(r.name), 1), g("span", je, c(f(N)(f(M).savedBlocks.blockCount, { count: r.content.length })), 1)]),
246
- r.category ? (n(), u("div", Me, [g("span", Ne, c(r.category), 1)])) : d("", !0),
245
+ g("div", ke, [g("span", Ae, p(r.name), 1), g("span", je, p(d(N)(d(M).savedBlocks.blockCount, { count: r.content.length })), 1)]),
246
+ r.category ? (n(), l("div", Me, [g("span", Ne, p(r.category), 1)])) : u("", !0),
247
247
  g("div", Pe, [
248
- (n(!0), u(_, null, i(at(r), (e) => (n(), m(t(e.icon), {
248
+ (n(!0), l(_, null, i(at(r), (e) => (n(), m(t(e.icon), {
249
249
  key: e.type,
250
250
  size: 14,
251
251
  "stroke-width": 1.5,
252
252
  class: "tpl:text-[var(--tpl-text-dim)]"
253
253
  }))), 128)),
254
- J(r) > 0 ? (n(), u("span", Fe, " +" + c(J(r)), 1)) : d("", !0),
255
- Y(r) ? (n(), u("span", {
254
+ J(r) > 0 ? (n(), l("span", Fe, " +" + p(J(r)), 1)) : u("", !0),
255
+ Y(r) ? (n(), l("span", {
256
256
  key: 1,
257
257
  "data-testid": "saved-block-updated",
258
258
  class: "tpl:ml-1 tpl:truncate tpl:text-[10px] tpl:text-[var(--tpl-text-dim)]",
259
259
  title: ot(r)
260
- }, c(Y(r)), 9, Ie)) : d("", !0),
261
- z.value === r.id ? (n(), u("button", {
260
+ }, p(Y(r)), 9, Ie)) : u("", !0),
261
+ z.value === r.id ? (n(), l("button", {
262
262
  key: 2,
263
- "aria-label": f(M).savedBlocks.deleteConfirm,
263
+ "aria-label": d(M).savedBlocks.deleteConfirm,
264
264
  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:duration-100 tpl:border-[var(--tpl-danger)] tpl:text-[var(--tpl-danger)]",
265
265
  style: { "background-color": "transparent" },
266
- onClick: p((e) => st(r.id), ["stop"])
267
- }, c(f(M).savedBlocks.deleteConfirm), 9, Le)) : (n(), u(_, { key: 3 }, [f(P).canUpdateBlock(r) ? (n(), u("button", {
266
+ onClick: f((e) => st(r.id), ["stop"])
267
+ }, p(d(M).savedBlocks.deleteConfirm), 9, Le)) : (n(), l(_, { key: 3 }, [d(P).canUpdateBlock(r) ? (n(), l("button", {
268
268
  key: 0,
269
269
  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:duration-100 tpl:text-[var(--tpl-text-dim)]",
270
- "aria-label": f(M).savedBlocks.rename,
271
- title: f(M).savedBlocks.rename,
272
- onClick: p((e) => ct(r), ["stop"])
273
- }, [y(f(de), {
270
+ "aria-label": d(M).savedBlocks.rename,
271
+ title: d(M).savedBlocks.rename,
272
+ onClick: f((e) => ct(r), ["stop"])
273
+ }, [y(d(ce), {
274
274
  size: 12,
275
275
  "stroke-width": 1.5
276
- })], 8, Re)) : d("", !0), f(P).canDeleteBlock(r) ? (n(), u("button", {
276
+ })], 8, Re)) : u("", !0), d(P).canDeleteBlock(r) ? (n(), l("button", {
277
277
  key: 1,
278
- class: ne(["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:duration-100 tpl:text-[var(--tpl-text-dim)]", { "tpl:ml-auto": !f(P).canUpdateBlock(r) }]),
279
- "aria-label": f(M).savedBlocks.delete,
280
- title: f(M).savedBlocks.delete,
281
- onClick: p((e) => z.value = r.id, ["stop"])
282
- }, [y(f(le), {
278
+ class: re(["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:duration-100 tpl:text-[var(--tpl-text-dim)]", { "tpl:ml-auto": !d(P).canUpdateBlock(r) }]),
279
+ "aria-label": d(M).savedBlocks.delete,
280
+ title: d(M).savedBlocks.delete,
281
+ onClick: f((e) => z.value = r.id, ["stop"])
282
+ }, [y(d(ue), {
283
283
  size: 12,
284
284
  "stroke-width": 1.5
285
- })], 10, ze)) : d("", !0)], 64))
285
+ })], 10, ze)) : u("", !0)], 64))
286
286
  ])
287
- ], 12, Oe))], 64))), 128))])) : (n(), u("div", Be, [y(f(x), {
287
+ ], 12, Oe))], 64))), 128))])) : (n(), l("div", Be, [y(d(x), {
288
288
  size: 32,
289
289
  "stroke-width": 1,
290
290
  class: "tpl:text-[var(--tpl-text-dim)]"
291
- }), nt.value ? (n(), u("p", Ve, c(f(M).savedBlocks.noResults), 1)) : (n(), u(_, { key: 1 }, [g("p", He, c(f(M).savedBlocks.empty), 1), g("p", Ue, c(f(M).savedBlocks.emptyHint), 1)], 64))]))])
292
- ]), g("div", We, [q.value ? (n(), u("div", Ge, [g("div", Ke, [y(f(tt), { blocks: q.value.content }, null, 8, ["blocks"])])])) : (n(), u("div", qe, [y(f(x), {
291
+ }), nt.value ? (n(), l("p", Ve, p(d(M).savedBlocks.noResults), 1)) : (n(), l(_, { key: 1 }, [g("p", He, p(d(M).savedBlocks.empty), 1), g("p", Ue, p(d(M).savedBlocks.emptyHint), 1)], 64))]))])
292
+ ]), g("div", We, [q.value ? (n(), l("div", Ge, [g("div", Ke, [y(d(tt), { blocks: q.value.content }, null, 8, ["blocks"])])])) : (n(), l("div", qe, [y(d(x), {
293
293
  size: 32,
294
294
  "stroke-width": 1,
295
295
  class: "tpl:text-[var(--tpl-text-dim)]"
296
- }), g("p", Je, c(f(M).savedBlocks.selectToPreview), 1)]))])]),
297
- g("div", Ye, [g("div", Xe, [g("label", k, c(f(M).savedBlocks.insertPosition), 1), e(g("select", {
296
+ }), g("p", Je, p(d(M).savedBlocks.selectToPreview), 1)]))])]),
297
+ g("div", Ye, [g("div", Xe, [g("label", k, p(d(M).savedBlocks.insertPosition), 1), e(g("select", {
298
298
  "onUpdate:modelValue": a[6] ||= (e) => W.value = e,
299
299
  class: "tpl:h-7 tpl:max-w-[220px] tpl:rounded-md tpl:border tpl:px-2 tpl:text-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]"
300
- }, [(n(!0), u(_, null, i(rt.value, (e) => (n(), u("option", {
300
+ }, [(n(!0), l(_, null, i(rt.value, (e) => (n(), l("option", {
301
301
  key: e.value,
302
302
  value: e.value
303
- }, c(e.label), 9, Ze))), 128))], 512), [[o, W.value]])]), g("div", Qe, [g("button", {
303
+ }, p(e.label), 9, Ze))), 128))], 512), [[o, W.value]])]), g("div", Qe, [g("button", {
304
304
  type: "button",
305
305
  "data-testid": "saved-blocks-browser-close",
306
306
  class: "tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]",
307
307
  onClick: $
308
- }, c(f(M).savedBlocks.close), 1), g("button", {
308
+ }, p(d(M).savedBlocks.close), 1), g("button", {
309
309
  type: "button",
310
310
  class: "tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]",
311
311
  disabled: !q.value,
312
312
  onClick: Q
313
- }, c(f(M).savedBlocks.insert), 9, $e)])])
313
+ }, p(d(M).savedBlocks.insert), 9, $e)])])
314
314
  ])]),
315
315
  _: 1
316
316
  }, 8, ["visible"]));
@@ -319,4 +319,4 @@ var me = {
319
319
  //#endregion
320
320
  export { A as default };
321
321
 
322
- //# sourceMappingURL=SavedBlocksBrowserModal-jMnNd4wn.js.map
322
+ //# sourceMappingURL=SavedBlocksBrowserModal-B-sjsIJv.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SavedBlocksBrowserModal-jMnNd4wn.js","names":[],"sources":["../../../src/components/SavedBlocksBrowserModal.vue","../../../src/components/SavedBlocksBrowserModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport { SAVED_BLOCKS_KEY, EDITOR_KEY, requireInject } from \"../keys\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport { Package, Pencil, Search, Trash2, X } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, nextTick, ref, watch } 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\");\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.savedBlocks.time, format) ?? \"\";\n}\n\n/** Absolute timestamp for the row's tooltip — locale-formatted, no i18n keys. */\nfunction absoluteLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\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` keeps it inside narrow viewports, expressed\n against `--tpl-base-size` rather than `rem` so the editor stays immune to\n the host page's root font-size (issue #209); the 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:mx-4 tpl:flex tpl:w-[1000px] tpl:max-w-[calc(100vw_-_2*var(--tpl-base-size))] 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 max-height: 90vh;\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:duration-100 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: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: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: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: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:duration-100 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:duration-100 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:duration-100 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: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-xs tpl:transition-all tpl:duration-150 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=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!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 TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport { SAVED_BLOCKS_KEY, EDITOR_KEY, requireInject } from \"../keys\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport { Package, Pencil, Search, Trash2, X } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, nextTick, ref, watch } 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\");\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.savedBlocks.time, format) ?? \"\";\n}\n\n/** Absolute timestamp for the row's tooltip — locale-formatted, no i18n keys. */\nfunction absoluteLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\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` keeps it inside narrow viewports, expressed\n against `--tpl-base-size` rather than `rem` so the editor stays immune to\n the host page's root font-size (issue #209); the 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:mx-4 tpl:flex tpl:w-[1000px] tpl:max-w-[calc(100vw_-_2*var(--tpl-base-size))] 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 max-height: 90vh;\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:duration-100 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: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: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: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: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:duration-100 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:duration-100 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:duration-100 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: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-xs tpl:transition-all tpl:duration-150 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=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!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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"}
1
+ {"version":3,"file":"SavedBlocksBrowserModal-B-sjsIJv.js","names":[],"sources":["../../../src/components/SavedBlocksBrowserModal.vue","../../../src/components/SavedBlocksBrowserModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport { SAVED_BLOCKS_KEY, EDITOR_KEY, requireInject } from \"../keys\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport { Package, Pencil, Search, Trash2, X } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, nextTick, ref, watch } 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\");\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.savedBlocks.time, format) ?? \"\";\n}\n\n/** Absolute timestamp for the row's tooltip — locale-formatted, no i18n keys. */\nfunction absoluteLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\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` keeps it inside narrow viewports, expressed\n against `--tpl-base-size` rather than `rem` so the editor stays immune to\n the host page's root font-size (issue #209); the 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:mx-4 tpl:flex tpl:w-[1000px] tpl:max-w-[calc(100vw_-_2*var(--tpl-base-size))] 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 max-height: 90vh;\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:duration-100 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: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: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: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: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:duration-100 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:duration-100 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:duration-100 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: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-xs tpl:transition-all tpl:duration-150 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=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!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 TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\nimport { SAVED_BLOCKS_KEY, EDITOR_KEY, requireInject } from \"../keys\";\nimport type { SavedBlock } from \"@templatical/types\";\nimport { Package, Pencil, Search, Trash2, X } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, nextTick, ref, watch } 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\");\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.savedBlocks.time, format) ?? \"\";\n}\n\n/** Absolute timestamp for the row's tooltip — locale-formatted, no i18n keys. */\nfunction absoluteLabel(saved: SavedBlock): string {\n const raw = saved.updatedAt ?? saved.createdAt;\n if (!raw) return \"\";\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\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` keeps it inside narrow viewports, expressed\n against `--tpl-base-size` rather than `rem` so the editor stays immune to\n the host page's root font-size (issue #209); the 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:mx-4 tpl:flex tpl:w-[1000px] tpl:max-w-[calc(100vw_-_2*var(--tpl-base-size))] 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 max-height: 90vh;\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:duration-100 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: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: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: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: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:duration-100 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:duration-100 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:duration-100 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: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-xs tpl:transition-all tpl:duration-150 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=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!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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"}