@fictjs/router 0.2.2 → 0.3.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.cjs CHANGED
@@ -20,12 +20,2382 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- Router: () => Router
23
+ Form: () => Form,
24
+ HashRouter: () => HashRouter,
25
+ Link: () => Link,
26
+ MemoryRouter: () => MemoryRouter,
27
+ NavLink: () => NavLink,
28
+ Navigate: () => Navigate,
29
+ Outlet: () => Outlet,
30
+ Redirect: () => Redirect,
31
+ Route: () => Route,
32
+ Router: () => Router,
33
+ Routes: () => Routes,
34
+ StaticRouter: () => StaticRouter,
35
+ action: () => action,
36
+ cleanupDataUtilities: () => cleanupDataUtilities,
37
+ clearAllScrollPositions: () => clearAllScrollPositions,
38
+ clearScrollPosition: () => clearScrollPosition,
39
+ compileRoute: () => compileRoute,
40
+ configureScrollRestoration: () => configureScrollRestoration,
41
+ createBranches: () => createBranches,
42
+ createBrowserHistory: () => createBrowserHistory,
43
+ createHashHistory: () => createHashHistory,
44
+ createLazyRoutes: () => createLazyRoutes,
45
+ createLocation: () => createLocation,
46
+ createMemoryHistory: () => createMemoryHistory,
47
+ createPreload: () => createPreload,
48
+ createResource: () => createResource,
49
+ createRouter: () => createRouter,
50
+ createRoutes: () => createRoutes,
51
+ createScrollRestoration: () => createScrollRestoration,
52
+ createStaticHistory: () => createStaticHistory,
53
+ createURL: () => createURL,
54
+ getAction: () => getAction,
55
+ getScrollRestoration: () => getScrollRestoration,
56
+ isBrowser: () => isBrowser,
57
+ isLazyComponent: () => isLazyComponent,
58
+ isServer: () => isServer,
59
+ joinPaths: () => joinPaths,
60
+ lazy: () => lazy,
61
+ lazyRoute: () => lazyRoute,
62
+ locationsAreEqual: () => locationsAreEqual,
63
+ matchRoutes: () => matchRoutes,
64
+ normalizePath: () => normalizePath,
65
+ parseSearchParams: () => parseSearchParams,
66
+ parseURL: () => parseURL,
67
+ preloadLazy: () => preloadLazy,
68
+ preloadQuery: () => preloadQuery,
69
+ prependBasePath: () => prependBasePath,
70
+ query: () => query,
71
+ resolvePath: () => resolvePath,
72
+ restoreScrollPosition: () => restoreScrollPosition,
73
+ revalidate: () => revalidate,
74
+ saveScrollPosition: () => saveScrollPosition,
75
+ scoreRoute: () => scoreRoute,
76
+ scrollToHash: () => scrollToHash,
77
+ scrollToTop: () => scrollToTop,
78
+ stringifySearchParams: () => stringifySearchParams,
79
+ stripBasePath: () => stripBasePath,
80
+ submitAction: () => submitAction,
81
+ useBeforeLeave: () => useBeforeLeave,
82
+ useHref: () => useHref,
83
+ useIsActive: () => useIsActive,
84
+ useIsRouting: () => useIsRouting,
85
+ useLocation: () => useLocation,
86
+ useMatch: () => useMatch,
87
+ useMatches: () => useMatches,
88
+ useNavigate: () => useNavigate,
89
+ useParams: () => useParams,
90
+ usePendingLocation: () => usePendingLocation,
91
+ useResolvedPath: () => useResolvedPath,
92
+ useRoute: () => useRoute,
93
+ useRouteData: () => useRouteData,
94
+ useRouteError: () => useRouteError,
95
+ useRouter: () => useRouter,
96
+ useSearchParams: () => useSearchParams,
97
+ useSubmission: () => useSubmission,
98
+ useSubmissions: () => useSubmissions
24
99
  });
25
100
  module.exports = __toCommonJS(index_exports);
26
- var Router = () => {
101
+
102
+ // src/components.tsx
103
+ var import_runtime2 = require("@fictjs/runtime");
104
+ var import_advanced = require("@fictjs/runtime/advanced");
105
+
106
+ // src/context.ts
107
+ var import_runtime = require("@fictjs/runtime");
108
+
109
+ // src/utils.ts
110
+ function normalizePath(path) {
111
+ if (!path || path === "/") return "/";
112
+ let normalized = path.startsWith("/") ? path : "/" + path;
113
+ if (normalized.length > 1 && normalized.endsWith("/")) {
114
+ normalized = normalized.slice(0, -1);
115
+ }
116
+ return normalized;
117
+ }
118
+ function joinPaths(...paths) {
119
+ return normalizePath(
120
+ paths.filter((p) => p != null && p !== "").join("/").replace(/\/+/g, "/")
121
+ );
122
+ }
123
+ function resolvePath(base, to) {
124
+ const toPath = typeof to === "string" ? to : to.pathname || "";
125
+ if (toPath.startsWith("/")) {
126
+ return normalizePath(toPath);
127
+ }
128
+ const baseSegments = base.split("/").filter(Boolean);
129
+ const toSegments = toPath.split("/").filter(Boolean);
130
+ for (const segment of toSegments) {
131
+ if (segment === "..") {
132
+ baseSegments.pop();
133
+ } else if (segment !== ".") {
134
+ baseSegments.push(segment);
135
+ }
136
+ }
137
+ return "/" + baseSegments.join("/");
138
+ }
139
+ function createLocation(to, state, key) {
140
+ if (typeof to === "string") {
141
+ const url = parseURL(to);
142
+ return {
143
+ pathname: url.pathname,
144
+ search: url.search,
145
+ hash: url.hash,
146
+ state: state ?? null,
147
+ key: key ?? createKey()
148
+ };
149
+ }
150
+ return {
151
+ pathname: to.pathname || "/",
152
+ search: to.search || "",
153
+ hash: to.hash || "",
154
+ state: state ?? to.state ?? null,
155
+ key: key ?? to.key ?? createKey()
156
+ };
157
+ }
158
+ function parseURL(url) {
159
+ const hashIndex = url.indexOf("#");
160
+ let hash = "";
161
+ if (hashIndex >= 0) {
162
+ hash = url.slice(hashIndex);
163
+ url = url.slice(0, hashIndex);
164
+ }
165
+ const searchIndex = url.indexOf("?");
166
+ let search = "";
167
+ if (searchIndex >= 0) {
168
+ search = url.slice(searchIndex);
169
+ url = url.slice(0, searchIndex);
170
+ }
171
+ return {
172
+ pathname: normalizePath(url || "/"),
173
+ search,
174
+ hash
175
+ };
176
+ }
177
+ function createURL(location) {
178
+ const pathname = location.pathname || "/";
179
+ const search = location.search || "";
180
+ const hash = location.hash || "";
181
+ return pathname + search + hash;
182
+ }
183
+ var keyIndex = 0;
184
+ function createKey() {
185
+ return String(++keyIndex);
186
+ }
187
+ function parsePathPattern(pattern) {
188
+ const segments = [];
189
+ const parts = pattern.split("/").filter(Boolean);
190
+ for (const part of parts) {
191
+ if (part === "*" || part.startsWith("*")) {
192
+ const paramName = part.length > 1 ? part.slice(1) : "*";
193
+ segments.push({ type: "splat", value: part, paramName });
194
+ } else if (part.startsWith(":")) {
195
+ const isOptional = part.endsWith("?");
196
+ const paramName = isOptional ? part.slice(1, -1) : part.slice(1);
197
+ segments.push({
198
+ type: isOptional ? "optional" : "dynamic",
199
+ value: part,
200
+ paramName
201
+ });
202
+ } else {
203
+ segments.push({ type: "static", value: part.toLowerCase() });
204
+ }
205
+ }
206
+ return segments;
207
+ }
208
+ function scoreRoute(pattern, isIndex = false) {
209
+ const segments = parsePathPattern(pattern);
210
+ let score = 0;
211
+ for (const segment of segments) {
212
+ switch (segment.type) {
213
+ case "static":
214
+ score += 3;
215
+ break;
216
+ case "dynamic":
217
+ score += 2;
218
+ break;
219
+ case "optional":
220
+ score += 1;
221
+ break;
222
+ case "splat":
223
+ score += 0.5;
224
+ break;
225
+ }
226
+ }
227
+ if (isIndex) {
228
+ score += 0.5;
229
+ }
230
+ return score;
231
+ }
232
+ function createMatcher(pattern, matchFilters) {
233
+ const segments = parsePathPattern(pattern);
234
+ const normalizedPattern = normalizePath(pattern);
235
+ return (pathname) => {
236
+ const pathSegments = pathname.split("/").filter(Boolean);
237
+ const params = {};
238
+ let matchedPath = "";
239
+ let pathIndex = 0;
240
+ for (let i = 0; i < segments.length; i++) {
241
+ const segment = segments[i];
242
+ const pathSegment = pathSegments[pathIndex];
243
+ switch (segment.type) {
244
+ case "static":
245
+ if (!pathSegment || pathSegment.toLowerCase() !== segment.value) {
246
+ return null;
247
+ }
248
+ matchedPath += "/" + pathSegment;
249
+ pathIndex++;
250
+ break;
251
+ case "dynamic":
252
+ if (!pathSegment) {
253
+ return null;
254
+ }
255
+ if (matchFilters && segment.paramName && matchFilters[segment.paramName]) {
256
+ if (!validateParam(pathSegment, matchFilters[segment.paramName])) {
257
+ return null;
258
+ }
259
+ }
260
+ params[segment.paramName] = decodeURIComponent(pathSegment);
261
+ matchedPath += "/" + pathSegment;
262
+ pathIndex++;
263
+ break;
264
+ case "optional": {
265
+ if (pathSegment) {
266
+ const nextSegment = segments[i + 1];
267
+ if (nextSegment && nextSegment.type === "static" && pathSegment.toLowerCase() === nextSegment.value) {
268
+ break;
269
+ }
270
+ if (matchFilters && segment.paramName && matchFilters[segment.paramName]) {
271
+ if (!validateParam(pathSegment, matchFilters[segment.paramName])) {
272
+ break;
273
+ }
274
+ }
275
+ params[segment.paramName] = decodeURIComponent(pathSegment);
276
+ matchedPath += "/" + pathSegment;
277
+ pathIndex++;
278
+ }
279
+ break;
280
+ }
281
+ case "splat": {
282
+ const remainingSegments = pathSegments.slice(pathIndex);
283
+ const decodedSegments = remainingSegments.map((seg) => {
284
+ try {
285
+ return decodeURIComponent(seg);
286
+ } catch {
287
+ return seg;
288
+ }
289
+ });
290
+ params[segment.paramName] = decodedSegments.join("/");
291
+ matchedPath += remainingSegments.length > 0 ? "/" + remainingSegments.join("/") : "";
292
+ pathIndex = pathSegments.length;
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ if (pathIndex < pathSegments.length) {
298
+ return null;
299
+ }
300
+ return {
301
+ route: {},
302
+ // Will be filled in by caller
303
+ pathname: matchedPath || "/",
304
+ params,
305
+ pattern: normalizedPattern
306
+ };
307
+ };
308
+ }
309
+ function validateParam(value, filter) {
310
+ if (filter instanceof RegExp) {
311
+ return filter.test(value);
312
+ }
313
+ if (Array.isArray(filter)) {
314
+ return filter.includes(value);
315
+ }
316
+ if (typeof filter === "function") {
317
+ return filter(value);
318
+ }
319
+ return true;
320
+ }
321
+ var routeKeyCounter = 0;
322
+ function compileRoute(route, parentPattern = "") {
323
+ const pattern = normalizePath(
324
+ joinPaths(parentPattern, route.path || (route.index ? "" : void 0))
325
+ );
326
+ const compiled = {
327
+ route,
328
+ pattern,
329
+ matcher: createMatcher(pattern, route.matchFilters),
330
+ score: scoreRoute(pattern, route.index),
331
+ key: route.key || `route-${++routeKeyCounter}`
332
+ };
333
+ if (route.children && route.children.length > 0) {
334
+ compiled.children = route.children.map(
335
+ (child) => compileRoute(child, route.index ? parentPattern : pattern)
336
+ );
337
+ }
338
+ return compiled;
339
+ }
340
+ function createBranches(routes) {
341
+ const branches = [];
342
+ function buildBranches(route, parentRoutes = []) {
343
+ const currentRoutes = [...parentRoutes, route];
344
+ if (route.children && route.children.length > 0) {
345
+ for (const child of route.children) {
346
+ buildBranches(child, currentRoutes);
347
+ }
348
+ } else {
349
+ const score = currentRoutes.reduce((sum, r) => sum + r.score, 0);
350
+ const branchMatcher = (pathname) => {
351
+ const matches = [];
352
+ let remainingPath = pathname;
353
+ let accumulatedParams = {};
354
+ for (const compiledRoute of currentRoutes) {
355
+ const match = compiledRoute.matcher(remainingPath);
356
+ if (!match) {
357
+ return null;
358
+ }
359
+ accumulatedParams = { ...accumulatedParams, ...match.params };
360
+ matches.push({
361
+ ...match,
362
+ route: compiledRoute.route,
363
+ params: { ...accumulatedParams }
364
+ });
365
+ if (compiledRoute !== currentRoutes[currentRoutes.length - 1]) {
366
+ if (match.pathname !== "/") {
367
+ remainingPath = remainingPath.slice(match.pathname.length) || "/";
368
+ }
369
+ }
370
+ }
371
+ return matches;
372
+ };
373
+ branches.push({
374
+ routes: currentRoutes,
375
+ score,
376
+ matcher: branchMatcher
377
+ });
378
+ }
379
+ }
380
+ for (const route of routes) {
381
+ buildBranches(route);
382
+ }
383
+ branches.sort((a, b) => b.score - a.score);
384
+ return branches;
385
+ }
386
+ function matchRoutes(branches, pathname) {
387
+ const normalizedPath = normalizePath(pathname);
388
+ for (const branch of branches) {
389
+ const matches = branch.matcher(normalizedPath);
390
+ if (matches) {
391
+ return matches;
392
+ }
393
+ }
394
+ return null;
395
+ }
396
+ function parseSearchParams(search) {
397
+ return new URLSearchParams(search);
398
+ }
399
+ function stringifySearchParams(params) {
400
+ const searchParams = params instanceof URLSearchParams ? params : new URLSearchParams(params);
401
+ const str = searchParams.toString();
402
+ return str ? "?" + str : "";
403
+ }
404
+ function locationsAreEqual(a, b) {
405
+ return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
406
+ }
407
+ function stripBasePath(pathname, basePath) {
408
+ if (basePath === "/" || basePath === "") {
409
+ return pathname;
410
+ }
411
+ const normalizedBase = normalizePath(basePath);
412
+ if (pathname.startsWith(normalizedBase)) {
413
+ const stripped = pathname.slice(normalizedBase.length);
414
+ return stripped || "/";
415
+ }
416
+ return pathname;
417
+ }
418
+ function prependBasePath(pathname, basePath) {
419
+ if (basePath === "/" || basePath === "") {
420
+ return pathname;
421
+ }
422
+ return joinPaths(basePath, pathname);
423
+ }
424
+ function hashParams(params) {
425
+ const entries = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
426
+ return JSON.stringify(entries);
427
+ }
428
+ function isServer() {
429
+ return typeof window === "undefined";
430
+ }
431
+ function isBrowser() {
432
+ return typeof window !== "undefined";
433
+ }
434
+
435
+ // src/context.ts
436
+ var defaultRouterContext = {
437
+ location: () => ({
438
+ pathname: "/",
439
+ search: "",
440
+ hash: "",
441
+ state: null,
442
+ key: "default"
443
+ }),
444
+ params: () => ({}),
445
+ matches: () => [],
446
+ navigate: () => {
447
+ console.warn("[fict-router] No router found. Wrap your app in a <Router>");
448
+ },
449
+ isRouting: () => false,
450
+ pendingLocation: () => null,
451
+ base: "",
452
+ resolvePath: (to) => typeof to === "string" ? to : to.pathname || "/"
453
+ };
454
+ var RouterContext = (0, import_runtime.createContext)(defaultRouterContext);
455
+ RouterContext.displayName = "RouterContext";
456
+ function useRouter() {
457
+ return (0, import_runtime.useContext)(RouterContext);
458
+ }
459
+ var defaultRouteContext = {
460
+ match: () => void 0,
461
+ data: () => void 0,
462
+ outlet: () => null,
463
+ resolvePath: (to) => typeof to === "string" ? to : to.pathname || "/"
464
+ };
465
+ var RouteContext = (0, import_runtime.createContext)(defaultRouteContext);
466
+ RouteContext.displayName = "RouteContext";
467
+ function useRoute() {
468
+ return (0, import_runtime.useContext)(RouteContext);
469
+ }
470
+ var defaultBeforeLeaveContext = {
471
+ addHandler: () => () => {
472
+ },
473
+ confirm: async () => true
474
+ };
475
+ var BeforeLeaveContext = (0, import_runtime.createContext)(defaultBeforeLeaveContext);
476
+ BeforeLeaveContext.displayName = "BeforeLeaveContext";
477
+ function useBeforeLeaveContext() {
478
+ return (0, import_runtime.useContext)(BeforeLeaveContext);
479
+ }
480
+ var defaultRouteErrorContext = {
481
+ error: void 0,
482
+ reset: void 0
483
+ };
484
+ var RouteErrorContext = (0, import_runtime.createContext)(defaultRouteErrorContext);
485
+ RouteErrorContext.displayName = "RouteErrorContext";
486
+ function useNavigate() {
487
+ const router = useRouter();
488
+ return router.navigate;
489
+ }
490
+ function useLocation() {
491
+ const router = useRouter();
492
+ return router.location;
493
+ }
494
+ function useParams() {
495
+ const router = useRouter();
496
+ return router.params;
497
+ }
498
+ function useSearchParams() {
499
+ const router = useRouter();
500
+ const getSearchParams = () => {
501
+ const location = router.location();
502
+ return new URLSearchParams(location.search);
503
+ };
504
+ const setSearchParams = (params, options) => {
505
+ const searchParams = params instanceof URLSearchParams ? params : new URLSearchParams(params);
506
+ const search = searchParams.toString();
507
+ const location = router.location();
508
+ router.navigate(
509
+ {
510
+ pathname: location.pathname,
511
+ search: search ? "?" + search : "",
512
+ hash: location.hash
513
+ },
514
+ { replace: options?.replace }
515
+ );
516
+ };
517
+ return [getSearchParams, setSearchParams];
518
+ }
519
+ function useMatches() {
520
+ const router = useRouter();
521
+ return router.matches;
522
+ }
523
+ function useIsRouting() {
524
+ const router = useRouter();
525
+ return router.isRouting;
526
+ }
527
+ function usePendingLocation() {
528
+ const router = useRouter();
529
+ return router.pendingLocation;
530
+ }
531
+ function useRouteData() {
532
+ const route = useRoute();
533
+ return route.data;
534
+ }
535
+ function useRouteError() {
536
+ const errorContext = (0, import_runtime.useContext)(RouteErrorContext);
537
+ if (errorContext.error !== void 0) {
538
+ return errorContext.error;
539
+ }
540
+ const route = useRoute();
541
+ return route.error?.();
542
+ }
543
+ function useResolvedPath(to) {
544
+ const route = useRoute();
545
+ return () => {
546
+ const target = typeof to === "function" ? to() : to;
547
+ return route.resolvePath(target);
548
+ };
549
+ }
550
+ function useMatch(path) {
551
+ const router = useRouter();
552
+ return () => {
553
+ const targetPath = typeof path === "function" ? path() : path;
554
+ const matches = router.matches();
555
+ for (const match of matches) {
556
+ if (match.pattern === targetPath || match.pathname === targetPath) {
557
+ return match;
558
+ }
559
+ }
560
+ return null;
561
+ };
562
+ }
563
+ function useHref(to) {
564
+ const router = useRouter();
565
+ return () => {
566
+ const target = typeof to === "function" ? to() : to;
567
+ let pathname;
568
+ let search = "";
569
+ let hash = "";
570
+ if (typeof target === "string") {
571
+ let remaining = target;
572
+ const hashIndex = remaining.indexOf("#");
573
+ if (hashIndex >= 0) {
574
+ hash = remaining.slice(hashIndex);
575
+ remaining = remaining.slice(0, hashIndex);
576
+ }
577
+ const searchIndex = remaining.indexOf("?");
578
+ if (searchIndex >= 0) {
579
+ search = remaining.slice(searchIndex);
580
+ remaining = remaining.slice(0, searchIndex);
581
+ }
582
+ pathname = remaining;
583
+ } else {
584
+ pathname = target.pathname || "";
585
+ search = target.search || "";
586
+ hash = target.hash || "";
587
+ }
588
+ let resolved;
589
+ if (pathname === "") {
590
+ const currentPathname = router.location().pathname;
591
+ const normalizedBase = router.base === "/" || router.base === "" ? "" : router.base;
592
+ if (normalizedBase && !currentPathname.startsWith(normalizedBase)) {
593
+ return currentPathname + search + hash;
594
+ }
595
+ resolved = stripBasePath(currentPathname, router.base);
596
+ } else {
597
+ resolved = router.resolvePath(pathname);
598
+ }
599
+ const baseHref = prependBasePath(resolved, router.base);
600
+ return baseHref + search + hash;
601
+ };
602
+ }
603
+ function useIsActive(to, options) {
604
+ const router = useRouter();
605
+ return () => {
606
+ const target = typeof to === "function" ? to() : to;
607
+ const resolvedTargetPath = router.resolvePath(target);
608
+ const currentPath = router.location().pathname;
609
+ if (router.base && currentPath !== router.base && !currentPath.startsWith(router.base + "/")) {
610
+ return false;
611
+ }
612
+ const currentPathWithoutBase = stripBasePath(currentPath, router.base);
613
+ if (options?.end) {
614
+ return currentPathWithoutBase === resolvedTargetPath;
615
+ }
616
+ return currentPathWithoutBase === resolvedTargetPath || currentPathWithoutBase.startsWith(resolvedTargetPath + "/");
617
+ };
618
+ }
619
+ function useBeforeLeave(handler) {
620
+ const context = useBeforeLeaveContext();
621
+ const _cleanup = context.addHandler(handler);
622
+ }
623
+
624
+ // src/history.ts
625
+ function createHistoryState(location, index) {
626
+ return {
627
+ usr: location.state,
628
+ key: location.key,
629
+ idx: index
630
+ };
631
+ }
632
+ function readLocation(state, url) {
633
+ const { pathname, search, hash } = parseURL(url);
634
+ return {
635
+ pathname,
636
+ search,
637
+ hash,
638
+ state: state?.usr ?? null,
639
+ key: state?.key ?? createKey()
640
+ };
641
+ }
642
+ function createBrowserHistory() {
643
+ if (typeof window === "undefined") {
644
+ throw new Error(
645
+ "[fict-router] createBrowserHistory cannot be used in a server environment. Use createMemoryHistory or createStaticHistory for SSR."
646
+ );
647
+ }
648
+ const listeners = /* @__PURE__ */ new Set();
649
+ const blockers = /* @__PURE__ */ new Set();
650
+ let action2 = "POP";
651
+ let location = readLocation(
652
+ window.history.state,
653
+ window.location.pathname + window.location.search + window.location.hash
654
+ );
655
+ let index = window.history.state?.idx ?? 0;
656
+ function handlePopState(event) {
657
+ const nextLocation = readLocation(
658
+ event.state,
659
+ window.location.pathname + window.location.search + window.location.hash
660
+ );
661
+ const nextAction = "POP";
662
+ const nextIndex = event.state?.idx ?? 0;
663
+ if (blockers.size > 0) {
664
+ let blocked = false;
665
+ const retry = () => {
666
+ window.history.go(nextIndex - index);
667
+ };
668
+ for (const blocker of blockers) {
669
+ blocker({
670
+ action: nextAction,
671
+ location: nextLocation,
672
+ retry
673
+ });
674
+ blocked = true;
675
+ break;
676
+ }
677
+ if (blocked) {
678
+ window.history.go(index - nextIndex);
679
+ return;
680
+ }
681
+ }
682
+ action2 = nextAction;
683
+ location = nextLocation;
684
+ index = nextIndex;
685
+ notifyListeners();
686
+ }
687
+ window.addEventListener("popstate", handlePopState);
688
+ function notifyListeners() {
689
+ for (const listener of listeners) {
690
+ listener({ action: action2, location });
691
+ }
692
+ }
693
+ function push(to, state) {
694
+ const nextLocation = createLocation(to, state);
695
+ const nextAction = "PUSH";
696
+ if (blockers.size > 0) {
697
+ let blocked = false;
698
+ const retry = () => push(to, state);
699
+ for (const blocker of blockers) {
700
+ blocker({
701
+ action: nextAction,
702
+ location: nextLocation,
703
+ retry
704
+ });
705
+ blocked = true;
706
+ break;
707
+ }
708
+ if (blocked) return;
709
+ }
710
+ action2 = nextAction;
711
+ location = nextLocation;
712
+ index++;
713
+ const historyState = createHistoryState(location, index);
714
+ window.history.pushState(historyState, "", createURL(location));
715
+ notifyListeners();
716
+ }
717
+ function replace(to, state) {
718
+ const nextLocation = createLocation(to, state);
719
+ const nextAction = "REPLACE";
720
+ if (blockers.size > 0) {
721
+ let blocked = false;
722
+ const retry = () => replace(to, state);
723
+ for (const blocker of blockers) {
724
+ blocker({
725
+ action: nextAction,
726
+ location: nextLocation,
727
+ retry
728
+ });
729
+ blocked = true;
730
+ break;
731
+ }
732
+ if (blocked) return;
733
+ }
734
+ action2 = nextAction;
735
+ location = nextLocation;
736
+ const historyState = createHistoryState(location, index);
737
+ window.history.replaceState(historyState, "", createURL(location));
738
+ notifyListeners();
739
+ }
740
+ function go(delta) {
741
+ window.history.go(delta);
742
+ }
743
+ if (window.history.state === null) {
744
+ const historyState = createHistoryState(location, index);
745
+ window.history.replaceState(historyState, "", createURL(location));
746
+ }
747
+ return {
748
+ get action() {
749
+ return action2;
750
+ },
751
+ get location() {
752
+ return location;
753
+ },
754
+ push,
755
+ replace,
756
+ go,
757
+ back() {
758
+ go(-1);
759
+ },
760
+ forward() {
761
+ go(1);
762
+ },
763
+ listen(listener) {
764
+ listeners.add(listener);
765
+ return () => listeners.delete(listener);
766
+ },
767
+ createHref(to) {
768
+ const loc = typeof to === "string" ? parseURL(to) : to;
769
+ return createURL(loc);
770
+ },
771
+ block(blocker) {
772
+ blockers.add(blocker);
773
+ if (blockers.size === 1) {
774
+ window.addEventListener("beforeunload", handleBeforeUnload);
775
+ }
776
+ return () => {
777
+ blockers.delete(blocker);
778
+ if (blockers.size === 0) {
779
+ window.removeEventListener("beforeunload", handleBeforeUnload);
780
+ }
781
+ };
782
+ }
783
+ };
784
+ }
785
+ function handleBeforeUnload(event) {
786
+ event.preventDefault();
787
+ event.returnValue = "";
788
+ }
789
+ function createHashHistory(options = {}) {
790
+ if (typeof window === "undefined") {
791
+ throw new Error(
792
+ "[fict-router] createHashHistory cannot be used in a server environment. Use createMemoryHistory or createStaticHistory for SSR."
793
+ );
794
+ }
795
+ const { hashType = "slash" } = options;
796
+ const listeners = /* @__PURE__ */ new Set();
797
+ const blockers = /* @__PURE__ */ new Set();
798
+ let action2 = "POP";
799
+ let location = readHashLocation();
800
+ let index = 0;
801
+ function readHashLocation() {
802
+ let hash = window.location.hash.slice(1);
803
+ if (hashType === "slash" && !hash.startsWith("/")) {
804
+ hash = "/" + hash;
805
+ } else if (hashType === "noslash" && hash.startsWith("/")) {
806
+ hash = hash.slice(1);
807
+ }
808
+ const { pathname, search, hash: innerHash } = parseURL(hash || "/");
809
+ return {
810
+ pathname: normalizePath(pathname),
811
+ search,
812
+ hash: innerHash,
813
+ state: window.history.state?.usr ?? null,
814
+ key: window.history.state?.key ?? createKey()
815
+ };
816
+ }
817
+ function createHashHref(location2) {
818
+ const url = createURL(location2);
819
+ if (hashType === "noslash") {
820
+ return "#" + url.slice(1);
821
+ }
822
+ return "#" + url;
823
+ }
824
+ function handleHashChange() {
825
+ const nextLocation = readHashLocation();
826
+ const nextAction = "POP";
827
+ if (blockers.size > 0) {
828
+ let blocked = false;
829
+ const retry = () => {
830
+ window.location.hash = createHashHref(nextLocation);
831
+ };
832
+ for (const blocker of blockers) {
833
+ blocker({
834
+ action: nextAction,
835
+ location: nextLocation,
836
+ retry
837
+ });
838
+ blocked = true;
839
+ break;
840
+ }
841
+ if (blocked) {
842
+ window.location.hash = createHashHref(location);
843
+ return;
844
+ }
845
+ }
846
+ action2 = nextAction;
847
+ location = nextLocation;
848
+ notifyListeners();
849
+ }
850
+ window.addEventListener("hashchange", handleHashChange);
851
+ function notifyListeners() {
852
+ for (const listener of listeners) {
853
+ listener({ action: action2, location });
854
+ }
855
+ }
856
+ function push(to, state) {
857
+ const nextLocation = createLocation(to, state);
858
+ const nextAction = "PUSH";
859
+ if (blockers.size > 0) {
860
+ let blocked = false;
861
+ const retry = () => push(to, state);
862
+ for (const blocker of blockers) {
863
+ blocker({
864
+ action: nextAction,
865
+ location: nextLocation,
866
+ retry
867
+ });
868
+ blocked = true;
869
+ break;
870
+ }
871
+ if (blocked) return;
872
+ }
873
+ action2 = nextAction;
874
+ location = nextLocation;
875
+ index++;
876
+ const historyState = createHistoryState(location, index);
877
+ window.history.pushState(historyState, "", createHashHref(location));
878
+ notifyListeners();
879
+ }
880
+ function replace(to, state) {
881
+ const nextLocation = createLocation(to, state);
882
+ const nextAction = "REPLACE";
883
+ if (blockers.size > 0) {
884
+ let blocked = false;
885
+ const retry = () => replace(to, state);
886
+ for (const blocker of blockers) {
887
+ blocker({
888
+ action: nextAction,
889
+ location: nextLocation,
890
+ retry
891
+ });
892
+ blocked = true;
893
+ break;
894
+ }
895
+ if (blocked) return;
896
+ }
897
+ action2 = nextAction;
898
+ location = nextLocation;
899
+ const historyState = createHistoryState(location, index);
900
+ window.history.replaceState(historyState, "", createHashHref(location));
901
+ notifyListeners();
902
+ }
903
+ function go(delta) {
904
+ window.history.go(delta);
905
+ }
906
+ return {
907
+ get action() {
908
+ return action2;
909
+ },
910
+ get location() {
911
+ return location;
912
+ },
913
+ push,
914
+ replace,
915
+ go,
916
+ back() {
917
+ go(-1);
918
+ },
919
+ forward() {
920
+ go(1);
921
+ },
922
+ listen(listener) {
923
+ listeners.add(listener);
924
+ return () => listeners.delete(listener);
925
+ },
926
+ createHref(to) {
927
+ const loc = createLocation(to);
928
+ return createHashHref(loc);
929
+ },
930
+ block(blocker) {
931
+ blockers.add(blocker);
932
+ if (blockers.size === 1) {
933
+ window.addEventListener("beforeunload", handleBeforeUnload);
934
+ }
935
+ return () => {
936
+ blockers.delete(blocker);
937
+ if (blockers.size === 0) {
938
+ window.removeEventListener("beforeunload", handleBeforeUnload);
939
+ }
940
+ };
941
+ }
942
+ };
943
+ }
944
+ function createMemoryHistory(options = {}) {
945
+ const { initialEntries = ["/"], initialIndex } = options;
946
+ const listeners = /* @__PURE__ */ new Set();
947
+ const blockers = /* @__PURE__ */ new Set();
948
+ const entries = initialEntries.map((entry, i) => createLocation(entry, null, `${i}`));
949
+ let index = initialIndex ?? entries.length - 1;
950
+ let action2 = "POP";
951
+ index = Math.max(0, Math.min(index, entries.length - 1));
952
+ function notifyListeners() {
953
+ const location = entries[index];
954
+ for (const listener of listeners) {
955
+ listener({ action: action2, location });
956
+ }
957
+ }
958
+ function push(to, state) {
959
+ const nextLocation = createLocation(to, state);
960
+ const nextAction = "PUSH";
961
+ if (blockers.size > 0) {
962
+ let blocked = false;
963
+ const retry = () => push(to, state);
964
+ for (const blocker of blockers) {
965
+ blocker({
966
+ action: nextAction,
967
+ location: nextLocation,
968
+ retry
969
+ });
970
+ blocked = true;
971
+ break;
972
+ }
973
+ if (blocked) return;
974
+ }
975
+ action2 = nextAction;
976
+ entries.splice(index + 1);
977
+ entries.push(nextLocation);
978
+ index = entries.length - 1;
979
+ notifyListeners();
980
+ }
981
+ function replace(to, state) {
982
+ const nextLocation = createLocation(to, state);
983
+ const nextAction = "REPLACE";
984
+ if (blockers.size > 0) {
985
+ let blocked = false;
986
+ const retry = () => replace(to, state);
987
+ for (const blocker of blockers) {
988
+ blocker({
989
+ action: nextAction,
990
+ location: nextLocation,
991
+ retry
992
+ });
993
+ blocked = true;
994
+ break;
995
+ }
996
+ if (blocked) return;
997
+ }
998
+ action2 = nextAction;
999
+ entries[index] = nextLocation;
1000
+ notifyListeners();
1001
+ }
1002
+ function go(delta) {
1003
+ const nextIndex = Math.max(0, Math.min(index + delta, entries.length - 1));
1004
+ if (nextIndex === index) return;
1005
+ const nextLocation = entries[nextIndex];
1006
+ const nextAction = "POP";
1007
+ if (blockers.size > 0) {
1008
+ let blocked = false;
1009
+ const retry = () => go(delta);
1010
+ for (const blocker of blockers) {
1011
+ blocker({
1012
+ action: nextAction,
1013
+ location: nextLocation,
1014
+ retry
1015
+ });
1016
+ blocked = true;
1017
+ break;
1018
+ }
1019
+ if (blocked) return;
1020
+ }
1021
+ action2 = nextAction;
1022
+ index = nextIndex;
1023
+ notifyListeners();
1024
+ }
1025
+ return {
1026
+ get action() {
1027
+ return action2;
1028
+ },
1029
+ get location() {
1030
+ return entries[index];
1031
+ },
1032
+ push,
1033
+ replace,
1034
+ go,
1035
+ back() {
1036
+ go(-1);
1037
+ },
1038
+ forward() {
1039
+ go(1);
1040
+ },
1041
+ listen(listener) {
1042
+ listeners.add(listener);
1043
+ return () => listeners.delete(listener);
1044
+ },
1045
+ createHref(to) {
1046
+ const loc = typeof to === "string" ? parseURL(to) : to;
1047
+ return createURL(loc);
1048
+ },
1049
+ block(blocker) {
1050
+ blockers.add(blocker);
1051
+ return () => blockers.delete(blocker);
1052
+ }
1053
+ };
1054
+ }
1055
+ function createStaticHistory(url) {
1056
+ const location = createLocation(url);
1057
+ return {
1058
+ get action() {
1059
+ return "POP";
1060
+ },
1061
+ get location() {
1062
+ return location;
1063
+ },
1064
+ push() {
1065
+ console.warn("[fict-router] Cannot push on static history (SSR)");
1066
+ },
1067
+ replace() {
1068
+ console.warn("[fict-router] Cannot replace on static history (SSR)");
1069
+ },
1070
+ go() {
1071
+ console.warn("[fict-router] Cannot go on static history (SSR)");
1072
+ },
1073
+ back() {
1074
+ },
1075
+ forward() {
1076
+ },
1077
+ listen() {
1078
+ return () => {
1079
+ };
1080
+ },
1081
+ createHref(to) {
1082
+ const loc = typeof to === "string" ? parseURL(to) : to;
1083
+ return createURL(loc);
1084
+ },
1085
+ block() {
1086
+ return () => {
1087
+ };
1088
+ }
1089
+ };
1090
+ }
1091
+
1092
+ // src/scroll.ts
1093
+ var scrollPositions = /* @__PURE__ */ new Map();
1094
+ var MAX_STORED_POSITIONS = 100;
1095
+ function saveScrollPosition(key) {
1096
+ if (!isBrowser()) return;
1097
+ scrollPositions.set(key, {
1098
+ x: window.scrollX,
1099
+ y: window.scrollY
1100
+ });
1101
+ if (scrollPositions.size > MAX_STORED_POSITIONS) {
1102
+ const firstKey = scrollPositions.keys().next().value;
1103
+ if (firstKey) {
1104
+ scrollPositions.delete(firstKey);
1105
+ }
1106
+ }
1107
+ }
1108
+ function clearScrollPosition(key) {
1109
+ scrollPositions.delete(key);
1110
+ }
1111
+ function clearAllScrollPositions() {
1112
+ scrollPositions.clear();
1113
+ }
1114
+ function scrollTo(x, y, behavior = "auto") {
1115
+ if (!isBrowser()) return;
1116
+ window.scrollTo({
1117
+ left: x,
1118
+ top: y,
1119
+ behavior
1120
+ });
1121
+ }
1122
+ function scrollToTop(behavior = "auto") {
1123
+ scrollTo(0, 0, behavior);
1124
+ }
1125
+ function scrollToHash(hash, behavior = "auto") {
1126
+ if (!isBrowser() || !hash) return false;
1127
+ const id = hash.startsWith("#") ? hash.slice(1) : hash;
1128
+ if (!id) return false;
1129
+ const element = document.getElementById(id);
1130
+ if (element) {
1131
+ element.scrollIntoView({ behavior });
1132
+ return true;
1133
+ }
1134
+ return false;
1135
+ }
1136
+ function restoreScrollPosition(key) {
1137
+ if (!isBrowser()) return false;
1138
+ const position = scrollPositions.get(key);
1139
+ if (position) {
1140
+ requestAnimationFrame(() => {
1141
+ scrollTo(position.x, position.y);
1142
+ });
1143
+ return true;
1144
+ }
1145
+ return false;
1146
+ }
1147
+ var defaultOptions = {
1148
+ enabled: true,
1149
+ restoreOnPop: true,
1150
+ scrollToTopOnPush: true,
1151
+ behavior: "auto"
27
1152
  };
1153
+ function createScrollRestoration(options = {}) {
1154
+ const config = { ...defaultOptions, ...options };
1155
+ if (isBrowser() && "scrollRestoration" in history) {
1156
+ history.scrollRestoration = "manual";
1157
+ }
1158
+ function handleNavigation(from, to, action2) {
1159
+ if (!config.enabled || !isBrowser()) return;
1160
+ if (from?.key) {
1161
+ saveScrollPosition(from.key);
1162
+ }
1163
+ if (action2 === "POP" && config.restoreOnPop) {
1164
+ if (!restoreScrollPosition(to.key)) {
1165
+ if (to.hash) {
1166
+ requestAnimationFrame(() => {
1167
+ if (!scrollToHash(to.hash, config.behavior)) {
1168
+ scrollToTop(config.behavior);
1169
+ }
1170
+ });
1171
+ } else {
1172
+ scrollToTop(config.behavior);
1173
+ }
1174
+ }
1175
+ } else if ((action2 === "PUSH" || action2 === "REPLACE") && config.scrollToTopOnPush) {
1176
+ requestAnimationFrame(() => {
1177
+ if (to.hash) {
1178
+ if (!scrollToHash(to.hash, config.behavior)) {
1179
+ scrollToTop(config.behavior);
1180
+ }
1181
+ } else {
1182
+ scrollToTop(config.behavior);
1183
+ }
1184
+ });
1185
+ }
1186
+ }
1187
+ function reset() {
1188
+ if (isBrowser() && "scrollRestoration" in history) {
1189
+ history.scrollRestoration = "auto";
1190
+ }
1191
+ clearAllScrollPositions();
1192
+ }
1193
+ return {
1194
+ handleNavigation,
1195
+ saveScrollPosition,
1196
+ restoreScrollPosition,
1197
+ scrollToTop: () => scrollToTop(config.behavior),
1198
+ scrollToHash: (hash) => scrollToHash(hash, config.behavior),
1199
+ reset,
1200
+ config
1201
+ };
1202
+ }
1203
+ var defaultScrollRestoration = null;
1204
+ function getScrollRestoration() {
1205
+ if (!defaultScrollRestoration) {
1206
+ defaultScrollRestoration = createScrollRestoration();
1207
+ }
1208
+ return defaultScrollRestoration;
1209
+ }
1210
+ function configureScrollRestoration(options) {
1211
+ defaultScrollRestoration = createScrollRestoration(options);
1212
+ }
1213
+
1214
+ // src/components.tsx
1215
+ var import_jsx_runtime = require("fict/jsx-runtime");
1216
+ var import_meta = {};
1217
+ var isDevEnv = typeof import_meta !== "undefined" && import_meta.env?.DEV === true || typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1218
+ var didWarnBaseMismatch = false;
1219
+ function hasBasePrefix(pathname, base) {
1220
+ if (!base) return true;
1221
+ return pathname === base || pathname.startsWith(base + "/");
1222
+ }
1223
+ function stripBaseOrWarn(pathname, base) {
1224
+ if (!base) return pathname;
1225
+ if (!hasBasePrefix(pathname, base)) {
1226
+ if (isDevEnv && !didWarnBaseMismatch) {
1227
+ didWarnBaseMismatch = true;
1228
+ console.warn(
1229
+ `[fict-router] Location "${pathname}" does not start with base "${base}". No routes matched.`
1230
+ );
1231
+ }
1232
+ return null;
1233
+ }
1234
+ return stripBasePath(pathname, base);
1235
+ }
1236
+ function stripBaseIfPresent(pathname, base) {
1237
+ if (!base) return pathname;
1238
+ if (hasBasePrefix(pathname, base)) {
1239
+ return stripBasePath(pathname, base);
1240
+ }
1241
+ return pathname;
1242
+ }
1243
+ function createRouterState(history2, routes, base = "") {
1244
+ const normalizedBase = normalizePath(base);
1245
+ const baseForStrip = normalizedBase === "/" ? "" : normalizedBase;
1246
+ const compiledRoutes = routes.map((r) => compileRoute(r));
1247
+ const branches = createBranches(compiledRoutes);
1248
+ const matchWithBase = (pathname) => {
1249
+ const strippedPath = stripBaseOrWarn(pathname, baseForStrip);
1250
+ if (strippedPath == null) return [];
1251
+ return matchRoutes(branches, strippedPath) || [];
1252
+ };
1253
+ const initialLocation = history2.location;
1254
+ const initialMatches = matchWithBase(initialLocation.pathname);
1255
+ const locationSignal = (0, import_advanced.createSignal)(initialLocation);
1256
+ const matchesSignal = (0, import_advanced.createSignal)(initialMatches);
1257
+ const isRoutingSignal = (0, import_advanced.createSignal)(false);
1258
+ const pendingLocationSignal = (0, import_advanced.createSignal)(null);
1259
+ const beforeLeaveHandlers = /* @__PURE__ */ new Set();
1260
+ let navigationToken = 0;
1261
+ const beforeLeave = {
1262
+ addHandler(handler) {
1263
+ beforeLeaveHandlers.add(handler);
1264
+ return () => beforeLeaveHandlers.delete(handler);
1265
+ },
1266
+ async confirm(to, from) {
1267
+ if (beforeLeaveHandlers.size === 0) return true;
1268
+ const currentToken = ++navigationToken;
1269
+ let defaultPrevented = false;
1270
+ let retryRequested = false;
1271
+ let forceRetry = false;
1272
+ const event = {
1273
+ to,
1274
+ from,
1275
+ get defaultPrevented() {
1276
+ return defaultPrevented;
1277
+ },
1278
+ preventDefault() {
1279
+ defaultPrevented = true;
1280
+ },
1281
+ retry(force) {
1282
+ retryRequested = true;
1283
+ forceRetry = force ?? false;
1284
+ }
1285
+ };
1286
+ for (const handler of beforeLeaveHandlers) {
1287
+ await handler(event);
1288
+ if (currentToken !== navigationToken) {
1289
+ return false;
1290
+ }
1291
+ if (defaultPrevented && !retryRequested) {
1292
+ return false;
1293
+ }
1294
+ if (retryRequested && forceRetry) {
1295
+ return true;
1296
+ }
1297
+ }
1298
+ if (currentToken !== navigationToken) {
1299
+ return false;
1300
+ }
1301
+ return !defaultPrevented || retryRequested;
1302
+ }
1303
+ };
1304
+ const navigate = (toOrDelta, options) => {
1305
+ if (typeof toOrDelta === "number") {
1306
+ history2.go(toOrDelta);
1307
+ return;
1308
+ }
1309
+ const currentLocation = locationSignal();
1310
+ const to = toOrDelta;
1311
+ let toPathname;
1312
+ let toSearch = "";
1313
+ let toHash = "";
1314
+ if (typeof to === "string") {
1315
+ let remaining = to;
1316
+ const hashIndex = remaining.indexOf("#");
1317
+ if (hashIndex >= 0) {
1318
+ toHash = remaining.slice(hashIndex);
1319
+ remaining = remaining.slice(0, hashIndex);
1320
+ }
1321
+ const searchIndex = remaining.indexOf("?");
1322
+ if (searchIndex >= 0) {
1323
+ toSearch = remaining.slice(searchIndex);
1324
+ remaining = remaining.slice(0, searchIndex);
1325
+ }
1326
+ toPathname = remaining;
1327
+ } else {
1328
+ toPathname = to.pathname || "";
1329
+ toSearch = to.search || "";
1330
+ toHash = to.hash || "";
1331
+ }
1332
+ let targetPath;
1333
+ const currentPathWithoutBase = stripBaseOrWarn(currentLocation.pathname, baseForStrip) || "/";
1334
+ if (typeof to === "string") {
1335
+ if (toPathname === "") {
1336
+ targetPath = currentPathWithoutBase;
1337
+ } else if (options?.relative === "route") {
1338
+ const matches = matchesSignal();
1339
+ const currentMatch = matches[matches.length - 1];
1340
+ const currentRoutePath = currentMatch?.pathname || currentPathWithoutBase;
1341
+ targetPath = resolvePath(currentRoutePath, toPathname);
1342
+ } else {
1343
+ targetPath = toPathname.startsWith("/") ? stripBaseIfPresent(toPathname, baseForStrip) : resolvePath(currentPathWithoutBase, toPathname);
1344
+ }
1345
+ } else {
1346
+ const rawTargetPath = toPathname || currentPathWithoutBase;
1347
+ targetPath = stripBaseIfPresent(rawTargetPath, baseForStrip);
1348
+ }
1349
+ const toState = typeof to === "object" ? to.state : void 0;
1350
+ const toKey = typeof to === "object" ? to.key : void 0;
1351
+ const finalState = options?.state !== void 0 ? options.state : toState;
1352
+ const targetPathWithBase = prependBasePath(targetPath, baseForStrip);
1353
+ const locationSpec = {
1354
+ pathname: targetPathWithBase,
1355
+ search: toSearch,
1356
+ hash: toHash
1357
+ };
1358
+ if (finalState !== void 0) {
1359
+ locationSpec.state = finalState;
1360
+ }
1361
+ if (toKey !== void 0) {
1362
+ locationSpec.key = toKey;
1363
+ }
1364
+ const targetLocation = createLocation(locationSpec, finalState, toKey);
1365
+ (0, import_runtime2.untrack)(async () => {
1366
+ const canNavigate = await beforeLeave.confirm(targetLocation, currentLocation);
1367
+ if (!canNavigate) {
1368
+ pendingLocationSignal(null);
1369
+ return;
1370
+ }
1371
+ (0, import_runtime2.batch)(() => {
1372
+ isRoutingSignal(true);
1373
+ pendingLocationSignal(targetLocation);
1374
+ });
1375
+ (0, import_runtime2.startTransition)(() => {
1376
+ const prevLocation = history2.location;
1377
+ if (options?.replace) {
1378
+ history2.replace(targetLocation, finalState);
1379
+ } else {
1380
+ history2.push(targetLocation, finalState);
1381
+ }
1382
+ if (options?.scroll !== false && isBrowser()) {
1383
+ const scrollRestoration = getScrollRestoration();
1384
+ scrollRestoration.handleNavigation(
1385
+ prevLocation,
1386
+ history2.location,
1387
+ options?.replace ? "REPLACE" : "PUSH"
1388
+ );
1389
+ }
1390
+ if (locationsAreEqual(prevLocation, history2.location)) {
1391
+ (0, import_runtime2.batch)(() => {
1392
+ isRoutingSignal(false);
1393
+ pendingLocationSignal(null);
1394
+ });
1395
+ }
1396
+ });
1397
+ });
1398
+ };
1399
+ const unlisten = history2.listen(({ action: action2, location: newLocation }) => {
1400
+ const prevLocation = locationSignal();
1401
+ (0, import_runtime2.batch)(() => {
1402
+ locationSignal(newLocation);
1403
+ const newMatches = matchWithBase(newLocation.pathname);
1404
+ matchesSignal(newMatches);
1405
+ isRoutingSignal(false);
1406
+ pendingLocationSignal(null);
1407
+ });
1408
+ if (action2 === "POP" && isBrowser()) {
1409
+ const scrollRestoration = getScrollRestoration();
1410
+ scrollRestoration.handleNavigation(prevLocation, newLocation, "POP");
1411
+ }
1412
+ });
1413
+ const state = () => ({
1414
+ location: locationSignal(),
1415
+ matches: matchesSignal(),
1416
+ isRouting: isRoutingSignal(),
1417
+ pendingLocation: pendingLocationSignal()
1418
+ });
1419
+ return {
1420
+ state,
1421
+ navigate,
1422
+ beforeLeave,
1423
+ cleanup: unlisten,
1424
+ normalizedBase: baseForStrip
1425
+ };
1426
+ }
1427
+ function RouterProvider(props) {
1428
+ const { state, navigate, beforeLeave, cleanup, normalizedBase } = createRouterState(
1429
+ props.history,
1430
+ props.routes,
1431
+ props.base
1432
+ );
1433
+ (0, import_runtime2.onCleanup)(cleanup);
1434
+ const routerContext = {
1435
+ location: () => state().location,
1436
+ params: () => {
1437
+ const matches = state().matches;
1438
+ const allParams = {};
1439
+ for (const match of matches) {
1440
+ Object.assign(allParams, match.params);
1441
+ }
1442
+ return allParams;
1443
+ },
1444
+ matches: () => state().matches,
1445
+ navigate,
1446
+ isRouting: () => state().isRouting,
1447
+ pendingLocation: () => state().pendingLocation,
1448
+ base: normalizedBase,
1449
+ resolvePath: (to) => {
1450
+ const location = state().location;
1451
+ const currentPathWithoutBase = stripBaseOrWarn(location.pathname, normalizedBase) || "/";
1452
+ const rawTargetPath = typeof to === "string" ? to : to.pathname || "/";
1453
+ const targetPath = rawTargetPath.startsWith("/") ? stripBaseIfPresent(rawTargetPath, normalizedBase) : rawTargetPath;
1454
+ return resolvePath(currentPathWithoutBase, targetPath);
1455
+ }
1456
+ };
1457
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterContext.Provider, { value: routerContext, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BeforeLeaveContext.Provider, { value: beforeLeave, children: props.children }) });
1458
+ }
1459
+ function Router(props) {
1460
+ const history2 = props.history || createBrowserHistory();
1461
+ const routes = extractRoutes(props.children);
1462
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Routes, { children: props.children }) });
1463
+ }
1464
+ function HashRouter(props) {
1465
+ const hashOptions = props.hashType ? { hashType: props.hashType } : void 0;
1466
+ const history2 = createHashHistory(hashOptions);
1467
+ const routes = extractRoutes(props.children);
1468
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Routes, { children: props.children }) });
1469
+ }
1470
+ function MemoryRouter(props) {
1471
+ const memoryOptions = {};
1472
+ if (props.initialEntries !== void 0) {
1473
+ memoryOptions.initialEntries = props.initialEntries;
1474
+ }
1475
+ if (props.initialIndex !== void 0) {
1476
+ memoryOptions.initialIndex = props.initialIndex;
1477
+ }
1478
+ const history2 = createMemoryHistory(
1479
+ Object.keys(memoryOptions).length > 0 ? memoryOptions : void 0
1480
+ );
1481
+ const routes = extractRoutes(props.children);
1482
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Routes, { children: props.children }) });
1483
+ }
1484
+ function StaticRouter(props) {
1485
+ const history2 = createStaticHistory(props.url);
1486
+ const routes = extractRoutes(props.children);
1487
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Routes, { children: props.children }) });
1488
+ }
1489
+ function Routes(props) {
1490
+ const router = useRouter();
1491
+ const parentRoute = useRoute();
1492
+ const routes = extractRoutes(props.children);
1493
+ const compiledRoutes = routes.map((r) => compileRoute(r));
1494
+ const branches = createBranches(compiledRoutes);
1495
+ const currentMatches = (0, import_runtime2.createMemo)(() => {
1496
+ const location = router.location();
1497
+ const parentMatch = parentRoute.match();
1498
+ const locationPath = stripBaseOrWarn(location.pathname, router.base);
1499
+ if (locationPath == null) return [];
1500
+ let basePath = "/";
1501
+ if (parentMatch) {
1502
+ basePath = parentMatch.pathname;
1503
+ }
1504
+ const relativePath = locationPath.startsWith(basePath) ? locationPath.slice(basePath.length) || "/" : locationPath;
1505
+ return matchRoutes(branches, relativePath) || [];
1506
+ });
1507
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: renderMatches(currentMatches(), 0) });
1508
+ }
1509
+ function renderMatches(matches, index) {
1510
+ if (index >= matches.length) {
1511
+ return null;
1512
+ }
1513
+ const match = matches[index];
1514
+ const route = match.route;
1515
+ const router = useRouter();
1516
+ const dataState = (0, import_advanced.createSignal)({
1517
+ data: void 0,
1518
+ error: void 0,
1519
+ loading: !!route.preload
1520
+ });
1521
+ let preloadToken = 0;
1522
+ if (route.preload) {
1523
+ (0, import_runtime2.createEffect)(() => {
1524
+ const location = router.location();
1525
+ const preloadArgs = {
1526
+ params: match.params,
1527
+ location,
1528
+ intent: "navigate"
1529
+ };
1530
+ const currentToken = ++preloadToken;
1531
+ dataState({ data: void 0, error: void 0, loading: true });
1532
+ Promise.resolve(route.preload(preloadArgs)).then((result) => {
1533
+ if (currentToken === preloadToken) {
1534
+ dataState({ data: result, error: void 0, loading: false });
1535
+ }
1536
+ }).catch((error) => {
1537
+ if (currentToken === preloadToken) {
1538
+ dataState({ data: void 0, error, loading: false });
1539
+ }
1540
+ });
1541
+ });
1542
+ }
1543
+ const routeContext = {
1544
+ match: () => match,
1545
+ data: () => dataState().data,
1546
+ error: () => dataState().error,
1547
+ outlet: () => renderMatches(matches, index + 1),
1548
+ resolvePath: (to) => {
1549
+ const basePath = match.pathname;
1550
+ const targetPath = typeof to === "string" ? to : to.pathname || "/";
1551
+ return resolvePath(basePath, targetPath);
1552
+ }
1553
+ };
1554
+ const renderContent = () => {
1555
+ const state = dataState();
1556
+ if (state.error !== void 0 && route.errorElement) {
1557
+ return route.errorElement;
1558
+ }
1559
+ if (state.loading && route.loadingElement) {
1560
+ return route.loadingElement;
1561
+ }
1562
+ if (route.component) {
1563
+ const Component = route.component;
1564
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { params: match.params, location: router.location(), data: state.data, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Outlet, {}) });
1565
+ } else if (route.element) {
1566
+ return route.element;
1567
+ } else if (route.children) {
1568
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Outlet, {});
1569
+ }
1570
+ return null;
1571
+ };
1572
+ let content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouteContext.Provider, { value: routeContext, children: renderContent() });
1573
+ if (route.errorElement) {
1574
+ content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1575
+ import_runtime2.ErrorBoundary,
1576
+ {
1577
+ fallback: (err, reset) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouteErrorContext.Provider, { value: { error: err, reset }, children: route.errorElement }),
1578
+ children: content
1579
+ }
1580
+ );
1581
+ }
1582
+ if (route.loadingElement) {
1583
+ content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_runtime2.Suspense, { fallback: route.loadingElement, children: content });
1584
+ }
1585
+ return content;
1586
+ }
1587
+ function Route(_props) {
1588
+ return null;
1589
+ }
1590
+ function Outlet() {
1591
+ const route = useRoute();
1592
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: route.outlet() });
1593
+ }
1594
+ function Navigate(props) {
1595
+ const router = useRouter();
1596
+ (0, import_runtime2.createEffect)(() => {
1597
+ router.navigate(props.to, {
1598
+ replace: props.replace ?? true,
1599
+ state: props.state
1600
+ });
1601
+ });
1602
+ return null;
1603
+ }
1604
+ function Redirect(props) {
1605
+ const router = useRouter();
1606
+ (0, import_runtime2.createEffect)(() => {
1607
+ router.navigate(props.to, {
1608
+ replace: props.push !== true,
1609
+ // Replace by default, push only if explicitly requested
1610
+ state: props.state
1611
+ });
1612
+ });
1613
+ return null;
1614
+ }
1615
+ function extractRoutes(children) {
1616
+ const routes = [];
1617
+ if (children == null) return routes;
1618
+ const childArray = Array.isArray(children) ? children : [children];
1619
+ for (const child of childArray) {
1620
+ if (child == null || typeof child !== "object") continue;
1621
+ const vnode = child;
1622
+ if (vnode.type === Route) {
1623
+ const props = vnode.props || {};
1624
+ const routeDef = {};
1625
+ if (props.path !== void 0) routeDef.path = props.path;
1626
+ if (props.component !== void 0) routeDef.component = props.component;
1627
+ if (props.element !== void 0) routeDef.element = props.element;
1628
+ if (props.index !== void 0) routeDef.index = props.index;
1629
+ if (props.preload !== void 0)
1630
+ routeDef.preload = props.preload;
1631
+ if (props.errorElement !== void 0) routeDef.errorElement = props.errorElement;
1632
+ if (props.loadingElement !== void 0)
1633
+ routeDef.loadingElement = props.loadingElement;
1634
+ if (props.children) routeDef.children = extractRoutes(props.children);
1635
+ routes.push(routeDef);
1636
+ } else if (vnode.type === import_runtime2.Fragment && vnode.props?.children) {
1637
+ routes.push(...extractRoutes(vnode.props.children));
1638
+ }
1639
+ }
1640
+ return routes;
1641
+ }
1642
+ function createRoutes(routes) {
1643
+ return routes;
1644
+ }
1645
+ function createRouter(routes, options) {
1646
+ return {
1647
+ Router: (props) => {
1648
+ const history2 = options?.history || createBrowserHistory();
1649
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, { history: history2, routes, base: options?.base, children: props.children || /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Routes, { children: routesToElements(routes) }) });
1650
+ }
1651
+ };
1652
+ }
1653
+ function routesToElements(routes) {
1654
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: routes.map((route, i) => {
1655
+ const routeProps = { key: route.key || `route-${i}` };
1656
+ if (route.path !== void 0) routeProps.path = route.path;
1657
+ if (route.component !== void 0) routeProps.component = route.component;
1658
+ if (route.element !== void 0) routeProps.element = route.element;
1659
+ if (route.index !== void 0) routeProps.index = route.index;
1660
+ if (route.children) routeProps.children = routesToElements(route.children);
1661
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, { ...routeProps });
1662
+ }) });
1663
+ }
1664
+
1665
+ // src/link.tsx
1666
+ var import_runtime3 = require("@fictjs/runtime");
1667
+ var import_jsx_runtime2 = require("fict/jsx-runtime");
1668
+ function Link(props) {
1669
+ const router = useRouter();
1670
+ const href = useHref(() => props.to);
1671
+ let preloadTriggered = false;
1672
+ const handleClick = (event) => {
1673
+ if (props.onClick) {
1674
+ props.onClick(event);
1675
+ }
1676
+ if (event.defaultPrevented) return;
1677
+ if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
1678
+ if (event.button !== 0) return;
1679
+ if (props.reloadDocument) return;
1680
+ if (props.disabled) return;
1681
+ const target = event.currentTarget.target;
1682
+ if (target && target !== "_self") return;
1683
+ event.preventDefault();
1684
+ const options = {
1685
+ replace: props.replace,
1686
+ state: props.state,
1687
+ scroll: props.scroll,
1688
+ relative: props.relative
1689
+ };
1690
+ router.navigate(props.to, options);
1691
+ };
1692
+ const triggerPreload = () => {
1693
+ if (preloadTriggered || props.disabled || props.prefetch === "none") return;
1694
+ preloadTriggered = true;
1695
+ const hrefValue = href();
1696
+ if (typeof window !== "undefined" && window.dispatchEvent) {
1697
+ window.dispatchEvent(
1698
+ new CustomEvent("fict-router:preload", {
1699
+ detail: { href: hrefValue, to: props.to }
1700
+ })
1701
+ );
1702
+ }
1703
+ };
1704
+ const handleMouseEnter = (event) => {
1705
+ if (props.prefetch === "intent" || props.prefetch === void 0) {
1706
+ triggerPreload();
1707
+ }
1708
+ const onMouseEnter = props.onMouseEnter;
1709
+ if (onMouseEnter) onMouseEnter(event);
1710
+ };
1711
+ const handleFocus = (event) => {
1712
+ if (props.prefetch === "intent" || props.prefetch === void 0) {
1713
+ triggerPreload();
1714
+ }
1715
+ const onFocus = props.onFocus;
1716
+ if (onFocus) onFocus(event);
1717
+ };
1718
+ const {
1719
+ to: _to,
1720
+ replace: _replace,
1721
+ state: _state,
1722
+ scroll: _scroll,
1723
+ relative: _relative,
1724
+ reloadDocument: _reloadDocument,
1725
+ prefetch,
1726
+ disabled,
1727
+ onClick: _onClick,
1728
+ children,
1729
+ ...anchorProps
1730
+ } = props;
1731
+ if (disabled) {
1732
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { ...anchorProps, children });
1733
+ }
1734
+ if (prefetch === "render") {
1735
+ triggerPreload();
1736
+ }
1737
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1738
+ "a",
1739
+ {
1740
+ ...anchorProps,
1741
+ href: href(),
1742
+ onClick: handleClick,
1743
+ onMouseEnter: handleMouseEnter,
1744
+ onFocus: handleFocus,
1745
+ children
1746
+ }
1747
+ );
1748
+ }
1749
+ function NavLink(props) {
1750
+ const router = useRouter();
1751
+ const isActive = useIsActive(() => props.to, { end: props.end });
1752
+ const href = useHref(() => props.to);
1753
+ const pendingLocation = usePendingLocation();
1754
+ const computeIsPending = () => {
1755
+ const pending = pendingLocation();
1756
+ if (!pending) return false;
1757
+ const resolvedHref = href();
1758
+ const baseToStrip = router.base === "/" ? "" : router.base;
1759
+ const pendingPathWithoutBase = stripBasePath(pending.pathname, baseToStrip);
1760
+ const parsed = parseURL(resolvedHref);
1761
+ const targetPathWithoutBase = stripBasePath(parsed.pathname, baseToStrip);
1762
+ if (props.end) {
1763
+ return pendingPathWithoutBase === targetPathWithoutBase;
1764
+ }
1765
+ return pendingPathWithoutBase === targetPathWithoutBase || pendingPathWithoutBase.startsWith(targetPathWithoutBase + "/");
1766
+ };
1767
+ const getRenderProps = () => ({
1768
+ isActive: isActive(),
1769
+ isPending: computeIsPending(),
1770
+ isTransitioning: router.isRouting()
1771
+ });
1772
+ const computedClassName = (0, import_runtime3.createMemo)(() => {
1773
+ const renderProps = getRenderProps();
1774
+ const classes = [];
1775
+ if (typeof props.className === "function") {
1776
+ const result = props.className(renderProps);
1777
+ if (result) classes.push(result);
1778
+ } else if (props.className) {
1779
+ classes.push(props.className);
1780
+ }
1781
+ if (renderProps.isActive && props.activeClassName) {
1782
+ classes.push(props.activeClassName);
1783
+ }
1784
+ if (renderProps.isPending && props.pendingClassName) {
1785
+ classes.push(props.pendingClassName);
1786
+ }
1787
+ return classes.join(" ") || void 0;
1788
+ });
1789
+ const computedStyle = (0, import_runtime3.createMemo)(() => {
1790
+ const renderProps = getRenderProps();
1791
+ const style = {};
1792
+ if (typeof props.style === "function") {
1793
+ const result = props.style(renderProps);
1794
+ if (result) Object.assign(style, result);
1795
+ } else if (props.style) {
1796
+ Object.assign(style, props.style);
1797
+ }
1798
+ if (renderProps.isActive && props.activeStyle) {
1799
+ Object.assign(style, props.activeStyle);
1800
+ }
1801
+ if (renderProps.isPending && props.pendingStyle) {
1802
+ Object.assign(style, props.pendingStyle);
1803
+ }
1804
+ return Object.keys(style).length > 0 ? style : void 0;
1805
+ });
1806
+ const computedChildren = (0, import_runtime3.createMemo)(() => {
1807
+ const renderProps = getRenderProps();
1808
+ if (typeof props.children === "function") {
1809
+ return props.children(renderProps);
1810
+ }
1811
+ return props.children;
1812
+ });
1813
+ const ariaCurrent = (0, import_runtime3.createMemo)(() => {
1814
+ const renderProps = getRenderProps();
1815
+ if (!renderProps.isActive) return void 0;
1816
+ return props["aria-current"] || "page";
1817
+ });
1818
+ const handleClick = (event) => {
1819
+ if (props.onClick) {
1820
+ props.onClick(event);
1821
+ }
1822
+ if (event.defaultPrevented) return;
1823
+ if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
1824
+ if (event.button !== 0) return;
1825
+ if (props.reloadDocument) return;
1826
+ if (props.disabled) return;
1827
+ const target = event.currentTarget.target;
1828
+ if (target && target !== "_self") return;
1829
+ event.preventDefault();
1830
+ router.navigate(props.to, {
1831
+ replace: props.replace,
1832
+ state: props.state,
1833
+ scroll: props.scroll,
1834
+ relative: props.relative
1835
+ });
1836
+ };
1837
+ const {
1838
+ to: _to,
1839
+ replace: _replace,
1840
+ state: _state,
1841
+ scroll: _scroll,
1842
+ relative: _relative,
1843
+ reloadDocument: _reloadDocument,
1844
+ prefetch: _prefetch,
1845
+ disabled,
1846
+ onClick: _onClick,
1847
+ children: _children,
1848
+ className: _className,
1849
+ style: _style,
1850
+ end: _end,
1851
+ caseSensitive: _caseSensitive,
1852
+ activeClassName: _activeClassName,
1853
+ pendingClassName: _pendingClassName,
1854
+ activeStyle: _activeStyle,
1855
+ pendingStyle: _pendingStyle,
1856
+ "aria-current": _ariaCurrent,
1857
+ ...anchorProps
1858
+ } = props;
1859
+ if (disabled) {
1860
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { ...anchorProps, className: computedClassName(), style: computedStyle(), children: computedChildren() });
1861
+ }
1862
+ const finalClassName = computedClassName();
1863
+ const finalStyle = computedStyle();
1864
+ const finalAriaCurrent = ariaCurrent();
1865
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1866
+ "a",
1867
+ {
1868
+ ...anchorProps,
1869
+ href: href(),
1870
+ ...finalClassName !== void 0 ? { className: finalClassName } : {},
1871
+ ...finalStyle !== void 0 ? { style: finalStyle } : {},
1872
+ ...finalAriaCurrent !== void 0 ? { "aria-current": finalAriaCurrent } : {},
1873
+ onClick: handleClick,
1874
+ children: computedChildren()
1875
+ }
1876
+ );
1877
+ }
1878
+ function Form(props) {
1879
+ const router = useRouter();
1880
+ const handleSubmit = (event) => {
1881
+ if (props.onSubmit) {
1882
+ props.onSubmit(event);
1883
+ }
1884
+ if (event.defaultPrevented) return;
1885
+ const form = event.currentTarget;
1886
+ const target = form.target;
1887
+ if (target && target !== "_self") return;
1888
+ event.preventDefault();
1889
+ const formData = new FormData(form);
1890
+ const method2 = props.method?.toUpperCase() || "GET";
1891
+ const actionUrl = props.action || router.location().pathname;
1892
+ if (method2 === "GET") {
1893
+ const searchParams = new URLSearchParams();
1894
+ formData.forEach((value, key) => {
1895
+ if (typeof value === "string") {
1896
+ searchParams.append(key, value);
1897
+ }
1898
+ });
1899
+ router.navigate(
1900
+ {
1901
+ pathname: actionUrl,
1902
+ search: "?" + searchParams.toString()
1903
+ },
1904
+ { replace: props.replace }
1905
+ );
1906
+ } else {
1907
+ submitFormAction(form, actionUrl, method2, formData, {
1908
+ navigate: props.navigate !== false,
1909
+ replace: props.replace ?? false,
1910
+ router
1911
+ });
1912
+ }
1913
+ };
1914
+ async function submitFormAction(formElement, url, method2, formData, options) {
1915
+ try {
1916
+ const response = await fetch(url, {
1917
+ method: method2,
1918
+ body: formData,
1919
+ headers: {
1920
+ // Let the browser set Content-Type for FormData (includes boundary)
1921
+ Accept: "application/json"
1922
+ }
1923
+ });
1924
+ if (!response.ok) {
1925
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1926
+ }
1927
+ const contentType = response.headers.get("Content-Type");
1928
+ let data = null;
1929
+ if (contentType?.includes("application/json")) {
1930
+ data = await response.json();
1931
+ }
1932
+ const redirectUrl = response.headers.get("X-Redirect") || response.headers.get("Location");
1933
+ if (options.navigate && redirectUrl) {
1934
+ options.router.navigate(redirectUrl, { replace: options.replace });
1935
+ }
1936
+ formElement.dispatchEvent(
1937
+ new CustomEvent("formsubmit", {
1938
+ bubbles: true,
1939
+ detail: { data, response }
1940
+ })
1941
+ );
1942
+ return { data, response };
1943
+ } catch (error) {
1944
+ formElement.dispatchEvent(
1945
+ new CustomEvent("formerror", {
1946
+ bubbles: true,
1947
+ detail: { error }
1948
+ })
1949
+ );
1950
+ console.error("[fict-router] Form submission failed:", error);
1951
+ throw error;
1952
+ }
1953
+ }
1954
+ const {
1955
+ action: action2,
1956
+ method,
1957
+ replace: _replace,
1958
+ relative: _relative,
1959
+ preventScrollReset: _preventScrollReset,
1960
+ navigate: _navigate,
1961
+ fetcherKey: _fetcherKey,
1962
+ children,
1963
+ onSubmit: _onSubmit,
1964
+ ...formProps
1965
+ } = props;
1966
+ const htmlMethod = method && ["get", "post"].includes(method) ? method : void 0;
1967
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1968
+ "form",
1969
+ {
1970
+ ...formProps,
1971
+ ...action2 !== void 0 ? { action: action2 } : {},
1972
+ ...htmlMethod !== void 0 ? { method: htmlMethod } : {},
1973
+ onSubmit: handleSubmit,
1974
+ children
1975
+ }
1976
+ );
1977
+ }
1978
+
1979
+ // src/data.ts
1980
+ var import_runtime4 = require("@fictjs/runtime");
1981
+ var import_advanced2 = require("@fictjs/runtime/advanced");
1982
+ var CACHE_DURATION = 3 * 60 * 1e3;
1983
+ var PRELOAD_CACHE_DURATION = 5 * 1e3;
1984
+ var MAX_CACHE_SIZE = 500;
1985
+ var NORMAL_CLEANUP_INTERVAL = 60 * 1e3;
1986
+ var FAST_CLEANUP_INTERVAL = 10 * 1e3;
1987
+ var queryCache = /* @__PURE__ */ new Map();
1988
+ var cacheCleanupTimer;
1989
+ var currentCleanupInterval = NORMAL_CLEANUP_INTERVAL;
1990
+ function evictOldestEntries() {
1991
+ if (queryCache.size <= MAX_CACHE_SIZE) return;
1992
+ const entries = Array.from(queryCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
1993
+ const toRemove = queryCache.size - MAX_CACHE_SIZE;
1994
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
1995
+ queryCache.delete(entries[i][0]);
1996
+ }
1997
+ }
1998
+ function runCacheCleanup() {
1999
+ const now = Date.now();
2000
+ for (const [key, entry] of queryCache) {
2001
+ const maxAge = entry.intent === "preload" ? PRELOAD_CACHE_DURATION : CACHE_DURATION;
2002
+ if (now - entry.timestamp > maxAge) {
2003
+ queryCache.delete(key);
2004
+ }
2005
+ }
2006
+ const newInterval = queryCache.size > MAX_CACHE_SIZE / 2 ? FAST_CLEANUP_INTERVAL : NORMAL_CLEANUP_INTERVAL;
2007
+ if (newInterval !== currentCleanupInterval) {
2008
+ currentCleanupInterval = newInterval;
2009
+ stopCacheCleanup();
2010
+ startCacheCleanup();
2011
+ }
2012
+ }
2013
+ function startCacheCleanup() {
2014
+ if (cacheCleanupTimer) return;
2015
+ cacheCleanupTimer = setInterval(runCacheCleanup, currentCleanupInterval);
2016
+ }
2017
+ function stopCacheCleanup() {
2018
+ if (cacheCleanupTimer) {
2019
+ clearInterval(cacheCleanupTimer);
2020
+ cacheCleanupTimer = void 0;
2021
+ }
2022
+ }
2023
+ function query(fn, name) {
2024
+ startCacheCleanup();
2025
+ return (...args) => {
2026
+ const cacheKey = `${name}:${hashParams(args)}`;
2027
+ const cached = queryCache.get(cacheKey);
2028
+ if (cached && cached.result !== void 0) {
2029
+ const maxAge = cached.intent === "preload" ? PRELOAD_CACHE_DURATION : CACHE_DURATION;
2030
+ if (Date.now() - cached.timestamp < maxAge) {
2031
+ return () => cached.result;
2032
+ }
2033
+ }
2034
+ const resultSignal = (0, import_advanced2.createSignal)(cached?.result);
2035
+ const errorSignal = (0, import_advanced2.createSignal)(void 0);
2036
+ const loadingSignal = (0, import_advanced2.createSignal)(true);
2037
+ const promise = Promise.resolve(fn(...args)).then((result) => {
2038
+ const entry = {
2039
+ timestamp: Date.now(),
2040
+ promise,
2041
+ result,
2042
+ intent: "navigate"
2043
+ };
2044
+ queryCache.set(cacheKey, entry);
2045
+ evictOldestEntries();
2046
+ (0, import_runtime4.batch)(() => {
2047
+ resultSignal(result);
2048
+ loadingSignal(false);
2049
+ });
2050
+ return result;
2051
+ }).catch((error) => {
2052
+ (0, import_runtime4.batch)(() => {
2053
+ errorSignal(error);
2054
+ loadingSignal(false);
2055
+ });
2056
+ throw error;
2057
+ });
2058
+ if (!cached) {
2059
+ queryCache.set(cacheKey, {
2060
+ timestamp: Date.now(),
2061
+ promise,
2062
+ intent: "navigate"
2063
+ });
2064
+ }
2065
+ return () => resultSignal();
2066
+ };
2067
+ }
2068
+ function revalidate(keys) {
2069
+ if (!keys) {
2070
+ queryCache.clear();
2071
+ return;
2072
+ }
2073
+ if (typeof keys === "string") {
2074
+ for (const cacheKey of queryCache.keys()) {
2075
+ if (cacheKey.startsWith(keys)) {
2076
+ queryCache.delete(cacheKey);
2077
+ }
2078
+ }
2079
+ return;
2080
+ }
2081
+ if (Array.isArray(keys)) {
2082
+ for (const key of keys) {
2083
+ for (const cacheKey of queryCache.keys()) {
2084
+ if (cacheKey.startsWith(key)) {
2085
+ queryCache.delete(cacheKey);
2086
+ }
2087
+ }
2088
+ }
2089
+ return;
2090
+ }
2091
+ if (keys instanceof RegExp) {
2092
+ for (const cacheKey of queryCache.keys()) {
2093
+ if (keys.test(cacheKey)) {
2094
+ queryCache.delete(cacheKey);
2095
+ }
2096
+ }
2097
+ }
2098
+ }
2099
+ var actionRegistry = /* @__PURE__ */ new Map();
2100
+ var submissionCounter = 0;
2101
+ function action(fn, name) {
2102
+ const actionName = name || `action-${++submissionCounter}`;
2103
+ const actionUrl = `/_action/${actionName}`;
2104
+ actionRegistry.set(actionUrl, fn);
2105
+ return {
2106
+ url: actionUrl,
2107
+ name: actionName,
2108
+ submit: async (formData) => {
2109
+ const baseUrl = typeof window !== "undefined" && window.location ? window.location.origin : "http://localhost";
2110
+ const request = new Request(new URL(actionUrl, baseUrl).href, {
2111
+ method: "POST",
2112
+ body: formData
2113
+ });
2114
+ return fn(formData, { params: {}, request });
2115
+ }
2116
+ };
2117
+ }
2118
+ function getAction(url) {
2119
+ return actionRegistry.get(url);
2120
+ }
2121
+ var activeSubmissions = (0, import_advanced2.createSignal)(/* @__PURE__ */ new Map());
2122
+ function useSubmission(actionOrUrl) {
2123
+ const url = typeof actionOrUrl === "string" ? actionOrUrl : actionOrUrl.url;
2124
+ return () => {
2125
+ const submissions = activeSubmissions();
2126
+ return submissions.get(url);
2127
+ };
2128
+ }
2129
+ function useSubmissions() {
2130
+ return () => Array.from(activeSubmissions().values());
2131
+ }
2132
+ async function submitAction(action2, formData, params = {}) {
2133
+ const key = `submission-${++submissionCounter}`;
2134
+ const submission = {
2135
+ key,
2136
+ formData,
2137
+ state: "submitting",
2138
+ clear: () => {
2139
+ const submissions2 = new Map(activeSubmissions());
2140
+ submissions2.delete(action2.url);
2141
+ activeSubmissions(submissions2);
2142
+ },
2143
+ retry: () => {
2144
+ submitAction(action2, formData, params);
2145
+ }
2146
+ };
2147
+ const submissions = new Map(activeSubmissions());
2148
+ submissions.set(action2.url, submission);
2149
+ activeSubmissions(submissions);
2150
+ try {
2151
+ const result = await action2.submit(formData);
2152
+ submission.result = result;
2153
+ submission.state = "idle";
2154
+ const updatedSubmissions = new Map(activeSubmissions());
2155
+ updatedSubmissions.set(action2.url, submission);
2156
+ activeSubmissions(updatedSubmissions);
2157
+ return result;
2158
+ } catch (error) {
2159
+ submission.error = error;
2160
+ submission.state = "idle";
2161
+ const updatedSubmissions = new Map(activeSubmissions());
2162
+ updatedSubmissions.set(action2.url, submission);
2163
+ activeSubmissions(updatedSubmissions);
2164
+ throw error;
2165
+ }
2166
+ }
2167
+ function preloadQuery(queryFn, ...args) {
2168
+ queryFn(...args);
2169
+ }
2170
+ function createPreload(fn) {
2171
+ return async (args) => {
2172
+ return fn(args);
2173
+ };
2174
+ }
2175
+ function createResource(source, fetcher) {
2176
+ const dataSignal = (0, import_advanced2.createSignal)(void 0);
2177
+ const loadingSignal = (0, import_advanced2.createSignal)(true);
2178
+ const errorSignal = (0, import_advanced2.createSignal)(void 0);
2179
+ const latestSignal = (0, import_advanced2.createSignal)(void 0);
2180
+ let currentSource;
2181
+ let fetchId = 0;
2182
+ const doFetch = async (s, id) => {
2183
+ loadingSignal(true);
2184
+ errorSignal(void 0);
2185
+ try {
2186
+ const result = await fetcher(s);
2187
+ if (id === fetchId) {
2188
+ (0, import_runtime4.batch)(() => {
2189
+ dataSignal(result);
2190
+ latestSignal(result);
2191
+ loadingSignal(false);
2192
+ });
2193
+ return result;
2194
+ }
2195
+ return void 0;
2196
+ } catch (err) {
2197
+ if (id === fetchId) {
2198
+ (0, import_runtime4.batch)(() => {
2199
+ errorSignal(err);
2200
+ loadingSignal(false);
2201
+ });
2202
+ }
2203
+ return void 0;
2204
+ }
2205
+ };
2206
+ (0, import_runtime4.createEffect)(() => {
2207
+ const s = source();
2208
+ if (s !== currentSource) {
2209
+ currentSource = s;
2210
+ const currentFetchId = ++fetchId;
2211
+ doFetch(s, currentFetchId);
2212
+ }
2213
+ });
2214
+ const resource = (() => dataSignal());
2215
+ resource.loading = () => loadingSignal();
2216
+ resource.error = () => errorSignal();
2217
+ resource.latest = () => latestSignal();
2218
+ resource.refetch = () => {
2219
+ const currentFetchId = ++fetchId;
2220
+ return doFetch(currentSource, currentFetchId);
2221
+ };
2222
+ return resource;
2223
+ }
2224
+ function cleanupDataUtilities() {
2225
+ stopCacheCleanup();
2226
+ queryCache.clear();
2227
+ actionRegistry.clear();
2228
+ activeSubmissions(/* @__PURE__ */ new Map());
2229
+ }
2230
+
2231
+ // src/lazy.tsx
2232
+ var import_runtime5 = require("@fictjs/runtime");
2233
+ var import_advanced3 = require("@fictjs/runtime/advanced");
2234
+ var import_jsx_runtime3 = require("fict/jsx-runtime");
2235
+ function lazy(loader) {
2236
+ let cachedComponent = null;
2237
+ let loadPromise = null;
2238
+ const LazyComponent = (props) => {
2239
+ const state = (0, import_advanced3.createSignal)({
2240
+ component: cachedComponent,
2241
+ error: null,
2242
+ loading: !cachedComponent
2243
+ });
2244
+ if (cachedComponent) {
2245
+ const CachedComponent = cachedComponent;
2246
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CachedComponent, { ...props });
2247
+ }
2248
+ if (!loadPromise) {
2249
+ loadPromise = loader().then((module2) => {
2250
+ const component = "default" in module2 ? module2.default : module2;
2251
+ cachedComponent = component;
2252
+ return component;
2253
+ });
2254
+ }
2255
+ loadPromise.then((component) => {
2256
+ state({ component, error: null, loading: false });
2257
+ }).catch((error) => {
2258
+ state({ component: null, error, loading: false });
2259
+ });
2260
+ const currentState = state();
2261
+ if (currentState.error) {
2262
+ throw currentState.error;
2263
+ }
2264
+ if (currentState.loading || !currentState.component) {
2265
+ throw loadPromise;
2266
+ }
2267
+ const LoadedComponent = currentState.component;
2268
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoadedComponent, { ...props });
2269
+ };
2270
+ LazyComponent.__lazy = true;
2271
+ LazyComponent.__preload = () => {
2272
+ if (!loadPromise) {
2273
+ loadPromise = loader().then((module2) => {
2274
+ const component = "default" in module2 ? module2.default : module2;
2275
+ cachedComponent = component;
2276
+ return component;
2277
+ });
2278
+ }
2279
+ return loadPromise;
2280
+ };
2281
+ return LazyComponent;
2282
+ }
2283
+ function preloadLazy(component) {
2284
+ const lazyComp = component;
2285
+ if (lazyComp.__lazy && lazyComp.__preload) {
2286
+ return lazyComp.__preload();
2287
+ }
2288
+ return Promise.resolve();
2289
+ }
2290
+ function isLazyComponent(component) {
2291
+ return !!(component && typeof component === "function" && component.__lazy);
2292
+ }
2293
+ function lazyRoute(config) {
2294
+ const LazyComponent = lazy(config.component);
2295
+ const routeDef = {
2296
+ component: LazyComponent
2297
+ };
2298
+ if (config.path !== void 0) routeDef.path = config.path;
2299
+ if (config.loadingElement !== void 0) routeDef.loadingElement = config.loadingElement;
2300
+ if (config.errorElement !== void 0) routeDef.errorElement = config.errorElement;
2301
+ if (config.preload !== void 0) routeDef.preload = config.preload;
2302
+ if (config.children !== void 0) routeDef.children = config.children;
2303
+ if (config.index !== void 0) routeDef.index = config.index;
2304
+ if (config.key !== void 0) routeDef.key = config.key;
2305
+ return routeDef;
2306
+ }
2307
+ function createLazyRoutes(modules, options = {}) {
2308
+ const routes = [];
2309
+ for (const [filePath, loader] of Object.entries(modules)) {
2310
+ const path = options.pathTransform ? options.pathTransform(filePath) : filePath.replace(/^\.\/pages/, "").replace(/\.(tsx?|jsx?)$/, "").toLowerCase();
2311
+ routes.push(
2312
+ lazyRoute({
2313
+ path,
2314
+ component: loader,
2315
+ loadingElement: options.loadingElement,
2316
+ errorElement: options.errorElement
2317
+ })
2318
+ );
2319
+ }
2320
+ return routes;
2321
+ }
28
2322
  // Annotate the CommonJS export names for ESM import in node:
29
2323
  0 && (module.exports = {
30
- Router
2324
+ Form,
2325
+ HashRouter,
2326
+ Link,
2327
+ MemoryRouter,
2328
+ NavLink,
2329
+ Navigate,
2330
+ Outlet,
2331
+ Redirect,
2332
+ Route,
2333
+ Router,
2334
+ Routes,
2335
+ StaticRouter,
2336
+ action,
2337
+ cleanupDataUtilities,
2338
+ clearAllScrollPositions,
2339
+ clearScrollPosition,
2340
+ compileRoute,
2341
+ configureScrollRestoration,
2342
+ createBranches,
2343
+ createBrowserHistory,
2344
+ createHashHistory,
2345
+ createLazyRoutes,
2346
+ createLocation,
2347
+ createMemoryHistory,
2348
+ createPreload,
2349
+ createResource,
2350
+ createRouter,
2351
+ createRoutes,
2352
+ createScrollRestoration,
2353
+ createStaticHistory,
2354
+ createURL,
2355
+ getAction,
2356
+ getScrollRestoration,
2357
+ isBrowser,
2358
+ isLazyComponent,
2359
+ isServer,
2360
+ joinPaths,
2361
+ lazy,
2362
+ lazyRoute,
2363
+ locationsAreEqual,
2364
+ matchRoutes,
2365
+ normalizePath,
2366
+ parseSearchParams,
2367
+ parseURL,
2368
+ preloadLazy,
2369
+ preloadQuery,
2370
+ prependBasePath,
2371
+ query,
2372
+ resolvePath,
2373
+ restoreScrollPosition,
2374
+ revalidate,
2375
+ saveScrollPosition,
2376
+ scoreRoute,
2377
+ scrollToHash,
2378
+ scrollToTop,
2379
+ stringifySearchParams,
2380
+ stripBasePath,
2381
+ submitAction,
2382
+ useBeforeLeave,
2383
+ useHref,
2384
+ useIsActive,
2385
+ useIsRouting,
2386
+ useLocation,
2387
+ useMatch,
2388
+ useMatches,
2389
+ useNavigate,
2390
+ useParams,
2391
+ usePendingLocation,
2392
+ useResolvedPath,
2393
+ useRoute,
2394
+ useRouteData,
2395
+ useRouteError,
2396
+ useRouter,
2397
+ useSearchParams,
2398
+ useSubmission,
2399
+ useSubmissions
31
2400
  });
2401
+ //# sourceMappingURL=index.cjs.map