@urbicon-ui/sveltekit-utils 6.51.0 → 7.0.1

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
@@ -59,7 +59,7 @@ updateUrlSearchParams({ page: '1', tag: ['a', 'b'] }, { replaceState: true });
59
59
 
60
60
  ## Table Query ↔ URL (`table-query` + `url.svelte`)
61
61
 
62
- Opt-in URL sync for `@urbicon-ui/table` in `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 and can be shared as a link.
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.
63
63
 
64
64
  ```svelte
65
65
  <script lang="ts">
@@ -74,12 +74,7 @@ Opt-in URL sync for `@urbicon-ui/table` in `mode="server"`: the `TableQuery` the
74
74
  mode="server"
75
75
  {columns}
76
76
  itemsPerPage={25}
77
- initialPage={sync.initialQuery.page}
78
- initialGroupBy={sync.initialQuery.groupByKey}
79
- initialSort={sync.initialQuery.sortColumn
80
- ? { column: sync.initialQuery.sortColumn, direction: sync.initialQuery.sortDirection }
81
- : undefined}
82
- initialFilters={sync.initialQuery.activeFilters}
77
+ query={sync.viewState}
83
78
  queryFn={async (query, { signal }) => {
84
79
  sync.syncQuery(query); // mirror the query onto the URL (replaceState)
85
80
  const res = await fetch(`/api/users?${tableQueryToSearchParams(query)}`, { signal });
@@ -89,6 +84,12 @@ Opt-in URL sync for `@urbicon-ui/table` in `mode="server"`: the `TableQuery` the
89
84
  />
90
85
  ```
91
86
 
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.
88
+
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.
90
+
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.
92
+
92
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`.
93
94
 
94
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:
@@ -109,7 +110,7 @@ export const load = async ({ url }) => {
109
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.
110
111
  - **Namespacing** — `prefix: 't_'` scopes all keys (`?t_q=…`) for multiple synced tables on one page; unrelated params are always preserved.
111
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).
112
- - **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.
113
+ - **Seeding, the variant without `query`** — every axis the URL carries can also be handed to the table as a seed: `initialPage`, `initialGroupBy`, `initialSort`, `initialFilters` (plus a 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. Its precedence is the **inverse** of `query`'s, and that is the reason to prefer `query`: an `initial*` prop seeds only an axis `persistenceConfig` has nothing stored for, so with both active a persisted sort/filter/group beats the link including a persisted _empty_ value (the user cleared that axis once, so it stays cleared). The seeds do reach the server-rendered HTML they are applied during construction, which runs under SSR too (measured: `initialSort`, `initialFilters`, `initialGroupBy` and `initialPage` all land in the server markup; only a controlled `searchTerm`, applied in an effect, does not). What they lack is the per-axis precedence above and any reaction to the back button, since a seed is read once. Use them only where the axis must not be controlled; otherwise `query={sync.viewState}` (+ `persistControlled` when the reader's own state should outlive the link).
113
114
 
114
115
  ## Cron Runner (`cron`)
115
116
 
@@ -59,6 +59,31 @@ 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
+ }
62
87
  /**
63
88
  * Baseline used for default elision: query values equal to these defaults are
64
89
  * omitted from the URL, and missing params parse back to them.
@@ -136,6 +161,33 @@ export declare function tableQueryToSearchParams(query: TableQueryParams, option
136
161
  * @returns Complete query object (assignable to `TableQuery`).
137
162
  */
138
163
  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;
139
191
  /**
140
192
  * Merge a table query into existing search params: all managed keys (`q`,
141
193
  * `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
@@ -206,6 +206,65 @@ export function searchParamsToTableQuery(params, options = {}) {
206
206
  groupByKey: rawGroup !== null ? (rawGroup === '' ? null : rawGroup) : d.groupByKey
207
207
  };
208
208
  }
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
+ }
209
268
  /**
210
269
  * Merge a table query into existing search params: all managed keys (`q`,
211
270
  * `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
@@ -1,4 +1,4 @@
1
- import { type TableQueryParams, type TableQueryUrlOptions } from './table-query.js';
1
+ import { type TableQueryParams, type TableQueryUrlOptions, type TableQueryViewState } from './table-query.js';
2
2
  /**
3
3
  * How {@link useUrlArrayParam} maps an array onto the URL:
4
4
  * - `repeat` — one entry per key: `?tag=a&tag=b`
@@ -145,17 +145,20 @@ export interface TableQueryUrlSyncOptions extends TableQueryUrlOptions {
145
145
  replaceState?: boolean;
146
146
  }
147
147
  /**
148
- * Opt-in URL sync for a table in server mode: mirrors the `TableQuery` the
149
- * table emits onto `?q=…&sort=…&page=…` query params, so the view state
150
- * survives reloads and can be shared as a link.
148
+ * Opt-in URL sync for a table: mirrors the `TableQuery` the table emits onto
149
+ * `?q=…&sort=…&page=…` query params, so the view state survives reloads, can
150
+ * be shared as a link, and unlike `localStorage` is visible to the server.
151
+ * Works in client mode as well as server mode; the table emits its query in
152
+ * both.
151
153
  *
152
154
  * Two directions, both explicit:
153
- * - **URL → query**: `initialQuery` is parsed once at creation (SSR-safe)
154
- * seed the table (`initialPage`, `initialGroupBy`, `initialSort`,
155
- * `initialFilters`, controlled `searchTerm`) and run the first fetch from
156
- * it. The seeds land before the table's first query emission, so URL sort/
157
- * filter params survive it; note that a value restored via the table's
158
- * `persistenceConfig` wins over an `initial*` seed for its axis.
155
+ * - **URL → query**: `viewState` carries the axes the URL actually names, and
156
+ * re-reads the URL on every navigation. Hand it to the table's `query` prop,
157
+ * which controls exactly those axes per field, and ahead of both
158
+ * `persistenceConfig` and the `initial*` seeds. Because a controlled axis is
159
+ * a derived rather than a write, it resolves during server rendering too,
160
+ * which is the point: the server renders the view the link asked for instead
161
+ * of a default one the client swaps out on hydration.
159
162
  * - **query → URL**: pass `syncQuery` the query from `onQueryChange`, or call
160
163
  * it inside `queryFn` (when `queryFn` is set, `onQueryChange` does not
161
164
  * fire). It rewrites only its own — optionally prefixed — params via
@@ -167,7 +170,7 @@ export interface TableQueryUrlSyncOptions extends TableQueryUrlOptions {
167
170
  * `initialPage`, `initialGroupBy`) so the elision baseline matches the state
168
171
  * the table actually starts in.
169
172
  *
170
- * @example
173
+ * @example Client mode — the whole wiring is two props
171
174
  * ```svelte
172
175
  * <script lang="ts">
173
176
  * import { Table } from '@urbicon-ui/table';
@@ -176,11 +179,32 @@ export interface TableQueryUrlSyncOptions extends TableQueryUrlOptions {
176
179
  * const sync = createTableQueryUrlSync({ defaults: { itemsPerPage: 25 } });
177
180
  * </script>
178
181
  *
182
+ * <Table {items} {columns} query={sync.viewState} onQueryChange={sync.syncQuery} />
183
+ * ```
184
+ *
185
+ * `query` is a *controlled* prop, which is what makes this work on the server:
186
+ * the URL is parsed during SSR and the table renders the linked view rather
187
+ * than an unfiltered one the client then replaces. It also outranks
188
+ * `persistenceConfig` per axis, so there is no longer a caveat about a stored
189
+ * value beating the link.
190
+ *
191
+ * Pass `viewState`, never `initialQuery`. `initialQuery` describes **every**
192
+ * axis, so as a `query` value it claims every axis — including the ones the URL
193
+ * says nothing about, whose values it filled in from the defaults. A table
194
+ * wired that way ignores `persistenceConfig`, `initialSort`, `initialFilters`
195
+ * and `initialGroupBy` entirely, on any URL, and says nothing about it.
196
+ *
197
+ * @example Server mode — the same two directions, with the fetch in between
198
+ * ```svelte
199
+ * <script lang="ts">
200
+ * const sync = createTableQueryUrlSync({ defaults: { itemsPerPage: 25 } });
201
+ * </script>
202
+ *
179
203
  * <Table
180
204
  * mode="server"
181
- * columns={columns}
205
+ * {columns}
182
206
  * itemsPerPage={25}
183
- * initialPage={sync.initialQuery.page}
207
+ * query={sync.viewState}
184
208
  * queryFn={async (query, { signal }) => {
185
209
  * sync.syncQuery(query);
186
210
  * const res = await fetch(`/api/users?${new URLSearchParams(...)}`, { signal });
@@ -191,10 +215,22 @@ export interface TableQueryUrlSyncOptions extends TableQueryUrlOptions {
191
215
  * ```
192
216
  *
193
217
  * @param options - Elision defaults, key prefix, history behaviour.
194
- * @returns `initialQuery` (the URL parsed at creation time) + `syncQuery`
195
- * (write a query back to the URL).
218
+ * @returns `viewState` (the controlled axes, re-read on every navigation),
219
+ * `initialQuery` (a complete snapshot for a fetch) + `syncQuery` (write a
220
+ * query back to the URL).
196
221
  */
197
222
  export declare function createTableQueryUrlSync(options?: TableQueryUrlSyncOptions): {
223
+ /**
224
+ * The axes the URL names, re-read on every navigation.
225
+ *
226
+ * A getter, not a captured value: `createTableQueryUrlSync` runs once per
227
+ * page component, and SvelteKit does not remount that component when only
228
+ * the query string changes. A snapshot would therefore never see the back
229
+ * button — the table would stay sorted after the user navigated back to a
230
+ * URL with no sort param. Reading `page.url` here makes every consumer of
231
+ * this getter a reader of SvelteKit's own reactive page state.
232
+ */
233
+ readonly viewState: TableQueryViewState;
198
234
  readonly initialQuery: TableQueryParams;
199
235
  readonly syncQuery: (query: TableQueryParams) => void;
200
236
  };
@@ -1,7 +1,7 @@
1
1
  import { building } from '$app/environment';
2
2
  import { goto } from '$app/navigation';
3
3
  import { page } from '$app/state';
4
- import { applyTableQueryToSearchParams, searchParamsToTableQuery } from './table-query.js';
4
+ import { applyTableQueryToSearchParams, searchParamsToTableQuery, searchParamsToTableViewState } from './table-query.js';
5
5
  /**
6
6
  * Low-level escape hatch to update several params at once via `goto` (without a
7
7
  * full navigation). Starts from the current URL, applies `next`, and keeps
@@ -176,17 +176,20 @@ export function useUrlArrayParam(key, opts) {
176
176
  return useUrlParam(key, { parse, serialize, initial: opts.initial });
177
177
  }
178
178
  /**
179
- * Opt-in URL sync for a table in server mode: mirrors the `TableQuery` the
180
- * table emits onto `?q=…&sort=…&page=…` query params, so the view state
181
- * survives reloads and can be shared as a link.
179
+ * Opt-in URL sync for a table: mirrors the `TableQuery` the table emits onto
180
+ * `?q=…&sort=…&page=…` query params, so the view state survives reloads, can
181
+ * be shared as a link, and unlike `localStorage` is visible to the server.
182
+ * Works in client mode as well as server mode; the table emits its query in
183
+ * both.
182
184
  *
183
185
  * Two directions, both explicit:
184
- * - **URL → query**: `initialQuery` is parsed once at creation (SSR-safe)
185
- * seed the table (`initialPage`, `initialGroupBy`, `initialSort`,
186
- * `initialFilters`, controlled `searchTerm`) and run the first fetch from
187
- * it. The seeds land before the table's first query emission, so URL sort/
188
- * filter params survive it; note that a value restored via the table's
189
- * `persistenceConfig` wins over an `initial*` seed for its axis.
186
+ * - **URL → query**: `viewState` carries the axes the URL actually names, and
187
+ * re-reads the URL on every navigation. Hand it to the table's `query` prop,
188
+ * which controls exactly those axes per field, and ahead of both
189
+ * `persistenceConfig` and the `initial*` seeds. Because a controlled axis is
190
+ * a derived rather than a write, it resolves during server rendering too,
191
+ * which is the point: the server renders the view the link asked for instead
192
+ * of a default one the client swaps out on hydration.
190
193
  * - **query → URL**: pass `syncQuery` the query from `onQueryChange`, or call
191
194
  * it inside `queryFn` (when `queryFn` is set, `onQueryChange` does not
192
195
  * fire). It rewrites only its own — optionally prefixed — params via
@@ -198,7 +201,7 @@ export function useUrlArrayParam(key, opts) {
198
201
  * `initialPage`, `initialGroupBy`) so the elision baseline matches the state
199
202
  * the table actually starts in.
200
203
  *
201
- * @example
204
+ * @example Client mode — the whole wiring is two props
202
205
  * ```svelte
203
206
  * <script lang="ts">
204
207
  * import { Table } from '@urbicon-ui/table';
@@ -207,11 +210,32 @@ export function useUrlArrayParam(key, opts) {
207
210
  * const sync = createTableQueryUrlSync({ defaults: { itemsPerPage: 25 } });
208
211
  * </script>
209
212
  *
213
+ * <Table {items} {columns} query={sync.viewState} onQueryChange={sync.syncQuery} />
214
+ * ```
215
+ *
216
+ * `query` is a *controlled* prop, which is what makes this work on the server:
217
+ * the URL is parsed during SSR and the table renders the linked view rather
218
+ * than an unfiltered one the client then replaces. It also outranks
219
+ * `persistenceConfig` per axis, so there is no longer a caveat about a stored
220
+ * value beating the link.
221
+ *
222
+ * Pass `viewState`, never `initialQuery`. `initialQuery` describes **every**
223
+ * axis, so as a `query` value it claims every axis — including the ones the URL
224
+ * says nothing about, whose values it filled in from the defaults. A table
225
+ * wired that way ignores `persistenceConfig`, `initialSort`, `initialFilters`
226
+ * and `initialGroupBy` entirely, on any URL, and says nothing about it.
227
+ *
228
+ * @example Server mode — the same two directions, with the fetch in between
229
+ * ```svelte
230
+ * <script lang="ts">
231
+ * const sync = createTableQueryUrlSync({ defaults: { itemsPerPage: 25 } });
232
+ * </script>
233
+ *
210
234
  * <Table
211
235
  * mode="server"
212
- * columns={columns}
236
+ * {columns}
213
237
  * itemsPerPage={25}
214
- * initialPage={sync.initialQuery.page}
238
+ * query={sync.viewState}
215
239
  * queryFn={async (query, { signal }) => {
216
240
  * sync.syncQuery(query);
217
241
  * const res = await fetch(`/api/users?${new URLSearchParams(...)}`, { signal });
@@ -222,13 +246,17 @@ export function useUrlArrayParam(key, opts) {
222
246
  * ```
223
247
  *
224
248
  * @param options - Elision defaults, key prefix, history behaviour.
225
- * @returns `initialQuery` (the URL parsed at creation time) + `syncQuery`
226
- * (write a query back to the URL).
249
+ * @returns `viewState` (the controlled axes, re-read on every navigation),
250
+ * `initialQuery` (a complete snapshot for a fetch) + `syncQuery` (write a
251
+ * query back to the URL).
227
252
  */
228
253
  export function createTableQueryUrlSync(options = {}) {
229
254
  // Same prerender rule as `useUrlParam`: no query string exists while
230
- // building, so the seed parses from empty params instead of throwing.
231
- const initialQuery = searchParamsToTableQuery(building ? new URLSearchParams() : page.url.searchParams, options);
255
+ // building, so both readers parse from empty params instead of throwing.
256
+ const currentParams = () => (building ? new URLSearchParams() : page.url.searchParams);
257
+ // Parsed once, on purpose — a snapshot to seed a fetch with, not a source of
258
+ // truth. `viewState` below is the one the table binds to.
259
+ const initialQuery = searchParamsToTableQuery(currentParams(), options);
232
260
  function syncQuery(query) {
233
261
  const next = applyTableQueryToSearchParams(page.url.searchParams, query, options);
234
262
  const qs = next.toString();
@@ -238,5 +266,21 @@ export function createTableQueryUrlSync(options = {}) {
238
266
  keepFocus: true
239
267
  });
240
268
  }
241
- return { initialQuery, syncQuery };
269
+ return {
270
+ /**
271
+ * The axes the URL names, re-read on every navigation.
272
+ *
273
+ * A getter, not a captured value: `createTableQueryUrlSync` runs once per
274
+ * page component, and SvelteKit does not remount that component when only
275
+ * the query string changes. A snapshot would therefore never see the back
276
+ * button — the table would stay sorted after the user navigated back to a
277
+ * URL with no sort param. Reading `page.url` here makes every consumer of
278
+ * this getter a reader of SvelteKit's own reactive page state.
279
+ */
280
+ get viewState() {
281
+ return searchParamsToTableViewState(currentParams(), options);
282
+ },
283
+ initialQuery,
284
+ syncQuery
285
+ };
242
286
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/sveltekit-utils",
3
- "version": "6.51.0",
3
+ "version": "7.0.1",
4
4
  "description": "SvelteKit helper utilities — createCronRunner, streamSse, and URL-state runes",
5
5
  "license": "MIT",
6
6
  "repository": {