@passui/react 0.1.45 → 0.1.47

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 (141) 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 +38 -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 -402
  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 +278 -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 +374 -0
  72. package/src/components/popover/popover-root.tsx +97 -0
  73. package/src/components/popover/popover-trigger.tsx +71 -0
  74. package/src/components/profile-image/profile-image.tsx +80 -64
  75. package/src/components/progress-view/progress-view.tsx +61 -59
  76. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  77. package/src/components/radio-group/radio-group.tsx +71 -68
  78. package/src/components/search-field/expanding-search-field.tsx +191 -177
  79. package/src/components/search-field/search-field.tsx +59 -50
  80. package/src/components/search-list/search-list.tsx +246 -236
  81. package/src/components/section/section.tsx +58 -58
  82. package/src/components/secure-field/secure-field.tsx +3 -3
  83. package/src/components/segmented-control/segmented-control.tsx +136 -136
  84. package/src/components/sheet/modal-environment.ts +71 -64
  85. package/src/components/sheet/sheet-portal.tsx +58 -53
  86. package/src/components/sheet/sheet.tsx +146 -141
  87. package/src/components/sheet/use-sheet-focus.ts +97 -82
  88. package/src/components/sidebar/sidebar.tsx +78 -78
  89. package/src/components/skeleton/skeleton.tsx +30 -21
  90. package/src/components/slider/slider.tsx +113 -104
  91. package/src/components/slosh-slider/liquid-step.ts +32 -31
  92. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  93. package/src/components/stat-tile/index.ts +1 -1
  94. package/src/components/stat-tile/stat-tile.tsx +79 -68
  95. package/src/components/status-indicator/status-indicator.tsx +40 -37
  96. package/src/components/stepper/rolling-digit.tsx +82 -68
  97. package/src/components/stepper/stepper.tsx +122 -113
  98. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  99. package/src/components/tab-bar/tab-bar.tsx +149 -148
  100. package/src/components/tab-view/tab-view.tsx +140 -137
  101. package/src/components/table/table.tsx +117 -98
  102. package/src/components/text-editor/text-editor.tsx +34 -29
  103. package/src/components/text-field/text-field.tsx +182 -180
  104. package/src/components/toast/toast-store.ts +127 -115
  105. package/src/components/toast/toaster.tsx +361 -347
  106. package/src/components/toggle/toggle.tsx +297 -282
  107. package/src/components/tooltip/tooltip-host.tsx +157 -150
  108. package/src/components/view-controller/index.ts +1 -1
  109. package/src/components/view-controller/view-controller.tsx +64 -53
  110. package/src/components/wake-slider/wake-slider.tsx +220 -205
  111. package/src/hooks/scroll/minimize-decision.ts +11 -7
  112. package/src/hooks/use-anchored-disclosure.ts +19 -19
  113. package/src/hooks/use-anchored-placement.ts +29 -25
  114. package/src/hooks/use-dismiss.ts +33 -29
  115. package/src/hooks/use-floating-dismiss.ts +24 -22
  116. package/src/hooks/use-media-query.ts +67 -60
  117. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  118. package/src/hooks/use-visual-viewport.ts +63 -63
  119. package/src/hooks/viewport/types.ts +16 -16
  120. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  121. package/src/icons/icons.tsx +440 -328
  122. package/src/index.ts +91 -91
  123. package/src/lib/anchored-placement.ts +70 -65
  124. package/src/lib/class-names.ts +14 -13
  125. package/src/lib/clipboard.ts +8 -7
  126. package/src/lib/date.ts +61 -60
  127. package/src/lib/floating-layers.ts +39 -24
  128. package/src/lib/haptics.ts +10 -10
  129. package/src/lib/language.ts +9 -9
  130. package/src/lib/range-value.ts +27 -21
  131. package/src/lib/screen-edges.ts +33 -30
  132. package/src/lib/shapes.ts +38 -34
  133. package/src/machines/popover.machine.ts +51 -0
  134. package/src/motion/transitions.ts +46 -43
  135. package/src/styles/code-block.css +69 -43
  136. package/src/styles/motion-controls.css +393 -191
  137. package/src/tokens.ts +1 -1
  138. package/dist/types/src/components/popover/popover-fit.d.ts +0 -21
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover-fit.ts +0 -69
  141. package/src/components/popover/popover.tsx +0 -334
@@ -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
  }