@happyvertical/smrt-ui 0.42.3 → 0.42.5
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.
- package/README.md +275 -0
- package/dist/components/data/DataTable.svelte +1444 -172
- package/dist/components/data/DataTable.svelte.d.ts +1 -1
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +235 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -0
- package/dist/components/data/DataTableController.js +792 -0
- package/dist/components/data/DataTableIdentity.d.ts +21 -0
- package/dist/components/data/DataTableIdentity.d.ts.map +1 -0
- package/dist/components/data/DataTableIdentity.js +28 -0
- package/dist/components/data/DataTableLayout.d.ts +37 -0
- package/dist/components/data/DataTableLayout.d.ts.map +1 -0
- package/dist/components/data/DataTableLayout.js +135 -0
- package/dist/components/data/DataTablePerformance.d.ts +31 -0
- package/dist/components/data/DataTablePerformance.d.ts.map +1 -0
- package/dist/components/data/DataTablePerformance.js +46 -0
- package/dist/components/data/DataTableVirtualization.d.ts +71 -0
- package/dist/components/data/DataTableVirtualization.d.ts.map +1 -0
- package/dist/components/data/DataTableVirtualization.js +100 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.d.ts +2 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.d.ts.map +1 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.js +53 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts +27 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts.map +1 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.js +141 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts +10 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts.map +1 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.js +23 -0
- package/dist/components/data/__tests__/DataTable.test.js +644 -21
- package/dist/components/data/__tests__/DataTableConformance.test.js +212 -0
- package/dist/components/data/__tests__/DataTableController.test.js +336 -0
- package/dist/components/data/__tests__/DataTableIdentity.test.js +29 -0
- package/dist/components/data/__tests__/DataTableLayout.test.js +195 -0
- package/dist/components/data/__tests__/DataTablePerformance.test.js +21 -0
- package/dist/components/data/__tests__/DataTableVirtualization.test.js +80 -0
- package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +255 -0
- package/dist/components/data/__tests__/data-surface.test.js +675 -0
- package/dist/components/data/data-surface.d.ts +246 -0
- package/dist/components/data/data-surface.d.ts.map +1 -0
- package/dist/components/data/data-surface.js +1102 -0
- package/dist/components/data/index.d.ts +5 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +5 -0
- package/dist/components/data/types.d.ts +110 -2
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/i18n/strings.d.ts +26 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +28 -2
- package/dist/svelte/playground/DataTablePreview.svelte +400 -10
- package/dist/svelte/playground/DataTablePreview.svelte.d.ts.map +1 -1
- package/dist/svelte/playground.js +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -102,6 +102,280 @@ apply, clear, or undo. Agent mutations are denied for secret/read-only controls
|
|
|
102
102
|
and require explicit confirmation before apply, clear, or undo. Staging is
|
|
103
103
|
separate so the UI can show a proposal before it changes user state.
|
|
104
104
|
|
|
105
|
+
## DataTable controller
|
|
106
|
+
|
|
107
|
+
`DataTable` can share one headless `DataTableController` between rendered
|
|
108
|
+
controls and a programmatic adapter. Search, declarative filters, ordered
|
|
109
|
+
multi-column sorting, pagination, columns, selection, and expansion all become
|
|
110
|
+
plain-data commands; a header click and `controller.dispatch()` take the same
|
|
111
|
+
transition path.
|
|
112
|
+
|
|
113
|
+
```svelte
|
|
114
|
+
<script lang="ts">
|
|
115
|
+
import {
|
|
116
|
+
createDataTableController,
|
|
117
|
+
DataTable,
|
|
118
|
+
type DataTableColumn,
|
|
119
|
+
} from '@happyvertical/smrt-ui/data';
|
|
120
|
+
|
|
121
|
+
const controller = createDataTableController({
|
|
122
|
+
columnIds: ['name', 'status'],
|
|
123
|
+
initialState: {
|
|
124
|
+
pageSize: 25,
|
|
125
|
+
sorting: [{ columnId: 'name', direction: 'asc' }],
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
controller.dispatch({
|
|
130
|
+
type: 'setFilters',
|
|
131
|
+
filters: [{ columnId: 'status', operator: 'equals', value: 'active' }],
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<DataTable {controller} data={rows} {columns} rowKey="id" sortable selectable />
|
|
136
|
+
```
|
|
137
|
+
|
|
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
|
+
storage handles, tenant/principal data, query objects, or authority. URL and
|
|
142
|
+
saved-view adapters remain application-owned: persist the snapshot (normally
|
|
143
|
+
excluding selection and expansion IDs), validate it with
|
|
144
|
+
`hydrateDataTableSnapshot`, and feed the state into a new controller or
|
|
145
|
+
`replaceState`. `smrt-ui` does not read or write the URL, browser storage, or a
|
|
146
|
+
database.
|
|
147
|
+
|
|
148
|
+
### Controlled and migration use
|
|
149
|
+
|
|
150
|
+
Pass `state` plus `onStateChange` for controlled state. A controlled controller
|
|
151
|
+
emits a candidate and waits for the host to call `replaceState`; an
|
|
152
|
+
uncontrolled controller owns the state initialized by `initialState`.
|
|
153
|
+
|
|
154
|
+
The existing Svelte bindables remain supported during migration:
|
|
155
|
+
|
|
156
|
+
| Existing prop | Controller state |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `bind:sort` | first entry of ordered `sorting` (single-sort compatibility) |
|
|
159
|
+
| `bind:page`, `pageSize` | `page`, `pageSize` |
|
|
160
|
+
| `bind:selected`, `bind:expanded` | legacy explicit `selectedRowIds`, canonical `selection` and `expandedRowIds` |
|
|
161
|
+
| `visibleColumnIds` | `columnVisibility` intersected with static `column.hidden` |
|
|
162
|
+
| `manualSorting`, `manualPagination` | sorting/pagination entries in `modes` |
|
|
163
|
+
| `filterFn` | local-only legacy predicate; never serialized |
|
|
164
|
+
|
|
165
|
+
An explicit `controller` takes precedence over `state` and legacy bindables.
|
|
166
|
+
Without one, the component creates an internal controller and maps the legacy
|
|
167
|
+
props. Multi-column sorting and persisted layouts use the controller state;
|
|
168
|
+
the legacy `SortState` remains intentionally single-column.
|
|
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
|
+
|
|
190
|
+
### Row identity and selection
|
|
191
|
+
|
|
192
|
+
`rowKey` is required for selectable, expandable, manual/server, and
|
|
193
|
+
`agentAddressable` tables. Its values must be unique non-empty strings or finite
|
|
194
|
+
numbers. This fails closed before a renderer can reuse the wrong row after a
|
|
195
|
+
sort, refresh, or server-page change. The historical source-index fallback
|
|
196
|
+
exists only for local presentational tables with no durable row state.
|
|
197
|
+
|
|
198
|
+
The controller stores a `selection` union alongside the deprecated
|
|
199
|
+
`selectedRowIds` shorthand:
|
|
200
|
+
|
|
201
|
+
| Scope | Stored value | Lifecycle |
|
|
202
|
+
| --- | --- | --- |
|
|
203
|
+
| `page` | IDs from the current rendered page | Cleared when page, page size, search, filters, or sorting changes. |
|
|
204
|
+
| `explicit` | Explicit stable IDs across pages | Persists across page and query navigation until changed by the caller. |
|
|
205
|
+
| `allMatching` | `queryFingerprint`, `queryRevision`, and `expectedCount` only | Never stores loaded IDs; query-shape changes clear it. |
|
|
206
|
+
|
|
207
|
+
The built-in header checkbox explicitly means **Select all rows on this page**.
|
|
208
|
+
For query-wide selection, dispatch `selectAllMatching` with the caller-owned
|
|
209
|
+
query fingerprint, revision, and expected count. A destructive domain action
|
|
210
|
+
must call `assertDataTableSelectionCurrent(selection, currentQuery)` immediately
|
|
211
|
+
before applying it; a mismatched fingerprint or revision throws rather than
|
|
212
|
+
acting on stale results.
|
|
213
|
+
|
|
214
|
+
`index` passed to row callbacks, cells, expansion snippets, and `rowClass` is
|
|
215
|
+
the zero-based display index on the currently rendered page. The source index
|
|
216
|
+
is the zero-based position in the supplied `data` array and is used only by the
|
|
217
|
+
non-durable fallback. It must never be saved, sent to an agent, or used as a
|
|
218
|
+
remote identity.
|
|
219
|
+
|
|
220
|
+
### Transformation ownership and page rules
|
|
221
|
+
|
|
222
|
+
`modes` makes each stage explicit. A `manual` stage renders caller-supplied
|
|
223
|
+
results and bypasses that local stage, so rows are never double-filtered,
|
|
224
|
+
double-sorted, or double-paged.
|
|
225
|
+
|
|
226
|
+
| Filtering | Sorting | Pagination | Renderer behavior |
|
|
227
|
+
| --- | --- | --- | --- |
|
|
228
|
+
| `local` | `local` | `local` | filter → ordered multi-sort → slice |
|
|
229
|
+
| `manual` | `local` | `local` | sort and slice supplied rows |
|
|
230
|
+
| `local` | `manual` | `local` | filter and slice supplied rows |
|
|
231
|
+
| `local` | `local` | `manual` | filter and sort supplied page; never slice it |
|
|
232
|
+
| `manual` | `manual` | `manual` | render supplied rows unchanged |
|
|
233
|
+
|
|
234
|
+
Every combination follows the same rule per column in the table: each local
|
|
235
|
+
stage runs once and each manual stage runs zero times. For manual pagination,
|
|
236
|
+
`totalRows` supplies the total; when it is unknown the component does not guess
|
|
237
|
+
the last page or render misleading pagination controls. A supplied `totalRows`
|
|
238
|
+
must be a non-negative integer and is rejected unless pagination mode is
|
|
239
|
+
`manual`.
|
|
240
|
+
|
|
241
|
+
Changing search, filters, sorting, or page size resets the page to 1 only when
|
|
242
|
+
the value changes. Data or total changes clamp an out-of-range page but do not
|
|
243
|
+
otherwise reset it; empty known totals normalize to page 1. Column layout,
|
|
244
|
+
selection, and expansion never change the page.
|
|
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
|
+
|
|
105
379
|
## Themes
|
|
106
380
|
|
|
107
381
|
`@happyvertical/smrt-ui/themes` is the canonical theme API and includes the
|
|
@@ -116,6 +390,7 @@ hand-authored, and every text pairing clears WCAG AA.
|
|
|
116
390
|
```svelte
|
|
117
391
|
<script>
|
|
118
392
|
import { ThemeProvider } from '@happyvertical/smrt-ui/themes';
|
|
393
|
+
import '@happyvertical/smrt-ui/themes/styles/all.css';
|
|
119
394
|
import '@happyvertical/smrt-ui/themes/styles/fonts.css';
|
|
120
395
|
</script>
|
|
121
396
|
|