@nexub/react 7.3.0 → 7.5.0

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 (38) hide show
  1. package/accessibility.json +1 -1
  2. package/dist/components/Avatar/Avatar.d.ts +3 -0
  3. package/dist/components/Avatar/Avatar.js +28 -5
  4. package/dist/components/Avatar/index.d.ts +1 -1
  5. package/dist/components/Button/Button.d.ts +2 -1
  6. package/dist/components/Button/Button.js +37 -7
  7. package/dist/components/Combobox/Combobox.d.ts +6 -9
  8. package/dist/components/Combobox/Combobox.js +30 -9
  9. package/dist/components/DataTable/DataTable.d.ts +5 -1
  10. package/dist/components/DataTable/DataTable.js +6 -2
  11. package/dist/components/DatePicker/DatePicker.d.ts +16 -1
  12. package/dist/components/DatePicker/DatePicker.js +100 -26
  13. package/dist/components/FileUpload/FileUpload.js +86 -32
  14. package/dist/components/Input/Input.d.ts +6 -0
  15. package/dist/components/Input/Input.js +47 -5
  16. package/dist/components/Progress/Progress.d.ts +4 -0
  17. package/dist/components/Progress/Progress.js +73 -20
  18. package/dist/components/Select/Select.d.ts +18 -3
  19. package/dist/components/Select/Select.js +170 -71
  20. package/dist/components/Skeleton/Skeleton.d.ts +9 -0
  21. package/dist/components/Skeleton/Skeleton.js +18 -1
  22. package/dist/components/Skeleton/index.d.ts +2 -2
  23. package/dist/components/Skeleton/index.js +2 -1
  24. package/dist/components/Slider/Slider.d.ts +10 -0
  25. package/dist/components/Slider/Slider.js +48 -3
  26. package/dist/components/Slider/index.d.ts +1 -1
  27. package/dist/components/Switch/Switch.d.ts +6 -0
  28. package/dist/components/Switch/Switch.js +29 -2
  29. package/dist/components/Toast/Toaster.js +68 -23
  30. package/dist/components/Toast/index.d.ts +1 -1
  31. package/dist/components/Toast/useToast.d.ts +18 -1
  32. package/dist/components/Toast/useToast.js +14 -0
  33. package/dist/hooks/useAsyncAction.d.ts +27 -0
  34. package/dist/hooks/useAsyncAction.js +36 -0
  35. package/dist/index.d.ts +7 -4
  36. package/dist/index.js +4 -1
  37. package/dist/styles.css +1 -1
  38. package/package.json +6 -6
@@ -2,37 +2,94 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as RadixSelect from "@radix-ui/react-select";
5
- import { Check, ChevronDown, ChevronUp } from "@nexub/icons";
5
+ import { Check, ChevronDown, ChevronUp, X } from "@nexub/icons";
6
6
  import { cn } from "../../utils/cn.js";
7
7
  import { field, fieldFocusVisible, fieldInvalid } from "../../utils/control.js";
8
8
  import { floatingSurface, menuLabel, menuSeparator } from "../../utils/surface.js";
9
- const Root = RadixSelect.Root;
10
- const Group = RadixSelect.Group;
11
- const Value = RadixSelect.Value;
12
- const Trigger = React.forwardRef(
13
- ({ className, children, invalid, ...props }, ref) => /* @__PURE__ */ jsxs(
14
- RadixSelect.Trigger,
9
+ import { useControllableState } from "../../hooks/useControllableState.js";
10
+ import { useMergedRefs } from "../../hooks/useMergedRefs.js";
11
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
12
+ import { IconButton } from "../IconButton/IconButton.js";
13
+ const SelectContext = React.createContext({
14
+ clearable: false,
15
+ hasValue: false,
16
+ clear: () => {
17
+ },
18
+ query: "",
19
+ setQuery: () => {
20
+ }
21
+ });
22
+ const Root = ({ clearable = false, value, defaultValue, onValueChange, onOpenChange, ...props }) => {
23
+ const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
24
+ const [query, setQuery] = React.useState("");
25
+ const ctx = React.useMemo(
26
+ () => ({ clearable, hasValue: Boolean(current), clear: () => setCurrent(""), query, setQuery }),
27
+ [clearable, current, setCurrent, query]
28
+ );
29
+ return /* @__PURE__ */ jsx(SelectContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
30
+ RadixSelect.Root,
15
31
  {
16
- ref,
17
- className: cn(
18
- "flex h-control-md w-full items-center justify-between gap-2 px-3.5 text-body-md",
19
- field,
20
- fieldFocusVisible,
21
- // Open reads like focus: the 2px primary border.
22
- "data-[state=open]:border-[var(--color-primary)] data-[state=open]:shadow-[inset_0_0_0_1px_var(--color-primary)]",
23
- "data-[placeholder]:text-[var(--color-on-surface-muted)]",
24
- "disabled:cursor-not-allowed disabled:border-[var(--color-outline-variant)] disabled:bg-[var(--color-surface-container-low)] disabled:text-[var(--color-text-disabled)]",
25
- invalid && fieldInvalid,
26
- "[&>span]:line-clamp-1",
27
- className
28
- ),
29
32
  ...props,
30
- children: [
31
- children,
32
- /* @__PURE__ */ jsx(RadixSelect.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDown, { size: "sm", className: "shrink-0 text-[var(--color-on-surface-variant)]", "aria-hidden": "true" }) })
33
- ]
33
+ ...clearable ? { value: current ?? "", onValueChange: setCurrent } : { value, defaultValue, onValueChange },
34
+ onOpenChange: (open) => {
35
+ if (!open) setQuery("");
36
+ onOpenChange?.(open);
37
+ }
34
38
  }
35
- )
39
+ ) });
40
+ };
41
+ Root.displayName = "Select.Root";
42
+ const Group = RadixSelect.Group;
43
+ const Value = RadixSelect.Value;
44
+ const Trigger = React.forwardRef(
45
+ ({ className, children, invalid, ...props }, ref) => {
46
+ const ctx = React.useContext(SelectContext);
47
+ const messages = useNexubMessages();
48
+ const innerRef = React.useRef(null);
49
+ const mergedRef = useMergedRefs(ref, innerRef);
50
+ const showClear = ctx.clearable && ctx.hasValue && !props.disabled;
51
+ const trigger = /* @__PURE__ */ jsxs(
52
+ RadixSelect.Trigger,
53
+ {
54
+ ref: mergedRef,
55
+ className: cn(
56
+ "flex h-control-md w-full items-center justify-between gap-2 px-3.5 text-body-md",
57
+ field,
58
+ fieldFocusVisible,
59
+ // Open reads like focus: the 2px primary border.
60
+ "data-[state=open]:border-[var(--color-primary)] data-[state=open]:shadow-[inset_0_0_0_1px_var(--color-primary)]",
61
+ "data-[placeholder]:text-[var(--color-on-surface-muted)]",
62
+ "disabled:cursor-not-allowed disabled:border-[var(--color-outline-variant)] disabled:bg-[var(--color-surface-container-low)] disabled:text-[var(--color-text-disabled)]",
63
+ invalid && fieldInvalid,
64
+ "[&>span]:line-clamp-1",
65
+ className
66
+ ),
67
+ ...props,
68
+ children: [
69
+ children,
70
+ showClear && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ms-auto w-control-xs shrink-0" }),
71
+ /* @__PURE__ */ jsx(RadixSelect.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDown, { size: "sm", className: "shrink-0 text-[var(--color-on-surface-variant)]", "aria-hidden": "true" }) })
72
+ ]
73
+ }
74
+ );
75
+ if (!ctx.clearable) return trigger;
76
+ return /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
77
+ trigger,
78
+ showClear && /* @__PURE__ */ jsx(
79
+ IconButton,
80
+ {
81
+ size: "xs",
82
+ label: messages.clearSelection,
83
+ className: "absolute end-9.5 top-1/2 -translate-y-1/2",
84
+ onClick: () => {
85
+ ctx.clear();
86
+ innerRef.current?.focus();
87
+ },
88
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4", "aria-hidden": "true" })
89
+ }
90
+ )
91
+ ] });
92
+ }
36
93
  );
37
94
  Trigger.displayName = "Select.Trigger";
38
95
  const ScrollUpButton = React.forwardRef(
@@ -60,35 +117,66 @@ const ScrollDownButton = React.forwardRef(
60
117
  );
61
118
  ScrollDownButton.displayName = "Select.ScrollDownButton";
62
119
  const Content = React.forwardRef(
63
- ({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(RadixSelect.Portal, { children: /* @__PURE__ */ jsxs(
64
- RadixSelect.Content,
65
- {
66
- ref,
67
- className: cn(
68
- "relative z-[var(--z-popover)] max-h-96 min-w-32 overflow-hidden",
69
- floatingSurface,
70
- "nx-motion nx-pop origin-[var(--radix-select-content-transform-origin)]",
71
- position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1 w-[var(--radix-select-trigger-width)]",
72
- className
73
- ),
74
- position,
75
- ...props,
76
- children: [
77
- /* @__PURE__ */ jsx(ScrollUpButton, {}),
78
- /* @__PURE__ */ jsx(
79
- RadixSelect.Viewport,
80
- {
81
- className: cn(
82
- "p-1.5",
83
- position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
84
- ),
85
- children
86
- }
120
+ ({ className, children, position = "popper", searchable, onKeyDown, ...props }, ref) => {
121
+ const ctx = React.useContext(SelectContext);
122
+ const messages = useNexubMessages();
123
+ const searchRef = React.useRef(null);
124
+ return /* @__PURE__ */ jsx(RadixSelect.Portal, { children: /* @__PURE__ */ jsxs(
125
+ RadixSelect.Content,
126
+ {
127
+ ref,
128
+ className: cn(
129
+ "relative z-[var(--z-popover)] max-h-96 min-w-32 overflow-hidden",
130
+ floatingSurface,
131
+ "nx-motion nx-pop origin-[var(--radix-select-content-transform-origin)]",
132
+ position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1 w-[var(--radix-select-trigger-width)]",
133
+ className
87
134
  ),
88
- /* @__PURE__ */ jsx(ScrollDownButton, {})
89
- ]
90
- }
91
- ) })
135
+ position,
136
+ onKeyDown: (e) => {
137
+ onKeyDown?.(e);
138
+ if (searchable && !e.defaultPrevented && e.target !== searchRef.current && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
139
+ const input = searchRef.current;
140
+ if (!input) return;
141
+ e.preventDefault();
142
+ input.focus();
143
+ input.setRangeText(e.key, input.value.length, input.value.length, "end");
144
+ input.dispatchEvent(new Event("input", { bubbles: true }));
145
+ }
146
+ },
147
+ ...props,
148
+ children: [
149
+ searchable && /* @__PURE__ */ jsx("div", { className: "p-1.5 pb-0", children: /* @__PURE__ */ jsx(
150
+ "input",
151
+ {
152
+ ref: searchRef,
153
+ type: "text",
154
+ value: ctx.query,
155
+ onChange: (e) => ctx.setQuery(e.target.value),
156
+ onKeyDown: (e) => {
157
+ if (e.key.length === 1 || e.key === "Home" || e.key === "End") e.stopPropagation();
158
+ },
159
+ placeholder: messages.searchPlaceholder,
160
+ "aria-label": messages.search,
161
+ className: cn("h-control-sm w-full px-3 text-body-sm", field, fieldFocusVisible)
162
+ }
163
+ ) }),
164
+ /* @__PURE__ */ jsx(ScrollUpButton, {}),
165
+ /* @__PURE__ */ jsx(
166
+ RadixSelect.Viewport,
167
+ {
168
+ className: cn(
169
+ "p-1.5",
170
+ position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
171
+ ),
172
+ children
173
+ }
174
+ ),
175
+ /* @__PURE__ */ jsx(ScrollDownButton, {})
176
+ ]
177
+ }
178
+ ) });
179
+ }
92
180
  );
93
181
  Content.displayName = "Select.Content";
94
182
  const Label = React.forwardRef(
@@ -103,24 +191,35 @@ const Label = React.forwardRef(
103
191
  );
104
192
  Label.displayName = "Select.Label";
105
193
  const Item = React.forwardRef(
106
- ({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
107
- RadixSelect.Item,
108
- {
109
- ref,
110
- className: cn(
111
- "relative flex w-full cursor-default select-none items-center rounded-[10px] py-2 ps-8 pe-2 text-body-sm text-[var(--color-on-surface)] outline-none",
112
- // Highlight = the on-surface state layer at hover opacity.
113
- "focus:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)]",
114
- "data-[disabled]:pointer-events-none data-[disabled]:text-[var(--color-text-disabled)]",
115
- className
116
- ),
117
- ...props,
118
- children: [
119
- /* @__PURE__ */ jsx("span", { className: "absolute start-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(RadixSelect.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { size: "xs", "aria-hidden": "true" }) }) }),
120
- /* @__PURE__ */ jsx(RadixSelect.ItemText, { children })
121
- ]
122
- }
123
- )
194
+ ({ className, children, description, ...props }, ref) => {
195
+ const { query } = React.useContext(SelectContext);
196
+ const descriptionId = React.useId();
197
+ const text = props.textValue ?? (typeof children === "string" ? children : void 0);
198
+ const filtered = Boolean(query.trim()) && text !== void 0 && !text.toLowerCase().includes(query.trim().toLowerCase());
199
+ return /* @__PURE__ */ jsxs(
200
+ RadixSelect.Item,
201
+ {
202
+ ref,
203
+ "aria-describedby": description ? descriptionId : void 0,
204
+ className: cn(
205
+ "relative flex w-full cursor-default select-none items-center rounded-[10px] py-2 ps-8 pe-2 text-body-sm text-[var(--color-on-surface)] outline-none",
206
+ // Highlight = the on-surface state layer at hover opacity.
207
+ "focus:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)]",
208
+ "data-[disabled]:pointer-events-none data-[disabled]:text-[var(--color-text-disabled)]",
209
+ className,
210
+ filtered && "hidden"
211
+ ),
212
+ ...props,
213
+ children: [
214
+ /* @__PURE__ */ jsx("span", { className: "absolute start-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(RadixSelect.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { size: "xs", "aria-hidden": "true" }) }) }),
215
+ description ? /* @__PURE__ */ jsxs("span", { className: "flex flex-col", children: [
216
+ /* @__PURE__ */ jsx(RadixSelect.ItemText, { children }),
217
+ /* @__PURE__ */ jsx("span", { id: descriptionId, className: "text-label-sm text-[var(--color-on-surface-muted)]", children: description })
218
+ ] }) : /* @__PURE__ */ jsx(RadixSelect.ItemText, { children })
219
+ ]
220
+ }
221
+ );
222
+ }
124
223
  );
125
224
  Item.displayName = "Select.Item";
126
225
  const Separator = React.forwardRef(
@@ -10,5 +10,14 @@ export interface SkeletonProps extends React.HTMLAttributes<HTMLSpanElement>, Va
10
10
  width?: string | number;
11
11
  height?: string | number;
12
12
  animated?: boolean;
13
+ /** Effect when `animated`: `pulse` fades, `shimmer` sweeps a highlight across. Both stop under reduced motion. @default 'pulse' */
14
+ animation?: 'pulse' | 'shimmer';
13
15
  }
14
16
  export declare const Skeleton: React.ForwardRefExoticComponent<SkeletonProps & React.RefAttributes<HTMLSpanElement>>;
17
+ export interface SkeletonTextProps extends React.HTMLAttributes<HTMLSpanElement>, Pick<SkeletonProps, 'animated' | 'animation'> {
18
+ /** Number of lines; the last is shorter. @default 3 */
19
+ lines?: number;
20
+ }
21
+ /** Placeholder paragraph. Each line is one line-height tall (inherited from the
22
+ * surrounding text style), so swapping in the real text doesn't shift layout. */
23
+ export declare const SkeletonText: React.ForwardRefExoticComponent<SkeletonTextProps & React.RefAttributes<HTMLSpanElement>>;
@@ -29,6 +29,7 @@ const Skeleton = React.forwardRef(
29
29
  width,
30
30
  height,
31
31
  animated = true,
32
+ animation = "pulse",
32
33
  style,
33
34
  ...props
34
35
  }, ref) => {
@@ -40,7 +41,11 @@ const Skeleton = React.forwardRef(
40
41
  {
41
42
  ref,
42
43
  "aria-hidden": "true",
43
- className: cn(skeletonVariants({ shape, animated }), className),
44
+ className: cn(
45
+ skeletonVariants({ shape, animated: animated && animation === "pulse" }),
46
+ animated && animation === "shimmer" && "nx-skeleton-shimmer",
47
+ className
48
+ ),
44
49
  style: sizeStyle || style ? { ...sizeStyle, ...style } : void 0,
45
50
  ...props
46
51
  }
@@ -48,7 +53,19 @@ const Skeleton = React.forwardRef(
48
53
  }
49
54
  );
50
55
  Skeleton.displayName = "Skeleton";
56
+ const SkeletonText = React.forwardRef(
57
+ ({ className, lines = 3, animated, animation, ...props }, ref) => /* @__PURE__ */ jsx("span", { ref, "aria-hidden": "true", className: cn("block", className), ...props, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx("span", { className: "flex h-[1lh] items-center", children: /* @__PURE__ */ jsx(
58
+ Skeleton,
59
+ {
60
+ animated,
61
+ animation,
62
+ className: i === lines - 1 && lines > 1 ? "w-3/5" : void 0
63
+ }
64
+ ) }, i)) })
65
+ );
66
+ SkeletonText.displayName = "SkeletonText";
51
67
  export {
52
68
  Skeleton,
69
+ SkeletonText,
53
70
  skeletonVariants
54
71
  };
@@ -1,2 +1,2 @@
1
- export { Skeleton, skeletonVariants } from './Skeleton.js';
2
- export type { SkeletonProps } from './Skeleton.js';
1
+ export { Skeleton, SkeletonText, skeletonVariants } from './Skeleton.js';
2
+ export type { SkeletonProps, SkeletonTextProps } from './Skeleton.js';
@@ -1,5 +1,6 @@
1
- import { Skeleton, skeletonVariants } from "./Skeleton.js";
1
+ import { Skeleton, SkeletonText, skeletonVariants } from "./Skeleton.js";
2
2
  export {
3
3
  Skeleton,
4
+ SkeletonText,
4
5
  skeletonVariants
5
6
  };
@@ -1,7 +1,17 @@
1
1
  import * as React from 'react';
2
2
  import * as RadixSlider from '@radix-ui/react-slider';
3
+ export type SliderMark = number | {
4
+ value: number;
5
+ label?: string;
6
+ };
3
7
  export interface SliderProps extends RadixSlider.SliderProps {
4
8
  /** Accessible name per thumb (e.g. ['Minimum', 'Maximum']). Falls back to `aria-label`. */
5
9
  thumbLabels?: string[];
10
+ /** Ticks along the track, optionally labelled (`[0, { value: 50, label: '50%' }, 100]`). */
11
+ marks?: SliderMark[];
12
+ /** Value bubble above each thumb: `true` always, `'hover'` on hover, focus and drag. */
13
+ showValue?: boolean | 'hover';
14
+ /** Formats the bubble and each thumb's `aria-valuetext`. */
15
+ formatValue?: (value: number) => string;
6
16
  }
7
17
  export declare const Slider: React.ForwardRefExoticComponent<SliderProps & React.RefAttributes<HTMLSpanElement>>;
@@ -4,25 +4,70 @@ import * as React from "react";
4
4
  import * as RadixSlider from "@radix-ui/react-slider";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  const Slider = React.forwardRef(
7
- ({ className, thumbLabels, "aria-label": ariaLabel, ...props }, ref) => {
7
+ ({ className, thumbLabels, marks, showValue, formatValue, "aria-label": ariaLabel, onValueChange, ...props }, ref) => {
8
+ const { min = 0, max = 100, inverted } = props;
9
+ const [own, setOwn] = React.useState(props.defaultValue ?? [min]);
10
+ const values = props.value ?? own;
8
11
  const count = (props.value ?? props.defaultValue ?? [0]).length;
12
+ const format = formatValue ?? String;
13
+ const at = (v) => {
14
+ const p = (v - min) / (max - min) * 100;
15
+ const pct = inverted ? 100 - p : p;
16
+ return { insetInlineStart: `calc(${pct}% + ${(0.5 - pct / 100) * 1.25}rem)` };
17
+ };
18
+ const list = marks?.map((m) => typeof m === "number" ? { value: m } : m);
19
+ const labelled = list?.some((m) => m.label);
9
20
  return /* @__PURE__ */ jsxs(
10
21
  RadixSlider.Root,
11
22
  {
12
23
  ref,
13
- className: cn("group relative flex h-control-sm w-full touch-none select-none items-center", className),
24
+ className: cn(
25
+ "group relative flex h-control-sm w-full touch-none select-none items-center",
26
+ labelled && "mb-5",
27
+ className
28
+ ),
29
+ onValueChange: (v) => {
30
+ setOwn(v);
31
+ onValueChange?.(v);
32
+ },
14
33
  ...props,
15
34
  children: [
16
35
  /* @__PURE__ */ jsx(RadixSlider.Track, { className: "relative h-1.5 w-full grow overflow-hidden rounded-full bg-[var(--color-surface-container-highest)]", children: /* @__PURE__ */ jsx(RadixSlider.Range, { className: "absolute h-full bg-[var(--color-primary)] group-data-[disabled]:bg-[var(--color-text-disabled)]" }) }),
36
+ list?.map((m) => /* @__PURE__ */ jsxs(
37
+ "span",
38
+ {
39
+ "aria-hidden": "true",
40
+ "data-mark": m.value,
41
+ className: "pointer-events-none absolute inset-y-0 flex w-0 flex-col items-center justify-center",
42
+ style: at(m.value),
43
+ children: [
44
+ /* @__PURE__ */ jsx("span", { className: "h-1 w-1 shrink-0 rounded-full bg-[var(--color-on-surface-muted)]" }),
45
+ m.label && /* @__PURE__ */ jsx("span", { className: "absolute top-full whitespace-nowrap text-label-sm text-[var(--color-on-surface-muted)]", children: m.label })
46
+ ]
47
+ },
48
+ m.value
49
+ )),
17
50
  Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx(
18
51
  RadixSlider.Thumb,
19
52
  {
20
53
  "aria-label": thumbLabels?.[i] ?? ariaLabel,
54
+ "aria-valuetext": formatValue && values[i] !== void 0 ? formatValue(values[i]) : void 0,
21
55
  className: cn(
22
- "nx-state-layer block h-5 w-5 rounded-full border-2 border-[var(--color-surface-container-lowest)] bg-[var(--color-primary)] text-[var(--color-primary)] shadow-elevation-1",
56
+ "nx-state-layer group/thumb relative block h-5 w-5 rounded-full border-2 border-[var(--color-surface-container-lowest)] bg-[var(--color-primary)] text-[var(--color-primary)] shadow-elevation-1",
23
57
  "before:-inset-2.5 cursor-grab active:cursor-grabbing",
24
58
  // Opaque disabled knob (text-disabled is translucent and would show the range through it).
25
59
  "group-data-[disabled]:pointer-events-none group-data-[disabled]:bg-[color-mix(in_srgb,var(--color-on-surface)_38%,var(--color-surface))]"
60
+ ),
61
+ children: showValue && values[i] !== void 0 && /* @__PURE__ */ jsx(
62
+ "span",
63
+ {
64
+ "aria-hidden": "true",
65
+ className: cn(
66
+ "pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 whitespace-nowrap rounded-shape-tooltip bg-[var(--color-inverse-surface)] px-2 py-1 text-label-md text-[var(--color-inverse-on-surface)] shadow-elevation-1 transition-opacity",
67
+ showValue === "hover" && "opacity-0 group-hover/thumb:opacity-100 group-focus/thumb:opacity-100"
68
+ ),
69
+ children: format(values[i])
70
+ }
26
71
  )
27
72
  },
28
73
  i
@@ -1,2 +1,2 @@
1
1
  export { Slider } from './Slider.js';
2
- export type { SliderProps } from './Slider.js';
2
+ export type { SliderProps, SliderMark } from './Slider.js';
@@ -8,5 +8,11 @@ export declare const thumbVariants: (props?: ({
8
8
  size?: "sm" | "md" | "lg" | null | undefined;
9
9
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
10
10
  export interface SwitchProps extends RadixSwitch.SwitchProps, VariantProps<typeof switchVariants> {
11
+ /** Spinner in the thumb and `aria-busy`; clicks don't toggle while loading. Stays focusable. */
12
+ loading?: boolean;
13
+ /** Small icon inside the thumb while on. */
14
+ checkedIcon?: React.ReactNode;
15
+ /** Small icon inside the thumb while off. */
16
+ uncheckedIcon?: React.ReactNode;
11
17
  }
12
18
  export declare const Switch: React.ForwardRefExoticComponent<SwitchProps & React.RefAttributes<HTMLButtonElement>>;
@@ -1,9 +1,10 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as RadixSwitch from "@radix-ui/react-switch";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { cva } from "class-variance-authority";
7
+ import { spinnerVariants } from "../Spinner/Spinner.js";
7
8
  const switchVariants = cva(
8
9
  // Off: tonal track with a 2px outline (>= 3:1, WCAG 1.4.11). On: primary.
9
10
  // Focus = the global ring (it follows the capsule).
@@ -43,8 +44,34 @@ const thumbVariants = cva(
43
44
  defaultVariants: { size: "md" }
44
45
  }
45
46
  );
47
+ const thumbSlot = "hidden h-full w-full items-center justify-center [&>svg]:h-2/3 [&>svg]:w-2/3";
46
48
  const Switch = React.forwardRef(
47
- ({ className, size, ...props }, ref) => /* @__PURE__ */ jsx(RadixSwitch.Root, { ref, className: cn(switchVariants({ size }), "nx-hit", className), ...props, children: /* @__PURE__ */ jsx(RadixSwitch.Thumb, { className: cn(thumbVariants({ size })) }) })
49
+ ({ className, size, loading, checkedIcon, uncheckedIcon, onClick, ...props }, ref) => /* @__PURE__ */ jsx(
50
+ RadixSwitch.Root,
51
+ {
52
+ ref,
53
+ className: cn(switchVariants({ size }), "nx-hit", loading && "cursor-progress", className),
54
+ "aria-busy": loading || void 0,
55
+ onClick: (e) => {
56
+ onClick?.(e);
57
+ if (loading) e.preventDefault();
58
+ },
59
+ ...props,
60
+ children: /* @__PURE__ */ jsx(
61
+ RadixSwitch.Thumb,
62
+ {
63
+ className: cn(
64
+ thumbVariants({ size }),
65
+ (loading || checkedIcon || uncheckedIcon) && "group/thumb flex items-center justify-center data-[state=checked]:text-[var(--color-primary)] data-[state=unchecked]:text-[var(--color-surface-container-highest)]"
66
+ ),
67
+ children: loading ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(spinnerVariants({ size: "xs" }), "h-2/3 w-2/3 border-current border-t-transparent") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
68
+ checkedIcon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(thumbSlot, "group-data-[state=checked]/thumb:flex"), children: checkedIcon }),
69
+ uncheckedIcon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(thumbSlot, "group-data-[state=unchecked]/thumb:flex"), children: uncheckedIcon })
70
+ ] })
71
+ }
72
+ )
73
+ }
74
+ )
48
75
  );
49
76
  Switch.displayName = "Switch";
50
77
  export {