@scarlett-player/ui 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1566 @@
1
+ // src/styles.ts
2
+ var styles = `
3
+ /* ============================================
4
+ Container & Base
5
+ ============================================ */
6
+ .sp-container {
7
+ position: relative;
8
+ width: 100%;
9
+ height: 100%;
10
+ background: #000;
11
+ overflow: hidden;
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13
+ }
14
+
15
+ .sp-container video {
16
+ width: 100%;
17
+ height: 100%;
18
+ display: block;
19
+ object-fit: contain;
20
+ }
21
+
22
+ .sp-container:focus {
23
+ outline: none;
24
+ }
25
+
26
+ /* ============================================
27
+ Gradient Overlay
28
+ ============================================ */
29
+ .sp-gradient {
30
+ position: absolute;
31
+ bottom: 0;
32
+ left: 0;
33
+ right: 0;
34
+ height: 160px;
35
+ background: linear-gradient(
36
+ to top,
37
+ rgba(0, 0, 0, 0.8) 0%,
38
+ rgba(0, 0, 0, 0.4) 50%,
39
+ transparent 100%
40
+ );
41
+ pointer-events: none;
42
+ opacity: 0;
43
+ transition: opacity 0.25s ease;
44
+ z-index: 5;
45
+ }
46
+
47
+ .sp-gradient--visible {
48
+ opacity: 1;
49
+ }
50
+
51
+ /* ============================================
52
+ Controls Container
53
+ ============================================ */
54
+ .sp-controls {
55
+ position: absolute;
56
+ bottom: 0;
57
+ left: 0;
58
+ right: 0;
59
+ display: flex;
60
+ align-items: center;
61
+ padding: 0 12px 12px;
62
+ gap: 4px;
63
+ opacity: 0;
64
+ transform: translateY(4px);
65
+ transition: opacity 0.25s ease, transform 0.25s ease;
66
+ z-index: 10;
67
+ }
68
+
69
+ .sp-controls--visible {
70
+ opacity: 1;
71
+ transform: translateY(0);
72
+ }
73
+
74
+ .sp-controls--hidden {
75
+ opacity: 0;
76
+ transform: translateY(4px);
77
+ pointer-events: none;
78
+ }
79
+
80
+ /* ============================================
81
+ Progress Bar (Above Controls)
82
+ ============================================ */
83
+ .sp-progress-wrapper {
84
+ position: absolute;
85
+ bottom: 48px;
86
+ left: 12px;
87
+ right: 12px;
88
+ height: 20px;
89
+ display: flex;
90
+ align-items: center;
91
+ cursor: pointer;
92
+ z-index: 10;
93
+ opacity: 0;
94
+ transition: opacity 0.25s ease;
95
+ }
96
+
97
+ .sp-progress-wrapper--visible {
98
+ opacity: 1;
99
+ }
100
+
101
+ .sp-progress {
102
+ position: relative;
103
+ width: 100%;
104
+ height: 3px;
105
+ background: rgba(255, 255, 255, 0.3);
106
+ border-radius: 1.5px;
107
+ transition: height 0.15s ease;
108
+ }
109
+
110
+ .sp-progress-wrapper:hover .sp-progress,
111
+ .sp-progress--dragging {
112
+ height: 5px;
113
+ }
114
+
115
+ .sp-progress__track {
116
+ position: absolute;
117
+ top: 0;
118
+ left: 0;
119
+ right: 0;
120
+ bottom: 0;
121
+ border-radius: inherit;
122
+ overflow: hidden;
123
+ }
124
+
125
+ .sp-progress__buffered {
126
+ position: absolute;
127
+ top: 0;
128
+ left: 0;
129
+ height: 100%;
130
+ background: rgba(255, 255, 255, 0.4);
131
+ border-radius: inherit;
132
+ transition: width 0.1s linear;
133
+ }
134
+
135
+ .sp-progress__filled {
136
+ position: absolute;
137
+ top: 0;
138
+ left: 0;
139
+ height: 100%;
140
+ background: var(--sp-accent, #e50914);
141
+ border-radius: inherit;
142
+ }
143
+
144
+ .sp-progress__handle {
145
+ position: absolute;
146
+ top: 50%;
147
+ width: 14px;
148
+ height: 14px;
149
+ background: var(--sp-accent, #e50914);
150
+ border-radius: 50%;
151
+ transform: translate(-50%, -50%) scale(0);
152
+ transition: transform 0.15s ease;
153
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
154
+ }
155
+
156
+ .sp-progress-wrapper:hover .sp-progress__handle,
157
+ .sp-progress--dragging .sp-progress__handle {
158
+ transform: translate(-50%, -50%) scale(1);
159
+ }
160
+
161
+ /* Progress Tooltip */
162
+ .sp-progress__tooltip {
163
+ position: absolute;
164
+ bottom: calc(100% + 8px);
165
+ padding: 6px 10px;
166
+ background: rgba(20, 20, 20, 0.95);
167
+ color: #fff;
168
+ font-size: 12px;
169
+ font-weight: 500;
170
+ font-variant-numeric: tabular-nums;
171
+ border-radius: 4px;
172
+ white-space: nowrap;
173
+ transform: translateX(-50%);
174
+ pointer-events: none;
175
+ opacity: 0;
176
+ transition: opacity 0.15s ease;
177
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
178
+ }
179
+
180
+ .sp-progress-wrapper:hover .sp-progress__tooltip {
181
+ opacity: 1;
182
+ }
183
+
184
+ /* ============================================
185
+ Control Buttons
186
+ ============================================ */
187
+ .sp-control {
188
+ background: none;
189
+ border: none;
190
+ color: rgba(255, 255, 255, 0.9);
191
+ cursor: pointer;
192
+ padding: 8px;
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ border-radius: 4px;
197
+ transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;
198
+ flex-shrink: 0;
199
+ }
200
+
201
+ .sp-control:hover {
202
+ color: #fff;
203
+ background: rgba(255, 255, 255, 0.1);
204
+ }
205
+
206
+ .sp-control:active {
207
+ transform: scale(0.92);
208
+ }
209
+
210
+ .sp-control:focus-visible {
211
+ outline: 2px solid var(--sp-accent, #e50914);
212
+ outline-offset: 2px;
213
+ }
214
+
215
+ .sp-control:disabled {
216
+ opacity: 0.4;
217
+ cursor: not-allowed;
218
+ transform: none;
219
+ }
220
+
221
+ .sp-control:disabled:hover {
222
+ background: none;
223
+ }
224
+
225
+ .sp-control svg {
226
+ width: 24px;
227
+ height: 24px;
228
+ fill: currentColor;
229
+ display: block;
230
+ }
231
+
232
+ .sp-control--small svg {
233
+ width: 20px;
234
+ height: 20px;
235
+ }
236
+
237
+ /* ============================================
238
+ Spacer
239
+ ============================================ */
240
+ .sp-spacer {
241
+ flex: 1;
242
+ min-width: 0;
243
+ }
244
+
245
+ /* ============================================
246
+ Time Display
247
+ ============================================ */
248
+ .sp-time {
249
+ font-size: 13px;
250
+ font-variant-numeric: tabular-nums;
251
+ color: rgba(255, 255, 255, 0.9);
252
+ white-space: nowrap;
253
+ padding: 0 4px;
254
+ letter-spacing: 0.02em;
255
+ }
256
+
257
+ /* ============================================
258
+ Volume Control
259
+ ============================================ */
260
+ .sp-volume {
261
+ display: flex;
262
+ align-items: center;
263
+ position: relative;
264
+ }
265
+
266
+ .sp-volume__slider-wrap {
267
+ width: 0;
268
+ overflow: hidden;
269
+ transition: width 0.2s ease;
270
+ }
271
+
272
+ .sp-volume:hover .sp-volume__slider-wrap,
273
+ .sp-volume:focus-within .sp-volume__slider-wrap {
274
+ width: 64px;
275
+ }
276
+
277
+ .sp-volume__slider {
278
+ width: 64px;
279
+ height: 3px;
280
+ background: rgba(255, 255, 255, 0.3);
281
+ border-radius: 1.5px;
282
+ cursor: pointer;
283
+ position: relative;
284
+ margin: 0 8px 0 4px;
285
+ }
286
+
287
+ .sp-volume__level {
288
+ position: absolute;
289
+ top: 0;
290
+ left: 0;
291
+ height: 100%;
292
+ background: #fff;
293
+ border-radius: inherit;
294
+ transition: width 0.1s ease;
295
+ }
296
+
297
+ /* ============================================
298
+ Live Indicator
299
+ ============================================ */
300
+ .sp-live {
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 6px;
304
+ font-size: 11px;
305
+ font-weight: 600;
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.05em;
308
+ color: var(--sp-accent, #e50914);
309
+ cursor: pointer;
310
+ padding: 6px 10px;
311
+ border-radius: 4px;
312
+ transition: background 0.15s ease, opacity 0.15s ease;
313
+ }
314
+
315
+ .sp-live:hover {
316
+ background: rgba(255, 255, 255, 0.1);
317
+ }
318
+
319
+ .sp-live__dot {
320
+ width: 8px;
321
+ height: 8px;
322
+ background: currentColor;
323
+ border-radius: 50%;
324
+ animation: sp-pulse 2s ease-in-out infinite;
325
+ }
326
+
327
+ .sp-live--behind {
328
+ opacity: 0.6;
329
+ }
330
+
331
+ .sp-live--behind .sp-live__dot {
332
+ animation: none;
333
+ }
334
+
335
+ @keyframes sp-pulse {
336
+ 0%, 100% { opacity: 1; }
337
+ 50% { opacity: 0.4; }
338
+ }
339
+
340
+ /* ============================================
341
+ Quality / Settings Menu
342
+ ============================================ */
343
+ .sp-quality {
344
+ position: relative;
345
+ }
346
+
347
+ .sp-quality__btn {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 4px;
351
+ }
352
+
353
+ .sp-quality__label {
354
+ font-size: 12px;
355
+ font-weight: 500;
356
+ opacity: 0.9;
357
+ }
358
+
359
+ .sp-quality-menu {
360
+ position: absolute;
361
+ bottom: calc(100% + 8px);
362
+ right: 0;
363
+ background: rgba(20, 20, 20, 0.95);
364
+ backdrop-filter: blur(8px);
365
+ -webkit-backdrop-filter: blur(8px);
366
+ border-radius: 8px;
367
+ padding: 8px 0;
368
+ min-width: 150px;
369
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
370
+ opacity: 0;
371
+ visibility: hidden;
372
+ transform: translateY(8px);
373
+ transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
374
+ z-index: 20;
375
+ }
376
+
377
+ .sp-quality-menu--open {
378
+ opacity: 1;
379
+ visibility: visible;
380
+ transform: translateY(0);
381
+ }
382
+
383
+ .sp-quality-menu__item {
384
+ display: flex;
385
+ align-items: center;
386
+ justify-content: space-between;
387
+ padding: 10px 16px;
388
+ font-size: 13px;
389
+ color: rgba(255, 255, 255, 0.8);
390
+ cursor: pointer;
391
+ transition: background 0.1s ease, color 0.1s ease;
392
+ }
393
+
394
+ .sp-quality-menu__item:hover {
395
+ background: rgba(255, 255, 255, 0.1);
396
+ color: #fff;
397
+ }
398
+
399
+ .sp-quality-menu__item--active {
400
+ color: var(--sp-accent, #e50914);
401
+ }
402
+
403
+ .sp-quality-menu__check {
404
+ width: 16px;
405
+ height: 16px;
406
+ fill: currentColor;
407
+ margin-left: 8px;
408
+ opacity: 0;
409
+ }
410
+
411
+ .sp-quality-menu__item--active .sp-quality-menu__check {
412
+ opacity: 1;
413
+ }
414
+
415
+ /* ============================================
416
+ Cast Button States
417
+ ============================================ */
418
+ .sp-cast--active {
419
+ color: var(--sp-accent, #e50914);
420
+ }
421
+
422
+ .sp-cast--unavailable {
423
+ opacity: 0.4;
424
+ }
425
+
426
+ /* ============================================
427
+ Buffering Indicator
428
+ ============================================ */
429
+ .sp-buffering {
430
+ position: absolute;
431
+ top: 50%;
432
+ left: 50%;
433
+ transform: translate(-50%, -50%);
434
+ z-index: 15;
435
+ pointer-events: none;
436
+ opacity: 0;
437
+ transition: opacity 0.2s ease;
438
+ }
439
+
440
+ .sp-buffering--visible {
441
+ opacity: 1;
442
+ }
443
+
444
+ .sp-buffering svg {
445
+ width: 48px;
446
+ height: 48px;
447
+ fill: rgba(255, 255, 255, 0.9);
448
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
449
+ }
450
+
451
+ @keyframes sp-spin {
452
+ from { transform: rotate(0deg); }
453
+ to { transform: rotate(360deg); }
454
+ }
455
+
456
+ .sp-spin {
457
+ animation: sp-spin 0.8s linear infinite;
458
+ }
459
+
460
+ /* ============================================
461
+ Reduced Motion
462
+ ============================================ */
463
+ @media (prefers-reduced-motion: reduce) {
464
+ .sp-gradient,
465
+ .sp-controls,
466
+ .sp-progress-wrapper,
467
+ .sp-progress,
468
+ .sp-progress__handle,
469
+ .sp-progress__tooltip,
470
+ .sp-control,
471
+ .sp-volume__slider-wrap,
472
+ .sp-quality-menu,
473
+ .sp-buffering {
474
+ transition: none;
475
+ }
476
+
477
+ .sp-live__dot,
478
+ .sp-spin {
479
+ animation: none;
480
+ }
481
+ }
482
+
483
+ /* ============================================
484
+ CSS Custom Properties (Theming)
485
+ ============================================ */
486
+ :root {
487
+ --sp-accent: #e50914;
488
+ --sp-color: #fff;
489
+ --sp-bg: rgba(0, 0, 0, 0.8);
490
+ --sp-control-height: 48px;
491
+ --sp-icon-size: 24px;
492
+ }
493
+ `;
494
+
495
+ // src/icons.ts
496
+ var icons = {
497
+ play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
498
+ pause: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>`,
499
+ replay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
500
+ volumeHigh: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`,
501
+ volumeLow: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>`,
502
+ volumeMute: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`,
503
+ fullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`,
504
+ exitFullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>`,
505
+ pip: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>`,
506
+ exitPip: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 9h6v2H9z"/></svg>`,
507
+ settings: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>`,
508
+ chromecast: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
509
+ chromecastConnected: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
510
+ airplay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 22h12l-6-6-6 6zM21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4v-2H3V5h18v12h-4v2h4c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
511
+ captions: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z"/></svg>`,
512
+ captionsOff: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 5.5v13h-15v-13h15zM19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2z"/></svg>`,
513
+ checkmark: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`,
514
+ chevronUp: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></svg>`,
515
+ chevronDown: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>`,
516
+ spinner: `<svg viewBox="0 0 24 24" fill="currentColor" class="sp-spin"><path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/></svg>`,
517
+ skipForward: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>`,
518
+ skipBack: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/></svg>`,
519
+ forward10: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z"/><text x="12" y="15" text-anchor="middle" font-size="7" font-weight="600">10</text></svg>`,
520
+ replay10: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/><text x="12" y="15" text-anchor="middle" font-size="7" font-weight="600">10</text></svg>`
521
+ };
522
+
523
+ // src/utils/dom.ts
524
+ function createElement(tag, attrs, children) {
525
+ const el = document.createElement(tag);
526
+ if (attrs) {
527
+ for (const [key, value] of Object.entries(attrs)) {
528
+ if (key === "className") {
529
+ el.className = value;
530
+ } else {
531
+ el.setAttribute(key, value);
532
+ }
533
+ }
534
+ }
535
+ if (children) {
536
+ for (const child of children) {
537
+ if (typeof child === "string") {
538
+ el.appendChild(document.createTextNode(child));
539
+ } else {
540
+ el.appendChild(child);
541
+ }
542
+ }
543
+ }
544
+ return el;
545
+ }
546
+ function createButton(className, label, icon) {
547
+ const btn = createElement("button", {
548
+ className: `sp-control ${className}`,
549
+ "aria-label": label,
550
+ type: "button"
551
+ });
552
+ btn.innerHTML = icon;
553
+ return btn;
554
+ }
555
+ function getVideo(container) {
556
+ return container.querySelector("video");
557
+ }
558
+
559
+ // src/utils/format.ts
560
+ function formatTime(seconds) {
561
+ if (!isFinite(seconds) || isNaN(seconds)) {
562
+ return "0:00";
563
+ }
564
+ const absSeconds = Math.abs(seconds);
565
+ const h = Math.floor(absSeconds / 3600);
566
+ const m = Math.floor(absSeconds % 3600 / 60);
567
+ const s = Math.floor(absSeconds % 60);
568
+ const sign = seconds < 0 ? "-" : "";
569
+ if (h > 0) {
570
+ return `${sign}${h}:${pad(m)}:${pad(s)}`;
571
+ }
572
+ return `${sign}${m}:${pad(s)}`;
573
+ }
574
+ function pad(n) {
575
+ return n < 10 ? `0${n}` : `${n}`;
576
+ }
577
+ function formatLiveTime(behindLive) {
578
+ if (behindLive <= 0) {
579
+ return "LIVE";
580
+ }
581
+ return `-${formatTime(behindLive)}`;
582
+ }
583
+
584
+ // src/controls/PlayButton.ts
585
+ var PlayButton = class {
586
+ constructor(api) {
587
+ this.clickHandler = () => {
588
+ this.toggle();
589
+ };
590
+ this.api = api;
591
+ this.el = createButton("sp-play", "Play", icons.play);
592
+ this.el.addEventListener("click", this.clickHandler);
593
+ }
594
+ render() {
595
+ return this.el;
596
+ }
597
+ update() {
598
+ const playing = this.api.getState("playing");
599
+ const ended = this.api.getState("ended");
600
+ let icon;
601
+ let label;
602
+ if (ended) {
603
+ icon = icons.replay;
604
+ label = "Replay";
605
+ } else if (playing) {
606
+ icon = icons.pause;
607
+ label = "Pause";
608
+ } else {
609
+ icon = icons.play;
610
+ label = "Play";
611
+ }
612
+ this.el.innerHTML = icon;
613
+ this.el.setAttribute("aria-label", label);
614
+ }
615
+ toggle() {
616
+ const video = getVideo(this.api.container);
617
+ if (!video) return;
618
+ const ended = this.api.getState("ended");
619
+ const playing = this.api.getState("playing");
620
+ if (ended) {
621
+ video.currentTime = 0;
622
+ video.play().catch(() => {
623
+ });
624
+ } else if (playing) {
625
+ video.pause();
626
+ } else {
627
+ video.play().catch(() => {
628
+ });
629
+ }
630
+ }
631
+ destroy() {
632
+ this.el.removeEventListener("click", this.clickHandler);
633
+ this.el.remove();
634
+ }
635
+ };
636
+
637
+ // src/controls/ProgressBar.ts
638
+ var ProgressBar = class {
639
+ constructor(api) {
640
+ this.isDragging = false;
641
+ this.lastSeekTime = 0;
642
+ this.seekThrottleMs = 100;
643
+ // Throttle seeks to max 10/sec
644
+ this.wasPlayingBeforeDrag = false;
645
+ this.onMouseDown = (e) => {
646
+ e.preventDefault();
647
+ const video = getVideo(this.api.container);
648
+ this.wasPlayingBeforeDrag = video ? !video.paused : false;
649
+ this.isDragging = true;
650
+ this.el.classList.add("sp-progress--dragging");
651
+ this.lastSeekTime = 0;
652
+ this.seek(e.clientX, true);
653
+ };
654
+ this.onDocMouseMove = (e) => {
655
+ if (this.isDragging) {
656
+ this.seek(e.clientX);
657
+ this.updateVisualPosition(e.clientX);
658
+ }
659
+ };
660
+ this.onMouseUp = (e) => {
661
+ if (this.isDragging) {
662
+ this.seek(e.clientX, true);
663
+ this.isDragging = false;
664
+ this.el.classList.remove("sp-progress--dragging");
665
+ if (this.wasPlayingBeforeDrag) {
666
+ const video = getVideo(this.api.container);
667
+ if (video && video.paused) {
668
+ video.play().catch(() => {
669
+ });
670
+ }
671
+ }
672
+ }
673
+ };
674
+ this.onMouseMove = (e) => {
675
+ this.updateTooltip(e.clientX);
676
+ };
677
+ this.onMouseLeave = () => {
678
+ if (!this.isDragging) {
679
+ this.tooltip.style.opacity = "0";
680
+ }
681
+ };
682
+ this.onKeyDown = (e) => {
683
+ const video = getVideo(this.api.container);
684
+ if (!video) return;
685
+ const step = 5;
686
+ const duration = this.api.getState("duration") || 0;
687
+ switch (e.key) {
688
+ case "ArrowLeft":
689
+ e.preventDefault();
690
+ video.currentTime = Math.max(0, video.currentTime - step);
691
+ break;
692
+ case "ArrowRight":
693
+ e.preventDefault();
694
+ video.currentTime = Math.min(duration, video.currentTime + step);
695
+ break;
696
+ case "Home":
697
+ e.preventDefault();
698
+ video.currentTime = 0;
699
+ break;
700
+ case "End":
701
+ e.preventDefault();
702
+ video.currentTime = duration;
703
+ break;
704
+ }
705
+ };
706
+ this.api = api;
707
+ this.wrapper = createElement("div", { className: "sp-progress-wrapper" });
708
+ this.el = createElement("div", { className: "sp-progress" });
709
+ const track = createElement("div", { className: "sp-progress__track" });
710
+ this.buffered = createElement("div", { className: "sp-progress__buffered" });
711
+ this.filled = createElement("div", { className: "sp-progress__filled" });
712
+ this.handle = createElement("div", { className: "sp-progress__handle" });
713
+ this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
714
+ this.tooltip.textContent = "0:00";
715
+ track.appendChild(this.buffered);
716
+ track.appendChild(this.filled);
717
+ track.appendChild(this.handle);
718
+ this.el.appendChild(track);
719
+ this.el.appendChild(this.tooltip);
720
+ this.wrapper.appendChild(this.el);
721
+ this.el.setAttribute("role", "slider");
722
+ this.el.setAttribute("aria-label", "Seek");
723
+ this.el.setAttribute("aria-valuemin", "0");
724
+ this.el.setAttribute("tabindex", "0");
725
+ this.wrapper.addEventListener("mousedown", this.onMouseDown);
726
+ this.wrapper.addEventListener("mousemove", this.onMouseMove);
727
+ this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
728
+ this.el.addEventListener("keydown", this.onKeyDown);
729
+ document.addEventListener("mousemove", this.onDocMouseMove);
730
+ document.addEventListener("mouseup", this.onMouseUp);
731
+ }
732
+ render() {
733
+ return this.wrapper;
734
+ }
735
+ /** Show the progress bar */
736
+ show() {
737
+ this.wrapper.classList.add("sp-progress-wrapper--visible");
738
+ }
739
+ /** Hide the progress bar */
740
+ hide() {
741
+ this.wrapper.classList.remove("sp-progress-wrapper--visible");
742
+ }
743
+ update() {
744
+ const currentTime = this.api.getState("currentTime") || 0;
745
+ const duration = this.api.getState("duration") || 0;
746
+ const bufferedRanges = this.api.getState("buffered");
747
+ if (duration > 0) {
748
+ const progress = currentTime / duration * 100;
749
+ this.filled.style.width = `${progress}%`;
750
+ this.handle.style.left = `${progress}%`;
751
+ if (bufferedRanges && bufferedRanges.length > 0) {
752
+ const bufferedEnd = bufferedRanges.end(bufferedRanges.length - 1);
753
+ const bufferedPercent = bufferedEnd / duration * 100;
754
+ this.buffered.style.width = `${bufferedPercent}%`;
755
+ }
756
+ this.el.setAttribute("aria-valuemax", String(Math.floor(duration)));
757
+ this.el.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
758
+ this.el.setAttribute("aria-valuetext", formatTime(currentTime));
759
+ }
760
+ }
761
+ getTimeFromPosition(clientX) {
762
+ const rect = this.el.getBoundingClientRect();
763
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
764
+ const duration = this.api.getState("duration") || 0;
765
+ return percent * duration;
766
+ }
767
+ updateTooltip(clientX) {
768
+ const rect = this.el.getBoundingClientRect();
769
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
770
+ const time = this.getTimeFromPosition(clientX);
771
+ this.tooltip.textContent = formatTime(time);
772
+ this.tooltip.style.left = `${percent * 100}%`;
773
+ }
774
+ updateVisualPosition(clientX) {
775
+ const rect = this.el.getBoundingClientRect();
776
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
777
+ this.filled.style.width = `${percent * 100}%`;
778
+ this.handle.style.left = `${percent * 100}%`;
779
+ }
780
+ seek(clientX, force = false) {
781
+ const video = getVideo(this.api.container);
782
+ if (!video) return;
783
+ const now = Date.now();
784
+ if (!force && this.isDragging && now - this.lastSeekTime < this.seekThrottleMs) {
785
+ return;
786
+ }
787
+ this.lastSeekTime = now;
788
+ const time = this.getTimeFromPosition(clientX);
789
+ video.currentTime = time;
790
+ }
791
+ destroy() {
792
+ this.wrapper.removeEventListener("mousedown", this.onMouseDown);
793
+ this.wrapper.removeEventListener("mousemove", this.onMouseMove);
794
+ this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
795
+ document.removeEventListener("mousemove", this.onDocMouseMove);
796
+ document.removeEventListener("mouseup", this.onMouseUp);
797
+ this.wrapper.remove();
798
+ }
799
+ };
800
+
801
+ // src/controls/TimeDisplay.ts
802
+ var TimeDisplay = class {
803
+ constructor(api) {
804
+ this.api = api;
805
+ this.el = createElement("div", { className: "sp-time" });
806
+ this.el.setAttribute("aria-live", "off");
807
+ }
808
+ render() {
809
+ return this.el;
810
+ }
811
+ update() {
812
+ const live = this.api.getState("live");
813
+ const currentTime = this.api.getState("currentTime") || 0;
814
+ const duration = this.api.getState("duration") || 0;
815
+ if (live) {
816
+ const seekableRange = this.api.getState("seekableRange");
817
+ if (seekableRange) {
818
+ const behindLive = seekableRange.end - currentTime;
819
+ this.el.textContent = formatLiveTime(behindLive);
820
+ } else {
821
+ this.el.textContent = formatLiveTime(0);
822
+ }
823
+ } else {
824
+ this.el.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
825
+ }
826
+ }
827
+ destroy() {
828
+ this.el.remove();
829
+ }
830
+ };
831
+
832
+ // src/controls/VolumeControl.ts
833
+ var VolumeControl = class {
834
+ constructor(api) {
835
+ this.isDragging = false;
836
+ this.onMouseDown = (e) => {
837
+ e.preventDefault();
838
+ this.isDragging = true;
839
+ this.setVolume(this.getVolumeFromPosition(e.clientX));
840
+ };
841
+ this.onDocMouseMove = (e) => {
842
+ if (this.isDragging) {
843
+ this.setVolume(this.getVolumeFromPosition(e.clientX));
844
+ }
845
+ };
846
+ this.onMouseUp = () => {
847
+ this.isDragging = false;
848
+ };
849
+ this.onKeyDown = (e) => {
850
+ const video = getVideo(this.api.container);
851
+ if (!video) return;
852
+ const step = 0.1;
853
+ switch (e.key) {
854
+ case "ArrowUp":
855
+ case "ArrowRight":
856
+ e.preventDefault();
857
+ this.setVolume(video.volume + step);
858
+ break;
859
+ case "ArrowDown":
860
+ case "ArrowLeft":
861
+ e.preventDefault();
862
+ this.setVolume(video.volume - step);
863
+ break;
864
+ }
865
+ };
866
+ this.api = api;
867
+ this.el = createElement("div", { className: "sp-volume" });
868
+ this.btn = createElement("button", {
869
+ className: "sp-control sp-volume__btn",
870
+ "aria-label": "Mute",
871
+ type: "button"
872
+ });
873
+ this.btn.innerHTML = icons.volumeHigh;
874
+ this.btn.onclick = () => this.toggleMute();
875
+ const sliderWrap = createElement("div", { className: "sp-volume__slider-wrap" });
876
+ this.slider = createElement("div", { className: "sp-volume__slider" });
877
+ this.slider.setAttribute("role", "slider");
878
+ this.slider.setAttribute("aria-label", "Volume");
879
+ this.slider.setAttribute("aria-valuemin", "0");
880
+ this.slider.setAttribute("aria-valuemax", "100");
881
+ this.slider.setAttribute("tabindex", "0");
882
+ this.level = createElement("div", { className: "sp-volume__level" });
883
+ this.slider.appendChild(this.level);
884
+ sliderWrap.appendChild(this.slider);
885
+ this.el.appendChild(this.btn);
886
+ this.el.appendChild(sliderWrap);
887
+ this.slider.addEventListener("mousedown", this.onMouseDown);
888
+ this.slider.addEventListener("keydown", this.onKeyDown);
889
+ document.addEventListener("mousemove", this.onDocMouseMove);
890
+ document.addEventListener("mouseup", this.onMouseUp);
891
+ }
892
+ render() {
893
+ return this.el;
894
+ }
895
+ update() {
896
+ const volume = this.api.getState("volume") ?? 1;
897
+ const muted = this.api.getState("muted") ?? false;
898
+ let icon;
899
+ let label;
900
+ if (muted || volume === 0) {
901
+ icon = icons.volumeMute;
902
+ label = "Unmute";
903
+ } else if (volume < 0.5) {
904
+ icon = icons.volumeLow;
905
+ label = "Mute";
906
+ } else {
907
+ icon = icons.volumeHigh;
908
+ label = "Mute";
909
+ }
910
+ this.btn.innerHTML = icon;
911
+ this.btn.setAttribute("aria-label", label);
912
+ const displayVolume = muted ? 0 : volume;
913
+ this.level.style.width = `${displayVolume * 100}%`;
914
+ this.slider.setAttribute("aria-valuenow", String(Math.round(displayVolume * 100)));
915
+ }
916
+ toggleMute() {
917
+ const video = getVideo(this.api.container);
918
+ if (!video) return;
919
+ video.muted = !video.muted;
920
+ }
921
+ setVolume(percent) {
922
+ const video = getVideo(this.api.container);
923
+ if (!video) return;
924
+ const vol = Math.max(0, Math.min(1, percent));
925
+ video.volume = vol;
926
+ if (vol > 0 && video.muted) {
927
+ video.muted = false;
928
+ }
929
+ }
930
+ getVolumeFromPosition(clientX) {
931
+ const rect = this.slider.getBoundingClientRect();
932
+ return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
933
+ }
934
+ destroy() {
935
+ document.removeEventListener("mousemove", this.onDocMouseMove);
936
+ document.removeEventListener("mouseup", this.onMouseUp);
937
+ this.el.remove();
938
+ }
939
+ };
940
+
941
+ // src/controls/LiveIndicator.ts
942
+ var LiveIndicator = class {
943
+ constructor(api) {
944
+ this.api = api;
945
+ this.el = createElement("div", { className: "sp-live" });
946
+ this.el.innerHTML = '<div class="sp-live__dot"></div><span>LIVE</span>';
947
+ this.el.setAttribute("role", "button");
948
+ this.el.setAttribute("aria-label", "Seek to live");
949
+ this.el.setAttribute("tabindex", "0");
950
+ this.el.onclick = () => this.seekToLive();
951
+ this.el.onkeydown = (e) => {
952
+ if (e.key === "Enter" || e.key === " ") {
953
+ e.preventDefault();
954
+ this.seekToLive();
955
+ }
956
+ };
957
+ }
958
+ render() {
959
+ return this.el;
960
+ }
961
+ update() {
962
+ const live = this.api.getState("live");
963
+ const liveEdge = this.api.getState("liveEdge");
964
+ this.el.style.display = live ? "" : "none";
965
+ if (liveEdge) {
966
+ this.el.classList.remove("sp-live--behind");
967
+ } else {
968
+ this.el.classList.add("sp-live--behind");
969
+ }
970
+ }
971
+ seekToLive() {
972
+ const video = getVideo(this.api.container);
973
+ if (!video) return;
974
+ const seekableRange = this.api.getState("seekableRange");
975
+ if (seekableRange) {
976
+ video.currentTime = seekableRange.end;
977
+ }
978
+ }
979
+ destroy() {
980
+ this.el.remove();
981
+ }
982
+ };
983
+
984
+ // src/controls/QualityMenu.ts
985
+ var QualityMenu = class {
986
+ constructor(api) {
987
+ this.isOpen = false;
988
+ this.lastQualitiesJson = "";
989
+ this.api = api;
990
+ this.el = createElement("div", { className: "sp-quality" });
991
+ this.btn = createButton("sp-quality__btn", "Quality", icons.settings);
992
+ this.btnLabel = createElement("span", { className: "sp-quality__label" });
993
+ this.btnLabel.textContent = "Auto";
994
+ this.btn.appendChild(this.btnLabel);
995
+ this.btn.addEventListener("click", (e) => {
996
+ e.stopPropagation();
997
+ this.toggle();
998
+ });
999
+ this.menu = createElement("div", { className: "sp-quality-menu" });
1000
+ this.menu.setAttribute("role", "menu");
1001
+ this.menu.addEventListener("click", (e) => {
1002
+ e.stopPropagation();
1003
+ });
1004
+ this.el.appendChild(this.btn);
1005
+ this.el.appendChild(this.menu);
1006
+ this.closeHandler = (e) => {
1007
+ if (!this.el.contains(e.target)) {
1008
+ this.close();
1009
+ }
1010
+ };
1011
+ document.addEventListener("click", this.closeHandler);
1012
+ }
1013
+ render() {
1014
+ return this.el;
1015
+ }
1016
+ update() {
1017
+ const qualities = this.api.getState("qualities") || [];
1018
+ const currentQuality = this.api.getState("currentQuality");
1019
+ this.el.style.display = qualities.length > 0 ? "" : "none";
1020
+ this.btnLabel.textContent = currentQuality?.label || "Auto";
1021
+ const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));
1022
+ const currentId = currentQuality?.id || "auto";
1023
+ if (qualitiesJson !== this.lastQualitiesJson) {
1024
+ this.lastQualitiesJson = qualitiesJson;
1025
+ this.rebuildMenu(qualities);
1026
+ }
1027
+ this.updateActiveStates(currentId);
1028
+ }
1029
+ rebuildMenu(qualities) {
1030
+ this.menu.innerHTML = "";
1031
+ const autoItem = this.createMenuItem("Auto", "auto");
1032
+ this.menu.appendChild(autoItem);
1033
+ const sorted = [...qualities].sort((a, b) => b.height - a.height);
1034
+ for (const q of sorted) {
1035
+ if (q.id === "auto") continue;
1036
+ const item = this.createMenuItem(q.label, q.id);
1037
+ this.menu.appendChild(item);
1038
+ }
1039
+ }
1040
+ updateActiveStates(activeId) {
1041
+ const items = this.menu.querySelectorAll(".sp-quality-menu__item");
1042
+ items.forEach((item) => {
1043
+ const id = item.getAttribute("data-quality-id");
1044
+ const isActive = id === activeId;
1045
+ item.classList.toggle("sp-quality-menu__item--active", isActive);
1046
+ });
1047
+ }
1048
+ createMenuItem(label, qualityId) {
1049
+ const item = createElement("div", {
1050
+ className: "sp-quality-menu__item"
1051
+ });
1052
+ item.setAttribute("role", "menuitem");
1053
+ item.setAttribute("data-quality-id", qualityId);
1054
+ const labelSpan = createElement("span", { className: "sp-quality-menu__label" });
1055
+ labelSpan.textContent = label;
1056
+ item.appendChild(labelSpan);
1057
+ item.addEventListener("click", (e) => {
1058
+ e.preventDefault();
1059
+ e.stopPropagation();
1060
+ this.selectQuality(qualityId);
1061
+ });
1062
+ return item;
1063
+ }
1064
+ selectQuality(qualityId) {
1065
+ this.api.emit("quality:select", {
1066
+ quality: qualityId,
1067
+ auto: qualityId === "auto"
1068
+ });
1069
+ this.close();
1070
+ }
1071
+ toggle() {
1072
+ this.isOpen ? this.close() : this.open();
1073
+ }
1074
+ open() {
1075
+ this.isOpen = true;
1076
+ this.menu.classList.add("sp-quality-menu--open");
1077
+ this.btn.setAttribute("aria-expanded", "true");
1078
+ }
1079
+ close() {
1080
+ this.isOpen = false;
1081
+ this.menu.classList.remove("sp-quality-menu--open");
1082
+ this.btn.setAttribute("aria-expanded", "false");
1083
+ }
1084
+ destroy() {
1085
+ document.removeEventListener("click", this.closeHandler);
1086
+ this.el.remove();
1087
+ }
1088
+ };
1089
+
1090
+ // src/controls/CastButton.ts
1091
+ function isChromecastSupported() {
1092
+ if (typeof navigator === "undefined") return false;
1093
+ const ua = navigator.userAgent;
1094
+ return /Chrome/.test(ua) && !/Edge|Edg/.test(ua);
1095
+ }
1096
+ function isAirPlaySupported() {
1097
+ if (typeof HTMLVideoElement === "undefined") return false;
1098
+ return typeof HTMLVideoElement.prototype.webkitShowPlaybackTargetPicker === "function";
1099
+ }
1100
+ var CastButton = class {
1101
+ constructor(api, type) {
1102
+ this.api = api;
1103
+ this.type = type;
1104
+ this.supported = type === "chromecast" ? isChromecastSupported() : isAirPlaySupported();
1105
+ const icon = type === "chromecast" ? icons.chromecast : icons.airplay;
1106
+ const label = type === "chromecast" ? "Cast" : "AirPlay";
1107
+ this.el = createButton(`sp-cast sp-cast--${type}`, label, icon);
1108
+ this.el.addEventListener("click", () => this.handleClick());
1109
+ if (!this.supported) {
1110
+ this.el.style.display = "none";
1111
+ }
1112
+ }
1113
+ render() {
1114
+ return this.el;
1115
+ }
1116
+ update() {
1117
+ if (!this.supported) {
1118
+ this.el.style.display = "none";
1119
+ return;
1120
+ }
1121
+ if (this.type === "chromecast") {
1122
+ const available = this.api.getState("chromecastAvailable");
1123
+ const active = this.api.getState("chromecastActive");
1124
+ this.el.style.display = "";
1125
+ this.el.disabled = !available && !active;
1126
+ this.el.classList.toggle("sp-cast--active", !!active);
1127
+ this.el.classList.toggle("sp-cast--unavailable", !available && !active);
1128
+ if (active) {
1129
+ this.el.innerHTML = icons.chromecastConnected;
1130
+ this.el.setAttribute("aria-label", "Stop casting");
1131
+ } else {
1132
+ this.el.innerHTML = icons.chromecast;
1133
+ this.el.setAttribute("aria-label", available ? "Cast" : "No Cast devices found");
1134
+ }
1135
+ } else {
1136
+ const active = this.api.getState("airplayActive");
1137
+ this.el.style.display = "";
1138
+ this.el.disabled = false;
1139
+ this.el.classList.toggle("sp-cast--active", !!active);
1140
+ this.el.classList.remove("sp-cast--unavailable");
1141
+ this.el.setAttribute("aria-label", active ? "Stop AirPlay" : "AirPlay");
1142
+ }
1143
+ }
1144
+ handleClick() {
1145
+ if (this.type === "chromecast") {
1146
+ this.handleChromecast();
1147
+ } else {
1148
+ this.handleAirPlay();
1149
+ }
1150
+ }
1151
+ handleChromecast() {
1152
+ const chromecast = this.api.getPlugin("chromecast");
1153
+ if (!chromecast) return;
1154
+ if (chromecast.isConnected()) {
1155
+ chromecast.endSession();
1156
+ } else {
1157
+ chromecast.requestSession().catch(() => {
1158
+ });
1159
+ }
1160
+ }
1161
+ async handleAirPlay() {
1162
+ const airplayPlugin = this.api.getPlugin("airplay");
1163
+ if (airplayPlugin) {
1164
+ await airplayPlugin.showPicker();
1165
+ } else {
1166
+ const video = getVideo(this.api.container);
1167
+ video?.webkitShowPlaybackTargetPicker?.();
1168
+ }
1169
+ }
1170
+ destroy() {
1171
+ this.el.remove();
1172
+ }
1173
+ };
1174
+
1175
+ // src/controls/PipButton.ts
1176
+ var PipButton = class {
1177
+ constructor(api) {
1178
+ this.clickHandler = () => {
1179
+ this.toggle();
1180
+ };
1181
+ this.api = api;
1182
+ const video = document.createElement("video");
1183
+ this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in video;
1184
+ this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
1185
+ this.el.addEventListener("click", this.clickHandler);
1186
+ if (!this.supported) {
1187
+ this.el.style.display = "none";
1188
+ }
1189
+ }
1190
+ render() {
1191
+ return this.el;
1192
+ }
1193
+ update() {
1194
+ if (!this.supported) return;
1195
+ const pip = this.api.getState("pip");
1196
+ this.el.setAttribute("aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1197
+ this.el.classList.toggle("sp-pip--active", !!pip);
1198
+ }
1199
+ async toggle() {
1200
+ const video = getVideo(this.api.container);
1201
+ if (!video) {
1202
+ this.api.logger.warn("PiP: video element not found");
1203
+ return;
1204
+ }
1205
+ try {
1206
+ const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1207
+ if (isInPip) {
1208
+ if (document.pictureInPictureElement) {
1209
+ await document.exitPictureInPicture();
1210
+ } else if (video.webkitSetPresentationMode) {
1211
+ video.webkitSetPresentationMode("inline");
1212
+ }
1213
+ this.api.logger.debug("PiP: exited");
1214
+ } else {
1215
+ if (video.requestPictureInPicture) {
1216
+ await video.requestPictureInPicture();
1217
+ } else if (video.webkitSetPresentationMode) {
1218
+ video.webkitSetPresentationMode("picture-in-picture");
1219
+ }
1220
+ this.api.logger.debug("PiP: entered");
1221
+ }
1222
+ } catch (e) {
1223
+ this.api.logger.warn("PiP: failed", { error: e.message });
1224
+ }
1225
+ }
1226
+ destroy() {
1227
+ this.el.removeEventListener("click", this.clickHandler);
1228
+ this.el.remove();
1229
+ }
1230
+ };
1231
+
1232
+ // src/controls/FullscreenButton.ts
1233
+ var FullscreenButton = class {
1234
+ constructor(api) {
1235
+ this.clickHandler = () => {
1236
+ this.toggle();
1237
+ };
1238
+ this.api = api;
1239
+ this.el = createButton("sp-fullscreen", "Fullscreen", icons.fullscreen);
1240
+ this.el.addEventListener("click", this.clickHandler);
1241
+ }
1242
+ render() {
1243
+ return this.el;
1244
+ }
1245
+ update() {
1246
+ const fullscreen = this.api.getState("fullscreen");
1247
+ if (fullscreen) {
1248
+ this.el.innerHTML = icons.exitFullscreen;
1249
+ this.el.setAttribute("aria-label", "Exit fullscreen");
1250
+ } else {
1251
+ this.el.innerHTML = icons.fullscreen;
1252
+ this.el.setAttribute("aria-label", "Fullscreen");
1253
+ }
1254
+ }
1255
+ async toggle() {
1256
+ const container = this.api.container;
1257
+ const video = getVideo(container);
1258
+ try {
1259
+ if (document.fullscreenElement) {
1260
+ await document.exitFullscreen();
1261
+ } else if (container.requestFullscreen) {
1262
+ await container.requestFullscreen();
1263
+ } else if (video?.webkitEnterFullscreen) {
1264
+ video.webkitEnterFullscreen();
1265
+ }
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ destroy() {
1270
+ this.el.removeEventListener("click", this.clickHandler);
1271
+ this.el.remove();
1272
+ }
1273
+ };
1274
+
1275
+ // src/controls/Spacer.ts
1276
+ var Spacer = class {
1277
+ constructor() {
1278
+ this.el = createElement("div", { className: "sp-spacer" });
1279
+ }
1280
+ render() {
1281
+ return this.el;
1282
+ }
1283
+ update() {
1284
+ }
1285
+ destroy() {
1286
+ this.el.remove();
1287
+ }
1288
+ };
1289
+
1290
+ // src/index.ts
1291
+ var DEFAULT_LAYOUT = [
1292
+ "play",
1293
+ "volume",
1294
+ "time",
1295
+ "live-indicator",
1296
+ "spacer",
1297
+ "quality",
1298
+ "chromecast",
1299
+ "airplay",
1300
+ "pip",
1301
+ "fullscreen"
1302
+ ];
1303
+ var DEFAULT_HIDE_DELAY = 3e3;
1304
+ function uiPlugin(config = {}) {
1305
+ let api;
1306
+ let controlBar = null;
1307
+ let gradient = null;
1308
+ let progressBar = null;
1309
+ let bufferingIndicator = null;
1310
+ let styleEl = null;
1311
+ let controls = [];
1312
+ let hideTimeout = null;
1313
+ let stateUnsubscribe = null;
1314
+ let controlsVisible = true;
1315
+ const layout = config.controls || DEFAULT_LAYOUT;
1316
+ const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
1317
+ const createControl = (slot) => {
1318
+ switch (slot) {
1319
+ case "play":
1320
+ return new PlayButton(api);
1321
+ case "volume":
1322
+ return new VolumeControl(api);
1323
+ case "progress":
1324
+ return null;
1325
+ case "time":
1326
+ return new TimeDisplay(api);
1327
+ case "live-indicator":
1328
+ return new LiveIndicator(api);
1329
+ case "quality":
1330
+ return new QualityMenu(api);
1331
+ case "chromecast":
1332
+ return new CastButton(api, "chromecast");
1333
+ case "airplay":
1334
+ return new CastButton(api, "airplay");
1335
+ case "pip":
1336
+ return new PipButton(api);
1337
+ case "fullscreen":
1338
+ return new FullscreenButton(api);
1339
+ case "spacer":
1340
+ return new Spacer();
1341
+ default:
1342
+ return null;
1343
+ }
1344
+ };
1345
+ const updateControls = () => {
1346
+ controls.forEach((c) => c.update());
1347
+ progressBar?.update();
1348
+ const waiting = api?.getState("waiting");
1349
+ const seeking = api?.getState("seeking");
1350
+ const playbackState = api?.getState("playbackState");
1351
+ const isLoading = playbackState === "loading";
1352
+ const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
1353
+ bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
1354
+ };
1355
+ const showControls = () => {
1356
+ if (controlsVisible) {
1357
+ resetHideTimer();
1358
+ return;
1359
+ }
1360
+ controlsVisible = true;
1361
+ controlBar?.classList.add("sp-controls--visible");
1362
+ controlBar?.classList.remove("sp-controls--hidden");
1363
+ gradient?.classList.add("sp-gradient--visible");
1364
+ progressBar?.show();
1365
+ api?.setState("controlsVisible", true);
1366
+ resetHideTimer();
1367
+ };
1368
+ const hideControls = () => {
1369
+ const paused = api?.getState("paused");
1370
+ if (paused) return;
1371
+ controlsVisible = false;
1372
+ controlBar?.classList.remove("sp-controls--visible");
1373
+ controlBar?.classList.add("sp-controls--hidden");
1374
+ gradient?.classList.remove("sp-gradient--visible");
1375
+ progressBar?.hide();
1376
+ api?.setState("controlsVisible", false);
1377
+ };
1378
+ const resetHideTimer = () => {
1379
+ if (hideTimeout) {
1380
+ clearTimeout(hideTimeout);
1381
+ }
1382
+ hideTimeout = setTimeout(hideControls, hideDelay);
1383
+ };
1384
+ const handleInteraction = () => {
1385
+ showControls();
1386
+ };
1387
+ const handleMouseLeave = () => {
1388
+ hideControls();
1389
+ };
1390
+ const handleKeyDown = (e) => {
1391
+ if (!api.container.contains(document.activeElement)) return;
1392
+ const video = api.container.querySelector("video");
1393
+ if (!video) return;
1394
+ switch (e.key) {
1395
+ case " ":
1396
+ case "k":
1397
+ e.preventDefault();
1398
+ video.paused ? video.play() : video.pause();
1399
+ break;
1400
+ case "m":
1401
+ e.preventDefault();
1402
+ video.muted = !video.muted;
1403
+ break;
1404
+ case "f":
1405
+ e.preventDefault();
1406
+ if (document.fullscreenElement) {
1407
+ document.exitFullscreen();
1408
+ } else {
1409
+ api.container.requestFullscreen?.();
1410
+ }
1411
+ break;
1412
+ case "ArrowLeft":
1413
+ e.preventDefault();
1414
+ video.currentTime = Math.max(0, video.currentTime - 5);
1415
+ showControls();
1416
+ break;
1417
+ case "ArrowRight":
1418
+ e.preventDefault();
1419
+ video.currentTime = Math.min(video.duration || 0, video.currentTime + 5);
1420
+ showControls();
1421
+ break;
1422
+ case "ArrowUp":
1423
+ e.preventDefault();
1424
+ video.volume = Math.min(1, video.volume + 0.1);
1425
+ showControls();
1426
+ break;
1427
+ case "ArrowDown":
1428
+ e.preventDefault();
1429
+ video.volume = Math.max(0, video.volume - 0.1);
1430
+ showControls();
1431
+ break;
1432
+ }
1433
+ };
1434
+ return {
1435
+ id: "ui-controls",
1436
+ name: "UI Controls",
1437
+ type: "ui",
1438
+ version: "1.0.0",
1439
+ async init(pluginApi) {
1440
+ api = pluginApi;
1441
+ styleEl = document.createElement("style");
1442
+ styleEl.textContent = styles;
1443
+ document.head.appendChild(styleEl);
1444
+ if (config.theme) {
1445
+ this.setTheme(config.theme);
1446
+ }
1447
+ const container = api.container;
1448
+ if (!container) {
1449
+ api.logger.error("UI plugin: container not found");
1450
+ return;
1451
+ }
1452
+ const containerStyle = getComputedStyle(container);
1453
+ if (containerStyle.position === "static") {
1454
+ container.style.position = "relative";
1455
+ }
1456
+ gradient = document.createElement("div");
1457
+ gradient.className = "sp-gradient sp-gradient--visible";
1458
+ container.appendChild(gradient);
1459
+ bufferingIndicator = document.createElement("div");
1460
+ bufferingIndicator.className = "sp-buffering";
1461
+ bufferingIndicator.innerHTML = icons.spinner;
1462
+ bufferingIndicator.setAttribute("aria-hidden", "true");
1463
+ container.appendChild(bufferingIndicator);
1464
+ progressBar = new ProgressBar(api);
1465
+ container.appendChild(progressBar.render());
1466
+ progressBar.show();
1467
+ controlBar = document.createElement("div");
1468
+ controlBar.className = "sp-controls sp-controls--visible";
1469
+ controlBar.setAttribute("role", "toolbar");
1470
+ controlBar.setAttribute("aria-label", "Video controls");
1471
+ for (const slot of layout) {
1472
+ const control = createControl(slot);
1473
+ if (control) {
1474
+ controls.push(control);
1475
+ controlBar.appendChild(control.render());
1476
+ }
1477
+ }
1478
+ container.appendChild(controlBar);
1479
+ container.addEventListener("mousemove", handleInteraction);
1480
+ container.addEventListener("mouseenter", handleInteraction);
1481
+ container.addEventListener("mouseleave", handleMouseLeave);
1482
+ container.addEventListener("touchstart", handleInteraction, { passive: true });
1483
+ container.addEventListener("click", handleInteraction);
1484
+ document.addEventListener("keydown", handleKeyDown);
1485
+ stateUnsubscribe = api.subscribeToState(updateControls);
1486
+ document.addEventListener("fullscreenchange", updateControls);
1487
+ updateControls();
1488
+ if (!container.hasAttribute("tabindex")) {
1489
+ container.setAttribute("tabindex", "0");
1490
+ }
1491
+ api.logger.debug("UI controls plugin initialized");
1492
+ },
1493
+ async destroy() {
1494
+ if (hideTimeout) {
1495
+ clearTimeout(hideTimeout);
1496
+ hideTimeout = null;
1497
+ }
1498
+ stateUnsubscribe?.();
1499
+ stateUnsubscribe = null;
1500
+ if (api?.container) {
1501
+ api.container.removeEventListener("mousemove", handleInteraction);
1502
+ api.container.removeEventListener("mouseenter", handleInteraction);
1503
+ api.container.removeEventListener("mouseleave", handleMouseLeave);
1504
+ api.container.removeEventListener("touchstart", handleInteraction);
1505
+ api.container.removeEventListener("click", handleInteraction);
1506
+ }
1507
+ document.removeEventListener("keydown", handleKeyDown);
1508
+ document.removeEventListener("fullscreenchange", updateControls);
1509
+ controls.forEach((c) => c.destroy());
1510
+ controls = [];
1511
+ progressBar?.destroy();
1512
+ progressBar = null;
1513
+ controlBar?.remove();
1514
+ controlBar = null;
1515
+ gradient?.remove();
1516
+ gradient = null;
1517
+ bufferingIndicator?.remove();
1518
+ bufferingIndicator = null;
1519
+ styleEl?.remove();
1520
+ styleEl = null;
1521
+ api?.logger.debug("UI controls plugin destroyed");
1522
+ },
1523
+ // Public API
1524
+ show() {
1525
+ showControls();
1526
+ },
1527
+ hide() {
1528
+ controlsVisible = false;
1529
+ controlBar?.classList.remove("sp-controls--visible");
1530
+ controlBar?.classList.add("sp-controls--hidden");
1531
+ gradient?.classList.remove("sp-gradient--visible");
1532
+ progressBar?.hide();
1533
+ api?.setState("controlsVisible", false);
1534
+ },
1535
+ setTheme(theme) {
1536
+ const root = api?.container || document.documentElement;
1537
+ if (theme.primaryColor) {
1538
+ root.style.setProperty("--sp-color", theme.primaryColor);
1539
+ }
1540
+ if (theme.accentColor) {
1541
+ root.style.setProperty("--sp-accent", theme.accentColor);
1542
+ }
1543
+ if (theme.backgroundColor) {
1544
+ root.style.setProperty("--sp-bg", theme.backgroundColor);
1545
+ }
1546
+ if (theme.controlBarHeight) {
1547
+ root.style.setProperty("--sp-control-height", `${theme.controlBarHeight}px`);
1548
+ }
1549
+ if (theme.iconSize) {
1550
+ root.style.setProperty("--sp-icon-size", `${theme.iconSize}px`);
1551
+ }
1552
+ },
1553
+ getControlBar() {
1554
+ return controlBar;
1555
+ }
1556
+ };
1557
+ }
1558
+ var index_default = uiPlugin;
1559
+ export {
1560
+ index_default as default,
1561
+ formatLiveTime,
1562
+ formatTime,
1563
+ icons,
1564
+ styles,
1565
+ uiPlugin
1566
+ };