@aglyn/plugins-mui 1.0.0-beta.143

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 (177) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +22 -0
  3. package/package.json +47 -0
  4. package/src/index.d.ts +21 -0
  5. package/src/index.js +22 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/accordion.d.ts +96 -0
  8. package/src/lib/components/accordion.js +392 -0
  9. package/src/lib/components/accordion.js.map +1 -0
  10. package/src/lib/components/app-bar.d.ts +32 -0
  11. package/src/lib/components/app-bar.js +86 -0
  12. package/src/lib/components/app-bar.js.map +1 -0
  13. package/src/lib/components/blocks.d.ts +46 -0
  14. package/src/lib/components/blocks.js +1007 -0
  15. package/src/lib/components/blocks.js.map +1 -0
  16. package/src/lib/components/box.d.ts +47 -0
  17. package/src/lib/components/box.js +125 -0
  18. package/src/lib/components/box.js.map +1 -0
  19. package/src/lib/components/breadcrumbs.d.ts +44 -0
  20. package/src/lib/components/breadcrumbs.js +172 -0
  21. package/src/lib/components/breadcrumbs.js.map +1 -0
  22. package/src/lib/components/button.d.ts +89 -0
  23. package/src/lib/components/button.js +272 -0
  24. package/src/lib/components/button.js.map +1 -0
  25. package/src/lib/components/card.d.ts +61 -0
  26. package/src/lib/components/card.js +336 -0
  27. package/src/lib/components/card.js.map +1 -0
  28. package/src/lib/components/collection.d.ts +514 -0
  29. package/src/lib/components/collection.js +2999 -0
  30. package/src/lib/components/collection.js.map +1 -0
  31. package/src/lib/components/container.d.ts +42 -0
  32. package/src/lib/components/container.js +169 -0
  33. package/src/lib/components/container.js.map +1 -0
  34. package/src/lib/components/custom-html.d.ts +78 -0
  35. package/src/lib/components/custom-html.js +219 -0
  36. package/src/lib/components/custom-html.js.map +1 -0
  37. package/src/lib/components/data-table.d.ts +57 -0
  38. package/src/lib/components/data-table.js +216 -0
  39. package/src/lib/components/data-table.js.map +1 -0
  40. package/src/lib/components/document-root.d.ts +77 -0
  41. package/src/lib/components/document-root.js +118 -0
  42. package/src/lib/components/document-root.js.map +1 -0
  43. package/src/lib/components/drawer.d.ts +93 -0
  44. package/src/lib/components/drawer.js +566 -0
  45. package/src/lib/components/drawer.js.map +1 -0
  46. package/src/lib/components/function-controls.d.ts +66 -0
  47. package/src/lib/components/function-controls.js +207 -0
  48. package/src/lib/components/function-controls.js.map +1 -0
  49. package/src/lib/components/function-scope.d.ts +110 -0
  50. package/src/lib/components/function-scope.js +478 -0
  51. package/src/lib/components/function-scope.js.map +1 -0
  52. package/src/lib/components/function-widget.d.ts +75 -0
  53. package/src/lib/components/function-widget.js +307 -0
  54. package/src/lib/components/function-widget.js.map +1 -0
  55. package/src/lib/components/grid.d.ts +91 -0
  56. package/src/lib/components/grid.js +322 -0
  57. package/src/lib/components/grid.js.map +1 -0
  58. package/src/lib/components/icon.d.ts +54 -0
  59. package/src/lib/components/icon.js +163 -0
  60. package/src/lib/components/icon.js.map +1 -0
  61. package/src/lib/components/image-list.d.ts +58 -0
  62. package/src/lib/components/image-list.js +336 -0
  63. package/src/lib/components/image-list.js.map +1 -0
  64. package/src/lib/components/image.d.ts +154 -0
  65. package/src/lib/components/image.js +451 -0
  66. package/src/lib/components/image.js.map +1 -0
  67. package/src/lib/components/inline-text.d.ts +74 -0
  68. package/src/lib/components/inline-text.js +407 -0
  69. package/src/lib/components/inline-text.js.map +1 -0
  70. package/src/lib/components/language-switcher.d.ts +35 -0
  71. package/src/lib/components/language-switcher.js +179 -0
  72. package/src/lib/components/language-switcher.js.map +1 -0
  73. package/src/lib/components/layout-slot.d.ts +68 -0
  74. package/src/lib/components/layout-slot.js +192 -0
  75. package/src/lib/components/layout-slot.js.map +1 -0
  76. package/src/lib/components/link-box-accessible-name.d.ts +79 -0
  77. package/src/lib/components/link-box-accessible-name.js +158 -0
  78. package/src/lib/components/link-box-accessible-name.js.map +1 -0
  79. package/src/lib/components/link-box.d.ts +66 -0
  80. package/src/lib/components/link-box.js +212 -0
  81. package/src/lib/components/link-box.js.map +1 -0
  82. package/src/lib/components/list-item-text.d.ts +22 -0
  83. package/src/lib/components/list-item-text.js +86 -0
  84. package/src/lib/components/list-item-text.js.map +1 -0
  85. package/src/lib/components/list-item.d.ts +32 -0
  86. package/src/lib/components/list-item.js +127 -0
  87. package/src/lib/components/list-item.js.map +1 -0
  88. package/src/lib/components/list.d.ts +34 -0
  89. package/src/lib/components/list.js +108 -0
  90. package/src/lib/components/list.js.map +1 -0
  91. package/src/lib/components/markdown.d.ts +129 -0
  92. package/src/lib/components/markdown.js +853 -0
  93. package/src/lib/components/markdown.js.map +1 -0
  94. package/src/lib/components/nav-menu.d.ts +68 -0
  95. package/src/lib/components/nav-menu.js +683 -0
  96. package/src/lib/components/nav-menu.js.map +1 -0
  97. package/src/lib/components/pagination.d.ts +59 -0
  98. package/src/lib/components/pagination.js +243 -0
  99. package/src/lib/components/pagination.js.map +1 -0
  100. package/src/lib/components/paper.d.ts +47 -0
  101. package/src/lib/components/paper.js +202 -0
  102. package/src/lib/components/paper.js.map +1 -0
  103. package/src/lib/components/plugin-frame.d.ts +64 -0
  104. package/src/lib/components/plugin-frame.js +335 -0
  105. package/src/lib/components/plugin-frame.js.map +1 -0
  106. package/src/lib/components/plugin.d.ts +95 -0
  107. package/src/lib/components/plugin.js +304 -0
  108. package/src/lib/components/plugin.js.map +1 -0
  109. package/src/lib/components/product.d.ts +40 -0
  110. package/src/lib/components/product.js +287 -0
  111. package/src/lib/components/product.js.map +1 -0
  112. package/src/lib/components/reusable-instance.d.ts +57 -0
  113. package/src/lib/components/reusable-instance.js +126 -0
  114. package/src/lib/components/reusable-instance.js.map +1 -0
  115. package/src/lib/components/screen-link.d.ts +85 -0
  116. package/src/lib/components/screen-link.js +324 -0
  117. package/src/lib/components/screen-link.js.map +1 -0
  118. package/src/lib/components/search-box.d.ts +41 -0
  119. package/src/lib/components/search-box.js +125 -0
  120. package/src/lib/components/search-box.js.map +1 -0
  121. package/src/lib/components/section.d.ts +56 -0
  122. package/src/lib/components/section.js +132 -0
  123. package/src/lib/components/section.js.map +1 -0
  124. package/src/lib/components/stack.d.ts +55 -0
  125. package/src/lib/components/stack.js +309 -0
  126. package/src/lib/components/stack.js.map +1 -0
  127. package/src/lib/components/tabs.d.ts +288 -0
  128. package/src/lib/components/tabs.js +788 -0
  129. package/src/lib/components/tabs.js.map +1 -0
  130. package/src/lib/components/theme-mode-switcher.d.ts +38 -0
  131. package/src/lib/components/theme-mode-switcher.js +168 -0
  132. package/src/lib/components/theme-mode-switcher.js.map +1 -0
  133. package/src/lib/components/toolbar.d.ts +37 -0
  134. package/src/lib/components/toolbar.js +117 -0
  135. package/src/lib/components/toolbar.js.map +1 -0
  136. package/src/lib/components/typography.d.ts +33 -0
  137. package/src/lib/components/typography.js +433 -0
  138. package/src/lib/components/typography.js.map +1 -0
  139. package/src/lib/components/video-lightbox-controls.d.ts +131 -0
  140. package/src/lib/components/video-lightbox-controls.js +536 -0
  141. package/src/lib/components/video-lightbox-controls.js.map +1 -0
  142. package/src/lib/components/video-lightbox.d.ts +60 -0
  143. package/src/lib/components/video-lightbox.js +247 -0
  144. package/src/lib/components/video-lightbox.js.map +1 -0
  145. package/src/lib/components/video-playback-beacon.d.ts +80 -0
  146. package/src/lib/components/video-playback-beacon.js +128 -0
  147. package/src/lib/components/video-playback-beacon.js.map +1 -0
  148. package/src/lib/components/video-player-frame.d.ts +58 -0
  149. package/src/lib/components/video-player-frame.js +51 -0
  150. package/src/lib/components/video-player-frame.js.map +1 -0
  151. package/src/lib/components/video.d.ts +220 -0
  152. package/src/lib/components/video.js +874 -0
  153. package/src/lib/components/video.js.map +1 -0
  154. package/src/lib/constants/bundle-common.d.ts +17 -0
  155. package/src/lib/constants/bundle-common.js +18 -0
  156. package/src/lib/constants/bundle-common.js.map +1 -0
  157. package/src/lib/constants/field-presets.d.ts +93 -0
  158. package/src/lib/constants/field-presets.js +248 -0
  159. package/src/lib/constants/field-presets.js.map +1 -0
  160. package/src/lib/plugin.d.ts +80 -0
  161. package/src/lib/plugin.js +599 -0
  162. package/src/lib/plugin.js.map +1 -0
  163. package/src/lib/utils/author-html-label.d.ts +73 -0
  164. package/src/lib/utils/author-html-label.js +89 -0
  165. package/src/lib/utils/author-html-label.js.map +1 -0
  166. package/src/lib/utils/drop-cleared-props.d.ts +18 -0
  167. package/src/lib/utils/drop-cleared-props.js +74 -0
  168. package/src/lib/utils/drop-cleared-props.js.map +1 -0
  169. package/src/lib/utils/element-picker.d.ts +99 -0
  170. package/src/lib/utils/element-picker.js +140 -0
  171. package/src/lib/utils/element-picker.js.map +1 -0
  172. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  173. package/src/lib/utils/generate-preset-id.js +25 -0
  174. package/src/lib/utils/generate-preset-id.js.map +1 -0
  175. package/src/lib/utils/link-target-props.d.ts +53 -0
  176. package/src/lib/utils/link-target-props.js +53 -0
  177. package/src/lib/utils/link-target-props.js.map +1 -0
@@ -0,0 +1,536 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The lightbox player's own controls (AGL-2802).
18
+ *
19
+ * ## Why not the browser's
20
+ *
21
+ * A `<video controls>` draws its buttons inside the browser's shadow tree, and
22
+ * Chrome's media controls consume `Escape` there before the page is sent
23
+ * anything: from the play, timeline, volume and full-screen stops, no
24
+ * `keydown` or `keyup` for it reaches the document in either phase. MUI's
25
+ * Modal closes on a bubbling `keydown`, so the dialog could not close from the
26
+ * four focus stops a keyboard visitor is on while watching, and no listener,
27
+ * capture phase included, can hear a key the page is never sent.
28
+ *
29
+ * These are ordinary buttons and range inputs. Every key pressed on one is
30
+ * dispatched through the document like any other key, so `Escape` bubbles to
31
+ * the Modal from each of them and focus goes back to the trigger.
32
+ *
33
+ * ## Why a module of its own
34
+ *
35
+ * Only `video-lightbox.tsx` imports it, and nothing else may. That module is
36
+ * reached solely through `lazy(() => import())` in `video.tsx`, so this file,
37
+ * and MUI's Slider with it, travels in the dialog's chunk: fetched by a
38
+ * visitor who asked to watch, never by a page that only shows a poster
39
+ * (AGL-2744).
40
+ *
41
+ * ## The keyboard
42
+ *
43
+ * | key | from | does |
44
+ * | -- | -- | -- |
45
+ * | `Space` | any stop but a button, which `Space` presses | play or pause |
46
+ * | `K` | any stop | play or pause |
47
+ * | `←` `→` | any stop but a slider | back or forward five seconds |
48
+ * | `M` | any stop | mute or unmute |
49
+ * | `F` | any stop | full screen on or off |
50
+ * | arrows | the seek slider | five seconds, ten with `Shift` |
51
+ * | `Page Up` `Page Down` | the seek slider | ten seconds |
52
+ * | `Home` `End` | the seek slider | the start, the end |
53
+ * | arrows | the volume slider | five percent |
54
+ * | `Escape` | any stop | leaves full screen if on, otherwise closes |
55
+ *
56
+ * A key held with `Ctrl`, `Alt` or `Meta` is never a shortcut: those belong
57
+ * to the browser and the operating system.
58
+ */ 'use client';
59
+ import { _ as _extends } from "@swc/helpers/_/_extends";
60
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
61
+ import { mdiClosedCaption, mdiClosedCaptionOutline, mdiFullscreen, mdiFullscreenExit, mdiPause, mdiPlay, mdiVolumeHigh, mdiVolumeOff } from "@aglyn/shared-data-mdi";
62
+ import { MdiIcon } from "@aglyn/shared-ui-jsx";
63
+ import Box from "@mui/material/Box";
64
+ import { touchRippleClasses } from "@mui/material/ButtonBase";
65
+ import IconButton from "@mui/material/IconButton";
66
+ import Slider, { sliderClasses } from "@mui/material/Slider";
67
+ import { useTheme } from "@mui/material/styles";
68
+ import Typography from "@mui/material/Typography";
69
+ import useMediaQuery from "@mui/material/useMediaQuery";
70
+ import { useEffect, useRef, useState } from "react";
71
+ /** One arrow press on the playhead, on the seek slider or from any other stop. */ export const SEEK_STEP_SECONDS = 5;
72
+ /** `Page Up` and `Page Down` on the seek slider, and an arrow held with `Shift`. */ export const SEEK_PAGE_SECONDS = 10;
73
+ /** One arrow press on the volume slider, in percent. */ const VOLUME_STEP_PERCENT = 5;
74
+ const ignore = ()=>undefined;
75
+ /**
76
+ * A player clock: `m:ss`, or `h:mm:ss` from an hour up.
77
+ *
78
+ * A duration the browser does not know yet (`NaN`, before metadata) or cannot
79
+ * know (`Infinity`, a stream) reads as `0:00` rather than as `NaN:NaN`.
80
+ */ export function formatMediaTime(seconds) {
81
+ const whole = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
82
+ const hours = Math.floor(whole / 3600);
83
+ const minutes = Math.floor(whole / 60) % 60;
84
+ const rest = String(whole % 60).padStart(2, '0');
85
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}:${rest}` : `${minutes}:${rest}`;
86
+ }
87
+ /** What the seek slider says its value is: `1:23 of 4:10`. */ export function seekValueText(position, duration) {
88
+ return `${formatMediaTime(position)} of ${formatMediaTime(duration)}`;
89
+ }
90
+ /**
91
+ * Play or pause.
92
+ *
93
+ * `play()` rejects when the browser refuses playback, and the refusal needs
94
+ * no handling: no `play` event fires, so the control goes on reporting a
95
+ * paused film, which is the truth.
96
+ */ export function togglePlayback(video) {
97
+ if (video.paused || video.ended) {
98
+ void Promise.resolve(video.play()).catch(ignore);
99
+ } else {
100
+ video.pause();
101
+ }
102
+ }
103
+ /**
104
+ * Moves the playhead, clamped to the film, and returns where it landed.
105
+ * Before the duration is known there is nowhere to land, and nothing moves.
106
+ */ export function seekPlayback(video, seconds) {
107
+ const { duration } = video;
108
+ if (!Number.isFinite(duration) || duration <= 0) return undefined;
109
+ const landed = Math.min(Math.max(seconds, 0), duration);
110
+ video.currentTime = landed;
111
+ return landed;
112
+ }
113
+ export function toggleMute(video) {
114
+ video.muted = !video.muted;
115
+ }
116
+ /**
117
+ * Whether a full-screen control would do anything here.
118
+ *
119
+ * `fullscreenEnabled` is false inside a frame whose embedder did not allow it,
120
+ * and there the control is left out rather than shown doing nothing. A phone
121
+ * with no element full screen at all still gets the video-only kind, which
122
+ * plays under the platform's own controls.
123
+ */ export function fullscreenAvailable() {
124
+ if (typeof document === 'undefined') return false;
125
+ if (document.fullscreenEnabled) return true;
126
+ return !('requestFullscreen' in Element.prototype) && 'webkitEnterFullscreen' in HTMLVideoElement.prototype;
127
+ }
128
+ export function toggleFullscreen({ video, player }) {
129
+ var _this, _phone_webkitEnterFullscreen;
130
+ const doc = (_this = player != null ? player : video) == null ? void 0 : _this.ownerDocument;
131
+ if (!doc) return;
132
+ if (player && doc.fullscreenElement === player) {
133
+ void Promise.resolve(doc.exitFullscreen()).catch(ignore);
134
+ return;
135
+ }
136
+ if (player && doc.fullscreenEnabled) {
137
+ void Promise.resolve(player.requestFullscreen()).catch(ignore);
138
+ return;
139
+ }
140
+ const phone = video;
141
+ phone == null ? void 0 : (_phone_webkitEnterFullscreen = phone.webkitEnterFullscreen) == null ? void 0 : _phone_webkitEnterFullscreen.call(phone);
142
+ }
143
+ /** The tracks a captions toggle turns on and off. */ function captionTracks(video) {
144
+ var _video_textTracks;
145
+ // Absent where a DOM implements no media pipeline.
146
+ const tracks = (_video_textTracks = video.textTracks) != null ? _video_textTracks : [];
147
+ return Array.from(tracks).filter((track)=>track.kind === 'captions' || track.kind === 'subtitles');
148
+ }
149
+ /** The keys a focused slider moves itself with. */ const SLIDER_KEYS = new Set([
150
+ 'ArrowLeft',
151
+ 'ArrowRight',
152
+ 'ArrowUp',
153
+ 'ArrowDown',
154
+ 'PageUp',
155
+ 'PageDown',
156
+ 'Home',
157
+ 'End'
158
+ ]);
159
+ const ARROW_KEYS = new Set([
160
+ 'ArrowLeft',
161
+ 'ArrowRight',
162
+ 'ArrowUp',
163
+ 'ArrowDown'
164
+ ]);
165
+ /**
166
+ * Whether the focused element uses `key` for itself, in which case no shortcut
167
+ * may take it: `Space` and `Enter` press a focused button, the arrows and
168
+ * `Home`/`End` move a focused slider, and a text field wants every key.
169
+ */ export function keyBelongsToFocus(target, key) {
170
+ if (!(target instanceof Element)) return false;
171
+ if (target instanceof HTMLElement && target.isContentEditable) return true;
172
+ if (target.matches('input[type="range"], [role="slider"]')) {
173
+ return SLIDER_KEYS.has(key);
174
+ }
175
+ if (target.matches('button, [role="button"], a[href], summary, input[type="checkbox"], input[type="radio"]')) {
176
+ return key === ' ' || key === 'Enter';
177
+ }
178
+ return target.matches('input, textarea, select');
179
+ }
180
+ const SHORTCUTS = new Map([
181
+ [
182
+ ' ',
183
+ {
184
+ run: togglePlayback,
185
+ repeats: false
186
+ }
187
+ ],
188
+ [
189
+ 'k',
190
+ {
191
+ run: togglePlayback,
192
+ repeats: false
193
+ }
194
+ ],
195
+ [
196
+ 'ArrowLeft',
197
+ {
198
+ run: (video)=>seekPlayback(video, video.currentTime - SEEK_STEP_SECONDS),
199
+ repeats: true
200
+ }
201
+ ],
202
+ [
203
+ 'ArrowRight',
204
+ {
205
+ run: (video)=>seekPlayback(video, video.currentTime + SEEK_STEP_SECONDS),
206
+ repeats: true
207
+ }
208
+ ],
209
+ [
210
+ 'm',
211
+ {
212
+ run: toggleMute,
213
+ repeats: false
214
+ }
215
+ ],
216
+ [
217
+ 'f',
218
+ {
219
+ run: (video, player)=>toggleFullscreen({
220
+ video,
221
+ player
222
+ }),
223
+ repeats: false
224
+ }
225
+ ]
226
+ ]);
227
+ /**
228
+ * The dialog's `onKeyDown`: the player's shortcuts, from every stop inside it.
229
+ *
230
+ * Bound on the Modal root rather than on the controls, because focus is not
231
+ * always on a control — a click on the film leaves it on the dialog container
232
+ * — and a key pressed anywhere in the dialog bubbles through the root. MUI
233
+ * calls this before its own `Escape` handling, and nothing here touches
234
+ * `Escape`.
235
+ *
236
+ * A key the focused control uses for itself is left alone, and so is any key
237
+ * a control already claimed: MUI's Slider calls `preventDefault` on the keys
238
+ * it moves with, which is how an arrow on a slider stays that slider's.
239
+ */ export function handlePlayerShortcut(event, { video, player }) {
240
+ const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
241
+ const shortcut = SHORTCUTS.get(key);
242
+ if (!shortcut || !video) return;
243
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing) {
244
+ return;
245
+ }
246
+ if (keyBelongsToFocus(event.target, event.key)) return;
247
+ event.preventDefault();
248
+ if (event.repeat && !shortcut.repeats) return;
249
+ shortcut.run(video, player);
250
+ }
251
+ /** A focus ring that reads on the player's black ground, whatever the site's palette. */ const focusRing = {
252
+ outline: '2px solid',
253
+ outlineColor: 'common.white',
254
+ outlineOffset: 2
255
+ };
256
+ /**
257
+ * Every button on the player, the dialog's close button included.
258
+ *
259
+ * IconButton's own hover rule reads `--IconButton-hoverBg`, `hover: none`
260
+ * reset and all; its default is a tint of `action.active`, which on a light
261
+ * site's palette is black on this black ground and never shows. The ripple is
262
+ * motion, so it goes for a visitor who asked for less, and it is gated in CSS
263
+ * as the poster's play badge is so the setting resolves in both directions.
264
+ */ export const controlButtonSx = (theme)=>({
265
+ color: 'common.white',
266
+ '--IconButton-hoverBg': (theme.vars || theme).palette.grey[800],
267
+ '&:focus-visible': focusRing,
268
+ '@media (prefers-reduced-motion: reduce)': {
269
+ [`& .${touchRippleClasses.root}`]: {
270
+ display: 'none'
271
+ }
272
+ }
273
+ });
274
+ const sliderSx = {
275
+ color: 'common.white',
276
+ // The focused element is the thumb's visually hidden range input, so the
277
+ // ring goes on the thumb MUI marks as focus-visible.
278
+ [`& .${sliderClasses.thumb}.${sliderClasses.focusVisible}`]: focusRing,
279
+ // The thumb glides to each new value; that glide is motion too.
280
+ '@media (prefers-reduced-motion: reduce)': {
281
+ [`& .${sliderClasses.thumb}, & .${sliderClasses.track}`]: {
282
+ transition: 'none'
283
+ }
284
+ }
285
+ };
286
+ /** Every event after which the element reports something the controls draw. */ const MEDIA_EVENTS = [
287
+ 'play',
288
+ 'pause',
289
+ 'ended',
290
+ 'timeupdate',
291
+ 'seeked',
292
+ 'durationchange',
293
+ 'loadedmetadata',
294
+ 'volumechange',
295
+ 'emptied'
296
+ ];
297
+ const snapshotOf = (video)=>({
298
+ paused: video.paused,
299
+ currentTime: video.currentTime,
300
+ duration: video.duration,
301
+ muted: video.muted,
302
+ volume: video.volume
303
+ });
304
+ /**
305
+ * The bar under the film: a seek slider across the top, then play, the clock,
306
+ * mute, volume, captions and full screen.
307
+ *
308
+ * Each control is a real `<button>` or a real range input with a name and a
309
+ * state: `aria-pressed` on the toggles, a spoken `1:23 of 4:10` on the seek
310
+ * slider, `aria-keyshortcuts` where a key does the same thing.
311
+ *
312
+ * What the controls draw is read back off the `<video>` on its own events
313
+ * rather than kept beside it, so a button, a shortcut, a click on the film and
314
+ * the platform's media keys all end in one place, and the play beacon's
315
+ * handlers on the element hear every one of them.
316
+ */ export function VideoLightboxControls(props) {
317
+ const { videoRef, playerRef, captions = false } = props;
318
+ const [media, setMedia] = useState({
319
+ paused: true,
320
+ currentTime: 0,
321
+ duration: Number.NaN,
322
+ muted: false,
323
+ volume: 1
324
+ });
325
+ const [fullscreen, setFullscreen] = useState(false);
326
+ const [captionsShowing, setCaptionsShowing] = useState(captions);
327
+ const [canFullscreen] = useState(fullscreenAvailable);
328
+ const fullscreenButtonRef = useRef(null);
329
+ /**
330
+ * No volume slider at phone width: a phone's volume belongs to its hardware
331
+ * buttons, iOS ignores a page that sets it, and the row has no room for it.
332
+ * Read on the first render, so the slider never flashes in and out on open.
333
+ */ const theme = useTheme();
334
+ const phone = useMediaQuery(theme.breakpoints.down('sm'), {
335
+ noSsr: true
336
+ });
337
+ useEffect(()=>{
338
+ var _tracks_addEventListener;
339
+ const video = videoRef.current;
340
+ if (!video) return undefined;
341
+ const doc = video.ownerDocument;
342
+ const tracks = video.textTracks;
343
+ const syncMedia = ()=>setMedia(snapshotOf(video));
344
+ /**
345
+ * Full screen moves the player into the top layer, and the browser blurs
346
+ * whatever had focus outside it: the dialog's own surface, where focus
347
+ * lands on open, is outside it. Focus then sits on the body, outside the
348
+ * Modal, where no shortcut and no `Escape` reaches the dialog. So focus
349
+ * follows the player in, onto the control that toggles it, and is put
350
+ * back there on the way out if the browser dropped it again.
351
+ */ const syncFullscreen = ()=>{
352
+ const player = playerRef.current;
353
+ setFullscreen(player !== null && doc.fullscreenElement === player);
354
+ if (player && !player.contains(doc.activeElement)) {
355
+ var _fullscreenButtonRef_current;
356
+ (_fullscreenButtonRef_current = fullscreenButtonRef.current) == null ? void 0 : _fullscreenButtonRef_current.focus();
357
+ }
358
+ };
359
+ const syncCaptions = ()=>setCaptionsShowing(captionTracks(video).some((track)=>track.mode === 'showing'));
360
+ syncMedia();
361
+ for (const type of MEDIA_EVENTS)video.addEventListener(type, syncMedia);
362
+ doc.addEventListener('fullscreenchange', syncFullscreen);
363
+ tracks == null ? void 0 : (_tracks_addEventListener = tracks.addEventListener) == null ? void 0 : _tracks_addEventListener.call(tracks, 'change', syncCaptions);
364
+ return ()=>{
365
+ var _tracks_removeEventListener;
366
+ for (const type of MEDIA_EVENTS)video.removeEventListener(type, syncMedia);
367
+ doc.removeEventListener('fullscreenchange', syncFullscreen);
368
+ tracks == null ? void 0 : (_tracks_removeEventListener = tracks.removeEventListener) == null ? void 0 : _tracks_removeEventListener.call(tracks, 'change', syncCaptions);
369
+ };
370
+ }, [
371
+ videoRef,
372
+ playerRef
373
+ ]);
374
+ const seekable = Number.isFinite(media.duration) && media.duration > 0;
375
+ const position = seekable ? Math.min(Math.max(media.currentTime, 0), media.duration) : 0;
376
+ const playing = !media.paused;
377
+ const level = media.muted ? 0 : Math.round(media.volume * 100);
378
+ const withVideo = (action)=>()=>{
379
+ const video = videoRef.current;
380
+ if (video) action(video);
381
+ };
382
+ /**
383
+ * MUI's Slider steps an arrow by `step`, which is also the grain a pointer
384
+ * drag snaps to. One second keeps a drag as fine as the clock it moves, and
385
+ * this scales an arrow up to the same five seconds the arrows give from
386
+ * every other stop, in the direction MUI chose, so a right-to-left page's
387
+ * arrows still point the way the track runs.
388
+ */ const onSeek = (event, value)=>{
389
+ const video = videoRef.current;
390
+ if (!video || typeof value !== 'number') return;
391
+ const arrow = event.type === 'keydown' && ARROW_KEYS.has(event.key);
392
+ const step = event.shiftKey ? SEEK_PAGE_SECONDS : SEEK_STEP_SECONDS;
393
+ const target = arrow ? video.currentTime + Math.sign(value - position) * step : value;
394
+ const landed = seekPlayback(video, target);
395
+ // Drawn now rather than on `seeked`, so the thumb does not spring back
396
+ // to where it was for the frames the seek takes.
397
+ if (landed !== undefined) {
398
+ setMedia((previous)=>_extends({}, previous, {
399
+ currentTime: landed
400
+ }));
401
+ }
402
+ };
403
+ /**
404
+ * Zero mutes rather than zeroing the volume, so unmuting brings back the
405
+ * last level the visitor could hear.
406
+ */ const onVolume = (_event, value)=>{
407
+ const video = videoRef.current;
408
+ if (!video || typeof value !== 'number') return;
409
+ if (value <= 0) {
410
+ video.muted = true;
411
+ return;
412
+ }
413
+ video.volume = value / 100;
414
+ video.muted = false;
415
+ };
416
+ const onCaptions = withVideo((video)=>{
417
+ const next = !captionsShowing;
418
+ for (const track of captionTracks(video)){
419
+ track.mode = next ? 'showing' : 'hidden';
420
+ }
421
+ setCaptionsShowing(next);
422
+ });
423
+ return /*#__PURE__*/ _jsxs(Box, {
424
+ children: [
425
+ /*#__PURE__*/ _jsx(Box, {
426
+ sx: {
427
+ px: 2
428
+ },
429
+ children: /*#__PURE__*/ _jsx(Slider, {
430
+ "aria-label": "Seek",
431
+ size: "small",
432
+ min: 0,
433
+ max: seekable ? media.duration : 1,
434
+ step: 1,
435
+ shiftStep: SEEK_PAGE_SECONDS,
436
+ value: position,
437
+ disabled: !seekable,
438
+ getAriaValueText: (value)=>seekValueText(value, media.duration),
439
+ onChange: onSeek,
440
+ sx: sliderSx
441
+ })
442
+ }),
443
+ /*#__PURE__*/ _jsxs(Box, {
444
+ sx: {
445
+ display: 'flex',
446
+ alignItems: 'center',
447
+ gap: 1,
448
+ px: 1,
449
+ pb: 1
450
+ },
451
+ children: [
452
+ /*#__PURE__*/ _jsx(IconButton, {
453
+ "aria-label": "Play",
454
+ "aria-pressed": playing,
455
+ "aria-keyshortcuts": "k Space",
456
+ onClick: withVideo(togglePlayback),
457
+ sx: controlButtonSx,
458
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
459
+ path: playing ? mdiPause.path : mdiPlay.path
460
+ })
461
+ }),
462
+ /*#__PURE__*/ _jsx(Typography, {
463
+ component: "span",
464
+ variant: "body2",
465
+ // The seek slider already speaks this, as `1:23 of 4:10`.
466
+ "aria-hidden": true,
467
+ sx: {
468
+ color: 'common.white',
469
+ fontVariantNumeric: 'tabular-nums',
470
+ whiteSpace: 'nowrap'
471
+ },
472
+ children: `${formatMediaTime(position)} / ${formatMediaTime(media.duration)}`
473
+ }),
474
+ /*#__PURE__*/ _jsx(Box, {
475
+ sx: {
476
+ flexGrow: 1
477
+ }
478
+ }),
479
+ /*#__PURE__*/ _jsx(IconButton, {
480
+ "aria-label": "Mute",
481
+ "aria-pressed": media.muted,
482
+ "aria-keyshortcuts": "m",
483
+ onClick: withVideo(toggleMute),
484
+ sx: controlButtonSx,
485
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
486
+ path: media.muted || media.volume === 0 ? mdiVolumeOff.path : mdiVolumeHigh.path
487
+ })
488
+ }),
489
+ phone ? null : /*#__PURE__*/ _jsx(Slider, {
490
+ "aria-label": "Volume",
491
+ size: "small",
492
+ min: 0,
493
+ max: 100,
494
+ step: VOLUME_STEP_PERCENT,
495
+ value: level,
496
+ getAriaValueText: (value)=>value === 0 ? 'Muted' : `${value}%`,
497
+ onChange: onVolume,
498
+ sx: [
499
+ sliderSx,
500
+ {
501
+ width: theme.spacing(12),
502
+ mx: 1
503
+ }
504
+ ]
505
+ }),
506
+ captions ? /*#__PURE__*/ _jsx(IconButton, {
507
+ "aria-label": "Captions",
508
+ "aria-pressed": captionsShowing,
509
+ onClick: onCaptions,
510
+ sx: controlButtonSx,
511
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
512
+ path: captionsShowing ? mdiClosedCaption.path : mdiClosedCaptionOutline.path
513
+ })
514
+ }) : null,
515
+ canFullscreen ? /*#__PURE__*/ _jsx(IconButton, {
516
+ ref: fullscreenButtonRef,
517
+ "aria-label": "Full screen",
518
+ "aria-pressed": fullscreen,
519
+ "aria-keyshortcuts": "f",
520
+ onClick: ()=>toggleFullscreen({
521
+ video: videoRef.current,
522
+ player: playerRef.current
523
+ }),
524
+ sx: controlButtonSx,
525
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
526
+ path: fullscreen ? mdiFullscreenExit.path : mdiFullscreen.path
527
+ })
528
+ }) : null
529
+ ]
530
+ })
531
+ ]
532
+ });
533
+ }
534
+ VideoLightboxControls.displayName = 'VideoLightboxControls';
535
+
536
+ //# sourceMappingURL=video-lightbox-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/video-lightbox-controls.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The lightbox player's own controls (AGL-2802).\n *\n * ## Why not the browser's\n *\n * A `<video controls>` draws its buttons inside the browser's shadow tree, and\n * Chrome's media controls consume `Escape` there before the page is sent\n * anything: from the play, timeline, volume and full-screen stops, no\n * `keydown` or `keyup` for it reaches the document in either phase. MUI's\n * Modal closes on a bubbling `keydown`, so the dialog could not close from the\n * four focus stops a keyboard visitor is on while watching, and no listener,\n * capture phase included, can hear a key the page is never sent.\n *\n * These are ordinary buttons and range inputs. Every key pressed on one is\n * dispatched through the document like any other key, so `Escape` bubbles to\n * the Modal from each of them and focus goes back to the trigger.\n *\n * ## Why a module of its own\n *\n * Only `video-lightbox.tsx` imports it, and nothing else may. That module is\n * reached solely through `lazy(() => import())` in `video.tsx`, so this file,\n * and MUI's Slider with it, travels in the dialog's chunk: fetched by a\n * visitor who asked to watch, never by a page that only shows a poster\n * (AGL-2744).\n *\n * ## The keyboard\n *\n * | key | from | does |\n * | -- | -- | -- |\n * | `Space` | any stop but a button, which `Space` presses | play or pause |\n * | `K` | any stop | play or pause |\n * | `←` `→` | any stop but a slider | back or forward five seconds |\n * | `M` | any stop | mute or unmute |\n * | `F` | any stop | full screen on or off |\n * | arrows | the seek slider | five seconds, ten with `Shift` |\n * | `Page Up` `Page Down` | the seek slider | ten seconds |\n * | `Home` `End` | the seek slider | the start, the end |\n * | arrows | the volume slider | five percent |\n * | `Escape` | any stop | leaves full screen if on, otherwise closes |\n *\n * A key held with `Ctrl`, `Alt` or `Meta` is never a shortcut: those belong\n * to the browser and the operating system.\n */\n'use client'\n\nimport {\n mdiClosedCaption,\n mdiClosedCaptionOutline,\n mdiFullscreen,\n mdiFullscreenExit,\n mdiPause,\n mdiPlay,\n mdiVolumeHigh,\n mdiVolumeOff,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport Box from '@mui/material/Box'\nimport { touchRippleClasses } from '@mui/material/ButtonBase'\nimport IconButton from '@mui/material/IconButton'\nimport Slider, { sliderClasses } from '@mui/material/Slider'\nimport { type Theme, useTheme } from '@mui/material/styles'\nimport Typography from '@mui/material/Typography'\nimport useMediaQuery from '@mui/material/useMediaQuery'\nimport {\n type KeyboardEvent,\n type RefObject,\n useEffect,\n useRef,\n useState,\n} from 'react'\n\n/** One arrow press on the playhead, on the seek slider or from any other stop. */\nexport const SEEK_STEP_SECONDS = 5\n\n/** `Page Up` and `Page Down` on the seek slider, and an arrow held with `Shift`. */\nexport const SEEK_PAGE_SECONDS = 10\n\n/** One arrow press on the volume slider, in percent. */\nconst VOLUME_STEP_PERCENT = 5\n\nconst ignore = () => undefined\n\n/**\n * A player clock: `m:ss`, or `h:mm:ss` from an hour up.\n *\n * A duration the browser does not know yet (`NaN`, before metadata) or cannot\n * know (`Infinity`, a stream) reads as `0:00` rather than as `NaN:NaN`.\n */\nexport function formatMediaTime(seconds: number): string {\n const whole = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0\n const hours = Math.floor(whole / 3600)\n const minutes = Math.floor(whole / 60) % 60\n const rest = String(whole % 60).padStart(2, '0')\n return hours > 0\n ? `${hours}:${String(minutes).padStart(2, '0')}:${rest}`\n : `${minutes}:${rest}`\n}\n\n/** What the seek slider says its value is: `1:23 of 4:10`. */\nexport function seekValueText(position: number, duration: number): string {\n return `${formatMediaTime(position)} of ${formatMediaTime(duration)}`\n}\n\n/** The two elements the controls and the keyboard act on. */\nexport interface LightboxPlayerElements {\n video: HTMLVideoElement | null\n /**\n * The frame that goes full screen: the film AND these controls, so they are\n * still on screen, and still reachable from the keyboard, once it has.\n */\n player: HTMLElement | null\n}\n\n/**\n * Play or pause.\n *\n * `play()` rejects when the browser refuses playback, and the refusal needs\n * no handling: no `play` event fires, so the control goes on reporting a\n * paused film, which is the truth.\n */\nexport function togglePlayback(video: HTMLVideoElement): void {\n if (video.paused || video.ended) {\n void Promise.resolve(video.play()).catch(ignore)\n } else {\n video.pause()\n }\n}\n\n/**\n * Moves the playhead, clamped to the film, and returns where it landed.\n * Before the duration is known there is nowhere to land, and nothing moves.\n */\nexport function seekPlayback(\n video: HTMLVideoElement,\n seconds: number,\n): number | undefined {\n const { duration } = video\n if (!Number.isFinite(duration) || duration <= 0) return undefined\n const landed = Math.min(Math.max(seconds, 0), duration)\n video.currentTime = landed\n return landed\n}\n\nexport function toggleMute(video: HTMLVideoElement): void {\n video.muted = !video.muted\n}\n\n/** iOS Safari on a phone, which can put only the `<video>` itself full screen. */\ntype PhoneVideoElement = HTMLVideoElement & { webkitEnterFullscreen?: () => void }\n\n/**\n * Whether a full-screen control would do anything here.\n *\n * `fullscreenEnabled` is false inside a frame whose embedder did not allow it,\n * and there the control is left out rather than shown doing nothing. A phone\n * with no element full screen at all still gets the video-only kind, which\n * plays under the platform's own controls.\n */\nexport function fullscreenAvailable(): boolean {\n if (typeof document === 'undefined') return false\n if (document.fullscreenEnabled) return true\n return (\n !('requestFullscreen' in Element.prototype) &&\n 'webkitEnterFullscreen' in HTMLVideoElement.prototype\n )\n}\n\nexport function toggleFullscreen({ video, player }: LightboxPlayerElements): void {\n const doc = (player ?? video)?.ownerDocument\n if (!doc) return\n if (player && doc.fullscreenElement === player) {\n void Promise.resolve(doc.exitFullscreen()).catch(ignore)\n return\n }\n if (player && doc.fullscreenEnabled) {\n void Promise.resolve(player.requestFullscreen()).catch(ignore)\n return\n }\n const phone = video as PhoneVideoElement | null\n phone?.webkitEnterFullscreen?.()\n}\n\n/** The tracks a captions toggle turns on and off. */\nfunction captionTracks(video: HTMLVideoElement): TextTrack[] {\n // Absent where a DOM implements no media pipeline.\n const tracks: ArrayLike<TextTrack> = video.textTracks ?? []\n return Array.from(tracks).filter(\n (track) => track.kind === 'captions' || track.kind === 'subtitles',\n )\n}\n\n/** The keys a focused slider moves itself with. */\nconst SLIDER_KEYS = new Set([\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'ArrowDown',\n 'PageUp',\n 'PageDown',\n 'Home',\n 'End',\n])\n\nconst ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])\n\n/**\n * Whether the focused element uses `key` for itself, in which case no shortcut\n * may take it: `Space` and `Enter` press a focused button, the arrows and\n * `Home`/`End` move a focused slider, and a text field wants every key.\n */\nexport function keyBelongsToFocus(\n target: EventTarget | null,\n key: string,\n): boolean {\n if (!(target instanceof Element)) return false\n if (target instanceof HTMLElement && target.isContentEditable) return true\n if (target.matches('input[type=\"range\"], [role=\"slider\"]')) {\n return SLIDER_KEYS.has(key)\n }\n if (\n target.matches(\n 'button, [role=\"button\"], a[href], summary, input[type=\"checkbox\"], input[type=\"radio\"]',\n )\n ) {\n return key === ' ' || key === 'Enter'\n }\n return target.matches('input, textarea, select')\n}\n\ninterface Shortcut {\n run: (video: HTMLVideoElement, player: HTMLElement | null) => void\n /** Whether holding the key keeps doing it. A toggle held down must not flicker. */\n repeats: boolean\n}\n\nconst SHORTCUTS = new Map<string, Shortcut>([\n [' ', { run: togglePlayback, repeats: false }],\n ['k', { run: togglePlayback, repeats: false }],\n [\n 'ArrowLeft',\n {\n run: (video) => seekPlayback(video, video.currentTime - SEEK_STEP_SECONDS),\n repeats: true,\n },\n ],\n [\n 'ArrowRight',\n {\n run: (video) => seekPlayback(video, video.currentTime + SEEK_STEP_SECONDS),\n repeats: true,\n },\n ],\n ['m', { run: toggleMute, repeats: false }],\n [\n 'f',\n { run: (video, player) => toggleFullscreen({ video, player }), repeats: false },\n ],\n])\n\n/**\n * The dialog's `onKeyDown`: the player's shortcuts, from every stop inside it.\n *\n * Bound on the Modal root rather than on the controls, because focus is not\n * always on a control — a click on the film leaves it on the dialog container\n * — and a key pressed anywhere in the dialog bubbles through the root. MUI\n * calls this before its own `Escape` handling, and nothing here touches\n * `Escape`.\n *\n * A key the focused control uses for itself is left alone, and so is any key\n * a control already claimed: MUI's Slider calls `preventDefault` on the keys\n * it moves with, which is how an arrow on a slider stays that slider's.\n */\nexport function handlePlayerShortcut(\n event: KeyboardEvent<Element>,\n { video, player }: LightboxPlayerElements,\n): void {\n const key = event.key.length === 1 ? event.key.toLowerCase() : event.key\n const shortcut = SHORTCUTS.get(key)\n if (!shortcut || !video) return\n if (\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.nativeEvent.isComposing\n ) {\n return\n }\n if (keyBelongsToFocus(event.target, event.key)) return\n event.preventDefault()\n if (event.repeat && !shortcut.repeats) return\n shortcut.run(video, player)\n}\n\n/** A focus ring that reads on the player's black ground, whatever the site's palette. */\nconst focusRing = {\n outline: '2px solid',\n outlineColor: 'common.white',\n outlineOffset: 2,\n}\n\n/**\n * Every button on the player, the dialog's close button included.\n *\n * IconButton's own hover rule reads `--IconButton-hoverBg`, `hover: none`\n * reset and all; its default is a tint of `action.active`, which on a light\n * site's palette is black on this black ground and never shows. The ripple is\n * motion, so it goes for a visitor who asked for less, and it is gated in CSS\n * as the poster's play badge is so the setting resolves in both directions.\n */\nexport const controlButtonSx = (theme: Theme) => ({\n color: 'common.white',\n '--IconButton-hoverBg': (theme.vars || theme).palette.grey[800],\n '&:focus-visible': focusRing,\n '@media (prefers-reduced-motion: reduce)': {\n [`& .${touchRippleClasses.root}`]: { display: 'none' },\n },\n})\n\nconst sliderSx = {\n color: 'common.white',\n // The focused element is the thumb's visually hidden range input, so the\n // ring goes on the thumb MUI marks as focus-visible.\n [`& .${sliderClasses.thumb}.${sliderClasses.focusVisible}`]: focusRing,\n // The thumb glides to each new value; that glide is motion too.\n '@media (prefers-reduced-motion: reduce)': {\n [`& .${sliderClasses.thumb}, & .${sliderClasses.track}`]: {\n transition: 'none',\n },\n },\n}\n\n/** The film's state as the controls draw it. */\ninterface MediaSnapshot {\n paused: boolean\n currentTime: number\n duration: number\n muted: boolean\n volume: number\n}\n\n/** Every event after which the element reports something the controls draw. */\nconst MEDIA_EVENTS = [\n 'play',\n 'pause',\n 'ended',\n 'timeupdate',\n 'seeked',\n 'durationchange',\n 'loadedmetadata',\n 'volumechange',\n 'emptied',\n] as const\n\nconst snapshotOf = (video: HTMLVideoElement): MediaSnapshot => ({\n paused: video.paused,\n currentTime: video.currentTime,\n duration: video.duration,\n muted: video.muted,\n volume: video.volume,\n})\n\nexport interface VideoLightboxControlsProps {\n videoRef: RefObject<HTMLVideoElement | null>\n /** See {@link LightboxPlayerElements.player}. */\n playerRef: RefObject<HTMLElement | null>\n /** The film carries a captions track, so a toggle for it belongs here. */\n captions?: boolean\n}\n\n/**\n * The bar under the film: a seek slider across the top, then play, the clock,\n * mute, volume, captions and full screen.\n *\n * Each control is a real `<button>` or a real range input with a name and a\n * state: `aria-pressed` on the toggles, a spoken `1:23 of 4:10` on the seek\n * slider, `aria-keyshortcuts` where a key does the same thing.\n *\n * What the controls draw is read back off the `<video>` on its own events\n * rather than kept beside it, so a button, a shortcut, a click on the film and\n * the platform's media keys all end in one place, and the play beacon's\n * handlers on the element hear every one of them.\n */\nexport function VideoLightboxControls(props: VideoLightboxControlsProps) {\n const { videoRef, playerRef, captions = false } = props\n const [media, setMedia] = useState<MediaSnapshot>({\n paused: true,\n currentTime: 0,\n duration: Number.NaN,\n muted: false,\n volume: 1,\n })\n const [fullscreen, setFullscreen] = useState(false)\n const [captionsShowing, setCaptionsShowing] = useState(captions)\n const [canFullscreen] = useState(fullscreenAvailable)\n const fullscreenButtonRef = useRef<HTMLButtonElement>(null)\n /**\n * No volume slider at phone width: a phone's volume belongs to its hardware\n * buttons, iOS ignores a page that sets it, and the row has no room for it.\n * Read on the first render, so the slider never flashes in and out on open.\n */\n const theme = useTheme()\n const phone = useMediaQuery(theme.breakpoints.down('sm'), { noSsr: true })\n\n useEffect(() => {\n const video = videoRef.current\n if (!video) return undefined\n const doc = video.ownerDocument\n const tracks = video.textTracks as TextTrackList | undefined\n const syncMedia = () => setMedia(snapshotOf(video))\n /**\n * Full screen moves the player into the top layer, and the browser blurs\n * whatever had focus outside it: the dialog's own surface, where focus\n * lands on open, is outside it. Focus then sits on the body, outside the\n * Modal, where no shortcut and no `Escape` reaches the dialog. So focus\n * follows the player in, onto the control that toggles it, and is put\n * back there on the way out if the browser dropped it again.\n */\n const syncFullscreen = () => {\n const player = playerRef.current\n setFullscreen(player !== null && doc.fullscreenElement === player)\n if (player && !player.contains(doc.activeElement)) {\n fullscreenButtonRef.current?.focus()\n }\n }\n const syncCaptions = () =>\n setCaptionsShowing(\n captionTracks(video).some((track) => track.mode === 'showing'),\n )\n syncMedia()\n for (const type of MEDIA_EVENTS) video.addEventListener(type, syncMedia)\n doc.addEventListener('fullscreenchange', syncFullscreen)\n tracks?.addEventListener?.('change', syncCaptions)\n return () => {\n for (const type of MEDIA_EVENTS) video.removeEventListener(type, syncMedia)\n doc.removeEventListener('fullscreenchange', syncFullscreen)\n tracks?.removeEventListener?.('change', syncCaptions)\n }\n }, [videoRef, playerRef])\n\n const seekable = Number.isFinite(media.duration) && media.duration > 0\n const position = seekable\n ? Math.min(Math.max(media.currentTime, 0), media.duration)\n : 0\n const playing = !media.paused\n const level = media.muted ? 0 : Math.round(media.volume * 100)\n\n const withVideo = (action: (video: HTMLVideoElement) => void) => () => {\n const video = videoRef.current\n if (video) action(video)\n }\n\n /**\n * MUI's Slider steps an arrow by `step`, which is also the grain a pointer\n * drag snaps to. One second keeps a drag as fine as the clock it moves, and\n * this scales an arrow up to the same five seconds the arrows give from\n * every other stop, in the direction MUI chose, so a right-to-left page's\n * arrows still point the way the track runs.\n */\n const onSeek = (event: Event, value: number | number[]) => {\n const video = videoRef.current\n if (!video || typeof value !== 'number') return\n const arrow =\n event.type === 'keydown' &&\n ARROW_KEYS.has((event as globalThis.KeyboardEvent).key)\n const step = (event as globalThis.KeyboardEvent).shiftKey\n ? SEEK_PAGE_SECONDS\n : SEEK_STEP_SECONDS\n const target = arrow\n ? video.currentTime + Math.sign(value - position) * step\n : value\n const landed = seekPlayback(video, target)\n // Drawn now rather than on `seeked`, so the thumb does not spring back\n // to where it was for the frames the seek takes.\n if (landed !== undefined) {\n setMedia((previous) => ({ ...previous, currentTime: landed }))\n }\n }\n\n /**\n * Zero mutes rather than zeroing the volume, so unmuting brings back the\n * last level the visitor could hear.\n */\n const onVolume = (_event: Event, value: number | number[]) => {\n const video = videoRef.current\n if (!video || typeof value !== 'number') return\n if (value <= 0) {\n video.muted = true\n return\n }\n video.volume = value / 100\n video.muted = false\n }\n\n const onCaptions = withVideo((video) => {\n const next = !captionsShowing\n for (const track of captionTracks(video)) {\n track.mode = next ? 'showing' : 'hidden'\n }\n setCaptionsShowing(next)\n })\n\n return (\n <Box>\n <Box sx={{ px: 2 }}>\n <Slider\n aria-label=\"Seek\"\n size=\"small\"\n min={0}\n max={seekable ? media.duration : 1}\n step={1}\n shiftStep={SEEK_PAGE_SECONDS}\n value={position}\n disabled={!seekable}\n getAriaValueText={(value) => seekValueText(value, media.duration)}\n onChange={onSeek}\n sx={sliderSx}\n />\n </Box>\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 1, pb: 1 }}>\n <IconButton\n aria-label=\"Play\"\n aria-pressed={playing}\n aria-keyshortcuts=\"k Space\"\n onClick={withVideo(togglePlayback)}\n sx={controlButtonSx}\n >\n <MdiIcon path={playing ? mdiPause.path : mdiPlay.path} />\n </IconButton>\n <Typography\n component=\"span\"\n variant=\"body2\"\n // The seek slider already speaks this, as `1:23 of 4:10`.\n aria-hidden\n sx={{\n color: 'common.white',\n fontVariantNumeric: 'tabular-nums',\n whiteSpace: 'nowrap',\n }}\n >\n {`${formatMediaTime(position)} / ${formatMediaTime(media.duration)}`}\n </Typography>\n <Box sx={{ flexGrow: 1 }} />\n <IconButton\n aria-label=\"Mute\"\n aria-pressed={media.muted}\n aria-keyshortcuts=\"m\"\n onClick={withVideo(toggleMute)}\n sx={controlButtonSx}\n >\n <MdiIcon\n path={\n media.muted || media.volume === 0\n ? mdiVolumeOff.path\n : mdiVolumeHigh.path\n }\n />\n </IconButton>\n {phone ? null : (\n <Slider\n aria-label=\"Volume\"\n size=\"small\"\n min={0}\n max={100}\n step={VOLUME_STEP_PERCENT}\n value={level}\n getAriaValueText={(value) => (value === 0 ? 'Muted' : `${value}%`)}\n onChange={onVolume}\n sx={[sliderSx, { width: theme.spacing(12), mx: 1 }]}\n />\n )}\n {captions ? (\n <IconButton\n aria-label=\"Captions\"\n aria-pressed={captionsShowing}\n onClick={onCaptions}\n sx={controlButtonSx}\n >\n <MdiIcon\n path={\n captionsShowing\n ? mdiClosedCaption.path\n : mdiClosedCaptionOutline.path\n }\n />\n </IconButton>\n ) : null}\n {canFullscreen ? (\n <IconButton\n ref={fullscreenButtonRef}\n aria-label=\"Full screen\"\n aria-pressed={fullscreen}\n aria-keyshortcuts=\"f\"\n onClick={() =>\n toggleFullscreen({\n video: videoRef.current,\n player: playerRef.current,\n })\n }\n sx={controlButtonSx}\n >\n <MdiIcon path={fullscreen ? mdiFullscreenExit.path : mdiFullscreen.path} />\n </IconButton>\n ) : null}\n </Box>\n </Box>\n )\n}\nVideoLightboxControls.displayName = 'VideoLightboxControls'\n"],"names":["mdiClosedCaption","mdiClosedCaptionOutline","mdiFullscreen","mdiFullscreenExit","mdiPause","mdiPlay","mdiVolumeHigh","mdiVolumeOff","MdiIcon","Box","touchRippleClasses","IconButton","Slider","sliderClasses","useTheme","Typography","useMediaQuery","useEffect","useRef","useState","SEEK_STEP_SECONDS","SEEK_PAGE_SECONDS","VOLUME_STEP_PERCENT","ignore","undefined","formatMediaTime","seconds","whole","Number","isFinite","Math","floor","hours","minutes","rest","String","padStart","seekValueText","position","duration","togglePlayback","video","paused","ended","Promise","resolve","play","catch","pause","seekPlayback","landed","min","max","currentTime","toggleMute","muted","fullscreenAvailable","document","fullscreenEnabled","Element","prototype","HTMLVideoElement","toggleFullscreen","player","phone","doc","ownerDocument","fullscreenElement","exitFullscreen","requestFullscreen","webkitEnterFullscreen","captionTracks","tracks","textTracks","Array","from","filter","track","kind","SLIDER_KEYS","Set","ARROW_KEYS","keyBelongsToFocus","target","key","HTMLElement","isContentEditable","matches","has","SHORTCUTS","Map","run","repeats","handlePlayerShortcut","event","length","toLowerCase","shortcut","get","defaultPrevented","altKey","ctrlKey","metaKey","nativeEvent","isComposing","preventDefault","repeat","focusRing","outline","outlineColor","outlineOffset","controlButtonSx","theme","color","vars","palette","grey","root","display","sliderSx","thumb","focusVisible","transition","MEDIA_EVENTS","snapshotOf","volume","VideoLightboxControls","props","videoRef","playerRef","captions","media","setMedia","NaN","fullscreen","setFullscreen","captionsShowing","setCaptionsShowing","canFullscreen","fullscreenButtonRef","breakpoints","down","noSsr","current","syncMedia","syncFullscreen","contains","activeElement","focus","syncCaptions","some","mode","type","addEventListener","removeEventListener","seekable","playing","level","round","withVideo","action","onSeek","value","arrow","step","shiftKey","sign","previous","onVolume","_event","onCaptions","next","sx","px","aria-label","size","shiftStep","disabled","getAriaValueText","onChange","alignItems","gap","pb","aria-pressed","aria-keyshortcuts","onClick","path","component","variant","aria-hidden","fontVariantNumeric","whiteSpace","flexGrow","width","spacing","mx","ref","displayName"],"mappings":"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"}