@funstack/router 0.0.3-alpha.1 → 0.0.3

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,4 +1,4 @@
1
- import { a as RouteDefinition, i as RouteComponentPropsWithData, n as PathParams, o as route, r as RouteComponentProps, s as routeState, t as LoaderArgs } from "./route-BXVQqSlR.mjs";
1
+ import { a as LoaderArgs, c as RouteComponentProps, d as TypefulOpaqueRouteDefinition, f as route, i as ExtractRouteState, l as RouteComponentPropsWithData, n as ExtractRouteId, o as OpaqueRouteDefinition, p as routeState, r as ExtractRouteParams, s as PathParams, t as ExtractRouteData, u as RouteDefinition } from "./route-sfdBlIcc.mjs";
2
2
  import { ComponentType, ReactNode } from "react";
3
3
 
4
4
  //#region src/types.d.ts
@@ -7,20 +7,22 @@ declare const InternalRouteDefinitionSymbol: unique symbol;
7
7
  * Internal structure for storing per-route state in NavigationHistoryEntry.
8
8
  * Each route in the matched stack gets its own state slot indexed by match position.
9
9
  */
10
-
11
10
  /**
12
11
  * Route definition for the router.
13
12
  * When a loader is defined, the component receives the loader result as a `data` prop.
14
13
  */
15
14
  type InternalRouteDefinition = {
16
- [InternalRouteDefinitionSymbol]: never;
17
- /** Path pattern to match (e.g., "users/:id") */
18
- path: string;
19
- /** Child routes for nested routing */
15
+ [InternalRouteDefinitionSymbol]: never; /** Path pattern to match (e.g., "users/:id"). If omitted, the route is pathless (always matches, consumes nothing). */
16
+ path?: string; /** Child routes for nested routing */
20
17
  children?: InternalRouteDefinition[];
21
- /** Data loader function for this route */
22
- loader?: (args: LoaderArgs) => unknown;
23
- /** Component to render when this route matches */
18
+ /**
19
+ * Whether this route requires an exact match.
20
+ * - true: Only matches exact pathname
21
+ * - false: Matches as prefix
22
+ * - undefined: Default (exact for leaf, prefix for parent)
23
+ */
24
+ exact?: boolean; /** Data loader function for this route */
25
+ loader?: (args: LoaderArgs) => unknown; /** Component to render when this route matches */
24
26
  component?: ComponentType<{
25
27
  data?: unknown;
26
28
  params?: Record<string, string>;
@@ -35,29 +37,29 @@ type InternalRouteDefinition = {
35
37
  * A matched route with its parameters.
36
38
  */
37
39
  type MatchedRoute = {
38
- /** The original route definition */
39
- route: InternalRouteDefinition;
40
- /** Extracted path parameters */
41
- params: Record<string, string>;
42
- /** The matched pathname segment */
40
+ /** The original route definition */route: InternalRouteDefinition; /** Extracted path parameters */
41
+ params: Record<string, string>; /** The matched pathname segment */
43
42
  pathname: string;
44
43
  };
45
44
  /**
46
45
  * A matched route with loader data.
47
46
  */
48
47
  type MatchedRouteWithData = MatchedRoute & {
49
- /** Data returned from the loader (undefined if no loader) */
50
- data: unknown | undefined;
48
+ /** Data returned from the loader (undefined if no loader) */data: unknown | undefined;
49
+ };
50
+ /**
51
+ * Information passed to onNavigate callback.
52
+ */
53
+ type OnNavigateInfo = {
54
+ /** Array of matched routes, or null if no routes matched */matches: readonly MatchedRoute[] | null; /** Whether the router will intercept this navigation (before user's preventDefault() call) */
55
+ intercepting: boolean;
51
56
  };
52
57
  /**
53
58
  * Options for navigation.
54
59
  */
55
60
  type NavigateOptions = {
56
- /** Replace current history entry instead of pushing */
57
- replace?: boolean;
58
- /** State to associate with the navigation */
59
- state?: unknown;
60
- /** Ephemeral info for this navigation only (not persisted in history) */
61
+ /** Replace current history entry instead of pushing */replace?: boolean; /** State to associate with the navigation */
62
+ state?: unknown; /** Ephemeral info for this navigation only (not persisted in history) */
61
63
  info?: unknown;
62
64
  };
63
65
  /**
@@ -73,9 +75,9 @@ type Location = {
73
75
  * Call `event.preventDefault()` to prevent the router from handling this navigation.
74
76
  *
75
77
  * @param event - The NavigateEvent from the Navigation API
76
- * @param matched - Array of matched routes, or null if no routes matched
78
+ * @param info - Information about the navigation including matched routes and whether it will be intercepted
77
79
  */
78
- type OnNavigateCallback = (event: NavigateEvent, matched: readonly MatchedRoute[] | null) => void;
80
+ type OnNavigateCallback = (event: NavigateEvent, info: OnNavigateInfo) => void;
79
81
  /**
80
82
  * Fallback mode when Navigation API is unavailable.
81
83
  *
@@ -92,7 +94,7 @@ type RouterProps = {
92
94
  * Call `event.preventDefault()` to prevent the router from handling this navigation.
93
95
  *
94
96
  * @param event - The NavigateEvent from the Navigation API
95
- * @param matched - Array of matched routes, or null if no routes matched
97
+ * @param info - Information about the navigation including matched routes and whether it will be intercepted
96
98
  */
97
99
  onNavigate?: OnNavigateCallback;
98
100
  /**
@@ -172,21 +174,91 @@ type UseBlockerOptions = {
172
174
  */
173
175
  declare function useBlocker(options: UseBlockerOptions): void;
174
176
  //#endregion
177
+ //#region src/hooks/useRouteParams.d.ts
178
+ /**
179
+ * Returns typed route parameters for the given route definition.
180
+ * Throws an error if called outside a matching route or if the route ID is not found
181
+ * in the current route hierarchy.
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const userRoute = route({
186
+ * id: "user",
187
+ * path: "/users/:userId",
188
+ * component: UserPage,
189
+ * });
190
+ *
191
+ * function UserPage() {
192
+ * const params = useRouteParams(userRoute);
193
+ * // params is typed as { userId: string }
194
+ * return <div>User ID: {params.userId}</div>;
195
+ * }
196
+ * ```
197
+ */
198
+ declare function useRouteParams<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteParams<T>;
199
+ //#endregion
200
+ //#region src/hooks/useRouteState.d.ts
201
+ /**
202
+ * Returns typed navigation state for the given route definition.
203
+ * Throws an error if called outside a matching route or if the route ID is not found
204
+ * in the current route hierarchy.
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * type ScrollState = { scrollPos: number };
209
+ * const scrollRoute = routeState<ScrollState>()({
210
+ * id: "scroll",
211
+ * path: "/scroll",
212
+ * component: ScrollPage,
213
+ * });
214
+ *
215
+ * function ScrollPage() {
216
+ * const state = useRouteState(scrollRoute);
217
+ * // state is typed as ScrollState | undefined
218
+ * return <div>Scroll position: {state?.scrollPos ?? 0}</div>;
219
+ * }
220
+ * ```
221
+ */
222
+ declare function useRouteState<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteState<T> | undefined;
223
+ //#endregion
224
+ //#region src/hooks/useRouteData.d.ts
225
+ /**
226
+ * Returns typed loader data for the given route definition.
227
+ * Throws an error if called outside a matching route or if the route ID is not found
228
+ * in the current route hierarchy.
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * const userRoute = route({
233
+ * id: "user",
234
+ * path: "/users/:userId",
235
+ * loader: async ({ params }) => {
236
+ * const res = await fetch(`/api/users/${params.userId}`);
237
+ * return res.json() as Promise<{ name: string; age: number }>;
238
+ * },
239
+ * component: UserPage,
240
+ * });
241
+ *
242
+ * function UserPage() {
243
+ * const data = useRouteData(userRoute);
244
+ * // data is typed as Promise<{ name: string; age: number }>
245
+ * return <div>User: {data.name}</div>;
246
+ * }
247
+ * ```
248
+ */
249
+ declare function useRouteData<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteData<T>;
250
+ //#endregion
175
251
  //#region src/core/RouterAdapter.d.ts
176
252
  /**
177
253
  * Represents the current location state.
178
254
  * Abstracts NavigationHistoryEntry for static mode compatibility.
179
255
  */
180
256
  type LocationEntry = {
181
- /** The current URL */
182
- url: URL;
183
- /** Unique key for this entry (used for loader caching) */
184
- key: string;
185
- /** State associated with this entry */
186
- state: unknown;
187
- /** Ephemeral info from current navigation (undefined if not from navigation event) */
257
+ /** The current URL */url: URL; /** Unique key for this entry (used for loader caching) */
258
+ key: string; /** State associated with this entry */
259
+ state: unknown; /** Ephemeral info from current navigation (undefined if not from navigation event) */
188
260
  info: unknown;
189
261
  };
190
262
  //#endregion
191
- export { type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type UseBlockerOptions, route, routeState, useBlocker, useLocation, useNavigate, useSearchParams };
263
+ export { type ExtractRouteData, type ExtractRouteId, type ExtractRouteParams, type ExtractRouteState, type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, type OnNavigateInfo, type OpaqueRouteDefinition, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type TypefulOpaqueRouteDefinition, type UseBlockerOptions, route, routeState, useBlocker, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
192
264
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/core/RouterAdapter.ts"],"sourcesContent":[],"mappings":";;;;cAGM;;AAFwD;AAgB9D;;;;;;;AA4CY,KA5CA,uBAAA,GA8CH;EAUG,CAvDT,6BAAA,CAuD6B,EAAA,KAAG;EAQvB;EAYA,IAAA,EAAA,MAAQ;EAaR;EAWA,QAAA,CAAA,EA/FC,uBA+FW,EAAA;;kBAzFN;;ECAN,SAAA,CAAA,EDGN,aCHiB,CAAA;IACb,IAAA,CAAA,EAAA,OAAA;IAQK,MAAA,CAAA,EDJE,MCIF,CAAA,MAAA,EAAA,MAAA,CAAA;IAOF,KAAA,CAAA,EAAA,OAAA;IAAY,QAAA,CAAA,EAAA,CAAA,KAAA,EAAA,OAAA,GAAA,CAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAA,OAAA,CAAA,EAAA,GDPZ,OCOY,CAAA,IAAA,CAAA;IAGT,YAAM,CAAA,EAAA,CAAA,KAAA,EAAA,OAAA,GAAA,CAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAA,OAAA,CAAA,EAAA,GAAA,IAAA;IACZ,UAAA,CAAA,EAAA,GAAA,GAAA,IAAA;IACR,IAAA,CAAA,EAAA,OAAA;EACA,CAAA,CAAA,GDRI,SCQJ;CACC;;AE5CH;;KHsDY,YAAA;;EItDI,KAAA,EJwDP,uBIxDsB;;UJ0DrB;;EK9DL,QAAA,EAAA,MAAA;CAEC;;;;AAE8C,KLkExC,oBAAA,GAAuB,YKlEiB,GAAA;EAAM;EAM1C,IAAA,EAAA,OAAA,GAAA,SAAe;;;;ACV/B;AAmCgB,KN2CJ,eAAA,GM3CwB;;;;EC5BxB,KAAA,CAAA,EAAA,OAAA;;;;;;;KPmFA,QAAA;;;;;;;;;;;;KAaA,kBAAA,WACH,iCACW;;;;;;;KASR,YAAA;;;KCzFA,WAAA;EDzBN,MAAA,EC0BI,eD1BJ,EAAA;EAcM;;;;;;;EAyBN,UAAA,CAAA,ECLS,kBDKT;EAAS;AAmBf;AAYA;AAQA;AAYA;AAaA;EAWY,QAAA,CAAA,ECzEC,YDyEW;;iBCtER,MAAA;UACN;;;GAGP,cAAc;;;;;;ADlD6C;AAgBlD,iBEVI,MAAA,CAAA,CFUmB,EEVT,SFUS;;;;;;AAd7B,iBGIU,WAAA,CAAA,CHJ8B,EAAA,CAAA,EAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EGIQ,eHJR,EAAA,GAAA,IAAA;;;;;;AAAxC,iBIIU,WAAA,CAAA,CJJ8B,EIIf,QJJe;;;KKAzC,eAAA,YAEC,kBACA,iCACQ,oBAAoB,kBAAkB;;;;ALJ9C,iBKUU,eAAA,CAAA,CLV8B,EAAA,CKUV,eLVU,EKUO,eLVP,CAAA;;;KMAlC,iBAAA;;;;ANFkD;EAgBlD,WAAA,EAAA,GAAA,GAAA,OAAuB;CAChC;;;;;;;;AA2CH;AAYA;AAQA;AAYA;AAaA;AAWA;;;;ACzFA;;;;;AAmBA;;;;;;AAI0B,iBKbV,UAAA,CLaU,OAAA,EKbU,iBLaV,CAAA,EAAA,IAAA;;;;;;ADlDoC;AAgBlD,KOPA,aAAA,GPOA;EACT;EAIU,GAAA,EOVN,GPUM;EAMK;EAKD,GAAA,EAAA,MAAA;EAIJ;EANP,KAAA,EAAA,OAAA;EAWA;EAAS,IAAA,EAAA,OAAA;AAmBf,CAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/hooks/useRouteParams.ts","../src/hooks/useRouteState.ts","../src/hooks/useRouteData.ts","../src/core/RouterAdapter.ts"],"mappings":";;;;cAGM,6BAAA;;AAFwD;;;;;;;KAgBlD,uBAAA;EAAA,CACT,6BAAA,UA0BU;EAxBX,IAAA,WA6BI;EA3BJ,QAAA,GAAW,uBAAA;EA2BE;;;;;;EApBb,KAAA,YAMgB;EAAhB,MAAA,IAAU,IAAA,EAAM,UAAA,cAEhB;EAAA,SAAA,GACI,aAAA;IACE,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA;IACA,QAAA,IACE,KAAA,cAAmB,IAAA,2BAChB,OAAA;IACL,YAAA,IAAgB,KAAA,cAAmB,IAAA;IACnC,UAAA;IACA,IAAA;EAAA,KAEF,SAAA;AAAA;;;;KAmBM,YAAA;EAEH,oCAAP,KAAA,EAAO,uBAAA,EAEC;EAAR,MAAA,EAAQ,MAAA,kBAEA;EAAR,QAAA;AAAA;;;;KAMU,oBAAA,GAAuB,YAAA;EAQvB,6DANV,IAAA;AAAA;;;;KAMU,cAAA;EAIE,4DAFZ,OAAA,WAAkB,YAAA,WAQR;EANV,YAAA;AAAA;;;;KAMU,eAAA;EAMN,uDAJJ,OAAA,YAUU;EARV,KAAA;EAEA,IAAA;AAAA;;;;KAMU,QAAA;EACV,QAAA;EACA,MAAA;EACA,IAAA;AAAA;;;;;;;AAqBF;KAXY,kBAAA,IACV,KAAA,EAAO,aAAA,EACP,IAAA,EAAM,cAAA;;;;;;;KASI,YAAA;;;KC1GA,WAAA;EACV,MAAA,EAAQ,eAAA;ED1BoC;;;;AAc9C;;;ECoBE,UAAA,GAAa,kBAAA;EDfF;;;;;;ECsBX,QAAA,GAAW,YAAA;AAAA;AAAA,iBAGG,MAAA,CAAA;EACd,MAAA,EAAQ,WAAA;EACR,UAAA;EACA;AAAA,GACC,WAAA,GAAc,SAAA;;;;;;ADlD6C;iBEM9C,MAAA,CAAA,GAAU,SAAA;;;;;;iBCAV,WAAA,CAAA,IAAgB,EAAA,UAAY,OAAA,GAAU,eAAA;;;;;;iBCAtC,WAAA,CAAA,GAAe,QAAA;;;KCJ1B,eAAA,IACH,MAAA,EACI,eAAA,GACA,MAAA,qBACE,IAAA,EAAM,eAAA,KAAoB,eAAA,GAAkB,MAAA;;;;iBAMpC,eAAA,CAAA,IAAoB,eAAA,EAAiB,eAAA;;;KCVzC,iBAAA;;;;ANFkD;EMO5D,WAAA;AAAA;;;ANSF;;;;;;;;;;;;;;;;;;;;;;;;;iBMqBgB,UAAA,CAAW,OAAA,EAAS,iBAAA;;;;;;ANrC0B;;;;;AAgB9D;;;;;;;;;;;;iBOSgB,cAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;;;;APhC8B;;;;;AAgB9D;;;;;;;;;;;;;iBQUgB,aAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,iBAAA,CAAkB,CAAA;;;;;;ARjC+B;;;;;AAgB9D;;;;;;;;;;;;;;;;iBSagB,YAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,gBAAA,CAAiB,CAAA;;;;;;ATpCgC;KUSlD,aAAA;wBAEV,GAAA,EAAK,GAAA,EVTuC;EUW5C,GAAA,UVGiC;EUDjC,KAAA,WVEC;EUAD,IAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { n as routeState, t as route } from "./route-DiO6FM2A.mjs";
3
+ import { n as routeState, t as route } from "./route-p_gr5yPI.mjs";
4
4
  import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState, useSyncExternalStore } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
 
@@ -10,6 +10,18 @@ const RouterContext = createContext(null);
10
10
  //#endregion
11
11
  //#region src/context/RouteContext.ts
12
12
  const RouteContext = createContext(null);
13
+ /**
14
+ * Find a route context by ID in the ancestor chain.
15
+ * Returns the matching context or null if not found.
16
+ */
17
+ function findRouteContextById(context, id) {
18
+ let current = context;
19
+ while (current !== null) {
20
+ if (current.id === id) return current;
21
+ current = current.parent;
22
+ }
23
+ return null;
24
+ }
13
25
 
14
26
  //#endregion
15
27
  //#region src/context/BlockerContext.ts
@@ -54,8 +66,8 @@ function internalRoutes(routes) {
54
66
  * Returns null if no match is found.
55
67
  */
56
68
  function matchRoutes(routes, pathname) {
57
- for (const route$1 of routes) {
58
- const matched = matchRoute(route$1, pathname);
69
+ for (const route of routes) {
70
+ const matched = matchRoute(route, pathname);
59
71
  if (matched) return matched;
60
72
  }
61
73
  return null;
@@ -63,12 +75,29 @@ function matchRoutes(routes, pathname) {
63
75
  /**
64
76
  * Match a single route and its children recursively.
65
77
  */
66
- function matchRoute(route$1, pathname) {
67
- const hasChildren = Boolean(route$1.children?.length);
68
- const { matched, params, consumedPathname } = matchPath(route$1.path, pathname, !hasChildren);
78
+ function matchRoute(route, pathname) {
79
+ const hasChildren = Boolean(route.children?.length);
80
+ if (route.path === void 0) {
81
+ const result = {
82
+ route,
83
+ params: {},
84
+ pathname: ""
85
+ };
86
+ if (hasChildren) {
87
+ for (const child of route.children) {
88
+ const childMatch = matchRoute(child, pathname);
89
+ if (childMatch) return [result, ...childMatch];
90
+ }
91
+ if (route.component) return [result];
92
+ return null;
93
+ }
94
+ return [result];
95
+ }
96
+ const isExact = route.exact ?? !hasChildren;
97
+ const { matched, params, consumedPathname } = matchPath(route.path, pathname, isExact);
69
98
  if (!matched) return null;
70
99
  const result = {
71
- route: route$1,
100
+ route,
72
101
  params,
73
102
  pathname: consumedPathname
74
103
  };
@@ -76,7 +105,7 @@ function matchRoute(route$1, pathname) {
76
105
  let remainingPathname = pathname.slice(consumedPathname.length);
77
106
  if (!remainingPathname.startsWith("/")) remainingPathname = "/" + remainingPathname;
78
107
  if (remainingPathname === "") remainingPathname = "/";
79
- for (const child of route$1.children) {
108
+ for (const child of route.children) {
80
109
  const childMatch = matchRoute(child, remainingPathname);
81
110
  if (childMatch) return [result, ...childMatch.map((m) => ({
82
111
  ...m,
@@ -86,7 +115,7 @@ function matchRoute(route$1, pathname) {
86
115
  }
87
116
  }))];
88
117
  }
89
- if (route$1.component) return [result];
118
+ if (route.component) return [result];
90
119
  return null;
91
120
  }
92
121
  return [result];
@@ -133,10 +162,10 @@ const loaderCache = /* @__PURE__ */ new Map();
133
162
  * Get or create a loader result from cache.
134
163
  * If the result is not cached, executes the loader and caches the result.
135
164
  */
136
- function getOrCreateLoaderResult(entryId, matchIndex, route$1, args) {
137
- if (!route$1.loader) return;
165
+ function getOrCreateLoaderResult(entryId, matchIndex, route, args) {
166
+ if (!route.loader) return;
138
167
  const cacheKey = `${entryId}:${matchIndex}`;
139
- if (!loaderCache.has(cacheKey)) loaderCache.set(cacheKey, route$1.loader(args));
168
+ if (!loaderCache.has(cacheKey)) loaderCache.set(cacheKey, route.loader(args));
140
169
  return loaderCache.get(cacheKey);
141
170
  }
142
171
  /**
@@ -151,8 +180,8 @@ function createLoaderRequest(url) {
151
180
  */
152
181
  function executeLoaders(matchedRoutes, entryId, request, signal) {
153
182
  return matchedRoutes.map((match, index) => {
154
- const { route: route$1, params } = match;
155
- const data = getOrCreateLoaderResult(entryId, index, route$1, {
183
+ const { route, params } = match;
184
+ const data = getOrCreateLoaderResult(entryId, index, route, {
156
185
  params,
157
186
  request,
158
187
  signal
@@ -255,17 +284,23 @@ var NavigationAPIAdapter = class {
255
284
  return;
256
285
  }
257
286
  if (!event.canIntercept) {
258
- onNavigate?.(event, []);
287
+ onNavigate?.(event, {
288
+ matches: [],
289
+ intercepting: false
290
+ });
259
291
  return;
260
292
  }
261
293
  const url = new URL(event.destination.url);
262
294
  const matched = matchRoutes(routes, url.pathname);
295
+ const willIntercept = matched !== null && !event.hashChange && event.downloadRequest === null;
263
296
  if (onNavigate) {
264
- onNavigate(event, matched);
297
+ onNavigate(event, {
298
+ matches: matched,
299
+ intercepting: willIntercept
300
+ });
265
301
  if (event.defaultPrevented) return;
266
302
  }
267
- if (event.hashChange || event.downloadRequest !== null) return;
268
- if (!matched) return;
303
+ if (!willIntercept) return;
269
304
  if (idleController) {
270
305
  idleController.abort();
271
306
  idleController = null;
@@ -450,13 +485,14 @@ function Router({ routes: inputRoutes, onNavigate, fallback = "none" }) {
450
485
  * Recursively render matched routes with proper context.
451
486
  */
452
487
  function RouteRenderer({ matchedRoutes, index }) {
488
+ const parentRouteContext = useContext(RouteContext);
453
489
  const match = matchedRoutes[index];
454
490
  if (!match) return null;
455
- const { route: route$1, params, pathname, data } = match;
491
+ const { route, params, pathname, data } = match;
456
492
  const routerContext = useContext(RouterContext);
457
493
  if (!routerContext) throw new Error("RouteRenderer must be used within RouterContext");
458
494
  const { locationEntry, url, navigateAsync, updateCurrentEntryState } = routerContext;
459
- const routeState$1 = locationEntry.state?.__routeStates?.[index];
495
+ const routeState = locationEntry.state?.__routeStates?.[index];
460
496
  const setStateSync = useCallback((stateOrUpdater) => {
461
497
  const currentStates = locationEntry.state?.__routeStates ?? [];
462
498
  const currentRouteState = currentStates[index];
@@ -498,28 +534,37 @@ function RouteRenderer({ matchedRoutes, index }) {
498
534
  matchedRoutes,
499
535
  index: index + 1
500
536
  }) : null;
537
+ const routeId = route.id;
501
538
  const routeContextValue = useMemo(() => ({
539
+ id: routeId,
502
540
  params,
503
541
  matchedPath: pathname,
504
- outlet
542
+ state: routeState,
543
+ data,
544
+ outlet,
545
+ parent: parentRouteContext
505
546
  }), [
547
+ routeId,
506
548
  params,
507
549
  pathname,
508
- outlet
550
+ routeState,
551
+ data,
552
+ outlet,
553
+ parentRouteContext
509
554
  ]);
510
555
  const renderComponent = () => {
511
- const componentOrElement = route$1.component;
556
+ const componentOrElement = route.component;
512
557
  if (componentOrElement == null) return outlet;
513
558
  if (typeof componentOrElement !== "function") return componentOrElement;
514
559
  const Component = componentOrElement;
515
560
  const stateProps = {
516
- state: routeState$1,
561
+ state: routeState,
517
562
  setState,
518
563
  setStateSync,
519
564
  resetState
520
565
  };
521
566
  const { info } = locationEntry;
522
- if (route$1.loader) return /* @__PURE__ */ jsx(Component, {
567
+ if (route.loader) return /* @__PURE__ */ jsx(Component, {
523
568
  data,
524
569
  params,
525
570
  ...stateProps,
@@ -644,5 +689,113 @@ function useBlocker(options) {
644
689
  }
645
690
 
646
691
  //#endregion
647
- export { Outlet, Router, route, routeState, useBlocker, useLocation, useNavigate, useSearchParams };
692
+ //#region src/hooks/useRouteContext.ts
693
+ /**
694
+ * Internal hook that returns the RouteContextValue for the given route.
695
+ * If the route has an ID, it searches the ancestor chain for a matching route.
696
+ * If no ID is provided, it returns the current (nearest) route context.
697
+ *
698
+ * @param hookName - Name of the calling hook (for error messages)
699
+ * @param routeId - Optional route ID to search for in the ancestor chain
700
+ * @returns The matching RouteContextValue
701
+ * @throws If called outside a route component or if the route ID is not found
702
+ * @internal
703
+ */
704
+ function useRouteContext(hookName, routeId) {
705
+ const context = useContext(RouteContext);
706
+ if (!context) throw new Error(`${hookName} must be used within a route component`);
707
+ if (routeId === void 0) return context;
708
+ const matchedContext = findRouteContextById(context, routeId);
709
+ if (!matchedContext) throw new Error(`${hookName}: Route ID "${routeId}" not found in current route hierarchy. Current route is "${context.id ?? "(no id)"}"`);
710
+ return matchedContext;
711
+ }
712
+
713
+ //#endregion
714
+ //#region src/hooks/useRouteParams.ts
715
+ /**
716
+ * Returns typed route parameters for the given route definition.
717
+ * Throws an error if called outside a matching route or if the route ID is not found
718
+ * in the current route hierarchy.
719
+ *
720
+ * @example
721
+ * ```typescript
722
+ * const userRoute = route({
723
+ * id: "user",
724
+ * path: "/users/:userId",
725
+ * component: UserPage,
726
+ * });
727
+ *
728
+ * function UserPage() {
729
+ * const params = useRouteParams(userRoute);
730
+ * // params is typed as { userId: string }
731
+ * return <div>User ID: {params.userId}</div>;
732
+ * }
733
+ * ```
734
+ */
735
+ function useRouteParams(route) {
736
+ const routeId = route.id;
737
+ return useRouteContext("useRouteParams", routeId).params;
738
+ }
739
+
740
+ //#endregion
741
+ //#region src/hooks/useRouteState.ts
742
+ /**
743
+ * Returns typed navigation state for the given route definition.
744
+ * Throws an error if called outside a matching route or if the route ID is not found
745
+ * in the current route hierarchy.
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * type ScrollState = { scrollPos: number };
750
+ * const scrollRoute = routeState<ScrollState>()({
751
+ * id: "scroll",
752
+ * path: "/scroll",
753
+ * component: ScrollPage,
754
+ * });
755
+ *
756
+ * function ScrollPage() {
757
+ * const state = useRouteState(scrollRoute);
758
+ * // state is typed as ScrollState | undefined
759
+ * return <div>Scroll position: {state?.scrollPos ?? 0}</div>;
760
+ * }
761
+ * ```
762
+ */
763
+ function useRouteState(route) {
764
+ const routeId = route.id;
765
+ return useRouteContext("useRouteState", routeId).state;
766
+ }
767
+
768
+ //#endregion
769
+ //#region src/hooks/useRouteData.ts
770
+ /**
771
+ * Returns typed loader data for the given route definition.
772
+ * Throws an error if called outside a matching route or if the route ID is not found
773
+ * in the current route hierarchy.
774
+ *
775
+ * @example
776
+ * ```typescript
777
+ * const userRoute = route({
778
+ * id: "user",
779
+ * path: "/users/:userId",
780
+ * loader: async ({ params }) => {
781
+ * const res = await fetch(`/api/users/${params.userId}`);
782
+ * return res.json() as Promise<{ name: string; age: number }>;
783
+ * },
784
+ * component: UserPage,
785
+ * });
786
+ *
787
+ * function UserPage() {
788
+ * const data = useRouteData(userRoute);
789
+ * // data is typed as Promise<{ name: string; age: number }>
790
+ * return <div>User: {data.name}</div>;
791
+ * }
792
+ * ```
793
+ */
794
+ function useRouteData(route) {
795
+ const routeId = route.id;
796
+ return useRouteContext("useRouteData", routeId).data;
797
+ }
798
+
799
+ //#endregion
800
+ export { Outlet, Router, route, routeState, useBlocker, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
648
801
  //# sourceMappingURL=index.mjs.map