@atscript/ui-table 0.1.58

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,1034 @@
1
+ import { FilterExpr, Uniquery } from "@uniqu/core";
2
+ import { ColumnDef, PaginationControl, SortControl, TableDef } from "@atscript/ui";
3
+ import { Client } from "@atscript/db-client";
4
+
5
+ //#region src/filters/filter-types.d.ts
6
+ /** Filter condition type identifiers. */
7
+ type FilterConditionType = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | "starts" | "ends" | "bw" | "null" | "notNull" | "regex";
8
+ /**
9
+ * A single filter condition applied to a field.
10
+ *
11
+ * - Most conditions use `value[0]`.
12
+ * - `bw` (between) uses `value[0]` (low) and `value[1]` (high).
13
+ * - `null`/`notNull` ignore value.
14
+ */
15
+ interface FilterCondition {
16
+ type: FilterConditionType;
17
+ value: (string | number | boolean)[];
18
+ }
19
+ /** Filter conditions grouped by field path. */
20
+ type FieldFilters = Record<string, FilterCondition[]>;
21
+ //#endregion
22
+ //#region src/filters/filter-conditions.d.ts
23
+ /** Conditions that operate purely on nullability — value is ignored. */
24
+ declare const NULL_OPS: ReadonlySet<FilterConditionType>;
25
+ /** Check if a condition has a filled/meaningful value. */
26
+ declare function isFilled(condition: FilterCondition): boolean;
27
+ /** Check if a condition type requires a second value (between). */
28
+ declare function hasSecondValue(type: FilterConditionType): boolean;
29
+ declare function isSimpleEq(condition: FilterCondition): boolean;
30
+ /** Human-readable label for a condition type. */
31
+ declare function conditionLabel(type: FilterConditionType): string;
32
+ /** Count of fields that have at least one filled condition. */
33
+ declare function filledFilterCount(filters: FieldFilters): number;
34
+ /** Summarize a field's conditions into a human-readable token label. */
35
+ declare function filterTokenLabel(path: string, conditions: FilterCondition[], columnLabel?: string): string;
36
+ //#endregion
37
+ //#region src/filters/filter-conditions-map.d.ts
38
+ /** Column type categories for condition availability. */
39
+ type ColumnFilterType = "text" | "number" | "date" | "boolean" | "enum" | "ref";
40
+ /**
41
+ * Available filter conditions for a given column filter type.
42
+ * Non-nullable columns drop `null` / `notNull` since they can never match.
43
+ */
44
+ declare function conditionsForType(type: ColumnFilterType, nullable?: boolean): readonly FilterConditionType[];
45
+ /** Map a ColumnDef display type string to a ColumnFilterType. */
46
+ declare function columnFilterType(columnType: string): ColumnFilterType;
47
+ //#endregion
48
+ //#region src/filters/escape-regex.d.ts
49
+ /** Escape special regex characters in user input for safe embedding in $regex. */
50
+ declare function escapeRegex(input: string): string;
51
+ /** Reverse of {@link escapeRegex}. */
52
+ declare function unescapeRegex(input: string): string;
53
+ //#endregion
54
+ //#region src/filters/filter-input-format.d.ts
55
+ /** Default condition type when no symbol matches the input. */
56
+ declare function defaultCondition(columnType: ColumnFilterType): FilterConditionType;
57
+ /**
58
+ * Parse a user-typed filter input string into a FilterCondition.
59
+ *
60
+ * Supports operator symbols:
61
+ * *text* → contains
62
+ * text* → starts with
63
+ * *text → ends with
64
+ * =value → eq (explicit)
65
+ * !=value → ne
66
+ * >=value → gte
67
+ * >value → gt
68
+ * <=value → lte
69
+ * <value → lt
70
+ * lo...hi → bw (between)
71
+ * <empty> → null
72
+ * !<empty> → notNull
73
+ * /pattern/ → regex
74
+ *
75
+ * When no symbol matches, the default depends on columnType:
76
+ * text/enum/ref → contains
77
+ * number/date/boolean → eq
78
+ *
79
+ * Returns undefined for empty/invalid input or if the parsed operator
80
+ * is not available for the column type.
81
+ */
82
+ declare function parseFilterInput(text: string, columnType: ColumnFilterType, nullable?: boolean): FilterCondition | undefined;
83
+ /**
84
+ * Format a FilterCondition for chip/token display using operator symbols.
85
+ *
86
+ * Round-trips with parseFilterInput:
87
+ * formatFilterCondition(parseFilterInput(text, type)) ≈ text
88
+ */
89
+ declare function formatFilterCondition(condition: FilterCondition): string;
90
+ //#endregion
91
+ //#region src/filters/filters-to-uniquery.d.ts
92
+ /**
93
+ * Convert UI filter model to a Uniquery FilterExpr.
94
+ *
95
+ * Combination logic:
96
+ * - Per field: inclusion (positive) conditions are OR'd, exclusion (negative) conditions are AND'd.
97
+ * - Across fields: all groups are AND'd at the top level.
98
+ *
99
+ * Returns `undefined` when no filled conditions exist.
100
+ */
101
+ declare function filtersToUniqueryFilter(fieldFilters: FieldFilters): FilterExpr | undefined;
102
+ //#endregion
103
+ //#region src/filters/uniquery-to-filters.d.ts
104
+ /**
105
+ * Convert a Uniquery `FilterExpr` back into the UI's `FieldFilters` shape.
106
+ *
107
+ * Inverse of `filtersToUniqueryFilter`. Drops:
108
+ * - conditions on fields not in `knownFields` (when provided)
109
+ * - operators not in the supported `FilterConditionType` union
110
+ * - `$not` branches (no native UI representation; lossy on hand-crafted URLs)
111
+ *
112
+ * Returns `{}` for an empty/missing expression. Never throws.
113
+ */
114
+ declare function uniqueryFilterToFieldFilters(expr: FilterExpr | undefined, knownFields?: Iterable<string> | Set<string>): FieldFilters;
115
+ //#endregion
116
+ //#region src/filters/date-shortcuts.d.ts
117
+ /** A date shortcut produces a label and a [start, end] ISO date range. */
118
+ interface DateShortcut {
119
+ label: string;
120
+ dates: [start: string, end: string];
121
+ }
122
+ /**
123
+ * Generate date filter shortcuts relative to the given date (defaults to now).
124
+ * Each shortcut produces a `bw` (between) condition range using ISO date strings (YYYY-MM-DD).
125
+ */
126
+ declare function dateShortcuts(now?: Date): DateShortcut[];
127
+ //#endregion
128
+ //#region src/presets/preset-types.d.ts
129
+ /**
130
+ * In-memory snapshot of table state for preset persistence. Dict-shaped
131
+ * aspects (`columns.columnWidths`, `filterOps`) match the runtime state
132
+ * shape. The wire form (entries-arrays for atscript validation) is
133
+ * `PresetSnapshotWire` in `./preset-wire-types`.
134
+ *
135
+ * Per-aspect opt-in: a key's presence claims that aspect; absent keys are
136
+ * left untouched on apply.
137
+ */
138
+ interface PresetSnapshot {
139
+ columns?: {
140
+ columnNames: string[]; /** Override-only diff against column defaults. Never serialise the default. */
141
+ columnWidths?: Record<string, string>;
142
+ };
143
+ /** Displayed filter field paths (the visible-input list). */
144
+ filters?: string[];
145
+ /** Applied filter conditions, dict keyed by field path. */
146
+ filterOps?: FieldFilters;
147
+ sorters?: SortControl[];
148
+ itemsPerPage?: number;
149
+ }
150
+ //#endregion
151
+ //#region src/presets/preset-wire-types.d.ts
152
+ interface PresetColumnWidthEntry {
153
+ field: string;
154
+ width: string;
155
+ }
156
+ interface PresetFilterOpEntry {
157
+ field: string;
158
+ conditions: FilterCondition[];
159
+ }
160
+ interface PresetSorterEntry {
161
+ field: string;
162
+ direction: "asc" | "desc";
163
+ }
164
+ /**
165
+ * Wire-format snapshot stored in `AsPresetEntry.data.content`. Dict-shaped
166
+ * in-memory aspects (`columnWidths`, `filterOps`) become entries-arrays at
167
+ * the serializer boundary so atscript can validate them strictly — atscript
168
+ * has no `Record<string, T>` / `additionalProperties` construct.
169
+ *
170
+ * The client-side dict form is `PresetSnapshot` in `./preset-types`. Use
171
+ * `toWireSnapshot` / `fromWireSnapshot` (in `./preset-wire`) to bridge them.
172
+ */
173
+ interface PresetSnapshotWire {
174
+ columns?: {
175
+ columnNames: string[];
176
+ columnWidths?: PresetColumnWidthEntry[];
177
+ };
178
+ filters?: string[];
179
+ filterOps?: PresetFilterOpEntry[];
180
+ sorters?: PresetSorterEntry[];
181
+ itemsPerPage?: number;
182
+ }
183
+ //#endregion
184
+ //#region src/presets/preset-aspects.d.ts
185
+ declare const PRESET_ASPECTS: readonly ["columns", "filters", "filterOps", "sorters", "itemsPerPage"];
186
+ type PresetAspect = (typeof PRESET_ASPECTS)[number];
187
+ /**
188
+ * Per-aspect opt-in/out for `captureSnapshot(mask)`. A `true` flag includes
189
+ * the aspect; `false` / absent excludes. The capture filter intersects the
190
+ * mask with `availablePresetAspects` so unavailable aspects never leak in.
191
+ */
192
+ type AspectMask = Partial<Record<PresetAspect, boolean>>;
193
+ declare function derivePresetAspects(content: unknown): PresetAspect[];
194
+ //#endregion
195
+ //#region src/presets/preset-wire.d.ts
196
+ /**
197
+ * Convert in-memory dict-form snapshot to the wire form persisted on the
198
+ * server. Entries-arrays are sorted by `field` so consumers (server-side
199
+ * aspect derivation, dirty detection, equality checks) see a stable order.
200
+ */
201
+ declare function toWireSnapshot(snapshot: PresetSnapshot): PresetSnapshotWire;
202
+ declare function fromWireSnapshot(wire: PresetSnapshotWire): PresetSnapshot;
203
+ //#endregion
204
+ //#region src/presets/preset-data-types.d.ts
205
+ /**
206
+ * Wire-shape `data` payload for `type='preset'` rows on `AsPresetEntry`. The
207
+ * server validates `content` against the wire shape; the client converts to
208
+ * dict-form `PresetSnapshot` via `fromWireSnapshot` for in-memory use.
209
+ */
210
+ interface PresetData {
211
+ /** User-visible label. Public-name uniqueness within `(app, tableKey)`. */
212
+ label: string;
213
+ /**
214
+ * Wire-form snapshot. The set of present top-level keys drives the
215
+ * top-level `aspects` column on the row — derived automatically on every
216
+ * preset write so picker queries can project `aspects` without loading
217
+ * `data`.
218
+ */
219
+ content?: PresetSnapshotWire;
220
+ }
221
+ /**
222
+ * Per-user, per-table configuration row (`type='userConf'`, deterministic
223
+ * id `uc:${user}:${app}:${tableKey}`). Carries preferences that are
224
+ * inherently per-table (preset pins / favorites); app-wide prefs live on
225
+ * `AppConfData`.
226
+ */
227
+ interface UserConfData {
228
+ /**
229
+ * Stable preset id (or `'sys:<id>'` for system presets) pinned as the
230
+ * default for this table. Resolution is tolerant — a stale id is left
231
+ * intact and may reactivate if the referenced preset returns.
232
+ */
233
+ defaultPresetId?: string;
234
+ favPresetIds?: string[];
235
+ }
236
+ /**
237
+ * Per-user, app-wide configuration row (`type='appConf'`, deterministic id
238
+ * `ac:${user}:${app}`). Holds preferences that are inherently app-scoped —
239
+ * duplicating per-table would let them drift.
240
+ */
241
+ interface AppConfData {
242
+ appearance?: "system" | "light" | "dark";
243
+ /** BCP-47 language tag (`'en'`, `'en-US'`). Max 5 chars. */
244
+ language?: string;
245
+ /** IANA timezone name (`'America/New_York'`). Max 64 chars. */
246
+ timezone?: string;
247
+ density?: "compact" | "cozy" | "comfortable";
248
+ dateFormat?: "iso" | "us" | "eu";
249
+ /** 0 = Sunday, 1 = Monday, 6 = Saturday. */
250
+ firstDayOfWeek?: 0 | 1 | 6;
251
+ /** Escape hatch for app-specific user prefs. Capped at 1024 chars. */
252
+ customJson?: string;
253
+ }
254
+ type AsPresetEntryData = PresetData | UserConfData | AppConfData;
255
+ type AsPresetsErrorCode = "preset_limit_reached" | "reserved_id" | "public_name_conflict" | "missing_scope" | "missing_id" | "invalid_type" | "type_immutable" | "identity_immutable" | "preset_not_found" | "publish_forbidden" | "action_unsupported";
256
+ /** 409 body returned when a user's per-`(app, tableKey)` preset cap would be exceeded. */
257
+ interface PresetLimitReachedBody {
258
+ code: "preset_limit_reached";
259
+ limit: number;
260
+ count: number;
261
+ }
262
+ /**
263
+ * Browser-side row shape for `AsPresetEntry`. The server-authoritative
264
+ * shape lives in `@atscript/moost-ui-presets`'s generated `.as.d.ts`; this
265
+ * type duplicates the runtime view so client code (composables, picker,
266
+ * dialog) never imports the server-only package.
267
+ */
268
+ interface AsPresetEntryRow {
269
+ id: string;
270
+ type: "preset" | "userConf" | "appConf";
271
+ app: string;
272
+ tableKey?: string;
273
+ user: string;
274
+ /**
275
+ * Display label for `user` (e.g. their username) — server-stamped on every
276
+ * write via `AsPresetsController.getUserLabel`. Optional: when the
277
+ * controller doesn't override the hook, this is `undefined` and surfaces
278
+ * fall back to rendering `user`.
279
+ */
280
+ userLabel?: string;
281
+ /** Preset-only top-level mirror; controller stamps on every preset write. */
282
+ public?: boolean;
283
+ /** Preset-only top-level mirror of `data.label`; stamped by controller. */
284
+ label?: string;
285
+ /** Equals `label` when `public=true`, else absent. Composite-unique on `(app,tableKey,publicLabel)`. */
286
+ publicLabel?: string;
287
+ /** Derived by the controller from `data.content` keys. */
288
+ aspects?: PresetAspect[];
289
+ data: AsPresetEntryData;
290
+ createdAt: number;
291
+ updatedAt: number;
292
+ }
293
+ /**
294
+ * Per-`(app, tableKey, user)` capabilities surfaced to the client so the UI
295
+ * can hide "Save as public" / disable "Save" before the user submits. Backed
296
+ * by the same hooks the write path uses, so client-side checks and
297
+ * server-side enforcement can never diverge.
298
+ */
299
+ interface PresetCapabilities {
300
+ /** Whether the current user may set `public: true` on presets in this scope. */
301
+ canPublish: boolean;
302
+ /** Per-`(app, tableKey, user)` preset cap. Picker derives "near limit" by counting owned rows locally. */
303
+ presetLimit: number;
304
+ /**
305
+ * Server-known opaque identity for the current user (whatever
306
+ * `getCurrentUser` returns). Used by the picker / dialog to classify rows
307
+ * as "owned" without needing to derive it from the presets list — which
308
+ * fails when a user has *only* public presets (every private-row check
309
+ * falls back to null and own-public rows look like others-public). The
310
+ * controller is the single source of truth for identity, so plumbing it
311
+ * here also closes a class of off-by-one bugs across surfaces.
312
+ */
313
+ userId: string;
314
+ }
315
+ //#endregion
316
+ //#region src/presets/preset-id.d.ts
317
+ /** Reserved id namespace for synthetic system presets; rejected on write. */
318
+ declare const SYSTEM_PRESET_PREFIX = "sys:";
319
+ /** Deterministic id prefix for `type='userConf'` rows: `uc:${user}:${app}:${tableKey}`. */
320
+ declare const USER_CONF_PREFIX = "uc:";
321
+ /** Deterministic id prefix for `type='appConf'` rows: `ac:${user}:${app}`. */
322
+ declare const APP_CONF_PREFIX = "ac:";
323
+ /** All prefixes the server owns; client-supplied ids starting with any of these are rejected. */
324
+ declare const RESERVED_ID_PREFIXES: readonly ["sys:", "uc:", "ac:"];
325
+ /** Always-materialised system preset id; consumer may override label/content. */
326
+ declare const STANDARD_PRESET_ID: "sys:standard";
327
+ declare function userConfId(user: string, app: string, tableKey: string): string;
328
+ declare function appConfId(user: string, app: string): string;
329
+ declare function isSystemPresetId(id: string | null | undefined): boolean;
330
+ /**
331
+ * Auto-prefix a bare system-preset id (`'monitoring'` → `'sys:monitoring'`).
332
+ * Returns the input unchanged if it already carries the prefix.
333
+ */
334
+ declare function normaliseSystemPresetId(id: string): string;
335
+ //#endregion
336
+ //#region src/presets/system-presets.d.ts
337
+ /**
338
+ * Synthetic preset that lives only in memory — never persisted. Always
339
+ * present in the picker; users branch off via Save-as into a regular owned
340
+ * preset to evolve it.
341
+ */
342
+ interface SystemPreset {
343
+ /** Canonical `sys:*` form after normalisation. */
344
+ id: string;
345
+ label: string;
346
+ /** Baseline snapshot. Empty `{}` falls through to factory defaults. */
347
+ content: PresetSnapshot;
348
+ }
349
+ /**
350
+ * Consumer-supplied entry. `content` is optional so a consumer can override
351
+ * Standard's label without supplying a snapshot.
352
+ */
353
+ interface SystemPresetInput {
354
+ /** Bare ids are auto-prefixed (`'monitoring'` → `'sys:monitoring'`). */
355
+ id: string;
356
+ label: string;
357
+ content?: PresetSnapshot;
358
+ }
359
+ /**
360
+ * Resolve the consumer's `:system-presets` prop into the canonical render
361
+ * order: Standard at index 0 (consumer override or default empty fallback),
362
+ * named system presets in array order. Duplicate ids are dropped with a
363
+ * console.warn (first wins).
364
+ */
365
+ declare function resolveSystemPresets(input?: SystemPresetInput[]): SystemPreset[];
366
+ //#endregion
367
+ //#region src/presets/preset-dirty.d.ts
368
+ /**
369
+ * JSON-stringify with object keys sorted alphabetically at every depth so
370
+ * dict-shaped values (`columnWidths`, `filterOps` after deserialisation)
371
+ * compare structurally regardless of insertion order. Arrays preserve order.
372
+ *
373
+ * Used by the localStorage draft serializer (where a stable string is
374
+ * needed for storage); dirty detection prefers `isDirtyAgainst` (no
375
+ * stringify, short-circuits on first mismatch).
376
+ */
377
+ declare function stableStringify(value: unknown): string;
378
+ /**
379
+ * Per-aspect dirty check: only aspects the active preset claims (key
380
+ * present) are compared. A column-only preset stays clean while filters
381
+ * change; a filter-ops-only preset doesn't dirty when columns reorder.
382
+ *
383
+ * `current` should be the full snapshot of all available aspects (i.e.
384
+ * `captureSnapshot()` output) so each claimed aspect on the active preset
385
+ * has something to compare against.
386
+ */
387
+ declare function isDirtyAgainst(active: PresetSnapshot, current: PresetSnapshot): boolean;
388
+ //#endregion
389
+ //#region src/presets/preset-draft.d.ts
390
+ /**
391
+ * localStorage overlay shape. Subset of `PresetSnapshot` — `filterOps` is
392
+ * intentionally excluded (filter values are situational; restoring them on
393
+ * mount surprises users — see PRESETS-PHASE-2 §3.8). `itemsPerPage` is
394
+ * gated on `availableAspects`.
395
+ */
396
+ type PresetDraft = Omit<PresetSnapshot, "filterOps">;
397
+ /** Aspects eligible for local-draft persistence. `filterOps` is excluded by design. */
398
+ declare const DRAFT_PERSISTED_ASPECTS: readonly ["columns", "filters", "sorters", "itemsPerPage"];
399
+ type DraftPersistedAspect = (typeof DRAFT_PERSISTED_ASPECTS)[number];
400
+ /**
401
+ * Capture the persisted-aspect subset of a full snapshot. Aspects not in
402
+ * `availableAspects` are skipped (forward-compat: a deploy that toggles an
403
+ * aspect off doesn't error on previously-saved drafts).
404
+ */
405
+ declare function serializeDraft(snapshot: PresetSnapshot, availableAspects: readonly PresetAspect[]): PresetDraft;
406
+ /**
407
+ * Convert a localStorage draft back to a partial snapshot suitable for
408
+ * `applyPreset`. Aspects no longer in `availableAspects` are silently
409
+ * skipped.
410
+ */
411
+ declare function deserializeDraft(draft: PresetDraft, availableAspects: readonly PresetAspect[]): PresetSnapshot;
412
+ /** True when the draft has zero persisted aspects to apply. */
413
+ declare function isEmptyDraft(draft: PresetDraft): boolean;
414
+ /**
415
+ * True when the draft's persisted aspects exactly match the active
416
+ * preset's persisted aspects. Used by the watcher to decide whether to
417
+ * `localStorage.removeItem` instead of writing — keeps storage tidy and
418
+ * avoids `localStorage.length` noise after the user reverts edits.
419
+ */
420
+ declare function draftMatchesPreset(draft: PresetDraft, presetSnapshot: PresetSnapshot, availableAspects: readonly PresetAspect[]): boolean;
421
+ //#endregion
422
+ //#region src/presets/presets-client.d.ts
423
+ /**
424
+ * Configuration for a `PresetsClient`. Either pass a pre-built `client`
425
+ * (already wired with auth/fetch by the consumer's `ClientFactory`), or
426
+ * pass `clientFactory + url` so the wrapper builds one. The capabilities
427
+ * endpoint is fetched outside the standard CRUD path; if your app uses
428
+ * non-cookie auth, supply `fetch` so headers/credentials propagate.
429
+ */
430
+ interface PresetsClientConfig {
431
+ /** Controller mount URL, e.g. `"/db/_presets"`. Required. */
432
+ url: string;
433
+ app: string;
434
+ tableKey: string;
435
+ /** Pre-built Client (auth-configured by the host). Wins over `clientFactory`. */
436
+ client?: Client;
437
+ /** Builds a Client for the given URL. Defaults to the app-wide `getDefaultClientFactory()`. */
438
+ clientFactory?: (url: string) => Client;
439
+ /**
440
+ * Fetch implementation for the `GET /capabilities` side-channel. Defaults
441
+ * to `globalThis.fetch`. Cookie-based auth needs no override.
442
+ */
443
+ fetch?: typeof globalThis.fetch;
444
+ }
445
+ interface PresetsListResult {
446
+ /** type='preset' rows. */
447
+ presets: AsPresetEntryRow[];
448
+ /** type='userConf' row for this `(user, app, tableKey)`, or null. */
449
+ userConf: AsPresetEntryRow | null;
450
+ /**
451
+ * Server-issued capabilities. `null` when capabilities load failed (auth,
452
+ * network). `undefined` when this call skipped the capabilities fetch
453
+ * (refresh-after-mutation) — callers should leave their cached value
454
+ * untouched.
455
+ */
456
+ capabilities: PresetCapabilities | null | undefined;
457
+ /** True when the controller responded 401/403 — UI silently hides. */
458
+ denied: boolean;
459
+ }
460
+ interface PresetsSaveAsOptions {
461
+ public?: boolean;
462
+ }
463
+ interface PresetsSaveResult {
464
+ id: string;
465
+ }
466
+ /**
467
+ * Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the
468
+ * `AsPresetEntry` table. Handles wire serialisation, list-splitting by
469
+ * `type`, capabilities side-channel, and 401/403 → `denied` semantics.
470
+ *
471
+ * Stateless: every method is a fresh request. The Vue composable layer
472
+ * holds reactive state; this class only translates intent → HTTP.
473
+ */
474
+ declare class PresetsClient {
475
+ private readonly url;
476
+ private readonly app;
477
+ private readonly tableKey;
478
+ private readonly client;
479
+ private readonly fetchImpl;
480
+ constructor(cfg: PresetsClientConfig);
481
+ /**
482
+ * Lists owned + public preset rows AND the user's userConf row for this
483
+ * `(app, tableKey)`. By default also fetches `capabilities` in parallel —
484
+ * pass `{ capabilities: false }` for refresh-after-mutation calls (fav
485
+ * toggle, default change, save/save-as, public toggle, rename, delete)
486
+ * where role-derived capabilities can't have changed. Auth errors
487
+ * (401/403) collapse to `denied: true` with empty data so the UI hides
488
+ * itself silently.
489
+ */
490
+ list(opts?: {
491
+ capabilities?: boolean;
492
+ }): Promise<PresetsListResult>;
493
+ /** GET `${url}/capabilities?app=…&tableKey=…`. Out-of-band of CRUD plumbing. */
494
+ loadCapabilities(): Promise<PresetCapabilities>;
495
+ /**
496
+ * Overwrite the active preset's content. Server shallow-merges `data` so
497
+ * `label` is preserved on the row regardless — the caller sends it
498
+ * anyway because the client-side validator can't pick the preset
499
+ * variant of the `data` union without `label` present (it's required
500
+ * on that variant).
501
+ */
502
+ savePreset(id: string, label: string, snapshot: PresetSnapshot): Promise<void>;
503
+ /**
504
+ * Create a new preset row. Server stamps `user`, generates a UUID `id`,
505
+ * and derives `aspects` from the snapshot keys.
506
+ */
507
+ savePresetAs(label: string, snapshot: PresetSnapshot, opts?: PresetsSaveAsOptions): Promise<PresetsSaveResult>;
508
+ /** Update only the label. Server re-stamps top-level `label` + `publicLabel`. */
509
+ renamePreset(id: string, label: string): Promise<void>;
510
+ /** Toggle `public`. Server re-stamps `publicLabel` accordingly. */
511
+ setPublic(id: string, value: boolean): Promise<void>;
512
+ deletePreset(id: string): Promise<void>;
513
+ /**
514
+ * Upsert the userConf row keyed on `${USER_CONF_PREFIX}${user}:${app}:${tableKey}`.
515
+ * Caller must know whether the row exists from a prior `list()` so we
516
+ * pick the right verb. Server forces `user` and `id` on insert.
517
+ */
518
+ upsertUserConf(existing: AsPresetEntryRow | null, patch: Partial<UserConfData>, user?: string): Promise<void>;
519
+ }
520
+ /**
521
+ * Error raised for non-2xx responses on the capabilities side-channel.
522
+ * Standard CRUD errors flow through `Client`'s `ClientError`.
523
+ */
524
+ declare class PresetsHttpError extends Error {
525
+ readonly status: number;
526
+ constructor(status: number, message: string);
527
+ }
528
+ /** True for HTTP 401/403 across both `ClientError` and `PresetsHttpError`. */
529
+ declare function isAuthError(err: unknown): boolean;
530
+ //#endregion
531
+ //#region src/presets/app-prefs-client.d.ts
532
+ /**
533
+ * Configuration for an `AppPrefsClient`. App-wide user prefs (`appConf`)
534
+ * have no `tableKey` — they're scoped per `(user, app)` only.
535
+ */
536
+ interface AppPrefsClientConfig {
537
+ /** Same controller URL the presets table is mounted on. */
538
+ url: string;
539
+ app: string;
540
+ /** Pre-built Client (auth-configured by host). */
541
+ client?: Client;
542
+ /** Builds a Client when `client` is absent. Defaults to the app-wide `getDefaultClientFactory()`. */
543
+ clientFactory?: (url: string) => Client;
544
+ }
545
+ interface AppPrefsLoadResult {
546
+ /** Full row (server-stamped id, user, timestamps), or `null` when none exists. */
547
+ row: AsPresetEntryRow | null;
548
+ /** Convenience accessor for `row.data` (the typed prefs payload), or `null`. */
549
+ prefs: AppConfData | null;
550
+ /** True when the controller responded 401/403. */
551
+ denied: boolean;
552
+ }
553
+ /**
554
+ * Framework-agnostic wrapper for app-wide user preferences (`type='appConf'`).
555
+ * Independent of the preset/userConf surface — devs use this directly to
556
+ * read/write `appearance`, `language`, `density`, etc., without involving
557
+ * any table.
558
+ */
559
+ declare class AppPrefsClient {
560
+ private readonly app;
561
+ private readonly client;
562
+ constructor(cfg: AppPrefsClientConfig);
563
+ /** Fetch the single `appConf` row for `(user, app)`. */
564
+ load(): Promise<AppPrefsLoadResult>;
565
+ /**
566
+ * Upsert the `appConf` row. Server forces `id` from session and shallow-
567
+ * merges `data` so partial patches don't wipe unrelated fields. Caller
568
+ * passes the prior `existing` row from `load()` so we pick the right verb.
569
+ *
570
+ * Returns the row id that owns the value after the write — useful for
571
+ * state tracking after the first insert (subsequent saves take the
572
+ * update path).
573
+ */
574
+ save(existing: AsPresetEntryRow | null, patch: Partial<AppConfData>, user?: string): Promise<string | null>;
575
+ }
576
+ //#endregion
577
+ //#region src/query/build-table-query.d.ts
578
+ /** Options for building a Uniquery from table state. */
579
+ interface BuildTableQueryOptions {
580
+ /** Paths of visible columns — used for `$select` projection. */
581
+ visibleColumnPaths: string[];
582
+ /** User-configured sorters. */
583
+ sorters: SortControl[];
584
+ /** Always-applied sorters (prepended before user sorters). */
585
+ forceSorters?: SortControl[];
586
+ /** User-configured field filters. */
587
+ filters: FieldFilters;
588
+ /** Always-applied Uniquery filter (AND'd with user filters). */
589
+ forceFilters?: FilterExpr;
590
+ /** Full-text search term. */
591
+ search?: string;
592
+ /** Search index name for `$search`. */
593
+ searchIndex?: string;
594
+ /**
595
+ * Set `controls.$actions = true` so each returned row carries
596
+ * `$actions: string[]` — server-evaluated names of row/rows-level actions
597
+ * NOT disabled for that row. Off by default; renderers flip it on when a
598
+ * row-actions column will render gateable actions.
599
+ */
600
+ includeActions?: boolean;
601
+ }
602
+ /**
603
+ * Build a Uniquery object from table UI state.
604
+ *
605
+ * Pure function — no framework dependencies.
606
+ * Combines user filters with force filters, merges sorters,
607
+ * projects visible columns, and applies pagination.
608
+ */
609
+ declare function buildTableQuery(opts: BuildTableQueryOptions): Uniquery;
610
+ //#endregion
611
+ //#region src/query/merge-sorters.d.ts
612
+ /**
613
+ * Merge force sorters with user sorters.
614
+ *
615
+ * Force sorters come first and take priority — if a field appears
616
+ * in both lists, the force entry wins and the user entry is dropped.
617
+ */
618
+ declare function mergeSorters(forceSorters: SortControl[], userSorters: SortControl[]): SortControl[];
619
+ //#endregion
620
+ //#region src/query/merge-filters.d.ts
621
+ /**
622
+ * AND-merge two filter expressions, producing a wire shape that survives
623
+ * `@uniqu/url`'s `mergeConjunction` parser collapse.
624
+ *
625
+ * The collapse problem: when two `$and` siblings target the same field
626
+ * with the same op (e.g. `{status: 'cancelled'}` AND `{status: 'shipped'}`),
627
+ * the parser silently merges them on the receiving end and one clause is
628
+ * dropped. This would let a colliding user-side filter erase a
629
+ * `forceFilters` clause, breaking that contract.
630
+ *
631
+ * Fix: detect same-field same-op collisions and wrap each repeat in
632
+ * `$not({$not: ...})`. `$not` nodes are preserved verbatim by the parser,
633
+ * and `!!p ≡ p` is a semantic identity, so the server evaluator sees the
634
+ * same AND. Non-colliding merges produce the canonical `$and` shape.
635
+ */
636
+ declare function mergeFilters(a: FilterExpr | undefined, b: FilterExpr | undefined): FilterExpr | undefined;
637
+ //#endregion
638
+ //#region src/query/url-query.d.ts
639
+ /** State subset that round-trips through the URL bridge. */
640
+ interface UrlQueryStateLike {
641
+ filters: FieldFilters;
642
+ sorters: SortControl[];
643
+ /** 1-based page number. `1` is the default and is omitted from the URL. */
644
+ page?: number;
645
+ /** Per-page size. Omitted from the URL when equal to `defaultItemsPerPage`. */
646
+ itemsPerPage?: number;
647
+ /** Full-text search term. Omitted from the URL when empty. */
648
+ searchTerm?: string;
649
+ }
650
+ /** Snapshot recovered from a URL string — partial on purpose so callers can layer it onto state. */
651
+ interface UrlQueryStateSnapshot {
652
+ filters: FieldFilters;
653
+ sorters: SortControl[];
654
+ /**
655
+ * Raw record offset from `$skip` (omitted when no `$skip` in URL). The
656
+ * decoder does NOT compute a page index — that requires `itemsPerPage`,
657
+ * which is the consumer's private preference. Consumers compute
658
+ * `page = Math.floor(skip / currentItemsPerPage) + 1`.
659
+ */
660
+ skip?: number;
661
+ searchTerm: string;
662
+ }
663
+ /**
664
+ * Per-aspect opt-in/out for the URL bridge. Honoured symmetrically by both
665
+ * `stateToUrlQueryString` (encoder) and `urlQueryStringToState` (decoder) —
666
+ * the same gate must apply to both directions or the `lastEmittedUrl`
667
+ * echo guard mismatches and produces self-echoing URLs.
668
+ *
669
+ * Default (omitted, or any field `undefined` / `true`): full sync — backward
670
+ * compatible with pre-`UrlQuerySync` behaviour.
671
+ */
672
+ interface UrlQuerySync {
673
+ /**
674
+ * Filters round-trip.
675
+ * - `true` / `undefined` (default): all filters.
676
+ * - `false` / `[]`: no filters in URL; `applyUrlQuery` skips filter writes.
677
+ * - `string[]`: only listed field paths; non-allowlist filters stay private.
678
+ */
679
+ filters?: boolean | string[];
680
+ /** Sorters — same `boolean | string[]` semantics, allowlist matches `SortControl.field`. */
681
+ sorters?: boolean | string[];
682
+ /** Whether `searchTerm` syncs as `$search`. Default true. */
683
+ search?: boolean;
684
+ /** Whether pagination (`$skip` + `$limit`) syncs. Page and limit are coupled — one knob. */
685
+ pagination?: boolean;
686
+ }
687
+ interface UrlQueryDefaults {
688
+ /** Consumer's `:limit` prop. Used to omit `$limit` from the URL when state matches it. */
689
+ defaultItemsPerPage: number;
690
+ /** Per-aspect sync gates. Omitted = full sync (existing behaviour). */
691
+ sync?: UrlQuerySync;
692
+ }
693
+ /**
694
+ * Resolve a `boolean | string[]` aspect gate into a tri-state:
695
+ * - `"all"` → pass through unchanged
696
+ * - `"none"` → empty / off
697
+ * - `Set<string>` → allowlist
698
+ */
699
+ type AspectGate = "all" | "none" | Set<string>;
700
+ declare function resolveAspectGate(value: boolean | string[] | undefined): AspectGate;
701
+ /**
702
+ * Serialize the table state subset into a URL query string.
703
+ *
704
+ * Reuses `buildTableQuery` for the filter/sort/search shape (no `$select`,
705
+ * `$actions`, `forceFilters`, `forceSorters` — those are not user state) and
706
+ * appends `$skip` / `$limit` for pagination.
707
+ *
708
+ * Returns `""` (no leading `?`) for the default view.
709
+ */
710
+ declare function stateToUrlQueryString(state: UrlQueryStateLike, defaults: UrlQueryDefaults): string;
711
+ interface UrlQueryParseOptions {
712
+ /**
713
+ * Field paths the table knows about. Conditions on fields outside this set
714
+ * are silently dropped. Omit to accept any field (useful when the table
715
+ * definition isn't loaded yet).
716
+ */
717
+ knownFields?: Iterable<string>;
718
+ /** Per-aspect sync gates — must match the encoder's config to keep the round-trip symmetric. */
719
+ sync?: UrlQuerySync;
720
+ }
721
+ /**
722
+ * Parse a URL query string back into the table state subset.
723
+ *
724
+ * Robust by design — schema drift and copy-paste errors must not break the
725
+ * recipient's view:
726
+ * - unknown fields (not in `knownFields`) → silently dropped
727
+ * - unsupported operators → silently dropped
728
+ * - unknown controls (e.g. `$weird=42`) → silently ignored
729
+ * - malformed query → `{ filters: {}, sorters: [], searchTerm: "" }`
730
+ *
731
+ * `page` and `itemsPerPage` are returned only when the URL specified them
732
+ * (`$skip` / `$limit`); callers compose them onto state without overwriting
733
+ * defaults when the URL was silent.
734
+ */
735
+ declare function urlQueryStringToState(urlString: string, opts?: UrlQueryParseOptions): UrlQueryStateSnapshot;
736
+ //#endregion
737
+ //#region src/selection/selection-fns.d.ts
738
+ /** Selection mode. */
739
+ type SelectionMode = "none" | "single" | "multi";
740
+ /**
741
+ * Toggle a single PK in the selection.
742
+ *
743
+ * - `"none"`: no-op (returns the same array reference).
744
+ * - `"single"`: replaces the selection. Toggling the already-selected PK clears it.
745
+ * - `"multi"`: adds when absent, removes when present. Order preserved on add (append).
746
+ *
747
+ * Always returns a new array on actual mutation; returns the same reference for `"none"`.
748
+ */
749
+ declare function togglePk(selection: readonly unknown[], pk: unknown, mode: SelectionMode): unknown[];
750
+ /**
751
+ * Drop every PK in `selection` that's not in `presentPks`.
752
+ *
753
+ * Returns the same `selection` reference when nothing was dropped, so callers
754
+ * can rely on identity comparison to detect a no-op without an explicit
755
+ * length check.
756
+ */
757
+ declare function trimSelection(selection: readonly unknown[], presentPks: ReadonlySet<unknown>): unknown[];
758
+ /** Map rows to their PKs via `rowValueFn`. */
759
+ declare function rowsToPks(rows: readonly Record<string, unknown>[], rowValueFn: (row: Record<string, unknown>) => unknown): unknown[];
760
+ //#endregion
761
+ //#region src/columns/column-widths.d.ts
762
+ /** Per-column width entry: `w` is the rendered width, `d` is the default. */
763
+ interface ColumnWidthEntry {
764
+ w: string;
765
+ d: string;
766
+ }
767
+ /** Map keyed by `ColumnDef.path`. Always fully populated for every column. */
768
+ type ColumnWidthsMap = Record<string, ColumnWidthEntry>;
769
+ /** Upper bound applied only to default-width computation. Annotation widths and manual resize are not capped. */
770
+ declare const MAX_DEFAULT_COLUMN_WIDTH_PX = 320;
771
+ /**
772
+ * Computes a sane default width for a column from its type + length annotation.
773
+ *
774
+ * Resolution order:
775
+ * 1. col.width (`@ui.table.width` annotation) wins outright.
776
+ * 2. Otherwise pick from a type-based table; for `text` columns with `maxLen`,
777
+ * derive from char-count using an 8px-per-char heuristic.
778
+ * 3. Cap the result at MAX_DEFAULT_COLUMN_WIDTH_PX (does not apply to step 1).
779
+ */
780
+ declare function computeDefaultColumnWidth(col: ColumnDef): string;
781
+ /**
782
+ * Reconciles a current ColumnWidthsMap against the latest column list:
783
+ * - new columns get a fresh `{ w: d, d }` entry where `d = computeDefaultColumnWidth(col)`
784
+ * - existing columns keep `w` (preserves manual resize) but refresh `d`
785
+ * - paths no longer present are kept (preserves user widths if columns reappear)
786
+ *
787
+ * Returns a NEW map only when something actually changed; otherwise returns `current`
788
+ * so a shallowRef writer can short-circuit.
789
+ */
790
+ declare function reconcileColumnWidthDefaults(allColumns: ColumnDef[], current: ColumnWidthsMap): ColumnWidthsMap;
791
+ //#endregion
792
+ //#region src/state/table-state-types.d.ts
793
+ type ConfigTab = "columns" | "filters" | "sorters";
794
+ /**
795
+ * Framework-agnostic table state data.
796
+ *
797
+ * Defines the contract that any framework wrapper (Vue/React) must implement.
798
+ * Values are plain — the framework wraps them in its own reactive primitives.
799
+ */
800
+ interface TableStateData {
801
+ /** Resolved table definition (null until metadata loads). */
802
+ tableDef: TableDef | null;
803
+ /** True while the table metadata (TableDef) is being loaded. */
804
+ loadingMetadata: boolean;
805
+ /** Names of visible columns, in display order. */
806
+ columnNames: string[];
807
+ /** Current visible columns (derived from columnNames + allColumns). */
808
+ columns: ColumnDef[];
809
+ /** All columns from tableDef (including hidden). */
810
+ allColumns: ColumnDef[];
811
+ /**
812
+ * Per-column widths keyed by column path. Always fully populated for every
813
+ * column in `allColumns` once the TableDef has loaded. Each entry carries
814
+ * the current rendered width (`w`) and the default (`d`) it would reset to.
815
+ * Default sourcing: `@ui.table.width` annotation > type+@expect.maxLen-derived.
816
+ */
817
+ columnWidths: ColumnWidthsMap;
818
+ /** Names of filter fields displayed in the filter bar. */
819
+ filterFields: string[];
820
+ /** Active field filters. */
821
+ filters: FieldFilters;
822
+ /** Active sorters. */
823
+ sorters: SortControl[];
824
+ /**
825
+ * Rows of the contiguous "results island" anchored at `resultsStart`. This
826
+ * is a real array (not a computed view) — same row references also live in
827
+ * `windowCache` at their absolute indices. Writes flow through both, so
828
+ * extending `results` automatically updates the cache.
829
+ */
830
+ results: Record<string, unknown>[];
831
+ /**
832
+ * Absolute row index where `results[0]` sits. Reset by `query()` and
833
+ * `invalidate()` from `pagination.page`; shifted by backward merges in
834
+ * `loadRange`'s digest.
835
+ */
836
+ resultsStart: number;
837
+ /**
838
+ * Universal storage for every loaded row keyed by absolute index. Includes
839
+ * the rows in `results` (same row references, two indexings).
840
+ */
841
+ windowCache: Map<number, Record<string, unknown>>;
842
+ /**
843
+ * Block firstIndex values currently being fetched by `loadRange`. A row at
844
+ * `absIdx` is in flight when its block (`floor(absIdx / blockSize) * blockSize`)
845
+ * is in this set. Drives the per-row skeleton placeholder in window-mode
846
+ * rendering. NOT set by `query()` (uses `querying`) or `queryNext()` (uses
847
+ * `queryingNext`).
848
+ */
849
+ windowLoading: Set<number>;
850
+ /** Absolute row index at the top of a windowed renderer's visible viewport. */
851
+ topIndex: number;
852
+ /** Number of fixed-pool rows a windowed renderer is currently displaying. */
853
+ viewportRowCount: number;
854
+ /** Whether a query is currently in flight. */
855
+ querying: boolean;
856
+ /** Whether a "load more" query is in flight. */
857
+ queryingNext: boolean;
858
+ /** Total row count from the server. */
859
+ totalCount: number;
860
+ /** Number of rows currently loaded. */
861
+ loadedCount: number;
862
+ /** Pagination state. */
863
+ pagination: PaginationControl;
864
+ /** Last query error (null when successful). */
865
+ queryError: Error | null;
866
+ /** Metadata loading error (null when successful). */
867
+ metadataError: Error | null;
868
+ /** True when state changed during an in-flight query (results are stale). */
869
+ mustRefresh: boolean;
870
+ /** Full-text search term. */
871
+ searchTerm: string;
872
+ }
873
+ /**
874
+ * Methods that any table state implementation must provide.
875
+ *
876
+ * The table is model-driven: these mutators are thin wrappers that each touch
877
+ * exactly one entity (`filterFields` OR `filters`, never both). Side effects
878
+ * like pagination reset or re-query happen in watchers on the root state arrays,
879
+ * not inside the mutators — so any writer (dialog, external v-model, custom
880
+ * toolbar) gets identical behaviour.
881
+ */
882
+ interface TableStateMethods {
883
+ /**
884
+ * Microtask-coalesced refresh. Synchronously sets `querying = true` so
885
+ * consumers see the loading state immediately, then schedules the actual
886
+ * fetch on the next microtask. Multiple `query()` calls and watcher-driven
887
+ * scheduleQuery calls in the same synchronous block coalesce into one
888
+ * fetch. Use this from dialogs / mutators where you don't need to await.
889
+ */
890
+ query(): void;
891
+ /**
892
+ * Synchronous refresh — fires the fetch now and returns a Promise that
893
+ * settles when the response (or error) has been processed. Cancels any
894
+ * pending coalesced query so we don't double-fire. Honours `blockQuery`,
895
+ * `forceFilters`, `forceSorters`, `queryFn`. Use this from refresh
896
+ * buttons / programmatic flows that need the result.
897
+ */
898
+ queryImmediate(): Promise<void>;
899
+ /**
900
+ * Append-style extension. Does NOT mutate `pagination.page` (extension
901
+ * runs parallel to pagination). Re-entry guarded via `queryingNext`.
902
+ */
903
+ queryNext(): void;
904
+ /**
905
+ * Page-aligned slice fetch into `windowCache` with merge-on-contiguous.
906
+ * Returns a Promise that settles when ALL dispatched blocks have settled
907
+ * (success, error, OR generation-discarded); never rejects. Resolves on
908
+ * the next microtask when zero blocks need dispatching.
909
+ */
910
+ loadRange(skip: number, limit: number): Promise<void>;
911
+ /**
912
+ * Explicit immediate wipe — clears results / cache / loading, resets
913
+ * `resultsStart` from `pagination.page`, sets `totalCount = 0`. Does NOT
914
+ * auto-refetch.
915
+ */
916
+ invalidate(): void;
917
+ /** O(1) read of `windowCache.get(absIdx)`. */
918
+ dataAt(absIdx: number): Record<string, unknown> | undefined;
919
+ /** True if the block covering `absIdx` is currently being fetched. */
920
+ loadingAt(absIdx: number): boolean;
921
+ /** Returns the last error attached to the block covering `absIdx`, or null. */
922
+ errorAt(absIdx: number): Error | null;
923
+ /** Clear all applied filters. Does not touch `filterFields`. */
924
+ resetFilters(): void;
925
+ /** Open the config dialog (optionally to a specific tab). */
926
+ showConfigDialog(tab?: ConfigTab): void;
927
+ /** Append a field to the displayed filter fields (deduped). */
928
+ addFilterField(path: string): void;
929
+ /**
930
+ * Remove a field from the displayed filter fields. Does NOT clear the
931
+ * applied filter value — display state and applied state are independent.
932
+ * Use `removeFieldFilter` to clear the value.
933
+ */
934
+ removeFilterField(path: string): void;
935
+ /** Set applied filter conditions for a field. Does not touch `filterFields`. */
936
+ setFieldFilter(path: string, conditions: FilterCondition[]): void;
937
+ /**
938
+ * Set the rendered width for a column. No-op if the column is unknown or
939
+ * the value is unchanged. Does not modify the column's default (`d`).
940
+ */
941
+ setColumnWidth(path: string, width: string): void;
942
+ /**
943
+ * Reset the rendered width back to the column's default (`d`). No-op if
944
+ * the column is unknown or already at default.
945
+ */
946
+ resetColumnWidth(path: string): void;
947
+ /**
948
+ * Clear the applied filter for a field. Does NOT remove the field from
949
+ * `filterFields` — the input row stays visible.
950
+ */
951
+ removeFieldFilter(path: string): void;
952
+ /** Open filter dialog for a column. */
953
+ openFilterDialog(column: ColumnDef): void;
954
+ /** Close filter dialog. */
955
+ closeFilterDialog(): void;
956
+ }
957
+ //#endregion
958
+ //#region src/state/tokens.d.ts
959
+ declare const DEFAULT_ROW_HEIGHT_PX = 32;
960
+ //#endregion
961
+ //#region src/state/window/page-aligned-blocks.d.ts
962
+ /**
963
+ * Clamp a window's `topIndex` to the valid `[0, totalCount - viewport]` range.
964
+ * `viewport` past `totalCount` (or zero/negative totals) collapses to `0`.
965
+ */
966
+ declare function clampTopIndex(topIndex: number, totalCount: number, viewport: number): number;
967
+ /** Index of the first row in the block that contains `absIdx`. */
968
+ declare function blockStartFor(absIdx: number, blockSize: number): number;
969
+ interface PageAlignedBlock {
970
+ page: number;
971
+ firstIndex: number;
972
+ }
973
+ declare function pageAlignedBlocksFor(skip: number, limit: number, blockSize: number): PageAlignedBlock[];
974
+ //#endregion
975
+ //#region src/state/window/results-merge.d.ts
976
+ type Row = Record<string, unknown>;
977
+ interface MergeResult {
978
+ newResults: Row[];
979
+ newResultsStart: number;
980
+ }
981
+ declare function walkForwardAbsorb(results: Row[], resultsStart: number, cache: Map<number, Row>): MergeResult;
982
+ declare function walkBackwardAbsorb(results: Row[], resultsStart: number, cache: Map<number, Row>): MergeResult;
983
+ //#endregion
984
+ //#region src/state/window/plan-fetch.d.ts
985
+ type FetchPlanMode = "jump" | "steady";
986
+ interface FetchPlan {
987
+ skip: number;
988
+ limit: number;
989
+ mode: FetchPlanMode;
990
+ }
991
+ interface PlanFetchArgs {
992
+ top: number;
993
+ viewport: number;
994
+ totalCount: number;
995
+ cache: Map<number, unknown>;
996
+ blockSize: number;
997
+ /** Threshold below which a steady prefetch fires. Typically `blockSize / 4`. */
998
+ buffer: number;
999
+ }
1000
+ declare function planFetch(args: PlanFetchArgs): FetchPlan | null;
1001
+ //#endregion
1002
+ //#region src/utils/debounce.d.ts
1003
+ /**
1004
+ * Creates a debounced version of a function that delays invocation
1005
+ * until `ms` milliseconds have elapsed since the last call.
1006
+ */
1007
+ declare function debounce<T extends (...args: unknown[]) => void>(fn: T, ms: number): T & {
1008
+ cancel(): void;
1009
+ };
1010
+ //#endregion
1011
+ //#region src/utils/equality.d.ts
1012
+ declare function arraysEqual<T>(a: readonly T[], b: readonly T[]): boolean;
1013
+ declare function sameColumnSet<T>(a: readonly T[], b: readonly T[]): boolean;
1014
+ declare function setsEqual<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean;
1015
+ declare function sortersEqual(a: SortControl[], b: SortControl[]): boolean;
1016
+ //#endregion
1017
+ //#region src/utils/reorder-column-names.d.ts
1018
+ type ColumnReorderPosition = "before" | "after";
1019
+ /**
1020
+ * Permute a column-names array: move `fromPath` next to `toPath` at the
1021
+ * resolved insertion side. Pure: returns a new array, never mutates input.
1022
+ *
1023
+ * Returns the input array unchanged when:
1024
+ * - `fromPath === toPath`
1025
+ * - either path is missing from `names`
1026
+ * - the resolved insertion index leaves the array identical
1027
+ * (e.g. `[a,b,c]` from=`b` to=`a` "after" — `b` is already after `a`).
1028
+ *
1029
+ * The insertion index is resolved against post-removal positions to keep
1030
+ * the math symmetric across left-to-right and right-to-left moves.
1031
+ */
1032
+ declare function reorderColumnNames(names: string[], fromPath: string, toPath: string, position: ColumnReorderPosition): string[];
1033
+ //#endregion
1034
+ export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DraftPersistedAspect, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };