@happyvertical/smrt-ui 0.42.2 → 0.42.4

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 CHANGED
@@ -102,6 +102,127 @@ 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-2 `{ version,
139
+ modes, state }` envelope. `hydrateDataTableSnapshot()` accepts version 1 and
140
+ migrates it to version 2. 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
+ ### Row identity and selection
171
+
172
+ `rowKey` is required for selectable, expandable, manual/server, and
173
+ `agentAddressable` tables. Its values must be unique non-empty strings or finite
174
+ numbers. This fails closed before a renderer can reuse the wrong row after a
175
+ sort, refresh, or server-page change. The historical source-index fallback
176
+ exists only for local presentational tables with no durable row state.
177
+
178
+ The controller stores a `selection` union alongside the deprecated
179
+ `selectedRowIds` shorthand:
180
+
181
+ | Scope | Stored value | Lifecycle |
182
+ | --- | --- | --- |
183
+ | `page` | IDs from the current rendered page | Cleared when page, page size, search, filters, or sorting changes. |
184
+ | `explicit` | Explicit stable IDs across pages | Persists across page and query navigation until changed by the caller. |
185
+ | `allMatching` | `queryFingerprint`, `queryRevision`, and `expectedCount` only | Never stores loaded IDs; query-shape changes clear it. |
186
+
187
+ The built-in header checkbox explicitly means **Select all rows on this page**.
188
+ For query-wide selection, dispatch `selectAllMatching` with the caller-owned
189
+ query fingerprint, revision, and expected count. A destructive domain action
190
+ must call `assertDataTableSelectionCurrent(selection, currentQuery)` immediately
191
+ before applying it; a mismatched fingerprint or revision throws rather than
192
+ acting on stale results.
193
+
194
+ `index` passed to row callbacks, cells, expansion snippets, and `rowClass` is
195
+ the zero-based display index on the currently rendered page. The source index
196
+ is the zero-based position in the supplied `data` array and is used only by the
197
+ non-durable fallback. It must never be saved, sent to an agent, or used as a
198
+ remote identity.
199
+
200
+ ### Transformation ownership and page rules
201
+
202
+ `modes` makes each stage explicit. A `manual` stage renders caller-supplied
203
+ results and bypasses that local stage, so rows are never double-filtered,
204
+ double-sorted, or double-paged.
205
+
206
+ | Filtering | Sorting | Pagination | Renderer behavior |
207
+ | --- | --- | --- | --- |
208
+ | `local` | `local` | `local` | filter → ordered multi-sort → slice |
209
+ | `manual` | `local` | `local` | sort and slice supplied rows |
210
+ | `local` | `manual` | `local` | filter and slice supplied rows |
211
+ | `local` | `local` | `manual` | filter and sort supplied page; never slice it |
212
+ | `manual` | `manual` | `manual` | render supplied rows unchanged |
213
+
214
+ Every combination follows the same rule per column in the table: each local
215
+ stage runs once and each manual stage runs zero times. For manual pagination,
216
+ `totalRows` supplies the total; when it is unknown the component does not guess
217
+ the last page or render misleading pagination controls. A supplied `totalRows`
218
+ must be a non-negative integer and is rejected unless pagination mode is
219
+ `manual`.
220
+
221
+ Changing search, filters, sorting, or page size resets the page to 1 only when
222
+ the value changes. Data or total changes clamp an out-of-range page but do not
223
+ otherwise reset it; empty known totals normalize to page 1. Column layout,
224
+ selection, and expansion never change the page.
225
+
105
226
  ## Themes
106
227
 
107
228
  `@happyvertical/smrt-ui/themes` is the canonical theme API and includes the