@giddaa-housing/ui 1.0.0 → 1.2.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 (331) hide show
  1. package/css/generated/shared.css +1 -1
  2. package/css/giddaa.css +12 -2
  3. package/dist/accordion.d.ts +9 -10
  4. package/dist/accordion.js +41 -87
  5. package/dist/activity-timeline.d.ts +12 -16
  6. package/dist/activity-timeline.js +83 -198
  7. package/dist/alert.d.ts +8 -9
  8. package/dist/alert.js +53 -111
  9. package/dist/auth-layout.d.ts +4 -4
  10. package/dist/auth-layout.js +143 -294
  11. package/dist/avatar.d.ts +6 -6
  12. package/dist/avatar.js +49 -19
  13. package/dist/badge.d.ts +12 -14
  14. package/dist/badge.js +71 -11
  15. package/dist/bar-chart.d.ts +65 -67
  16. package/dist/bar-chart.js +232 -207
  17. package/dist/breadcrumb.d.ts +5 -5
  18. package/dist/breadcrumb.js +56 -118
  19. package/dist/bullet-chart.d.ts +41 -37
  20. package/dist/bullet-chart.js +125 -148
  21. package/dist/button-BZIeyUw6.d.ts +17 -0
  22. package/dist/button-group.d.ts +13 -16
  23. package/dist/button-group.js +39 -88
  24. package/dist/button.d.ts +2 -19
  25. package/dist/button.js +94 -13
  26. package/dist/calendar.d.ts +6 -6
  27. package/dist/calendar.js +138 -17
  28. package/dist/card.d.ts +14 -16
  29. package/dist/card.js +59 -145
  30. package/dist/carousel.d.ts +34 -35
  31. package/dist/carousel.js +335 -20
  32. package/dist/chart.d.ts +90 -63
  33. package/dist/chart.js +573 -28
  34. package/dist/checkbox.d.ts +10 -12
  35. package/dist/checkbox.js +82 -47
  36. package/dist/cn-BI_4DMBf.js +23 -0
  37. package/dist/collapsible.d.ts +7 -8
  38. package/dist/collapsible.js +22 -47
  39. package/dist/combobox.d.ts +10 -10
  40. package/dist/combobox.js +236 -44
  41. package/dist/cta.d.ts +12 -14
  42. package/dist/cta.js +175 -341
  43. package/dist/currency-selector.d.ts +31 -32
  44. package/dist/currency-selector.js +106 -184
  45. package/dist/data-table.d.ts +168 -0
  46. package/dist/data-table.js +676 -0
  47. package/dist/date-picker.d.ts +22 -23
  48. package/dist/date-picker.js +119 -192
  49. package/dist/details-card.d.ts +7 -7
  50. package/dist/details-card.js +104 -253
  51. package/dist/dialog.d.ts +13 -14
  52. package/dist/dialog.js +116 -209
  53. package/dist/dropdown-menu.d.ts +13 -13
  54. package/dist/dropdown-menu.js +180 -37
  55. package/dist/dropzone.d.ts +55 -55
  56. package/dist/dropzone.js +327 -439
  57. package/dist/editorial-card.d.ts +23 -25
  58. package/dist/editorial-card.js +129 -296
  59. package/dist/empty-state.d.ts +8 -9
  60. package/dist/empty-state.js +48 -105
  61. package/dist/field.d.ts +23 -25
  62. package/dist/field.js +110 -221
  63. package/dist/filter.d.ts +118 -0
  64. package/dist/filter.js +775 -0
  65. package/dist/footer.d.ts +8 -15
  66. package/dist/footer.js +197 -400
  67. package/dist/funnel-chart.d.ts +49 -48
  68. package/dist/funnel-chart.js +170 -189
  69. package/dist/gallery-indicator.d.ts +11 -12
  70. package/dist/gallery-indicator.js +75 -12
  71. package/dist/heatmap-chart.d.ts +47 -50
  72. package/dist/heatmap-chart.js +186 -237
  73. package/dist/infotip.d.ts +8 -9
  74. package/dist/infotip.js +33 -59
  75. package/dist/input-CfoEJ8A6.d.ts +12 -0
  76. package/dist/input-group.d.ts +16 -20
  77. package/dist/input-group.js +99 -23
  78. package/dist/input-size-context-C-SFx9sx.d.ts +10 -0
  79. package/dist/input-size-context.d.ts +2 -11
  80. package/dist/input-size-context.js +17 -9
  81. package/dist/input.d.ts +2 -13
  82. package/dist/input.js +27 -11
  83. package/dist/item.d.ts +7 -8
  84. package/dist/item.js +22 -59
  85. package/dist/kpi-card.d.ts +18 -20
  86. package/dist/kpi-card.js +84 -192
  87. package/dist/label.d.ts +6 -6
  88. package/dist/label.js +40 -8
  89. package/dist/line-chart.d.ts +200 -129
  90. package/dist/line-chart.js +330 -105
  91. package/dist/list-item.d.ts +22 -0
  92. package/dist/list-item.js +84 -0
  93. package/dist/logo.d.ts +4 -4
  94. package/dist/logo.js +92 -9
  95. package/dist/map-indicator.d.ts +7 -7
  96. package/dist/map-indicator.js +80 -106
  97. package/dist/match.d.ts +10 -11
  98. package/dist/match.js +62 -107
  99. package/dist/media-gallery-hero.d.ts +40 -42
  100. package/dist/media-gallery-hero.js +182 -343
  101. package/dist/media-player.d.ts +17 -17
  102. package/dist/media-player.js +873 -1348
  103. package/dist/multi-step-form.d.ts +56 -0
  104. package/dist/multi-step-form.js +342 -0
  105. package/dist/number-input.d.ts +34 -34
  106. package/dist/number-input.js +64 -117
  107. package/dist/page-header.d.ts +10 -11
  108. package/dist/page-header.js +34 -79
  109. package/dist/pagination.d.ts +17 -21
  110. package/dist/pagination.js +132 -207
  111. package/dist/password-input.d.ts +5 -13
  112. package/dist/password-input.js +31 -55
  113. package/dist/phone-input.d.ts +19 -20
  114. package/dist/phone-input.js +226 -344
  115. package/dist/photo-gallery.d.ts +19 -19
  116. package/dist/photo-gallery.js +313 -449
  117. package/dist/picker-lzgmsNfG.js +32 -0
  118. package/dist/pie-chart.d.ts +121 -90
  119. package/dist/pie-chart.js +183 -149
  120. package/dist/popover.d.ts +9 -9
  121. package/dist/popover.js +72 -23
  122. package/dist/price-tag.d.ts +26 -27
  123. package/dist/price-tag.js +113 -173
  124. package/dist/progress.d.ts +16 -17
  125. package/dist/progress.js +169 -24
  126. package/dist/property-listing-card.d.ts +14 -17
  127. package/dist/property-listing-card.js +102 -220
  128. package/dist/purchase-option-card.d.ts +79 -0
  129. package/dist/purchase-option-card.js +176 -0
  130. package/dist/radar-chart.d.ts +29 -31
  131. package/dist/radar-chart.js +162 -137
  132. package/dist/radio-group.d.ts +11 -13
  133. package/dist/radio-group.js +46 -60
  134. package/dist/review-block.d.ts +14 -16
  135. package/dist/review-block.js +154 -10
  136. package/dist/rich-text-editor.d.ts +30 -14
  137. package/dist/rich-text-editor.js +592 -716
  138. package/dist/scatter-chart.d.ts +45 -47
  139. package/dist/scatter-chart.js +236 -240
  140. package/dist/search-input.d.ts +8 -16
  141. package/dist/search-input.js +57 -109
  142. package/dist/select.d.ts +8 -8
  143. package/dist/select.js +144 -27
  144. package/dist/separator.d.ts +8 -9
  145. package/dist/separator.js +32 -8
  146. package/dist/sheet.d.ts +11 -11
  147. package/dist/sheet.js +103 -24
  148. package/dist/sidebar.d.ts +32 -37
  149. package/dist/sidebar.js +364 -634
  150. package/dist/size-context-D4mYvcg8.d.ts +10 -0
  151. package/dist/size-context.d.ts +2 -11
  152. package/dist/size-context.js +17 -9
  153. package/dist/skeleton.d.ts +4 -5
  154. package/dist/skeleton.js +12 -8
  155. package/dist/slider.d.ts +8 -9
  156. package/dist/slider.js +64 -135
  157. package/dist/sparkline.d.ts +68 -58
  158. package/dist/sparkline.js +315 -251
  159. package/dist/stepper.d.ts +23 -24
  160. package/dist/stepper.js +149 -246
  161. package/dist/styles.css +1004 -116
  162. package/dist/switch.d.ts +10 -12
  163. package/dist/switch.js +48 -70
  164. package/dist/table-card.d.ts +35 -0
  165. package/dist/table-card.js +125 -0
  166. package/dist/table.d.ts +18 -14
  167. package/dist/table.js +117 -154
  168. package/dist/tabs.d.ts +33 -15
  169. package/dist/tabs.js +110 -18
  170. package/dist/tag.d.ts +21 -23
  171. package/dist/tag.js +103 -159
  172. package/dist/testimonial-block.d.ts +18 -19
  173. package/dist/testimonial-block.js +189 -358
  174. package/dist/textarea-IS_D8pXf.d.ts +11 -0
  175. package/dist/textarea.d.ts +2 -12
  176. package/dist/textarea.js +24 -11
  177. package/dist/time-picker.d.ts +11 -11
  178. package/dist/time-picker.js +245 -302
  179. package/dist/toast.d.ts +16 -18
  180. package/dist/toast.js +154 -205
  181. package/dist/tooltip.d.ts +8 -9
  182. package/dist/tooltip.js +37 -12
  183. package/dist/tree-map.d.ts +59 -62
  184. package/dist/tree-map.js +226 -269
  185. package/dist/use-uncontrolled-CLfM1XbB.js +21 -0
  186. package/dist/waterfall-chart.d.ts +94 -97
  187. package/dist/waterfall-chart.js +455 -552
  188. package/package.json +36 -9
  189. package/dist/accordion.js.map +0 -1
  190. package/dist/activity-timeline.js.map +0 -1
  191. package/dist/alert.js.map +0 -1
  192. package/dist/auth-layout.js.map +0 -1
  193. package/dist/avatar.js.map +0 -1
  194. package/dist/badge.js.map +0 -1
  195. package/dist/bar-chart.js.map +0 -1
  196. package/dist/breadcrumb.js.map +0 -1
  197. package/dist/bullet-chart.js.map +0 -1
  198. package/dist/button-group.js.map +0 -1
  199. package/dist/button.js.map +0 -1
  200. package/dist/calendar.js.map +0 -1
  201. package/dist/card.js.map +0 -1
  202. package/dist/carousel.js.map +0 -1
  203. package/dist/chart.js.map +0 -1
  204. package/dist/checkbox.js.map +0 -1
  205. package/dist/chunk-322YCNVL.js +0 -278
  206. package/dist/chunk-322YCNVL.js.map +0 -1
  207. package/dist/chunk-3ACQVT7A.js +0 -25
  208. package/dist/chunk-3ACQVT7A.js.map +0 -1
  209. package/dist/chunk-3T65TLJX.js +0 -334
  210. package/dist/chunk-3T65TLJX.js.map +0 -1
  211. package/dist/chunk-6ZSGQHH2.js +0 -192
  212. package/dist/chunk-6ZSGQHH2.js.map +0 -1
  213. package/dist/chunk-7TBMLFGW.js +0 -41
  214. package/dist/chunk-7TBMLFGW.js.map +0 -1
  215. package/dist/chunk-B4H6RNDH.js +0 -135
  216. package/dist/chunk-B4H6RNDH.js.map +0 -1
  217. package/dist/chunk-BUXCGZ6X.js +0 -219
  218. package/dist/chunk-BUXCGZ6X.js.map +0 -1
  219. package/dist/chunk-CHII2QHH.js +0 -58
  220. package/dist/chunk-CHII2QHH.js.map +0 -1
  221. package/dist/chunk-DMNOXRRW.js +0 -147
  222. package/dist/chunk-DMNOXRRW.js.map +0 -1
  223. package/dist/chunk-EFSOSSIC.js +0 -130
  224. package/dist/chunk-EFSOSSIC.js.map +0 -1
  225. package/dist/chunk-FZDSVMKY.js +0 -363
  226. package/dist/chunk-FZDSVMKY.js.map +0 -1
  227. package/dist/chunk-GJOFIJS3.js +0 -246
  228. package/dist/chunk-GJOFIJS3.js.map +0 -1
  229. package/dist/chunk-HL3ZSYES.js +0 -23
  230. package/dist/chunk-HL3ZSYES.js.map +0 -1
  231. package/dist/chunk-JVLUBAPB.js +0 -129
  232. package/dist/chunk-JVLUBAPB.js.map +0 -1
  233. package/dist/chunk-KDPYSOYZ.js +0 -64
  234. package/dist/chunk-KDPYSOYZ.js.map +0 -1
  235. package/dist/chunk-KSMDANKI.js +0 -43
  236. package/dist/chunk-KSMDANKI.js.map +0 -1
  237. package/dist/chunk-MEKCCOP5.js +0 -46
  238. package/dist/chunk-MEKCCOP5.js.map +0 -1
  239. package/dist/chunk-NOFXVRWL.js +0 -451
  240. package/dist/chunk-NOFXVRWL.js.map +0 -1
  241. package/dist/chunk-PC5MHNVA.js +0 -171
  242. package/dist/chunk-PC5MHNVA.js.map +0 -1
  243. package/dist/chunk-QKAQZ2GU.js +0 -122
  244. package/dist/chunk-QKAQZ2GU.js.map +0 -1
  245. package/dist/chunk-QZHSHSVV.js +0 -26
  246. package/dist/chunk-QZHSHSVV.js.map +0 -1
  247. package/dist/chunk-RBMK26XD.js +0 -19
  248. package/dist/chunk-RBMK26XD.js.map +0 -1
  249. package/dist/chunk-SFNJGF6M.js +0 -262
  250. package/dist/chunk-SFNJGF6M.js.map +0 -1
  251. package/dist/chunk-UJJNLGTM.js +0 -112
  252. package/dist/chunk-UJJNLGTM.js.map +0 -1
  253. package/dist/chunk-VGR4ZWJ3.js +0 -418
  254. package/dist/chunk-VGR4ZWJ3.js.map +0 -1
  255. package/dist/chunk-XG47P6D4.js +0 -99
  256. package/dist/chunk-XG47P6D4.js.map +0 -1
  257. package/dist/chunk-XZZAHNVM.js +0 -21
  258. package/dist/chunk-XZZAHNVM.js.map +0 -1
  259. package/dist/chunk-Y4TTYVNE.js +0 -36
  260. package/dist/chunk-Y4TTYVNE.js.map +0 -1
  261. package/dist/chunk-YDWIHW6Q.js +0 -69
  262. package/dist/chunk-YDWIHW6Q.js.map +0 -1
  263. package/dist/collapsible.js.map +0 -1
  264. package/dist/combobox.js.map +0 -1
  265. package/dist/cta.js.map +0 -1
  266. package/dist/currency-selector.js.map +0 -1
  267. package/dist/date-picker.js.map +0 -1
  268. package/dist/details-card.js.map +0 -1
  269. package/dist/dialog.js.map +0 -1
  270. package/dist/dropdown-menu.js.map +0 -1
  271. package/dist/dropzone.js.map +0 -1
  272. package/dist/editorial-card.js.map +0 -1
  273. package/dist/empty-state.js.map +0 -1
  274. package/dist/field.js.map +0 -1
  275. package/dist/footer.js.map +0 -1
  276. package/dist/funnel-chart.js.map +0 -1
  277. package/dist/gallery-indicator.js.map +0 -1
  278. package/dist/heatmap-chart.js.map +0 -1
  279. package/dist/infotip.js.map +0 -1
  280. package/dist/input-group.js.map +0 -1
  281. package/dist/input-size-context.js.map +0 -1
  282. package/dist/input.js.map +0 -1
  283. package/dist/item.js.map +0 -1
  284. package/dist/kpi-card.js.map +0 -1
  285. package/dist/label.js.map +0 -1
  286. package/dist/line-chart.js.map +0 -1
  287. package/dist/logo.js.map +0 -1
  288. package/dist/map-indicator.js.map +0 -1
  289. package/dist/match.js.map +0 -1
  290. package/dist/media-gallery-hero.js.map +0 -1
  291. package/dist/media-player.js.map +0 -1
  292. package/dist/number-input.js.map +0 -1
  293. package/dist/page-header.js.map +0 -1
  294. package/dist/pagination.js.map +0 -1
  295. package/dist/password-input.js.map +0 -1
  296. package/dist/phone-input.js.map +0 -1
  297. package/dist/photo-gallery.js.map +0 -1
  298. package/dist/pie-chart.js.map +0 -1
  299. package/dist/popover.js.map +0 -1
  300. package/dist/price-tag.js.map +0 -1
  301. package/dist/progress.js.map +0 -1
  302. package/dist/property-listing-card.js.map +0 -1
  303. package/dist/radar-chart.js.map +0 -1
  304. package/dist/radio-group.js.map +0 -1
  305. package/dist/review-block.js.map +0 -1
  306. package/dist/rich-text-editor.js.map +0 -1
  307. package/dist/scatter-chart.js.map +0 -1
  308. package/dist/search-input.js.map +0 -1
  309. package/dist/select.js.map +0 -1
  310. package/dist/separator.js.map +0 -1
  311. package/dist/sheet.js.map +0 -1
  312. package/dist/sidebar.js.map +0 -1
  313. package/dist/size-context.js.map +0 -1
  314. package/dist/skeleton.js.map +0 -1
  315. package/dist/slider.js.map +0 -1
  316. package/dist/sparkline.js.map +0 -1
  317. package/dist/stepper.js.map +0 -1
  318. package/dist/switch.js.map +0 -1
  319. package/dist/tab-list.d.ts +0 -38
  320. package/dist/tab-list.js +0 -123
  321. package/dist/tab-list.js.map +0 -1
  322. package/dist/table.js.map +0 -1
  323. package/dist/tabs.js.map +0 -1
  324. package/dist/tag.js.map +0 -1
  325. package/dist/testimonial-block.js.map +0 -1
  326. package/dist/textarea.js.map +0 -1
  327. package/dist/time-picker.js.map +0 -1
  328. package/dist/toast.js.map +0 -1
  329. package/dist/tooltip.js.map +0 -1
  330. package/dist/tree-map.js.map +0 -1
  331. package/dist/waterfall-chart.js.map +0 -1
@@ -1,1394 +1,919 @@
1
1
  "use client";
2
- import {
3
- Tooltip,
4
- TooltipContent,
5
- TooltipTrigger
6
- } from "./chunk-KDPYSOYZ.js";
7
- import {
8
- DropdownMenu,
9
- DropdownMenuContent,
10
- DropdownMenuRadioGroup,
11
- DropdownMenuRadioItem,
12
- DropdownMenuSub,
13
- DropdownMenuSubContent,
14
- DropdownMenuSubTrigger,
15
- DropdownMenuTrigger
16
- } from "./chunk-3T65TLJX.js";
17
- import "./chunk-HL3ZSYES.js";
18
- import {
19
- cn
20
- } from "./chunk-Y4TTYVNE.js";
21
-
22
- // src/media-player.tsx
23
- import { Slider as SliderPrimitive } from "@base-ui/react/slider";
24
- import {
25
- Captions,
26
- CaptionsOff,
27
- Maximize,
28
- Minimize,
29
- Pause,
30
- PictureInPicture2,
31
- Play,
32
- RefreshCcw,
33
- Repeat2,
34
- RotateCcw,
35
- RotateCw,
36
- Settings,
37
- TriangleAlert,
38
- Volume1,
39
- Volume2,
40
- VolumeX
41
- } from "lucide-react";
42
- import {
43
- MediaActionTypes,
44
- MediaProvider,
45
- useMediaDispatch,
46
- useMediaFullscreenRef,
47
- useMediaRef,
48
- useMediaSelector,
49
- useMediaStore
50
- } from "media-chrome/react/media-store";
2
+ import { t as cn } from "./cn-BI_4DMBf.js";
3
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./dropdown-menu.js";
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
5
+ import { Captions, CaptionsOff, Maximize, Minimize, Pause, PictureInPicture2, Play, RefreshCcw, Repeat2, RotateCcw, RotateCw, Settings, TriangleAlert, Volume1, Volume2, VolumeX } from "lucide-react";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
51
7
  import * as React from "react";
8
+ import { Slider } from "@base-ui/react/slider";
9
+ import { MediaActionTypes, MediaProvider, useMediaDispatch, useMediaFullscreenRef, useMediaRef, useMediaSelector, useMediaStore } from "media-chrome/react/media-store";
52
10
  import * as ReactDOM from "react-dom";
53
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
54
- var DEFAULT_PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
55
- var CONTROLS_HIDE_DELAY = 2500;
56
- var SEEK_PREVIEW_WIDTH = 160;
57
- var MediaPlayerContext = React.createContext({
58
- onMenuOpenChange: () => {
59
- },
60
- portalContainer: null,
61
- rootNode: null
11
+ //#region src/media-player.tsx
12
+ const DEFAULT_PLAYBACK_RATES = [
13
+ .5,
14
+ .75,
15
+ 1,
16
+ 1.25,
17
+ 1.5,
18
+ 1.75,
19
+ 2
20
+ ];
21
+ const CONTROLS_HIDE_DELAY = 2500;
22
+ const SEEK_PREVIEW_WIDTH = 160;
23
+ const MediaPlayerContext = React.createContext({
24
+ onMenuOpenChange: () => {},
25
+ portalContainer: null,
26
+ rootNode: null
62
27
  });
63
28
  function setRef(ref, value) {
64
- if (typeof ref === "function") {
65
- ref(value);
66
- return;
67
- }
68
- if (ref) {
69
- ref.current = value;
70
- }
29
+ if (typeof ref === "function") {
30
+ ref(value);
31
+ return;
32
+ }
33
+ if (ref) ref.current = value;
71
34
  }
72
35
  function formatTime(value) {
73
- if (!Number.isFinite(value)) {
74
- return "0:00";
75
- }
76
- const safeValue = Math.max(0, Math.floor(value ?? 0));
77
- const hours = Math.floor(safeValue / 3600);
78
- const minutes = Math.floor(safeValue % 3600 / 60);
79
- const seconds = safeValue % 60;
80
- if (hours > 0) {
81
- return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
82
- }
83
- return `${minutes}:${String(seconds).padStart(2, "0")}`;
36
+ if (!Number.isFinite(value)) return "0:00";
37
+ const safeValue = Math.max(0, Math.floor(value ?? 0));
38
+ const hours = Math.floor(safeValue / 3600);
39
+ const minutes = Math.floor(safeValue % 3600 / 60);
40
+ const seconds = safeValue % 60;
41
+ if (hours > 0) return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
42
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
84
43
  }
85
44
  function formatRate(value) {
86
- if (!value || value === 1) {
87
- return "1x";
88
- }
89
- return `${Number.parseFloat(value.toFixed(2))}x`;
45
+ if (!value || value === 1) return "1x";
46
+ return `${Number.parseFloat(value.toFixed(2))}x`;
90
47
  }
91
48
  function subtitleTrackId(track) {
92
- return `${track.kind}:${track.language}:${track.label}`;
49
+ return `${track.kind}:${track.language}:${track.label}`;
93
50
  }
94
51
  function subtitleTrackLabel(track) {
95
- return track.label || track.language || "Captions";
52
+ return track.label || track.language || "Captions";
96
53
  }
97
54
  function hasAudioChild(children) {
98
- return React.Children.toArray(children).some((child) => {
99
- if (!React.isValidElement(child)) {
100
- return false;
101
- }
102
- if (child.type === MediaPlayerAudio) {
103
- return true;
104
- }
105
- const props = child.props;
106
- return hasAudioChild(props.children);
107
- });
55
+ return React.Children.toArray(children).some((child) => {
56
+ if (!React.isValidElement(child)) return false;
57
+ if (child.type === MediaPlayerAudio) return true;
58
+ const props = child.props;
59
+ return hasAudioChild(props.children);
60
+ });
108
61
  }
109
62
  function getSeekBounds(seekable, duration) {
110
- const min = seekable?.[0] ?? 0;
111
- const fallbackMax = Number.isFinite(duration) ? duration ?? 0 : 0;
112
- const max = seekable?.[1] ?? fallbackMax;
113
- return {
114
- min,
115
- max: Number.isFinite(max) ? Math.max(min, max) : min
116
- };
63
+ const min = seekable?.[0] ?? 0;
64
+ const fallbackMax = Number.isFinite(duration) ? duration ?? 0 : 0;
65
+ const max = seekable?.[1] ?? fallbackMax;
66
+ return {
67
+ min,
68
+ max: Number.isFinite(max) ? Math.max(min, max) : min
69
+ };
117
70
  }
118
71
  function clamp(value, min, max) {
119
- return Math.min(Math.max(value, min), max);
72
+ return Math.min(Math.max(value, min), max);
120
73
  }
121
74
  function getSeekValueFromPointer(event, min, max) {
122
- const rect = event.currentTarget.getBoundingClientRect();
123
- const percent = clamp((event.clientX - rect.left) / rect.width, 0, 1);
124
- return {
125
- percent: percent * 100,
126
- value: min + percent * (max - min)
127
- };
75
+ const rect = event.currentTarget.getBoundingClientRect();
76
+ const percent = clamp((event.clientX - rect.left) / rect.width, 0, 1);
77
+ return {
78
+ percent: percent * 100,
79
+ value: min + percent * (max - min)
80
+ };
128
81
  }
129
82
  function MediaPlayer({ mediaStore, ...props }) {
130
- const audio = props.audio ?? (hasAudioChild(props.children) ? true : void 0);
131
- return /* @__PURE__ */ jsx(MediaProvider, { mediaStore, children: /* @__PURE__ */ jsx(MediaPlayerRoot, { ...props, audio }) });
83
+ const audio = props.audio ?? (hasAudioChild(props.children) ? true : void 0);
84
+ return /* @__PURE__ */ jsx(MediaProvider, {
85
+ mediaStore,
86
+ children: /* @__PURE__ */ jsx(MediaPlayerRoot, {
87
+ ...props,
88
+ audio
89
+ })
90
+ });
132
91
  }
133
- function MediaPlayerRoot({
134
- audio,
135
- children,
136
- className,
137
- onBlurCapture,
138
- onFocusCapture,
139
- onKeyDown,
140
- onPointerDown,
141
- onPointerLeave,
142
- onPointerMove,
143
- ref,
144
- ...props
145
- }) {
146
- const dispatch = useMediaDispatch();
147
- const store = useMediaStore();
148
- const fullscreenRef = useMediaFullscreenRef();
149
- const paused = useMediaSelector((state) => state.mediaPaused !== false);
150
- const muted = useMediaSelector((state) => state.mediaMuted === true);
151
- const isFullscreen = useMediaSelector(
152
- (state) => state.mediaIsFullscreen === true
153
- );
154
- const [rootNode, setRootNode] = React.useState(null);
155
- const [controlsVisible, setControlsVisible] = React.useState(true);
156
- const [menuOpenCount, setMenuOpenCount] = React.useState(0);
157
- const [hoveringControls, setHoveringControls] = React.useState(false);
158
- const [focusPinned, setFocusPinned] = React.useState(false);
159
- const hideTimerRef = React.useRef(void 0);
160
- const lastActivityRef = React.useRef(0);
161
- const alwaysVisible = Boolean(audio) || paused || menuOpenCount > 0 || hoveringControls || focusPinned;
162
- const alwaysVisibleRef = React.useRef(alwaysVisible);
163
- alwaysVisibleRef.current = alwaysVisible;
164
- const scheduleHide = React.useCallback(function schedule(delay = CONTROLS_HIDE_DELAY) {
165
- window.clearTimeout(hideTimerRef.current);
166
- hideTimerRef.current = window.setTimeout(() => {
167
- if (alwaysVisibleRef.current) {
168
- return;
169
- }
170
- const idleFor = Date.now() - lastActivityRef.current;
171
- if (idleFor < CONTROLS_HIDE_DELAY) {
172
- schedule(CONTROLS_HIDE_DELAY - idleFor);
173
- return;
174
- }
175
- setControlsVisible(false);
176
- }, delay);
177
- }, []);
178
- const wake = React.useCallback(() => {
179
- lastActivityRef.current = Date.now();
180
- setControlsVisible(true);
181
- scheduleHide();
182
- }, [scheduleHide]);
183
- React.useEffect(() => {
184
- if (alwaysVisible) {
185
- window.clearTimeout(hideTimerRef.current);
186
- setControlsVisible(true);
187
- return;
188
- }
189
- scheduleHide();
190
- }, [alwaysVisible, scheduleHide]);
191
- React.useEffect(() => {
192
- return () => window.clearTimeout(hideTimerRef.current);
193
- }, []);
194
- React.useEffect(() => {
195
- if (!rootNode) {
196
- return;
197
- }
198
- const loadChapters = () => {
199
- const media = rootNode.querySelector("video, audio");
200
- for (const track of media?.textTracks ?? []) {
201
- if (track.kind === "chapters" && track.mode === "disabled") {
202
- track.mode = "hidden";
203
- }
204
- }
205
- };
206
- loadChapters();
207
- const textTracks = rootNode.querySelector("video, audio")?.textTracks;
208
- textTracks?.addEventListener("addtrack", loadChapters);
209
- return () => textTracks?.removeEventListener("addtrack", loadChapters);
210
- }, [rootNode]);
211
- const onMenuOpenChange = React.useCallback((open) => {
212
- setMenuOpenCount((count) => open ? count + 1 : Math.max(0, count - 1));
213
- }, []);
214
- const contextValue = React.useMemo(
215
- () => ({
216
- onMenuOpenChange,
217
- portalContainer: isFullscreen ? rootNode : null,
218
- rootNode
219
- }),
220
- [isFullscreen, onMenuOpenChange, rootNode]
221
- );
222
- const fullscreenRefRef = React.useRef(fullscreenRef);
223
- fullscreenRefRef.current = fullscreenRef;
224
- const rootRef = React.useCallback(
225
- (node) => {
226
- fullscreenRefRef.current(node);
227
- setRootNode(node);
228
- setRef(ref, node);
229
- },
230
- [ref]
231
- );
232
- return /* @__PURE__ */ jsx(MediaPlayerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
233
- "div",
234
- {
235
- ref: rootRef,
236
- "aria-label": audio ? "Audio player" : "Video player",
237
- "data-audio": audio ? "" : void 0,
238
- "data-controls": controlsVisible ? "visible" : "hidden",
239
- "data-fullscreen": isFullscreen ? "" : void 0,
240
- "data-slot": "media-player",
241
- role: "region",
242
- tabIndex: 0,
243
- className: cn(
244
- "group/media-player relative isolate block w-full overflow-hidden rounded-2xl bg-fg-primary text-fg-on-brand shadow-2 outline-none",
245
- "data-[controls=hidden]:cursor-none",
246
- "data-[audio]:bg-[image:var(--gradient-accent-forest-to-brass)]",
247
- "data-[fullscreen]:rounded-none data-[fullscreen]:shadow-none",
248
- className
249
- ),
250
- ...props,
251
- onBlurCapture: (event) => {
252
- onBlurCapture?.(event);
253
- if (!event.currentTarget.contains(event.relatedTarget)) {
254
- setFocusPinned(false);
255
- }
256
- },
257
- onFocusCapture: (event) => {
258
- onFocusCapture?.(event);
259
- const keyboardFocus = event.target.matches?.(":focus-visible") ?? false;
260
- setFocusPinned(keyboardFocus);
261
- if (keyboardFocus) {
262
- setControlsVisible(true);
263
- }
264
- },
265
- onKeyDown: (event) => {
266
- onKeyDown?.(event);
267
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) {
268
- return;
269
- }
270
- const target = event.target;
271
- if (target.closest(
272
- "input, textarea, select, [contenteditable], [role='slider'], [role='menu'], [role='menuitem'], [role='menuitemradio']"
273
- )) {
274
- return;
275
- }
276
- const state = store.getState();
277
- const currentTime = state.mediaCurrentTime ?? 0;
278
- const { min, max } = getSeekBounds(
279
- state.mediaSeekable,
280
- state.mediaDuration
281
- );
282
- switch (event.key) {
283
- case " ":
284
- if (target.closest("button")) {
285
- return;
286
- }
287
- event.preventDefault();
288
- dispatch({
289
- type: paused ? MediaActionTypes.MEDIA_PLAY_REQUEST : MediaActionTypes.MEDIA_PAUSE_REQUEST
290
- });
291
- break;
292
- case "k":
293
- case "K":
294
- dispatch({
295
- type: paused ? MediaActionTypes.MEDIA_PLAY_REQUEST : MediaActionTypes.MEDIA_PAUSE_REQUEST
296
- });
297
- break;
298
- case "m":
299
- case "M":
300
- dispatch({
301
- type: muted ? MediaActionTypes.MEDIA_UNMUTE_REQUEST : MediaActionTypes.MEDIA_MUTE_REQUEST
302
- });
303
- break;
304
- case "f":
305
- case "F":
306
- if (audio) {
307
- return;
308
- }
309
- dispatch({
310
- type: isFullscreen ? MediaActionTypes.MEDIA_EXIT_FULLSCREEN_REQUEST : MediaActionTypes.MEDIA_ENTER_FULLSCREEN_REQUEST
311
- });
312
- break;
313
- case "c":
314
- case "C":
315
- dispatch({
316
- type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST
317
- });
318
- break;
319
- case "ArrowLeft":
320
- event.preventDefault();
321
- dispatch({
322
- type: MediaActionTypes.MEDIA_SEEK_REQUEST,
323
- detail: clamp(currentTime - 5, min, max)
324
- });
325
- break;
326
- case "ArrowRight":
327
- event.preventDefault();
328
- dispatch({
329
- type: MediaActionTypes.MEDIA_SEEK_REQUEST,
330
- detail: clamp(currentTime + 5, min, max)
331
- });
332
- break;
333
- default:
334
- return;
335
- }
336
- wake();
337
- },
338
- onPointerDown: (event) => {
339
- onPointerDown?.(event);
340
- wake();
341
- },
342
- onPointerLeave: (event) => {
343
- onPointerLeave?.(event);
344
- setHoveringControls(false);
345
- if (!alwaysVisibleRef.current) {
346
- window.clearTimeout(hideTimerRef.current);
347
- setControlsVisible(false);
348
- }
349
- },
350
- onPointerMove: (event) => {
351
- onPointerMove?.(event);
352
- wake();
353
- const controls = event.currentTarget.querySelector(
354
- "[data-slot='media-player-controls']"
355
- );
356
- const rect = controls?.getBoundingClientRect();
357
- setHoveringControls(
358
- !!rect && event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom
359
- );
360
- },
361
- children
362
- }
363
- ) });
92
+ function MediaPlayerRoot({ audio, children, className, onBlurCapture, onFocusCapture, onKeyDown, onPointerDown, onPointerLeave, onPointerMove, ref, ...props }) {
93
+ const dispatch = useMediaDispatch();
94
+ const store = useMediaStore();
95
+ const fullscreenRef = useMediaFullscreenRef();
96
+ const paused = useMediaSelector((state) => state.mediaPaused !== false);
97
+ const muted = useMediaSelector((state) => state.mediaMuted === true);
98
+ const isFullscreen = useMediaSelector((state) => state.mediaIsFullscreen === true);
99
+ const [rootNode, setRootNode] = React.useState(null);
100
+ const [controlsVisible, setControlsVisible] = React.useState(true);
101
+ const [menuOpenCount, setMenuOpenCount] = React.useState(0);
102
+ const [hoveringControls, setHoveringControls] = React.useState(false);
103
+ const [focusPinned, setFocusPinned] = React.useState(false);
104
+ const hideTimerRef = React.useRef(void 0);
105
+ const lastActivityRef = React.useRef(0);
106
+ const alwaysVisible = Boolean(audio) || paused || menuOpenCount > 0 || hoveringControls || focusPinned;
107
+ const alwaysVisibleRef = React.useRef(alwaysVisible);
108
+ alwaysVisibleRef.current = alwaysVisible;
109
+ const scheduleHide = React.useCallback(function schedule(delay = CONTROLS_HIDE_DELAY) {
110
+ window.clearTimeout(hideTimerRef.current);
111
+ hideTimerRef.current = window.setTimeout(() => {
112
+ if (alwaysVisibleRef.current) return;
113
+ const idleFor = Date.now() - lastActivityRef.current;
114
+ if (idleFor < CONTROLS_HIDE_DELAY) {
115
+ schedule(CONTROLS_HIDE_DELAY - idleFor);
116
+ return;
117
+ }
118
+ setControlsVisible(false);
119
+ }, delay);
120
+ }, []);
121
+ const wake = React.useCallback(() => {
122
+ lastActivityRef.current = Date.now();
123
+ setControlsVisible(true);
124
+ scheduleHide();
125
+ }, [scheduleHide]);
126
+ React.useEffect(() => {
127
+ if (alwaysVisible) {
128
+ window.clearTimeout(hideTimerRef.current);
129
+ setControlsVisible(true);
130
+ return;
131
+ }
132
+ scheduleHide();
133
+ }, [alwaysVisible, scheduleHide]);
134
+ React.useEffect(() => {
135
+ return () => window.clearTimeout(hideTimerRef.current);
136
+ }, []);
137
+ React.useEffect(() => {
138
+ if (!rootNode) return;
139
+ const loadChapters = () => {
140
+ const media = rootNode.querySelector("video, audio");
141
+ for (const track of media?.textTracks ?? []) if (track.kind === "chapters" && track.mode === "disabled") track.mode = "hidden";
142
+ };
143
+ loadChapters();
144
+ const textTracks = rootNode.querySelector("video, audio")?.textTracks;
145
+ textTracks?.addEventListener("addtrack", loadChapters);
146
+ return () => textTracks?.removeEventListener("addtrack", loadChapters);
147
+ }, [rootNode]);
148
+ const onMenuOpenChange = React.useCallback((open) => {
149
+ setMenuOpenCount((count) => open ? count + 1 : Math.max(0, count - 1));
150
+ }, []);
151
+ const contextValue = React.useMemo(() => ({
152
+ onMenuOpenChange,
153
+ portalContainer: isFullscreen ? rootNode : null,
154
+ rootNode
155
+ }), [
156
+ isFullscreen,
157
+ onMenuOpenChange,
158
+ rootNode
159
+ ]);
160
+ const fullscreenRefRef = React.useRef(fullscreenRef);
161
+ fullscreenRefRef.current = fullscreenRef;
162
+ const rootRef = React.useCallback((node) => {
163
+ fullscreenRefRef.current(node);
164
+ setRootNode(node);
165
+ setRef(ref, node);
166
+ }, [ref]);
167
+ return /* @__PURE__ */ jsx(MediaPlayerContext.Provider, {
168
+ value: contextValue,
169
+ children: /* @__PURE__ */ jsx("div", {
170
+ ref: rootRef,
171
+ "aria-label": audio ? "Audio player" : "Video player",
172
+ "data-audio": audio ? "" : void 0,
173
+ "data-controls": controlsVisible ? "visible" : "hidden",
174
+ "data-fullscreen": isFullscreen ? "" : void 0,
175
+ "data-slot": "media-player",
176
+ role: "region",
177
+ tabIndex: 0,
178
+ className: cn("group/media-player relative isolate block w-full overflow-hidden rounded-2xl bg-fg-primary text-fg-on-brand shadow-2 outline-none", "data-[controls=hidden]:cursor-none", "data-[audio]:bg-[image:var(--gradient-accent-forest-to-brass)]", "data-[fullscreen]:rounded-none data-[fullscreen]:shadow-none", className),
179
+ ...props,
180
+ onBlurCapture: (event) => {
181
+ onBlurCapture?.(event);
182
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocusPinned(false);
183
+ },
184
+ onFocusCapture: (event) => {
185
+ onFocusCapture?.(event);
186
+ const keyboardFocus = event.target.matches?.(":focus-visible") ?? false;
187
+ setFocusPinned(keyboardFocus);
188
+ if (keyboardFocus) setControlsVisible(true);
189
+ },
190
+ onKeyDown: (event) => {
191
+ onKeyDown?.(event);
192
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
193
+ const target = event.target;
194
+ if (target.closest("input, textarea, select, [contenteditable], [role='slider'], [role='menu'], [role='menuitem'], [role='menuitemradio']")) return;
195
+ const state = store.getState();
196
+ const currentTime = state.mediaCurrentTime ?? 0;
197
+ const { min, max } = getSeekBounds(state.mediaSeekable, state.mediaDuration);
198
+ switch (event.key) {
199
+ case " ":
200
+ if (target.closest("button")) return;
201
+ event.preventDefault();
202
+ dispatch({ type: paused ? MediaActionTypes.MEDIA_PLAY_REQUEST : MediaActionTypes.MEDIA_PAUSE_REQUEST });
203
+ break;
204
+ case "k":
205
+ case "K":
206
+ dispatch({ type: paused ? MediaActionTypes.MEDIA_PLAY_REQUEST : MediaActionTypes.MEDIA_PAUSE_REQUEST });
207
+ break;
208
+ case "m":
209
+ case "M":
210
+ dispatch({ type: muted ? MediaActionTypes.MEDIA_UNMUTE_REQUEST : MediaActionTypes.MEDIA_MUTE_REQUEST });
211
+ break;
212
+ case "f":
213
+ case "F":
214
+ if (audio) return;
215
+ dispatch({ type: isFullscreen ? MediaActionTypes.MEDIA_EXIT_FULLSCREEN_REQUEST : MediaActionTypes.MEDIA_ENTER_FULLSCREEN_REQUEST });
216
+ break;
217
+ case "c":
218
+ case "C":
219
+ dispatch({ type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST });
220
+ break;
221
+ case "ArrowLeft":
222
+ event.preventDefault();
223
+ dispatch({
224
+ type: MediaActionTypes.MEDIA_SEEK_REQUEST,
225
+ detail: clamp(currentTime - 5, min, max)
226
+ });
227
+ break;
228
+ case "ArrowRight":
229
+ event.preventDefault();
230
+ dispatch({
231
+ type: MediaActionTypes.MEDIA_SEEK_REQUEST,
232
+ detail: clamp(currentTime + 5, min, max)
233
+ });
234
+ break;
235
+ default: return;
236
+ }
237
+ wake();
238
+ },
239
+ onPointerDown: (event) => {
240
+ onPointerDown?.(event);
241
+ wake();
242
+ },
243
+ onPointerLeave: (event) => {
244
+ onPointerLeave?.(event);
245
+ setHoveringControls(false);
246
+ if (!alwaysVisibleRef.current) {
247
+ window.clearTimeout(hideTimerRef.current);
248
+ setControlsVisible(false);
249
+ }
250
+ },
251
+ onPointerMove: (event) => {
252
+ onPointerMove?.(event);
253
+ wake();
254
+ const rect = event.currentTarget.querySelector("[data-slot='media-player-controls']")?.getBoundingClientRect();
255
+ setHoveringControls(!!rect && event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom);
256
+ },
257
+ children
258
+ })
259
+ });
364
260
  }
365
261
  function MediaPlayerVideo({ className, ref, ...props }) {
366
- const mediaRef = useMediaRef();
367
- const mediaRefRef = React.useRef(mediaRef);
368
- mediaRefRef.current = mediaRef;
369
- const videoRef = React.useCallback(
370
- (node) => {
371
- mediaRefRef.current(node);
372
- setRef(ref, node);
373
- },
374
- [ref]
375
- );
376
- return /* @__PURE__ */ jsx(
377
- "video",
378
- {
379
- ref: videoRef,
380
- "data-slot": "media-player-video",
381
- className: cn(
382
- "block aspect-video size-full object-cover",
383
- "group-data-[fullscreen]/media-player:aspect-auto group-data-[fullscreen]/media-player:object-contain",
384
- className
385
- ),
386
- playsInline: true,
387
- ...props
388
- }
389
- );
262
+ const mediaRef = useMediaRef();
263
+ const mediaRefRef = React.useRef(mediaRef);
264
+ mediaRefRef.current = mediaRef;
265
+ return /* @__PURE__ */ jsx("video", {
266
+ ref: React.useCallback((node) => {
267
+ mediaRefRef.current(node);
268
+ setRef(ref, node);
269
+ }, [ref]),
270
+ "data-slot": "media-player-video",
271
+ className: cn("block aspect-video size-full object-cover", "group-data-[fullscreen]/media-player:aspect-auto group-data-[fullscreen]/media-player:object-contain", className),
272
+ playsInline: true,
273
+ ...props
274
+ });
390
275
  }
391
276
  function MediaPlayerAudio({ className, ref, ...props }) {
392
- const mediaRef = useMediaRef();
393
- const mediaRefRef = React.useRef(mediaRef);
394
- mediaRefRef.current = mediaRef;
395
- const audioRef = React.useCallback(
396
- (node) => {
397
- mediaRefRef.current(node);
398
- setRef(ref, node);
399
- },
400
- [ref]
401
- );
402
- return /* @__PURE__ */ jsx(
403
- "audio",
404
- {
405
- ref: audioRef,
406
- "data-slot": "media-player-audio",
407
- className: cn("sr-only", className),
408
- ...props
409
- }
410
- );
277
+ const mediaRef = useMediaRef();
278
+ const mediaRefRef = React.useRef(mediaRef);
279
+ mediaRefRef.current = mediaRef;
280
+ return /* @__PURE__ */ jsx("audio", {
281
+ ref: React.useCallback((node) => {
282
+ mediaRefRef.current(node);
283
+ setRef(ref, node);
284
+ }, [ref]),
285
+ "data-slot": "media-player-audio",
286
+ className: cn("sr-only", className),
287
+ ...props
288
+ });
411
289
  }
412
- function MediaPlayerControls({
413
- className,
414
- ...props
415
- }) {
416
- return /* @__PURE__ */ jsx(
417
- "div",
418
- {
419
- "data-slot": "media-player-controls",
420
- className: cn(
421
- "absolute inset-x-0 bottom-0 z-10 flex w-full items-center gap-2 p-3 text-fg-on-brand md:p-4",
422
- "transition-opacity duration-300 ease-out group-data-[controls=hidden]/media-player:pointer-events-none group-data-[controls=hidden]/media-player:opacity-0",
423
- className
424
- ),
425
- ...props
426
- }
427
- );
290
+ function MediaPlayerControls({ className, ...props }) {
291
+ return /* @__PURE__ */ jsx("div", {
292
+ "data-slot": "media-player-controls",
293
+ className: cn("absolute inset-x-0 bottom-0 z-10 flex w-full items-center gap-2 p-3 text-fg-on-brand md:p-4", "transition-opacity duration-300 ease-out group-data-[controls=hidden]/media-player:pointer-events-none group-data-[controls=hidden]/media-player:opacity-0", className),
294
+ ...props
295
+ });
428
296
  }
429
- function MediaPlayerControlsOverlay({
430
- className,
431
- ...props
432
- }) {
433
- return /* @__PURE__ */ jsx(
434
- "div",
435
- {
436
- "aria-hidden": "true",
437
- "data-slot": "media-player-controls-overlay",
438
- className: cn(
439
- "pointer-events-none absolute inset-x-0 bottom-0 -z-10 h-36 bg-[image:var(--gradient-overlay-photo-bottom)]",
440
- className
441
- ),
442
- ...props
443
- }
444
- );
297
+ function MediaPlayerControlsOverlay({ className, ...props }) {
298
+ return /* @__PURE__ */ jsx("div", {
299
+ "aria-hidden": "true",
300
+ "data-slot": "media-player-controls-overlay",
301
+ className: cn("pointer-events-none absolute inset-x-0 bottom-0 -z-10 h-36 bg-[image:var(--gradient-overlay-photo-bottom)]", className),
302
+ ...props
303
+ });
445
304
  }
446
- function MediaControlButton({
447
- className,
448
- shortcut,
449
- tooltip,
450
- ...props
451
- }) {
452
- const { portalContainer } = React.useContext(MediaPlayerContext);
453
- const button = /* @__PURE__ */ jsx(
454
- "button",
455
- {
456
- type: "button",
457
- className: cn(
458
- "inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-surface-glass-overlay-button text-fg-on-brand shadow-1 backdrop-blur-sm transition-[background-color,color,box-shadow,transform] outline-none select-none hover:bg-surface-brand hover:text-fg-on-brand focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] active:scale-95 aria-pressed:bg-surface-brand disabled:pointer-events-none disabled:opacity-45 [&_svg]:size-4.5",
459
- className
460
- ),
461
- ...props
462
- }
463
- );
464
- if (!tooltip) {
465
- return button;
466
- }
467
- return /* @__PURE__ */ jsxs(Tooltip, { children: [
468
- /* @__PURE__ */ jsx(TooltipTrigger, { delay: 300, render: button }),
469
- /* @__PURE__ */ jsxs(
470
- TooltipContent,
471
- {
472
- className: "border-line-subtle px-2.5 py-1.5 text-gdt-xs font-bold",
473
- container: portalContainer ?? void 0,
474
- side: "top",
475
- sideOffset: 8,
476
- children: [
477
- tooltip,
478
- shortcut ? /* @__PURE__ */ jsx(
479
- "kbd",
480
- {
481
- "data-slot": "kbd",
482
- className: "rounded-sm border border-line bg-surface px-1.5 py-0.5 font-sans text-gdt-xs font-bold text-fg-primary",
483
- children: shortcut
484
- }
485
- ) : null
486
- ]
487
- }
488
- )
489
- ] });
305
+ function MediaControlButton({ className, shortcut, tooltip, ...props }) {
306
+ const { portalContainer } = React.useContext(MediaPlayerContext);
307
+ const button = /* @__PURE__ */ jsx("button", {
308
+ type: "button",
309
+ className: cn("inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-surface-glass-overlay-button text-fg-on-brand shadow-1 backdrop-blur-sm transition-[background-color,color,box-shadow,transform] outline-none select-none hover:bg-surface-brand hover:text-fg-on-brand focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] active:scale-95 aria-pressed:bg-surface-brand disabled:pointer-events-none disabled:opacity-45 [&_svg]:size-4.5", className),
310
+ ...props
311
+ });
312
+ if (!tooltip) return button;
313
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
314
+ delay: 300,
315
+ render: button
316
+ }), /* @__PURE__ */ jsxs(TooltipContent, {
317
+ className: "border-line-subtle px-2.5 py-1.5 text-gdt-xs font-bold",
318
+ container: portalContainer ?? void 0,
319
+ side: "top",
320
+ sideOffset: 8,
321
+ children: [tooltip, shortcut ? /* @__PURE__ */ jsx("kbd", {
322
+ "data-slot": "kbd",
323
+ className: "rounded-sm border border-line bg-surface px-1.5 py-0.5 font-sans text-gdt-xs font-bold text-fg-primary",
324
+ children: shortcut
325
+ }) : null]
326
+ })] });
490
327
  }
491
- function MediaPlayerPlay({
492
- className,
493
- onClick,
494
- ...props
495
- }) {
496
- const dispatch = useMediaDispatch();
497
- const paused = useMediaSelector((state) => state.mediaPaused !== false);
498
- return /* @__PURE__ */ jsx(
499
- MediaControlButton,
500
- {
501
- "aria-label": paused ? "Play media" : "Pause media",
502
- "aria-pressed": !paused,
503
- "data-slot": "media-player-play",
504
- className,
505
- shortcut: "Space",
506
- tooltip: paused ? "Play" : "Pause",
507
- onClick: (event) => {
508
- onClick?.(event);
509
- if (event.defaultPrevented) {
510
- return;
511
- }
512
- dispatch({
513
- type: paused ? MediaActionTypes.MEDIA_PLAY_REQUEST : MediaActionTypes.MEDIA_PAUSE_REQUEST
514
- });
515
- },
516
- ...props,
517
- children: paused ? /* @__PURE__ */ jsx(Play, { fill: "currentColor" }) : /* @__PURE__ */ jsx(Pause, { fill: "currentColor" })
518
- }
519
- );
328
+ function MediaPlayerPlay({ className, onClick, ...props }) {
329
+ const dispatch = useMediaDispatch();
330
+ const paused = useMediaSelector((state) => state.mediaPaused !== false);
331
+ return /* @__PURE__ */ jsx(MediaControlButton, {
332
+ "aria-label": paused ? "Play media" : "Pause media",
333
+ "aria-pressed": !paused,
334
+ "data-slot": "media-player-play",
335
+ className,
336
+ shortcut: "Space",
337
+ tooltip: paused ? "Play" : "Pause",
338
+ onClick: (event) => {
339
+ onClick?.(event);
340
+ if (event.defaultPrevented) return;
341
+ dispatch({ type: paused ? MediaActionTypes.MEDIA_PLAY_REQUEST : MediaActionTypes.MEDIA_PAUSE_REQUEST });
342
+ },
343
+ ...props,
344
+ children: paused ? /* @__PURE__ */ jsx(Play, { fill: "currentColor" }) : /* @__PURE__ */ jsx(Pause, { fill: "currentColor" })
345
+ });
520
346
  }
521
- function MediaPlayerSeekBackward({
522
- className,
523
- onClick,
524
- seekOffset = 10,
525
- ...props
526
- }) {
527
- const dispatch = useMediaDispatch();
528
- const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
529
- return /* @__PURE__ */ jsx(
530
- MediaControlButton,
531
- {
532
- "aria-label": `Seek back ${seekOffset} seconds`,
533
- "data-slot": "media-player-seek-backward",
534
- className,
535
- tooltip: `Back ${seekOffset}s`,
536
- onClick: (event) => {
537
- onClick?.(event);
538
- if (event.defaultPrevented) {
539
- return;
540
- }
541
- dispatch({
542
- type: MediaActionTypes.MEDIA_SEEK_REQUEST,
543
- detail: Math.max(0, currentTime - seekOffset)
544
- });
545
- },
546
- ...props,
547
- children: /* @__PURE__ */ jsx(RotateCcw, {})
548
- }
549
- );
347
+ function MediaPlayerSeekBackward({ className, onClick, seekOffset = 10, ...props }) {
348
+ const dispatch = useMediaDispatch();
349
+ const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
350
+ return /* @__PURE__ */ jsx(MediaControlButton, {
351
+ "aria-label": `Seek back ${seekOffset} seconds`,
352
+ "data-slot": "media-player-seek-backward",
353
+ className,
354
+ tooltip: `Back ${seekOffset}s`,
355
+ onClick: (event) => {
356
+ onClick?.(event);
357
+ if (event.defaultPrevented) return;
358
+ dispatch({
359
+ type: MediaActionTypes.MEDIA_SEEK_REQUEST,
360
+ detail: Math.max(0, currentTime - seekOffset)
361
+ });
362
+ },
363
+ ...props,
364
+ children: /* @__PURE__ */ jsx(RotateCcw, {})
365
+ });
550
366
  }
551
- function MediaPlayerSeekForward({
552
- className,
553
- onClick,
554
- seekOffset = 10,
555
- ...props
556
- }) {
557
- const dispatch = useMediaDispatch();
558
- const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
559
- const duration = useMediaSelector((state) => state.mediaDuration);
560
- const seekable = useMediaSelector((state) => state.mediaSeekable);
561
- const { max } = getSeekBounds(seekable, duration);
562
- return /* @__PURE__ */ jsx(
563
- MediaControlButton,
564
- {
565
- "aria-label": `Seek forward ${seekOffset} seconds`,
566
- "data-slot": "media-player-seek-forward",
567
- className,
568
- tooltip: `Forward ${seekOffset}s`,
569
- onClick: (event) => {
570
- onClick?.(event);
571
- if (event.defaultPrevented) {
572
- return;
573
- }
574
- dispatch({
575
- type: MediaActionTypes.MEDIA_SEEK_REQUEST,
576
- detail: max > 0 ? Math.min(max, currentTime + seekOffset) : currentTime + seekOffset
577
- });
578
- },
579
- ...props,
580
- children: /* @__PURE__ */ jsx(RotateCw, {})
581
- }
582
- );
367
+ function MediaPlayerSeekForward({ className, onClick, seekOffset = 10, ...props }) {
368
+ const dispatch = useMediaDispatch();
369
+ const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
370
+ const duration = useMediaSelector((state) => state.mediaDuration);
371
+ const { max } = getSeekBounds(useMediaSelector((state) => state.mediaSeekable), duration);
372
+ return /* @__PURE__ */ jsx(MediaControlButton, {
373
+ "aria-label": `Seek forward ${seekOffset} seconds`,
374
+ "data-slot": "media-player-seek-forward",
375
+ className,
376
+ tooltip: `Forward ${seekOffset}s`,
377
+ onClick: (event) => {
378
+ onClick?.(event);
379
+ if (event.defaultPrevented) return;
380
+ dispatch({
381
+ type: MediaActionTypes.MEDIA_SEEK_REQUEST,
382
+ detail: max > 0 ? Math.min(max, currentTime + seekOffset) : currentTime + seekOffset
383
+ });
384
+ },
385
+ ...props,
386
+ children: /* @__PURE__ */ jsx(RotateCw, {})
387
+ });
583
388
  }
584
- function MediaPlayerVolume({
585
- className,
586
- disabled,
587
- expandable = false,
588
- onClick,
589
- rangeClassName,
590
- ...props
591
- }) {
592
- const dispatch = useMediaDispatch();
593
- const muted = useMediaSelector((state) => state.mediaMuted === true);
594
- const volume = useMediaSelector((state) => state.mediaVolume ?? 1);
595
- const unavailable = useMediaSelector(
596
- (state) => !!state.mediaVolumeUnavailable
597
- );
598
- const resolvedDisabled = disabled || unavailable;
599
- const Icon = muted || volume === 0 ? VolumeX : volume < 0.5 ? Volume1 : Volume2;
600
- return /* @__PURE__ */ jsxs(
601
- "div",
602
- {
603
- "data-slot": "media-player-volume",
604
- className: cn(
605
- "group/volume flex items-center gap-2 rounded-full",
606
- expandable && "w-9 overflow-visible transition-[width] hover:w-32 focus-within:w-32"
607
- ),
608
- children: [
609
- /* @__PURE__ */ jsx(
610
- MediaControlButton,
611
- {
612
- "aria-label": muted ? "Unmute media" : "Mute media",
613
- "aria-pressed": muted,
614
- className,
615
- disabled: resolvedDisabled,
616
- shortcut: "M",
617
- tooltip: "Volume",
618
- onClick: (event) => {
619
- onClick?.(event);
620
- if (event.defaultPrevented) {
621
- return;
622
- }
623
- dispatch({
624
- type: muted ? MediaActionTypes.MEDIA_UNMUTE_REQUEST : MediaActionTypes.MEDIA_MUTE_REQUEST
625
- });
626
- },
627
- ...props,
628
- children: /* @__PURE__ */ jsx(Icon, {})
629
- }
630
- ),
631
- /* @__PURE__ */ jsx(
632
- SliderPrimitive.Root,
633
- {
634
- "data-slot": "media-player-volume-slider",
635
- className: cn(
636
- "flex h-8 w-20 min-w-20 touch-none items-center",
637
- resolvedDisabled && "pointer-events-none opacity-45",
638
- expandable && "pointer-events-none opacity-0 transition-opacity group-hover/volume:pointer-events-auto group-hover/volume:opacity-100 group-focus-within/volume:pointer-events-auto group-focus-within/volume:opacity-100",
639
- rangeClassName
640
- ),
641
- disabled: resolvedDisabled,
642
- max: 1,
643
- min: 0,
644
- onValueChange: (value) => {
645
- const nextValue = Array.isArray(value) ? value[0] ?? 0 : value;
646
- dispatch({
647
- type: MediaActionTypes.MEDIA_VOLUME_REQUEST,
648
- detail: nextValue
649
- });
650
- if (muted && nextValue > 0) {
651
- dispatch({ type: MediaActionTypes.MEDIA_UNMUTE_REQUEST });
652
- }
653
- },
654
- step: 0.02,
655
- value: muted ? 0 : volume,
656
- children: /* @__PURE__ */ jsx(SliderPrimitive.Control, { className: "flex h-8 w-full items-center", children: /* @__PURE__ */ jsxs(SliderPrimitive.Track, { className: "h-1.5 w-full rounded-full bg-fg-on-brand/30", children: [
657
- /* @__PURE__ */ jsx(SliderPrimitive.Indicator, { className: "rounded-full bg-fg-on-brand" }),
658
- /* @__PURE__ */ jsx(
659
- SliderPrimitive.Thumb,
660
- {
661
- getAriaLabel: () => "Media volume",
662
- className: "size-3 rounded-full bg-fg-on-brand shadow-1 outline-none focus-visible:[filter:drop-shadow(0_0_0.25rem_var(--color-line-focus))]"
663
- }
664
- )
665
- ] }) })
666
- }
667
- )
668
- ]
669
- }
670
- );
389
+ function MediaPlayerVolume({ className, disabled, expandable = false, onClick, rangeClassName, ...props }) {
390
+ const dispatch = useMediaDispatch();
391
+ const muted = useMediaSelector((state) => state.mediaMuted === true);
392
+ const volume = useMediaSelector((state) => state.mediaVolume ?? 1);
393
+ const unavailable = useMediaSelector((state) => !!state.mediaVolumeUnavailable);
394
+ const resolvedDisabled = disabled || unavailable;
395
+ const Icon = muted || volume === 0 ? VolumeX : volume < .5 ? Volume1 : Volume2;
396
+ return /* @__PURE__ */ jsxs("div", {
397
+ "data-slot": "media-player-volume",
398
+ className: cn("group/volume flex items-center gap-2 rounded-full", expandable && "w-9 overflow-visible transition-[width] hover:w-32 focus-within:w-32"),
399
+ children: [/* @__PURE__ */ jsx(MediaControlButton, {
400
+ "aria-label": muted ? "Unmute media" : "Mute media",
401
+ "aria-pressed": muted,
402
+ className,
403
+ disabled: resolvedDisabled,
404
+ shortcut: "M",
405
+ tooltip: "Volume",
406
+ onClick: (event) => {
407
+ onClick?.(event);
408
+ if (event.defaultPrevented) return;
409
+ dispatch({ type: muted ? MediaActionTypes.MEDIA_UNMUTE_REQUEST : MediaActionTypes.MEDIA_MUTE_REQUEST });
410
+ },
411
+ ...props,
412
+ children: /* @__PURE__ */ jsx(Icon, {})
413
+ }), /* @__PURE__ */ jsx(Slider.Root, {
414
+ "data-slot": "media-player-volume-slider",
415
+ className: cn("flex h-8 w-20 min-w-20 touch-none items-center", resolvedDisabled && "pointer-events-none opacity-45", expandable && "pointer-events-none opacity-0 transition-opacity group-hover/volume:pointer-events-auto group-hover/volume:opacity-100 group-focus-within/volume:pointer-events-auto group-focus-within/volume:opacity-100", rangeClassName),
416
+ disabled: resolvedDisabled,
417
+ max: 1,
418
+ min: 0,
419
+ onValueChange: (value) => {
420
+ const nextValue = Array.isArray(value) ? value[0] ?? 0 : value;
421
+ dispatch({
422
+ type: MediaActionTypes.MEDIA_VOLUME_REQUEST,
423
+ detail: nextValue
424
+ });
425
+ if (muted && nextValue > 0) dispatch({ type: MediaActionTypes.MEDIA_UNMUTE_REQUEST });
426
+ },
427
+ step: .02,
428
+ value: muted ? 0 : volume,
429
+ children: /* @__PURE__ */ jsx(Slider.Control, {
430
+ className: "flex h-8 w-full items-center",
431
+ children: /* @__PURE__ */ jsxs(Slider.Track, {
432
+ className: "h-1.5 w-full rounded-full bg-fg-on-brand/30",
433
+ children: [/* @__PURE__ */ jsx(Slider.Indicator, { className: "rounded-full bg-fg-on-brand" }), /* @__PURE__ */ jsx(Slider.Thumb, {
434
+ getAriaLabel: () => "Media volume",
435
+ className: "size-3 rounded-full bg-fg-on-brand shadow-1 outline-none focus-visible:[filter:drop-shadow(0_0_0.25rem_var(--color-line-focus))]"
436
+ })]
437
+ })
438
+ })
439
+ })]
440
+ });
671
441
  }
672
- function MediaPlayerPlaybackSpeed({
673
- className,
674
- rates = DEFAULT_PLAYBACK_RATES,
675
- ...props
676
- }) {
677
- const dispatch = useMediaDispatch();
678
- const { onMenuOpenChange, portalContainer } = React.useContext(MediaPlayerContext);
679
- const playbackRate = useMediaSelector(
680
- (state) => state.mediaPlaybackRate ?? 1
681
- );
682
- const rateLabel = formatRate(playbackRate);
683
- return /* @__PURE__ */ jsxs(DropdownMenu, { size: "sm", onOpenChange: onMenuOpenChange, children: [
684
- /* @__PURE__ */ jsx(
685
- DropdownMenuTrigger,
686
- {
687
- render: /* @__PURE__ */ jsx(
688
- MediaControlButton,
689
- {
690
- "aria-label": `Playback speed ${rateLabel}`,
691
- "data-slot": "media-player-playback-speed",
692
- className: cn(
693
- "w-auto px-3 text-gdt-xs font-bold tabular-nums",
694
- className
695
- ),
696
- tooltip: "Playback speed",
697
- ...props,
698
- children: rateLabel
699
- }
700
- )
701
- }
702
- ),
703
- /* @__PURE__ */ jsx(
704
- DropdownMenuContent,
705
- {
706
- align: "end",
707
- className: "min-w-24",
708
- container: portalContainer ?? void 0,
709
- side: "top",
710
- sideOffset: 8,
711
- children: /* @__PURE__ */ jsx(
712
- DropdownMenuRadioGroup,
713
- {
714
- value: String(playbackRate),
715
- onValueChange: (value) => {
716
- dispatch({
717
- type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,
718
- detail: Number(value)
719
- });
720
- },
721
- children: rates.map((rate) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, { closeOnClick: true, value: String(rate), children: formatRate(rate) }, rate))
722
- }
723
- )
724
- }
725
- )
726
- ] });
442
+ function MediaPlayerPlaybackSpeed({ className, rates = DEFAULT_PLAYBACK_RATES, ...props }) {
443
+ const dispatch = useMediaDispatch();
444
+ const { onMenuOpenChange, portalContainer } = React.useContext(MediaPlayerContext);
445
+ const playbackRate = useMediaSelector((state) => state.mediaPlaybackRate ?? 1);
446
+ const rateLabel = formatRate(playbackRate);
447
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
448
+ size: "sm",
449
+ onOpenChange: onMenuOpenChange,
450
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, { render: /* @__PURE__ */ jsx(MediaControlButton, {
451
+ "aria-label": `Playback speed ${rateLabel}`,
452
+ "data-slot": "media-player-playback-speed",
453
+ className: cn("w-auto px-3 text-gdt-xs font-bold tabular-nums", className),
454
+ tooltip: "Playback speed",
455
+ ...props,
456
+ children: rateLabel
457
+ }) }), /* @__PURE__ */ jsx(DropdownMenuContent, {
458
+ align: "end",
459
+ className: "min-w-24",
460
+ container: portalContainer ?? void 0,
461
+ side: "top",
462
+ sideOffset: 8,
463
+ children: /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
464
+ value: String(playbackRate),
465
+ onValueChange: (value) => {
466
+ dispatch({
467
+ type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,
468
+ detail: Number(value)
469
+ });
470
+ },
471
+ children: rates.map((rate) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, {
472
+ closeOnClick: true,
473
+ value: String(rate),
474
+ children: formatRate(rate)
475
+ }, rate))
476
+ })
477
+ })]
478
+ });
727
479
  }
728
- function MediaPlayerCaptions({
729
- className,
730
- disabled,
731
- onClick,
732
- ...props
733
- }) {
734
- const dispatch = useMediaDispatch();
735
- const subtitlesList = useMediaSelector((state) => state.mediaSubtitlesList);
736
- const subtitlesShowing = useMediaSelector(
737
- (state) => state.mediaSubtitlesShowing
738
- );
739
- const hasSubtitles = (subtitlesList?.length ?? 0) > 0;
740
- const showing = (subtitlesShowing?.length ?? 0) > 0;
741
- const Icon = showing ? Captions : CaptionsOff;
742
- return /* @__PURE__ */ jsx(
743
- MediaControlButton,
744
- {
745
- "aria-label": showing ? "Disable captions" : "Enable captions",
746
- "aria-pressed": showing,
747
- "data-slot": "media-player-captions",
748
- className,
749
- disabled: disabled || !hasSubtitles,
750
- shortcut: "C",
751
- tooltip: "Captions",
752
- onClick: (event) => {
753
- onClick?.(event);
754
- if (event.defaultPrevented) {
755
- return;
756
- }
757
- dispatch({ type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST });
758
- },
759
- ...props,
760
- children: /* @__PURE__ */ jsx(Icon, {})
761
- }
762
- );
480
+ function MediaPlayerCaptions({ className, disabled, onClick, ...props }) {
481
+ const dispatch = useMediaDispatch();
482
+ const subtitlesList = useMediaSelector((state) => state.mediaSubtitlesList);
483
+ const subtitlesShowing = useMediaSelector((state) => state.mediaSubtitlesShowing);
484
+ const hasSubtitles = (subtitlesList?.length ?? 0) > 0;
485
+ const showing = (subtitlesShowing?.length ?? 0) > 0;
486
+ const Icon = showing ? Captions : CaptionsOff;
487
+ return /* @__PURE__ */ jsx(MediaControlButton, {
488
+ "aria-label": showing ? "Disable captions" : "Enable captions",
489
+ "aria-pressed": showing,
490
+ "data-slot": "media-player-captions",
491
+ className,
492
+ disabled: disabled || !hasSubtitles,
493
+ shortcut: "C",
494
+ tooltip: "Captions",
495
+ onClick: (event) => {
496
+ onClick?.(event);
497
+ if (event.defaultPrevented) return;
498
+ dispatch({ type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST });
499
+ },
500
+ ...props,
501
+ children: /* @__PURE__ */ jsx(Icon, {})
502
+ });
763
503
  }
764
- function MediaPlayerSettings({
765
- className,
766
- rates = DEFAULT_PLAYBACK_RATES,
767
- ...props
768
- }) {
769
- const dispatch = useMediaDispatch();
770
- const { onMenuOpenChange, portalContainer } = React.useContext(MediaPlayerContext);
771
- const playbackRate = useMediaSelector(
772
- (state) => state.mediaPlaybackRate ?? 1
773
- );
774
- const subtitlesList = useMediaSelector((state) => state.mediaSubtitlesList);
775
- const subtitlesShowing = useMediaSelector(
776
- (state) => state.mediaSubtitlesShowing
777
- );
778
- const subtitles = subtitlesList ?? [];
779
- const showingTrack = subtitlesShowing?.[0];
780
- const captionsValue = showingTrack ? subtitleTrackId(showingTrack) : "off";
781
- const captionsLabel = showingTrack ? subtitleTrackLabel(showingTrack) : "Off";
782
- return /* @__PURE__ */ jsxs(DropdownMenu, { size: "sm", onOpenChange: onMenuOpenChange, children: [
783
- /* @__PURE__ */ jsx(
784
- DropdownMenuTrigger,
785
- {
786
- render: /* @__PURE__ */ jsx(
787
- MediaControlButton,
788
- {
789
- "aria-label": "Player settings",
790
- "data-slot": "media-player-settings",
791
- className,
792
- tooltip: "Settings",
793
- ...props,
794
- children: /* @__PURE__ */ jsx(Settings, {})
795
- }
796
- )
797
- }
798
- ),
799
- /* @__PURE__ */ jsxs(
800
- DropdownMenuContent,
801
- {
802
- align: "end",
803
- className: "min-w-48",
804
- container: portalContainer ?? void 0,
805
- side: "top",
806
- sideOffset: 8,
807
- children: [
808
- /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [
809
- /* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: [
810
- "Speed",
811
- /* @__PURE__ */ jsx("span", { className: "ml-auto rounded-md bg-surface px-1.5 py-0.5 text-gdt-xs font-bold tabular-nums", children: formatRate(playbackRate) })
812
- ] }),
813
- /* @__PURE__ */ jsx(DropdownMenuSubContent, { container: portalContainer ?? void 0, children: /* @__PURE__ */ jsx(
814
- DropdownMenuRadioGroup,
815
- {
816
- value: String(playbackRate),
817
- onValueChange: (value) => {
818
- dispatch({
819
- type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,
820
- detail: Number(value)
821
- });
822
- },
823
- children: rates.map((rate) => /* @__PURE__ */ jsx(
824
- DropdownMenuRadioItem,
825
- {
826
- closeOnClick: true,
827
- value: String(rate),
828
- children: formatRate(rate)
829
- },
830
- rate
831
- ))
832
- }
833
- ) })
834
- ] }),
835
- subtitles.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [
836
- /* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: [
837
- "Captions",
838
- /* @__PURE__ */ jsx("span", { className: "ml-auto rounded-md bg-surface px-1.5 py-0.5 text-gdt-xs font-bold", children: captionsLabel })
839
- ] }),
840
- /* @__PURE__ */ jsx(DropdownMenuSubContent, { container: portalContainer ?? void 0, children: /* @__PURE__ */ jsxs(
841
- DropdownMenuRadioGroup,
842
- {
843
- value: captionsValue,
844
- onValueChange: (value) => {
845
- if (subtitlesShowing?.length) {
846
- dispatch({
847
- type: MediaActionTypes.MEDIA_DISABLE_SUBTITLES_REQUEST,
848
- detail: subtitlesShowing
849
- });
850
- }
851
- const track = subtitles.find(
852
- (candidate) => subtitleTrackId(candidate) === value
853
- );
854
- if (track) {
855
- dispatch({
856
- type: MediaActionTypes.MEDIA_SHOW_SUBTITLES_REQUEST,
857
- detail: [track]
858
- });
859
- }
860
- },
861
- children: [
862
- /* @__PURE__ */ jsx(DropdownMenuRadioItem, { closeOnClick: true, value: "off", children: "Off" }),
863
- subtitles.map((track) => /* @__PURE__ */ jsx(
864
- DropdownMenuRadioItem,
865
- {
866
- closeOnClick: true,
867
- value: subtitleTrackId(track),
868
- children: subtitleTrackLabel(track)
869
- },
870
- subtitleTrackId(track)
871
- ))
872
- ]
873
- }
874
- ) })
875
- ] }) : null
876
- ]
877
- }
878
- )
879
- ] });
504
+ function MediaPlayerSettings({ className, rates = DEFAULT_PLAYBACK_RATES, ...props }) {
505
+ const dispatch = useMediaDispatch();
506
+ const { onMenuOpenChange, portalContainer } = React.useContext(MediaPlayerContext);
507
+ const playbackRate = useMediaSelector((state) => state.mediaPlaybackRate ?? 1);
508
+ const subtitlesList = useMediaSelector((state) => state.mediaSubtitlesList);
509
+ const subtitlesShowing = useMediaSelector((state) => state.mediaSubtitlesShowing);
510
+ const subtitles = subtitlesList ?? [];
511
+ const showingTrack = subtitlesShowing?.[0];
512
+ const captionsValue = showingTrack ? subtitleTrackId(showingTrack) : "off";
513
+ const captionsLabel = showingTrack ? subtitleTrackLabel(showingTrack) : "Off";
514
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
515
+ size: "sm",
516
+ onOpenChange: onMenuOpenChange,
517
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, { render: /* @__PURE__ */ jsx(MediaControlButton, {
518
+ "aria-label": "Player settings",
519
+ "data-slot": "media-player-settings",
520
+ className,
521
+ tooltip: "Settings",
522
+ ...props,
523
+ children: /* @__PURE__ */ jsx(Settings, {})
524
+ }) }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
525
+ align: "end",
526
+ className: "min-w-48",
527
+ container: portalContainer ?? void 0,
528
+ side: "top",
529
+ sideOffset: 8,
530
+ children: [/* @__PURE__ */ jsxs(DropdownMenuSub, { children: [/* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: ["Speed", /* @__PURE__ */ jsx("span", {
531
+ className: "ml-auto rounded-md bg-surface px-1.5 py-0.5 text-gdt-xs font-bold tabular-nums",
532
+ children: formatRate(playbackRate)
533
+ })] }), /* @__PURE__ */ jsx(DropdownMenuSubContent, {
534
+ container: portalContainer ?? void 0,
535
+ children: /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
536
+ value: String(playbackRate),
537
+ onValueChange: (value) => {
538
+ dispatch({
539
+ type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,
540
+ detail: Number(value)
541
+ });
542
+ },
543
+ children: rates.map((rate) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, {
544
+ closeOnClick: true,
545
+ value: String(rate),
546
+ children: formatRate(rate)
547
+ }, rate))
548
+ })
549
+ })] }), subtitles.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [/* @__PURE__ */ jsxs(DropdownMenuSubTrigger, { children: ["Captions", /* @__PURE__ */ jsx("span", {
550
+ className: "ml-auto rounded-md bg-surface px-1.5 py-0.5 text-gdt-xs font-bold",
551
+ children: captionsLabel
552
+ })] }), /* @__PURE__ */ jsx(DropdownMenuSubContent, {
553
+ container: portalContainer ?? void 0,
554
+ children: /* @__PURE__ */ jsxs(DropdownMenuRadioGroup, {
555
+ value: captionsValue,
556
+ onValueChange: (value) => {
557
+ if (subtitlesShowing?.length) dispatch({
558
+ type: MediaActionTypes.MEDIA_DISABLE_SUBTITLES_REQUEST,
559
+ detail: subtitlesShowing
560
+ });
561
+ const track = subtitles.find((candidate) => subtitleTrackId(candidate) === value);
562
+ if (track) dispatch({
563
+ type: MediaActionTypes.MEDIA_SHOW_SUBTITLES_REQUEST,
564
+ detail: [track]
565
+ });
566
+ },
567
+ children: [/* @__PURE__ */ jsx(DropdownMenuRadioItem, {
568
+ closeOnClick: true,
569
+ value: "off",
570
+ children: "Off"
571
+ }), subtitles.map((track) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, {
572
+ closeOnClick: true,
573
+ value: subtitleTrackId(track),
574
+ children: subtitleTrackLabel(track)
575
+ }, subtitleTrackId(track)))]
576
+ })
577
+ })] }) : null]
578
+ })]
579
+ });
880
580
  }
881
- function MediaPlayerTime({
882
- className,
883
- showDuration = true,
884
- ...props
885
- }) {
886
- const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
887
- const duration = useMediaSelector((state) => state.mediaDuration);
888
- return /* @__PURE__ */ jsx(
889
- "span",
890
- {
891
- "data-slot": "media-player-time",
892
- className: cn(
893
- "inline-flex shrink-0 items-center px-1 text-gdt-sm font-medium tabular-nums text-fg-on-brand/80",
894
- className
895
- ),
896
- ...props,
897
- children: showDuration ? `${formatTime(currentTime)} / ${formatTime(duration)}` : formatTime(currentTime)
898
- }
899
- );
581
+ function MediaPlayerTime({ className, showDuration = true, ...props }) {
582
+ const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
583
+ const duration = useMediaSelector((state) => state.mediaDuration);
584
+ return /* @__PURE__ */ jsx("span", {
585
+ "data-slot": "media-player-time",
586
+ className: cn("inline-flex shrink-0 items-center px-1 text-gdt-sm font-medium tabular-nums text-fg-on-brand/80", className),
587
+ ...props,
588
+ children: showDuration ? `${formatTime(currentTime)} / ${formatTime(duration)}` : formatTime(currentTime)
589
+ });
900
590
  }
901
- function MediaPlayerSeek({
902
- className,
903
- disabled,
904
- onValueChange,
905
- "aria-label": ariaLabel,
906
- step = 5,
907
- withTime = true,
908
- ...props
909
- }) {
910
- const dispatch = useMediaDispatch();
911
- const { portalContainer } = React.useContext(MediaPlayerContext);
912
- const controlRef = React.useRef(null);
913
- const [dragValue, setDragValue] = React.useState(null);
914
- const [hoverValue, setHoverValue] = React.useState(null);
915
- const [pendingSeek, setPendingSeek] = React.useState(null);
916
- const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
917
- const duration = useMediaSelector((state) => state.mediaDuration);
918
- const seekable = useMediaSelector((state) => state.mediaSeekable);
919
- const buffered = useMediaSelector((state) => state.mediaBuffered);
920
- const previewImage = useMediaSelector((state) => state.mediaPreviewImage);
921
- const previewCoords = useMediaSelector((state) => state.mediaPreviewCoords);
922
- const chapterCues = useMediaSelector((state) => state.mediaChaptersCues);
923
- const { min, max } = getSeekBounds(seekable, duration);
924
- const canSeek = max > min;
925
- const resolvedDisabled = disabled || !canSeek;
926
- const seekRafRef = React.useRef(null);
927
- const latestSeekValueRef = React.useRef(0);
928
- const displayTime = clamp(dragValue ?? pendingSeek ?? currentTime, min, max);
929
- const sliderStep = canSeek ? clamp((max - min) / 500, 0.05, 1) : 1;
930
- React.useEffect(() => {
931
- if (pendingSeek !== null && Math.abs(currentTime - pendingSeek) < 0.5) {
932
- setPendingSeek(null);
933
- }
934
- }, [currentTime, pendingSeek]);
935
- React.useEffect(() => {
936
- return () => {
937
- if (seekRafRef.current !== null) {
938
- cancelAnimationFrame(seekRafRef.current);
939
- }
940
- };
941
- }, []);
942
- const queueSeek = (value) => {
943
- latestSeekValueRef.current = value;
944
- if (seekRafRef.current !== null) {
945
- return;
946
- }
947
- seekRafRef.current = requestAnimationFrame(() => {
948
- seekRafRef.current = null;
949
- dispatch({
950
- type: MediaActionTypes.MEDIA_SEEK_REQUEST,
951
- detail: latestSeekValueRef.current
952
- });
953
- });
954
- };
955
- const previewAt = (value) => {
956
- dispatch({
957
- type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,
958
- detail: clamp(value, min, max)
959
- });
960
- };
961
- const clearPreview = () => {
962
- setHoverValue(null);
963
- dispatch({
964
- type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,
965
- detail: void 0
966
- });
967
- };
968
- const previewValue = dragValue ?? hoverValue;
969
- const previewPercent = previewValue !== null && canSeek ? (clamp(previewValue, min, max) - min) / (max - min) * 100 : 0;
970
- const previewChapter = previewValue !== null ? chapterCues?.find(
971
- (cue) => previewValue >= cue.startTime && previewValue < cue.endTime
972
- ) : void 0;
973
- const previewCoordValues = previewCoords?.map(Number);
974
- const hasSpriteCoords = previewCoordValues?.length === 4 && (previewCoordValues[2] ?? 0) > 0;
975
- const previewScale = hasSpriteCoords ? SEEK_PREVIEW_WIDTH / (previewCoordValues?.[2] ?? 1) : 1;
976
- const previewClampPx = previewImage ? SEEK_PREVIEW_WIDTH / 2 + 4 : 32;
977
- const controlRect = withTime && previewValue !== null ? controlRef.current?.getBoundingClientRect() : void 0;
978
- const previewLeft = controlRect ? clamp(
979
- controlRect.left + previewPercent / 100 * controlRect.width,
980
- controlRect.left + previewClampPx,
981
- controlRect.right - previewClampPx
982
- ) : 0;
983
- const segmentBounds = canSeek ? [
984
- min,
985
- ...(chapterCues ?? []).map((cue) => cue.startTime).filter((time) => time > min && time < max).sort((a, b) => a - b),
986
- max
987
- ] : [0, 1];
988
- const segments = segmentBounds.slice(0, -1).map((start, index) => ({
989
- start,
990
- end: segmentBounds[index + 1] ?? start
991
- }));
992
- return /* @__PURE__ */ jsxs(
993
- SliderPrimitive.Root,
994
- {
995
- "data-slot": "media-player-seek",
996
- className: cn(
997
- "relative flex h-8 w-full min-w-0 touch-none items-center",
998
- resolvedDisabled && "pointer-events-none opacity-45",
999
- className
1000
- ),
1001
- disabled: resolvedDisabled,
1002
- largeStep: step,
1003
- max: canSeek ? max : 100,
1004
- min,
1005
- onValueChange: (value) => {
1006
- const nextValue = Array.isArray(value) ? value[0] ?? 0 : value;
1007
- setDragValue(nextValue);
1008
- previewAt(nextValue);
1009
- queueSeek(nextValue);
1010
- onValueChange?.(nextValue);
1011
- },
1012
- onValueCommitted: (value) => {
1013
- const nextValue = clamp(
1014
- Array.isArray(value) ? value[0] ?? 0 : value,
1015
- min,
1016
- max
1017
- );
1018
- if (seekRafRef.current !== null) {
1019
- cancelAnimationFrame(seekRafRef.current);
1020
- seekRafRef.current = null;
1021
- }
1022
- dispatch({
1023
- type: MediaActionTypes.MEDIA_SEEK_REQUEST,
1024
- detail: nextValue
1025
- });
1026
- setPendingSeek(nextValue);
1027
- setDragValue(null);
1028
- clearPreview();
1029
- },
1030
- step: sliderStep,
1031
- value: displayTime,
1032
- ...props,
1033
- children: [
1034
- withTime && previewValue !== null && controlRect && ReactDOM.createPortal(
1035
- /* @__PURE__ */ jsxs(
1036
- "div",
1037
- {
1038
- className: "pointer-events-none fixed z-50 flex -translate-x-1/2 -translate-y-full flex-col items-center overflow-hidden rounded-lg bg-surface-overlay text-fg-primary shadow-2",
1039
- "data-slot": "media-player-seek-preview",
1040
- style: { left: previewLeft, top: controlRect.top - 6 },
1041
- children: [
1042
- previewImage ? hasSpriteCoords ? /* @__PURE__ */ jsx(
1043
- "span",
1044
- {
1045
- className: "relative block overflow-hidden bg-line-subtle/50",
1046
- style: {
1047
- width: SEEK_PREVIEW_WIDTH,
1048
- height: (previewCoordValues?.[3] ?? 0) * previewScale
1049
- },
1050
- children: /* @__PURE__ */ jsx(
1051
- "img",
1052
- {
1053
- src: previewImage,
1054
- alt: "",
1055
- draggable: false,
1056
- className: "absolute top-0 left-0 max-w-none origin-top-left",
1057
- style: {
1058
- transform: `scale(${previewScale}) translate(${-(previewCoordValues?.[0] ?? 0)}px, ${-(previewCoordValues?.[1] ?? 0)}px)`
1059
- }
1060
- }
1061
- )
1062
- }
1063
- ) : /* @__PURE__ */ jsx(
1064
- "img",
1065
- {
1066
- src: previewImage,
1067
- alt: "",
1068
- draggable: false,
1069
- className: "block aspect-video bg-line-subtle/50 object-cover",
1070
- style: { width: SEEK_PREVIEW_WIDTH }
1071
- }
1072
- ) : null,
1073
- previewChapter?.text ? /* @__PURE__ */ jsx("span", { className: "max-w-40 truncate px-2.5 pt-1 text-gdt-xs font-bold", children: previewChapter.text }) : null,
1074
- /* @__PURE__ */ jsx(
1075
- "span",
1076
- {
1077
- className: cn(
1078
- "px-2.5 py-1 text-gdt-xs font-bold tabular-nums",
1079
- (previewImage || previewChapter?.text) && "pt-0.5 pb-1.5"
1080
- ),
1081
- children: formatTime(previewValue)
1082
- }
1083
- )
1084
- ]
1085
- }
1086
- ),
1087
- portalContainer ?? document.body
1088
- ),
1089
- /* @__PURE__ */ jsxs(
1090
- SliderPrimitive.Control,
1091
- {
1092
- ref: controlRef,
1093
- className: "flex h-8 w-full items-center",
1094
- onPointerLeave: () => {
1095
- if (dragValue === null) {
1096
- clearPreview();
1097
- }
1098
- },
1099
- onPointerMove: (event) => {
1100
- if (resolvedDisabled || dragValue !== null) {
1101
- return;
1102
- }
1103
- const next = getSeekValueFromPointer(event, min, max);
1104
- setHoverValue(next.value);
1105
- previewAt(next.value);
1106
- },
1107
- children: [
1108
- /* @__PURE__ */ jsx(SliderPrimitive.Track, { className: "relative flex h-2 w-full items-center gap-[3px]", children: segments.map(({ start, end }) => {
1109
- const span = end - start || 1;
1110
- const fraction = (value) => clamp((clamp(value, start, end) - start) / span, 0, 1);
1111
- const playedWidth = fraction(displayTime) * 100;
1112
- return /* @__PURE__ */ jsxs(
1113
- "span",
1114
- {
1115
- "data-slot": "media-player-seek-segment",
1116
- className: "relative h-full overflow-hidden rounded-full bg-line-subtle/65",
1117
- style: { width: `${span / (max - min || 1) * 100}%` },
1118
- children: [
1119
- canSeek && buffered?.map(
1120
- ([bufferStart, bufferEnd]) => bufferEnd > start && bufferStart < end ? /* @__PURE__ */ jsx(
1121
- "span",
1122
- {
1123
- "data-slot": "media-player-seek-buffered",
1124
- className: "absolute inset-y-0 bg-fg-on-brand/25",
1125
- style: {
1126
- left: `${fraction(bufferStart) * 100}%`,
1127
- width: `${(fraction(bufferEnd) - fraction(bufferStart)) * 100}%`
1128
- }
1129
- },
1130
- `${bufferStart}-${bufferEnd}`
1131
- ) : null
1132
- ),
1133
- /* @__PURE__ */ jsx(
1134
- "span",
1135
- {
1136
- "data-slot": "media-player-seek-progress",
1137
- className: cn(
1138
- "absolute inset-y-0 left-0 bg-surface-brand",
1139
- dragValue === null && "transition-[width] duration-200 ease-linear"
1140
- ),
1141
- style: { width: `${playedWidth}%` }
1142
- }
1143
- )
1144
- ]
1145
- },
1146
- start
1147
- );
1148
- }) }),
1149
- /* @__PURE__ */ jsx(
1150
- SliderPrimitive.Thumb,
1151
- {
1152
- getAriaLabel: () => ariaLabel ?? "Media timeline",
1153
- getAriaValueText: (_formatted, value) => `${formatTime(value)} of ${formatTime(max)}`,
1154
- className: cn(
1155
- "size-3.5 -translate-x-1/2 rounded-full bg-surface-overlay shadow-2 ring-2 ring-surface-brand outline-none",
1156
- "transition-[inset-inline-start,transform] duration-200 ease-linear data-[dragging]:scale-125 data-[dragging]:transition-transform",
1157
- "focus-visible:[filter:drop-shadow(0_0_0.25rem_var(--color-line-focus))]"
1158
- )
1159
- }
1160
- )
1161
- ]
1162
- }
1163
- )
1164
- ]
1165
- }
1166
- );
591
+ function MediaPlayerSeek({ className, disabled, onValueChange, "aria-label": ariaLabel, step = 5, withTime = true, ...props }) {
592
+ const dispatch = useMediaDispatch();
593
+ const { portalContainer } = React.useContext(MediaPlayerContext);
594
+ const controlRef = React.useRef(null);
595
+ const [dragValue, setDragValue] = React.useState(null);
596
+ const [hoverValue, setHoverValue] = React.useState(null);
597
+ const [pendingSeek, setPendingSeek] = React.useState(null);
598
+ const currentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
599
+ const duration = useMediaSelector((state) => state.mediaDuration);
600
+ const seekable = useMediaSelector((state) => state.mediaSeekable);
601
+ const buffered = useMediaSelector((state) => state.mediaBuffered);
602
+ const previewImage = useMediaSelector((state) => state.mediaPreviewImage);
603
+ const previewCoords = useMediaSelector((state) => state.mediaPreviewCoords);
604
+ const chapterCues = useMediaSelector((state) => state.mediaChaptersCues);
605
+ const { min, max } = getSeekBounds(seekable, duration);
606
+ const canSeek = max > min;
607
+ const resolvedDisabled = disabled || !canSeek;
608
+ const seekRafRef = React.useRef(null);
609
+ const latestSeekValueRef = React.useRef(0);
610
+ const displayTime = clamp(dragValue ?? pendingSeek ?? currentTime, min, max);
611
+ const sliderStep = canSeek ? clamp((max - min) / 500, .05, 1) : 1;
612
+ React.useEffect(() => {
613
+ if (pendingSeek !== null && Math.abs(currentTime - pendingSeek) < .5) setPendingSeek(null);
614
+ }, [currentTime, pendingSeek]);
615
+ React.useEffect(() => {
616
+ return () => {
617
+ if (seekRafRef.current !== null) cancelAnimationFrame(seekRafRef.current);
618
+ };
619
+ }, []);
620
+ const queueSeek = (value) => {
621
+ latestSeekValueRef.current = value;
622
+ if (seekRafRef.current !== null) return;
623
+ seekRafRef.current = requestAnimationFrame(() => {
624
+ seekRafRef.current = null;
625
+ dispatch({
626
+ type: MediaActionTypes.MEDIA_SEEK_REQUEST,
627
+ detail: latestSeekValueRef.current
628
+ });
629
+ });
630
+ };
631
+ const previewAt = (value) => {
632
+ dispatch({
633
+ type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,
634
+ detail: clamp(value, min, max)
635
+ });
636
+ };
637
+ const clearPreview = () => {
638
+ setHoverValue(null);
639
+ dispatch({
640
+ type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,
641
+ detail: void 0
642
+ });
643
+ };
644
+ const previewValue = dragValue ?? hoverValue;
645
+ const previewPercent = previewValue !== null && canSeek ? (clamp(previewValue, min, max) - min) / (max - min) * 100 : 0;
646
+ const previewChapter = previewValue !== null ? chapterCues?.find((cue) => previewValue >= cue.startTime && previewValue < cue.endTime) : void 0;
647
+ const previewCoordValues = previewCoords?.map(Number);
648
+ const hasSpriteCoords = previewCoordValues?.length === 4 && (previewCoordValues[2] ?? 0) > 0;
649
+ const previewScale = hasSpriteCoords ? SEEK_PREVIEW_WIDTH / (previewCoordValues?.[2] ?? 1) : 1;
650
+ const previewClampPx = previewImage ? 84 : 32;
651
+ const controlRect = withTime && previewValue !== null ? controlRef.current?.getBoundingClientRect() : void 0;
652
+ const previewLeft = controlRect ? clamp(controlRect.left + previewPercent / 100 * controlRect.width, controlRect.left + previewClampPx, controlRect.right - previewClampPx) : 0;
653
+ const segmentBounds = canSeek ? [
654
+ min,
655
+ ...(chapterCues ?? []).map((cue) => cue.startTime).filter((time) => time > min && time < max).sort((a, b) => a - b),
656
+ max
657
+ ] : [0, 1];
658
+ const segments = segmentBounds.slice(0, -1).map((start, index) => ({
659
+ start,
660
+ end: segmentBounds[index + 1] ?? start
661
+ }));
662
+ return /* @__PURE__ */ jsxs(Slider.Root, {
663
+ "data-slot": "media-player-seek",
664
+ className: cn("relative flex h-8 w-full min-w-0 touch-none items-center", resolvedDisabled && "pointer-events-none opacity-45", className),
665
+ disabled: resolvedDisabled,
666
+ largeStep: step,
667
+ max: canSeek ? max : 100,
668
+ min,
669
+ onValueChange: (value) => {
670
+ const nextValue = Array.isArray(value) ? value[0] ?? 0 : value;
671
+ setDragValue(nextValue);
672
+ previewAt(nextValue);
673
+ queueSeek(nextValue);
674
+ onValueChange?.(nextValue);
675
+ },
676
+ onValueCommitted: (value) => {
677
+ const nextValue = clamp(Array.isArray(value) ? value[0] ?? 0 : value, min, max);
678
+ if (seekRafRef.current !== null) {
679
+ cancelAnimationFrame(seekRafRef.current);
680
+ seekRafRef.current = null;
681
+ }
682
+ dispatch({
683
+ type: MediaActionTypes.MEDIA_SEEK_REQUEST,
684
+ detail: nextValue
685
+ });
686
+ setPendingSeek(nextValue);
687
+ setDragValue(null);
688
+ clearPreview();
689
+ },
690
+ step: sliderStep,
691
+ value: displayTime,
692
+ ...props,
693
+ children: [withTime && previewValue !== null && controlRect && ReactDOM.createPortal(/* @__PURE__ */ jsxs("div", {
694
+ className: "pointer-events-none fixed z-50 flex -translate-x-1/2 -translate-y-full flex-col items-center overflow-hidden rounded-lg bg-surface-overlay text-fg-primary shadow-2",
695
+ "data-slot": "media-player-seek-preview",
696
+ style: {
697
+ left: previewLeft,
698
+ top: controlRect.top - 6
699
+ },
700
+ children: [
701
+ previewImage ? hasSpriteCoords ? /* @__PURE__ */ jsx("span", {
702
+ className: "relative block overflow-hidden bg-line-subtle/50",
703
+ style: {
704
+ width: SEEK_PREVIEW_WIDTH,
705
+ height: (previewCoordValues?.[3] ?? 0) * previewScale
706
+ },
707
+ children: /* @__PURE__ */ jsx("img", {
708
+ src: previewImage,
709
+ alt: "",
710
+ draggable: false,
711
+ className: "absolute top-0 left-0 max-w-none origin-top-left",
712
+ style: { transform: `scale(${previewScale}) translate(${-(previewCoordValues?.[0] ?? 0)}px, ${-(previewCoordValues?.[1] ?? 0)}px)` }
713
+ })
714
+ }) : /* @__PURE__ */ jsx("img", {
715
+ src: previewImage,
716
+ alt: "",
717
+ draggable: false,
718
+ className: "block aspect-video bg-line-subtle/50 object-cover",
719
+ style: { width: SEEK_PREVIEW_WIDTH }
720
+ }) : null,
721
+ previewChapter?.text ? /* @__PURE__ */ jsx("span", {
722
+ className: "max-w-40 truncate px-2.5 pt-1 text-gdt-xs font-bold",
723
+ children: previewChapter.text
724
+ }) : null,
725
+ /* @__PURE__ */ jsx("span", {
726
+ className: cn("px-2.5 py-1 text-gdt-xs font-bold tabular-nums", (previewImage || previewChapter?.text) && "pt-0.5 pb-1.5"),
727
+ children: formatTime(previewValue)
728
+ })
729
+ ]
730
+ }), portalContainer ?? document.body), /* @__PURE__ */ jsxs(Slider.Control, {
731
+ ref: controlRef,
732
+ className: "flex h-8 w-full items-center",
733
+ onPointerLeave: () => {
734
+ if (dragValue === null) clearPreview();
735
+ },
736
+ onPointerMove: (event) => {
737
+ if (resolvedDisabled || dragValue !== null) return;
738
+ const next = getSeekValueFromPointer(event, min, max);
739
+ setHoverValue(next.value);
740
+ previewAt(next.value);
741
+ },
742
+ children: [/* @__PURE__ */ jsx(Slider.Track, {
743
+ className: "relative flex h-2 w-full items-center gap-[3px]",
744
+ children: segments.map(({ start, end }) => {
745
+ const span = end - start || 1;
746
+ const fraction = (value) => clamp((clamp(value, start, end) - start) / span, 0, 1);
747
+ const playedWidth = fraction(displayTime) * 100;
748
+ return /* @__PURE__ */ jsxs("span", {
749
+ "data-slot": "media-player-seek-segment",
750
+ className: "relative h-full overflow-hidden rounded-full bg-line-subtle/65",
751
+ style: { width: `${span / (max - min || 1) * 100}%` },
752
+ children: [canSeek && buffered?.map(([bufferStart, bufferEnd]) => bufferEnd > start && bufferStart < end ? /* @__PURE__ */ jsx("span", {
753
+ "data-slot": "media-player-seek-buffered",
754
+ className: "absolute inset-y-0 bg-fg-on-brand/25",
755
+ style: {
756
+ left: `${fraction(bufferStart) * 100}%`,
757
+ width: `${(fraction(bufferEnd) - fraction(bufferStart)) * 100}%`
758
+ }
759
+ }, `${bufferStart}-${bufferEnd}`) : null), /* @__PURE__ */ jsx("span", {
760
+ "data-slot": "media-player-seek-progress",
761
+ className: cn("absolute inset-y-0 left-0 bg-surface-brand", dragValue === null && "transition-[width] duration-200 ease-linear"),
762
+ style: { width: `${playedWidth}%` }
763
+ })]
764
+ }, start);
765
+ })
766
+ }), /* @__PURE__ */ jsx(Slider.Thumb, {
767
+ getAriaLabel: () => ariaLabel ?? "Media timeline",
768
+ getAriaValueText: (_formatted, value) => `${formatTime(value)} of ${formatTime(max)}`,
769
+ className: cn("size-3.5 -translate-x-1/2 rounded-full bg-surface-overlay shadow-2 ring-2 ring-surface-brand outline-none", "transition-[inset-inline-start,transform] duration-200 ease-linear data-[dragging]:scale-125 data-[dragging]:transition-transform", "focus-visible:[filter:drop-shadow(0_0_0.25rem_var(--color-line-focus))]")
770
+ })]
771
+ })]
772
+ });
1167
773
  }
1168
- function MediaPlayerLoop({
1169
- className,
1170
- onClick,
1171
- ...props
1172
- }) {
1173
- const dispatch = useMediaDispatch();
1174
- const loop = useMediaSelector((state) => state.mediaLoop === true);
1175
- return /* @__PURE__ */ jsx(
1176
- MediaControlButton,
1177
- {
1178
- "aria-label": loop ? "Disable loop" : "Enable loop",
1179
- "aria-pressed": loop,
1180
- "data-slot": "media-player-loop",
1181
- className,
1182
- tooltip: loop ? "Disable loop" : "Enable loop",
1183
- onClick: (event) => {
1184
- onClick?.(event);
1185
- if (event.defaultPrevented) {
1186
- return;
1187
- }
1188
- dispatch({
1189
- type: MediaActionTypes.MEDIA_LOOP_REQUEST,
1190
- detail: !loop
1191
- });
1192
- },
1193
- ...props,
1194
- children: /* @__PURE__ */ jsx(Repeat2, {})
1195
- }
1196
- );
774
+ function MediaPlayerLoop({ className, onClick, ...props }) {
775
+ const dispatch = useMediaDispatch();
776
+ const loop = useMediaSelector((state) => state.mediaLoop === true);
777
+ return /* @__PURE__ */ jsx(MediaControlButton, {
778
+ "aria-label": loop ? "Disable loop" : "Enable loop",
779
+ "aria-pressed": loop,
780
+ "data-slot": "media-player-loop",
781
+ className,
782
+ tooltip: loop ? "Disable loop" : "Enable loop",
783
+ onClick: (event) => {
784
+ onClick?.(event);
785
+ if (event.defaultPrevented) return;
786
+ dispatch({
787
+ type: MediaActionTypes.MEDIA_LOOP_REQUEST,
788
+ detail: !loop
789
+ });
790
+ },
791
+ ...props,
792
+ children: /* @__PURE__ */ jsx(Repeat2, {})
793
+ });
1197
794
  }
1198
- function MediaPlayerPiP({
1199
- className,
1200
- disabled,
1201
- onClick,
1202
- ...props
1203
- }) {
1204
- const dispatch = useMediaDispatch();
1205
- const isPip = useMediaSelector((state) => state.mediaIsPip === true);
1206
- const unavailable = useMediaSelector((state) => !!state.mediaPipUnavailable);
1207
- return /* @__PURE__ */ jsx(
1208
- MediaControlButton,
1209
- {
1210
- "aria-label": isPip ? "Exit picture in picture" : "Enter picture in picture",
1211
- "aria-pressed": isPip,
1212
- "data-slot": "media-player-pip",
1213
- className,
1214
- disabled: disabled || unavailable,
1215
- tooltip: isPip ? "Exit picture in picture" : "Picture in picture",
1216
- onClick: (event) => {
1217
- onClick?.(event);
1218
- if (event.defaultPrevented) {
1219
- return;
1220
- }
1221
- dispatch({
1222
- type: isPip ? MediaActionTypes.MEDIA_EXIT_PIP_REQUEST : MediaActionTypes.MEDIA_ENTER_PIP_REQUEST
1223
- });
1224
- },
1225
- ...props,
1226
- children: /* @__PURE__ */ jsx(PictureInPicture2, {})
1227
- }
1228
- );
795
+ function MediaPlayerPiP({ className, disabled, onClick, ...props }) {
796
+ const dispatch = useMediaDispatch();
797
+ const isPip = useMediaSelector((state) => state.mediaIsPip === true);
798
+ const unavailable = useMediaSelector((state) => !!state.mediaPipUnavailable);
799
+ return /* @__PURE__ */ jsx(MediaControlButton, {
800
+ "aria-label": isPip ? "Exit picture in picture" : "Enter picture in picture",
801
+ "aria-pressed": isPip,
802
+ "data-slot": "media-player-pip",
803
+ className,
804
+ disabled: disabled || unavailable,
805
+ tooltip: isPip ? "Exit picture in picture" : "Picture in picture",
806
+ onClick: (event) => {
807
+ onClick?.(event);
808
+ if (event.defaultPrevented) return;
809
+ dispatch({ type: isPip ? MediaActionTypes.MEDIA_EXIT_PIP_REQUEST : MediaActionTypes.MEDIA_ENTER_PIP_REQUEST });
810
+ },
811
+ ...props,
812
+ children: /* @__PURE__ */ jsx(PictureInPicture2, {})
813
+ });
1229
814
  }
1230
- function MediaPlayerFullscreen({
1231
- className,
1232
- disabled,
1233
- onClick,
1234
- ...props
1235
- }) {
1236
- const dispatch = useMediaDispatch();
1237
- const isFullscreen = useMediaSelector(
1238
- (state) => state.mediaIsFullscreen === true
1239
- );
1240
- const unavailable = useMediaSelector(
1241
- (state) => !!state.mediaFullscreenUnavailable
1242
- );
1243
- const Icon = isFullscreen ? Minimize : Maximize;
1244
- return /* @__PURE__ */ jsx(
1245
- MediaControlButton,
1246
- {
1247
- "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
1248
- "aria-pressed": isFullscreen,
1249
- "data-slot": "media-player-fullscreen",
1250
- className,
1251
- disabled: disabled || unavailable,
1252
- shortcut: "F",
1253
- tooltip: isFullscreen ? "Exit fullscreen" : "Fullscreen",
1254
- onClick: (event) => {
1255
- onClick?.(event);
1256
- if (event.defaultPrevented) {
1257
- return;
1258
- }
1259
- dispatch({
1260
- type: isFullscreen ? MediaActionTypes.MEDIA_EXIT_FULLSCREEN_REQUEST : MediaActionTypes.MEDIA_ENTER_FULLSCREEN_REQUEST
1261
- });
1262
- },
1263
- ...props,
1264
- children: /* @__PURE__ */ jsx(Icon, {})
1265
- }
1266
- );
815
+ function MediaPlayerFullscreen({ className, disabled, onClick, ...props }) {
816
+ const dispatch = useMediaDispatch();
817
+ const isFullscreen = useMediaSelector((state) => state.mediaIsFullscreen === true);
818
+ const unavailable = useMediaSelector((state) => !!state.mediaFullscreenUnavailable);
819
+ const Icon = isFullscreen ? Minimize : Maximize;
820
+ return /* @__PURE__ */ jsx(MediaControlButton, {
821
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
822
+ "aria-pressed": isFullscreen,
823
+ "data-slot": "media-player-fullscreen",
824
+ className,
825
+ disabled: disabled || unavailable,
826
+ shortcut: "F",
827
+ tooltip: isFullscreen ? "Exit fullscreen" : "Fullscreen",
828
+ onClick: (event) => {
829
+ onClick?.(event);
830
+ if (event.defaultPrevented) return;
831
+ dispatch({ type: isFullscreen ? MediaActionTypes.MEDIA_EXIT_FULLSCREEN_REQUEST : MediaActionTypes.MEDIA_ENTER_FULLSCREEN_REQUEST });
832
+ },
833
+ ...props,
834
+ children: /* @__PURE__ */ jsx(Icon, {})
835
+ });
1267
836
  }
1268
- var MEDIA_ERROR_CONTENT = {
1269
- 1: {
1270
- title: "Playback interrupted",
1271
- description: "Loading the media was interrupted."
1272
- },
1273
- 2: {
1274
- title: "Connection error",
1275
- description: "A network error kept the media from loading."
1276
- },
1277
- 3: {
1278
- title: "Playback error",
1279
- description: "The media could not be decoded."
1280
- },
1281
- 4: {
1282
- title: "Unsupported format",
1283
- description: "The media format is not supported."
1284
- }
837
+ const MEDIA_ERROR_CONTENT = {
838
+ 1: {
839
+ title: "Playback interrupted",
840
+ description: "Loading the media was interrupted."
841
+ },
842
+ 2: {
843
+ title: "Connection error",
844
+ description: "A network error kept the media from loading."
845
+ },
846
+ 3: {
847
+ title: "Playback error",
848
+ description: "The media could not be decoded."
849
+ },
850
+ 4: {
851
+ title: "Unsupported format",
852
+ description: "The media format is not supported."
853
+ }
1285
854
  };
1286
- function MediaPlayerError({
1287
- children,
1288
- className,
1289
- ...props
1290
- }) {
1291
- const { rootNode } = React.useContext(MediaPlayerContext);
1292
- const mediaError = useMediaSelector((state) => state.mediaError);
1293
- const [sourceFailed, setSourceFailed] = React.useState(false);
1294
- React.useEffect(() => {
1295
- if (!rootNode) {
1296
- return;
1297
- }
1298
- const update = () => {
1299
- const media = rootNode.querySelector("video, audio");
1300
- setSourceFailed(
1301
- !!media && media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE && media.readyState === HTMLMediaElement.HAVE_NOTHING
1302
- );
1303
- };
1304
- update();
1305
- rootNode.addEventListener("error", update, true);
1306
- rootNode.addEventListener("loadstart", update, true);
1307
- rootNode.addEventListener("loadedmetadata", update, true);
1308
- return () => {
1309
- rootNode.removeEventListener("error", update, true);
1310
- rootNode.removeEventListener("loadstart", update, true);
1311
- rootNode.removeEventListener("loadedmetadata", update, true);
1312
- };
1313
- }, [rootNode]);
1314
- const error = mediaError ?? (sourceFailed ? { code: 4 } : null);
1315
- if (!error) {
1316
- return null;
1317
- }
1318
- const content = MEDIA_ERROR_CONTENT[error.code ?? 0] ?? {
1319
- title: "Something went wrong",
1320
- description: "The media could not be played."
1321
- };
1322
- return /* @__PURE__ */ jsx(
1323
- "div",
1324
- {
1325
- "data-slot": "media-player-error",
1326
- role: "alert",
1327
- className: cn(
1328
- "absolute inset-0 z-20 flex flex-col items-center justify-center gap-1 bg-canvas/85 px-6 text-center text-fg-primary backdrop-blur-sm rounded-2xl",
1329
- className
1330
- ),
1331
- ...props,
1332
- children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1333
- /* @__PURE__ */ jsx(
1334
- TriangleAlert,
1335
- {
1336
- "aria-hidden": "true",
1337
- className: "mb-2 size-9 text-fg-danger"
1338
- }
1339
- ),
1340
- /* @__PURE__ */ jsx("p", { className: "text-gdt-lg font-bold", children: content.title }),
1341
- /* @__PURE__ */ jsx("p", { className: "text-gdt-sm text-fg-secondary", children: content.description }),
1342
- /* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-center gap-2", children: [
1343
- /* @__PURE__ */ jsxs(
1344
- MediaControlButton,
1345
- {
1346
- className: "w-auto gap-1.5 px-4 text-gdt-xs font-bold [&_svg]:size-3.5",
1347
- onClick: () => {
1348
- rootNode?.querySelector("video, audio")?.load();
1349
- },
1350
- children: [
1351
- /* @__PURE__ */ jsx(RefreshCcw, {}),
1352
- "Try again"
1353
- ]
1354
- }
1355
- ),
1356
- /* @__PURE__ */ jsxs(
1357
- MediaControlButton,
1358
- {
1359
- className: "w-auto gap-1.5 px-4 text-gdt-xs font-bold [&_svg]:size-3.5",
1360
- onClick: () => {
1361
- window.location.reload();
1362
- },
1363
- children: [
1364
- /* @__PURE__ */ jsx(RotateCcw, {}),
1365
- "Reload page"
1366
- ]
1367
- }
1368
- )
1369
- ] })
1370
- ] })
1371
- }
1372
- );
855
+ function MediaPlayerError({ children, className, ...props }) {
856
+ const { rootNode } = React.useContext(MediaPlayerContext);
857
+ const mediaError = useMediaSelector((state) => state.mediaError);
858
+ const [sourceFailed, setSourceFailed] = React.useState(false);
859
+ React.useEffect(() => {
860
+ if (!rootNode) return;
861
+ const update = () => {
862
+ const media = rootNode.querySelector("video, audio");
863
+ setSourceFailed(!!media && media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE && media.readyState === HTMLMediaElement.HAVE_NOTHING);
864
+ };
865
+ update();
866
+ rootNode.addEventListener("error", update, true);
867
+ rootNode.addEventListener("loadstart", update, true);
868
+ rootNode.addEventListener("loadedmetadata", update, true);
869
+ return () => {
870
+ rootNode.removeEventListener("error", update, true);
871
+ rootNode.removeEventListener("loadstart", update, true);
872
+ rootNode.removeEventListener("loadedmetadata", update, true);
873
+ };
874
+ }, [rootNode]);
875
+ const error = mediaError ?? (sourceFailed ? { code: 4 } : null);
876
+ if (!error) return null;
877
+ const content = MEDIA_ERROR_CONTENT[error.code ?? 0] ?? {
878
+ title: "Something went wrong",
879
+ description: "The media could not be played."
880
+ };
881
+ return /* @__PURE__ */ jsx("div", {
882
+ "data-slot": "media-player-error",
883
+ role: "alert",
884
+ className: cn("absolute inset-0 z-20 flex flex-col items-center justify-center gap-1 bg-canvas/85 px-6 text-center text-fg-primary backdrop-blur-sm rounded-2xl", className),
885
+ ...props,
886
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
887
+ /* @__PURE__ */ jsx(TriangleAlert, {
888
+ "aria-hidden": "true",
889
+ className: "mb-2 size-9 text-fg-danger"
890
+ }),
891
+ /* @__PURE__ */ jsx("p", {
892
+ className: "text-gdt-lg font-bold",
893
+ children: content.title
894
+ }),
895
+ /* @__PURE__ */ jsx("p", {
896
+ className: "text-gdt-sm text-fg-secondary",
897
+ children: content.description
898
+ }),
899
+ /* @__PURE__ */ jsxs("div", {
900
+ className: "mt-4 flex items-center gap-2",
901
+ children: [/* @__PURE__ */ jsxs(MediaControlButton, {
902
+ className: "w-auto gap-1.5 px-4 text-gdt-xs font-bold [&_svg]:size-3.5",
903
+ onClick: () => {
904
+ rootNode?.querySelector("video, audio")?.load();
905
+ },
906
+ children: [/* @__PURE__ */ jsx(RefreshCcw, {}), "Try again"]
907
+ }), /* @__PURE__ */ jsxs(MediaControlButton, {
908
+ className: "w-auto gap-1.5 px-4 text-gdt-xs font-bold [&_svg]:size-3.5",
909
+ onClick: () => {
910
+ window.location.reload();
911
+ },
912
+ children: [/* @__PURE__ */ jsx(RotateCcw, {}), "Reload page"]
913
+ })]
914
+ })
915
+ ] })
916
+ });
1373
917
  }
1374
- export {
1375
- MediaPlayer,
1376
- MediaPlayerAudio,
1377
- MediaPlayerCaptions,
1378
- MediaPlayerControls,
1379
- MediaPlayerControlsOverlay,
1380
- MediaPlayerError,
1381
- MediaPlayerFullscreen,
1382
- MediaPlayerLoop,
1383
- MediaPlayerPiP,
1384
- MediaPlayerPlay,
1385
- MediaPlayerPlaybackSpeed,
1386
- MediaPlayerSeek,
1387
- MediaPlayerSeekBackward,
1388
- MediaPlayerSeekForward,
1389
- MediaPlayerSettings,
1390
- MediaPlayerTime,
1391
- MediaPlayerVideo,
1392
- MediaPlayerVolume
1393
- };
1394
- //# sourceMappingURL=media-player.js.map
918
+ //#endregion
919
+ export { MediaPlayer, MediaPlayerAudio, MediaPlayerCaptions, MediaPlayerControls, MediaPlayerControlsOverlay, MediaPlayerError, MediaPlayerFullscreen, MediaPlayerLoop, MediaPlayerPiP, MediaPlayerPlay, MediaPlayerPlaybackSpeed, MediaPlayerSeek, MediaPlayerSeekBackward, MediaPlayerSeekForward, MediaPlayerSettings, MediaPlayerTime, MediaPlayerVideo, MediaPlayerVolume };