@jackuait/blok 0.10.2 → 0.10.4

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 (293) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-D-T1XZ92.mjs → blok-NcdNQ0I6.mjs} +2304 -2101
  3. package/dist/chunks/{constants-CaB-mlB5.mjs → constants-DtfShkXT.mjs} +414 -317
  4. package/dist/chunks/{i18next-loader-CDnSPae_.mjs → i18next-loader-D32EUWLr.mjs} +1 -1
  5. package/dist/chunks/{lightweight-i18n-DZmo8dAI.mjs → lightweight-i18n-DpkvRXEd.mjs} +19 -0
  6. package/dist/{messages-Ddq3Ce3E2.mjs → chunks/messages-AD17iDBx.mjs} +18 -0
  7. package/dist/{messages-neGD3WGq.mjs → chunks/messages-B03yUEra2.mjs} +18 -0
  8. package/dist/chunks/{messages-CIfUm1Oa.mjs → messages-B2dU00Z3.mjs} +18 -0
  9. package/dist/chunks/{messages-BKN3YVIj.mjs → messages-B8hICx3L.mjs} +18 -0
  10. package/dist/{messages-Dnd5YSWv.mjs → chunks/messages-BBe45sPH.mjs} +18 -0
  11. package/dist/{messages-C7lJg8fy2.mjs → chunks/messages-BCifzMVO2.mjs} +18 -0
  12. package/dist/chunks/{messages-D7dx_6k8.mjs → messages-BGmvvtg_.mjs} +18 -0
  13. package/dist/{messages-Q5sQeVap2.mjs → chunks/messages-BJNFCDv42.mjs} +18 -0
  14. package/dist/chunks/{messages-BlxwW7M6.mjs → messages-BNy4e7Xl.mjs} +18 -0
  15. package/dist/chunks/{messages-C15z2r5U.mjs → messages-BcHZf9o-.mjs} +18 -0
  16. package/dist/{messages-A96tMxeU.mjs → chunks/messages-BjyYZeBm2.mjs} +18 -0
  17. package/dist/{messages-BbJ7ZXY8.mjs → chunks/messages-Bop7vrhU.mjs} +18 -0
  18. package/dist/{messages-BiTMwiKH.mjs → chunks/messages-BouFtpfO.mjs} +18 -0
  19. package/dist/chunks/{messages-ElIGUi0O2.mjs → messages-Br6bE1FD2.mjs} +18 -0
  20. package/dist/chunks/{messages-BHMiK51R.mjs → messages-C-EBhOHE.mjs} +18 -0
  21. package/dist/chunks/{messages-kGmxkeFH.mjs → messages-C3X7dv3f.mjs} +18 -0
  22. package/dist/chunks/{messages-4Ck88DYZ2.mjs → messages-C7Pjof0d2.mjs} +18 -0
  23. package/dist/{messages-D0lLw9KM.mjs → chunks/messages-C7sBaZOO2.mjs} +18 -0
  24. package/dist/chunks/{messages-QMOmwcZb.mjs → messages-C85zv_7x.mjs} +18 -0
  25. package/dist/chunks/{messages-DSrdy9Nw2.mjs → messages-CCEgR9GN2.mjs} +18 -0
  26. package/dist/chunks/{messages-DUr9WAkD.mjs → messages-CDSyoUft.mjs} +18 -0
  27. package/dist/{messages-bkGniiaz.mjs → chunks/messages-CGFlOwst.mjs} +18 -0
  28. package/dist/chunks/{messages-DBMaLL8b2.mjs → messages-CGLTjtRv2.mjs} +18 -0
  29. package/dist/{messages-2ZWBTerL.mjs → chunks/messages-CGPxUESo.mjs} +18 -0
  30. package/dist/chunks/{messages-BfAcUavP.mjs → messages-CNaaqQVz.mjs} +18 -0
  31. package/dist/{messages-DBhvm8NK.mjs → chunks/messages-CPFB2_m-2.mjs} +18 -0
  32. package/dist/chunks/{messages-zt6zdYWh.mjs → messages-CTdSIOAc.mjs} +18 -0
  33. package/dist/chunks/{messages-1_6UkKLS.mjs → messages-CXVWb9js.mjs} +18 -0
  34. package/dist/{messages-CdEASHDp2.mjs → chunks/messages-Cfbmwdep2.mjs} +18 -0
  35. package/dist/chunks/{messages-DxHh0O8j2.mjs → messages-ChayV9WY2.mjs} +18 -0
  36. package/dist/chunks/{messages-BgM91Lxm2.mjs → messages-Ci7UXRVI2.mjs} +18 -0
  37. package/dist/chunks/{messages-Clku7Cf-2.mjs → messages-CpXvyGWv2.mjs} +18 -0
  38. package/dist/chunks/{messages-DjvaiALg2.mjs → messages-Cql2ozf_2.mjs} +18 -0
  39. package/dist/{messages-DODrhcop.mjs → chunks/messages-Cxy_E2IS.mjs} +18 -0
  40. package/dist/chunks/{messages-CZSlfnkO2.mjs → messages-D9syZVGi2.mjs} +18 -0
  41. package/dist/chunks/{messages-BRAoJpOu.mjs → messages-D9uWAWjW.mjs} +18 -0
  42. package/dist/chunks/{messages-BK8Cp2d0.mjs → messages-DRJxSTqs.mjs} +18 -0
  43. package/dist/{messages-C_Qn9SbQ.mjs → chunks/messages-DSbI0vJf.mjs} +18 -0
  44. package/dist/chunks/{messages-CD_MnBln.mjs → messages-DVvrZRyZ.mjs} +18 -0
  45. package/dist/{messages-BE_z-zrb.mjs → chunks/messages-DY8zPIZW.mjs} +18 -0
  46. package/dist/chunks/{messages-Bz0-KNEB.mjs → messages-D_kZN9rB.mjs} +18 -0
  47. package/dist/{messages-C1vc5584.mjs → chunks/messages-DjSuq0-y2.mjs} +18 -0
  48. package/dist/chunks/{messages-DPzHD51Y.mjs → messages-DkP3Jf4F.mjs} +18 -0
  49. package/dist/{messages-_PLyRfVw.mjs → chunks/messages-DoPdy75l.mjs} +18 -0
  50. package/dist/chunks/{messages-JSQjKQ8I.mjs → messages-DpydMd36.mjs} +18 -0
  51. package/dist/{messages-BckDk9aq2.mjs → chunks/messages-DtZ9U9g72.mjs} +18 -0
  52. package/dist/{messages-JNrYldAa2.mjs → chunks/messages-H6vLy8wJ.mjs} +18 -0
  53. package/dist/chunks/{messages-DTN1XGll.mjs → messages-HzH9_QH8.mjs} +18 -0
  54. package/dist/chunks/{messages-C0IFfhnp.mjs → messages-O6FOfUgF.mjs} +18 -0
  55. package/dist/{messages-Be_2RHZD.mjs → chunks/messages-OSP4Hj5o.mjs} +18 -0
  56. package/dist/chunks/{messages-DMoERagV2.mjs → messages-RiqdVwuN2.mjs} +18 -0
  57. package/dist/chunks/{messages-BJ-vT1SU2.mjs → messages-SP659Sal2.mjs} +18 -0
  58. package/dist/{messages-Che99vKP.mjs → chunks/messages-THR8q8bJ.mjs} +18 -0
  59. package/dist/chunks/{messages-CvANwuht2.mjs → messages-VlEyFUxF2.mjs} +18 -0
  60. package/dist/{messages-apA6BStA.mjs → chunks/messages-VtfKWZ2S.mjs} +18 -0
  61. package/dist/{messages-DpJGbx3q.mjs → chunks/messages-YbckahVx2.mjs} +18 -0
  62. package/dist/{messages-DYuD5-rO.mjs → chunks/messages-ZhHLC6dk.mjs} +18 -0
  63. package/dist/{messages-C0GSBBCo2.mjs → chunks/messages-bFEdH3lv.mjs} +18 -0
  64. package/dist/chunks/{messages-euM2m3wQ.mjs → messages-dpXwA3Sz.mjs} +18 -0
  65. package/dist/chunks/{messages-CQBo3lmL2.mjs → messages-fbL5y58u2.mjs} +18 -0
  66. package/dist/chunks/{messages-CxiURE2X.mjs → messages-oPV2oMxM.mjs} +18 -0
  67. package/dist/{messages-DM4Gjc9h.mjs → chunks/messages-oXBbHW9A.mjs} +18 -0
  68. package/dist/chunks/{messages-QilfinOn2.mjs → messages-vDgsEqQW2.mjs} +18 -0
  69. package/dist/{messages-ClGvlFcH2.mjs → chunks/messages-wYQksm10.mjs} +18 -0
  70. package/dist/{messages-CnuH-BZK2.mjs → chunks/messages-yGedmr61.mjs} +18 -0
  71. package/dist/chunks/{messages-sDdNf8O9.mjs → messages-zQOpKjl3.mjs} +18 -0
  72. package/dist/chunks/{messages-eFd4YYzt.mjs → messages-zWqsggJh.mjs} +18 -0
  73. package/dist/chunks/{tools-BFK2MvVI.mjs → tools-DMSi-3RW.mjs} +3434 -1240
  74. package/dist/full.mjs +10 -10
  75. package/dist/locales.mjs +86 -67
  76. package/dist/{messages-BK_LsgY4.mjs → messages-0lOPMv8u.mjs} +18 -0
  77. package/dist/{messages-LYJbLq_F.mjs → messages-5wuR90qS.mjs} +18 -0
  78. package/dist/{messages-98nQiC7t2.mjs → messages-6eX0fWGR2.mjs} +18 -0
  79. package/dist/{chunks/messages-DUeiPraX.mjs → messages-9L4qqCKh2.mjs} +18 -0
  80. package/dist/{chunks/messages-Q7-4ZJLB2.mjs → messages-B4zPxKl62.mjs} +18 -0
  81. package/dist/{messages-D0005ti32.mjs → messages-BCMFYqKc2.mjs} +18 -0
  82. package/dist/{chunks/messages-CC_noR8y.mjs → messages-BKXjO3NH.mjs} +18 -0
  83. package/dist/{messages-CRNogopy2.mjs → messages-BLW2GX7J2.mjs} +18 -0
  84. package/dist/{messages-D81w6AmW.mjs → messages-BLfK27kX.mjs} +18 -0
  85. package/dist/{chunks/messages-CPBN4zWc.mjs → messages-BM2kx9Td.mjs} +18 -0
  86. package/dist/{messages-E8NjqzWq2.mjs → messages-BORkMoil2.mjs} +18 -0
  87. package/dist/{messages-Dqu4aX9s.mjs → messages-BPw_x-6H.mjs} +18 -0
  88. package/dist/{messages-DSmxJWju2.mjs → messages-BRY51SEw2.mjs} +18 -0
  89. package/dist/{chunks/messages-BONyZroH.mjs → messages-BSNsrZVN.mjs} +18 -0
  90. package/dist/{chunks/messages-BAlZjPcl.mjs → messages-B_UKuqrH.mjs} +18 -0
  91. package/dist/{chunks/messages-DB_-5Xln.mjs → messages-BrYeJsSE2.mjs} +18 -0
  92. package/dist/{messages-Brd5R-da2.mjs → messages-BwttyHDI2.mjs} +18 -0
  93. package/dist/{messages-qfvXgPpu2.mjs → messages-C-8qb9sf2.mjs} +18 -0
  94. package/dist/{chunks/messages-BbEW9bQz.mjs → messages-C34dTwF72.mjs} +18 -0
  95. package/dist/{messages-BmH2cQHQ.mjs → messages-C67YUZ9-.mjs} +18 -0
  96. package/dist/{messages-DpwMKDV0.mjs → messages-C6yKu_PJ.mjs} +18 -0
  97. package/dist/{messages-Do7Xjy0n.mjs → messages-CA6J_QoC.mjs} +18 -0
  98. package/dist/{messages-DVL0KZE5.mjs → messages-CFUBJfnf.mjs} +18 -0
  99. package/dist/{chunks/messages-DVr1sqfI2.mjs → messages-CLUBh7O_.mjs} +18 -0
  100. package/dist/{chunks/messages-wl8YrvGG.mjs → messages-CLZoy5fQ.mjs} +18 -0
  101. package/dist/{messages-CisR4PNV.mjs → messages-CNGwdIEz.mjs} +18 -0
  102. package/dist/{messages-DopaMHC42.mjs → messages-CR4gHjd82.mjs} +18 -0
  103. package/dist/{messages-DK6dA0O2.mjs → messages-CVMngZNA.mjs} +18 -0
  104. package/dist/{messages-Xc0KUbYl.mjs → messages-Cd5CW5Tt.mjs} +18 -0
  105. package/dist/{chunks/messages-ChK7v1PV.mjs → messages-CrjQ2Op0.mjs} +18 -0
  106. package/dist/{chunks/messages-CRF7nNrO.mjs → messages-Cv1PSaNk.mjs} +18 -0
  107. package/dist/{messages-DOGbHYv-2.mjs → messages-CxZarWTm2.mjs} +18 -0
  108. package/dist/{messages-D3rwCtKn.mjs → messages-D0eT_eWA.mjs} +18 -0
  109. package/dist/{messages-C6ONf71u2.mjs → messages-D6RYu9JW2.mjs} +18 -0
  110. package/dist/{messages-DQORja0D.mjs → messages-D8U5D391.mjs} +18 -0
  111. package/dist/{chunks/messages-EDMC5ukV.mjs → messages-D8dO6OMN.mjs} +18 -0
  112. package/dist/{messages-DfFZ6Yj5.mjs → messages-DA4T9WBe.mjs} +18 -0
  113. package/dist/{chunks/messages-D22e9h7V2.mjs → messages-DB4UKN8D.mjs} +18 -0
  114. package/dist/{chunks/messages-DEBy3nuJ2.mjs → messages-DCdP2ujL.mjs} +18 -0
  115. package/dist/{messages-D05jqBIa2.mjs → messages-DPFuzIdF2.mjs} +18 -0
  116. package/dist/{chunks/messages-DrfRYiM32.mjs → messages-DQ1icG7L.mjs} +18 -0
  117. package/dist/{chunks/messages-a07QVz8U.mjs → messages-DT7dwzEe.mjs} +18 -0
  118. package/dist/{chunks/messages-CszmHAvQ.mjs → messages-DUYxMxrQ2.mjs} +18 -0
  119. package/dist/{chunks/messages-DtoId_bw2.mjs → messages-D_V0kHD7.mjs} +18 -0
  120. package/dist/{messages-BesJaI6A.mjs → messages-DfqM_XvD.mjs} +18 -0
  121. package/dist/{messages-CT-Kdas6.mjs → messages-Di3-WVzq.mjs} +18 -0
  122. package/dist/{messages-BcVB3osF.mjs → messages-Dl0bfeA-.mjs} +18 -0
  123. package/dist/{chunks/messages-C1S9ztpF.mjs → messages-Do3mHd9U.mjs} +18 -0
  124. package/dist/{chunks/messages-BeGZqQwz.mjs → messages-DqDlcEPn.mjs} +18 -0
  125. package/dist/{chunks/messages-CTCe595D2.mjs → messages-DwiykEgr2.mjs} +18 -0
  126. package/dist/{chunks/messages-8Ld7P_9j2.mjs → messages-Dx5n6MLQ2.mjs} +18 -0
  127. package/dist/{messages-LMaR2_bE.mjs → messages-DxEiqa-B.mjs} +18 -0
  128. package/dist/{chunks/messages-CxxyR4vY.mjs → messages-Dxr1BBvo.mjs} +18 -0
  129. package/dist/{messages-D6VIFnSW.mjs → messages-DzknMM7W.mjs} +18 -0
  130. package/dist/{chunks/messages-oMc7qugU2.mjs → messages-ELvF3qMl2.mjs} +18 -0
  131. package/dist/{messages-53w0fPZS2.mjs → messages-JVJdC0Er2.mjs} +18 -0
  132. package/dist/{chunks/messages-BMD37y3q2.mjs → messages-KVerxvZC.mjs} +18 -0
  133. package/dist/{chunks/messages-Du2BffA7.mjs → messages-OOiDDmVw.mjs} +18 -0
  134. package/dist/{messages-uwK7ktqk.mjs → messages-PyOr_YgV.mjs} +18 -0
  135. package/dist/{messages-qbKjjvgd2.mjs → messages-VrQw3tQ62.mjs} +18 -0
  136. package/dist/{messages-CTTmWn4Y2.mjs → messages-WsUHzXMu2.mjs} +18 -0
  137. package/dist/{messages-CZbcxlZt2.mjs → messages-ZHgPRUj02.mjs} +18 -0
  138. package/dist/{messages-DKHbt-7l2.mjs → messages-aoO_TtoE2.mjs} +18 -0
  139. package/dist/{chunks/messages-CW35K1pq.mjs → messages-bh8BiOee2.mjs} +18 -0
  140. package/dist/{messages-BrOWqNCu2.mjs → messages-gZEhkRrR2.mjs} +18 -0
  141. package/dist/{chunks/messages-BRoa9tGl.mjs → messages-hya8YLMj.mjs} +18 -0
  142. package/dist/{messages-CdduYw-q.mjs → messages-tb1FD_ge.mjs} +18 -0
  143. package/dist/react.mjs +2 -2
  144. package/dist/tools.mjs +3 -3
  145. package/dist/vendor.LICENSE.txt +135 -0
  146. package/package.json +2 -1
  147. package/src/blok.ts +48 -0
  148. package/src/components/block/index.ts +21 -2
  149. package/src/components/block-tunes/block-tune-copy-link.ts +82 -0
  150. package/src/components/i18n/locales/am/messages.json +18 -0
  151. package/src/components/i18n/locales/ar/messages.json +18 -0
  152. package/src/components/i18n/locales/az/messages.json +18 -0
  153. package/src/components/i18n/locales/bg/messages.json +18 -0
  154. package/src/components/i18n/locales/bn/messages.json +18 -0
  155. package/src/components/i18n/locales/bs/messages.json +18 -0
  156. package/src/components/i18n/locales/cs/messages.json +18 -0
  157. package/src/components/i18n/locales/da/messages.json +18 -0
  158. package/src/components/i18n/locales/de/messages.json +18 -0
  159. package/src/components/i18n/locales/dv/messages.json +18 -0
  160. package/src/components/i18n/locales/el/messages.json +18 -0
  161. package/src/components/i18n/locales/en/messages.json +19 -0
  162. package/src/components/i18n/locales/es/messages.json +18 -0
  163. package/src/components/i18n/locales/et/messages.json +18 -0
  164. package/src/components/i18n/locales/fa/messages.json +18 -0
  165. package/src/components/i18n/locales/fi/messages.json +18 -0
  166. package/src/components/i18n/locales/fil/messages.json +18 -0
  167. package/src/components/i18n/locales/fr/messages.json +18 -0
  168. package/src/components/i18n/locales/gu/messages.json +18 -0
  169. package/src/components/i18n/locales/he/messages.json +18 -0
  170. package/src/components/i18n/locales/hi/messages.json +18 -0
  171. package/src/components/i18n/locales/hr/messages.json +18 -0
  172. package/src/components/i18n/locales/hu/messages.json +18 -0
  173. package/src/components/i18n/locales/hy/messages.json +18 -0
  174. package/src/components/i18n/locales/id/messages.json +18 -0
  175. package/src/components/i18n/locales/it/messages.json +18 -0
  176. package/src/components/i18n/locales/ja/messages.json +18 -0
  177. package/src/components/i18n/locales/ka/messages.json +18 -0
  178. package/src/components/i18n/locales/km/messages.json +18 -0
  179. package/src/components/i18n/locales/kn/messages.json +18 -0
  180. package/src/components/i18n/locales/ko/messages.json +18 -0
  181. package/src/components/i18n/locales/ku/messages.json +18 -0
  182. package/src/components/i18n/locales/lo/messages.json +18 -0
  183. package/src/components/i18n/locales/lt/messages.json +18 -0
  184. package/src/components/i18n/locales/lv/messages.json +18 -0
  185. package/src/components/i18n/locales/mk/messages.json +18 -0
  186. package/src/components/i18n/locales/ml/messages.json +18 -0
  187. package/src/components/i18n/locales/mn/messages.json +18 -0
  188. package/src/components/i18n/locales/mr/messages.json +18 -0
  189. package/src/components/i18n/locales/ms/messages.json +18 -0
  190. package/src/components/i18n/locales/my/messages.json +18 -0
  191. package/src/components/i18n/locales/ne/messages.json +18 -0
  192. package/src/components/i18n/locales/nl/messages.json +18 -0
  193. package/src/components/i18n/locales/no/messages.json +18 -0
  194. package/src/components/i18n/locales/pa/messages.json +18 -0
  195. package/src/components/i18n/locales/pl/messages.json +18 -0
  196. package/src/components/i18n/locales/ps/messages.json +18 -0
  197. package/src/components/i18n/locales/pt/messages.json +18 -0
  198. package/src/components/i18n/locales/ro/messages.json +18 -0
  199. package/src/components/i18n/locales/ru/messages.json +18 -0
  200. package/src/components/i18n/locales/sd/messages.json +18 -0
  201. package/src/components/i18n/locales/si/messages.json +18 -0
  202. package/src/components/i18n/locales/sk/messages.json +18 -0
  203. package/src/components/i18n/locales/sl/messages.json +18 -0
  204. package/src/components/i18n/locales/sq/messages.json +18 -0
  205. package/src/components/i18n/locales/sr/messages.json +18 -0
  206. package/src/components/i18n/locales/sv/messages.json +18 -0
  207. package/src/components/i18n/locales/sw/messages.json +18 -0
  208. package/src/components/i18n/locales/ta/messages.json +18 -0
  209. package/src/components/i18n/locales/te/messages.json +18 -0
  210. package/src/components/i18n/locales/th/messages.json +18 -0
  211. package/src/components/i18n/locales/tr/messages.json +18 -0
  212. package/src/components/i18n/locales/ug/messages.json +18 -0
  213. package/src/components/i18n/locales/uk/messages.json +18 -0
  214. package/src/components/i18n/locales/ur/messages.json +18 -0
  215. package/src/components/i18n/locales/vi/messages.json +18 -0
  216. package/src/components/i18n/locales/yi/messages.json +18 -0
  217. package/src/components/i18n/locales/zh/messages.json +18 -0
  218. package/src/components/icons/index.ts +65 -0
  219. package/src/components/inline-tools/inline-tool-bold.ts +10 -0
  220. package/src/components/inline-tools/inline-tool-code.ts +54 -1
  221. package/src/components/inline-tools/inline-tool-italic.ts +54 -1
  222. package/src/components/inline-tools/inline-tool-strikethrough.ts +54 -1
  223. package/src/components/inline-tools/inline-tool-underline.ts +54 -1
  224. package/src/components/inline-tools/services/bold-normalization-pass.ts +29 -3
  225. package/src/components/inline-tools/utils/formatting-range-utils.ts +83 -0
  226. package/src/components/modules/api/tools.ts +19 -0
  227. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +5 -5
  228. package/src/components/modules/blockManager/blockManager.ts +22 -0
  229. package/src/components/modules/blockManager/event-binder.ts +12 -1
  230. package/src/components/modules/blockManager/factory.ts +4 -0
  231. package/src/components/modules/blockManager/types.ts +4 -0
  232. package/src/components/modules/blockManager/yjs-sync.ts +16 -2
  233. package/src/components/modules/paste/google-docs-preprocessor.ts +49 -3
  234. package/src/components/modules/paste/handlers/table-cells-handler.ts +12 -2
  235. package/src/components/modules/paste/index.ts +8 -4
  236. package/src/components/modules/paste/types.ts +2 -0
  237. package/src/components/modules/renderer.ts +22 -2
  238. package/src/components/modules/saver.ts +19 -1
  239. package/src/components/modules/themeManager.ts +3 -1
  240. package/src/components/modules/toolbar/blockSettings.ts +51 -1
  241. package/src/components/modules/toolbar/index.ts +95 -3
  242. package/src/components/modules/toolbar/inline/index.ts +0 -3
  243. package/src/components/modules/toolbar/plus-button.ts +37 -0
  244. package/src/components/modules/toolbar/settings-toggler.ts +6 -0
  245. package/src/components/modules/tools.ts +5 -0
  246. package/src/components/modules/uiControllers/controllers/keyboard.ts +85 -22
  247. package/src/components/modules/uiControllers/controllers/selection.ts +14 -2
  248. package/src/components/modules/yjs/document-store.ts +22 -0
  249. package/src/components/modules/yjs/index.ts +10 -0
  250. package/src/components/modules/yjs/serializer.ts +20 -0
  251. package/src/components/selection/cursor.ts +12 -2
  252. package/src/components/ui/toolbox.ts +31 -5
  253. package/src/components/utils/id-generator.ts +11 -0
  254. package/src/components/utils/key-icon.ts +187 -0
  255. package/src/components/utils/popover/components/hint/hint.ts +3 -1
  256. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +18 -5
  257. package/src/components/utils/popover/popover-abstract.ts +45 -0
  258. package/src/components/utils/popover/popover-desktop.ts +1 -0
  259. package/src/components/utils/popover/popover-position.ts +12 -5
  260. package/src/components/utils/popover/popover.const.ts +2 -0
  261. package/src/components/utils.ts +1 -0
  262. package/src/styles/main.css +1269 -0
  263. package/src/tools/code/index.ts +4 -0
  264. package/src/tools/database/database-backend-sync.ts +132 -0
  265. package/src/tools/database/database-board-view.ts +410 -0
  266. package/src/tools/database/database-card-drag.ts +306 -0
  267. package/src/tools/database/database-card-drawer.ts +546 -0
  268. package/src/tools/database/database-column-controls.ts +141 -0
  269. package/src/tools/database/database-column-drag.ts +262 -0
  270. package/src/tools/database/database-keyboard.ts +35 -0
  271. package/src/tools/database/database-list-row-drag.ts +245 -0
  272. package/src/tools/database/database-list-view.ts +333 -0
  273. package/src/tools/database/database-model.ts +214 -0
  274. package/src/tools/database/database-property-type-popover.ts +108 -0
  275. package/src/tools/database/database-tab-bar.ts +558 -0
  276. package/src/tools/database/database-view-popover.ts +129 -0
  277. package/src/tools/database/database-view-renderer.ts +25 -0
  278. package/src/tools/database/index.ts +1223 -0
  279. package/src/tools/database/types.ts +152 -0
  280. package/src/tools/database-row/index.ts +74 -0
  281. package/src/tools/index.ts +4 -0
  282. package/src/tools/table/index.ts +10 -19
  283. package/src/tools/table/table-cell-selection.ts +126 -7
  284. package/src/tools/table/table-core.ts +59 -5
  285. package/src/tools/table/table-model.ts +8 -0
  286. package/src/tools/table/table-row-col-controls.ts +40 -18
  287. package/types/api/tools.d.ts +18 -0
  288. package/types/configs/blok-config.d.ts +27 -0
  289. package/types/data-formats/output-data.d.ts +13 -0
  290. package/types/index.d.ts +17 -0
  291. package/types/tools/database.d.ts +152 -0
  292. package/types/tools-entry.d.ts +7 -4
  293. package/types/utils/popover/popover.d.ts +6 -0
@@ -91,14 +91,14 @@ export class KeyboardNavigation extends BlockEventComposer {
91
91
  return;
92
92
  }
93
93
 
94
- const isNavigated = event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);
94
+ event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);
95
95
 
96
96
  /**
97
- * If we have next Block/input to focus, then focus it. Otherwise, leave native Tab behaviour
97
+ * Always prevent default Tab behaviour to keep focus inside the editor.
98
+ * Without this, pressing Tab on the last block of a nested editor (e.g. a
99
+ * database card drawer) lets the browser move focus out of the editor entirely.
98
100
  */
99
- if (isNavigated) {
100
- event.preventDefault();
101
- }
101
+ event.preventDefault();
102
102
  }
103
103
 
104
104
  /**
@@ -300,6 +300,17 @@ export class BlockManager extends Module {
300
300
  eventsDispatcher: this.eventsDispatcher,
301
301
  getBlockIndex: (block) => this.repository.getBlockIndex(block),
302
302
  onBlockMutated: this.blockDidMutated.bind(this),
303
+ shouldHandleEvent: (event: Event) => {
304
+ const target = event.target;
305
+
306
+ if (target instanceof Element) {
307
+ const closestEditor = target.closest('[data-blok-testid="blok-editor"]');
308
+
309
+ return closestEditor === null || closestEditor === this.Blok.UI.nodes.wrapper;
310
+ }
311
+
312
+ return true;
313
+ },
303
314
  });
304
315
 
305
316
  // Initialize factory
@@ -438,6 +449,8 @@ export class BlockManager extends Module {
438
449
  parentId?: string;
439
450
  contentIds?: string[];
440
451
  bindEventsImmediately?: boolean;
452
+ lastEditedAt?: number;
453
+ lastEditedBy?: string | null;
441
454
  }): Block {
442
455
  return this.factory.composeBlock(options);
443
456
  }
@@ -1086,6 +1099,11 @@ export class BlockManager extends Module {
1086
1099
  // Also skip if a pointer drag is active — the browser can mutate contenteditable DOM across
1087
1100
  // cell boundaries during a drag, and we must not write that corrupted state to Yjs.
1088
1101
  if (mutationType === BlockChangedMutationType && !this.yjsSync.isSyncingFromYjs && !this._isPointerDragActive) {
1102
+ // eslint-disable-next-line no-param-reassign
1103
+ block.lastEditedAt = Date.now();
1104
+ // eslint-disable-next-line no-param-reassign
1105
+ block.lastEditedBy = this.config.user?.name ?? null;
1106
+
1089
1107
  void this.syncBlockDataToYjs(block);
1090
1108
  }
1091
1109
 
@@ -1140,5 +1158,9 @@ export class BlockManager extends Module {
1140
1158
  for (const [key, value] of Object.entries(savedData.data)) {
1141
1159
  this.Blok.YjsManager.updateBlockData(block.id, key, value);
1142
1160
  }
1161
+
1162
+ if (block.lastEditedAt !== undefined) {
1163
+ this.Blok.YjsManager.updateBlockMetadata(block.id, block.lastEditedAt, block.lastEditedBy);
1164
+ }
1143
1165
  }
1144
1166
  }
@@ -51,6 +51,8 @@ export interface BlockEventBinderDependencies {
51
51
  getBlockIndex: (block: Block) => number;
52
52
  /** Callback when block is mutated */
53
53
  onBlockMutated: BlockMutationCallback;
54
+ /** Check if an event should be handled by this editor instance */
55
+ shouldHandleEvent?: (event: Event) => boolean;
54
56
  }
55
57
 
56
58
  /**
@@ -76,22 +78,31 @@ export class BlockEventBinder {
76
78
  * @param block - Block to bind events to
77
79
  */
78
80
  public bindBlockEvents(block: Block): void {
79
- const { blockEvents, listeners, onBlockMutated, getBlockIndex } = this.dependencies;
81
+ const { blockEvents, listeners, onBlockMutated, getBlockIndex, shouldHandleEvent } = this.dependencies;
80
82
 
81
83
  listeners.on(block.holder, 'keydown', (event: Event) => {
82
84
  if (event instanceof KeyboardEvent) {
85
+ if (shouldHandleEvent && !shouldHandleEvent(event)) {
86
+ return;
87
+ }
83
88
  blockEvents.keydown(event);
84
89
  }
85
90
  });
86
91
 
87
92
  listeners.on(block.holder, 'keyup', (event: Event) => {
88
93
  if (event instanceof KeyboardEvent) {
94
+ if (shouldHandleEvent && !shouldHandleEvent(event)) {
95
+ return;
96
+ }
89
97
  blockEvents.keyup(event);
90
98
  }
91
99
  });
92
100
 
93
101
  listeners.on(block.holder, 'input', (event: Event) => {
94
102
  if (event instanceof InputEvent) {
103
+ if (shouldHandleEvent && !shouldHandleEvent(event)) {
104
+ return;
105
+ }
95
106
  blockEvents.input(event);
96
107
  }
97
108
  });
@@ -64,6 +64,8 @@ export class BlockFactory {
64
64
  parentId,
65
65
  contentIds,
66
66
  bindEventsImmediately = false,
67
+ lastEditedAt,
68
+ lastEditedBy,
67
69
  } = options;
68
70
 
69
71
  const tool = this.dependencies.tools.get(name);
@@ -82,6 +84,8 @@ export class BlockFactory {
82
84
  parentId,
83
85
  contentIds,
84
86
  bindMutationWatchersImmediately: bindEventsImmediately,
87
+ lastEditedAt,
88
+ lastEditedBy,
85
89
  }, this.dependencies.eventsDispatcher);
86
90
 
87
91
  if (this.readOnlyState) {
@@ -26,6 +26,10 @@ export interface ComposeBlockOptions {
26
26
  contentIds?: string[];
27
27
  /** Bind events immediately instead of deferring via requestIdleCallback */
28
28
  bindEventsImmediately?: boolean;
29
+ /** Timestamp of the last edit (milliseconds since epoch) */
30
+ lastEditedAt?: number;
31
+ /** Display name of the user who last edited this block */
32
+ lastEditedBy?: string | null;
29
33
  }
30
34
 
31
35
  /**
@@ -247,6 +247,8 @@ export class BlockYjsSync {
247
247
  const data = this.dependencies.YjsManager.yMapToObject(yblock.get('data') as YMap<unknown>);
248
248
  const ytunes = yblock.get('tunes') as YMap<unknown> | undefined;
249
249
  const tunes = ytunes !== undefined ? this.dependencies.YjsManager.yMapToObject(ytunes) : {};
250
+ const lastEditedAt = yblock.get('lastEditedAt') as number | undefined;
251
+ const lastEditedBy = (yblock.get('lastEditedBy') as string | undefined) ?? null;
250
252
 
251
253
  // Check if tunes have changed - if so, we need to recreate the block
252
254
  // because tunes are instantiated during block construction
@@ -267,6 +269,8 @@ export class BlockYjsSync {
267
269
  contentIds: block.contentIds.length > 0 ? [...block.contentIds] : undefined,
268
270
  parentId: block.parentId ?? undefined,
269
271
  bindEventsImmediately: true,
272
+ lastEditedAt,
273
+ lastEditedBy,
270
274
  });
271
275
 
272
276
  // Use atomic operation with RAF extension to prevent DOM mutation observers
@@ -298,6 +302,8 @@ export class BlockYjsSync {
298
302
  contentIds: block.contentIds.length > 0 ? [...block.contentIds] : undefined,
299
303
  parentId: block.parentId ?? undefined,
300
304
  bindEventsImmediately: true,
305
+ lastEditedAt,
306
+ lastEditedBy,
301
307
  });
302
308
 
303
309
  this.handlers.replaceBlock(blockIndex, newBlock);
@@ -330,6 +336,8 @@ export class BlockYjsSync {
330
336
  const toolName = yblock.get('type') as string;
331
337
  const data = this.dependencies.YjsManager.yMapToObject(yblock.get('data') as YMap<unknown>);
332
338
  const parentId = yblock.get('parentId') as string | undefined;
339
+ const lastEditedAt = yblock.get('lastEditedAt') as number | undefined;
340
+ const lastEditedBy = (yblock.get('lastEditedBy') as string | undefined) ?? null;
333
341
 
334
342
  // Find the index of this block in Yjs to insert at correct position
335
343
  const yjsBlocks = this.dependencies.YjsManager.toJSON();
@@ -350,6 +358,8 @@ export class BlockYjsSync {
350
358
  data,
351
359
  parentId: parentId ?? undefined,
352
360
  bindEventsImmediately: true,
361
+ lastEditedAt,
362
+ lastEditedBy,
353
363
  });
354
364
 
355
365
  this.blocksStore.insert(targetIndex, block);
@@ -438,7 +448,7 @@ export class BlockYjsSync {
438
448
  const yjsBlocks = this.dependencies.YjsManager.toJSON();
439
449
 
440
450
  // Collect blocks to create — skip any that already exist
441
- const toCreate: Array<{ blockId: string; toolName: string; data: Record<string, unknown>; parentId: string | undefined; targetIndex: number }> = [];
451
+ const toCreate: Array<{ blockId: string; toolName: string; data: Record<string, unknown>; parentId: string | undefined; lastEditedAt: number | undefined; lastEditedBy: string | null; targetIndex: number }> = [];
442
452
 
443
453
  for (const blockId of blockIds) {
444
454
  if (this.repository.getBlockById(blockId) !== undefined) {
@@ -454,13 +464,15 @@ export class BlockYjsSync {
454
464
  const toolName = yblock.get('type') as string;
455
465
  const data = this.dependencies.YjsManager.yMapToObject(yblock.get('data') as YMap<unknown>);
456
466
  const parentId = yblock.get('parentId') as string | undefined;
467
+ const lastEditedAt = yblock.get('lastEditedAt') as number | undefined;
468
+ const lastEditedBy = (yblock.get('lastEditedBy') as string | undefined) ?? null;
457
469
  const targetIndex = yjsBlocks.findIndex((b) => b.id === blockId);
458
470
 
459
471
  if (targetIndex === -1) {
460
472
  continue;
461
473
  }
462
474
 
463
- toCreate.push({ blockId, toolName, data, parentId: parentId ?? undefined, targetIndex });
475
+ toCreate.push({ blockId, toolName, data, parentId: parentId ?? undefined, lastEditedAt, lastEditedBy, targetIndex });
464
476
  }
465
477
 
466
478
  if (toCreate.length === 0) {
@@ -478,6 +490,8 @@ export class BlockYjsSync {
478
490
  data: entry.data,
479
491
  parentId: entry.parentId,
480
492
  bindEventsImmediately: true,
493
+ lastEditedAt: entry.lastEditedAt,
494
+ lastEditedBy: entry.lastEditedBy,
481
495
  });
482
496
 
483
497
  this.blocksStore.addToArray(entry.targetIndex, block);
@@ -85,7 +85,9 @@ function isDefaultBlack(color: string): boolean {
85
85
  }
86
86
 
87
87
  /**
88
- * Compute the relative luminance of a CSS color value (rgb() or hex format).
88
+ * Compute the relative luminance of a CSS color value.
89
+ * Supports rgb(), rgba(), hsl(), hsla(), and hex (#rrggbb / #rgb) formats.
90
+ * Alpha components are ignored — only the base RGB channels are used.
89
91
  * Returns a value in [0, 1], or -1 if the format is unrecognized.
90
92
  * Uses simplified linear luminance (no gamma correction), adequate for
91
93
  * threshold comparisons at this scale.
@@ -93,7 +95,8 @@ function isDefaultBlack(color: string): boolean {
93
95
  function computeRelativeLuminance(color: string): number {
94
96
  const normalized = color.replace(/\s/g, '').toLowerCase();
95
97
 
96
- const rgbMatch = /^rgb\((\d+),(\d+),(\d+)\)$/.exec(normalized);
98
+ /* rgb() and rgba() — alpha component is optional and ignored */
99
+ const rgbMatch = /^rgba?\((\d+),(\d+),(\d+)(?:,[\d.]+)?\)$/.exec(normalized);
97
100
 
98
101
  if (rgbMatch) {
99
102
  const r = parseInt(rgbMatch[1], 10) / 255;
@@ -103,6 +106,42 @@ function computeRelativeLuminance(color: string): number {
103
106
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
104
107
  }
105
108
 
109
+ /* hsl() and hsla() — alpha component is optional and ignored */
110
+ const hslMatch = /^hsla?\(([\d.]+),([\d.]+)%,([\d.]+)%(?:,[\d.]+)?\)$/.exec(normalized);
111
+
112
+ if (hslMatch) {
113
+ const h = parseFloat(hslMatch[1]) / 360;
114
+ const s = parseFloat(hslMatch[2]) / 100;
115
+ const l = parseFloat(hslMatch[3]) / 100;
116
+
117
+ if (s === 0) {
118
+ return 0.2126 * l + 0.7152 * l + 0.0722 * l; // achromatic: r = g = b = l
119
+ }
120
+
121
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
122
+ const p = 2 * l - q;
123
+ const hueToChannel = (t: number): number => {
124
+ const wrapped = (() => {
125
+ if (t < 0) return t + 1;
126
+ if (t > 1) return t - 1;
127
+
128
+ return t;
129
+ })();
130
+
131
+ if (wrapped < 1 / 6) { return p + (q - p) * 6 * wrapped; }
132
+ if (wrapped < 1 / 2) { return q; }
133
+ if (wrapped < 2 / 3) { return p + (q - p) * (2 / 3 - wrapped) * 6; }
134
+
135
+ return p;
136
+ };
137
+
138
+ const r = hueToChannel(h + 1 / 3);
139
+ const g = hueToChannel(h);
140
+ const b = hueToChannel(h - 1 / 3);
141
+
142
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
143
+ }
144
+
106
145
  const hexMatch = /^#([0-9a-f]{6}|[0-9a-f]{3})$/.exec(normalized);
107
146
 
108
147
  if (hexMatch) {
@@ -156,9 +195,16 @@ function isDefaultDarkBackground(bgColor: string): boolean {
156
195
  *
157
196
  * Uses relative luminance > 0.75, which is above all Blok text presets while
158
197
  * catching typical dark mode default text colors.
198
+ *
199
+ * Returns false for unrecognized color formats (luminance === -1) so unknown
200
+ * formats are treated conservatively: they are not filtered out here, but any
201
+ * color that cannot be parsed also cannot be mapped to a preset, so the
202
+ * sanitizer will strip it regardless.
159
203
  */
160
204
  function isDefaultLightText(color: string): boolean {
161
- return computeRelativeLuminance(color) > 0.75;
205
+ const luminance = computeRelativeLuminance(color);
206
+
207
+ return luminance >= 0 && luminance > 0.75;
162
208
  }
163
209
 
164
210
  /**
@@ -41,10 +41,20 @@ export class TableCellsHandler extends BasePasteHandler implements PasteHandler
41
41
  return false;
42
42
  }
43
43
 
44
- // If cursor is inside a table cell, let the grid paste listener handle it
44
+ // If cursor is inside a table cell, let the grid paste listener handle it.
45
+ // Also bail when focus was lost (e.g. due to a React re-render) but the
46
+ // current block's holder is still inside a table-cell-blocks container —
47
+ // the paste was intended for the table, not for creating a new one.
48
+ // Additionally, bail when the paste event's original target was inside a
49
+ // table cell, even if document.activeElement is now body (covers the case
50
+ // where the event fired on the cell-blocks container itself).
45
51
  const activeElement = document.activeElement as HTMLElement | null;
46
52
 
47
- if (activeElement?.closest('[data-blok-table-cell]')) {
53
+ if (
54
+ activeElement?.closest('[data-blok-table-cell]') ||
55
+ context.currentBlock?.holder?.closest('[data-blok-table-cell-blocks]') ||
56
+ context.pasteTarget?.closest('[data-blok-table-cell]')
57
+ ) {
48
58
  return false;
49
59
  }
50
60
 
@@ -103,13 +103,13 @@ export class Paste extends Module {
103
103
  /**
104
104
  * Handle pasted data transfer object.
105
105
  */
106
- public async processDataTransfer(dataTransfer: DataTransfer): Promise<void> {
106
+ public async processDataTransfer(dataTransfer: DataTransfer, pasteTarget?: Element): Promise<void> {
107
107
  const blokData = dataTransfer.getData(this.MIME_TYPE);
108
108
  const plainData = dataTransfer.getData('text/plain');
109
109
  const rawHtmlData = dataTransfer.getData('text/html');
110
110
 
111
111
  // Route to handlers based on data type
112
- const handled = await this.routeToHandlers(dataTransfer, plainData, rawHtmlData, blokData);
112
+ const handled = await this.routeToHandlers(dataTransfer, plainData, rawHtmlData, blokData, pasteTarget);
113
113
 
114
114
  if (handled) {
115
115
  return;
@@ -126,7 +126,8 @@ export class Paste extends Module {
126
126
  dataTransfer: DataTransfer,
127
127
  plainData: string,
128
128
  rawHtmlData: string,
129
- blokData: string
129
+ blokData: string,
130
+ pasteTarget?: Element
130
131
  ): Promise<boolean> {
131
132
  const { BlockManager } = this.Blok;
132
133
  const currentBlock = BlockManager.currentBlock;
@@ -142,6 +143,7 @@ export class Paste extends Module {
142
143
  canReplaceCurrentBlock,
143
144
  currentBlock: currentBlock ?? undefined,
144
145
  plainData,
146
+ pasteTarget,
145
147
  };
146
148
 
147
149
  // Try handlers in priority order
@@ -313,7 +315,9 @@ export class Paste extends Module {
313
315
  event.preventDefault();
314
316
 
315
317
  if (event.clipboardData) {
316
- await this.processDataTransfer(event.clipboardData);
318
+ const pasteTarget = event.target instanceof Element ? event.target : undefined;
319
+
320
+ await this.processDataTransfer(event.clipboardData, pasteTarget);
317
321
  }
318
322
 
319
323
  Toolbar.moveAndOpen();
@@ -50,6 +50,8 @@ export interface HandlerContext {
50
50
  canReplaceCurrentBlock: boolean;
51
51
  currentBlock?: Block;
52
52
  plainData?: string;
53
+ /** The DOM element that was the target of the paste event. */
54
+ pasteTarget?: Element;
53
55
  }
54
56
 
55
57
  /**
@@ -3,7 +3,7 @@ import type { StubData } from '../../tools/stub';
3
3
  import { Module } from '../__module';
4
4
  import type { Block } from '../block';
5
5
  import type { BlockToolAdapter } from '../tools/block';
6
- import { log, logLabeled } from '../utils';
6
+ import { generateBlockId, log, logLabeled } from '../utils';
7
7
  import {
8
8
  analyzeDataFormat,
9
9
  expandToHierarchical,
@@ -94,11 +94,27 @@ export class Renderer extends Module {
94
94
 
95
95
  // Note: Yjs data layer is loaded via BlockManager.insertMany() with the correct block IDs
96
96
 
97
+ /**
98
+ * Track seen IDs to detect and resolve duplicates
99
+ */
100
+ const seenIds = new Set<string>();
101
+
97
102
  /**
98
103
  * Create Blocks instances
99
104
  */
100
105
  const blocks = processedBlocks.map((blockData: OutputBlockData) => {
101
- const { tunes, id, parent, content } = blockData;
106
+ const { tunes, parent, content, lastEditedAt, lastEditedBy } = blockData;
107
+ const hasDuplicateId = blockData.id !== undefined && seenIds.has(blockData.id);
108
+
109
+ if (hasDuplicateId) {
110
+ logLabeled(`Duplicate block id «${blockData.id}» replaced with a generated id to ensure uniqueness`, 'warn');
111
+ }
112
+
113
+ const id = hasDuplicateId ? generateBlockId() : blockData.id;
114
+
115
+ if (id !== undefined) {
116
+ seenIds.add(id);
117
+ }
102
118
  const originalTool = blockData.type;
103
119
 
104
120
  /**
@@ -145,6 +161,8 @@ export class Renderer extends Module {
145
161
  tunes,
146
162
  parentId: parent,
147
163
  contentIds: content,
164
+ lastEditedAt,
165
+ lastEditedBy,
148
166
  });
149
167
  } catch (error) {
150
168
  log(`Block «${tool}» skipped because of plugins error`, 'error', {
@@ -164,6 +182,8 @@ export class Renderer extends Module {
164
182
  tunes,
165
183
  parentId: parent,
166
184
  contentIds: content,
185
+ lastEditedAt,
186
+ lastEditedBy,
167
187
  });
168
188
  }
169
189
  };
@@ -24,6 +24,14 @@ type SaverValidatedData = ValidatedData & {
24
24
  * Array of child block ids (Notion-like flat-with-references model)
25
25
  */
26
26
  contentIds?: string[];
27
+ /**
28
+ * Timestamp of the last edit to this block
29
+ */
30
+ lastEditedAt?: number;
31
+ /**
32
+ * Identifier of the user who last edited this block
33
+ */
34
+ lastEditedBy?: string | null;
27
35
  };
28
36
 
29
37
  type SanitizableBlockData = SaverValidatedData & Pick<SavedData, 'data' | 'tool'>;
@@ -163,6 +171,8 @@ export class Saver extends Module {
163
171
  isValid,
164
172
  parentId: block.parentId,
165
173
  contentIds: block.contentIds,
174
+ lastEditedAt: block.lastEditedAt,
175
+ lastEditedBy: block.lastEditedBy,
166
176
  };
167
177
  }
168
178
 
@@ -174,7 +184,7 @@ export class Saver extends Module {
174
184
  private makeOutput(allExtractedData: SaverValidatedData[]): OutputData {
175
185
  const extractedBlocks: OutputData['blocks'] = [];
176
186
 
177
- allExtractedData.forEach(({ id, tool, data, tunes, isValid, parentId, contentIds }) => {
187
+ allExtractedData.forEach(({ id, tool, data, tunes, isValid, parentId, contentIds, lastEditedAt, lastEditedBy }) => {
178
188
  const hasParent = parentId !== undefined && parentId !== null;
179
189
 
180
190
  if (!isValid && !hasParent) {
@@ -204,6 +214,8 @@ export class Saver extends Module {
204
214
 
205
215
  const isTunesEmpty = tunes === undefined || isEmpty(tunes);
206
216
  const hasContent = contentIds !== undefined && contentIds.length > 0;
217
+ const hasLastEdited = lastEditedAt !== undefined;
218
+ const hasLastEditedBy = lastEditedBy !== undefined && lastEditedBy !== null;
207
219
 
208
220
  const output: OutputData['blocks'][number] = {
209
221
  id,
@@ -218,6 +230,12 @@ export class Saver extends Module {
218
230
  ...hasContent && {
219
231
  content: contentIds,
220
232
  },
233
+ ...hasLastEdited && {
234
+ lastEditedAt,
235
+ },
236
+ ...hasLastEditedBy && {
237
+ lastEditedBy,
238
+ },
221
239
  };
222
240
 
223
241
  extractedBlocks.push(output);
@@ -108,7 +108,9 @@ export class ThemeManager extends Module {
108
108
  }
109
109
 
110
110
  if (this.mode === 'auto') {
111
- this.removeAttribute();
111
+ if (state.activeInstances <= 1) {
112
+ this.removeAttribute();
113
+ }
112
114
  } else {
113
115
  document.documentElement.setAttribute(ATTR, this.mode);
114
116
  }
@@ -191,7 +191,6 @@ export class BlockSettings extends Module<BlockSettingsNodes> {
191
191
  searchable: true,
192
192
  trigger: trigger || this.nodes.wrapper,
193
193
  items: await this.getTunesItems(block, commonTunes, toolTunes),
194
- scopeElement: this.Blok.API.methods.ui.nodes.redactor,
195
194
  messages: {
196
195
  nothingFound: this.Blok.I18n.t('popover.nothingFound'),
197
196
  search: this.Blok.I18n.t('popover.search'),
@@ -431,9 +430,60 @@ export class BlockSettings extends Module<BlockSettingsNodes> {
431
430
  });
432
431
  }
433
432
 
433
+ if (currentBlock.lastEditedAt !== undefined) {
434
+ items.push({
435
+ type: PopoverItemType.Separator,
436
+ });
437
+ items.push({
438
+ type: PopoverItemType.Html,
439
+ element: this.createEditMetadataFooter(currentBlock),
440
+ name: 'edit-metadata',
441
+ });
442
+ }
443
+
434
444
  return items;
435
445
  }
436
446
 
447
+ /**
448
+ * Creates the "Last edited by..." footer element for block settings.
449
+ * @param block - the block whose metadata to display
450
+ * @returns the footer DOM element
451
+ */
452
+ private createEditMetadataFooter(block: Block): HTMLElement {
453
+ const container = document.createElement('div');
454
+
455
+ container.classList.add(
456
+ 'px-3', 'py-2', 'text-xs', 'leading-snug',
457
+ 'text-[color:var(--popover-text-secondary,_#888)]',
458
+ 'select-none'
459
+ );
460
+
461
+ const label = document.createElement('div');
462
+
463
+ label.setAttribute('data-edit-meta-label', '');
464
+
465
+ if (block.lastEditedBy !== null && block.lastEditedBy !== undefined) {
466
+ label.textContent = `Last edited by ${block.lastEditedBy}`;
467
+ } else {
468
+ label.textContent = 'Last edited';
469
+ }
470
+
471
+ container.appendChild(label);
472
+
473
+ if (block.lastEditedAt !== undefined) {
474
+ const dateEl = document.createElement('div');
475
+ const formatted = new Intl.DateTimeFormat(undefined, {
476
+ dateStyle: 'medium',
477
+ timeStyle: 'short',
478
+ }).format(new Date(block.lastEditedAt));
479
+
480
+ dateEl.textContent = formatted;
481
+ container.appendChild(dateEl);
482
+ }
483
+
484
+ return container;
485
+ }
486
+
437
487
  /**
438
488
  * Handles popover close event
439
489
  */