@openwebf/react-router 0.22.18 → 0.24.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.esm.js CHANGED
@@ -1,4 +1,3 @@
1
- import { webf } from '@openwebf/webf-enterprise-typings';
2
1
  import React, { useRef, useMemo, useState, createContext, useContext, useEffect, Children, isValidElement } from 'react';
3
2
  import { createWebFComponent } from '@openwebf/react-core-ui';
4
3
 
@@ -34,6 +33,26 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
34
33
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
35
34
  };
36
35
 
36
+ /**
37
+ * Router management module
38
+ *
39
+ * Encapsulates routing navigation functionality with route guard mechanism for permission checks
40
+ */
41
+ function getHybridHistory() {
42
+ var _a;
43
+ return (_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.webf) === null || _a === void 0 ? void 0 : _a.hybridHistory;
44
+ }
45
+ let ensureRouteMountedCallback = null;
46
+ function __unstable_setEnsureRouteMountedCallback(callback) {
47
+ ensureRouteMountedCallback = callback;
48
+ }
49
+ function ensureRouteMounted(pathname) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ if (!ensureRouteMountedCallback)
52
+ return;
53
+ yield ensureRouteMountedCallback(pathname);
54
+ });
55
+ }
37
56
  /**
38
57
  * WebF Router object - provides comprehensive navigation APIs
39
58
  * Combines web-like history management with Flutter-like navigation patterns
@@ -43,112 +62,231 @@ const WebFRouter = {
43
62
  * Get the current state object associated with the history entry
44
63
  */
45
64
  get state() {
46
- return webf.hybridHistory.state;
65
+ var _a;
66
+ return (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.state;
47
67
  },
48
68
  /**
49
69
  * Get the full hybrid router build context stack.
50
70
  * The stack is ordered from root route (index 0) to the current top route (last element).
51
71
  */
52
72
  get stack() {
53
- return webf.hybridHistory.buildContextStack;
73
+ var _a, _b;
74
+ return (_b = (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.buildContextStack) !== null && _b !== void 0 ? _b : [];
54
75
  },
55
76
  /**
56
77
  * Get the current route path
57
78
  */
58
79
  get path() {
59
- return webf.hybridHistory.path;
80
+ var _a, _b;
81
+ return (_b = (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '/';
60
82
  },
61
83
  /**
62
84
  * Navigate to a specified route
63
85
  * Applies route guards for permission checks before navigation
64
86
  */
65
87
  push: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
66
- webf.hybridHistory.pushNamed(path, { arguments: state });
88
+ const hybridHistory = getHybridHistory();
89
+ if (!hybridHistory)
90
+ throw new Error('WebF hybridHistory is not available');
91
+ yield ensureRouteMounted(path);
92
+ hybridHistory.pushNamed(path, { arguments: state });
67
93
  }),
68
94
  /**
69
95
  * Replace the current route without adding to history
70
96
  * Applies route guards for permission checks before navigation
71
97
  */
72
98
  replace: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
73
- webf.hybridHistory.pushReplacementNamed(path, { arguments: state });
99
+ const hybridHistory = getHybridHistory();
100
+ if (!hybridHistory)
101
+ throw new Error('WebF hybridHistory is not available');
102
+ yield ensureRouteMounted(path);
103
+ hybridHistory.pushReplacementNamed(path, { arguments: state });
74
104
  }),
75
105
  /**
76
106
  * Navigate back to the previous route
77
107
  */
78
108
  back: () => {
79
- webf.hybridHistory.back();
109
+ const hybridHistory = getHybridHistory();
110
+ if (!hybridHistory)
111
+ throw new Error('WebF hybridHistory is not available');
112
+ hybridHistory.back();
80
113
  },
81
114
  /**
82
115
  * Close the current screen and return to the previous one
83
116
  * Flutter-style navigation method
84
117
  */
85
118
  pop: (result) => {
86
- webf.hybridHistory.pop(result);
119
+ const hybridHistory = getHybridHistory();
120
+ if (!hybridHistory)
121
+ throw new Error('WebF hybridHistory is not available');
122
+ hybridHistory.pop(result);
87
123
  },
88
124
  /**
89
125
  * Pop routes until reaching a specific route
90
126
  */
91
127
  popUntil: (path) => {
92
- webf.hybridHistory.popUntil(path);
128
+ const hybridHistory = getHybridHistory();
129
+ if (!hybridHistory)
130
+ throw new Error('WebF hybridHistory is not available');
131
+ hybridHistory.popUntil(path);
93
132
  },
94
133
  /**
95
134
  * Pop the current route and push a new named route
96
135
  */
97
136
  popAndPushNamed: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
98
- webf.hybridHistory.popAndPushNamed(path, { arguments: state });
137
+ const hybridHistory = getHybridHistory();
138
+ if (!hybridHistory)
139
+ throw new Error('WebF hybridHistory is not available');
140
+ yield ensureRouteMounted(path);
141
+ hybridHistory.popAndPushNamed(path, { arguments: state });
99
142
  }),
100
143
  /**
101
144
  * Push a new route and remove routes until reaching a specific route
102
145
  */
103
146
  pushNamedAndRemoveUntil: (path, state, untilPath) => __awaiter(void 0, void 0, void 0, function* () {
104
- webf.hybridHistory.pushNamedAndRemoveUntil(state, path, untilPath);
147
+ const hybridHistory = getHybridHistory();
148
+ if (!hybridHistory)
149
+ throw new Error('WebF hybridHistory is not available');
150
+ yield ensureRouteMounted(path);
151
+ hybridHistory.pushNamedAndRemoveUntil(state, path, untilPath);
105
152
  }),
106
153
  /**
107
154
  * Push a new route and remove all routes until a specific route (Flutter-style)
108
155
  */
109
156
  pushNamedAndRemoveUntilRoute: (newPath, untilPath, state) => __awaiter(void 0, void 0, void 0, function* () {
110
- webf.hybridHistory.pushNamedAndRemoveUntilRoute(newPath, untilPath, { arguments: state });
157
+ const hybridHistory = getHybridHistory();
158
+ if (!hybridHistory)
159
+ throw new Error('WebF hybridHistory is not available');
160
+ yield ensureRouteMounted(newPath);
161
+ hybridHistory.pushNamedAndRemoveUntilRoute(newPath, untilPath, { arguments: state });
111
162
  }),
112
163
  /**
113
164
  * Check if the navigator can go back
114
165
  */
115
166
  canPop: () => {
116
- return webf.hybridHistory.canPop();
167
+ const hybridHistory = getHybridHistory();
168
+ if (!hybridHistory)
169
+ return false;
170
+ return hybridHistory.canPop();
117
171
  },
118
172
  /**
119
173
  * Pop the current route if possible
120
174
  * Returns true if the pop was successful, false otherwise
121
175
  */
122
176
  maybePop: (result) => {
123
- return webf.hybridHistory.maybePop(result);
177
+ const hybridHistory = getHybridHistory();
178
+ if (!hybridHistory)
179
+ return false;
180
+ return hybridHistory.maybePop(result);
124
181
  },
125
182
  /**
126
183
  * Push a new state to the history stack (web-style navigation)
127
184
  */
128
185
  pushState: (state, name) => {
129
- webf.hybridHistory.pushState(state, name);
186
+ const hybridHistory = getHybridHistory();
187
+ if (!hybridHistory)
188
+ throw new Error('WebF hybridHistory is not available');
189
+ hybridHistory.pushState(state, name);
130
190
  },
131
191
  /**
132
192
  * Replace the current history entry with a new one (web-style navigation)
133
193
  */
134
194
  replaceState: (state, name) => {
135
- webf.hybridHistory.replaceState(state, name);
195
+ const hybridHistory = getHybridHistory();
196
+ if (!hybridHistory)
197
+ throw new Error('WebF hybridHistory is not available');
198
+ hybridHistory.replaceState(state, name);
136
199
  },
137
200
  /**
138
201
  * Pop and push with restoration capability
139
202
  * Returns a restoration ID string
140
203
  */
141
204
  restorablePopAndPushState: (state, name) => {
142
- return webf.hybridHistory.restorablePopAndPushState(state, name);
205
+ const hybridHistory = getHybridHistory();
206
+ if (!hybridHistory)
207
+ throw new Error('WebF hybridHistory is not available');
208
+ return hybridHistory.restorablePopAndPushState(state, name);
143
209
  },
144
210
  /**
145
211
  * Pop and push named route with restoration capability
146
212
  * Returns a restoration ID string
147
213
  */
148
214
  restorablePopAndPushNamed: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
149
- return webf.hybridHistory.restorablePopAndPushNamed(path, { arguments: state });
215
+ const hybridHistory = getHybridHistory();
216
+ if (!hybridHistory)
217
+ throw new Error('WebF hybridHistory is not available');
218
+ yield ensureRouteMounted(path);
219
+ return hybridHistory.restorablePopAndPushNamed(path, { arguments: state });
150
220
  })
151
221
  };
222
+ /**
223
+ * Convert a route pattern to a regular expression
224
+ * @param pattern Route pattern like "/user/:userId" or "/category/:catId/product/:prodId"
225
+ * @returns Object with regex and parameter names
226
+ */
227
+ function pathToRegex(pattern) {
228
+ const paramNames = [];
229
+ if (pattern === '*') {
230
+ paramNames.push('*');
231
+ return { regex: /^(.*)$/, paramNames };
232
+ }
233
+ // Escape special regex characters except : and *
234
+ let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
235
+ // Replace :param with named capture groups
236
+ regexPattern = regexPattern.replace(/:([^\/]+)/g, (_, paramName) => {
237
+ paramNames.push(paramName);
238
+ return '([^/]+)';
239
+ });
240
+ // Replace * with a splat capture group (matches across segments)
241
+ regexPattern = regexPattern.replace(/\*/g, () => {
242
+ paramNames.push('*');
243
+ return '(.*)';
244
+ });
245
+ // Add anchors for exact matching
246
+ regexPattern = `^${regexPattern}$`;
247
+ return {
248
+ regex: new RegExp(regexPattern),
249
+ paramNames
250
+ };
251
+ }
252
+ /**
253
+ * Match a pathname against a route pattern and extract parameters
254
+ * @param pattern Route pattern like "/user/:userId"
255
+ * @param pathname Actual pathname like "/user/123"
256
+ * @returns Match result with extracted parameters or null if no match
257
+ */
258
+ function matchPath(pattern, pathname) {
259
+ const { regex, paramNames } = pathToRegex(pattern);
260
+ const match = pathname.match(regex);
261
+ if (!match) {
262
+ return null;
263
+ }
264
+ // Extract parameters from capture groups
265
+ const params = {};
266
+ paramNames.forEach((paramName, index) => {
267
+ params[paramName] = match[index + 1]; // +1 because match[0] is the full match
268
+ });
269
+ return {
270
+ path: pattern,
271
+ params,
272
+ isExact: true
273
+ };
274
+ }
275
+ /**
276
+ * Find the best matching route from a list of route patterns
277
+ * @param routes Array of route patterns
278
+ * @param pathname Current pathname
279
+ * @returns Best match or null if no routes match
280
+ */
281
+ function matchRoutes(routes, pathname) {
282
+ for (const route of routes) {
283
+ const match = matchPath(route, pathname);
284
+ if (match) {
285
+ return match;
286
+ }
287
+ }
288
+ return null;
289
+ }
152
290
 
153
291
  var isFunction = function (value) {
154
292
  return typeof value === 'function';
@@ -184,7 +322,7 @@ const RawWebFRouterLink = createWebFComponent({
184
322
  tagName: 'webf-router-link',
185
323
  displayName: 'WebFRouterLink',
186
324
  // Map props to attributes
187
- attributeProps: ['path', 'title'],
325
+ attributeProps: ['path', 'title', 'theme'],
188
326
  // Event handlers
189
327
  events: [
190
328
  {
@@ -213,7 +351,7 @@ const WebFRouterLink = function (props) {
213
351
  props.onScreen(event);
214
352
  }
215
353
  };
216
- return (React.createElement(RawWebFRouterLink, { title: props.title, path: props.path, onScreen: handleOnScreen, offScreen: props.offScreen }, isRender ? props.children : null));
354
+ return (React.createElement(RawWebFRouterLink, { title: props.title, path: props.path, theme: props.theme, onScreen: handleOnScreen, offScreen: props.offScreen }, isRender ? props.children : null));
217
355
  };
218
356
 
219
357
  /**
@@ -230,7 +368,7 @@ const WebFRouterLink = function (props) {
230
368
  *
231
369
  * Responsible for managing page rendering, lifecycle and navigation bar
232
370
  */
233
- function Route({ path, prerender = false, element, title }) {
371
+ function Route({ path, mountedPath, prerender = false, element, title, theme }) {
234
372
  // Mark whether the page has been rendered
235
373
  const [hasRendered, updateRender] = useState(false);
236
374
  /**
@@ -249,7 +387,7 @@ function Route({ path, prerender = false, element, title }) {
249
387
  */
250
388
  const handleOffScreen = useMemoizedFn(() => {
251
389
  });
252
- return (React.createElement(WebFRouterLink, { path: path, title: title, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
390
+ return (React.createElement(WebFRouterLink, { path: mountedPath !== null && mountedPath !== void 0 ? mountedPath : path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
253
391
  }
254
392
 
255
393
  /**
@@ -257,7 +395,9 @@ function Route({ path, prerender = false, element, title }) {
257
395
  */
258
396
  const RouteContext = createContext({
259
397
  path: undefined,
398
+ mountedPath: undefined,
260
399
  params: undefined,
400
+ routeParams: undefined,
261
401
  activePath: undefined,
262
402
  routeEventKind: undefined
263
403
  });
@@ -276,9 +416,9 @@ const RouteContext = createContext({
276
416
  */
277
417
  function useRouteContext() {
278
418
  const context = useContext(RouteContext);
279
- // isActive is true only for push events with matching path
280
- const isActive = (context.routeEventKind === 'didPush' || context.routeEventKind === 'didPushNext')
281
- && context.path === context.activePath;
419
+ const isActive = context.activePath !== undefined
420
+ && context.mountedPath !== undefined
421
+ && context.activePath === context.mountedPath;
282
422
  return Object.assign(Object.assign({}, context), { isActive });
283
423
  }
284
424
  /**
@@ -292,6 +432,7 @@ function useRouteContext() {
292
432
  * const location = useLocation();
293
433
  *
294
434
  * console.log('Current path:', location.pathname);
435
+
295
436
  * console.log('Location state:', location.state);
296
437
  * console.log('Is active:', location.isActive);
297
438
  *
@@ -303,25 +444,43 @@ function useLocation() {
303
444
  const context = useRouteContext();
304
445
  // Create location object from context
305
446
  const location = useMemo(() => {
306
- // For active routes, return the current location with state
307
- if (context.isActive) {
308
- return {
309
- pathname: context.path || context.activePath || WebFRouter.path,
310
- state: context.params,
311
- isActive: true,
312
- key: `${context.path}-active-${Date.now()}`
313
- };
314
- }
315
- // For inactive routes, return the global location without state
447
+ const pathname = context.activePath || WebFRouter.path;
448
+ // Get state - prioritize context params, fallback to WebFRouter.state
449
+ const state = context.isActive
450
+ ? (context.params || WebFRouter.state)
451
+ : WebFRouter.state;
316
452
  return {
317
- pathname: context.activePath || WebFRouter.path,
318
- state: undefined,
319
- isActive: false,
320
- key: `${context.activePath}-inactive`
453
+ pathname,
454
+ state,
455
+ isActive: context.isActive,
456
+ key: `${pathname}-${Date.now()}`
321
457
  };
322
- }, [context.isActive, context.path, context.activePath, context.params]);
458
+ }, [context.isActive, context.activePath, context.params]);
323
459
  return location;
324
460
  }
461
+ /**
462
+ * Hook to get route parameters from dynamic routes
463
+ *
464
+ * @returns Route parameters object with parameter names as keys and values as strings
465
+ *
466
+ * @example
467
+ * ```tsx
468
+ * // For route pattern "/user/:userId" and actual path "/user/123"
469
+ * function UserPage() {
470
+ * const params = useParams();
471
+ *
472
+ * console.log(params.userId); // "123"
473
+ *
474
+ * return <div>User ID: {params.userId}</div>;
475
+ * }
476
+ * ```
477
+ */
478
+ function useParams() {
479
+ const context = useRouteContext();
480
+ return useMemo(() => {
481
+ return context.routeParams || {};
482
+ }, [context.routeParams]);
483
+ }
325
484
  /**
326
485
  * Routes component that wraps multiple Route components and provides shared context
327
486
  *
@@ -337,50 +496,113 @@ function useLocation() {
337
496
  /**
338
497
  * Route-specific context provider that only updates when the route is active
339
498
  */
340
- function RouteContextProvider({ path, children }) {
499
+ function RouteContextProvider({ patternPath, mountedPath, children, }) {
341
500
  const globalContext = useContext(RouteContext);
342
501
  // Create a route-specific context that only updates when this route is active
343
502
  const routeSpecificContext = useMemo(() => {
344
- // Only update if this route is the active one
345
- if (globalContext.activePath === path) {
503
+ const isActive = globalContext.activePath !== undefined && globalContext.activePath === mountedPath;
504
+ const match = isActive ? matchPath(patternPath, mountedPath) : null;
505
+ if (isActive && match) {
506
+ const effectiveParams = globalContext.params !== undefined ? globalContext.params : WebFRouter.state;
346
507
  return {
347
- path,
348
- params: globalContext.params,
508
+ path: patternPath,
509
+ mountedPath,
510
+ params: effectiveParams,
511
+ routeParams: match.params,
349
512
  activePath: globalContext.activePath,
350
513
  routeEventKind: globalContext.routeEventKind
351
514
  };
352
515
  }
353
- // Return previous values if not active
354
516
  return {
355
- path,
517
+ path: patternPath,
518
+ mountedPath,
356
519
  params: undefined,
520
+ routeParams: undefined,
357
521
  activePath: globalContext.activePath,
358
522
  routeEventKind: undefined
359
523
  };
360
- }, [path, globalContext.activePath, globalContext.params, globalContext.routeEventKind]);
524
+ }, [patternPath, mountedPath, globalContext.activePath, globalContext.params, globalContext.routeEventKind]);
361
525
  return (React.createElement(RouteContext.Provider, { value: routeSpecificContext }, children));
362
526
  }
527
+ function patternScore(pattern) {
528
+ if (pattern === '*')
529
+ return 0;
530
+ const segments = pattern.split('/').filter(Boolean);
531
+ let score = 0;
532
+ for (const segment of segments) {
533
+ if (segment === '*')
534
+ score += 1;
535
+ else if (segment.startsWith(':'))
536
+ score += 2;
537
+ else
538
+ score += 3;
539
+ }
540
+ return score * 100 + segments.length;
541
+ }
542
+ function findBestMatch(patterns, pathname) {
543
+ var _a;
544
+ let best = null;
545
+ for (const pattern of patterns) {
546
+ const match = matchPath(pattern, pathname);
547
+ if (!match)
548
+ continue;
549
+ const score = patternScore(pattern);
550
+ if (!best || score > best.score)
551
+ best = { match, score };
552
+ }
553
+ return (_a = best === null || best === void 0 ? void 0 : best.match) !== null && _a !== void 0 ? _a : null;
554
+ }
555
+ function escapeAttributeValue(value) {
556
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
557
+ }
363
558
  function Routes({ children }) {
364
559
  // State to track current route information
365
560
  const [routeState, setRouteState] = useState({
366
561
  path: undefined,
367
- activePath: undefined,
562
+ mountedPath: undefined,
563
+ activePath: WebFRouter.path, // Initialize with current path
368
564
  params: undefined,
565
+ routeParams: undefined,
369
566
  routeEventKind: undefined
370
567
  });
568
+ const [stack, setStack] = useState(() => WebFRouter.stack);
569
+ const [preMountedPaths, setPreMountedPaths] = useState([]);
570
+ const routePatternsRef = useRef([]);
571
+ const pendingEnsureResolversRef = useRef(new Map());
572
+ // Keep a stable view of declared route patterns for event handlers.
573
+ useEffect(() => {
574
+ const patterns = [];
575
+ Children.forEach(children, (child) => {
576
+ if (!isValidElement(child))
577
+ return;
578
+ if (child.type !== Route)
579
+ return;
580
+ patterns.push(child.props.path);
581
+ });
582
+ routePatternsRef.current = patterns;
583
+ }, [children]);
371
584
  // Listen to hybridrouterchange event
372
585
  useEffect(() => {
373
586
  const handleRouteChange = (event) => {
587
+ var _a, _b, _c;
374
588
  const routeEvent = event;
375
- // Only update activePath for push events
376
- const newActivePath = (routeEvent.kind === 'didPushNext' || routeEvent.kind === 'didPush')
377
- ? routeEvent.path
378
- : routeState.activePath;
589
+ // Check for new event detail structure with params
590
+ const eventDetail = event.detail;
591
+ const newActivePath = WebFRouter.path;
592
+ const newStack = WebFRouter.stack;
593
+ setStack(newStack);
594
+ setPreMountedPaths((prev) => prev.filter((p) => newStack.some((entry) => entry.path === p)));
595
+ const bestMatch = newActivePath ? findBestMatch(routePatternsRef.current, newActivePath) : null;
596
+ const routeParams = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.params) || (bestMatch === null || bestMatch === void 0 ? void 0 : bestMatch.params) || undefined;
597
+ const activeEntry = [...newStack].reverse().find((entry) => entry.path === newActivePath);
598
+ const eventState = (_c = (_b = (_a = activeEntry === null || activeEntry === void 0 ? void 0 : activeEntry.state) !== null && _a !== void 0 ? _a : eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.state) !== null && _b !== void 0 ? _b : routeEvent.state) !== null && _c !== void 0 ? _c : WebFRouter.state;
379
599
  // Update state based on event kind
380
600
  setRouteState({
381
601
  path: routeEvent.path,
602
+ mountedPath: routeEvent.path,
382
603
  activePath: newActivePath,
383
- params: routeEvent.state,
604
+ params: eventState,
605
+ routeParams: routeParams, // Use params from Flutter if available
384
606
  routeEventKind: routeEvent.kind
385
607
  });
386
608
  };
@@ -390,30 +612,108 @@ function Routes({ children }) {
390
612
  return () => {
391
613
  document.removeEventListener('hybridrouterchange', handleRouteChange);
392
614
  };
393
- }, [routeState.activePath]);
615
+ }, []);
616
+ useEffect(() => {
617
+ __unstable_setEnsureRouteMountedCallback((pathname) => {
618
+ var _a;
619
+ if (!pathname)
620
+ return;
621
+ const bestMatch = findBestMatch(routePatternsRef.current, pathname);
622
+ if (!bestMatch)
623
+ return;
624
+ const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
625
+ if (document.querySelector(selector))
626
+ return;
627
+ let resolveFn;
628
+ const promise = new Promise((resolve) => {
629
+ resolveFn = resolve;
630
+ });
631
+ pendingEnsureResolversRef.current.set(pathname, [
632
+ ...((_a = pendingEnsureResolversRef.current.get(pathname)) !== null && _a !== void 0 ? _a : []),
633
+ resolveFn,
634
+ ]);
635
+ setPreMountedPaths((prev) => (prev.includes(pathname) ? prev : [...prev, pathname]));
636
+ return promise;
637
+ });
638
+ return () => {
639
+ __unstable_setEnsureRouteMountedCallback(null);
640
+ };
641
+ }, []);
642
+ useEffect(() => {
643
+ const pending = pendingEnsureResolversRef.current;
644
+ for (const [pathname, resolvers] of pending.entries()) {
645
+ const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
646
+ if (!document.querySelector(selector))
647
+ continue;
648
+ for (const resolve of resolvers)
649
+ resolve();
650
+ pending.delete(pathname);
651
+ }
652
+ }, [children, stack, preMountedPaths, routeState.activePath]);
394
653
  // Global context value
395
654
  const globalContextValue = useMemo(() => ({
396
655
  path: undefined,
656
+ mountedPath: undefined,
397
657
  params: routeState.params,
658
+ routeParams: routeState.routeParams, // Pass through route params from Flutter
398
659
  activePath: routeState.activePath,
399
660
  routeEventKind: routeState.routeEventKind
400
- }), [routeState.activePath, routeState.params, routeState.routeEventKind]);
401
- // Wrap each Route component with its own context provider
661
+ }), [routeState.activePath, routeState.params, routeState.routeParams, routeState.routeEventKind]);
402
662
  const wrappedChildren = useMemo(() => {
403
- return Children.map(children, (child) => {
663
+ const declaredRoutes = [];
664
+ const patterns = [];
665
+ const declaredPaths = new Set();
666
+ Children.forEach(children, (child) => {
667
+ var _a;
404
668
  if (!isValidElement(child)) {
405
- return child;
669
+ declaredRoutes.push(child);
670
+ return;
406
671
  }
407
- // Ensure only Route components are direct children
408
672
  if (child.type !== Route) {
409
673
  console.warn('Routes component should only contain Route components as direct children');
410
- return child;
674
+ declaredRoutes.push(child);
675
+ return;
411
676
  }
412
- // Wrap each Route with its own context provider
413
- const routePath = child.props.path;
414
- return (React.createElement(RouteContextProvider, { key: routePath, path: routePath }, child));
677
+ const patternPath = child.props.path;
678
+ patterns.push(patternPath);
679
+ declaredPaths.add(patternPath);
680
+ const mountedPath = (_a = child.props.mountedPath) !== null && _a !== void 0 ? _a : patternPath;
681
+ declaredRoutes.push(React.createElement(RouteContextProvider, { key: `declared:${patternPath}`, patternPath: patternPath, mountedPath: mountedPath }, child));
415
682
  });
416
- }, [children]);
683
+ const mountedPaths = [];
684
+ for (const entry of stack)
685
+ mountedPaths.push(entry.path);
686
+ for (const path of preMountedPaths)
687
+ mountedPaths.push(path);
688
+ if (routeState.activePath && !mountedPaths.includes(routeState.activePath))
689
+ mountedPaths.push(routeState.activePath);
690
+ const dynamicRoutes = [];
691
+ const seenMountedPaths = new Set();
692
+ for (const mountedPath of mountedPaths) {
693
+ if (seenMountedPaths.has(mountedPath))
694
+ continue;
695
+ seenMountedPaths.add(mountedPath);
696
+ if (declaredPaths.has(mountedPath))
697
+ continue;
698
+ const bestMatch = findBestMatch(patterns, mountedPath);
699
+ if (!bestMatch)
700
+ continue;
701
+ const matchingRouteElement = Children.toArray(children).find((node) => {
702
+ if (!isValidElement(node))
703
+ return false;
704
+ if (node.type !== Route)
705
+ return false;
706
+ return node.props.path === bestMatch.path;
707
+ });
708
+ if (!matchingRouteElement)
709
+ continue;
710
+ const routeInstance = React.cloneElement(matchingRouteElement, {
711
+ mountedPath,
712
+ });
713
+ dynamicRoutes.push(React.createElement(RouteContextProvider, { key: `dynamic:${mountedPath}`, patternPath: bestMatch.path, mountedPath: mountedPath }, routeInstance));
714
+ }
715
+ return [...declaredRoutes, ...dynamicRoutes];
716
+ }, [children, stack, preMountedPaths, routeState.activePath]);
417
717
  return (React.createElement(RouteContext.Provider, { value: globalContextValue }, wrappedChildren));
418
718
  }
419
719
  /**
@@ -443,7 +743,7 @@ function useRoutes(routes) {
443
743
  if (route.children && route.children.length > 0) {
444
744
  console.warn('Nested routes are not supported yet');
445
745
  }
446
- return (React.createElement(Route, { key: route.path, path: route.path, element: route.element, prerender: route.prerender }));
746
+ return (React.createElement(Route, { key: route.path, path: route.path, element: route.element, prerender: route.prerender, theme: route.theme }));
447
747
  });
448
748
  }, [routes]);
449
749
  // Return Routes component with Route children
@@ -513,5 +813,5 @@ function useNavigate() {
513
813
  }, []);
514
814
  }
515
815
 
516
- export { Route, Routes, WebFRouter, WebFRouterLink, useLocation, useNavigate, useRouteContext, useRoutes };
816
+ export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
517
817
  //# sourceMappingURL=index.esm.js.map