@vertz/ui 0.2.0 → 0.2.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.
Files changed (35) hide show
  1. package/README.md +339 -857
  2. package/dist/css/public.d.ts +24 -27
  3. package/dist/css/public.js +5 -1
  4. package/dist/form/public.d.ts +94 -38
  5. package/dist/form/public.js +5 -3
  6. package/dist/index.d.ts +696 -167
  7. package/dist/index.js +461 -84
  8. package/dist/internals.d.ts +192 -23
  9. package/dist/internals.js +151 -102
  10. package/dist/jsx-runtime/index.d.ts +44 -17
  11. package/dist/jsx-runtime/index.js +26 -7
  12. package/dist/query/public.d.ts +62 -7
  13. package/dist/query/public.js +12 -4
  14. package/dist/router/public.d.ts +186 -26
  15. package/dist/router/public.js +22 -7
  16. package/dist/shared/{chunk-f1ynwam4.js → chunk-0p5f7gmg.js} +155 -32
  17. package/dist/shared/{chunk-j8vzvne3.js → chunk-9e92w0wt.js} +4 -1
  18. package/dist/shared/{chunk-xd9d7q5p.js → chunk-cq7xg4xe.js} +59 -10
  19. package/dist/shared/chunk-g4rch80a.js +33 -0
  20. package/dist/shared/{chunk-pgymxpn1.js → chunk-hrd0mft1.js} +136 -34
  21. package/dist/shared/chunk-nmjyj8p9.js +290 -0
  22. package/dist/shared/chunk-pp3a6xbn.js +483 -0
  23. package/dist/shared/chunk-prj7nm08.js +67 -0
  24. package/dist/shared/chunk-q6cpe5k7.js +230 -0
  25. package/dist/shared/chunk-ryb49346.js +374 -0
  26. package/dist/shared/chunk-v3yyf79g.js +48 -0
  27. package/dist/shared/chunk-vx0kzack.js +103 -0
  28. package/dist/shared/chunk-wv6kkj1w.js +464 -0
  29. package/dist/test/index.d.ts +67 -6
  30. package/dist/test/index.js +4 -3
  31. package/package.json +13 -8
  32. package/dist/shared/chunk-bp3v6s9j.js +0 -62
  33. package/dist/shared/chunk-d8h2eh8d.js +0 -141
  34. package/dist/shared/chunk-tsdpgmks.js +0 -98
  35. package/dist/shared/chunk-zbbvx05f.js +0 -202
@@ -0,0 +1,103 @@
1
+ // src/router/server-nav.ts
2
+ function parseSSE(buffer) {
3
+ const events = [];
4
+ const blocks = buffer.split(`
5
+
6
+ `);
7
+ const remaining = blocks.pop() ?? "";
8
+ for (const block of blocks) {
9
+ if (block.trim() === "")
10
+ continue;
11
+ let type = "";
12
+ let data = "";
13
+ for (const line of block.split(`
14
+ `)) {
15
+ if (line.startsWith("event: ")) {
16
+ type = line.slice(7).trim();
17
+ } else if (line.startsWith("data: ")) {
18
+ data = line.slice(6);
19
+ }
20
+ }
21
+ if (type) {
22
+ events.push({ type, data });
23
+ }
24
+ }
25
+ return { events, remaining };
26
+ }
27
+ function ensureSSRDataBus() {
28
+ const g = globalThis;
29
+ g.__VERTZ_SSR_DATA__ = [];
30
+ g.__VERTZ_SSR_PUSH__ = (key, data) => {
31
+ g.__VERTZ_SSR_DATA__.push({ key, data });
32
+ document.dispatchEvent(new CustomEvent("vertz:ssr-data", { detail: { key, data } }));
33
+ };
34
+ }
35
+ function pushNavData(key, data) {
36
+ const push = globalThis.__VERTZ_SSR_PUSH__;
37
+ if (typeof push === "function") {
38
+ push(key, data);
39
+ }
40
+ }
41
+ function isNavPrefetchActive() {
42
+ return globalThis.__VERTZ_NAV_PREFETCH_ACTIVE__ === true;
43
+ }
44
+ function setNavPrefetchActive(active) {
45
+ globalThis.__VERTZ_NAV_PREFETCH_ACTIVE__ = active;
46
+ }
47
+ function dispatchPrefetchDone() {
48
+ setNavPrefetchActive(false);
49
+ document.dispatchEvent(new CustomEvent("vertz:nav-prefetch-done"));
50
+ }
51
+ function prefetchNavData(url, options) {
52
+ const controller = new AbortController;
53
+ const timeout = options?.timeout ?? 5000;
54
+ ensureSSRDataBus();
55
+ setNavPrefetchActive(true);
56
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
57
+ const done = fetch(url, {
58
+ headers: { "X-Vertz-Nav": "1" },
59
+ signal: controller.signal
60
+ }).then(async (response) => {
61
+ if (!response.body) {
62
+ dispatchPrefetchDone();
63
+ return;
64
+ }
65
+ const reader = response.body.getReader();
66
+ const decoder = new TextDecoder;
67
+ let buffer = "";
68
+ while (true) {
69
+ const { done: streamDone, value } = await reader.read();
70
+ if (streamDone)
71
+ break;
72
+ buffer += decoder.decode(value, { stream: true });
73
+ const parsed = parseSSE(buffer);
74
+ buffer = parsed.remaining;
75
+ for (const event of parsed.events) {
76
+ if (event.type === "data") {
77
+ try {
78
+ const { key, data } = JSON.parse(event.data);
79
+ pushNavData(key, data);
80
+ } catch {}
81
+ } else if (event.type === "done") {
82
+ dispatchPrefetchDone();
83
+ clearTimeout(timeoutId);
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ dispatchPrefetchDone();
89
+ }).catch(() => {
90
+ dispatchPrefetchDone();
91
+ }).finally(() => {
92
+ clearTimeout(timeoutId);
93
+ });
94
+ return {
95
+ abort: () => {
96
+ controller.abort();
97
+ clearTimeout(timeoutId);
98
+ },
99
+ done
100
+ };
101
+ }
102
+
103
+ export { isNavPrefetchActive, prefetchNavData };
@@ -0,0 +1,464 @@
1
+ import {
2
+ isNavPrefetchActive
3
+ } from "./chunk-vx0kzack.js";
4
+ import {
5
+ getAdapter,
6
+ isRenderNode
7
+ } from "./chunk-g4rch80a.js";
8
+ import {
9
+ _tryOnCleanup,
10
+ computed,
11
+ domEffect,
12
+ lifecycleEffect,
13
+ popScope,
14
+ pushScope,
15
+ runCleanups,
16
+ setReadValueCallback,
17
+ signal,
18
+ untrack
19
+ } from "./chunk-hrd0mft1.js";
20
+
21
+ // src/query/cache.ts
22
+ class MemoryCache {
23
+ _store = new Map;
24
+ get(key) {
25
+ return this._store.get(key);
26
+ }
27
+ set(key, value) {
28
+ this._store.set(key, value);
29
+ }
30
+ delete(key) {
31
+ this._store.delete(key);
32
+ }
33
+ clear() {
34
+ this._store.clear();
35
+ }
36
+ }
37
+
38
+ // src/query/key-derivation.ts
39
+ function deriveKey(thunk) {
40
+ return `__q:${hashString(thunk.toString())}`;
41
+ }
42
+ function hashString(str) {
43
+ let hash = 5381;
44
+ for (let i = 0;i < str.length; i++) {
45
+ hash = hash * 33 ^ str.charCodeAt(i);
46
+ }
47
+ return (hash >>> 0).toString(36);
48
+ }
49
+
50
+ // src/query/query.ts
51
+ import { isQueryDescriptor } from "@vertz/fetch";
52
+
53
+ // src/query/ssr-hydration.ts
54
+ function hydrateQueryFromSSR(key, resolve) {
55
+ const ssrData = globalThis.__VERTZ_SSR_DATA__;
56
+ if (!ssrData)
57
+ return null;
58
+ const existing = ssrData.find((entry) => entry.key === key);
59
+ if (existing) {
60
+ resolve(existing.data);
61
+ return () => {};
62
+ }
63
+ const handler = (event) => {
64
+ const detail = event.detail;
65
+ if (detail.key === key) {
66
+ resolve(detail.data);
67
+ document.removeEventListener("vertz:ssr-data", handler);
68
+ }
69
+ };
70
+ document.addEventListener("vertz:ssr-data", handler);
71
+ return () => {
72
+ document.removeEventListener("vertz:ssr-data", handler);
73
+ };
74
+ }
75
+
76
+ // src/query/query.ts
77
+ function isSSR() {
78
+ const check = typeof globalThis !== "undefined" && globalThis.__VERTZ_IS_SSR__;
79
+ return typeof check === "function" ? check() : false;
80
+ }
81
+ function getGlobalSSRTimeout() {
82
+ const g = globalThis;
83
+ const getter = typeof globalThis !== "undefined" && g.__VERTZ_GET_GLOBAL_SSR_TIMEOUT__;
84
+ return typeof getter === "function" ? getter() : undefined;
85
+ }
86
+ var defaultCache = new MemoryCache;
87
+ var inflight = new Map;
88
+ function clearDefaultQueryCache() {
89
+ defaultCache.clear();
90
+ inflight.clear();
91
+ }
92
+ globalThis.__VERTZ_CLEAR_QUERY_CACHE__ = clearDefaultQueryCache;
93
+ function query(source, options = {}) {
94
+ if (isQueryDescriptor(source)) {
95
+ return query(async () => {
96
+ const result = await source._fetch();
97
+ if (!result.ok)
98
+ throw result.error;
99
+ return result.data;
100
+ }, { ...options, key: source._key });
101
+ }
102
+ const thunk = source;
103
+ const {
104
+ initialData,
105
+ debounce: debounceMs,
106
+ enabled = true,
107
+ key: customKey,
108
+ cache = defaultCache
109
+ } = options;
110
+ const baseKey = deriveKey(thunk);
111
+ const depHashSignal = signal("");
112
+ const cacheKeyComputed = computed(() => {
113
+ const dh = depHashSignal.value;
114
+ return customKey ?? (dh ? `${baseKey}:${dh}` : `${baseKey}:init`);
115
+ });
116
+ function getCacheKey() {
117
+ return cacheKeyComputed.value;
118
+ }
119
+ function callThunkWithCapture() {
120
+ const captured = [];
121
+ const prevCb = setReadValueCallback((v) => captured.push(v));
122
+ let promise;
123
+ try {
124
+ promise = thunk();
125
+ } finally {
126
+ setReadValueCallback(prevCb);
127
+ }
128
+ const serialized = captured.map((v) => JSON.stringify(v)).join("|");
129
+ untrack(() => {
130
+ depHashSignal.value = hashString(serialized);
131
+ });
132
+ return promise;
133
+ }
134
+ const data = signal(initialData);
135
+ const loading = signal(initialData === undefined && enabled);
136
+ const revalidating = signal(false);
137
+ const error = signal(undefined);
138
+ if (initialData !== undefined) {
139
+ cache.set(getCacheKey(), initialData);
140
+ }
141
+ const ssrTimeout = options.ssrTimeout ?? getGlobalSSRTimeout() ?? 300;
142
+ if (isSSR() && enabled && ssrTimeout !== 0 && initialData === undefined) {
143
+ const promise = callThunkWithCapture();
144
+ const key = untrack(() => getCacheKey());
145
+ const cached = cache.get(key);
146
+ if (cached !== undefined) {
147
+ promise.catch(() => {});
148
+ data.value = cached;
149
+ loading.value = false;
150
+ } else {
151
+ promise.catch(() => {});
152
+ const register = globalThis.__VERTZ_SSR_REGISTER_QUERY__;
153
+ if (typeof register === "function") {
154
+ register({
155
+ promise,
156
+ timeout: ssrTimeout,
157
+ resolve: (result) => {
158
+ data.value = result;
159
+ loading.value = false;
160
+ cache.set(key, result);
161
+ },
162
+ key
163
+ });
164
+ }
165
+ }
166
+ }
167
+ let ssrHydrationCleanup = null;
168
+ let ssrHydrated = false;
169
+ let navPrefetchDeferred = false;
170
+ if (!isSSR() && enabled && initialData === undefined) {
171
+ const hydrationKey = customKey ?? baseKey;
172
+ ssrHydrationCleanup = hydrateQueryFromSSR(hydrationKey, (result) => {
173
+ data.value = result;
174
+ loading.value = false;
175
+ cache.set(hydrationKey, result);
176
+ ssrHydrated = true;
177
+ });
178
+ if (!ssrHydrated && ssrHydrationCleanup !== null && isNavPrefetchActive()) {
179
+ if (customKey) {
180
+ const cached = cache.get(customKey);
181
+ if (cached !== undefined) {
182
+ data.value = cached;
183
+ loading.value = false;
184
+ ssrHydrated = true;
185
+ }
186
+ }
187
+ }
188
+ if (!ssrHydrated && ssrHydrationCleanup !== null && isNavPrefetchActive()) {
189
+ navPrefetchDeferred = true;
190
+ const doneHandler = () => {
191
+ document.removeEventListener("vertz:nav-prefetch-done", doneHandler);
192
+ if (data.peek() === undefined) {
193
+ refetchTrigger.value = refetchTrigger.peek() + 1;
194
+ }
195
+ };
196
+ document.addEventListener("vertz:nav-prefetch-done", doneHandler);
197
+ const prevCleanup = ssrHydrationCleanup;
198
+ ssrHydrationCleanup = () => {
199
+ prevCleanup?.();
200
+ document.removeEventListener("vertz:nav-prefetch-done", doneHandler);
201
+ };
202
+ }
203
+ }
204
+ let fetchId = 0;
205
+ let debounceTimer;
206
+ const inflightKeys = new Set;
207
+ const refetchTrigger = signal(0);
208
+ function handleFetchPromise(promise, id, key) {
209
+ promise.then((result) => {
210
+ inflight.delete(key);
211
+ inflightKeys.delete(key);
212
+ if (id !== fetchId)
213
+ return;
214
+ cache.set(key, result);
215
+ data.value = result;
216
+ loading.value = false;
217
+ revalidating.value = false;
218
+ }, (err) => {
219
+ inflight.delete(key);
220
+ inflightKeys.delete(key);
221
+ if (id !== fetchId)
222
+ return;
223
+ error.value = err;
224
+ loading.value = false;
225
+ revalidating.value = false;
226
+ });
227
+ }
228
+ function startFetch(fetchPromise, key) {
229
+ const id = ++fetchId;
230
+ untrack(() => {
231
+ if (data.value !== undefined) {
232
+ revalidating.value = true;
233
+ } else {
234
+ loading.value = true;
235
+ }
236
+ error.value = undefined;
237
+ });
238
+ const existing = inflight.get(key);
239
+ if (existing) {
240
+ handleFetchPromise(existing, id, key);
241
+ return;
242
+ }
243
+ inflight.set(key, fetchPromise);
244
+ inflightKeys.add(key);
245
+ handleFetchPromise(fetchPromise, id, key);
246
+ }
247
+ function refetch() {
248
+ const key = getCacheKey();
249
+ cache.delete(key);
250
+ inflight.delete(key);
251
+ refetchTrigger.value = refetchTrigger.peek() + 1;
252
+ }
253
+ let disposeFn;
254
+ if (enabled) {
255
+ let isFirst = true;
256
+ disposeFn = lifecycleEffect(() => {
257
+ refetchTrigger.value;
258
+ if (isFirst && ssrHydrated) {
259
+ isFirst = false;
260
+ return;
261
+ }
262
+ if (isFirst && navPrefetchDeferred) {
263
+ if (customKey) {
264
+ const cached = untrack(() => cache.get(customKey));
265
+ if (cached !== undefined) {
266
+ untrack(() => {
267
+ data.value = cached;
268
+ loading.value = false;
269
+ });
270
+ isFirst = false;
271
+ return;
272
+ }
273
+ }
274
+ isFirst = false;
275
+ return;
276
+ }
277
+ if (customKey) {
278
+ const existing = untrack(() => inflight.get(customKey));
279
+ if (existing) {
280
+ const id = ++fetchId;
281
+ untrack(() => {
282
+ if (data.value !== undefined) {
283
+ revalidating.value = true;
284
+ } else {
285
+ loading.value = true;
286
+ }
287
+ error.value = undefined;
288
+ });
289
+ handleFetchPromise(existing, id, customKey);
290
+ isFirst = false;
291
+ return;
292
+ }
293
+ }
294
+ const promise = callThunkWithCapture();
295
+ const key = untrack(() => getCacheKey());
296
+ if (!customKey) {
297
+ const existing = untrack(() => inflight.get(key));
298
+ if (existing) {
299
+ promise.catch(() => {});
300
+ const id = ++fetchId;
301
+ untrack(() => {
302
+ if (data.value !== undefined) {
303
+ revalidating.value = true;
304
+ } else {
305
+ loading.value = true;
306
+ }
307
+ error.value = undefined;
308
+ });
309
+ handleFetchPromise(existing, id, key);
310
+ isFirst = false;
311
+ return;
312
+ }
313
+ }
314
+ const shouldCheckCache = isFirst ? !!customKey : !customKey;
315
+ if (shouldCheckCache) {
316
+ const cached = untrack(() => cache.get(key));
317
+ if (cached !== undefined) {
318
+ promise.catch(() => {});
319
+ untrack(() => {
320
+ data.value = cached;
321
+ loading.value = false;
322
+ error.value = undefined;
323
+ });
324
+ isFirst = false;
325
+ return;
326
+ }
327
+ }
328
+ if (isFirst && initialData !== undefined) {
329
+ promise.catch(() => {});
330
+ isFirst = false;
331
+ return;
332
+ }
333
+ isFirst = false;
334
+ if (debounceMs !== undefined && debounceMs > 0) {
335
+ clearTimeout(debounceTimer);
336
+ promise.catch(() => {});
337
+ debounceTimer = setTimeout(() => {
338
+ startFetch(promise, key);
339
+ }, debounceMs);
340
+ } else {
341
+ startFetch(promise, key);
342
+ }
343
+ });
344
+ }
345
+ function dispose() {
346
+ disposeFn?.();
347
+ ssrHydrationCleanup?.();
348
+ clearTimeout(debounceTimer);
349
+ fetchId++;
350
+ for (const key of inflightKeys) {
351
+ inflight.delete(key);
352
+ }
353
+ inflightKeys.clear();
354
+ }
355
+ _tryOnCleanup(dispose);
356
+ return {
357
+ data,
358
+ loading,
359
+ revalidating,
360
+ error,
361
+ refetch,
362
+ revalidate: refetch,
363
+ dispose
364
+ };
365
+ }
366
+
367
+ // src/query/query-match.ts
368
+ var cache = new WeakMap;
369
+ function queryMatch(queryResult, handlers) {
370
+ const key = queryResult;
371
+ const existing = cache.get(key);
372
+ if (existing && !existing.disposed) {
373
+ existing.handlers = handlers;
374
+ return existing.wrapper;
375
+ }
376
+ if (existing) {
377
+ cache.delete(key);
378
+ }
379
+ const wrapper = getAdapter().createElement("span");
380
+ wrapper.style.display = "contents";
381
+ const entry = { wrapper, handlers, disposed: false };
382
+ cache.set(key, entry);
383
+ let currentBranch = null;
384
+ let branchCleanups = [];
385
+ const outerScope = pushScope();
386
+ domEffect(() => {
387
+ const isLoading = queryResult.loading.value;
388
+ const err = queryResult.error.value;
389
+ const dataValue = queryResult.data.value;
390
+ let branch;
391
+ if (isLoading || err === undefined && dataValue === undefined) {
392
+ branch = "loading";
393
+ } else if (err !== undefined) {
394
+ branch = "error";
395
+ } else {
396
+ branch = "data";
397
+ }
398
+ if (branch === currentBranch && branch !== "data") {
399
+ return;
400
+ }
401
+ runCleanups(branchCleanups);
402
+ while (wrapper.firstChild) {
403
+ wrapper.removeChild(wrapper.firstChild);
404
+ }
405
+ currentBranch = branch;
406
+ const scope = pushScope();
407
+ let branchResult = null;
408
+ if (branch === "loading") {
409
+ branchResult = entry.handlers.loading();
410
+ } else if (branch === "error") {
411
+ branchResult = entry.handlers.error(err);
412
+ } else {
413
+ const dataSignal = queryResult.data;
414
+ const dataProxy = new Proxy({}, {
415
+ get(_target, prop, receiver) {
416
+ const current = dataSignal.value;
417
+ if (current == null)
418
+ return;
419
+ const value = Reflect.get(current, prop, receiver);
420
+ if (typeof value === "function") {
421
+ return value.bind(current);
422
+ }
423
+ return value;
424
+ },
425
+ has(_target, prop) {
426
+ const current = dataSignal.value;
427
+ if (current == null)
428
+ return false;
429
+ return Reflect.has(current, prop);
430
+ },
431
+ ownKeys() {
432
+ const current = dataSignal.value;
433
+ if (current == null)
434
+ return [];
435
+ return Reflect.ownKeys(current);
436
+ },
437
+ getOwnPropertyDescriptor(_target, prop) {
438
+ const current = dataSignal.value;
439
+ if (current == null)
440
+ return;
441
+ return Reflect.getOwnPropertyDescriptor(current, prop);
442
+ }
443
+ });
444
+ branchResult = entry.handlers.data(dataProxy);
445
+ }
446
+ popScope();
447
+ branchCleanups = scope;
448
+ if (branchResult != null && isRenderNode(branchResult)) {
449
+ wrapper.appendChild(branchResult);
450
+ }
451
+ });
452
+ popScope();
453
+ const dispose = () => {
454
+ entry.disposed = true;
455
+ runCleanups(branchCleanups);
456
+ runCleanups(outerScope);
457
+ cache.delete(key);
458
+ };
459
+ wrapper.dispose = dispose;
460
+ _tryOnCleanup(dispose);
461
+ return wrapper;
462
+ }
463
+
464
+ export { MemoryCache, deriveKey, query, queryMatch };
@@ -138,9 +138,34 @@ type ExtractParams<T extends string> = [ExtractParamsFromSegments<WithoutWildcar
138
138
  } : Record<string, never> : HasWildcard<T> extends true ? { [K in ExtractParamsFromSegments<WithoutWildcard<T>>] : string } & {
139
139
  "*": string;
140
140
  } : { [K in ExtractParamsFromSegments<WithoutWildcard<T>>] : string };
141
+ /**
142
+ * Convert a route pattern to the union of URL shapes it accepts.
143
+ * - Static: `'/'` → `'/'`
144
+ * - Param: `'/tasks/:id'` → `` `/tasks/${string}` ``
145
+ * - Wildcard: `'/files/*'` → `` `/files/${string}` ``
146
+ * - Multi: `'/users/:id/posts/:postId'` → `` `/users/${string}/posts/${string}` ``
147
+ * - Fallback: `string` → `string` (backward compat)
148
+ */
149
+ type PathWithParams<T extends string> = T extends `${infer Before}*` ? `${PathWithParams<Before>}${string}` : T extends `${infer Before}:${string}/${infer After}` ? `${Before}${string}/${PathWithParams<`${After}`>}` : T extends `${infer Before}:${string}` ? `${Before}${string}` : T;
150
+ /**
151
+ * Union of all valid URL shapes for a route map.
152
+ * Maps each route pattern key through `PathWithParams` to produce the accepted URL shapes.
153
+ *
154
+ * Example:
155
+ * ```
156
+ * RoutePaths<{ '/': ..., '/tasks/:id': ... }> = '/' | `/tasks/${string}`
157
+ * ```
158
+ */
159
+ type RoutePaths<TRouteMap extends Record<string, unknown>> = { [K in keyof TRouteMap & string] : PathWithParams<K> }[keyof TRouteMap & string];
141
160
  /** Simple schema interface for search param parsing. */
142
161
  interface SearchParamSchema<T> {
143
- parse(data: unknown): T;
162
+ parse(data: unknown): {
163
+ ok: true;
164
+ data: T;
165
+ } | {
166
+ ok: false;
167
+ error: unknown;
168
+ };
144
169
  }
145
170
  /** A route configuration for a single path. */
146
171
  interface RouteConfig<
@@ -168,6 +193,30 @@ interface RouteConfig<
168
193
  interface RouteDefinitionMap {
169
194
  [pattern: string]: RouteConfig;
170
195
  }
196
+ /**
197
+ * Loose route config used as the generic constraint for `defineRoutes`.
198
+ * Uses `Record<string, string>` for loader params so any concrete loader
199
+ * that accesses string params (e.g., `params.id`) satisfies the constraint.
200
+ */
201
+ interface RouteConfigLike {
202
+ component: () => Node | Promise<{
203
+ default: () => Node;
204
+ }>;
205
+ /**
206
+ * Method syntax (`loader?(ctx): R`) is intentional — it enables **bivariant**
207
+ * parameter checking under `strictFunctionTypes`. Property syntax
208
+ * (`loader?: (ctx) => R`) would be contravariant, causing `RouteConfig<string>`
209
+ * (whose loader has `params: Record<string, never>`) to fail assignability
210
+ * against this constraint's `params: Record<string, string>`.
211
+ */
212
+ loader?(ctx: {
213
+ params: Record<string, string>;
214
+ signal: AbortSignal;
215
+ }): unknown;
216
+ errorComponent?: (error: Error) => Node;
217
+ searchParams?: SearchParamSchema<unknown>;
218
+ children?: Record<string, RouteConfigLike>;
219
+ }
171
220
  /** Internal compiled route. */
172
221
  interface CompiledRoute {
173
222
  /** The original path pattern. */
@@ -219,8 +268,20 @@ interface NavigateOptions {
219
268
  /** Use history.replaceState instead of pushState. */
220
269
  replace?: boolean;
221
270
  }
222
- /** The router instance returned by createRouter. */
223
- interface Router {
271
+ /**
272
+ * The router instance returned by createRouter.
273
+ *
274
+ * Generic over the route map `T`. Defaults to `RouteDefinitionMap` (string
275
+ * index signature) for backward compatibility — unparameterized `Router`
276
+ * accepts any string in `navigate()`.
277
+ *
278
+ * Method syntax on `navigate`, `revalidate`, and `dispose` enables bivariant
279
+ * parameter checking under `strictFunctionTypes`. This means `Router<T>` is
280
+ * assignable to `Router` (the unparameterized default), which is required for
281
+ * storing typed routers in the `RouterContext` without contravariance errors.
282
+ * At call sites, TypeScript still enforces the `RoutePaths<T>` constraint.
283
+ */
284
+ interface Router<T extends Record<string, RouteConfigLike> = RouteDefinitionMap> {
224
285
  /** Current matched route (reactive signal). */
225
286
  current: Signal<RouteMatch | null>;
226
287
  /** Loader data from the current route's loaders (reactive signal). */
@@ -230,11 +291,11 @@ interface Router {
230
291
  /** Parsed search params from the current route (reactive signal). */
231
292
  searchParams: Signal<Record<string, unknown>>;
232
293
  /** Navigate to a new URL path. */
233
- navigate: (url: string, options?: NavigateOptions) => Promise<void>;
294
+ navigate(url: RoutePaths<T>, options?: NavigateOptions): Promise<void>;
234
295
  /** Re-run all loaders for the current route. */
235
- revalidate: () => Promise<void>;
296
+ revalidate(): Promise<void>;
236
297
  /** Remove popstate listener and clean up the router. */
237
- dispose: () => void;
298
+ dispose(): void;
238
299
  }
239
300
  /** Options for `createTestRouter`. */
240
301
  interface TestRouterOptions {
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  createRouter
3
- } from "../shared/chunk-xd9d7q5p.js";
3
+ } from "../shared/chunk-cq7xg4xe.js";
4
4
  import {
5
5
  defineRoutes
6
- } from "../shared/chunk-j8vzvne3.js";
7
- import"../shared/chunk-pgymxpn1.js";
6
+ } from "../shared/chunk-9e92w0wt.js";
7
+ import"../shared/chunk-vx0kzack.js";
8
+ import"../shared/chunk-hrd0mft1.js";
8
9
 
9
10
  // src/test/interactions.ts
10
11
  async function click(el) {