@void-snippets/react 0.3.0 → 0.6.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/dist/index.d.mts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
+ import { InfiniteData } from '@tanstack/react-query';
2
3
  import { VSAdapters, VSQueryParams, VSPagination, VSListResult } from '@void-snippets/core';
4
+ import { Socket } from 'socket.io-client';
3
5
  import { ReactNode } from 'react';
4
6
 
5
7
  interface WithResourceTypes {
@@ -23,81 +25,480 @@ type SingleRaw<S extends WithResourceTypes> = S["__types"]["singleRaw"];
23
25
  interface VSUseListReturn<TBase> {
24
26
  list: TBase[];
25
27
  pagination: VSPagination;
28
+ /**
29
+ * True only on the very first fetch when there is no cached data yet.
30
+ * Use this to render a full-page spinner or skeleton.
31
+ * Does NOT become true during background refetches — for that see `isRefetching`.
32
+ */
26
33
  isLoading: boolean;
34
+ /**
35
+ * True during any fetch in progress — initial load or background refetch.
36
+ * Use for a subtle progress indicator that doesn't blank out the list.
37
+ */
38
+ isFetching: boolean;
39
+ /**
40
+ * True during a background refetch while cached data is already present.
41
+ * Equivalent to `isFetching && !isLoading`.
42
+ * Use for a lightweight "Refreshing…" badge overlaid on the existing list.
43
+ */
44
+ isRefetching: boolean;
45
+ /** True when the last fetch attempt resulted in an error. */
46
+ isError: boolean;
27
47
  error: Error | null;
48
+ /**
49
+ * Re-runs this specific query. Wire to a retry button in your error state.
50
+ * Narrower than `invalidate` — only refetches this exact param variant.
51
+ */
52
+ refetch: () => Promise<unknown>;
53
+ /**
54
+ * Marks all active queries under this resource prefix as stale and
55
+ * triggers a background refetch. Broader than `refetch` — affects every
56
+ * mounted param variant of this resource simultaneously.
57
+ */
28
58
  invalidate: () => void;
29
59
  }
30
60
  interface VSUseGetReturn<TDetail> {
31
61
  item: TDetail | undefined;
62
+ /** True only on the first fetch when no cached data exists. */
32
63
  isLoading: boolean;
64
+ /** True during any fetch in progress. */
65
+ isFetching: boolean;
66
+ /** True during a background refetch while cached data is already present. */
67
+ isRefetching: boolean;
68
+ /** True when the last fetch attempt resulted in an error. */
69
+ isError: boolean;
33
70
  error: Error | null;
34
- refetch: () => void;
71
+ /** Re-runs this query. */
72
+ refetch: () => Promise<unknown>;
35
73
  }
36
- interface VSResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
74
+ /**
75
+ * The operation context passed to `onError` and `onSuccess` callbacks.
76
+ * Discriminate by `kind` to handle each mutation type differently.
77
+ */
78
+ type VSOptimisticOperation<TId, TCreate, TUpdate> = {
79
+ kind: "create";
80
+ payload: TCreate;
81
+ tempId: string;
82
+ } | {
83
+ kind: "update";
84
+ _id: TId;
85
+ payload: TUpdate;
86
+ } | {
87
+ kind: "remove";
88
+ _id: TId;
89
+ };
90
+ interface VSOptimisticHandlers<TBase, TId, TUpdate, TCreate = Partial<TBase>, TDetail = TBase> {
37
91
  /**
38
- * Adapters map your API's raw response to the library's internal format.
39
- * Omit if your API matches the default shape:
40
- * List: { data: { items, page, limit, totalPages, totalDocuments } }
41
- * Single: { data: <item> }
92
+ * Optimistically transforms the list when `update.mutate()` fires.
93
+ * `_id` is the mutation target it is **separate** from `payload`.
94
+ * Applied to every active `useList` and `useInfinite` cache.
95
+ * The `useGet` cache is shallow-merged automatically; override with `updateSingle`.
96
+ * Return a new array — never mutate `cache` in place.
42
97
  *
43
98
  * @example
44
- * createResourceHooks("contacts", ContactsApis, {
45
- * adapters: {
46
- * fromList: (raw) => ({
47
- * items: raw.results,
48
- * pagination: {
49
- * page: raw.meta.page,
50
- * limit: raw.meta.perPage,
51
- * totalPages: raw.meta.lastPage,
52
- * totalDocuments: raw.meta.total,
53
- * },
54
- * }),
55
- * fromSingle: (raw) => raw.payload,
56
- * },
57
- * })
99
+ * update: (cache, { _id, payload }) =>
100
+ * cache.map(item => item._id === _id ? { ...item, ...payload } : item)
58
101
  */
59
- adapters?: VSAdapters<TListRaw, TBase, TSingleRaw, TDetail>;
102
+ update?: (cache: TBase[], args: {
103
+ _id: TId;
104
+ payload: TUpdate;
105
+ }) => TBase[];
106
+ /**
107
+ * Overrides the default `{ ...current, ...payload }` shallow-merge for the
108
+ * `useGet` cache. Only needed when `TDetail` has nested objects requiring
109
+ * a deep merge. Ignored if `update` is not also provided.
110
+ */
111
+ updateSingle?: (current: TDetail, payload: TUpdate) => TDetail;
60
112
  /**
61
- * Default params passed to useList and useInfinite when none are provided.
62
- * @default { page: 1, limit: 10 }
113
+ * Optimistically removes an item when `remove.mutate()` fires.
114
+ * `totalDocuments` / `totalPages` are patched automatically from the diff.
115
+ * The matching `useGet` entry is staled (keeps showing data until confirmed).
116
+ * Return a new array — never mutate `cache` in place.
117
+ *
118
+ * @example
119
+ * remove: (cache, id) => cache.filter(item => item._id !== id)
63
120
  */
121
+ remove?: (cache: TBase[], id: TId) => TBase[];
122
+ /**
123
+ * Optimistically inserts an item when `create.mutate()` fires.
124
+ * Applied to every `useList` cache and the **first page** of every
125
+ * `useInfinite` cache. `totalDocuments` / `totalPages` are patched automatically.
126
+ * `tempId` is a library-generated UUID — use it to set `_id` on the item.
127
+ * Return a new array — never mutate `cache` in place.
128
+ *
129
+ * @example
130
+ * create: (cache, { payload, tempId }) => [
131
+ * { ...payload, _id: tempId as Contact.Id },
132
+ * ...cache,
133
+ * ]
134
+ */
135
+ create?: (cache: TBase[], args: {
136
+ payload: TCreate;
137
+ tempId: string;
138
+ }) => TBase[];
139
+ /**
140
+ * Called after the optimistic rollback completes when a mutation fails.
141
+ * The cache is already restored to the correct state when this fires.
142
+ * Use for resource-level error notifications across all call sites.
143
+ *
144
+ * @example
145
+ * onError: (error, operation) => {
146
+ * toast.error(`Failed to ${operation.kind} item: ${error.message}`)
147
+ * }
148
+ */
149
+ onError?: (error: Error, operation: VSOptimisticOperation<TId, TCreate, TUpdate>) => void;
150
+ /**
151
+ * Called after `effectiveBase` has been advanced for this operation.
152
+ * Fires once per successfully confirmed mutation, before the final
153
+ * invalidation when all pending operations have settled.
154
+ *
155
+ * @example
156
+ * onSuccess: (operation) => {
157
+ * if (operation.kind === 'create') analytics.track('item_created')
158
+ * }
159
+ */
160
+ onSuccess?: (operation: VSOptimisticOperation<TId, TCreate, TUpdate>) => void;
161
+ }
162
+ interface VSResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail, TId = unknown, TUpdate = unknown, TCreate = unknown> {
163
+ adapters?: VSAdapters<TListRaw, TBase, TSingleRaw, TDetail>;
64
164
  defaultParams?: VSQueryParams;
165
+ optimistic?: VSOptimisticHandlers<TBase, TId, TUpdate, TCreate, TDetail>;
166
+ }
167
+ type MutationContext = {
168
+ operationId: symbol;
169
+ };
170
+ declare function createResourceHooks<K extends string, S extends WithResourceTypes>(queryKeyPrefix: K, apiService: S, options?: VSResourceHooksOptions<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>, Id<S>, Update<S>, Create<S>>): {
171
+ useList: (params?: VSQueryParams) => VSUseListReturn<Base<S>>;
172
+ useGet: (id: Id<S>, staleTime?: number) => VSUseGetReturn<Detail<S>>;
173
+ useMutations: () => {
174
+ create: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Create<S>, MutationContext | undefined>;
175
+ update: _tanstack_react_query.UseMutationResult<Detail<S>, Error, {
176
+ _id: Id<S>;
177
+ payload: Update<S>;
178
+ }, MutationContext | undefined>;
179
+ remove: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Id<S>, MutationContext | undefined>;
180
+ };
181
+ useInfinite: (params?: VSQueryParams) => _tanstack_react_query.UseInfiniteQueryResult<InfiniteData<VSListResult<Base<S>>, unknown>, Error>;
182
+ };
183
+
184
+ type EventParams<TEvents, K extends keyof TEvents> = TEvents[K] extends (...args: infer P) => void ? P : never;
185
+ type LastIsCallback<P extends readonly unknown[]> = P extends [...infer _Rest, infer Last] ? Last extends (...args: any[]) => void ? true : false : false;
186
+ type NoAckArgs<TEvents, K extends keyof TEvents> = EventParams<TEvents, K> extends [...infer Rest, infer Last] ? Last extends (...args: any[]) => void ? Rest : EventParams<TEvents, K> : EventParams<TEvents, K>;
187
+ type AckEventKeys<TEvents> = {
188
+ [K in keyof TEvents]: LastIsCallback<TEvents[K] extends (...args: infer P) => void ? P : never> extends true ? K : never;
189
+ }[keyof TEvents];
190
+ type AckResponseType<TEvents, K extends keyof TEvents> = EventParams<TEvents, K> extends [...infer _Rest, infer Last] ? Last extends (arg: infer R, ...rest: any[]) => void ? R : never : never;
191
+ interface VSSocketConnectionReturn {
192
+ /** True when the socket has an active, confirmed connection. */
193
+ isConnected: boolean;
194
+ /**
195
+ * True while a connection or reconnection attempt is in progress.
196
+ * Resets to false when `connect` or `connect_error` fires.
197
+ */
198
+ isConnecting: boolean;
199
+ /** The socket ID assigned by the server. Undefined when disconnected. */
200
+ socketId: string | undefined;
201
+ /** The error from the last failed connection attempt. Null on success or before any attempt. */
202
+ error: Error | null;
203
+ /**
204
+ * Initiates a connection. No-op if already connected.
205
+ * Sets `isConnecting: true` until `connect` or `connect_error` fires.
206
+ */
207
+ connect: () => void;
208
+ /** Gracefully closes the connection and stops all reconnection attempts. */
209
+ disconnect: () => void;
65
210
  }
66
211
  /**
67
- * Creates a set of TanStack Query hooks for a resource.
68
- * All types are fully inferred from the `apiService` instance — no generics needed.
212
+ * Creates three type-safe Socket.IO hooks bound to a specific socket instance.
69
213
  *
70
- * @param queryKeyPrefix - TanStack Query cache key. Used to scope the cache
71
- * and for auto-invalidation. e.g. "contacts"
72
- * @param apiService - An instance of ResourceService (or a subclass).
73
- * @param options - Optional adapters and default params.
214
+ * Call once at module level the returned hooks close over the socket and both
215
+ * event-map generics, so no type parameters are needed at individual call sites.
74
216
  *
75
- * @example
76
- * export const contactHooks = createResourceHooks('contacts', ContactsApis);
217
+ * Requires socket.io-client ≥4.6.0 (for native `socket.emitWithAck` support).
77
218
  *
78
- * // useList — generic fixed shape
79
- * const { list, isLoading, pagination, error, invalidate } = contactHooks.useList();
80
- * // list is typed as Contact.Base[] ✅
219
+ * @example
220
+ * // socket-hooks.ts (create once, export and use everywhere)
221
+ * import { createSocketHooks } from '@void-snippets/react';
222
+ * import { io } from 'socket.io-client';
81
223
  *
82
- * // useGet
83
- * const { item, isLoading, error, refetch } = contactHooks.useGet(id);
84
- * // item is typed as Contact.WithCreatedBy ✅
224
+ * const socket = io(process.env.SOCKET_URL, { autoConnect: false });
85
225
  *
86
- * // useMutations
87
- * const { create, update, remove } = contactHooks.useMutations();
226
+ * export const { useSocketEmit, useSocketListener, useSocketConnection } =
227
+ * createSocketHooks<IClientToServerEvents, IServerToClientEvents>(socket);
88
228
  */
89
- declare function createResourceHooks<K extends string, S extends WithResourceTypes>(queryKeyPrefix: K, apiService: S, options?: VSResourceHooksOptions<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>): {
90
- useList: (params?: VSQueryParams) => VSUseListReturn<Base<S>>;
91
- useGet: (id: Id<S>, staleTime?: number) => VSUseGetReturn<Detail<S>>;
92
- useMutations: () => {
93
- create: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Create<S>, unknown>;
94
- update: _tanstack_react_query.UseMutationResult<Detail<S>, Error, {
95
- _id: Id<S>;
96
- payload: Update<S>;
97
- }, unknown>;
98
- remove: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Id<S>, unknown>;
229
+ declare function createSocketHooks<TClientEvents extends Record<string, (...args: any[]) => void>, TServerEvents extends Record<string, (...args: any[]) => void>>(socket: Socket<TServerEvents, TClientEvents>): {
230
+ useSocketEmit: () => {
231
+ emit: <K extends keyof TClientEvents>(event: K, ...args: NoAckArgs<TClientEvents, K>) => void;
232
+ emitWithAck: <K extends AckEventKeys<TClientEvents>>(event: K, ...args: NoAckArgs<TClientEvents, K>) => Promise<AckResponseType<TClientEvents, K>>;
99
233
  };
100
- useInfinite: (params?: VSQueryParams) => _tanstack_react_query.UseInfiniteQueryResult<_tanstack_react_query.InfiniteData<VSListResult<Base<S>>, unknown>, Error>;
234
+ useSocketListener: <K extends keyof TServerEvents>(event: K, handler: TServerEvents[K], options?: {
235
+ enabled?: boolean;
236
+ }) => void;
237
+ useSocketConnection: () => VSSocketConnectionReturn;
238
+ };
239
+
240
+ /**
241
+ * Recursively extracts named path parameters from a route path string using
242
+ * TypeScript template literal inference. Handles both required and optional
243
+ * segments, and composes correctly across nested slashes.
244
+ *
245
+ * '/users/:userId/posts/:postId' → { userId: string | number; postId: string | number }
246
+ * '/files/:path?' → { path?: string | number }
247
+ */
248
+ type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? (Param extends `${infer P}?` ? {
249
+ [K in P]?: string | number;
250
+ } : {
251
+ [K in Param]: string | number;
252
+ }) & ExtractRouteParams<`/${Rest}`> : T extends `${infer _Start}:${infer Param}` ? Param extends `${infer P}?` ? {
253
+ [K in P]?: string | number;
254
+ } : {
255
+ [K in Param]: string | number;
256
+ } : {};
257
+ /** Collapses intersection types into a single object for cleaner IDE tooltips. */
258
+ type Prettify<T> = {
259
+ [K in keyof T]: T[K];
260
+ } & {};
261
+ /** All named path parameters for a given route path literal. */
262
+ type RouteParams<Path extends string> = Prettify<ExtractRouteParams<Path>>;
263
+ /** True when the path string contains at least one named parameter. */
264
+ type HasParams<Path extends string> = keyof RouteParams<Path> extends never ? false : true;
265
+ /**
266
+ * True when Search is the `never` type — meaning no search params were
267
+ * declared on this route. Array wrapping avoids distributive evaluation.
268
+ */
269
+ type SearchIsAbsent<Search> = [Search] extends [never] ? true : false;
270
+ /**
271
+ * True when Search has at least one required (non-optional) key.
272
+ * { page: number; sort?: string } → true (page is required)
273
+ * { sort?: string } → false (all optional)
274
+ * never → false
275
+ */
276
+ type HasRequiredSearchKeys<Search> = [
277
+ Search
278
+ ] extends [never] ? false : {} extends Search ? false : true;
279
+ /** Params portion of the build() argument. Empty object when path has no params. */
280
+ type ParamsPart<Path extends string> = HasParams<Path> extends true ? {
281
+ params: RouteParams<Path>;
282
+ } : {};
283
+ /**
284
+ * Search portion of the build() argument.
285
+ * Absent when Search is never.
286
+ * Required when Search has at least one required key.
287
+ * Optional when all Search keys are optional.
288
+ */
289
+ type SearchPart<Search> = SearchIsAbsent<Search> extends true ? {} : HasRequiredSearchKeys<Search> extends true ? {
290
+ search: Search;
291
+ } : {
292
+ search?: Search;
293
+ };
294
+ /** Full combined build() options, flattened for IDE readability. */
295
+ type BuildArgs<Path extends string, Search> = Prettify<ParamsPart<Path> & SearchPart<Search>>;
296
+ /**
297
+ * True when build() can be called with zero arguments:
298
+ * no path params AND (no search OR all search keys are optional).
299
+ */
300
+ type ArgIsOptional<Path extends string, Search> = HasParams<Path> extends true ? false : SearchIsAbsent<Search> extends true ? true : HasRequiredSearchKeys<Search> extends true ? false : true;
301
+ /**
302
+ * True when BuildArgs produces an empty object — meaning build()
303
+ * takes no arguments whatsoever (no params, no search).
304
+ */
305
+ type BuildArgIsEmpty<Path extends string, Search> = HasParams<Path> extends false ? SearchIsAbsent<Search> extends true ? true : false : false;
306
+ /**
307
+ * The fully conditioned build() function signature.
308
+ *
309
+ * Four cases:
310
+ * no params + no search → () => string
311
+ * no params + all-optional search → (options?: { search?: S }) => string
312
+ * params (+ optional search) → (options: { params: P; search?: S }) => string
313
+ * required search key → (options: { search: S }) => string
314
+ */
315
+ type BuildFn<Path extends string, Search> = BuildArgIsEmpty<Path, Search> extends true ? () => string : ArgIsOptional<Path, Search> extends true ? (options?: BuildArgs<Path, Search>) => string : (options: BuildArgs<Path, Search>) => string;
316
+ /** Optional metadata that can be attached to any route definition. */
317
+ interface RouteMetadata {
318
+ /**
319
+ * Permission identifiers required to access this route.
320
+ * Read via the `handle` property in your React Router config.
321
+ *
322
+ * @example
323
+ * permissions: ['ADMIN', 'SUPER_ADMIN']
324
+ */
325
+ permissions?: string[];
326
+ /**
327
+ * Human-readable label used for breadcrumb navigation.
328
+ * Read via the `handle` property in your React Router config.
329
+ */
330
+ breadcrumb?: string;
331
+ /**
332
+ * Document title for the page.
333
+ * Read via the `handle` property in your React Router config.
334
+ */
335
+ title?: string;
336
+ /**
337
+ * Arbitrary custom metadata — analytics tags, loader IDs, feature flags, etc.
338
+ * Read via the `handle` property in your React Router config.
339
+ *
340
+ * @example
341
+ * meta: { analyticsId: 'user-detail-view', loaderKey: 'userLoader' }
342
+ */
343
+ meta?: Record<string, unknown>;
344
+ }
345
+ /**
346
+ * The intermediate type returned by `defineRoute()`.
347
+ * Consumed directly by `createRouteContract()` — you do not use this type
348
+ * directly in application code.
349
+ *
350
+ * @internal
351
+ */
352
+ type RouteDefinition<Path extends string = string, Search = never> = RouteMetadata & {
353
+ /** The absolute path string for this route. */
354
+ readonly path: Path;
355
+ /**
356
+ * Phantom type anchor — carries the Search type for downstream inference.
357
+ * Always `undefined` at runtime. Do not read or write this directly.
358
+ * @internal
359
+ */
360
+ readonly _search: Search;
361
+ /**
362
+ * Declares typed search parameters for this route. Chain immediately after
363
+ * `defineRoute()`. The generic type argument is the only input — no value
364
+ * needs to be passed.
365
+ *
366
+ * @example
367
+ * defineRoute('/users').search<{ page: number; sort?: 'asc' | 'desc' }>()
368
+ */
369
+ search<S>(): RouteDefinition<Path, S>;
370
+ };
371
+ /**
372
+ * A fully processed route node produced by `createRouteContract()`.
373
+ * This is the type you interact with throughout the application.
374
+ */
375
+ type ProcessedRoute<Path extends string = string, Search = never> = RouteMetadata & {
376
+ /** The absolute path string — use this in `createBrowserRouter`. */
377
+ readonly path: Path;
378
+ /**
379
+ * Phantom type anchor — used by `useTypedSearchParams` to infer `Search`.
380
+ * Always `undefined` at runtime. Do not read or write this directly.
381
+ * @internal
382
+ */
383
+ readonly _search: Search;
384
+ /**
385
+ * Builds a fully qualified URL for this route.
386
+ *
387
+ * TypeScript enforces at compile time that:
388
+ * - `params` is provided and fully satisfied when the path has dynamic segments.
389
+ * - `search` matches the exact shape declared via `.search<T>()`.
390
+ * - No argument is needed for routes with neither params nor required search.
391
+ */
392
+ readonly build: BuildFn<Path, Search>;
393
+ };
394
+ /** The input shape `createRouteContract` accepts. */
395
+ type RouteTree = {
396
+ [K: string]: RouteDefinition<string, any> | RouteTree;
397
+ };
398
+ /** Maps a RouteTree recursively to a tree of ProcessedRoutes. */
399
+ type ProcessedTree<T> = {
400
+ [K in keyof T]: T[K] extends RouteDefinition<infer Path, infer Search> ? ProcessedRoute<Path, Search> : T[K] extends RouteTree ? ProcessedTree<T[K]> : never;
401
+ };
402
+ /**
403
+ * Defines a single route. Pass directly to `createRouteContract()`.
404
+ *
405
+ * The second argument is purely metadata — permissions, breadcrumbs, titles.
406
+ * Chain `.search<SearchType>()` to declare typed search parameters for the route.
407
+ *
408
+ * **Use absolute paths.** Concatenating parent/child paths via template literals
409
+ * causes TypeScript server slowdowns on large apps. Be explicit.
410
+ *
411
+ * @example
412
+ * // Plain route — no params, no search
413
+ * defineRoute('/dashboard', { breadcrumb: 'Home', title: 'Dashboard' })
414
+ *
415
+ * // Route with typed search params
416
+ * defineRoute('/users', { permissions: ['ADMIN'] })
417
+ * .search<{ page: number; sort?: 'asc' | 'desc' }>()
418
+ *
419
+ * // Route with path params and optional search
420
+ * defineRoute('/users/:userId', { breadcrumb: 'User Detail' })
421
+ * .search<{ tab?: 'profile' | 'settings' }>()
422
+ *
423
+ * // Route with path params, no search
424
+ * defineRoute('/users/:userId/posts/:postId')
425
+ */
426
+ declare function defineRoute<Path extends string>(path: Path, config?: RouteMetadata): RouteDefinition<Path, never>;
427
+ /**
428
+ * Processes a tree of `defineRoute()` definitions into a fully typed contract.
429
+ *
430
+ * Every route leaf gains a `build()` function whose signature is automatically
431
+ * conditioned on the presence of path params and search params. Nested groups
432
+ * are preserved as plain objects. All metadata (`path`, `permissions`,
433
+ * `breadcrumb`, `title`, `meta`) flows through to the output unchanged.
434
+ *
435
+ * Call once at module level and export. Import `AppRoutes` wherever you need
436
+ * to build a URL, wire up the router, or access route metadata.
437
+ *
438
+ * @example
439
+ * // routes.ts
440
+ * export const AppRoutes = createRouteContract({
441
+ * auth: {
442
+ * login: defineRoute('/auth/login').search<{ redirect?: string }>(),
443
+ * register: defineRoute('/auth/register'),
444
+ * },
445
+ * dashboard: {
446
+ * root: defineRoute('/dashboard', { breadcrumb: 'Home', title: 'Dashboard' }),
447
+ * users: {
448
+ * list: defineRoute('/dashboard/users', {
449
+ * permissions: ['ADMIN'],
450
+ * breadcrumb: 'Users',
451
+ * }).search<{ page: number; sort?: 'asc' | 'desc' }>(),
452
+ * detail: defineRoute('/dashboard/users/:userId', {
453
+ * permissions: ['ADMIN'],
454
+ * breadcrumb: 'User Detail',
455
+ * }).search<{ tab?: 'profile' | 'settings' }>(),
456
+ * },
457
+ * },
458
+ * });
459
+ */
460
+ declare function createRouteContract<T extends RouteTree>(tree: T): ProcessedTree<T>;
461
+ /**
462
+ * Returns the current URL search params typed to the shape declared on the
463
+ * route, plus a type-safe setter and a clear function.
464
+ *
465
+ * Pass any processed route that was created with `.search<T>()`. TypeScript
466
+ * infers `T` automatically — no generics needed at the call site.
467
+ *
468
+ * **`setSearch` merges** — it does not replace the entire query string.
469
+ * Pass only the keys you want to change; everything else is preserved.
470
+ * Set a key to `undefined` or `null` to remove it from the URL.
471
+ *
472
+ * ⚠️ **Runtime coercion note:** React Router's `useSearchParams` returns all
473
+ * values as strings. If you declare `page: number`, `search.page` will be
474
+ * the string `"1"` at runtime even though TypeScript types it as `number`.
475
+ * Coerce where needed: `Number(search.page)`. This is a deliberate trade-off
476
+ * that avoids requiring a runtime schema library.
477
+ *
478
+ * @example
479
+ * // Inside the /dashboard/users page component
480
+ * const { search, setSearch, clearSearch } =
481
+ * useTypedSearchParams(AppRoutes.dashboard.users.list);
482
+ *
483
+ * // search.page is typed as `number | undefined`
484
+ * // but is a string at runtime — coerce explicitly:
485
+ * const page = Number(search.page ?? 1);
486
+ *
487
+ * setSearch({ page: 2 }) // keeps sort, q; updates page
488
+ * setSearch({ page: 1, sort: 'asc' }) // updates page and sort; keeps q
489
+ * setSearch({ sort: undefined }) // removes sort from the URL
490
+ * clearSearch() // wipes all search params
491
+ */
492
+ declare function useTypedSearchParams<P extends string, S>(_route: ProcessedRoute<P, S>): {
493
+ /** Current search params, typed as a partial of the declared search shape. */
494
+ readonly search: Readonly<Partial<S>>;
495
+ /**
496
+ * Merges the given partial update into the current search params and
497
+ * pushes a new URL entry. Set a key to `undefined` to remove it.
498
+ */
499
+ setSearch: (update: Partial<S>) => void;
500
+ /** Removes all search parameters from the URL. */
501
+ clearSearch: () => void;
101
502
  };
102
503
 
103
504
  type VSAlertVariant = "success" | "info" | "error";
@@ -238,4 +639,4 @@ interface VSPaginationReturn {
238
639
  */
239
640
  declare function usePagination(initialPage?: number, initialLimit?: number): VSPaginationReturn;
240
641
 
241
- export { type VSAlertState, type VSAlertVariant, type VSAsyncStatus, type VSModalReturn, type VSPaginationReturn, type VSResourceHooksOptions, type VSUseAsyncStateReturn, type VSUseGetReturn, type VSUseListReturn, createResourceHooks, useAlertMessage, useAsyncState, useCallTimer, useModal, usePagination };
642
+ export { type ProcessedRoute, type RouteDefinition, type RouteMetadata, type VSAlertState, type VSAlertVariant, type VSAsyncStatus, type VSModalReturn, type VSOptimisticHandlers, type VSOptimisticOperation, type VSPaginationReturn, type VSResourceHooksOptions, type VSSocketConnectionReturn, type VSUseAsyncStateReturn, type VSUseGetReturn, type VSUseListReturn, createResourceHooks, createRouteContract, createSocketHooks, defineRoute, useAlertMessage, useAsyncState, useCallTimer, useModal, usePagination, useTypedSearchParams };