@happyvertical/smrt-ui 0.42.4 → 0.42.6

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 (48) hide show
  1. package/README.md +157 -3
  2. package/dist/components/data/DataTable.svelte +1097 -111
  3. package/dist/components/data/DataTable.svelte.d.ts +1 -1
  4. package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
  5. package/dist/components/data/DataTableController.d.ts +45 -5
  6. package/dist/components/data/DataTableController.d.ts.map +1 -1
  7. package/dist/components/data/DataTableController.js +153 -11
  8. package/dist/components/data/DataTableLayout.d.ts +37 -0
  9. package/dist/components/data/DataTableLayout.d.ts.map +1 -0
  10. package/dist/components/data/DataTableLayout.js +135 -0
  11. package/dist/components/data/DataTablePerformance.d.ts +31 -0
  12. package/dist/components/data/DataTablePerformance.d.ts.map +1 -0
  13. package/dist/components/data/DataTablePerformance.js +46 -0
  14. package/dist/components/data/DataTableVirtualization.d.ts +71 -0
  15. package/dist/components/data/DataTableVirtualization.d.ts.map +1 -0
  16. package/dist/components/data/DataTableVirtualization.js +100 -0
  17. package/dist/components/data/__benchmarks__/DataTable.bench.d.ts +2 -0
  18. package/dist/components/data/__benchmarks__/DataTable.bench.d.ts.map +1 -0
  19. package/dist/components/data/__benchmarks__/DataTable.bench.js +53 -0
  20. package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts +27 -0
  21. package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts.map +1 -0
  22. package/dist/components/data/__fixtures__/DataTableConformanceFixture.js +141 -0
  23. package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts +10 -0
  24. package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts.map +1 -0
  25. package/dist/components/data/__fixtures__/DataTablePerformanceFixture.js +23 -0
  26. package/dist/components/data/__tests__/DataTable.test.js +393 -8
  27. package/dist/components/data/__tests__/DataTableConformance.test.js +212 -0
  28. package/dist/components/data/__tests__/DataTableController.test.js +124 -2
  29. package/dist/components/data/__tests__/DataTableLayout.test.js +195 -0
  30. package/dist/components/data/__tests__/DataTablePerformance.test.js +21 -0
  31. package/dist/components/data/__tests__/DataTableVirtualization.test.js +80 -0
  32. package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +255 -0
  33. package/dist/components/data/__tests__/data-surface.test.js +675 -0
  34. package/dist/components/data/data-surface.d.ts +246 -0
  35. package/dist/components/data/data-surface.d.ts.map +1 -0
  36. package/dist/components/data/data-surface.js +1102 -0
  37. package/dist/components/data/index.d.ts +3 -0
  38. package/dist/components/data/index.d.ts.map +1 -1
  39. package/dist/components/data/index.js +3 -0
  40. package/dist/components/data/types.d.ts +83 -1
  41. package/dist/components/data/types.d.ts.map +1 -1
  42. package/dist/i18n/strings.d.ts +25 -0
  43. package/dist/i18n/strings.d.ts.map +1 -1
  44. package/dist/i18n/strings.js +27 -2
  45. package/dist/svelte/playground/DataTablePreview.svelte +374 -5
  46. package/dist/svelte/playground/DataTablePreview.svelte.d.ts.map +1 -1
  47. package/dist/svelte/playground.js +2 -2
  48. package/package.json +3 -2
@@ -11,6 +11,8 @@ const state = {
11
11
  { columnId: 'age', visible: true },
12
12
  { columnId: 'name', visible: true },
13
13
  ],
14
+ columnWidths: [],
15
+ columnPinning: [],
14
16
  selection: { scope: 'explicit', rowIds: [] },
15
17
  selectedRowIds: [],
16
18
  expandedRowIds: [],
@@ -155,15 +157,135 @@ describe('DataTableController', () => {
155
157
  modes: controller.getModes(),
156
158
  state: legacyState,
157
159
  })).toMatchObject({
158
- version: 2,
160
+ version: 3,
159
161
  state: { selection: { scope: 'explicit', rowIds: [] } },
160
162
  });
161
- expect(() => hydrateDataTableSnapshot({ version: 3 })).toThrow(/version/);
163
+ expect(() => hydrateDataTableSnapshot({ version: 4 })).toThrow(/version/);
162
164
  expect(() => transitionDataTableState(state, { type: 'setPageSize', pageSize: 0 })).toThrow(/pageSize/);
163
165
  for (const page of [0, -1, 1.5]) {
164
166
  expect(() => transitionDataTableState(state, { type: 'setPage', page })).toThrow(/page/);
165
167
  }
166
168
  });
169
+ it('persists canonical column widths and pinning without resetting the table order', () => {
170
+ const controller = createDataTableController({
171
+ columnIds: ['name', 'age', 'status'],
172
+ initialState: {
173
+ ...state,
174
+ columnOrder: ['status', 'name', 'age'],
175
+ columnWidths: [
176
+ { columnId: 'status', width: 96 },
177
+ { columnId: 'name', width: 220 },
178
+ ],
179
+ columnPinning: [
180
+ { columnId: 'status', position: 'end' },
181
+ { columnId: 'name', position: 'start' },
182
+ ],
183
+ },
184
+ });
185
+ expect(controller.snapshot()).toMatchObject({
186
+ version: 3,
187
+ state: {
188
+ columnOrder: ['status', 'name', 'age'],
189
+ columnWidths: [
190
+ { columnId: 'name', width: 220 },
191
+ { columnId: 'status', width: 96 },
192
+ ],
193
+ columnPinning: [
194
+ { columnId: 'name', position: 'start' },
195
+ { columnId: 'status', position: 'end' },
196
+ ],
197
+ },
198
+ });
199
+ controller.dispatch({ type: 'setColumnWidth', columnId: 'age', width: 80 });
200
+ controller.dispatch({
201
+ type: 'setColumnPin',
202
+ columnId: 'age',
203
+ position: 'start',
204
+ });
205
+ expect(controller.getState().columnWidths).toEqual([
206
+ { columnId: 'age', width: 80 },
207
+ { columnId: 'name', width: 220 },
208
+ { columnId: 'status', width: 96 },
209
+ ]);
210
+ expect(controller.getState().columnPinning).toEqual([
211
+ { columnId: 'age', position: 'start' },
212
+ { columnId: 'name', position: 'start' },
213
+ { columnId: 'status', position: 'end' },
214
+ ]);
215
+ controller.dispatch({
216
+ type: 'setColumnWidth',
217
+ columnId: 'age',
218
+ width: null,
219
+ });
220
+ controller.dispatch({
221
+ type: 'setColumnPin',
222
+ columnId: 'age',
223
+ position: null,
224
+ });
225
+ expect(controller.getState().columnWidths).not.toContainEqual({
226
+ columnId: 'age',
227
+ width: 80,
228
+ });
229
+ expect(controller.getState().columnPinning).not.toContainEqual({
230
+ columnId: 'age',
231
+ position: 'start',
232
+ });
233
+ });
234
+ it('never restores or commands a static hidden column visible', () => {
235
+ const controller = createDataTableController({
236
+ columnIds: ['name', 'internal', 'age'],
237
+ hiddenColumnIds: ['internal'],
238
+ initialState: {
239
+ ...state,
240
+ columnVisibility: [
241
+ { columnId: 'internal', visible: true },
242
+ { columnId: 'name', visible: true },
243
+ ],
244
+ },
245
+ });
246
+ expect(controller.getState().columnVisibility).toContainEqual({
247
+ columnId: 'internal',
248
+ visible: false,
249
+ });
250
+ controller.dispatch({
251
+ type: 'setColumnVisibility',
252
+ columns: [{ columnId: 'internal', visible: true }],
253
+ });
254
+ expect(controller.getState().columnVisibility).toContainEqual({
255
+ columnId: 'internal',
256
+ visible: false,
257
+ });
258
+ controller.replaceState({
259
+ ...controller.getState(),
260
+ columnVisibility: [{ columnId: 'internal', visible: true }],
261
+ });
262
+ expect(controller.getState().columnVisibility).toContainEqual({
263
+ columnId: 'internal',
264
+ visible: false,
265
+ });
266
+ });
267
+ it('restores the saved visibility when a static hidden constraint is removed', () => {
268
+ const controller = createDataTableController({
269
+ columnIds: ['name', 'internal'],
270
+ initialState: {
271
+ ...state,
272
+ columnVisibility: [
273
+ { columnId: 'name', visible: true },
274
+ { columnId: 'internal', visible: true },
275
+ ],
276
+ },
277
+ });
278
+ controller.setColumnIds(['name', 'internal'], ['internal']);
279
+ expect(controller.getState().columnVisibility).toContainEqual({
280
+ columnId: 'internal',
281
+ visible: false,
282
+ });
283
+ controller.setColumnIds(['name', 'internal']);
284
+ expect(controller.getState().columnVisibility).toContainEqual({
285
+ columnId: 'internal',
286
+ visible: true,
287
+ });
288
+ });
167
289
  it('models current-page and explicit row selections separately', () => {
168
290
  const controller = createDataTableController({ initialState: state });
169
291
  controller.dispatch({
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveDataTableLayout } from '../DataTableLayout.js';
3
+ const columns = [
4
+ {
5
+ id: 'account',
6
+ label: 'Account',
7
+ accessor: 'account',
8
+ width: '180px',
9
+ },
10
+ {
11
+ id: 'actual',
12
+ label: 'Actual',
13
+ accessor: 'actual',
14
+ headerPath: [{ id: 'measures', label: 'Measures' }],
15
+ },
16
+ {
17
+ id: 'budget',
18
+ label: 'Budget',
19
+ accessor: 'budget',
20
+ headerPath: [{ id: 'measures', label: 'Measures' }],
21
+ },
22
+ {
23
+ id: 'status',
24
+ label: 'Status',
25
+ accessor: 'status',
26
+ role: 'status',
27
+ responsive: { keepVisible: true, priority: 10 },
28
+ },
29
+ ];
30
+ const state = {
31
+ search: '',
32
+ filters: [],
33
+ sorting: [],
34
+ page: 1,
35
+ pageSize: null,
36
+ columnOrder: ['account', 'actual', 'budget', 'status'],
37
+ columnVisibility: columns.map((column) => ({
38
+ columnId: column.id,
39
+ visible: true,
40
+ })),
41
+ columnWidths: [],
42
+ columnPinning: [],
43
+ selection: { scope: 'explicit', rowIds: [] },
44
+ selectedRowIds: [],
45
+ expandedRowIds: [],
46
+ };
47
+ describe('resolveDataTableLayout', () => {
48
+ it('rebuilds grouped headers after visibility, order, and pinning are resolved', () => {
49
+ const layout = resolveDataTableLayout(columns, {
50
+ ...state,
51
+ columnOrder: ['budget', 'account', 'actual', 'status'],
52
+ columnVisibility: state.columnVisibility.map((entry) => entry.columnId === 'actual' ? { ...entry, visible: false } : entry),
53
+ columnPinning: [
54
+ { columnId: 'account', position: 'start' },
55
+ { columnId: 'status', position: 'end' },
56
+ ],
57
+ columnWidths: [{ columnId: 'account', width: 220 }],
58
+ });
59
+ expect(layout.columns.map(({ column }) => column.id)).toEqual([
60
+ 'account',
61
+ 'budget',
62
+ 'status',
63
+ ]);
64
+ expect(layout.columns[0]).toMatchObject({
65
+ pin: 'start',
66
+ width: 220,
67
+ stickyOffset: '0px',
68
+ });
69
+ expect(layout.columns[2]).toMatchObject({
70
+ pin: 'end',
71
+ stickyOffset: '0px',
72
+ });
73
+ expect(layout.headerRows).toHaveLength(2);
74
+ expect(layout.headerRows[0]).toEqual([
75
+ expect.objectContaining({ kind: 'leaf', rowspan: 2 }),
76
+ expect.objectContaining({
77
+ kind: 'group',
78
+ label: 'Measures',
79
+ colspan: 1,
80
+ }),
81
+ expect.objectContaining({ kind: 'leaf', rowspan: 2 }),
82
+ ]);
83
+ expect(layout.headerRows[1]).toEqual([
84
+ expect.objectContaining({
85
+ kind: 'leaf',
86
+ column: expect.objectContaining({
87
+ column: expect.objectContaining({ id: 'budget' }),
88
+ }),
89
+ }),
90
+ ]);
91
+ });
92
+ it('splits a repeated group when restored order separates its leaves', () => {
93
+ const layout = resolveDataTableLayout(columns, {
94
+ ...state,
95
+ columnOrder: ['actual', 'status', 'budget', 'account'],
96
+ });
97
+ const groups = layout.headerRows[0].filter((cell) => cell.kind === 'group');
98
+ expect(groups).toHaveLength(2);
99
+ expect(groups).toEqual([
100
+ expect.objectContaining({ label: 'Measures', colspan: 1 }),
101
+ expect.objectContaining({ label: 'Measures', colspan: 1 }),
102
+ ]);
103
+ });
104
+ it('keeps an uneven header path rectangular with leaf row spans', () => {
105
+ const layout = resolveDataTableLayout([
106
+ columns[0],
107
+ {
108
+ ...columns[1],
109
+ headerPath: [{ id: 'performance', label: 'Performance' }],
110
+ },
111
+ {
112
+ ...columns[2],
113
+ headerPath: [
114
+ { id: 'performance', label: 'Performance' },
115
+ { id: 'plan', label: 'Plan' },
116
+ ],
117
+ },
118
+ ], {
119
+ ...state,
120
+ columnOrder: ['account', 'actual', 'budget'],
121
+ columnVisibility: [
122
+ { columnId: 'account', visible: true },
123
+ { columnId: 'actual', visible: true },
124
+ { columnId: 'budget', visible: true },
125
+ ],
126
+ });
127
+ expect(layout.headerRows).toHaveLength(3);
128
+ expect(layout.headerRows[0]).toEqual([
129
+ expect.objectContaining({ kind: 'leaf', rowspan: 3 }),
130
+ expect.objectContaining({
131
+ kind: 'group',
132
+ label: 'Performance',
133
+ colspan: 2,
134
+ rowspan: 1,
135
+ }),
136
+ ]);
137
+ expect(layout.headerRows[1]).toEqual([
138
+ expect.objectContaining({ kind: 'leaf', rowspan: 2 }),
139
+ expect.objectContaining({ kind: 'group', label: 'Plan' }),
140
+ ]);
141
+ expect(layout.headerRows[2]).toEqual([
142
+ expect.objectContaining({ kind: 'leaf', rowspan: 1 }),
143
+ ]);
144
+ });
145
+ it('uses measured auto widths for sibling pins and keeps an all-pinned group aligned', () => {
146
+ const layout = resolveDataTableLayout([columns[1], columns[2], columns[0]], {
147
+ ...state,
148
+ columnOrder: ['actual', 'budget', 'account'],
149
+ columnVisibility: [
150
+ { columnId: 'actual', visible: true },
151
+ { columnId: 'budget', visible: true },
152
+ { columnId: 'account', visible: true },
153
+ ],
154
+ columnPinning: [
155
+ { columnId: 'actual', position: 'start' },
156
+ { columnId: 'budget', position: 'start' },
157
+ ],
158
+ }, { actual: 104, budget: 116 });
159
+ expect(layout.columns[0]).toMatchObject({
160
+ column: { id: 'actual' },
161
+ pin: 'start',
162
+ stickyOffset: '0px',
163
+ });
164
+ expect(layout.columns[1]).toMatchObject({
165
+ column: { id: 'budget' },
166
+ pin: 'start',
167
+ stickyOffset: 'calc(0px + 104px)',
168
+ });
169
+ expect(layout.headerRows[0]).toContainEqual(expect.objectContaining({
170
+ kind: 'group',
171
+ label: 'Measures',
172
+ pin: 'start',
173
+ stickyOffset: '0px',
174
+ }));
175
+ });
176
+ it('uses a rendered width before a restored width for pinned offsets', () => {
177
+ const layout = resolveDataTableLayout([columns[1], columns[2]], {
178
+ ...state,
179
+ columnOrder: ['actual', 'budget'],
180
+ columnVisibility: [
181
+ { columnId: 'actual', visible: true },
182
+ { columnId: 'budget', visible: true },
183
+ ],
184
+ columnPinning: [
185
+ { columnId: 'actual', position: 'start' },
186
+ { columnId: 'budget', position: 'start' },
187
+ ],
188
+ columnWidths: [{ columnId: 'actual', width: 80 }],
189
+ }, { actual: 200, budget: 120 });
190
+ expect(layout.columns[1]).toMatchObject({
191
+ column: { id: 'budget' },
192
+ stickyOffset: 'calc(0px + 200px)',
193
+ });
194
+ });
195
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createDataTablePerformanceColumns, createDataTablePerformanceRows, } from '../__fixtures__/DataTablePerformanceFixture.js';
3
+ import { DATA_TABLE_SCALE_THRESHOLDS, recommendDataTableScaleStrategy, } from '../DataTablePerformance.js';
4
+ describe('DataTable scale thresholds', () => {
5
+ it('uses deterministic, stable-identity benchmark fixtures', () => {
6
+ const rows = createDataTablePerformanceRows(3, 2);
7
+ expect(rows.map((row) => row.id)).toEqual(['row-0', 'row-1', 'row-2']);
8
+ expect(rows[2]?.values).toEqual({ value0: 6, value1: 8 });
9
+ expect(createDataTablePerformanceColumns(2).map((column) => column.id)).toEqual(['name', 'group', 'value0', 'value1']);
10
+ });
11
+ it('recommends a bounded scale strategy from documented thresholds', () => {
12
+ expect(recommendDataTableScaleStrategy(250, 20)).toBe('local');
13
+ expect(recommendDataTableScaleStrategy(251, 20)).toBe('virtualization');
14
+ expect(recommendDataTableScaleStrategy(1_001, 2)).toBe('manual-paging');
15
+ expect(DATA_TABLE_SCALE_THRESHOLDS.manualPaging.recommendedPageSize).toBe(100);
16
+ });
17
+ it('rejects ambiguous scale inputs', () => {
18
+ expect(() => recommendDataTableScaleStrategy(-1, 2)).toThrow(/non-negative/);
19
+ expect(() => recommendDataTableScaleStrategy(2.5, 2)).toThrow(/integers/);
20
+ });
21
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { maximumDataTableVirtualScrollTop, resolveDataTableVirtualWindow, scrollTopForDataTableRow, } from '../DataTableVirtualization.js';
3
+ const options = { rowHeight: 20, viewportHeight: 100, overscan: 2 };
4
+ describe('DataTable virtualization window', () => {
5
+ it('renders a fixed-height window with bounded overscan', () => {
6
+ expect(resolveDataTableVirtualWindow({
7
+ options,
8
+ rowCount: 100,
9
+ scrollTop: 200,
10
+ })).toMatchObject({
11
+ enabled: true,
12
+ startIndex: 8,
13
+ endIndex: 17,
14
+ topSpacerHeight: 160,
15
+ bottomSpacerHeight: 1_660,
16
+ totalBodyHeight: 2_000,
17
+ });
18
+ });
19
+ it('clamps a restored scroll position to the body extent', () => {
20
+ expect(resolveDataTableVirtualWindow({
21
+ options,
22
+ rowCount: 10,
23
+ scrollTop: 1_000,
24
+ })).toMatchObject({
25
+ startIndex: 3,
26
+ endIndex: 10,
27
+ topSpacerHeight: 60,
28
+ bottomSpacerHeight: 0,
29
+ });
30
+ });
31
+ it('falls back to a semantic full body for variable-height data rows', () => {
32
+ expect(resolveDataTableVirtualWindow({
33
+ options,
34
+ rowCount: 12,
35
+ scrollTop: 100,
36
+ hasVariableRowHeight: true,
37
+ headerRowCount: 2,
38
+ summaryRowCount: 1,
39
+ })).toEqual({
40
+ enabled: false,
41
+ reason: 'variable-row-height',
42
+ startIndex: 0,
43
+ endIndex: 12,
44
+ topSpacerHeight: 0,
45
+ bottomSpacerHeight: 0,
46
+ totalBodyHeight: 0,
47
+ headerRowCount: 2,
48
+ summaryRowCount: 1,
49
+ });
50
+ });
51
+ it('keeps headers and summaries outside the body calculation', () => {
52
+ const withStructuralRows = resolveDataTableVirtualWindow({
53
+ options,
54
+ rowCount: 100,
55
+ scrollTop: 0,
56
+ headerRowCount: 2,
57
+ summaryRowCount: 3,
58
+ });
59
+ expect(withStructuralRows.totalBodyHeight).toBe(2_000);
60
+ expect(withStructuralRows.headerRowCount).toBe(2);
61
+ expect(withStructuralRows.summaryRowCount).toBe(3);
62
+ });
63
+ it('extends the virtual scroll range by a measured summary footer', () => {
64
+ expect(maximumDataTableVirtualScrollTop(100, options, 24)).toBe(1_924);
65
+ expect(() => maximumDataTableVirtualScrollTop(1, options, -1)).toThrow(/footerHeight/);
66
+ });
67
+ it('derives scroll restoration from a row position, not a display key', () => {
68
+ expect(scrollTopForDataTableRow(0, 100, options, 400)).toBe(0);
69
+ expect(scrollTopForDataTableRow(25, 100, options, 0)).toBe(420);
70
+ expect(scrollTopForDataTableRow(99, 100, options, 0)).toBe(1_900);
71
+ });
72
+ it('rejects invalid fixed-height configuration', () => {
73
+ expect(() => resolveDataTableVirtualWindow({
74
+ options: { rowHeight: 0, viewportHeight: 100 },
75
+ rowCount: 1,
76
+ scrollTop: 0,
77
+ })).toThrow(/rowHeight/);
78
+ expect(() => scrollTopForDataTableRow(2, 2, options)).toThrow(/rowIndex/);
79
+ });
80
+ });