@nexub/react 8.3.0 → 8.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "8.3.0",
2
+ "version": "8.5.0",
3
3
  "standard": "WCAG 2.2 Level AA",
4
4
  "method": {
5
5
  "automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
@@ -9,7 +9,15 @@ export declare const buttonVariants: (props?: ({
9
9
  fullWidth?: boolean | null | undefined;
10
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
11
  export type ButtonStatus = 'idle' | 'loading' | 'success' | 'error';
12
+ /**
13
+ * @deprecated 8.4: the 1.x variant names, removed in 9.0. Use filled, tonal, outlined, text
14
+ * (`npx nexub-migrate --rules button-aliases --write`).
15
+ */
16
+ export type ButtonLegacyVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
12
17
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
18
+ /** Visual style. primary, secondary, outline and ghost are deprecated aliases of
19
+ * filled, tonal, outlined and text, removed in 9.0. @default 'filled' */
20
+ variant?: VariantProps<typeof buttonVariants>['variant'];
13
21
  tone?: ToneValue;
14
22
  /** Async feedback. `loading` blocks clicks (aria-disabled, focus kept),
15
23
  * `success` draws a check, `error` shakes once. Not applied with `asChild`. */
@@ -62,7 +62,7 @@ const buttonVariants = cva(
62
62
  }
63
63
  },
64
64
  defaultVariants: {
65
- variant: "primary",
65
+ variant: "filled",
66
66
  size: "md"
67
67
  }
68
68
  }
@@ -121,7 +121,7 @@ const Button = React.forwardRef(
121
121
  onPointerMove: handlePointerMove,
122
122
  "data-tone": tone,
123
123
  // Styling hook for opt-in layers (vivid, marketing); no styles depend on it by default.
124
- "data-variant": variant ?? "primary",
124
+ "data-variant": variant ?? "filled",
125
125
  ...variant === "glass" && { "data-material": "glass", "data-glass-thickness": "thin" },
126
126
  ...props
127
127
  };
@@ -1,2 +1,2 @@
1
1
  export { Button, buttonVariants } from './Button.js';
2
- export type { ButtonProps, ButtonStatus, ToneValue } from './Button.js';
2
+ export type { ButtonProps, ButtonStatus, ButtonLegacyVariant, ToneValue } from './Button.js';
@@ -49,20 +49,27 @@ const Root = React.forwardRef(
49
49
  const messages = useNexubMessages();
50
50
  const [pages = [], setPages] = useControllableState({ value: pagesProp, defaultValue: defaultPages, onChange: onPagesChange });
51
51
  const [open = false, setOpenState] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });
52
- const setOpen = (next) => {
53
- setOpenState(next);
54
- if (!next && pages.length) setPages([]);
55
- };
52
+ const setOpen = React.useCallback(
53
+ (next) => {
54
+ setOpenState(next);
55
+ if (!next && pages.length) setPages([]);
56
+ },
57
+ [setOpenState, setPages, pages.length]
58
+ );
56
59
  const [titles, setTitles] = React.useState({});
57
60
  const [recents, setRecents] = React.useState([]);
58
61
  const handlers = React.useRef(/* @__PURE__ */ new Map()).current;
62
+ const setTitle = React.useCallback(
63
+ (page, title) => setTitles((t) => t[page] === title ? t : { ...t, [page]: title }),
64
+ []
65
+ );
59
66
  React.useEffect(() => setRecents(recentKey ? readRecents(recentKey) : []), [recentKey]);
60
67
  const ctx = {
61
68
  pages,
62
69
  push: (page) => setPages([...pages, page]),
63
70
  pop: () => setPages(pages.slice(0, -1)),
64
71
  titles,
65
- setTitle: (page, title) => setTitles((t) => t[page] === title ? t : { ...t, [page]: title }),
72
+ setTitle,
66
73
  loading,
67
74
  recents,
68
75
  record: (recent) => {
@@ -132,11 +139,12 @@ const Input = React.forwardRef(({ className, value: valueProp, onValueChange, on
132
139
  const pages = palette?.pages ?? [];
133
140
  const [value = "", setValue] = useControllableState({ value: valueProp, defaultValue: "", onChange: onValueChange });
134
141
  const depth = pages.length;
135
- const first = React.useRef(true);
142
+ const shownDepth = React.useRef(depth);
136
143
  React.useEffect(() => {
137
- if (first.current) first.current = false;
138
- else setValue("");
139
- }, [depth]);
144
+ if (shownDepth.current === depth) return;
145
+ shownDepth.current = depth;
146
+ setValue("");
147
+ }, [depth, setValue]);
140
148
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 border-b border-[var(--color-outline-variant)] px-3", children: [
141
149
  /* @__PURE__ */ jsx(Search, { size: "sm", className: "me-1 shrink-0 opacity-50", "aria-hidden": "true" }),
142
150
  pages.map((page) => /* @__PURE__ */ jsx(Badge, { variant: "soft", size: "sm", className: "shrink-0", children: palette?.titles[page] ?? page }, page)),
@@ -271,7 +279,7 @@ function Page({ id, title, children }) {
271
279
  const setTitle = palette?.setTitle;
272
280
  useIsomorphicLayoutEffect(() => {
273
281
  if (id && title) setTitle?.(id, title);
274
- }, [id, title]);
282
+ }, [id, title, setTitle]);
275
283
  const pages = palette?.pages ?? [];
276
284
  const active = id ? pages[pages.length - 1] === id : pages.length === 0;
277
285
  return active ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
@@ -314,6 +314,8 @@ function DataTable({
314
314
  () => withSelectColumn && pinningIn.left?.length && !pinningIn.left.includes("__select") ? { ...pinningIn, left: ["__select", ...pinningIn.left] } : pinningIn,
315
315
  [withSelectColumn, pinningIn]
316
316
  );
317
+ const rowReorderRef = React.useRef(rowReorder);
318
+ rowReorderRef.current = rowReorder;
317
319
  const withRowHandles = React.useCallback(
318
320
  (cols) => reorderRows ? [
319
321
  {
@@ -321,13 +323,11 @@ function DataTable({
321
323
  enableSorting: false,
322
324
  enableHiding: false,
323
325
  header: () => null,
324
- cell: ({ row }) => row.depth === 0 ? rowReorder.handle([0, row.index]) : null
326
+ cell: ({ row }) => row.depth === 0 ? rowReorderRef.current.handle([0, row.index]) : null
325
327
  },
326
328
  ...cols
327
329
  ] : cols,
328
- // rowReorder changes every render; the handle reads fresh data through it.
329
- // eslint-disable-next-line react-hooks/exhaustive-deps
330
- [reorderRows, data]
330
+ [reorderRows]
331
331
  );
332
332
  const allColumns = React.useMemo(
333
333
  () => withRowHandles(withSelectColumn ? [
@@ -436,7 +436,7 @@ function DataTable({
436
436
  if (grid) gridNav.sync(t);
437
437
  });
438
438
  const visibleColumns = table.getVisibleLeafColumns();
439
- const viewButton = /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ms-auto", children: [
439
+ const viewButton = /* @__PURE__ */ jsxs(Button, { variant: "outlined", size: "sm", className: "ms-auto", children: [
440
440
  /* @__PURE__ */ jsx(SlidersHorizontal, { size: "sm", "aria-hidden": "true" }),
441
441
  messages.view
442
442
  ] });
@@ -670,7 +670,7 @@ function DataTable({
670
670
  selectedRows.map((r) => r.original),
671
671
  clear
672
672
  ),
673
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: clear, "aria-label": messages.clearSelection, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
673
+ /* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", onClick: clear, "aria-label": messages.clearSelection, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
674
674
  ]
675
675
  }
676
676
  )
@@ -22,6 +22,9 @@ export declare function useCalendarLocale(localeProp?: Locale): {
22
22
  };
23
23
  export interface DatePickerProps {
24
24
  value?: Date;
25
+ /** Called with the new value. */
26
+ onValueChange?: (date: Date | undefined) => void;
27
+ /** @deprecated 8.5: use `onValueChange` (the name every other Nexub input, and React Native, uses). Removed in 9.0. */
25
28
  onChange?: (date: Date | undefined) => void;
26
29
  placeholder?: string;
27
30
  disabled?: boolean;
@@ -88,6 +88,7 @@ const calendarClassNames = {
88
88
  const DatePicker = React.forwardRef(
89
89
  function DatePicker2({
90
90
  value,
91
+ onValueChange,
91
92
  onChange,
92
93
  placeholder: placeholderProp,
93
94
  disabled = false,
@@ -102,6 +103,10 @@ const DatePicker = React.forwardRef(
102
103
  typeable = false
103
104
  }, ref) {
104
105
  const [open, setOpen] = React.useState(false);
106
+ const change = (v) => {
107
+ onValueChange?.(v);
108
+ onChange?.(v);
109
+ };
105
110
  const messages = useNexubMessages();
106
111
  const placeholder = placeholderProp ?? messages.pickDate;
107
112
  const calendar = useCalendarLocale(localeProp);
@@ -112,11 +117,11 @@ const DatePicker = React.forwardRef(
112
117
  const invalid = typed !== void 0 && (!isValid(typed) || minDate !== void 0 && typed < minDate || maxDate !== void 0 && typed > maxDate || disabledDates !== void 0 && dateMatchModifiers(typed, disabledDates));
113
118
  const commit = () => {
114
119
  if (text === null || invalid) return;
115
- onChange?.(typed);
120
+ change(typed);
116
121
  setText(null);
117
122
  };
118
123
  const pick = (date) => {
119
- onChange?.(date);
124
+ change(date);
120
125
  setText(null);
121
126
  setOpen(false);
122
127
  };
@@ -215,7 +220,7 @@ const DatePicker = React.forwardRef(
215
220
  ...calendar.dayPickerProps
216
221
  }
217
222
  ),
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)) })
223
+ 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: "text", size: "xs", onClick: () => pick(p.date), children: p.label }, p.label)) })
219
224
  ]
220
225
  }
221
226
  ) })
@@ -3,6 +3,9 @@ import type { Locale } from 'date-fns';
3
3
  export interface DateTimePickerProps {
4
4
  /** Date and time in the reader's local time. */
5
5
  value?: Date;
6
+ /** Called with the new value. */
7
+ onValueChange?: (value: Date | undefined) => void;
8
+ /** @deprecated 8.5: use `onValueChange` (the name every other Nexub input, and React Native, uses). Removed in 9.0. */
6
9
  onChange?: (value: Date | undefined) => void;
7
10
  /** Time used when a date is picked first. @default '09:00' */
8
11
  defaultTime?: string;
@@ -17,4 +20,4 @@ export interface DateTimePickerProps {
17
20
  locale?: Locale;
18
21
  }
19
22
  /** A DatePicker and a TimePicker that edit one `Date` together. */
20
- export declare function DateTimePicker({ value, onChange, defaultTime, minuteStep, minDate, maxDate, disabled, timeZone, 'aria-label': ariaLabel, className, locale, }: DateTimePickerProps): React.JSX.Element;
23
+ export declare function DateTimePicker({ value, onValueChange, onChange, defaultTime, minuteStep, minDate, maxDate, disabled, timeZone, 'aria-label': ariaLabel, className, locale, }: DateTimePickerProps): React.JSX.Element;
@@ -15,6 +15,7 @@ const withTime = (d, time) => {
15
15
  };
16
16
  function DateTimePicker({
17
17
  value,
18
+ onValueChange,
18
19
  onChange,
19
20
  defaultTime = "09:00",
20
21
  minuteStep,
@@ -26,6 +27,10 @@ function DateTimePicker({
26
27
  className,
27
28
  locale
28
29
  }) {
30
+ const change = (v) => {
31
+ onValueChange?.(v);
32
+ onChange?.(v);
33
+ };
29
34
  const messages = useNexubMessages();
30
35
  const intlLocale = useIntlLocale();
31
36
  const [pendingTime, setPendingTime] = React.useState(defaultTime);
@@ -37,7 +42,7 @@ function DateTimePicker({
37
42
  {
38
43
  "aria-label": messages.dateLabel,
39
44
  value,
40
- onChange: (date) => onChange?.(date ? withTime(date, time) : void 0),
45
+ onValueChange: (date) => change(date ? withTime(date, time) : void 0),
41
46
  minDate,
42
47
  maxDate,
43
48
  disabled,
@@ -54,7 +59,7 @@ function DateTimePicker({
54
59
  disabled,
55
60
  onValueChange: (next) => {
56
61
  if (!next) return;
57
- if (value) onChange?.(withTime(value, next));
62
+ if (value) change(withTime(value, next));
58
63
  else setPendingTime(next);
59
64
  }
60
65
  }
@@ -82,14 +82,14 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
82
82
  return isFilterGroup(r) ? /* @__PURE__ */ jsx(GroupView, { group: r, fields, depth: depth + 1, maxDepth, onChange: replace, onRemove: remove }, r.id) : /* @__PURE__ */ jsx(RuleRow, { rule: r, fields, onChange: replace, onRemove: remove }, r.id);
83
83
  }),
84
84
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
85
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setRules((rules) => [...rules, newRule()]), disabled: !first, children: [
85
+ /* @__PURE__ */ jsxs(Button, { variant: "text", size: "sm", onClick: () => setRules((rules) => [...rules, newRule()]), disabled: !first, children: [
86
86
  /* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
87
87
  t.addFilter
88
88
  ] }),
89
89
  depth < maxDepth && /* @__PURE__ */ jsxs(
90
90
  Button,
91
91
  {
92
- variant: "ghost",
92
+ variant: "text",
93
93
  size: "sm",
94
94
  onClick: () => setRules((rules) => [...rules, { id: uid(), combinator: "and", rules: [newRule()] }]),
95
95
  disabled: !first,
@@ -53,10 +53,9 @@ const ListTransition = React.forwardRef(function ListTransition2({ as: Tag = "di
53
53
  const exiting = rendered.filter((e) => e.state === "exit").map((e) => e.key).join("|");
54
54
  React.useEffect(() => {
55
55
  if (!exiting) return;
56
- const keys = rendered.filter((e) => e.state === "exit").map((e) => e.key);
57
- const t = setTimeout(() => keys.forEach(remove), EXIT_FALLBACK_MS);
56
+ const t = setTimeout(() => setEntries((list) => list.filter((e) => e.state !== "exit")), EXIT_FALLBACK_MS);
58
57
  return () => clearTimeout(t);
59
- }, [exiting, remove]);
58
+ }, [exiting]);
60
59
  return /* @__PURE__ */ jsx(Tag, { ref, ...rest, children: rendered.map((e) => /* @__PURE__ */ jsx(
61
60
  Item,
62
61
  {
@@ -9,6 +9,9 @@ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInp
9
9
  showSteppers?: boolean;
10
10
  value?: number;
11
11
  defaultValue?: number;
12
+ /** Called with the new value. */
13
+ onValueChange?: (value: number) => void;
14
+ /** @deprecated 8.5: use `onValueChange` (the name every other Nexub input, and React Native, uses). Removed in 9.0. */
12
15
  onChange?: (value: number) => void;
13
16
  invalid?: boolean;
14
17
  }
@@ -26,19 +26,24 @@ const NumberInput = React.forwardRef(
26
26
  showSteppers = false,
27
27
  value,
28
28
  defaultValue = 0,
29
+ onValueChange,
29
30
  onChange,
30
31
  invalid,
31
32
  disabled,
32
33
  ...props
33
34
  }, ref) => {
34
35
  const [internalValue, setInternalValue] = React.useState(defaultValue);
36
+ const change = (v) => {
37
+ onValueChange?.(v);
38
+ onChange?.(v);
39
+ };
35
40
  const messages = useNexubMessages();
36
41
  const controlled = value !== void 0;
37
42
  const currentValue = controlled ? value : internalValue;
38
43
  const commit = (next) => {
39
44
  const clamped = clampValue(next, min, max);
40
45
  if (!controlled) setInternalValue(clamped);
41
- onChange?.(clamped);
46
+ change(clamped);
42
47
  };
43
48
  const handleChange = (e) => {
44
49
  const parsed = parseFloat(e.target.value);
@@ -14,7 +14,7 @@ function SavedViews({ views, activeId, onSelect, onSave, onUpdate, onDelete, cla
14
14
  const active = views.find((v) => v.id === activeId);
15
15
  return /* @__PURE__ */ jsxs(Fragment, { children: [
16
16
  /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
17
- /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className, children: [
17
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outlined", size: "sm", className, children: [
18
18
  /* @__PURE__ */ jsx(Bookmark, { size: "sm", "aria-hidden": "true" }),
19
19
  active?.name ?? t.views,
20
20
  /* @__PURE__ */ jsx(ChevronDown, { size: "sm", "aria-hidden": "true" })
@@ -51,7 +51,7 @@ function SavedViews({ views, activeId, onSelect, onSave, onUpdate, onDelete, cla
51
51
  /* @__PURE__ */ jsx(Dialog.Header, { children: /* @__PURE__ */ jsx(Dialog.Title, { children: t.saveView.replace(/…$/, "") }) }),
52
52
  /* @__PURE__ */ jsx(Input, { "aria-label": t.viewName, placeholder: t.viewName, value: name, onChange: (e) => setName(e.target.value), autoFocus: true }),
53
53
  /* @__PURE__ */ jsxs(Dialog.Footer, { children: [
54
- /* @__PURE__ */ jsx(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", children: t.cancel }) }),
54
+ /* @__PURE__ */ jsx(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "text", children: t.cancel }) }),
55
55
  /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !name.trim(), children: t.save })
56
56
  ] })
57
57
  ]
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
4
5
  import { GripVertical } from "@nexub/icons";
5
6
  import { cn } from "../../utils/cn.js";
6
7
  import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
@@ -152,7 +153,11 @@ function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
152
153
  setDragging(null);
153
154
  setMark(null);
154
155
  };
155
- React.useEffect(() => () => finish(false), []);
156
+ const finishRef = React.useRef(finish);
157
+ useIsomorphicLayoutEffect(() => {
158
+ finishRef.current = finish;
159
+ });
160
+ React.useEffect(() => () => finishRef.current(false), []);
156
161
  const begin = (d) => {
157
162
  d.active = true;
158
163
  d.el = itemOf(d.handle) ?? d.handle;
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export type { CitationProps, CitationSource, CitationMessages, SourceChipProps,
26
26
  export { NexubProvider, useNexubMessages, useLocale, defaultMessages } from './components/NexubProvider/index.js';
27
27
  export type { NexubProviderProps, NexubMessages, Direction as TextDirection } from './components/NexubProvider/index.js';
28
28
  export { Button, buttonVariants } from './components/Button/index.js';
29
- export type { ButtonProps, ButtonStatus, ToneValue } from './components/Button/index.js';
29
+ export type { ButtonProps, ButtonStatus, ButtonLegacyVariant, ToneValue } from './components/Button/index.js';
30
30
  export { ButtonGroup } from './components/ButtonGroup/index.js';
31
31
  export type { ButtonGroupProps } from './components/ButtonGroup/index.js';
32
32
  export { SplitButton } from './components/SplitButton/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexub/react",
3
- "version": "8.3.0",
3
+ "version": "8.5.0",
4
4
  "description": "React component library for Nexub Design System — 90+ accessible, themeable UI components built on Radix UI primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -86,14 +86,14 @@
86
86
  "get-nonce": "^1.0.1",
87
87
  "react-day-picker": "^10.0.2",
88
88
  "tailwind-merge": "^3.7.0",
89
- "@nexub/core": "8.3.0",
90
- "@nexub/icons": "8.3.0"
89
+ "@nexub/core": "8.5.0",
90
+ "@nexub/icons": "8.5.0"
91
91
  },
92
92
  "peerDependencies": {
93
93
  "react": ">=19 <20",
94
94
  "react-dom": ">=19 <20",
95
95
  "@tanstack/react-virtual": "^3.14.22",
96
- "@nexub/tokens": "^8.3.0"
96
+ "@nexub/tokens": "^8.5.0"
97
97
  },
98
98
  "peerDependenciesMeta": {
99
99
  "@tanstack/react-virtual": {
@@ -126,8 +126,8 @@
126
126
  "vaul": "1.1.2",
127
127
  "vite": "^8.3.5",
128
128
  "vitest": "^5.0.5",
129
- "@nexub/themes": "8.3.0",
130
- "@nexub/tokens": "8.3.0"
129
+ "@nexub/themes": "8.5.0",
130
+ "@nexub/tokens": "8.5.0"
131
131
  },
132
132
  "scripts": {
133
133
  "build": "rm -rf dist && tsup && tsc -p tsconfig.build.json && rm -f dist/vendor/*.d.ts && node scripts/check-dts.mjs && tailwindcss -i src/styles.css -o dist/styles.css --minify && cp -R ../themes/src/fonts dist/fonts",