@nexub/react 7.4.0 → 7.6.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 (37) hide show
  1. package/accessibility.json +1 -1
  2. package/dist/components/Badge/Badge.d.ts +9 -0
  3. package/dist/components/Badge/Badge.js +40 -2
  4. package/dist/components/Button/Button.d.ts +1 -1
  5. package/dist/components/Combobox/Combobox.d.ts +6 -9
  6. package/dist/components/Combobox/Combobox.js +30 -9
  7. package/dist/components/CommandPalette/CommandPalette.d.ts +34 -13
  8. package/dist/components/CommandPalette/CommandPalette.js +174 -40
  9. package/dist/components/CommandPalette/index.d.ts +1 -1
  10. package/dist/components/DatePicker/DatePicker.d.ts +16 -1
  11. package/dist/components/DatePicker/DatePicker.js +100 -26
  12. package/dist/components/Dialog/Dialog.d.ts +7 -0
  13. package/dist/components/Dialog/Dialog.js +35 -8
  14. package/dist/components/EmptyState/EmptyState.d.ts +7 -1
  15. package/dist/components/EmptyState/EmptyState.js +21 -5
  16. package/dist/components/EmptyState/EmptyStatePreset.d.ts +2 -1
  17. package/dist/components/FileUpload/FileUpload.js +86 -32
  18. package/dist/components/Heading/Heading.d.ts +1 -1
  19. package/dist/components/IconButton/IconButton.d.ts +5 -1
  20. package/dist/components/IconButton/IconButton.js +14 -3
  21. package/dist/components/Input/Input.d.ts +6 -0
  22. package/dist/components/Input/Input.js +47 -5
  23. package/dist/components/Select/Select.d.ts +18 -3
  24. package/dist/components/Select/Select.js +170 -71
  25. package/dist/components/Slider/Slider.d.ts +10 -0
  26. package/dist/components/Slider/Slider.js +48 -3
  27. package/dist/components/Slider/index.d.ts +1 -1
  28. package/dist/components/Switch/Switch.d.ts +6 -0
  29. package/dist/components/Switch/Switch.js +29 -2
  30. package/dist/components/Tabs/Tabs.d.ts +5 -1
  31. package/dist/components/Tabs/Tabs.js +98 -35
  32. package/dist/components/Text/Text.d.ts +1 -1
  33. package/dist/components/Tooltip/Tooltip.d.ts +10 -3
  34. package/dist/components/Tooltip/Tooltip.js +16 -4
  35. package/dist/index.d.ts +3 -1
  36. package/dist/styles.css +1 -1
  37. package/package.json +6 -6
@@ -1,8 +1,14 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
3
  import * as React from "react";
4
+ import { X } from "@nexub/icons";
3
5
  import { cva } from "class-variance-authority";
4
6
  import { cn } from "../../utils/cn.js";
5
7
  import { field, fieldDisabledWithin, fieldFocusWithin, fieldInvalid } from "../../utils/control.js";
8
+ import { useMergedRefs } from "../../hooks/useMergedRefs.js";
9
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
10
+ import { IconButton } from "../IconButton/IconButton.js";
11
+ import { Spinner } from "../Spinner/Spinner.js";
6
12
  const inputVariants = cva(
7
13
  [
8
14
  // The wrapper owns ALL focus styling (border + ring). Strip every possible
@@ -67,6 +73,7 @@ const addonVariants = cva(
67
73
  }
68
74
  }
69
75
  );
76
+ const nearLimit = (length, max) => length >= max - Math.max(1, Math.ceil(max * 0.1));
70
77
  const Input = React.forwardRef(
71
78
  ({
72
79
  className,
@@ -78,26 +85,61 @@ const Input = React.forwardRef(
78
85
  trailingIcon,
79
86
  wrapperClassName,
80
87
  disabled,
88
+ clearable,
89
+ showCount,
90
+ loading,
81
91
  ...props
82
92
  }, ref) => {
83
93
  const hasAddon = Boolean(leadingAddon ?? trailingAddon);
94
+ const messages = useNexubMessages();
95
+ const innerRef = React.useRef(null);
96
+ const mergedRef = useMergedRefs(ref, innerRef);
97
+ const countId = React.useId();
98
+ const [uncontrolledLength, setLength] = React.useState(String(props.defaultValue ?? "").length);
99
+ const length = props.value !== void 0 && props.value !== null ? String(props.value).length : uncontrolledLength;
100
+ const max = props.maxLength;
101
+ const counting = Boolean(showCount && max !== void 0 && max >= 0);
102
+ const handleChange = (e) => {
103
+ setLength(e.target.value.length);
104
+ props.onChange?.(e);
105
+ };
106
+ const clear = () => {
107
+ const el = innerRef.current;
108
+ if (!el) return;
109
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(el, "");
110
+ el.dispatchEvent(new Event("input", { bubbles: true }));
111
+ el.focus();
112
+ };
84
113
  const inputEl = /* @__PURE__ */ jsx(
85
114
  "input",
86
115
  {
87
- ref,
88
116
  className: cn(inputVariants({ size }), className),
89
117
  "aria-invalid": invalid ?? void 0,
118
+ "aria-busy": loading || void 0,
90
119
  disabled,
91
- ...props
120
+ ...props,
121
+ "aria-describedby": [props["aria-describedby"], counting && countId].filter(Boolean).join(" ") || void 0,
122
+ onChange: handleChange,
123
+ ref: mergedRef
92
124
  }
93
125
  );
126
+ const showClear = clearable && length > 0 && !disabled && !props.readOnly;
127
+ const trailing = /* @__PURE__ */ jsxs(Fragment, { children: [
128
+ counting && /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-label-sm tabular-nums text-[var(--color-on-surface-muted)]", children: [
129
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: `${length}/${max}` }),
130
+ /* @__PURE__ */ jsx("span", { id: countId, className: "sr-only", "aria-live": nearLimit(length, max) ? "polite" : "off", children: messages.characterCount(length, max) })
131
+ ] }),
132
+ loading && /* @__PURE__ */ jsx(Spinner, { size: "sm", className: "shrink-0" }),
133
+ showClear && /* @__PURE__ */ jsx(IconButton, { size: "xs", label: messages.clear, onClick: clear, className: "-me-1.5", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4", "aria-hidden": "true" }) }),
134
+ trailingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: trailingIcon })
135
+ ] });
94
136
  if (hasAddon) {
95
137
  return /* @__PURE__ */ jsxs("div", { className: cn("flex w-full overflow-hidden", field, fieldFocusWithin, invalid && fieldInvalid), children: [
96
138
  leadingAddon && /* @__PURE__ */ jsx("span", { className: cn(addonVariants({ side: "leading" })), children: leadingAddon }),
97
139
  /* @__PURE__ */ jsxs("div", { className: cn(inputWrapperVariants({ size, invalid }), "rounded-none border-0 shadow-none focus-within:shadow-none", wrapperClassName), children: [
98
140
  leadingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: leadingIcon }),
99
141
  inputEl,
100
- trailingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: trailingIcon })
142
+ trailing
101
143
  ] }),
102
144
  trailingAddon && /* @__PURE__ */ jsx("span", { className: cn(addonVariants({ side: "trailing" })), children: trailingAddon })
103
145
  ] });
@@ -105,7 +147,7 @@ const Input = React.forwardRef(
105
147
  return /* @__PURE__ */ jsxs("div", { className: cn(inputWrapperVariants({ size, invalid }), wrapperClassName), children: [
106
148
  leadingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: leadingIcon }),
107
149
  inputEl,
108
- trailingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: trailingIcon })
150
+ trailing
109
151
  ] });
110
152
  }
111
153
  );
@@ -1,7 +1,22 @@
1
1
  import * as React from 'react';
2
2
  import * as RadixSelect from '@radix-ui/react-select';
3
+ export interface SelectRootProps extends RadixSelect.SelectProps {
4
+ /** Shows a clear button on the Trigger while there is a value; clearing calls `onValueChange('')` and shows the placeholder. */
5
+ clearable?: boolean;
6
+ }
7
+ export interface SelectContentProps extends RadixSelect.SelectContentProps {
8
+ /** A filter field at the top; items whose text (or `textValue`) doesn't match are hidden. Typing on an item moves to the field. */
9
+ searchable?: boolean;
10
+ }
11
+ export interface SelectItemProps extends RadixSelect.SelectItemProps {
12
+ /** A secondary line under the label: the option's accessible description, not part of its name. */
13
+ description?: React.ReactNode;
14
+ }
3
15
  export declare const Select: {
4
- Root: React.FC<RadixSelect.SelectProps>;
16
+ Root: {
17
+ ({ clearable, value, defaultValue, onValueChange, onOpenChange, ...props }: SelectRootProps): React.JSX.Element;
18
+ displayName: string;
19
+ };
5
20
  Group: React.ForwardRefExoticComponent<RadixSelect.SelectGroupProps & React.RefAttributes<HTMLDivElement>>;
6
21
  Value: React.ForwardRefExoticComponent<RadixSelect.SelectValueProps & React.RefAttributes<HTMLSpanElement>>;
7
22
  Trigger: React.ForwardRefExoticComponent<RadixSelect.SelectTriggerProps & {
@@ -9,8 +24,8 @@ export declare const Select: {
9
24
  } & React.RefAttributes<HTMLButtonElement>>;
10
25
  ScrollUpButton: React.ForwardRefExoticComponent<RadixSelect.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>>;
11
26
  ScrollDownButton: React.ForwardRefExoticComponent<RadixSelect.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>>;
12
- Content: React.ForwardRefExoticComponent<RadixSelect.SelectContentProps & React.RefAttributes<HTMLDivElement>>;
27
+ Content: React.ForwardRefExoticComponent<SelectContentProps & React.RefAttributes<HTMLDivElement>>;
13
28
  Label: React.ForwardRefExoticComponent<RadixSelect.SelectLabelProps & React.RefAttributes<HTMLDivElement>>;
14
- Item: React.ForwardRefExoticComponent<RadixSelect.SelectItemProps & React.RefAttributes<HTMLDivElement>>;
29
+ Item: React.ForwardRefExoticComponent<SelectItemProps & React.RefAttributes<HTMLDivElement>>;
15
30
  Separator: React.ForwardRefExoticComponent<RadixSelect.SelectSeparatorProps & React.RefAttributes<HTMLDivElement>>;
16
31
  };
@@ -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(
@@ -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 {
@@ -1,8 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import * as RadixTabs from '@radix-ui/react-tabs';
3
+ export interface TabsListProps extends RadixTabs.TabsListProps {
4
+ /** `line` (default): underline. `pill`: a raised thumb slides behind the active tab. */
5
+ variant?: 'line' | 'pill';
6
+ }
3
7
  export declare const Tabs: {
4
8
  Root: React.ForwardRefExoticComponent<RadixTabs.TabsProps & React.RefAttributes<HTMLDivElement>>;
5
- List: React.ForwardRefExoticComponent<RadixTabs.TabsListProps & React.RefAttributes<HTMLDivElement>>;
9
+ List: React.ForwardRefExoticComponent<TabsListProps & React.RefAttributes<HTMLDivElement>>;
6
10
  Trigger: React.ForwardRefExoticComponent<RadixTabs.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>>;
7
11
  Content: React.ForwardRefExoticComponent<RadixTabs.TabsContentProps & React.RefAttributes<HTMLDivElement>>;
8
12
  };