@jackuait/blok 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-BfcBwAfE.mjs → blok-u_68bnlk.mjs} +1489 -1486
  3. package/dist/chunks/{constants-QNVyXALL.mjs → constants-VDhCUk4c.mjs} +55 -48
  4. package/dist/chunks/{i18next-loader-CwsYu0n6.mjs → i18next-loader-CDnSPae_.mjs} +1 -1
  5. package/dist/chunks/{lightweight-i18n-Cvv8CWh4.mjs → lightweight-i18n-DZmo8dAI.mjs} +1 -0
  6. package/dist/chunks/{messages-DG-4DPmP.mjs → messages-1_6UkKLS.mjs} +1 -0
  7. package/dist/{messages-CqXtJTpU.mjs → chunks/messages-4Ck88DYZ2.mjs} +1 -0
  8. package/dist/{messages-96iaAUds2.mjs → chunks/messages-8Ld7P_9j2.mjs} +1 -0
  9. package/dist/{messages-DLX_iBDJ.mjs → chunks/messages-BAlZjPcl.mjs} +1 -0
  10. package/dist/chunks/{messages-p1mbe__S.mjs → messages-BHMiK51R.mjs} +1 -0
  11. package/dist/chunks/{messages-Cdf0W9H02.mjs → messages-BJ-vT1SU2.mjs} +1 -0
  12. package/dist/{messages-Smt4GBbj.mjs → chunks/messages-BK8Cp2d0.mjs} +1 -0
  13. package/dist/chunks/{messages-Bphq_Bt3.mjs → messages-BKN3YVIj.mjs} +1 -0
  14. package/dist/{messages-DTh9a8mR.mjs → chunks/messages-BMD37y3q2.mjs} +1 -0
  15. package/dist/chunks/{messages-DlonA3wa.mjs → messages-BONyZroH.mjs} +1 -0
  16. package/dist/chunks/{messages-DMVXnAYj.mjs → messages-BRAoJpOu.mjs} +1 -0
  17. package/dist/chunks/{messages-C6Mpiacw.mjs → messages-BRoa9tGl.mjs} +1 -0
  18. package/dist/{messages-aWZH50vu2.mjs → chunks/messages-BbEW9bQz.mjs} +1 -0
  19. package/dist/{messages-BIHc0KHY.mjs → chunks/messages-BeGZqQwz.mjs} +1 -0
  20. package/dist/chunks/{messages-C7VGpihw.mjs → messages-BfAcUavP.mjs} +1 -0
  21. package/dist/chunks/{messages-Cu-Wevxs2.mjs → messages-BgM91Lxm2.mjs} +1 -0
  22. package/dist/{messages-BYNcD6uR.mjs → chunks/messages-BlxwW7M6.mjs} +1 -0
  23. package/dist/chunks/{messages-rCd0Rrw6.mjs → messages-Bz0-KNEB.mjs} +1 -0
  24. package/dist/{messages-7QD-X6XT2.mjs → chunks/messages-C0IFfhnp.mjs} +1 -0
  25. package/dist/chunks/{messages-Dr0Ekmbz.mjs → messages-C15z2r5U.mjs} +1 -0
  26. package/dist/chunks/{messages-MxpWO1db.mjs → messages-C1S9ztpF.mjs} +1 -0
  27. package/dist/chunks/{messages-8IHf7ZP3.mjs → messages-CC_noR8y.mjs} +1 -0
  28. package/dist/chunks/{messages-COO5xmcA.mjs → messages-CD_MnBln.mjs} +1 -0
  29. package/dist/{messages-BiiongNz2.mjs → chunks/messages-CIfUm1Oa.mjs} +1 -0
  30. package/dist/chunks/{messages-B9kmbUWV.mjs → messages-CPBN4zWc.mjs} +1 -0
  31. package/dist/chunks/{messages-DlLXpgWM2.mjs → messages-CQBo3lmL2.mjs} +1 -0
  32. package/dist/{messages-DnGJD4TL.mjs → chunks/messages-CRF7nNrO.mjs} +1 -0
  33. package/dist/{messages-D8FQWulF2.mjs → chunks/messages-CTCe595D2.mjs} +1 -0
  34. package/dist/{messages-BRZX964b2.mjs → chunks/messages-CW35K1pq.mjs} +1 -0
  35. package/dist/chunks/{messages-CVdpweyf2.mjs → messages-CZSlfnkO2.mjs} +1 -0
  36. package/dist/chunks/{messages-iS34FHFB.mjs → messages-ChK7v1PV.mjs} +1 -0
  37. package/dist/chunks/{messages-BjadX8jR2.mjs → messages-Clku7Cf-2.mjs} +1 -0
  38. package/dist/{messages-DIJlIqlQ2.mjs → chunks/messages-CszmHAvQ.mjs} +1 -0
  39. package/dist/{messages-D0aw5_0k2.mjs → chunks/messages-CvANwuht2.mjs} +1 -0
  40. package/dist/chunks/{messages-BQYvBqm2.mjs → messages-CxiURE2X.mjs} +1 -0
  41. package/dist/chunks/{messages-A_MkXDlG.mjs → messages-CxxyR4vY.mjs} +1 -0
  42. package/dist/chunks/{messages-BUVhHx0q2.mjs → messages-D22e9h7V2.mjs} +1 -0
  43. package/dist/{messages-DYTTu0O12.mjs → chunks/messages-D7dx_6k8.mjs} +1 -0
  44. package/dist/chunks/{messages-BA8Iv99Y2.mjs → messages-DBMaLL8b2.mjs} +1 -0
  45. package/dist/{messages-DbySKTKt2.mjs → chunks/messages-DB_-5Xln.mjs} +1 -0
  46. package/dist/chunks/{messages-JyvWu4rf2.mjs → messages-DEBy3nuJ2.mjs} +1 -0
  47. package/dist/chunks/{messages-NEqrrYvE2.mjs → messages-DMoERagV2.mjs} +1 -0
  48. package/dist/{messages-Dy-Y_nEI.mjs → chunks/messages-DPzHD51Y.mjs} +1 -0
  49. package/dist/chunks/{messages-jfVpL9c-2.mjs → messages-DSrdy9Nw2.mjs} +1 -0
  50. package/dist/chunks/{messages-Cmf6NhSC.mjs → messages-DTN1XGll.mjs} +1 -0
  51. package/dist/chunks/{messages-BE6lHKwf.mjs → messages-DUeiPraX.mjs} +1 -0
  52. package/dist/{messages-Ddnj2iTG2.mjs → chunks/messages-DUr9WAkD.mjs} +1 -0
  53. package/dist/chunks/{messages-Ck81cQkn2.mjs → messages-DVr1sqfI2.mjs} +1 -0
  54. package/dist/chunks/{messages-aZcy0JQq2.mjs → messages-DjvaiALg2.mjs} +1 -0
  55. package/dist/{messages-DkLU_rWm.mjs → chunks/messages-DrfRYiM32.mjs} +1 -0
  56. package/dist/{messages-Dw__BcTj.mjs → chunks/messages-DtoId_bw2.mjs} +1 -0
  57. package/dist/{messages-Bq3F2Tp_.mjs → chunks/messages-Du2BffA7.mjs} +1 -0
  58. package/dist/{messages-CjbnogEC.mjs → chunks/messages-DxHh0O8j2.mjs} +1 -0
  59. package/dist/{messages-BECMxmfX.mjs → chunks/messages-EDMC5ukV.mjs} +1 -0
  60. package/dist/{messages-BTQPpoM42.mjs → chunks/messages-ElIGUi0O2.mjs} +1 -0
  61. package/dist/{messages-B1ZUQagA.mjs → chunks/messages-JSQjKQ8I.mjs} +1 -0
  62. package/dist/{messages-CWIXvnDf2.mjs → chunks/messages-Q7-4ZJLB2.mjs} +1 -0
  63. package/dist/{messages-BJeGJksD.mjs → chunks/messages-QMOmwcZb.mjs} +1 -0
  64. package/dist/chunks/{messages-xuqyb6Ff2.mjs → messages-QilfinOn2.mjs} +1 -0
  65. package/dist/{messages-BmAn22OX.mjs → chunks/messages-a07QVz8U.mjs} +1 -0
  66. package/dist/{messages-nlhESX9t.mjs → chunks/messages-eFd4YYzt.mjs} +1 -0
  67. package/dist/{messages-Dnp9N6RU2.mjs → chunks/messages-euM2m3wQ.mjs} +1 -0
  68. package/dist/chunks/{messages-BVjoM7P0.mjs → messages-kGmxkeFH.mjs} +1 -0
  69. package/dist/chunks/{messages-D9N2MvQx2.mjs → messages-oMc7qugU2.mjs} +1 -0
  70. package/dist/{messages-Dvn35ksS.mjs → chunks/messages-sDdNf8O9.mjs} +1 -0
  71. package/dist/{messages-Bm0Feca1.mjs → chunks/messages-wl8YrvGG.mjs} +1 -0
  72. package/dist/{messages-j7o5rT9s.mjs → chunks/messages-zt6zdYWh.mjs} +1 -0
  73. package/dist/chunks/{tools-DHtzbrxy.mjs → tools-1ZFajlGN.mjs} +1022 -893
  74. package/dist/full.mjs +3 -3
  75. package/dist/locales.mjs +68 -67
  76. package/dist/{messages-F2xRoY1w.mjs → messages-2ZWBTerL.mjs} +1 -0
  77. package/dist/{messages-Dl3Sv6Rq2.mjs → messages-53w0fPZS2.mjs} +1 -0
  78. package/dist/{chunks/messages-BDZA10kl2.mjs → messages-98nQiC7t2.mjs} +1 -0
  79. package/dist/{messages-CcF4y-E4.mjs → messages-A96tMxeU.mjs} +1 -0
  80. package/dist/{messages-Ce6KVEbT.mjs → messages-BE_z-zrb.mjs} +1 -0
  81. package/dist/{chunks/messages-CSJ_zb3a2.mjs → messages-BK_LsgY4.mjs} +1 -0
  82. package/dist/{messages-CJTy6JZt.mjs → messages-BbJ7ZXY8.mjs} +1 -0
  83. package/dist/{messages-BwHs4cm1.mjs → messages-BcVB3osF.mjs} +1 -0
  84. package/dist/{chunks/messages-CSL-6xfb2.mjs → messages-BckDk9aq2.mjs} +1 -0
  85. package/dist/{chunks/messages-C0HvoMPb.mjs → messages-Be_2RHZD.mjs} +1 -0
  86. package/dist/{messages-Dl5Y2-Ia.mjs → messages-BesJaI6A.mjs} +1 -0
  87. package/dist/{chunks/messages-D3zojZ94.mjs → messages-BiTMwiKH.mjs} +1 -0
  88. package/dist/{chunks/messages-BhzzNkN-.mjs → messages-BmH2cQHQ.mjs} +1 -0
  89. package/dist/{chunks/messages-Bfnq1xv4.mjs → messages-BrOWqNCu2.mjs} +1 -0
  90. package/dist/{chunks/messages-BSghd0ez.mjs → messages-Brd5R-da2.mjs} +1 -0
  91. package/dist/{chunks/messages-DJoNVjqP.mjs → messages-C0GSBBCo2.mjs} +1 -0
  92. package/dist/{chunks/messages-B2N4fUi72.mjs → messages-C1vc5584.mjs} +1 -0
  93. package/dist/{messages-aMXpHt5X2.mjs → messages-C6ONf71u2.mjs} +1 -0
  94. package/dist/{chunks/messages-BeFqtIrc2.mjs → messages-C7lJg8fy2.mjs} +1 -0
  95. package/dist/{messages-CSUHBs4c2.mjs → messages-CRNogopy2.mjs} +1 -0
  96. package/dist/{messages-DLlc9QPw.mjs → messages-CT-Kdas6.mjs} +1 -0
  97. package/dist/{messages-DPA-mMWC2.mjs → messages-CTTmWn4Y2.mjs} +1 -0
  98. package/dist/{chunks/messages-DWu1r4gc2.mjs → messages-CZbcxlZt2.mjs} +1 -0
  99. package/dist/{chunks/messages-Bp8qin1R.mjs → messages-C_Qn9SbQ.mjs} +1 -0
  100. package/dist/{chunks/messages-DGL1ySqb2.mjs → messages-CdEASHDp2.mjs} +1 -0
  101. package/dist/{chunks/messages-Bxvi1ebN.mjs → messages-CdduYw-q.mjs} +1 -0
  102. package/dist/{chunks/messages-Je5YvxiY.mjs → messages-Che99vKP.mjs} +1 -0
  103. package/dist/{chunks/messages-BYxLFj7y.mjs → messages-CisR4PNV.mjs} +1 -0
  104. package/dist/{messages-5jvKxQNu2.mjs → messages-ClGvlFcH2.mjs} +1 -0
  105. package/dist/{chunks/messages-FWfsxpBz.mjs → messages-CnuH-BZK2.mjs} +1 -0
  106. package/dist/{messages-BYmmMDrN2.mjs → messages-D0005ti32.mjs} +1 -0
  107. package/dist/{messages-B7ieAJBd2.mjs → messages-D05jqBIa2.mjs} +1 -0
  108. package/dist/{chunks/messages-BXM80fdr2.mjs → messages-D0lLw9KM.mjs} +1 -0
  109. package/dist/{chunks/messages-ihCjSFJI2.mjs → messages-D3rwCtKn.mjs} +1 -0
  110. package/dist/{messages-hWwSRF-2.mjs → messages-D6VIFnSW.mjs} +1 -0
  111. package/dist/{chunks/messages-KdawW5Na.mjs → messages-D81w6AmW.mjs} +1 -0
  112. package/dist/{messages-ynAe7ewZ.mjs → messages-DBhvm8NK.mjs} +1 -0
  113. package/dist/{messages-CmB406HW.mjs → messages-DK6dA0O2.mjs} +1 -0
  114. package/dist/{messages-rk-A1Wa42.mjs → messages-DKHbt-7l2.mjs} +1 -0
  115. package/dist/{messages-BIoeoik5.mjs → messages-DM4Gjc9h.mjs} +1 -0
  116. package/dist/{messages-BbYq1pk-.mjs → messages-DODrhcop.mjs} +1 -0
  117. package/dist/{messages-BsycN_JI2.mjs → messages-DOGbHYv-2.mjs} +1 -0
  118. package/dist/{messages-nUVjeh7K.mjs → messages-DQORja0D.mjs} +1 -0
  119. package/dist/{messages-DnG0ef8t2.mjs → messages-DSmxJWju2.mjs} +1 -0
  120. package/dist/{messages-CR_L_UtK.mjs → messages-DVL0KZE5.mjs} +1 -0
  121. package/dist/{chunks/messages-Cs81Z_Bh.mjs → messages-DYuD5-rO.mjs} +1 -0
  122. package/dist/{chunks/messages-CKBhDGI3.mjs → messages-Ddq3Ce3E2.mjs} +1 -0
  123. package/dist/{messages-xh2eOLvs.mjs → messages-DfFZ6Yj5.mjs} +1 -0
  124. package/dist/{messages-DY4IqlhY.mjs → messages-Dnd5YSWv.mjs} +1 -0
  125. package/dist/{chunks/messages-DOuS1Qge.mjs → messages-Do7Xjy0n.mjs} +1 -0
  126. package/dist/{chunks/messages-C3aX3q0H.mjs → messages-DopaMHC42.mjs} +1 -0
  127. package/dist/{messages-dv19AkyJ.mjs → messages-DpJGbx3q.mjs} +1 -0
  128. package/dist/{messages-DBiVgUs2.mjs → messages-DpwMKDV0.mjs} +1 -0
  129. package/dist/{chunks/messages-BAsb5CgZ.mjs → messages-Dqu4aX9s.mjs} +1 -0
  130. package/dist/{messages-BiUGXvYr2.mjs → messages-E8NjqzWq2.mjs} +1 -0
  131. package/dist/{chunks/messages-RInp1ytX.mjs → messages-JNrYldAa2.mjs} +1 -0
  132. package/dist/{chunks/messages-B4UMuyjT.mjs → messages-LMaR2_bE.mjs} +1 -0
  133. package/dist/{messages-E_ZuzGDt.mjs → messages-LYJbLq_F.mjs} +1 -0
  134. package/dist/{messages-DMr62KiO2.mjs → messages-Q5sQeVap2.mjs} +1 -0
  135. package/dist/{messages-Ci0KqX-J.mjs → messages-Xc0KUbYl.mjs} +1 -0
  136. package/dist/{messages-B19o-Teb.mjs → messages-_PLyRfVw.mjs} +1 -0
  137. package/dist/{chunks/messages-TRUuyiFB.mjs → messages-apA6BStA.mjs} +1 -0
  138. package/dist/{chunks/messages-B0vPBsWq.mjs → messages-bkGniiaz.mjs} +1 -0
  139. package/dist/{chunks/messages-D55HRx5O2.mjs → messages-neGD3WGq.mjs} +1 -0
  140. package/dist/{chunks/messages-G416eyjY.mjs → messages-qbKjjvgd2.mjs} +1 -0
  141. package/dist/{chunks/messages-BYlSMRkd.mjs → messages-qfvXgPpu2.mjs} +1 -0
  142. package/dist/{chunks/messages-DzTk8bJ5.mjs → messages-uwK7ktqk.mjs} +1 -0
  143. package/dist/react.mjs +2 -2
  144. package/dist/tools.mjs +2 -2
  145. package/package.json +1 -1
  146. package/src/components/block/index.ts +14 -0
  147. package/src/components/i18n/locales/am/messages.json +1 -0
  148. package/src/components/i18n/locales/ar/messages.json +1 -0
  149. package/src/components/i18n/locales/az/messages.json +1 -0
  150. package/src/components/i18n/locales/bg/messages.json +1 -0
  151. package/src/components/i18n/locales/bn/messages.json +1 -0
  152. package/src/components/i18n/locales/bs/messages.json +1 -0
  153. package/src/components/i18n/locales/cs/messages.json +1 -0
  154. package/src/components/i18n/locales/da/messages.json +1 -0
  155. package/src/components/i18n/locales/de/messages.json +1 -0
  156. package/src/components/i18n/locales/dv/messages.json +1 -0
  157. package/src/components/i18n/locales/el/messages.json +1 -0
  158. package/src/components/i18n/locales/en/messages.json +1 -0
  159. package/src/components/i18n/locales/es/messages.json +1 -0
  160. package/src/components/i18n/locales/et/messages.json +1 -0
  161. package/src/components/i18n/locales/fa/messages.json +1 -0
  162. package/src/components/i18n/locales/fi/messages.json +1 -0
  163. package/src/components/i18n/locales/fil/messages.json +1 -0
  164. package/src/components/i18n/locales/fr/messages.json +1 -0
  165. package/src/components/i18n/locales/gu/messages.json +1 -0
  166. package/src/components/i18n/locales/he/messages.json +1 -0
  167. package/src/components/i18n/locales/hi/messages.json +1 -0
  168. package/src/components/i18n/locales/hr/messages.json +1 -0
  169. package/src/components/i18n/locales/hu/messages.json +1 -0
  170. package/src/components/i18n/locales/hy/messages.json +1 -0
  171. package/src/components/i18n/locales/id/messages.json +1 -0
  172. package/src/components/i18n/locales/it/messages.json +1 -0
  173. package/src/components/i18n/locales/ja/messages.json +1 -0
  174. package/src/components/i18n/locales/ka/messages.json +1 -0
  175. package/src/components/i18n/locales/km/messages.json +1 -0
  176. package/src/components/i18n/locales/kn/messages.json +1 -0
  177. package/src/components/i18n/locales/ko/messages.json +1 -0
  178. package/src/components/i18n/locales/ku/messages.json +1 -0
  179. package/src/components/i18n/locales/lo/messages.json +1 -0
  180. package/src/components/i18n/locales/lt/messages.json +1 -0
  181. package/src/components/i18n/locales/lv/messages.json +1 -0
  182. package/src/components/i18n/locales/mk/messages.json +1 -0
  183. package/src/components/i18n/locales/ml/messages.json +1 -0
  184. package/src/components/i18n/locales/mn/messages.json +1 -0
  185. package/src/components/i18n/locales/mr/messages.json +1 -0
  186. package/src/components/i18n/locales/ms/messages.json +1 -0
  187. package/src/components/i18n/locales/my/messages.json +1 -0
  188. package/src/components/i18n/locales/ne/messages.json +1 -0
  189. package/src/components/i18n/locales/nl/messages.json +1 -0
  190. package/src/components/i18n/locales/no/messages.json +1 -0
  191. package/src/components/i18n/locales/pa/messages.json +1 -0
  192. package/src/components/i18n/locales/pl/messages.json +1 -0
  193. package/src/components/i18n/locales/ps/messages.json +1 -0
  194. package/src/components/i18n/locales/pt/messages.json +1 -0
  195. package/src/components/i18n/locales/ro/messages.json +1 -0
  196. package/src/components/i18n/locales/ru/messages.json +1 -0
  197. package/src/components/i18n/locales/sd/messages.json +1 -0
  198. package/src/components/i18n/locales/si/messages.json +1 -0
  199. package/src/components/i18n/locales/sk/messages.json +1 -0
  200. package/src/components/i18n/locales/sl/messages.json +1 -0
  201. package/src/components/i18n/locales/sq/messages.json +1 -0
  202. package/src/components/i18n/locales/sr/messages.json +1 -0
  203. package/src/components/i18n/locales/sv/messages.json +1 -0
  204. package/src/components/i18n/locales/sw/messages.json +1 -0
  205. package/src/components/i18n/locales/ta/messages.json +1 -0
  206. package/src/components/i18n/locales/te/messages.json +1 -0
  207. package/src/components/i18n/locales/th/messages.json +1 -0
  208. package/src/components/i18n/locales/tr/messages.json +1 -0
  209. package/src/components/i18n/locales/ug/messages.json +1 -0
  210. package/src/components/i18n/locales/uk/messages.json +1 -0
  211. package/src/components/i18n/locales/ur/messages.json +1 -0
  212. package/src/components/i18n/locales/vi/messages.json +1 -0
  213. package/src/components/i18n/locales/yi/messages.json +1 -0
  214. package/src/components/i18n/locales/zh/messages.json +1 -0
  215. package/src/components/icons/index.ts +22 -27
  216. package/src/components/modules/toolbar/positioning.ts +11 -2
  217. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +11 -0
  218. package/src/components/utils/popover/popover-abstract.ts +7 -0
  219. package/src/tools/code/constants.ts +24 -12
  220. package/src/tools/code/dom-builder.ts +122 -89
  221. package/src/tools/code/index.ts +251 -104
  222. package/src/tools/code/language-detector.ts +118 -0
  223. package/types/tools/block-tool.d.ts +10 -0
  224. package/types/utils/popover/popover-item.d.ts +6 -0
@@ -10,32 +10,37 @@ import type {
10
10
  } from '../../../types';
11
11
  import type { MenuConfig } from '../../../types/tools/menu-config';
12
12
  import type { CodeData } from '../../../types/tools/code';
13
- import { IconCodeBlock } from '../../components/icons';
14
- import { buildCodeDOM } from './dom-builder';
13
+ import { IconCodeBlock, IconCheck, IconWand } from '../../components/icons';
14
+ import { buildCodeDOM, setActiveViewMode } from './dom-builder';
15
15
  import type { CodeDOMRefs } from './dom-builder';
16
16
  import { handleCodeKeydown } from './code-keyboard';
17
17
  import { PopoverDesktop } from '../../components/utils/popover';
18
+ import { onHover as tooltipOnHover } from '../../components/utils/tooltip';
18
19
  import type { PopoverItemParams } from '@/types/utils/popover/popover-item';
20
+ import { PopoverItemType } from '@/types/utils/popover/popover-item-type';
19
21
  import {
20
22
  DEFAULT_LANGUAGE,
21
23
  LANGUAGES,
22
- CODE_AREA_STYLES,
23
24
  COPY_CODE_KEY,
24
- WRAP_LINES_KEY,
25
- LINE_NUMBERS_KEY,
26
25
  COPIED_KEY,
27
26
  LANGUAGE_KEY,
28
27
  SEARCH_LANGUAGE_KEY,
29
28
  COPIED_FEEDBACK_STYLES,
30
29
  PREVIEWABLE_LANGUAGES,
31
- PREVIEW_TOGGLE_KEY,
30
+ CODE_TAB_KEY,
31
+ PREVIEW_TAB_KEY,
32
+ SIDE_BY_SIDE_KEY,
32
33
  PREVIEW_AREA_STYLES,
33
34
  GUTTER_LINE_STYLES,
35
+ SPLIT_CONTAINER_STYLES,
36
+ SPLIT_CONTAINER_SPLIT_STYLES,
34
37
  } from './constants';
38
+ import type { CodeViewMode } from './constants';
35
39
  import { renderLatex } from './katex-loader';
36
40
  import { renderMermaid } from './mermaid-loader';
37
41
  import { tokenizeCode, isHighlightable } from './shiki-loader';
38
42
  import { applyHighlights, isHighlightingSupported } from './highlight-applier';
43
+ import { detectLanguage } from './language-detector';
39
44
 
40
45
  const COPIED_FEEDBACK_DURATION = 1500;
41
46
 
@@ -44,13 +49,14 @@ export class CodeTool implements BlockTool {
44
49
  private readOnly: boolean;
45
50
  private _data: CodeData;
46
51
  private _dom: CodeDOMRefs | null = null;
47
- private _wrapping = true;
48
52
  private _lineNumbers = true;
49
53
  private _picker: PopoverDesktop | null = null;
50
- private _previewActive = false;
54
+ private _viewMode: CodeViewMode = 'preview';
51
55
  private _previewContainer: HTMLElement | null = null;
52
56
  private _disposeHighlights: (() => void) | null = null;
53
57
  private _highlightRafId: number | null = null;
58
+ private _detectedLanguage: string | null = null;
59
+ private _detectionTimeoutId: ReturnType<typeof setTimeout> | null = null;
54
60
 
55
61
  constructor({ data, api, readOnly }: BlockToolConstructorOptions<CodeData>) {
56
62
  this.api = api;
@@ -71,20 +77,18 @@ export class CodeTool implements BlockTool {
71
77
  languageName: this.getLanguageName(this._data.language),
72
78
  readOnly: this.readOnly,
73
79
  copyLabel: this.api.i18n.t(COPY_CODE_KEY),
74
- wrapLabel: this.api.i18n.t(WRAP_LINES_KEY),
75
- lineNumbersLabel: this.api.i18n.t(LINE_NUMBERS_KEY),
76
80
  previewable: this.readOnly ? false : isPreviewable,
77
- previewToggleLabel: this.api.i18n.t(PREVIEW_TOGGLE_KEY),
81
+ viewModeLabels: this.readOnly ? undefined : {
82
+ code: this.api.i18n.t(CODE_TAB_KEY),
83
+ preview: this.api.i18n.t(PREVIEW_TAB_KEY),
84
+ split: this.api.i18n.t(SIDE_BY_SIDE_KEY),
85
+ },
78
86
  });
79
87
 
80
88
  this._dom = dom;
81
89
 
82
90
  // Line numbers gutter visibility
83
91
  dom.gutterElement.hidden = !this._lineNumbers;
84
- dom.lineNumbersButton.addEventListener('click', () => this.toggleLineNumbers());
85
-
86
- // More menu toggle
87
- dom.moreButton.addEventListener('click', () => this.toggleMoreMenu());
88
92
 
89
93
  // Read-only + previewable: show preview only, hide code, no toggle
90
94
  if (this.readOnly && isPreviewable) {
@@ -99,16 +103,33 @@ export class CodeTool implements BlockTool {
99
103
  void this.renderPreview();
100
104
  }
101
105
 
102
- // Edit mode + previewable: show preview toggle, default to preview
103
- if (!this.readOnly && isPreviewable && dom.previewToggleButton && dom.previewElement) {
104
- this._previewActive = true;
105
- dom.preElement.hidden = true;
106
- dom.gutterElement.hidden = true;
107
- dom.previewElement.hidden = false;
106
+ // Edit mode + previewable: default to preview mode and render
107
+ if (!this.readOnly && isPreviewable && dom.previewElement) {
108
+ this._viewMode = 'preview';
108
109
  this._previewContainer = dom.previewElement;
110
+
111
+ // Apply initial state: preview mode
112
+ this.applyViewMode();
109
113
  void this.renderPreview();
114
+ }
110
115
 
111
- dom.previewToggleButton.addEventListener('click', () => this.togglePreview());
116
+ // Edit mode: wire view mode button listeners (buttons always present in edit mode)
117
+ if (!this.readOnly && dom.viewModeContainer) {
118
+ const modeButtons = Array.from(dom.viewModeContainer.querySelectorAll<HTMLButtonElement>('[data-mode]'));
119
+
120
+ for (const btn of modeButtons) {
121
+ const label = btn.getAttribute('aria-label') ?? '';
122
+
123
+ tooltipOnHover(btn, label, { placement: 'bottom' });
124
+
125
+ btn.addEventListener('click', () => {
126
+ const mode = btn.getAttribute('data-mode') as CodeViewMode;
127
+
128
+ if (mode && mode !== this._viewMode) {
129
+ this.setViewMode(mode);
130
+ }
131
+ });
132
+ }
112
133
  }
113
134
 
114
135
  if (!this.readOnly) {
@@ -120,6 +141,7 @@ export class CodeTool implements BlockTool {
120
141
  this.syncTrailingBr();
121
142
  this.updateGutter();
122
143
  this.scheduleHighlight();
144
+ this.scheduleDetection();
123
145
  }
124
146
  });
125
147
 
@@ -127,88 +149,99 @@ export class CodeTool implements BlockTool {
127
149
  this.syncTrailingBr();
128
150
  this.updateGutter();
129
151
  this.scheduleHighlight();
152
+ this.scheduleDetection();
130
153
  });
131
154
  }
132
155
 
133
156
  dom.copyButton.addEventListener('click', () => this.copyCode());
134
- dom.wrapButton.addEventListener('click', () => this.toggleWrap());
157
+ tooltipOnHover(dom.copyButton, this.api.i18n.t(COPY_CODE_KEY), { placement: 'bottom' });
135
158
 
136
159
  if (!this.readOnly) {
137
- const languageItems: PopoverItemParams[] = LANGUAGES.map((lang) => ({
138
- title: lang.name,
139
- name: lang.id,
140
- toggle: 'language',
141
- isActive: (): boolean => this._data.language === lang.id,
142
- closeOnActivate: true,
143
- onActivate: (): void => this.setLanguage(lang.id),
144
- }));
145
-
146
- this._picker = new PopoverDesktop({
147
- items: languageItems,
148
- trigger: dom.languageButton,
149
- leftAlignElement: dom.wrapper,
150
- searchable: true,
151
- width: '200px',
152
- messages: {
153
- search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
154
- },
155
- });
160
+ this._picker = this.buildLanguagePicker(dom.languageButton, dom.wrapper);
156
161
 
157
162
  dom.languageButton.addEventListener('click', () => {
158
- this._picker?.show();
163
+ if (this._picker?.isShown) {
164
+ this._picker.hide();
165
+ } else {
166
+ this._picker?.show();
167
+ }
159
168
  });
160
169
  }
161
170
 
162
171
  return dom.wrapper;
163
172
  }
164
173
 
174
+ /**
175
+ * Returns the wrapper element as the toolbar anchor so the toolbar
176
+ * centers on the block's visual top, not on the deeply nested
177
+ * contenteditable code element below the header bar.
178
+ */
179
+ public getToolbarAnchorElement(): HTMLElement | undefined {
180
+ return this._dom?.wrapper;
181
+ }
182
+
165
183
  public rendered(): void {
166
184
  void this.highlightCode();
167
185
  }
168
186
 
169
- private togglePreview(): void {
170
- if (this._previewActive) {
171
- this.showCode();
172
- } else {
173
- this.showPreview();
174
- }
175
- }
187
+ private setViewMode(mode: CodeViewMode): void {
188
+ this._viewMode = mode;
189
+ this.applyViewMode();
176
190
 
177
- private showCode(): void {
178
- if (!this._dom?.previewElement || !this._dom.previewToggleButton) {
179
- return;
191
+ if (mode === 'preview' || mode === 'split') {
192
+ void this.renderPreview();
180
193
  }
181
-
182
- this._previewActive = false;
183
- this._dom.preElement.hidden = false;
184
- this._dom.gutterElement.hidden = !this._lineNumbers;
185
- this._dom.previewElement.hidden = true;
186
194
  }
187
195
 
188
- private showPreview(): void {
189
- if (!this._dom?.previewElement || !this._dom.previewToggleButton) {
196
+ private applyViewMode(): void {
197
+ if (!this._dom?.previewElement || !this._dom.viewModeContainer || !this._dom.splitContainer) {
190
198
  return;
191
199
  }
192
200
 
193
- this._previewActive = true;
194
- this._dom.preElement.hidden = true;
195
- this._dom.gutterElement.hidden = true;
196
- this._dom.previewElement.hidden = false;
201
+ // Update segmented control active state
202
+ setActiveViewMode(this._dom.viewModeContainer, this._viewMode);
197
203
 
198
- // Re-render preview with current code content
199
- void this.renderPreview();
200
- }
204
+ const codeBody = this._dom.preElement.parentElement?.parentElement;
201
205
 
202
- private toggleMoreMenu(): void {
203
- if (!this._dom) {
204
- return;
206
+ switch (this._viewMode) {
207
+ case 'code':
208
+ this._dom.preElement.hidden = false;
209
+ this._dom.gutterElement.hidden = !this._lineNumbers;
210
+ this._dom.previewElement.hidden = true;
211
+ if (codeBody) {
212
+ codeBody.hidden = false;
213
+ }
214
+ this._dom.splitContainer.className = SPLIT_CONTAINER_STYLES;
215
+ break;
216
+
217
+ case 'preview':
218
+ this._dom.preElement.hidden = true;
219
+ this._dom.gutterElement.hidden = true;
220
+ this._dom.previewElement.hidden = false;
221
+ if (codeBody) {
222
+ codeBody.hidden = true;
223
+ }
224
+ this._dom.splitContainer.className = SPLIT_CONTAINER_STYLES;
225
+ break;
226
+
227
+ case 'split':
228
+ this._dom.preElement.hidden = false;
229
+ this._dom.gutterElement.hidden = !this._lineNumbers;
230
+ this._dom.previewElement.hidden = false;
231
+ if (codeBody) {
232
+ codeBody.hidden = false;
233
+ }
234
+ this._dom.splitContainer.className = SPLIT_CONTAINER_SPLIT_STYLES;
235
+ break;
205
236
  }
206
-
207
- this._dom.moreMenu.hidden = !this._dom.moreMenu.hidden;
208
237
  }
209
238
 
210
239
  private async renderPreview(): Promise<void> {
211
- if (!this._previewContainer) {
240
+ // Capture the container reference before the async gap so that if the language
241
+ // changes mid-flight (nulling _previewContainer), we don't write to null.
242
+ const container = this._previewContainer;
243
+
244
+ if (!container) {
212
245
  return;
213
246
  }
214
247
 
@@ -217,7 +250,7 @@ export class CodeTool implements BlockTool {
217
250
  ? await renderMermaid(code)
218
251
  : await renderLatex(code);
219
252
 
220
- this._previewContainer.innerHTML = rendered;
253
+ container.innerHTML = rendered;
221
254
  }
222
255
 
223
256
  public setReadOnly(state: boolean): void {
@@ -259,19 +292,40 @@ export class CodeTool implements BlockTool {
259
292
  }
260
293
 
261
294
  public renderSettings(): MenuConfig {
295
+ const selectedId = this._data.language;
296
+ const detectedId = this._detectedLanguage;
297
+ const showDetected = detectedId !== null && detectedId !== selectedId;
298
+
299
+ const childItems: PopoverItemParams[] = [];
300
+
301
+ if (showDetected) {
302
+ const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
303
+ if (detectedLanguage) {
304
+ childItems.push({
305
+ title: detectedLanguage.name,
306
+ secondaryLabel: 'auto',
307
+ icon: IconWand,
308
+ onActivate: (): void => this.setLanguage(detectedLanguage.id),
309
+ closeOnActivate: true,
310
+ isActive: (): boolean => this._data.language === detectedLanguage.id,
311
+ });
312
+ childItems.push({ type: PopoverItemType.Separator });
313
+ }
314
+ }
315
+
316
+ childItems.push(...LANGUAGES.map((lang) => ({
317
+ title: lang.name,
318
+ trailingIcon: lang.id === selectedId ? IconCheck : undefined,
319
+ onActivate: (): void => this.setLanguage(lang.id),
320
+ closeOnActivate: true,
321
+ })));
322
+
262
323
  return [
263
324
  {
264
325
  icon: IconCodeBlock,
265
326
  title: this.api.i18n.t(LANGUAGE_KEY),
266
327
  name: 'code-language',
267
- children: {
268
- items: LANGUAGES.map((lang) => ({
269
- title: lang.name,
270
- onActivate: (): void => this.setLanguage(lang.id),
271
- closeOnActivate: true,
272
- isActive: this._data.language === lang.id,
273
- })),
274
- },
328
+ children: { items: childItems },
275
329
  },
276
330
  ];
277
331
  }
@@ -301,6 +355,7 @@ export class CodeTool implements BlockTool {
301
355
 
302
356
  private setLanguage(id: string): void {
303
357
  this._data.language = id;
358
+ const isPreviewable = PREVIEWABLE_LANGUAGES.has(id);
304
359
 
305
360
  if (this._dom) {
306
361
  // Update the text span inside the language button (first child)
@@ -309,11 +364,94 @@ export class CodeTool implements BlockTool {
309
364
  if (textSpan) {
310
365
  textSpan.textContent = this.getLanguageName(id);
311
366
  }
367
+
368
+ // Show or hide the view mode segmented control based on previewability
369
+ if (this._dom.viewModeContainer) {
370
+ this._dom.viewModeContainer.hidden = !isPreviewable;
371
+ }
372
+
373
+ // When switching to a previewable language, activate preview mode
374
+ if (isPreviewable && this._dom.previewElement) {
375
+ this._previewContainer = this._dom.previewElement;
376
+ this._viewMode = 'preview';
377
+ this.applyViewMode();
378
+ void this.renderPreview();
379
+ }
380
+
381
+ // When switching away from a previewable language, reset to code mode
382
+ if (!isPreviewable) {
383
+ this._previewContainer = null;
384
+ this._viewMode = 'code';
385
+ this.applyViewMode();
386
+ }
387
+
388
+ // Rebuild the language picker so the selected language check icon updates
389
+ if (this._picker) {
390
+ this._picker.destroy();
391
+ }
392
+ this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
312
393
  }
313
394
 
314
395
  void this.highlightCode();
315
396
  }
316
397
 
398
+ /**
399
+ * Builds the language items array. When a detected language differs from the
400
+ * chosen one, it appears first with a wand icon and "auto" secondary label.
401
+ * The currently selected language is shown with a trailing check icon
402
+ * in its natural position in the full language list.
403
+ */
404
+ private buildLanguagePickerItems(): PopoverItemParams[] {
405
+ const selectedId = this._data.language;
406
+ const detectedId = this._detectedLanguage;
407
+ const showDetected = detectedId !== null && detectedId !== selectedId;
408
+
409
+ const items: PopoverItemParams[] = [];
410
+
411
+ if (showDetected) {
412
+ const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
413
+ if (detectedLanguage) {
414
+ items.push({
415
+ title: detectedLanguage.name,
416
+ name: detectedLanguage.id,
417
+ icon: IconWand,
418
+ toggle: 'language',
419
+ isActive: (): boolean => this._data.language === detectedLanguage.id,
420
+ closeOnActivate: true,
421
+ onActivate: (): void => this.setLanguage(detectedLanguage.id),
422
+ });
423
+ items.push({ type: PopoverItemType.Separator });
424
+ }
425
+ }
426
+
427
+ items.push(...LANGUAGES.map((lang) => ({
428
+ title: lang.name,
429
+ name: lang.id,
430
+ trailingIcon: lang.id === selectedId ? IconCheck : undefined,
431
+ toggle: 'language',
432
+ closeOnActivate: true,
433
+ onActivate: (): void => this.setLanguage(lang.id),
434
+ })));
435
+
436
+ return items;
437
+ }
438
+
439
+ /**
440
+ * Creates a new PopoverDesktop instance for the language picker.
441
+ */
442
+ private buildLanguagePicker(trigger: HTMLElement, leftAlignElement: HTMLElement): PopoverDesktop {
443
+ return new PopoverDesktop({
444
+ items: this.buildLanguagePickerItems(),
445
+ trigger,
446
+ leftAlignElement,
447
+ searchable: true,
448
+ width: '200px',
449
+ messages: {
450
+ search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
451
+ },
452
+ });
453
+ }
454
+
317
455
  private getLanguageName(id: string): string {
318
456
  const entry = LANGUAGES.find((lang) => lang.id === id);
319
457
 
@@ -339,28 +477,6 @@ export class CodeTool implements BlockTool {
339
477
  }).catch(() => { /* clipboard unavailable */ });
340
478
  }
341
479
 
342
- private toggleWrap(): void {
343
- this._wrapping = !this._wrapping;
344
-
345
- if (!this._dom) {
346
- return;
347
- }
348
-
349
- if (this._wrapping) {
350
- this._dom.codeElement.className = CODE_AREA_STYLES;
351
- } else {
352
- this._dom.codeElement.className = CODE_AREA_STYLES.replace('whitespace-pre-wrap', 'whitespace-pre');
353
- }
354
- }
355
-
356
- private toggleLineNumbers(): void {
357
- this._lineNumbers = !this._lineNumbers;
358
-
359
- if (this._dom) {
360
- this._dom.gutterElement.hidden = !this._lineNumbers;
361
- }
362
- }
363
-
364
480
  private updateGutter(): void {
365
481
  if (!this._dom) {
366
482
  return;
@@ -419,6 +535,30 @@ export class CodeTool implements BlockTool {
419
535
  });
420
536
  }
421
537
 
538
+ private scheduleDetection(): void {
539
+ if (this._detectionTimeoutId !== null) {
540
+ clearTimeout(this._detectionTimeoutId);
541
+ }
542
+
543
+ this._detectionTimeoutId = setTimeout(() => {
544
+ this._detectionTimeoutId = null;
545
+ const code = this._dom?.codeElement.textContent ?? '';
546
+ void detectLanguage(code).then((detected) => {
547
+ if (detected === this._detectedLanguage) {
548
+ return;
549
+ }
550
+ this._detectedLanguage = detected;
551
+ // Rebuild picker so the detected language section updates
552
+ if (this._dom) {
553
+ if (this._picker) {
554
+ this._picker.destroy();
555
+ }
556
+ this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
557
+ }
558
+ });
559
+ }, 600);
560
+ }
561
+
422
562
  private async highlightCode(): Promise<void> {
423
563
  if (!isHighlightingSupported() || !isHighlightable(this._data.language)) {
424
564
  this._disposeHighlights?.();
@@ -460,10 +600,17 @@ export class CodeTool implements BlockTool {
460
600
  this._highlightRafId = null;
461
601
  }
462
602
 
603
+ if (this._detectionTimeoutId !== null) {
604
+ clearTimeout(this._detectionTimeoutId);
605
+ this._detectionTimeoutId = null;
606
+ }
607
+
463
608
  if (this._picker) {
464
609
  this._picker.destroy();
465
610
  this._picker = null;
466
611
  }
612
+
613
+ this._dom = null;
467
614
  }
468
615
 
469
616
  public static get toolbox(): ToolboxConfig {
@@ -0,0 +1,118 @@
1
+ import { tokenizeCode } from './shiki-loader';
2
+
3
+ /**
4
+ * Languages considered for auto-detection.
5
+ * Kept to ~15 common ones to keep detection fast.
6
+ */
7
+ export const DETECTION_CANDIDATE_LANGUAGES = [
8
+ 'javascript',
9
+ 'typescript',
10
+ 'python',
11
+ 'java',
12
+ 'html',
13
+ 'css',
14
+ 'json',
15
+ 'bash',
16
+ 'sql',
17
+ 'rust',
18
+ 'go',
19
+ 'cpp',
20
+ 'yaml',
21
+ 'markdown',
22
+ 'php',
23
+ ] as const;
24
+
25
+ /** Minimum code length (characters) before attempting detection. */
26
+ const MIN_CODE_LENGTH = 20;
27
+
28
+ /**
29
+ * Threshold: if the winning language's fg-token ratio is above this value,
30
+ * no meaningful detection was possible and we return null.
31
+ * A ratio of 1.0 means all tokens are unrecognized (plain fg color).
32
+ * A ratio of 0.75 means 75% of characters are unrecognized.
33
+ */
34
+ const MAX_ACCEPTABLE_FG_RATIO = 0.75;
35
+
36
+ /**
37
+ * Minimum number of distinct non-fg colors a tokenization must produce
38
+ * to be considered a genuine match. Languages that colorize everything
39
+ * with a single non-fg color (e.g. YAML treating code as a string block)
40
+ * produce a deceptively low fg-ratio without actually recognizing the syntax.
41
+ */
42
+ const MIN_DISTINCT_COLORS = 2;
43
+
44
+ /**
45
+ * Scores how well shiki tokenized the code in a given language.
46
+ * Returns a value in [0, 1] — lower is better (fewer unrecognized tokens),
47
+ * or 1 if the tokenization didn't use enough distinct colors to be a real match.
48
+ *
49
+ * Strategy: compute the ratio of characters colored with the theme's
50
+ * foreground color (= unrecognized/plain text) to total characters.
51
+ * A well-matched language has many distinctly-colored tokens; a poorly-matched
52
+ * language produces mostly fg-colored (unrecognized) tokens.
53
+ *
54
+ * Guard: if fewer than MIN_DISTINCT_COLORS non-fg colors appear, the grammar
55
+ * is treating everything as the same token type (e.g. YAML string), which is
56
+ * a false positive. Return 1 in that case.
57
+ */
58
+ function scoreTokens(tokens: Array<Array<{ content: string; color: string }>>, fg: string): number {
59
+ const allTokens = tokens.flat();
60
+ const totalChars = allTokens.reduce((sum, token) => sum + token.content.length, 0);
61
+
62
+ if (totalChars === 0) return 1;
63
+
64
+ const { fgChars, nonFgColors } = allTokens.reduce(
65
+ (acc, token) => ({
66
+ fgChars: acc.fgChars + (token.color === fg ? token.content.length : 0),
67
+ nonFgColors: token.color === fg ? acc.nonFgColors : acc.nonFgColors.add(token.color),
68
+ }),
69
+ { fgChars: 0, nonFgColors: new Set<string>() }
70
+ );
71
+
72
+ // Reject tokenizations that use fewer than MIN_DISTINCT_COLORS non-fg colors —
73
+ // these are grammars that "colorize" everything as a single token type
74
+ // (e.g. YAML interpreting code as block scalars), which is a false positive.
75
+ if (nonFgColors.size < MIN_DISTINCT_COLORS) {
76
+ return 1;
77
+ }
78
+
79
+ return fgChars / totalChars;
80
+ }
81
+
82
+ /**
83
+ * Detects the most likely programming language for the given code.
84
+ * Returns a language ID from DETECTION_CANDIDATE_LANGUAGES, or null if:
85
+ * - code is too short
86
+ * - shiki isn't loaded yet
87
+ * - no language scores clearly better than plain text
88
+ */
89
+ export async function detectLanguage(code: string): Promise<string | null> {
90
+ if (code.length < MIN_CODE_LENGTH) {
91
+ return null;
92
+ }
93
+
94
+ // Tokenize all candidate languages concurrently
95
+ const results = await Promise.all(
96
+ DETECTION_CANDIDATE_LANGUAGES.map(async (lang) => {
97
+ const tokens = await tokenizeCode(code, lang);
98
+ return { lang, tokens };
99
+ })
100
+ );
101
+
102
+ const best = results.reduce<{ lang: string | null; score: number }>(
103
+ (acc, { lang, tokens }) => {
104
+ if (!tokens) return acc;
105
+
106
+ const score = scoreTokens(tokens.light.tokens, tokens.light.fg);
107
+
108
+ return score < acc.score ? { lang, score } : acc;
109
+ },
110
+ { lang: null, score: Infinity }
111
+ );
112
+
113
+ if (best.lang === null || best.score >= MAX_ACCEPTABLE_FG_RATIO) {
114
+ return null;
115
+ }
116
+
117
+ return best.lang;
118
+ }
@@ -88,6 +88,16 @@ export interface BlockTool extends BaseTool {
88
88
  */
89
89
  getContentOffset?(hoveredElement: Element): { left: number } | undefined;
90
90
 
91
+ /**
92
+ * Returns the element that the toolbar should vertically center on.
93
+ * Used by tools whose editable area is deeply nested below non-editable UI
94
+ * (e.g., a header bar), where the default contenteditable-descendant search
95
+ * would position the toolbar too far down inside the block.
96
+ *
97
+ * Return undefined to use the default positioning logic.
98
+ */
99
+ getToolbarAnchorElement?(): HTMLElement | undefined;
100
+
91
101
  /**
92
102
  * Called when read-only mode is toggled without re-rendering the block.
93
103
  * Implementations should update the DOM in place: toggle contentEditable,
@@ -151,6 +151,12 @@ export interface PopoverItemDefaultBaseParams {
151
151
  */
152
152
  icon?: string;
153
153
 
154
+ /**
155
+ * Icon to be displayed on the trailing (right) side of the item.
156
+ * Rendered without a box background, suitable for indicators like a checkmark.
157
+ */
158
+ trailingIcon?: string;
159
+
154
160
  /**
155
161
  * Additional displayed text
156
162
  */