@void-snippets/react 0.2.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +610 -75
- package/dist/index.d.ts +610 -75
- package/dist/index.js +737 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +727 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +31 -5
- package/README.md +0 -178
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
2
|
+
import { InfiniteData } from '@tanstack/react-query';
|
|
3
|
+
import { VSAdapters, VSQueryParams, VSPagination, VSListResult } from '@void-snippets/core';
|
|
4
|
+
import { Socket } from 'socket.io-client';
|
|
5
|
+
import { ReactNode } from 'react';
|
|
3
6
|
|
|
4
|
-
type CapitalizeStr<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S;
|
|
5
7
|
interface WithResourceTypes {
|
|
6
8
|
readonly __types: {
|
|
7
9
|
id: unknown;
|
|
@@ -20,88 +22,621 @@ type Create<S extends WithResourceTypes> = S["__types"]["create"];
|
|
|
20
22
|
type Update<S extends WithResourceTypes> = S["__types"]["update"];
|
|
21
23
|
type ListRaw<S extends WithResourceTypes> = S["__types"]["listRaw"];
|
|
22
24
|
type SingleRaw<S extends WithResourceTypes> = S["__types"]["singleRaw"];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
interface VSUseListReturn<TBase> {
|
|
26
|
+
list: TBase[];
|
|
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
|
+
*/
|
|
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;
|
|
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
|
+
*/
|
|
58
|
+
invalidate: () => void;
|
|
59
|
+
}
|
|
60
|
+
interface VSUseGetReturn<TDetail> {
|
|
61
|
+
item: TDetail | undefined;
|
|
62
|
+
/** True only on the first fetch when no cached data exists. */
|
|
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;
|
|
70
|
+
error: Error | null;
|
|
71
|
+
/** Re-runs this query. */
|
|
72
|
+
refetch: () => Promise<unknown>;
|
|
73
|
+
}
|
|
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;
|
|
33
89
|
};
|
|
34
|
-
interface
|
|
90
|
+
interface VSOptimisticHandlers<TBase, TId, TUpdate, TCreate = Partial<TBase>, TDetail = TBase> {
|
|
35
91
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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.
|
|
40
97
|
*
|
|
41
98
|
* @example
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
99
|
+
* update: (cache, { _id, payload }) =>
|
|
100
|
+
* cache.map(item => item._id === _id ? { ...item, ...payload } : item)
|
|
101
|
+
*/
|
|
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;
|
|
112
|
+
/**
|
|
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)
|
|
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;
|
|
63
161
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* @example
|
|
76
|
-
* // contacts.hooks.ts
|
|
77
|
-
* import { createResourceHooks } from '@void-snippets/react';
|
|
78
|
-
* import { ContactsApis } from './contacts.api';
|
|
79
|
-
*
|
|
80
|
-
* // No generics needed — all types are inferred from ContactsApis
|
|
81
|
-
* export const contactHooks = createResourceHooks('contacts', ContactsApis);
|
|
82
|
-
*
|
|
83
|
-
* // In a component:
|
|
84
|
-
* const { contacts, isContactsLoading } = contactHooks.useList();
|
|
85
|
-
* const { data } = contactHooks.useGet(id); // data: Contact.WithCreatedBy
|
|
86
|
-
* const { create, update, delete: remove } = contactHooks.useMutations();
|
|
87
|
-
*/
|
|
88
|
-
declare function createResourceHooks<K extends string, S extends WithResourceTypes>(queryKeyPrefix: K, apiService: S, options?: CreateResourceHooksOptions<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>): {
|
|
89
|
-
useList: (params?: TQueryParams) => UseListReturn<K, Base<S>>;
|
|
90
|
-
useGet: (id: Id<S>, staleTime?: number) => {
|
|
91
|
-
data: _tanstack_react_query.NoInfer<Detail<S>> | undefined;
|
|
92
|
-
isLoading: boolean;
|
|
93
|
-
error: Error | null;
|
|
94
|
-
refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<_tanstack_react_query.NoInfer<Detail<S>>, Error>>;
|
|
95
|
-
};
|
|
162
|
+
interface VSResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail, TId = unknown, TUpdate = unknown, TCreate = unknown> {
|
|
163
|
+
adapters?: VSAdapters<TListRaw, TBase, TSingleRaw, TDetail>;
|
|
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>>;
|
|
96
173
|
useMutations: () => {
|
|
97
|
-
create: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Create<S>,
|
|
174
|
+
create: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Create<S>, MutationContext | undefined>;
|
|
98
175
|
update: _tanstack_react_query.UseMutationResult<Detail<S>, Error, {
|
|
99
176
|
_id: Id<S>;
|
|
100
177
|
payload: Update<S>;
|
|
101
|
-
},
|
|
102
|
-
|
|
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;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Creates three type-safe Socket.IO hooks bound to a specific socket instance.
|
|
213
|
+
*
|
|
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.
|
|
216
|
+
*
|
|
217
|
+
* Requires socket.io-client ≥4.6.0 (for native `socket.emitWithAck` support).
|
|
218
|
+
*
|
|
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';
|
|
223
|
+
*
|
|
224
|
+
* const socket = io(process.env.SOCKET_URL, { autoConnect: false });
|
|
225
|
+
*
|
|
226
|
+
* export const { useSocketEmit, useSocketListener, useSocketConnection } =
|
|
227
|
+
* createSocketHooks<IClientToServerEvents, IServerToClientEvents>(socket);
|
|
228
|
+
*/
|
|
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>>;
|
|
103
233
|
};
|
|
104
|
-
|
|
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;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
type VSAlertVariant = "success" | "info" | "error";
|
|
505
|
+
interface VSAlertState {
|
|
506
|
+
message: ReactNode | string;
|
|
507
|
+
type: VSAlertVariant;
|
|
508
|
+
isVisible: boolean;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Manages alert/toast message state with optional auto-hide.
|
|
512
|
+
*
|
|
513
|
+
* @param autoHideDuration - ms before alert hides automatically. Pass 0 to disable. Default: 3000
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* const { alert, showAlert, hideAlert } = useAlertMessage();
|
|
517
|
+
* showAlert('Saved successfully!', 'success');
|
|
518
|
+
* showAlert(<b>Something went wrong</b>, 'error');
|
|
519
|
+
*/
|
|
520
|
+
declare function useAlertMessage(autoHideDuration?: number): {
|
|
521
|
+
alert: VSAlertState;
|
|
522
|
+
showAlert: (message: ReactNode | string, type?: VSAlertVariant) => void;
|
|
523
|
+
hideAlert: () => void;
|
|
105
524
|
};
|
|
106
525
|
|
|
107
|
-
|
|
526
|
+
type VSAsyncStatus = "idle" | "pending" | "success" | "error";
|
|
527
|
+
interface VSAsyncState<T> {
|
|
528
|
+
data: T | null;
|
|
529
|
+
status: VSAsyncStatus;
|
|
530
|
+
error: Error | null;
|
|
531
|
+
}
|
|
532
|
+
interface VSUseAsyncStateReturn<T> extends VSAsyncState<T> {
|
|
533
|
+
isLoading: boolean;
|
|
534
|
+
isSuccess: boolean;
|
|
535
|
+
isError: boolean;
|
|
536
|
+
setData: (data: T | null) => void;
|
|
537
|
+
setError: (error: Error | null) => void;
|
|
538
|
+
reset: () => void;
|
|
539
|
+
/**
|
|
540
|
+
* Executes an async function, updates state, and returns a [err, data] tuple.
|
|
541
|
+
* Allows immediate result handling without try/catch.
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* const [err, data] = await execute(() => ContactsApis.create(payload));
|
|
545
|
+
* if (err) return showAlert(err.message, 'error');
|
|
546
|
+
* showAlert('Created!', 'success');
|
|
547
|
+
*/
|
|
548
|
+
execute: (asyncFn: () => Promise<T>, options?: {
|
|
549
|
+
onSuccess?: (data: T) => void;
|
|
550
|
+
onError?: (error: Error) => void;
|
|
551
|
+
}) => Promise<[Error, null] | [null, T]>;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Generic async state machine — tracks data, status, and error for any async operation.
|
|
555
|
+
* Pair with any async function: API calls, file reads, timers, etc.
|
|
556
|
+
*
|
|
557
|
+
* @param initialData - Optional initial data value. Default: null
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* const { data, isLoading, isError, execute } = useAsyncState<User>();
|
|
561
|
+
*
|
|
562
|
+
* const handleSubmit = async () => {
|
|
563
|
+
* const [err, user] = await execute(() => fetchUser(id));
|
|
564
|
+
* if (err) return;
|
|
565
|
+
* console.log(user.name);
|
|
566
|
+
* };
|
|
567
|
+
*/
|
|
568
|
+
declare function useAsyncState<T>(initialData?: T | null): VSUseAsyncStateReturn<T>;
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Tracks elapsed time from a given start timestamp — useful for call durations,
|
|
572
|
+
* countdowns, or any elapsed-time display.
|
|
573
|
+
*
|
|
574
|
+
* @param startedAt - Unix timestamp in ms (e.g. Date.now()). Pass null/undefined to reset.
|
|
575
|
+
* @returns Formatted duration string "MM:SS"
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* const duration = useCallTimer(call.startedAt);
|
|
579
|
+
* // duration → "02:45"
|
|
580
|
+
*
|
|
581
|
+
* // Reset when no active call
|
|
582
|
+
* const duration = useCallTimer(activeCall ? activeCall.startedAt : null);
|
|
583
|
+
*/
|
|
584
|
+
declare function useCallTimer(startedAt?: number | null): string;
|
|
585
|
+
|
|
586
|
+
interface VSModalReturn<T> {
|
|
587
|
+
isOpen: boolean;
|
|
588
|
+
data: T | null;
|
|
589
|
+
isLoading: boolean;
|
|
590
|
+
openCreateModal: () => void;
|
|
591
|
+
openEditModal: (editData: T) => void;
|
|
592
|
+
setLoading: (loading: boolean) => void;
|
|
593
|
+
closeModal: () => void;
|
|
594
|
+
setModal: (open: boolean, editData?: T | null) => void;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Manages modal open/close state with optional data payload and loading state.
|
|
598
|
+
* Works for both create and edit modals — pass data to distinguish the mode.
|
|
599
|
+
*
|
|
600
|
+
* @typeParam T - The type of data the modal operates on (e.g. a Contact, User, etc.)
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* const modal = useModal<Contact.Base>();
|
|
604
|
+
*
|
|
605
|
+
* modal.openCreateModal(); // data → null (create mode)
|
|
606
|
+
* modal.openEditModal(contact); // data → contact (edit mode)
|
|
607
|
+
*
|
|
608
|
+
* if (modal.data) {
|
|
609
|
+
* // Edit mode — modal.data is Contact.Base
|
|
610
|
+
* } else {
|
|
611
|
+
* // Create mode
|
|
612
|
+
* }
|
|
613
|
+
*/
|
|
614
|
+
declare function useModal<T = unknown>(): VSModalReturn<T>;
|
|
615
|
+
|
|
616
|
+
interface VSPaginationReturn {
|
|
617
|
+
page: number;
|
|
618
|
+
limit: number;
|
|
619
|
+
onPaginationChange: (newPage: number, newLimit: number) => void;
|
|
620
|
+
resetPagination: () => void;
|
|
621
|
+
setPage: (page: number) => void;
|
|
622
|
+
setLimit: (limit: number) => void;
|
|
623
|
+
/** Ready-to-use query params object — pass directly to useList() */
|
|
624
|
+
queryParams: VSQueryParams;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Manages pagination state and produces a ready-to-use queryParams object
|
|
628
|
+
* compatible with createResourceHooks' useList() and useInfinite().
|
|
629
|
+
*
|
|
630
|
+
* @param initialPage - Starting page. Default: 1
|
|
631
|
+
* @param initialLimit - Items per page. Default: 10
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* const { queryParams, onPaginationChange } = usePagination(1, 20);
|
|
635
|
+
*
|
|
636
|
+
* const { list, isLoading } = contactHooks.useList(queryParams);
|
|
637
|
+
*
|
|
638
|
+
* <Pagination onChange={onPaginationChange} total={pagination.totalDocuments} />
|
|
639
|
+
*/
|
|
640
|
+
declare function usePagination(initialPage?: number, initialLimit?: number): VSPaginationReturn;
|
|
641
|
+
|
|
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 };
|