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