@enigmax/primitives 0.24.0 → 0.26.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 (59) hide show
  1. package/dist/chunk-4SZ5N7BA.js +121 -0
  2. package/dist/chunk-EYZ366LP.js +189 -0
  3. package/dist/chunk-ISXB5RUH.js +380 -0
  4. package/dist/chunk-JJVZYW5C.js +946 -0
  5. package/dist/chunk-KVLSOQTI.js +66 -0
  6. package/dist/chunk-QIIV2QSV.js +108 -0
  7. package/dist/{chunk-QJJ34E4G.js → chunk-RMNXVWQ4.js} +32 -15
  8. package/dist/chunk-RUEQ3TGP.js +155 -0
  9. package/dist/clipboard-menu-BvXJCSFN.d.ts +93 -0
  10. package/dist/{color-OPV3BJV6.js → color-T63FLJNH.js} +183 -120
  11. package/dist/context-BfjYGPnH.d.ts +54 -0
  12. package/dist/{clipboard-menu-B_ouitfS.d.ts → context-menu-D3FtTn7v.d.ts} +1 -91
  13. package/dist/{index-ZPvlf9vs.d.ts → index-qXOkYQCU.d.ts} +4 -0
  14. package/dist/index.d.ts +266 -2
  15. package/dist/index.js +4 -2
  16. package/dist/menu-MSQSE6U5.js +54 -0
  17. package/dist/menu-PNQRTRNV.js +33 -0
  18. package/dist/next/index.d.ts +7 -3
  19. package/dist/next/index.js +11 -7
  20. package/dist/react/context-menu.d.ts +8 -54
  21. package/dist/react/image.d.ts +109 -0
  22. package/dist/react/image.js +3 -0
  23. package/dist/react/index.d.ts +8 -4
  24. package/dist/react/index.js +11 -7
  25. package/dist/react/input.d.ts +1 -1
  26. package/dist/react/input.js +2 -2
  27. package/dist/react/video.d.ts +120 -0
  28. package/dist/react/video.js +3 -0
  29. package/dist/react-router/index.d.ts +7 -3
  30. package/dist/react-router/index.js +11 -7
  31. package/dist/viewer-73LSE6RR.js +3 -0
  32. package/package.json +15 -2
  33. package/recipes/color/styles.css +20 -0
  34. package/recipes/image/styles.css +182 -0
  35. package/recipes/video/styles.css +154 -0
  36. package/registry.json +477 -3
  37. package/src/core/image-viewer.ts +222 -0
  38. package/src/core/player.ts +303 -0
  39. package/src/index.ts +52 -0
  40. package/src/react/image/icons.tsx +55 -0
  41. package/src/react/image/index.tsx +185 -0
  42. package/src/react/image/menu.tsx +76 -0
  43. package/src/react/image/styles.ts +213 -0
  44. package/src/react/image/types.ts +113 -0
  45. package/src/react/image/viewer.tsx +571 -0
  46. package/src/react/index.ts +3 -0
  47. package/src/react/input/color-styles.ts +20 -0
  48. package/src/react/input/color-swatch.tsx +96 -0
  49. package/src/react/input/color.tsx +84 -23
  50. package/src/react/input/index.tsx +27 -4
  51. package/src/react/input/types.ts +4 -0
  52. package/src/react/video/icons.tsx +135 -0
  53. package/src/react/video/index.tsx +724 -0
  54. package/src/react/video/menu.tsx +66 -0
  55. package/src/react/video/rail.tsx +99 -0
  56. package/src/react/video/styles.ts +161 -0
  57. package/src/react/video/types.ts +135 -0
  58. package/dist/chunk-S7EE57YB.js +0 -9
  59. /package/dist/{chunk-MSOCCQGH.js → chunk-T7I6WJSN.js} +0 -0
@@ -0,0 +1,946 @@
1
+ import { SPEEDS, supportsPip, supportsRemote, watchRemote, showCaption, captionTracks, activeCaption, toggleFullscreen, togglePip, promptRemote, isTypingTarget, commandFor, progress, formatTime, bufferedAhead, fullscreenElement, fractionAt } from './chunk-RUEQ3TGP.js';
2
+ import { lazy, useLayoutEffect, useRef, useState, useMemo, useEffect, useCallback, Suspense } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ function Rail({ value, secondary, onChange, onCommit, label, valueText, step = 0.05 }) {
6
+ const ref = useRef(null);
7
+ const [dragging, setDragging] = useState(false);
8
+ const fractionAt2 = useCallback((clientX) => {
9
+ const box = ref.current?.getBoundingClientRect();
10
+ return box ? fractionAt(box, clientX) : 0;
11
+ }, []);
12
+ const onPointerDown = useCallback((event) => {
13
+ if (event.button !== 0 && event.pointerType === "mouse") return;
14
+ event.preventDefault();
15
+ event.currentTarget.focus();
16
+ setDragging(true);
17
+ try {
18
+ event.currentTarget.setPointerCapture(event.pointerId);
19
+ } catch {
20
+ }
21
+ onChange(fractionAt2(event.clientX));
22
+ }, [fractionAt2, onChange]);
23
+ const onPointerMove = useCallback((event) => {
24
+ if (!dragging) return;
25
+ onChange(fractionAt2(event.clientX));
26
+ }, [dragging, fractionAt2, onChange]);
27
+ const end = useCallback((event) => {
28
+ if (!dragging) return;
29
+ setDragging(false);
30
+ onCommit?.(fractionAt2(event.clientX));
31
+ }, [dragging, fractionAt2, onCommit]);
32
+ const onKeyDown = useCallback((event) => {
33
+ const moves = {
34
+ ArrowLeft: -step,
35
+ ArrowDown: -step,
36
+ ArrowRight: step,
37
+ ArrowUp: step
38
+ };
39
+ const by = moves[event.key];
40
+ if (by === void 0 && event.key !== "Home" && event.key !== "End") return;
41
+ event.preventDefault();
42
+ event.stopPropagation();
43
+ const next = event.key === "Home" ? 0 : event.key === "End" ? 1 : Math.min(Math.max(value + (by ?? 0), 0), 1);
44
+ onChange(next);
45
+ onCommit?.(next);
46
+ }, [step, value, onChange, onCommit]);
47
+ const percent = Math.round(value * 100);
48
+ return /* @__PURE__ */ jsxs(
49
+ "div",
50
+ {
51
+ ref,
52
+ "data-enigma-video-rail": "",
53
+ role: "slider",
54
+ tabIndex: 0,
55
+ "aria-label": label,
56
+ "aria-valuemin": 0,
57
+ "aria-valuemax": 100,
58
+ "aria-valuenow": percent,
59
+ "aria-valuetext": valueText ?? `${percent}%`,
60
+ onPointerDown,
61
+ onPointerMove,
62
+ onPointerUp: end,
63
+ onPointerCancel: end,
64
+ onKeyDown,
65
+ children: [
66
+ /* @__PURE__ */ jsxs("div", { "data-enigma-video-track": "", children: [
67
+ secondary !== void 0 && /* @__PURE__ */ jsx("span", { "data-enigma-video-buffer": "", style: { width: `${Math.round(secondary * 100)}%` } }),
68
+ /* @__PURE__ */ jsx("span", { "data-enigma-video-played": "", style: { width: `${percent}%` } })
69
+ ] }),
70
+ /* @__PURE__ */ jsx("span", { "data-enigma-video-knob": "", style: { left: `${percent}%` } })
71
+ ]
72
+ }
73
+ );
74
+ }
75
+ function Glyph({ children, filled = false }) {
76
+ return /* @__PURE__ */ jsx(
77
+ "svg",
78
+ {
79
+ viewBox: "0 0 24 24",
80
+ width: "1em",
81
+ height: "1em",
82
+ fill: filled ? "currentColor" : "none",
83
+ stroke: filled ? "none" : "currentColor",
84
+ strokeWidth: 2,
85
+ strokeLinecap: "round",
86
+ strokeLinejoin: "round",
87
+ "aria-hidden": "true",
88
+ children
89
+ }
90
+ );
91
+ }
92
+ function Play() {
93
+ return /* @__PURE__ */ jsx(Glyph, { filled: true, children: /* @__PURE__ */ jsx("path", { d: "M8 5v14l11-7z" }) });
94
+ }
95
+ function Pause() {
96
+ return /* @__PURE__ */ jsx(Glyph, { filled: true, children: /* @__PURE__ */ jsx("path", { d: "M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" }) });
97
+ }
98
+ function Volume() {
99
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
100
+ /* @__PURE__ */ jsx("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
101
+ /* @__PURE__ */ jsx("path", { d: "M15.5 8.5a5 5 0 0 1 0 7" }),
102
+ /* @__PURE__ */ jsx("path", { d: "M19 5a10 10 0 0 1 0 14" })
103
+ ] });
104
+ }
105
+ function VolumeLow() {
106
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
107
+ /* @__PURE__ */ jsx("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
108
+ /* @__PURE__ */ jsx("path", { d: "M15.5 8.5a5 5 0 0 1 0 7" })
109
+ ] });
110
+ }
111
+ function Muted() {
112
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
113
+ /* @__PURE__ */ jsx("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
114
+ /* @__PURE__ */ jsx("path", { d: "m16 9 6 6M22 9l-6 6" })
115
+ ] });
116
+ }
117
+ function Captions() {
118
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
119
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "5", width: "20", height: "14", rx: "2.5" }),
120
+ /* @__PURE__ */ jsx("path", { d: "M7 11h3M7 15h5M14 11h3M14 15h3" })
121
+ ] });
122
+ }
123
+ function CaptionsOff() {
124
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
125
+ /* @__PURE__ */ jsx("path", { d: "M10.5 5H19.5a2.5 2.5 0 0 1 2.5 2.5v9M18 19H4.5A2.5 2.5 0 0 1 2 16.5v-9A2.5 2.5 0 0 1 4.5 5" }),
126
+ /* @__PURE__ */ jsx("path", { d: "M7 11h3M7 15h5" }),
127
+ /* @__PURE__ */ jsx("path", { d: "m3 3 18 18" })
128
+ ] });
129
+ }
130
+ function Settings() {
131
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
132
+ /* @__PURE__ */ jsx("path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z" }),
133
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
134
+ ] });
135
+ }
136
+ function Pip() {
137
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
138
+ /* @__PURE__ */ jsx("path", { d: "M21 11V6.5A2.5 2.5 0 0 0 18.5 4h-13A2.5 2.5 0 0 0 3 6.5v9A2.5 2.5 0 0 0 5.5 18H9" }),
139
+ /* @__PURE__ */ jsx("rect", { x: "12", y: "12", width: "9", height: "7", rx: "1.5" })
140
+ ] });
141
+ }
142
+ function Cast() {
143
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
144
+ /* @__PURE__ */ jsx("path", { d: "M3 8V6.5A2.5 2.5 0 0 1 5.5 4h13A2.5 2.5 0 0 1 21 6.5v11a2.5 2.5 0 0 1-2.5 2.5H13" }),
145
+ /* @__PURE__ */ jsx("path", { d: "M3 12a8 8 0 0 1 8 8" }),
146
+ /* @__PURE__ */ jsx("path", { d: "M3 16a4 4 0 0 1 4 4" }),
147
+ /* @__PURE__ */ jsx("path", { d: "M3 20h.01" })
148
+ ] });
149
+ }
150
+ function Expand() {
151
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" }) });
152
+ }
153
+ function Collapse() {
154
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" }) });
155
+ }
156
+ function Download() {
157
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 4v11M7 11l5 5 5-5M4 19h16" }) });
158
+ }
159
+ function Check() {
160
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "m5 12.5 4.5 4.5L19 7" }) });
161
+ }
162
+ function Loop() {
163
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
164
+ /* @__PURE__ */ jsx("path", { d: "M17 2.5 20.5 6 17 9.5" }),
165
+ /* @__PURE__ */ jsx("path", { d: "M3.5 12V9a3 3 0 0 1 3-3h14" }),
166
+ /* @__PURE__ */ jsx("path", { d: "M7 21.5 3.5 18 7 14.5" }),
167
+ /* @__PURE__ */ jsx("path", { d: "M20.5 12v3a3 3 0 0 1-3 3h-14" })
168
+ ] });
169
+ }
170
+ function Link() {
171
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
172
+ /* @__PURE__ */ jsx("path", { d: "M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07L11.5 4.4" }),
173
+ /* @__PURE__ */ jsx("path", { d: "M14 11a5 5 0 0 0-7.07 0L4.1 13.83a5 5 0 0 0 7.07 7.07l1.3-1.3" })
174
+ ] });
175
+ }
176
+ function Clock() {
177
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
178
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
179
+ /* @__PURE__ */ jsx("path", { d: "M12 7v5l3.5 2" })
180
+ ] });
181
+ }
182
+ function Speed() {
183
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
184
+ /* @__PURE__ */ jsx("path", { d: "M12 20a8 8 0 1 0-8-8" }),
185
+ /* @__PURE__ */ jsx("path", { d: "M4 20h16" }),
186
+ /* @__PURE__ */ jsx("path", { d: "m12 12 4.5-4.5" })
187
+ ] });
188
+ }
189
+
190
+ // src/react/video/styles.ts
191
+ var VIDEO_STYLES = `
192
+ :root {
193
+ --enigma-video-accent: #3b82f6;
194
+ --enigma-video-bg: #000000;
195
+ --enigma-video-text: #ffffff;
196
+ --enigma-video-muted: rgba(255, 255, 255, 0.72);
197
+ --enigma-video-rail: rgba(255, 255, 255, 0.28);
198
+ --enigma-video-buffer: rgba(255, 255, 255, 0.45);
199
+ --enigma-video-panel-bg: rgba(23, 23, 23, 0.96);
200
+ --enigma-video-radius: 0.5rem;
201
+ --enigma-video-control-size: 2rem;
202
+ /* The icons are sized HERE and not in em of the bar's text: 1.25em of a 13px label is a
203
+ 16px icon, and the same markup in a project with larger type silently grew a different
204
+ player. */
205
+ --enigma-video-icon-size: 1.125rem;
206
+ --enigma-video-rail-height: 0.3125rem;
207
+ --enigma-video-rail-hover: 0.5rem;
208
+ --enigma-video-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
209
+ }
210
+
211
+ [data-enigma-video] {
212
+ position: relative; display: block; width: 100%; overflow: hidden;
213
+ background: var(--enigma-video-bg); border-radius: var(--enigma-video-radius);
214
+ color: var(--enigma-video-text);
215
+ font-size: 0.8125rem; line-height: 1;
216
+ }
217
+ [data-enigma-video] video { display: block; width: 100%; height: 100%; }
218
+ /* Fullscreen needs no sizing of its own: the UA stylesheet pins the fullscreen element to the
219
+ screen with !important, which outranks anything a page or this sheet can say, and a video
220
+ element already fits itself with object-fit: contain. Only the corner is ours. */
221
+ [data-enigma-video][data-fullscreen] { border-radius: 0; }
222
+ [data-enigma-video][data-hidden] { cursor: none; }
223
+
224
+ /* The big one, while nothing is playing. Plyr's shape: a filled circle over the poster,
225
+ large enough to be the obvious target on a phone. */
226
+ [data-enigma-video-big] {
227
+ position: absolute; inset: 0; margin: auto;
228
+ display: grid; place-items: center;
229
+ width: 3.75rem; height: 3.75rem; padding: 0;
230
+ color: var(--enigma-video-text); background: var(--enigma-video-accent);
231
+ border: 0; border-radius: 999px; box-shadow: var(--enigma-video-shadow);
232
+ cursor: pointer;
233
+ }
234
+ [data-enigma-video-big]:focus-visible { outline: 2px solid var(--enigma-video-text); outline-offset: 3px; }
235
+
236
+ [data-enigma-video-overlay] { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
237
+ [data-enigma-video-overlay] > * { pointer-events: auto; }
238
+
239
+ [data-enigma-video-spinner] {
240
+ width: 2rem; height: 2rem;
241
+ border: 2px solid rgba(255, 255, 255, 0.25); border-top-color: var(--enigma-video-text);
242
+ border-radius: 999px; animation: enigma-video-spin 700ms linear infinite;
243
+ }
244
+ @keyframes enigma-video-spin { to { rotate: 360deg; } }
245
+ @media (prefers-reduced-motion: reduce) {
246
+ [data-enigma-video-spinner] { animation-duration: 2s; }
247
+ }
248
+
249
+ [data-enigma-video-controls] {
250
+ position: absolute; inset-inline: 0; bottom: 0;
251
+ display: flex; align-items: center; gap: 0.375rem;
252
+ padding: 2.5rem 0.625rem 0.625rem;
253
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
254
+ transition: opacity 180ms ease, translate 180ms ease;
255
+ }
256
+ [data-enigma-video][data-hidden] [data-enigma-video-controls] { opacity: 0; translate: 0 0.5rem; pointer-events: none; }
257
+ @media (prefers-reduced-motion: reduce) {
258
+ [data-enigma-video-controls] { transition: none; }
259
+ }
260
+
261
+ [data-enigma-video-button] {
262
+ display: grid; place-items: center; flex: none;
263
+ width: var(--enigma-video-control-size); height: var(--enigma-video-control-size);
264
+ padding: 0; color: var(--enigma-video-text); background: none; border: 0; border-radius: 0.375rem;
265
+ cursor: pointer;
266
+ }
267
+ [data-enigma-video-button] svg { width: var(--enigma-video-icon-size); height: var(--enigma-video-icon-size); flex: none; }
268
+ [data-enigma-video-big] svg { width: 1.5rem; height: 1.5rem; }
269
+ [data-enigma-video-button]:hover { background: rgba(255, 255, 255, 0.16); }
270
+ [data-enigma-video-button]:focus-visible { outline: 2px solid var(--enigma-video-text); outline-offset: 1px; }
271
+ [data-enigma-video-button][aria-pressed="true"] { color: var(--enigma-video-accent); }
272
+
273
+ [data-enigma-video-time] { flex: none; color: var(--enigma-video-muted); font-variant-numeric: tabular-nums; }
274
+
275
+ /* The rail. Its hit area is the row, not the 5px line: a scrubber you have to hit exactly is
276
+ one people miss and then seek by accident. */
277
+ [data-enigma-video-rail] {
278
+ position: relative; flex: 1; min-width: 3rem; height: 1rem;
279
+ display: flex; align-items: center;
280
+ cursor: pointer; touch-action: none;
281
+ }
282
+ [data-enigma-video-track] {
283
+ position: relative; width: 100%; height: var(--enigma-video-rail-height);
284
+ background: var(--enigma-video-rail); border-radius: 999px; overflow: hidden;
285
+ transition: height 120ms ease;
286
+ }
287
+ [data-enigma-video-rail]:hover [data-enigma-video-track],
288
+ [data-enigma-video-rail]:focus-visible [data-enigma-video-track] { height: var(--enigma-video-rail-hover); }
289
+ [data-enigma-video-rail]:focus-visible { outline: 2px solid var(--enigma-video-text); outline-offset: 2px; border-radius: 999px; }
290
+ [data-enigma-video-buffer] { position: absolute; inset-block: 0; left: 0; background: var(--enigma-video-buffer); }
291
+ [data-enigma-video-played] { position: absolute; inset-block: 0; left: 0; background: var(--enigma-video-accent); }
292
+ [data-enigma-video-knob] {
293
+ position: absolute; top: 50%; translate: -50% -50%;
294
+ width: 0.75rem; height: 0.75rem; border-radius: 999px;
295
+ background: var(--enigma-video-text); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
296
+ pointer-events: none;
297
+ }
298
+
299
+ /* The volume rail folds away until the control is reached, which is what keeps a narrow
300
+ player from spending a third of its bar on something rarely touched. */
301
+ [data-enigma-video-volume] { display: flex; align-items: center; flex: none; }
302
+ [data-enigma-video-volume] [data-enigma-video-rail] {
303
+ width: 0; min-width: 0; opacity: 0; transition: width 140ms ease, opacity 140ms ease;
304
+ }
305
+ [data-enigma-video-volume]:hover [data-enigma-video-rail],
306
+ [data-enigma-video-volume]:focus-within [data-enigma-video-rail] { width: 4.5rem; min-width: 4.5rem; opacity: 1; }
307
+
308
+ [data-enigma-video-menu] { position: relative; flex: none; }
309
+ /* The panel opens upward, and the player CLIPS - it has to, or a rounded corner is not
310
+ rounded. So its height is bounded by the room above the button that opened it (measured, and
311
+ set inline) and what does not fit scrolls. Without this a speed list plus a language list is
312
+ a panel with its top cut off on any player under about 300px. */
313
+ [data-enigma-video-panel] {
314
+ position: absolute; right: 0; bottom: calc(100% + 0.5rem); z-index: 2;
315
+ box-sizing: border-box; min-width: 8.5rem; max-width: calc(100vw - 2rem); padding: 0.25rem;
316
+ max-height: 14rem; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
317
+ background: var(--enigma-video-panel-bg); border-radius: 0.5rem; box-shadow: var(--enigma-video-shadow);
318
+ }
319
+ [data-enigma-video-option] {
320
+ display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
321
+ width: 100%; padding: 0.4375rem 0.5rem;
322
+ color: var(--enigma-video-text); background: none; border: 0; border-radius: 0.375rem;
323
+ font: inherit; text-align: left; cursor: pointer;
324
+ }
325
+ [data-enigma-video-option]:hover { background: rgba(255, 255, 255, 0.12); }
326
+ [data-enigma-video-option][aria-checked="true"] { color: var(--enigma-video-accent); }
327
+ [data-enigma-video-option]:focus-visible { outline: 2px solid var(--enigma-video-text); outline-offset: -2px; }
328
+ /* Sticky because the panel scrolls: the language you are choosing between should still say
329
+ which list it belongs to when the speeds have scrolled past it. */
330
+ [data-enigma-video-heading] {
331
+ position: sticky; top: 0; z-index: 1; margin: 0;
332
+ padding: 0.375rem 0.5rem; color: var(--enigma-video-muted);
333
+ background: var(--enigma-video-panel-bg);
334
+ font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.06em;
335
+ }
336
+ [data-enigma-video-option] svg { width: 0.875rem; height: 0.875rem; flex: none; }
337
+ [data-enigma-video-option] span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
338
+ `;
339
+
340
+ // src/react/video/types.ts
341
+ var CONTROL_DEFAULTS = {
342
+ play: true,
343
+ progress: true,
344
+ currentTime: true,
345
+ duration: true,
346
+ volume: true,
347
+ captions: true,
348
+ settings: true,
349
+ pip: true,
350
+ cast: true,
351
+ fullscreen: true,
352
+ download: false
353
+ };
354
+ var VideoMenu = lazy(() => import('./menu-PNQRTRNV.js').then((module) => ({ default: module.VideoMenu })));
355
+ var injected = false;
356
+ function injectStyles() {
357
+ if (injected || typeof document === "undefined") return;
358
+ injected = true;
359
+ if (document.querySelector("[data-enigma-video-styles]")) return;
360
+ const element = document.createElement("style");
361
+ element.setAttribute("data-enigma-video-styles", "");
362
+ element.textContent = VIDEO_STYLES;
363
+ document.head.prepend(element);
364
+ }
365
+ function same(a, b) {
366
+ return a.length === b.length && a.every((track, at) => track.index === b[at]?.index && track.label === b[at]?.label);
367
+ }
368
+ var PANEL_GAP = 12;
369
+ var PANEL_MIN = 48;
370
+ function Video(props) {
371
+ const {
372
+ src,
373
+ poster,
374
+ tracks,
375
+ controls = true,
376
+ speeds = SPEEDS,
377
+ keyboard = true,
378
+ clickToPlay = true,
379
+ autoHide = true,
380
+ autoHideDelay = 2600,
381
+ contextMenu = true,
382
+ styles = true,
383
+ labels = {},
384
+ download,
385
+ children,
386
+ wrapperProps,
387
+ ...rest
388
+ } = props;
389
+ useLayoutEffect(() => {
390
+ if (styles) injectStyles();
391
+ }, [styles]);
392
+ const videoRef = useRef(null);
393
+ const wrapperRef = useRef(null);
394
+ const settingsRef = useRef(null);
395
+ const [playing, setPlaying] = useState(false);
396
+ const [waiting, setWaiting] = useState(false);
397
+ const [time, setTime] = useState(0);
398
+ const [duration, setDuration] = useState(0);
399
+ const [buffered, setBuffered] = useState(0);
400
+ const [volume, setVolume] = useState(1);
401
+ const [muted, setMuted] = useState(false);
402
+ const [speed, setSpeed] = useState(1);
403
+ const [looping, setLooping] = useState(false);
404
+ const [captionList, setCaptionList] = useState([]);
405
+ const [caption, setCaption] = useState(-1);
406
+ const [remote, setRemote] = useState(null);
407
+ const [fullscreen, setFullscreen] = useState(false);
408
+ const [menu, setMenu] = useState(false);
409
+ const [panelMax, setPanelMax] = useState(null);
410
+ const [idle, setIdle] = useState(false);
411
+ const [overBar, setOverBar] = useState(false);
412
+ const [armed, setArmed] = useState(false);
413
+ const show = useMemo(() => controls === false ? null : { ...CONTROL_DEFAULTS, ...controls === true ? {} : controls }, [controls]);
414
+ const trackKey = (tracks ?? []).map((track) => `${track.src}|${track.srcLang}|${track.label}`).join("\0");
415
+ const sources = useMemo(() => typeof src === "string" ? [{ src }] : src ?? [], [src]);
416
+ const pip = show?.pip && supportsPip();
417
+ const cast = Boolean(show?.cast && remote?.available);
418
+ const menuOptions = useMemo(() => contextMenu === false ? null : contextMenu === true ? {} : contextMenu, [contextMenu]);
419
+ useEffect(() => {
420
+ const video = videoRef.current;
421
+ if (!video) return;
422
+ const sync = () => {
423
+ setPlaying(!video.paused && !video.ended);
424
+ setVolume(video.volume);
425
+ setMuted(video.muted);
426
+ setSpeed(video.playbackRate);
427
+ setLooping(video.loop);
428
+ };
429
+ const onTime = () => {
430
+ setTime(video.currentTime);
431
+ setBuffered(bufferedAhead(video.buffered, video.currentTime, video.duration));
432
+ };
433
+ const onDuration = () => setDuration(Number.isFinite(video.duration) ? video.duration : 0);
434
+ const events = [
435
+ ["play", sync],
436
+ ["pause", sync],
437
+ ["ended", sync],
438
+ ["volumechange", sync],
439
+ ["ratechange", sync],
440
+ ["timeupdate", onTime],
441
+ ["progress", onTime],
442
+ ["seeked", onTime],
443
+ ["durationchange", onDuration],
444
+ ["loadedmetadata", onDuration],
445
+ ["waiting", () => setWaiting(true)],
446
+ ["playing", () => {
447
+ setWaiting(false);
448
+ sync();
449
+ }],
450
+ ["canplay", () => setWaiting(false)]
451
+ ];
452
+ for (const [name, handler] of events) video.addEventListener(name, handler);
453
+ sync();
454
+ onTime();
455
+ onDuration();
456
+ return () => {
457
+ for (const [name, handler] of events) video.removeEventListener(name, handler);
458
+ };
459
+ }, [sources]);
460
+ useEffect(() => {
461
+ const list = videoRef.current?.textTracks;
462
+ if (!list) return;
463
+ const sync = () => {
464
+ const found = captionTracks(list);
465
+ setCaptionList((current) => same(current, found) ? current : found);
466
+ setCaption(activeCaption(list));
467
+ };
468
+ sync();
469
+ for (const name of ["addtrack", "removetrack", "change"]) list.addEventListener(name, sync);
470
+ return () => {
471
+ for (const name of ["addtrack", "removetrack", "change"]) list.removeEventListener(name, sync);
472
+ };
473
+ }, [trackKey, sources]);
474
+ useEffect(() => {
475
+ const video = videoRef.current;
476
+ if (!video || !supportsRemote(video)) return;
477
+ setRemote({ available: false, connected: false });
478
+ return watchRemote(video, setRemote);
479
+ }, [sources]);
480
+ useEffect(() => {
481
+ const onChange = () => setFullscreen(fullscreenElement() === wrapperRef.current);
482
+ document.addEventListener("fullscreenchange", onChange);
483
+ document.addEventListener("webkitfullscreenchange", onChange);
484
+ return () => {
485
+ document.removeEventListener("fullscreenchange", onChange);
486
+ document.removeEventListener("webkitfullscreenchange", onChange);
487
+ };
488
+ }, []);
489
+ const toggle = useCallback(() => {
490
+ const video = videoRef.current;
491
+ if (!video) return;
492
+ if (video.paused) void video.play().catch(() => setPlaying(false));
493
+ else video.pause();
494
+ }, []);
495
+ const seekTo = useCallback((fraction) => {
496
+ const video = videoRef.current;
497
+ if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return;
498
+ video.currentTime = fraction * video.duration;
499
+ setTime(video.currentTime);
500
+ }, []);
501
+ const seekBy = useCallback((seconds) => {
502
+ const video = videoRef.current;
503
+ if (!video) return;
504
+ video.currentTime = Math.min(Math.max(video.currentTime + seconds, 0), video.duration || 0);
505
+ setTime(video.currentTime);
506
+ }, []);
507
+ const setVolumeTo = useCallback((fraction) => {
508
+ const video = videoRef.current;
509
+ if (!video) return;
510
+ video.volume = Math.min(Math.max(fraction, 0), 1);
511
+ video.muted = video.volume === 0;
512
+ }, []);
513
+ const toggleMute = useCallback(() => {
514
+ const video = videoRef.current;
515
+ if (!video) return;
516
+ video.muted = !video.muted;
517
+ if (!video.muted && video.volume === 0) video.volume = 0.5;
518
+ }, []);
519
+ const preferred = useRef(null);
520
+ const chooseCaption = useCallback((index) => {
521
+ const list = videoRef.current?.textTracks;
522
+ if (!list) return;
523
+ if (index !== -1) preferred.current = index;
524
+ showCaption(list, index);
525
+ setCaption(index);
526
+ }, []);
527
+ const toggleCaptions = useCallback(() => {
528
+ const list = videoRef.current?.textTracks;
529
+ const available = captionTracks(list);
530
+ if (available.length === 0) return;
531
+ const active = activeCaption(list);
532
+ if (active !== -1) return chooseCaption(-1);
533
+ const back = preferred.current;
534
+ chooseCaption(back !== null && available.some((track) => track.index === back) ? back : available[0].index);
535
+ }, [chooseCaption]);
536
+ const setRate = useCallback((rate) => {
537
+ const video = videoRef.current;
538
+ if (!video) return;
539
+ video.playbackRate = rate;
540
+ setSpeed(rate);
541
+ }, []);
542
+ const setLoop = useCallback((on) => {
543
+ const video = videoRef.current;
544
+ if (!video) return;
545
+ video.loop = on;
546
+ setLooping(on);
547
+ }, []);
548
+ const goFullscreen = useCallback(() => {
549
+ const wrapper = wrapperRef.current;
550
+ if (wrapper) void toggleFullscreen(wrapper, videoRef.current);
551
+ }, []);
552
+ const goPip = useCallback(() => {
553
+ const video = videoRef.current;
554
+ if (video) void togglePip(video).catch(() => {
555
+ });
556
+ }, []);
557
+ const goCast = useCallback(() => {
558
+ const video = videoRef.current;
559
+ if (video) void promptRemote(video).catch(() => {
560
+ });
561
+ }, []);
562
+ const linkTo = useCallback((atTime) => {
563
+ const video = videoRef.current;
564
+ const raw = video?.currentSrc || (typeof src === "string" ? src : sources[0]?.src);
565
+ if (!raw) return null;
566
+ try {
567
+ const url = new URL(raw, typeof location === "undefined" ? void 0 : location.href);
568
+ if (atTime) url.hash = `t=${Math.floor(video?.currentTime ?? 0)}`;
569
+ return url.href;
570
+ } catch {
571
+ return raw;
572
+ }
573
+ }, [src, sources]);
574
+ const wake = useCallback(() => {
575
+ setIdle(false);
576
+ setArmed(true);
577
+ }, []);
578
+ const onKeyDown = useCallback((event) => {
579
+ if (!keyboard || event.defaultPrevented || isTypingTarget(event.target)) return;
580
+ const command = commandFor(event.key);
581
+ if (!command) return;
582
+ event.preventDefault();
583
+ wake();
584
+ if (command.type === "toggle") return toggle();
585
+ if (command.type === "seek") return seekBy(command.by);
586
+ if (command.type === "seekTo") return seekTo(command.fraction);
587
+ if (command.type === "volume") return setVolumeTo((videoRef.current?.volume ?? 0) + command.by);
588
+ if (command.type === "mute") return toggleMute();
589
+ if (command.type === "fullscreen") return goFullscreen();
590
+ if (command.type === "captions") return toggleCaptions();
591
+ if (command.type === "pip" && pip) return goPip();
592
+ }, [keyboard, wake, toggle, seekBy, seekTo, setVolumeTo, toggleMute, goFullscreen, toggleCaptions, goPip, pip]);
593
+ useEffect(() => {
594
+ if (!autoHide || !playing || menu || idle || overBar) return;
595
+ const timer = window.setTimeout(() => setIdle(true), autoHideDelay);
596
+ return () => window.clearTimeout(timer);
597
+ }, [autoHide, playing, menu, idle, overBar, autoHideDelay, time]);
598
+ useEffect(() => {
599
+ if (!playing) setIdle(false);
600
+ }, [playing]);
601
+ useEffect(() => {
602
+ if (!menu) return;
603
+ const onPointerDown = (event) => {
604
+ if (!wrapperRef.current?.querySelector("[data-enigma-video-menu]")?.contains(event.target)) setMenu(false);
605
+ };
606
+ document.addEventListener("pointerdown", onPointerDown, true);
607
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
608
+ }, [menu]);
609
+ useLayoutEffect(() => {
610
+ if (!menu) {
611
+ setPanelMax(null);
612
+ return;
613
+ }
614
+ const wrapper = wrapperRef.current;
615
+ const button = settingsRef.current;
616
+ if (!wrapper || !button) return;
617
+ const room = button.getBoundingClientRect().top - wrapper.getBoundingClientRect().top;
618
+ setPanelMax(Math.max(PANEL_MIN, room - PANEL_GAP));
619
+ }, [menu, fullscreen]);
620
+ const [opener, setOpener] = useState(null);
621
+ const onMenuReady = useCallback((open) => setOpener(() => open), []);
622
+ const text = {
623
+ play: labels.play ?? "Play",
624
+ pause: labels.pause ?? "Pause",
625
+ ...labels
626
+ };
627
+ const captionsLabel = labels.captions ?? "Captions";
628
+ const subtitlesLabel = labels.subtitles ?? "Subtitles";
629
+ const offLabel = labels.captionsOff ?? "Off";
630
+ const speedLabel = labels.speed ?? "Speed";
631
+ const normalLabel = labels.normal ?? "Normal";
632
+ const fullscreenLabel = fullscreen ? labels.exitFullscreen ?? "Exit fullscreen" : labels.enterFullscreen ?? "Fullscreen";
633
+ const menuRows = useMemo(() => {
634
+ if (!menuOptions) return [];
635
+ const link = menuOptions.copyUrl !== false && sources.length > 0;
636
+ const blocks = [
637
+ [
638
+ { id: "toggle", label: playing ? text.pause : text.play, icon: playing ? /* @__PURE__ */ jsx(Pause, {}) : /* @__PURE__ */ jsx(Play, {}), shortcut: "K" },
639
+ { id: "loop", label: labels.loop ?? "Loop", icon: /* @__PURE__ */ jsx(Loop, {}), checked: looping },
640
+ {
641
+ id: "speed",
642
+ label: speedLabel,
643
+ icon: /* @__PURE__ */ jsx(Speed, {}),
644
+ items: speeds.map((rate) => ({
645
+ id: `speed:${rate}`,
646
+ label: rate === 1 ? normalLabel : `${rate}x`,
647
+ group: "speed",
648
+ checked: speed === rate
649
+ }))
650
+ },
651
+ ...captionList.length > 0 ? [{
652
+ id: "captions",
653
+ label: subtitlesLabel,
654
+ icon: caption === -1 ? /* @__PURE__ */ jsx(CaptionsOff, {}) : /* @__PURE__ */ jsx(Captions, {}),
655
+ items: [
656
+ { id: "caption:off", label: offLabel, group: "caption", checked: caption === -1 },
657
+ ...captionList.map((track) => ({
658
+ id: `caption:${track.index}`,
659
+ label: track.label,
660
+ group: "caption",
661
+ checked: caption === track.index
662
+ }))
663
+ ]
664
+ }] : []
665
+ ],
666
+ link ? [
667
+ { id: "copy-url", label: labels.copyUrl ?? "Copy video URL", icon: /* @__PURE__ */ jsx(Link, {}) },
668
+ { id: "copy-url-time", label: labels.copyUrlAtTime ?? "Copy video URL at current time", icon: /* @__PURE__ */ jsx(Clock, {}) }
669
+ ] : [],
670
+ [
671
+ ...pip ? [{ id: "pip", label: labels.pip ?? "Picture in picture", icon: /* @__PURE__ */ jsx(Pip, {}), shortcut: "P" }] : [],
672
+ ...cast ? [{
673
+ id: "cast",
674
+ label: remote?.connected ? labels.stopCast ?? "Stop casting" : labels.cast ?? "Play on a TV",
675
+ icon: /* @__PURE__ */ jsx(Cast, {}),
676
+ checked: remote?.connected
677
+ }] : [],
678
+ { id: "fullscreen", label: fullscreenLabel, icon: fullscreen ? /* @__PURE__ */ jsx(Collapse, {}) : /* @__PURE__ */ jsx(Expand, {}), shortcut: "F" }
679
+ ],
680
+ [...menuOptions.items ?? []]
681
+ ];
682
+ return blocks.filter((block) => block.length > 0).flatMap((block, at) => at === 0 ? block : [{ type: "separator" }, ...block]);
683
+ }, [
684
+ menuOptions,
685
+ sources,
686
+ playing,
687
+ text.play,
688
+ text.pause,
689
+ looping,
690
+ labels.loop,
691
+ labels.copyUrl,
692
+ labels.copyUrlAtTime,
693
+ labels.pip,
694
+ labels.cast,
695
+ labels.stopCast,
696
+ speedLabel,
697
+ normalLabel,
698
+ speeds,
699
+ speed,
700
+ captionList,
701
+ caption,
702
+ subtitlesLabel,
703
+ offLabel,
704
+ pip,
705
+ cast,
706
+ remote?.connected,
707
+ fullscreen,
708
+ fullscreenLabel
709
+ ]);
710
+ const onMenuSelect = useCallback((item) => {
711
+ const id = item.id;
712
+ if (id === "toggle") toggle();
713
+ else if (id === "loop") setLoop(!looping);
714
+ else if (id.startsWith("speed:")) setRate(Number(id.slice("speed:".length)));
715
+ else if (id === "caption:off") chooseCaption(-1);
716
+ else if (id.startsWith("caption:")) chooseCaption(Number(id.slice("caption:".length)));
717
+ else if (id === "copy-url" || id === "copy-url-time") {
718
+ const url = linkTo(id === "copy-url-time");
719
+ if (url) void navigator.clipboard?.writeText(url).catch(() => {
720
+ });
721
+ } else if (id === "pip") goPip();
722
+ else if (id === "cast") goCast();
723
+ else if (id === "fullscreen") goFullscreen();
724
+ menuOptions?.onSelect?.(item);
725
+ }, [toggle, setLoop, looping, setRate, chooseCaption, linkTo, goPip, goCast, goFullscreen, menuOptions]);
726
+ const onContextMenu = useCallback((event) => {
727
+ if (!menuOptions || !opener) return;
728
+ event.preventDefault();
729
+ opener({ x: event.clientX, y: event.clientY });
730
+ }, [menuOptions, opener]);
731
+ const played = progress(time, duration);
732
+ return /* @__PURE__ */ jsxs(
733
+ "div",
734
+ {
735
+ ...wrapperProps,
736
+ ref: wrapperRef,
737
+ "data-enigma-video": "",
738
+ "data-playing": playing ? "" : void 0,
739
+ "data-hidden": idle ? "" : void 0,
740
+ "data-fullscreen": fullscreen ? "" : void 0,
741
+ "data-casting": remote?.connected ? "" : void 0,
742
+ "data-menu": opener ? "" : void 0,
743
+ role: "region",
744
+ "aria-label": labels.player ?? "Video player",
745
+ tabIndex: -1,
746
+ onKeyDown,
747
+ onPointerMove: wake,
748
+ onPointerEnter: () => setArmed(true),
749
+ onPointerDown: () => setArmed(true),
750
+ onPointerLeave: () => {
751
+ if (playing && autoHide) setIdle(true);
752
+ },
753
+ onFocus: wake,
754
+ onContextMenu,
755
+ children: [
756
+ /* @__PURE__ */ jsxs(
757
+ "video",
758
+ {
759
+ ...rest,
760
+ ref: videoRef,
761
+ poster,
762
+ src: typeof src === "string" ? src : void 0,
763
+ playsInline: rest.playsInline ?? true,
764
+ onClick: () => {
765
+ if (clickToPlay) toggle();
766
+ },
767
+ children: [
768
+ typeof src !== "string" && sources.map((source) => /* @__PURE__ */ jsx("source", { src: source.src, type: source.type }, source.src)),
769
+ tracks?.map((track) => /* @__PURE__ */ jsx(
770
+ "track",
771
+ {
772
+ src: track.src,
773
+ kind: track.kind ?? "subtitles",
774
+ srcLang: track.srcLang,
775
+ label: track.label,
776
+ default: track.default
777
+ },
778
+ track.src
779
+ ))
780
+ ]
781
+ }
782
+ ),
783
+ /* @__PURE__ */ jsxs("div", { "data-enigma-video-overlay": "", children: [
784
+ waiting && /* @__PURE__ */ jsx("span", { "data-enigma-video-spinner": "", role: "progressbar", "aria-label": labels.player ?? "Video player" }),
785
+ show?.play && !playing && !waiting && /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-video-big": "", "aria-label": text.play, title: text.play, onClick: toggle, children: /* @__PURE__ */ jsx(Play, {}) }),
786
+ children
787
+ ] }),
788
+ show && /* @__PURE__ */ jsxs(
789
+ "div",
790
+ {
791
+ "data-enigma-video-controls": "",
792
+ onPointerEnter: () => setOverBar(true),
793
+ onPointerLeave: () => setOverBar(false),
794
+ children: [
795
+ show.play && /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-video-button": "", "aria-label": playing ? text.pause : text.play, title: playing ? text.pause : text.play, onClick: toggle, children: playing ? /* @__PURE__ */ jsx(Pause, {}) : /* @__PURE__ */ jsx(Play, {}) }),
796
+ show.currentTime && /* @__PURE__ */ jsx("span", { "data-enigma-video-time": "", children: formatTime(time, duration) }),
797
+ show.progress && /* @__PURE__ */ jsx(
798
+ Rail,
799
+ {
800
+ value: played,
801
+ secondary: buffered,
802
+ label: labels.seek ?? "Seek",
803
+ valueText: `${formatTime(time, duration)} of ${formatTime(duration, duration)}`,
804
+ onChange: seekTo
805
+ }
806
+ ),
807
+ show.duration && /* @__PURE__ */ jsx("span", { "data-enigma-video-time": "", children: formatTime(duration, duration) }),
808
+ show.volume && /* @__PURE__ */ jsxs("div", { "data-enigma-video-volume": "", children: [
809
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-video-button": "", "aria-label": muted ? labels.unmute ?? "Unmute" : labels.mute ?? "Mute", title: muted ? labels.unmute ?? "Unmute" : labels.mute ?? "Mute", onClick: toggleMute, children: muted || volume === 0 ? /* @__PURE__ */ jsx(Muted, {}) : volume < 0.5 ? /* @__PURE__ */ jsx(VolumeLow, {}) : /* @__PURE__ */ jsx(Volume, {}) }),
810
+ /* @__PURE__ */ jsx(Rail, { value: muted ? 0 : volume, label: labels.volume ?? "Volume", onChange: setVolumeTo })
811
+ ] }),
812
+ show.captions && captionList.length > 0 && /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-video-button": "", "aria-pressed": caption !== -1, "aria-label": captionsLabel, title: captionsLabel, onClick: toggleCaptions, children: caption === -1 ? /* @__PURE__ */ jsx(CaptionsOff, {}) : /* @__PURE__ */ jsx(Captions, {}) }),
813
+ show.settings && /* @__PURE__ */ jsxs("div", { "data-enigma-video-menu": "", children: [
814
+ /* @__PURE__ */ jsx(
815
+ "button",
816
+ {
817
+ ref: settingsRef,
818
+ type: "button",
819
+ "data-enigma-video-button": "",
820
+ "aria-haspopup": "menu",
821
+ "aria-expanded": menu,
822
+ "aria-label": labels.settings ?? "Settings",
823
+ title: labels.settings ?? "Settings",
824
+ onClick: () => setMenu((current) => !current),
825
+ children: /* @__PURE__ */ jsx(Settings, {})
826
+ }
827
+ ),
828
+ menu && /* @__PURE__ */ jsxs(
829
+ "div",
830
+ {
831
+ "data-enigma-video-panel": "",
832
+ role: "menu",
833
+ "aria-label": labels.settings ?? "Settings",
834
+ style: panelMax === null ? void 0 : { maxHeight: `${panelMax}px` },
835
+ onKeyDown: (event) => {
836
+ if (event.key === "Escape") setMenu(false);
837
+ },
838
+ children: [
839
+ /* @__PURE__ */ jsx("p", { "data-enigma-video-heading": "", children: speedLabel }),
840
+ speeds.map((rate) => /* @__PURE__ */ jsxs(
841
+ "button",
842
+ {
843
+ type: "button",
844
+ role: "menuitemradio",
845
+ "data-enigma-video-option": "speed",
846
+ "aria-checked": speed === rate,
847
+ onClick: () => {
848
+ setRate(rate);
849
+ setMenu(false);
850
+ },
851
+ children: [
852
+ /* @__PURE__ */ jsx("span", { children: rate === 1 ? normalLabel : `${rate}x` }),
853
+ speed === rate && /* @__PURE__ */ jsx(Check, {})
854
+ ]
855
+ },
856
+ rate
857
+ )),
858
+ captionList.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
859
+ /* @__PURE__ */ jsx("p", { "data-enigma-video-heading": "", children: subtitlesLabel }),
860
+ /* @__PURE__ */ jsxs(
861
+ "button",
862
+ {
863
+ type: "button",
864
+ role: "menuitemradio",
865
+ "data-enigma-video-option": "captions",
866
+ "aria-checked": caption === -1,
867
+ onClick: () => {
868
+ chooseCaption(-1);
869
+ setMenu(false);
870
+ },
871
+ children: [
872
+ /* @__PURE__ */ jsx("span", { children: offLabel }),
873
+ caption === -1 && /* @__PURE__ */ jsx(Check, {})
874
+ ]
875
+ }
876
+ ),
877
+ captionList.map((track) => /* @__PURE__ */ jsxs(
878
+ "button",
879
+ {
880
+ type: "button",
881
+ role: "menuitemradio",
882
+ "data-enigma-video-option": "captions",
883
+ "aria-checked": caption === track.index,
884
+ lang: track.language || void 0,
885
+ onClick: () => {
886
+ chooseCaption(track.index);
887
+ setMenu(false);
888
+ },
889
+ children: [
890
+ /* @__PURE__ */ jsx("span", { children: track.label }),
891
+ caption === track.index && /* @__PURE__ */ jsx(Check, {})
892
+ ]
893
+ },
894
+ track.index
895
+ ))
896
+ ] })
897
+ ]
898
+ }
899
+ )
900
+ ] }),
901
+ show.download && /* @__PURE__ */ jsx(
902
+ "a",
903
+ {
904
+ "data-enigma-video-button": "",
905
+ href: download ?? sources[0]?.src ?? "#",
906
+ download: true,
907
+ rel: "noopener",
908
+ "aria-label": labels.download ?? "Download",
909
+ title: labels.download ?? "Download",
910
+ children: /* @__PURE__ */ jsx(Download, {})
911
+ }
912
+ ),
913
+ cast && /* @__PURE__ */ jsx(
914
+ "button",
915
+ {
916
+ type: "button",
917
+ "data-enigma-video-button": "",
918
+ "aria-pressed": remote?.connected ?? false,
919
+ "aria-label": remote?.connected ? labels.stopCast ?? "Stop casting" : labels.cast ?? "Play on a TV",
920
+ title: remote?.connected ? labels.stopCast ?? "Stop casting" : labels.cast ?? "Play on a TV",
921
+ onClick: goCast,
922
+ children: /* @__PURE__ */ jsx(Cast, {})
923
+ }
924
+ ),
925
+ pip && /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-video-button": "", "aria-label": labels.pip ?? "Picture in picture", title: labels.pip ?? "Picture in picture", onClick: goPip, children: /* @__PURE__ */ jsx(Pip, {}) }),
926
+ show.fullscreen && /* @__PURE__ */ jsx(
927
+ "button",
928
+ {
929
+ type: "button",
930
+ "data-enigma-video-button": "",
931
+ "aria-label": fullscreenLabel,
932
+ title: fullscreenLabel,
933
+ onClick: goFullscreen,
934
+ children: fullscreen ? /* @__PURE__ */ jsx(Collapse, {}) : /* @__PURE__ */ jsx(Expand, {})
935
+ }
936
+ )
937
+ ]
938
+ }
939
+ ),
940
+ menuOptions && armed && menuRows.length > 0 && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VideoMenu, { rows: menuRows, title: menuOptions.title, onSelect: onMenuSelect, onReady: onMenuReady }) })
941
+ ]
942
+ }
943
+ );
944
+ }
945
+
946
+ export { Video };