@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.
- package/dist/blok.cjs +1 -1
- package/dist/blok.iife.js +4 -4
- package/dist/blok.mjs +2 -2
- package/dist/blok.umd.js +41 -41
- package/dist/chunks/{blok-C8_pY9Bg.cjs → blok-Cz71HTuP.cjs} +13 -13
- package/dist/chunks/{blok-DCc23rzA.mjs → blok-x8D2aON2.mjs} +2645 -2116
- package/dist/chunks/constants-BK6kLyZB.cjs +606 -0
- package/dist/chunks/{constants-C3-Lh6Fs.mjs → constants-CLzXh7Tb.mjs} +392 -267
- package/dist/chunks/{i18next-loader-BBPMegxc.cjs → i18next-loader-DVakgVCy.cjs} +1 -1
- package/dist/chunks/{i18next-loader-K-ir8UKk.mjs → i18next-loader-DfIonnVO.mjs} +1 -1
- package/dist/chunks/{lightweight-i18n-LFW8S8u-.cjs → lightweight-i18n-BnIqQ5Cf.cjs} +1 -1
- package/dist/chunks/{lightweight-i18n-DJpMke7a.mjs → lightweight-i18n-Cj88c8gl.mjs} +15 -0
- package/dist/chunks/{messages-Bb80Csl3.mjs → messages-0EAci1N0.mjs} +15 -0
- package/dist/chunks/{messages-BkgBVEiE.cjs → messages-1jzEPeW5.cjs} +1 -1
- package/dist/chunks/{messages-CATH_uz1.mjs → messages-4CqVAaCY.mjs} +16 -1
- package/dist/chunks/{messages-DFHNw6PQ.cjs → messages-4Qka94cm.cjs} +1 -1
- package/dist/chunks/{messages-CBbgEXuO.cjs → messages-7wDDggws.cjs} +1 -1
- package/dist/chunks/{messages-B7st3eF_.cjs → messages-9VzB9Djh.cjs} +1 -1
- package/dist/chunks/{messages-B_cHWyWC.cjs → messages-9bK437Rh.cjs} +1 -1
- package/dist/chunks/{messages-CZddldga.cjs → messages-AtTKtjsQ.cjs} +1 -1
- package/dist/chunks/{messages-HJnG9r2l.mjs → messages-B1inw201.mjs} +16 -1
- package/dist/chunks/{messages-B-u5B7tY.cjs → messages-B4ZVgIh5.cjs} +1 -1
- package/dist/chunks/{messages-CJFSq8Fl.cjs → messages-B5bSFRCa.cjs} +1 -1
- package/dist/{messages-HVgCKIww2.mjs → chunks/messages-B705gxqz.mjs} +16 -1
- package/dist/{messages-Dq_RdEny2.mjs → chunks/messages-B7F3tX4u.mjs} +16 -1
- package/dist/chunks/{messages-NJAjMFXd.cjs → messages-BDer7I67.cjs} +1 -1
- package/dist/chunks/{messages-BV22YIZ_.cjs → messages-BEEGgtPo.cjs} +1 -1
- package/dist/{messages-bN4Zmk2m.mjs → chunks/messages-BGlEWmsB.mjs} +16 -1
- package/dist/{messages-Ao2vCNAH2.mjs → chunks/messages-BJJsuZhy.mjs} +16 -1
- package/dist/chunks/{messages-DjrfovG0.cjs → messages-BJcc9h6p.cjs} +1 -1
- package/dist/chunks/{messages-Hfo6uCIC.cjs → messages-BL-E2Kw3.cjs} +1 -1
- package/dist/{messages-8__-vX3U.mjs → chunks/messages-BLJGzntb.mjs} +16 -1
- package/dist/chunks/{messages-Au1UpgkX.cjs → messages-BLNieIb-.cjs} +1 -1
- package/dist/chunks/{messages-DQJcAz4s.cjs → messages-BRbGk1-Z.cjs} +1 -1
- package/dist/chunks/{messages-BwOUSDJR.mjs → messages-BUlZRNoO.mjs} +15 -0
- package/dist/{messages-BKGg4XYm2.mjs → chunks/messages-BWQF9y2r.mjs} +16 -1
- package/dist/chunks/{messages-UXS3zL9A2.cjs → messages-BWh9I5Jv2.cjs} +1 -1
- package/dist/chunks/{messages-B6dbtt1c.cjs → messages-BX86v7Ev.cjs} +1 -1
- package/dist/chunks/{messages-DIFwztFo.cjs → messages-BYpvkG9S.cjs} +1 -1
- package/dist/{messages-CDueJTe8.mjs → chunks/messages-Bdw_vAdq2.mjs} +16 -1
- package/dist/chunks/{messages-DIetNXTx.cjs → messages-BeQVEC9q.cjs} +1 -1
- package/dist/{messages-CusWGsl42.mjs → chunks/messages-BeTZTXZm.mjs} +16 -1
- package/dist/chunks/{messages-CmsAbu_A.cjs → messages-BhTlR4PP.cjs} +1 -1
- package/dist/chunks/{messages-C3rGCBi_.mjs → messages-BhnZTZbQ.mjs} +16 -1
- package/dist/{messages-BpwhHkFb2.mjs → chunks/messages-BqJdoBHm.mjs} +16 -1
- package/dist/chunks/{messages-zPUQsksJ.mjs → messages-BqPiH7QC.mjs} +16 -1
- package/dist/chunks/{messages-CjfEt9bg.cjs → messages-Br_qu_wR.cjs} +1 -1
- package/dist/chunks/{messages-DtaPHADA.cjs → messages-BscUmD6D.cjs} +1 -1
- package/dist/{messages-Cg6a88Bb2.mjs → chunks/messages-Btapuvxo.mjs} +16 -1
- package/dist/chunks/{messages-CEySIig9.cjs → messages-BwCw0TJ1.cjs} +1 -1
- package/dist/chunks/{messages-BO-L5oN9.mjs → messages-C3RDWmXV.mjs} +15 -0
- package/dist/chunks/{messages-CZJZ-au3.cjs → messages-C3SZsVB9.cjs} +1 -1
- package/dist/chunks/{messages-DQr7bXMD.mjs → messages-C65mLRVK.mjs} +16 -1
- package/dist/chunks/{messages-CT8djlV6.cjs → messages-C73dKqPT.cjs} +1 -1
- package/dist/chunks/{messages-DOqJ6ogK.cjs → messages-C8f4kUOB.cjs} +1 -1
- package/dist/{messages-yuNINyS1.mjs → chunks/messages-C9vfrzKQ.mjs} +16 -1
- package/dist/{messages-Grhv5SE_.mjs → chunks/messages-CEbe34zh.mjs} +15 -0
- package/dist/chunks/{messages-CcsM8Q7g.cjs → messages-CHEpECGv.cjs} +1 -1
- package/dist/chunks/{messages-DEg5FAg1.cjs → messages-CHP0nYRn.cjs} +1 -1
- package/dist/chunks/{messages-CjCn5IsF.mjs → messages-CIbh-0oJ.mjs} +16 -1
- package/dist/chunks/{messages-BHCepNsI.cjs → messages-CJC94Hci.cjs} +1 -1
- package/dist/chunks/{messages-ByNR9m9W.cjs → messages-CMP5dbqQ.cjs} +1 -1
- package/dist/chunks/{messages-DpNmnBjK.cjs → messages-CMm8FeOL.cjs} +1 -1
- package/dist/chunks/{messages-Da8yFc7l.cjs → messages-CNMPtWiq.cjs} +1 -1
- package/dist/{messages-CqZgsrVP2.mjs → chunks/messages-CQUr4lBa.mjs} +16 -1
- package/dist/{messages-Cvq2EFPT2.mjs → chunks/messages-CUuOX1k3.mjs} +16 -1
- package/dist/chunks/{messages-BsTzdfZF.cjs → messages-CVerbLou.cjs} +1 -1
- package/dist/{messages-CGiVoFTi.mjs → chunks/messages-CWW9OwKO.mjs} +16 -1
- package/dist/{messages-Da1woCgt2.mjs → chunks/messages-CWqap54p.mjs} +16 -1
- package/dist/{messages-CEGqNXnP.mjs → chunks/messages-CXNQBFHp.mjs} +16 -1
- package/dist/chunks/{messages-DgF8OkNr.mjs → messages-CYDhuSF4.mjs} +16 -1
- package/dist/chunks/{messages-Bp0LdGKj.mjs → messages-CYy3Zd3F.mjs} +15 -0
- package/dist/{messages-CUcUO5bC2.mjs → chunks/messages-C_BM_XHo.mjs} +16 -1
- package/dist/chunks/{messages-IqNWRaNS.cjs → messages-CbKOqXT6.cjs} +1 -1
- package/dist/chunks/{messages-Bmq--WTx.mjs → messages-CbxXU_Um.mjs} +15 -0
- package/dist/chunks/{messages-CVrOxu3J.cjs → messages-CcO-3_4k.cjs} +1 -1
- package/dist/chunks/{messages-D41fO84y.cjs → messages-CcTkC677.cjs} +1 -1
- package/dist/chunks/{messages-Cy-2_lIA.cjs → messages-CdEZ98_p.cjs} +1 -1
- package/dist/{messages-KyJQtOkh2.mjs → chunks/messages-CeZ1lo-o.mjs} +16 -1
- package/dist/chunks/{messages-D7H9JUDr.cjs → messages-CgFAGUJX.cjs} +1 -1
- package/dist/chunks/{messages-Dwl878eS.mjs → messages-CprMp4Va.mjs} +16 -1
- package/dist/{messages-wmS6Z2m82.mjs → chunks/messages-CtXDxPOq.mjs} +16 -1
- package/dist/{messages-DcQNeoPB.mjs → chunks/messages-Cutnm4AZ.mjs} +15 -0
- package/dist/chunks/{messages-cSBZ5qVe.mjs → messages-CwIC_sse.mjs} +16 -1
- package/dist/chunks/{messages-7LWW3BVh.mjs → messages-CwbTr9lL.mjs} +16 -1
- package/dist/chunks/{messages-ETJ7tQ9O.cjs → messages-CxIknrw4.cjs} +1 -1
- package/dist/{messages-VIQ8vjvS.mjs → chunks/messages-D0Jj7Dtw.mjs} +15 -0
- package/dist/chunks/{messages-SozFksSf.cjs → messages-D0th-I9S.cjs} +1 -1
- package/dist/chunks/{messages-gh5G15J1.cjs → messages-D1XTyTJz.cjs} +1 -1
- package/dist/chunks/{messages-DNtAzacv.mjs → messages-D24EoKbO.mjs} +16 -1
- package/dist/chunks/{messages-DDJkNbJy.mjs → messages-D2chjyLM.mjs} +16 -1
- package/dist/chunks/{messages-CqRtDVFO.cjs → messages-D6y13edn.cjs} +1 -1
- package/dist/chunks/{messages-EsX3eikv.cjs → messages-D86p1eQY.cjs} +1 -1
- package/dist/{messages-BwCgJUdq.mjs → chunks/messages-D8L_8amv.mjs} +16 -1
- package/dist/chunks/{messages-C2ptZOWh.cjs → messages-DC-yFRyF.cjs} +1 -1
- package/dist/{messages-BgPDPIkO.mjs → chunks/messages-DEPxxCNU.mjs} +16 -1
- package/dist/chunks/{messages-VeyvtTce.cjs → messages-DG9l5agQ.cjs} +1 -1
- package/dist/chunks/{messages-B6794_2J.cjs → messages-DIeUb2w-.cjs} +1 -1
- package/dist/chunks/{messages-COx3hDcP.cjs → messages-DJ-RX5EJ.cjs} +1 -1
- package/dist/chunks/{messages-Cvg_uTfy.mjs → messages-DJ7lbpBX.mjs} +16 -1
- package/dist/{messages-Cn3uvFq3.mjs → chunks/messages-DJH_kd46.mjs} +16 -1
- package/dist/chunks/{messages-CWFSMzug2.cjs → messages-DLYl6KdK2.cjs} +1 -1
- package/dist/{messages-Ch-SytN8.mjs → chunks/messages-DMcCkOtN.mjs} +16 -1
- package/dist/chunks/{messages-BdL8bmTL.mjs → messages-DQks74hq.mjs} +16 -1
- package/dist/chunks/{messages-D-QWtvWg.mjs → messages-DTiqggtG.mjs} +16 -1
- package/dist/chunks/{messages-typO_i_k.mjs → messages-DTsSYToI.mjs} +15 -0
- package/dist/{messages-CwvHO8gp2.mjs → chunks/messages-DU6TOz6v.mjs} +16 -1
- package/dist/chunks/{messages-DmjccFHq.cjs → messages-DUbZA-w_.cjs} +1 -1
- package/dist/chunks/{messages-DCwrXfck.cjs → messages-DVAU-8rH.cjs} +1 -1
- package/dist/chunks/{messages-Z4LYgfWc.cjs → messages-Df6vXDrY.cjs} +1 -1
- package/dist/chunks/{messages-9B1F-k48.cjs → messages-Dh79ToYM.cjs} +1 -1
- package/dist/chunks/{messages-CuMtymEs.cjs → messages-Dh9psewW.cjs} +1 -1
- package/dist/chunks/{messages-BjYhwRk7.cjs → messages-Dln2T0zJ.cjs} +1 -1
- package/dist/chunks/{messages-DT-JnTnw.cjs → messages-DoA0Ecoi.cjs} +1 -1
- package/dist/chunks/{messages-D-p-omWy.mjs → messages-Dre_MB7o.mjs} +16 -1
- package/dist/chunks/{messages-BveiGA2x.cjs → messages-Dv4oyF7T.cjs} +1 -1
- package/dist/{messages-C6R0bzW5.mjs → chunks/messages-FMAQkmg4.mjs} +15 -0
- package/dist/{messages-CQzOV7VE2.mjs → chunks/messages-GownDU-J.mjs} +16 -1
- package/dist/chunks/{messages-CH9SBR-i.cjs → messages-JYea02IT.cjs} +1 -1
- package/dist/chunks/{messages-BXp_U3uS.cjs → messages-JyzorVPx.cjs} +1 -1
- package/dist/{messages-BIgvPd6I2.mjs → chunks/messages-L70TFQAY.mjs} +16 -1
- package/dist/{messages-CskLLTiG2.mjs → chunks/messages-L8CslBBZ.mjs} +16 -1
- package/dist/chunks/{messages-CM8uUnPx.cjs → messages-LX9PuLsT.cjs} +1 -1
- package/dist/{messages-BarhGl2S2.mjs → chunks/messages-NFUr7Sxk.mjs} +16 -1
- package/dist/chunks/{messages-CGdCpNZ62.cjs → messages-PpRvDwOa2.cjs} +1 -1
- package/dist/chunks/{messages-ZMG7qQ3D.mjs → messages-QQ5-ET8y.mjs} +16 -1
- package/dist/{messages-DbE-riR52.mjs → chunks/messages-Stf6rn2I.mjs} +16 -1
- package/dist/chunks/{messages-D7sU-NqL.cjs → messages-T8hCmys7.cjs} +1 -1
- package/dist/chunks/{messages-DDX6I6MX.mjs → messages-TZtv60nI.mjs} +16 -1
- package/dist/chunks/{messages-CeLv1whu2.mjs → messages-V9bvFOFc2.mjs} +16 -1
- package/dist/chunks/{messages-tR0BdeVG.cjs → messages-Vp8CpYh3.cjs} +1 -1
- package/dist/chunks/{messages-DUWCkLYR.cjs → messages-XqCqLy0k.cjs} +1 -1
- package/dist/chunks/{messages-B01D7aHY.mjs → messages-_1y6EX7X.mjs} +16 -1
- package/dist/chunks/{messages-CXk-iM-i.mjs → messages-aTZglEV9.mjs} +16 -1
- package/dist/chunks/{messages-BHdKd7pH.cjs → messages-cSHh8Zgv.cjs} +1 -1
- package/dist/{messages-P5B61L1J2.mjs → chunks/messages-e8gxPi7a.mjs} +16 -1
- package/dist/{messages-CUFac9Qe.mjs → chunks/messages-hiwlrrr-.mjs} +16 -1
- package/dist/{messages-CeqpzGl9.mjs → chunks/messages-icPZhZ81.mjs} +15 -0
- package/dist/chunks/{messages-Byn9-dhb2.mjs → messages-lbwOYFxS2.mjs} +16 -1
- package/dist/chunks/{messages-B6yMdb3B.cjs → messages-lvo-9Be0.cjs} +1 -1
- package/dist/chunks/{messages-B2iaIrIu.mjs → messages-rm6jXexA.mjs} +16 -1
- package/dist/chunks/{messages-Ciq5_PsP.cjs → messages-sTyGkmbu.cjs} +1 -1
- package/dist/{messages-CF5MeYEt2.mjs → chunks/messages-uBwK-Ze2.mjs} +16 -1
- package/dist/chunks/{messages-BaVZgTrw.cjs → messages-uOISsfmj.cjs} +1 -1
- package/dist/{messages-L4sYfXpM.mjs → chunks/messages-wq0vxJ3p.mjs} +16 -1
- package/dist/chunks/{messages-CQT8TtiF.cjs → messages-z2DzBRgO.cjs} +1 -1
- package/dist/chunks/tools-BKjP4TXy.cjs +208 -0
- package/dist/chunks/{tools-fEI-EVng.mjs → tools-Bw8_CkhE.mjs} +2537 -2469
- package/dist/full.cjs +1 -1
- package/dist/full.mjs +3 -3
- package/dist/locales.mjs +82 -67
- package/dist/{messages-BlvF3pot.mjs → messages--sHkn4PD.mjs} +16 -1
- package/dist/{chunks/messages-QCaF3d92.mjs → messages-4mxgsYKZ2.mjs} +16 -1
- package/dist/{messages-zOl6jpOa.mjs → messages-6fQMeTiI.mjs} +16 -1
- package/dist/{messages-BRLWpOnP.mjs → messages-B22G0fir.mjs} +16 -1
- package/dist/{chunks/messages-kqnzps-X.mjs → messages-B908tGhl.mjs} +16 -1
- package/dist/{chunks/messages-C8_r3iTR2.mjs → messages-BAUvKI0_.mjs} +16 -1
- package/dist/{chunks/messages-BpMWMH9_.mjs → messages-BAfMwQnT2.mjs} +16 -1
- package/dist/{messages-Bmu3rJwO.mjs → messages-BCk7o4Pz.mjs} +16 -1
- package/dist/{messages-D4dovFzY2.mjs → messages-BG2BE6Sg2.mjs} +16 -1
- package/dist/{messages-DR2JBU-H.mjs → messages-BGe6GS_j.mjs} +16 -1
- package/dist/{chunks/messages-CeIzWL_4.mjs → messages-BHLrVFmZ2.mjs} +16 -1
- package/dist/{messages-BOWDzfkH.mjs → messages-BJjffJIu.mjs} +16 -1
- package/dist/{chunks/messages-DHv7knYB.mjs → messages-BSGZSOH32.mjs} +16 -1
- package/dist/{chunks/messages-BRTWvTAP.mjs → messages-BT6TyDYQ2.mjs} +16 -1
- package/dist/{messages-jQsl7cga2.mjs → messages-BWZ0_zUi2.mjs} +16 -1
- package/dist/{messages-lnxRz5rM.mjs → messages-BWhJgyhl.mjs} +16 -1
- package/dist/{chunks/messages-Xh-c7d4C.mjs → messages-BXqLaS2g2.mjs} +16 -1
- package/dist/{messages-gTUy3xjb.mjs → messages-BZiuFv_O.mjs} +15 -0
- package/dist/{chunks/messages-B_d857cA.mjs → messages-Bb-XEtyl.mjs} +15 -0
- package/dist/{messages-C9Kq8259.mjs → messages-BkKAnFz_.mjs} +15 -0
- package/dist/{chunks/messages-BUvlOqll.mjs → messages-BoLu5_Wp2.mjs} +16 -1
- package/dist/{messages-RXQKVFCJ.mjs → messages-BpqRzil6.mjs} +16 -1
- package/dist/{chunks/messages-CkosWGGH.mjs → messages-C2E8rQjb.mjs} +15 -0
- package/dist/{chunks/messages-jEbEE90_.mjs → messages-C2JJxi5D2.mjs} +16 -1
- package/dist/{messages-jsrzDVZy2.mjs → messages-C4d1_z432.mjs} +16 -1
- package/dist/{chunks/messages-MPe9FjYe.mjs → messages-C5Igyij0.mjs} +16 -1
- package/dist/{messages-CAnOyxrm.mjs → messages-C8fscB2J.mjs} +16 -1
- package/dist/{chunks/messages-CX0tJpOb.mjs → messages-CD8CgT1F.mjs} +16 -1
- package/dist/{messages-BKG0QHVl.mjs → messages-CDxPk3xE.mjs} +15 -0
- package/dist/{messages-hGop1gbF.mjs → messages-CGXrIdHT.mjs} +15 -0
- package/dist/{chunks/messages-DuHFGMCJ.mjs → messages-CHfkCxGQ2.mjs} +16 -1
- package/dist/{messages-fAEKzdUq.mjs → messages-CMvNzksb.mjs} +16 -1
- package/dist/{chunks/messages-t26x8wxB.mjs → messages-CSIhcdDx2.mjs} +16 -1
- package/dist/{messages-UJx6PvCL.mjs → messages-CW5z5hSz.mjs} +16 -1
- package/dist/{chunks/messages-CUPup5e3.mjs → messages-CYvpDx4p2.mjs} +16 -1
- package/dist/{chunks/messages-Ca9zIrBo.mjs → messages-CcTlmrYs.mjs} +16 -1
- package/dist/{messages-CcF4SO9W.mjs → messages-Ce8ShL0-.mjs} +16 -1
- package/dist/{chunks/messages-CbyVDo8y.mjs → messages-CgT32m3D.mjs} +15 -0
- package/dist/{chunks/messages-ypIHI9Fr.mjs → messages-CmCBnNtA2.mjs} +16 -1
- package/dist/{messages-DGAWjKQZ.mjs → messages-CneVTJSN.mjs} +16 -1
- package/dist/{messages-C5IE9mXm.mjs → messages-CpNerer8.mjs} +15 -0
- package/dist/{chunks/messages-D-s4_vnU.mjs → messages-Cs0L1gyv.mjs} +16 -1
- package/dist/{chunks/messages-BYCHzM7F.mjs → messages-CsGnp1wE2.mjs} +16 -1
- package/dist/{chunks/messages-D4u2PSQw.mjs → messages-CvbIxcr7.mjs} +16 -1
- package/dist/{chunks/messages-DuD4ut1b.mjs → messages-Cx6oQfMR2.mjs} +16 -1
- package/dist/{messages-BvutXmgt.mjs → messages-DBESJOhQ.mjs} +16 -1
- package/dist/{chunks/messages-Cl_pM31G.mjs → messages-DGOBPnKC2.mjs} +16 -1
- package/dist/{messages-B13GEFFu.mjs → messages-DK2AwXAp.mjs} +16 -1
- package/dist/{chunks/messages-CI2W7P-j.mjs → messages-DMbnesQ9.mjs} +16 -1
- package/dist/{messages-4drvqPN8.mjs → messages-DOiSOenW.mjs} +16 -1
- package/dist/{chunks/messages-5339ApgH.mjs → messages-DQqp_ANL.mjs} +16 -1
- package/dist/{chunks/messages-Ck1-ddbo.mjs → messages-DThG2jW-2.mjs} +16 -1
- package/dist/{chunks/messages-DQfi2DoY.mjs → messages-DXlB7Vpm.mjs} +16 -1
- package/dist/{chunks/messages-DjZoCJ00.mjs → messages-De0NAxJt.mjs} +15 -0
- package/dist/{messages-BI5B9bxE.mjs → messages-Depz8d3Q.mjs} +15 -0
- package/dist/{messages-BQaiScU1.mjs → messages-Dfsw3a8v.mjs} +16 -1
- package/dist/{messages-D9qgYMU82.mjs → messages-Dhmmqob82.mjs} +16 -1
- package/dist/{chunks/messages-DiODNXET.mjs → messages-DmNUAv0e2.mjs} +16 -1
- package/dist/{chunks/messages-3jkCmB_A.mjs → messages-DrQaJrDO.mjs} +16 -1
- package/dist/{messages-B2YnZIU2.mjs → messages-DsksuHtZ.mjs} +16 -1
- package/dist/{chunks/messages-m-A6ArIM.mjs → messages-DzOVTq3E2.mjs} +16 -1
- package/dist/{chunks/messages-C0swL9CY.mjs → messages-Igpt5WIA2.mjs} +16 -1
- package/dist/{chunks/messages-CIBYzzFy.mjs → messages-JhpGtjjS.mjs} +16 -1
- package/dist/{chunks/messages-BbZz_Anm.mjs → messages-WCy1wiGS2.mjs} +16 -1
- package/dist/{chunks/messages-CRUb9o5k.mjs → messages-XbA-jXKc.mjs} +15 -0
- package/dist/{chunks/messages-BeS9EOEO.mjs → messages-imHGm9SI2.mjs} +16 -1
- package/dist/{messages-BQhHthxq2.mjs → messages-yTNyxXRV2.mjs} +16 -1
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/i18n/locales/am/messages.json +16 -1
- package/src/components/i18n/locales/ar/messages.json +16 -1
- package/src/components/i18n/locales/az/messages.json +16 -1
- package/src/components/i18n/locales/bg/messages.json +16 -1
- package/src/components/i18n/locales/bn/messages.json +16 -1
- package/src/components/i18n/locales/bs/messages.json +16 -1
- package/src/components/i18n/locales/cs/messages.json +16 -1
- package/src/components/i18n/locales/da/messages.json +16 -1
- package/src/components/i18n/locales/de/messages.json +16 -1
- package/src/components/i18n/locales/dv/messages.json +16 -1
- package/src/components/i18n/locales/el/messages.json +16 -1
- package/src/components/i18n/locales/en/messages.json +15 -0
- package/src/components/i18n/locales/es/messages.json +15 -0
- package/src/components/i18n/locales/et/messages.json +15 -0
- package/src/components/i18n/locales/fa/messages.json +15 -0
- package/src/components/i18n/locales/fi/messages.json +15 -0
- package/src/components/i18n/locales/fil/messages.json +15 -0
- package/src/components/i18n/locales/fr/messages.json +15 -0
- package/src/components/i18n/locales/gu/messages.json +15 -0
- package/src/components/i18n/locales/he/messages.json +15 -0
- package/src/components/i18n/locales/hi/messages.json +15 -0
- package/src/components/i18n/locales/hr/messages.json +15 -0
- package/src/components/i18n/locales/hu/messages.json +15 -0
- package/src/components/i18n/locales/hy/messages.json +16 -1
- package/src/components/i18n/locales/id/messages.json +16 -1
- package/src/components/i18n/locales/it/messages.json +16 -1
- package/src/components/i18n/locales/ja/messages.json +16 -1
- package/src/components/i18n/locales/ka/messages.json +16 -1
- package/src/components/i18n/locales/km/messages.json +16 -1
- package/src/components/i18n/locales/kn/messages.json +16 -1
- package/src/components/i18n/locales/ko/messages.json +16 -1
- package/src/components/i18n/locales/ku/messages.json +16 -1
- package/src/components/i18n/locales/lo/messages.json +16 -1
- package/src/components/i18n/locales/lt/messages.json +16 -1
- package/src/components/i18n/locales/lv/messages.json +16 -1
- package/src/components/i18n/locales/mk/messages.json +16 -1
- package/src/components/i18n/locales/ml/messages.json +16 -1
- package/src/components/i18n/locales/mn/messages.json +16 -1
- package/src/components/i18n/locales/mr/messages.json +16 -1
- package/src/components/i18n/locales/ms/messages.json +16 -1
- package/src/components/i18n/locales/my/messages.json +16 -1
- package/src/components/i18n/locales/ne/messages.json +16 -1
- package/src/components/i18n/locales/nl/messages.json +16 -1
- package/src/components/i18n/locales/no/messages.json +16 -1
- package/src/components/i18n/locales/pa/messages.json +16 -1
- package/src/components/i18n/locales/pl/messages.json +16 -1
- package/src/components/i18n/locales/ps/messages.json +16 -1
- package/src/components/i18n/locales/pt/messages.json +16 -1
- package/src/components/i18n/locales/ro/messages.json +16 -1
- package/src/components/i18n/locales/ru/messages.json +16 -1
- package/src/components/i18n/locales/sd/messages.json +16 -1
- package/src/components/i18n/locales/si/messages.json +16 -1
- package/src/components/i18n/locales/sk/messages.json +16 -1
- package/src/components/i18n/locales/sl/messages.json +16 -1
- package/src/components/i18n/locales/sq/messages.json +16 -1
- package/src/components/i18n/locales/sr/messages.json +16 -1
- package/src/components/i18n/locales/sv/messages.json +16 -1
- package/src/components/i18n/locales/sw/messages.json +16 -1
- package/src/components/i18n/locales/ta/messages.json +16 -1
- package/src/components/i18n/locales/te/messages.json +16 -1
- package/src/components/i18n/locales/th/messages.json +16 -1
- package/src/components/i18n/locales/tr/messages.json +16 -1
- package/src/components/i18n/locales/ug/messages.json +16 -1
- package/src/components/i18n/locales/uk/messages.json +16 -1
- package/src/components/i18n/locales/ur/messages.json +16 -1
- package/src/components/i18n/locales/vi/messages.json +16 -1
- package/src/components/i18n/locales/yi/messages.json +16 -1
- package/src/components/i18n/locales/zh/messages.json +16 -1
- package/src/components/modules/paste/handlers/blok-data-handler.ts +39 -23
- package/src/components/modules/paste/index.ts +38 -4
- package/src/components/modules/paste/notion-blocks-v3.ts +932 -0
- package/src/components/modules/paste/notion-preprocessor.ts +67 -0
- package/src/components/utils/media-empty-state.ts +48 -5
- package/src/styles/audio.css +348 -36
- package/src/styles/tables.css +10 -0
- package/src/tools/audio/constants.ts +6 -0
- package/src/tools/audio/controls.ts +53 -7
- package/src/tools/audio/cover-picker.ts +126 -0
- package/src/tools/audio/disc.ts +137 -0
- package/src/tools/audio/index.ts +114 -6
- package/src/tools/audio/liveliness.ts +26 -8
- package/src/tools/audio/ui.ts +39 -4
- package/dist/chunks/constants-R70ypRAK.cjs +0 -606
- package/dist/chunks/tools-DcbePIHF.cjs +0 -208
package/src/styles/tables.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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,
|
|
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
|
+
}
|
package/src/tools/audio/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
499
|
-
if (e.propertyName === '
|
|
500
|
-
}
|
|
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.
|
|
85
|
+
const AMBIENT_BASE = 0.04;
|
|
75
86
|
/** Extra ambient swing layered onto the played side (the shimmering wake). */
|
|
76
|
-
const AMBIENT_PLAYED = 0.
|
|
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
|
|
129
|
-
//
|
|
136
|
+
// Layer 1 — focal 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
|
|
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
|
|