@jackuait/blok 0.6.0-beta.13 → 0.6.0-beta.14

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 (246) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-DzeTRJ_i.mjs → blok-Cl30YOLl.mjs} +1398 -1132
  3. package/dist/chunks/{i18next-loader-CgW4H90H.mjs → i18next-loader-DbTDlMux.mjs} +1 -1
  4. package/dist/chunks/{index-BWKk7PIS.mjs → index-DJmaYswj.mjs} +1 -1
  5. package/dist/chunks/{inline-tool-convert-DrSwadw_.mjs → inline-tool-convert-MjSaP8r7.mjs} +161 -145
  6. package/dist/{messages-B1Aww8q7.mjs → chunks/messages-1mYyrppy.mjs} +1 -0
  7. package/dist/chunks/{messages-DnIhyAJk.mjs → messages-4gHfbcDp.mjs} +1 -0
  8. package/dist/{messages-CQwpzUFp.mjs → chunks/messages-6xYv-U8A.mjs} +1 -0
  9. package/dist/chunks/{messages-CMkNSDTo.mjs → messages-7jqF79FO.mjs} +1 -0
  10. package/dist/chunks/{messages-BSbjsyHY.mjs → messages-B3EUuA7o.mjs} +1 -0
  11. package/dist/{messages-LPVfA-8K.mjs → chunks/messages-B6srtRn7.mjs} +1 -0
  12. package/dist/chunks/{messages-DJDG55Vq.mjs → messages-B85OIs0E.mjs} +1 -0
  13. package/dist/{messages-_ErNTNhk.mjs → chunks/messages-BCYBBG_u.mjs} +1 -0
  14. package/dist/{messages-CZygwLwM.mjs → chunks/messages-BDLNPiaW.mjs} +1 -0
  15. package/dist/{messages-C2htQ_3F.mjs → chunks/messages-BKpNd1Zb.mjs} +1 -0
  16. package/dist/chunks/{messages-CnwibSvh.mjs → messages-BLFUid_o.mjs} +1 -0
  17. package/dist/{messages-CVw84KdI.mjs → chunks/messages-BNPTVKnc.mjs} +1 -0
  18. package/dist/{messages-BMv4xwIr.mjs → chunks/messages-BQPXSkri.mjs} +1 -0
  19. package/dist/{messages-Dz9L52ol.mjs → chunks/messages-BQu6VNDq.mjs} +1 -0
  20. package/dist/{messages-DprmQg6V.mjs → chunks/messages-BRodFKUo.mjs} +1 -0
  21. package/dist/{messages-CznZadDf.mjs → chunks/messages-BWIikA8H.mjs} +1 -0
  22. package/dist/{messages-CqWJcCbY.mjs → chunks/messages-BXQvMilx.mjs} +1 -0
  23. package/dist/chunks/{messages-Z9nEU2xK.mjs → messages-BZ92luiw.mjs} +1 -0
  24. package/dist/chunks/{messages-BC86qLvI.mjs → messages-Bg0Phlcj.mjs} +1 -0
  25. package/dist/chunks/{messages-DnXLrlHh.mjs → messages-Bh6RBIu5.mjs} +1 -0
  26. package/dist/chunks/{messages-DhLKYm2j.mjs → messages-BnFHdIpg.mjs} +1 -0
  27. package/dist/{messages-BU2nlrLK.mjs → chunks/messages-Bq82EVsw.mjs} +1 -0
  28. package/dist/chunks/{messages-Diu6jAaR.mjs → messages-BxbcTiHh.mjs} +1 -0
  29. package/dist/{messages-BoJc_p1r.mjs → chunks/messages-ByO6bSV2.mjs} +1 -0
  30. package/dist/chunks/{messages-BrPFGbM-.mjs → messages-ByUNPiQ4.mjs} +1 -0
  31. package/dist/chunks/{messages-BB5z9Uba.mjs → messages-Byw0EHyf.mjs} +1 -0
  32. package/dist/chunks/{messages-D1Hv8XGo.mjs → messages-C-aP90zq.mjs} +1 -0
  33. package/dist/chunks/{messages-DBRw-7Zc.mjs → messages-C2f44jci.mjs} +1 -0
  34. package/dist/chunks/{messages-CJdUsQ-c.mjs → messages-CKYoAmm9.mjs} +1 -0
  35. package/dist/chunks/{messages-CvGLfqmV.mjs → messages-CSQJrJ_N.mjs} +1 -0
  36. package/dist/chunks/{messages-1fC8IMyX.mjs → messages-CX_o4Cgc.mjs} +1 -0
  37. package/dist/chunks/{messages-BdeLo0N9.mjs → messages-C_tfXllT.mjs} +1 -0
  38. package/dist/chunks/{messages-DBn76jVV.mjs → messages-Cj6TZ_By.mjs} +1 -0
  39. package/dist/{messages-CLhcMlTc.mjs → chunks/messages-CjdYkfzj.mjs} +1 -0
  40. package/dist/{messages-7W4d0DwD.mjs → chunks/messages-CmvWFWR6.mjs} +1 -0
  41. package/dist/{messages-CzTufCHu.mjs → chunks/messages-CnV-kxh6.mjs} +1 -0
  42. package/dist/chunks/{messages-BMXCuEKO.mjs → messages-CpuDfpdi.mjs} +1 -0
  43. package/dist/chunks/{messages-DT4dP5uK.mjs → messages-Cspe1oXk.mjs} +1 -0
  44. package/dist/{messages-D-ZtY5v0.mjs → chunks/messages-D2_d-Jt4.mjs} +1 -0
  45. package/dist/{messages-BELRf6DU.mjs → chunks/messages-DADSbW8l.mjs} +1 -0
  46. package/dist/chunks/{messages-O5tQus_0.mjs → messages-DHSeTQ4S.mjs} +1 -0
  47. package/dist/{messages-BFG6Wlgy.mjs → chunks/messages-DKeO2RsW.mjs} +1 -0
  48. package/dist/chunks/{messages-9SihnaXQ.mjs → messages-DLdlX_dY.mjs} +1 -0
  49. package/dist/chunks/{messages-BL0tXcDf.mjs → messages-DVxlJ4DU.mjs} +1 -0
  50. package/dist/chunks/{messages-CY8_RyFE.mjs → messages-DWSxENRk.mjs} +1 -0
  51. package/dist/{messages-Q7AO_FLv.mjs → chunks/messages-D_bVNZ12.mjs} +1 -0
  52. package/dist/chunks/{messages-BYyy6Wqf.mjs → messages-DdzjVwmG.mjs} +1 -0
  53. package/dist/chunks/{messages-DLfR5bMd.mjs → messages-DhyBKTi0.mjs} +1 -0
  54. package/dist/chunks/{messages-DvFLX36Q.mjs → messages-DlC9jDXa.mjs} +1 -0
  55. package/dist/chunks/{messages-D5C3J9qr.mjs → messages-DmBGk8ed.mjs} +1 -0
  56. package/dist/chunks/{messages-R3hUSvr3.mjs → messages-Dm_VqpU6.mjs} +1 -0
  57. package/dist/chunks/{messages-w7v1GNaE.mjs → messages-Dn9hI_ER.mjs} +1 -0
  58. package/dist/{messages-uKX8WBaD.mjs → chunks/messages-Dssp4AXT.mjs} +1 -0
  59. package/dist/chunks/{messages-Xq8UmkVs.mjs → messages-DzCc0JgS.mjs} +1 -0
  60. package/dist/chunks/{messages-JELdtT6E.mjs → messages-N6THNxo0.mjs} +1 -0
  61. package/dist/chunks/{messages-DqM1LFg5.mjs → messages-OctOxF2l.mjs} +1 -0
  62. package/dist/chunks/{messages-_ncGrKHh.mjs → messages-QT3JA95K.mjs} +1 -0
  63. package/dist/{messages-C9eaarcK.mjs → chunks/messages-QqWK83BF.mjs} +1 -0
  64. package/dist/chunks/{messages-BogRq8lt.mjs → messages-RsGaDzbb.mjs} +1 -0
  65. package/dist/chunks/{messages-Dzwxv9v1.mjs → messages-c7YlI6Wm.mjs} +1 -0
  66. package/dist/{messages-D5iv1Kox.mjs → chunks/messages-lZ8aF-4r.mjs} +1 -0
  67. package/dist/chunks/{messages-7QoX8DkW.mjs → messages-uGDrchjb.mjs} +1 -0
  68. package/dist/chunks/{messages-CKI54h6O.mjs → messages-xc2yNvog.mjs} +1 -0
  69. package/dist/{messages-Bmu_S7GM.mjs → chunks/messages-y5Q-ymIx.mjs} +1 -0
  70. package/dist/{messages-BWF-zUpY.mjs → chunks/messages-yJnyedAd.mjs} +1 -0
  71. package/dist/{messages-kep5wtm4.mjs → chunks/messages-yQ_4upHh.mjs} +1 -0
  72. package/dist/chunks/{messages-C99mq906.mjs → messages-ysbWD56Q.mjs} +1 -0
  73. package/dist/full.mjs +2 -2
  74. package/dist/locales.mjs +68 -67
  75. package/dist/{chunks/messages-B1Aww8q7.mjs → messages-1mYyrppy.mjs} +1 -0
  76. package/dist/{messages-DnIhyAJk.mjs → messages-4gHfbcDp.mjs} +1 -0
  77. package/dist/{chunks/messages-CQwpzUFp.mjs → messages-6xYv-U8A.mjs} +1 -0
  78. package/dist/{messages-CMkNSDTo.mjs → messages-7jqF79FO.mjs} +1 -0
  79. package/dist/{messages-BSbjsyHY.mjs → messages-B3EUuA7o.mjs} +1 -0
  80. package/dist/{chunks/messages-LPVfA-8K.mjs → messages-B6srtRn7.mjs} +1 -0
  81. package/dist/{messages-DJDG55Vq.mjs → messages-B85OIs0E.mjs} +1 -0
  82. package/dist/{chunks/messages-_ErNTNhk.mjs → messages-BCYBBG_u.mjs} +1 -0
  83. package/dist/{chunks/messages-CZygwLwM.mjs → messages-BDLNPiaW.mjs} +1 -0
  84. package/dist/{chunks/messages-C2htQ_3F.mjs → messages-BKpNd1Zb.mjs} +1 -0
  85. package/dist/{messages-CnwibSvh.mjs → messages-BLFUid_o.mjs} +1 -0
  86. package/dist/{chunks/messages-CVw84KdI.mjs → messages-BNPTVKnc.mjs} +1 -0
  87. package/dist/{chunks/messages-BMv4xwIr.mjs → messages-BQPXSkri.mjs} +1 -0
  88. package/dist/{chunks/messages-Dz9L52ol.mjs → messages-BQu6VNDq.mjs} +1 -0
  89. package/dist/{chunks/messages-DprmQg6V.mjs → messages-BRodFKUo.mjs} +1 -0
  90. package/dist/{chunks/messages-CznZadDf.mjs → messages-BWIikA8H.mjs} +1 -0
  91. package/dist/{chunks/messages-CqWJcCbY.mjs → messages-BXQvMilx.mjs} +1 -0
  92. package/dist/{messages-Z9nEU2xK.mjs → messages-BZ92luiw.mjs} +1 -0
  93. package/dist/{messages-BC86qLvI.mjs → messages-Bg0Phlcj.mjs} +1 -0
  94. package/dist/{messages-DnXLrlHh.mjs → messages-Bh6RBIu5.mjs} +1 -0
  95. package/dist/{messages-DhLKYm2j.mjs → messages-BnFHdIpg.mjs} +1 -0
  96. package/dist/{chunks/messages-BU2nlrLK.mjs → messages-Bq82EVsw.mjs} +1 -0
  97. package/dist/{messages-Diu6jAaR.mjs → messages-BxbcTiHh.mjs} +1 -0
  98. package/dist/{chunks/messages-BoJc_p1r.mjs → messages-ByO6bSV2.mjs} +1 -0
  99. package/dist/{messages-BrPFGbM-.mjs → messages-ByUNPiQ4.mjs} +1 -0
  100. package/dist/{messages-BB5z9Uba.mjs → messages-Byw0EHyf.mjs} +1 -0
  101. package/dist/{messages-D1Hv8XGo.mjs → messages-C-aP90zq.mjs} +1 -0
  102. package/dist/{messages-DBRw-7Zc.mjs → messages-C2f44jci.mjs} +1 -0
  103. package/dist/{messages-CJdUsQ-c.mjs → messages-CKYoAmm9.mjs} +1 -0
  104. package/dist/{messages-CvGLfqmV.mjs → messages-CSQJrJ_N.mjs} +1 -0
  105. package/dist/{messages-1fC8IMyX.mjs → messages-CX_o4Cgc.mjs} +1 -0
  106. package/dist/{messages-BdeLo0N9.mjs → messages-C_tfXllT.mjs} +1 -0
  107. package/dist/{messages-DBn76jVV.mjs → messages-Cj6TZ_By.mjs} +1 -0
  108. package/dist/{chunks/messages-CLhcMlTc.mjs → messages-CjdYkfzj.mjs} +1 -0
  109. package/dist/{chunks/messages-7W4d0DwD.mjs → messages-CmvWFWR6.mjs} +1 -0
  110. package/dist/{chunks/messages-CzTufCHu.mjs → messages-CnV-kxh6.mjs} +1 -0
  111. package/dist/{messages-BMXCuEKO.mjs → messages-CpuDfpdi.mjs} +1 -0
  112. package/dist/{messages-DT4dP5uK.mjs → messages-Cspe1oXk.mjs} +1 -0
  113. package/dist/{chunks/messages-D-ZtY5v0.mjs → messages-D2_d-Jt4.mjs} +1 -0
  114. package/dist/{chunks/messages-BELRf6DU.mjs → messages-DADSbW8l.mjs} +1 -0
  115. package/dist/{messages-O5tQus_0.mjs → messages-DHSeTQ4S.mjs} +1 -0
  116. package/dist/{chunks/messages-BFG6Wlgy.mjs → messages-DKeO2RsW.mjs} +1 -0
  117. package/dist/{messages-9SihnaXQ.mjs → messages-DLdlX_dY.mjs} +1 -0
  118. package/dist/{messages-BL0tXcDf.mjs → messages-DVxlJ4DU.mjs} +1 -0
  119. package/dist/{messages-CY8_RyFE.mjs → messages-DWSxENRk.mjs} +1 -0
  120. package/dist/{chunks/messages-Q7AO_FLv.mjs → messages-D_bVNZ12.mjs} +1 -0
  121. package/dist/{messages-BYyy6Wqf.mjs → messages-DdzjVwmG.mjs} +1 -0
  122. package/dist/{messages-DLfR5bMd.mjs → messages-DhyBKTi0.mjs} +1 -0
  123. package/dist/{messages-DvFLX36Q.mjs → messages-DlC9jDXa.mjs} +1 -0
  124. package/dist/{messages-D5C3J9qr.mjs → messages-DmBGk8ed.mjs} +1 -0
  125. package/dist/{messages-R3hUSvr3.mjs → messages-Dm_VqpU6.mjs} +1 -0
  126. package/dist/{messages-w7v1GNaE.mjs → messages-Dn9hI_ER.mjs} +1 -0
  127. package/dist/{chunks/messages-uKX8WBaD.mjs → messages-Dssp4AXT.mjs} +1 -0
  128. package/dist/{messages-Xq8UmkVs.mjs → messages-DzCc0JgS.mjs} +1 -0
  129. package/dist/{messages-JELdtT6E.mjs → messages-N6THNxo0.mjs} +1 -0
  130. package/dist/{messages-DqM1LFg5.mjs → messages-OctOxF2l.mjs} +1 -0
  131. package/dist/{messages-_ncGrKHh.mjs → messages-QT3JA95K.mjs} +1 -0
  132. package/dist/{chunks/messages-C9eaarcK.mjs → messages-QqWK83BF.mjs} +1 -0
  133. package/dist/{messages-BogRq8lt.mjs → messages-RsGaDzbb.mjs} +1 -0
  134. package/dist/{messages-Dzwxv9v1.mjs → messages-c7YlI6Wm.mjs} +1 -0
  135. package/dist/{chunks/messages-D5iv1Kox.mjs → messages-lZ8aF-4r.mjs} +1 -0
  136. package/dist/{messages-7QoX8DkW.mjs → messages-uGDrchjb.mjs} +1 -0
  137. package/dist/{messages-CKI54h6O.mjs → messages-xc2yNvog.mjs} +1 -0
  138. package/dist/{chunks/messages-Bmu_S7GM.mjs → messages-y5Q-ymIx.mjs} +1 -0
  139. package/dist/{chunks/messages-BWF-zUpY.mjs → messages-yJnyedAd.mjs} +1 -0
  140. package/dist/{chunks/messages-kep5wtm4.mjs → messages-yQ_4upHh.mjs} +1 -0
  141. package/dist/{messages-C99mq906.mjs → messages-ysbWD56Q.mjs} +1 -0
  142. package/dist/tools.mjs +887 -701
  143. package/package.json +2 -1
  144. package/src/components/block/style-manager.ts +5 -1
  145. package/src/components/blocks.ts +109 -9
  146. package/src/components/i18n/locales/am/messages.json +1 -0
  147. package/src/components/i18n/locales/ar/messages.json +1 -0
  148. package/src/components/i18n/locales/az/messages.json +1 -0
  149. package/src/components/i18n/locales/bg/messages.json +1 -0
  150. package/src/components/i18n/locales/bn/messages.json +1 -0
  151. package/src/components/i18n/locales/bs/messages.json +1 -0
  152. package/src/components/i18n/locales/cs/messages.json +1 -0
  153. package/src/components/i18n/locales/da/messages.json +1 -0
  154. package/src/components/i18n/locales/de/messages.json +1 -0
  155. package/src/components/i18n/locales/dv/messages.json +1 -0
  156. package/src/components/i18n/locales/el/messages.json +1 -0
  157. package/src/components/i18n/locales/en/messages.json +1 -0
  158. package/src/components/i18n/locales/es/messages.json +1 -0
  159. package/src/components/i18n/locales/et/messages.json +1 -0
  160. package/src/components/i18n/locales/fa/messages.json +1 -0
  161. package/src/components/i18n/locales/fi/messages.json +1 -0
  162. package/src/components/i18n/locales/fil/messages.json +1 -0
  163. package/src/components/i18n/locales/fr/messages.json +1 -0
  164. package/src/components/i18n/locales/gu/messages.json +1 -0
  165. package/src/components/i18n/locales/he/messages.json +1 -0
  166. package/src/components/i18n/locales/hi/messages.json +1 -0
  167. package/src/components/i18n/locales/hr/messages.json +1 -0
  168. package/src/components/i18n/locales/hu/messages.json +1 -0
  169. package/src/components/i18n/locales/hy/messages.json +1 -0
  170. package/src/components/i18n/locales/id/messages.json +1 -0
  171. package/src/components/i18n/locales/it/messages.json +1 -0
  172. package/src/components/i18n/locales/ja/messages.json +1 -0
  173. package/src/components/i18n/locales/ka/messages.json +1 -0
  174. package/src/components/i18n/locales/km/messages.json +1 -0
  175. package/src/components/i18n/locales/kn/messages.json +1 -0
  176. package/src/components/i18n/locales/ko/messages.json +1 -0
  177. package/src/components/i18n/locales/ku/messages.json +1 -0
  178. package/src/components/i18n/locales/lo/messages.json +1 -0
  179. package/src/components/i18n/locales/lt/messages.json +1 -0
  180. package/src/components/i18n/locales/lv/messages.json +1 -0
  181. package/src/components/i18n/locales/mk/messages.json +1 -0
  182. package/src/components/i18n/locales/ml/messages.json +1 -0
  183. package/src/components/i18n/locales/mn/messages.json +1 -0
  184. package/src/components/i18n/locales/mr/messages.json +1 -0
  185. package/src/components/i18n/locales/ms/messages.json +1 -0
  186. package/src/components/i18n/locales/my/messages.json +1 -0
  187. package/src/components/i18n/locales/ne/messages.json +1 -0
  188. package/src/components/i18n/locales/nl/messages.json +1 -0
  189. package/src/components/i18n/locales/no/messages.json +1 -0
  190. package/src/components/i18n/locales/pa/messages.json +1 -0
  191. package/src/components/i18n/locales/pl/messages.json +1 -0
  192. package/src/components/i18n/locales/ps/messages.json +1 -0
  193. package/src/components/i18n/locales/pt/messages.json +1 -0
  194. package/src/components/i18n/locales/ro/messages.json +1 -0
  195. package/src/components/i18n/locales/ru/messages.json +1 -0
  196. package/src/components/i18n/locales/sd/messages.json +1 -0
  197. package/src/components/i18n/locales/si/messages.json +1 -0
  198. package/src/components/i18n/locales/sk/messages.json +1 -0
  199. package/src/components/i18n/locales/sl/messages.json +1 -0
  200. package/src/components/i18n/locales/sq/messages.json +1 -0
  201. package/src/components/i18n/locales/sr/messages.json +1 -0
  202. package/src/components/i18n/locales/sv/messages.json +1 -0
  203. package/src/components/i18n/locales/sw/messages.json +1 -0
  204. package/src/components/i18n/locales/ta/messages.json +1 -0
  205. package/src/components/i18n/locales/te/messages.json +1 -0
  206. package/src/components/i18n/locales/th/messages.json +1 -0
  207. package/src/components/i18n/locales/tr/messages.json +1 -0
  208. package/src/components/i18n/locales/ug/messages.json +1 -0
  209. package/src/components/i18n/locales/uk/messages.json +1 -0
  210. package/src/components/i18n/locales/ur/messages.json +1 -0
  211. package/src/components/i18n/locales/vi/messages.json +1 -0
  212. package/src/components/i18n/locales/yi/messages.json +1 -0
  213. package/src/components/i18n/locales/zh/messages.json +1 -0
  214. package/src/components/icons/index.ts +8 -0
  215. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +51 -10
  216. package/src/components/modules/blockEvents/index.ts +2 -5
  217. package/src/components/modules/blockManager/blockManager.ts +10 -0
  218. package/src/components/modules/blockManager/operations.ts +11 -0
  219. package/src/components/modules/blockManager/repository.ts +22 -0
  220. package/src/components/modules/blockManager/yjs-sync.ts +148 -31
  221. package/src/components/modules/crossBlockSelection.ts +11 -3
  222. package/src/components/modules/drag/preview/DragPreview.ts +8 -0
  223. package/src/components/modules/drag/target/DropTargetDetector.ts +100 -11
  224. package/src/components/modules/paste/handlers/base.ts +3 -4
  225. package/src/components/modules/paste/index.ts +1 -1
  226. package/src/components/modules/rectangleSelection.ts +5 -2
  227. package/src/components/modules/toolbar/blockSettings.ts +52 -44
  228. package/src/components/modules/toolbar/index.ts +27 -7
  229. package/src/components/modules/toolbar/inline/index.ts +1 -1
  230. package/src/components/modules/uiControllers/controllers/blockHover.ts +16 -2
  231. package/src/components/modules/uiControllers/handlers/touch.ts +83 -10
  232. package/src/components/modules/yjs/block-observer.ts +9 -3
  233. package/src/components/modules/yjs/document-store.ts +4 -2
  234. package/src/components/modules/yjs/types.ts +8 -6
  235. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +6 -4
  236. package/src/components/utils/popover/popover-desktop.ts +12 -0
  237. package/src/stories/Popover.stories.ts +0 -2
  238. package/src/styles/main.css +9 -1
  239. package/src/tools/list/caret-manager.ts +28 -10
  240. package/src/tools/table/index.ts +180 -37
  241. package/src/tools/table/table-add-controls.ts +51 -14
  242. package/src/tools/table/table-cell-blocks.ts +41 -2
  243. package/src/tools/table/table-cell-selection.ts +27 -1
  244. package/src/tools/table/table-operations.ts +12 -15
  245. package/src/tools/table/table-row-col-controls.ts +69 -6
  246. package/src/tools/table/table-scroll-haze.ts +152 -0
@@ -112,21 +112,43 @@ export class BlockYjsSync {
112
112
  * @param fn - Function to execute
113
113
  * @param options - Options for controlling the atomic operation behavior
114
114
  */
115
- public withAtomicOperation<T>(fn: () => T, options?: { extendThroughRAF?: boolean }): T {
115
+ /**
116
+ * Begin an atomic operation by incrementing sync count and suppressing stop capturing.
117
+ *
118
+ * @returns cleanup function to call when operation completes
119
+ */
120
+ private beginAtomicOperation(): () => void {
116
121
  this.yjsSyncCount++;
117
122
  const operations = this.dependencies.operations;
118
- const shouldExtend = options?.extendThroughRAF === true;
119
123
 
120
124
  if (operations) {
121
125
  operations.suppressStopCapturing = true;
122
126
  }
123
127
 
124
- const decrementSyncCount = (): void => {
128
+ return (): void => {
125
129
  this.yjsSyncCount--;
126
130
  if (operations && this.yjsSyncCount === 0) {
127
131
  operations.suppressStopCapturing = false;
128
132
  }
129
133
  };
134
+ }
135
+
136
+ /**
137
+ * End an atomic operation, optionally deferring cleanup through RAF.
138
+ *
139
+ * @param cleanup - function to call to decrement sync count
140
+ * @param extendThroughRAF - if true, defer cleanup until after next animation frame
141
+ */
142
+ private endAtomicOperation(cleanup: () => void, extendThroughRAF: boolean): void {
143
+ if (extendThroughRAF) {
144
+ requestAnimationFrame(cleanup);
145
+ } else {
146
+ cleanup();
147
+ }
148
+ }
149
+
150
+ public withAtomicOperation<T>(fn: () => T, options?: { extendThroughRAF?: boolean }): T {
151
+ const cleanup = this.beginAtomicOperation();
130
152
 
131
153
  try {
132
154
  const result = fn();
@@ -134,16 +156,34 @@ export class BlockYjsSync {
134
156
  // If extendThroughRAF is true, delay decrementing yjsSyncCount until after requestAnimationFrame callbacks
135
157
  // This ensures that DOM updates scheduled by rendered() hooks don't trigger
136
158
  // block data sync to Yjs, which would create new undo entries and clear the redo stack
137
- if (shouldExtend) {
138
- requestAnimationFrame(decrementSyncCount);
139
- } else {
140
- decrementSyncCount();
141
- }
159
+ this.endAtomicOperation(cleanup, options?.extendThroughRAF === true);
142
160
 
143
161
  return result;
144
162
  } catch (error) {
145
- // If an error occurs, decrement immediately
146
- decrementSyncCount();
163
+ cleanup();
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Async version of withAtomicOperation for operations that return promises.
170
+ * Keeps yjsSyncCount elevated until the async work completes, then optionally
171
+ * extends through RAF to cover deferred DOM callbacks.
172
+ *
173
+ * @param fn - Async function to execute
174
+ * @param options - Options for controlling the atomic operation behavior
175
+ */
176
+ public async withAtomicOperationAsync(
177
+ fn: () => Promise<void>,
178
+ options?: { extendThroughRAF?: boolean }
179
+ ): Promise<void> {
180
+ const cleanup = this.beginAtomicOperation();
181
+
182
+ try {
183
+ await fn();
184
+ this.endAtomicOperation(cleanup, options?.extendThroughRAF === true);
185
+ } catch (error) {
186
+ cleanup();
147
187
  throw error;
148
188
  }
149
189
  }
@@ -165,25 +205,28 @@ export class BlockYjsSync {
165
205
  * @param event - the block change event from YjsManager
166
206
  */
167
207
  private syncBlockFromYjs(event: BlockChangeEvent): void {
168
- const { blockId, type: changeType } = event;
169
-
170
- if (changeType === 'update') {
171
- this.handleYjsUpdate(blockId);
208
+ if (event.type === 'update') {
209
+ this.handleYjsUpdate(event.blockId);
172
210
  return;
173
211
  }
174
212
 
175
- if (changeType === 'move') {
213
+ if (event.type === 'move') {
176
214
  this.handleYjsMove();
177
215
  return;
178
216
  }
179
217
 
180
- if (changeType === 'add') {
181
- this.handleYjsAdd(blockId);
218
+ if (event.type === 'add') {
219
+ this.handleYjsAdd(event.blockId);
182
220
  return;
183
221
  }
184
222
 
185
- if (changeType === 'remove') {
186
- this.handleYjsRemove(blockId);
223
+ if (event.type === 'batch-add') {
224
+ this.handleYjsBatchAdd(event.blockIds);
225
+ return;
226
+ }
227
+
228
+ if (event.type === 'remove') {
229
+ this.handleYjsRemove(event.blockId);
187
230
  }
188
231
  }
189
232
 
@@ -221,17 +264,19 @@ export class BlockYjsSync {
221
264
  bindEventsImmediately: true,
222
265
  });
223
266
 
224
- // Increment counter to prevent syncing back to Yjs during undo/redo
225
- this.yjsSyncCount++;
226
- try {
267
+ // Use atomic operation with RAF extension to prevent DOM mutation observers
268
+ // from syncing back to Yjs after block replacement
269
+ this.withAtomicOperation(() => {
227
270
  this.handlers.replaceBlock(blockIndex, newBlock);
228
- } finally {
229
- this.yjsSyncCount--;
230
- }
271
+ }, { extendThroughRAF: true });
231
272
  } else {
232
- // Update data in-place; if tool can't handle it, recreate the block
233
- this.yjsSyncCount++;
234
- void block.setData(data).then(success => {
273
+ // Update data in-place; if tool can't handle it, recreate the block.
274
+ // Use async atomic operation with RAF extension to keep isSyncingFromYjs
275
+ // true through the entire setData lifecycle + one RAF frame, preventing
276
+ // DOM mutation observers from writing back to Yjs and clearing the redo stack.
277
+ void this.withAtomicOperationAsync(async () => {
278
+ const success = await block.setData(data);
279
+
235
280
  if (!success) {
236
281
  const blockIndex = this.handlers.getBlockIndex(block);
237
282
  const newBlock = this.factory.composeBlock({
@@ -244,9 +289,7 @@ export class BlockYjsSync {
244
289
 
245
290
  this.handlers.replaceBlock(blockIndex, newBlock);
246
291
  }
247
- }).finally(() => {
248
- this.yjsSyncCount--;
249
- });
292
+ }, { extendThroughRAF: true });
250
293
  }
251
294
  }
252
295
 
@@ -303,6 +346,80 @@ export class BlockYjsSync {
303
346
  }, { extendThroughRAF: true });
304
347
  }
305
348
 
349
+ /**
350
+ * Handle batch block add from Yjs (undo/redo).
351
+ *
352
+ * When multiple blocks are restored at once (e.g. a table + its cell
353
+ * paragraphs), we use a two-pass approach:
354
+ * 1. Create ALL blocks and insert them into the blocks array (no DOM).
355
+ * 2. Activate each block (DOM insert + RENDERED lifecycle hook).
356
+ *
357
+ * This ensures that when a parent tool's `rendered()` hook fires (pass 2),
358
+ * child blocks already exist in BlockManager, so helpers like
359
+ * `mountBlocksInCell()` can find them by ID.
360
+ */
361
+ private handleYjsBatchAdd(blockIds: string[]): void {
362
+ const yjsBlocks = this.dependencies.YjsManager.toJSON();
363
+
364
+ // Collect blocks to create — skip any that already exist
365
+ const toCreate: Array<{ blockId: string; toolName: string; data: Record<string, unknown>; parentId: string | undefined; targetIndex: number }> = [];
366
+
367
+ for (const blockId of blockIds) {
368
+ if (this.repository.getBlockById(blockId) !== undefined) {
369
+ continue;
370
+ }
371
+
372
+ const yblock = this.dependencies.YjsManager.getBlockById(blockId);
373
+
374
+ if (yblock === undefined) {
375
+ continue;
376
+ }
377
+
378
+ const toolName = yblock.get('type') as string;
379
+ const data = this.dependencies.YjsManager.yMapToObject(yblock.get('data') as YMap<unknown>);
380
+ const parentId = yblock.get('parentId') as string | undefined;
381
+ const targetIndex = yjsBlocks.findIndex((b) => b.id === blockId);
382
+
383
+ if (targetIndex === -1) {
384
+ continue;
385
+ }
386
+
387
+ toCreate.push({ blockId, toolName, data, parentId: parentId ?? undefined, targetIndex });
388
+ }
389
+
390
+ if (toCreate.length === 0) {
391
+ return;
392
+ }
393
+
394
+ this.withAtomicOperation(() => {
395
+ // Pass 1 — create blocks and add to array (no DOM, no RENDERED)
396
+ const created: Array<{ block: Block; targetIndex: number; parentId: string | undefined }> = [];
397
+
398
+ for (const entry of toCreate) {
399
+ const block = this.factory.composeBlock({
400
+ id: entry.blockId,
401
+ tool: entry.toolName,
402
+ data: entry.data,
403
+ parentId: entry.parentId,
404
+ bindEventsImmediately: true,
405
+ });
406
+
407
+ this.blocksStore.addToArray(entry.targetIndex, block);
408
+ created.push({ block, targetIndex: entry.targetIndex, parentId: entry.parentId });
409
+ }
410
+
411
+ // Pass 2 — activate blocks (DOM insert + RENDERED), then emit events
412
+ for (const { block, targetIndex, parentId } of created) {
413
+ this.blocksStore.activateBlock(block);
414
+ this.handlers.onBlockAdded(block, targetIndex);
415
+
416
+ if (parentId !== undefined) {
417
+ this.handlers.updateIndentation(block);
418
+ }
419
+ }
420
+ }, { extendThroughRAF: true });
421
+ }
422
+
306
423
  /**
307
424
  * Handle block remove from Yjs (undo/redo - removing a previously added block)
308
425
  */
@@ -269,13 +269,21 @@ export class CrossBlockSelection extends Module {
269
269
  return;
270
270
  }
271
271
 
272
- const relatedBlock = BlockManager.getBlockByChildNode(mouseEvent.relatedTarget as Node) || this.lastSelectedBlock;
273
- const targetBlock = BlockManager.getBlockByChildNode(mouseEvent.target as Node);
272
+ const rawRelatedBlock = BlockManager.getBlockByChildNode(mouseEvent.relatedTarget as Node) || this.lastSelectedBlock;
273
+ const rawTargetBlock = BlockManager.getBlockByChildNode(mouseEvent.target as Node);
274
274
 
275
- if (!relatedBlock || !targetBlock) {
275
+ if (!rawRelatedBlock || !rawTargetBlock) {
276
276
  return;
277
277
  }
278
278
 
279
+ /**
280
+ * Resolve child blocks (e.g. paragraphs inside table cells) to their root parent.
281
+ * Without this, dragging across a table would select individual cell blocks
282
+ * from the flat blocks array instead of treating the table as a single unit.
283
+ */
284
+ const relatedBlock = BlockManager.resolveToRootBlock(rawRelatedBlock);
285
+ const targetBlock = BlockManager.resolveToRootBlock(rawTargetBlock);
286
+
279
287
  if (targetBlock === relatedBlock) {
280
288
  return;
281
289
  }
@@ -23,6 +23,14 @@ export class DragPreview {
23
23
  // Reset styles on clone
24
24
  clone.className = twMerge(PREVIEW_STYLES.content, isStretched ? 'max-w-none' : '');
25
25
 
26
+ // Set explicit width on clone so percentage-based children (e.g. table cells)
27
+ // have a sizing reference inside the position:fixed preview container
28
+ const computedWidth = contentElement.getBoundingClientRect().width;
29
+
30
+ if (computedWidth > 0) {
31
+ clone.style.width = `${computedWidth}px`;
32
+ }
33
+
26
34
  // Reset margin on the tool's rendered element (first child) to prevent offset
27
35
  const toolElement = clone.firstElementChild as HTMLElement | null;
28
36
 
@@ -138,6 +138,20 @@ export class DropTargetDetector {
138
138
  return null;
139
139
  }
140
140
 
141
+ /**
142
+ * If the target block is inside a table cell and the source block is NOT
143
+ * in the same cell, redirect the drop target to the table block itself.
144
+ * This prevents blocks from being dropped into table cells via drag & drop.
145
+ */
146
+ const targetCellContainer = targetBlock.holder.closest('[data-blok-table-cell-blocks]');
147
+ const sourceCellContainer = sourceBlock.holder.closest('[data-blok-table-cell-blocks]');
148
+ const isTargetInCell = targetCellContainer !== null;
149
+ const isCrossCellDrop = sourceCellContainer !== targetCellContainer;
150
+
151
+ if (isTargetInCell && isCrossCellDrop) {
152
+ return this.redirectToTableBlock(targetCellContainer, clientY);
153
+ }
154
+
141
155
  // Determine edge (top or bottom half of block)
142
156
  const rect = blockHolder.getBoundingClientRect();
143
157
  const isTopHalf = clientY < rect.top + rect.height / 2;
@@ -151,24 +165,29 @@ export class DropTargetDetector {
151
165
  const canUsePreviousBlock = previousBlock && !this.sourceBlocks.includes(previousBlock);
152
166
 
153
167
  if (isTopHalf && targetIndex > 0 && canUsePreviousBlock) {
154
- const targetDepth = this.calculateTargetDepth(previousBlock, 'bottom');
168
+ const targetDepth = this.calculateTargetDepth(previousBlock, 'bottom', sourceBlock);
155
169
  return { block: previousBlock, edge: 'bottom', depth: targetDepth };
156
170
  }
157
171
 
158
172
  // First block top half, or any block bottom half
159
173
  const edge: 'top' | 'bottom' = isTopHalf ? 'top' : 'bottom';
160
- const targetDepth = this.calculateTargetDepth(targetBlock, edge);
174
+ const targetDepth = this.calculateTargetDepth(targetBlock, edge, sourceBlock);
161
175
 
162
176
  return { block: targetBlock, edge, depth: targetDepth };
163
177
  }
164
178
 
165
179
  /**
166
- * Calculates the target depth for list item nesting
180
+ * Calculates the target depth for list item nesting.
181
+ * When a sourceBlock is provided and is a list item, the depth is calculated
182
+ * to match what ListDepthValidator.getTargetDepthForMove will produce post-drop,
183
+ * ensuring the visual indicator accurately predicts the final depth.
184
+ *
167
185
  * @param targetBlock - Block being dropped onto
168
186
  * @param targetEdge - Edge of target ('top' or 'bottom')
187
+ * @param sourceBlock - Optional block being dragged (for accurate list item depth prediction)
169
188
  * @returns The target depth (0 for root level, 1+ for nested)
170
189
  */
171
- calculateTargetDepth(targetBlock: Block, targetEdge: 'top' | 'bottom'): number {
190
+ calculateTargetDepth(targetBlock: Block, targetEdge: 'top' | 'bottom', sourceBlock?: Block): number {
172
191
  const targetIndex = this.blockManager.getBlockIndex(targetBlock);
173
192
  const dropIndex = targetEdge === 'top' ? targetIndex : targetIndex + 1;
174
193
 
@@ -184,23 +203,93 @@ export class DropTargetDetector {
184
203
  return 0;
185
204
  }
186
205
 
187
- const previousDepth = this.listItemDepth.getDepth(previousBlock) ?? 0;
206
+ const previousDepth = this.listItemDepth.getDepth(previousBlock);
207
+ const previousIsListItem = previousDepth !== null;
208
+ const prevDepthValue = previousDepth ?? 0;
188
209
 
189
210
  // Get the block that will be immediately after the drop position
190
211
  const nextBlock = this.blockManager.getBlockByIndex(dropIndex);
191
- const nextDepth = nextBlock ? (this.listItemDepth.getDepth(nextBlock) ?? 0) : 0;
212
+ const nextDepth = nextBlock ? this.listItemDepth.getDepth(nextBlock) : null;
213
+ const nextIsListItem = nextDepth !== null;
214
+ const nextDepthValue = nextDepth ?? 0;
215
+
216
+ // When dragging a list item, predict the exact depth ListDepthValidator will
217
+ // compute post-drop — so the visual indicator matches the actual result.
218
+ const sourceDepth = sourceBlock ? this.listItemDepth.getDepth(sourceBlock) : null;
219
+
220
+ if (sourceDepth !== null) {
221
+ return this.predictListItemDepth(
222
+ sourceDepth, prevDepthValue, previousIsListItem, nextDepthValue, nextIsListItem
223
+ );
224
+ }
225
+
226
+ // For non-list blocks (or when sourceBlock not provided), use neighbor-based
227
+ // depth for cosmetic indicator positioning only.
228
+ if (nextDepthValue > 0 && nextDepthValue <= prevDepthValue + 1) {
229
+ return nextDepthValue;
230
+ }
231
+
232
+ if (prevDepthValue > 0) {
233
+ return prevDepthValue;
234
+ }
235
+
236
+ return 0;
237
+ }
238
+
239
+ /**
240
+ * Mirrors ListDepthValidator.getTargetDepthForMove to predict the exact
241
+ * post-drop depth for a list item. This ensures the visual indicator
242
+ * shows the same depth the item will actually have after being dropped.
243
+ */
244
+ private predictListItemDepth(
245
+ currentDepth: number,
246
+ previousDepth: number,
247
+ previousIsListItem: boolean,
248
+ nextDepth: number,
249
+ nextIsListItem: boolean
250
+ ): number {
251
+ const maxAllowedDepth = previousIsListItem ? previousDepth + 1 : 0;
252
+
253
+ // Cap current depth at max allowed
254
+ if (currentDepth > maxAllowedDepth) {
255
+ return maxAllowedDepth;
256
+ }
192
257
 
193
- // If next item is nested, match its depth (become sibling)
194
- if (nextDepth > 0 && nextDepth <= previousDepth + 1) {
258
+ // Match next depth if it's deeper than current and within bounds
259
+ if (nextIsListItem && nextDepth > currentDepth && nextDepth <= maxAllowedDepth) {
195
260
  return nextDepth;
196
261
  }
197
262
 
198
- // If previous item is nested, match its depth
199
- if (previousDepth > 0) {
263
+ // Match previous depth if deeper than current and no next list item
264
+ if (previousIsListItem && !nextIsListItem && previousDepth > currentDepth && previousDepth <= maxAllowedDepth) {
200
265
  return previousDepth;
201
266
  }
202
267
 
203
- return 0;
268
+ return currentDepth;
269
+ }
270
+
271
+ /**
272
+ * Redirect a drop target to the table block that contains the given cell container.
273
+ * Finds the outermost [data-blok-element] ancestor and returns it as the target.
274
+ *
275
+ * @param cellContainer - The [data-blok-table-cell-blocks] element containing the target
276
+ * @param clientY - Cursor Y position for edge calculation
277
+ * @returns Drop target pointing to the table block, or null if table block not found
278
+ */
279
+ private redirectToTableBlock(cellContainer: Element, clientY: number): DropTarget | null {
280
+ const tableHolder = cellContainer.closest(createSelector(DATA_ATTR.element));
281
+ const tableBlock = tableHolder
282
+ ? this.blockManager.blocks.find(b => b.holder === tableHolder)
283
+ : undefined;
284
+
285
+ if (!tableBlock) {
286
+ return null;
287
+ }
288
+
289
+ const tableRect = tableBlock.holder.getBoundingClientRect();
290
+ const isTopHalf = clientY < tableRect.top + tableRect.height / 2;
291
+
292
+ return { block: tableBlock, edge: isTopHalf ? 'top' : 'bottom', depth: 0 };
204
293
  }
205
294
 
206
295
  }
@@ -1,5 +1,6 @@
1
1
  import type { PasteEvent, PasteEventDetail } from '../../../../../types';
2
2
  import type { BlokModules } from '../../../../types-internal/blok-modules';
3
+ import { getRestrictedTools } from '../../../../tools/table/table-restrictions';
3
4
  import type { SanitizerConfigBuilder } from '../sanitizer-config';
4
5
  import type { ToolRegistry } from '../tool-registry';
5
6
  import type { HandlerContext, PasteData } from '../types';
@@ -65,9 +66,6 @@ export abstract class BasePasteHandler implements PasteHandler {
65
66
  return isCurrentBlockDefault && currentBlock.isEmpty;
66
67
  }
67
68
 
68
- /** Tools that cannot be nested inside table cells */
69
- private static readonly TOOLS_RESTRICTED_IN_TABLE_CELLS = new Set(['table', 'header']);
70
-
71
69
  /**
72
70
  * If we're inside a table cell and any pasted item uses a tool that can't
73
71
  * be nested in table cells (e.g. table, header), redirect the insertion
@@ -78,7 +76,8 @@ export abstract class BasePasteHandler implements PasteHandler {
78
76
  private redirectToTableParentIfNeeded(data: PasteData[], BlockManager: BlokModules['BlockManager']): void {
79
77
  const currentBlock = BlockManager.currentBlock;
80
78
  const isInsideTableCell = currentBlock?.holder?.closest('[data-blok-table-cell-blocks]');
81
- const hasRestrictedTools = data.some(item => BasePasteHandler.TOOLS_RESTRICTED_IN_TABLE_CELLS.has(item.tool));
79
+ const restricted = new Set(getRestrictedTools());
80
+ const hasRestrictedTools = data.some(item => restricted.has(item.tool));
82
81
 
83
82
  if (!isInsideTableCell || !hasRestrictedTools || currentBlock === undefined) {
84
83
  return;
@@ -305,6 +305,6 @@ export class Paste extends Module {
305
305
  await this.processDataTransfer(event.clipboardData);
306
306
  }
307
307
 
308
- Toolbar.close();
308
+ Toolbar.moveAndOpen();
309
309
  };
310
310
  }
@@ -611,9 +611,12 @@ export class RectangleSelection extends Module {
611
611
  };
612
612
  }
613
613
  const blockInCurrentPos = this.Blok.BlockManager.getBlockByChildNode(elementUnderMouse);
614
+ const rootBlock = blockInCurrentPos !== undefined
615
+ ? this.Blok.BlockManager.resolveToRootBlock(blockInCurrentPos)
616
+ : undefined;
614
617
 
615
- const index = blockInCurrentPos !== undefined
616
- ? this.Blok.BlockManager.blocks.findIndex((block) => block.holder === blockInCurrentPos.holder)
618
+ const index = rootBlock !== undefined
619
+ ? this.Blok.BlockManager.blocks.findIndex((block) => block.holder === rootBlock.holder)
617
620
  : undefined;
618
621
 
619
622
  return {
@@ -160,61 +160,69 @@ export class BlockSettings extends Module<BlockSettingsNodes> {
160
160
  * Set isOpening flag BEFORE async operations to prevent toolbar from moving
161
161
  * while menu items are being created. This fixes a bug where hovering over a different
162
162
  * block during async getTunesItems() causes the toolbar to reposition incorrectly.
163
+ *
164
+ * Wrapped in try/catch to guarantee isOpening is always reset — if any step
165
+ * (getTunes, getTunesItems, PopoverClass constructor) throws, without cleanup
166
+ * the flag stays true and the toolbar permanently stops appearing on hover.
163
167
  */
164
168
  this.isOpening = true;
165
169
 
166
- /**
167
- * If block settings contains any inputs, focus will be set there,
168
- * so we need to save current selection to restore it after block settings is closed
169
- */
170
- this.selection.save();
170
+ try {
171
+ /**
172
+ * If block settings contains any inputs, focus will be set there,
173
+ * so we need to save current selection to restore it after block settings is closed
174
+ */
175
+ this.selection.save();
171
176
 
172
- /**
173
- * Highlight content of a Block we are working with
174
- * For multiple blocks, they should already be selected
175
- */
176
- if (!hasMultipleBlocksSelected) {
177
- this.Blok.BlockSelection.selectBlock(block);
178
- this.Blok.BlockSelection.clearCache();
179
- }
177
+ /**
178
+ * Highlight content of a Block we are working with
179
+ * For multiple blocks, they should already be selected
180
+ */
181
+ if (!hasMultipleBlocksSelected) {
182
+ this.Blok.BlockSelection.selectBlock(block);
183
+ this.Blok.BlockSelection.clearCache();
184
+ }
180
185
 
181
- /** Get tool's settings data - only relevant for single block selection */
182
- const { toolTunes, commonTunes } = block.getTunes();
183
-
184
- const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;
185
- const popoverParams: PopoverParams & { flipper?: Flipper } = {
186
- searchable: false,
187
- trigger: trigger || this.nodes.wrapper,
188
- items: await this.getTunesItems(block, commonTunes, toolTunes),
189
- scopeElement: this.Blok.API.methods.ui.nodes.redactor,
190
- messages: {
191
- nothingFound: this.Blok.I18n.t('popover.nothingFound'),
192
- search: this.Blok.I18n.t('popover.search'),
193
- },
194
- };
186
+ /** Get tool's settings data - only relevant for single block selection */
187
+ const { toolTunes, commonTunes } = block.getTunes();
188
+
189
+ const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;
190
+ const popoverParams: PopoverParams & { flipper?: Flipper } = {
191
+ searchable: false,
192
+ trigger: trigger || this.nodes.wrapper,
193
+ items: await this.getTunesItems(block, commonTunes, toolTunes),
194
+ scopeElement: this.Blok.API.methods.ui.nodes.redactor,
195
+ messages: {
196
+ nothingFound: this.Blok.I18n.t('popover.nothingFound'),
197
+ search: this.Blok.I18n.t('popover.search'),
198
+ },
199
+ };
195
200
 
196
- if (PopoverClass === PopoverDesktop) {
197
- popoverParams.flipper = this.flipperInstance;
198
- }
201
+ if (PopoverClass === PopoverDesktop) {
202
+ popoverParams.flipper = this.flipperInstance;
203
+ }
199
204
 
200
- this.popover = new PopoverClass(popoverParams);
201
- this.popover.getElement().setAttribute('data-blok-testid', 'block-tunes-popover');
205
+ this.popover = new PopoverClass(popoverParams);
206
+ this.popover.getElement().setAttribute('data-blok-testid', 'block-tunes-popover');
202
207
 
203
- this.popover.on(PopoverEvent.Closed, this.onPopoverClose);
208
+ this.popover.on(PopoverEvent.Closed, this.onPopoverClose);
204
209
 
205
- /**
206
- * Set opened flag AFTER popover is created to prevent race conditions
207
- * where close() is called during the async getTunesItems() call
208
- * when opened=true but popover is still null
209
- */
210
- this.opened = true;
211
- this.isOpening = false; // Clear isOpening flag after popover is created
210
+ /**
211
+ * Set opened flag AFTER popover is created to prevent race conditions
212
+ * where close() is called during the async getTunesItems() call
213
+ * when opened=true but popover is still null
214
+ */
215
+ this.opened = true;
216
+ this.isOpening = false;
212
217
 
213
- /** Tell to subscribers that block settings is opened */
214
- this.eventsDispatcher.emit(this.events.opened);
218
+ /** Tell to subscribers that block settings is opened */
219
+ this.eventsDispatcher.emit(this.events.opened);
215
220
 
216
- this.popover.show();
217
- this.attachFlipperKeydownListener(block);
221
+ this.popover.show();
222
+ this.attachFlipperKeydownListener(block);
223
+ } catch {
224
+ this.isOpening = false;
225
+ }
218
226
  }
219
227
 
220
228
  /**
@@ -336,11 +336,15 @@ export class Toolbar extends Module<ToolbarNodes> {
336
336
  }
337
337
 
338
338
  /**
339
- * Track whether the original block is inside a table cell.
340
- * Check the DOM directly rather than relying on resolution success,
341
- * so that the flag is correct even when resolution falls back to the original block.
339
+ * Track whether the block itself lives inside a table cell.
340
+ * The blockHover controller already resolves cell paragraphs to the parent
341
+ * table block, so when hovering a table, `unresolvedBlock` IS the table
342
+ * block (whose holder is NOT inside a cell). We only check the block's
343
+ * holder — not the raw mouse target — to avoid hiding the plus button
344
+ * for the table block itself.
342
345
  */
343
- this.hoveredBlockIsFromTableCell = unresolvedBlock.holder.closest('[data-blok-table-cell-blocks]') !== null;
346
+ this.hoveredBlockIsFromTableCell =
347
+ unresolvedBlock.holder.closest('[data-blok-table-cell-blocks]') !== null;
344
348
 
345
349
  const targetBlock = this.resolveTableCellBlock(unresolvedBlock);
346
350
 
@@ -588,6 +592,16 @@ export class Toolbar extends Module<ToolbarNodes> {
588
592
  this.settingsTogglerHandler.skipNextToggle();
589
593
  }
590
594
 
595
+ /**
596
+ * Hides the block actions (plus button and settings toggler) without
597
+ * closing the entire toolbar or setting explicitlyClosed.
598
+ * Used when the toolbar should remain positioned but its action buttons
599
+ * should temporarily step aside (e.g., during typing or inline toolbar use).
600
+ */
601
+ public hideBlockActions(): void {
602
+ this.blockActions.hide();
603
+ }
604
+
591
605
  /**
592
606
  * Resets the explicitlyClosed flag to allow the toolbar to reopen on hover.
593
607
  * Called when drag is cancelled to re-enable hover-based toolbar opening.
@@ -876,11 +890,17 @@ export class Toolbar extends Module<ToolbarNodes> {
876
890
  }
877
891
 
878
892
  /**
879
- * Do not move toolbar if it was explicitly closed (e.g., after block deletion).
880
- * This prevents the toolbar from reopening on subsequent block-hovered events.
893
+ * Do not move toolbar if it was explicitly closed and the user is still
894
+ * hovering the same block. When the user hovers a DIFFERENT block
895
+ * (or hoveredBlock is null after close()), reset the flag and allow
896
+ * the toolbar to reopen — this is an intentional user action.
881
897
  */
882
898
  if (this.explicitlyClosed) {
883
- return;
899
+ if (this.hoveredBlock !== null && this.hoveredBlock === hoveredBlock) {
900
+ return;
901
+ }
902
+
903
+ this.explicitlyClosed = false;
884
904
  }
885
905
 
886
906
  /**