analytica-frontend-lib 1.1.10 → 1.1.11

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.
@@ -145,6 +145,85 @@ var formatTime = (seconds) => {
145
145
  const secs = Math.floor(seconds % 60);
146
146
  return `${mins}:${secs.toString().padStart(2, "0")}`;
147
147
  };
148
+ var ProgressBar = ({
149
+ currentTime,
150
+ duration,
151
+ progressPercentage,
152
+ onSeek
153
+ }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "px-4 pb-2", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
154
+ "input",
155
+ {
156
+ type: "range",
157
+ min: 0,
158
+ max: duration || 100,
159
+ value: currentTime,
160
+ onChange: (e) => onSeek(parseFloat(e.target.value)),
161
+ className: "w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500",
162
+ "aria-label": "Video progress",
163
+ style: {
164
+ background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)`
165
+ }
166
+ }
167
+ ) });
168
+ var VolumeControls = ({
169
+ volume,
170
+ isMuted,
171
+ onVolumeChange,
172
+ onToggleMute
173
+ }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
174
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
175
+ IconButton_default,
176
+ {
177
+ icon: isMuted ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.SpeakerSlash, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.SpeakerHigh, { size: 24 }),
178
+ onClick: onToggleMute,
179
+ "aria-label": isMuted ? "Unmute" : "Mute",
180
+ className: "!bg-transparent !text-white hover:!bg-white/20"
181
+ }
182
+ ),
183
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
184
+ "input",
185
+ {
186
+ type: "range",
187
+ min: 0,
188
+ max: 100,
189
+ value: Math.round(volume * 100),
190
+ onChange: (e) => onVolumeChange(parseInt(e.target.value)),
191
+ className: "w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500",
192
+ "aria-label": "Volume control",
193
+ style: {
194
+ background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)`
195
+ }
196
+ }
197
+ )
198
+ ] });
199
+ var SpeedMenu = ({
200
+ showSpeedMenu,
201
+ playbackRate,
202
+ onToggleMenu,
203
+ onSpeedChange
204
+ }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", children: [
205
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
206
+ IconButton_default,
207
+ {
208
+ icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.DotsThreeVertical, { size: 24 }),
209
+ onClick: onToggleMenu,
210
+ "aria-label": "Playback speed",
211
+ className: "!bg-transparent !text-white hover:!bg-white/20"
212
+ }
213
+ ),
214
+ showSpeedMenu && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute bottom-12 right-0 bg-black/90 rounded-lg p-2 min-w-20", children: [0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
215
+ "button",
216
+ {
217
+ onClick: () => onSpeedChange(speed),
218
+ className: `block w-full text-left px-3 py-1 text-sm rounded hover:bg-white/20 transition-colors ${playbackRate === speed ? "text-primary-400" : "text-white"}`,
219
+ children: [
220
+ speed,
221
+ "x"
222
+ ]
223
+ },
224
+ speed
225
+ )) })
226
+ ] });
148
227
  var VideoPlayer = ({
149
228
  src,
150
229
  poster,
@@ -169,10 +248,51 @@ var VideoPlayer = ({
169
248
  const [showControls, setShowControls] = (0, import_react2.useState)(true);
170
249
  const [hasCompleted, setHasCompleted] = (0, import_react2.useState)(false);
171
250
  const [showCaptions, setShowCaptions] = (0, import_react2.useState)(false);
251
+ (0, import_react2.useEffect)(() => {
252
+ setHasCompleted(false);
253
+ }, [src]);
172
254
  const [playbackRate, setPlaybackRate] = (0, import_react2.useState)(1);
173
255
  const [showSpeedMenu, setShowSpeedMenu] = (0, import_react2.useState)(false);
174
256
  const lastSaveTimeRef = (0, import_react2.useRef)(0);
175
257
  const trackRef = (0, import_react2.useRef)(null);
258
+ const controlsTimeoutRef = (0, import_react2.useRef)(null);
259
+ const lastMousePositionRef = (0, import_react2.useRef)({ x: 0, y: 0 });
260
+ const mouseMoveTimeoutRef = (0, import_react2.useRef)(null);
261
+ const clearControlsTimeout = (0, import_react2.useCallback)(() => {
262
+ if (controlsTimeoutRef.current) {
263
+ clearTimeout(controlsTimeoutRef.current);
264
+ controlsTimeoutRef.current = null;
265
+ }
266
+ }, []);
267
+ const clearMouseMoveTimeout = (0, import_react2.useCallback)(() => {
268
+ if (mouseMoveTimeoutRef.current) {
269
+ clearTimeout(mouseMoveTimeoutRef.current);
270
+ mouseMoveTimeoutRef.current = null;
271
+ }
272
+ }, []);
273
+ const showControlsWithTimer = (0, import_react2.useCallback)(() => {
274
+ setShowControls(true);
275
+ clearControlsTimeout();
276
+ if (isPlaying) {
277
+ const timeout = isFullscreen ? 2e3 : 3e3;
278
+ controlsTimeoutRef.current = window.setTimeout(() => {
279
+ setShowControls(false);
280
+ }, timeout);
281
+ }
282
+ }, [isPlaying, isFullscreen, clearControlsTimeout]);
283
+ const handleMouseMove = (0, import_react2.useCallback)(
284
+ (event) => {
285
+ const currentX = event.clientX;
286
+ const currentY = event.clientY;
287
+ const lastPos = lastMousePositionRef.current;
288
+ const hasMoved = Math.abs(currentX - lastPos.x) > 5 || Math.abs(currentY - lastPos.y) > 5;
289
+ if (hasMoved) {
290
+ lastMousePositionRef.current = { x: currentX, y: currentY };
291
+ showControlsWithTimer();
292
+ }
293
+ },
294
+ [showControlsWithTimer]
295
+ );
176
296
  (0, import_react2.useEffect)(() => {
177
297
  if (videoRef.current) {
178
298
  videoRef.current.volume = volume;
@@ -180,84 +300,129 @@ var VideoPlayer = ({
180
300
  }
181
301
  }, [volume, isMuted]);
182
302
  (0, import_react2.useEffect)(() => {
183
- if (!autoSave || !storageKey) return;
184
- const raw = localStorage.getItem(`${storageKey}-${src}`);
185
- const saved = raw !== null ? Number(raw) : NaN;
186
- const hasValidSaved = Number.isFinite(saved) && saved >= 0;
187
- const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;
188
- let start;
189
- if (hasValidInitial) {
190
- start = initialTime;
191
- } else if (hasValidSaved) {
192
- start = saved;
303
+ const video = videoRef.current;
304
+ if (!video) return;
305
+ const onPlay = () => setIsPlaying(true);
306
+ const onPause = () => setIsPlaying(false);
307
+ const onEnded = () => setIsPlaying(false);
308
+ video.addEventListener("play", onPlay);
309
+ video.addEventListener("pause", onPause);
310
+ video.addEventListener("ended", onEnded);
311
+ return () => {
312
+ video.removeEventListener("play", onPlay);
313
+ video.removeEventListener("pause", onPause);
314
+ video.removeEventListener("ended", onEnded);
315
+ };
316
+ }, []);
317
+ (0, import_react2.useEffect)(() => {
318
+ if (isPlaying) {
319
+ showControlsWithTimer();
193
320
  } else {
194
- start = void 0;
321
+ clearControlsTimeout();
322
+ setShowControls(true);
195
323
  }
324
+ }, [isPlaying, showControlsWithTimer, clearControlsTimeout]);
325
+ (0, import_react2.useEffect)(() => {
326
+ const handleFullscreenChange = () => {
327
+ const isCurrentlyFullscreen = !!document.fullscreenElement;
328
+ setIsFullscreen(isCurrentlyFullscreen);
329
+ if (isCurrentlyFullscreen) {
330
+ showControlsWithTimer();
331
+ }
332
+ };
333
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
334
+ return () => {
335
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
336
+ };
337
+ }, [showControlsWithTimer]);
338
+ const getInitialTime = (0, import_react2.useCallback)(() => {
339
+ if (!autoSave || !storageKey) {
340
+ return Number.isFinite(initialTime) && initialTime >= 0 ? initialTime : void 0;
341
+ }
342
+ const saved = Number(localStorage.getItem(`${storageKey}-${src}`) || NaN);
343
+ const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;
344
+ const hasValidSaved = Number.isFinite(saved) && saved >= 0;
345
+ if (hasValidInitial) return initialTime;
346
+ if (hasValidSaved) return saved;
347
+ return void 0;
348
+ }, [autoSave, storageKey, src, initialTime]);
349
+ (0, import_react2.useEffect)(() => {
350
+ const start = getInitialTime();
196
351
  if (start !== void 0 && videoRef.current) {
197
352
  videoRef.current.currentTime = start;
198
353
  setCurrentTime(start);
199
354
  }
200
- }, [src, storageKey, autoSave, initialTime]);
201
- const saveProgress = (0, import_react2.useCallback)(() => {
202
- if (!autoSave || !storageKey) return;
203
- const now = Date.now();
204
- if (now - lastSaveTimeRef.current > 5e3) {
205
- localStorage.setItem(`${storageKey}-${src}`, currentTime.toString());
206
- lastSaveTimeRef.current = now;
207
- }
208
- }, [autoSave, storageKey, src, currentTime]);
209
- const togglePlayPause = (0, import_react2.useCallback)(() => {
210
- if (videoRef.current) {
211
- if (isPlaying) {
212
- videoRef.current.pause();
213
- } else {
214
- videoRef.current.play();
355
+ }, [getInitialTime]);
356
+ const saveProgress = (0, import_react2.useCallback)(
357
+ (time) => {
358
+ if (!autoSave || !storageKey) return;
359
+ const now = Date.now();
360
+ if (now - lastSaveTimeRef.current > 5e3) {
361
+ localStorage.setItem(`${storageKey}-${src}`, time.toString());
362
+ lastSaveTimeRef.current = now;
215
363
  }
216
- setIsPlaying(!isPlaying);
364
+ },
365
+ [autoSave, storageKey, src]
366
+ );
367
+ const togglePlayPause = (0, import_react2.useCallback)(async () => {
368
+ const video = videoRef.current;
369
+ if (!video) return;
370
+ if (!video.paused) {
371
+ video.pause();
372
+ return;
373
+ }
374
+ try {
375
+ await video.play();
376
+ } catch {
217
377
  }
218
- }, [isPlaying]);
378
+ }, []);
219
379
  const handleVolumeChange = (0, import_react2.useCallback)(
220
380
  (newVolume) => {
221
- if (videoRef.current) {
222
- const volumeValue = newVolume / 100;
223
- videoRef.current.volume = volumeValue;
224
- setVolume(volumeValue);
225
- if (volumeValue === 0) {
226
- videoRef.current.muted = true;
227
- setIsMuted(true);
228
- } else if (isMuted) {
229
- videoRef.current.muted = false;
230
- setIsMuted(false);
231
- }
381
+ const video = videoRef.current;
382
+ if (!video) return;
383
+ const volumeValue = newVolume / 100;
384
+ video.volume = volumeValue;
385
+ setVolume(volumeValue);
386
+ const shouldMute = volumeValue === 0;
387
+ const shouldUnmute = volumeValue > 0 && isMuted;
388
+ if (shouldMute) {
389
+ video.muted = true;
390
+ setIsMuted(true);
391
+ } else if (shouldUnmute) {
392
+ video.muted = false;
393
+ setIsMuted(false);
232
394
  }
233
395
  },
234
396
  [isMuted]
235
397
  );
236
398
  const toggleMute = (0, import_react2.useCallback)(() => {
237
- if (videoRef.current) {
238
- if (isMuted) {
239
- const restoreVolume = volume > 0 ? volume : 0.5;
240
- videoRef.current.volume = restoreVolume;
241
- videoRef.current.muted = false;
242
- setVolume(restoreVolume);
243
- setIsMuted(false);
244
- } else {
245
- videoRef.current.muted = true;
246
- setIsMuted(true);
247
- }
399
+ const video = videoRef.current;
400
+ if (!video) return;
401
+ if (isMuted) {
402
+ const restoreVolume = volume > 0 ? volume : 0.5;
403
+ video.volume = restoreVolume;
404
+ video.muted = false;
405
+ setVolume(restoreVolume);
406
+ setIsMuted(false);
407
+ } else {
408
+ video.muted = true;
409
+ setIsMuted(true);
248
410
  }
249
411
  }, [isMuted, volume]);
412
+ const handleSeek = (0, import_react2.useCallback)((newTime) => {
413
+ const video = videoRef.current;
414
+ if (video) {
415
+ video.currentTime = newTime;
416
+ }
417
+ }, []);
250
418
  const toggleFullscreen = (0, import_react2.useCallback)(() => {
251
419
  const container = videoRef.current?.parentElement;
252
420
  if (!container) return;
253
- if (!isFullscreen) {
254
- if (container.requestFullscreen) {
255
- container.requestFullscreen();
256
- }
257
- } else if (document.exitFullscreen) {
421
+ if (!isFullscreen && container.requestFullscreen) {
422
+ container.requestFullscreen();
423
+ } else if (isFullscreen && document.exitFullscreen) {
258
424
  document.exitFullscreen();
259
425
  }
260
- setIsFullscreen(!isFullscreen);
261
426
  }, [isFullscreen]);
262
427
  const handleSpeedChange = (0, import_react2.useCallback)((speed) => {
263
428
  if (videoRef.current) {
@@ -275,29 +440,28 @@ var VideoPlayer = ({
275
440
  setShowCaptions(newShowCaptions);
276
441
  trackRef.current.track.mode = newShowCaptions && subtitles ? "showing" : "hidden";
277
442
  }, [showCaptions, subtitles]);
278
- const handleTimeUpdate = (0, import_react2.useCallback)(() => {
279
- if (videoRef.current) {
280
- const current = videoRef.current.currentTime;
281
- setCurrentTime(current);
282
- saveProgress();
283
- onTimeUpdate?.(current);
284
- if (duration > 0) {
285
- const progressPercent = current / duration * 100;
286
- onProgress?.(progressPercent);
287
- if (progressPercent >= 95 && !hasCompleted) {
288
- setHasCompleted(true);
289
- onVideoComplete?.();
290
- }
443
+ const checkVideoCompletion = (0, import_react2.useCallback)(
444
+ (progressPercent) => {
445
+ if (progressPercent >= 95 && !hasCompleted) {
446
+ setHasCompleted(true);
447
+ onVideoComplete?.();
291
448
  }
449
+ },
450
+ [hasCompleted, onVideoComplete]
451
+ );
452
+ const handleTimeUpdate = (0, import_react2.useCallback)(() => {
453
+ const video = videoRef.current;
454
+ if (!video) return;
455
+ const current = video.currentTime;
456
+ setCurrentTime(current);
457
+ saveProgress(current);
458
+ onTimeUpdate?.(current);
459
+ if (duration > 0) {
460
+ const progressPercent = current / duration * 100;
461
+ onProgress?.(progressPercent);
462
+ checkVideoCompletion(progressPercent);
292
463
  }
293
- }, [
294
- duration,
295
- saveProgress,
296
- onTimeUpdate,
297
- onProgress,
298
- onVideoComplete,
299
- hasCompleted
300
- ]);
464
+ }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);
301
465
  const handleLoadedMetadata = (0, import_react2.useCallback)(() => {
302
466
  if (videoRef.current) {
303
467
  setDuration(videoRef.current.duration);
@@ -326,9 +490,78 @@ var VideoPlayer = ({
326
490
  return () => {
327
491
  document.removeEventListener("visibilitychange", handleVisibilityChange);
328
492
  window.removeEventListener("blur", handleBlur);
493
+ clearControlsTimeout();
494
+ clearMouseMoveTimeout();
329
495
  };
330
- }, [isPlaying]);
496
+ }, [isPlaying, clearControlsTimeout, clearMouseMoveTimeout]);
331
497
  const progressPercentage = duration > 0 ? currentTime / duration * 100 : 0;
498
+ const getTopControlsOpacity = (0, import_react2.useCallback)(() => {
499
+ if (isFullscreen) {
500
+ return showControls ? "opacity-100" : "opacity-0";
501
+ }
502
+ return !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100";
503
+ }, [isFullscreen, showControls, isPlaying]);
504
+ const getBottomControlsOpacity = (0, import_react2.useCallback)(() => {
505
+ if (isFullscreen) {
506
+ return showControls ? "opacity-100" : "opacity-0";
507
+ }
508
+ return !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100";
509
+ }, [isFullscreen, showControls, isPlaying]);
510
+ const handleVideoKeyDown = (0, import_react2.useCallback)(
511
+ (e) => {
512
+ if (e.key) {
513
+ e.stopPropagation();
514
+ showControlsWithTimer();
515
+ }
516
+ switch (e.key) {
517
+ case " ":
518
+ case "Enter":
519
+ e.preventDefault();
520
+ togglePlayPause();
521
+ break;
522
+ case "ArrowLeft":
523
+ e.preventDefault();
524
+ if (videoRef.current) {
525
+ videoRef.current.currentTime -= 10;
526
+ }
527
+ break;
528
+ case "ArrowRight":
529
+ e.preventDefault();
530
+ if (videoRef.current) {
531
+ videoRef.current.currentTime += 10;
532
+ }
533
+ break;
534
+ case "ArrowUp":
535
+ e.preventDefault();
536
+ handleVolumeChange(Math.min(100, volume * 100 + 10));
537
+ break;
538
+ case "ArrowDown":
539
+ e.preventDefault();
540
+ handleVolumeChange(Math.max(0, volume * 100 - 10));
541
+ break;
542
+ case "m":
543
+ case "M":
544
+ e.preventDefault();
545
+ toggleMute();
546
+ break;
547
+ case "f":
548
+ case "F":
549
+ e.preventDefault();
550
+ toggleFullscreen();
551
+ break;
552
+ default:
553
+ break;
554
+ }
555
+ },
556
+ [
557
+ showControlsWithTimer,
558
+ togglePlayPause,
559
+ handleVolumeChange,
560
+ volume,
561
+ toggleMute,
562
+ toggleFullscreen
563
+ ]
564
+ );
332
565
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("flex flex-col", className), children: [
333
566
  (title || subtitleText) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bg-subject-1 rounded-t-xl px-8 py-4 flex items-end justify-between min-h-20", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-1", children: [
334
567
  title && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -355,12 +588,17 @@ var VideoPlayer = ({
355
588
  )
356
589
  ] }) }),
357
590
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
358
- "div",
591
+ "section",
359
592
  {
360
593
  className: cn(
361
594
  "relative w-full bg-background overflow-hidden group",
362
- title || subtitleText ? "rounded-b-xl" : "rounded-xl"
595
+ title || subtitleText ? "rounded-b-xl" : "rounded-xl",
596
+ // Hide cursor when controls are hidden and video is playing in fullscreen
597
+ isFullscreen && isPlaying && !showControls ? "cursor-none" : "cursor-default"
363
598
  ),
599
+ "aria-label": title ? `Video player: ${title}` : "Video player",
600
+ onMouseMove: isFullscreen ? handleMouseMove : showControlsWithTimer,
601
+ onMouseEnter: showControlsWithTimer,
364
602
  children: [
365
603
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
366
604
  "video",
@@ -373,39 +611,7 @@ var VideoPlayer = ({
373
611
  onTimeUpdate: handleTimeUpdate,
374
612
  onLoadedMetadata: handleLoadedMetadata,
375
613
  onClick: togglePlayPause,
376
- onKeyDown: (e) => {
377
- if (e.key) {
378
- setShowControls(true);
379
- }
380
- if (e.key === " " || e.key === "Enter") {
381
- e.preventDefault();
382
- togglePlayPause();
383
- }
384
- if (e.key === "ArrowLeft" && videoRef.current) {
385
- e.preventDefault();
386
- videoRef.current.currentTime -= 10;
387
- }
388
- if (e.key === "ArrowRight" && videoRef.current) {
389
- e.preventDefault();
390
- videoRef.current.currentTime += 10;
391
- }
392
- if (e.key === "ArrowUp") {
393
- e.preventDefault();
394
- handleVolumeChange(Math.min(100, volume * 100 + 10));
395
- }
396
- if (e.key === "ArrowDown") {
397
- e.preventDefault();
398
- handleVolumeChange(Math.max(0, volume * 100 - 10));
399
- }
400
- if (e.key === "m" || e.key === "M") {
401
- e.preventDefault();
402
- toggleMute();
403
- }
404
- if (e.key === "f" || e.key === "F") {
405
- e.preventDefault();
406
- toggleFullscreen();
407
- }
408
- },
614
+ onKeyDown: handleVideoKeyDown,
409
615
  tabIndex: 0,
410
616
  "aria-label": title ? `Video: ${title}` : "Video player",
411
617
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -435,9 +641,9 @@ var VideoPlayer = ({
435
641
  {
436
642
  className: cn(
437
643
  "absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity",
438
- !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100"
644
+ getTopControlsOpacity()
439
645
  ),
440
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "ml-auto block", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
646
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
441
647
  IconButton_default,
442
648
  {
443
649
  icon: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.ArrowsInSimple, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.ArrowsOutSimple, { size: 24 }),
@@ -453,29 +659,18 @@ var VideoPlayer = ({
453
659
  {
454
660
  className: cn(
455
661
  "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity",
456
- !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100"
662
+ getBottomControlsOpacity()
457
663
  ),
458
664
  children: [
459
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "px-4 pb-2", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
460
- "input",
665
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
666
+ ProgressBar,
461
667
  {
462
- type: "range",
463
- min: 0,
464
- max: duration || 100,
465
- value: currentTime,
466
- onChange: (e) => {
467
- const newTime = parseFloat(e.target.value);
468
- if (videoRef.current) {
469
- videoRef.current.currentTime = newTime;
470
- }
471
- },
472
- className: "w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500",
473
- "aria-label": "Video progress",
474
- style: {
475
- background: `linear-gradient(to right, #2271C4 ${progressPercentage}%, #D5D4D4 ${progressPercentage}%)`
476
- }
668
+ currentTime,
669
+ duration,
670
+ progressPercentage,
671
+ onSeek: handleSeek
477
672
  }
478
- ) }),
673
+ ),
479
674
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center justify-between px-4 pb-4", children: [
480
675
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-4", children: [
481
676
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -487,32 +682,15 @@ var VideoPlayer = ({
487
682
  className: "!bg-transparent !text-white hover:!bg-white/20"
488
683
  }
489
684
  ),
490
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
491
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
492
- IconButton_default,
493
- {
494
- icon: isMuted ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.SpeakerSlash, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.SpeakerHigh, { size: 24 }),
495
- onClick: toggleMute,
496
- "aria-label": isMuted ? "Unmute" : "Mute",
497
- className: "!bg-transparent !text-white hover:!bg-white/20"
498
- }
499
- ),
500
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
501
- "input",
502
- {
503
- type: "range",
504
- min: 0,
505
- max: 100,
506
- value: Math.round(volume * 100),
507
- onChange: (e) => handleVolumeChange(parseInt(e.target.value)),
508
- className: "w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500",
509
- "aria-label": "Volume control",
510
- style: {
511
- background: `linear-gradient(to right, #2271C4 ${volume * 100}%, #D5D4D4 ${volume * 100}%)`
512
- }
513
- }
514
- )
515
- ] }),
685
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
686
+ VolumeControls,
687
+ {
688
+ volume,
689
+ isMuted,
690
+ onVolumeChange: handleVolumeChange,
691
+ onToggleMute: toggleMute
692
+ }
693
+ ),
516
694
  subtitles && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
517
695
  IconButton_default,
518
696
  {
@@ -531,29 +709,15 @@ var VideoPlayer = ({
531
709
  formatTime(duration)
532
710
  ] })
533
711
  ] }),
534
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", children: [
535
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
536
- IconButton_default,
537
- {
538
- icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.DotsThreeVertical, { size: 24 }),
539
- onClick: toggleSpeedMenu,
540
- "aria-label": "Playback speed",
541
- className: "!bg-transparent !text-white hover:!bg-white/20"
542
- }
543
- ),
544
- showSpeedMenu && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute bottom-12 right-0 bg-black/90 rounded-lg p-2 min-w-20", children: [0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
545
- "button",
546
- {
547
- onClick: () => handleSpeedChange(speed),
548
- className: `block w-full text-left px-3 py-1 text-sm rounded hover:bg-white/20 transition-colors ${playbackRate === speed ? "text-primary-400" : "text-white"}`,
549
- children: [
550
- speed,
551
- "x"
552
- ]
553
- },
554
- speed
555
- )) })
556
- ] }) })
712
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
713
+ SpeedMenu,
714
+ {
715
+ showSpeedMenu,
716
+ playbackRate,
717
+ onToggleMenu: toggleSpeedMenu,
718
+ onSpeedChange: handleSpeedChange
719
+ }
720
+ ) })
557
721
  ] })
558
722
  ]
559
723
  }