@boyernick/standard-ui-react 0.4.2-canary.59 → 0.4.2-canary.60
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 +1 -1
- package/src/carousel.tsx +46 -24
- package/src/lib/media-glass.ts +14 -0
- package/src/modal.tsx +122 -114
- package/src/video-player.tsx +7 -18
package/package.json
CHANGED
package/src/carousel.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type KeyboardEvent,
|
|
16
16
|
} from "react"
|
|
17
17
|
import { Button } from "./button"
|
|
18
|
+
import { Glass, GlassRoot } from "./glass"
|
|
18
19
|
import { IconChevronRightSmall } from "./icons"
|
|
19
20
|
import { cn } from "./lib/cn"
|
|
20
21
|
import { focusRing, focusRingBorder } from "./lib/focus"
|
|
@@ -553,32 +554,53 @@ export const CarouselDots = ({ className, ...props }: CarouselDotsProps) => {
|
|
|
553
554
|
data-slot="carousel-dots"
|
|
554
555
|
role="tablist"
|
|
555
556
|
aria-label="Slide indicators"
|
|
556
|
-
className={cn("mt-4 flex items-center justify-start
|
|
557
|
+
className={cn("mt-4 flex items-center justify-start pl-4", className)}
|
|
557
558
|
{...props}
|
|
558
559
|
>
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
560
|
+
<GlassRoot interactiveLighting className="w-fit rounded-full">
|
|
561
|
+
<span aria-hidden className="absolute inset-0 rounded-full bg-background-secondary" />
|
|
562
|
+
<Glass
|
|
563
|
+
data-glass-radius="css"
|
|
564
|
+
className="glass-optical flex items-center gap-1 rounded-full px-3 py-2.5"
|
|
565
|
+
config={{
|
|
566
|
+
blurAmount: 0,
|
|
567
|
+
refraction: 0.12,
|
|
568
|
+
zRadius: 3,
|
|
569
|
+
chromAberration: 0,
|
|
570
|
+
tintStrength: 0,
|
|
571
|
+
edgeHighlight: 0.045,
|
|
572
|
+
specular: 0.02,
|
|
573
|
+
fresnel: 1,
|
|
574
|
+
shadowOpacity: 0.065,
|
|
575
|
+
shadowSpread: 12,
|
|
576
|
+
shadowOffsetY: 3,
|
|
577
|
+
}}
|
|
578
|
+
>
|
|
579
|
+
{scrollSnaps.map((_, index) => {
|
|
580
|
+
const isSelected = index === selectedIndex
|
|
581
|
+
|
|
582
|
+
return (
|
|
583
|
+
<button
|
|
584
|
+
key={index}
|
|
585
|
+
type="button"
|
|
586
|
+
role="tab"
|
|
587
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
588
|
+
aria-selected={isSelected}
|
|
589
|
+
data-selected={isSelected ? "" : undefined}
|
|
590
|
+
className={cn(
|
|
591
|
+
"h-1.5 cursor-pointer rounded-full outline-none",
|
|
592
|
+
motion.all,
|
|
593
|
+
"focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background-primary",
|
|
594
|
+
isSelected
|
|
595
|
+
? "w-5 bg-fg-tertiary"
|
|
596
|
+
: "w-1.5 bg-fg-quaternary/50 hover:bg-fg-quaternary",
|
|
597
|
+
)}
|
|
598
|
+
onClick={() => scrollTo(index)}
|
|
599
|
+
/>
|
|
600
|
+
)
|
|
601
|
+
})}
|
|
602
|
+
</Glass>
|
|
603
|
+
</GlassRoot>
|
|
582
604
|
</div>
|
|
583
605
|
)
|
|
584
606
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared optical treatment for controls over images and video. */
|
|
2
|
+
export const mediaGlass = {
|
|
3
|
+
blurAmount: 0.08,
|
|
4
|
+
refraction: 0.12,
|
|
5
|
+
zRadius: 3,
|
|
6
|
+
chromAberration: 0,
|
|
7
|
+
brightness: -0.3,
|
|
8
|
+
edgeHighlight: 0.045,
|
|
9
|
+
specular: 0.02,
|
|
10
|
+
fresnel: 1,
|
|
11
|
+
shadowOpacity: 0.065,
|
|
12
|
+
shadowSpread: 12,
|
|
13
|
+
shadowOffsetY: 3,
|
|
14
|
+
}
|
package/src/modal.tsx
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
type ReactElement,
|
|
15
15
|
type ReactNode,
|
|
16
16
|
} from "react"
|
|
17
|
-
import {
|
|
17
|
+
import { buttonVariants } from "./button"
|
|
18
|
+
import { GlassButton, GlassRoot } from "./glass"
|
|
19
|
+
import { mediaGlass } from "./lib/media-glass"
|
|
18
20
|
import {
|
|
19
21
|
Carousel,
|
|
20
22
|
CarouselContent,
|
|
@@ -51,10 +53,9 @@ import {
|
|
|
51
53
|
const TRACKPAD_LABEL =
|
|
52
54
|
"Swipe, use the arrow keys, or use the controls to navigate"
|
|
53
55
|
|
|
54
|
-
//
|
|
55
|
-
// hover fill is a page grey and is cleared, or it shows through the body.
|
|
56
|
+
// Match the video controls while preserving the image viewer focus treatment.
|
|
56
57
|
const modalControlClassName =
|
|
57
|
-
"glass glass-
|
|
58
|
+
"glass-optical glass-optical-media text-fg-scrim hover:text-fg-scrim outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-surface-scrim focus-visible:ring-ring/20"
|
|
58
59
|
|
|
59
60
|
export const modalContentVariants = cva(
|
|
60
61
|
"h-dvh max-h-dvh w-screen max-w-none gap-0 overflow-hidden rounded-none border-0 bg-transparent p-0 shadow-none max-sm:max-w-none",
|
|
@@ -249,11 +250,14 @@ const ModalDownload = ({
|
|
|
249
250
|
<ModalControlTooltip label="Download image">
|
|
250
251
|
<a
|
|
251
252
|
href={src}
|
|
253
|
+
data-slot="glass"
|
|
254
|
+
data-glass-radius="css"
|
|
255
|
+
data-config={JSON.stringify({ ...mediaGlass, button: true })}
|
|
252
256
|
download={name ?? ""}
|
|
253
257
|
aria-label="Download image"
|
|
254
258
|
className={cn(
|
|
255
259
|
buttonVariants({
|
|
256
|
-
variant: "
|
|
260
|
+
variant: "glass",
|
|
257
261
|
size: "md",
|
|
258
262
|
rounded: true,
|
|
259
263
|
iconOnly: true,
|
|
@@ -273,9 +277,9 @@ const ModalDismiss = () => (
|
|
|
273
277
|
<ModalControlTooltip label="Close image">
|
|
274
278
|
<DialogClose
|
|
275
279
|
render={
|
|
276
|
-
<
|
|
280
|
+
<GlassButton
|
|
277
281
|
type="button"
|
|
278
|
-
|
|
282
|
+
config={mediaGlass}
|
|
279
283
|
size="md"
|
|
280
284
|
iconOnly
|
|
281
285
|
rounded
|
|
@@ -322,22 +326,24 @@ export const ModalContent = ({
|
|
|
322
326
|
{...props}
|
|
323
327
|
>
|
|
324
328
|
<TooltipProvider delay={250}>
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
329
|
+
<GlassRoot interactiveLighting defaults={mediaGlass} className="size-full">
|
|
330
|
+
<ModalBackground src={src} />
|
|
331
|
+
<DialogTitle className="sr-only">{alt}</DialogTitle>
|
|
332
|
+
<div
|
|
333
|
+
data-slot="modal-stage"
|
|
334
|
+
className="relative z-10 flex size-full flex-col items-center justify-center p-4"
|
|
335
|
+
>
|
|
336
|
+
<ModalImage src={src} alt={alt} imgProps={imgProps} />
|
|
337
|
+
{variant === "caption" && (caption || children) ? (
|
|
338
|
+
<ModalCaption>{caption ?? children}</ModalCaption>
|
|
339
|
+
) : null}
|
|
340
|
+
</div>
|
|
341
|
+
<ModalDismiss />
|
|
342
|
+
<ModalDownload
|
|
343
|
+
src={downloadSrc ?? src}
|
|
344
|
+
name={downloadName}
|
|
345
|
+
/>
|
|
346
|
+
</GlassRoot>
|
|
341
347
|
</TooltipProvider>
|
|
342
348
|
</DialogPopup>
|
|
343
349
|
</DialogPortal>
|
|
@@ -493,99 +499,101 @@ const GalleryContent = ({
|
|
|
493
499
|
)}
|
|
494
500
|
>
|
|
495
501
|
<TooltipProvider delay={250}>
|
|
496
|
-
<
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
<div className="flex min-h-0 w-full items-center justify-center
|
|
518
|
-
<
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
502
|
+
<GlassRoot interactiveLighting defaults={mediaGlass} className="size-full">
|
|
503
|
+
<ModalBackground src={activeImage.src} />
|
|
504
|
+
<DialogTitle className="sr-only">{activeImage.alt}</DialogTitle>
|
|
505
|
+
<p className="sr-only" aria-live="polite">
|
|
506
|
+
Image {activeIndex + 1} of {images.length}. {TRACKPAD_LABEL}.
|
|
507
|
+
</p>
|
|
508
|
+
<div className="relative z-10 flex size-full flex-col gap-4 p-4">
|
|
509
|
+
<Carousel
|
|
510
|
+
setApi={setApi}
|
|
511
|
+
opts={{ loop, startIndex: activeIndex, duration: 24, dragFree: false }}
|
|
512
|
+
data-slot="modal-stage"
|
|
513
|
+
className="relative min-h-0 w-full min-w-0 flex-1 [&_[data-slot=carousel-viewport]]:h-full [&_[data-slot=carousel-viewport]]:min-h-0"
|
|
514
|
+
aria-label="Gallery"
|
|
515
|
+
>
|
|
516
|
+
<CarouselContent className="h-full !ml-0 !pr-0">
|
|
517
|
+
{images.map((image, index) => (
|
|
518
|
+
<CarouselItem
|
|
519
|
+
key={`${image.src}-${index}`}
|
|
520
|
+
className="!basis-full flex h-full min-h-0 flex-col items-center justify-center !pl-0"
|
|
521
|
+
aria-label={`${index + 1} of ${images.length}`}
|
|
522
|
+
>
|
|
523
|
+
<div className="flex max-h-full min-h-0 w-full flex-col items-center justify-center">
|
|
524
|
+
<div className="flex min-h-0 w-full items-center justify-center overflow-hidden">
|
|
525
|
+
<ModalImage
|
|
526
|
+
{...image}
|
|
527
|
+
fit="contain"
|
|
528
|
+
imgProps={{
|
|
529
|
+
...image.imgProps,
|
|
530
|
+
className: cn(
|
|
531
|
+
"max-w-[min(94vw,72rem)]",
|
|
532
|
+
image.imgProps?.className,
|
|
533
|
+
),
|
|
534
|
+
}}
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
{variant === "caption" && image.caption ? (
|
|
538
|
+
<ModalCaption>{image.caption}</ModalCaption>
|
|
539
|
+
) : null}
|
|
529
540
|
</div>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
541
|
+
</CarouselItem>
|
|
542
|
+
))}
|
|
543
|
+
</CarouselContent>
|
|
544
|
+
</Carousel>
|
|
545
|
+
<GalleryFilmstrip
|
|
546
|
+
images={images}
|
|
547
|
+
activeIndex={activeIndex}
|
|
548
|
+
onSelect={handleSelect}
|
|
549
|
+
/>
|
|
550
|
+
</div>
|
|
551
|
+
<ModalDismiss />
|
|
552
|
+
<ModalDownload
|
|
553
|
+
src={activeImage.downloadSrc ?? activeImage.src}
|
|
554
|
+
name={activeImage.downloadName}
|
|
542
555
|
/>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
<IconChevronRightSmall aria-hidden />
|
|
585
|
-
</Button>
|
|
586
|
-
</ModalControlTooltip>
|
|
587
|
-
</>
|
|
588
|
-
) : null}
|
|
556
|
+
{images.length > 1 ? (
|
|
557
|
+
<>
|
|
558
|
+
<ModalControlTooltip label="Previous image" side="right">
|
|
559
|
+
<GlassButton
|
|
560
|
+
type="button"
|
|
561
|
+
config={mediaGlass}
|
|
562
|
+
size="lg"
|
|
563
|
+
iconOnly
|
|
564
|
+
rounded
|
|
565
|
+
aria-label="Previous image"
|
|
566
|
+
disabled={!loop && !api?.canScrollPrev()}
|
|
567
|
+
onClick={() => api?.scrollPrev()}
|
|
568
|
+
className={cn(
|
|
569
|
+
modalControlClassName,
|
|
570
|
+
"absolute top-1/2 left-4 z-20 -translate-y-1/2 active:!-translate-y-1/2",
|
|
571
|
+
)}
|
|
572
|
+
>
|
|
573
|
+
<IconChevronRightSmall className="rotate-180" aria-hidden />
|
|
574
|
+
</GlassButton>
|
|
575
|
+
</ModalControlTooltip>
|
|
576
|
+
<ModalControlTooltip label="Next image" side="left">
|
|
577
|
+
<GlassButton
|
|
578
|
+
type="button"
|
|
579
|
+
config={mediaGlass}
|
|
580
|
+
size="lg"
|
|
581
|
+
iconOnly
|
|
582
|
+
rounded
|
|
583
|
+
aria-label="Next image"
|
|
584
|
+
disabled={!loop && !api?.canScrollNext()}
|
|
585
|
+
onClick={() => api?.scrollNext()}
|
|
586
|
+
className={cn(
|
|
587
|
+
modalControlClassName,
|
|
588
|
+
"absolute top-1/2 right-4 z-20 -translate-y-1/2 active:!-translate-y-1/2",
|
|
589
|
+
)}
|
|
590
|
+
>
|
|
591
|
+
<IconChevronRightSmall aria-hidden />
|
|
592
|
+
</GlassButton>
|
|
593
|
+
</ModalControlTooltip>
|
|
594
|
+
</>
|
|
595
|
+
) : null}
|
|
596
|
+
</GlassRoot>
|
|
589
597
|
</TooltipProvider>
|
|
590
598
|
</DialogPopup>
|
|
591
599
|
</DialogPortal>
|
package/src/video-player.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import { IconVolumeFull } from "@central-icons-react/round-filled-radius-2-strok
|
|
|
18
18
|
import { IconVolumeOff } from "@central-icons-react/round-filled-radius-2-stroke-2/IconVolumeOff"
|
|
19
19
|
import { cn } from "./lib/cn"
|
|
20
20
|
import { Glass, GlassButton, GlassRoot } from "./glass"
|
|
21
|
+
import { mediaGlass } from "./lib/media-glass"
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Picture-in-Picture support is a static browser capability, not React state.
|
|
@@ -36,19 +37,7 @@ const formatTime = (seconds: number) => {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// All panes are direct children of the video root, sharing one WebGL context.
|
|
39
|
-
|
|
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
|
-
}
|
|
40
|
+
|
|
52
41
|
|
|
53
42
|
export type VideoPlayerProps = ComponentProps<"div"> & {
|
|
54
43
|
src: string
|
|
@@ -284,7 +273,7 @@ export const VideoPlayer = ({
|
|
|
284
273
|
return (
|
|
285
274
|
<GlassRoot
|
|
286
275
|
interactiveLighting
|
|
287
|
-
defaults={
|
|
276
|
+
defaults={mediaGlass}
|
|
288
277
|
ref={playerRef}
|
|
289
278
|
data-slot="video-player"
|
|
290
279
|
data-playing={playing || undefined}
|
|
@@ -350,7 +339,7 @@ export const VideoPlayer = ({
|
|
|
350
339
|
type="button"
|
|
351
340
|
rounded
|
|
352
341
|
iconOnly
|
|
353
|
-
config={
|
|
342
|
+
config={mediaGlass}
|
|
354
343
|
className={cn(controlVisibility, "left-2 group-data-[fullscreen]:left-5", (failed || loading) && "invisible")}
|
|
355
344
|
aria-label={ended ? "Replay" : playing ? "Pause" : "Play"}
|
|
356
345
|
onClick={handleTogglePlay}
|
|
@@ -365,7 +354,7 @@ export const VideoPlayer = ({
|
|
|
365
354
|
type="button"
|
|
366
355
|
rounded
|
|
367
356
|
iconOnly
|
|
368
|
-
config={
|
|
357
|
+
config={mediaGlass}
|
|
369
358
|
className={cn(controlVisibility, "left-13 group-data-[fullscreen]:left-16", (failed || loading) && "invisible")}
|
|
370
359
|
aria-label={muted ? "Unmute" : "Mute"}
|
|
371
360
|
onClick={handleToggleMute}
|
|
@@ -473,7 +462,7 @@ export const VideoPlayer = ({
|
|
|
473
462
|
type="button"
|
|
474
463
|
rounded
|
|
475
464
|
iconOnly
|
|
476
|
-
config={
|
|
465
|
+
config={mediaGlass}
|
|
477
466
|
className={cn(controlVisibility, "right-13 group-data-[fullscreen]:right-16", (!pipSupported || failed || loading) && "invisible")}
|
|
478
467
|
aria-label={
|
|
479
468
|
pipActive
|
|
@@ -494,7 +483,7 @@ export const VideoPlayer = ({
|
|
|
494
483
|
type="button"
|
|
495
484
|
rounded
|
|
496
485
|
iconOnly
|
|
497
|
-
config={
|
|
486
|
+
config={mediaGlass}
|
|
498
487
|
className={cn(controlVisibility, "right-2 group-data-[fullscreen]:right-5", (failed || loading) && "invisible")}
|
|
499
488
|
aria-label={fullscreen ? "Exit fullscreen" : "Fullscreen"}
|
|
500
489
|
aria-pressed={fullscreen}
|