@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.
@@ -0,0 +1,241 @@
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 { TABLE_QUERY_FILTER_OPERATORS } from './table-query.js';
18
+ /** All six view axes, in vocabulary order. */
19
+ export const TABLE_VIEW_AXES = [
20
+ 'search',
21
+ 'sort',
22
+ 'page',
23
+ 'pageSize',
24
+ 'filters',
25
+ 'groupBy'
26
+ ];
27
+ /** URL keys per axis, unprefixed — the §3.2 key scheme, grouped by owning axis. */
28
+ const AXIS_KEYS = {
29
+ search: ['q'],
30
+ sort: ['sort', 'dir'],
31
+ page: ['page'],
32
+ pageSize: ['size'],
33
+ filters: ['filter'],
34
+ groupBy: ['group']
35
+ };
36
+ /** The URL keys a set of axes owns, with the configured prefix applied. */
37
+ export function viewAxisKeys(axes, prefix = '') {
38
+ return axes.flatMap((axis) => AXIS_KEYS[axis].map((key) => `${prefix}${key}`));
39
+ }
40
+ function isFilterOperator(value) {
41
+ return TABLE_QUERY_FILTER_OPERATORS.includes(value);
42
+ }
43
+ /** Same per-entry tolerance as the table-query parser: malformed → null → skipped. */
44
+ function parseFilterParam(raw) {
45
+ const parts = raw.split(':');
46
+ if (parts.length !== 3)
47
+ return null;
48
+ const [encodedColumn, operator, encodedValue] = parts;
49
+ if (!isFilterOperator(operator))
50
+ return null;
51
+ try {
52
+ const column = decodeURIComponent(encodedColumn);
53
+ if (!column)
54
+ return null;
55
+ return { column, operator, value: decodeURIComponent(encodedValue) };
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /** The axes a URL names — presence only for params it actually carries. */
62
+ export function viewAxesNamedBy(sp, prefix = '') {
63
+ const axes = [];
64
+ if (sp.get(`${prefix}q`) !== null)
65
+ axes.push('search');
66
+ if (sp.get(`${prefix}sort`) !== null)
67
+ axes.push('sort');
68
+ if (sp.get(`${prefix}page`) !== null)
69
+ axes.push('page');
70
+ if (sp.get(`${prefix}size`) !== null)
71
+ axes.push('pageSize');
72
+ if (sp.getAll(`${prefix}filter`).length > 0)
73
+ axes.push('filters');
74
+ if (sp.get(`${prefix}group`) !== null)
75
+ axes.push('groupBy');
76
+ return axes;
77
+ }
78
+ /**
79
+ * Parse search params into a **partial** view snapshot — a key per axis the
80
+ * URL actually carries, and nothing else. Read tolerant per key: an
81
+ * unparsable value on a *present* key falls back to the configured default
82
+ * for that axis (the key was present, so the axis stays claimed), and
83
+ * malformed filter entries are skipped individually.
84
+ */
85
+ export function searchParamsToViewPartial(sp, defaults, prefix = '') {
86
+ const partial = {};
87
+ const rawSearch = sp.get(`${prefix}q`);
88
+ if (rawSearch !== null)
89
+ partial.search = rawSearch;
90
+ const rawSort = sp.get(`${prefix}sort`);
91
+ if (rawSort !== null) {
92
+ partial.sort =
93
+ rawSort === ''
94
+ ? null // `sort: null` — "unsorted" is a value, not a sentinel
95
+ : { column: rawSort, direction: sp.get(`${prefix}dir`) === 'desc' ? 'desc' : 'asc' };
96
+ }
97
+ const rawPage = sp.get(`${prefix}page`);
98
+ if (rawPage !== null && /^\d+$/.test(rawPage) && Number(rawPage) >= 1) {
99
+ partial.page = Number(rawPage);
100
+ }
101
+ else if (rawPage !== null) {
102
+ partial.page = defaults.page;
103
+ }
104
+ const rawSize = sp.get(`${prefix}size`);
105
+ if (rawSize !== null && /^\d+$/.test(rawSize) && Number(rawSize) >= 1) {
106
+ partial.pageSize = Number(rawSize);
107
+ }
108
+ else if (rawSize !== null) {
109
+ partial.pageSize = defaults.pageSize;
110
+ }
111
+ // `filter=` (empty marker) and `filter=a:contains:b` both claim the axis;
112
+ // the empty marker claims it as *empty* — the compatible format extension.
113
+ const rawFilters = sp.getAll(`${prefix}filter`);
114
+ if (rawFilters.length > 0) {
115
+ partial.filters = rawFilters
116
+ .map(parseFilterParam)
117
+ .filter((f) => f !== null);
118
+ }
119
+ const rawGroup = sp.get(`${prefix}group`);
120
+ if (rawGroup !== null)
121
+ partial.groupBy = rawGroup === '' ? null : rawGroup;
122
+ return partial;
123
+ }
124
+ /**
125
+ * Resolve a full view snapshot from search params: every axis the URL names
126
+ * comes from the URL, every other one from `defaults` — the same resolution
127
+ * the URL binding performs at init, for code that has no view (a server
128
+ * `load`).
129
+ */
130
+ export function searchParamsToViewSnapshot(sp, defaults = {}, prefix = '') {
131
+ const resolved = resolveViewDefaults(defaults);
132
+ return { ...resolved, ...searchParamsToViewPartial(sp, resolved, prefix) };
133
+ }
134
+ /** Fill an unset axis with the table's own default — never `undefined` anywhere. */
135
+ function resolveViewDefaults(defaults) {
136
+ return {
137
+ search: defaults.search ?? '',
138
+ sort: defaults.sort ?? null,
139
+ page: defaults.page ?? 1,
140
+ pageSize: defaults.pageSize ?? 10,
141
+ filters: defaults.filters ?? [],
142
+ groupBy: defaults.groupBy || null
143
+ };
144
+ }
145
+ /**
146
+ * Project a view snapshot into the wire shape a backend speaks — the same
147
+ * mapping the table applies before calling a managed `source.query`, so a
148
+ * server `load` and the table's own fetches send identical field names.
149
+ */
150
+ export function viewSnapshotToTableQuery(snapshot) {
151
+ return {
152
+ page: snapshot.page,
153
+ itemsPerPage: snapshot.pageSize,
154
+ sortColumn: snapshot.sort?.column ?? '',
155
+ sortDirection: snapshot.sort?.direction ?? 'asc',
156
+ searchTerm: snapshot.search,
157
+ activeFilters: [...snapshot.filters],
158
+ groupByKey: snapshot.groupBy
159
+ };
160
+ }
161
+ /**
162
+ * The load-path counterpart of the URL binding: parse search params against
163
+ * the **view's own defaults** and hand back the query a fetch needs.
164
+ *
165
+ * The point is the defaults argument. `searchParamsToTableQuery` takes its
166
+ * baseline in the wire vocabulary (`itemsPerPage`, `sortColumn`/`sortDirection`,
167
+ * `groupByKey`) and has no field for filters at all, so a `load` had to keep a
168
+ * second, differently-spelled copy of what `createTableView({ defaults })`
169
+ * already says — and could not express a default filter set no matter how it
170
+ * was written (#157 finding 2). This one takes the very object the view takes:
171
+ * one spelling, all six axes, so the server cannot resolve an absent param
172
+ * differently from the client.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * // shared with the component that calls createTableView({ defaults })
177
+ * export const invoiceView = { pageSize: 25, sort: { column: 'date', direction: 'desc' } };
178
+ *
179
+ * export const load = async ({ url }) => ({
180
+ * initialResult: await fetchInvoices(searchParamsToViewQuery(url.searchParams, invoiceView))
181
+ * });
182
+ * ```
183
+ */
184
+ export function searchParamsToViewQuery(sp, defaults = {}, prefix = '') {
185
+ return viewSnapshotToTableQuery(searchParamsToViewSnapshot(sp, defaults, prefix));
186
+ }
187
+ /**
188
+ * Serialize a snapshot, eliding every axis that equals the defaults — the
189
+ * elision baseline *is* the view's defaults, structurally. `axes` restricts
190
+ * the output to a binding's own axes: an unbound axis never reaches the URL,
191
+ * no matter what the view holds.
192
+ */
193
+ export function viewSnapshotToSearchParams(snapshot, defaults, axes = TABLE_VIEW_AXES, prefix = '') {
194
+ const sp = new URLSearchParams();
195
+ const bound = (axis) => axes.includes(axis);
196
+ if (bound('search') && snapshot.search !== defaults.search)
197
+ sp.set(`${prefix}q`, snapshot.search);
198
+ if (bound('page') && snapshot.page !== defaults.page)
199
+ sp.set(`${prefix}page`, String(snapshot.page));
200
+ if (bound('pageSize') && snapshot.pageSize !== defaults.pageSize)
201
+ sp.set(`${prefix}size`, String(snapshot.pageSize));
202
+ // `bound('sort')` gates BOTH branches — gating only the null-mismatch one
203
+ // let an unbound sort leak into the URL (an operator-precedence slip the
204
+ // spike caught; pinned by the axis-subset test).
205
+ const sortDiffers = bound('sort') &&
206
+ ((snapshot.sort === null) !== (defaults.sort === null) ||
207
+ (snapshot.sort !== null &&
208
+ defaults.sort !== null &&
209
+ (snapshot.sort.column !== defaults.sort.column ||
210
+ snapshot.sort.direction !== defaults.sort.direction)));
211
+ if (sortDiffers) {
212
+ if (snapshot.sort === null) {
213
+ sp.set(`${prefix}sort`, ''); // explicitly unsorted — only reachable when defaults sort
214
+ }
215
+ else {
216
+ sp.set(`${prefix}sort`, snapshot.sort.column);
217
+ if (snapshot.sort.direction === 'desc')
218
+ sp.set(`${prefix}dir`, 'desc');
219
+ }
220
+ }
221
+ if (bound('groupBy') && snapshot.groupBy !== defaults.groupBy)
222
+ sp.set(`${prefix}group`, snapshot.groupBy ?? '');
223
+ const filtersDiffer = bound('filters') &&
224
+ (snapshot.filters.length !== defaults.filters.length ||
225
+ snapshot.filters.some((f, i) => f.column !== defaults.filters[i].column ||
226
+ f.operator !== defaults.filters[i].operator ||
227
+ f.value !== defaults.filters[i].value));
228
+ if (filtersDiffer) {
229
+ if (snapshot.filters.length === 0) {
230
+ // The format extension: an empty marker, analogous to `sort=`, so a
231
+ // cleared filter set elides like every other axis.
232
+ sp.set(`${prefix}filter`, '');
233
+ }
234
+ else {
235
+ for (const filter of snapshot.filters) {
236
+ sp.append(`${prefix}filter`, `${encodeURIComponent(filter.column)}:${filter.operator}:${encodeURIComponent(filter.value)}`);
237
+ }
238
+ }
239
+ }
240
+ return sp;
241
+ }
@@ -1,4 +1,4 @@
1
- import { type TableQueryParams, type TableQueryUrlOptions, type TableQueryViewState } from './table-query.js';
1
+ export { bindViewToUrl, type UrlViewBindingOptions } from './view-binding.svelte.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`
@@ -134,103 +134,3 @@ export declare function useUrlArrayParam(key: string, opts: {
134
134
  strategy?: UrlArrayStrategy;
135
135
  delimiter?: string;
136
136
  }): readonly [() => string[], (next: string[]) => void];
137
- /** Options for {@link createTableQueryUrlSync}. */
138
- export interface TableQueryUrlSyncOptions extends TableQueryUrlOptions {
139
- /**
140
- * Replace the current history entry instead of pushing a new one. The
141
- * default (`true`) keeps every sort/filter/page interaction from polluting
142
- * the back button.
143
- * @default true
144
- */
145
- replaceState?: boolean;
146
- }
147
- /**
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.
153
- *
154
- * Two directions, both explicit:
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.
162
- * - **query → URL**: pass `syncQuery` the query from `onQueryChange`, or call
163
- * it inside `queryFn` (when `queryFn` is set, `onQueryChange` does not
164
- * fire). It rewrites only its own — optionally prefixed — params via
165
- * `goto` (`replaceState`, `noScroll`, `keepFocus`); unrelated params are
166
- * preserved. Values equal to `options.defaults` are elided, so a table in
167
- * its default state leaves the URL clean.
168
- *
169
- * Set `options.defaults` to the table's initial props (`itemsPerPage`,
170
- * `initialPage`, `initialGroupBy`) so the elision baseline matches the state
171
- * the table actually starts in.
172
- *
173
- * @example Client mode — the whole wiring is two props
174
- * ```svelte
175
- * <script lang="ts">
176
- * import { Table } from '@urbicon-ui/table';
177
- * import { createTableQueryUrlSync } from '@urbicon-ui/sveltekit-utils/url.svelte';
178
- *
179
- * const sync = createTableQueryUrlSync({ defaults: { itemsPerPage: 25 } });
180
- * </script>
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
- *
203
- * <Table
204
- * mode="server"
205
- * {columns}
206
- * itemsPerPage={25}
207
- * query={sync.viewState}
208
- * queryFn={async (query, { signal }) => {
209
- * sync.syncQuery(query);
210
- * const res = await fetch(`/api/users?${new URLSearchParams(...)}`, { signal });
211
- * const data = await res.json();
212
- * return { items: data.results, totalItems: data.total };
213
- * }}
214
- * />
215
- * ```
216
- *
217
- * @param options - Elision defaults, key prefix, history behaviour.
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).
221
- */
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;
234
- readonly initialQuery: TableQueryParams;
235
- readonly syncQuery: (query: TableQueryParams) => void;
236
- };
@@ -1,7 +1,15 @@
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, searchParamsToTableViewState } from './table-query.js';
4
+ // The v8 view-object binding lives in its own module; re-exported here so the
5
+ // documented import path (`@urbicon-ui/sveltekit-utils/url.svelte`) carries it.
6
+ // The mirror types (TableViewLike, TableViewSnapshot, …) are exported from the
7
+ // package root via `table-view` — not re-exported here, which would make the
8
+ // root's star exports ambiguous and silently drop them. The v7
9
+ // `createTableQueryUrlSync` factory is gone with the table's `query` prop —
10
+ // the URL home of a table view is `bindViewToUrl`; the load-path serializers
11
+ // (`searchParamsToTableQuery` & friends) live on in `table-query`.
12
+ export { bindViewToUrl } from './view-binding.svelte.js';
5
13
  /**
6
14
  * Low-level escape hatch to update several params at once via `goto` (without a
7
15
  * full navigation). Starts from the current URL, applies `next`, and keeps
@@ -175,112 +183,3 @@ export function useUrlArrayParam(key, opts) {
175
183
  };
176
184
  return useUrlParam(key, { parse, serialize, initial: opts.initial });
177
185
  }
178
- /**
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.
184
- *
185
- * Two directions, both explicit:
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.
193
- * - **query → URL**: pass `syncQuery` the query from `onQueryChange`, or call
194
- * it inside `queryFn` (when `queryFn` is set, `onQueryChange` does not
195
- * fire). It rewrites only its own — optionally prefixed — params via
196
- * `goto` (`replaceState`, `noScroll`, `keepFocus`); unrelated params are
197
- * preserved. Values equal to `options.defaults` are elided, so a table in
198
- * its default state leaves the URL clean.
199
- *
200
- * Set `options.defaults` to the table's initial props (`itemsPerPage`,
201
- * `initialPage`, `initialGroupBy`) so the elision baseline matches the state
202
- * the table actually starts in.
203
- *
204
- * @example Client mode — the whole wiring is two props
205
- * ```svelte
206
- * <script lang="ts">
207
- * import { Table } from '@urbicon-ui/table';
208
- * import { createTableQueryUrlSync } from '@urbicon-ui/sveltekit-utils/url.svelte';
209
- *
210
- * const sync = createTableQueryUrlSync({ defaults: { itemsPerPage: 25 } });
211
- * </script>
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
- *
234
- * <Table
235
- * mode="server"
236
- * {columns}
237
- * itemsPerPage={25}
238
- * query={sync.viewState}
239
- * queryFn={async (query, { signal }) => {
240
- * sync.syncQuery(query);
241
- * const res = await fetch(`/api/users?${new URLSearchParams(...)}`, { signal });
242
- * const data = await res.json();
243
- * return { items: data.results, totalItems: data.total };
244
- * }}
245
- * />
246
- * ```
247
- *
248
- * @param options - Elision defaults, key prefix, history behaviour.
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).
252
- */
253
- export function createTableQueryUrlSync(options = {}) {
254
- // Same prerender rule as `useUrlParam`: no query string exists while
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);
260
- function syncQuery(query) {
261
- const next = applyTableQueryToSearchParams(page.url.searchParams, query, options);
262
- const qs = next.toString();
263
- goto(`${page.url.pathname}${qs ? `?${qs}` : ''}${page.url.hash}`, {
264
- replaceState: options.replaceState ?? true,
265
- noScroll: true,
266
- keepFocus: true
267
- });
268
- }
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
- };
286
- }
@@ -0,0 +1,73 @@
1
+ import { type TableViewAxis, type TableViewLike } from './table-view.js';
2
+ /**
3
+ * Reset the module-scope writer between tests. The writer's pending-set and
4
+ * memo are keyed to a page's navigation stream; a test runner reusing the
5
+ * module across tests would otherwise leak one test's in-flight markers into
6
+ * the next.
7
+ * @internal test-only — not part of the public API.
8
+ */
9
+ export declare function __resetUrlWriterForTests(): void;
10
+ /**
11
+ * Size of the writer's live-key registry — lets the SSR suite assert that
12
+ * the module-global registry does not grow across simulated server requests.
13
+ * @internal test-only — not part of the public API.
14
+ */
15
+ export declare function __urlWriterLiveKeyCountForTests(): number;
16
+ /** Options for {@link bindViewToUrl}. */
17
+ export interface UrlViewBindingOptions {
18
+ /** Axes to bind. @default all six */
19
+ axes?: readonly TableViewAxis[];
20
+ /** Debounce for view → URL writes in ms. @default 300 */
21
+ debounceMs?: number;
22
+ /**
23
+ * Replace the current history entry instead of pushing a new one, so rapid
24
+ * sort/filter/page edits do not flood the back button.
25
+ *
26
+ * Decided for v8: this default covers **all** binding writes — v7
27
+ * continuity, keeping interactions from polluting the back button — and
28
+ * there is deliberately no per-interaction-type history mapping (a page
29
+ * turn pushing while a keystroke replaces); this one global option is the
30
+ * whole knob. Mirror-only writes (`reflectExternal`) always replace,
31
+ * whatever this is set to.
32
+ * @default true
33
+ */
34
+ replaceState?: boolean;
35
+ /**
36
+ * Key prefix (`prefix: 't_'` → `?t_q=…&t_page=…`) — namespace for a second
37
+ * bound table on the same page.
38
+ * @default ''
39
+ */
40
+ prefix?: string;
41
+ /**
42
+ * Whether *external* changes (a storage seed) are mirrored into the URL.
43
+ *
44
+ * - `false` (default): the address bar does not change without a reader
45
+ * interaction — a restored "yesterday's view" stays out of the URL until
46
+ * the reader's first edit, which then serializes the full snapshot.
47
+ * - `true`: an external application reaches the URL immediately, always
48
+ * via `replaceState` (a mirror is not a reader action, so it never
49
+ * mints a history entry) — the restored state becomes shareable without
50
+ * an interaction.
51
+ * @default false
52
+ */
53
+ reflectExternal?: boolean;
54
+ }
55
+ /**
56
+ * Bind a view to the page URL. Call during component initialisation: the
57
+ * init half runs synchronously (SSR-safe — a `?sort=…` link renders sorted
58
+ * server HTML), the runtime halves are effects.
59
+ *
60
+ * @example
61
+ * ```svelte
62
+ * <script lang="ts">
63
+ * import { Table, createTableView } from '@urbicon-ui/table';
64
+ * import { bindViewToUrl } from '@urbicon-ui/sveltekit-utils/url.svelte';
65
+ *
66
+ * const view = createTableView({ defaults: { pageSize: 25 } });
67
+ * bindViewToUrl(view);
68
+ * </script>
69
+ *
70
+ * <Table {items} {columns} {view} />
71
+ * ```
72
+ */
73
+ export declare function bindViewToUrl(view: TableViewLike, options?: UrlViewBindingOptions): void;