@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
@@ -2,13 +2,14 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as RadixPopover from "@radix-ui/react-popover";
5
- import { DayPicker } from "react-day-picker";
6
- import { format } from "date-fns";
5
+ import { DayPicker, dateMatchModifiers } from "react-day-picker";
6
+ import { format, isValid, parse } from "date-fns";
7
7
  import { CalendarDays, ChevronLeft, ChevronRight } from "@nexub/icons";
8
8
  import { cn } from "../../utils/cn.js";
9
9
  import { useIntlLocale, useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
10
- import { field, fieldFocusVisible } from "../../utils/control.js";
10
+ import { field, fieldDisabled, fieldFocusVisible, fieldFocusWithin, fieldInvalid } from "../../utils/control.js";
11
11
  import { floatingSurface } from "../../utils/surface.js";
12
+ import { Button } from "../Button/Button.js";
12
13
  function formatDate(date, locale, dateLocale) {
13
14
  if (dateLocale) return format(date, "PP", { locale: dateLocale });
14
15
  return locale ? new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(date) : format(date, "PP");
@@ -71,7 +72,18 @@ const calendarClassNames = {
71
72
  today: "[&>button]:border-[var(--color-primary)]",
72
73
  outside: "[&:not([data-selected]):not([data-disabled])>button]:text-[var(--color-on-surface-muted)]",
73
74
  disabled: "",
74
- hidden: "invisible"
75
+ hidden: "invisible",
76
+ // Month change (animate): the months crossfade. react-day-picker adds each of
77
+ // these with classList.add, so one class apiece. Keyframes from @nexub/themes
78
+ // core.css; reduced motion collapses them and animationend still cleans up.
79
+ caption_after_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
80
+ caption_before_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
81
+ weeks_after_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
82
+ weeks_before_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
83
+ caption_after_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]",
84
+ caption_before_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]",
85
+ weeks_after_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]",
86
+ weeks_before_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]"
75
87
  };
76
88
  const DatePicker = React.forwardRef(
77
89
  function DatePicker2({
@@ -84,15 +96,75 @@ const DatePicker = React.forwardRef(
84
96
  className,
85
97
  id,
86
98
  "aria-label": ariaLabel,
87
- locale: localeProp
99
+ locale: localeProp,
100
+ disabledDates,
101
+ presets,
102
+ typeable = false
88
103
  }, ref) {
89
104
  const [open, setOpen] = React.useState(false);
90
105
  const messages = useNexubMessages();
91
106
  const placeholder = placeholderProp ?? messages.pickDate;
92
107
  const calendar = useCalendarLocale(localeProp);
93
108
  const display = value ? calendar.format(value) : placeholder;
109
+ const [text, setText] = React.useState(null);
110
+ const dateLocale = calendar.dayPickerProps.locale;
111
+ const typed = text?.trim() ? parse(text.trim(), "P", /* @__PURE__ */ new Date(), { locale: dateLocale }) : void 0;
112
+ const invalid = typed !== void 0 && (!isValid(typed) || minDate !== void 0 && typed < minDate || maxDate !== void 0 && typed > maxDate || disabledDates !== void 0 && dateMatchModifiers(typed, disabledDates));
113
+ const commit = () => {
114
+ if (text === null || invalid) return;
115
+ onChange?.(typed);
116
+ setText(null);
117
+ };
118
+ const pick = (date) => {
119
+ onChange?.(date);
120
+ setText(null);
121
+ setOpen(false);
122
+ };
94
123
  return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange: setOpen, children: [
95
- /* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
124
+ typeable ? /* @__PURE__ */ jsx(RadixPopover.Anchor, { asChild: true, children: /* @__PURE__ */ jsxs(
125
+ "div",
126
+ {
127
+ className: cn(
128
+ "flex h-control-md w-full items-center gap-1 ps-3.5 pe-1",
129
+ field,
130
+ fieldFocusWithin,
131
+ invalid && fieldInvalid,
132
+ disabled && fieldDisabled,
133
+ className
134
+ ),
135
+ children: [
136
+ /* @__PURE__ */ jsx(
137
+ "input",
138
+ {
139
+ id,
140
+ type: "text",
141
+ disabled,
142
+ "aria-label": ariaLabel,
143
+ "aria-invalid": invalid || void 0,
144
+ placeholder,
145
+ value: text ?? (value ? format(value, "P", { locale: dateLocale }) : ""),
146
+ onChange: (e) => setText(e.target.value),
147
+ onBlur: commit,
148
+ onKeyDown: (e) => {
149
+ if (e.key === "Enter") commit();
150
+ },
151
+ className: "min-w-0 flex-1 bg-transparent text-body-md outline-none placeholder:text-[var(--color-on-surface-muted)] disabled:cursor-not-allowed"
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(
155
+ "button",
156
+ {
157
+ ref,
158
+ type: "button",
159
+ disabled,
160
+ "aria-label": messages.pickDate,
161
+ className: "nx-state-layer inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full disabled:pointer-events-none",
162
+ children: /* @__PURE__ */ jsx(CalendarDays, { size: "sm", "aria-hidden": "true" })
163
+ }
164
+ ) })
165
+ ]
166
+ }
167
+ ) }) : /* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
96
168
  "button",
97
169
  {
98
170
  ref,
@@ -115,7 +187,7 @@ const DatePicker = React.forwardRef(
115
187
  ]
116
188
  }
117
189
  ) }),
118
- /* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsx(
190
+ /* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsxs(
119
191
  RadixPopover.Content,
120
192
  {
121
193
  align: "start",
@@ -124,25 +196,27 @@ const DatePicker = React.forwardRef(
124
196
  floatingSurface,
125
197
  "nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
126
198
  ),
127
- children: /* @__PURE__ */ jsx(
128
- DayPicker,
129
- {
130
- mode: "single",
131
- selected: value,
132
- defaultMonth: value,
133
- onSelect: (date) => {
134
- onChange?.(date);
135
- setOpen(false);
136
- },
137
- startMonth: minDate,
138
- endMonth: maxDate,
139
- autoFocus: true,
140
- showOutsideDays: true,
141
- className: "relative p-3",
142
- classNames: calendarClassNames,
143
- ...calendar.dayPickerProps
144
- }
145
- )
199
+ children: [
200
+ /* @__PURE__ */ jsx(
201
+ DayPicker,
202
+ {
203
+ mode: "single",
204
+ selected: value,
205
+ defaultMonth: value,
206
+ onSelect: pick,
207
+ disabled: disabledDates,
208
+ startMonth: minDate,
209
+ endMonth: maxDate,
210
+ autoFocus: true,
211
+ animate: true,
212
+ showOutsideDays: true,
213
+ className: "relative p-3",
214
+ classNames: calendarClassNames,
215
+ ...calendar.dayPickerProps
216
+ }
217
+ ),
218
+ presets && presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 border-t border-[var(--color-outline-variant)] p-2", children: presets.map((p) => /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "xs", onClick: () => pick(p.date), children: p.label }, p.label)) })
219
+ ]
146
220
  }
147
221
  ) })
148
222
  ] });
@@ -15,6 +15,12 @@ declare function Root({ scrollLock, modal, ...props }: DialogRootProps): React.J
15
15
  interface ContentProps extends RadixDialog.DialogContentProps {
16
16
  size?: Size | 'xl' | 'full';
17
17
  surface?: SurfaceValue;
18
+ /** Adds a close button in the top-end corner. @default false */
19
+ showClose?: boolean;
20
+ /** Accessible name of the close button. @default messages.close */
21
+ closeLabel?: string;
22
+ /** `true`: bottom sheet. `'auto'`: bottom sheet below the `md` breakpoint, centred above. @default false */
23
+ sheet?: boolean | 'auto';
18
24
  }
19
25
  export declare const Dialog: {
20
26
  Root: typeof Root;
@@ -26,6 +32,7 @@ export declare const Dialog: {
26
32
  Title: React.ForwardRefExoticComponent<RadixDialog.DialogTitleProps & React.RefAttributes<HTMLHeadingElement>>;
27
33
  Description: React.ForwardRefExoticComponent<RadixDialog.DialogDescriptionProps & React.RefAttributes<HTMLParagraphElement>>;
28
34
  Header: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
35
+ Body: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
29
36
  Footer: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
30
37
  };
31
38
  export {};
@@ -1,11 +1,15 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as RadixDialog from "@radix-ui/react-dialog";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { overlaySurface, scrim } from "../../utils/surface.js";
7
7
  import { glassSurfaceClass } from "../../utils/glass.js";
8
8
  import { ClassLockOverlay, ScrollLockContext } from "../../hooks/useClassScrollLock.js";
9
+ import { useMediaQuery } from "../../hooks/useMediaQuery.js";
10
+ import { X } from "@nexub/icons";
11
+ import { IconButton } from "../IconButton/IconButton.js";
12
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
9
13
  function Root({ scrollLock = "style", modal = true, ...props }) {
10
14
  const lock = React.useMemo(() => ({ scrollLock, modal }), [scrollLock, modal]);
11
15
  return /* @__PURE__ */ jsx(ScrollLockContext.Provider, { value: lock, children: /* @__PURE__ */ jsx(RadixDialog.Root, { modal, ...props }) });
@@ -33,8 +37,19 @@ const Overlay = React.forwardRef(
33
37
  }
34
38
  );
35
39
  Overlay.displayName = "Dialog.Overlay";
40
+ const StackContext = React.createContext(null);
36
41
  const Content = React.forwardRef(
37
- function DialogContent({ className, size = "md", surface = "solid", children, ...props }, ref) {
42
+ function DialogContent({ className, size = "md", surface = "solid", showClose = false, closeLabel, sheet = false, children, ...props }, ref) {
43
+ const messages = useNexubMessages();
44
+ const narrow = useMediaQuery("(max-width: 47.99rem)");
45
+ const asSheet = sheet === true || sheet === "auto" && narrow;
46
+ const parent = React.useContext(StackContext);
47
+ const [openChildren, setOpenChildren] = React.useState(0);
48
+ React.useEffect(() => {
49
+ if (!parent) return;
50
+ parent((n) => n + 1);
51
+ return () => parent((n) => n - 1);
52
+ }, [parent]);
38
53
  const sizeMap = {
39
54
  sm: "max-w-sm",
40
55
  md: "max-w-lg",
@@ -42,23 +57,28 @@ const Content = React.forwardRef(
42
57
  xl: "max-w-4xl",
43
58
  full: "max-w-[calc(100vw-2rem)]"
44
59
  };
45
- return /* @__PURE__ */ jsx(
60
+ return /* @__PURE__ */ jsxs(
46
61
  RadixDialog.Content,
47
62
  {
48
63
  ref,
49
64
  className: cn(
50
- "fixed left-1/2 top-1/2 z-[var(--z-modal)] w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
65
+ asSheet ? "fixed inset-x-0 bottom-0 z-[var(--z-modal)] w-full" : "fixed left-1/2 top-1/2 z-[var(--z-modal)] w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
51
66
  "nx-motion nx-dialog",
52
67
  overlaySurface,
53
68
  "max-h-[90vh] overflow-y-auto p-6 focus:outline-none",
54
- sizeMap[size],
69
+ asSheet ? "rounded-b-none" : sizeMap[size],
55
70
  surface === "glass" && glassSurfaceClass,
56
71
  className
57
72
  ),
58
73
  "data-material": surface === "glass" ? "glass" : void 0,
59
74
  "data-glass-thickness": surface === "glass" ? "thick" : void 0,
75
+ "data-sheet": asSheet ? "" : void 0,
76
+ "data-nested": openChildren > 0 ? "" : void 0,
60
77
  ...props,
61
- children
78
+ children: [
79
+ /* @__PURE__ */ jsx(StackContext.Provider, { value: setOpenChildren, children }),
80
+ showClose && /* @__PURE__ */ jsx(RadixDialog.Close, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { label: closeLabel ?? messages.close, variant: "ghost", size: "sm", className: "absolute end-3 top-3", children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) }) })
81
+ ]
62
82
  }
63
83
  );
64
84
  }
@@ -92,16 +112,22 @@ const Description = React.forwardRef(
92
112
  Description.displayName = "Dialog.Description";
93
113
  const Header = React.forwardRef(
94
114
  function DialogHeader({ className, ...props }, ref) {
95
- return /* @__PURE__ */ jsx("div", { ref, className: cn("mb-4 flex flex-col gap-1", className), ...props });
115
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-header mb-4 flex flex-col gap-1", className), ...props });
96
116
  }
97
117
  );
98
118
  Header.displayName = "Dialog.Header";
99
119
  const Footer = React.forwardRef(
100
120
  function DialogFooter({ className, ...props }, ref) {
101
- return /* @__PURE__ */ jsx("div", { ref, className: cn("mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props });
121
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-footer mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props });
102
122
  }
103
123
  );
104
124
  Footer.displayName = "Dialog.Footer";
125
+ const Body = React.forwardRef(
126
+ function DialogBody({ className, ...props }, ref) {
127
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-body", className), ...props });
128
+ }
129
+ );
130
+ Body.displayName = "Dialog.Body";
105
131
  const Dialog = {
106
132
  Root,
107
133
  Trigger,
@@ -112,6 +138,7 @@ const Dialog = {
112
138
  Title,
113
139
  Description,
114
140
  Header,
141
+ Body,
115
142
  Footer
116
143
  };
117
144
  export {
@@ -1,6 +1,12 @@
1
1
  import * as React from 'react';
2
+ export interface EmptyStateRootProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ /** Scales spacing, icon and text. @default 'md' */
4
+ size?: 'sm' | 'md' | 'lg';
5
+ /** Fades in on mount (nx-reveal; reduced motion keeps the crossfade). */
6
+ animate?: boolean;
7
+ }
2
8
  export declare const EmptyState: {
3
- Root: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
9
+ Root: React.ForwardRefExoticComponent<EmptyStateRootProps & React.RefAttributes<HTMLDivElement>>;
4
10
  Illustration: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
5
11
  Title: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLHeadingElement>>;
6
12
  Description: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
@@ -1,13 +1,17 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "../../utils/cn.js";
4
+ const rootSizes = { sm: "gap-3 py-8", md: "gap-4 py-16", lg: "gap-6 py-24" };
4
5
  const Root = React.forwardRef(
5
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
6
+ ({ className, size = "md", animate, ...props }, ref) => /* @__PURE__ */ jsx(
6
7
  "div",
7
8
  {
8
9
  ref,
10
+ "data-size": size,
9
11
  className: cn(
10
- "flex flex-col items-center justify-center gap-4 py-16 text-center",
12
+ "group/empty-state flex flex-col items-center justify-center text-center",
13
+ rootSizes[size],
14
+ animate && "nx-reveal",
11
15
  className
12
16
  ),
13
17
  ...props
@@ -20,7 +24,11 @@ const Illustration = React.forwardRef(
20
24
  "div",
21
25
  {
22
26
  ref,
23
- className: cn("text-[var(--color-on-surface-muted)]", className),
27
+ className: cn(
28
+ "text-[var(--color-on-surface-muted)]",
29
+ "group-data-[size=sm]/empty-state:[&_svg]:size-8 group-data-[size=lg]/empty-state:[&_svg]:size-16",
30
+ className
31
+ ),
24
32
  ...props
25
33
  }
26
34
  )
@@ -31,7 +39,11 @@ const Title = React.forwardRef(
31
39
  "h3",
32
40
  {
33
41
  ref,
34
- className: cn("text-lg font-semibold text-[var(--color-on-surface)]", className),
42
+ className: cn(
43
+ "text-lg font-semibold text-[var(--color-on-surface)]",
44
+ "group-data-[size=sm]/empty-state:text-base group-data-[size=lg]/empty-state:text-xl",
45
+ className
46
+ ),
35
47
  ...props
36
48
  }
37
49
  )
@@ -41,7 +53,11 @@ const Description = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
41
53
  "p",
42
54
  {
43
55
  ref,
44
- className: cn("max-w-sm text-sm text-[var(--color-on-surface-muted)]", className),
56
+ className: cn(
57
+ "max-w-sm text-sm text-[var(--color-on-surface-muted)]",
58
+ "group-data-[size=sm]/empty-state:text-xs group-data-[size=lg]/empty-state:text-base",
59
+ className
60
+ ),
45
61
  ...props
46
62
  }
47
63
  ));
@@ -1,7 +1,8 @@
1
1
  import * as React from 'react';
2
2
  import type { NexubMessages } from '@nexub/core';
3
+ import { type EmptyStateRootProps } from './EmptyState.js';
3
4
  export type EmptyStateKind = keyof NexubMessages['emptyStates'];
4
- export interface EmptyStatePresetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
5
+ export interface EmptyStatePresetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>, Pick<EmptyStateRootProps, 'size' | 'animate'> {
5
6
  /** noData, noResults, noAccess (403), serverError (500) or offline. */
6
7
  kind: EmptyStateKind;
7
8
  /** Overrides the preset's title; the description too. Both default to localised text. */
@@ -5,6 +5,7 @@ import { Upload, X } from "@nexub/icons";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
7
7
  import { Progress } from "../Progress/Progress.js";
8
+ import { SPRING_MS } from "../Motion/useMotion.js";
8
9
  function formatFileSize(bytes, locale) {
9
10
  const [value, unit] = bytes < 1024 ? [bytes, "byte"] : bytes < 1024 * 1024 ? [bytes / 1024, "kilobyte"] : [bytes / (1024 * 1024), "megabyte"];
10
11
  return new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "short", maximumFractionDigits: 1 }).format(value);
@@ -27,7 +28,7 @@ function useFileUpload() {
27
28
  return ctx;
28
29
  }
29
30
  const Root = React.forwardRef(
30
- ({ className, accept, maxFiles, maxSize, onFilesChange, onReject, validate, children, ...props }, ref) => {
31
+ ({ className, accept, maxFiles, maxSize, onFilesChange, onReject, validate, onPaste, children, ...props }, ref) => {
31
32
  const [files, setFiles] = React.useState([]);
32
33
  const [rejections, setRejections] = React.useState([]);
33
34
  const [isDragging, setIsDragging] = React.useState(false);
@@ -65,7 +66,22 @@ const Root = React.forwardRef(
65
66
  () => ({ accept, maxFiles, maxSize, onFilesChange, files, rejections, addFiles, removeFile, isDragging, setIsDragging }),
66
67
  [accept, maxFiles, maxSize, onFilesChange, files, rejections, addFiles, removeFile, isDragging, setIsDragging]
67
68
  );
68
- return /* @__PURE__ */ jsx(FileUploadContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { ref, className: cn("flex flex-col gap-4", className), ...props, children }) });
69
+ return /* @__PURE__ */ jsx(FileUploadContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
70
+ "div",
71
+ {
72
+ ref,
73
+ className: cn("flex flex-col gap-4", className),
74
+ onPaste: (e) => {
75
+ onPaste?.(e);
76
+ const pasted = Array.from(e.clipboardData?.files ?? []);
77
+ if (e.defaultPrevented || !pasted.length) return;
78
+ e.preventDefault();
79
+ addFiles(pasted);
80
+ },
81
+ ...props,
82
+ children
83
+ }
84
+ ) });
69
85
  }
70
86
  );
71
87
  Root.displayName = "FileUpload.Root";
@@ -140,7 +156,7 @@ const List = React.forwardRef(
140
156
  ({ className, ...props }, ref) => {
141
157
  const { files } = useFileUpload();
142
158
  if (files.length === 0) return null;
143
- return /* @__PURE__ */ jsx("ul", { ref, className: cn("flex flex-col gap-2", className), ...props });
159
+ return /* @__PURE__ */ jsx("ul", { ref, className: cn("flex flex-col", className), ...props });
144
160
  }
145
161
  );
146
162
  List.displayName = "FileUpload.List";
@@ -149,40 +165,78 @@ const Item = React.forwardRef(({ className, index, progress, onCancel, error, ..
149
165
  const messages = useNexubMessages();
150
166
  const { locale } = useLocale();
151
167
  const file = files[index];
168
+ const [exiting, setExiting] = React.useState(null);
169
+ const [entered, setEntered] = React.useState(false);
170
+ const [thumb, setThumb] = React.useState();
171
+ React.useEffect(() => {
172
+ if (!file?.type.startsWith("image/") || typeof URL.createObjectURL !== "function") return;
173
+ const url = URL.createObjectURL(file);
174
+ setThumb(url);
175
+ return () => {
176
+ URL.revokeObjectURL(url);
177
+ setThumb(void 0);
178
+ };
179
+ }, [file]);
180
+ const isExiting = !!file && exiting === file;
181
+ React.useEffect(() => {
182
+ if (!isExiting) return;
183
+ const t = setTimeout(() => removeFile(index), SPRING_MS.effects.default + 50);
184
+ return () => clearTimeout(t);
185
+ }, [isExiting, index, removeFile]);
152
186
  if (!file) return null;
153
187
  const uploading = progress !== void 0 && progress < 100;
154
188
  const percent = Math.round(Math.max(0, Math.min(100, progress ?? 0)));
155
- return /* @__PURE__ */ jsxs(
156
- "li",
157
- {
158
- ref,
159
- className: cn(
160
- "flex items-center justify-between gap-3 rounded-shape-container border border-[var(--color-outline-variant)] bg-[var(--color-surface)] p-3",
161
- className
162
- ),
163
- ...props,
164
- children: [
165
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
166
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-[var(--color-on-surface)]", children: file.name }),
167
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-[var(--color-on-surface-muted)]", children: [
168
- formatFileSize(file.size, locale),
169
- uploading && ` \xB7 ${percent}%`
170
- ] }),
171
- uploading && /* @__PURE__ */ jsx(Progress, { value: percent, size: "sm", className: "mt-1.5", "aria-label": messages.uploadProgress(file.name, percent) }),
172
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-[var(--color-error)]", children: error })
173
- ] }),
174
- /* @__PURE__ */ jsx(
175
- "button",
189
+ return (
190
+ // nx-list-item: grows in on add, collapses out on remove (grid rows, spring tokens).
191
+ /* @__PURE__ */ jsx(
192
+ "li",
193
+ {
194
+ ref,
195
+ className: "nx-list-item",
196
+ "data-state": isExiting ? "exit" : entered ? void 0 : "enter",
197
+ onAnimationEnd: (e) => {
198
+ if (e.target !== e.currentTarget) return;
199
+ if (isExiting) removeFile(index);
200
+ else setEntered(true);
201
+ },
202
+ ...props,
203
+ children: /* @__PURE__ */ jsx("div", { className: "nx-list-item-inner", children: /* @__PURE__ */ jsx("div", { className: "[li:not(:last-child)>.nx-list-item-inner>&]:pb-2", children: /* @__PURE__ */ jsxs(
204
+ "div",
176
205
  {
177
- type: "button",
178
- onClick: () => uploading && onCancel ? onCancel() : removeFile(index),
179
- "aria-label": uploading && onCancel ? messages.cancelUpload(file.name) : messages.removeFile(file.name),
180
- className: "nx-state-layer shrink-0 rounded-full p-1 text-[var(--color-on-surface-muted)] hover:text-[var(--color-on-surface)] transition-colors",
181
- children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" })
206
+ className: cn(
207
+ "flex items-center justify-between gap-3 rounded-shape-container border border-[var(--color-outline-variant)] bg-[var(--color-surface)] p-3",
208
+ className
209
+ ),
210
+ children: [
211
+ thumb && /* @__PURE__ */ jsx("img", { src: thumb, alt: "", className: "h-10 w-10 shrink-0 rounded-shape-min object-cover" }),
212
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
213
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-[var(--color-on-surface)]", children: file.name }),
214
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-[var(--color-on-surface-muted)]", children: [
215
+ formatFileSize(file.size, locale),
216
+ uploading && ` \xB7 ${percent}%`
217
+ ] }),
218
+ uploading && /* @__PURE__ */ jsx(Progress, { value: percent, size: "sm", className: "mt-1.5", "aria-label": messages.uploadProgress(file.name, percent) }),
219
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-[var(--color-error)]", children: error })
220
+ ] }),
221
+ /* @__PURE__ */ jsx(
222
+ "button",
223
+ {
224
+ type: "button",
225
+ onClick: () => {
226
+ if (uploading && onCancel) return onCancel();
227
+ setEntered(true);
228
+ setExiting(file);
229
+ },
230
+ "aria-label": uploading && onCancel ? messages.cancelUpload(file.name) : messages.removeFile(file.name),
231
+ className: "nx-state-layer shrink-0 rounded-full p-1 text-[var(--color-on-surface-muted)] hover:text-[var(--color-on-surface)] transition-colors",
232
+ children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" })
233
+ }
234
+ )
235
+ ]
182
236
  }
183
- )
184
- ]
185
- }
237
+ ) }) })
238
+ }
239
+ )
186
240
  );
187
241
  });
188
242
  Item.displayName = "FileUpload.Item";
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import type { ToneValue } from '../Button/Button.js';
3
3
  export declare const headingVariants: (props?: ({
4
- size?: "display" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | null | undefined;
4
+ size?: "display" | "h2" | "h3" | "h1" | "h4" | "h5" | "h6" | null | undefined;
5
5
  weight?: "bold" | "regular" | "medium" | "semibold" | "extrabold" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  type HeadingSize = 'display' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
@@ -1,11 +1,15 @@
1
1
  import * as React from 'react';
2
2
  import { type VariantProps } from 'class-variance-authority';
3
3
  export declare const iconButtonVariants: (props?: ({
4
- variant?: "outline" | "solid" | "ghost" | "subtle" | null | undefined;
4
+ variant?: "outline" | "solid" | "subtle" | "ghost" | null | undefined;
5
5
  size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof iconButtonVariants> {
8
8
  label: string;
9
9
  asChild?: boolean;
10
+ /** Spinner replaces the icon (size kept); clicks are blocked, focus stays. */
11
+ loading?: boolean;
12
+ /** Built-in tooltip: content, or `true` to show `label`. No Provider needed. */
13
+ tooltip?: React.ReactNode;
10
14
  }
11
15
  export declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -1,9 +1,12 @@
1
+ "use client";
1
2
  import { jsx } from "react/jsx-runtime";
2
3
  import * as React from "react";
3
4
  import { Slot } from "@radix-ui/react-slot";
4
5
  import { cva } from "class-variance-authority";
5
6
  import { cn } from "../../utils/cn.js";
6
7
  import { disabledContainer, disabledText, pressable } from "../../utils/control.js";
8
+ import { spinnerVariants } from "../Spinner/Spinner.js";
9
+ const LazyTooltip = React.lazy(() => import("../Tooltip/Tooltip.js").then((m) => ({ default: m.Tooltip })));
7
10
  const iconButtonVariants = cva(
8
11
  [pressable, "relative inline-flex items-center justify-center flex-shrink-0"],
9
12
  {
@@ -30,18 +33,26 @@ const iconButtonVariants = cva(
30
33
  }
31
34
  );
32
35
  const IconButton = React.forwardRef(
33
- ({ className, variant, size, label, asChild = false, children, ...props }, ref) => {
36
+ ({ className, variant, size, label, asChild = false, loading, tooltip, onClick, children, ...props }, ref) => {
34
37
  const Comp = asChild ? Slot : "button";
35
- return /* @__PURE__ */ jsx(
38
+ const button = /* @__PURE__ */ jsx(
36
39
  Comp,
37
40
  {
38
41
  ref,
39
42
  "aria-label": label,
43
+ "aria-busy": loading || void 0,
44
+ "aria-disabled": loading || props["aria-disabled"] || void 0,
40
45
  className: cn(iconButtonVariants({ variant, size }), "nx-hit", className),
46
+ onClick: (e) => {
47
+ if (loading) return e.preventDefault();
48
+ onClick?.(e);
49
+ },
41
50
  ...props,
42
- children
51
+ children: loading && !asChild ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(spinnerVariants({ size: size === "xs" ? "xs" : "sm" }), "border-current border-t-transparent") }) : children
43
52
  }
44
53
  );
54
+ if (tooltip == null || tooltip === false) return button;
55
+ return /* @__PURE__ */ jsx(React.Suspense, { fallback: button, children: /* @__PURE__ */ jsx(LazyTooltip, { label: tooltip === true ? label : tooltip, children: button }) });
45
56
  }
46
57
  );
47
58
  IconButton.displayName = "IconButton";
@@ -17,5 +17,11 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
17
17
  leadingIcon?: React.ReactNode;
18
18
  trailingIcon?: React.ReactNode;
19
19
  wrapperClassName?: string;
20
+ /** Shows a clear button while there is a value. Clearing fires `onChange` with "". */
21
+ clearable?: boolean;
22
+ /** Shows a "12/80" counter; needs `maxLength`. Announced politely near the limit. */
23
+ showCount?: boolean;
24
+ /** Spinner in the trailing slot and `aria-busy` on the input. */
25
+ loading?: boolean;
20
26
  }
21
27
  export declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;