@leavepulse/ui 0.81.0 → 0.83.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 (270) hide show
  1. package/dist/assets/LpVideoTrim-BcGs3Sh0.css +8 -0
  2. package/dist/assets/VideoFlash-VuPkC7QV.css +23 -0
  3. package/dist/chunks/{ContextMenuItems-DLK4q-mm.js → ContextMenuItems-h-apPY2d.js} +1 -1
  4. package/dist/chunks/{DropzoneLinkField-CcJu8De3.js → DropzoneLinkField-_Op8b_ee.js} +1 -1
  5. package/dist/chunks/{FieldLeading-CldGLINn.js → FieldLeading-mOMcwNqi.js} +1 -1
  6. package/dist/chunks/{FieldWrapper-Cj8ZgoKp.js → FieldWrapper-DtT-AIcn.js} +1 -1
  7. package/dist/chunks/{IconCloseButton-CV4iHRpy.js → IconCloseButton-YmisskyZ.js} +1 -1
  8. package/dist/chunks/{LayoutCanvas-BQbq7Ekw.js → LayoutCanvas-BrISZEBr.js} +1 -1
  9. package/dist/chunks/{LayoutNode-BMedRk3g.js → LayoutNode-CwMQ8ACa.js} +1 -1
  10. package/dist/chunks/{LpActionBar-C7g52Cyo.js → LpActionBar-CVVC6YbC.js} +1 -1
  11. package/dist/chunks/{LpAddressInput-1uakhQk5.js → LpAddressInput-tiuJ-bru.js} +7 -7
  12. package/dist/chunks/{LpAlert-DWFeR82L.js → LpAlert-8-x4H_Nk.js} +1 -1
  13. package/dist/chunks/{LpAppShell-BBINfN1v.js → LpAppShell-BW2YMq-A.js} +4 -4
  14. package/dist/chunks/{LpAutocomplete-DY3LvpB9.js → LpAutocomplete-BZODIops.js} +4 -4
  15. package/dist/chunks/{LpAvatar-CEZmMcGh.js → LpAvatar-BZq4nH53.js} +1 -1
  16. package/dist/chunks/{LpBadge-C_VqLHtg.js → LpBadge-Cz0pSSUu.js} +1 -1
  17. package/dist/chunks/{LpBottomNav-4I89sPDZ.js → LpBottomNav-DVdR5Mry.js} +1 -1
  18. package/dist/chunks/{LpBreadcrumbs-1BBzrfxG.js → LpBreadcrumbs-B2jphAel.js} +1 -1
  19. package/dist/chunks/{LpCalendar-D2dcXjUK.js → LpCalendar-C0_N5LVT.js} +1 -1
  20. package/dist/chunks/{LpCheckbox-CN-MEnbX.js → LpCheckbox-CwqLNdw4.js} +2 -2
  21. package/dist/chunks/{LpCodeBlock-Cj7zk4QE.js → LpCodeBlock-Dcss6bnl.js} +2 -2
  22. package/dist/chunks/{LpCodeDisplay-COzosm77.js → LpCodeDisplay-CLms1zDY.js} +4 -4
  23. package/dist/chunks/{LpColorPicker-BezW6DX1.js → LpColorPicker-CbIY450X.js} +3 -3
  24. package/dist/chunks/{LpConfirmDialog-D9GkDMYr.js → LpConfirmDialog-RmP4z6va.js} +2 -2
  25. package/dist/chunks/{LpConfirmHost-BH7kf0dm.js → LpConfirmHost-DheBgRMB.js} +1 -1
  26. package/dist/chunks/{LpContextMenu-bn5bc9t7.js → LpContextMenu-DIYhlap8.js} +2 -2
  27. package/dist/chunks/{LpContextSwitcher-CGr6ZAuq.js → LpContextSwitcher-fDA6ERl-.js} +2 -2
  28. package/dist/chunks/{LpCopyButton-BSr6YaFR.js → LpCopyButton-DSqKFGEb.js} +1 -1
  29. package/dist/chunks/{LpDataList-Cv7ZUlyy.js → LpDataList-f9UjSx9m.js} +2 -2
  30. package/dist/chunks/{LpDatePicker-CR7vsb5M.js → LpDatePicker-3kDQLVbg.js} +5 -5
  31. package/dist/chunks/{LpDateRangePicker-B9fRO3G6.js → LpDateRangePicker-DUPiTt0_.js} +3 -3
  32. package/dist/chunks/{LpDisclosure-CE4mneaE.js → LpDisclosure-C5WZjhbQ.js} +1 -1
  33. package/dist/chunks/{LpDrawer-DU_5MqBz.js → LpDrawer-CkYvIGIa.js} +1 -1
  34. package/dist/chunks/{LpDropdownMenu-_u6GPWhp.js → LpDropdownMenu-zSsK2Xra.js} +6 -3
  35. package/dist/chunks/{LpEditable-DYoMayjt.js → LpEditable-CP7Y_lKg.js} +2 -2
  36. package/dist/chunks/{LpEmptyState-DpdHeSfz.js → LpEmptyState-dsc5x-ya.js} +1 -1
  37. package/dist/chunks/{LpFieldNotice-DONFCWV7.js → LpFieldNotice-TOTbWvgv.js} +1 -1
  38. package/dist/chunks/{LpFileTree-f5ktGqsN.js → LpFileTree-BPjnOTln.js} +4 -4
  39. package/dist/chunks/{LpFileTreeNode-nbp-JwV9.js → LpFileTreeNode-_-MiWmie.js} +4 -4
  40. package/dist/chunks/{LpFormField-Bz50_LZW.js → LpFormField-BJwD1iEF.js} +1 -1
  41. package/dist/chunks/{LpHintLayer-B6bA5Cb_.js → LpHintLayer-CmfGnxYu.js} +1 -1
  42. package/dist/chunks/{LpHints-DyJ5hVcr.js → LpHints-C2Xvc1Fd.js} +3 -3
  43. package/dist/chunks/{LpIcon-BCQLuHSP.js → LpIcon-CoOLz1oJ.js} +1 -1
  44. package/dist/chunks/{LpInfraNode-BS1RXYAs.js → LpInfraNode-CW3qsM-U.js} +2 -2
  45. package/dist/chunks/{LpInput-BjbXdjcW.js → LpInput-BVWhkPuN.js} +3 -3
  46. package/dist/chunks/{LpInspector-BIyJyM6r.js → LpInspector-REmoX_es.js} +4 -4
  47. package/dist/chunks/{LpLightbox-CSAJuUaJ.js → LpLightbox-C3FFTYP5.js} +65 -20
  48. package/dist/chunks/{LpLogInspector-OJEz1vod.js → LpLogInspector-Cs8Z_fM0.js} +2 -2
  49. package/dist/chunks/{LpLogViewer-CRLwQpk0.js → LpLogViewer-DQRGe-ZK.js} +6 -6
  50. package/dist/chunks/{LpMapPicker-DW2EnrCJ.js → LpMapPicker-CQnQPgKm.js} +1 -1
  51. package/dist/chunks/{LpMediaGallery-BNEYctDP.js → LpMediaGallery-DkKqTy9P.js} +20 -8
  52. package/dist/chunks/{LpModal-DdTqdf4x.js → LpModal-DwejR7VP.js} +1 -1
  53. package/dist/chunks/{LpModalNav-BL7Dp0om.js → LpModalNav-D1-dSe6f.js} +3 -3
  54. package/dist/chunks/{LpNotificationBell-d0F6MWxD.js → LpNotificationBell-EKLkN0sp.js} +4 -4
  55. package/dist/chunks/{LpNotificationFeed-BIe5lapb.js → LpNotificationFeed-Dc-yQ4NH.js} +4 -4
  56. package/dist/chunks/{LpNumberField-D5Dx-hzD.js → LpNumberField-DvUjEMp-.js} +3 -3
  57. package/dist/chunks/{LpOtpInput-BVl74Lfl.js → LpOtpInput-xvDO14f4.js} +1 -1
  58. package/dist/chunks/{LpPagination-D0wb-Uru.js → LpPagination-BFZY8dFn.js} +1 -1
  59. package/dist/chunks/{LpPaletteTheme-DdilCBXA.js → LpPaletteTheme-EBJv0Bwz.js} +2 -2
  60. package/dist/chunks/{LpPasswordInput-DUtqWMkA.js → LpPasswordInput-DRJR3fF1.js} +2 -2
  61. package/dist/chunks/{LpPhoneInput-CalRjkqc.js → LpPhoneInput-BGOOtUoW.js} +3 -3
  62. package/dist/chunks/{LpPie-BVAEmVBT.js → LpPie-Deyn8h5J.js} +2 -2
  63. package/dist/chunks/{LpPipeline-Dk7_sRP1.js → LpPipeline-9ChjeNh5.js} +2 -2
  64. package/dist/chunks/{LpQuota-DKx0IZYA.js → LpQuota-CNh5eF_p.js} +1 -1
  65. package/dist/chunks/{LpRadioGroup-E3SP6j9h.js → LpRadioGroup-Br2Nl9Gr.js} +2 -2
  66. package/dist/chunks/{LpRangeCalendar-BFJFaen4.js → LpRangeCalendar-D66Y3U_w.js} +1 -1
  67. package/dist/chunks/{LpRangeSlider-b6ipywRB.js → LpRangeSlider-eX-fIlOw.js} +1 -1
  68. package/dist/chunks/{LpRepeater-BLrw62Bh.js → LpRepeater-0XvnlVd_.js} +2 -2
  69. package/dist/chunks/{LpSecretField-BV8BNjKc.js → LpSecretField-1RCooChq.js} +5 -5
  70. package/dist/chunks/{LpSection-CghNNxIv.js → LpSection-B9gbJJcP.js} +1 -1
  71. package/dist/chunks/{LpSegmented-DjvrHL_G.js → LpSegmented-hIjGzGTo.js} +2 -2
  72. package/dist/chunks/{LpSelect-DYPimzAZ.js → LpSelect-DugubwTc.js} +4 -4
  73. package/dist/chunks/{LpSidebar-D3S8rYFf.js → LpSidebar-DevSoN8W.js} +4 -4
  74. package/dist/chunks/{LpSidebarNav-Dlse8vxT.js → LpSidebarNav-DwQQ3KXe.js} +3 -3
  75. package/dist/chunks/{LpSidebarRow-Cqd_6tGG.js → LpSidebarRow-CAGgtJCS.js} +3 -3
  76. package/dist/chunks/{LpSlider-B9XIC75O.js → LpSlider-1fHpVxpB.js} +1 -1
  77. package/dist/chunks/{LpStat-kyLRmX-m.js → LpStat-Cm9-FaWQ.js} +2 -2
  78. package/dist/chunks/{LpStatus-szB2dWzM.js → LpStatus-CE8FSAT1.js} +1 -1
  79. package/dist/chunks/{LpStepMark-DP3CDSm9.js → LpStepMark-BWSlV69R.js} +1 -1
  80. package/dist/chunks/{LpStepper-BS3iaJIz.js → LpStepper-1D0ZA7Nx.js} +1 -1
  81. package/dist/chunks/{LpSwitch-DRFy8RNr.js → LpSwitch-D_kvE0oQ.js} +1 -1
  82. package/dist/chunks/{LpTabFace-C9-OperL.js → LpTabFace-CpbFKi-N.js} +2 -2
  83. package/dist/chunks/{LpTableFilter-CEIwLupk.js → LpTableFilter-BYHyCu38.js} +2 -2
  84. package/dist/chunks/{LpTagsInput-ClDeF_aU.js → LpTagsInput-Cqm38t5N.js} +5 -5
  85. package/dist/chunks/{LpTextarea-DEY6pUBy.js → LpTextarea-DaSKBkiV.js} +1 -1
  86. package/dist/chunks/{LpThemeStudio-WY72Z1Db.js → LpThemeStudio-BNdfi-Ds.js} +5 -5
  87. package/dist/chunks/{LpThemeSwitcher-9uCH9c-K.js → LpThemeSwitcher-CbC1nqBP.js} +3 -3
  88. package/dist/chunks/{LpTimeField-BmgxCzQO.js → LpTimeField-CCfPCWPT.js} +2 -2
  89. package/dist/chunks/{LpToaster-DLKO-efn.js → LpToaster-BhlI7h-h.js} +2 -2
  90. package/dist/chunks/{LpTopologyCanvas-B-PhPa7k.js → LpTopologyCanvas-DlKbu7E5.js} +4 -4
  91. package/dist/chunks/{LpTour-ODlBcat2.js → LpTour-CSLerA1B.js} +1 -1
  92. package/dist/chunks/{LpTruncate--nhT0iUo.js → LpTruncate-Ct26BSYB.js} +1 -1
  93. package/dist/chunks/{LpVideoBackdrop-Ct4St4ZL.js → LpVideoBackdrop-57i8RY8g.js} +1 -2
  94. package/dist/chunks/LpVideoPlayer-DcWd9DDW.js +813 -0
  95. package/dist/chunks/{LpWindow-DJxE8Zl-.js → LpWindow-YnF_knSj.js} +2 -2
  96. package/dist/chunks/{LpWorkspaceSwitcher-Bpf41Bw5.js → LpWorkspaceSwitcher-B-5UU6o4.js} +3 -3
  97. package/dist/chunks/{MenuTitle-CMjvLsvC.js → MenuTitle-BnZbFSwq.js} +1 -1
  98. package/dist/chunks/{OptionContent-DY8NaG8s.js → OptionContent-D5e_eklA.js} +1 -1
  99. package/dist/chunks/{PickerField-CgfkaBgg.js → PickerField-KpHiowNI.js} +1 -1
  100. package/dist/chunks/{RevealToggle-CPMXxaKv.js → RevealToggle-CE1hAIJB.js} +1 -1
  101. package/dist/chunks/SteadyText-CwH-kduj.js +25 -0
  102. package/dist/chunks/{TourCard-D4b3lMHi.js → TourCard-t2csiTDS.js} +1 -1
  103. package/dist/chunks/VideoThumb-Csf9bUz3.js +67 -0
  104. package/dist/chunks/VideoVolume-B4_DNmNi.js +68 -0
  105. package/dist/chunks/icons-CdSlCrle.js +59 -0
  106. package/dist/component-names.d.ts +1 -1
  107. package/dist/components/ContextMenuItems.vue.js +1 -1
  108. package/dist/components/DropzoneLinkField.vue.js +1 -1
  109. package/dist/components/FieldLeading.vue.js +1 -1
  110. package/dist/components/FieldWrapper.vue.js +1 -1
  111. package/dist/components/IconCloseButton.vue.js +1 -1
  112. package/dist/components/LayoutCanvas.vue.js +1 -1
  113. package/dist/components/LayoutNode.vue.js +1 -1
  114. package/dist/components/LpActionBar.vue.js +1 -1
  115. package/dist/components/LpAddressInput.vue.js +1 -1
  116. package/dist/components/LpAlert.vue.js +1 -1
  117. package/dist/components/LpAppShell.vue.js +1 -1
  118. package/dist/components/LpAutocomplete.vue.js +1 -1
  119. package/dist/components/LpAvatar.vue.js +1 -1
  120. package/dist/components/LpBadge.vue.js +1 -1
  121. package/dist/components/LpBottomNav.vue.js +1 -1
  122. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  123. package/dist/components/LpCalendar.vue.js +1 -1
  124. package/dist/components/LpChart.vue.js +2 -2
  125. package/dist/components/LpCheckbox.vue.js +1 -1
  126. package/dist/components/LpCodeBlock.vue.js +1 -1
  127. package/dist/components/LpCodeDisplay.vue.js +1 -1
  128. package/dist/components/LpColorPicker.vue.js +1 -1
  129. package/dist/components/LpCommandPalette.vue.js +2 -2
  130. package/dist/components/LpConfirmDialog.vue.js +1 -1
  131. package/dist/components/LpConfirmHost.vue.js +1 -1
  132. package/dist/components/LpContextMenu.vue.js +1 -1
  133. package/dist/components/LpContextSwitcher.vue.js +1 -1
  134. package/dist/components/LpCopyButton.vue.js +1 -1
  135. package/dist/components/LpDataList.vue.js +1 -1
  136. package/dist/components/LpDatePicker.vue.js +1 -1
  137. package/dist/components/LpDateRangePicker.vue.js +1 -1
  138. package/dist/components/LpDisclosure.vue.js +1 -1
  139. package/dist/components/LpDock.vue.js +1 -1
  140. package/dist/components/LpDrawer.vue.js +1 -1
  141. package/dist/components/LpDropdownMenu.vue.d.ts +5 -1
  142. package/dist/components/LpDropdownMenu.vue.js +1 -1
  143. package/dist/components/LpDropzone.vue.js +2 -2
  144. package/dist/components/LpEditable.vue.js +1 -1
  145. package/dist/components/LpEmptyState.vue.js +1 -1
  146. package/dist/components/LpFieldNotice.vue.js +1 -1
  147. package/dist/components/LpFileTree.vue.js +1 -1
  148. package/dist/components/LpFileTreeNode.vue.js +1 -1
  149. package/dist/components/LpFormField.vue.js +1 -1
  150. package/dist/components/LpHintLayer.vue.js +1 -1
  151. package/dist/components/LpHints.vue.js +1 -1
  152. package/dist/components/LpIcon.vue.js +1 -1
  153. package/dist/components/LpImageEditor.vue.js +1 -1
  154. package/dist/components/LpInfraNode.vue.js +1 -1
  155. package/dist/components/LpInput.vue.js +1 -1
  156. package/dist/components/LpInspector.vue.js +1 -1
  157. package/dist/components/LpLightbox.vue.js +1 -1
  158. package/dist/components/LpLogInspector.vue.js +1 -1
  159. package/dist/components/LpLogViewer.vue.js +1 -1
  160. package/dist/components/LpMapPicker.vue.js +1 -1
  161. package/dist/components/LpMediaGallery.vue.d.ts +1 -1
  162. package/dist/components/LpMediaGallery.vue.js +1 -1
  163. package/dist/components/LpModal.vue.js +1 -1
  164. package/dist/components/LpModalNav.vue.js +1 -1
  165. package/dist/components/LpNotificationBell.vue.js +1 -1
  166. package/dist/components/LpNotificationFeed.vue.js +1 -1
  167. package/dist/components/LpNumberField.vue.js +1 -1
  168. package/dist/components/LpOtpInput.vue.js +1 -1
  169. package/dist/components/LpPagination.vue.js +1 -1
  170. package/dist/components/LpPaletteFacts.vue.js +1 -1
  171. package/dist/components/LpPaletteHeader.vue.js +2 -2
  172. package/dist/components/LpPaletteTheme.vue.js +1 -1
  173. package/dist/components/LpPasswordInput.vue.js +1 -1
  174. package/dist/components/LpPhoneInput.vue.js +1 -1
  175. package/dist/components/LpPie.vue.js +1 -1
  176. package/dist/components/LpPipeline.vue.js +1 -1
  177. package/dist/components/LpQuota.vue.js +1 -1
  178. package/dist/components/LpRadioGroup.vue.js +1 -1
  179. package/dist/components/LpRangeCalendar.vue.js +1 -1
  180. package/dist/components/LpRangeSlider.vue.js +1 -1
  181. package/dist/components/LpRepeater.vue.js +1 -1
  182. package/dist/components/LpSecretField.vue.js +1 -1
  183. package/dist/components/LpSection.vue.js +1 -1
  184. package/dist/components/LpSegmented.vue.js +1 -1
  185. package/dist/components/LpSelect.vue.js +1 -1
  186. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  187. package/dist/components/LpSidebar.vue.js +1 -1
  188. package/dist/components/LpSidebarNav.vue.js +1 -1
  189. package/dist/components/LpSidebarRow.vue.js +1 -1
  190. package/dist/components/LpSlider.vue.js +1 -1
  191. package/dist/components/LpStat.vue.js +1 -1
  192. package/dist/components/LpStatus.vue.js +1 -1
  193. package/dist/components/LpStepMark.vue.js +1 -1
  194. package/dist/components/LpStepper.vue.js +1 -1
  195. package/dist/components/LpSwitch.vue.js +1 -1
  196. package/dist/components/LpTabFace.vue.js +1 -1
  197. package/dist/components/LpTable.vue.js +9 -9
  198. package/dist/components/LpTableFilter.vue.js +1 -1
  199. package/dist/components/LpTabs.vue.js +2 -2
  200. package/dist/components/LpTagsInput.vue.js +1 -1
  201. package/dist/components/LpTextarea.vue.js +1 -1
  202. package/dist/components/LpThemeStudio.vue.js +1 -1
  203. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  204. package/dist/components/LpTimeField.vue.js +1 -1
  205. package/dist/components/LpToaster.vue.js +1 -1
  206. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  207. package/dist/components/LpTour.vue.js +1 -1
  208. package/dist/components/LpTruncate.vue.js +1 -1
  209. package/dist/components/LpVideoBackdrop.vue.js +1 -1
  210. package/dist/components/LpVideoPlayer.vue.d.ts +111 -0
  211. package/dist/components/LpVideoPlayer.vue.js +4 -0
  212. package/dist/components/LpVideoTrim.vue.d.ts +82 -0
  213. package/dist/components/LpVideoTrim.vue.js +1106 -0
  214. package/dist/components/LpWindow.vue.js +1 -1
  215. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  216. package/dist/components/MenuTitle.vue.js +1 -1
  217. package/dist/components/OptionContent.vue.js +1 -1
  218. package/dist/components/PickerField.vue.js +1 -1
  219. package/dist/components/RevealToggle.vue.js +1 -1
  220. package/dist/components/SteadyText.vue.d.ts +7 -0
  221. package/dist/components/SteadyText.vue.js +4 -0
  222. package/dist/components/TourCard.vue.js +1 -1
  223. package/dist/components/VideoFlash.vue.d.ts +10 -0
  224. package/dist/components/VideoFlash.vue.js +33 -0
  225. package/dist/components/VideoThumb.vue.d.ts +12 -0
  226. package/dist/components/VideoThumb.vue.js +4 -0
  227. package/dist/components/VideoVolume.vue.d.ts +15 -0
  228. package/dist/components/VideoVolume.vue.js +4 -0
  229. package/dist/components/chartWindow.js +4 -8
  230. package/dist/components/lightbox.d.ts +13 -0
  231. package/dist/components/lightbox.js +11 -1
  232. package/dist/components/menuWhen.js +1 -1
  233. package/dist/components/videoBackdrop.d.ts +15 -2
  234. package/dist/components/videoBackdrop.js +8 -2
  235. package/dist/components/videoFrames.d.ts +37 -0
  236. package/dist/components/videoFrames.js +134 -0
  237. package/dist/components/videoMotion.d.ts +28 -0
  238. package/dist/components/videoMotion.js +26 -0
  239. package/dist/components/videoTrim.d.ts +68 -0
  240. package/dist/components/videoTrim.js +121 -0
  241. package/dist/composables/span.d.ts +10 -0
  242. package/dist/composables/span.js +15 -0
  243. package/dist/composables/useVideoMedia.d.ts +74 -0
  244. package/dist/composables/useVideoMedia.js +291 -0
  245. package/dist/icons-all.js +1 -1
  246. package/dist/index.d.ts +7 -1
  247. package/dist/index.js +186 -174
  248. package/package.json +1 -1
  249. package/src/component-names.ts +2 -0
  250. package/src/components/LpDropdownMenu.vue +4 -0
  251. package/src/components/LpLightbox.vue +61 -8
  252. package/src/components/LpMediaGallery.vue +19 -2
  253. package/src/components/LpVideoBackdrop.vue +1 -7
  254. package/src/components/LpVideoPlayer.vue +774 -0
  255. package/src/components/LpVideoTrim.vue +1066 -0
  256. package/src/components/SteadyText.vue +21 -0
  257. package/src/components/VideoFlash.vue +52 -0
  258. package/src/components/VideoThumb.vue +66 -0
  259. package/src/components/VideoVolume.vue +63 -0
  260. package/src/components/chartWindow.ts +4 -8
  261. package/src/components/lightbox.ts +23 -0
  262. package/src/components/videoBackdrop.ts +20 -3
  263. package/src/components/videoFrames.ts +171 -0
  264. package/src/components/videoMotion.ts +38 -0
  265. package/src/components/videoTrim.ts +186 -0
  266. package/src/composables/span.ts +21 -0
  267. package/src/composables/useVideoMedia.ts +411 -0
  268. package/src/index.ts +7 -1
  269. package/src/kit-icons.ts +1 -1
  270. package/dist/chunks/icons-C1vppR4f.js +0 -59
@@ -0,0 +1,186 @@
1
+ /*
2
+ * The arithmetic behind LpVideoTrim, kept apart from the component so the
3
+ * rules a server will rely on (what range comes out, what a typed time means)
4
+ * are tested without a video.
5
+ */
6
+ import { clamp } from "../composables/clamp"
7
+ import { shiftSpan, zoomSpan } from "../composables/span"
8
+
9
+ export type TimeRange = [number, number]
10
+
11
+ /** A rectangle on the video's frame, every value a fraction of it (0..1). */
12
+ export interface FrameRegion {
13
+ x: number
14
+ y: number
15
+ w: number
16
+ h: number
17
+ }
18
+
19
+ export interface RangeLimits {
20
+ duration: number
21
+ /** Shortest clip, seconds. */
22
+ minClip: number
23
+ /** Longest clip, seconds; Infinity for none. */
24
+ maxClip: number
25
+ }
26
+
27
+ /* Milliseconds: finer than any frame, and what an ffmpeg -ss argument takes. */
28
+ const round = (t: number) => Math.round(t * 1000) / 1000
29
+
30
+ /**
31
+ * A typed time, the inverse of `formatClipTime`: "75", "75.5", "1:15",
32
+ * "75:00.5", and "1:02:03.25" for whoever types the hours. A comma reads as
33
+ * the decimal point, as a Russian keyboard's numpad types it. Anything else,
34
+ * a negative or a seconds field past 59 after a colon, is null.
35
+ */
36
+ export function parseClipTime(text: string): number | null {
37
+ const value = text.trim().replace(",", ".")
38
+ if (!/^\d+(:\d{1,2}){0,2}(\.\d*)?$/.test(value)) return null
39
+ const parts = value.split(":")
40
+ let total = 0
41
+ for (const [i, part] of parts.entries()) {
42
+ const n = Number(part)
43
+ if (!Number.isFinite(n)) return null
44
+ if (i > 0 && n >= 60) return null
45
+ total = total * 60 + n
46
+ }
47
+ return total
48
+ }
49
+
50
+ function limitsOf(l: RangeLimits) {
51
+ const duration = Math.max(0, l.duration)
52
+ const min = clamp(l.minClip, 0, duration)
53
+ const max = Math.max(min, l.maxClip)
54
+ return { duration, min, max }
55
+ }
56
+
57
+ /** A range that keeps every limit: inside the video, no shorter than minClip, no longer than maxClip. */
58
+ export function normalizeRange(range: TimeRange | null | undefined, limits: RangeLimits): TimeRange {
59
+ const { duration, min, max } = limitsOf(limits)
60
+ if (!range || !Number.isFinite(range[0]) || !Number.isFinite(range[1])) return [0, round(Math.min(duration, max))]
61
+ let [a, b] = range[0] <= range[1] ? range : [range[1], range[0]]
62
+ a = clamp(a, 0, Math.max(0, duration - min))
63
+ b = clamp(b, a + min, Math.min(duration, a + max))
64
+ return [round(a), round(b)]
65
+ }
66
+
67
+ /**
68
+ * One end dragged to `t`. The dragged end stops at a limit and the other end
69
+ * stays put: a handle that pushed its partner would move the clip's other
70
+ * edge without the person touching it.
71
+ */
72
+ export function dragEnd(range: TimeRange, end: 0 | 1, t: number, limits: RangeLimits): TimeRange {
73
+ const { duration, min, max } = limitsOf(limits)
74
+ const [a, b] = range
75
+ if (end === 0) return [round(clamp(t, Math.max(0, b - max), b - min)), b]
76
+ return [a, round(clamp(t, a + min, Math.min(duration, a + max)))]
77
+ }
78
+
79
+ /**
80
+ * One end set to `t` on purpose (the I or O key, a typed time). That end goes
81
+ * where it was told, and the other follows when a limit asks for it.
82
+ */
83
+ export function setEnd(range: TimeRange, end: 0 | 1, t: number, limits: RangeLimits): TimeRange {
84
+ const { duration, min, max } = limitsOf(limits)
85
+ const [a, b] = range
86
+ if (end === 0) {
87
+ const start = clamp(t, 0, duration - min)
88
+ return [round(start), round(clamp(b, start + min, Math.min(duration, start + max)))]
89
+ }
90
+ const stop = clamp(t, min, duration)
91
+ return [round(clamp(a, Math.max(0, stop - max), stop - min)), round(stop)]
92
+ }
93
+
94
+ export interface TimeView {
95
+ start: number
96
+ end: number
97
+ }
98
+
99
+ /** The view zoomed by `factor` (above 1 zooms in) around `anchor`, which stays where it is on screen. */
100
+ export function zoomView(view: TimeView, anchor: number, factor: number, duration: number, minSpan: number): TimeView {
101
+ const span = view.end - view.start
102
+ // No wider than the video: the shared zoom only floors the width, so cap the factor here.
103
+ const next = zoomSpan(view.start, view.end, anchor, Math.min(1 / factor, duration / (span || 1)), Math.min(minSpan, duration))
104
+ return shiftView(next, 0, duration)
105
+ }
106
+
107
+ /** The view moved by `delta` seconds, held inside the video. */
108
+ export function shiftView(view: TimeView, delta: number, duration: number): TimeView {
109
+ const span = Math.min(view.end - view.start, duration)
110
+ return shiftSpan(view.start, view.start + span, delta, 0, duration)
111
+ }
112
+
113
+ /** The view moved just enough to show `t`, with `margin` of its span to spare. */
114
+ export function revealTime(view: TimeView, t: number, duration: number, margin = 0.1): TimeView {
115
+ const span = view.end - view.start
116
+ const pad = span * margin
117
+ if (t < view.start + pad) return shiftView(view, t - pad - view.start, duration)
118
+ if (t > view.end - pad) return shiftView(view, t + pad - view.end, duration)
119
+ return view
120
+ }
121
+
122
+ const TICK_STEPS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200]
123
+
124
+ /** Seconds between ruler marks, so that marks stand at least `minPx` apart. */
125
+ export function tickStep(span: number, width: number, minPx = 72): number {
126
+ if (span <= 0 || width <= 0) return 1
127
+ const perPx = span / width
128
+ return TICK_STEPS.find((step) => step / perPx >= minPx) ?? TICK_STEPS[TICK_STEPS.length - 1]!
129
+ }
130
+
131
+ /** The ruler's marks within the view. */
132
+ export function ticks(view: TimeView, step: number): number[] {
133
+ const out: number[] = []
134
+ const first = Math.ceil(view.start / step - 1e-9) * step
135
+ for (let t = first; t <= view.end + 1e-9 && out.length < 500; t += step) out.push(round(t))
136
+ return out
137
+ }
138
+
139
+ /**
140
+ * Where the picture sits in a box that shows it whole (object-fit: contain):
141
+ * the region is drawn on the picture, not on the black bars around it.
142
+ */
143
+ export function containRect(boxWidth: number, boxHeight: number, videoWidth: number, videoHeight: number) {
144
+ if (!videoWidth || !videoHeight || !boxWidth || !boxHeight) return { x: 0, y: 0, width: boxWidth, height: boxHeight }
145
+ const scale = Math.min(boxWidth / videoWidth, boxHeight / videoHeight)
146
+ const width = videoWidth * scale
147
+ const height = videoHeight * scale
148
+ return { x: (boxWidth - width) / 2, y: (boxHeight - height) / 2, width, height }
149
+ }
150
+
151
+ const fix = (n: number) => Math.round(n * 10000) / 10000
152
+
153
+ /** A region held inside the frame and no smaller than `minSize` a side. */
154
+ export function clampRegion(r: FrameRegion, minSize = 0.02): FrameRegion {
155
+ const w = clamp(r.w, minSize, 1)
156
+ const h = clamp(r.h, minSize, 1)
157
+ return { x: fix(clamp(r.x, 0, 1 - w)), y: fix(clamp(r.y, 0, 1 - h)), w: fix(w), h: fix(h) }
158
+ }
159
+
160
+ /** The region spanned by two corners, in either order, cut to the frame. */
161
+ export function regionBetween(ax: number, ay: number, bx: number, by: number): FrameRegion {
162
+ const x0 = clamp(Math.min(ax, bx), 0, 1)
163
+ const y0 = clamp(Math.min(ay, by), 0, 1)
164
+ const x1 = clamp(Math.max(ax, bx), 0, 1)
165
+ const y1 = clamp(Math.max(ay, by), 0, 1)
166
+ return { x: fix(x0), y: fix(y0), w: fix(x1 - x0), h: fix(y1 - y0) }
167
+ }
168
+
169
+ export type RegionHandle = "move" | "nw" | "ne" | "sw" | "se"
170
+
171
+ /** The region after one of its handles moved by (dx, dy), fractions of the frame. */
172
+ export function adjustRegion(r: FrameRegion, handle: RegionHandle, dx: number, dy: number, minSize = 0.02): FrameRegion {
173
+ if (handle === "move") return clampRegion({ ...r, x: r.x + dx, y: r.y + dy }, minSize)
174
+ let x0 = r.x
175
+ let y0 = r.y
176
+ let x1 = r.x + r.w
177
+ let y1 = r.y + r.h
178
+ if (handle === "nw" || handle === "sw") x0 = clamp(x0 + dx, 0, x1 - minSize)
179
+ else x1 = clamp(x1 + dx, x0 + minSize, 1)
180
+ if (handle === "nw" || handle === "ne") y0 = clamp(y0 + dy, 0, y1 - minSize)
181
+ else y1 = clamp(y1 + dy, y0 + minSize, 1)
182
+ return { x: fix(x0), y: fix(y0), w: fix(x1 - x0), h: fix(y1 - y0) }
183
+ }
184
+
185
+ /** Why the editor has no video to show; see `VideoErrorCause`. */
186
+ export type { VideoErrorCause as VideoTrimErrorCause } from "../composables/useVideoMedia"
@@ -0,0 +1,21 @@
1
+ /*
2
+ * A window onto a line (a chart's points, a video's seconds): zoomed around a
3
+ * point that stays put, or slid along without changing its width. Shared by
4
+ * LpChart's window and LpVideoTrim's timeline, which differ only in what they
5
+ * round to and how they hold the result inside their bounds.
6
+ */
7
+
8
+ /** The window zoomed by `factor` (below 1 is in) around `anchor`, a position on the line; no wider than it was asked, no narrower than `minSpan`. */
9
+ export function zoomSpan(start: number, end: number, anchor: number, factor: number, minSpan: number): { start: number; end: number } {
10
+ const span = end - start
11
+ const next = Math.max(minSpan, span * factor)
12
+ const from = anchor - (anchor - start) * (next / (span || 1))
13
+ return { start: from, end: from + next }
14
+ }
15
+
16
+ /** The window moved by `delta`, its width kept, stopped at `lo` and `hi`. */
17
+ export function shiftSpan(start: number, end: number, delta: number, lo: number, hi: number): { start: number; end: number } {
18
+ const span = end - start
19
+ const from = Math.max(lo, Math.min(hi - span, start + delta))
20
+ return { start: from, end: from + span }
21
+ }
@@ -0,0 +1,411 @@
1
+ import { onBeforeUnmount, ref, watch, type Ref } from "vue"
2
+ import { SLOWEST_RATE } from "../components/videoBackdrop"
3
+ import { nodeWindow } from "./realm"
4
+ import { usePersisted } from "./usePersisted"
5
+ import { usePrefersReducedMotion } from "./useReducedMotion"
6
+ import { useTween } from "./useTween"
7
+
8
+ /*
9
+ * The state of a <video> as a component that draws its own controls needs it,
10
+ * shared by LpVideoPlayer and LpVideoTrim so both know the same things about
11
+ * a video the same way:
12
+ *
13
+ * - One seek at a time, the latest wins: a drag asks for a position on every
14
+ * pointer move, and WebKitGTK's pipeline stalls on a seek started inside
15
+ * another.
16
+ * - A MediaRecorder WebM carries no duration until its end has been seen: it
17
+ * is found by seeking far past it once.
18
+ * - Chromium answers a 404, or a login page served in place of an expired
19
+ * session's video, with "format not supported", the same code as a real
20
+ * codec problem; one byte of the address tells the two apart.
21
+ * - The playhead follows the picture every frame while playing, not the four
22
+ * `timeupdate`s a second.
23
+ *
24
+ * Bind `listeners` on the element with `v-on`.
25
+ */
26
+
27
+ /**
28
+ * Why there is no video to show:
29
+ * - "load": the address did not answer (network, 404, an expired session);
30
+ * - "decode": the file arrived but is damaged;
31
+ * - "unsupported": a format or codec this browser cannot play (H.265 in
32
+ * Chromium without hardware support, ProRes anywhere).
33
+ */
34
+ export type VideoErrorCause = "load" | "decode" | "unsupported"
35
+
36
+ export function mediaErrorCause(error: MediaError | null): VideoErrorCause {
37
+ if (error?.code === 3) return "decode"
38
+ if (error?.code === 4) return "unsupported"
39
+ return "load"
40
+ }
41
+
42
+ export interface UseVideoMediaOptions {
43
+ src: () => string
44
+ /** Every frame while playing, after `time` is updated. */
45
+ onFrame?: () => void
46
+ onError?: (cause: VideoErrorCause) => void
47
+ /** Ease starts and stops (true by default); false cuts in and out at once. */
48
+ ease?: () => boolean
49
+ /**
50
+ * Keep volume, mute and speed between videos on this device, shared by every
51
+ * player and editor of the kit: someone who turned the sound down turned it
52
+ * down, whichever component the next video is in. False forgets.
53
+ */
54
+ remember?: () => boolean
55
+ }
56
+
57
+ interface Sound {
58
+ volume: number
59
+ muted: boolean
60
+ rate: number
61
+ }
62
+ /*
63
+ * A kept record is used only when it is one a person could have chosen: a
64
+ * speed outside what the menus offer, or a volume outside 0..1, is a record
65
+ * written by an easing caught mid-way (a sixteenth speed and silence, once),
66
+ * and restoring it would start every video stuck.
67
+ */
68
+ const isSound = (v: unknown): v is Sound => {
69
+ if (!v || typeof v !== "object") return false
70
+ const { volume, muted, rate } = v as Sound
71
+ return typeof muted === "boolean" && typeof volume === "number" && volume >= 0 && volume <= 1 && typeof rate === "number" && rate >= 0.25 && rate <= 4
72
+ }
73
+
74
+ export function useVideoMedia(el: Ref<HTMLVideoElement | null>, options: UseVideoMediaOptions) {
75
+ const duration = ref(0)
76
+ const time = ref(0)
77
+ const playing = ref(false)
78
+ /** Playback stalled for data. */
79
+ const waiting = ref(false)
80
+ const ended = ref(false)
81
+ const muted = ref(false)
82
+ const volume = ref(1)
83
+ const rate = ref(1)
84
+ /** Buffered stretches as fractions of the duration. */
85
+ const buffered = ref<[number, number][]>([])
86
+ const failure = ref<VideoErrorCause | null>(null)
87
+ const size = ref({ width: 0, height: 0 })
88
+
89
+ const sound = usePersisted<Sound>(() => (options.remember?.() ?? true) && "lp-video:sound", { volume: 1, muted: false, rate: 1 }, { validate: isSound })
90
+ /* Applied once per address, when it first knows its duration; saved only after, so a fresh element's defaults never overwrite what was kept. */
91
+ let applied = ""
92
+
93
+ let seekBusy = false
94
+ let seekTarget: number | null = null
95
+ let probingDuration = false
96
+ let frame = 0
97
+
98
+ function seek(t: number) {
99
+ const video = el.value
100
+ const target = Math.min(Math.max(0, t), duration.value || t)
101
+ time.value = target
102
+ ended.value = false
103
+ if (!video) return
104
+ if (seekBusy) {
105
+ seekTarget = target
106
+ return
107
+ }
108
+ seekBusy = true
109
+ video.currentTime = target
110
+ }
111
+
112
+ /*
113
+ * `currentTime` moves in steps, one per decoded frame: at a few frames a
114
+ * second (a screen recording, a slideshow) a bar or a playhead driven by it
115
+ * jumps along. Between steps the position runs on the clock from the last
116
+ * one, at the playback rate; it never goes back (a step that lands a hair
117
+ * behind the estimate is held), and never runs more than a moment ahead of
118
+ * the video, so a stall shows as a stop, not as a bar that keeps going.
119
+ */
120
+ const AHEAD_LIMIT = 0.25
121
+ let lastMedia = -1
122
+ let lastAt = 0
123
+ function smoothTime(video: HTMLVideoElement, now: number): number {
124
+ const t = video.currentTime
125
+ if (t !== lastMedia) {
126
+ lastMedia = t
127
+ lastAt = now
128
+ }
129
+ const estimate = Math.min(lastMedia + ((now - lastAt) / 1000) * video.playbackRate, lastMedia + AHEAD_LIMIT, duration.value || Infinity)
130
+ return Math.max(estimate, Math.min(time.value, lastMedia + AHEAD_LIMIT))
131
+ }
132
+
133
+ function tick(now: number) {
134
+ frame = 0
135
+ const video = el.value
136
+ if (!video || !playing.value) return
137
+ if (!seekBusy) time.value = smoothTime(video, now)
138
+ options.onFrame?.()
139
+ if (playing.value) frame = nodeWindow(video).requestAnimationFrame(tick)
140
+ }
141
+ function stopTicking() {
142
+ if (frame && el.value) nodeWindow(el.value).cancelAnimationFrame(frame)
143
+ frame = 0
144
+ }
145
+
146
+ function readBuffered(video: HTMLVideoElement) {
147
+ const d = duration.value
148
+ if (!d) return
149
+ const out: [number, number][] = []
150
+ for (let i = 0; i < video.buffered.length; i++) out.push([video.buffered.start(i) / d, video.buffered.end(i) / d])
151
+ buffered.value = out
152
+ }
153
+
154
+ async function probe(src: string): Promise<VideoErrorCause> {
155
+ try {
156
+ const res = await fetch(src, { headers: { Range: "bytes=0-0" }, credentials: "same-origin" })
157
+ void res.body?.cancel()
158
+ const type = res.headers.get("content-type") ?? ""
159
+ return res.ok && !/^text\/|json|xml/.test(type) ? "unsupported" : "load"
160
+ } catch {
161
+ // Another origin without CORS: there is no telling, so the browser's word stands.
162
+ return "unsupported"
163
+ }
164
+ }
165
+
166
+ const listeners = {
167
+ loadedmetadata() {
168
+ const video = el.value
169
+ if (!video) return
170
+ size.value = { width: video.videoWidth, height: video.videoHeight }
171
+ if (applied !== options.src()) {
172
+ applied = options.src()
173
+ volume.value = sound.value.volume
174
+ rate.value = sound.value.rate
175
+ video.muted = video.muted || sound.value.muted
176
+ muted.value = video.muted
177
+ video.volume = volume.value
178
+ video.playbackRate = rate.value
179
+ }
180
+ if (Number.isFinite(video.duration)) {
181
+ duration.value = video.duration
182
+ return
183
+ }
184
+ probingDuration = true
185
+ video.currentTime = 1e101
186
+ },
187
+ durationchange() {
188
+ const video = el.value
189
+ if (!video || !Number.isFinite(video.duration) || !video.duration) return
190
+ duration.value = video.duration
191
+ if (probingDuration) {
192
+ probingDuration = false
193
+ video.currentTime = 0
194
+ }
195
+ },
196
+ seeked() {
197
+ seekBusy = false
198
+ lastMedia = -1
199
+ if (seekTarget !== null) {
200
+ const next = seekTarget
201
+ seekTarget = null
202
+ seek(next)
203
+ } else if (el.value && !playing.value) time.value = el.value.currentTime
204
+ },
205
+ play() {
206
+ lastMedia = -1
207
+ // Started from outside (media keys, the picture-in-picture window): it wants to play now.
208
+ wantsPlay.value = true
209
+ playing.value = true
210
+ ended.value = false
211
+ if (!frame && el.value) frame = nodeWindow(el.value).requestAnimationFrame(tick)
212
+ },
213
+ pause() {
214
+ wantsPlay.value = false
215
+ playing.value = false
216
+ stopTicking()
217
+ if (el.value && !seekBusy) time.value = el.value.currentTime
218
+ },
219
+ ended() {
220
+ instant = true
221
+ wantsPlay.value = false
222
+ ended.value = true
223
+ },
224
+ waiting() {
225
+ waiting.value = true
226
+ },
227
+ playing() {
228
+ waiting.value = false
229
+ },
230
+ canplay() {
231
+ waiting.value = false
232
+ },
233
+ timeupdate() {
234
+ if (el.value && !playing.value && !seekBusy) time.value = el.value.currentTime
235
+ },
236
+ progress() {
237
+ if (el.value) readBuffered(el.value)
238
+ },
239
+ volumechange() {
240
+ const video = el.value
241
+ if (!video) return
242
+ // Firefox reports itself muted below a quarter speed: during the easing that is the easing's.
243
+ if (!easing()) muted.value = video.muted
244
+ if (easing() || video.volume === written.volume) return
245
+ volume.value = video.volume
246
+ remember()
247
+ },
248
+ ratechange() {
249
+ const video = el.value
250
+ if (!video || easing() || video.playbackRate === written.rate) return
251
+ rate.value = video.playbackRate
252
+ remember()
253
+ },
254
+ async error() {
255
+ const video = el.value
256
+ if (!video?.error) return
257
+ const src = options.src()
258
+ let cause = mediaErrorCause(video.error)
259
+ if (cause === "unsupported") cause = await probe(src)
260
+ if (src !== options.src()) return
261
+ failure.value = cause
262
+ waiting.value = false
263
+ options.onError?.(cause)
264
+ },
265
+ }
266
+
267
+ /*
268
+ * Start and stop are eased, as LpVideoBackdrop's `easePlayback` is: a play
269
+ * gathers speed and sound from a standstill over a moment, a pause slows and
270
+ * quietens to one, instead of the picture and the sound cutting in and out
271
+ * in a single frame (and the sound with a click). `wantsPlay` turns at once,
272
+ * so a button's icon answers the press; the element follows. A pause for a
273
+ * seek or a drag, or at a clip's end, is `instant`: there the exact frame
274
+ * matters more than the glide. Reduced motion has no easing at all.
275
+ */
276
+ const wantsPlay = ref(false)
277
+ let instant = false
278
+ const reducedMotion = usePrefersReducedMotion()
279
+ const throttle = useTween<number>(
280
+ () => (wantsPlay.value ? 1 : 0),
281
+ (from, to, k) => from + (to - from) * k,
282
+ { enabled: () => !instant && !reducedMotion.value && (options.ease?.() ?? true) },
283
+ )
284
+ /*
285
+ * Only around playback: a paused element keeps its real rate and volume.
286
+ * Firefox silences sound below a quarter speed and reports the element as
287
+ * muted meanwhile, so a paused video held at the slowest rate came up muted
288
+ * and would not be unmuted. While the rate eases, what the element reports
289
+ * about its sound is the easing's, not a choice, and is not taken as one.
290
+ */
291
+ /* What the easing itself last wrote: the element's later report of the same value is an echo, not a choice. */
292
+ const written = { rate: Number.NaN, volume: Number.NaN }
293
+ function applyThrottle(video: HTMLVideoElement, k: number) {
294
+ written.rate = video.playbackRate = k >= 1 ? rate.value : Math.max(SLOWEST_RATE, rate.value * k)
295
+ written.volume = video.volume = k >= 1 ? volume.value : volume.value * k
296
+ }
297
+ watch(throttle, (k) => {
298
+ const video = el.value
299
+ if (!video) return
300
+ if (k === 0 && !wantsPlay.value) {
301
+ if (!video.paused) video.pause()
302
+ applyThrottle(video, 1)
303
+ instant = false
304
+ return
305
+ }
306
+ applyThrottle(video, k)
307
+ if (k === 1) instant = false
308
+ })
309
+ /*
310
+ * Whether the element's rate and volume are the easing's rather than the
311
+ * chosen ones: from the press of play until full speed, and from the press
312
+ * of pause until the stop. The element reports a change a moment after it
313
+ * is made, so the bounds are the intent, not the throttle alone: a start
314
+ * reported while the throttle still read 0 was taken for a choice of a
315
+ * sixteenth speed and silence.
316
+ */
317
+ const easing = () => (wantsPlay.value ? throttle.value < 1 : throttle.value > 0)
318
+
319
+ function remember() {
320
+ if (applied) sound.value = { volume: volume.value, muted: muted.value, rate: rate.value }
321
+ }
322
+
323
+ /** Play, quietly: a browser that refuses (autoplay with sound) just leaves it paused. */
324
+ function play() {
325
+ const video = el.value
326
+ if (!video || failure.value) return
327
+ if (ended.value) seek(0)
328
+ wantsPlay.value = true
329
+ // From a standstill the first frame already plays slow and quiet, not at full speed for one frame.
330
+ if (!instant && !reducedMotion.value && (options.ease?.() ?? true) && throttle.value === 0) applyThrottle(video, 0)
331
+ void video.play().catch(() => {
332
+ wantsPlay.value = false
333
+ })
334
+ }
335
+ /** Pause, easing to a stop unless `instant` (a seek, a drag, a clip's end, where the frame must be exact). */
336
+ function pause(options: { instant?: boolean } = {}) {
337
+ if (options.instant) instant = true
338
+ wantsPlay.value = false
339
+ }
340
+ function toggle() {
341
+ if (wantsPlay.value) pause()
342
+ else play()
343
+ }
344
+ function setMuted(value: boolean) {
345
+ if (el.value) el.value.muted = value
346
+ muted.value = value
347
+ remember()
348
+ }
349
+ function setVolume(value: number) {
350
+ const v = Math.min(1, Math.max(0, value))
351
+ volume.value = v
352
+ if (el.value) {
353
+ el.value.volume = easing() ? v * throttle.value : v
354
+ // Turning the sound up is asking to hear it.
355
+ if (v > 0 && el.value.muted) el.value.muted = false
356
+ }
357
+ // Said now, not on the element's event: a slider bound to "muted ? 0 : volume" would spring back to 0 meanwhile.
358
+ if (v > 0) muted.value = false
359
+ remember()
360
+ }
361
+ function setRate(value: number) {
362
+ rate.value = value
363
+ if (el.value) el.value.playbackRate = easing() ? Math.max(SLOWEST_RATE, value * throttle.value) : value
364
+ remember()
365
+ }
366
+
367
+ /**
368
+ * The frame on screen as a PNG, for "copy frame". Null when there is none
369
+ * yet, or when the video came from another origin without CORS and the
370
+ * browser will not let it be read back.
371
+ */
372
+ function captureFrame(): Promise<Blob | null> {
373
+ const video = el.value
374
+ if (!video?.videoWidth) return Promise.resolve(null)
375
+ const canvas = video.ownerDocument.createElement("canvas")
376
+ canvas.width = video.videoWidth
377
+ canvas.height = video.videoHeight
378
+ try {
379
+ canvas.getContext("2d")?.drawImage(video, 0, 0)
380
+ return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), "image/png"))
381
+ } catch {
382
+ return Promise.resolve(null)
383
+ }
384
+ }
385
+
386
+ watch(options.src, () => {
387
+ duration.value = 0
388
+ time.value = 0
389
+ failure.value = null
390
+ buffered.value = []
391
+ ended.value = false
392
+ waiting.value = false
393
+ seekBusy = false
394
+ seekTarget = null
395
+ probingDuration = false
396
+ })
397
+
398
+ onBeforeUnmount(() => {
399
+ stopTicking()
400
+ const video = el.value
401
+ if (!video) return
402
+ // Stop the stream from fetching on: an element that is merely detached keeps its connection.
403
+ video.pause()
404
+ video.removeAttribute("src")
405
+ video.load()
406
+ })
407
+
408
+ return { duration, time, playing, wantsPlay, waiting, ended, muted, volume, rate, buffered, failure, size, listeners, seek, play, pause, toggle, setMuted, setVolume, setRate, captureFrame }
409
+ }
410
+
411
+ export type VideoMedia = ReturnType<typeof useVideoMedia>
package/src/index.ts CHANGED
@@ -156,7 +156,7 @@ export { default as LpInput } from "./components/LpInput.vue"
156
156
  export { default as LpLightbox } from "./components/LpLightbox.vue"
157
157
  export type { LightboxLabels } from "./components/LpLightbox.vue"
158
158
  export type { LightboxItem } from "./components/lightbox"
159
- export { fileNameOf } from "./components/lightbox"
159
+ export { fileNameOf, isVideoItem } from "./components/lightbox"
160
160
  export { default as LpLink } from "./components/LpLink.vue"
161
161
  export { default as LpLogViewer } from "./components/LpLogViewer.vue"
162
162
  export { default as LpLogInspector } from "./components/LpLogInspector.vue"
@@ -226,6 +226,12 @@ export { default as LpRangeSlider } from "./components/LpRangeSlider.vue"
226
226
  export { default as LpVideoBackdrop } from "./components/LpVideoBackdrop.vue"
227
227
  export { formatClipTime, isNativeLoop, planLoop, seamProgress, seamReached } from "./components/videoBackdrop"
228
228
  export type { LoopPlan } from "./components/videoBackdrop"
229
+ export { default as LpVideoTrim } from "./components/LpVideoTrim.vue"
230
+ export { default as LpVideoPlayer } from "./components/LpVideoPlayer.vue"
231
+ export { useVideoMedia } from "./composables/useVideoMedia"
232
+ export type { VideoErrorCause, VideoMedia } from "./composables/useVideoMedia"
233
+ export { dragEnd, normalizeRange as normalizeTrimRange, parseClipTime, setEnd } from "./components/videoTrim"
234
+ export type { FrameRegion, TimeRange, VideoTrimErrorCause } from "./components/videoTrim"
229
235
  export { default as LpSparkline } from "./components/LpSparkline.vue"
230
236
  export { default as LpStat } from "./components/LpStat.vue"
231
237
  export { default as LpStepper } from "./components/LpStepper.vue"