@boyernick/standard-ui-react 0.4.2-canary.57 → 0.4.2-canary.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/src/button.tsx +6 -1
- package/src/glass.tsx +201 -0
- package/src/index.ts +5 -0
- package/src/lib/glass-lifecycle.ts +33 -0
- package/src/lib/glass-lighting.ts +33 -0
- package/src/lib/liquidglass/NOTICE.md +24 -0
- package/src/lib/liquidglass/index.d.ts +45 -0
- package/src/lib/liquidglass/index.js +2862 -0
- package/src/video-player.tsx +206 -222
package/src/video-player.tsx
CHANGED
|
@@ -17,13 +17,14 @@ import { IconPlay } from "@central-icons-react/round-filled-radius-2-stroke-2/Ic
|
|
|
17
17
|
import { IconVolumeFull } from "@central-icons-react/round-filled-radius-2-stroke-2/IconVolumeFull"
|
|
18
18
|
import { IconVolumeOff } from "@central-icons-react/round-filled-radius-2-stroke-2/IconVolumeOff"
|
|
19
19
|
import { cn } from "./lib/cn"
|
|
20
|
+
import { Glass, GlassButton, GlassRoot } from "./glass"
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Picture-in-Picture support is a static browser capability, not React state.
|
|
23
24
|
* Reading it through an external store keeps the server snapshot `false` so
|
|
24
25
|
* hydration matches, without a post-mount setState that re-renders the player.
|
|
25
26
|
*/
|
|
26
|
-
const subscribePipSupport = () => () => {}
|
|
27
|
+
const subscribePipSupport = () => () => { }
|
|
27
28
|
const getPipSupported = () => document.pictureInPictureEnabled
|
|
28
29
|
const getPipSupportedOnServer = () => false
|
|
29
30
|
|
|
@@ -34,26 +35,20 @@ const formatTime = (seconds: number) => {
|
|
|
34
35
|
return `${mins}:${secs.toString().padStart(2, "0")}`
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const overlayControl =
|
|
52
|
-
"glass glass-dark [--glass-alpha:52%] inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full text-white/85 outline-none transition-[filter,color,transform] duration-[var(--duration-sm)] ease-enter hover:text-white hover:brightness-150 active:scale-95 focus-visible:text-white focus-visible:ring-2 focus-visible:ring-white/50 motion-reduce:transition-none motion-reduce:active:scale-100"
|
|
53
|
-
|
|
54
|
-
/** The scrubber and its two timecodes travel together as one object. */
|
|
55
|
-
const overlayTrack =
|
|
56
|
-
"glass glass-dark [--glass-alpha:52%] flex h-9 min-w-0 flex-1 items-center gap-2 rounded-full px-3"
|
|
38
|
+
// All panes are direct children of the video root, sharing one WebGL context.
|
|
39
|
+
const videoGlass = {
|
|
40
|
+
blurAmount: 0.08,
|
|
41
|
+
refraction: 0.12,
|
|
42
|
+
zRadius: 3,
|
|
43
|
+
chromAberration: 0,
|
|
44
|
+
brightness: -0.3,
|
|
45
|
+
edgeHighlight: 0.045,
|
|
46
|
+
specular: 0.02,
|
|
47
|
+
fresnel: 1,
|
|
48
|
+
shadowOpacity: 0.065,
|
|
49
|
+
shadowSpread: 12,
|
|
50
|
+
shadowOffsetY: 3,
|
|
51
|
+
}
|
|
57
52
|
|
|
58
53
|
export type VideoPlayerProps = ComponentProps<"div"> & {
|
|
59
54
|
src: string
|
|
@@ -279,8 +274,17 @@ export const VideoPlayer = ({
|
|
|
279
274
|
const loaded = duration > 0 ? (buffered / duration) * 100 : 0
|
|
280
275
|
const remaining = Math.max(duration - current, 0)
|
|
281
276
|
|
|
277
|
+
const controlVisibility = cn(
|
|
278
|
+
"glass-optical glass-optical-media absolute bottom-2 transition-opacity duration-[var(--duration-md)] ease-enter group-data-[fullscreen]:bottom-5 motion-reduce:transition-none",
|
|
279
|
+
"group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto",
|
|
280
|
+
"[@media(hover:none)]:opacity-100 [@media(hover:none)]:pointer-events-auto",
|
|
281
|
+
playing ? "pointer-events-none opacity-0" : "opacity-100",
|
|
282
|
+
)
|
|
283
|
+
|
|
282
284
|
return (
|
|
283
|
-
<
|
|
285
|
+
<GlassRoot
|
|
286
|
+
interactiveLighting
|
|
287
|
+
defaults={videoGlass}
|
|
284
288
|
ref={playerRef}
|
|
285
289
|
data-slot="video-player"
|
|
286
290
|
data-playing={playing || undefined}
|
|
@@ -313,216 +317,196 @@ export const VideoPlayer = ({
|
|
|
313
317
|
onClick={handleTogglePlay}
|
|
314
318
|
/>
|
|
315
319
|
|
|
316
|
-
{/*
|
|
317
|
-
|
|
320
|
+
{/* The status pane stays mounted so startup owns a stable set of panes.
|
|
321
|
+
It remains inert; clicking the video still toggles playback. */}
|
|
322
|
+
<Glass
|
|
323
|
+
data-glass-radius="css"
|
|
324
|
+
className={cn(
|
|
325
|
+
"glass-optical glass-optical-media pointer-events-none absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center text-fg-scrim",
|
|
326
|
+
failed ? "max-w-sm flex-col gap-2 rounded-xl px-5 py-4 text-center" : ended ? "h-8 gap-1 rounded-full px-2.5" : "size-11 rounded-full",
|
|
327
|
+
playing && !buffering && !loading && !failed && "invisible",
|
|
328
|
+
)}
|
|
329
|
+
>
|
|
318
330
|
{failed ? (
|
|
319
|
-
<div
|
|
320
|
-
|
|
321
|
-
className="mx-6 flex max-w-sm flex-col items-center gap-2 rounded-xl glass glass-dark px-5 py-4 text-center text-white"
|
|
322
|
-
>
|
|
323
|
-
<IconExclamationTriangle
|
|
324
|
-
size={22}
|
|
325
|
-
mode="raw"
|
|
326
|
-
className="size-5.5"
|
|
327
|
-
aria-hidden
|
|
328
|
-
/>
|
|
331
|
+
<div role="alert" className="flex flex-col items-center gap-2">
|
|
332
|
+
<IconExclamationTriangle size={22} mode="raw" aria-hidden />
|
|
329
333
|
<span className="text-sm-strong">Video unavailable</span>
|
|
330
|
-
<span className="text-xs text-
|
|
331
|
-
Check the source and try again.
|
|
332
|
-
</span>
|
|
334
|
+
<span className="text-xs text-fg-scrim-secondary">Check the source and try again.</span>
|
|
333
335
|
</div>
|
|
334
336
|
) : loading || buffering ? (
|
|
335
|
-
<div
|
|
336
|
-
|
|
337
|
-
className="
|
|
338
|
-
>
|
|
339
|
-
<span className="size-6 animate-spin rounded-full border-2 border-white/30 border-t-white motion-reduce:animate-none" />
|
|
340
|
-
<span className="sr-only">
|
|
341
|
-
{buffering ? "Buffering video" : "Loading video"}
|
|
342
|
-
</span>
|
|
337
|
+
<div role="status">
|
|
338
|
+
<span className="block size-6 animate-spin rounded-full border-2 border-fg-scrim/30 border-t-fg-scrim motion-reduce:animate-none" />
|
|
339
|
+
<span className="sr-only">{buffering ? "Buffering video" : "Loading video"}</span>
|
|
343
340
|
</div>
|
|
344
|
-
) :
|
|
345
|
-
|
|
346
|
-
className={
|
|
347
|
-
"flex items-center justify-center rounded-full glass glass-dark text-white",
|
|
348
|
-
ended ? "h-8 gap-1 px-2.5" : "size-11",
|
|
349
|
-
)}
|
|
350
|
-
>
|
|
351
|
-
<IconPlay
|
|
352
|
-
size={ended ? 12 : 18}
|
|
353
|
-
mode="raw"
|
|
354
|
-
// No nudge: Central's play glyph is already optically centred in
|
|
355
|
-
// its 24-unit box (its mass sits at x 11.7), so a margin shifts
|
|
356
|
-
// it right a second time.
|
|
357
|
-
className={cn(ended ? "size-3" : "size-4.5")}
|
|
358
|
-
aria-hidden
|
|
359
|
-
/>
|
|
341
|
+
) : (
|
|
342
|
+
<>
|
|
343
|
+
<IconPlay size={ended ? 12 : 18} mode="raw" className={ended ? "size-3" : "size-4.5"} aria-hidden />
|
|
360
344
|
{ended ? <span className="text-xs-strong">Replay</span> : null}
|
|
361
|
-
|
|
362
|
-
)
|
|
363
|
-
</
|
|
345
|
+
</>
|
|
346
|
+
)}
|
|
347
|
+
</Glass>
|
|
364
348
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
<span className="text-xs min-w-9 shrink-0 tabular-nums text-white/85">
|
|
418
|
-
{formatTime(current)}
|
|
419
|
-
</span>
|
|
349
|
+
<GlassButton
|
|
350
|
+
type="button"
|
|
351
|
+
rounded
|
|
352
|
+
iconOnly
|
|
353
|
+
config={videoGlass}
|
|
354
|
+
className={cn(controlVisibility, "left-2 group-data-[fullscreen]:left-5", (failed || loading) && "invisible")}
|
|
355
|
+
aria-label={ended ? "Replay" : playing ? "Pause" : "Play"}
|
|
356
|
+
onClick={handleTogglePlay}
|
|
357
|
+
>
|
|
358
|
+
{playing ? (
|
|
359
|
+
<IconPause size={14} mode="raw" className="size-3.5" aria-hidden />
|
|
360
|
+
) : (
|
|
361
|
+
<IconPlay size={14} mode="raw" className="size-3.5" aria-hidden />
|
|
362
|
+
)}
|
|
363
|
+
</GlassButton>
|
|
364
|
+
<GlassButton
|
|
365
|
+
type="button"
|
|
366
|
+
rounded
|
|
367
|
+
iconOnly
|
|
368
|
+
config={videoGlass}
|
|
369
|
+
className={cn(controlVisibility, "left-13 group-data-[fullscreen]:left-16", (failed || loading) && "invisible")}
|
|
370
|
+
aria-label={muted ? "Unmute" : "Mute"}
|
|
371
|
+
onClick={handleToggleMute}
|
|
372
|
+
>
|
|
373
|
+
{muted ? (
|
|
374
|
+
<IconVolumeOff
|
|
375
|
+
size={14}
|
|
376
|
+
mode="raw"
|
|
377
|
+
className="size-3.5"
|
|
378
|
+
aria-hidden
|
|
379
|
+
/>
|
|
380
|
+
) : (
|
|
381
|
+
<IconVolumeFull
|
|
382
|
+
size={14}
|
|
383
|
+
mode="raw"
|
|
384
|
+
className="size-3.5"
|
|
385
|
+
aria-hidden
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
</GlassButton>
|
|
389
|
+
<Glass
|
|
390
|
+
data-glass-radius="css"
|
|
391
|
+
className={cn(
|
|
392
|
+
controlVisibility,
|
|
393
|
+
"left-24 flex h-9 min-w-0 items-center gap-2 rounded-full px-3 group-data-[fullscreen]:left-27",
|
|
394
|
+
pipSupported ? "right-24 group-data-[fullscreen]:right-27" : "right-13 group-data-[fullscreen]:right-16",
|
|
395
|
+
(failed || loading) && "invisible",
|
|
396
|
+
)}
|
|
397
|
+
>
|
|
398
|
+
<span className="text-xs min-w-9 shrink-0 tabular-nums text-white/85">
|
|
399
|
+
{formatTime(current)}
|
|
400
|
+
</span>
|
|
420
401
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
402
|
+
<label className="sr-only" htmlFor={seekId}>
|
|
403
|
+
Seek
|
|
404
|
+
</label>
|
|
405
|
+
<div className="group/seek relative flex h-5 min-w-0 flex-1 items-center">
|
|
406
|
+
<span
|
|
407
|
+
ref={seekPreviewRef}
|
|
408
|
+
className="text-xs pointer-events-none absolute -top-8 z-10 -translate-x-1/2 rounded-md bg-black/80 px-1.5 py-1 text-white opacity-0 shadow-sm transition-opacity duration-[var(--duration-sm)] data-[visible]:opacity-100 motion-reduce:transition-none"
|
|
409
|
+
/>
|
|
410
|
+
{/* Track, buffered fill and played fill sit under the input, which
|
|
430
411
|
stays transparent so its thumb is the only thing it paints. */}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
412
|
+
<span
|
|
413
|
+
aria-hidden
|
|
414
|
+
className="pointer-events-none absolute inset-x-0 h-0.75 rounded-full bg-white/25 transition-[height] duration-[var(--duration-sm)] group-hover/seek:h-1.5 group-focus-within/seek:h-1.5 motion-reduce:transition-none"
|
|
415
|
+
/>
|
|
416
|
+
<span
|
|
417
|
+
aria-hidden
|
|
418
|
+
className="pointer-events-none absolute left-0 h-0.75 rounded-full bg-white/35 transition-[height] duration-[var(--duration-sm)] group-hover/seek:h-1.5 group-focus-within/seek:h-1.5 motion-reduce:transition-none"
|
|
419
|
+
style={{ width: `${loaded}%` }}
|
|
420
|
+
/>
|
|
421
|
+
<span
|
|
422
|
+
aria-hidden
|
|
423
|
+
className="pointer-events-none absolute left-0 h-0.75 rounded-full bg-white transition-[height] duration-[var(--duration-sm)] group-hover/seek:h-1.5 group-focus-within/seek:h-1.5 motion-reduce:transition-none"
|
|
424
|
+
style={{ width: `${played}%` }}
|
|
425
|
+
/>
|
|
426
|
+
<input
|
|
427
|
+
id={seekId}
|
|
428
|
+
type="range"
|
|
429
|
+
min={0}
|
|
430
|
+
max={duration || 0}
|
|
431
|
+
step={0.01}
|
|
432
|
+
value={current}
|
|
433
|
+
onChange={(event) => {
|
|
434
|
+
const value = Number(event.target.value)
|
|
435
|
+
handleSeek(value)
|
|
436
|
+
showSeekPreview(value)
|
|
437
|
+
}}
|
|
438
|
+
onPointerMove={(event) => {
|
|
439
|
+
if (duration <= 0) return
|
|
440
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
441
|
+
const ratio = Math.min(
|
|
442
|
+
Math.max((event.clientX - rect.left) / rect.width, 0),
|
|
443
|
+
1,
|
|
444
|
+
)
|
|
445
|
+
showSeekPreview(ratio * duration)
|
|
446
|
+
}}
|
|
447
|
+
onPointerLeave={(event) => {
|
|
448
|
+
if (document.activeElement !== event.currentTarget) {
|
|
449
|
+
hideSeekPreview()
|
|
450
|
+
}
|
|
451
|
+
}}
|
|
452
|
+
onFocus={() => showSeekPreview(current)}
|
|
453
|
+
onBlur={hideSeekPreview}
|
|
454
|
+
aria-valuetext={`${formatTime(current)} of ${formatTime(duration)}`}
|
|
455
|
+
className={cn(
|
|
456
|
+
"relative m-0 h-5 w-full cursor-pointer appearance-none bg-transparent outline-none",
|
|
457
|
+
// Both vendor thumbs, or Firefox falls back to a default one.
|
|
458
|
+
"[&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:scale-75 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:opacity-0 [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:transition-[opacity,transform] [&:hover::-webkit-slider-thumb]:scale-100 [&:hover::-webkit-slider-thumb]:opacity-100",
|
|
459
|
+
"[&::-moz-range-thumb]:size-3.5 [&::-moz-range-thumb]:scale-75 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:opacity-0 [&::-moz-range-thumb]:transition-[opacity,transform] [&:hover::-moz-range-thumb]:scale-100 [&:hover::-moz-range-thumb]:opacity-100",
|
|
460
|
+
"focus-visible:[&::-webkit-slider-thumb]:ring-2 focus-visible:[&::-webkit-slider-thumb]:ring-white/70",
|
|
461
|
+
"focus-visible:[&::-webkit-slider-thumb]:scale-100 focus-visible:[&::-webkit-slider-thumb]:opacity-100 focus-visible:[&::-moz-range-thumb]:scale-100 focus-visible:[&::-moz-range-thumb]:opacity-100",
|
|
462
|
+
)}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
484
465
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
466
|
+
<span className="text-xs min-w-10 shrink-0 text-right tabular-nums text-white/85">
|
|
467
|
+
-{formatTime(remaining)}
|
|
468
|
+
</span>
|
|
469
|
+
</Glass>
|
|
489
470
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
471
|
+
|
|
472
|
+
<GlassButton
|
|
473
|
+
type="button"
|
|
474
|
+
rounded
|
|
475
|
+
iconOnly
|
|
476
|
+
config={videoGlass}
|
|
477
|
+
className={cn(controlVisibility, "right-13 group-data-[fullscreen]:right-16", (!pipSupported || failed || loading) && "invisible")}
|
|
478
|
+
aria-label={
|
|
479
|
+
pipActive
|
|
480
|
+
? "Exit picture in picture"
|
|
481
|
+
: "Picture in picture"
|
|
482
|
+
}
|
|
483
|
+
aria-pressed={pipActive}
|
|
484
|
+
onClick={handlePictureInPicture}
|
|
485
|
+
>
|
|
486
|
+
<IconPictureInPicture
|
|
487
|
+
size={14}
|
|
488
|
+
mode="raw"
|
|
489
|
+
className="size-3.5"
|
|
490
|
+
aria-hidden
|
|
491
|
+
/>
|
|
492
|
+
</GlassButton>
|
|
493
|
+
<GlassButton
|
|
494
|
+
type="button"
|
|
495
|
+
rounded
|
|
496
|
+
iconOnly
|
|
497
|
+
config={videoGlass}
|
|
498
|
+
className={cn(controlVisibility, "right-2 group-data-[fullscreen]:right-5", (failed || loading) && "invisible")}
|
|
499
|
+
aria-label={fullscreen ? "Exit fullscreen" : "Fullscreen"}
|
|
500
|
+
aria-pressed={fullscreen}
|
|
501
|
+
onClick={handleFullscreen}
|
|
502
|
+
>
|
|
503
|
+
<IconFullScreen
|
|
504
|
+
size={14}
|
|
505
|
+
mode="raw"
|
|
506
|
+
className="size-3.5"
|
|
507
|
+
aria-hidden
|
|
508
|
+
/>
|
|
509
|
+
</GlassButton>
|
|
510
|
+
</GlassRoot>
|
|
527
511
|
)
|
|
528
512
|
}
|