@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
@@ -1,8 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
+ import {
4
+ createDataTableConformanceRows,
5
+ dataTableConformanceColumns,
6
+ dataTableConformanceRows,
7
+ dataTableConformanceScenarios,
8
+ } from '../../components/data/__fixtures__/DataTableConformanceFixture.js';
3
9
  import DataTable from '../../components/data/DataTable.svelte';
4
10
  import { createDataTableController } from '../../components/data/DataTableController.js';
5
- import type { DataTableColumn } from '../../components/data/types.js';
11
+ import type {
12
+ DataTableColumn,
13
+ DataTableStructuralRow,
14
+ } from '../../components/data/types.js';
6
15
  import Toggle from '../../components/forms/Toggle.svelte';
7
16
  import Badge from '../../components/ui/Badge.svelte';
8
17
  import Button from '../../components/ui/Button.svelte';
@@ -16,6 +25,16 @@ interface WorkspaceUser {
16
25
  lastSeen: string;
17
26
  }
18
27
 
28
+ interface ReportLine {
29
+ id: string;
30
+ account: string;
31
+ actual: number;
32
+ forecast: number;
33
+ variance: number;
34
+ status: string;
35
+ action: string;
36
+ }
37
+
19
38
  const users: WorkspaceUser[] = [
20
39
  {
21
40
  id: 'user-ada',
@@ -67,20 +86,157 @@ const columns: DataTableColumn<WorkspaceUser>[] = [
67
86
  { id: 'lastSeen', label: 'Last seen', sortable: true, align: 'right' },
68
87
  ];
69
88
 
89
+ const reportRows: ReportLine[] = [
90
+ {
91
+ id: 'revenue',
92
+ account: 'Subscription revenue',
93
+ actual: 182400,
94
+ forecast: 176000,
95
+ variance: 6400,
96
+ status: 'On track',
97
+ action: 'Review',
98
+ },
99
+ {
100
+ id: 'services',
101
+ account: 'Professional services',
102
+ actual: 48300,
103
+ forecast: 52000,
104
+ variance: -3700,
105
+ status: 'Watch',
106
+ action: 'Review',
107
+ },
108
+ ];
109
+
110
+ const reportColumns: DataTableColumn<ReportLine>[] = [
111
+ {
112
+ id: 'account',
113
+ label: 'Account',
114
+ headerPath: [{ id: 'dimension', label: 'Dimensions' }],
115
+ minWidth: '13rem',
116
+ resizable: true,
117
+ },
118
+ {
119
+ id: 'actual',
120
+ label: 'Actual',
121
+ headerPath: [{ id: 'performance', label: 'Performance' }],
122
+ align: 'right',
123
+ resizable: true,
124
+ },
125
+ {
126
+ id: 'forecast',
127
+ label: 'Forecast',
128
+ headerPath: [{ id: 'performance', label: 'Performance' }],
129
+ align: 'right',
130
+ resizable: true,
131
+ },
132
+ {
133
+ id: 'variance',
134
+ label: 'Variance',
135
+ headerPath: [{ id: 'performance', label: 'Performance' }],
136
+ align: 'right',
137
+ },
138
+ {
139
+ id: 'status',
140
+ label: 'Status',
141
+ role: 'status',
142
+ responsive: { keepVisible: true, priority: 10 },
143
+ },
144
+ {
145
+ id: 'action',
146
+ label: 'Action',
147
+ role: 'action',
148
+ responsive: { keepVisible: true, priority: 10 },
149
+ },
150
+ ];
151
+
152
+ const reportStructuralRows: DataTableStructuralRow<ReportLine>[] = [
153
+ {
154
+ id: 'forecast-subtotal',
155
+ kind: 'subtotal',
156
+ label: 'Current forecast',
157
+ values: { actual: 230700, forecast: 228000, variance: 2700 },
158
+ },
159
+ {
160
+ id: 'forecast-total',
161
+ kind: 'footer',
162
+ label: 'All accounts',
163
+ values: { actual: 230700, forecast: 228000, variance: 2700 },
164
+ },
165
+ ];
166
+
70
167
  let loading = $state(false);
71
168
  let dense = $state(false);
72
169
  const tableController = createDataTableController({
73
170
  columnIds: columns.map((column) => column.id),
74
171
  initialState: { pageSize: 3 },
75
172
  });
173
+ const reportController = createDataTableController({
174
+ columnIds: reportColumns.map((column) => column.id),
175
+ initialState: {
176
+ columnOrder: reportColumns.map((column) => column.id),
177
+ columnWidths: [
178
+ { columnId: 'account', width: 240 },
179
+ { columnId: 'actual', width: 112 },
180
+ { columnId: 'forecast', width: 112 },
181
+ ],
182
+ columnPinning: [
183
+ { columnId: 'account', position: 'start' },
184
+ { columnId: 'action', position: 'end' },
185
+ ],
186
+ },
187
+ });
188
+ const manualController = createDataTableController({
189
+ columnIds: dataTableConformanceColumns.map((column) => column.id),
190
+ modes: { filtering: 'manual', sorting: 'manual', pagination: 'manual' },
191
+ initialState: { page: 2, pageSize: 25 },
192
+ });
193
+ const virtualRows = createDataTableConformanceRows(120);
194
+ const virtualColumns = dataTableConformanceColumns.slice(0, 2);
195
+ const virtualization = { rowHeight: 32, viewportHeight: 176, overscan: 3 };
196
+ const manualQueryCommands = new Set([
197
+ 'setSearch',
198
+ 'setFilters',
199
+ 'setSorting',
200
+ 'toggleSorting',
201
+ 'setPage',
202
+ 'setPageSize',
203
+ ]);
76
204
  let tableState = $state(tableController.getState());
77
-
78
- onMount(() =>
79
- tableController.subscribe((transition) => {
80
- tableState = transition.next.state;
205
+ let manualState = $state(manualController.getState());
206
+ let manualLifecycle = $state<'ready' | 'refreshing' | 'stale' | 'error'>(
207
+ 'ready',
208
+ );
209
+ let manualQueryRevision = $state(1);
210
+ const manualQueryFingerprint = $derived(
211
+ JSON.stringify({
212
+ search: manualState.search,
213
+ filters: manualState.filters,
214
+ sorting: manualState.sorting,
215
+ page: manualState.page,
216
+ pageSize: manualState.pageSize,
81
217
  }),
82
218
  );
83
219
 
220
+ onMount(() => {
221
+ const unsubscribeTable = tableController.subscribe((transition) => {
222
+ tableState = transition.next.state;
223
+ });
224
+ const unsubscribeManual = manualController.subscribe((transition) => {
225
+ manualState = transition.next.state;
226
+ if (
227
+ transition.command &&
228
+ manualQueryCommands.has(transition.command.type)
229
+ ) {
230
+ manualQueryRevision += 1;
231
+ manualLifecycle = 'refreshing';
232
+ }
233
+ });
234
+ return () => {
235
+ unsubscribeTable();
236
+ unsubscribeManual();
237
+ };
238
+ });
239
+
84
240
  function clearSelection() {
85
241
  tableController.dispatch({ type: 'setSelectedRows', rowIds: [] });
86
242
  }
@@ -93,6 +249,62 @@ function toggleActiveFilter() {
93
249
  : [{ columnId: 'status', operator: 'equals', value: 'Active' }],
94
250
  });
95
251
  }
252
+
253
+ function restoreReportLayout() {
254
+ reportController.replaceState({
255
+ ...reportController.getState(),
256
+ columnOrder: reportColumns.map((column) => column.id),
257
+ columnVisibility: reportColumns.map((column) => ({
258
+ columnId: column.id,
259
+ visible: true,
260
+ })),
261
+ columnWidths: [
262
+ { columnId: 'account', width: 240 },
263
+ { columnId: 'actual', width: 112 },
264
+ { columnId: 'forecast', width: 112 },
265
+ ],
266
+ columnPinning: [
267
+ { columnId: 'account', position: 'start' },
268
+ { columnId: 'action', position: 'end' },
269
+ ],
270
+ });
271
+ }
272
+
273
+ function showStaleManualResult() {
274
+ manualQueryRevision += 1;
275
+ manualLifecycle = 'stale';
276
+ }
277
+
278
+ function resolveManualQuery() {
279
+ manualLifecycle = 'ready';
280
+ }
281
+
282
+ function retryManualQuery() {
283
+ manualLifecycle = 'refreshing';
284
+ manualQueryRevision += 1;
285
+ }
286
+
287
+ function showManualFailure() {
288
+ manualLifecycle = 'error';
289
+ }
290
+
291
+ function toggleManualFilter() {
292
+ manualController.dispatch({
293
+ type: 'setFilters',
294
+ filters: manualState.filters.length
295
+ ? []
296
+ : [{ columnId: 'status', operator: 'equals', value: 'On track' }],
297
+ });
298
+ }
299
+
300
+ function selectAllMatching() {
301
+ manualController.dispatch({
302
+ type: 'selectAllMatching',
303
+ queryFingerprint: manualQueryFingerprint,
304
+ queryRevision: `revision-${manualQueryRevision}`,
305
+ expectedCount: 120,
306
+ });
307
+ }
96
308
  </script>
97
309
 
98
310
  <div class="workbench">
@@ -149,6 +361,121 @@ function toggleActiveFilter() {
149
361
  />
150
362
  </div>
151
363
 
364
+ <section class="report-fixture" aria-labelledby="report-fixture-title">
365
+ <div class="report-fixture__header">
366
+ <div>
367
+ <p class="eyebrow">Report structure</p>
368
+ <h4 id="report-fixture-title">Forecast report</h4>
369
+ <p class="supporting">
370
+ Grouped headers, structural totals, restored widths, and pinned status/action columns.
371
+ </p>
372
+ </div>
373
+ <Button variant="ghost" size="sm" onclick={restoreReportLayout}>
374
+ Restore saved layout
375
+ </Button>
376
+ </div>
377
+ <div class="table-frame">
378
+ <DataTable
379
+ data={reportRows}
380
+ columns={reportColumns}
381
+ rowKey="id"
382
+ structuralRows={reportStructuralRows}
383
+ controller={reportController}
384
+ caption="Forecast report"
385
+ hoverable
386
+ stickyHeader
387
+ />
388
+ </div>
389
+ </section>
390
+
391
+ <section class="manual-fixture" aria-labelledby="manual-fixture-title">
392
+ <div class="report-fixture__header">
393
+ <div>
394
+ <p class="eyebrow">Manual query and async states</p>
395
+ <h4 id="manual-fixture-title">Server-owned results</h4>
396
+ <p class="supporting">
397
+ Supplied rows remain usable through refresh, stale, and error states. The caller owns
398
+ query revisions and discards late responses.
399
+ </p>
400
+ </div>
401
+ <Badge variant={manualLifecycle === 'error' ? 'error' : 'default'}>
402
+ Revision {manualQueryRevision}
403
+ </Badge>
404
+ </div>
405
+ <div class="table-controls" aria-label="Manual query controls">
406
+ <Button variant="ghost" size="sm" onclick={showStaleManualResult}>
407
+ Show stale result
408
+ </Button>
409
+ <Button variant="ghost" size="sm" onclick={resolveManualQuery}>
410
+ Accept newer result
411
+ </Button>
412
+ <Button variant="ghost" size="sm" onclick={showManualFailure}>
413
+ Show failure
414
+ </Button>
415
+ <Button variant="ghost" size="sm" onclick={toggleManualFilter}>
416
+ {manualState.filters.length ? 'Clear manual filter' : 'Request manual filter'}
417
+ </Button>
418
+ <Button variant="ghost" size="sm" onclick={selectAllMatching}>
419
+ Select all 120 matching
420
+ </Button>
421
+ {#if manualState.selection.scope === 'allMatching'}
422
+ <span>All matching selection is bound to {manualState.selection.queryRevision}.</span>
423
+ {/if}
424
+ </div>
425
+ <div class="table-frame">
426
+ <DataTable
427
+ data={dataTableConformanceRows}
428
+ columns={dataTableConformanceColumns}
429
+ rowKey="id"
430
+ selectable
431
+ controller={manualController}
432
+ sortable
433
+ totalRows={120}
434
+ refreshing={manualLifecycle === 'refreshing'}
435
+ stale={manualLifecycle === 'stale'}
436
+ partialResults={manualLifecycle === 'stale'}
437
+ error={manualLifecycle === 'error' ? 'The query failed. Retry to request the current revision.' : null}
438
+ onRetry={retryManualQuery}
439
+ caption="Manual workspace report"
440
+ stickyHeader
441
+ />
442
+ </div>
443
+ </section>
444
+
445
+ <section class="scale-fixture" aria-labelledby="scale-fixture-title">
446
+ <div>
447
+ <p class="eyebrow">Scale boundary</p>
448
+ <h4 id="scale-fixture-title">Virtualized result window</h4>
449
+ <p class="supporting">
450
+ A fixed-height, keyboard-scrollable body renders a stable row-key window while its
451
+ semantic header remains visible.
452
+ </p>
453
+ </div>
454
+ <div class="table-frame table-frame--virtual">
455
+ <DataTable
456
+ data={virtualRows}
457
+ columns={virtualColumns}
458
+ rowKey="id"
459
+ {virtualization}
460
+ caption="Virtual workspace report"
461
+ striped
462
+ stickyHeader
463
+ />
464
+ </div>
465
+ </section>
466
+
467
+ <section class="coverage-fixture" aria-labelledby="coverage-fixture-title">
468
+ <div>
469
+ <p class="eyebrow">Release conformance</p>
470
+ <h4 id="coverage-fixture-title">Every supported DataTable surface</h4>
471
+ </div>
472
+ <ul>
473
+ {#each dataTableConformanceScenarios as scenario}
474
+ <li><strong>{scenario.title}:</strong> {scenario.contracts.join(', ')}</li>
475
+ {/each}
476
+ </ul>
477
+ </section>
478
+
152
479
  <div class="empty-state">
153
480
  <div>
154
481
  <strong>Empty state</strong>
@@ -243,6 +570,47 @@ function toggleActiveFilter() {
243
570
  border-top: 1px solid var(--smrt-color-outline-variant);
244
571
  }
245
572
 
573
+ .report-fixture {
574
+ display: grid;
575
+ gap: var(--smrt-spacing-3);
576
+ padding-top: var(--smrt-spacing-4);
577
+ border-top: 1px solid var(--smrt-color-outline-variant);
578
+ }
579
+
580
+ .manual-fixture,
581
+ .scale-fixture,
582
+ .coverage-fixture {
583
+ display: grid;
584
+ gap: var(--smrt-spacing-3);
585
+ padding-top: var(--smrt-spacing-4);
586
+ border-top: 1px solid var(--smrt-color-outline-variant);
587
+ }
588
+
589
+ .table-frame--virtual {
590
+ max-height: 20rem;
591
+ }
592
+
593
+ .coverage-fixture ul {
594
+ display: grid;
595
+ gap: var(--smrt-spacing-2);
596
+ padding: 0;
597
+ margin: 0;
598
+ color: var(--smrt-color-on-surface-variant);
599
+ font: var(--smrt-typography-body-small-font);
600
+ list-style: none;
601
+ }
602
+
603
+ .coverage-fixture strong {
604
+ color: var(--smrt-color-on-surface);
605
+ }
606
+
607
+ .report-fixture__header {
608
+ display: flex;
609
+ align-items: flex-start;
610
+ justify-content: space-between;
611
+ gap: var(--smrt-spacing-4);
612
+ }
613
+
246
614
  .empty-state > div:first-child {
247
615
  justify-content: space-between;
248
616
  gap: var(--smrt-spacing-4);
@@ -255,6 +623,7 @@ function toggleActiveFilter() {
255
623
 
256
624
  @media (max-width: 760px) {
257
625
  .workbench__header,
626
+ .report-fixture__header,
258
627
  .empty-state > div:first-child {
259
628
  align-items: flex-start;
260
629
  flex-direction: column;
@@ -1 +1 @@
1
- {"version":3,"file":"DataTablePreview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/playground/DataTablePreview.svelte.ts"],"names":[],"mappings":"AA+JA,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"DataTablePreview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/playground/DataTablePreview.svelte.ts"],"names":[],"mappings":"AAucA,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -51,10 +51,10 @@ export default {
51
51
  {
52
52
  id: 'data-table',
53
53
  title: 'Data Table',
54
- description: 'Sortable and selectable tabular data with loading, density, and empty states.',
54
+ description: 'Accessible local and manual tables with query lifecycle states, grouped reports, saved layout, responsive overflow, and virtualization.',
55
55
  loadComponent: loadDataTable,
56
56
  order: 5,
57
- tags: ['data', 'table', 'grid'],
57
+ tags: ['data', 'table', 'manual-query', 'reporting', 'accessibility'],
58
58
  modes: {
59
59
  mock: {
60
60
  label: 'Interactive',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-ui",
3
- "version": "0.42.4",
3
+ "version": "0.42.6",
4
4
  "description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -124,7 +124,7 @@
124
124
  },
125
125
  "dependencies": {
126
126
  "esm-env": "^1.2.2",
127
- "@happyvertical/smrt-types": "0.42.4"
127
+ "@happyvertical/smrt-types": "0.42.6"
128
128
  },
129
129
  "peerDependencies": {
130
130
  "svelte": "^5.56.4"
@@ -152,6 +152,7 @@
152
152
  "build": "svelte-package -i src --tsconfig tsconfig.svelte.json",
153
153
  "dev": "svelte-package -i src --watch",
154
154
  "check": "pnpm exec svelte-check --tsconfig ./tsconfig.svelte.json",
155
+ "bench:data-table": "vitest bench src/components/data/__benchmarks__/DataTable.bench.ts",
155
156
  "generate:themes": "tsx scripts/generate-theme-styles.ts",
156
157
  "test": "vitest run",
157
158
  "typecheck": "tsc --noEmit && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json",