@douglasneuroinformatics/libui 4.9.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/components.d.ts +5 -48
  2. package/dist/components.js +1110 -723
  3. package/dist/components.js.map +1 -1
  4. package/dist/hooks.d.ts +4 -2
  5. package/dist/{types-9zYgx7C8.d.ts → types-CQ7qbFhC.d.ts} +57 -1
  6. package/package.json +3 -2
  7. package/src/components/DataTable/DataTable.stories.tsx +207 -37
  8. package/src/components/DataTable/DataTable.tsx +22 -279
  9. package/src/components/DataTable/DataTableBody.tsx +69 -0
  10. package/src/components/DataTable/DataTableContent.tsx +36 -0
  11. package/src/components/DataTable/DataTableControls.tsx +55 -0
  12. package/src/components/DataTable/DataTableEmptyState.tsx +25 -0
  13. package/src/components/DataTable/DataTableHead.tsx +58 -0
  14. package/src/components/DataTable/DataTablePagination.tsx +62 -0
  15. package/src/components/DataTable/DataTableRowActionCell.tsx +67 -0
  16. package/src/components/DataTable/__tests__/DataTable.spec.tsx +60 -0
  17. package/src/components/DataTable/constants.ts +7 -0
  18. package/src/components/DataTable/context.ts +5 -0
  19. package/src/components/DataTable/hooks.ts +60 -0
  20. package/src/components/DataTable/store.ts +203 -0
  21. package/src/components/DataTable/types.ts +99 -0
  22. package/src/components/DataTable/utils.tsx +138 -0
  23. package/src/hooks/useDestructiveAction/useDestructiveActionStore.test.ts +2 -7
  24. package/src/hooks/useNotificationsStore/useNotificationsStore.test.ts +1 -8
  25. package/src/testing/setup-tests.ts +1 -3
  26. package/src/components/DataTable/DestructiveActionDialog.tsx +0 -67
  27. package/src/components/DataTable/RowActionsDropdown.tsx +0 -64
package/dist/hooks.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { C as ChartConfig, U as UseStorageOptions } from './types-9zYgx7C8.js';
2
- export { D as DEFAULT_THEME, a as SYS_DARK_MEDIA_QUERY, S as StorageName, b as THEME_ATTRIBUTE, c as THEME_KEY, T as Theme, u as useStorage, d as useTheme } from './types-9zYgx7C8.js';
1
+ import { C as ChartConfig, U as UseStorageOptions } from './types-CQ7qbFhC.js';
2
+ export { a as DEFAULT_THEME, b as SYS_DARK_MEDIA_QUERY, S as StorageName, c as THEME_ATTRIBUTE, d as THEME_KEY, T as Theme, u as useStorage, e as useTheme } from './types-CQ7qbFhC.js';
3
3
  import { Promisable } from 'type-fest';
4
4
  import { RefObject, useEffect, Dispatch, SetStateAction } from 'react';
5
5
  import * as zustand from 'zustand';
6
6
  import { T as TranslatorType, a as TranslationKey, b as TranslationNamespace, c as TranslationKeyForNamespace } from './types-CwW4nA_v.js';
7
+ import '@tanstack/table-core';
8
+ import 'lucide-react';
7
9
 
8
10
  declare function useChart(): {
9
11
  config: ChartConfig;
@@ -1,4 +1,7 @@
1
+ import { RowData, Table, ColumnDef, ColumnFiltersState, ColumnPinningState, SortingState, TableMeta } from '@tanstack/table-core';
2
+ import { Promisable } from 'type-fest';
1
3
  import { Dispatch, SetStateAction } from 'react';
4
+ import { LucideIcon } from 'lucide-react';
2
5
 
3
6
  type StorageName = 'localStorage' | 'sessionStorage';
4
7
  type StorageEventMap = {
@@ -72,4 +75,57 @@ type ChartConfig = {
72
75
  });
73
76
  };
74
77
 
75
- export { type ChartConfig as C, DEFAULT_THEME as D, type StorageName as S, type Theme as T, type UseStorageOptions as U, SYS_DARK_MEDIA_QUERY as a, THEME_ATTRIBUTE as b, THEME_KEY as c, useTheme as d, useStorage as u };
78
+ declare const ROW_ACTIONS_METADATA_KEY: unique symbol;
79
+ declare const TABLE_NAME_METADATA_KEY: unique symbol;
80
+
81
+ type DataTableEmptyStateProps = {
82
+ className?: string;
83
+ description?: string;
84
+ icon?: LucideIcon;
85
+ title: string;
86
+ };
87
+
88
+ declare module '@tanstack/table-core' {
89
+ interface ColumnMeta<TData, TValue> {
90
+ [key: string]: unknown;
91
+ }
92
+ type GlobalFilter = string | undefined;
93
+ interface TableMeta<TData extends RowData> {
94
+ [key: string]: unknown;
95
+ [ROW_ACTIONS_METADATA_KEY]?: DataTableRowAction<TData>[];
96
+ [TABLE_NAME_METADATA_KEY]?: string;
97
+ }
98
+ interface TableState {
99
+ globalFilter: GlobalFilter;
100
+ }
101
+ }
102
+ type DataTableInitialState = {
103
+ columnFilters?: ColumnFiltersState;
104
+ columnPinning?: ColumnPinningState;
105
+ sorting?: SortingState;
106
+ };
107
+ type DataTableProps<T extends RowData> = DataTableContentProps<T> & DataTableStoreParams<T>;
108
+ type DataTableContentProps<T extends RowData> = {
109
+ emptyStateProps?: Partial<DataTableEmptyStateProps>;
110
+ onSearchChange?: SearchChangeHandler<NoInfer<T>>;
111
+ togglesComponent?: React.FC<{
112
+ table: Table<T>;
113
+ }>;
114
+ };
115
+ type DataTableRowAction<T extends RowData> = {
116
+ destructive?: boolean;
117
+ disabled?: ((row: T) => boolean) | boolean;
118
+ label: string;
119
+ onSelect: (row: T, table: Table<T>) => Promisable<void>;
120
+ };
121
+ type DataTableStoreParams<T extends RowData> = {
122
+ columns: ColumnDef<NoInfer<T>>[];
123
+ data: T[];
124
+ initialState?: DataTableInitialState;
125
+ meta?: TableMeta<NoInfer<T>>;
126
+ rowActions?: DataTableRowAction<NoInfer<T>>[];
127
+ tableName?: string;
128
+ };
129
+ type SearchChangeHandler<T = any> = (value: string, table: Table<T>) => void;
130
+
131
+ export { type ChartConfig as C, type DataTableProps as D, type StorageName as S, type Theme as T, type UseStorageOptions as U, DEFAULT_THEME as a, SYS_DARK_MEDIA_QUERY as b, THEME_ATTRIBUTE as c, THEME_KEY as d, useTheme as e, useStorage as u };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@douglasneuroinformatics/libui",
3
3
  "type": "module",
4
- "version": "4.9.1",
4
+ "version": "5.0.0",
5
5
  "packageManager": "pnpm@10.7.1",
6
6
  "description": "Generic UI components for DNP projects, built using React and Tailwind CSS",
7
7
  "author": "Joshua Unrau",
@@ -31,11 +31,11 @@
31
31
  "types": "./dist/i18n.d.ts",
32
32
  "import": "./dist/i18n.js"
33
33
  },
34
+ "./package.json": "./package.json",
34
35
  "./providers": {
35
36
  "types": "./dist/providers.d.ts",
36
37
  "import": "./dist/providers.js"
37
38
  },
38
- "./package.json": "./package.json",
39
39
  "./tailwind/globals.css": "./dist/tailwind/globals.css",
40
40
  "./utils": {
41
41
  "types": "./dist/utils.d.ts",
@@ -97,6 +97,7 @@
97
97
  "@radix-ui/react-tabs": "^1.1.3",
98
98
  "@radix-ui/react-tooltip": "^1.1.8",
99
99
  "@tanstack/react-table": "^8.21.3",
100
+ "@tanstack/table-core": "^8.21.3",
100
101
  "class-variance-authority": "^0.7.1",
101
102
  "clsx": "^2.1.1",
102
103
  "cmdk": "^1.1.1",
@@ -1,76 +1,246 @@
1
- import { range, toBasicISOString, unwrap } from '@douglasneuroinformatics/libjs';
1
+ import { useState } from 'react';
2
+
2
3
  import { faker } from '@faker-js/faker';
3
4
  import type { Meta, StoryObj } from '@storybook/react-vite';
5
+ import type { ColumnDef } from '@tanstack/table-core';
6
+ import { range } from 'lodash-es';
7
+ import { ChevronDownIcon } from 'lucide-react';
4
8
 
9
+ import { Button } from '../Button';
10
+ import { DropdownMenu } from '../DropdownMenu';
5
11
  import { DataTable } from './DataTable';
12
+ import { useDataTableHandle } from './hooks';
6
13
 
7
- import type { DataTableColumn } from './DataTable';
14
+ type PaymentStatus = 'failed' | 'pending' | 'processing' | 'success';
8
15
 
9
- type User = {
10
- birthday: Date;
16
+ type Payment = {
17
+ amount: number;
11
18
  email: string;
12
- firstName: string;
13
- lastName: string;
19
+ id: string;
20
+ status: PaymentStatus;
14
21
  };
15
22
 
16
- type Story = StoryObj<typeof DataTable<User>>;
23
+ type Story = StoryObj<typeof DataTable<Payment>>;
17
24
 
18
- const columns: DataTableColumn<User>[] = [
25
+ const columns: ColumnDef<Payment>[] = [
19
26
  {
20
- format: (value) => {
21
- return toBasicISOString(value);
27
+ accessorKey: 'status',
28
+ enableSorting: false,
29
+ filterFn: (row, id, filter: PaymentStatus[]) => {
30
+ return filter.includes(row.getValue(id));
22
31
  },
23
- key: 'birthday',
24
- label: 'Birthday'
32
+ header: 'Status'
33
+ },
34
+ {
35
+ accessorKey: 'email',
36
+ header: 'Email'
25
37
  },
26
38
  {
27
- format: 'email',
28
- key: 'email',
29
- label: 'Email',
30
- sortable: true
39
+ accessorKey: 'amount',
40
+ header: 'Amount'
31
41
  }
32
42
  ];
33
43
 
34
- const data: User[] = unwrap(range(60)).map(() => ({
35
- birthday: faker.date.birthdate(),
36
- email: faker.internet.email(),
37
- firstName: faker.person.firstName(),
38
- lastName: faker.person.lastName()
39
- }));
44
+ const statuses: readonly PaymentStatus[] = Object.freeze(['failed', 'pending', 'processing', 'success']);
45
+
46
+ const createData = (n: number): Payment[] => {
47
+ return range(n).map((i) => ({
48
+ amount: faker.number.int({ max: 100, min: 0 }),
49
+ email: faker.internet.email(),
50
+ id: String(i + 1),
51
+ status: faker.helpers.arrayElement(statuses)
52
+ }));
53
+ };
54
+
55
+ const Toggles = () => {
56
+ const table = useDataTableHandle('table', true);
57
+ const columns = table.getAllColumns();
58
+ const statusColumn = columns.find((column) => column.id === 'status')!;
59
+
60
+ const filterValue = statusColumn.getFilterValue() as PaymentStatus[];
61
+
62
+ return (
63
+ <>
64
+ <DropdownMenu>
65
+ <DropdownMenu.Trigger asChild>
66
+ <Button className="flex items-center gap-2" variant="outline">
67
+ Columns
68
+ <ChevronDownIcon />
69
+ </Button>
70
+ </DropdownMenu.Trigger>
71
+ <DropdownMenu.Content align="end">
72
+ {columns
73
+ .filter((column) => column.getCanHide())
74
+ .map((column) => {
75
+ return (
76
+ <DropdownMenu.CheckboxItem
77
+ checked={column.getIsVisible()}
78
+ className="capitalize"
79
+ key={column.id}
80
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
81
+ >
82
+ {column.id}
83
+ </DropdownMenu.CheckboxItem>
84
+ );
85
+ })}
86
+ </DropdownMenu.Content>
87
+ </DropdownMenu>
88
+ <DropdownMenu>
89
+ <DropdownMenu.Trigger asChild>
90
+ <Button className="flex items-center gap-2" variant="outline">
91
+ Filters
92
+ <ChevronDownIcon />
93
+ </Button>
94
+ </DropdownMenu.Trigger>
95
+ <DropdownMenu.Content widthFull align="start">
96
+ {statuses.map((option) => (
97
+ <DropdownMenu.CheckboxItem
98
+ checked={filterValue.includes(option)}
99
+ className="capitalize"
100
+ key={option}
101
+ onCheckedChange={(checked) => {
102
+ statusColumn.setFilterValue((prevValue: PaymentStatus[]) => {
103
+ if (checked) {
104
+ return [...prevValue, option];
105
+ }
106
+ return prevValue.filter((item) => item !== option);
107
+ });
108
+ }}
109
+ >
110
+ {option}
111
+ </DropdownMenu.CheckboxItem>
112
+ ))}
113
+ </DropdownMenu.Content>
114
+ </DropdownMenu>
115
+ </>
116
+ );
117
+ };
40
118
 
41
- export default { component: DataTable } as Meta<typeof DataTable<User>>;
119
+ export default {
120
+ component: DataTable
121
+ } as Meta<typeof DataTable>;
42
122
 
43
123
  export const Default: Story = {
124
+ decorators: [
125
+ (Story) => {
126
+ const [tableData, setTableData] = useState(createData(10));
127
+ return (
128
+ <div>
129
+ <Story
130
+ args={{
131
+ columns,
132
+ data: tableData
133
+ }}
134
+ />
135
+ <div className="fixed bottom-0 py-2">
136
+ <button
137
+ className="rounded-md border px-2 py-1.5 text-sm"
138
+ type="button"
139
+ onClick={() => setTableData(createData(10))}
140
+ >
141
+ New Data
142
+ </button>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
147
+ ]
148
+ };
149
+
150
+ export const WithActions: Story = {
44
151
  args: {
45
- columns,
46
- data,
47
- headerActions: [
152
+ columns: [
153
+ ...columns,
48
154
  {
49
- label: 'Do Something',
50
- onClick: () => {
51
- alert('Something!');
52
- }
155
+ accessorKey: 'notes',
156
+ header: 'Notes'
53
157
  }
54
158
  ],
159
+ data: createData(100).map((payment) => ({ ...payment, notes: faker.lorem.paragraph() })),
160
+ onSearchChange: () => {
161
+ return;
162
+ },
55
163
  rowActions: [
164
+ {
165
+ label: 'Modify',
166
+ onSelect: () => {
167
+ alert('Modify');
168
+ }
169
+ },
56
170
  {
57
171
  destructive: true,
58
172
  label: 'Delete',
59
- onSelect: (row) => {
60
- alert(`Delete User: ${row.firstName} ${row.lastName}`);
173
+ onSelect: () => {
174
+ alert('Delete');
61
175
  }
62
176
  }
63
177
  ],
64
- search: {
65
- key: 'email',
66
- placeholder: 'Filter emails...'
67
- }
178
+ tableName: 'action-table'
179
+ }
180
+ };
181
+
182
+ export const WithToggles: Story = {
183
+ args: {
184
+ columns,
185
+ data: createData(100),
186
+ initialState: {
187
+ columnFilters: [
188
+ {
189
+ id: 'status',
190
+ value: [...statuses]
191
+ }
192
+ ]
193
+ },
194
+ onSearchChange: () => {
195
+ return;
196
+ },
197
+ togglesComponent: Toggles
68
198
  }
69
199
  };
70
200
 
71
201
  export const Empty: Story = {
72
202
  args: {
73
203
  columns,
74
- data: []
204
+ data: [],
205
+ onSearchChange: () => {
206
+ return;
207
+ }
208
+ }
209
+ };
210
+
211
+ export const Grouped: Story = {
212
+ args: {
213
+ columns: [
214
+ {
215
+ columns: [
216
+ {
217
+ accessorKey: 'id',
218
+ header: 'ID'
219
+ },
220
+ {
221
+ accessorKey: 'status',
222
+ header: 'Status'
223
+ }
224
+ ],
225
+ header: 'Internal'
226
+ },
227
+ {
228
+ columns: [
229
+ {
230
+ accessorKey: 'email',
231
+ header: 'Email'
232
+ },
233
+ {
234
+ accessorKey: 'amount',
235
+ header: 'Amount'
236
+ }
237
+ ],
238
+ header: 'Details'
239
+ }
240
+ ],
241
+ data: createData(100),
242
+ onSearchChange: () => {
243
+ return;
244
+ }
75
245
  }
76
246
  };