@zentauri-ui/zentauri-components 2.1.6 → 2.1.8

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 (152) hide show
  1. package/README.md +12 -8
  2. package/cli/cli.integration.test.ts +36 -0
  3. package/cli/index.mjs +43 -7
  4. package/cli/props.json +462 -3
  5. package/cli/registry.json +29 -0
  6. package/cli/rewrite-imports.mjs +29 -4
  7. package/cli/rewrite-imports.test.ts +35 -0
  8. package/dist/{chunk-WWKAJHIV.mjs → chunk-4PAHLHYF.mjs} +3 -3
  9. package/dist/{chunk-WWKAJHIV.mjs.map → chunk-4PAHLHYF.mjs.map} +1 -1
  10. package/dist/chunk-4SLVTSHM.js +241 -0
  11. package/dist/chunk-4SLVTSHM.js.map +1 -0
  12. package/dist/chunk-6OVDBAMI.js +19 -0
  13. package/dist/{chunk-3W2UUKWP.js.map → chunk-6OVDBAMI.js.map} +1 -1
  14. package/dist/chunk-74SKXGTM.js +4 -0
  15. package/dist/chunk-74SKXGTM.js.map +1 -0
  16. package/dist/{chunk-QE7OJW4J.js → chunk-BAAXQPZ7.js} +6 -6
  17. package/dist/{chunk-QE7OJW4J.js.map → chunk-BAAXQPZ7.js.map} +1 -1
  18. package/dist/chunk-CYKSS5S5.mjs +128 -0
  19. package/dist/chunk-CYKSS5S5.mjs.map +1 -0
  20. package/dist/chunk-D7ZTSAA6.mjs +221 -0
  21. package/dist/chunk-D7ZTSAA6.mjs.map +1 -0
  22. package/dist/{chunk-VA6SB6NN.js → chunk-DPNTQ4AK.js} +73 -6
  23. package/dist/chunk-DPNTQ4AK.js.map +1 -0
  24. package/dist/chunk-HMDH4BQJ.js +123 -0
  25. package/dist/chunk-HMDH4BQJ.js.map +1 -0
  26. package/dist/chunk-I7EBE7BD.js +98 -0
  27. package/dist/chunk-I7EBE7BD.js.map +1 -0
  28. package/dist/chunk-IHDM7AHY.mjs +233 -0
  29. package/dist/chunk-IHDM7AHY.mjs.map +1 -0
  30. package/dist/chunk-L5QORCUO.js +225 -0
  31. package/dist/chunk-L5QORCUO.js.map +1 -0
  32. package/dist/chunk-LHBJD57K.mjs +143 -0
  33. package/dist/chunk-LHBJD57K.mjs.map +1 -0
  34. package/dist/{chunk-CHI6MBTI.mjs → chunk-OWVQVAOY.mjs} +3 -3
  35. package/dist/{chunk-CHI6MBTI.mjs.map → chunk-OWVQVAOY.mjs.map} +1 -1
  36. package/dist/chunk-OYAJG2BO.js +83 -0
  37. package/dist/chunk-OYAJG2BO.js.map +1 -0
  38. package/dist/chunk-PTU5ZAYX.js +145 -0
  39. package/dist/chunk-PTU5ZAYX.js.map +1 -0
  40. package/dist/chunk-QKO5DA4N.mjs +81 -0
  41. package/dist/chunk-QKO5DA4N.mjs.map +1 -0
  42. package/dist/chunk-T7PIKDUZ.js +130 -0
  43. package/dist/chunk-T7PIKDUZ.js.map +1 -0
  44. package/dist/chunk-TDK5TVJE.mjs +3 -0
  45. package/dist/chunk-TDK5TVJE.mjs.map +1 -0
  46. package/dist/{chunk-A4IB3C23.mjs → chunk-UVP3MUBU.mjs} +58 -7
  47. package/dist/chunk-UVP3MUBU.mjs.map +1 -0
  48. package/dist/chunk-VBNW2B4D.mjs +3 -0
  49. package/dist/chunk-VBNW2B4D.mjs.map +1 -0
  50. package/dist/chunk-W6DO36XD.mjs +96 -0
  51. package/dist/chunk-W6DO36XD.mjs.map +1 -0
  52. package/dist/chunk-XR3J46TZ.js +4 -0
  53. package/dist/chunk-XR3J46TZ.js.map +1 -0
  54. package/dist/chunk-ZOHCADDL.mjs +121 -0
  55. package/dist/chunk-ZOHCADDL.mjs.map +1 -0
  56. package/dist/design-system/data-table.d.ts +8 -0
  57. package/dist/design-system/data-table.d.ts.map +1 -0
  58. package/dist/design-system/facade.js +6 -6
  59. package/dist/design-system/facade.mjs +5 -5
  60. package/dist/design-system/index.d.ts +2 -0
  61. package/dist/design-system/index.d.ts.map +1 -1
  62. package/dist/design-system/split-button.d.ts +25 -0
  63. package/dist/design-system/split-button.d.ts.map +1 -0
  64. package/dist/hooks/useTableFilter.js +6 -116
  65. package/dist/hooks/useTableFilter.js.map +1 -1
  66. package/dist/hooks/useTableFilter.mjs +1 -118
  67. package/dist/hooks/useTableFilter.mjs.map +1 -1
  68. package/dist/hooks/useTableSort.js +6 -91
  69. package/dist/hooks/useTableSort.js.map +1 -1
  70. package/dist/hooks/useTableSort.mjs +1 -93
  71. package/dist/hooks/useTableSort.mjs.map +1 -1
  72. package/dist/hooks/useVirtualList.js +6 -76
  73. package/dist/hooks/useVirtualList.js.map +1 -1
  74. package/dist/hooks/useVirtualList.mjs +1 -78
  75. package/dist/hooks/useVirtualList.mjs.map +1 -1
  76. package/dist/ui/buttons/animated.js +8 -8
  77. package/dist/ui/buttons/animated.mjs +6 -6
  78. package/dist/ui/buttons.js +10 -9
  79. package/dist/ui/buttons.mjs +8 -7
  80. package/dist/ui/checkbox.js +7 -123
  81. package/dist/ui/checkbox.js.map +1 -1
  82. package/dist/ui/checkbox.mjs +2 -126
  83. package/dist/ui/checkbox.mjs.map +1 -1
  84. package/dist/ui/data-table/data-table-base.d.ts +6 -0
  85. package/dist/ui/data-table/data-table-base.d.ts.map +1 -0
  86. package/dist/ui/data-table/data-table.d.ts +6 -0
  87. package/dist/ui/data-table/data-table.d.ts.map +1 -0
  88. package/dist/ui/data-table/index.d.ts +4 -0
  89. package/dist/ui/data-table/index.d.ts.map +1 -0
  90. package/dist/ui/data-table/types.d.ts +92 -0
  91. package/dist/ui/data-table/types.d.ts.map +1 -0
  92. package/dist/ui/data-table/variants.d.ts +8 -0
  93. package/dist/ui/data-table/variants.d.ts.map +1 -0
  94. package/dist/ui/data-table.js +620 -0
  95. package/dist/ui/data-table.js.map +1 -0
  96. package/dist/ui/data-table.mjs +611 -0
  97. package/dist/ui/data-table.mjs.map +1 -0
  98. package/dist/ui/dropdown/dropdown.d.ts +1 -1
  99. package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
  100. package/dist/ui/dropdown/types.d.ts +2 -2
  101. package/dist/ui/dropdown/types.d.ts.map +1 -1
  102. package/dist/ui/dropdown.js +31 -231
  103. package/dist/ui/dropdown.js.map +1 -1
  104. package/dist/ui/dropdown.mjs +4 -229
  105. package/dist/ui/dropdown.mjs.map +1 -1
  106. package/dist/ui/dynamic-stepper.js +18 -18
  107. package/dist/ui/dynamic-stepper.mjs +7 -7
  108. package/dist/ui/inputs.js +7 -138
  109. package/dist/ui/inputs.js.map +1 -1
  110. package/dist/ui/inputs.mjs +2 -141
  111. package/dist/ui/inputs.mjs.map +1 -1
  112. package/dist/ui/pagination.js +20 -221
  113. package/dist/ui/pagination.js.map +1 -1
  114. package/dist/ui/pagination.mjs +8 -223
  115. package/dist/ui/pagination.mjs.map +1 -1
  116. package/dist/ui/split-button/index.d.ts +4 -0
  117. package/dist/ui/split-button/index.d.ts.map +1 -0
  118. package/dist/ui/split-button/split-button-base.d.ts +6 -0
  119. package/dist/ui/split-button/split-button-base.d.ts.map +1 -0
  120. package/dist/ui/split-button/split-button.d.ts +6 -0
  121. package/dist/ui/split-button/split-button.d.ts.map +1 -0
  122. package/dist/ui/split-button/types.d.ts +30 -0
  123. package/dist/ui/split-button/types.d.ts.map +1 -0
  124. package/dist/ui/split-button/variants.d.ts +16 -0
  125. package/dist/ui/split-button/variants.d.ts.map +1 -0
  126. package/dist/ui/split-button.js +287 -0
  127. package/dist/ui/split-button.js.map +1 -0
  128. package/dist/ui/split-button.mjs +278 -0
  129. package/dist/ui/split-button.mjs.map +1 -0
  130. package/dist/ui/table.js +1 -0
  131. package/dist/ui/table.mjs +1 -0
  132. package/package.json +1 -1
  133. package/src/design-system/data-table.ts +20 -0
  134. package/src/design-system/index.ts +2 -0
  135. package/src/design-system/split-button.ts +38 -0
  136. package/src/ui/data-table/data-table-base.tsx +701 -0
  137. package/src/ui/data-table/data-table.test.tsx +389 -0
  138. package/src/ui/data-table/data-table.tsx +11 -0
  139. package/src/ui/data-table/index.ts +24 -0
  140. package/src/ui/data-table/types.ts +121 -0
  141. package/src/ui/data-table/variants.ts +21 -0
  142. package/src/ui/dropdown/dropdown.tsx +7 -3
  143. package/src/ui/dropdown/types.ts +2 -2
  144. package/src/ui/split-button/index.ts +19 -0
  145. package/src/ui/split-button/split-button-base.tsx +232 -0
  146. package/src/ui/split-button/split-button.test.tsx +208 -0
  147. package/src/ui/split-button/split-button.tsx +9 -0
  148. package/src/ui/split-button/types.ts +46 -0
  149. package/src/ui/split-button/variants.ts +46 -0
  150. package/dist/chunk-3W2UUKWP.js +0 -19
  151. package/dist/chunk-A4IB3C23.mjs.map +0 -1
  152. package/dist/chunk-VA6SB6NN.js.map +0 -1
@@ -0,0 +1,389 @@
1
+ import { act, fireEvent, render, screen, within } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { DataTable } from "./data-table";
6
+ import type { DataTableColumn } from "./types";
7
+
8
+ type Person = {
9
+ id: string;
10
+ name: string;
11
+ email: string;
12
+ role: string;
13
+ status: "Active" | "Invited";
14
+ score: number;
15
+ };
16
+
17
+ const people: Person[] = [
18
+ {
19
+ id: "ada",
20
+ name: "Ada Lovelace",
21
+ email: "ada@example.com",
22
+ role: "Engineer",
23
+ status: "Active",
24
+ score: 92,
25
+ },
26
+ {
27
+ id: "grace",
28
+ name: "Grace Hopper",
29
+ email: "grace@example.com",
30
+ role: "Architect",
31
+ status: "Invited",
32
+ score: 98,
33
+ },
34
+ {
35
+ id: "katherine",
36
+ name: "Katherine Johnson",
37
+ email: "katherine@example.com",
38
+ role: "Analyst",
39
+ status: "Active",
40
+ score: 95,
41
+ },
42
+ ];
43
+
44
+ const columns: DataTableColumn<Person>[] = [
45
+ {
46
+ id: "name",
47
+ header: "Name",
48
+ accessor: "name",
49
+ sortable: true,
50
+ filterable: true,
51
+ cell: ({ value }) => <strong>{String(value)}</strong>,
52
+ },
53
+ {
54
+ id: "email",
55
+ header: "Email",
56
+ accessor: "email",
57
+ filterable: true,
58
+ },
59
+ {
60
+ id: "role",
61
+ header: "Role",
62
+ accessor: "role",
63
+ filterable: true,
64
+ },
65
+ {
66
+ id: "score",
67
+ header: "Score",
68
+ accessor: "score",
69
+ sortable: true,
70
+ textAlign: "right",
71
+ },
72
+ ];
73
+
74
+ function renderPeopleTable(
75
+ props: Partial<React.ComponentProps<typeof DataTable<Person>>> = {},
76
+ ) {
77
+ return render(
78
+ <DataTable
79
+ aria-label="People"
80
+ columns={columns}
81
+ data={people}
82
+ getRowId={(row) => row.id}
83
+ {...props}
84
+ />,
85
+ );
86
+ }
87
+
88
+ function getBodyRows() {
89
+ return screen
90
+ .getAllByRole("row")
91
+ .filter((row) => within(row).queryAllByRole("cell").length > 0);
92
+ }
93
+
94
+ describe("DataTable", () => {
95
+ it("should expose a stable displayName", () => {
96
+ expect(DataTable.displayName).toBe("DataTable");
97
+ });
98
+
99
+ it("should render typed columns and rows with custom cells", () => {
100
+ renderPeopleTable();
101
+
102
+ expect(screen.getByRole("table", { name: "People" })).toBeVisible();
103
+ expect(screen.getByRole("columnheader", { name: "Name" })).toBeVisible();
104
+ expect(screen.getByRole("columnheader", { name: "Email" })).toBeVisible();
105
+ expect(
106
+ screen.getByRole("rowheader", { name: "Ada Lovelace" }),
107
+ ).toContainHTML("strong");
108
+ expect(screen.getByRole("cell", { name: "Engineer" })).toBeVisible();
109
+ });
110
+
111
+ it("should filter rows through the built-in search control", async () => {
112
+ const user = userEvent.setup();
113
+ renderPeopleTable({ search: { placeholder: "Search people" } });
114
+
115
+ await user.type(
116
+ screen.getByRole("searchbox", { name: "Search table" }),
117
+ "grace",
118
+ );
119
+
120
+ expect(
121
+ screen.getByRole("rowheader", { name: "Grace Hopper" }),
122
+ ).toBeVisible();
123
+ expect(
124
+ screen.queryByRole("rowheader", { name: "Ada Lovelace" }),
125
+ ).toBeNull();
126
+ expect(
127
+ screen.queryByRole("rowheader", { name: "Katherine Johnson" }),
128
+ ).toBeNull();
129
+ });
130
+
131
+ it("should warn and fall back to all filterable columns when filterColumnIds is empty", async () => {
132
+ const user = userEvent.setup();
133
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
134
+
135
+ renderPeopleTable({
136
+ search: { filterColumnIds: [], placeholder: "Search people" },
137
+ });
138
+
139
+ await user.type(
140
+ screen.getByRole("searchbox", { name: "Search table" }),
141
+ "grace",
142
+ );
143
+
144
+ expect(warn).toHaveBeenCalledWith(
145
+ "DataTable search.filterColumnIds is empty. It should include at least one column id.",
146
+ );
147
+ expect(
148
+ screen.getByRole("rowheader", { name: "Grace Hopper" }),
149
+ ).toBeVisible();
150
+ expect(
151
+ screen.queryByRole("rowheader", { name: "Ada Lovelace" }),
152
+ ).toBeNull();
153
+
154
+ warn.mockRestore();
155
+ });
156
+
157
+ it("should sort rows when sortable headers are activated", async () => {
158
+ const user = userEvent.setup();
159
+ renderPeopleTable();
160
+
161
+ await user.click(screen.getByRole("columnheader", { name: "Score" }));
162
+ expect(getBodyRows().map((row) => row.textContent)).toEqual([
163
+ expect.stringContaining("Ada Lovelace"),
164
+ expect.stringContaining("Katherine Johnson"),
165
+ expect.stringContaining("Grace Hopper"),
166
+ ]);
167
+
168
+ await user.click(screen.getByRole("columnheader", { name: "Score" }));
169
+ expect(getBodyRows().map((row) => row.textContent)).toEqual([
170
+ expect.stringContaining("Grace Hopper"),
171
+ expect.stringContaining("Katherine Johnson"),
172
+ expect.stringContaining("Ada Lovelace"),
173
+ ]);
174
+ });
175
+
176
+ it("should give duplicate rows distinct ids so selection does not collapse", async () => {
177
+ const user = userEvent.setup();
178
+ const handleSelectionChange = vi.fn();
179
+ const dup: Person = {
180
+ id: "dup",
181
+ name: "Dup",
182
+ email: "dup@example.com",
183
+ role: "Engineer",
184
+ status: "Active",
185
+ score: 1,
186
+ };
187
+ render(
188
+ <DataTable
189
+ aria-label="Duplicates"
190
+ columns={columns}
191
+ data={[dup, dup]}
192
+ enableRowSelection
193
+ onRowSelectionChange={handleSelectionChange}
194
+ />,
195
+ );
196
+
197
+ const checkboxes = screen.getAllByRole("checkbox", { name: "Select Dup" });
198
+ expect(checkboxes).toHaveLength(2);
199
+
200
+ const [firstCheckbox, secondCheckbox] = checkboxes;
201
+
202
+ await user.click(firstCheckbox!);
203
+
204
+ expect(firstCheckbox).toBeChecked();
205
+ expect(secondCheckbox).not.toBeChecked();
206
+ expect(handleSelectionChange).toHaveBeenLastCalledWith(["0"], [dup]);
207
+ });
208
+
209
+ it("should support row selection and bulk actions", async () => {
210
+ const user = userEvent.setup();
211
+ const handleArchive = vi.fn();
212
+ renderPeopleTable({
213
+ enableRowSelection: true,
214
+ bulkActions: [{ label: "Archive selected", onSelect: handleArchive }],
215
+ });
216
+
217
+ await user.click(
218
+ screen.getByRole("checkbox", { name: "Select Ada Lovelace" }),
219
+ );
220
+ await user.click(screen.getByRole("button", { name: "Archive selected" }));
221
+
222
+ expect(handleArchive).toHaveBeenCalledWith([
223
+ expect.objectContaining({ id: "ada", name: "Ada Lovelace" }),
224
+ ]);
225
+ });
226
+
227
+ it("should keep row selection stable across paginated fallback row ids", async () => {
228
+ const user = userEvent.setup();
229
+ const handleSelectionChange = vi.fn();
230
+ const handleArchive = vi.fn();
231
+ render(
232
+ <DataTable
233
+ aria-label="People"
234
+ columns={columns}
235
+ data={people}
236
+ enableRowSelection
237
+ pagination={{ pageSize: 2 }}
238
+ onRowSelectionChange={handleSelectionChange}
239
+ bulkActions={[{ label: "Archive selected", onSelect: handleArchive }]}
240
+ />,
241
+ );
242
+
243
+ await user.click(
244
+ screen.getByRole("checkbox", { name: "Select Ada Lovelace" }),
245
+ );
246
+ await user.click(screen.getByRole("button", { name: "Page 2" }));
247
+
248
+ expect(
249
+ screen.getByRole("checkbox", { name: "Select Katherine Johnson" }),
250
+ ).not.toBeChecked();
251
+
252
+ await user.click(screen.getByRole("button", { name: "Archive selected" }));
253
+
254
+ expect(handleSelectionChange).toHaveBeenLastCalledWith(
255
+ ["0"],
256
+ [expect.objectContaining({ name: "Ada Lovelace" })],
257
+ );
258
+ expect(handleArchive).toHaveBeenCalledWith([
259
+ expect.objectContaining({ name: "Ada Lovelace" }),
260
+ ]);
261
+ });
262
+
263
+ it("should let users hide optional columns", async () => {
264
+ const user = userEvent.setup();
265
+ renderPeopleTable({ enableColumnVisibility: true });
266
+
267
+ await user.click(screen.getByRole("button", { name: "Columns" }));
268
+ await user.click(screen.getByRole("checkbox", { name: "Email column" }));
269
+
270
+ expect(screen.queryByRole("columnheader", { name: "Email" })).toBeNull();
271
+ expect(screen.queryByRole("cell", { name: "ada@example.com" })).toBeNull();
272
+ expect(screen.getByRole("columnheader", { name: "Name" })).toBeVisible();
273
+ });
274
+
275
+ it("should paginate processed rows", async () => {
276
+ const user = userEvent.setup();
277
+ renderPeopleTable({ pagination: { pageSize: 2 } });
278
+
279
+ expect(
280
+ screen.getByRole("rowheader", { name: "Ada Lovelace" }),
281
+ ).toBeVisible();
282
+ expect(
283
+ screen.queryByRole("rowheader", { name: "Katherine Johnson" }),
284
+ ).toBeNull();
285
+
286
+ await user.click(screen.getByRole("button", { name: "Page 2" }));
287
+
288
+ expect(
289
+ screen.getByRole("rowheader", { name: "Katherine Johnson" }),
290
+ ).toBeVisible();
291
+ expect(
292
+ screen.queryByRole("rowheader", { name: "Ada Lovelace" }),
293
+ ).toBeNull();
294
+ });
295
+
296
+ it("should hide the row count when showRowCount is false", () => {
297
+ renderPeopleTable({ pagination: { pageSize: 2 }, showRowCount: false });
298
+
299
+ expect(screen.queryByText("Showing 2 of 3")).toBeNull();
300
+ expect(screen.getByRole("button", { name: "Page 2" })).toBeVisible();
301
+ });
302
+
303
+ it("should offset virtualized rows while scrolling", () => {
304
+ const descriptor = Object.getOwnPropertyDescriptor(
305
+ HTMLElement.prototype,
306
+ "clientHeight",
307
+ );
308
+ Object.defineProperty(HTMLElement.prototype, "clientHeight", {
309
+ configurable: true,
310
+ get() {
311
+ return this.getAttribute("data-slot") === "data-table-virtual-scroll"
312
+ ? 80
313
+ : 0;
314
+ },
315
+ });
316
+ const rows = Array.from({ length: 20 }, (_, index) => ({
317
+ id: `person-${index}`,
318
+ name: `Person ${index}`,
319
+ email: `person-${index}@example.com`,
320
+ role: "Engineer",
321
+ status: "Active" as const,
322
+ score: index,
323
+ }));
324
+
325
+ try {
326
+ render(
327
+ <DataTable
328
+ aria-label="People"
329
+ columns={columns}
330
+ data={rows}
331
+ getRowId={(row) => row.id}
332
+ virtualization={{
333
+ enabled: true,
334
+ rowHeight: 40,
335
+ height: 80,
336
+ overscan: 0,
337
+ }}
338
+ />,
339
+ );
340
+ const scrollArea = document.querySelector(
341
+ '[data-slot="data-table-virtual-scroll"]',
342
+ );
343
+ expect(scrollArea).not.toBeNull();
344
+
345
+ act(() => {
346
+ if (scrollArea) {
347
+ scrollArea.scrollTop = 200;
348
+ fireEvent.scroll(scrollArea);
349
+ }
350
+ });
351
+
352
+ const offset = document.querySelector(
353
+ '[data-slot="data-table-virtual-offset"]',
354
+ );
355
+ expect(offset).toHaveStyle({ transform: "translateY(200px)" });
356
+ expect(screen.getByRole("rowheader", { name: "Person 5" })).toBeVisible();
357
+ } finally {
358
+ if (descriptor) {
359
+ Object.defineProperty(
360
+ HTMLElement.prototype,
361
+ "clientHeight",
362
+ descriptor,
363
+ );
364
+ } else {
365
+ delete (HTMLElement.prototype as { clientHeight?: number })
366
+ .clientHeight;
367
+ }
368
+ }
369
+ });
370
+
371
+ it("should render loading and empty states", () => {
372
+ const { rerender } = render(
373
+ <DataTable
374
+ columns={columns}
375
+ data={[]}
376
+ loading
377
+ loadingContent="Loading people"
378
+ />,
379
+ );
380
+
381
+ expect(screen.getByText("Loading people")).toBeVisible();
382
+
383
+ rerender(
384
+ <DataTable columns={columns} data={[]} emptyContent="No people found" />,
385
+ );
386
+
387
+ expect(screen.getByText("No people found")).toBeVisible();
388
+ });
389
+ });
@@ -0,0 +1,11 @@
1
+ // data-table.tsx — default static entry (no framer-motion)
2
+ import { DataTableBase } from "./data-table-base";
3
+ import type { DataTableProps } from "./types";
4
+
5
+ export function DataTable<TData, TKey extends string = string>(
6
+ props: DataTableProps<TData, TKey>,
7
+ ) {
8
+ return <DataTableBase {...props} />;
9
+ }
10
+
11
+ DataTable.displayName = "DataTable";
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ export { DataTable } from "./data-table";
4
+ export type {
5
+ DataTableBulkAction,
6
+ DataTableCellContext,
7
+ DataTableColumn,
8
+ DataTableColumnTextAlign,
9
+ DataTableColumnValue,
10
+ DataTableHeaderContext,
11
+ DataTablePaginationOptions,
12
+ DataTableProps,
13
+ DataTableSearchOptions,
14
+ DataTableVirtualizationOptions,
15
+ } from "./types";
16
+ export {
17
+ dataTableColumnPanelVariants,
18
+ dataTableRootVariants,
19
+ dataTableStateCellVariants,
20
+ dataTableStatusVariants,
21
+ dataTableToolbarGroupVariants,
22
+ dataTableToolbarVariants,
23
+ dataTableVirtualScrollVariants,
24
+ } from "./variants";
@@ -0,0 +1,121 @@
1
+ import type {
2
+ ComponentPropsWithRef,
3
+ ReactNode,
4
+ Ref,
5
+ TdHTMLAttributes,
6
+ ThHTMLAttributes,
7
+ } from "react";
8
+
9
+ import type { TableProps, TableSortDirection } from "../table";
10
+
11
+ export type DataTableColumnValue =
12
+ | string
13
+ | number
14
+ | boolean
15
+ | Date
16
+ | null
17
+ | undefined;
18
+
19
+ export type DataTableColumnTextAlign = NonNullable<TableProps["textAlign"]>;
20
+
21
+ export type DataTableCellContext<TData> = {
22
+ row: TData;
23
+ value: unknown;
24
+ column: DataTableColumn<TData>;
25
+ rowIndex: number;
26
+ };
27
+
28
+ export type DataTableHeaderContext<TData> = {
29
+ column: DataTableColumn<TData>;
30
+ };
31
+
32
+ export type DataTableColumn<TData, TKey extends string = string> = {
33
+ id: TKey;
34
+ header: ReactNode | ((context: DataTableHeaderContext<TData>) => ReactNode);
35
+ accessor?: keyof TData | ((row: TData) => unknown);
36
+ cell?: (context: DataTableCellContext<TData>) => ReactNode;
37
+ sortable?: boolean;
38
+ sortValue?: (row: TData) => DataTableColumnValue;
39
+ filterable?: boolean;
40
+ filterValue?: (row: TData) => DataTableColumnValue;
41
+ visible?: boolean;
42
+ enableHiding?: boolean;
43
+ textAlign?: DataTableColumnTextAlign;
44
+ className?: string;
45
+ headerClassName?: string;
46
+ cellClassName?: string;
47
+ headerProps?: ThHTMLAttributes<HTMLTableCellElement>;
48
+ cellProps?: TdHTMLAttributes<HTMLTableCellElement>;
49
+ };
50
+
51
+ export type DataTableSearchOptions<TKey extends string = string> = {
52
+ value?: string;
53
+ defaultValue?: string;
54
+ onValueChange?: (value: string) => void;
55
+ placeholder?: string;
56
+ label?: string;
57
+ filterColumnIds?: readonly TKey[];
58
+ };
59
+
60
+ export type DataTablePaginationOptions = {
61
+ pageSize?: number;
62
+ page?: number;
63
+ defaultPage?: number;
64
+ onPageChange?: (page: number) => void;
65
+ siblingCount?: number;
66
+ boundaryCount?: number;
67
+ };
68
+
69
+ export type DataTableBulkAction<TData> = {
70
+ label: ReactNode;
71
+ onSelect: (selectedRows: TData[]) => void;
72
+ disabled?: boolean;
73
+ };
74
+
75
+ export type DataTableVirtualizationOptions = {
76
+ enabled?: boolean;
77
+ rowHeight: number;
78
+ height: number;
79
+ overscan?: number;
80
+ };
81
+
82
+ export type DataTableProps<TData, TKey extends string = string> = Omit<
83
+ ComponentPropsWithRef<"section">,
84
+ "children" | "onChange"
85
+ > &
86
+ Pick<TableProps, "appearance" | "size" | "stickyHeader" | "textAlign"> & {
87
+ data: readonly TData[];
88
+ columns: readonly DataTableColumn<TData, TKey>[];
89
+ getRowId?: (row: TData, index: number) => string;
90
+ caption?: ReactNode;
91
+ tableClassName?: string;
92
+ tableScrollAreaAriaLabel?: string;
93
+ search?: boolean | DataTableSearchOptions<TKey>;
94
+ sortKey?: TKey;
95
+ defaultSortKey?: TKey;
96
+ sortDirection?: TableSortDirection;
97
+ defaultSortDirection?: TableSortDirection;
98
+ onSortChange?: (sort: {
99
+ sortKey?: TKey;
100
+ sortDirection: TableSortDirection;
101
+ }) => void;
102
+ enableRowSelection?: boolean;
103
+ selectedRowIds?: readonly string[];
104
+ defaultSelectedRowIds?: readonly string[];
105
+ onRowSelectionChange?: (
106
+ selectedRowIds: string[],
107
+ selectedRows: TData[],
108
+ ) => void;
109
+ enableColumnVisibility?: boolean;
110
+ visibleColumnIds?: readonly TKey[];
111
+ defaultVisibleColumnIds?: readonly TKey[];
112
+ onColumnVisibilityChange?: (visibleColumnIds: TKey[]) => void;
113
+ bulkActions?: readonly DataTableBulkAction<TData>[];
114
+ pagination?: boolean | DataTablePaginationOptions;
115
+ virtualization?: DataTableVirtualizationOptions;
116
+ showRowCount?: boolean;
117
+ loading?: boolean;
118
+ loadingContent?: ReactNode;
119
+ emptyContent?: ReactNode;
120
+ ref?: Ref<HTMLElement>;
121
+ };
@@ -0,0 +1,21 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiDataTableColumnPanelBase,
5
+ zuiDataTableRootBase,
6
+ zuiDataTableStateCellBase,
7
+ zuiDataTableStatusBase,
8
+ zuiDataTableToolbarBase,
9
+ zuiDataTableToolbarGroupBase,
10
+ zuiDataTableVirtualScrollBase,
11
+ } from "../../design-system/data-table";
12
+
13
+ export const dataTableRootVariants = cva(zuiDataTableRootBase);
14
+ export const dataTableToolbarVariants = cva(zuiDataTableToolbarBase);
15
+ export const dataTableToolbarGroupVariants = cva(zuiDataTableToolbarGroupBase);
16
+ export const dataTableColumnPanelVariants = cva(zuiDataTableColumnPanelBase);
17
+ export const dataTableStatusVariants = cva(zuiDataTableStatusBase);
18
+ export const dataTableStateCellVariants = cva(zuiDataTableStateCellBase);
19
+ export const dataTableVirtualScrollVariants = cva(
20
+ zuiDataTableVirtualScrollBase,
21
+ );
@@ -37,10 +37,12 @@ const useDropdown = () => {
37
37
  ========================= */
38
38
  export const Dropdown = ({
39
39
  children,
40
+ className,
40
41
  defaultOpen = false,
41
42
  open: controlledOpen,
42
43
  onOpenChange,
43
44
  multiSelect = false,
45
+ ...props
44
46
  }: DropdownProps) => {
45
47
  const menuId = `${useId()}-menu`;
46
48
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
@@ -82,7 +84,9 @@ export const Dropdown = ({
82
84
  menuId,
83
85
  }}
84
86
  >
85
- <div className="relative inline-block">{children}</div>
87
+ <div className={cn("relative inline-block", className)} {...props}>
88
+ {children}
89
+ </div>
86
90
  </DropdownContext.Provider>
87
91
  );
88
92
  };
@@ -180,10 +184,10 @@ export const DropdownItem = ({
180
184
  ...props
181
185
  }: DropdownItemProps) => {
182
186
  const { toggleSelect, selectedValues } = useDropdown();
183
- const isSelected = selectedValues.includes(value);
187
+ const isSelected = value !== undefined && selectedValues.includes(value);
184
188
 
185
189
  const handleClick = () => {
186
- toggleSelect(value);
190
+ if (value !== undefined) toggleSelect(value);
187
191
  onSelect?.();
188
192
  };
189
193
 
@@ -14,7 +14,7 @@ export type DropdownContextType = {
14
14
 
15
15
  type Variant = keyof typeof zuiDropdownTriggerVariants;
16
16
 
17
- export type DropdownProps = {
17
+ export type DropdownProps = HTMLAttributes<HTMLDivElement> & {
18
18
  children: ReactNode;
19
19
  defaultOpen?: boolean;
20
20
  open?: boolean;
@@ -37,7 +37,7 @@ export type DropdownContentProps = HTMLAttributes<HTMLDivElement> & {
37
37
 
38
38
  export type DropdownItemProps = HTMLAttributes<HTMLDivElement> & {
39
39
  children: ReactNode;
40
- value: string;
40
+ value?: string;
41
41
  onSelect?: () => void;
42
42
  leftIcon?: ReactNode;
43
43
  rightIcon?: ReactNode;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ export { SplitButton } from "./split-button";
4
+ export type {
5
+ SplitButtonAppearance,
6
+ SplitButtonItem,
7
+ SplitButtonProps,
8
+ SplitButtonSize,
9
+ SplitButtonVariant,
10
+ } from "./types";
11
+ export {
12
+ splitButtonContentVariants,
13
+ splitButtonDropdownVariants,
14
+ splitButtonGroupVariants,
15
+ splitButtonItemDisabledVariants,
16
+ splitButtonPrimaryVariants,
17
+ splitButtonRootVariants,
18
+ splitButtonTriggerVariants,
19
+ } from "./variants";