@leavepulse/ui 0.83.0 → 0.85.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 (323) hide show
  1. package/README.md +22 -0
  2. package/dist/assets/{LpDropzone-DNbh_NFJ.css → LpDropzone-CfL3Vrx4.css} +10 -10
  3. package/dist/assets/{LpImageEditor-Bgu20XLs.css → LpImageEditor-y3B-aW3H.css} +6 -6
  4. package/dist/assets/{LpKeys-C5VMcBOW.css → LpKeys-5ijsXX-M.css} +6 -6
  5. package/dist/assets/{LpShortcutsDialog-C6uzPeMf.css → LpShortcutsDialog-BBdVWxZq.css} +7 -7
  6. package/dist/assets/LpVideoTrim-CMoUw3dJ.css +8 -0
  7. package/dist/chunks/{ContextMenuItems-h-apPY2d.js → ContextMenuItems-D15cGEw9.js} +1 -1
  8. package/dist/chunks/{DropzoneLinkField-_Op8b_ee.js → DropzoneLinkField-Dz7fIaLh.js} +1 -1
  9. package/dist/chunks/{FieldLeading-mOMcwNqi.js → FieldLeading-DZZLK02U.js} +1 -1
  10. package/dist/chunks/{FieldWrapper-DtT-AIcn.js → FieldWrapper-BDDw1F9H.js} +1 -1
  11. package/dist/chunks/{IconCloseButton-YmisskyZ.js → IconCloseButton-Cn_w4aN3.js} +1 -1
  12. package/dist/chunks/{LayoutCanvas-BrISZEBr.js → LayoutCanvas-Dr5eSfAu.js} +1 -1
  13. package/dist/chunks/{LayoutNode-CwMQ8ACa.js → LayoutNode-CViJnx6q.js} +1 -1
  14. package/dist/chunks/{LpActionBar-CVVC6YbC.js → LpActionBar-C0LK-Aqs.js} +1 -1
  15. package/dist/chunks/{LpAddressInput-tiuJ-bru.js → LpAddressInput-B9sSvodU.js} +7 -7
  16. package/dist/chunks/{LpAlert-8-x4H_Nk.js → LpAlert-Bpd7122M.js} +1 -1
  17. package/dist/chunks/{LpAppShell-BW2YMq-A.js → LpAppShell-DW8JmY1o.js} +4 -4
  18. package/dist/chunks/{LpAutocomplete-BZODIops.js → LpAutocomplete-CQID7Zc-.js} +4 -4
  19. package/dist/chunks/{LpAvatar-BZq4nH53.js → LpAvatar-DLkiMGKx.js} +1 -1
  20. package/dist/chunks/{LpBadge-Cz0pSSUu.js → LpBadge-RFp8uHFt.js} +1 -1
  21. package/dist/chunks/{LpBottomNav-DVdR5Mry.js → LpBottomNav-BcJVkAhh.js} +1 -1
  22. package/dist/chunks/{LpBreadcrumbs-B2jphAel.js → LpBreadcrumbs-DJ_DB45r.js} +1 -1
  23. package/dist/chunks/{LpCalendar-C0_N5LVT.js → LpCalendar-D6tfCr2G.js} +1 -1
  24. package/dist/chunks/{LpCheckbox-CwqLNdw4.js → LpCheckbox-DcXC2qd5.js} +2 -2
  25. package/dist/chunks/{LpCodeBlock-Dcss6bnl.js → LpCodeBlock-Dx0J9xlm.js} +2 -2
  26. package/dist/chunks/{LpCodeDisplay-CLms1zDY.js → LpCodeDisplay-BlN7U8Z-.js} +4 -4
  27. package/dist/chunks/{LpColorPicker-CbIY450X.js → LpColorPicker-Cjna_Bio.js} +3 -3
  28. package/dist/chunks/{LpConfirmDialog-RmP4z6va.js → LpConfirmDialog-bfcX5F9d.js} +2 -2
  29. package/dist/chunks/{LpConfirmHost-DheBgRMB.js → LpConfirmHost-DIgloymi.js} +1 -1
  30. package/dist/chunks/{LpContextMenu-DIYhlap8.js → LpContextMenu-CUFZIQX2.js} +2 -2
  31. package/dist/chunks/{LpContextSwitcher-fDA6ERl-.js → LpContextSwitcher-CMweLbv2.js} +2 -2
  32. package/dist/chunks/{LpCopyButton-DSqKFGEb.js → LpCopyButton-BrbvIVMG.js} +1 -1
  33. package/dist/chunks/{LpDataList-f9UjSx9m.js → LpDataList-DhETPjzv.js} +2 -2
  34. package/dist/chunks/{LpDatePicker-3kDQLVbg.js → LpDatePicker-B5gzwI3T.js} +5 -5
  35. package/dist/chunks/{LpDateRangePicker-DUPiTt0_.js → LpDateRangePicker-CwNnpyoL.js} +3 -3
  36. package/dist/chunks/{LpDisclosure-C5WZjhbQ.js → LpDisclosure-CsJ7nnFz.js} +1 -1
  37. package/dist/chunks/{LpDrawer-CkYvIGIa.js → LpDrawer-C1q2bl1r.js} +1 -1
  38. package/dist/chunks/{LpDropdownMenu-zSsK2Xra.js → LpDropdownMenu-DRABI-Bl.js} +2 -2
  39. package/dist/chunks/{LpEditable-CP7Y_lKg.js → LpEditable-DCPdDDBb.js} +2 -2
  40. package/dist/chunks/{LpEmptyState-dsc5x-ya.js → LpEmptyState-DT8U4R8L.js} +1 -1
  41. package/dist/chunks/{LpFieldNotice-TOTbWvgv.js → LpFieldNotice-mcmO-O-J.js} +1 -1
  42. package/dist/chunks/{LpFileTree-BPjnOTln.js → LpFileTree-wiXExcZI.js} +4 -4
  43. package/dist/chunks/{LpFileTreeNode-_-MiWmie.js → LpFileTreeNode-DfvIGmSz.js} +4 -4
  44. package/dist/chunks/{LpFormField-BJwD1iEF.js → LpFormField-C5GOuOY-.js} +1 -1
  45. package/dist/chunks/{LpHintLayer-CmfGnxYu.js → LpHintLayer-B0choo1r.js} +1 -1
  46. package/dist/chunks/{LpHints-C2Xvc1Fd.js → LpHints-BEetUVTl.js} +3 -3
  47. package/dist/chunks/{LpIcon-CoOLz1oJ.js → LpIcon-DCGhNsCU.js} +1 -1
  48. package/dist/chunks/{LpInfraNode-CW3qsM-U.js → LpInfraNode-BsExhdPE.js} +2 -2
  49. package/dist/chunks/{LpInput-BVWhkPuN.js → LpInput-CKMblRtp.js} +3 -3
  50. package/dist/chunks/{LpInspector-REmoX_es.js → LpInspector-CqPgVXrm.js} +4 -4
  51. package/dist/chunks/{LpLightbox-C3FFTYP5.js → LpLightbox-D7iJqdmH.js} +4 -4
  52. package/dist/chunks/{LpLogInspector-Cs8Z_fM0.js → LpLogInspector-Yj310k4n.js} +2 -2
  53. package/dist/chunks/{LpLogViewer-DQRGe-ZK.js → LpLogViewer-Bki-IdNC.js} +6 -6
  54. package/dist/chunks/{LpMapPicker-CQnQPgKm.js → LpMapPicker-CnpJhiY0.js} +1 -1
  55. package/dist/chunks/{LpMediaGallery-DkKqTy9P.js → LpMediaGallery-D6DyJDvL.js} +4 -4
  56. package/dist/chunks/{LpModal-DwejR7VP.js → LpModal-B5y9_LgA.js} +1 -1
  57. package/dist/chunks/{LpModalNav-D1-dSe6f.js → LpModalNav-BLvDVl2-.js} +3 -3
  58. package/dist/chunks/{LpNotificationBell-EKLkN0sp.js → LpNotificationBell-Bf0u3t5q.js} +4 -4
  59. package/dist/chunks/{LpNotificationFeed-Dc-yQ4NH.js → LpNotificationFeed-BPZM3zkS.js} +4 -4
  60. package/dist/chunks/{LpNumberField-DvUjEMp-.js → LpNumberField-BeYT7zzt.js} +3 -3
  61. package/dist/chunks/{LpOtpInput-xvDO14f4.js → LpOtpInput-DqwY-U5R.js} +1 -1
  62. package/dist/chunks/{LpPagination-BFZY8dFn.js → LpPagination-V1wEhu1K.js} +1 -1
  63. package/dist/chunks/{LpPaletteTheme-EBJv0Bwz.js → LpPaletteTheme-Bi6pD1GL.js} +2 -2
  64. package/dist/chunks/{LpPasswordInput-DRJR3fF1.js → LpPasswordInput-LaOXNYuV.js} +2 -2
  65. package/dist/chunks/{LpPhoneInput-BGOOtUoW.js → LpPhoneInput-D7Tq2iaz.js} +3 -3
  66. package/dist/chunks/{LpPie-Deyn8h5J.js → LpPie-Bzv8mnVR.js} +2 -2
  67. package/dist/chunks/{LpPipeline-9ChjeNh5.js → LpPipeline-D-uPcbMN.js} +2 -2
  68. package/dist/chunks/{LpQuota-CNh5eF_p.js → LpQuota-BR8ck2a9.js} +1 -1
  69. package/dist/chunks/{LpRadioGroup-Br2Nl9Gr.js → LpRadioGroup-DSOCL9MD.js} +2 -2
  70. package/dist/chunks/{LpRangeCalendar-D66Y3U_w.js → LpRangeCalendar-QepHrmba.js} +1 -1
  71. package/dist/chunks/{LpRangeSlider-eX-fIlOw.js → LpRangeSlider-BaAM7XIs.js} +1 -1
  72. package/dist/chunks/{LpRepeater-0XvnlVd_.js → LpRepeater-BKq5XPh4.js} +3 -3
  73. package/dist/chunks/{LpSecretField-1RCooChq.js → LpSecretField-_B2qPfk8.js} +5 -5
  74. package/dist/chunks/{LpSection-B9gbJJcP.js → LpSection-BGzlKIWO.js} +1 -1
  75. package/dist/chunks/{LpSegmented-hIjGzGTo.js → LpSegmented-CiaF9DUK.js} +2 -2
  76. package/dist/chunks/{LpSelect-DugubwTc.js → LpSelect-CB6Xbnfk.js} +4 -4
  77. package/dist/chunks/{LpSidebar-DevSoN8W.js → LpSidebar-DnqbKULj.js} +4 -4
  78. package/dist/chunks/{LpSidebarNav-DwQQ3KXe.js → LpSidebarNav-B3Eo_VIS.js} +4 -4
  79. package/dist/chunks/{LpSidebarRow-CAGgtJCS.js → LpSidebarRow-CDBnvvd8.js} +3 -3
  80. package/dist/chunks/{LpSlider-1fHpVxpB.js → LpSlider-DTb_Ivkm.js} +1 -1
  81. package/dist/chunks/{LpStat-Cm9-FaWQ.js → LpStat-BtxSto7K.js} +2 -2
  82. package/dist/chunks/{LpStatus-CE8FSAT1.js → LpStatus-_fIwWXQV.js} +1 -1
  83. package/dist/chunks/{LpStepMark-BWSlV69R.js → LpStepMark-iDZw1DYf.js} +1 -1
  84. package/dist/chunks/{LpStepper-1D0ZA7Nx.js → LpStepper-CjC3cp1F.js} +1 -1
  85. package/dist/chunks/{LpSwitch-D_kvE0oQ.js → LpSwitch-BNHyYDhX.js} +1 -1
  86. package/dist/chunks/{LpTabFace-CpbFKi-N.js → LpTabFace-gATEe9pQ.js} +2 -2
  87. package/dist/chunks/{LpTableFilter-BYHyCu38.js → LpTableFilter-B_BFoBqS.js} +2 -2
  88. package/dist/chunks/{LpTagsInput-Cqm38t5N.js → LpTagsInput-By7LMb47.js} +5 -5
  89. package/dist/chunks/{LpTextarea-DaSKBkiV.js → LpTextarea-Ck6RAfzu.js} +1 -1
  90. package/dist/chunks/{LpThemeStudio-BNdfi-Ds.js → LpThemeStudio-C1xKiv8C.js} +5 -5
  91. package/dist/chunks/{LpThemeSwitcher-CbC1nqBP.js → LpThemeSwitcher-Dk4Bk6u1.js} +3 -3
  92. package/dist/chunks/{LpTimeField-CCfPCWPT.js → LpTimeField-BTKJVeNt.js} +2 -2
  93. package/dist/chunks/{LpToaster-BhlI7h-h.js → LpToaster-C_CFWSys.js} +2 -2
  94. package/dist/chunks/{LpTopologyCanvas-DlKbu7E5.js → LpTopologyCanvas-paC1Dup-.js} +4 -4
  95. package/dist/chunks/{LpTour-CSLerA1B.js → LpTour-D_AN_SsZ.js} +1 -1
  96. package/dist/chunks/{LpTruncate-Ct26BSYB.js → LpTruncate-CBUKnr36.js} +1 -1
  97. package/dist/chunks/LpVideoCompress-DD2nShyT.js +242 -0
  98. package/dist/chunks/{LpVideoPlayer-DcWd9DDW.js → LpVideoPlayer-koYKHKkC.js} +4 -4
  99. package/dist/chunks/{LpWindow-YnF_knSj.js → LpWindow-Bo5qQgVY.js} +2 -2
  100. package/dist/chunks/{LpWorkspaceSwitcher-B-5UU6o4.js → LpWorkspaceSwitcher-D50sHs9N.js} +3 -3
  101. package/dist/chunks/{MenuTitle-BnZbFSwq.js → MenuTitle-DCHcfyPH.js} +1 -1
  102. package/dist/chunks/{OptionContent-D5e_eklA.js → OptionContent-o58kS7Zh.js} +1 -1
  103. package/dist/chunks/{PickerField-KpHiowNI.js → PickerField-DNo1k89A.js} +1 -1
  104. package/dist/chunks/{RevealToggle-CE1hAIJB.js → RevealToggle-w7-BMcgC.js} +1 -1
  105. package/dist/chunks/{TourCard-t2csiTDS.js → TourCard-0yYxSdd4.js} +1 -1
  106. package/dist/chunks/{VideoThumb-Csf9bUz3.js → VideoThumb-DL_QkAJY.js} +1 -1
  107. package/dist/chunks/{VideoVolume-B4_DNmNi.js → VideoVolume-D-2TmsVj.js} +2 -2
  108. package/dist/chunks/{icons-CdSlCrle.js → icons-V0k93Puc.js} +3 -3
  109. package/dist/component-names.d.ts +1 -1
  110. package/dist/components/ContextMenuItems.vue.js +1 -1
  111. package/dist/components/DropzoneLinkField.vue.js +1 -1
  112. package/dist/components/FieldLeading.vue.js +1 -1
  113. package/dist/components/FieldWrapper.vue.js +1 -1
  114. package/dist/components/HintSampleTag.vue.d.ts +5 -3
  115. package/dist/components/IconCloseButton.vue.js +1 -1
  116. package/dist/components/LayoutCanvas.vue.js +1 -1
  117. package/dist/components/LayoutNode.vue.js +1 -1
  118. package/dist/components/LpActionBar.vue.d.ts +5 -3
  119. package/dist/components/LpActionBar.vue.js +1 -1
  120. package/dist/components/LpActivityLine.vue.d.ts +6 -6
  121. package/dist/components/LpAddressInput.vue.js +1 -1
  122. package/dist/components/LpAlert.vue.d.ts +1 -1
  123. package/dist/components/LpAlert.vue.js +1 -1
  124. package/dist/components/LpAppShell.vue.d.ts +10 -14
  125. package/dist/components/LpAppShell.vue.js +1 -1
  126. package/dist/components/LpAutocomplete.vue.js +1 -1
  127. package/dist/components/LpAvatar.vue.js +1 -1
  128. package/dist/components/LpBadge.vue.d.ts +4 -0
  129. package/dist/components/LpBadge.vue.js +1 -1
  130. package/dist/components/LpBottomNav.vue.d.ts +6 -4
  131. package/dist/components/LpBottomNav.vue.js +1 -1
  132. package/dist/components/LpBreadcrumbs.vue.d.ts +6 -6
  133. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  134. package/dist/components/LpButton.vue.d.ts +6 -6
  135. package/dist/components/LpCalendar.vue.js +1 -1
  136. package/dist/components/LpChart.vue.js +2 -2
  137. package/dist/components/LpCheckbox.vue.js +1 -1
  138. package/dist/components/LpCodeBlock.vue.d.ts +12 -2
  139. package/dist/components/LpCodeBlock.vue.js +1 -1
  140. package/dist/components/LpCodeDisplay.vue.d.ts +1 -1
  141. package/dist/components/LpCodeDisplay.vue.js +1 -1
  142. package/dist/components/LpColorPicker.vue.js +1 -1
  143. package/dist/components/LpCommandPalette.vue.d.ts +1 -1
  144. package/dist/components/LpCommandPalette.vue.js +2 -2
  145. package/dist/components/LpConfirmDialog.vue.js +1 -1
  146. package/dist/components/LpConfirmHost.vue.js +1 -1
  147. package/dist/components/LpContextMenu.vue.js +1 -1
  148. package/dist/components/LpContextSwitcher.vue.d.ts +6 -4
  149. package/dist/components/LpContextSwitcher.vue.js +1 -1
  150. package/dist/components/LpCopyButton.vue.js +1 -1
  151. package/dist/components/LpDataList.vue.js +1 -1
  152. package/dist/components/LpDatePicker.vue.js +1 -1
  153. package/dist/components/LpDateRangePicker.vue.js +1 -1
  154. package/dist/components/LpDisclosure.vue.js +1 -1
  155. package/dist/components/LpDock.vue.js +1 -1
  156. package/dist/components/LpDrawer.vue.d.ts +6 -6
  157. package/dist/components/LpDrawer.vue.js +1 -1
  158. package/dist/components/LpDropdownMenu.vue.js +1 -1
  159. package/dist/components/LpDropzone.vue.d.ts +10 -14
  160. package/dist/components/LpDropzone.vue.js +4 -4
  161. package/dist/components/LpEditable.vue.js +1 -1
  162. package/dist/components/LpEmptyState.vue.js +1 -1
  163. package/dist/components/LpFieldNotice.vue.js +1 -1
  164. package/dist/components/LpFileTree.vue.js +1 -1
  165. package/dist/components/LpFileTreeNode.vue.js +1 -1
  166. package/dist/components/LpFormField.vue.js +1 -1
  167. package/dist/components/LpHintExample.vue.d.ts +2 -3
  168. package/dist/components/LpHintLayer.vue.d.ts +7 -8
  169. package/dist/components/LpHintLayer.vue.js +1 -1
  170. package/dist/components/LpHints.vue.d.ts +6 -5
  171. package/dist/components/LpHints.vue.js +1 -1
  172. package/dist/components/LpIcon.vue.js +1 -1
  173. package/dist/components/LpImageEditor.vue.d.ts +14 -3
  174. package/dist/components/LpImageEditor.vue.js +3 -3
  175. package/dist/components/LpInfraNode.vue.js +1 -1
  176. package/dist/components/LpInput.vue.d.ts +6 -6
  177. package/dist/components/LpInput.vue.js +1 -1
  178. package/dist/components/LpInspector.vue.d.ts +8 -6
  179. package/dist/components/LpInspector.vue.js +1 -1
  180. package/dist/components/LpKeys.vue.js +2 -2
  181. package/dist/components/LpLightbox.vue.js +1 -1
  182. package/dist/components/LpLogInspector.vue.js +1 -1
  183. package/dist/components/LpLogViewer.vue.js +1 -1
  184. package/dist/components/LpMapPicker.vue.d.ts +8 -0
  185. package/dist/components/LpMapPicker.vue.js +1 -1
  186. package/dist/components/LpMediaGallery.vue.d.ts +3 -7
  187. package/dist/components/LpMediaGallery.vue.js +1 -1
  188. package/dist/components/LpModal.vue.d.ts +5 -3
  189. package/dist/components/LpModal.vue.js +1 -1
  190. package/dist/components/LpModalNav.vue.d.ts +4 -0
  191. package/dist/components/LpModalNav.vue.js +1 -1
  192. package/dist/components/LpNotificationBell.vue.js +1 -1
  193. package/dist/components/LpNotificationFeed.vue.js +1 -1
  194. package/dist/components/LpNumberField.vue.js +1 -1
  195. package/dist/components/LpOtpInput.vue.js +1 -1
  196. package/dist/components/LpPagination.vue.d.ts +8 -2
  197. package/dist/components/LpPagination.vue.js +1 -1
  198. package/dist/components/LpPaletteFacts.vue.js +1 -1
  199. package/dist/components/LpPaletteHeader.vue.js +2 -2
  200. package/dist/components/LpPaletteTheme.vue.d.ts +12 -0
  201. package/dist/components/LpPaletteTheme.vue.js +1 -1
  202. package/dist/components/LpPasswordInput.vue.js +1 -1
  203. package/dist/components/LpPhoneInput.vue.js +1 -1
  204. package/dist/components/LpPie.vue.js +1 -1
  205. package/dist/components/LpPipeline.vue.js +1 -1
  206. package/dist/components/LpQuota.vue.js +1 -1
  207. package/dist/components/LpRadioGroup.vue.js +1 -1
  208. package/dist/components/LpRangeCalendar.vue.js +1 -1
  209. package/dist/components/LpRangeSlider.vue.js +1 -1
  210. package/dist/components/LpRepeater.vue.d.ts +9 -7
  211. package/dist/components/LpRepeater.vue.js +1 -1
  212. package/dist/components/LpSecretField.vue.js +1 -1
  213. package/dist/components/LpSection.vue.js +1 -1
  214. package/dist/components/LpSegmented.vue.js +1 -1
  215. package/dist/components/LpSelect.vue.js +1 -1
  216. package/dist/components/LpShortcutsDialog.vue.d.ts +11 -1
  217. package/dist/components/LpShortcutsDialog.vue.js +3 -3
  218. package/dist/components/LpSidebar.vue.d.ts +13 -11
  219. package/dist/components/LpSidebar.vue.js +1 -1
  220. package/dist/components/LpSidebarNav.vue.js +1 -1
  221. package/dist/components/LpSidebarRow.vue.js +1 -1
  222. package/dist/components/LpSlider.vue.js +1 -1
  223. package/dist/components/LpSparkline.vue.d.ts +10 -1
  224. package/dist/components/LpStat.vue.js +1 -1
  225. package/dist/components/LpStatus.vue.js +1 -1
  226. package/dist/components/LpStepMark.vue.js +1 -1
  227. package/dist/components/LpStepper.vue.js +1 -1
  228. package/dist/components/LpSwitch.vue.js +1 -1
  229. package/dist/components/LpTabFace.vue.js +1 -1
  230. package/dist/components/LpTable.vue.js +9 -9
  231. package/dist/components/LpTableFilter.vue.js +1 -1
  232. package/dist/components/LpTableOfContents.vue.d.ts +5 -3
  233. package/dist/components/LpTabs.vue.js +2 -2
  234. package/dist/components/LpTagsInput.vue.js +1 -1
  235. package/dist/components/LpTextarea.vue.js +1 -1
  236. package/dist/components/LpThemeStudio.vue.js +1 -1
  237. package/dist/components/LpThemeSwitcher.vue.d.ts +8 -6
  238. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  239. package/dist/components/LpTimeField.vue.js +1 -1
  240. package/dist/components/LpToaster.vue.d.ts +7 -5
  241. package/dist/components/LpToaster.vue.js +1 -1
  242. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  243. package/dist/components/LpTour.vue.d.ts +13 -20
  244. package/dist/components/LpTour.vue.js +1 -1
  245. package/dist/components/LpTruncate.vue.js +1 -1
  246. package/dist/components/LpTypewriter.vue.d.ts +1 -1
  247. package/dist/components/LpUptimeBar.vue.d.ts +4 -2
  248. package/dist/components/LpVideoCompress.vue.d.ts +63 -0
  249. package/dist/components/LpVideoCompress.vue.js +4 -0
  250. package/dist/components/LpVideoPlayer.vue.d.ts +33 -3
  251. package/dist/components/LpVideoPlayer.vue.js +1 -1
  252. package/dist/components/LpVideoTrim.vue.d.ts +40 -3
  253. package/dist/components/LpVideoTrim.vue.js +6 -6
  254. package/dist/components/LpWindow.vue.d.ts +7 -8
  255. package/dist/components/LpWindow.vue.js +1 -1
  256. package/dist/components/LpWorkspaceSwitcher.vue.d.ts +13 -11
  257. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  258. package/dist/components/MenuTitle.vue.js +1 -1
  259. package/dist/components/OptionContent.vue.js +1 -1
  260. package/dist/components/PickerField.vue.js +1 -1
  261. package/dist/components/RevealToggle.vue.js +1 -1
  262. package/dist/components/TourCard.vue.js +1 -1
  263. package/dist/components/VideoFlash.vue.js +1 -1
  264. package/dist/components/VideoThumb.vue.js +1 -1
  265. package/dist/components/VideoVolume.vue.js +1 -1
  266. package/dist/components/menuWhen.js +1 -1
  267. package/dist/components/status.d.ts +3 -1
  268. package/dist/composables/usePaletteCommands.d.ts +4 -0
  269. package/dist/composables/videoTranscode.d.ts +55 -0
  270. package/dist/composables/videoTranscode.js +134 -0
  271. package/dist/icons-all.js +1 -1
  272. package/dist/index.d.ts +44 -10
  273. package/dist/index.js +748 -170
  274. package/dist/locales/uk.d.ts +165 -0
  275. package/package.json +6 -2
  276. package/src/component-names.ts +1 -0
  277. package/src/components/HintSampleTag.vue +7 -2
  278. package/src/components/LpActionBar.vue +7 -2
  279. package/src/components/LpActivityLine.vue +7 -2
  280. package/src/components/LpAppShell.vue +11 -2
  281. package/src/components/LpBadge.vue +6 -1
  282. package/src/components/LpBottomNav.vue +8 -2
  283. package/src/components/LpBreadcrumbs.vue +7 -2
  284. package/src/components/LpButton.vue +7 -2
  285. package/src/components/LpCodeBlock.vue +13 -2
  286. package/src/components/LpContextSwitcher.vue +8 -2
  287. package/src/components/LpDrawer.vue +7 -2
  288. package/src/components/LpDropzone.vue +11 -2
  289. package/src/components/LpHintExample.vue +2 -2
  290. package/src/components/LpHintLayer.vue +9 -3
  291. package/src/components/LpHints.vue +17 -3
  292. package/src/components/LpImageEditor.vue +14 -7
  293. package/src/components/LpInput.vue +7 -2
  294. package/src/components/LpInspector.vue +10 -2
  295. package/src/components/LpKeys.vue +2 -1
  296. package/src/components/LpMapPicker.vue +10 -1
  297. package/src/components/LpMediaGallery.vue +12 -3
  298. package/src/components/LpModal.vue +7 -2
  299. package/src/components/LpModalNav.vue +6 -1
  300. package/src/components/LpPagination.vue +9 -2
  301. package/src/components/LpPaletteTheme.vue +14 -1
  302. package/src/components/LpRepeater.vue +11 -2
  303. package/src/components/LpShortcutsDialog.vue +12 -1
  304. package/src/components/LpSidebar.vue +15 -12
  305. package/src/components/LpSparkline.vue +12 -2
  306. package/src/components/LpTableOfContents.vue +7 -2
  307. package/src/components/LpThemeSwitcher.vue +10 -2
  308. package/src/components/LpToaster.vue +9 -2
  309. package/src/components/LpTour.vue +15 -3
  310. package/src/components/LpUptimeBar.vue +5 -2
  311. package/src/components/LpVideoCompress.vue +237 -0
  312. package/src/components/LpVideoPlayer.vue +33 -7
  313. package/src/components/LpVideoTrim.vue +40 -9
  314. package/src/components/LpWindow.vue +8 -2
  315. package/src/components/LpWorkspaceSwitcher.vue +15 -12
  316. package/src/components/status.ts +4 -1
  317. package/src/composables/usePaletteCommands.ts +6 -1
  318. package/src/composables/videoTranscode.ts +192 -0
  319. package/src/index.ts +51 -10
  320. package/src/locales/uk.ts +746 -0
  321. package/src/nuxt.ts +5 -1
  322. package/src/theme/palette.ts +2 -1
  323. package/dist/assets/LpVideoTrim-BcGs3Sh0.css +0 -8
@@ -0,0 +1,237 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * Trim and shrink a video on the person's own machine before it is uploaded.
4
+ *
5
+ * <LpVideoCompress :file="file" @done="(out) => upload(out)" @skip="upload(file)" />
6
+ *
7
+ * - The picture plays from a local object URL, so nothing is sent while the
8
+ * person trims; LpVideoTrim does the trimming.
9
+ * - A preset picks the height and bitrate, or a target size ("fit for a chat"),
10
+ * and the line under it estimates the result against the original.
11
+ * - "Shrink" encodes with `transcodeVideo` (WebCodecs, hardware where there is
12
+ * some) with a progress bar and a cancel; `done` hands the new MP4 back.
13
+ * "Upload as is" emits `skip`: the original goes, untouched.
14
+ * - A browser without a video encoder only offers `skip`, and says why.
15
+ */
16
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue"
17
+
18
+ import { useLabels } from "../composables/useLpConfig"
19
+ import {
20
+ canTranscodeVideo,
21
+ clipLength,
22
+ estimateTranscodedBytes,
23
+ probeVideo,
24
+ transcodeVideo,
25
+ type TranscodeVideoOptions,
26
+ type VideoInfo,
27
+ } from "../composables/videoTranscode"
28
+ import { formatBytes } from "./format"
29
+ import LpButton from "./LpButton.vue"
30
+ import LpCollapse from "./LpCollapse.vue"
31
+ import LpProgress from "./LpProgress.vue"
32
+ import LpSegmented from "./LpSegmented.vue"
33
+ import LpVideoTrim from "./LpVideoTrim.vue"
34
+ import type { TimeRange } from "./videoTrim"
35
+
36
+ /** Wording of the compressor. `{size}`, `{original}`, `{percent}` and `{error}` are filled in. */
37
+ export type VideoCompressLabels = {
38
+ original: string
39
+ p1080: string
40
+ p720: string
41
+ p480: string
42
+ fit: string
43
+ quality: string
44
+ estimate: string
45
+ probing: string
46
+ shrink: string
47
+ skip: string
48
+ encoding: string
49
+ cancel: string
50
+ unsupported: string
51
+ failed: string
52
+ bigger: string
53
+ }
54
+
55
+ export interface VideoCompressPreset {
56
+ key: string
57
+ label: string
58
+ maxHeight?: number | null
59
+ targetBytes?: number | null
60
+ videoBitrate?: number | null
61
+ }
62
+
63
+ const props = withDefaults(
64
+ defineProps<{
65
+ file: File
66
+ /** The presets offered; the default set is original, 1080p, 720p, 480p and "fits 10 MB". */
67
+ presets?: VideoCompressPreset[]
68
+ /** The longest clip allowed, in seconds. */
69
+ maxClip?: number
70
+ disabled?: boolean
71
+ labels?: Partial<VideoCompressLabels>
72
+ }>(),
73
+ { presets: undefined, maxClip: undefined, labels: () => ({}) },
74
+ )
75
+
76
+ const preset = defineModel<string>("preset", { default: "p720" })
77
+
78
+ const emit = defineEmits<{
79
+ (e: "done", file: File, info: { range: TimeRange; preset: string }): void
80
+ (e: "skip"): void
81
+ (e: "busy", value: boolean): void
82
+ }>()
83
+
84
+ const text = useLabels<VideoCompressLabels>(
85
+ "videoCompress",
86
+ {
87
+ original: "Original",
88
+ p1080: "1080p",
89
+ p720: "720p",
90
+ p480: "480p",
91
+ /** `{size}` is replaced by the target size. */
92
+ fit: "Fits {size}",
93
+ quality: "Quality",
94
+ /** `{size}` and `{original}` are replaced by sizes; the size is a ceiling, simple pictures come out smaller. */
95
+ estimate: "Up to about {size} instead of {original}",
96
+ probing: "Reading the video…",
97
+ shrink: "Shrink",
98
+ skip: "Upload as is",
99
+ /** `{percent}` is replaced by a number. */
100
+ encoding: "Encoding on this device… {percent}%",
101
+ cancel: "Cancel",
102
+ unsupported: "This browser can't encode video, so the original will be uploaded.",
103
+ /** `{error}` is replaced by the reason. */
104
+ failed: "Couldn't encode: {error}",
105
+ bigger: "The result wouldn't be smaller, so the original is better.",
106
+ },
107
+ () => props.labels,
108
+ )
109
+
110
+ const FIT_BYTES = 10 * 1024 * 1024
111
+
112
+ const choices = computed<VideoCompressPreset[]>(() => props.presets ?? [
113
+ { key: "original", label: text.value.original },
114
+ { key: "p1080", label: text.value.p1080, maxHeight: 1080 },
115
+ { key: "p720", label: text.value.p720, maxHeight: 720 },
116
+ { key: "p480", label: text.value.p480, maxHeight: 480 },
117
+ { key: "fit", label: text.value.fit.replace("{size}", formatBytes(FIT_BYTES)), maxHeight: 720, targetBytes: FIT_BYTES },
118
+ ])
119
+
120
+ const url = ref("")
121
+ const info = shallowRef<VideoInfo | null>(null)
122
+ const supported = ref<boolean | null>(null)
123
+ const range = ref<TimeRange | null>(null)
124
+ const progress = ref<number | null>(null)
125
+ const error = ref("")
126
+ let controller: AbortController | null = null
127
+
128
+ function load(file: File): void {
129
+ if (url.value) URL.revokeObjectURL(url.value)
130
+ url.value = URL.createObjectURL(file)
131
+ info.value = null
132
+ range.value = null
133
+ error.value = ""
134
+ void probeVideo(file).then(
135
+ (found) => { if (props.file === file) info.value = found },
136
+ (cause: unknown) => { if (props.file === file) error.value = text.value.failed.replace("{error}", cause instanceof Error ? cause.message : String(cause)) },
137
+ )
138
+ }
139
+
140
+ onMounted(() => {
141
+ void canTranscodeVideo().then((ok) => (supported.value = ok))
142
+ })
143
+ watch(() => props.file, load, { immediate: true })
144
+ onBeforeUnmount(() => {
145
+ controller?.abort()
146
+ if (url.value) URL.revokeObjectURL(url.value)
147
+ })
148
+
149
+ const chosen = computed(() => choices.value.find((p) => p.key === preset.value) ?? choices.value[0])
150
+
151
+ const options = computed<TranscodeVideoOptions>(() => ({
152
+ range: range.value,
153
+ maxHeight: chosen.value?.maxHeight ?? null,
154
+ targetBytes: chosen.value?.targetBytes ?? null,
155
+ videoBitrate: chosen.value?.videoBitrate ?? null,
156
+ }))
157
+
158
+ const estimate = computed(() => (info.value ? estimateTranscodedBytes(info.value, options.value) : null))
159
+
160
+ const trimmed = computed(() => {
161
+ const found = info.value
162
+ return !!found && clipLength(found.duration, range.value) < found.duration - 0.05
163
+ })
164
+
165
+ const worthIt = computed(() => estimate.value !== null && (estimate.value < props.file.size * 0.95 || trimmed.value))
166
+
167
+ const estimateLine = computed(() => {
168
+ if (estimate.value === null) return text.value.probing
169
+ if (!worthIt.value) return text.value.bigger
170
+ return text.value.estimate.replace("{size}", formatBytes(estimate.value)).replace("{original}", formatBytes(props.file.size))
171
+ })
172
+
173
+ const busy = computed(() => progress.value !== null)
174
+ watch(busy, (value) => emit("busy", value))
175
+
176
+ async function shrink(): Promise<void> {
177
+ if (busy.value || !info.value) return
178
+ controller = new AbortController()
179
+ progress.value = 0
180
+ error.value = ""
181
+ try {
182
+ const out = await transcodeVideo(props.file, {
183
+ ...options.value,
184
+ onProgress: (fraction) => (progress.value = fraction),
185
+ signal: controller.signal,
186
+ })
187
+ emit("done", out, { range: range.value ?? [0, info.value.duration], preset: preset.value })
188
+ } catch (cause) {
189
+ if (!(cause instanceof DOMException && cause.name === "AbortError")) {
190
+ error.value = text.value.failed.replace("{error}", cause instanceof Error ? cause.message : String(cause))
191
+ }
192
+ } finally {
193
+ progress.value = null
194
+ controller = null
195
+ }
196
+ }
197
+
198
+ function cancel(): void {
199
+ controller?.abort()
200
+ }
201
+
202
+ const percent = computed(() => Math.round((progress.value ?? 0) * 100))
203
+ </script>
204
+
205
+ <template>
206
+ <div class="flex flex-col gap-4" data-lp-video-compress>
207
+ <LpVideoTrim v-model:range="range" :src="url" :max-clip="maxClip" :disabled="disabled || busy" />
208
+ <div v-if="supported !== false" class="flex flex-col gap-2">
209
+ <span class="text-xs font-medium text-muted">{{ text.quality }}</span>
210
+ <LpSegmented
211
+ v-model="preset"
212
+ :options="choices.map((p) => ({ value: p.key, label: p.label }))"
213
+ :disabled="disabled || busy"
214
+ size="sm"
215
+ class="max-w-full overflow-x-auto"
216
+ />
217
+ <p class="text-sm text-muted" aria-live="polite">{{ estimateLine }}</p>
218
+ </div>
219
+ <p v-else class="text-sm text-muted">{{ text.unsupported }}</p>
220
+ <LpCollapse :open="!!error">
221
+ <p class="text-sm text-danger" role="alert">{{ error }}</p>
222
+ </LpCollapse>
223
+ <LpCollapse :open="busy">
224
+ <div class="flex flex-col gap-1.5">
225
+ <LpProgress :value="percent" :max="100" />
226
+ <span class="text-xs text-muted" aria-live="polite">{{ text.encoding.replace("{percent}", String(percent)) }}</span>
227
+ </div>
228
+ </LpCollapse>
229
+ <div class="flex flex-wrap justify-end gap-2">
230
+ <LpButton v-if="busy" variant="ghost" @click="cancel">{{ text.cancel }}</LpButton>
231
+ <template v-else>
232
+ <LpButton variant="ghost" :disabled="disabled" @click="emit('skip')">{{ text.skip }}</LpButton>
233
+ <LpButton v-if="supported !== false" :disabled="disabled || !info || !worthIt" @click="shrink">{{ text.shrink }}</LpButton>
234
+ </template>
235
+ </div>
236
+ </div>
237
+ </template>
@@ -61,6 +61,37 @@ import { formatClipTime, widestClipTime } from "./videoBackdrop"
61
61
  import { FrameSampler } from "./videoFrames"
62
62
  import { FADE, FADE_SCALE, ICON_SWAP, OVERLAY_BUTTON } from "./videoMotion"
63
63
 
64
+ /** Wording of the video player. `{n}` is seconds, `{time}` the playhead's time. */
65
+ export type VideoPlayerLabels = {
66
+ player: string
67
+ play: string
68
+ pause: string
69
+ replay: string
70
+ mute: string
71
+ unmute: string
72
+ volume: string
73
+ speed: string
74
+ normalSpeed: string
75
+ fullscreen: string
76
+ exitFullscreen: string
77
+ pip: string
78
+ exitPip: string
79
+ captions: string
80
+ seek: string
81
+ failed: string
82
+ unsupported: string
83
+ back: string
84
+ forward: string
85
+ rewind: string
86
+ fastForward: string
87
+ menuAt: string
88
+ copyTime: string
89
+ copyFrame: string
90
+ copiedTime: string
91
+ copiedFrame: string
92
+ copyFailed: string
93
+ }
94
+
64
95
  const props = withDefaults(
65
96
  defineProps<{
66
97
  src: string
@@ -89,12 +120,7 @@ const props = withDefaults(
89
120
  fit?: boolean
90
121
  /** More items for the right-click menu. */
91
122
  menuItems?: () => ContextMenuItemDef[]
92
- labels?: Partial<Record<
93
- | "player" | "play" | "pause" | "replay" | "mute" | "unmute" | "volume" | "speed" | "normalSpeed"
94
- | "fullscreen" | "exitFullscreen" | "pip" | "exitPip" | "captions" | "seek" | "failed" | "unsupported"
95
- | "back" | "forward" | "rewind" | "fastForward" | "menuAt" | "copyTime" | "copyFrame" | "copiedTime" | "copiedFrame" | "copyFailed",
96
- string
97
- >>
123
+ labels?: Partial<VideoPlayerLabels>
98
124
  }>(),
99
125
  {
100
126
  poster: undefined,
@@ -115,7 +141,7 @@ const emit = defineEmits<{
115
141
  (e: "error", error: { cause: VideoErrorCause; message: string }): void
116
142
  }>()
117
143
 
118
- const text = useLabels(
144
+ const text = useLabels<VideoPlayerLabels>(
119
145
  "videoPlayer",
120
146
  {
121
147
  player: "Video player — Space plays, arrows seek, F fullscreen",
@@ -67,6 +67,44 @@ import {
67
67
  type VideoTrimErrorCause,
68
68
  } from "./videoTrim"
69
69
 
70
+ /** Wording of the video trimmer. `{left}`, `{max}` and `{time}` are times. */
71
+ export type VideoTrimLabels = {
72
+ play: string
73
+ pause: string
74
+ stepBack: string
75
+ stepForward: string
76
+ mute: string
77
+ unmute: string
78
+ zoomIn: string
79
+ zoomOut: string
80
+ zoomFit: string
81
+ start: string
82
+ end: string
83
+ position: string
84
+ length: string
85
+ left: string
86
+ overview: string
87
+ timeline: string
88
+ region: string
89
+ regionHint: string
90
+ regionClear: string
91
+ failed: string
92
+ unsupported: string
93
+ invalidTime: string
94
+ menuAt: string
95
+ setStart: string
96
+ setEnd: string
97
+ goStart: string
98
+ goEnd: string
99
+ zoomHere: string
100
+ volume: string
101
+ copyTime: string
102
+ copyFrame: string
103
+ copiedTime: string
104
+ copiedFrame: string
105
+ copyFailed: string
106
+ }
107
+
70
108
  const props = withDefaults(
71
109
  defineProps<{
72
110
  src: string
@@ -86,14 +124,7 @@ const props = withDefaults(
86
124
  /** More items for the right-click menu, given the time it was opened at (under the pointer on the timeline, the playhead elsewhere). */
87
125
  menuItems?: (at: number) => ContextMenuItemDef[]
88
126
  disabled?: boolean
89
- labels?: Partial<Record<
90
- | "play" | "pause" | "stepBack" | "stepForward" | "mute" | "unmute" | "zoomIn" | "zoomOut" | "zoomFit"
91
- | "start" | "end" | "position" | "length" | "left" | "overview" | "timeline"
92
- | "region" | "regionHint" | "regionClear" | "failed" | "unsupported" | "invalidTime"
93
- | "menuAt" | "setStart" | "setEnd" | "goStart" | "goEnd" | "zoomHere" | "volume"
94
- | "copyTime" | "copyFrame" | "copiedTime" | "copiedFrame" | "copyFailed",
95
- string
96
- >>
127
+ labels?: Partial<VideoTrimLabels>
97
128
  }>(),
98
129
  { poster: undefined, range: undefined, maxClip: undefined, minClip: 0.1, region: null, frameRate: 30, labels: () => ({}) },
99
130
  )
@@ -106,7 +137,7 @@ const emit = defineEmits<{
106
137
  (e: "error", error: { cause: VideoTrimErrorCause; message: string }): void
107
138
  }>()
108
139
 
109
- const text = useLabels(
140
+ const text = useLabels<VideoTrimLabels>(
110
141
  "videoTrim",
111
142
  {
112
143
  play: "Play",
@@ -1,5 +1,11 @@
1
1
  <script lang="ts">
2
2
  export default { inheritAttrs: false }
3
+
4
+ /** Wording of the window's controls. */
5
+ export type WindowLabels = {
6
+ close: string
7
+ move: string
8
+ }
3
9
  </script>
4
10
 
5
11
  <script setup lang="ts">
@@ -93,7 +99,7 @@ const props = withDefaults(
93
99
  guard?: CloseGuard
94
100
  /** Frosted glass instead of a solid panel. Unset follows `provideGlass`. */
95
101
  glass?: boolean
96
- labels?: Partial<{ close: string; move: string }>
102
+ labels?: Partial<WindowLabels>
97
103
  }>(),
98
104
  {
99
105
  minWidth: 240,
@@ -113,7 +119,7 @@ const emit = defineEmits<{
113
119
  (e: "change", value: { rect: WindowRect; dock: WindowDock | null }): void
114
120
  }>()
115
121
 
116
- const text = useLabels("window", { close: "Close", move: "Move window" }, () => props.labels)
122
+ const text = useLabels<WindowLabels>("window", { close: "Close", move: "Move window" }, () => props.labels)
117
123
  const frosted = useGlass(() => props.glass)
118
124
  const portalTo = usePortalTarget()
119
125
  const view = useView()
@@ -18,22 +18,25 @@ import LpDropdownMenu, { type MenuItem } from "./LpDropdownMenu.vue"
18
18
  import LpInput from "./LpInput.vue"
19
19
  import LpModal from "./LpModal.vue"
20
20
 
21
+ /** Wording of the workspace switcher. */
22
+ export type WorkspaceSwitcherLabels = {
23
+ menu: string
24
+ create: string
25
+ duplicate: string
26
+ rename: string
27
+ remove: string
28
+ name: string
29
+ save: string
30
+ cancel: string
31
+ required: string
32
+ }
33
+
21
34
  const props = defineProps<{
22
35
  workspaces: UseWorkspaces
23
- labels?: Partial<{
24
- menu: string
25
- create: string
26
- duplicate: string
27
- rename: string
28
- remove: string
29
- name: string
30
- save: string
31
- cancel: string
32
- required: string
33
- }>
36
+ labels?: Partial<WorkspaceSwitcherLabels>
34
37
  }>()
35
38
 
36
- const text = useLabels(
39
+ const text = useLabels<WorkspaceSwitcherLabels>(
37
40
  "workspaces",
38
41
  {
39
42
  menu: "Workspaces",
@@ -35,7 +35,10 @@ export const STATUS_LOOK: Record<StatusKind, StatusLook> = {
35
35
  unknown: { tone: "neutral", pulse: false },
36
36
  }
37
37
 
38
- export const STATUS_LABELS: Record<StatusKind, string> = {
38
+ /** A name for each status, as LpStatus shows and announces it. */
39
+ export type StatusLabels = Record<StatusKind, string>
40
+
41
+ export const STATUS_LABELS: StatusLabels = {
39
42
  online: "Online",
40
43
  offline: "Offline",
41
44
  degraded: "Degraded",
@@ -5,6 +5,11 @@ import type { ContextMenuItemDef } from "../components/LpContextMenu.vue"
5
5
  import { provideField, useField } from "./useField"
6
6
  import { useLabels } from "./useLpConfig"
7
7
 
8
+ /** Wording of the palette command a form control registers. */
9
+ export type PaletteControlLabels = {
10
+ focus: string
11
+ }
12
+
8
13
  export interface PaletteRegistration {
9
14
  commands: () => readonly Command[]
10
15
  priority: number
@@ -185,7 +190,7 @@ export function usePaletteControl(options: {
185
190
  const blocked = inject(BLOCKED, false)
186
191
  if (options.ownsChildren) suppressPaletteDiscovery()
187
192
  if (!instance || !scope || blocked) return
188
- const text = useLabels("paletteControl", { focus: "Go to field" })
193
+ const text = useLabels<PaletteControlLabels>("paletteControl", { focus: "Go to field" })
189
194
  const field = useField()
190
195
  const settings = () => (instance.attrs.palette ?? instance.props.palette) as PaletteControlOptions | undefined
191
196
  const label = () => (settings() || {}).label ?? options.label?.() ?? field?.value.label
@@ -0,0 +1,192 @@
1
+ /*
2
+ * Re-encoding a video in the browser before it is uploaded: cut to a range,
3
+ * scale down, squeeze to a bitrate or a target size. WebCodecs does the work
4
+ * (hardware encoders where the device has them) through mediabunny, which is
5
+ * loaded only when a video is actually probed or encoded, so pages that never
6
+ * touch a video don't download it.
7
+ */
8
+ import type { TimeRange } from "../components/videoTrim"
9
+
10
+ export interface VideoInfo {
11
+ /** Seconds. */
12
+ duration: number
13
+ /** Display size, after rotation. */
14
+ width: number
15
+ height: number
16
+ /** Bytes of the file as given. */
17
+ size: number
18
+ hasAudio: boolean
19
+ }
20
+
21
+ export interface TranscodeVideoOptions {
22
+ /** `[start, end]` in seconds; the whole video when unset. */
23
+ range?: TimeRange | null
24
+ /** Scale down to this height when the video is taller; never scales up. */
25
+ maxHeight?: number | null
26
+ /** Aim the result at this many bytes; the video bitrate is derived from it. */
27
+ targetBytes?: number | null
28
+ /** Video bitrate in bits per second; ignored when `targetBytes` is set. */
29
+ videoBitrate?: number | null
30
+ /** 0..1 as encoding goes. */
31
+ onProgress?: (fraction: number) => void
32
+ /** Aborting stops the encoders and rejects with an `AbortError`. */
33
+ signal?: AbortSignal
34
+ }
35
+
36
+ /** Audio rate the estimate and the encoder share. */
37
+ export const TRANSCODE_AUDIO_BITRATE = 96_000
38
+ const MIN_VIDEO_BITRATE = 250_000
39
+ /* Container and key-frame overhead, kept off a target so the result lands under it. */
40
+ const TARGET_HEADROOM = 0.92
41
+
42
+ const loadLib = () => import("mediabunny")
43
+
44
+ /** A sensible video bitrate for a height, bits per second. */
45
+ export function bitrateForHeight(height: number): number {
46
+ if (height <= 480) return 1_200_000
47
+ if (height <= 720) return 2_500_000
48
+ if (height <= 1080) return 5_000_000
49
+ return 8_000_000
50
+ }
51
+
52
+ /** The height the output will have: the source's, or `maxHeight` when the source is taller, always even. */
53
+ export function outputHeight(sourceHeight: number, maxHeight?: number | null): number {
54
+ const height = maxHeight && sourceHeight > maxHeight ? maxHeight : sourceHeight
55
+ return Math.max(2, Math.round(height / 2) * 2)
56
+ }
57
+
58
+ /** Length of the clip that will be encoded, seconds. */
59
+ export function clipLength(duration: number, range?: TimeRange | null): number {
60
+ if (!range) return duration
61
+ const start = Math.max(0, Math.min(range[0], range[1]))
62
+ const end = Math.min(duration, Math.max(range[0], range[1]))
63
+ return Math.max(0, end - start)
64
+ }
65
+
66
+ type Planned = Pick<VideoInfo, "duration" | "height" | "hasAudio"> & Partial<Pick<VideoInfo, "size">>
67
+
68
+ /** The source's own video bitrate, from its size and length; null when unknown. */
69
+ function sourceBitrate(info: Planned): number | null {
70
+ if (!info.size || info.duration <= 0) return null
71
+ const audio = info.hasAudio ? TRANSCODE_AUDIO_BITRATE : 0
72
+ return Math.max(MIN_VIDEO_BITRATE, Math.floor((info.size * 8) / info.duration - audio))
73
+ }
74
+
75
+ /**
76
+ * The video bitrate an encode will use, bits per second. Never above the
77
+ * source's own: re-encoding adds no detail, only bytes.
78
+ */
79
+ export function plannedVideoBitrate(info: Planned, options: TranscodeVideoOptions = {}): number {
80
+ const length = clipLength(info.duration, options.range)
81
+ const ceiling = sourceBitrate(info) ?? Infinity
82
+ if (options.targetBytes && length > 0) {
83
+ const audio = info.hasAudio ? TRANSCODE_AUDIO_BITRATE : 0
84
+ return Math.min(ceiling, Math.max(MIN_VIDEO_BITRATE, Math.floor((options.targetBytes * 8 * TARGET_HEADROOM) / length - audio)))
85
+ }
86
+ return Math.min(ceiling, options.videoBitrate ?? bitrateForHeight(outputHeight(info.height, options.maxHeight)))
87
+ }
88
+
89
+ /** Roughly how many bytes an encode with these options will produce. */
90
+ export function estimateTranscodedBytes(info: Planned, options: TranscodeVideoOptions = {}): number {
91
+ const length = clipLength(info.duration, options.range)
92
+ const audio = info.hasAudio ? TRANSCODE_AUDIO_BITRATE : 0
93
+ return Math.round(((plannedVideoBitrate(info, options) + audio) * length) / 8)
94
+ }
95
+
96
+ /** Whether this browser can encode video at all (WebCodecs with an H.264, VP9 or AV1 encoder). */
97
+ export async function canTranscodeVideo(): Promise<boolean> {
98
+ if (typeof globalThis.VideoEncoder === "undefined" || typeof globalThis.VideoDecoder === "undefined") return false
99
+ try {
100
+ const lib = await loadLib()
101
+ return (await lib.getFirstEncodableVideoCodec(["avc", "vp9", "av1"])) !== null
102
+ } catch {
103
+ return false
104
+ }
105
+ }
106
+
107
+ /** Length, size and whether there is sound, read from the file's headers without decoding it. */
108
+ export async function probeVideo(file: Blob): Promise<VideoInfo> {
109
+ const lib = await loadLib()
110
+ const input = new lib.Input({ source: new lib.BlobSource(file), formats: lib.ALL_FORMATS })
111
+ try {
112
+ const video = await input.getPrimaryVideoTrack()
113
+ if (!video) throw new Error("The file has no video track")
114
+ const [duration, width, height, audio] = await Promise.all([
115
+ input.computeDuration(),
116
+ video.getDisplayWidth(),
117
+ video.getDisplayHeight(),
118
+ input.getPrimaryAudioTrack(),
119
+ ])
120
+ return { duration, width, height, size: file.size, hasAudio: audio !== null }
121
+ } finally {
122
+ input.dispose()
123
+ }
124
+ }
125
+
126
+ function abortError(): DOMException {
127
+ return new DOMException("The video encode was aborted", "AbortError")
128
+ }
129
+
130
+ /** The file name with an `.mp4` extension, which is what comes out. */
131
+ export function transcodedName(name: string): string {
132
+ const dot = name.lastIndexOf(".")
133
+ return `${dot > 0 ? name.slice(0, dot) : name || "video"}.mp4`
134
+ }
135
+
136
+ /**
137
+ * An MP4 with the range cut out, scaled down to `maxHeight` and encoded at the
138
+ * planned bitrate. Sound is kept and re-encoded (AAC, or Opus where the
139
+ * browser has no AAC encoder); a browser that can encode neither the picture
140
+ * nor the sound rejects instead of returning a silent or broken file.
141
+ */
142
+ export async function transcodeVideo(file: File, options: TranscodeVideoOptions = {}): Promise<File> {
143
+ if (options.signal?.aborted) throw abortError()
144
+ const lib = await loadLib()
145
+ const info = await probeVideo(file)
146
+ const height = outputHeight(info.height, options.maxHeight)
147
+ const width = Math.max(2, Math.round((info.width * height) / info.height / 2) * 2)
148
+ const bitrate = plannedVideoBitrate(info, options)
149
+ const videoCodec = await lib.getFirstEncodableVideoCodec(["avc", "vp9", "av1"], { width, height, quality: new lib.Quality({ bitrate }) })
150
+ if (!videoCodec) throw new Error("This browser can't encode video of this size")
151
+ const audioCodec = info.hasAudio ? await lib.getFirstEncodableAudioCodec(["aac", "opus"], { quality: new lib.Quality({ bitrate: TRANSCODE_AUDIO_BITRATE }) }) : null
152
+ if (info.hasAudio && !audioCodec) throw new Error("This browser can't encode this video's sound")
153
+
154
+ const input = new lib.Input({ source: new lib.BlobSource(file), formats: lib.ALL_FORMATS })
155
+ const target = new lib.BufferTarget()
156
+ const output = new lib.Output({ format: new lib.Mp4OutputFormat({ fastStart: "in-memory" }), target })
157
+ try {
158
+ const range = options.range ?? null
159
+ const conversion = await lib.Conversion.init({
160
+ input,
161
+ output,
162
+ trim: range ? { start: Math.min(range[0], range[1]), end: Math.max(range[0], range[1]) } : undefined,
163
+ video: {
164
+ height: height === info.height ? undefined : height,
165
+ codec: videoCodec,
166
+ quality: new lib.Quality({ bitrate }),
167
+ forceTranscode: true,
168
+ },
169
+ audio: audioCodec ? { codec: audioCodec, quality: new lib.Quality({ bitrate: TRANSCODE_AUDIO_BITRATE }) } : { discard: true },
170
+ })
171
+ if (!conversion.isValid) {
172
+ const reasons = conversion.discardedTracks.map((t) => t.reason).join(", ")
173
+ throw new Error(`This video can't be re-encoded here (${reasons || "unknown reason"})`)
174
+ }
175
+ const onAbort = () => void conversion.cancel()
176
+ options.signal?.addEventListener("abort", onAbort, { once: true })
177
+ conversion.onProgress = (progress) => options.onProgress?.(progress)
178
+ try {
179
+ await conversion.execute()
180
+ } catch (error) {
181
+ if (options.signal?.aborted) throw abortError()
182
+ throw error
183
+ } finally {
184
+ options.signal?.removeEventListener("abort", onAbort)
185
+ }
186
+ if (!target.buffer) throw new Error("The encoder produced no file")
187
+ options.onProgress?.(1)
188
+ return new File([target.buffer], transcodedName(file.name), { type: "video/mp4", lastModified: Date.now() })
189
+ } finally {
190
+ input.dispose()
191
+ }
192
+ }