@nexub/react 7.6.0 → 7.7.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": "7.6.0",
2
+ "version": "7.7.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.",
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState, type ColumnSizingState, type ColumnOrderState, type ColumnPinningState, type ExpandedState } from '@tanstack/react-table';
2
+ import { type ColumnDef, type SortingState, type RowSelectionState, type RowData, type VisibilityState, type ColumnSizingState, type ColumnOrderState, type ColumnPinningState, type ExpandedState } from '@tanstack/react-table';
3
3
  declare module '@tanstack/react-table' {
4
4
  interface ColumnMeta<TData extends RowData, TValue> {
5
5
  /** Horizontal alignment of the header and cells. Use 'end' for numbers. @default 'start' */
@@ -75,6 +75,20 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
75
75
  pagination?: {
76
76
  pageSize?: number;
77
77
  };
78
+ /** Controlled sort (with `enableSorting`): `[{ id: 'name', desc: false }]`. */
79
+ sort?: SortingState;
80
+ onSortChange?: (sort: SortingState) => void;
81
+ /** Rows arrive sorted (server-side): headers still toggle `sort`, but rows aren't reordered. */
82
+ manualSort?: boolean;
83
+ /** Controlled page, zero-based. Turns pagination on. */
84
+ page?: number;
85
+ /** Rows per page; overrides `pagination.pageSize`. Turns pagination on. @default 20 */
86
+ pageSize?: number;
87
+ onPageChange?: (page: number) => void;
88
+ /** `data` is the current page only (server-side); set `rowCount` for the page count. Turns pagination on. */
89
+ manualPagination?: boolean;
90
+ /** Total rows across all pages, with `manualPagination`. */
91
+ rowCount?: number;
78
92
  /**
79
93
  * Nested rows (a tree table): return a row's children. The first column gets
80
94
  * an expand toggle and indents each level. Sorting sorts within each level.
@@ -96,4 +110,4 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
96
110
  /** Shown in a full-width row when there are no rows. @default messages.noResults */
97
111
  emptyState?: React.ReactNode;
98
112
  }
99
- export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, getSubRows, expanded: expandedProp, defaultExpanded, onExpandedChange, onRowOrderChange, getRowLabel, loading, emptyState, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
113
+ export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination: paginationProp, sort, onSortChange, manualSort, page, pageSize: pageSizeProp, onPageChange, manualPagination, rowCount, getSubRows, expanded: expandedProp, defaultExpanded, onExpandedChange, onRowOrderChange, getRowLabel, loading, emptyState, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
@@ -20,7 +20,6 @@ import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
20
20
  import { Table } from "../Table/Table.js";
21
21
  import { Checkbox } from "../Checkbox/Checkbox.js";
22
22
  import { Button } from "../Button/Button.js";
23
- import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
24
23
  import { createGridNavigation } from "./gridNavigation.js";
25
24
  import { useReorder } from "../Sortable/reorder.js";
26
25
  import { Skeleton } from "../Skeleton/Skeleton.js";
@@ -134,6 +133,67 @@ function TreeCell({
134
133
  ] });
135
134
  }
136
135
  const px = (n) => typeof n === "number" ? `${n}px` : n;
136
+ const ViewOptions = React.lazy(() => import("./ViewOptions.js").then((m) => ({ default: m.ViewOptions })));
137
+ const placeCell = (column, resizable) => (el) => {
138
+ if (!el) return;
139
+ el.style.width = resizable ? px(column.getSize()) : "";
140
+ const side = column.getIsPinned();
141
+ el.style.insetInlineStart = side === "left" ? px(column.getStart("left")) : "";
142
+ el.style.insetInlineEnd = side === "right" ? px(column.getAfter("right")) : "";
143
+ };
144
+ const pinClass = (column) => {
145
+ const side = column.getIsPinned();
146
+ if (!side) return void 0;
147
+ return cn(
148
+ "sticky z-[1] bg-[var(--color-surface)]",
149
+ "[tr:hover>&]:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,var(--color-surface))]",
150
+ "[tr[data-state=selected]>&]:bg-[color-mix(in_srgb,var(--color-primary)_10%,var(--color-surface))]",
151
+ side === "left" && column.getIsLastColumn("left") && "border-e border-[var(--color-outline-variant)]",
152
+ side === "right" && column.getIsFirstColumn("right") && "border-s border-[var(--color-outline-variant)]"
153
+ );
154
+ };
155
+ function RowViewImpl({
156
+ row,
157
+ index,
158
+ selected,
159
+ compact,
160
+ pinned,
161
+ resizable,
162
+ selectable,
163
+ tree,
164
+ virtual,
165
+ measure,
166
+ expandLabel,
167
+ collapseLabel,
168
+ dragProps
169
+ }) {
170
+ return /* @__PURE__ */ jsx(
171
+ TableRow,
172
+ {
173
+ ref: measure,
174
+ "data-index": index,
175
+ "aria-rowindex": virtual ? index + 2 : void 0,
176
+ "data-state": selectable && selected ? "selected" : void 0,
177
+ ...dragProps,
178
+ children: row.getVisibleCells().map((cell, _i, cells) => /* @__PURE__ */ jsx(
179
+ TableCell,
180
+ {
181
+ ref: pinned || resizable ? placeCell(cell.column, resizable) : void 0,
182
+ className: cn(
183
+ alignText[cell.column.columnDef.meta?.align ?? "start"],
184
+ compact && "py-1.5",
185
+ pinClass(cell.column),
186
+ resizable && "truncate"
187
+ ),
188
+ children: tree && cell === cells.find((c) => c.column.id !== "__select") ? /* @__PURE__ */ jsx(TreeCell, { row, expandLabel, collapseLabel, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }) : flexRender(cell.column.columnDef.cell, cell.getContext())
189
+ },
190
+ cell.id
191
+ ))
192
+ }
193
+ );
194
+ }
195
+ const sameRow = (a, b) => Object.keys(a).every((k) => k === "row" || a[k] === b[k]) && a.row.id === b.row.id && a.row.original === b.row.original && a.row.index === b.row.index && a.row.depth === b.row.depth;
196
+ const RowView = React.memo(RowViewImpl, sameRow);
137
197
  const apply = (u, prev) => typeof u === "function" ? u(prev) : u;
138
198
  const columnLabel = (c) => typeof c.columnDef.header === "string" ? c.columnDef.header : c.id;
139
199
  function DataTable({
@@ -164,7 +224,15 @@ function DataTable({
164
224
  maxHeight,
165
225
  grid = false,
166
226
  rowWindow,
167
- pagination,
227
+ pagination: paginationProp,
228
+ sort,
229
+ onSortChange,
230
+ manualSort = false,
231
+ page,
232
+ pageSize: pageSizeProp,
233
+ onPageChange,
234
+ manualPagination = false,
235
+ rowCount,
168
236
  getSubRows,
169
237
  expanded: expandedProp,
170
238
  defaultExpanded,
@@ -182,7 +250,11 @@ function DataTable({
182
250
  }) {
183
251
  const messages = useNexubMessages();
184
252
  const { locale, dir } = useLocale();
185
- const [sorting, setSorting] = React.useState([]);
253
+ const [sorting = [], setSorting] = useControllableState({
254
+ value: sort,
255
+ defaultValue: [],
256
+ onChange: onSortChange
257
+ });
186
258
  const [rowSelection = {}, setRowSelection] = useControllableState({
187
259
  value: rowSelectionProp,
188
260
  defaultValue: {},
@@ -222,10 +294,14 @@ function DataTable({
222
294
  const tableRef = React.useRef(null);
223
295
  const gridNav = React.useMemo(() => createGridNavigation(dir), [dir]);
224
296
  const dragged = React.useRef(null);
225
- const [paginationState, setPaginationState] = React.useState({
226
- pageIndex: 0,
227
- pageSize: pagination?.pageSize ?? 20
297
+ const pagination = !!paginationProp || page !== void 0 || pageSizeProp !== void 0 || manualPagination;
298
+ const [pageIndex = 0, setPageIndex] = useControllableState({
299
+ value: page,
300
+ defaultValue: 0,
301
+ onChange: onPageChange
228
302
  });
303
+ const pageSize = pageSizeProp ?? paginationProp?.pageSize ?? 20;
304
+ const paginationState = React.useMemo(() => ({ pageIndex, pageSize }), [pageIndex, pageSize]);
229
305
  const withSelectColumn = enableRowSelection && selectionColumn;
230
306
  const reorderRows = Boolean(onRowOrderChange) && sorting.length === 0;
231
307
  const rowReorder = useReorder({
@@ -312,7 +388,8 @@ function DataTable({
312
388
  columns: allColumns,
313
389
  state: tableState,
314
390
  getRowId,
315
- onSortingChange: enableSorting ? setSorting : void 0,
391
+ onSortingChange: enableSorting ? (u) => setSorting(apply(u, sorting)) : void 0,
392
+ manualSorting: manualSort,
316
393
  onRowSelectionChange: enableRowSelection ? (u) => setRowSelection(apply(u, rowSelection)) : void 0,
317
394
  onColumnVisibilityChange: (u) => setColumnVisibility(apply(u, columnVisibility)),
318
395
  onColumnSizingChange: (u) => setColumnSizing(apply(u, columnSizing)),
@@ -321,11 +398,15 @@ function DataTable({
321
398
  enableColumnResizing,
322
399
  columnResizeMode: "onChange",
323
400
  columnResizeDirection: dir,
324
- onPaginationChange: pagination ? setPaginationState : void 0,
401
+ onPaginationChange: pagination ? (u) => {
402
+ const next = apply(u, paginationState).pageIndex;
403
+ if (next !== pageIndex) setPageIndex(next);
404
+ } : void 0,
325
405
  getCoreRowModel: getCoreRowModel(),
326
- getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
406
+ getSortedRowModel: enableSorting && !manualSort ? getSortedRowModel() : void 0,
327
407
  getFilteredRowModel: getFilteredRowModel(),
328
- getPaginationRowModel: pagination ? getPaginationRowModel() : void 0,
408
+ getPaginationRowModel: pagination && !manualPagination ? getPaginationRowModel() : void 0,
409
+ rowCount,
329
410
  getSubRows,
330
411
  getExpandedRowModel: getSubRows ? getExpandedRowModel() : void 0,
331
412
  onExpandedChange: getSubRows ? (u) => setExpanded(apply(u, expanded)) : void 0,
@@ -333,7 +414,7 @@ function DataTable({
333
414
  // (TanStack then expands inside the page model; without pages it must not).
334
415
  paginateExpandedRows: !pagination,
335
416
  enableRowSelection,
336
- manualPagination: false
417
+ manualPagination
337
418
  });
338
419
  const compact = density === "compact";
339
420
  const selectedRows = enableRowSelection ? table.getSelectedRowModel().rows : [];
@@ -354,24 +435,11 @@ function DataTable({
354
435
  if (scroller) scroller.style.maxHeight = maxHeight == null ? "" : px(maxHeight);
355
436
  if (grid) gridNav.sync(t);
356
437
  });
357
- const placeCell = (column) => (el) => {
358
- if (!el) return;
359
- el.style.width = enableColumnResizing ? px(column.getSize()) : "";
360
- const side = column.getIsPinned();
361
- el.style.insetInlineStart = side === "left" ? px(column.getStart("left")) : "";
362
- el.style.insetInlineEnd = side === "right" ? px(column.getAfter("right")) : "";
363
- };
364
- const pinClass = (column) => {
365
- const side = column.getIsPinned();
366
- if (!side) return void 0;
367
- return cn(
368
- "sticky z-[1] bg-[var(--color-surface)]",
369
- "[tr:hover>&]:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,var(--color-surface))]",
370
- "[tr[data-state=selected]>&]:bg-[color-mix(in_srgb,var(--color-primary)_10%,var(--color-surface))]",
371
- side === "left" && column.getIsLastColumn("left") && "border-e border-[var(--color-outline-variant)]",
372
- side === "right" && column.getIsFirstColumn("right") && "border-s border-[var(--color-outline-variant)]"
373
- );
374
- };
438
+ const visibleColumns = table.getVisibleLeafColumns();
439
+ const viewButton = /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ms-auto", children: [
440
+ /* @__PURE__ */ jsx(SlidersHorizontal, { size: "sm", "aria-hidden": "true" }),
441
+ messages.view
442
+ ] });
375
443
  const moveColumn = (id, to) => {
376
444
  const ids = table.getAllLeafColumns().map((c) => c.id);
377
445
  const order = (columnOrder.length ? columnOrder : ids).filter((c) => c !== "__select" && ids.includes(c));
@@ -390,31 +458,21 @@ function DataTable({
390
458
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-density": density, ...props, children: [
391
459
  (toolbar != null || viewOptions) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
392
460
  toolbar,
393
- viewOptions && /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
394
- /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ms-auto", children: [
395
- /* @__PURE__ */ jsx(SlidersHorizontal, { size: "sm", "aria-hidden": "true" }),
396
- messages.view
397
- ] }) }),
398
- /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
399
- /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.density }),
400
- /* @__PURE__ */ jsxs(DropdownMenu.RadioGroup, { value: density, onValueChange: (v) => setDensity(v), children: [
401
- /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "comfortable", children: messages.comfortable }),
402
- /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "compact", children: messages.compact })
403
- ] }),
404
- /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
405
- /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.columns }),
406
- table.getAllLeafColumns().filter((c) => c.getCanHide()).map((c) => /* @__PURE__ */ jsx(
407
- DropdownMenu.CheckboxItem,
408
- {
409
- checked: c.getIsVisible(),
410
- onCheckedChange: (v) => c.toggleVisibility(v === true),
411
- onSelect: (e) => e.preventDefault(),
412
- children: columnLabel(c)
413
- },
414
- c.id
415
- ))
416
- ] })
417
- ] })
461
+ viewOptions && // The plain button stands in (same size, no layout shift) until the menu loads.
462
+ /* @__PURE__ */ jsx(React.Suspense, { fallback: viewButton, children: /* @__PURE__ */ jsx(
463
+ ViewOptions,
464
+ {
465
+ trigger: viewButton,
466
+ density,
467
+ onDensityChange: setDensity,
468
+ columns: table.getAllLeafColumns().filter((c) => c.getCanHide()).map((c) => ({
469
+ id: c.id,
470
+ label: columnLabel(c),
471
+ visible: c.getIsVisible(),
472
+ toggle: (v) => c.toggleVisibility(v)
473
+ }))
474
+ }
475
+ ) })
418
476
  ] }),
419
477
  offerAll && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 rounded-shape-container bg-[var(--color-surface-container-low)] px-4 py-2 text-body-sm", children: table.getIsAllRowsSelected() ? /* @__PURE__ */ jsxs(Fragment, { children: [
420
478
  /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(totalRows)) }),
@@ -447,7 +505,7 @@ function DataTable({
447
505
  return /* @__PURE__ */ jsxs(
448
506
  TableHead,
449
507
  {
450
- ref: placeCell(col),
508
+ ref: placeCell(col, enableColumnResizing),
451
509
  className: cn(
452
510
  alignText[align],
453
511
  compact && "h-8",
@@ -532,32 +590,29 @@ function DataTable({
532
590
  header.id
533
591
  );
534
592
  }) }, headerGroup.id)) }),
535
- /* @__PURE__ */ jsx(TableBody, { children: loading ? Array.from({ length: pagination?.pageSize ?? 5 }, (_, i) => /* @__PURE__ */ jsx(TableRow, { "data-loading": "", className: "hover:bg-transparent", children: table.getVisibleLeafColumns().map((c) => /* @__PURE__ */ jsx(TableCell, { className: cn(compact && "py-1.5"), children: /* @__PURE__ */ jsx(Skeleton, {}) }, c.id)) }, i)) : rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
593
+ /* @__PURE__ */ jsx(TableBody, { children: loading ? Array.from({ length: pagination ? pageSize : 5 }, (_, i) => /* @__PURE__ */ jsx(TableRow, { "data-loading": "", className: "hover:bg-transparent", children: table.getVisibleLeafColumns().map((c) => /* @__PURE__ */ jsx(TableCell, { className: cn(compact && "py-1.5"), children: /* @__PURE__ */ jsx(Skeleton, {}) }, c.id)) }, i)) : rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
536
594
  rowWin.before > 0 && /* @__PURE__ */ jsx(Spacer, { height: rowWin.before }),
537
595
  rowWin.indexes.map((i) => {
538
596
  const row = rows[i];
539
597
  return /* @__PURE__ */ jsx(
540
- TableRow,
598
+ RowView,
541
599
  {
542
- ref: rowWin.measure,
543
- "data-index": i,
544
- "aria-rowindex": rowWin.virtual ? i + 2 : void 0,
545
- "data-state": enableRowSelection && row.getIsSelected() ? "selected" : void 0,
546
- ...reorderRows && row.depth === 0 ? rowReorder.itemProps([0, row.index]) : {},
547
- children: row.getVisibleCells().map((cell, _i, cells) => /* @__PURE__ */ jsx(
548
- TableCell,
549
- {
550
- ref: pinned || enableColumnResizing ? placeCell(cell.column) : void 0,
551
- className: cn(
552
- alignText[cell.column.columnDef.meta?.align ?? "start"],
553
- compact && "py-1.5",
554
- pinClass(cell.column),
555
- enableColumnResizing && "truncate"
556
- ),
557
- children: getSubRows && cell === cells.find((c) => c.column.id !== "__select") ? /* @__PURE__ */ jsx(TreeCell, { row, expandLabel: messages.expandRow, collapseLabel: messages.collapseRow, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }) : flexRender(cell.column.columnDef.cell, cell.getContext())
558
- },
559
- cell.id
560
- ))
600
+ row,
601
+ index: i,
602
+ selected: enableRowSelection && row.getIsSelected(),
603
+ expanded: !!getSubRows && row.getIsExpanded(),
604
+ columns: visibleColumns,
605
+ columnSizing,
606
+ compact,
607
+ pinned,
608
+ resizable: enableColumnResizing,
609
+ selectable: enableRowSelection,
610
+ tree: !!getSubRows,
611
+ virtual: rowWin.virtual,
612
+ measure: rowWin.measure,
613
+ expandLabel: messages.expandRow,
614
+ collapseLabel: messages.collapseRow,
615
+ dragProps: reorderRows && row.depth === 0 ? rowReorder.itemProps([0, row.index]) : void 0
561
616
  },
562
617
  row.id
563
618
  );
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import type { DataTableDensity } from './DataTable.js';
3
+ export interface ViewOptionsProps {
4
+ trigger: React.ReactElement;
5
+ density: DataTableDensity;
6
+ onDensityChange: (density: DataTableDensity) => void;
7
+ /** Hideable columns. */
8
+ columns: {
9
+ id: string;
10
+ label: string;
11
+ visible: boolean;
12
+ toggle: (visible: boolean) => void;
13
+ }[];
14
+ }
15
+ export declare function ViewOptions({ trigger, density, onDensityChange, columns }: ViewOptionsProps): React.JSX.Element;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
5
+ import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
6
+ function ViewOptions({ trigger, density, onDensityChange, columns }) {
7
+ const messages = useNexubMessages();
8
+ return /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
9
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: trigger }),
10
+ /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
11
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.density }),
12
+ /* @__PURE__ */ jsxs(DropdownMenu.RadioGroup, { value: density, onValueChange: (v) => onDensityChange(v), children: [
13
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "comfortable", children: messages.comfortable }),
14
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "compact", children: messages.compact })
15
+ ] }),
16
+ /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
17
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.columns }),
18
+ columns.map((c) => /* @__PURE__ */ jsx(
19
+ DropdownMenu.CheckboxItem,
20
+ {
21
+ checked: c.visible,
22
+ onCheckedChange: (v) => c.toggle(v === true),
23
+ onSelect: (e) => e.preventDefault(),
24
+ children: c.label
25
+ },
26
+ c.id
27
+ ))
28
+ ] })
29
+ ] });
30
+ }
31
+ export {
32
+ ViewOptions
33
+ };
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { type DataTableProps } from '../DataTable/DataTable.js';
3
- export interface DataTableVirtualProps<TData> extends Omit<DataTableProps<TData>, 'pagination' | 'rowWindow'> {
3
+ export interface DataTableVirtualProps<TData> extends Omit<DataTableProps<TData>, 'pagination' | 'rowWindow' | 'page' | 'pageSize' | 'onPageChange' | 'manualPagination' | 'rowCount'> {
4
4
  /** Height of the scrolling area. Required: rows are windowed within it. */
5
5
  maxHeight: number | string;
6
6
  /** Row height before measuring, in px. Default 49, or 37 when compact. */
@@ -12,6 +12,7 @@ import {
12
12
  } from "@nexub/core";
13
13
  import { cn } from "../../utils/cn.js";
14
14
  import { useControllableState } from "../../hooks/useControllableState.js";
15
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
15
16
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
16
17
  import { Button } from "../Button/Button.js";
17
18
  import { IconButton } from "../IconButton/IconButton.js";
@@ -24,14 +25,20 @@ function FilterBuilder({ fields, value: valueProp, defaultValue, onValueChange,
24
25
  defaultValue: defaultValue ?? emptyFilter(),
25
26
  onChange: onValueChange
26
27
  });
28
+ const latest = React.useRef(value);
29
+ useIsomorphicLayoutEffect(() => {
30
+ latest.current = value;
31
+ });
32
+ const update = React.useCallback((fn) => setValue(fn(latest.current)), [setValue]);
33
+ const deferred = React.useDeferredValue(value);
27
34
  return /* @__PURE__ */ jsx(
28
- GroupView,
35
+ MemoGroupView,
29
36
  {
30
- group: value,
37
+ group: deferred,
31
38
  fields,
32
39
  depth: 0,
33
40
  maxDepth,
34
- onChange: setValue,
41
+ onChange: update,
35
42
  className
36
43
  }
37
44
  );
@@ -40,7 +47,7 @@ FilterBuilder.displayName = "FilterBuilder";
40
47
  function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, className }) {
41
48
  const t = useNexubMessages();
42
49
  const first = fields[0];
43
- const setRules = (rules) => onChange({ ...group, rules });
50
+ const setRules = (fn) => onChange((g) => ({ ...g, rules: fn(g.rules) }));
44
51
  const newRule = () => newFilterRule(first);
45
52
  return /* @__PURE__ */ jsxs(
46
53
  "div",
@@ -60,7 +67,7 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
60
67
  size: "sm",
61
68
  "aria-label": t.filterGroup(group.combinator),
62
69
  value: group.combinator,
63
- onValueChange: (v) => v && onChange({ ...group, combinator: v }),
70
+ onValueChange: (v) => v && onChange((g) => ({ ...g, combinator: v })),
64
71
  children: [
65
72
  /* @__PURE__ */ jsx(SegmentedControl.Item, { value: "and", children: t.matchAll }),
66
73
  /* @__PURE__ */ jsx(SegmentedControl.Item, { value: "or", children: t.matchAny })
@@ -69,13 +76,13 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
69
76
  ),
70
77
  onRemove && /* @__PURE__ */ jsx(IconButton, { label: t.removeGroup, variant: "ghost", size: "sm", className: "ms-auto", onClick: onRemove, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
71
78
  ] }),
72
- group.rules.map((r, i) => {
73
- const replace = (next) => setRules(group.rules.map((x, j) => j === i ? next : x));
74
- const remove = () => setRules(group.rules.filter((_, j) => j !== i));
79
+ group.rules.map((r) => {
80
+ const replace = (fn) => setRules((rules) => rules.map((x) => x.id === r.id ? fn(x) : x));
81
+ const remove = () => setRules((rules) => rules.filter((x) => x.id !== r.id));
75
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);
76
83
  }),
77
84
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
78
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setRules([...group.rules, newRule()]), disabled: !first, children: [
85
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setRules((rules) => [...rules, newRule()]), disabled: !first, children: [
79
86
  /* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
80
87
  t.addFilter
81
88
  ] }),
@@ -84,7 +91,7 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
84
91
  {
85
92
  variant: "ghost",
86
93
  size: "sm",
87
- onClick: () => setRules([...group.rules, { id: uid(), combinator: "and", rules: [newRule()] }]),
94
+ onClick: () => setRules((rules) => [...rules, { id: uid(), combinator: "and", rules: [newRule()] }]),
88
95
  disabled: !first,
89
96
  children: [
90
97
  /* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
@@ -97,6 +104,7 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
97
104
  }
98
105
  );
99
106
  }
107
+ const MemoGroupView = React.memo(GroupView);
100
108
  function RuleRow({
101
109
  rule,
102
110
  fields,
@@ -104,6 +112,12 @@ function RuleRow({
104
112
  onRemove
105
113
  }) {
106
114
  const t = useNexubMessages();
115
+ const [draft, setDraft] = React.useState(rule.value);
116
+ const [seen, setSeen] = React.useState(rule.value);
117
+ if (rule.value !== seen) {
118
+ setSeen(rule.value);
119
+ setDraft(rule.value);
120
+ }
107
121
  const field = fields.find((f) => f.id === rule.field) ?? fields[0];
108
122
  const ops = filterOperators[field?.type ?? "text"];
109
123
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
@@ -114,7 +128,7 @@ function RuleRow({
114
128
  onValueChange: (id) => {
115
129
  const f = fields.find((x) => x.id === id);
116
130
  const nextOps = filterOperators[f?.type ?? "text"];
117
- onChange({ ...rule, field: id, operator: nextOps.includes(rule.operator) ? rule.operator : nextOps[0], value: "" });
131
+ onChange((r) => ({ ...r, field: id, operator: nextOps.includes(r.operator) ? r.operator : nextOps[0], value: "" }));
118
132
  },
119
133
  children: [
120
134
  /* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterField, className: "w-auto min-w-36", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
@@ -122,11 +136,11 @@ function RuleRow({
122
136
  ]
123
137
  }
124
138
  ),
125
- /* @__PURE__ */ jsxs(Select.Root, { value: rule.operator, onValueChange: (op) => onChange({ ...rule, operator: op }), children: [
139
+ /* @__PURE__ */ jsxs(Select.Root, { value: rule.operator, onValueChange: (op) => onChange((r) => ({ ...r, operator: op })), children: [
126
140
  /* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterOperator, className: "w-auto min-w-32", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
127
141
  /* @__PURE__ */ jsx(Select.Content, { children: ops.map((op) => /* @__PURE__ */ jsx(Select.Item, { value: op, children: t.filterOperators[op] }, op)) })
128
142
  ] }),
129
- rule.operator !== "isEmpty" && (field?.type === "select" ? /* @__PURE__ */ jsxs(Select.Root, { value: rule.value || void 0, onValueChange: (v) => onChange({ ...rule, value: v }), children: [
143
+ rule.operator !== "isEmpty" && (field?.type === "select" ? /* @__PURE__ */ jsxs(Select.Root, { value: rule.value || void 0, onValueChange: (v) => onChange((r) => ({ ...r, value: v })), children: [
130
144
  /* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterValue, className: "w-auto min-w-36", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
131
145
  /* @__PURE__ */ jsx(Select.Content, { children: field.options?.map((o) => /* @__PURE__ */ jsx(Select.Item, { value: o.value, children: o.label }, o.value)) })
132
146
  ] }) : /* @__PURE__ */ jsx(
@@ -134,8 +148,12 @@ function RuleRow({
134
148
  {
135
149
  "aria-label": t.filterValue,
136
150
  type: field?.type === "number" ? "number" : field?.type === "date" ? "date" : "text",
137
- value: rule.value,
138
- onChange: (e) => onChange({ ...rule, value: e.target.value }),
151
+ value: draft,
152
+ onChange: (e) => {
153
+ const v = e.target.value;
154
+ setDraft(v);
155
+ onChange((r) => ({ ...r, value: v }));
156
+ },
139
157
  wrapperClassName: "w-auto min-w-36 flex-1"
140
158
  }
141
159
  )),
@@ -14,7 +14,7 @@ export interface KanbanProps<T> {
14
14
  className?: string;
15
15
  }
16
16
  /**
17
- * Columns of cards people can move: drag a card, or focus its handle and press
17
+ * Columns of cards people can move: drag a card's handle (Escape cancels), or focus it and press
18
18
  * Alt+Arrow keys (up/down within a column, left/right across columns).
19
19
  */
20
20
  export declare function Kanban<T>({ columns, getId, getLabel, onChange, renderCard, className }: KanbanProps<T>): React.JSX.Element;
@@ -12,7 +12,7 @@ export interface SortableProps<T> {
12
12
  className?: string;
13
13
  }
14
14
  /**
15
- * A list people can reorder: drag an item, or focus its handle and press
16
- * Alt+ArrowUp / Alt+ArrowDown. Each move is announced.
15
+ * A list people can reorder: drag an item's handle (mouse, pen or touch; Escape
16
+ * cancels), or focus the handle and press Alt+ArrowUp / Alt+ArrowDown. Each move is announced.
17
17
  */
18
18
  export declare function Sortable<T>({ items, getId, getLabel, onReorder, children, className, ...aria }: SortableProps<T>): React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- /** Internal: lists of items, moved by drag (native) or Alt+Arrow on a handle. */
2
+ /** Internal: lists of items, moved by dragging a handle (Pointer Events) or Alt+Arrow on it. */
3
3
  export type Pos = [list: number, index: number];
4
4
  export declare function moveItem<T>(lists: T[][], [fc, fi]: Pos, [tc, ti]: Pos): T[][];
5
5
  interface ReorderOptions<T> {
@@ -10,18 +10,17 @@ interface ReorderOptions<T> {
10
10
  listLabels?: string[];
11
11
  onMove: (next: T[][]) => void;
12
12
  }
13
- /** Shared engine: drag state, keyboard moves, focus after a move, announcements. */
13
+ type ItemProps = {
14
+ 'data-nx-reorder-item': '';
15
+ 'data-nx-dragging'?: '';
16
+ 'data-nx-drop'?: 'before' | 'after';
17
+ };
18
+ /** Shared engine: pointer drag, keyboard moves, focus after a move, FLIP settle, announcements. */
14
19
  export declare function useReorder<T>({ lists, getId, getLabel, listLabels, onMove }: ReorderOptions<T>): {
15
- itemProps: (pos: Pos) => {
16
- draggable: boolean;
17
- onDragStart: (e: React.DragEvent) => void;
18
- onDragOver: (e: React.DragEvent) => void;
19
- onDrop: (e: React.DragEvent) => void;
20
- onDragEnd: () => void;
21
- };
20
+ itemProps: (pos: Pos) => ItemProps;
22
21
  listProps: (list: number) => {
23
- onDragOver: (e: React.DragEvent) => void;
24
- onDrop: (e: React.DragEvent) => void;
22
+ ref: (el: HTMLElement | null) => void;
23
+ 'data-nx-drop': string | undefined;
25
24
  };
26
25
  handle: (pos: Pos, className?: string) => React.JSX.Element;
27
26
  live: React.JSX.Element;