@dashforge/tw 0.5.0-beta → 0.7.0-beta

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 (105) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/THEME-AUDIT.md +175 -0
  3. package/dist/index.esm.js +2504 -51
  4. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
  5. package/dist/src/components/Box/box.variants.d.ts.map +1 -1
  6. package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
  7. package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
  8. package/dist/src/components/DataGrid/dataGrid.types.d.ts +171 -0
  9. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
  11. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
  12. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
  13. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
  14. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
  15. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
  16. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
  17. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
  18. package/dist/src/components/Table/Table.d.ts +24 -0
  19. package/dist/src/components/Table/Table.d.ts.map +1 -0
  20. package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
  21. package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
  22. package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
  23. package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
  24. package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
  25. package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
  26. package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
  27. package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
  28. package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
  29. package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
  30. package/dist/src/components/Table/cells/index.d.ts +11 -0
  31. package/dist/src/components/Table/cells/index.d.ts.map +1 -0
  32. package/dist/src/components/Table/table.types.d.ts +312 -0
  33. package/dist/src/components/Table/table.types.d.ts.map +1 -0
  34. package/dist/src/components/Table/table.variants.d.ts +285 -0
  35. package/dist/src/components/Table/table.variants.d.ts.map +1 -0
  36. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  37. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
  38. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  39. package/dist/src/components/_shared/data/getNestedValue.d.ts +20 -0
  40. package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
  41. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts +45 -0
  42. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
  43. package/dist/src/components/_shared/data/useControllableState.d.ts +23 -0
  44. package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
  45. package/dist/src/components/_shared/data/useDebouncedValue.d.ts +10 -0
  46. package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
  47. package/dist/src/components/_shared/data/useTableFilter.d.ts +13 -0
  48. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
  49. package/dist/src/components/_shared/data/useTableSearch.d.ts +23 -0
  50. package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
  51. package/dist/src/components/_shared/data/useTableSelection.d.ts +25 -0
  52. package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
  53. package/dist/src/components/_shared/data/useTableSort.d.ts +20 -0
  54. package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
  55. package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
  56. package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
  57. package/dist/src/index.d.ts +10 -1
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/package.json +3 -3
  60. package/src/components/Accordion/accordion.variants.ts +5 -3
  61. package/src/components/Box/Box.test.tsx +32 -11
  62. package/src/components/Box/box.variants.ts +23 -4
  63. package/src/components/DataGrid/DataGrid.test.tsx +496 -0
  64. package/src/components/DataGrid/DataGrid.tsx +831 -0
  65. package/src/components/DataGrid/dataGrid.types.ts +189 -0
  66. package/src/components/DataGrid/dataGrid.variants.ts +144 -0
  67. package/src/components/Dialog/dialog.variants.ts +10 -6
  68. package/src/components/Divider/Divider.test.tsx +3 -1
  69. package/src/components/Divider/Divider.tsx +1 -1
  70. package/src/components/Divider/divider.variants.ts +2 -1
  71. package/src/components/Pagination/Pagination.tsx +3 -1
  72. package/src/components/Pagination/pagination.variants.ts +19 -13
  73. package/src/components/Popover/popover.variants.ts +10 -4
  74. package/src/components/Skeleton/skeleton.variants.ts +2 -1
  75. package/src/components/Table/Table.test.tsx +603 -0
  76. package/src/components/Table/Table.tsx +816 -0
  77. package/src/components/Table/cells/RenderButton.tsx +23 -0
  78. package/src/components/Table/cells/RenderChip.tsx +103 -0
  79. package/src/components/Table/cells/RenderText.tsx +37 -0
  80. package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
  81. package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
  82. package/src/components/Table/cells/index.ts +14 -0
  83. package/src/components/Table/table.types.ts +346 -0
  84. package/src/components/Table/table.variants.ts +207 -0
  85. package/src/components/Tabs/tabs.variants.ts +19 -8
  86. package/src/components/Tooltip/tooltip.variants.ts +12 -2
  87. package/src/components/Typography/Typography.test.tsx +3 -1
  88. package/src/components/Typography/typography.variants.ts +6 -2
  89. package/src/components/_shared/data/getNestedValue.test.ts +50 -0
  90. package/src/components/_shared/data/getNestedValue.ts +32 -0
  91. package/src/components/_shared/data/useColumnAutoDetect.test.ts +123 -0
  92. package/src/components/_shared/data/useColumnAutoDetect.ts +100 -0
  93. package/src/components/_shared/data/useControllableState.ts +48 -0
  94. package/src/components/_shared/data/useDebouncedValue.ts +24 -0
  95. package/src/components/_shared/data/useTableFilter.ts +37 -0
  96. package/src/components/_shared/data/useTableSearch.test.ts +115 -0
  97. package/src/components/_shared/data/useTableSearch.ts +66 -0
  98. package/src/components/_shared/data/useTableSelection.test.ts +100 -0
  99. package/src/components/_shared/data/useTableSelection.ts +100 -0
  100. package/src/components/_shared/data/useTableSort.test.ts +150 -0
  101. package/src/components/_shared/data/useTableSort.ts +141 -0
  102. package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
  103. package/src/components/_shared/data/useVirtualizer.ts +199 -0
  104. package/src/components/_shared/themeIdentity.test.ts +113 -0
  105. package/src/index.ts +59 -1
@@ -0,0 +1,496 @@
1
+ // @vitest-environment jsdom
2
+ import * as React from 'react';
3
+ import { describe, it, expect, afterEach, beforeAll } from 'vitest';
4
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
5
+ import { DataGrid } from './DataGrid.js';
6
+ import type { TableColumn } from '../Table/table.types.js';
7
+
8
+ void React;
9
+ afterEach(() => cleanup());
10
+
11
+ // jsdom doesn't implement ResizeObserver / IntersectionObserver —
12
+ // virtualizer + Radix Popover (used via shared cells lib) need them.
13
+ beforeAll(() => {
14
+ if (typeof globalThis.ResizeObserver === 'undefined') {
15
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
16
+ observe() {}
17
+ unobserve() {}
18
+ disconnect() {}
19
+ };
20
+ }
21
+ // jsdom returns 0 for clientHeight by default; the virtualizer
22
+ // uses initialViewportHeight=400 in the absence of a real measurement.
23
+ });
24
+
25
+ interface User {
26
+ id: string;
27
+ name: string;
28
+ email: string;
29
+ age: number;
30
+ status: 'active' | 'pending';
31
+ meta: { city: string };
32
+ }
33
+
34
+ const baseCols: TableColumn<User>[] = [
35
+ { field: 'name', header: 'Name', sortable: true, searchable: true },
36
+ { field: 'email', header: 'Email', searchable: true },
37
+ { field: 'age', header: 'Age', sortable: true },
38
+ { field: 'meta.city' as never, header: 'City' },
39
+ ];
40
+
41
+ function generateUsers(count: number): User[] {
42
+ return Array.from({ length: count }).map((_, i) => ({
43
+ id: `u${i}`,
44
+ name: `User ${i}`,
45
+ email: `user${i}@example.com`,
46
+ age: 20 + (i % 50),
47
+ status: i % 2 === 0 ? ('active' as const) : ('pending' as const),
48
+ meta: { city: ['Roma', 'Milano', 'Berlin'][i % 3] as string },
49
+ }));
50
+ }
51
+
52
+ const getRowId = (row: User) => row.id;
53
+
54
+ describe('DataGrid — rendering', () => {
55
+ it('renders a <table> element', () => {
56
+ const { container } = render(
57
+ <DataGrid
58
+ rows={generateUsers(50)}
59
+ cols={baseCols}
60
+ getRowId={getRowId}
61
+ rowHeight={48}
62
+ height="400px"
63
+ />,
64
+ );
65
+ expect(container.querySelector('table')).not.toBeNull();
66
+ });
67
+
68
+ it('renders column headers', () => {
69
+ render(
70
+ <DataGrid
71
+ rows={generateUsers(20)}
72
+ cols={baseCols}
73
+ getRowId={getRowId}
74
+ rowHeight={48}
75
+ height="400px"
76
+ />,
77
+ );
78
+ expect(screen.queryByText('Name')).not.toBeNull();
79
+ expect(screen.queryByText('Email')).not.toBeNull();
80
+ expect(screen.queryByText('Age')).not.toBeNull();
81
+ });
82
+
83
+ it('renders only a window of rows (virtualized)', () => {
84
+ const { container } = render(
85
+ <DataGrid
86
+ rows={generateUsers(1000)}
87
+ cols={baseCols}
88
+ getRowId={getRowId}
89
+ rowHeight={48}
90
+ height="400px"
91
+ />,
92
+ );
93
+ // Visible window with 400px height + 48px rows = ~9 rows + overscan
94
+ // (5 top + 5 bottom). Initial viewport height fallback = 400px.
95
+ // So at scrollTop=0 we render rows 0 to ~13 (no top overscan needed).
96
+ const dataRows = container.querySelectorAll('tbody tr[data-selected]');
97
+ expect(dataRows.length).toBeLessThan(50); // certainly not all 1000
98
+ expect(dataRows.length).toBeGreaterThan(5); // at least the visible window
99
+ });
100
+
101
+ it('renders spacer rows above and below the visible window', () => {
102
+ const { container } = render(
103
+ <DataGrid
104
+ rows={generateUsers(1000)}
105
+ cols={baseCols}
106
+ getRowId={getRowId}
107
+ rowHeight={48}
108
+ height="400px"
109
+ />,
110
+ );
111
+ // Spacer rows are <tr aria-hidden="true"> with inline height style.
112
+ const spacers = container.querySelectorAll('tbody tr[aria-hidden="true"]');
113
+ expect(spacers.length).toBeGreaterThanOrEqual(1);
114
+ });
115
+
116
+ it('renders empty state when rows is empty', () => {
117
+ render(
118
+ <DataGrid
119
+ rows={[]}
120
+ cols={baseCols}
121
+ getRowId={getRowId}
122
+ rowHeight={48}
123
+ height="400px"
124
+ />,
125
+ );
126
+ expect(screen.queryByText('No data')).not.toBeNull();
127
+ });
128
+
129
+ it('renders loading skeleton rows', () => {
130
+ const { container } = render(
131
+ <DataGrid
132
+ rows={[]}
133
+ cols={baseCols}
134
+ getRowId={getRowId}
135
+ rowHeight={48}
136
+ height="400px"
137
+ loading
138
+ loadingRowCount={6}
139
+ />,
140
+ );
141
+ const busyRows = container.querySelectorAll('tr[aria-busy="true"]');
142
+ expect(busyRows.length).toBe(6);
143
+ });
144
+ });
145
+
146
+ describe('DataGrid — sort', () => {
147
+ it('sortable header has aria-sort=none initially', () => {
148
+ render(
149
+ <DataGrid
150
+ rows={generateUsers(20)}
151
+ cols={baseCols}
152
+ getRowId={getRowId}
153
+ rowHeight={48}
154
+ height="400px"
155
+ />,
156
+ );
157
+ const header = screen.getByText('Name').closest('th');
158
+ expect(header?.getAttribute('aria-sort')).toBe('none');
159
+ });
160
+
161
+ it('click sortable header → asc', () => {
162
+ render(
163
+ <DataGrid
164
+ rows={generateUsers(20)}
165
+ cols={baseCols}
166
+ getRowId={getRowId}
167
+ rowHeight={48}
168
+ height="400px"
169
+ />,
170
+ );
171
+ fireEvent.click(screen.getByText('Name').closest('button')!);
172
+ expect(screen.getByText('Name').closest('th')?.getAttribute('aria-sort')).toBe(
173
+ 'ascending',
174
+ );
175
+ });
176
+
177
+ it('serverSideSort prevents local sort, emits onSortChange', () => {
178
+ let lastModel: unknown = null;
179
+ const rows = generateUsers(20);
180
+ const { container } = render(
181
+ <DataGrid
182
+ rows={rows}
183
+ cols={baseCols}
184
+ getRowId={getRowId}
185
+ rowHeight={48}
186
+ height="400px"
187
+ serverSideSort
188
+ onSortChange={(m) => (lastModel = m)}
189
+ />,
190
+ );
191
+ const firstNameBeforeSort = container.querySelector('tbody tr[data-selected] td')?.textContent;
192
+ fireEvent.click(screen.getByText('Name').closest('button')!);
193
+ expect(lastModel).toEqual([{ field: 'name', direction: 'asc' }]);
194
+ // Row order didn't change since server-side: still original order.
195
+ const firstNameAfterSort = container.querySelector('tbody tr[data-selected] td')?.textContent;
196
+ expect(firstNameAfterSort).toBe(firstNameBeforeSort);
197
+ });
198
+ });
199
+
200
+ describe('DataGrid — search', () => {
201
+ it('renders search input when enableSearch', () => {
202
+ render(
203
+ <DataGrid
204
+ rows={generateUsers(20)}
205
+ cols={baseCols}
206
+ getRowId={getRowId}
207
+ rowHeight={48}
208
+ height="400px"
209
+ enableSearch
210
+ />,
211
+ );
212
+ expect(screen.getByPlaceholderText('Search...')).not.toBeNull();
213
+ });
214
+
215
+ it('filters rows by search query (client-side)', () => {
216
+ const { container } = render(
217
+ <DataGrid
218
+ rows={generateUsers(20)}
219
+ cols={baseCols}
220
+ getRowId={getRowId}
221
+ rowHeight={48}
222
+ height="400px"
223
+ enableSearch
224
+ searchDebounceMs={0}
225
+ />,
226
+ );
227
+ const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
228
+ fireEvent.change(input, { target: { value: 'User 5' } });
229
+ // Only rows with 'User 5' in name/email survive (User 5, User 15, ...)
230
+ const rows = container.querySelectorAll('tbody tr[data-selected]');
231
+ expect(rows.length).toBeGreaterThan(0);
232
+ expect(rows.length).toBeLessThan(20);
233
+ });
234
+
235
+ it('serverSideSearch prevents local filter, emits onSearchQueryChange', () => {
236
+ let lastQuery = '';
237
+ render(
238
+ <DataGrid
239
+ rows={generateUsers(20)}
240
+ cols={baseCols}
241
+ getRowId={getRowId}
242
+ rowHeight={48}
243
+ height="400px"
244
+ enableSearch
245
+ searchDebounceMs={0}
246
+ serverSideSearch
247
+ onSearchQueryChange={(q) => (lastQuery = q)}
248
+ />,
249
+ );
250
+ const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
251
+ fireEvent.change(input, { target: { value: 'alice' } });
252
+ expect(lastQuery).toBe('alice');
253
+ });
254
+ });
255
+
256
+ describe('DataGrid — selection', () => {
257
+ it('renders checkbox for each visible row when rowSelection=multiple', () => {
258
+ const { container } = render(
259
+ <DataGrid
260
+ rows={generateUsers(20)}
261
+ cols={baseCols}
262
+ getRowId={getRowId}
263
+ rowHeight={48}
264
+ height="400px"
265
+ rowSelection="multiple"
266
+ />,
267
+ );
268
+ // 1 header + N row checkboxes (N = visible window).
269
+ const checkboxes = container.querySelectorAll('input[type="checkbox"]');
270
+ expect(checkboxes.length).toBeGreaterThan(2);
271
+ });
272
+
273
+ it('toggleRow updates selectedRowIds (controlled)', () => {
274
+ let lastIds: string[] = [];
275
+ const { container } = render(
276
+ <DataGrid
277
+ rows={generateUsers(10)}
278
+ cols={baseCols}
279
+ getRowId={getRowId}
280
+ rowHeight={48}
281
+ height="400px"
282
+ rowSelection="multiple"
283
+ selectedRowIds={[]}
284
+ onSelectionChange={(ids) => (lastIds = ids)}
285
+ />,
286
+ );
287
+ const firstRowCheckbox = container.querySelector(
288
+ 'tbody tr[data-selected] input[type="checkbox"]',
289
+ ) as HTMLInputElement;
290
+ fireEvent.click(firstRowCheckbox);
291
+ expect(lastIds.length).toBe(1);
292
+ });
293
+
294
+ it('selectAllScope=allLoaded toggles every row in the sorted set', () => {
295
+ let lastIds: string[] = [];
296
+ const { container } = render(
297
+ <DataGrid
298
+ rows={generateUsers(50)}
299
+ cols={baseCols}
300
+ getRowId={getRowId}
301
+ rowHeight={48}
302
+ height="400px"
303
+ rowSelection="multiple"
304
+ selectAllScope="allLoaded"
305
+ selectedRowIds={[]}
306
+ onSelectionChange={(ids) => (lastIds = ids)}
307
+ />,
308
+ );
309
+ const headerCheckbox = container.querySelector(
310
+ 'thead input[type="checkbox"]',
311
+ ) as HTMLInputElement;
312
+ fireEvent.click(headerCheckbox);
313
+ expect(lastIds.length).toBe(50); // all 50 rows toggled
314
+ });
315
+
316
+ it('selectAllScope=visible toggles only rendered rows', () => {
317
+ let lastIds: string[] = [];
318
+ const { container } = render(
319
+ <DataGrid
320
+ rows={generateUsers(1000)}
321
+ cols={baseCols}
322
+ getRowId={getRowId}
323
+ rowHeight={48}
324
+ height="400px"
325
+ rowSelection="multiple"
326
+ selectAllScope="visible"
327
+ selectedRowIds={[]}
328
+ onSelectionChange={(ids) => (lastIds = ids)}
329
+ />,
330
+ );
331
+ const headerCheckbox = container.querySelector(
332
+ 'thead input[type="checkbox"]',
333
+ ) as HTMLInputElement;
334
+ fireEvent.click(headerCheckbox);
335
+ // Only visible window — way less than 1000
336
+ expect(lastIds.length).toBeLessThan(50);
337
+ expect(lastIds.length).toBeGreaterThan(0);
338
+ });
339
+
340
+ it('renders bulk action footer when selection > 0', () => {
341
+ const { rerender } = render(
342
+ <DataGrid
343
+ rows={generateUsers(10)}
344
+ cols={baseCols}
345
+ getRowId={getRowId}
346
+ rowHeight={48}
347
+ height="400px"
348
+ rowSelection="multiple"
349
+ bulkActions={(rows) => <span>Bulk × {rows.length}</span>}
350
+ />,
351
+ );
352
+ expect(screen.queryByText(/Bulk ×/)).toBeNull();
353
+ rerender(
354
+ <DataGrid
355
+ rows={generateUsers(10)}
356
+ cols={baseCols}
357
+ getRowId={getRowId}
358
+ rowHeight={48}
359
+ height="400px"
360
+ rowSelection="multiple"
361
+ selectedRowIds={['u0', 'u1']}
362
+ bulkActions={(rows) => <span>Bulk × {rows.length}</span>}
363
+ />,
364
+ );
365
+ expect(screen.getByText('Bulk × 2')).not.toBeNull();
366
+ });
367
+ });
368
+
369
+ describe('DataGrid — RBAC', () => {
370
+ it('per-column hide removes the column entirely', () => {
371
+ const colsWithHide: TableColumn<User>[] = [
372
+ baseCols[0]!,
373
+ {
374
+ ...baseCols[1]!,
375
+ access: { resource: 'email', action: 'read', onUnauthorized: 'hide' },
376
+ },
377
+ baseCols[2]!,
378
+ ];
379
+ render(
380
+ <DataGrid
381
+ rows={generateUsers(20)}
382
+ cols={colsWithHide}
383
+ getRowId={getRowId}
384
+ rowHeight={48}
385
+ height="400px"
386
+ />,
387
+ );
388
+ expect(screen.queryByText('Email')).toBeNull();
389
+ });
390
+ });
391
+
392
+ describe('DataGrid — sticky left column', () => {
393
+ it('applies sticky CSS class to sticky="left" columns', () => {
394
+ const colsSticky: TableColumn<User>[] = [
395
+ { ...baseCols[0]!, sticky: 'left' },
396
+ ...baseCols.slice(1),
397
+ ];
398
+ render(
399
+ <DataGrid
400
+ rows={generateUsers(20)}
401
+ cols={colsSticky}
402
+ getRowId={getRowId}
403
+ rowHeight={48}
404
+ height="400px"
405
+ />,
406
+ );
407
+ const nameHeader = screen.getByText('Name').closest('th');
408
+ expect(nameHeader?.className).toContain('sticky');
409
+ expect(nameHeader?.className).toContain('left-0');
410
+ });
411
+ });
412
+
413
+ describe('DataGrid — internal pagination', () => {
414
+ it('renders Pagination footer when pagination prop is set', () => {
415
+ render(
416
+ <DataGrid
417
+ rows={generateUsers(100)}
418
+ cols={baseCols}
419
+ getRowId={getRowId}
420
+ rowHeight={48}
421
+ height="400px"
422
+ pagination={{
423
+ page: 1,
424
+ pageSize: 20,
425
+ onPageChange: () => {},
426
+ }}
427
+ />,
428
+ );
429
+ // Pagination renders a <nav aria-label="Pagination">
430
+ expect(screen.queryByRole('navigation', { name: 'Pagination' })).not.toBeNull();
431
+ });
432
+ });
433
+
434
+ describe('DataGrid — smart defaults', () => {
435
+ it('right-aligns number columns (Age)', () => {
436
+ render(
437
+ <DataGrid
438
+ rows={generateUsers(20)}
439
+ cols={baseCols}
440
+ getRowId={getRowId}
441
+ rowHeight={48}
442
+ height="400px"
443
+ />,
444
+ );
445
+ const ageHeader = screen.getByText('Age').closest('th');
446
+ expect(ageHeader?.className).toContain('text-right');
447
+ });
448
+
449
+ it('auto-applies tabular-nums to number cells (no font-mono)', () => {
450
+ const { container } = render(
451
+ <DataGrid
452
+ rows={generateUsers(5)}
453
+ cols={baseCols}
454
+ getRowId={getRowId}
455
+ rowHeight={48}
456
+ height="400px"
457
+ />,
458
+ );
459
+ const ageCell = container.querySelector('tbody tr[data-selected] td:nth-child(3)');
460
+ expect(ageCell?.className).toContain('tabular-nums');
461
+ expect(ageCell?.className).not.toContain('font-mono');
462
+ });
463
+ });
464
+
465
+ describe('DataGrid — i18n', () => {
466
+ it('column headers accept translated strings', () => {
467
+ const colsTranslated: TableColumn<User>[] = [
468
+ { field: 'name', header: /* t('users.name') */ 'Nome', sortable: true },
469
+ ];
470
+ render(
471
+ <DataGrid
472
+ rows={generateUsers(5)}
473
+ cols={colsTranslated}
474
+ getRowId={getRowId}
475
+ rowHeight={48}
476
+ height="400px"
477
+ />,
478
+ );
479
+ expect(screen.getByText('Nome')).not.toBeNull();
480
+ });
481
+
482
+ it('labels prop translates internal a11y strings', () => {
483
+ render(
484
+ <DataGrid
485
+ rows={generateUsers(5)}
486
+ cols={baseCols}
487
+ getRowId={getRowId}
488
+ rowHeight={48}
489
+ height="400px"
490
+ rowSelection="multiple"
491
+ labels={{ ariaSelectRow: 'Seleziona riga', ariaSelectAllRows: 'Seleziona tutto' }}
492
+ />,
493
+ );
494
+ expect(screen.queryByLabelText('Seleziona tutto')).not.toBeNull();
495
+ });
496
+ });