@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyernick/standard-ui-react",
3
- "version": "0.4.2-canary.59",
3
+ "version": "0.4.2-canary.60",
4
4
  "description": "React components for StandardUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 gap-1 pl-4", className)}
557
+ className={cn("mt-4 flex items-center justify-start pl-4", className)}
557
558
  {...props}
558
559
  >
559
- {scrollSnaps.map((_, index) => {
560
- const isSelected = index === selectedIndex
561
-
562
- return (
563
- <button
564
- key={index}
565
- type="button"
566
- role="tab"
567
- aria-label={`Go to slide ${index + 1}`}
568
- aria-selected={isSelected}
569
- data-selected={isSelected ? "" : undefined}
570
- className={cn(
571
- "h-1.5 cursor-pointer rounded-full outline-none",
572
- motion.all,
573
- "focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background-primary",
574
- isSelected
575
- ? "w-5 bg-fg-tertiary"
576
- : "w-1.5 bg-fg-quaternary/50 hover:bg-fg-quaternary",
577
- )}
578
- onClick={() => scrollTo(index)}
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 { Button, buttonVariants } from "./button"
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
- // Glass takes a glass hover: the lens brightens. The ghost button's own
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-dark hover:bg-transparent hover:brightness-150 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
+ "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: "ghost",
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
- <Button
280
+ <GlassButton
277
281
  type="button"
278
- variant="ghost"
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
- <ModalBackground src={src} />
326
- <DialogTitle className="sr-only">{alt}</DialogTitle>
327
- <div
328
- data-slot="modal-stage"
329
- className="relative z-10 flex size-full flex-col items-center justify-center p-4"
330
- >
331
- <ModalImage src={src} alt={alt} imgProps={imgProps} />
332
- {variant === "caption" && (caption || children) ? (
333
- <ModalCaption>{caption ?? children}</ModalCaption>
334
- ) : null}
335
- </div>
336
- <ModalDismiss />
337
- <ModalDownload
338
- src={downloadSrc ?? src}
339
- name={downloadName}
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
- <ModalBackground src={activeImage.src} />
497
- <DialogTitle className="sr-only">{activeImage.alt}</DialogTitle>
498
- <p className="sr-only" aria-live="polite">
499
- Image {activeIndex + 1} of {images.length}. {TRACKPAD_LABEL}.
500
- </p>
501
- <div className="relative z-10 flex size-full flex-col gap-4 p-4">
502
- <Carousel
503
- setApi={setApi}
504
- opts={{ loop, startIndex: activeIndex, duration: 24, dragFree: false }}
505
- data-slot="modal-stage"
506
- 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"
507
- aria-label="Gallery"
508
- >
509
- <CarouselContent className="h-full !ml-0 !pr-0">
510
- {images.map((image, index) => (
511
- <CarouselItem
512
- key={`${image.src}-${index}`}
513
- className="!basis-full flex h-full min-h-0 flex-col items-center justify-center !pl-0"
514
- aria-label={`${index + 1} of ${images.length}`}
515
- >
516
- <div className="flex max-h-full min-h-0 w-full flex-col items-center justify-center">
517
- <div className="flex min-h-0 w-full items-center justify-center overflow-hidden">
518
- <ModalImage
519
- {...image}
520
- fit="contain"
521
- imgProps={{
522
- ...image.imgProps,
523
- className: cn(
524
- "max-w-[min(94vw,72rem)]",
525
- image.imgProps?.className,
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
- {variant === "caption" && image.caption ? (
531
- <ModalCaption>{image.caption}</ModalCaption>
532
- ) : null}
533
- </div>
534
- </CarouselItem>
535
- ))}
536
- </CarouselContent>
537
- </Carousel>
538
- <GalleryFilmstrip
539
- images={images}
540
- activeIndex={activeIndex}
541
- onSelect={handleSelect}
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
- </div>
544
- <ModalDismiss />
545
- <ModalDownload
546
- src={activeImage.downloadSrc ?? activeImage.src}
547
- name={activeImage.downloadName}
548
- />
549
- {images.length > 1 ? (
550
- <>
551
- <ModalControlTooltip label="Previous image" side="right">
552
- <Button
553
- type="button"
554
- variant="ghost"
555
- size="lg"
556
- iconOnly
557
- rounded
558
- aria-label="Previous image"
559
- disabled={!loop && !api?.canScrollPrev()}
560
- onClick={() => api?.scrollPrev()}
561
- className={cn(
562
- modalControlClassName,
563
- "absolute top-1/2 left-4 z-20 -translate-y-1/2 active:!-translate-y-1/2",
564
- )}
565
- >
566
- <IconChevronRightSmall className="rotate-180" aria-hidden />
567
- </Button>
568
- </ModalControlTooltip>
569
- <ModalControlTooltip label="Next image" side="left">
570
- <Button
571
- type="button"
572
- variant="ghost"
573
- size="lg"
574
- iconOnly
575
- rounded
576
- aria-label="Next image"
577
- disabled={!loop && !api?.canScrollNext()}
578
- onClick={() => api?.scrollNext()}
579
- className={cn(
580
- modalControlClassName,
581
- "absolute top-1/2 right-4 z-20 -translate-y-1/2 active:!-translate-y-1/2",
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>
@@ -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
- 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
- }
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={videoGlass}
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={videoGlass}
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={videoGlass}
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={videoGlass}
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={videoGlass}
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}