@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
package/README.md CHANGED
@@ -135,9 +135,9 @@ transition path.
135
135
  <DataTable {controller} data={rows} {columns} rowKey="id" sortable selectable />
136
136
  ```
137
137
 
138
- `controller.snapshot()` returns the canonical JSON-safe version-2 `{ version,
139
- modes, state }` envelope. `hydrateDataTableSnapshot()` accepts version 1 and
140
- migrates it to version 2. The envelope contains no rows, callbacks, snippets,
138
+ `controller.snapshot()` returns the canonical JSON-safe version-3 `{ version,
139
+ modes, state }` envelope. `hydrateDataTableSnapshot()` accepts versions 1, 2,
140
+ and 3 and normalizes them to version 3. The envelope contains no rows, callbacks, snippets,
141
141
  storage handles, tenant/principal data, query objects, or authority. URL and
142
142
  saved-view adapters remain application-owned: persist the snapshot (normally
143
143
  excluding selection and expansion IDs), validate it with
@@ -167,6 +167,26 @@ Without one, the component creates an internal controller and maps the legacy
167
167
  props. Multi-column sorting and persisted layouts use the controller state;
168
168
  the legacy `SortState` remains intentionally single-column.
169
169
 
170
+ ### Public surface and supported combinations
171
+
172
+ `DataTable` supports the following contracts. These are intentionally composed
173
+ through the controller rather than through a separate report or remote-table
174
+ component.
175
+
176
+ | Need | Public API | Important constraint |
177
+ | --- | --- | --- |
178
+ | Stable row interaction | `rowKey`, `selectable`, `expanded`, `onRowClick`, `agentAddressable` | `rowKey` is mandatory whenever a row has durable or remote identity. |
179
+ | Declarative view state | `controller`, `state`, `initialState`, `onStateChange` | A supplied `controller` wins over controlled state and legacy bindables. |
180
+ | Local or remote transformations | `modes`, `manualSorting`, `manualPagination`, `filterFn`, `totalRows` | A manual stage never runs locally; never mix a local transform with an already transformed remote result. |
181
+ | Query lifecycle | `loading`, `refreshing`, `stale`, `partialResults`, `error`, `onRetry` | The caller owns request cancellation and revision checks; the table only presents the supplied result state. |
182
+ | Report layout | column `headerPath`, `resizable`, `role`, `responsive`; `structuralRows`; controller widths/pinning | Group structure follows final visible leaf columns. Structural rows are never selectable or virtualized. |
183
+ | Narrow screens | `visibleColumnIds` and responsive column metadata | The table preserves its semantic columns behind a named, keyboard-scrollable horizontal overflow region; it does not silently collapse content. |
184
+ | Continuous browsing | `virtualization` | Requires `rowKey` and a fixed-height body. Expanded rows deliberately use the normal semantic body. |
185
+
186
+ The interactive workbench's **Data Table** entry contains a release conformance
187
+ fixture for each row in this table: local interaction, manual query lifecycle,
188
+ responsive overflow, report layout, and virtualization.
189
+
170
190
  ### Row identity and selection
171
191
 
172
192
  `rowKey` is required for selectable, expandable, manual/server, and
@@ -223,6 +243,139 @@ the value changes. Data or total changes clamp an out-of-range page but do not
223
243
  otherwise reset it; empty known totals normalize to page 1. Column layout,
224
244
  selection, and expansion never change the page.
225
245
 
246
+ ### Manual query, retry, and race contract
247
+
248
+ When any stage is `manual`, the host owns the request and result lifecycle. On
249
+ each query-shape change, derive a stable `queryFingerprint` from every
250
+ server-owned input (search, filters, sort rules, page, and page size) and a
251
+ monotonically increasing `queryRevision`; start the request, retain the
252
+ currently displayed rows with `refreshing`/`stale` as appropriate, and only
253
+ commit a response when both values still match. A late response is discarded by
254
+ the host, not merged by `DataTable`.
255
+
256
+ ```ts
257
+ const queryFingerprint = JSON.stringify({ search, filters, sorting, page, pageSize });
258
+ const query = { queryFingerprint, queryRevision: String(revision) };
259
+ const result = await loadRows(query);
260
+
261
+ if (query.queryRevision === String(revision) && query.queryFingerprint === currentQueryFingerprint()) {
262
+ rows = result.rows;
263
+ totalRows = result.totalRows;
264
+ }
265
+ ```
266
+
267
+ Set `error` without clearing a usable page, and make `onRetry` create a new
268
+ revision. For query-wide actions, dispatch `selectAllMatching` with the same
269
+ fingerprint/revision and call `assertDataTableSelectionCurrent` directly before
270
+ the destructive request. This gives ContentList, reporting, admin, and agent
271
+ surfaces the same stale-result and selection guardrail.
272
+
273
+ ### Saved layout and report guidance
274
+
275
+ Use `headerPath` on every leaf that belongs to a grouped heading; matching IDs
276
+ at a given depth form a column group after visibility and restored column order
277
+ are applied. Keep report totals in `structuralRows` or `footer`, not in the
278
+ data array. Persist `controller.snapshot()` only after removing tenant-specific
279
+ selection and expansion IDs, then hydrate it before creating the next
280
+ controller. The version-3 snapshot includes `columnOrder`,
281
+ `columnVisibility`, `columnWidths`, and `columnPinning`, so a report can safely
282
+ restore layout without persisting row data or query authority.
283
+
284
+ ### Scale boundaries and virtualization
285
+
286
+ `DATA_TABLE_SCALE_THRESHOLDS` publishes the measured operating boundaries used
287
+ by the reproducible DataTable benchmark:
288
+
289
+ | Work | Boundary | Use after the boundary |
290
+ | --- | --- | --- |
291
+ | Ordinary local rendering | 250 rows / 5,000 cells | Page or virtualize the body. |
292
+ | Local filtering and sorting | 1,000 rows / 20,000 cells | Move the transform to the caller or server. |
293
+ | Manual/server paging | 100 supplied rows per page | Keep `totalRows` server-owned and bounded. |
294
+
295
+ Run `pnpm --filter @happyvertical/smrt-ui bench:data-table` to measure the
296
+ 250-row local render, 1,000-row client-transform, and 100-row manual-paging
297
+ fixtures. The fixture data has deterministic `rowKey` values so a browser or
298
+ renderer comparison does not depend on array-arrival identity.
299
+
300
+ `virtualization` is opt-in and requires `rowKey`. It virtualizes only a
301
+ fixed-height data body; table headers (including grouped headers) and the
302
+ `footer` summary remain normal semantic table sections and do not count toward
303
+ the window. The virtual scroll region keeps captions and headers sticky, is
304
+ keyboard-scrollable, and reports the full row count plus each rendered row's
305
+ logical row index. Supplying `expandedContent` makes data-row height variable,
306
+ so the component deliberately falls back to the full semantic body and does
307
+ not emit virtual scroll callbacks. Use controlled `scrollTop`/
308
+ `onScrollTopChange` for scroll restoration, and pair `focusedRowId` with
309
+ `onFocusedRowIdChange` to restore DOM focus to a stable row after a data
310
+ refresh. A measured footer extends the virtual scroll range, so keyboard End
311
+ and a controlled scroll position can still reveal the summary. Selection and
312
+ expansion continue to be controller state keyed by `rowKey`, never by a
313
+ rendered window index. With manual pagination, `totalRows` and the current page
314
+ set that full row count and each rendered row's global index.
315
+ ## Mounted data-surface registry
316
+
317
+ `createDataSurfaceRegistry()` is the transport-neutral sibling of the form
318
+ interaction registry. A mounted table, list, or report supplies serializable
319
+ discovery metadata, a revisioned view snapshot, and a small handler for its
320
+ declared visible controls. The registry rejects duplicate identities, validates
321
+ JSON-safe data, requires an `expectedRevision`, records monotonic event
322
+ sequences, serializes commands per mounted identity, and returns a cached
323
+ acknowledgement when the same `commandId` is replayed. The replay cache retains
324
+ only the 100 most recently used command IDs per mounted surface.
325
+
326
+ Visible-command and preview/apply-action envelopes are capped at 100,000 UTF-8
327
+ bytes (`DATA_SURFACE_MAX_REQUEST_BYTES`). JSON values reject prototype keys and
328
+ have fixed nesting and container-size bounds, so every browser-facing request
329
+ remains safe to normalize before host policy evaluates it.
330
+
331
+ ```ts
332
+ import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
333
+
334
+ const registry = createDataSurfaceRegistry();
335
+ let revision = 0;
336
+ let search = '';
337
+
338
+ registry.register({
339
+ descriptor: {
340
+ version: 1,
341
+ identity: { surfaceId: 'content-library', kind: 'table' },
342
+ schemaVersion: 1,
343
+ label: 'Content library',
344
+ rowKey: 'id',
345
+ columns: [
346
+ { id: 'id', label: 'ID', capabilities: ['read', 'project'] },
347
+ { id: 'title', label: 'Title', capabilities: ['read', 'search'] },
348
+ ],
349
+ query: { modes: ['rows', 'count'], projectableColumnIds: ['id', 'title'] },
350
+ controls: [{ id: 'set-search', label: 'Search' }],
351
+ actions: [],
352
+ limits: { maxQueryRows: 100, maxQueryBytes: 100_000, maxSelectionSize: 100 },
353
+ },
354
+ getSnapshot: () => ({ revision, state: { search } }),
355
+ execute: (command) => {
356
+ if (command.controlId === 'set-search') {
357
+ search = String((command.payload as { search?: string }).search ?? '');
358
+ revision += 1;
359
+ }
360
+ },
361
+ });
362
+ ```
363
+
364
+ `inspect()` and command results are deterministic `{ version, descriptor,
365
+ revision, state, selection }` envelopes; neither includes a timestamp, rows,
366
+ functions, authority fields, tenant/principal data, SQL, or a transport handle.
367
+ The registry rejects those boundary keys from both default and redacted snapshot
368
+ state. An optional registration `redact()` hook can remove sensitive view state
369
+ before it leaves the mounted host, but cannot alter the identity or revision.
370
+
371
+ The registry validates bounded projection/count/facet query envelopes (including
372
+ the UTF-8 byte length of their normalized JSON form) and preview/apply action
373
+ envelopes, but it does not execute either. Canonical query semantics belong to
374
+ the query protocol, browser command acknowledgement belongs to a transport
375
+ adapter, and authentication, tenancy, confirmation-token verification, and
376
+ durable actions remain server-side. URL state and saved views also remain
377
+ application-owned persistence adapters.
378
+
226
379
  ## Themes
227
380
 
228
381
  `@happyvertical/smrt-ui/themes` is the canonical theme API and includes the
@@ -237,6 +390,7 @@ hand-authored, and every text pairing clears WCAG AA.
237
390
  ```svelte
238
391
  <script>
239
392
  import { ThemeProvider } from '@happyvertical/smrt-ui/themes';
393
+ import '@happyvertical/smrt-ui/themes/styles/all.css';
240
394
  import '@happyvertical/smrt-ui/themes/styles/fonts.css';
241
395
  </script>
242
396