analytica-frontend-lib 1.1.10 → 1.1.12

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