@nexub/react 5.1.0 → 5.3.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": "5.1.0",
2
+ "version": "5.3.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 on every pull request; a new violation fails the build.",
@@ -693,6 +693,23 @@
693
693
  "keyboard": [],
694
694
  "screenReader": []
695
695
  },
696
+ {
697
+ "slug": "multi-select",
698
+ "name": "Multi Select",
699
+ "package": "@nexub/react",
700
+ "automated": "pass",
701
+ "exceptions": [],
702
+ "keyboard": [
703
+ "Type: Filters the options and opens the list.",
704
+ "ArrowDown / ArrowUp: Moves through the options.",
705
+ "Enter: Toggles the highlighted option.",
706
+ "Backspace: In an empty field, removes the last chip.",
707
+ "Escape: Closes the list."
708
+ ],
709
+ "screenReader": [
710
+ "Options carry `aria-checked`; each chip has a \"Remove <label>\" button."
711
+ ]
712
+ },
696
713
  {
697
714
  "slug": "navigation-rail",
698
715
  "name": "Navigation Rail",
@@ -17,9 +17,18 @@ export interface AutocompleteProps {
17
17
  * filtered (fetched for this text), so the built-in filtering is off.
18
18
  */
19
19
  onInputChange?: (text: string) => void;
20
+ /**
21
+ * `'inline'` (default): the list always shows under the input.
22
+ * `'popover'`: a field whose list opens below it while typing.
23
+ */
24
+ mode?: 'inline' | 'popover';
25
+ /** Adds a "Create “text”" option when the text matches no option exactly. */
26
+ onCreate?: (text: string) => void;
27
+ disabled?: boolean;
20
28
  className?: string;
29
+ 'aria-label'?: string;
21
30
  }
22
- export declare function Autocomplete({ options, value, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, className, }: AutocompleteProps): React.JSX.Element;
31
+ export declare function Autocomplete({ options, value, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, mode, onCreate, disabled, className, 'aria-label': ariaLabel, }: AutocompleteProps): React.JSX.Element;
23
32
  export declare namespace Autocomplete {
24
33
  var displayName: string;
25
34
  }
@@ -1,8 +1,11 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { Plus } from "@nexub/icons";
5
+ import { cn } from "../../utils/cn.js";
4
6
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
5
7
  import { Combobox } from "../Combobox/Combobox.js";
8
+ import { ComboboxLoading, ComboboxPopover } from "../Combobox/ComboboxPopover.js";
6
9
  function Autocomplete({
7
10
  options,
8
11
  value,
@@ -12,39 +15,84 @@ function Autocomplete({
12
15
  loading = false,
13
16
  inputValue,
14
17
  onInputChange,
15
- className
18
+ mode = "inline",
19
+ onCreate,
20
+ disabled,
21
+ className,
22
+ "aria-label": ariaLabel
16
23
  }) {
17
24
  const messages = useNexubMessages();
18
25
  const selectedLabel = options.find((o) => o.value === value)?.label ?? "";
19
26
  const [innerSearch, setInnerSearch] = React.useState(selectedLabel);
20
27
  const [prevValue, setPrevValue] = React.useState(value);
28
+ const [open, setOpen] = React.useState(false);
21
29
  const search = inputValue ?? innerSearch;
22
- const setSearch = (text) => {
23
- setInnerSearch(text);
24
- onInputChange?.(text);
30
+ const setSearch = (text2) => {
31
+ setInnerSearch(text2);
32
+ onInputChange?.(text2);
25
33
  };
26
34
  if (value !== prevValue) {
27
35
  setPrevValue(value);
28
36
  setInnerSearch(selectedLabel);
29
37
  }
30
- return /* @__PURE__ */ jsxs(Combobox.Root, { className, shouldFilter: !onInputChange, children: [
31
- /* @__PURE__ */ jsx(Combobox.Input, { placeholder: placeholder ?? messages.searchPlaceholder, value: search, onValueChange: setSearch }),
32
- /* @__PURE__ */ jsx(Combobox.List, { children: loading ? /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-sm text-[var(--color-on-surface-muted)]", children: messages.loading }) : /* @__PURE__ */ jsxs(Fragment, { children: [
33
- /* @__PURE__ */ jsx(Combobox.Empty, { children: emptyText ?? messages.noResults }),
34
- options.map((opt) => /* @__PURE__ */ jsx(
35
- Combobox.Item,
36
- {
37
- value: opt.label,
38
- keywords: [opt.value],
39
- onSelect: () => {
40
- onValueChange?.(opt.value);
41
- setSearch(opt.label);
42
- },
43
- children: opt.label
38
+ const text = search.trim();
39
+ const canCreate = !!onCreate && !loading && text !== "" && !options.some((o) => o.label.toLowerCase() === text.toLowerCase());
40
+ const list = /* @__PURE__ */ jsx(Combobox.List, { children: loading ? /* @__PURE__ */ jsx(ComboboxLoading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
41
+ !canCreate && /* @__PURE__ */ jsx(Combobox.Empty, { children: emptyText ?? messages.noResults }),
42
+ options.map((opt) => /* @__PURE__ */ jsx(
43
+ Combobox.Item,
44
+ {
45
+ value: opt.label,
46
+ keywords: [opt.value],
47
+ onSelect: () => {
48
+ onValueChange?.(opt.value);
49
+ setSearch(opt.label);
50
+ setOpen(false);
44
51
  },
45
- opt.value
46
- ))
47
- ] }) })
52
+ children: opt.label
53
+ },
54
+ opt.value
55
+ )),
56
+ canCreate && /* @__PURE__ */ jsxs(
57
+ Combobox.Item,
58
+ {
59
+ forceMount: true,
60
+ value: "\0",
61
+ onSelect: () => {
62
+ onCreate(text);
63
+ setOpen(false);
64
+ },
65
+ className: "gap-2",
66
+ children: [
67
+ /* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
68
+ messages.createOption(text)
69
+ ]
70
+ }
71
+ )
72
+ ] }) });
73
+ const input = {
74
+ placeholder: placeholder ?? messages.searchPlaceholder,
75
+ value: search,
76
+ "aria-label": ariaLabel,
77
+ onValueChange: (t) => {
78
+ setSearch(t);
79
+ setOpen(true);
80
+ }
81
+ };
82
+ if (mode === "popover") {
83
+ return /* @__PURE__ */ jsx(
84
+ Combobox.Root,
85
+ {
86
+ label: ariaLabel,
87
+ className: cn("overflow-visible rounded-none bg-transparent", className),
88
+ shouldFilter: !onInputChange,
89
+ children: /* @__PURE__ */ jsx(ComboboxPopover, { open: open && !disabled, onOpenChange: setOpen, input, disabled, children: list })
90
+ }
91
+ );
92
+ }
93
+ return /* @__PURE__ */ jsxs(Combobox.Root, { label: ariaLabel, className, shouldFilter: !onInputChange, children: [
94
+ /* @__PURE__ */ jsx(Combobox.Input, { ...input, disabled }),
95
+ list
48
96
  ] });
49
97
  }
50
98
  Autocomplete.displayName = "Autocomplete";
@@ -8,5 +8,7 @@ export declare const chipVariants: (props?: ({
8
8
  export interface ChipProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof chipVariants> {
9
9
  tone?: ToneValue;
10
10
  onRemove?: () => void;
11
+ /** Accessible name of the remove button. Defaults to the localised "Remove". */
12
+ removeLabel?: string;
11
13
  }
12
14
  export declare const Chip: React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLSpanElement>>;
@@ -32,7 +32,7 @@ const chipVariants = cva(
32
32
  }
33
33
  );
34
34
  const Chip = React.forwardRef(
35
- ({ className, size, tone = "neutral", onRemove, children, ...props }, ref) => {
35
+ ({ className, size, tone = "neutral", onRemove, removeLabel, children, ...props }, ref) => {
36
36
  const messages = useNexubMessages();
37
37
  return /* @__PURE__ */ jsxs(
38
38
  "span",
@@ -46,7 +46,7 @@ const Chip = React.forwardRef(
46
46
  "button",
47
47
  {
48
48
  type: "button",
49
- "aria-label": messages.remove,
49
+ "aria-label": removeLabel ?? messages.remove,
50
50
  onClick: (e) => {
51
51
  e.stopPropagation();
52
52
  onRemove();
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { Command } from 'cmdk';
3
+ export interface ComboboxPopoverProps {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ /** Rendered inside the field, before the input (MultiSelect chips). */
7
+ before?: React.ReactNode;
8
+ input: React.ComponentProps<typeof Command.Input>;
9
+ disabled?: boolean;
10
+ /** The list (a Combobox.List). */
11
+ children: React.ReactNode;
12
+ }
13
+ /**
14
+ * A field that keeps focus in its input while a list opens below it.
15
+ * The list is not portalled: cmdk finds its items inside the Command root.
16
+ */
17
+ export declare function ComboboxPopover({ open, onOpenChange, before, input, disabled, children }: ComboboxPopoverProps): React.JSX.Element;
18
+ /** Three placeholder rows while options load; the word "Loading" is read out. */
19
+ export declare function ComboboxLoading(): React.JSX.Element;
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import * as RadixPopover from "@radix-ui/react-popover";
5
+ import { Command } from "cmdk";
6
+ import { cn } from "../../utils/cn.js";
7
+ import { floatingSurface } from "../../utils/surface.js";
8
+ import { field, fieldDisabled, fieldFocusWithin } from "../../utils/control.js";
9
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
10
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
11
+ import { Skeleton } from "../Skeleton/Skeleton.js";
12
+ function ComboboxPopover({ open, onOpenChange, before, input, disabled, children }) {
13
+ const anchorRef = React.useRef(null);
14
+ const inputRef = React.useRef(null);
15
+ const labelledBy = input["aria-labelledby"];
16
+ useIsomorphicLayoutEffect(() => {
17
+ inputRef.current?.setAttribute("aria-expanded", String(open));
18
+ if (labelledBy) inputRef.current?.setAttribute("aria-labelledby", labelledBy);
19
+ });
20
+ return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange, children: [
21
+ /* @__PURE__ */ jsx(RadixPopover.Anchor, { asChild: true, children: /* @__PURE__ */ jsxs(
22
+ "div",
23
+ {
24
+ ref: anchorRef,
25
+ className: cn(
26
+ field,
27
+ fieldFocusWithin,
28
+ "flex min-h-control-md w-full flex-wrap items-center gap-1.5 px-3 py-1",
29
+ disabled && fieldDisabled
30
+ ),
31
+ onPointerDown: (e) => {
32
+ if (disabled) return;
33
+ if (e.target.closest("button")) return;
34
+ if (e.target !== inputRef.current) e.preventDefault();
35
+ inputRef.current?.focus();
36
+ onOpenChange(true);
37
+ },
38
+ children: [
39
+ before,
40
+ /* @__PURE__ */ jsx(
41
+ Command.Input,
42
+ {
43
+ ref: inputRef,
44
+ disabled,
45
+ ...input,
46
+ onKeyDown: (e) => {
47
+ if (e.key === "ArrowDown" && !open) onOpenChange(true);
48
+ input.onKeyDown?.(e);
49
+ },
50
+ className: cn(
51
+ "min-w-8 flex-1 bg-transparent text-body-md outline-none placeholder:text-[var(--color-on-surface-muted)]",
52
+ input.className
53
+ )
54
+ }
55
+ )
56
+ ]
57
+ }
58
+ ) }),
59
+ /* @__PURE__ */ jsx(
60
+ RadixPopover.Content,
61
+ {
62
+ align: "start",
63
+ sideOffset: 4,
64
+ onOpenAutoFocus: (e) => e.preventDefault(),
65
+ onCloseAutoFocus: (e) => e.preventDefault(),
66
+ onInteractOutside: (e) => {
67
+ if (anchorRef.current?.contains(e.target)) e.preventDefault();
68
+ },
69
+ className: cn(
70
+ "z-[var(--z-popover)] w-[var(--radix-popover-trigger-width)] overflow-hidden p-0 outline-none",
71
+ floatingSurface,
72
+ "nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
73
+ ),
74
+ children
75
+ }
76
+ )
77
+ ] });
78
+ }
79
+ function ComboboxLoading() {
80
+ const messages = useNexubMessages();
81
+ return /* @__PURE__ */ jsxs("div", { role: "status", "aria-busy": "true", className: "flex flex-col gap-2 px-2.5 py-2", children: [
82
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.loading }),
83
+ /* @__PURE__ */ jsx(Skeleton, { className: "w-3/4", "aria-hidden": "true" }),
84
+ /* @__PURE__ */ jsx(Skeleton, { className: "w-1/2", "aria-hidden": "true" }),
85
+ /* @__PURE__ */ jsx(Skeleton, { className: "w-2/3", "aria-hidden": "true" })
86
+ ] });
87
+ }
88
+ export {
89
+ ComboboxLoading,
90
+ ComboboxPopover
91
+ };
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type ColumnDef, type RowData } from '@tanstack/react-table';
2
+ import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState } 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' */
@@ -8,6 +8,7 @@ declare module '@tanstack/react-table' {
8
8
  }
9
9
  /** A DataTable column: TanStack's `ColumnDef`, with `meta.align` for numeric columns. */
10
10
  export type DataTableColumn<TData> = ColumnDef<TData, unknown>;
11
+ export type DataTableDensity = 'comfortable' | 'compact';
11
12
  export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
12
13
  columns: DataTableColumn<TData>[];
13
14
  /** Names the table for assistive tech (and is shown below it unless `captionHidden`). */
@@ -17,8 +18,29 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
17
18
  data: TData[];
18
19
  enableSorting?: boolean;
19
20
  enableRowSelection?: boolean;
21
+ /**
22
+ * With `enableRowSelection`: a checkbox column with select-page in its
23
+ * header, and "Select all N" across pages. On by default from 6.0.
24
+ */
25
+ selectionColumn?: boolean;
26
+ /** Controlled selection, keyed by row id (see `getRowId`). */
27
+ rowSelection?: RowSelectionState;
28
+ onRowSelectionChange?: (selection: RowSelectionState) => void;
29
+ /** Stable row ids for selection; defaults to the row index. */
30
+ getRowId?: (row: TData, index: number) => string;
31
+ /** A floating bar while rows are selected: render your actions for the selected rows. */
32
+ bulkActions?: (rows: TData[], clear: () => void) => React.ReactNode;
33
+ density?: DataTableDensity;
34
+ defaultDensity?: DataTableDensity;
35
+ onDensityChange?: (density: DataTableDensity) => void;
36
+ columnVisibility?: VisibilityState;
37
+ onColumnVisibilityChange?: (visibility: VisibilityState) => void;
38
+ /** A "View" menu above the table: density and which columns show. */
39
+ viewOptions?: boolean;
40
+ /** Content at the start of the row above the table (filters, a title). */
41
+ toolbar?: React.ReactNode;
20
42
  pagination?: {
21
43
  pageSize?: number;
22
44
  };
23
45
  }
24
- export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
46
+ export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import {
5
5
  useReactTable,
@@ -9,11 +9,16 @@ import {
9
9
  getPaginationRowModel,
10
10
  flexRender
11
11
  } from "@tanstack/react-table";
12
- import { ArrowUp, ArrowDown, ChevronsUpDown } from "@nexub/icons";
12
+ import { ArrowUp, ArrowDown, ChevronsUpDown, SlidersHorizontal, X } from "@nexub/icons";
13
13
  import { cn } from "../../utils/cn.js";
14
14
  import { disabledContainer, pressable } from "../../utils/control.js";
15
- import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
15
+ import { floatingSurface } from "../../utils/surface.js";
16
+ import { useControllableState } from "../../hooks/useControllableState.js";
17
+ import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
16
18
  import { Table } from "../Table/Table.js";
19
+ import { Checkbox } from "../Checkbox/Checkbox.js";
20
+ import { Button } from "../Button/Button.js";
21
+ import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
17
22
  const alignText = {
18
23
  start: "text-start",
19
24
  end: "text-end",
@@ -47,7 +52,7 @@ const TableRow = React.forwardRef(
47
52
  {
48
53
  ref,
49
54
  className: cn(
50
- "border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[var(--color-surface-container-low)]",
55
+ "border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[color-mix(in_srgb,var(--color-primary)_10%,transparent)]",
51
56
  className
52
57
  ),
53
58
  ...props
@@ -79,11 +84,25 @@ const TableCell = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
79
84
  }
80
85
  ));
81
86
  TableCell.displayName = "DataTable.TableCell";
87
+ const apply = (u, prev) => typeof u === "function" ? u(prev) : u;
88
+ const columnLabel = (c) => typeof c.columnDef.header === "string" ? c.columnDef.header : c.id;
82
89
  function DataTable({
83
90
  columns,
84
91
  data,
85
92
  enableSorting = false,
86
93
  enableRowSelection = false,
94
+ selectionColumn = false,
95
+ rowSelection: rowSelectionProp,
96
+ onRowSelectionChange,
97
+ getRowId,
98
+ bulkActions,
99
+ density: densityProp,
100
+ defaultDensity = "comfortable",
101
+ onDensityChange,
102
+ columnVisibility: columnVisibilityProp,
103
+ onColumnVisibilityChange,
104
+ viewOptions = false,
105
+ toolbar,
87
106
  pagination,
88
107
  caption,
89
108
  captionHidden = false,
@@ -93,26 +112,73 @@ function DataTable({
93
112
  ...props
94
113
  }) {
95
114
  const messages = useNexubMessages();
115
+ const { locale } = useLocale();
96
116
  const [sorting, setSorting] = React.useState([]);
97
- const [rowSelection, setRowSelection] = React.useState({});
117
+ const [rowSelection = {}, setRowSelection] = useControllableState({
118
+ value: rowSelectionProp,
119
+ defaultValue: {},
120
+ onChange: onRowSelectionChange
121
+ });
122
+ const [density = "comfortable", setDensity] = useControllableState({
123
+ value: densityProp,
124
+ defaultValue: defaultDensity,
125
+ onChange: onDensityChange
126
+ });
127
+ const [columnVisibility = {}, setColumnVisibility] = useControllableState({
128
+ value: columnVisibilityProp,
129
+ defaultValue: {},
130
+ onChange: onColumnVisibilityChange
131
+ });
98
132
  const [paginationState, setPaginationState] = React.useState({
99
133
  pageIndex: 0,
100
134
  pageSize: pagination?.pageSize ?? 20
101
135
  });
136
+ const withSelectColumn = enableRowSelection && selectionColumn;
137
+ const allColumns = React.useMemo(
138
+ () => withSelectColumn ? [
139
+ {
140
+ id: "__select",
141
+ enableSorting: false,
142
+ enableHiding: false,
143
+ header: ({ table: table2 }) => /* @__PURE__ */ jsx(
144
+ Checkbox,
145
+ {
146
+ "aria-label": messages.selectAllOnPage,
147
+ checked: table2.getIsAllPageRowsSelected() ? true : table2.getIsSomePageRowsSelected() ? "indeterminate" : false,
148
+ onCheckedChange: (v) => table2.toggleAllPageRowsSelected(v === true)
149
+ }
150
+ ),
151
+ cell: ({ row }) => /* @__PURE__ */ jsx(
152
+ Checkbox,
153
+ {
154
+ "aria-label": messages.selectRow,
155
+ checked: row.getIsSelected(),
156
+ disabled: !row.getCanSelect(),
157
+ onCheckedChange: (v) => row.toggleSelected(v === true)
158
+ }
159
+ )
160
+ },
161
+ ...columns
162
+ ] : columns,
163
+ [withSelectColumn, columns, messages.selectAllOnPage, messages.selectRow]
164
+ );
102
165
  const tableState = React.useMemo(
103
166
  () => ({
104
167
  sorting: enableSorting ? sorting : void 0,
105
168
  rowSelection: enableRowSelection ? rowSelection : void 0,
106
- pagination: pagination ? paginationState : void 0
169
+ pagination: pagination ? paginationState : void 0,
170
+ columnVisibility
107
171
  }),
108
- [enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState]
172
+ [enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState, columnVisibility]
109
173
  );
110
174
  const table = useReactTable({
111
175
  data,
112
- columns,
176
+ columns: allColumns,
113
177
  state: tableState,
178
+ getRowId,
114
179
  onSortingChange: enableSorting ? setSorting : void 0,
115
- onRowSelectionChange: enableRowSelection ? setRowSelection : void 0,
180
+ onRowSelectionChange: enableRowSelection ? (u) => setRowSelection(apply(u, rowSelection)) : void 0,
181
+ onColumnVisibilityChange: (u) => setColumnVisibility(apply(u, columnVisibility)),
116
182
  onPaginationChange: pagination ? setPaginationState : void 0,
117
183
  getCoreRowModel: getCoreRowModel(),
118
184
  getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
@@ -121,7 +187,49 @@ function DataTable({
121
187
  enableRowSelection,
122
188
  manualPagination: false
123
189
  });
124
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), ...props, children: [
190
+ const compact = density === "compact";
191
+ const selectedRows = enableRowSelection ? table.getSelectedRowModel().rows : [];
192
+ const totalRows = table.getCoreRowModel().rows.length;
193
+ const fmt = (n) => new Intl.NumberFormat(locale).format(n);
194
+ const multiSort = sorting.length > 1;
195
+ const offerAll = withSelectColumn && !!pagination && table.getIsAllPageRowsSelected() && totalRows > table.getRowModel().rows.length;
196
+ const clear = () => table.resetRowSelection(true);
197
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-density": density, ...props, children: [
198
+ (toolbar != null || viewOptions) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
199
+ toolbar,
200
+ viewOptions && /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
201
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ms-auto", children: [
202
+ /* @__PURE__ */ jsx(SlidersHorizontal, { size: "sm", "aria-hidden": "true" }),
203
+ messages.view
204
+ ] }) }),
205
+ /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
206
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.density }),
207
+ /* @__PURE__ */ jsxs(DropdownMenu.RadioGroup, { value: density, onValueChange: (v) => setDensity(v), children: [
208
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "comfortable", children: messages.comfortable }),
209
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "compact", children: messages.compact })
210
+ ] }),
211
+ /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
212
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.columns }),
213
+ table.getAllLeafColumns().filter((c) => c.getCanHide()).map((c) => /* @__PURE__ */ jsx(
214
+ DropdownMenu.CheckboxItem,
215
+ {
216
+ checked: c.getIsVisible(),
217
+ onCheckedChange: (v) => c.toggleVisibility(v === true),
218
+ onSelect: (e) => e.preventDefault(),
219
+ children: columnLabel(c)
220
+ },
221
+ c.id
222
+ ))
223
+ ] })
224
+ ] })
225
+ ] }),
226
+ 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: [
227
+ /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(totalRows)) }),
228
+ /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: clear, children: messages.clearSelection })
229
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
230
+ /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(selectedRows.length)) }),
231
+ /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: () => table.toggleAllRowsSelected(true), children: messages.selectAllRows(fmt(totalRows)) })
232
+ ] }) }),
125
233
  /* @__PURE__ */ jsxs(TableRoot, { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: [
126
234
  caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
127
235
  /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: headerGroup.headers.map((header) => {
@@ -129,7 +237,7 @@ function DataTable({
129
237
  return /* @__PURE__ */ jsx(
130
238
  TableHead,
131
239
  {
132
- className: alignText[align],
240
+ className: cn(alignText[align], compact && "h-8", header.column.id === "__select" && "w-10"),
133
241
  "aria-sort": enableSorting && header.column.getCanSort() ? header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : "none" : void 0,
134
242
  children: header.isPlaceholder ? null : enableSorting && header.column.getCanSort() ? /* @__PURE__ */ jsxs(
135
243
  "button",
@@ -142,6 +250,10 @@ function DataTable({
142
250
  ),
143
251
  children: [
144
252
  flexRender(header.column.columnDef.header, header.getContext()),
253
+ multiSort && header.column.getIsSorted() && /* @__PURE__ */ jsxs("span", { className: "text-[10px] tabular-nums leading-none", children: [
254
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: header.column.getSortIndex() + 1 }),
255
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.sortOrder(header.column.getSortIndex() + 1) })
256
+ ] }),
145
257
  header.column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { size: "xs", "aria-hidden": "true" }) : header.column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { size: "xs", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { size: "xs", className: "opacity-40", "aria-hidden": "true" })
146
258
  ]
147
259
  }
@@ -157,7 +269,7 @@ function DataTable({
157
269
  children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
158
270
  TableCell,
159
271
  {
160
- className: alignText[cell.column.columnDef.meta?.align ?? "start"],
272
+ className: cn(alignText[cell.column.columnDef.meta?.align ?? "start"], compact && "py-1.5"),
161
273
  children: flexRender(cell.column.columnDef.cell, cell.getContext())
162
274
  },
163
275
  cell.id
@@ -167,7 +279,7 @@ function DataTable({
167
279
  )) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
168
280
  TableCell,
169
281
  {
170
- colSpan: columns.length,
282
+ colSpan: table.getVisibleLeafColumns().length,
171
283
  className: "h-24 text-center text-[var(--color-on-surface-muted)]",
172
284
  children: messages.noResults
173
285
  }
@@ -197,7 +309,27 @@ function DataTable({
197
309
  }
198
310
  )
199
311
  ] })
200
- ] })
312
+ ] }),
313
+ bulkActions && selectedRows.length > 0 && /* @__PURE__ */ jsxs(
314
+ "div",
315
+ {
316
+ role: "region",
317
+ "aria-label": messages.selectedCount(fmt(selectedRows.length)),
318
+ className: cn(
319
+ floatingSurface,
320
+ "nx-motion sticky bottom-4 z-10 mx-auto flex w-fit max-w-full flex-wrap items-center gap-3 px-4 py-2"
321
+ ),
322
+ "data-state": "open",
323
+ children: [
324
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "text-label-lg tabular-nums", children: messages.selectedCount(fmt(selectedRows.length)) }),
325
+ bulkActions(
326
+ selectedRows.map((r) => r.original),
327
+ clear
328
+ ),
329
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: clear, "aria-label": messages.clearSelection, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
330
+ ]
331
+ }
332
+ )
201
333
  ] });
202
334
  }
203
335
  export {
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ export interface MultiSelectOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ export interface MultiSelectProps {
7
+ options: MultiSelectOption[];
8
+ value?: string[];
9
+ defaultValue?: string[];
10
+ onValueChange?: (value: string[]) => void;
11
+ placeholder?: string;
12
+ emptyText?: string;
13
+ loading?: boolean;
14
+ /** Controlled search text. */
15
+ inputValue?: string;
16
+ /** Called as the user types; options are then shown as given (no built-in filtering). */
17
+ onInputChange?: (text: string) => void;
18
+ /** Adds a "Create “text”" option when the text matches no option exactly. */
19
+ onCreate?: (text: string) => void;
20
+ /** Chips shown before the rest collapse into "+N more". Default 3. */
21
+ maxVisibleChips?: number;
22
+ disabled?: boolean;
23
+ className?: string;
24
+ id?: string;
25
+ 'aria-label'?: string;
26
+ 'aria-labelledby'?: string;
27
+ }
28
+ /**
29
+ * Pick several options: chips in the field, a filterable list below.
30
+ * Backspace in an empty field removes the last chip.
31
+ */
32
+ export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, onCreate, maxVisibleChips, disabled, className, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }: MultiSelectProps): React.JSX.Element;
33
+ export declare namespace MultiSelect {
34
+ var displayName: string;
35
+ }