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