@jackuait/blok 0.19.2 → 0.21.0

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 (307) hide show
  1. package/dist/blok.cjs +1 -1
  2. package/dist/blok.iife.js +4 -4
  3. package/dist/blok.mjs +2 -2
  4. package/dist/blok.umd.js +41 -41
  5. package/dist/chunks/{blok-C8_pY9Bg.cjs → blok-Cz71HTuP.cjs} +13 -13
  6. package/dist/chunks/{blok-DCc23rzA.mjs → blok-x8D2aON2.mjs} +2645 -2116
  7. package/dist/chunks/constants-BK6kLyZB.cjs +606 -0
  8. package/dist/chunks/{constants-C3-Lh6Fs.mjs → constants-CLzXh7Tb.mjs} +392 -267
  9. package/dist/chunks/{i18next-loader-BBPMegxc.cjs → i18next-loader-DVakgVCy.cjs} +1 -1
  10. package/dist/chunks/{i18next-loader-K-ir8UKk.mjs → i18next-loader-DfIonnVO.mjs} +1 -1
  11. package/dist/chunks/{lightweight-i18n-LFW8S8u-.cjs → lightweight-i18n-BnIqQ5Cf.cjs} +1 -1
  12. package/dist/chunks/{lightweight-i18n-DJpMke7a.mjs → lightweight-i18n-Cj88c8gl.mjs} +15 -0
  13. package/dist/chunks/{messages-Bb80Csl3.mjs → messages-0EAci1N0.mjs} +15 -0
  14. package/dist/chunks/{messages-BkgBVEiE.cjs → messages-1jzEPeW5.cjs} +1 -1
  15. package/dist/chunks/{messages-CATH_uz1.mjs → messages-4CqVAaCY.mjs} +16 -1
  16. package/dist/chunks/{messages-DFHNw6PQ.cjs → messages-4Qka94cm.cjs} +1 -1
  17. package/dist/chunks/{messages-CBbgEXuO.cjs → messages-7wDDggws.cjs} +1 -1
  18. package/dist/chunks/{messages-B7st3eF_.cjs → messages-9VzB9Djh.cjs} +1 -1
  19. package/dist/chunks/{messages-B_cHWyWC.cjs → messages-9bK437Rh.cjs} +1 -1
  20. package/dist/chunks/{messages-CZddldga.cjs → messages-AtTKtjsQ.cjs} +1 -1
  21. package/dist/chunks/{messages-HJnG9r2l.mjs → messages-B1inw201.mjs} +16 -1
  22. package/dist/chunks/{messages-B-u5B7tY.cjs → messages-B4ZVgIh5.cjs} +1 -1
  23. package/dist/chunks/{messages-CJFSq8Fl.cjs → messages-B5bSFRCa.cjs} +1 -1
  24. package/dist/{messages-HVgCKIww2.mjs → chunks/messages-B705gxqz.mjs} +16 -1
  25. package/dist/{messages-Dq_RdEny2.mjs → chunks/messages-B7F3tX4u.mjs} +16 -1
  26. package/dist/chunks/{messages-NJAjMFXd.cjs → messages-BDer7I67.cjs} +1 -1
  27. package/dist/chunks/{messages-BV22YIZ_.cjs → messages-BEEGgtPo.cjs} +1 -1
  28. package/dist/{messages-bN4Zmk2m.mjs → chunks/messages-BGlEWmsB.mjs} +16 -1
  29. package/dist/{messages-Ao2vCNAH2.mjs → chunks/messages-BJJsuZhy.mjs} +16 -1
  30. package/dist/chunks/{messages-DjrfovG0.cjs → messages-BJcc9h6p.cjs} +1 -1
  31. package/dist/chunks/{messages-Hfo6uCIC.cjs → messages-BL-E2Kw3.cjs} +1 -1
  32. package/dist/{messages-8__-vX3U.mjs → chunks/messages-BLJGzntb.mjs} +16 -1
  33. package/dist/chunks/{messages-Au1UpgkX.cjs → messages-BLNieIb-.cjs} +1 -1
  34. package/dist/chunks/{messages-DQJcAz4s.cjs → messages-BRbGk1-Z.cjs} +1 -1
  35. package/dist/chunks/{messages-BwOUSDJR.mjs → messages-BUlZRNoO.mjs} +15 -0
  36. package/dist/{messages-BKGg4XYm2.mjs → chunks/messages-BWQF9y2r.mjs} +16 -1
  37. package/dist/chunks/{messages-UXS3zL9A2.cjs → messages-BWh9I5Jv2.cjs} +1 -1
  38. package/dist/chunks/{messages-B6dbtt1c.cjs → messages-BX86v7Ev.cjs} +1 -1
  39. package/dist/chunks/{messages-DIFwztFo.cjs → messages-BYpvkG9S.cjs} +1 -1
  40. package/dist/{messages-CDueJTe8.mjs → chunks/messages-Bdw_vAdq2.mjs} +16 -1
  41. package/dist/chunks/{messages-DIetNXTx.cjs → messages-BeQVEC9q.cjs} +1 -1
  42. package/dist/{messages-CusWGsl42.mjs → chunks/messages-BeTZTXZm.mjs} +16 -1
  43. package/dist/chunks/{messages-CmsAbu_A.cjs → messages-BhTlR4PP.cjs} +1 -1
  44. package/dist/chunks/{messages-C3rGCBi_.mjs → messages-BhnZTZbQ.mjs} +16 -1
  45. package/dist/{messages-BpwhHkFb2.mjs → chunks/messages-BqJdoBHm.mjs} +16 -1
  46. package/dist/chunks/{messages-zPUQsksJ.mjs → messages-BqPiH7QC.mjs} +16 -1
  47. package/dist/chunks/{messages-CjfEt9bg.cjs → messages-Br_qu_wR.cjs} +1 -1
  48. package/dist/chunks/{messages-DtaPHADA.cjs → messages-BscUmD6D.cjs} +1 -1
  49. package/dist/{messages-Cg6a88Bb2.mjs → chunks/messages-Btapuvxo.mjs} +16 -1
  50. package/dist/chunks/{messages-CEySIig9.cjs → messages-BwCw0TJ1.cjs} +1 -1
  51. package/dist/chunks/{messages-BO-L5oN9.mjs → messages-C3RDWmXV.mjs} +15 -0
  52. package/dist/chunks/{messages-CZJZ-au3.cjs → messages-C3SZsVB9.cjs} +1 -1
  53. package/dist/chunks/{messages-DQr7bXMD.mjs → messages-C65mLRVK.mjs} +16 -1
  54. package/dist/chunks/{messages-CT8djlV6.cjs → messages-C73dKqPT.cjs} +1 -1
  55. package/dist/chunks/{messages-DOqJ6ogK.cjs → messages-C8f4kUOB.cjs} +1 -1
  56. package/dist/{messages-yuNINyS1.mjs → chunks/messages-C9vfrzKQ.mjs} +16 -1
  57. package/dist/{messages-Grhv5SE_.mjs → chunks/messages-CEbe34zh.mjs} +15 -0
  58. package/dist/chunks/{messages-CcsM8Q7g.cjs → messages-CHEpECGv.cjs} +1 -1
  59. package/dist/chunks/{messages-DEg5FAg1.cjs → messages-CHP0nYRn.cjs} +1 -1
  60. package/dist/chunks/{messages-CjCn5IsF.mjs → messages-CIbh-0oJ.mjs} +16 -1
  61. package/dist/chunks/{messages-BHCepNsI.cjs → messages-CJC94Hci.cjs} +1 -1
  62. package/dist/chunks/{messages-ByNR9m9W.cjs → messages-CMP5dbqQ.cjs} +1 -1
  63. package/dist/chunks/{messages-DpNmnBjK.cjs → messages-CMm8FeOL.cjs} +1 -1
  64. package/dist/chunks/{messages-Da8yFc7l.cjs → messages-CNMPtWiq.cjs} +1 -1
  65. package/dist/{messages-CqZgsrVP2.mjs → chunks/messages-CQUr4lBa.mjs} +16 -1
  66. package/dist/{messages-Cvq2EFPT2.mjs → chunks/messages-CUuOX1k3.mjs} +16 -1
  67. package/dist/chunks/{messages-BsTzdfZF.cjs → messages-CVerbLou.cjs} +1 -1
  68. package/dist/{messages-CGiVoFTi.mjs → chunks/messages-CWW9OwKO.mjs} +16 -1
  69. package/dist/{messages-Da1woCgt2.mjs → chunks/messages-CWqap54p.mjs} +16 -1
  70. package/dist/{messages-CEGqNXnP.mjs → chunks/messages-CXNQBFHp.mjs} +16 -1
  71. package/dist/chunks/{messages-DgF8OkNr.mjs → messages-CYDhuSF4.mjs} +16 -1
  72. package/dist/chunks/{messages-Bp0LdGKj.mjs → messages-CYy3Zd3F.mjs} +15 -0
  73. package/dist/{messages-CUcUO5bC2.mjs → chunks/messages-C_BM_XHo.mjs} +16 -1
  74. package/dist/chunks/{messages-IqNWRaNS.cjs → messages-CbKOqXT6.cjs} +1 -1
  75. package/dist/chunks/{messages-Bmq--WTx.mjs → messages-CbxXU_Um.mjs} +15 -0
  76. package/dist/chunks/{messages-CVrOxu3J.cjs → messages-CcO-3_4k.cjs} +1 -1
  77. package/dist/chunks/{messages-D41fO84y.cjs → messages-CcTkC677.cjs} +1 -1
  78. package/dist/chunks/{messages-Cy-2_lIA.cjs → messages-CdEZ98_p.cjs} +1 -1
  79. package/dist/{messages-KyJQtOkh2.mjs → chunks/messages-CeZ1lo-o.mjs} +16 -1
  80. package/dist/chunks/{messages-D7H9JUDr.cjs → messages-CgFAGUJX.cjs} +1 -1
  81. package/dist/chunks/{messages-Dwl878eS.mjs → messages-CprMp4Va.mjs} +16 -1
  82. package/dist/{messages-wmS6Z2m82.mjs → chunks/messages-CtXDxPOq.mjs} +16 -1
  83. package/dist/{messages-DcQNeoPB.mjs → chunks/messages-Cutnm4AZ.mjs} +15 -0
  84. package/dist/chunks/{messages-cSBZ5qVe.mjs → messages-CwIC_sse.mjs} +16 -1
  85. package/dist/chunks/{messages-7LWW3BVh.mjs → messages-CwbTr9lL.mjs} +16 -1
  86. package/dist/chunks/{messages-ETJ7tQ9O.cjs → messages-CxIknrw4.cjs} +1 -1
  87. package/dist/{messages-VIQ8vjvS.mjs → chunks/messages-D0Jj7Dtw.mjs} +15 -0
  88. package/dist/chunks/{messages-SozFksSf.cjs → messages-D0th-I9S.cjs} +1 -1
  89. package/dist/chunks/{messages-gh5G15J1.cjs → messages-D1XTyTJz.cjs} +1 -1
  90. package/dist/chunks/{messages-DNtAzacv.mjs → messages-D24EoKbO.mjs} +16 -1
  91. package/dist/chunks/{messages-DDJkNbJy.mjs → messages-D2chjyLM.mjs} +16 -1
  92. package/dist/chunks/{messages-CqRtDVFO.cjs → messages-D6y13edn.cjs} +1 -1
  93. package/dist/chunks/{messages-EsX3eikv.cjs → messages-D86p1eQY.cjs} +1 -1
  94. package/dist/{messages-BwCgJUdq.mjs → chunks/messages-D8L_8amv.mjs} +16 -1
  95. package/dist/chunks/{messages-C2ptZOWh.cjs → messages-DC-yFRyF.cjs} +1 -1
  96. package/dist/{messages-BgPDPIkO.mjs → chunks/messages-DEPxxCNU.mjs} +16 -1
  97. package/dist/chunks/{messages-VeyvtTce.cjs → messages-DG9l5agQ.cjs} +1 -1
  98. package/dist/chunks/{messages-B6794_2J.cjs → messages-DIeUb2w-.cjs} +1 -1
  99. package/dist/chunks/{messages-COx3hDcP.cjs → messages-DJ-RX5EJ.cjs} +1 -1
  100. package/dist/chunks/{messages-Cvg_uTfy.mjs → messages-DJ7lbpBX.mjs} +16 -1
  101. package/dist/{messages-Cn3uvFq3.mjs → chunks/messages-DJH_kd46.mjs} +16 -1
  102. package/dist/chunks/{messages-CWFSMzug2.cjs → messages-DLYl6KdK2.cjs} +1 -1
  103. package/dist/{messages-Ch-SytN8.mjs → chunks/messages-DMcCkOtN.mjs} +16 -1
  104. package/dist/chunks/{messages-BdL8bmTL.mjs → messages-DQks74hq.mjs} +16 -1
  105. package/dist/chunks/{messages-D-QWtvWg.mjs → messages-DTiqggtG.mjs} +16 -1
  106. package/dist/chunks/{messages-typO_i_k.mjs → messages-DTsSYToI.mjs} +15 -0
  107. package/dist/{messages-CwvHO8gp2.mjs → chunks/messages-DU6TOz6v.mjs} +16 -1
  108. package/dist/chunks/{messages-DmjccFHq.cjs → messages-DUbZA-w_.cjs} +1 -1
  109. package/dist/chunks/{messages-DCwrXfck.cjs → messages-DVAU-8rH.cjs} +1 -1
  110. package/dist/chunks/{messages-Z4LYgfWc.cjs → messages-Df6vXDrY.cjs} +1 -1
  111. package/dist/chunks/{messages-9B1F-k48.cjs → messages-Dh79ToYM.cjs} +1 -1
  112. package/dist/chunks/{messages-CuMtymEs.cjs → messages-Dh9psewW.cjs} +1 -1
  113. package/dist/chunks/{messages-BjYhwRk7.cjs → messages-Dln2T0zJ.cjs} +1 -1
  114. package/dist/chunks/{messages-DT-JnTnw.cjs → messages-DoA0Ecoi.cjs} +1 -1
  115. package/dist/chunks/{messages-D-p-omWy.mjs → messages-Dre_MB7o.mjs} +16 -1
  116. package/dist/chunks/{messages-BveiGA2x.cjs → messages-Dv4oyF7T.cjs} +1 -1
  117. package/dist/{messages-C6R0bzW5.mjs → chunks/messages-FMAQkmg4.mjs} +15 -0
  118. package/dist/{messages-CQzOV7VE2.mjs → chunks/messages-GownDU-J.mjs} +16 -1
  119. package/dist/chunks/{messages-CH9SBR-i.cjs → messages-JYea02IT.cjs} +1 -1
  120. package/dist/chunks/{messages-BXp_U3uS.cjs → messages-JyzorVPx.cjs} +1 -1
  121. package/dist/{messages-BIgvPd6I2.mjs → chunks/messages-L70TFQAY.mjs} +16 -1
  122. package/dist/{messages-CskLLTiG2.mjs → chunks/messages-L8CslBBZ.mjs} +16 -1
  123. package/dist/chunks/{messages-CM8uUnPx.cjs → messages-LX9PuLsT.cjs} +1 -1
  124. package/dist/{messages-BarhGl2S2.mjs → chunks/messages-NFUr7Sxk.mjs} +16 -1
  125. package/dist/chunks/{messages-CGdCpNZ62.cjs → messages-PpRvDwOa2.cjs} +1 -1
  126. package/dist/chunks/{messages-ZMG7qQ3D.mjs → messages-QQ5-ET8y.mjs} +16 -1
  127. package/dist/{messages-DbE-riR52.mjs → chunks/messages-Stf6rn2I.mjs} +16 -1
  128. package/dist/chunks/{messages-D7sU-NqL.cjs → messages-T8hCmys7.cjs} +1 -1
  129. package/dist/chunks/{messages-DDX6I6MX.mjs → messages-TZtv60nI.mjs} +16 -1
  130. package/dist/chunks/{messages-CeLv1whu2.mjs → messages-V9bvFOFc2.mjs} +16 -1
  131. package/dist/chunks/{messages-tR0BdeVG.cjs → messages-Vp8CpYh3.cjs} +1 -1
  132. package/dist/chunks/{messages-DUWCkLYR.cjs → messages-XqCqLy0k.cjs} +1 -1
  133. package/dist/chunks/{messages-B01D7aHY.mjs → messages-_1y6EX7X.mjs} +16 -1
  134. package/dist/chunks/{messages-CXk-iM-i.mjs → messages-aTZglEV9.mjs} +16 -1
  135. package/dist/chunks/{messages-BHdKd7pH.cjs → messages-cSHh8Zgv.cjs} +1 -1
  136. package/dist/{messages-P5B61L1J2.mjs → chunks/messages-e8gxPi7a.mjs} +16 -1
  137. package/dist/{messages-CUFac9Qe.mjs → chunks/messages-hiwlrrr-.mjs} +16 -1
  138. package/dist/{messages-CeqpzGl9.mjs → chunks/messages-icPZhZ81.mjs} +15 -0
  139. package/dist/chunks/{messages-Byn9-dhb2.mjs → messages-lbwOYFxS2.mjs} +16 -1
  140. package/dist/chunks/{messages-B6yMdb3B.cjs → messages-lvo-9Be0.cjs} +1 -1
  141. package/dist/chunks/{messages-B2iaIrIu.mjs → messages-rm6jXexA.mjs} +16 -1
  142. package/dist/chunks/{messages-Ciq5_PsP.cjs → messages-sTyGkmbu.cjs} +1 -1
  143. package/dist/{messages-CF5MeYEt2.mjs → chunks/messages-uBwK-Ze2.mjs} +16 -1
  144. package/dist/chunks/{messages-BaVZgTrw.cjs → messages-uOISsfmj.cjs} +1 -1
  145. package/dist/{messages-L4sYfXpM.mjs → chunks/messages-wq0vxJ3p.mjs} +16 -1
  146. package/dist/chunks/{messages-CQT8TtiF.cjs → messages-z2DzBRgO.cjs} +1 -1
  147. package/dist/chunks/tools-BKjP4TXy.cjs +208 -0
  148. package/dist/chunks/{tools-fEI-EVng.mjs → tools-Bw8_CkhE.mjs} +2537 -2469
  149. package/dist/full.cjs +1 -1
  150. package/dist/full.mjs +3 -3
  151. package/dist/locales.mjs +82 -67
  152. package/dist/{messages-BlvF3pot.mjs → messages--sHkn4PD.mjs} +16 -1
  153. package/dist/{chunks/messages-QCaF3d92.mjs → messages-4mxgsYKZ2.mjs} +16 -1
  154. package/dist/{messages-zOl6jpOa.mjs → messages-6fQMeTiI.mjs} +16 -1
  155. package/dist/{messages-BRLWpOnP.mjs → messages-B22G0fir.mjs} +16 -1
  156. package/dist/{chunks/messages-kqnzps-X.mjs → messages-B908tGhl.mjs} +16 -1
  157. package/dist/{chunks/messages-C8_r3iTR2.mjs → messages-BAUvKI0_.mjs} +16 -1
  158. package/dist/{chunks/messages-BpMWMH9_.mjs → messages-BAfMwQnT2.mjs} +16 -1
  159. package/dist/{messages-Bmu3rJwO.mjs → messages-BCk7o4Pz.mjs} +16 -1
  160. package/dist/{messages-D4dovFzY2.mjs → messages-BG2BE6Sg2.mjs} +16 -1
  161. package/dist/{messages-DR2JBU-H.mjs → messages-BGe6GS_j.mjs} +16 -1
  162. package/dist/{chunks/messages-CeIzWL_4.mjs → messages-BHLrVFmZ2.mjs} +16 -1
  163. package/dist/{messages-BOWDzfkH.mjs → messages-BJjffJIu.mjs} +16 -1
  164. package/dist/{chunks/messages-DHv7knYB.mjs → messages-BSGZSOH32.mjs} +16 -1
  165. package/dist/{chunks/messages-BRTWvTAP.mjs → messages-BT6TyDYQ2.mjs} +16 -1
  166. package/dist/{messages-jQsl7cga2.mjs → messages-BWZ0_zUi2.mjs} +16 -1
  167. package/dist/{messages-lnxRz5rM.mjs → messages-BWhJgyhl.mjs} +16 -1
  168. package/dist/{chunks/messages-Xh-c7d4C.mjs → messages-BXqLaS2g2.mjs} +16 -1
  169. package/dist/{messages-gTUy3xjb.mjs → messages-BZiuFv_O.mjs} +15 -0
  170. package/dist/{chunks/messages-B_d857cA.mjs → messages-Bb-XEtyl.mjs} +15 -0
  171. package/dist/{messages-C9Kq8259.mjs → messages-BkKAnFz_.mjs} +15 -0
  172. package/dist/{chunks/messages-BUvlOqll.mjs → messages-BoLu5_Wp2.mjs} +16 -1
  173. package/dist/{messages-RXQKVFCJ.mjs → messages-BpqRzil6.mjs} +16 -1
  174. package/dist/{chunks/messages-CkosWGGH.mjs → messages-C2E8rQjb.mjs} +15 -0
  175. package/dist/{chunks/messages-jEbEE90_.mjs → messages-C2JJxi5D2.mjs} +16 -1
  176. package/dist/{messages-jsrzDVZy2.mjs → messages-C4d1_z432.mjs} +16 -1
  177. package/dist/{chunks/messages-MPe9FjYe.mjs → messages-C5Igyij0.mjs} +16 -1
  178. package/dist/{messages-CAnOyxrm.mjs → messages-C8fscB2J.mjs} +16 -1
  179. package/dist/{chunks/messages-CX0tJpOb.mjs → messages-CD8CgT1F.mjs} +16 -1
  180. package/dist/{messages-BKG0QHVl.mjs → messages-CDxPk3xE.mjs} +15 -0
  181. package/dist/{messages-hGop1gbF.mjs → messages-CGXrIdHT.mjs} +15 -0
  182. package/dist/{chunks/messages-DuHFGMCJ.mjs → messages-CHfkCxGQ2.mjs} +16 -1
  183. package/dist/{messages-fAEKzdUq.mjs → messages-CMvNzksb.mjs} +16 -1
  184. package/dist/{chunks/messages-t26x8wxB.mjs → messages-CSIhcdDx2.mjs} +16 -1
  185. package/dist/{messages-UJx6PvCL.mjs → messages-CW5z5hSz.mjs} +16 -1
  186. package/dist/{chunks/messages-CUPup5e3.mjs → messages-CYvpDx4p2.mjs} +16 -1
  187. package/dist/{chunks/messages-Ca9zIrBo.mjs → messages-CcTlmrYs.mjs} +16 -1
  188. package/dist/{messages-CcF4SO9W.mjs → messages-Ce8ShL0-.mjs} +16 -1
  189. package/dist/{chunks/messages-CbyVDo8y.mjs → messages-CgT32m3D.mjs} +15 -0
  190. package/dist/{chunks/messages-ypIHI9Fr.mjs → messages-CmCBnNtA2.mjs} +16 -1
  191. package/dist/{messages-DGAWjKQZ.mjs → messages-CneVTJSN.mjs} +16 -1
  192. package/dist/{messages-C5IE9mXm.mjs → messages-CpNerer8.mjs} +15 -0
  193. package/dist/{chunks/messages-D-s4_vnU.mjs → messages-Cs0L1gyv.mjs} +16 -1
  194. package/dist/{chunks/messages-BYCHzM7F.mjs → messages-CsGnp1wE2.mjs} +16 -1
  195. package/dist/{chunks/messages-D4u2PSQw.mjs → messages-CvbIxcr7.mjs} +16 -1
  196. package/dist/{chunks/messages-DuD4ut1b.mjs → messages-Cx6oQfMR2.mjs} +16 -1
  197. package/dist/{messages-BvutXmgt.mjs → messages-DBESJOhQ.mjs} +16 -1
  198. package/dist/{chunks/messages-Cl_pM31G.mjs → messages-DGOBPnKC2.mjs} +16 -1
  199. package/dist/{messages-B13GEFFu.mjs → messages-DK2AwXAp.mjs} +16 -1
  200. package/dist/{chunks/messages-CI2W7P-j.mjs → messages-DMbnesQ9.mjs} +16 -1
  201. package/dist/{messages-4drvqPN8.mjs → messages-DOiSOenW.mjs} +16 -1
  202. package/dist/{chunks/messages-5339ApgH.mjs → messages-DQqp_ANL.mjs} +16 -1
  203. package/dist/{chunks/messages-Ck1-ddbo.mjs → messages-DThG2jW-2.mjs} +16 -1
  204. package/dist/{chunks/messages-DQfi2DoY.mjs → messages-DXlB7Vpm.mjs} +16 -1
  205. package/dist/{chunks/messages-DjZoCJ00.mjs → messages-De0NAxJt.mjs} +15 -0
  206. package/dist/{messages-BI5B9bxE.mjs → messages-Depz8d3Q.mjs} +15 -0
  207. package/dist/{messages-BQaiScU1.mjs → messages-Dfsw3a8v.mjs} +16 -1
  208. package/dist/{messages-D9qgYMU82.mjs → messages-Dhmmqob82.mjs} +16 -1
  209. package/dist/{chunks/messages-DiODNXET.mjs → messages-DmNUAv0e2.mjs} +16 -1
  210. package/dist/{chunks/messages-3jkCmB_A.mjs → messages-DrQaJrDO.mjs} +16 -1
  211. package/dist/{messages-B2YnZIU2.mjs → messages-DsksuHtZ.mjs} +16 -1
  212. package/dist/{chunks/messages-m-A6ArIM.mjs → messages-DzOVTq3E2.mjs} +16 -1
  213. package/dist/{chunks/messages-C0swL9CY.mjs → messages-Igpt5WIA2.mjs} +16 -1
  214. package/dist/{chunks/messages-CIBYzzFy.mjs → messages-JhpGtjjS.mjs} +16 -1
  215. package/dist/{chunks/messages-BbZz_Anm.mjs → messages-WCy1wiGS2.mjs} +16 -1
  216. package/dist/{chunks/messages-CRUb9o5k.mjs → messages-XbA-jXKc.mjs} +15 -0
  217. package/dist/{chunks/messages-BeS9EOEO.mjs → messages-imHGm9SI2.mjs} +16 -1
  218. package/dist/{messages-BQhHthxq2.mjs → messages-yTNyxXRV2.mjs} +16 -1
  219. package/dist/react.cjs +1 -1
  220. package/dist/react.mjs +1 -1
  221. package/dist/tools.cjs +1 -1
  222. package/dist/tools.mjs +2 -2
  223. package/package.json +1 -1
  224. package/src/components/i18n/locales/am/messages.json +16 -1
  225. package/src/components/i18n/locales/ar/messages.json +16 -1
  226. package/src/components/i18n/locales/az/messages.json +16 -1
  227. package/src/components/i18n/locales/bg/messages.json +16 -1
  228. package/src/components/i18n/locales/bn/messages.json +16 -1
  229. package/src/components/i18n/locales/bs/messages.json +16 -1
  230. package/src/components/i18n/locales/cs/messages.json +16 -1
  231. package/src/components/i18n/locales/da/messages.json +16 -1
  232. package/src/components/i18n/locales/de/messages.json +16 -1
  233. package/src/components/i18n/locales/dv/messages.json +16 -1
  234. package/src/components/i18n/locales/el/messages.json +16 -1
  235. package/src/components/i18n/locales/en/messages.json +15 -0
  236. package/src/components/i18n/locales/es/messages.json +15 -0
  237. package/src/components/i18n/locales/et/messages.json +15 -0
  238. package/src/components/i18n/locales/fa/messages.json +15 -0
  239. package/src/components/i18n/locales/fi/messages.json +15 -0
  240. package/src/components/i18n/locales/fil/messages.json +15 -0
  241. package/src/components/i18n/locales/fr/messages.json +15 -0
  242. package/src/components/i18n/locales/gu/messages.json +15 -0
  243. package/src/components/i18n/locales/he/messages.json +15 -0
  244. package/src/components/i18n/locales/hi/messages.json +15 -0
  245. package/src/components/i18n/locales/hr/messages.json +15 -0
  246. package/src/components/i18n/locales/hu/messages.json +15 -0
  247. package/src/components/i18n/locales/hy/messages.json +16 -1
  248. package/src/components/i18n/locales/id/messages.json +16 -1
  249. package/src/components/i18n/locales/it/messages.json +16 -1
  250. package/src/components/i18n/locales/ja/messages.json +16 -1
  251. package/src/components/i18n/locales/ka/messages.json +16 -1
  252. package/src/components/i18n/locales/km/messages.json +16 -1
  253. package/src/components/i18n/locales/kn/messages.json +16 -1
  254. package/src/components/i18n/locales/ko/messages.json +16 -1
  255. package/src/components/i18n/locales/ku/messages.json +16 -1
  256. package/src/components/i18n/locales/lo/messages.json +16 -1
  257. package/src/components/i18n/locales/lt/messages.json +16 -1
  258. package/src/components/i18n/locales/lv/messages.json +16 -1
  259. package/src/components/i18n/locales/mk/messages.json +16 -1
  260. package/src/components/i18n/locales/ml/messages.json +16 -1
  261. package/src/components/i18n/locales/mn/messages.json +16 -1
  262. package/src/components/i18n/locales/mr/messages.json +16 -1
  263. package/src/components/i18n/locales/ms/messages.json +16 -1
  264. package/src/components/i18n/locales/my/messages.json +16 -1
  265. package/src/components/i18n/locales/ne/messages.json +16 -1
  266. package/src/components/i18n/locales/nl/messages.json +16 -1
  267. package/src/components/i18n/locales/no/messages.json +16 -1
  268. package/src/components/i18n/locales/pa/messages.json +16 -1
  269. package/src/components/i18n/locales/pl/messages.json +16 -1
  270. package/src/components/i18n/locales/ps/messages.json +16 -1
  271. package/src/components/i18n/locales/pt/messages.json +16 -1
  272. package/src/components/i18n/locales/ro/messages.json +16 -1
  273. package/src/components/i18n/locales/ru/messages.json +16 -1
  274. package/src/components/i18n/locales/sd/messages.json +16 -1
  275. package/src/components/i18n/locales/si/messages.json +16 -1
  276. package/src/components/i18n/locales/sk/messages.json +16 -1
  277. package/src/components/i18n/locales/sl/messages.json +16 -1
  278. package/src/components/i18n/locales/sq/messages.json +16 -1
  279. package/src/components/i18n/locales/sr/messages.json +16 -1
  280. package/src/components/i18n/locales/sv/messages.json +16 -1
  281. package/src/components/i18n/locales/sw/messages.json +16 -1
  282. package/src/components/i18n/locales/ta/messages.json +16 -1
  283. package/src/components/i18n/locales/te/messages.json +16 -1
  284. package/src/components/i18n/locales/th/messages.json +16 -1
  285. package/src/components/i18n/locales/tr/messages.json +16 -1
  286. package/src/components/i18n/locales/ug/messages.json +16 -1
  287. package/src/components/i18n/locales/uk/messages.json +16 -1
  288. package/src/components/i18n/locales/ur/messages.json +16 -1
  289. package/src/components/i18n/locales/vi/messages.json +16 -1
  290. package/src/components/i18n/locales/yi/messages.json +16 -1
  291. package/src/components/i18n/locales/zh/messages.json +16 -1
  292. package/src/components/modules/paste/handlers/blok-data-handler.ts +39 -23
  293. package/src/components/modules/paste/index.ts +38 -4
  294. package/src/components/modules/paste/notion-blocks-v3.ts +932 -0
  295. package/src/components/modules/paste/notion-preprocessor.ts +67 -0
  296. package/src/components/utils/media-empty-state.ts +48 -5
  297. package/src/styles/audio.css +348 -36
  298. package/src/styles/tables.css +10 -0
  299. package/src/tools/audio/constants.ts +6 -0
  300. package/src/tools/audio/controls.ts +53 -7
  301. package/src/tools/audio/cover-picker.ts +126 -0
  302. package/src/tools/audio/disc.ts +137 -0
  303. package/src/tools/audio/index.ts +114 -6
  304. package/src/tools/audio/liveliness.ts +26 -8
  305. package/src/tools/audio/ui.ts +39 -4
  306. package/dist/chunks/constants-R70ypRAK.cjs +0 -606
  307. package/dist/chunks/tools-DcbePIHF.cjs +0 -208
@@ -152,3 +152,13 @@
152
152
  [data-blok-theme="dark"] [data-blok-interface] [data-blok-table-haze="right"] {
153
153
  background-image: linear-gradient(to left, var(--blok-dark), transparent);
154
154
  }
155
+
156
+ /* Table card pins its top toolbar anchor absolutely to the (position:relative)
157
+ wrapper top, so the +/tunes buttons sit flush with the table's top edge while
158
+ the marker stays out of the flow — it never becomes the wrapper's firstChild,
159
+ which the grid/scroll structure relies on. */
160
+ [data-blok-tool="table"] .blok-toolbar-top-anchor {
161
+ position: absolute;
162
+ top: 0;
163
+ left: 0;
164
+ }
@@ -19,3 +19,9 @@ export const COVER_DATA_URL_CAP_BYTES = 150 * 1024;
19
19
 
20
20
  /** Direct links to a playable audio file. */
21
21
  export const URL_PATTERN = /^https?:\/\/\S+\.(mp3|wav|ogg|oga|m4a|aac|flac|weba)(\?\S*)?$/i;
22
+
23
+ /** Accepted cover-image MIME types (family wildcard). */
24
+ export const COVER_TYPES = ['image/*'] as const;
25
+
26
+ /** Max cover-image size in bytes. */
27
+ export const COVER_MAX_SIZE = 10 * 1024 * 1024; // 10 MiB
@@ -1,4 +1,5 @@
1
1
  import {
2
+ IconMinus,
2
3
  IconPlayerLoop,
3
4
  IconPlayerPause,
4
5
  IconPlayerPlay,
@@ -6,7 +7,9 @@ import {
6
7
  IconPlayerSpeed,
7
8
  IconPlayerVolume,
8
9
  IconPlayerVolumeMute,
10
+ IconPlus,
9
11
  } from '../../components/icons';
12
+ import { createPlatterSpin } from './disc';
10
13
  import type { AudioData } from '../../../types/tools/audio';
11
14
 
12
15
  /** Minimal storage seam (localStorage-shaped) for volume + position persistence. */
@@ -99,11 +102,17 @@ export function attachControls({
99
102
 
100
103
  const state = { playing: false, selectedRate: 1 };
101
104
 
105
+ // Inertial spin for the no-cover vinyl record (no-op when a cover image is
106
+ // shown — the disc only exists in the placeholder). Spins up on play, coasts
107
+ // down on pause.
108
+ const platter = createPlatterSpin(figure);
109
+
102
110
  const setPlaying = (next: boolean): void => {
103
111
  state.playing = next;
104
112
  figure.setAttribute('data-playing', String(next));
105
113
  playToggle.innerHTML = next ? IconPlayerPause : IconPlayerPlay;
106
114
  playToggle.setAttribute('aria-label', next ? 'Pause' : 'Play');
115
+ if (next) platter.start(); else platter.stop();
107
116
  };
108
117
  setPlaying(false);
109
118
 
@@ -139,7 +148,10 @@ export function attachControls({
139
148
  muteToggle.innerHTML = muted ? IconPlayerVolumeMute : IconPlayerVolume;
140
149
  muteToggle.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');
141
150
  volumeInput.value = muted ? '0' : String(media.volume);
142
- volumeInput.style.setProperty('--blok-vol-pct', `${Number(volumeInput.value) * 100}%`);
151
+ // audio.css paints the filled track from --blok-audio-vol-pct (same naming
152
+ // as the speed slider's --blok-audio-speed-pct); writing any other name
153
+ // freezes the fill at its 100% default, so muted looks identical to full.
154
+ volumeInput.style.setProperty('--blok-audio-vol-pct', `${Number(volumeInput.value) * 100}%`);
143
155
  };
144
156
 
145
157
  // ----- persistence -----
@@ -207,6 +219,20 @@ export function attachControls({
207
219
  speedReadout.setAttribute('aria-hidden', 'true');
208
220
  speedReadout.textContent = speedLabel(state.selectedRate);
209
221
 
222
+ // − [slider] + — flat steppers flank the track, mirroring the video player.
223
+ // Identified by class only (no data-role) so the generic button[data-role]
224
+ // control-button rule doesn't size them into 30px pills.
225
+ const speedStepper = (icon: string, label: string): HTMLButtonElement => {
226
+ const btn = document.createElement('button');
227
+ btn.type = 'button';
228
+ btn.className = 'blok-audio-controls__speed-step';
229
+ btn.setAttribute('aria-label', label);
230
+ btn.innerHTML = icon;
231
+ return btn;
232
+ };
233
+ const speedDec = speedStepper(IconMinus, 'Decrease playback speed');
234
+ const speedInc = speedStepper(IconPlus, 'Increase playback speed');
235
+
210
236
  const speedSlider = document.createElement('input');
211
237
  speedSlider.type = 'range';
212
238
  speedSlider.className = 'blok-audio-controls__speed-slider';
@@ -216,6 +242,12 @@ export function attachControls({
216
242
  speedSlider.value = String(state.selectedRate);
217
243
  speedSlider.setAttribute('aria-label', 'Playback speed');
218
244
 
245
+ // Drive the elapsed-fill gradient from JS so the painted track tracks the
246
+ // native knob exactly (same approach as the volume + video sliders).
247
+ const speedFillPct = (rate: number): string =>
248
+ `${((rate - SPEED_MIN) / (SPEED_MAX - SPEED_MIN)) * 100}%`;
249
+ speedSlider.style.setProperty('--blok-audio-speed-pct', speedFillPct(state.selectedRate));
250
+
219
251
  const setRate = (rate: number): void => {
220
252
  const next = clampRate(rate);
221
253
  state.selectedRate = next;
@@ -223,11 +255,25 @@ export function attachControls({
223
255
  speedReadout.textContent = speedLabel(next);
224
256
  speedSlider.value = String(next);
225
257
  speedSlider.setAttribute('aria-valuetext', speedLabel(next));
258
+ speedSlider.style.setProperty('--blok-audio-speed-pct', speedFillPct(next));
259
+ speedDec.disabled = next <= SPEED_MIN;
260
+ speedInc.disabled = next >= SPEED_MAX;
226
261
  };
227
262
 
228
263
  const onSpeedSliderInput = (): void => setRate(Number(speedSlider.value));
229
264
  speedSlider.addEventListener('input', onSpeedSliderInput);
230
-
265
+ speedDec.addEventListener('click', () => setRate(state.selectedRate - SPEED_SLIDER_STEP));
266
+ speedInc.addEventListener('click', () => setRate(state.selectedRate + SPEED_SLIDER_STEP));
267
+ speedDec.disabled = state.selectedRate <= SPEED_MIN;
268
+ speedInc.disabled = state.selectedRate >= SPEED_MAX;
269
+
270
+ const speedSliderRow = document.createElement('div');
271
+ speedSliderRow.className = 'blok-audio-controls__speed-slider-row';
272
+ speedSliderRow.append(speedDec, speedSlider, speedInc);
273
+
274
+ // Quiet quick-jump presets (mirrors the video player): every chip reads the
275
+ // same muted weight, lifting only on hover. The live rate is communicated by
276
+ // the readout + slider knob, not a filled "selected" chip.
231
277
  const speedChipRow = document.createElement('div');
232
278
  speedChipRow.className = 'blok-audio-controls__speed-chips';
233
279
  SPEED_PRESETS.forEach((rate) => {
@@ -235,14 +281,13 @@ export function attachControls({
235
281
  chip.type = 'button';
236
282
  chip.className = 'blok-audio-controls__speed-chip';
237
283
  chip.textContent = `${rate}×`;
238
- chip.addEventListener('click', () => {
239
- setRate(rate);
240
- closeSpeedMenu();
241
- });
284
+ // Apply the preset but leave the menu open so the user can keep nudging the
285
+ // rate (slider/steppers/other presets) without re-opening it each time.
286
+ chip.addEventListener('click', () => setRate(rate));
242
287
  speedChipRow.appendChild(chip);
243
288
  });
244
289
 
245
- speedMenu.append(speedReadout, speedSlider, speedChipRow);
290
+ speedMenu.append(speedReadout, speedSliderRow, speedChipRow);
246
291
 
247
292
  // Gear + menu share a positioned wrapper so the menu anchors directly above
248
293
  // the gear button (rather than the card corner).
@@ -391,6 +436,7 @@ export function attachControls({
391
436
 
392
437
  // ----- destroy -----
393
438
  const destroy = (): void => {
439
+ platter.destroy();
394
440
  closeSpeedMenu();
395
441
 
396
442
  playToggle.removeEventListener('click', togglePlay);
@@ -0,0 +1,126 @@
1
+ import { matchesMime } from '../../components/utils/mime-match';
2
+ import type { I18nInstance } from '../../components/utils/tools';
3
+ import { tr } from './i18n';
4
+ import { COVER_TYPES, COVER_MAX_SIZE } from './constants';
5
+ import { promoteToTopLayer, removeFromTopLayer } from '../../components/utils/top-layer';
6
+ import {
7
+ renderMediaEmptyState,
8
+ type MediaEmptyStateElement,
9
+ } from '../../components/utils/media-empty-state';
10
+
11
+ /** Validate a chosen cover file. Returns an error message, or null when valid. */
12
+ export function validateCoverFile(file: File, i18n?: I18nInstance): string | null {
13
+ if (file.type && !matchesMime(file.type, [...COVER_TYPES])) {
14
+ return tr(i18n, 'tools.audio.coverErrorType', 'Choose an image file');
15
+ }
16
+ if (file.size > COVER_MAX_SIZE) {
17
+ return tr(i18n, 'tools.audio.coverErrorTooLarge', 'Image is too large');
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export interface OpenCoverPickerOptions {
23
+ anchor: HTMLElement;
24
+ onFile(file: File): void;
25
+ onUrl(url: string): void;
26
+ onClose?(): void;
27
+ i18n?: I18nInstance;
28
+ }
29
+
30
+ export interface CoverPickerHandle {
31
+ close(): void;
32
+ setError(message: string | null): void;
33
+ }
34
+
35
+ export function openCoverPicker(opts: OpenCoverPickerOptions): CoverPickerHandle {
36
+ const dialog = document.createElement('div');
37
+ dialog.className = 'blok-audio-cover-picker';
38
+ dialog.setAttribute('data-role', 'audio-cover-picker');
39
+ dialog.setAttribute('role', 'dialog');
40
+ dialog.setAttribute('aria-label', tr(opts.i18n, 'tools.audio.coverSet', 'Set cover image'));
41
+
42
+ const surface: MediaEmptyStateElement = renderMediaEmptyState({
43
+ acceptTypes: [...COVER_TYPES],
44
+ onFile: opts.onFile,
45
+ onUrl: opts.onUrl,
46
+ // The picker is a floating popover, so the inline height tween only reads as
47
+ // lag here. Slide the incoming panel in from the travel direction instead —
48
+ // the swap keeps its personality while the popover resizes instantly.
49
+ swap: 'slide',
50
+ labels: {
51
+ add: tr(opts.i18n, 'tools.audio.coverAdd', 'Add a cover'),
52
+ upload: tr(opts.i18n, 'tools.audio.coverUpload', 'Upload'),
53
+ embed: tr(opts.i18n, 'tools.audio.coverLink', 'Link'),
54
+ chooseFile: tr(opts.i18n, 'tools.audio.coverChooseFile', 'Choose file'),
55
+ orDropHere: tr(opts.i18n, 'tools.audio.coverOrDropHere', 'or drop an image here'),
56
+ dropToUpload: tr(opts.i18n, 'tools.audio.coverDropToUpload', 'Drop to upload'),
57
+ urlPlaceholder: tr(opts.i18n, 'tools.audio.coverUrlPlaceholder', 'Paste an image URL…'),
58
+ urlAria: tr(opts.i18n, 'tools.audio.coverUrlAria', 'Image URL'),
59
+ submit: tr(opts.i18n, 'tools.audio.coverInsert', 'Insert'),
60
+ sourceAria: tr(opts.i18n, 'tools.audio.coverSourceAria', 'Cover source'),
61
+ },
62
+ });
63
+ dialog.appendChild(surface);
64
+
65
+ document.body.appendChild(dialog);
66
+ promoteToTopLayer(dialog);
67
+ position(dialog, opts.anchor);
68
+
69
+ const state = { detached: false };
70
+
71
+ const detach = (): void => {
72
+ if (state.detached) return;
73
+ state.detached = true;
74
+ document.removeEventListener('keydown', onKeyDown, true);
75
+ document.removeEventListener('mousedown', onOutside);
76
+ window.removeEventListener('resize', reposition);
77
+ window.removeEventListener('scroll', reposition, true);
78
+ removeFromTopLayer(dialog);
79
+ dialog.remove();
80
+ };
81
+
82
+ const close = (): void => {
83
+ detach();
84
+ opts.onClose?.();
85
+ };
86
+
87
+ const onKeyDown = (event: KeyboardEvent): void => {
88
+ if (event.key === 'Escape') {
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+ close();
92
+ }
93
+ };
94
+
95
+ const onOutside = (event: MouseEvent): void => {
96
+ const target = event.target as Node | null;
97
+ if (target && (dialog.contains(target) || opts.anchor.contains(target))) return;
98
+ close();
99
+ };
100
+
101
+ function reposition(): void {
102
+ if (state.detached) return;
103
+ position(dialog, opts.anchor);
104
+ }
105
+
106
+ // Capture phase for Escape so it beats the editor's own key handling.
107
+ document.addEventListener('keydown', onKeyDown, true);
108
+ document.addEventListener('mousedown', onOutside);
109
+ window.addEventListener('resize', reposition);
110
+ window.addEventListener('scroll', reposition, true);
111
+
112
+ return {
113
+ close,
114
+ setError: (message) => surface.setError(message),
115
+ };
116
+ }
117
+
118
+ function position(dialog: HTMLElement, anchor: HTMLElement): void {
119
+ const rect = anchor.getBoundingClientRect();
120
+ const gap = 8;
121
+ const { style } = dialog;
122
+ style.position = 'fixed';
123
+ style.top = `${rect.bottom + gap}px`;
124
+ style.left = `${Math.max(8, rect.left)}px`;
125
+ style.right = 'auto';
126
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Inertial platter spin for the no-cover vinyl record.
3
+ *
4
+ * With the tonearm gone, the disc is the whole show — so it shouldn't just snap
5
+ * on and off. A real platter has mass: it spins UP when the motor engages and
6
+ * COASTS DOWN when power is cut. We model that with an angular velocity that
7
+ * eases toward a target (full speed while playing, 0 when stopped) and integrate
8
+ * it into an accumulated angle each animation frame, driving the disc's
9
+ * `transform: rotate()` directly. Spin-up is brisk; the wind-down uses a longer
10
+ * time constant, so pausing reads as a heavy record slowing to rest rather than
11
+ * stopping dead.
12
+ *
13
+ * The velocity easing is exponential (frame-rate independent — see stepPlatter),
14
+ * so it behaves identically on a 60Hz or 120Hz display. All of it is gated on
15
+ * prefers-reduced-motion: the record simply holds still.
16
+ */
17
+
18
+ /** Platter motion state: accumulated angle (deg, wrapped) + angular speed (deg/s). */
19
+ export interface PlatterState {
20
+ angle: number;
21
+ velocity: number;
22
+ }
23
+
24
+ /** Target speed at a 33⅓-feel — one full turn every ~4s. */
25
+ export const PLATTER_FULL_DPS = 90;
26
+ /** Time constant (s) for the motor coming up to speed — brisk. */
27
+ export const PLATTER_SPINUP_TAU = 0.5;
28
+ /** Time constant (s) for the platter coasting to rest — heavier, slower. */
29
+ export const PLATTER_SPINDOWN_TAU = 1.2;
30
+ /** Below this speed (deg/s) the coast is over — snap to rest and park the loop. */
31
+ const PLATTER_REST_DPS = 0.5;
32
+ /** Clamp per-frame dt so a backgrounded tab doesn't fling the disc on return. */
33
+ const MAX_FRAME_S = 0.05;
34
+
35
+ /**
36
+ * Advance the platter one frame (pure). Velocity eases toward `targetDps` with an
37
+ * exponential approach over `tauSec`, then the angle integrates that velocity.
38
+ *
39
+ * The approach uses `1 - e^(-dt/tau)`, which composes exactly across substeps
40
+ * (one step of 2·dt equals two steps of dt), so the spin speed is frame-rate
41
+ * independent. A non-positive dt is treated as a no-op.
42
+ */
43
+ export function stepPlatter(
44
+ state: PlatterState,
45
+ dtSec: number,
46
+ targetDps: number,
47
+ tauSec: number,
48
+ ): PlatterState {
49
+ if (dtSec <= 0) return { angle: state.angle, velocity: state.velocity };
50
+ const approach = 1 - Math.exp(-dtSec / tauSec);
51
+ const velocity = state.velocity + (targetDps - state.velocity) * approach;
52
+ const angle = ((state.angle + velocity * dtSec) % 360 + 360) % 360;
53
+ return { angle, velocity };
54
+ }
55
+
56
+ export interface PlatterHandle {
57
+ /** Engage the motor — the disc spins up to full speed. */
58
+ start(): void;
59
+ /** Cut the motor — the disc coasts down to rest. */
60
+ stop(): void;
61
+ /** Permanent teardown — cancels the frame loop. */
62
+ destroy(): void;
63
+ }
64
+
65
+ function prefersReduced(): boolean {
66
+ return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
67
+ }
68
+
69
+ /**
70
+ * Drive the no-cover record's spin with inertia. `start()`/`stop()` set the
71
+ * target speed; a single rAF loop eases velocity toward it and writes the disc's
72
+ * rotation each frame, parking itself once the platter has fully coasted to rest.
73
+ */
74
+ export function createPlatterSpin(figure: HTMLElement): PlatterHandle {
75
+ // Mutable state on a const record (the repo bans `let`).
76
+ const state: {
77
+ angle: number;
78
+ velocity: number;
79
+ target: number;
80
+ raf: number | null;
81
+ last: number;
82
+ } = { angle: 0, velocity: 0, target: 0, raf: null, last: 0 };
83
+
84
+ const disc = (): HTMLElement | null => figure.querySelector('.blok-audio-cover__disc');
85
+
86
+ const tick = (now: number): void => {
87
+ const dt = state.last ? Math.min((now - state.last) / 1000, MAX_FRAME_S) : 0;
88
+ state.last = now;
89
+ // Spin-up while accelerating toward the target; the heavier coast on the way down.
90
+ const tau = state.target > state.velocity ? PLATTER_SPINUP_TAU : PLATTER_SPINDOWN_TAU;
91
+ const next = stepPlatter({ angle: state.angle, velocity: state.velocity }, dt, state.target, tau);
92
+ state.angle = next.angle;
93
+ state.velocity = next.velocity;
94
+
95
+ const el = disc();
96
+ if (el) el.style.transform = `rotate(${state.angle.toFixed(2)}deg)`;
97
+
98
+ // Coasted to a halt — snap clean and let the loop sleep until the next start().
99
+ if (state.target === 0 && state.velocity < PLATTER_REST_DPS) {
100
+ state.velocity = 0;
101
+ state.raf = null;
102
+ return;
103
+ }
104
+ state.raf = globalThis.requestAnimationFrame(tick);
105
+ };
106
+
107
+ const ensureRunning = (): void => {
108
+ if (state.raf !== null) return;
109
+ state.last = 0; // first frame measures dt=0, so no jump from a stale timestamp
110
+ state.raf = globalThis.requestAnimationFrame(tick);
111
+ };
112
+
113
+ const start = (): void => {
114
+ if (prefersReduced()) return; // reduced motion: the record holds still
115
+ if (!disc()) return; // cover image shown (no placeholder disc) — nothing to spin
116
+ state.target = PLATTER_FULL_DPS;
117
+ ensureRunning();
118
+ };
119
+
120
+ const stop = (): void => {
121
+ state.target = 0;
122
+ // Keep the loop alive to animate the wind-down; if it's already at rest
123
+ // (or never spun up under reduced motion) there's nothing to coast.
124
+ if (state.velocity > 0) ensureRunning();
125
+ };
126
+
127
+ const destroy = (): void => {
128
+ if (state.raf !== null) {
129
+ globalThis.cancelAnimationFrame(state.raf);
130
+ state.raf = null;
131
+ }
132
+ state.velocity = 0;
133
+ state.target = 0;
134
+ };
135
+
136
+ return { start, stop, destroy };
137
+ }
@@ -18,15 +18,18 @@ import {
18
18
  IconCaption,
19
19
  IconCopy,
20
20
  IconDownload,
21
+ IconImage,
21
22
  IconMusic,
22
23
  IconPlayerLoop,
23
24
  IconReplace,
25
+ IconTrash,
24
26
  } from '../../components/icons';
25
27
  import { renderUploadingState, type UploadingStateElement } from '../image/uploading-state';
26
28
  import { DEFAULT_CAPTION_PLACEHOLDER, URL_PATTERN } from './constants';
27
29
  import { renderEmptyState, type EmptyStateElement } from './empty-state';
28
30
  import { tr } from './i18n';
29
31
  import { renderCaptionRow, renderNowPlaying } from './ui';
32
+ import { openCoverPicker, validateCoverFile, type CoverPickerHandle } from './cover-picker';
30
33
  import { attachControls, type ControlsHandle } from './controls';
31
34
  import { attachWaveform, decodePeaks, type WaveformHandle } from './waveform';
32
35
  import { readTrackMetadata, resolveCover } from './metadata';
@@ -52,6 +55,7 @@ export class AudioTool implements BlockTool {
52
55
  private controlsHandle: ControlsHandle | null = null;
53
56
  private waveformHandle: WaveformHandle | null = null;
54
57
  private destroyed = false;
58
+ private coverPicker: CoverPickerHandle | null = null;
55
59
  /** Pending fallback timer for the caption collapse-out animation. */
56
60
  private captionExitTimer: ReturnType<typeof setTimeout> | null = null;
57
61
 
@@ -124,7 +128,13 @@ export class AudioTool implements BlockTool {
124
128
  }
125
129
 
126
130
  public getToolbarAnchorElement(): HTMLElement | undefined {
127
- return this.root?.querySelector<HTMLElement>('[data-role="audio-figure"]') ?? undefined;
131
+ // Anchor on the top-of-card marker so the +/tunes buttons sit flush with the
132
+ // top of the cover art. Fall back to the figure if the marker isn't present.
133
+ return (
134
+ this.root?.querySelector<HTMLElement>('[data-role="audio-toolbar-anchor"]') ??
135
+ this.root?.querySelector<HTMLElement>('[data-role="audio-figure"]') ??
136
+ undefined
137
+ );
128
138
  }
129
139
 
130
140
  public setReadOnly(state: boolean): void {
@@ -182,6 +192,22 @@ export class AudioTool implements BlockTool {
182
192
  closeOnActivate: true,
183
193
  onActivate: (): void => this.transitionToEmpty(),
184
194
  },
195
+ {
196
+ icon: IconImage,
197
+ title: tr(i18n, 'tools.audio.coverSet', 'Set cover image'),
198
+ name: 'audio-cover-set',
199
+ closeOnActivate: true,
200
+ onActivate: (): void => this.openCoverPicker(),
201
+ },
202
+ ...(this.data.coverUrl
203
+ ? [{
204
+ icon: IconTrash,
205
+ title: tr(i18n, 'tools.audio.coverRemove', 'Remove cover'),
206
+ name: 'audio-cover-remove',
207
+ closeOnActivate: true,
208
+ onActivate: (): void => this.removeCover(),
209
+ }]
210
+ : []),
185
211
  {
186
212
  icon: IconDownload,
187
213
  title: tr(i18n, 'tools.audio.download', 'Download'),
@@ -209,6 +235,11 @@ export class AudioTool implements BlockTool {
209
235
  if (this.data.url.startsWith('blob:')) {
210
236
  URL.revokeObjectURL(this.data.url);
211
237
  }
238
+ if (this.data.coverUrl?.startsWith('blob:')) {
239
+ URL.revokeObjectURL(this.data.coverUrl);
240
+ }
241
+ this.coverPicker?.close();
242
+ this.coverPicker = null;
212
243
  }
213
244
 
214
245
  private startUpload(file: File): void {
@@ -373,10 +404,11 @@ export class AudioTool implements BlockTool {
373
404
  const titlePlaceholder = tr(this.api.i18n, 'tools.audio.titlePlaceholder', 'Track title');
374
405
  const artistPlaceholder = tr(this.api.i18n, 'tools.audio.artistPlaceholder', 'Artist');
375
406
 
376
- const { figure, audio, waveformMount, title, artist, body } = renderNowPlaying(this.data, {
407
+ const { figure, audio, waveformMount, title, artist, body, coverButton } = renderNowPlaying(this.data, {
377
408
  editable: !this.readOnly,
378
409
  titlePlaceholder,
379
410
  artistPlaceholder,
411
+ coverChangeLabel: tr(this.api.i18n, 'tools.audio.coverChange', 'Change cover'),
380
412
  });
381
413
 
382
414
  // Waveform (if peaks are available)
@@ -418,6 +450,8 @@ export class AudioTool implements BlockTool {
418
450
  }
419
451
  });
420
452
 
453
+ coverButton?.addEventListener('click', () => this.openCoverPicker());
454
+
421
455
  this.root.appendChild(figure);
422
456
  }
423
457
 
@@ -480,9 +514,11 @@ export class AudioTool implements BlockTool {
480
514
  return;
481
515
  }
482
516
  const row = this.createCaptionRow();
517
+ // Appended at full height immediately (a single layout step); is-collapsed
518
+ // only offsets/fades the inner, so the reveal animates on the compositor.
483
519
  row.classList.add('is-collapsed');
484
520
  figure.appendChild(row);
485
- // Force a reflow so the collapsed state is committed, then expand into view.
521
+ // Commit the collapsed start frame, then settle the inner into place.
486
522
  void row.offsetHeight;
487
523
  row.classList.remove('is-collapsed');
488
524
  }
@@ -493,11 +529,13 @@ export class AudioTool implements BlockTool {
493
529
  row.classList.add('is-collapsed');
494
530
  const finish = (): void => {
495
531
  if (this.captionExitTimer) { clearTimeout(this.captionExitTimer); this.captionExitTimer = null; }
532
+ row.removeEventListener('transitionend', onEnd);
496
533
  row.remove();
497
534
  };
498
- row.addEventListener('transitionend', (e) => {
499
- if (e.propertyName === 'grid-template-rows') finish();
500
- }, { once: true });
535
+ const onEnd = (e: TransitionEvent): void => {
536
+ if (e.propertyName === 'opacity') finish();
537
+ };
538
+ row.addEventListener('transitionend', onEnd);
501
539
  // Fallback in case transitionend never fires (display:none, interrupted, etc.).
502
540
  this.captionExitTimer = setTimeout(finish, 360);
503
541
  }
@@ -534,4 +572,74 @@ export class AudioTool implements BlockTool {
534
572
  void clip.writeText(this.data.url);
535
573
  }
536
574
  }
575
+
576
+ private openCoverPicker(): void {
577
+ if (this.readOnly) return;
578
+ const anchor = this.root?.querySelector<HTMLElement>('[data-role="audio-cover"]');
579
+ if (!anchor) return;
580
+ this.coverPicker?.close();
581
+ // Keep the cover's hover affordance (scrim + change icon) lit while the
582
+ // picker is open — the pointer has left the cover for the popup, so :hover
583
+ // alone would drop it.
584
+ anchor.classList.add('is-picker-open');
585
+ this.coverPicker = openCoverPicker({
586
+ anchor,
587
+ i18n: this.api.i18n,
588
+ onFile: (file) => this.applyCoverFile(file),
589
+ onUrl: (url) => this.applyCoverUrl(url),
590
+ onClose: () => {
591
+ anchor.classList.remove('is-picker-open');
592
+ this.coverPicker = null;
593
+ },
594
+ });
595
+ }
596
+
597
+ private applyCoverFile(file: File): void {
598
+ const error = validateCoverFile(file, this.api.i18n);
599
+ if (error) {
600
+ this.coverPicker?.setError(error);
601
+ return;
602
+ }
603
+ const uploader = this.config.uploader;
604
+ const upload = uploader?.uploadByFile
605
+ ? uploader.uploadByFile(file).then((res) => res.url)
606
+ : Promise.resolve(URL.createObjectURL(file));
607
+ void upload
608
+ .then((url) => {
609
+ if (this.destroyed) {
610
+ // Block was removed mid-upload: a no-uploader blob URL would otherwise
611
+ // leak (it was never assigned to coverUrl, so removed() can't revoke it).
612
+ if (url.startsWith('blob:')) URL.revokeObjectURL(url);
613
+ return;
614
+ }
615
+ this.setCover(url);
616
+ this.coverPicker?.close();
617
+ })
618
+ .catch(() => {
619
+ this.coverPicker?.setError(
620
+ tr(this.api.i18n, 'tools.audio.errorUploadFailed', 'Upload failed')
621
+ );
622
+ });
623
+ }
624
+
625
+ private applyCoverUrl(url: string): void {
626
+ this.setCover(url);
627
+ this.coverPicker?.close();
628
+ }
629
+
630
+ private setCover(url: string): void {
631
+ const prev = this.data.coverUrl;
632
+ if (prev && prev !== url && prev.startsWith('blob:')) URL.revokeObjectURL(prev);
633
+ this.data.coverUrl = url;
634
+ this.renderState();
635
+ this.block.dispatchChange();
636
+ }
637
+
638
+ private removeCover(): void {
639
+ const prev = this.data.coverUrl;
640
+ if (prev?.startsWith('blob:')) URL.revokeObjectURL(prev);
641
+ this.data.coverUrl = undefined;
642
+ this.renderState();
643
+ this.block.dispatchChange();
644
+ }
537
645
  }
@@ -70,10 +70,21 @@ export function ambientWave(index: number, timeSeconds: number): number {
70
70
  return a * 0.6 + b * 0.4;
71
71
  }
72
72
 
73
+ /** Comet-body lift at the playhead, tapering out along the wake/leading edge. */
74
+ const COMET_FLOOR = 0.34;
75
+ /** Extra lift layered onto the tight head spike right under the playhead. */
76
+ const HEAD_FLOOR = 0.5;
77
+ /**
78
+ * How hard the comet pulses, as a fraction of its floor. Kept < 1 so the
79
+ * downward half of the swing never drops a bar below its lifted base — the
80
+ * dance reads as a strong pump, never a hole punched into the waveform.
81
+ */
82
+ const DANCE_THROW = 0.82;
83
+
73
84
  /** Ambient swing applied to every bar (the faint preview-side breath). */
74
- const AMBIENT_BASE = 0.035;
85
+ const AMBIENT_BASE = 0.04;
75
86
  /** Extra ambient swing layered onto the played side (the shimmering wake). */
76
- const AMBIENT_PLAYED = 0.06;
87
+ const AMBIENT_PLAYED = 0.17;
77
88
  /** Bars ahead of the playhead over which the played-side shimmer fades out. */
78
89
  const AMBIENT_AHEAD_FADE = 26;
79
90
 
@@ -119,16 +130,23 @@ export function liveAmplitude(opts: LiveAmplitudeOptions): number {
119
130
  if (reduced || energy <= 0) return basePeak;
120
131
 
121
132
  const distance = index - playheadIndex;
122
-
123
- // Layer 1 — focal comet near the playhead.
124
133
  const env = cometEnvelope(distance);
125
134
  const head = headFocus(distance);
126
- const comet = env * 0.15 + head * 0.2 + env * 0.28 * barWobble(index, timeSeconds);
127
135
 
128
- // Layer 2global ambient ripple. Full strength behind the playhead (the
129
- // played wake), fading across AMBIENT_AHEAD_FADE bars into the preview ahead.
136
+ // Layer 1focal comet. A guaranteed-positive "alive" floor blooms at the
137
+ // playhead and tapers along the wake, lifting even silent bars into view. A
138
+ // vigorous bidirectional pulse then rides on top — but its swing is capped at
139
+ // DANCE_THROW of the floor, so the trough stays at/above the lifted base. Big
140
+ // visible motion, yet it can never gouge a bar back down to a flat dot.
141
+ const floor = env * COMET_FLOOR + head * HEAD_FLOOR;
142
+ const comet = floor * (1 + DANCE_THROW * barWobble(index, timeSeconds));
143
+
144
+ // Layer 2 — global ambient breath. Rectified to [0, 1] so it only adds, it
145
+ // keeps the whole field shimmering: full strength on the played wake, fading
146
+ // across AMBIENT_AHEAD_FADE bars into the preview ahead.
130
147
  const playedBias = distance <= 0 ? 1 : Math.max(0, 1 - distance / AMBIENT_AHEAD_FADE);
131
- const ambient = ambientWave(index, timeSeconds) * (AMBIENT_BASE + AMBIENT_PLAYED * playedBias);
148
+ const ripple = (ambientWave(index, timeSeconds) + 1) / 2;
149
+ const ambient = ripple * (AMBIENT_BASE + AMBIENT_PLAYED * playedBias);
132
150
 
133
151
  const amp = basePeak + (comet + ambient) * energy;
134
152