@jackuait/blok 0.10.0-beta.9 → 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 (269) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-DDu252IK.mjs → blok-u_68bnlk.mjs} +1617 -1562
  3. package/dist/chunks/{constants-DMW9a31I.mjs → constants-VDhCUk4c.mjs} +56 -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/chunks/{messages-DGL1ySqb2.mjs → 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/{messages-Ci0KqX-J.mjs → chunks/messages-BKN3YVIj.mjs} +1 -0
  14. package/dist/chunks/{messages-BXM80fdr2.mjs → messages-BMD37y3q2.mjs} +1 -0
  15. package/dist/{messages-B19o-Teb.mjs → chunks/messages-BONyZroH.mjs} +1 -0
  16. package/dist/{messages-BwHs4cm1.mjs → chunks/messages-BRAoJpOu.mjs} +1 -0
  17. package/dist/{messages-DY4IqlhY.mjs → chunks/messages-BRoa9tGl.mjs} +1 -0
  18. package/dist/chunks/{messages-RInp1ytX.mjs → messages-BbEW9bQz.mjs} +1 -0
  19. package/dist/{messages-BIHc0KHY.mjs → chunks/messages-BeGZqQwz.mjs} +1 -0
  20. package/dist/{messages-CmB406HW.mjs → chunks/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/{messages-Dl5Y2-Ia.mjs → chunks/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/chunks/{messages-BYlSMRkd.mjs → messages-CIfUm1Oa.mjs} +1 -0
  30. package/dist/{messages-BbYq1pk-.mjs → chunks/messages-CPBN4zWc.mjs} +1 -0
  31. package/dist/{messages-DPA-mMWC2.mjs → chunks/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/{messages-DnG0ef8t2.mjs → chunks/messages-CZSlfnkO2.mjs} +1 -0
  36. package/dist/chunks/{messages-iS34FHFB.mjs → messages-ChK7v1PV.mjs} +1 -0
  37. package/dist/{messages-BiUGXvYr2.mjs → chunks/messages-Clku7Cf-2.mjs} +1 -0
  38. package/dist/{messages-DIJlIqlQ2.mjs → chunks/messages-CszmHAvQ.mjs} +1 -0
  39. package/dist/chunks/{messages-DWu1r4gc2.mjs → messages-CvANwuht2.mjs} +1 -0
  40. package/dist/{messages-nUVjeh7K.mjs → chunks/messages-CxiURE2X.mjs} +1 -0
  41. package/dist/chunks/{messages-A_MkXDlG.mjs → messages-CxxyR4vY.mjs} +1 -0
  42. package/dist/{messages-ynAe7ewZ.mjs → chunks/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/{messages-CcF4y-E4.mjs → chunks/messages-DEBy3nuJ2.mjs} +1 -0
  47. package/dist/chunks/{messages-NEqrrYvE2.mjs → messages-DMoERagV2.mjs} +1 -0
  48. package/dist/chunks/{messages-Bxvi1ebN.mjs → 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/{messages-5jvKxQNu2.mjs → chunks/messages-DUeiPraX.mjs} +1 -0
  52. package/dist/chunks/{messages-G416eyjY.mjs → messages-DUr9WAkD.mjs} +1 -0
  53. package/dist/chunks/{messages-Ck81cQkn2.mjs → messages-DVr1sqfI2.mjs} +1 -0
  54. package/dist/{messages-BYmmMDrN2.mjs → chunks/messages-DjvaiALg2.mjs} +1 -0
  55. package/dist/chunks/{messages-D55HRx5O2.mjs → messages-DrfRYiM32.mjs} +1 -0
  56. package/dist/chunks/{messages-B2N4fUi72.mjs → 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/chunks/{messages-BhzzNkN-.mjs → messages-JSQjKQ8I.mjs} +1 -0
  62. package/dist/{messages-CWIXvnDf2.mjs → chunks/messages-Q7-4ZJLB2.mjs} +1 -0
  63. package/dist/chunks/{messages-DOuS1Qge.mjs → messages-QMOmwcZb.mjs} +1 -0
  64. package/dist/{messages-hWwSRF-2.mjs → chunks/messages-QilfinOn2.mjs} +1 -0
  65. package/dist/{messages-BmAn22OX.mjs → chunks/messages-a07QVz8U.mjs} +1 -0
  66. package/dist/chunks/{messages-BYxLFj7y.mjs → messages-eFd4YYzt.mjs} +1 -0
  67. package/dist/chunks/{messages-BSghd0ez.mjs → messages-euM2m3wQ.mjs} +1 -0
  68. package/dist/chunks/{messages-BVjoM7P0.mjs → messages-kGmxkeFH.mjs} +1 -0
  69. package/dist/{messages-DMr62KiO2.mjs → chunks/messages-oMc7qugU2.mjs} +1 -0
  70. package/dist/chunks/{messages-DzTk8bJ5.mjs → messages-sDdNf8O9.mjs} +1 -0
  71. package/dist/{messages-Bm0Feca1.mjs → chunks/messages-wl8YrvGG.mjs} +1 -0
  72. package/dist/chunks/{messages-BAsb5CgZ.mjs → messages-zt6zdYWh.mjs} +1 -0
  73. package/dist/chunks/{tools-XmzH2rgQ.mjs → tools-1ZFajlGN.mjs} +1619 -1307
  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/{chunks/messages-JyvWu4rf2.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/{chunks/messages-DMVXnAYj.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/{chunks/messages-Dr0Ekmbz.mjs → messages-BesJaI6A.mjs} +1 -0
  87. package/dist/{chunks/messages-D3zojZ94.mjs → messages-BiTMwiKH.mjs} +1 -0
  88. package/dist/{messages-B1ZUQagA.mjs → messages-BmH2cQHQ.mjs} +1 -0
  89. package/dist/{chunks/messages-Bfnq1xv4.mjs → messages-BrOWqNCu2.mjs} +1 -0
  90. package/dist/{messages-Dnp9N6RU2.mjs → messages-Brd5R-da2.mjs} +1 -0
  91. package/dist/{chunks/messages-DJoNVjqP.mjs → messages-C0GSBBCo2.mjs} +1 -0
  92. package/dist/{messages-Dw__BcTj.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/{chunks/messages-DlLXpgWM2.mjs → messages-CTTmWn4Y2.mjs} +1 -0
  98. package/dist/{messages-D0aw5_0k2.mjs → messages-CZbcxlZt2.mjs} +1 -0
  99. package/dist/{chunks/messages-Bp8qin1R.mjs → messages-C_Qn9SbQ.mjs} +1 -0
  100. package/dist/{messages-96iaAUds2.mjs → messages-CdEASHDp2.mjs} +1 -0
  101. package/dist/{messages-Dy-Y_nEI.mjs → messages-CdduYw-q.mjs} +1 -0
  102. package/dist/{chunks/messages-Je5YvxiY.mjs → messages-Che99vKP.mjs} +1 -0
  103. package/dist/{messages-nlhESX9t.mjs → messages-CisR4PNV.mjs} +1 -0
  104. package/dist/{chunks/messages-BE6lHKwf.mjs → messages-ClGvlFcH2.mjs} +1 -0
  105. package/dist/{chunks/messages-FWfsxpBz.mjs → messages-CnuH-BZK2.mjs} +1 -0
  106. package/dist/{chunks/messages-aZcy0JQq2.mjs → messages-D0005ti32.mjs} +1 -0
  107. package/dist/{messages-B7ieAJBd2.mjs → messages-D05jqBIa2.mjs} +1 -0
  108. package/dist/{messages-DTh9a8mR.mjs → messages-D0lLw9KM.mjs} +1 -0
  109. package/dist/{chunks/messages-ihCjSFJI2.mjs → messages-D3rwCtKn.mjs} +1 -0
  110. package/dist/{chunks/messages-xuqyb6Ff2.mjs → messages-D6VIFnSW.mjs} +1 -0
  111. package/dist/{chunks/messages-KdawW5Na.mjs → messages-D81w6AmW.mjs} +1 -0
  112. package/dist/{chunks/messages-BUVhHx0q2.mjs → messages-DBhvm8NK.mjs} +1 -0
  113. package/dist/{chunks/messages-C7VGpihw.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/{chunks/messages-B9kmbUWV.mjs → messages-DODrhcop.mjs} +1 -0
  117. package/dist/{messages-BsycN_JI2.mjs → messages-DOGbHYv-2.mjs} +1 -0
  118. package/dist/{chunks/messages-BQYvBqm2.mjs → messages-DQORja0D.mjs} +1 -0
  119. package/dist/{chunks/messages-CVdpweyf2.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/{chunks/messages-C6Mpiacw.mjs → messages-Dnd5YSWv.mjs} +1 -0
  125. package/dist/{messages-BJeGJksD.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/{messages-j7o5rT9s.mjs → messages-Dqu4aX9s.mjs} +1 -0
  130. package/dist/{chunks/messages-BjadX8jR2.mjs → messages-E8NjqzWq2.mjs} +1 -0
  131. package/dist/{messages-aWZH50vu2.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/{chunks/messages-D9N2MvQx2.mjs → messages-Q5sQeVap2.mjs} +1 -0
  135. package/dist/{chunks/messages-Bphq_Bt3.mjs → messages-Xc0KUbYl.mjs} +1 -0
  136. package/dist/{chunks/messages-DlonA3wa.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/{messages-DkLU_rWm.mjs → messages-neGD3WGq.mjs} +1 -0
  140. package/dist/{messages-Ddnj2iTG2.mjs → messages-qbKjjvgd2.mjs} +1 -0
  141. package/dist/{messages-BiiongNz2.mjs → messages-qfvXgPpu2.mjs} +1 -0
  142. package/dist/{messages-Dvn35ksS.mjs → messages-uwK7ktqk.mjs} +1 -0
  143. package/dist/react.mjs +2 -2
  144. package/dist/tools.mjs +2 -2
  145. package/package.json +3 -5
  146. package/src/cli/commands/convert-gdocs/index.ts +26 -0
  147. package/src/cli/commands/convert-html/block-builder.ts +392 -0
  148. package/src/cli/commands/convert-html/id-generator.ts +11 -0
  149. package/src/cli/commands/convert-html/index.ts +23 -0
  150. package/src/cli/commands/convert-html/preprocessor.ts +422 -0
  151. package/src/cli/commands/convert-html/sanitizer.ts +93 -0
  152. package/src/cli/commands/convert-html/types.ts +15 -0
  153. package/src/cli/index.ts +56 -5
  154. package/src/components/block/index.ts +58 -10
  155. package/src/components/constants/data-attributes.ts +10 -0
  156. package/src/components/i18n/locales/am/messages.json +1 -0
  157. package/src/components/i18n/locales/ar/messages.json +1 -0
  158. package/src/components/i18n/locales/az/messages.json +1 -0
  159. package/src/components/i18n/locales/bg/messages.json +1 -0
  160. package/src/components/i18n/locales/bn/messages.json +1 -0
  161. package/src/components/i18n/locales/bs/messages.json +1 -0
  162. package/src/components/i18n/locales/cs/messages.json +1 -0
  163. package/src/components/i18n/locales/da/messages.json +1 -0
  164. package/src/components/i18n/locales/de/messages.json +1 -0
  165. package/src/components/i18n/locales/dv/messages.json +1 -0
  166. package/src/components/i18n/locales/el/messages.json +1 -0
  167. package/src/components/i18n/locales/en/messages.json +1 -0
  168. package/src/components/i18n/locales/es/messages.json +1 -0
  169. package/src/components/i18n/locales/et/messages.json +1 -0
  170. package/src/components/i18n/locales/fa/messages.json +1 -0
  171. package/src/components/i18n/locales/fi/messages.json +1 -0
  172. package/src/components/i18n/locales/fil/messages.json +1 -0
  173. package/src/components/i18n/locales/fr/messages.json +1 -0
  174. package/src/components/i18n/locales/gu/messages.json +1 -0
  175. package/src/components/i18n/locales/he/messages.json +1 -0
  176. package/src/components/i18n/locales/hi/messages.json +1 -0
  177. package/src/components/i18n/locales/hr/messages.json +1 -0
  178. package/src/components/i18n/locales/hu/messages.json +1 -0
  179. package/src/components/i18n/locales/hy/messages.json +1 -0
  180. package/src/components/i18n/locales/id/messages.json +1 -0
  181. package/src/components/i18n/locales/it/messages.json +1 -0
  182. package/src/components/i18n/locales/ja/messages.json +1 -0
  183. package/src/components/i18n/locales/ka/messages.json +1 -0
  184. package/src/components/i18n/locales/km/messages.json +1 -0
  185. package/src/components/i18n/locales/kn/messages.json +1 -0
  186. package/src/components/i18n/locales/ko/messages.json +1 -0
  187. package/src/components/i18n/locales/ku/messages.json +1 -0
  188. package/src/components/i18n/locales/lo/messages.json +1 -0
  189. package/src/components/i18n/locales/lt/messages.json +1 -0
  190. package/src/components/i18n/locales/lv/messages.json +1 -0
  191. package/src/components/i18n/locales/mk/messages.json +1 -0
  192. package/src/components/i18n/locales/ml/messages.json +1 -0
  193. package/src/components/i18n/locales/mn/messages.json +1 -0
  194. package/src/components/i18n/locales/mr/messages.json +1 -0
  195. package/src/components/i18n/locales/ms/messages.json +1 -0
  196. package/src/components/i18n/locales/my/messages.json +1 -0
  197. package/src/components/i18n/locales/ne/messages.json +1 -0
  198. package/src/components/i18n/locales/nl/messages.json +1 -0
  199. package/src/components/i18n/locales/no/messages.json +1 -0
  200. package/src/components/i18n/locales/pa/messages.json +1 -0
  201. package/src/components/i18n/locales/pl/messages.json +1 -0
  202. package/src/components/i18n/locales/ps/messages.json +1 -0
  203. package/src/components/i18n/locales/pt/messages.json +1 -0
  204. package/src/components/i18n/locales/ro/messages.json +1 -0
  205. package/src/components/i18n/locales/ru/messages.json +1 -0
  206. package/src/components/i18n/locales/sd/messages.json +1 -0
  207. package/src/components/i18n/locales/si/messages.json +1 -0
  208. package/src/components/i18n/locales/sk/messages.json +1 -0
  209. package/src/components/i18n/locales/sl/messages.json +1 -0
  210. package/src/components/i18n/locales/sq/messages.json +1 -0
  211. package/src/components/i18n/locales/sr/messages.json +1 -0
  212. package/src/components/i18n/locales/sv/messages.json +1 -0
  213. package/src/components/i18n/locales/sw/messages.json +1 -0
  214. package/src/components/i18n/locales/ta/messages.json +1 -0
  215. package/src/components/i18n/locales/te/messages.json +1 -0
  216. package/src/components/i18n/locales/th/messages.json +1 -0
  217. package/src/components/i18n/locales/tr/messages.json +1 -0
  218. package/src/components/i18n/locales/ug/messages.json +1 -0
  219. package/src/components/i18n/locales/uk/messages.json +1 -0
  220. package/src/components/i18n/locales/ur/messages.json +1 -0
  221. package/src/components/i18n/locales/vi/messages.json +1 -0
  222. package/src/components/i18n/locales/yi/messages.json +1 -0
  223. package/src/components/i18n/locales/zh/messages.json +1 -0
  224. package/src/components/icons/index.ts +29 -18
  225. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
  226. package/src/components/modules/blockManager/hierarchy.ts +4 -1
  227. package/src/components/modules/readonly.ts +46 -0
  228. package/src/components/modules/rectangleSelection.ts +25 -5
  229. package/src/components/modules/toolbar/index.ts +96 -19
  230. package/src/components/modules/toolbar/positioning.ts +11 -2
  231. package/src/components/modules/toolbar/styles.ts +0 -2
  232. package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
  233. package/src/components/tools/block.ts +10 -0
  234. package/src/components/utils/placeholder.ts +9 -2
  235. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +11 -0
  236. package/src/components/utils/popover/popover-abstract.ts +7 -0
  237. package/src/styles/main.css +16 -0
  238. package/src/tools/callout/constants.ts +2 -1
  239. package/src/tools/callout/dom-builder.ts +13 -1
  240. package/src/tools/callout/index.ts +21 -7
  241. package/src/tools/code/constants.ts +28 -8
  242. package/src/tools/code/dom-builder.ts +133 -64
  243. package/src/tools/code/index.ts +280 -91
  244. package/src/tools/code/language-detector.ts +118 -0
  245. package/src/tools/divider/index.ts +5 -0
  246. package/src/tools/header/index.ts +47 -1
  247. package/src/tools/list/dom-builder.ts +3 -1
  248. package/src/tools/list/index.ts +55 -3
  249. package/src/tools/list/list-helpers.ts +2 -2
  250. package/src/tools/nested-blocks.ts +25 -0
  251. package/src/tools/paragraph/index.ts +47 -6
  252. package/src/tools/quote/index.ts +43 -8
  253. package/src/tools/stub/index.ts +10 -0
  254. package/src/tools/table/index.ts +238 -6
  255. package/src/tools/table/table-add-controls.ts +37 -5
  256. package/src/tools/table/table-cell-blocks.ts +57 -18
  257. package/src/tools/table/table-core.ts +2 -0
  258. package/src/tools/table/table-corner-drag.ts +247 -0
  259. package/src/tools/table/table-operations.ts +41 -14
  260. package/src/tools/toggle/dom-builder.ts +1 -0
  261. package/src/tools/toggle/index.ts +25 -0
  262. package/src/tools/toggle/toggle-lifecycle.ts +5 -4
  263. package/src/types-internal/jsdom.d.ts +9 -0
  264. package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
  265. package/types/tools/block-tool.d.ts +20 -0
  266. package/types/utils/popover/popover-item.d.ts +6 -0
  267. package/bin/blok.mjs +0 -10
  268. package/dist/cli.mjs +0 -37
  269. package/src/tools/code/language-picker.ts +0 -241
@@ -34,6 +34,13 @@ export class BlockHoverController extends Controller {
34
34
  */
35
35
  private static readonly HOVER_COOLDOWN_MS = 50;
36
36
 
37
+ /**
38
+ * Maximum horizontal distance from content edges for extended hover zone.
39
+ * When cursor is within this distance of the content area, nearest-block
40
+ * detection activates. Beyond this distance, no hover event is emitted.
41
+ */
42
+ private static readonly HOVER_ZONE_SIZE = 100;
43
+
37
44
  constructor(options: {
38
45
  config: Controller['config'];
39
46
  eventsDispatcher: Controller['eventsDispatcher'];
@@ -104,9 +111,10 @@ export class BlockHoverController extends Controller {
104
111
 
105
112
  /**
106
113
  * If no block element found directly, find the nearest block by Y distance
114
+ * but only if the cursor is within the extended hover zone (100px from content edges).
107
115
  */
108
116
  if (!hoveredBlockElement) {
109
- this.emitNearestBlockHovered(event.clientY);
117
+ this.emitNearestBlockHoveredInZone(event.clientX, event.clientY);
110
118
 
111
119
  return;
112
120
  }
@@ -169,6 +177,41 @@ export class BlockHoverController extends Controller {
169
177
  });
170
178
  }
171
179
 
180
+ /**
181
+ * Emits a BlockHovered event for the nearest block, but only if the cursor
182
+ * is within the extended hover zone (HOVER_ZONE_SIZE px from content edges).
183
+ * @param clientX - Cursor X position
184
+ * @param clientY - Cursor Y position
185
+ */
186
+ private emitNearestBlockHoveredInZone(clientX: number, clientY: number): void {
187
+ const blocks = this.Blok.BlockManager.blocks;
188
+ const topLevelBlocks = blocks.filter(block =>
189
+ block.holder.closest('[data-blok-table-cell-blocks], [data-blok-toggle-children]') === null
190
+ );
191
+
192
+ if (topLevelBlocks.length === 0) {
193
+ return;
194
+ }
195
+
196
+ const contentEl = topLevelBlocks[0].holder.querySelector<HTMLElement>('[data-blok-element-content]');
197
+
198
+ if (!contentEl) {
199
+ this.emitNearestBlockHovered(clientY);
200
+
201
+ return;
202
+ }
203
+
204
+ const contentRect = contentEl.getBoundingClientRect();
205
+ const distLeft = Math.abs(clientX - contentRect.left);
206
+ const distRight = Math.abs(clientX - contentRect.right);
207
+ const withinZone = distLeft <= BlockHoverController.HOVER_ZONE_SIZE
208
+ || distRight <= BlockHoverController.HOVER_ZONE_SIZE;
209
+
210
+ if (withinZone) {
211
+ this.emitNearestBlockHovered(clientY);
212
+ }
213
+ }
214
+
172
215
  /**
173
216
  * Finds the nearest block by vertical distance to cursor position.
174
217
  * Returns the block whose vertical center is closest to the cursor Y position.
@@ -69,6 +69,16 @@ export class BlockToolAdapter extends BaseToolAdapter<ToolType.Block, IBlockTool
69
69
  return (this.constructable as BlockToolConstructable)[InternalBlockToolSettings.IsReadOnlySupported] === true;
70
70
  }
71
71
 
72
+ /**
73
+ * Returns true if the Tool's prototype has a setReadOnly method,
74
+ * enabling the in-place read-only toggle path (no save/clear/render cycle).
75
+ */
76
+ public get supportsInPlaceReadOnly(): boolean {
77
+ const prototype = (this.constructable as unknown as { prototype?: { setReadOnly?: unknown } })?.prototype;
78
+
79
+ return typeof prototype?.setReadOnly === 'function';
80
+ }
81
+
72
82
  /**
73
83
  * Returns true if Tool supports linebreaks
74
84
  */
@@ -132,11 +132,18 @@ export const setupPlaceholder = (
132
132
  element: HTMLElement,
133
133
  placeholder?: string,
134
134
  attributeName: 'data-placeholder' | 'data-blok-placeholder-active' = 'data-placeholder'
135
- ): void => {
135
+ ): (() => void) => {
136
136
  // Always set the attribute, even if empty (for consistency and testing)
137
137
  element.setAttribute(attributeName, placeholder ?? '');
138
138
 
139
- element.addEventListener('focus', () => handleEmptyElement(element));
139
+ const handler = (): void => handleEmptyElement(element);
140
+
141
+ element.addEventListener('focus', handler);
142
+
143
+ return () => {
144
+ element.removeEventListener('focus', handler);
145
+ element.removeAttribute(attributeName);
146
+ };
140
147
  };
141
148
 
142
149
  /**
@@ -281,6 +281,17 @@ export class PopoverItemDefault extends PopoverItem {
281
281
  this.nodes.secondaryLabelEl = secondaryEl;
282
282
  }
283
283
 
284
+ // Trailing icon (right-side indicator, e.g. checkmark)
285
+ if (params.trailingIcon) {
286
+ const trailingEl = document.createElement('div');
287
+
288
+ trailingEl.className = 'ml-auto shrink-0 flex items-center justify-center [&_svg]:w-icon [&_svg]:h-icon';
289
+ trailingEl.setAttribute('data-blok-testid', 'popover-item-trailing-icon');
290
+ trailingEl.innerHTML = params.trailingIcon;
291
+
292
+ root.appendChild(trailingEl);
293
+ }
294
+
284
295
  // Chevron
285
296
  const showChevron = this.hasChildren && !this.isChevronHidden;
286
297
 
@@ -102,6 +102,13 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
102
102
  return this.nodes.popover;
103
103
  }
104
104
 
105
+ /**
106
+ * Whether the popover is currently shown
107
+ */
108
+ public get isShown(): boolean {
109
+ return this.nodes.popover.hasAttribute(DATA_ATTR.popoverOpened);
110
+ }
111
+
105
112
  /**
106
113
  * Open popover
107
114
  */
@@ -644,6 +644,10 @@
644
644
  --color-swatch-ring-hover: var(--blok-swatch-ring-hover);
645
645
  --color-swatch-ring-active: var(--blok-swatch-ring-active);
646
646
 
647
+ /* Surface tokens (code blocks, secondary containers) */
648
+ --color-bg-secondary: var(--blok-bg-secondary);
649
+ --color-border-secondary: var(--blok-border-secondary);
650
+
647
651
  }
648
652
 
649
653
  @layer utilities {
@@ -725,6 +729,10 @@
725
729
  --blok-swatch-ring-hover: rgba(0, 0, 0, 0.10);
726
730
  --blok-swatch-ring-active: rgba(0, 0, 0, 0.30);
727
731
 
732
+ /* Surface tokens (code blocks, secondary containers) */
733
+ --blok-bg-secondary: #f7f8fa;
734
+ --blok-border-secondary: rgba(55, 53, 47, 0.09);
735
+
728
736
  /* Marker colors — light theme */
729
737
  --blok-color-gray-text: #787774;
730
738
  --blok-color-gray-bg: #f1f1ef;
@@ -818,6 +826,10 @@
818
826
  --blok-swatch-ring-hover: rgba(255, 255, 255, 0.15);
819
827
  --blok-swatch-ring-active: rgba(255, 255, 255, 0.35);
820
828
 
829
+ /* Surface tokens (code blocks, secondary containers) */
830
+ --blok-bg-secondary: rgba(255, 255, 255, 0.04);
831
+ --blok-border-secondary: rgba(255, 255, 255, 0.08);
832
+
821
833
  /* Marker colors — dark theme */
822
834
  --blok-color-gray-text: #9b9b9b;
823
835
  --blok-color-gray-bg: #2f2f2f;
@@ -910,6 +922,10 @@
910
922
  --blok-swatch-ring-hover: rgba(255, 255, 255, 0.15);
911
923
  --blok-swatch-ring-active: rgba(255, 255, 255, 0.35);
912
924
 
925
+ /* Surface tokens (code blocks, secondary containers) */
926
+ --blok-bg-secondary: rgba(255, 255, 255, 0.04);
927
+ --blok-border-secondary: rgba(255, 255, 255, 0.08);
928
+
913
929
  /* Marker colors — dark theme */
914
930
  --blok-color-gray-text: #9b9b9b;
915
931
  --blok-color-gray-bg: #2f2f2f;
@@ -25,7 +25,8 @@ export const EMOJI_CATEGORY_FLAGS_KEY = 'tools.callout.emojiCategoryFlags';
25
25
  export const DEFAULT_EMOJI = '💡';
26
26
 
27
27
  // CSS — Tailwind classes
28
- export const WRAPPER_STYLES = 'rounded-xl px-4 py-[5px] my-1 flex items-start gap-2';
28
+ export const WRAPPER_STYLES = 'rounded-xl pl-8 pr-4 py-[5px] my-1 flex items-start gap-2 relative';
29
29
  // h-[38px] = py-[7px]×2 + 1.5rem×1 = 14+24; explicit height prevents platform-specific emoji font metrics from inflating the button
30
30
  export const EMOJI_BUTTON_STYLES = 'text-[1.5rem] leading-[1] cursor-pointer bg-transparent border-0 px-0 py-[7px] h-[38px] flex-shrink-0 select-none';
31
31
  export const CHILDREN_STYLES = 'flex-1 min-w-0';
32
+ export const DRAG_ZONE_STYLES = 'absolute left-0 top-0 h-full cursor-grab select-none';
@@ -1,16 +1,19 @@
1
1
  // src/tools/callout/dom-builder.ts
2
2
 
3
+ import { DATA_ATTR } from '../../components/constants/data-attributes';
3
4
  import { TOGGLE_ATTR } from '../toggle/constants';
4
5
  import {
5
6
  WRAPPER_STYLES,
6
7
  EMOJI_BUTTON_STYLES,
7
8
  CHILDREN_STYLES,
9
+ DRAG_ZONE_STYLES,
8
10
  } from './constants';
9
11
 
10
12
  export interface CalloutDOMRefs {
11
13
  wrapper: HTMLElement;
12
14
  emojiButton: HTMLButtonElement;
13
15
  childContainer: HTMLElement;
16
+ dragZone: HTMLElement;
14
17
  }
15
18
 
16
19
  export interface BuildCalloutDOMOptions {
@@ -43,11 +46,20 @@ export function buildCalloutDOM(options: BuildCalloutDOMOptions): CalloutDOMRefs
43
46
  const childContainer = document.createElement('div');
44
47
  childContainer.className = CHILDREN_STYLES;
45
48
  childContainer.setAttribute(TOGGLE_ATTR.toggleChildren, '');
49
+ childContainer.setAttribute(DATA_ATTR.nestedBlocks, '');
46
50
  childContainer.setAttribute('data-blok-child-toolbar', '');
47
51
  childContainer.setAttribute('data-blok-mutation-free', 'true');
48
52
 
49
53
  wrapper.appendChild(emojiButton);
50
54
  wrapper.appendChild(childContainer);
51
55
 
52
- return { wrapper, emojiButton, childContainer };
56
+ // Drag zone — covers left padding area (x=[0,16px]) for drag handle,
57
+ // sits behind emoji button so emoji clicks pass through
58
+ const dragZone = document.createElement('span');
59
+ dragZone.className = DRAG_ZONE_STYLES;
60
+ dragZone.style.width = '32px'; // matches pl-8 left padding
61
+ dragZone.setAttribute('data-callout-drag-zone', '');
62
+ wrapper.prepend(dragZone);
63
+
64
+ return { wrapper, emojiButton, childContainer, dragZone };
53
65
  }
@@ -16,6 +16,7 @@ import type { CalloutData, CalloutConfig } from './types';
16
16
  import { buildCalloutDOM, type CalloutDOMRefs } from './dom-builder';
17
17
  import { saveCallout } from './block-operations';
18
18
  import { handleCalloutFirstChildBackspace } from './callout-keyboard';
19
+ import { mountChildBlocks } from '../nested-blocks';
19
20
  import { createColorPicker, type ColorPickerHandle } from '../../components/shared/color-picker';
20
21
  import { colorVarName } from '../../components/shared/color-presets';
21
22
  import { mapToNearestPresetName } from '../../components/utils/color-mapping';
@@ -60,11 +61,12 @@ const VARIANT_TO_BG_PRESET: Record<string, string | null> = {
60
61
 
61
62
  export class CalloutTool implements BlockTool {
62
63
  private readonly api: API;
63
- private readonly readOnly: boolean;
64
+ private readOnly: boolean;
64
65
  private _data: CalloutData;
65
66
  private _dom: CalloutDOMRefs | null = null;
66
67
  private _emojiPicker: EmojiPicker | null = null;
67
68
  private _colorPicker: ColorPickerHandle | null = null;
69
+ private _dragZone: HTMLElement | null = null;
68
70
  private blockId?: string;
69
71
 
70
72
  constructor({ data, api, readOnly, block }: BlockToolConstructorOptions<CalloutData, CalloutConfig>) {
@@ -108,6 +110,10 @@ export class CalloutTool implements BlockTool {
108
110
  }
109
111
 
110
112
  public render(): HTMLElement {
113
+ if (this._dom) {
114
+ return this._dom.wrapper;
115
+ }
116
+
111
117
  const dom = buildCalloutDOM({
112
118
  emoji: this._data.emoji,
113
119
  readOnly: this.readOnly,
@@ -115,6 +121,7 @@ export class CalloutTool implements BlockTool {
115
121
  });
116
122
 
117
123
  this._dom = dom;
124
+ this._dragZone = dom.dragZone;
118
125
  this.applyColors();
119
126
 
120
127
  if (!this.readOnly) {
@@ -144,12 +151,7 @@ export class CalloutTool implements BlockTool {
144
151
 
145
152
  const children = this.api.blocks.getChildren(this.blockId);
146
153
 
147
- // Append existing children to the container
148
- for (const child of children) {
149
- if (child.holder.parentElement !== this._dom.childContainer) {
150
- this._dom.childContainer.appendChild(child.holder);
151
- }
152
- }
154
+ mountChildBlocks(this._dom.childContainer, children);
153
155
 
154
156
  // Auto-create initial paragraph child when callout has no children
155
157
  if (children.length === 0) {
@@ -252,6 +254,18 @@ export class CalloutTool implements BlockTool {
252
254
  // No-op — no subscriptions to clean up
253
255
  }
254
256
 
257
+ public setReadOnly(state: boolean): void {
258
+ this.readOnly = state;
259
+
260
+ if (this._dom) {
261
+ this._dom.emojiButton.disabled = state;
262
+ }
263
+ }
264
+
265
+ public get dragZone(): HTMLElement | null {
266
+ return this._dragZone;
267
+ }
268
+
255
269
  private syncPickerActiveColors(): void {
256
270
  if (this._colorPicker === null) {
257
271
  return;
@@ -5,7 +5,6 @@ export const PLACEHOLDER_KEY = 'tools.code.placeholder';
5
5
  export const LANGUAGE_KEY = 'tools.code.language';
6
6
  export const COPIED_KEY = 'tools.code.copied';
7
7
  export const COPY_CODE_KEY = 'tools.code.copyCode';
8
- export const WRAP_LINES_KEY = 'tools.code.wrapLines';
9
8
  export const SEARCH_LANGUAGE_KEY = 'tools.code.searchLanguage';
10
9
 
11
10
  // Default values
@@ -53,10 +52,12 @@ export const LANGUAGES: LanguageEntry[] = [
53
52
  ];
54
53
 
55
54
  // CSS — Tailwind classes
56
- export const WRAPPER_STYLES = 'flex flex-col rounded-lg bg-bg-secondary overflow-hidden my-1';
57
- export const HEADER_STYLES = 'flex items-center gap-1 px-3 py-1.5 border-b border-border-primary text-xs text-gray-text';
58
- export const LANGUAGE_BUTTON_STYLES = 'px-1.5 py-0.5 rounded cursor-pointer bg-transparent border-0 text-xs text-gray-text font-medium transition-colors can-hover:hover:bg-item-hover-bg select-none';
55
+ export const WRAPPER_STYLES = 'group/code flex flex-col rounded-xl border border-border-secondary bg-bg-secondary overflow-hidden my-2';
56
+ export const HEADER_STYLES = 'flex items-center gap-1 px-3 py-1.5 text-xs text-gray-text';
57
+ export const LANGUAGE_BUTTON_STYLES = 'inline-flex items-center px-1.5 py-0.5 rounded cursor-pointer bg-transparent border-0 text-xs text-gray-text font-medium transition-colors can-hover:hover:bg-item-hover-bg select-none';
58
+ export const HEADER_CONTROLS_STYLES = 'flex items-center gap-1 opacity-0 group-hover/code:opacity-100 transition-opacity';
59
59
  export const HEADER_BUTTON_STYLES = 'p-1 rounded cursor-pointer bg-transparent border-0 text-gray-text transition-colors can-hover:hover:bg-item-hover-bg flex items-center justify-center';
60
+ export const HEADER_BUTTON_MATCHED_STYLES = 'p-1.5 rounded-lg cursor-pointer bg-transparent border-0 text-gray-text transition-colors can-hover:hover:bg-item-hover-bg flex items-center justify-center';
60
61
  export const CODE_AREA_STYLES = 'block px-4 py-3 font-mono text-sm leading-relaxed outline-hidden whitespace-pre-wrap overflow-x-auto min-h-[1.5em]';
61
62
  export const COPIED_FEEDBACK_STYLES = 'text-xs text-gray-text font-medium select-none';
62
63
 
@@ -73,6 +74,28 @@ export const TAB_ACTIVE_STYLES = 'bg-blue-500 text-white';
73
74
  export const TAB_INACTIVE_STYLES = 'bg-transparent text-gray-text can-hover:hover:bg-item-hover-bg';
74
75
  export const PREVIEW_AREA_STYLES = 'px-4 py-3 overflow-x-auto min-h-[1.5em] flex justify-center';
75
76
 
77
+ // i18n key — preview toggle
78
+ export const PREVIEW_TOGGLE_KEY = 'tools.code.previewToggle';
79
+
80
+ // i18n key — side-by-side view mode
81
+ export const SIDE_BY_SIDE_KEY = 'tools.code.sideBySide';
82
+
83
+ // View mode type
84
+ export type CodeViewMode = 'code' | 'preview' | 'split';
85
+
86
+ // CSS — view mode segmented control
87
+ export const VIEW_MODE_CONTAINER_STYLES = 'flex items-center rounded-lg border border-border-secondary p-0.5 gap-0.5';
88
+ export const VIEW_MODE_BUTTON_STYLES = 'p-1 rounded cursor-pointer bg-transparent border-0 text-gray-text transition-colors flex items-center justify-center';
89
+ export const VIEW_MODE_BUTTON_ACTIVE_STYLES = 'p-1 rounded cursor-pointer bg-item-hover-bg border-0 text-primary transition-colors flex items-center justify-center';
90
+ // Preview icon is 16x16 (vs 20x20 for code/split), so extra 2px padding keeps containers equal
91
+ export const VIEW_MODE_PREVIEW_BUTTON_STYLES = 'p-[6px] rounded cursor-pointer bg-transparent border-0 text-gray-text transition-colors flex items-center justify-center';
92
+ export const VIEW_MODE_PREVIEW_BUTTON_ACTIVE_STYLES = 'p-[6px] rounded cursor-pointer bg-item-hover-bg border-0 text-primary transition-colors flex items-center justify-center';
93
+
94
+ // CSS — split container
95
+ export const SPLIT_CONTAINER_STYLES = 'flex flex-col overflow-hidden';
96
+ export const SPLIT_CONTAINER_SPLIT_STYLES = 'flex flex-row overflow-hidden';
97
+ export const SPLIT_HALF_STYLES = 'flex-1 min-w-0 overflow-hidden';
98
+
76
99
  // Shiki theme names for syntax highlighting
77
100
  export const SHIKI_LIGHT_THEME = 'one-light';
78
101
  export const SHIKI_DARK_THEME = 'vitesse-dark';
@@ -87,10 +110,7 @@ export const HIGHLIGHTABLE_LANGUAGES = new Set(
87
110
  .filter((id) => id !== DEFAULT_LANGUAGE)
88
111
  );
89
112
 
90
- // i18n key — line numbers toggle
91
- export const LINE_NUMBERS_KEY = 'tools.code.lineNumbers';
92
-
93
113
  // CSS — line number gutter
94
114
  export const CODE_BODY_STYLES = 'flex overflow-hidden';
95
- export const GUTTER_STYLES = 'select-none text-right pr-3 py-3 font-mono text-sm leading-relaxed text-gray-text/40 border-r border-border-primary shrink-0';
115
+ export const GUTTER_STYLES = 'select-none text-right pl-4 pr-3 py-3 font-mono text-sm leading-relaxed text-gray-text/40 shrink-0';
96
116
  export const GUTTER_LINE_STYLES = 'leading-relaxed';
@@ -2,30 +2,41 @@ import {
2
2
  WRAPPER_STYLES,
3
3
  HEADER_STYLES,
4
4
  LANGUAGE_BUTTON_STYLES,
5
+ HEADER_CONTROLS_STYLES,
5
6
  HEADER_BUTTON_STYLES,
7
+ HEADER_BUTTON_MATCHED_STYLES,
6
8
  CODE_AREA_STYLES,
7
- TAB_STYLES,
8
- TAB_ACTIVE_STYLES,
9
- TAB_INACTIVE_STYLES,
10
9
  PREVIEW_AREA_STYLES,
11
10
  CODE_BODY_STYLES,
12
11
  GUTTER_STYLES,
13
12
  GUTTER_LINE_STYLES,
13
+ VIEW_MODE_CONTAINER_STYLES,
14
+ VIEW_MODE_BUTTON_STYLES,
15
+ VIEW_MODE_BUTTON_ACTIVE_STYLES,
16
+ VIEW_MODE_PREVIEW_BUTTON_STYLES,
17
+ VIEW_MODE_PREVIEW_BUTTON_ACTIVE_STYLES,
18
+ SPLIT_CONTAINER_STYLES,
19
+ SPLIT_HALF_STYLES,
14
20
  } from './constants';
15
- import { IconCopy, IconWrap, IconLineNumbers } from '../../components/icons';
21
+ import type { CodeViewMode } from './constants';
22
+ import { IconCopy, IconCode, IconPreview, IconSplitView, IconChevronDown } from '../../components/icons';
16
23
 
17
24
  export interface CodeDOMRefs {
18
25
  wrapper: HTMLElement;
19
26
  languageButton: HTMLButtonElement;
20
- lineNumbersButton: HTMLButtonElement;
21
27
  copyButton: HTMLButtonElement;
22
- wrapButton: HTMLButtonElement;
23
28
  preElement: HTMLPreElement;
24
29
  codeElement: HTMLElement;
25
30
  gutterElement: HTMLElement;
26
- codeTab: HTMLButtonElement | null;
27
- previewTab: HTMLButtonElement | null;
31
+ viewModeContainer: HTMLElement | null;
28
32
  previewElement: HTMLDivElement | null;
33
+ splitContainer: HTMLElement | null;
34
+ }
35
+
36
+ export interface ViewModeLabels {
37
+ code: string;
38
+ preview: string;
39
+ split: string;
29
40
  }
30
41
 
31
42
  export interface BuildCodeDOMOptions {
@@ -33,41 +44,83 @@ export interface BuildCodeDOMOptions {
33
44
  languageName: string;
34
45
  readOnly: boolean;
35
46
  copyLabel: string;
36
- wrapLabel: string;
37
- lineNumbersLabel?: string;
38
47
  previewable?: boolean;
39
- codeTabLabel?: string;
40
- previewTabLabel?: string;
48
+ viewModeLabels?: ViewModeLabels;
41
49
  }
42
50
 
43
- function buildPreviewElements(
44
- codeTabLabel?: string,
45
- previewTabLabel?: string,
46
- ): { codeTab: HTMLButtonElement; previewTab: HTMLButtonElement; previewElement: HTMLDivElement } {
47
- const codeTab = document.createElement('button');
48
-
49
- codeTab.type = 'button';
50
- codeTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
51
- codeTab.textContent = codeTabLabel ?? 'Code';
52
- codeTab.setAttribute('data-blok-testid', 'code-code-tab');
53
-
54
- const previewTab = document.createElement('button');
51
+ interface ViewModeElements {
52
+ viewModeContainer: HTMLElement;
53
+ previewElement: HTMLDivElement;
54
+ splitContainer: HTMLElement;
55
+ }
55
56
 
56
- previewTab.type = 'button';
57
- previewTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
58
- previewTab.textContent = previewTabLabel ?? 'Preview';
59
- previewTab.setAttribute('data-blok-testid', 'code-preview-tab');
57
+ function buildViewModeElements(
58
+ labels: ViewModeLabels,
59
+ ): ViewModeElements {
60
+ // Segmented control container
61
+ const viewModeContainer = document.createElement('div');
62
+
63
+ viewModeContainer.className = VIEW_MODE_CONTAINER_STYLES;
64
+ viewModeContainer.setAttribute('role', 'group');
65
+ viewModeContainer.setAttribute('data-blok-testid', 'code-view-mode');
66
+
67
+ const modes: Array<{ mode: CodeViewMode; icon: string; label: string }> = [
68
+ { mode: 'code', icon: IconCode, label: labels.code },
69
+ { mode: 'preview', icon: IconPreview, label: labels.preview },
70
+ { mode: 'split', icon: IconSplitView, label: labels.split },
71
+ ];
72
+
73
+ for (const { mode, icon, label } of modes) {
74
+ const button = document.createElement('button');
75
+ const isPreview = mode === 'preview';
76
+
77
+ button.type = 'button';
78
+ button.className = isPreview ? VIEW_MODE_PREVIEW_BUTTON_STYLES : VIEW_MODE_BUTTON_STYLES;
79
+ button.innerHTML = icon;
80
+ button.setAttribute('aria-label', label);
81
+ button.setAttribute('aria-pressed', 'false');
82
+ button.setAttribute('data-blok-testid', `code-mode-${mode}`);
83
+ button.setAttribute('data-mode', mode);
84
+ viewModeContainer.appendChild(button);
85
+ }
60
86
 
87
+ // Preview container
61
88
  const previewElement = document.createElement('div');
62
89
 
63
90
  previewElement.className = PREVIEW_AREA_STYLES;
64
91
  previewElement.setAttribute('data-blok-testid', 'code-preview');
65
92
 
66
- return { codeTab, previewTab, previewElement };
93
+ // Split container wraps code body + preview
94
+ const splitContainer = document.createElement('div');
95
+
96
+ splitContainer.className = SPLIT_CONTAINER_STYLES;
97
+ splitContainer.setAttribute('data-blok-testid', 'code-split-container');
98
+
99
+ return { viewModeContainer, previewElement, splitContainer };
100
+ }
101
+
102
+ /**
103
+ * Set the active view mode button styling and aria-pressed state.
104
+ */
105
+ export function setActiveViewMode(viewModeContainer: HTMLElement, mode: CodeViewMode): void {
106
+ const buttons = Array.from(viewModeContainer.querySelectorAll<HTMLButtonElement>('[data-mode]'));
107
+
108
+ for (const btn of buttons) {
109
+ const isActive = btn.getAttribute('data-mode') === mode;
110
+ const isPreview = btn.getAttribute('data-mode') === 'preview';
111
+
112
+ btn.setAttribute('aria-pressed', String(isActive));
113
+
114
+ if (isPreview) {
115
+ btn.className = isActive ? VIEW_MODE_PREVIEW_BUTTON_ACTIVE_STYLES : VIEW_MODE_PREVIEW_BUTTON_STYLES;
116
+ } else {
117
+ btn.className = isActive ? VIEW_MODE_BUTTON_ACTIVE_STYLES : VIEW_MODE_BUTTON_STYLES;
118
+ }
119
+ }
67
120
  }
68
121
 
69
122
  export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
70
- const { code, languageName, readOnly, copyLabel, wrapLabel, lineNumbersLabel, previewable, codeTabLabel, previewTabLabel } = options;
123
+ const { code, languageName, readOnly, copyLabel, previewable, viewModeLabels } = options;
71
124
 
72
125
  // Wrapper
73
126
  const wrapper = document.createElement('div');
@@ -77,43 +130,43 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
77
130
  const header = document.createElement('div');
78
131
  header.className = HEADER_STYLES;
79
132
 
80
- // Language button (opens language picker)
133
+ // Language button (opens language picker) — includes text + chevron icon
81
134
  const languageButton = document.createElement('button');
82
135
  languageButton.type = 'button';
83
136
  languageButton.className = LANGUAGE_BUTTON_STYLES;
84
- languageButton.textContent = languageName;
85
137
  languageButton.setAttribute('aria-haspopup', 'listbox');
86
138
  languageButton.setAttribute('data-blok-testid', 'code-language-btn');
87
139
 
140
+ const langText = document.createElement('span');
141
+ langText.textContent = languageName;
142
+ languageButton.appendChild(langText);
143
+
144
+ const chevronSpan = document.createElement('span');
145
+ chevronSpan.className = 'inline-flex items-center ml-0.5 -mr-0.5';
146
+ chevronSpan.innerHTML = IconChevronDown;
147
+ languageButton.appendChild(chevronSpan);
148
+
88
149
  // Spacer
89
150
  const spacer = document.createElement('div');
90
151
  spacer.className = 'flex-1';
91
152
 
92
- // Tab buttons (only when previewable)
93
- const { codeTab, previewTab, previewElement } = previewable
94
- ? buildPreviewElements(codeTabLabel, previewTabLabel)
95
- : { codeTab: null, previewTab: null, previewElement: null };
96
-
97
- // Wrap toggle button
98
- const wrapButton = document.createElement('button');
99
- wrapButton.type = 'button';
100
- wrapButton.className = HEADER_BUTTON_STYLES;
101
- wrapButton.innerHTML = IconWrap;
102
- wrapButton.setAttribute('aria-label', wrapLabel);
103
- wrapButton.setAttribute('data-blok-testid', 'code-wrap-btn');
104
-
105
- // Line numbers toggle button
106
- const lineNumbersButton = document.createElement('button');
107
- lineNumbersButton.type = 'button';
108
- lineNumbersButton.className = HEADER_BUTTON_STYLES;
109
- lineNumbersButton.innerHTML = IconLineNumbers;
110
- lineNumbersButton.setAttribute('aria-label', lineNumbersLabel ?? 'Line numbers');
111
- lineNumbersButton.setAttribute('data-blok-testid', 'code-line-numbers-btn');
153
+ // View mode segmented control — always built in edit mode, hidden for non-previewable languages
154
+ const viewModeResult = !readOnly && viewModeLabels
155
+ ? buildViewModeElements(viewModeLabels)
156
+ : null;
157
+
158
+ const viewModeContainer = viewModeResult?.viewModeContainer ?? null;
159
+ const previewElement = viewModeResult?.previewElement ?? null;
160
+ const splitContainer = viewModeResult?.splitContainer ?? null;
161
+
162
+ if (viewModeContainer) {
163
+ viewModeContainer.hidden = !previewable;
164
+ }
112
165
 
113
166
  // Copy button
114
167
  const copyButton = document.createElement('button');
115
168
  copyButton.type = 'button';
116
- copyButton.className = HEADER_BUTTON_STYLES;
169
+ copyButton.className = previewable ? HEADER_BUTTON_MATCHED_STYLES : HEADER_BUTTON_STYLES;
117
170
  copyButton.innerHTML = IconCopy;
118
171
  copyButton.setAttribute('aria-label', copyLabel);
119
172
  copyButton.setAttribute('data-blok-testid', 'code-copy-btn');
@@ -146,18 +199,21 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
146
199
  gutterElement.appendChild(lineEl);
147
200
  });
148
201
 
149
- // Assemble header
202
+ // Assemble header: [language] [spacer] [controls: view mode? | copy]
150
203
  header.appendChild(languageButton);
151
204
  header.appendChild(spacer);
152
205
 
153
- if (codeTab && previewTab) {
154
- header.appendChild(codeTab);
155
- header.appendChild(previewTab);
206
+ // Controls container hidden by default, visible on wrapper hover
207
+ const controls = document.createElement('div');
208
+ controls.className = HEADER_CONTROLS_STYLES;
209
+
210
+ if (viewModeContainer) {
211
+ controls.appendChild(viewModeContainer);
156
212
  }
157
213
 
158
- header.appendChild(lineNumbersButton);
159
- header.appendChild(wrapButton);
160
- header.appendChild(copyButton);
214
+ controls.appendChild(copyButton);
215
+
216
+ header.appendChild(controls);
161
217
 
162
218
  // Pre wrapper for semantic HTML
163
219
  const preElement = document.createElement('pre');
@@ -171,11 +227,24 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
171
227
 
172
228
  // Assemble wrapper
173
229
  wrapper.appendChild(header);
174
- wrapper.appendChild(codeBody);
175
230
 
176
- if (previewElement) {
177
- wrapper.appendChild(previewElement);
231
+ if (splitContainer && previewElement) {
232
+ // Edit mode: always wrap code body + preview in split container.
233
+ // previewElement is hidden initially; shown when a previewable language is active.
234
+ const codeHalf = document.createElement('div');
235
+ codeHalf.className = SPLIT_HALF_STYLES;
236
+ codeHalf.appendChild(codeBody);
237
+
238
+ const previewHalf = document.createElement('div');
239
+ previewHalf.className = SPLIT_HALF_STYLES;
240
+ previewHalf.appendChild(previewElement);
241
+
242
+ splitContainer.appendChild(codeHalf);
243
+ splitContainer.appendChild(previewHalf);
244
+ wrapper.appendChild(splitContainer);
245
+ } else {
246
+ wrapper.appendChild(codeBody);
178
247
  }
179
248
 
180
- return { wrapper, languageButton, lineNumbersButton, copyButton, wrapButton, preElement, codeElement, gutterElement, codeTab, previewTab, previewElement };
249
+ return { wrapper, languageButton, copyButton, preElement, codeElement, gutterElement, viewModeContainer, previewElement, splitContainer };
181
250
  }