@passui/react 0.1.44 → 0.1.46

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.
Files changed (140) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +32 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -398
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +279 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -322
@@ -1,80 +1,96 @@
1
- import { createContext, useContext, useEffect, useState, type ComponentProps } from "react"
2
- import { cn } from "../../lib/class-names"
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ type ComponentProps,
7
+ } from "react";
8
+ import { cn } from "../../lib/class-names";
3
9
 
4
- export type ProfileImageSize = "sm" | "md" | "lg"
10
+ export type ProfileImageSize = "sm" | "md" | "lg";
5
11
 
6
12
  const diameters: Record<ProfileImageSize, string> = {
7
- sm: "size-7",
8
- md: "size-9",
9
- lg: "size-12",
10
- }
13
+ sm: "size-7",
14
+ md: "size-9",
15
+ lg: "size-12",
16
+ };
11
17
 
12
18
  const fallbackTextSizes: Record<ProfileImageSize, string> = {
13
- sm: "text-caption",
14
- md: "text-footnote",
15
- lg: "text-subheadline",
16
- }
19
+ sm: "text-caption",
20
+ md: "text-footnote",
21
+ lg: "text-subheadline",
22
+ };
17
23
 
18
24
  const ImageContext = createContext<{
19
- loaded: boolean
20
- setLoaded: (loaded: boolean) => void
21
- size: ProfileImageSize
22
- }>({ loaded: false, setLoaded: () => {}, size: "md" })
25
+ loaded: boolean;
26
+ setLoaded: (loaded: boolean) => void;
27
+ size: ProfileImageSize;
28
+ }>({ loaded: false, setLoaded: () => {}, size: "md" });
23
29
 
24
30
  export interface ProfileImageProps extends ComponentProps<"span"> {
25
- /** Avatar diameter; fallback initials scale with it. @default "md" */
26
- size?: ProfileImageSize
31
+ /** Avatar diameter; fallback initials scale with it. @default "md" */
32
+ size?: ProfileImageSize;
27
33
  }
28
34
 
29
- export function ProfileImage({ size = "md", className, ...props }: ProfileImageProps) {
30
- const [loaded, setLoaded] = useState(false)
31
- return (
32
- <ImageContext value={{ loaded, setLoaded, size }}>
33
- <span
34
- {...props}
35
- className={cn(
36
- "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-fill text-label-secondary",
37
- diameters[size],
38
- className
39
- )}
40
- />
41
- </ImageContext>
42
- )
35
+ export function ProfileImage({
36
+ size = "md",
37
+ className,
38
+ ...props
39
+ }: ProfileImageProps) {
40
+ const [loaded, setLoaded] = useState(false);
41
+ return (
42
+ <ImageContext value={{ loaded, setLoaded, size }}>
43
+ <span
44
+ {...props}
45
+ className={cn(
46
+ "bg-fill text-label-secondary relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full",
47
+ diameters[size],
48
+ className,
49
+ )}
50
+ />
51
+ </ImageContext>
52
+ );
43
53
  }
44
54
  export function ProfileImageContent({
45
- src,
46
- className,
47
- onLoad,
48
- onError,
49
- ...props
55
+ src,
56
+ className,
57
+ onLoad,
58
+ onError,
59
+ ...props
50
60
  }: ComponentProps<"img">) {
51
- const { setLoaded } = useContext(ImageContext)
52
- useEffect(() => {
53
- if (src) setLoaded(false)
54
- }, [src, setLoaded])
55
- if (!src) return null
56
- return (
57
- <img
58
- key={src}
59
- alt=""
60
- {...props}
61
- src={src}
62
- className={cn("absolute inset-0 size-full object-cover", className)}
63
- onLoad={(event) => {
64
- setLoaded(true)
65
- onLoad?.(event)
66
- }}
67
- onError={(event) => {
68
- setLoaded(false)
69
- onError?.(event)
70
- event.currentTarget.style.visibility = "hidden"
71
- }}
72
- />
73
- )
61
+ const { setLoaded } = useContext(ImageContext);
62
+ useEffect(() => {
63
+ if (src) setLoaded(false);
64
+ }, [src, setLoaded]);
65
+ if (!src) return null;
66
+ return (
67
+ <img
68
+ key={src}
69
+ alt=""
70
+ {...props}
71
+ src={src}
72
+ className={cn("absolute inset-0 size-full object-cover", className)}
73
+ onLoad={(event) => {
74
+ setLoaded(true);
75
+ onLoad?.(event);
76
+ }}
77
+ onError={(event) => {
78
+ setLoaded(false);
79
+ onError?.(event);
80
+ event.currentTarget.style.visibility = "hidden";
81
+ }}
82
+ />
83
+ );
74
84
  }
75
- export function ProfileImageFallback({ className, ...props }: ComponentProps<"span">) {
76
- const { loaded, size } = useContext(ImageContext)
77
- return loaded ? null : (
78
- <span {...props} className={cn(fallbackTextSizes[size], "font-medium", className)} />
79
- )
85
+ export function ProfileImageFallback({
86
+ className,
87
+ ...props
88
+ }: ComponentProps<"span">) {
89
+ const { loaded, size } = useContext(ImageContext);
90
+ return loaded ? null : (
91
+ <span
92
+ {...props}
93
+ className={cn(fallbackTextSizes[size], "font-medium", className)}
94
+ />
95
+ );
80
96
  }
@@ -1,70 +1,72 @@
1
- import { type HTMLAttributes, useRef } from "react"
2
- import { useLiquidProgress } from "./use-liquid-progress"
3
- import { cn } from "../../lib/class-names"
4
- import { clampRange } from "../../lib/range-value"
1
+ import { type HTMLAttributes, useRef } from "react";
2
+ import { useLiquidProgress } from "./use-liquid-progress";
3
+ import { cn } from "../../lib/class-names";
4
+ import { clampRange } from "../../lib/range-value";
5
5
 
6
- export type ProgressViewSize = "sm" | "md" | "lg"
6
+ export type ProgressViewSize = "sm" | "md" | "lg";
7
7
 
8
- export interface ProgressViewProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
9
- label: string
10
- /** Omit for an indeterminate operation. */
11
- value?: number
12
- max?: number
13
- size?: ProgressViewSize
14
- /** Secondary text, such as bytes uploaded. */
15
- detail?: string
8
+ export interface ProgressViewProps
9
+ extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
10
+ label: string;
11
+ /** Omit for an indeterminate operation. */
12
+ value?: number;
13
+ max?: number;
14
+ size?: ProgressViewSize;
15
+ /** Secondary text, such as bytes uploaded. */
16
+ detail?: string;
16
17
  }
17
18
 
18
19
  const trackHeight: Record<ProgressViewSize, string> = {
19
- sm: "h-1",
20
- md: "h-2",
21
- lg: "h-3",
22
- }
20
+ sm: "h-1",
21
+ md: "h-2",
22
+ lg: "h-3",
23
+ };
23
24
 
24
25
  /** A determinate or indeterminate progress track respecting reduced motion. */
25
26
  export function ProgressView({
26
- label,
27
- value,
28
- max = 100,
29
- size = "md",
30
- detail,
31
- className,
32
- ...props
27
+ label,
28
+ value,
29
+ max = 100,
30
+ size = "md",
31
+ detail,
32
+ className,
33
+ ...props
33
34
  }: ProgressViewProps) {
34
- const limit = Number.isFinite(max) && max > 0 ? max : 100
35
- const current = value === undefined ? undefined : clampRange(value, 0, limit)
36
- const progress = current === undefined ? undefined : (current / limit) * 100
37
- const fill = useRef<HTMLDivElement>(null)
38
- useLiquidProgress(fill, progress)
35
+ const limit = Number.isFinite(max) && max > 0 ? max : 100;
36
+ const current = value === undefined ? undefined : clampRange(value, 0, limit);
37
+ const progress = current === undefined ? undefined : (current / limit) * 100;
38
+ const fill = useRef<HTMLDivElement>(null);
39
+ useLiquidProgress(fill, progress);
39
40
 
40
- return (
41
- <div {...props} className={cn("flex min-w-0 flex-col gap-2", className)}>
42
- <div className="flex items-center justify-between gap-3 text-footnote">
43
- <span className="font-semibold text-label">{label}</span>
44
- {detail && <span className="text-label-secondary">{detail}</span>}
45
- </div>
46
- <div
47
- role="progressbar"
48
- aria-label={label}
49
- aria-valuemin={current === undefined ? undefined : 0}
50
- aria-valuemax={current === undefined ? undefined : limit}
51
- aria-valuenow={current}
52
- className={cn(
53
- "relative w-full overflow-hidden rounded-full bg-separator/70",
54
- trackHeight[size]
55
- )}
56
- >
57
- <div
58
- ref={fill}
59
- aria-hidden="true"
60
- className={cn(
61
- "h-full rounded-full bg-accent",
62
- current !== undefined && "passui-progress-fill",
63
- current === undefined && "w-1/3 animate-pulse motion-reduce:animate-none"
64
- )}
65
- style={progress === undefined ? undefined : { width: `${progress}%` }}
66
- />
67
- </div>
68
- </div>
69
- )
41
+ return (
42
+ <div {...props} className={cn("flex min-w-0 flex-col gap-2", className)}>
43
+ <div className="text-footnote flex items-center justify-between gap-3">
44
+ <span className="text-label font-semibold">{label}</span>
45
+ {detail && <span className="text-label-secondary">{detail}</span>}
46
+ </div>
47
+ <div
48
+ role="progressbar"
49
+ aria-label={label}
50
+ aria-valuemin={current === undefined ? undefined : 0}
51
+ aria-valuemax={current === undefined ? undefined : limit}
52
+ aria-valuenow={current}
53
+ className={cn(
54
+ "bg-separator/70 relative w-full overflow-hidden rounded-full",
55
+ trackHeight[size],
56
+ )}
57
+ >
58
+ <div
59
+ ref={fill}
60
+ aria-hidden="true"
61
+ className={cn(
62
+ "bg-accent h-full rounded-full",
63
+ current !== undefined && "passui-progress-fill",
64
+ current === undefined &&
65
+ "w-1/3 animate-pulse motion-reduce:animate-none",
66
+ )}
67
+ style={progress === undefined ? undefined : { width: `${progress}%` }}
68
+ />
69
+ </div>
70
+ </div>
71
+ );
70
72
  }
@@ -1,43 +1,43 @@
1
- import { type RefObject, useLayoutEffect, useRef } from "react"
2
- import { useMediaQuery } from "../../hooks/use-media-query"
3
- import { clampRange } from "../../lib/range-value"
4
- import { stepLiquid } from "../slosh-slider/liquid-step"
1
+ import { type RefObject, useLayoutEffect, useRef } from "react";
2
+ import { useMediaQuery } from "../../hooks/use-media-query";
3
+ import { clampRange } from "../../lib/range-value";
4
+ import { stepLiquid } from "../slosh-slider/liquid-step";
5
5
 
6
6
  /** Read-only progress shares the liquid fill without acquiring slider semantics. */
7
7
  export function useLiquidProgress(
8
- ref: RefObject<HTMLDivElement | null>,
9
- target: number | undefined
8
+ ref: RefObject<HTMLDivElement | null>,
9
+ target: number | undefined,
10
10
  ) {
11
- const reduced = useMediaQuery("(prefers-reduced-motion: reduce)")
12
- const state = useRef({ fill: target ?? 0, velocity: 0 })
13
- useLayoutEffect(() => {
14
- const element = ref.current
15
- if (!element || target === undefined) return
16
- let frame = 0
17
- let previous = 0
18
- const paint = () => {
19
- element.style.width = `${state.current.fill}%`
20
- element.style.setProperty(
21
- "--progress-lean",
22
- `${reduced ? 0 : clampRange(state.current.velocity * 0.4, -4, 4)}px`
23
- )
24
- }
25
- if (reduced) {
26
- state.current = { fill: target, velocity: 0 }
27
- paint()
28
- return
29
- }
30
- const tick = (now: number) => {
31
- const dt = previous ? Math.min(2.5, (now - previous) / 16.67) : 1
32
- previous = now
33
- state.current = stepLiquid(state.current, target, 15, dt)
34
- paint()
35
- if (state.current.fill !== target || state.current.velocity)
36
- frame = requestAnimationFrame(tick)
37
- }
38
- paint()
39
- if (state.current.fill !== target || state.current.velocity)
40
- frame = requestAnimationFrame(tick)
41
- return () => cancelAnimationFrame(frame)
42
- }, [ref, target, reduced])
11
+ const reduced = useMediaQuery("(prefers-reduced-motion: reduce)");
12
+ const state = useRef({ fill: target ?? 0, velocity: 0 });
13
+ useLayoutEffect(() => {
14
+ const element = ref.current;
15
+ if (!element || target === undefined) return;
16
+ let frame = 0;
17
+ let previous = 0;
18
+ const paint = () => {
19
+ element.style.width = `${state.current.fill}%`;
20
+ element.style.setProperty(
21
+ "--progress-lean",
22
+ `${reduced ? 0 : clampRange(state.current.velocity * 0.4, -4, 4)}px`,
23
+ );
24
+ };
25
+ if (reduced) {
26
+ state.current = { fill: target, velocity: 0 };
27
+ paint();
28
+ return;
29
+ }
30
+ const tick = (now: number) => {
31
+ const dt = previous ? Math.min(2.5, (now - previous) / 16.67) : 1;
32
+ previous = now;
33
+ state.current = stepLiquid(state.current, target, 15, dt);
34
+ paint();
35
+ if (state.current.fill !== target || state.current.velocity)
36
+ frame = requestAnimationFrame(tick);
37
+ };
38
+ paint();
39
+ if (state.current.fill !== target || state.current.velocity)
40
+ frame = requestAnimationFrame(tick);
41
+ return () => cancelAnimationFrame(frame);
42
+ }, [ref, target, reduced]);
43
43
  }
@@ -1,83 +1,86 @@
1
- import { useId } from "react"
2
- import { cn } from "../../lib/class-names"
1
+ import { useId } from "react";
2
+ import { cn } from "../../lib/class-names";
3
3
 
4
- export type RadioGroupSize = "sm" | "md" | "lg"
4
+ export type RadioGroupSize = "sm" | "md" | "lg";
5
5
 
6
6
  const legendSizes: Record<RadioGroupSize, string> = {
7
- sm: "mb-1.5 text-caption2 font-medium text-label-secondary",
8
- md: "mb-2 text-footnote font-medium text-label-secondary",
9
- lg: "mb-2.5 text-subheadline font-medium text-label-secondary",
10
- }
7
+ sm: "mb-1.5 text-caption2 font-medium text-label-secondary",
8
+ md: "mb-2 text-footnote font-medium text-label-secondary",
9
+ lg: "mb-2.5 text-subheadline font-medium text-label-secondary",
10
+ };
11
11
  const rowSizes: Record<RadioGroupSize, string> = {
12
- sm: "flex min-h-7 items-start gap-1.5 text-footnote text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-10",
13
- md: "flex min-h-8 items-start gap-2 text-subheadline text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-11",
14
- lg: "flex min-h-10 items-start gap-2.5 text-body text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-12",
15
- }
12
+ sm: "flex min-h-7 items-start gap-1.5 text-footnote text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-10",
13
+ md: "flex min-h-8 items-start gap-2 text-subheadline text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-11",
14
+ lg: "flex min-h-10 items-start gap-2.5 text-body text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-12",
15
+ };
16
16
  const radioSizes: Record<RadioGroupSize, string> = {
17
- sm: "mt-0.5 size-3.5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
18
- md: "mt-1 size-4 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
19
- lg: "mt-1 size-5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
20
- }
17
+ sm: "mt-0.5 size-3.5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
18
+ md: "mt-1 size-4 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
19
+ lg: "mt-1 size-5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
20
+ };
21
21
  const descriptionSizes: Record<RadioGroupSize, string> = {
22
- sm: "block text-caption2 text-label-secondary",
23
- md: "block text-footnote text-label-secondary",
24
- lg: "block text-subheadline text-label-secondary",
25
- }
22
+ sm: "block text-caption2 text-label-secondary",
23
+ md: "block text-footnote text-label-secondary",
24
+ lg: "block text-subheadline text-label-secondary",
25
+ };
26
26
 
27
27
  export interface RadioGroupOption<T extends string> {
28
- value: T
29
- label: string
30
- description?: string
31
- disabled?: boolean
28
+ value: T;
29
+ label: string;
30
+ description?: string;
31
+ disabled?: boolean;
32
32
  }
33
33
  export interface RadioGroupProps<T extends string> {
34
- value: T
35
- onChange: (value: NoInfer<T>) => void
36
- label: string
37
- options: readonly RadioGroupOption<T>[]
38
- disabled?: boolean
39
- name?: string
40
- className?: string
41
- /** Legend, row, radio glyph and description density. @default "md" */
42
- size?: RadioGroupSize
34
+ value: T;
35
+ onChange: (value: NoInfer<T>) => void;
36
+ label: string;
37
+ options: readonly RadioGroupOption<T>[];
38
+ disabled?: boolean;
39
+ name?: string;
40
+ className?: string;
41
+ /** Legend, row, radio glyph and description density. @default "md" */
42
+ size?: RadioGroupSize;
43
43
  }
44
44
  /** A labelled choice group using the browser's native radio keyboard behavior. */
45
45
  export function RadioGroup<T extends string>({
46
- value,
47
- onChange,
48
- label,
49
- options,
50
- disabled,
51
- name,
52
- className,
53
- size = "md",
46
+ value,
47
+ onChange,
48
+ label,
49
+ options,
50
+ disabled,
51
+ name,
52
+ className,
53
+ size = "md",
54
54
  }: RadioGroupProps<T>) {
55
- const id = useId()
56
- return (
57
- <fieldset disabled={disabled} className={cn("flex min-w-0 flex-col gap-2", className)}>
58
- <legend className={legendSizes[size]}>{label}</legend>
59
- {options.map((option, index) => (
60
- <label key={option.value} className={rowSizes[size]}>
61
- <input
62
- type="radio"
63
- name={name ?? id}
64
- value={option.value}
65
- checked={value === option.value}
66
- disabled={option.disabled}
67
- onChange={() => onChange(option.value)}
68
- aria-describedby={option.description ? `${id}-${index}` : undefined}
69
- className={radioSizes[size]}
70
- />
71
- <span>
72
- {option.label}
73
- {option.description && (
74
- <span id={`${id}-${index}`} className={descriptionSizes[size]}>
75
- {option.description}
76
- </span>
77
- )}
78
- </span>
79
- </label>
80
- ))}
81
- </fieldset>
82
- )
55
+ const id = useId();
56
+ return (
57
+ <fieldset
58
+ disabled={disabled}
59
+ className={cn("flex min-w-0 flex-col gap-2", className)}
60
+ >
61
+ <legend className={legendSizes[size]}>{label}</legend>
62
+ {options.map((option, index) => (
63
+ <label key={option.value} className={rowSizes[size]}>
64
+ <input
65
+ type="radio"
66
+ name={name ?? id}
67
+ value={option.value}
68
+ checked={value === option.value}
69
+ disabled={option.disabled}
70
+ onChange={() => onChange(option.value)}
71
+ aria-describedby={option.description ? `${id}-${index}` : undefined}
72
+ className={radioSizes[size]}
73
+ />
74
+ <span>
75
+ {option.label}
76
+ {option.description && (
77
+ <span id={`${id}-${index}`} className={descriptionSizes[size]}>
78
+ {option.description}
79
+ </span>
80
+ )}
81
+ </span>
82
+ </label>
83
+ ))}
84
+ </fieldset>
85
+ );
83
86
  }