@passui/react 0.1.45 → 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.
- package/README.md +57 -46
- package/dist/types/src/components/accordion/accordion.d.ts +1 -1
- package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
- package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
- package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
- package/dist/types/src/components/popover/index.d.ts +41 -0
- package/dist/types/src/components/popover/popover-content.d.ts +32 -0
- package/dist/types/src/components/popover/popover-root.d.ts +22 -0
- package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
- package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
- package/dist/types/src/components/table/table.d.ts +1 -1
- package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/machines/popover.machine.d.ts +30 -0
- package/package.json +307 -307
- package/src/appearance/index.ts +2 -2
- package/src/appearance/platform.tsx +76 -60
- package/src/appearance/theme.tsx +409 -377
- package/src/components/accordion/accordion.tsx +58 -49
- package/src/components/ambient/ambient.tsx +25 -25
- package/src/components/ambient/index.ts +1 -1
- package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
- package/src/components/breadcrumb/breadcrumb.tsx +63 -57
- package/src/components/button/button-styles.ts +39 -32
- package/src/components/button/button.tsx +91 -84
- package/src/components/button/icon-button.tsx +27 -25
- package/src/components/button/pending-spinner.tsx +8 -8
- package/src/components/button/use-pending-indicator.ts +11 -11
- package/src/components/chip/chip.tsx +35 -30
- package/src/components/code-block/code-block.tsx +40 -39
- package/src/components/combo-box/combo-box.tsx +5 -5
- package/src/components/command/command.tsx +124 -94
- package/src/components/compact-date-picker/calendar-month.ts +75 -53
- package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
- package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
- package/src/components/compact-time-picker/time-value.ts +32 -29
- package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
- package/src/components/content-view/content-view.tsx +168 -159
- package/src/components/content-view/index.ts +1 -1
- package/src/components/context-menu/conceal-menu-source.ts +25 -25
- package/src/components/context-menu/context-menu-host.tsx +439 -402
- package/src/components/context-menu/context-menu-store.ts +125 -123
- package/src/components/context-menu/menu-navigation.ts +25 -18
- package/src/components/context-menu/menu-position.ts +33 -29
- package/src/components/context-menu/menu-reveal.ts +32 -32
- package/src/components/context-menu/use-context-menu.ts +63 -56
- package/src/components/conversation/conversation.tsx +265 -263
- package/src/components/conversation/index.ts +1 -1
- package/src/components/date-picker/date-picker.tsx +95 -94
- package/src/components/date-picker/date-value.ts +20 -10
- package/src/components/dialog/dialog-host.tsx +105 -103
- package/src/components/dialog/dialog-store.ts +37 -36
- package/src/components/dialog/dialog.tsx +192 -179
- package/src/components/dialog/index.ts +1 -1
- package/src/components/disclosure-group/disclosure-group.tsx +93 -86
- package/src/components/divider/divider.tsx +24 -22
- package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
- package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
- package/src/components/labeled-content/labeled-content.tsx +68 -68
- package/src/components/locale-switcher/index.ts +1 -1
- package/src/components/locale-switcher/locale-switcher.tsx +279 -251
- package/src/components/menu/menu.tsx +75 -58
- package/src/components/morph/index.ts +2 -2
- package/src/components/morph/morph.tsx +227 -201
- package/src/components/morph/use-morph-disclosure.ts +19 -19
- package/src/components/one-time-code/one-time-code.tsx +216 -200
- package/src/components/picker/picker-trigger.ts +16 -13
- package/src/components/picker/picker.tsx +371 -345
- package/src/components/popover/anchored-surface.tsx +253 -229
- package/src/components/popover/index.ts +50 -0
- package/src/components/popover/popover-content.tsx +334 -0
- package/src/components/popover/popover-fit.ts +58 -53
- package/src/components/popover/popover-root.tsx +97 -0
- package/src/components/popover/popover-trigger.tsx +71 -0
- package/src/components/profile-image/profile-image.tsx +80 -64
- package/src/components/progress-view/progress-view.tsx +61 -59
- package/src/components/progress-view/use-liquid-progress.ts +38 -38
- package/src/components/radio-group/radio-group.tsx +71 -68
- package/src/components/search-field/expanding-search-field.tsx +191 -177
- package/src/components/search-field/search-field.tsx +59 -50
- package/src/components/search-list/search-list.tsx +246 -236
- package/src/components/section/section.tsx +58 -58
- package/src/components/secure-field/secure-field.tsx +3 -3
- package/src/components/segmented-control/segmented-control.tsx +136 -136
- package/src/components/sheet/modal-environment.ts +71 -64
- package/src/components/sheet/sheet-portal.tsx +58 -53
- package/src/components/sheet/sheet.tsx +146 -141
- package/src/components/sheet/use-sheet-focus.ts +97 -82
- package/src/components/sidebar/sidebar.tsx +78 -78
- package/src/components/skeleton/skeleton.tsx +30 -21
- package/src/components/slider/slider.tsx +113 -104
- package/src/components/slosh-slider/liquid-step.ts +32 -31
- package/src/components/slosh-slider/slosh-slider.tsx +265 -228
- package/src/components/stat-tile/index.ts +1 -1
- package/src/components/stat-tile/stat-tile.tsx +79 -68
- package/src/components/status-indicator/status-indicator.tsx +40 -37
- package/src/components/stepper/rolling-digit.tsx +82 -68
- package/src/components/stepper/stepper.tsx +122 -113
- package/src/components/swatch-picker/swatch-picker.tsx +120 -120
- package/src/components/tab-bar/tab-bar.tsx +149 -148
- package/src/components/tab-view/tab-view.tsx +140 -137
- package/src/components/table/table.tsx +117 -98
- package/src/components/text-editor/text-editor.tsx +34 -29
- package/src/components/text-field/text-field.tsx +182 -180
- package/src/components/toast/toast-store.ts +127 -115
- package/src/components/toast/toaster.tsx +361 -347
- package/src/components/toggle/toggle.tsx +297 -282
- package/src/components/tooltip/tooltip-host.tsx +157 -150
- package/src/components/view-controller/index.ts +1 -1
- package/src/components/view-controller/view-controller.tsx +64 -53
- package/src/components/wake-slider/wake-slider.tsx +220 -205
- package/src/hooks/scroll/minimize-decision.ts +11 -7
- package/src/hooks/use-anchored-disclosure.ts +19 -19
- package/src/hooks/use-anchored-placement.ts +29 -25
- package/src/hooks/use-dismiss.ts +33 -29
- package/src/hooks/use-floating-dismiss.ts +24 -22
- package/src/hooks/use-media-query.ts +67 -60
- package/src/hooks/use-minimize-on-scroll.ts +69 -60
- package/src/hooks/use-visual-viewport.ts +63 -63
- package/src/hooks/viewport/types.ts +16 -16
- package/src/hooks/viewport/viewport-metrics.ts +16 -10
- package/src/icons/icons.tsx +440 -328
- package/src/index.ts +91 -91
- package/src/lib/anchored-placement.ts +70 -65
- package/src/lib/class-names.ts +14 -13
- package/src/lib/clipboard.ts +8 -7
- package/src/lib/date.ts +61 -60
- package/src/lib/floating-layers.ts +39 -24
- package/src/lib/haptics.ts +10 -10
- package/src/lib/language.ts +9 -9
- package/src/lib/range-value.ts +27 -21
- package/src/lib/screen-edges.ts +33 -30
- package/src/lib/shapes.ts +38 -34
- package/src/machines/popover.machine.ts +51 -0
- package/src/motion/transitions.ts +46 -43
- package/src/styles/code-block.css +69 -43
- package/src/styles/motion-controls.css +393 -191
- package/src/tokens.ts +1 -1
- package/dist/types/src/components/popover/popover.d.ts +0 -21
- package/src/components/popover/popover.tsx +0 -334
|
@@ -1,80 +1,96 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
13
|
+
sm: "size-7",
|
|
14
|
+
md: "size-9",
|
|
15
|
+
lg: "size-12",
|
|
16
|
+
};
|
|
11
17
|
|
|
12
18
|
const fallbackTextSizes: Record<ProfileImageSize, string> = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
19
|
+
sm: "text-caption",
|
|
20
|
+
md: "text-footnote",
|
|
21
|
+
lg: "text-subheadline",
|
|
22
|
+
};
|
|
17
23
|
|
|
18
24
|
const ImageContext = createContext<{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
31
|
+
/** Avatar diameter; fallback initials scale with it. @default "md" */
|
|
32
|
+
size?: ProfileImageSize;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
export function ProfileImage({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
src,
|
|
56
|
+
className,
|
|
57
|
+
onLoad,
|
|
58
|
+
onError,
|
|
59
|
+
...props
|
|
50
60
|
}: ComponentProps<"img">) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
label,
|
|
28
|
+
value,
|
|
29
|
+
max = 100,
|
|
30
|
+
size = "md",
|
|
31
|
+
detail,
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
33
34
|
}: ProgressViewProps) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
9
|
-
|
|
8
|
+
ref: RefObject<HTMLDivElement | null>,
|
|
9
|
+
target: number | undefined,
|
|
10
10
|
) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
value: T;
|
|
29
|
+
label: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
32
|
}
|
|
33
33
|
export interface RadioGroupProps<T extends string> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
value,
|
|
47
|
+
onChange,
|
|
48
|
+
label,
|
|
49
|
+
options,
|
|
50
|
+
disabled,
|
|
51
|
+
name,
|
|
52
|
+
className,
|
|
53
|
+
size = "md",
|
|
54
54
|
}: RadioGroupProps<T>) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
}
|