@urbicon-ui/sveltekit-utils 7.0.0 → 8.0.0

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
@@ -5,7 +5,7 @@ Small, focused SvelteKit helpers that Urbicon apps share. Zero runtime dependenc
5
5
  Currently shipping:
6
6
 
7
7
  - **URL-state runes** — reactive `useUrlParam` / `useUrlArrayParam` that keep component state in sync with `?query=` parameters
8
- - **Table-query URL sync** — opt-in `?q=…&sort=…&page=…` mirroring for `@urbicon-ui/table` server mode, plus the pure serializers behind it
8
+ - **Table view ↔ URL** — `bindViewToUrl`, the URL home for a `@urbicon-ui/table` view object (`?q=…&sort=…&page=…`), plus the pure serializers for the load path
9
9
  - **Cron runner** — interval-based background fetcher for scheduled server endpoints
10
10
  - **SSE stream reader** — `streamSse`, a spec-correct async-generator client for one-shot POST `text/event-stream` endpoints (LLM relays)
11
11
 
@@ -57,60 +57,60 @@ updateUrlSearchParams({ page: '1', tag: ['a', 'b'] }, { replaceState: true });
57
57
  - URL updates use `goto()` with `replaceState: true`, `noScroll: true`, `keepFocus: true` — suited for filter/pagination UIs, not full page transitions.
58
58
  - `useUrlParam` returns getters (not Svelte stores) so consumers can read the value lazily inside `$derived`/`$effect`.
59
59
 
60
- ## Table Query ↔ URL (`table-query` + `url.svelte`)
60
+ ## Table View ↔ URL (`url.svelte` + `table-query`)
61
61
 
62
- Opt-in URL sync for `@urbicon-ui/table`, in client mode as well as `mode="server"`: the `TableQuery` the table emits (search, sort, page, page size, filters, grouping) is mirrored onto query parameters (`?q=…&sort=…&page=…`), so the view state survives reloads, can be shared as a link, and — unlike `localStorage` — is visible to the server.
62
+ `bindViewToUrl` gives the view object of `@urbicon-ui/table` search, sort, page, page size, filters, grouping the URL as its home: the axes are mirrored onto query parameters (`?q=…&sort=…&page=…`), so the view survives a reload, can be shared as a link, and — unlike `localStorage` — is visible to the server.
63
63
 
64
64
  ```svelte
65
65
  <script lang="ts">
66
- import { Table } from '@urbicon-ui/table';
67
- import { tableQueryToSearchParams } from '@urbicon-ui/sveltekit-utils/table-query';
68
- import { createTableQueryUrlSync } from '@urbicon-ui/sveltekit-utils/url.svelte';
66
+ import { Table, createTableView } from '@urbicon-ui/table';
67
+ import { bindViewToUrl } from '@urbicon-ui/sveltekit-utils/url.svelte';
69
68
 
70
- const sync = createTableQueryUrlSync({ defaults: { itemsPerPage: 25 } });
69
+ const view = createTableView({ defaults: { pageSize: 25 } });
70
+ bindViewToUrl(view);
71
71
  </script>
72
72
 
73
- <Table
74
- mode="server"
75
- {columns}
76
- itemsPerPage={25}
77
- query={sync.viewState}
78
- queryFn={async (query, { signal }) => {
79
- sync.syncQuery(query); // mirror the query onto the URL (replaceState)
80
- const res = await fetch(`/api/users?${tableQueryToSearchParams(query)}`, { signal });
81
- const data = await res.json();
82
- return { items: data.results, totalItems: data.total };
83
- }}
84
- />
73
+ <Table {items} {columns} {view} />
85
74
  ```
86
75
 
87
- `viewState` carries **only the axes the URL actually names**, and the table's `query` prop reads it by field presence: a present field controls that axis, an absent one leaves persistence and the `initial*` seeds alone. That is why this is not `initialQuery` — that one describes every axis, including the ones it filled in from the defaults, so a table wired to it ignores `persistenceConfig`, `initialSort`, `initialFilters` and `initialGroupBy` on every URL. `initialQuery` stays what its name says: a complete snapshot, parsed once, to seed a fetch with.
76
+ Both calls belong in the component's initialisation. The init half runs synchronously a `?sort=…` link renders sorted server HTML and the runtime halves are effects: URL navigations apply to the view, the reader's changes reach the URL debounced.
88
77
 
89
- Because `viewState` re-reads the URL rather than capturing it, the browser's back button works: navigating back to a URL without `?sort` returns the table to its unsorted view.
78
+ The second argument is optional; every option has a default:
90
79
 
91
- A controlled axis is not written to `localStorage` by default, so a visit that arrives without query params starts clean. For a business table where "my filters are still there tomorrow" is expected, pair the sync with `persistenceConfig={{ tableId: '…', persistControlled: true }}` — the table then stores what the reader themselves changed (never what a shared link brought), and hands it back on a bare visit. Reading order is unaffected: the URL still wins.
80
+ | Option | Default | Effect |
81
+ | ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | `axes` | all six | Which axes this binding manages. An unbound axis never reaches the URL, whatever the view holds. |
83
+ | `debounceMs` | `300` | Delay before a view change is written to the URL. |
84
+ | `replaceState` | `true` | Replace the current history entry instead of pushing one, so rapid sort/filter/page edits do not flood the back button. |
85
+ | `prefix` | `''` | Key namespace (`prefix: 't_'` → `?t_q=…&t_page=…`) for a second bound table on the same page. |
86
+ | `reflectExternal` | `false` | Mirror an externally applied value (a storage seed) into the URL immediately. Off by default: the address bar does not change without reader interaction, and the seed reaches the URL with the first one anyway. |
92
87
 
93
- With manual control (`onQueryChange` instead of `queryFn`), pass `sync.syncQuery` directly note that `onQueryChange` does not fire when `queryFn` is set, which is why the managed variant calls it inside `queryFn`.
88
+ Because the binding re-reads the URL rather than capturing it, the browser's back button works: navigating back to a URL that no longer names `?sort` returns the table to its default sort.
94
89
 
95
- The pure serializers live under `@urbicon-ui/sveltekit-utils/table-query` and work without SvelteKit — e.g. to parse the initial query in a server `load` and fetch the first page during SSR:
90
+ The pure serializers work without SvelteKit — e.g. to parse the incoming query in a server `load` and fetch the first page during SSR. Use `searchParamsToViewQuery` from `./table-view`: it takes the *same* defaults object the component hands `createTableView`, so the server cannot resolve an absent param differently from the client.
96
91
 
97
92
  ```typescript
93
+ // view-defaults.ts — imported by both the component and the load function
94
+ export const userView = { pageSize: 25, sort: { column: 'joined', direction: 'desc' } };
95
+
98
96
  // +page.server.ts
99
- import { searchParamsToTableQuery } from '@urbicon-ui/sveltekit-utils/table-query';
97
+ import { searchParamsToViewQuery } from '@urbicon-ui/sveltekit-utils/table-view';
98
+ import { userView } from './view-defaults';
100
99
 
101
- export const load = async ({ url }) => {
102
- const query = searchParamsToTableQuery(url.searchParams, { defaults: { itemsPerPage: 25 } });
103
- return { initialResult: await fetchUsers(query) };
104
- };
100
+ export const load = async ({ url }) => ({
101
+ initialResult: await fetchUsers(searchParamsToViewQuery(url.searchParams, userView))
102
+ });
105
103
  ```
106
104
 
105
+ `searchParamsToTableQuery` in `./table-query` does the same job with a baseline in the wire vocabulary (`itemsPerPage`, `sortColumn`/`sortDirection`, `groupByKey`). It has no field for a default filter set, which is why the view-vocabulary function exists.
106
+
107
107
  **Design notes**
108
108
 
109
- - **Default elision** — values equal to `defaults` are not written; a table in its default state leaves the URL clean. Set `defaults` to the table's initial props (`itemsPerPage`, `initialPage`, `initialGroupBy`, and `sortColumn`/`sortDirection` when the table ships a baked-in `initialSort`) so the elision baseline matches the state the table starts in.
110
- - **Read tolerant, write strict** — unparsable params fall back to the defaults and malformed `filter` entries are skipped; serializing a structurally invalid query (non-positive page, unknown operator) throws instead of writing corrupt state.
111
- - **Namespacing** — `prefix: 't_'` scopes all keys (`?t_q=…`) for multiple synced tables on one page; unrelated params are always preserved.
112
- - **Types** — `TableQueryParams` is a structural mirror of the table's `TableQuery` (no dependency on `@urbicon-ui/table`; a parity test in the table package guards against drift).
113
- - **Seeding** — every axis the URL carries can seed the table: `initialPage`, `initialGroupBy`, `initialSort`, `initialFilters` (plus controlled `searchTerm` with an `onSearchTermChange` write-back). The seeds land before the table's first query emission, so a shared URL's sort/filter params survive it the header indicator and filter chips show the URL state instead of the first emission wiping it. One precedence caveat: the `initial*` props seed only an axis `persistenceConfig` has nothing stored for when both are active, a persisted sort/filter/group wins over the URL, and that includes a persisted _empty_ value (the user cleared that axis once, so it stays cleared). Scope or disable persistence for URL-driven tables if the link should be the source of truth.
109
+ - **Default elision** — an axis whose value equals its default is not written; a table in its default state leaves the URL clean. The baseline *is* `view.defaults`, read off the object the binding decorates, so there is no second copy of the defaults to keep in step with the table's own.
110
+ - **Read tolerant, write strict** — an unparsable value on a param the URL actually carries falls back to that axis' default, and malformed `filter` entries are skipped individually; the `table-query` serializer throws on a structurally invalid query (non-positive page, unknown operator) instead of writing corrupt state.
111
+ - **Namespacing** — `prefix: 't_'` scopes all keys (`?t_q=…`) for multiple bound tables on one page; unrelated params are always preserved. Two prefixless bindings would manage the same keys, so that throws at registration instead of producing a link that loads the wrong table.
112
+ - **One writer per page** — every binding submits into one coalescing URL writer, so two tables land in a single navigation, each replacing only its own keys. A landing URL the writer itself sent is not applied back onto the view: an edit made while that navigation was in flight survives instead of being overwritten by the URL it raced.
113
+ - **Types** — `TableQueryParams` mirrors the table's `TableQuery`, and `TableViewLike` / `TableViewSnapshot` (from `./table-view`) mirror its view object structurally so this package carries no dependency on `@urbicon-ui/table`. A parity test in the table package (`tableQuery.parity.test.ts`) pins both mirrors: the snapshot shapes must stay mutually assignable, and the real `TableView` must satisfy `TableViewLike`, which is the entire mechanism by which this binding decorates a view it never imports.
114
114
 
115
115
  ## Cron Runner (`cron`)
116
116
 
@@ -207,13 +207,16 @@ export const POST = async ({ request }) => {
207
207
 
208
208
  ## Exports
209
209
 
210
- | Subpath | Contents |
211
- | --------------- | ------------------------------------------------------------------------------------------------------------------ |
212
- | `.` | Barrel of all modules |
213
- | `./url.svelte` | `useUrlParam`, `useUrlArrayParam`, `createUrlParam`, `updateUrlSearchParams`, `createTableQueryUrlSync`, types |
214
- | `./table-query` | `tableQueryToSearchParams`, `searchParamsToTableQuery`, `applyTableQueryToSearchParams`, `TableQueryParams`, types |
215
- | `./cron` | `createCronRunner`, `CronJob`, `CronRunnerConfig`, `CronRunner` |
216
- | `./sse` | `streamSse`, `SseEvent`, `StreamSseOptions`, `SseRequestError` |
210
+ | Subpath | Contents |
211
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
212
+ | `.` | Barrel of all modules |
213
+ | `./url.svelte` | `useUrlParam`, `useUrlArrayParam`, `createUrlParam`, `updateUrlSearchParams`, `bindViewToUrl`, types |
214
+ | `./table-view` | `searchParamsToViewQuery`, `searchParamsToViewSnapshot`, `viewSnapshotToTableQuery`, `searchParamsToViewPartial`, `viewSnapshotToSearchParams`, `viewAxesNamedBy`, `viewAxisKeys`, `TABLE_VIEW_AXES`, `TableViewLike`, types |
215
+ | `./table-query` | `tableQueryToSearchParams`, `searchParamsToTableQuery`, `applyTableQueryToSearchParams`, `TABLE_QUERY_FILTER_OPERATORS`, `TableQueryParams`, types |
216
+ | `./cron` | `createCronRunner`, `CronJob`, `CronRunnerConfig`, `CronRunner` |
217
+ | `./sse` | `streamSse`, `SseEvent`, `StreamSseOptions`, `SseRequestError` |
218
+
219
+ `bindViewToUrl` lives in its own module (`view-binding.svelte.ts`) and is re-exported from `./url.svelte`, which is its documented import path — it has no subpath of its own. `./table-view` and `./table-query` are SvelteKit-free (they touch no `$app/*`), which is what lets a `load` function and a plain test use them; `./url.svelte` is the half that needs the router.
217
220
 
218
221
  ## Development
219
222
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './cron.js';
2
2
  export * from './sse.js';
3
3
  export * from './table-query.js';
4
+ export * from './table-view.js';
4
5
  export * from './url.svelte.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './cron.js';
2
2
  export * from './sse.js';
3
3
  export * from './table-query.js';
4
+ export * from './table-view.js';
4
5
  export * from './url.svelte.js';
@@ -40,8 +40,8 @@ export interface TableQueryFilter {
40
40
  }
41
41
  /**
42
42
  * Query state of a table in server mode. Structural mirror of `TableQuery`
43
- * from `@urbicon-ui/table` — the object passed to `queryFn` / `onQueryChange`
44
- * is directly assignable.
43
+ * from `@urbicon-ui/table` — the object a managed `source.query` receives
44
+ * (or `viewToQuery` projects) is directly assignable.
45
45
  */
46
46
  export interface TableQueryParams {
47
47
  /** Current page (1-based). */
@@ -59,39 +59,17 @@ export interface TableQueryParams {
59
59
  /** Column ID for grouping, or null if ungrouped. */
60
60
  groupByKey: string | null;
61
61
  }
62
- /**
63
- * The partial half of {@link TableQueryParams} — structural mirror of
64
- * `TableViewState` from `@urbicon-ui/table`, the type its `query` prop takes.
65
- *
66
- * Every field optional, and that is the whole contract: **presence means
67
- * controlled**. A consumer may hand over the sort and leave paging to the
68
- * table, so "absent" has to stay distinguishable from "set to its default
69
- * value" — which is exactly what {@link TableQueryParams} cannot express.
70
- */
71
- export interface TableQueryViewState {
72
- /** Current page (1-based). */
73
- page?: number;
74
- /** Number of items per page. */
75
- itemsPerPage?: number;
76
- /** Column ID to sort by, or empty string for no sort. */
77
- sortColumn?: string;
78
- /** Sort direction. */
79
- sortDirection?: TableQuerySortDirection;
80
- /** Full-text search term. */
81
- searchTerm?: string;
82
- /** Active column filters. */
83
- activeFilters?: TableQueryFilter[];
84
- /** Column ID for grouping, or null for ungrouped. */
85
- groupByKey?: string | null;
86
- }
87
62
  /**
88
63
  * Baseline used for default elision: query values equal to these defaults are
89
64
  * omitted from the URL, and missing params parse back to them.
90
65
  *
91
- * Set the defaults to the table's **initial, uncontrolled state** i.e. the
92
- * values you pass as props (`itemsPerPage`, `initialPage`, `initialGroupBy`).
93
- * Unset fields fall back to the table's own defaults (page 1, 10 items per
94
- * page, no sort, empty search, ungrouped).
66
+ * A **wire-codec** baseline in the query shape when your defaults live on a
67
+ * view object (`createTableView({ defaults })`), skip the manual projection
68
+ * and use `searchParamsToViewQuery` from `./table-view`, which takes the view
69
+ * itself. Unset fields fall back to the table's own defaults (page 1, 10
70
+ * items per page, no sort, empty search, ungrouped). Filters have no default
71
+ * field: active filters are always written, an absent `filter` param parses
72
+ * to none.
95
73
  */
96
74
  export interface TableQueryDefaults {
97
75
  /** Default page. @default 1 */
@@ -136,8 +114,8 @@ export interface TableQueryUrlOptions {
136
114
  * When `sortColumn` is empty the sort direction is meaningless and is
137
115
  * normalized away (it parses back as `'asc'`).
138
116
  *
139
- * Also handy for building the backend request inside `queryFn` — the same
140
- * scheme works as an API query string.
117
+ * Also handy for building the backend request inside a managed
118
+ * `source.query` — the same scheme works as an API query string.
141
119
  *
142
120
  * @param query - Query emitted by the table (`TableQuery` is assignable).
143
121
  * @param options - Elision defaults + key prefix.
@@ -153,41 +131,17 @@ export declare function tableQueryToSearchParams(query: TableQueryParams, option
153
131
  * unknown `dir` becomes `'asc'`, and malformed `filter` entries (wrong shape,
154
132
  * unknown operator, broken percent-encoding) are skipped individually.
155
133
  *
156
- * Works anywhere a `URLSearchParams` exists including `url.searchParams`
157
- * in a server `load`, to run the initial server-mode fetch during SSR.
134
+ * The wire-level codec, for consumers who hold their defaults in the query
135
+ * shape. For the SSR load path of a v8 view — running the initial fetch in a
136
+ * server `load` from `url.searchParams` — prefer `searchParamsToViewQuery`
137
+ * from `./table-view`: same keys, but it reads the defaults off the view
138
+ * object instead of a hand-projected baseline.
158
139
  *
159
140
  * @param params - Search params to read (not mutated).
160
141
  * @param options - Fallback defaults + key prefix.
161
142
  * @returns Complete query object (assignable to `TableQuery`).
162
143
  */
163
144
  export declare function searchParamsToTableQuery(params: URLSearchParams, options?: TableQueryUrlOptions): TableQueryParams;
164
- /**
165
- * Parse `URLSearchParams` into the **partial** view state a table's `query`
166
- * prop takes — a key per axis the URL actually carries, and nothing else.
167
- *
168
- * This is the deliberate opposite of {@link searchParamsToTableQuery}, and the
169
- * distinction is load-bearing rather than cosmetic. The table reads `query` by
170
- * **field presence**: a present field means "this axis is controlled — outrank
171
- * persistence and the `initial*` seed"; an absent one means "the URL has no
172
- * opinion, carry on". A complete object therefore claims every axis, so
173
- * handing `searchParamsToTableQuery`'s output to `query` silently switches off
174
- * `persistenceConfig`, `initialSort`, `initialFilters` and `initialGroupBy` —
175
- * even on a URL with no params at all, where its every field is a default it
176
- * invented. That is not a hypothetical: it was the shipped wiring, and this
177
- * function exists because of it.
178
- *
179
- * `sortColumn` and `sortDirection` are emitted as a pair or not at all, so the
180
- * half-controlled sort (a direction with no column) cannot be produced here.
181
- *
182
- * Same tolerance as the full parser: an unparsable `page`/`size` falls back to
183
- * the resolved default *for that key* — the key was present, so the axis stays
184
- * controlled — and malformed filters are skipped individually.
185
- *
186
- * @param params - Search params to read (not mutated).
187
- * @param options - Fallback defaults + key prefix.
188
- * @returns Only the axes present in `params`.
189
- */
190
- export declare function searchParamsToTableViewState(params: URLSearchParams, options?: TableQueryUrlOptions): TableQueryViewState;
191
145
  /**
192
146
  * Merge a table query into existing search params: all managed keys (`q`,
193
147
  * `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
@@ -128,8 +128,8 @@ function parseFilterParam(raw) {
128
128
  * When `sortColumn` is empty the sort direction is meaningless and is
129
129
  * normalized away (it parses back as `'asc'`).
130
130
  *
131
- * Also handy for building the backend request inside `queryFn` — the same
132
- * scheme works as an API query string.
131
+ * Also handy for building the backend request inside a managed
132
+ * `source.query` — the same scheme works as an API query string.
133
133
  *
134
134
  * @param query - Query emitted by the table (`TableQuery` is assignable).
135
135
  * @param options - Elision defaults + key prefix.
@@ -172,8 +172,11 @@ export function tableQueryToSearchParams(query, options = {}) {
172
172
  * unknown `dir` becomes `'asc'`, and malformed `filter` entries (wrong shape,
173
173
  * unknown operator, broken percent-encoding) are skipped individually.
174
174
  *
175
- * Works anywhere a `URLSearchParams` exists including `url.searchParams`
176
- * in a server `load`, to run the initial server-mode fetch during SSR.
175
+ * The wire-level codec, for consumers who hold their defaults in the query
176
+ * shape. For the SSR load path of a v8 view — running the initial fetch in a
177
+ * server `load` from `url.searchParams` — prefer `searchParamsToViewQuery`
178
+ * from `./table-view`: same keys, but it reads the defaults off the view
179
+ * object instead of a hand-projected baseline.
177
180
  *
178
181
  * @param params - Search params to read (not mutated).
179
182
  * @param options - Fallback defaults + key prefix.
@@ -206,65 +209,6 @@ export function searchParamsToTableQuery(params, options = {}) {
206
209
  groupByKey: rawGroup !== null ? (rawGroup === '' ? null : rawGroup) : d.groupByKey
207
210
  };
208
211
  }
209
- /**
210
- * Parse `URLSearchParams` into the **partial** view state a table's `query`
211
- * prop takes — a key per axis the URL actually carries, and nothing else.
212
- *
213
- * This is the deliberate opposite of {@link searchParamsToTableQuery}, and the
214
- * distinction is load-bearing rather than cosmetic. The table reads `query` by
215
- * **field presence**: a present field means "this axis is controlled — outrank
216
- * persistence and the `initial*` seed"; an absent one means "the URL has no
217
- * opinion, carry on". A complete object therefore claims every axis, so
218
- * handing `searchParamsToTableQuery`'s output to `query` silently switches off
219
- * `persistenceConfig`, `initialSort`, `initialFilters` and `initialGroupBy` —
220
- * even on a URL with no params at all, where its every field is a default it
221
- * invented. That is not a hypothetical: it was the shipped wiring, and this
222
- * function exists because of it.
223
- *
224
- * `sortColumn` and `sortDirection` are emitted as a pair or not at all, so the
225
- * half-controlled sort (a direction with no column) cannot be produced here.
226
- *
227
- * Same tolerance as the full parser: an unparsable `page`/`size` falls back to
228
- * the resolved default *for that key* — the key was present, so the axis stays
229
- * controlled — and malformed filters are skipped individually.
230
- *
231
- * @param params - Search params to read (not mutated).
232
- * @param options - Fallback defaults + key prefix.
233
- * @returns Only the axes present in `params`.
234
- */
235
- export function searchParamsToTableViewState(params, options = {}) {
236
- const d = resolveDefaults(options.defaults);
237
- const k = paramKeys(options.prefix ?? '');
238
- const view = {};
239
- const rawPage = params.get(k.page);
240
- if (rawPage !== null)
241
- view.page = parsePositiveInt(rawPage) ?? d.page;
242
- const rawSize = params.get(k.size);
243
- if (rawSize !== null)
244
- view.itemsPerPage = parsePositiveInt(rawSize) ?? d.itemsPerPage;
245
- const rawSort = params.get(k.sort);
246
- if (rawSort !== null) {
247
- view.sortColumn = rawSort;
248
- view.sortDirection = rawSort !== '' && params.get(k.dir) === 'desc' ? 'desc' : 'asc';
249
- }
250
- const rawSearch = params.get(k.q);
251
- if (rawSearch !== null)
252
- view.searchTerm = rawSearch;
253
- const rawFilters = params.getAll(k.filter);
254
- if (rawFilters.length > 0) {
255
- const activeFilters = [];
256
- for (const raw of rawFilters) {
257
- const filter = parseFilterParam(raw);
258
- if (filter)
259
- activeFilters.push(filter);
260
- }
261
- view.activeFilters = activeFilters;
262
- }
263
- const rawGroup = params.get(k.group);
264
- if (rawGroup !== null)
265
- view.groupByKey = rawGroup === '' ? null : rawGroup;
266
- return view;
267
- }
268
212
  /**
269
213
  * Merge a table query into existing search params: all managed keys (`q`,
270
214
  * `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
@@ -0,0 +1,123 @@
1
+ /**
2
+ * URL (de)serialization for the v8 table view vocabulary (`TableView` from
3
+ * `@urbicon-ui/table`: search, sort, page, pageSize, filters, groupBy).
4
+ *
5
+ * The types in this module are a **structural mirror** of the table package's
6
+ * view types — deliberately not imported, so this package carries no
7
+ * dependency on `@urbicon-ui/table`. A type-parity test over there guards the
8
+ * shapes against drift; `bindViewToUrl` accepts any object shaped like
9
+ * {@link TableViewLike}, which the real `TableView` is.
10
+ *
11
+ * The key scheme is the shipped one (`q`, `page`, `size`, `sort`, `dir`,
12
+ * `group`, `filter`) — deep links written for v7 keep parsing. The write
13
+ * side adds one compatible extension: an empty `filter=` marker, analogous
14
+ * to `sort=`, so a cleared filter set elides like every other axis (the
15
+ * shipped read side already tolerated it).
16
+ */
17
+ import { type TableQueryFilter, type TableQueryParams } from './table-query.js';
18
+ /** One of the six view axes. Mirrors `ViewAxis` from `@urbicon-ui/table`. */
19
+ export type TableViewAxis = 'search' | 'sort' | 'page' | 'pageSize' | 'filters' | 'groupBy';
20
+ /** All six view axes, in vocabulary order. */
21
+ export declare const TABLE_VIEW_AXES: readonly TableViewAxis[];
22
+ /** Sort state of a view. Mirrors `ViewSort` from `@urbicon-ui/table`. */
23
+ export interface TableViewSort {
24
+ /** Column ID to sort by. */
25
+ column: string;
26
+ /** Sort direction. */
27
+ direction: 'asc' | 'desc';
28
+ }
29
+ /**
30
+ * A fully resolved view state — never `undefined` anywhere. Mirrors
31
+ * `TableViewSnapshot` from `@urbicon-ui/table`.
32
+ */
33
+ export interface TableViewSnapshot {
34
+ search: string;
35
+ sort: TableViewSort | null;
36
+ page: number;
37
+ pageSize: number;
38
+ filters: TableQueryFilter[];
39
+ groupBy: string | null;
40
+ }
41
+ /**
42
+ * The surface {@link bindViewToUrl} needs from a view object — a structural
43
+ * mirror of the table package's `TableView` class. Field reads are reactive,
44
+ * field writes count as the reader's own change; `applyExternal` is the
45
+ * binding write surface, `claimAxes`/`releaseAxes` the fail-loud composition
46
+ * registry, and `originOf` the per-axis (revision, origin) bookkeeping the
47
+ * bindings decide by.
48
+ */
49
+ export interface TableViewLike {
50
+ readonly defaults: TableViewSnapshot;
51
+ search: string;
52
+ sort: TableViewSort | null;
53
+ page: number;
54
+ pageSize: number;
55
+ filters: TableQueryFilter[];
56
+ groupBy: string | null;
57
+ applyExternal(partial: Partial<TableViewSnapshot>, origin: 'external' | 'system'): void;
58
+ claimAxes(kind: 'url' | 'storage', axes: readonly TableViewAxis[]): void;
59
+ releaseAxes(kind: 'url' | 'storage', axes: readonly TableViewAxis[]): void;
60
+ markInitApplied(axes: readonly TableViewAxis[]): void;
61
+ wasInitApplied(axis: TableViewAxis): boolean;
62
+ originOf(axis: TableViewAxis): {
63
+ revision: number;
64
+ origin: 'user' | 'external' | 'system' | 'init';
65
+ };
66
+ snapshot(): TableViewSnapshot;
67
+ }
68
+ /** The URL keys a set of axes owns, with the configured prefix applied. */
69
+ export declare function viewAxisKeys(axes: readonly TableViewAxis[], prefix?: string): string[];
70
+ /** The axes a URL names — presence only for params it actually carries. */
71
+ export declare function viewAxesNamedBy(sp: URLSearchParams, prefix?: string): TableViewAxis[];
72
+ /**
73
+ * Parse search params into a **partial** view snapshot — a key per axis the
74
+ * URL actually carries, and nothing else. Read tolerant per key: an
75
+ * unparsable value on a *present* key falls back to the configured default
76
+ * for that axis (the key was present, so the axis stays claimed), and
77
+ * malformed filter entries are skipped individually.
78
+ */
79
+ export declare function searchParamsToViewPartial(sp: URLSearchParams, defaults: Pick<TableViewSnapshot, 'page' | 'pageSize'>, prefix?: string): Partial<TableViewSnapshot>;
80
+ /**
81
+ * Resolve a full view snapshot from search params: every axis the URL names
82
+ * comes from the URL, every other one from `defaults` — the same resolution
83
+ * the URL binding performs at init, for code that has no view (a server
84
+ * `load`).
85
+ */
86
+ export declare function searchParamsToViewSnapshot(sp: URLSearchParams, defaults?: Partial<TableViewSnapshot>, prefix?: string): TableViewSnapshot;
87
+ /**
88
+ * Project a view snapshot into the wire shape a backend speaks — the same
89
+ * mapping the table applies before calling a managed `source.query`, so a
90
+ * server `load` and the table's own fetches send identical field names.
91
+ */
92
+ export declare function viewSnapshotToTableQuery(snapshot: TableViewSnapshot): TableQueryParams;
93
+ /**
94
+ * The load-path counterpart of the URL binding: parse search params against
95
+ * the **view's own defaults** and hand back the query a fetch needs.
96
+ *
97
+ * The point is the defaults argument. `searchParamsToTableQuery` takes its
98
+ * baseline in the wire vocabulary (`itemsPerPage`, `sortColumn`/`sortDirection`,
99
+ * `groupByKey`) and has no field for filters at all, so a `load` had to keep a
100
+ * second, differently-spelled copy of what `createTableView({ defaults })`
101
+ * already says — and could not express a default filter set no matter how it
102
+ * was written (#157 finding 2). This one takes the very object the view takes:
103
+ * one spelling, all six axes, so the server cannot resolve an absent param
104
+ * differently from the client.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // shared with the component that calls createTableView({ defaults })
109
+ * export const invoiceView = { pageSize: 25, sort: { column: 'date', direction: 'desc' } };
110
+ *
111
+ * export const load = async ({ url }) => ({
112
+ * initialResult: await fetchInvoices(searchParamsToViewQuery(url.searchParams, invoiceView))
113
+ * });
114
+ * ```
115
+ */
116
+ export declare function searchParamsToViewQuery(sp: URLSearchParams, defaults?: Partial<TableViewSnapshot>, prefix?: string): TableQueryParams;
117
+ /**
118
+ * Serialize a snapshot, eliding every axis that equals the defaults — the
119
+ * elision baseline *is* the view's defaults, structurally. `axes` restricts
120
+ * the output to a binding's own axes: an unbound axis never reaches the URL,
121
+ * no matter what the view holds.
122
+ */
123
+ export declare function viewSnapshotToSearchParams(snapshot: TableViewSnapshot, defaults: TableViewSnapshot, axes?: readonly TableViewAxis[], prefix?: string): URLSearchParams;