@nexub/react 8.3.0 → 8.4.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.4.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
  )
@@ -215,7 +215,7 @@ const DatePicker = React.forwardRef(
215
215
  ...calendar.dayPickerProps
216
216
  }
217
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)) })
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: "text", size: "xs", onClick: () => pick(p.date), children: p.label }, p.label)) })
219
219
  ]
220
220
  }
221
221
  ) })
@@ -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
  {
@@ -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.4.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.4.0",
90
+ "@nexub/icons": "8.4.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.4.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.4.0",
130
+ "@nexub/tokens": "8.4.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",