@jackuait/blok 0.10.0 → 0.10.2

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 (225) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-BfcBwAfE.mjs → blok-D-T1XZ92.mjs} +1494 -1491
  3. package/dist/chunks/{constants-QNVyXALL.mjs → constants-CaB-mlB5.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-BFK2MvVI.mjs} +1024 -895
  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/index.ts +21 -9
  217. package/src/components/modules/toolbar/positioning.ts +11 -2
  218. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +11 -0
  219. package/src/components/utils/popover/popover-abstract.ts +7 -0
  220. package/src/tools/code/constants.ts +24 -12
  221. package/src/tools/code/dom-builder.ts +122 -89
  222. package/src/tools/code/index.ts +255 -104
  223. package/src/tools/code/language-detector.ts +118 -0
  224. package/types/tools/block-tool.d.ts +10 -0
  225. 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 {
@@ -253,25 +286,48 @@ export class CodeTool implements BlockTool {
253
286
 
254
287
  if (this._dom) {
255
288
  this._dom.codeElement.textContent = this._data.code;
289
+ this.syncTrailingBr();
290
+ this.updateGutter();
256
291
  }
257
292
 
258
293
  void this.highlightCode();
259
294
  }
260
295
 
261
296
  public renderSettings(): MenuConfig {
297
+ const selectedId = this._data.language;
298
+ const detectedId = this._detectedLanguage;
299
+ const showDetected = detectedId !== null && detectedId !== selectedId;
300
+
301
+ const childItems: PopoverItemParams[] = [];
302
+
303
+ if (showDetected) {
304
+ const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
305
+ if (detectedLanguage) {
306
+ childItems.push({
307
+ title: detectedLanguage.name,
308
+ secondaryLabel: 'auto',
309
+ icon: IconWand,
310
+ onActivate: (): void => this.setLanguage(detectedLanguage.id),
311
+ closeOnActivate: true,
312
+ isActive: (): boolean => this._data.language === detectedLanguage.id,
313
+ });
314
+ childItems.push({ type: PopoverItemType.Separator });
315
+ }
316
+ }
317
+
318
+ childItems.push(...LANGUAGES.map((lang) => ({
319
+ title: lang.name,
320
+ trailingIcon: lang.id === selectedId ? IconCheck : undefined,
321
+ onActivate: (): void => this.setLanguage(lang.id),
322
+ closeOnActivate: true,
323
+ })));
324
+
262
325
  return [
263
326
  {
264
327
  icon: IconCodeBlock,
265
328
  title: this.api.i18n.t(LANGUAGE_KEY),
266
329
  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
- },
330
+ children: { items: childItems },
275
331
  },
276
332
  ];
277
333
  }
@@ -294,6 +350,8 @@ export class CodeTool implements BlockTool {
294
350
 
295
351
  if (this._dom) {
296
352
  this._dom.codeElement.textContent = this._data.code;
353
+ this.syncTrailingBr();
354
+ this.updateGutter();
297
355
  }
298
356
 
299
357
  void this.highlightCode();
@@ -301,6 +359,7 @@ export class CodeTool implements BlockTool {
301
359
 
302
360
  private setLanguage(id: string): void {
303
361
  this._data.language = id;
362
+ const isPreviewable = PREVIEWABLE_LANGUAGES.has(id);
304
363
 
305
364
  if (this._dom) {
306
365
  // Update the text span inside the language button (first child)
@@ -309,11 +368,94 @@ export class CodeTool implements BlockTool {
309
368
  if (textSpan) {
310
369
  textSpan.textContent = this.getLanguageName(id);
311
370
  }
371
+
372
+ // Show or hide the view mode segmented control based on previewability
373
+ if (this._dom.viewModeContainer) {
374
+ this._dom.viewModeContainer.hidden = !isPreviewable;
375
+ }
376
+
377
+ // When switching to a previewable language, activate preview mode
378
+ if (isPreviewable && this._dom.previewElement) {
379
+ this._previewContainer = this._dom.previewElement;
380
+ this._viewMode = 'preview';
381
+ this.applyViewMode();
382
+ void this.renderPreview();
383
+ }
384
+
385
+ // When switching away from a previewable language, reset to code mode
386
+ if (!isPreviewable) {
387
+ this._previewContainer = null;
388
+ this._viewMode = 'code';
389
+ this.applyViewMode();
390
+ }
391
+
392
+ // Rebuild the language picker so the selected language check icon updates
393
+ if (this._picker) {
394
+ this._picker.destroy();
395
+ }
396
+ this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
312
397
  }
313
398
 
314
399
  void this.highlightCode();
315
400
  }
316
401
 
402
+ /**
403
+ * Builds the language items array. When a detected language differs from the
404
+ * chosen one, it appears first with a wand icon and "auto" secondary label.
405
+ * The currently selected language is shown with a trailing check icon
406
+ * in its natural position in the full language list.
407
+ */
408
+ private buildLanguagePickerItems(): PopoverItemParams[] {
409
+ const selectedId = this._data.language;
410
+ const detectedId = this._detectedLanguage;
411
+ const showDetected = detectedId !== null && detectedId !== selectedId;
412
+
413
+ const items: PopoverItemParams[] = [];
414
+
415
+ if (showDetected) {
416
+ const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
417
+ if (detectedLanguage) {
418
+ items.push({
419
+ title: detectedLanguage.name,
420
+ name: detectedLanguage.id,
421
+ icon: IconWand,
422
+ toggle: 'language',
423
+ isActive: (): boolean => this._data.language === detectedLanguage.id,
424
+ closeOnActivate: true,
425
+ onActivate: (): void => this.setLanguage(detectedLanguage.id),
426
+ });
427
+ items.push({ type: PopoverItemType.Separator });
428
+ }
429
+ }
430
+
431
+ items.push(...LANGUAGES.map((lang) => ({
432
+ title: lang.name,
433
+ name: lang.id,
434
+ trailingIcon: lang.id === selectedId ? IconCheck : undefined,
435
+ toggle: 'language',
436
+ closeOnActivate: true,
437
+ onActivate: (): void => this.setLanguage(lang.id),
438
+ })));
439
+
440
+ return items;
441
+ }
442
+
443
+ /**
444
+ * Creates a new PopoverDesktop instance for the language picker.
445
+ */
446
+ private buildLanguagePicker(trigger: HTMLElement, leftAlignElement: HTMLElement): PopoverDesktop {
447
+ return new PopoverDesktop({
448
+ items: this.buildLanguagePickerItems(),
449
+ trigger,
450
+ leftAlignElement,
451
+ searchable: true,
452
+ width: '200px',
453
+ messages: {
454
+ search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
455
+ },
456
+ });
457
+ }
458
+
317
459
  private getLanguageName(id: string): string {
318
460
  const entry = LANGUAGES.find((lang) => lang.id === id);
319
461
 
@@ -339,28 +481,6 @@ export class CodeTool implements BlockTool {
339
481
  }).catch(() => { /* clipboard unavailable */ });
340
482
  }
341
483
 
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
484
  private updateGutter(): void {
365
485
  if (!this._dom) {
366
486
  return;
@@ -419,6 +539,30 @@ export class CodeTool implements BlockTool {
419
539
  });
420
540
  }
421
541
 
542
+ private scheduleDetection(): void {
543
+ if (this._detectionTimeoutId !== null) {
544
+ clearTimeout(this._detectionTimeoutId);
545
+ }
546
+
547
+ this._detectionTimeoutId = setTimeout(() => {
548
+ this._detectionTimeoutId = null;
549
+ const code = this._dom?.codeElement.textContent ?? '';
550
+ void detectLanguage(code).then((detected) => {
551
+ if (detected === this._detectedLanguage) {
552
+ return;
553
+ }
554
+ this._detectedLanguage = detected;
555
+ // Rebuild picker so the detected language section updates
556
+ if (this._dom) {
557
+ if (this._picker) {
558
+ this._picker.destroy();
559
+ }
560
+ this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
561
+ }
562
+ });
563
+ }, 600);
564
+ }
565
+
422
566
  private async highlightCode(): Promise<void> {
423
567
  if (!isHighlightingSupported() || !isHighlightable(this._data.language)) {
424
568
  this._disposeHighlights?.();
@@ -460,10 +604,17 @@ export class CodeTool implements BlockTool {
460
604
  this._highlightRafId = null;
461
605
  }
462
606
 
607
+ if (this._detectionTimeoutId !== null) {
608
+ clearTimeout(this._detectionTimeoutId);
609
+ this._detectionTimeoutId = null;
610
+ }
611
+
463
612
  if (this._picker) {
464
613
  this._picker.destroy();
465
614
  this._picker = null;
466
615
  }
616
+
617
+ this._dom = null;
467
618
  }
468
619
 
469
620
  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
  */