@ereo/client 0.1.6

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.js ADDED
@@ -0,0 +1,2081 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/hydration.ts
10
+ function parseHydrationDirective(props) {
11
+ if (props["client:load"]) {
12
+ return { strategy: "load" };
13
+ }
14
+ if (props["client:idle"]) {
15
+ return { strategy: "idle" };
16
+ }
17
+ if (props["client:visible"]) {
18
+ return { strategy: "visible" };
19
+ }
20
+ if (props["client:media"]) {
21
+ return { strategy: "media", media: props["client:media"] };
22
+ }
23
+ if (props["client:only"]) {
24
+ return { strategy: "load" };
25
+ }
26
+ return { strategy: "none" };
27
+ }
28
+ function shouldHydrate(strategy, media) {
29
+ switch (strategy) {
30
+ case "load":
31
+ return true;
32
+ case "idle":
33
+ return false;
34
+ case "visible":
35
+ return false;
36
+ case "media":
37
+ if (!media)
38
+ return false;
39
+ return () => window.matchMedia(media).matches;
40
+ case "none":
41
+ default:
42
+ return false;
43
+ }
44
+ }
45
+ function createHydrationTrigger(strategy, element, onHydrate, media) {
46
+ let cleanup = null;
47
+ switch (strategy) {
48
+ case "load":
49
+ onHydrate();
50
+ break;
51
+ case "idle":
52
+ if ("requestIdleCallback" in window) {
53
+ const id = requestIdleCallback(onHydrate);
54
+ cleanup = () => cancelIdleCallback(id);
55
+ } else {
56
+ const id = setTimeout(onHydrate, 200);
57
+ cleanup = () => clearTimeout(id);
58
+ }
59
+ break;
60
+ case "visible":
61
+ const observer = new IntersectionObserver((entries) => {
62
+ for (const entry of entries) {
63
+ if (entry.isIntersecting) {
64
+ observer.disconnect();
65
+ onHydrate();
66
+ break;
67
+ }
68
+ }
69
+ }, { rootMargin: "200px" });
70
+ observer.observe(element);
71
+ cleanup = () => observer.disconnect();
72
+ break;
73
+ case "media":
74
+ if (!media)
75
+ break;
76
+ const mql = window.matchMedia(media);
77
+ const handler = (e) => {
78
+ if (e.matches) {
79
+ mql.removeEventListener("change", handler);
80
+ onHydrate();
81
+ }
82
+ };
83
+ if (mql.matches) {
84
+ onHydrate();
85
+ } else {
86
+ mql.addEventListener("change", handler);
87
+ cleanup = () => mql.removeEventListener("change", handler);
88
+ }
89
+ break;
90
+ case "none":
91
+ default:
92
+ break;
93
+ }
94
+ return () => {
95
+ if (cleanup)
96
+ cleanup();
97
+ };
98
+ }
99
+ function stripHydrationProps(props) {
100
+ const {
101
+ "client:load": _load,
102
+ "client:idle": _idle,
103
+ "client:visible": _visible,
104
+ "client:media": _media,
105
+ "client:only": _only,
106
+ ...rest
107
+ } = props;
108
+ return rest;
109
+ }
110
+ var islandCounter = 0;
111
+ function generateIslandId() {
112
+ return `island-${++islandCounter}`;
113
+ }
114
+ function resetIslandCounter() {
115
+ islandCounter = 0;
116
+ }
117
+ function getIslandCount() {
118
+ return islandCounter;
119
+ }
120
+
121
+ // src/islands.ts
122
+ class IslandRegistry {
123
+ islands = new Map;
124
+ cleanups = new Map;
125
+ register(id, component, props, strategy, element, media) {
126
+ this.islands.set(id, {
127
+ id,
128
+ component,
129
+ props,
130
+ strategy,
131
+ media,
132
+ element,
133
+ hydrated: false
134
+ });
135
+ }
136
+ get(id) {
137
+ return this.islands.get(id);
138
+ }
139
+ markHydrated(id) {
140
+ const island = this.islands.get(id);
141
+ if (island) {
142
+ island.hydrated = true;
143
+ }
144
+ }
145
+ isHydrated(id) {
146
+ return this.islands.get(id)?.hydrated ?? false;
147
+ }
148
+ setCleanup(id, cleanup) {
149
+ this.cleanups.set(id, cleanup);
150
+ }
151
+ cleanup(id) {
152
+ const cleanupFn = this.cleanups.get(id);
153
+ if (cleanupFn) {
154
+ cleanupFn();
155
+ this.cleanups.delete(id);
156
+ }
157
+ this.islands.delete(id);
158
+ }
159
+ cleanupAll() {
160
+ for (const id of this.islands.keys()) {
161
+ this.cleanup(id);
162
+ }
163
+ }
164
+ getAll() {
165
+ return Array.from(this.islands.values());
166
+ }
167
+ getByStrategy(strategy) {
168
+ return this.getAll().filter((i) => i.strategy === strategy);
169
+ }
170
+ getPending() {
171
+ return this.getAll().filter((i) => !i.hydrated);
172
+ }
173
+ }
174
+ var islandRegistry = new IslandRegistry;
175
+ async function hydrateIslands() {
176
+ const { hydrateRoot } = await import("react-dom/client");
177
+ const { createElement } = await import("react");
178
+ const islandElements = document.querySelectorAll("[data-island]");
179
+ for (const element of islandElements) {
180
+ const islandId = element.getAttribute("data-island");
181
+ const componentName = element.getAttribute("data-component");
182
+ const propsJson = element.getAttribute("data-props");
183
+ const strategy = element.getAttribute("data-strategy") || "load";
184
+ const media = element.getAttribute("data-media") || undefined;
185
+ if (!islandId || !componentName)
186
+ continue;
187
+ const component = getIslandComponent(componentName);
188
+ if (!component) {
189
+ console.warn(`Island component not found: ${componentName}`);
190
+ continue;
191
+ }
192
+ const props = propsJson ? JSON.parse(propsJson) : {};
193
+ islandRegistry.register(islandId, component, props, strategy, element, media);
194
+ const cleanup = createHydrationTrigger(strategy, element, () => {
195
+ if (islandRegistry.isHydrated(islandId))
196
+ return;
197
+ const root = hydrateRoot(element, createElement(component, props));
198
+ islandRegistry.markHydrated(islandId);
199
+ islandRegistry.setCleanup(islandId, () => root.unmount());
200
+ }, media);
201
+ islandRegistry.setCleanup(islandId, cleanup);
202
+ }
203
+ }
204
+ var componentRegistry = new Map;
205
+ function registerIslandComponent(name, component) {
206
+ componentRegistry.set(name, component);
207
+ }
208
+ function getIslandComponent(name) {
209
+ return componentRegistry.get(name);
210
+ }
211
+ function registerIslandComponents(components) {
212
+ for (const [name, component] of Object.entries(components)) {
213
+ registerIslandComponent(name, component);
214
+ }
215
+ }
216
+ function createIsland(component, name) {
217
+ registerIslandComponent(name, component);
218
+ return function IslandWrapper(props) {
219
+ const { strategy, media } = parseHydrationDirective(props);
220
+ const cleanProps = stripHydrationProps(props);
221
+ const islandId = generateIslandId();
222
+ return {
223
+ type: "div",
224
+ props: {
225
+ "data-island": islandId,
226
+ "data-component": name,
227
+ "data-props": JSON.stringify(cleanProps),
228
+ "data-strategy": strategy,
229
+ "data-media": media || undefined,
230
+ children: {
231
+ type: component,
232
+ props: cleanProps
233
+ }
234
+ }
235
+ };
236
+ };
237
+ }
238
+ function initializeIslands() {
239
+ if (typeof document === "undefined")
240
+ return;
241
+ if (document.readyState === "loading") {
242
+ document.addEventListener("DOMContentLoaded", () => hydrateIslands());
243
+ } else {
244
+ hydrateIslands();
245
+ }
246
+ }
247
+ function cleanupIslands() {
248
+ islandRegistry.cleanupAll();
249
+ }
250
+
251
+ // src/navigation.ts
252
+ class ClientRouter {
253
+ listeners = new Set;
254
+ currentState;
255
+ constructor() {
256
+ this.currentState = this.getStateFromLocation();
257
+ this.setupPopState();
258
+ }
259
+ getStateFromLocation() {
260
+ if (typeof window === "undefined") {
261
+ return { pathname: "/", search: "", hash: "" };
262
+ }
263
+ return {
264
+ pathname: window.location.pathname,
265
+ search: window.location.search,
266
+ hash: window.location.hash,
267
+ state: window.history.state
268
+ };
269
+ }
270
+ setupPopState() {
271
+ if (typeof window === "undefined")
272
+ return;
273
+ window.addEventListener("popstate", (event) => {
274
+ const from = this.currentState;
275
+ this.currentState = this.getStateFromLocation();
276
+ this.notify({
277
+ type: "pop",
278
+ from,
279
+ to: this.currentState
280
+ });
281
+ });
282
+ }
283
+ async navigate(to, options = {}) {
284
+ if (typeof window === "undefined")
285
+ return;
286
+ const url = new URL(to, window.location.origin);
287
+ const from = this.currentState;
288
+ const newState = {
289
+ pathname: url.pathname,
290
+ search: url.search,
291
+ hash: url.hash,
292
+ state: options.state
293
+ };
294
+ if (options.replace) {
295
+ window.history.replaceState(options.state, "", to);
296
+ } else {
297
+ window.history.pushState(options.state, "", to);
298
+ }
299
+ this.currentState = newState;
300
+ this.notify({
301
+ type: options.replace ? "replace" : "push",
302
+ from,
303
+ to: newState
304
+ });
305
+ }
306
+ back() {
307
+ if (typeof window !== "undefined") {
308
+ window.history.back();
309
+ }
310
+ }
311
+ forward() {
312
+ if (typeof window !== "undefined") {
313
+ window.history.forward();
314
+ }
315
+ }
316
+ go(delta) {
317
+ if (typeof window !== "undefined") {
318
+ window.history.go(delta);
319
+ }
320
+ }
321
+ subscribe(listener) {
322
+ this.listeners.add(listener);
323
+ return () => this.listeners.delete(listener);
324
+ }
325
+ notify(event) {
326
+ for (const listener of this.listeners) {
327
+ listener(event);
328
+ }
329
+ }
330
+ getState() {
331
+ return this.currentState;
332
+ }
333
+ isActive(path, exact = false) {
334
+ if (exact) {
335
+ return this.currentState.pathname === path;
336
+ }
337
+ return this.currentState.pathname.startsWith(path);
338
+ }
339
+ }
340
+ var router = new ClientRouter;
341
+ function navigate(to, options) {
342
+ return router.navigate(to, options);
343
+ }
344
+ function goBack() {
345
+ router.back();
346
+ }
347
+ function goForward() {
348
+ router.forward();
349
+ }
350
+ function onNavigate(listener) {
351
+ return router.subscribe(listener);
352
+ }
353
+ function getNavigationState() {
354
+ return router.getState();
355
+ }
356
+ async function fetchLoaderData(pathname, params) {
357
+ const url = new URL(pathname, window.location.origin);
358
+ if (params) {
359
+ for (const [key, value] of Object.entries(params)) {
360
+ if (value !== undefined) {
361
+ url.searchParams.set(key, String(value));
362
+ }
363
+ }
364
+ }
365
+ const response = await fetch(url.toString(), {
366
+ headers: {
367
+ Accept: "application/json"
368
+ }
369
+ });
370
+ if (!response.ok) {
371
+ throw new Error(`Failed to fetch loader data: ${response.status}`);
372
+ }
373
+ const result = await response.json();
374
+ return result.data;
375
+ }
376
+ async function submitAction(pathname, formData, options = {}) {
377
+ const response = await fetch(pathname, {
378
+ method: options.method || "POST",
379
+ body: formData,
380
+ headers: {
381
+ Accept: "application/json"
382
+ }
383
+ });
384
+ if (!response.ok) {
385
+ throw new Error(`Action failed: ${response.status}`);
386
+ }
387
+ return response.json();
388
+ }
389
+ function setupScrollRestoration() {
390
+ if (typeof window === "undefined")
391
+ return;
392
+ if ("scrollRestoration" in history) {
393
+ history.scrollRestoration = "manual";
394
+ }
395
+ const scrollPositions = new Map;
396
+ router.subscribe((event) => {
397
+ scrollPositions.set(event.from.pathname, window.scrollY);
398
+ if (event.type === "pop") {
399
+ const savedPosition = scrollPositions.get(event.to.pathname);
400
+ if (savedPosition !== undefined) {
401
+ requestAnimationFrame(() => {
402
+ window.scrollTo(0, savedPosition);
403
+ });
404
+ }
405
+ } else {
406
+ window.scrollTo(0, 0);
407
+ }
408
+ });
409
+ }
410
+
411
+ // src/prefetch.ts
412
+ var prefetchCache = new Map;
413
+ var defaultOptions = {
414
+ strategy: "hover",
415
+ cacheDuration: 30000,
416
+ threshold: 0
417
+ };
418
+ function isCacheValid(entry, cacheDuration) {
419
+ return Date.now() - entry.timestamp < cacheDuration;
420
+ }
421
+ async function prefetch(url) {
422
+ const cached = prefetchCache.get(url);
423
+ if (cached && isCacheValid(cached, defaultOptions.cacheDuration)) {
424
+ return;
425
+ }
426
+ const entry = {
427
+ url,
428
+ timestamp: Date.now(),
429
+ loading: true
430
+ };
431
+ prefetchCache.set(url, entry);
432
+ try {
433
+ const response = await fetch(url, {
434
+ method: "GET",
435
+ headers: {
436
+ Accept: "application/json",
437
+ "X-Prefetch": "true"
438
+ },
439
+ priority: "low"
440
+ });
441
+ if (response.ok) {
442
+ entry.data = await response.json();
443
+ } else {
444
+ entry.error = new Error(`Prefetch failed: ${response.status}`);
445
+ }
446
+ } catch (error) {
447
+ entry.error = error instanceof Error ? error : new Error("Prefetch failed");
448
+ } finally {
449
+ entry.loading = false;
450
+ }
451
+ }
452
+ function getPrefetchedData(url) {
453
+ const entry = prefetchCache.get(url);
454
+ if (entry && isCacheValid(entry, defaultOptions.cacheDuration) && entry.data) {
455
+ return entry.data;
456
+ }
457
+ return;
458
+ }
459
+ function clearPrefetchCache() {
460
+ prefetchCache.clear();
461
+ }
462
+ function setupLinkPrefetch(element, options = {}) {
463
+ const { strategy, threshold } = { ...defaultOptions, ...options };
464
+ const href = element.href;
465
+ if (!href || !href.startsWith(window.location.origin)) {
466
+ return () => {};
467
+ }
468
+ const url = new URL(href).pathname;
469
+ let cleanup = null;
470
+ switch (strategy) {
471
+ case "hover": {
472
+ const onMouseEnter = () => prefetch(url);
473
+ element.addEventListener("mouseenter", onMouseEnter);
474
+ cleanup = () => element.removeEventListener("mouseenter", onMouseEnter);
475
+ break;
476
+ }
477
+ case "viewport": {
478
+ const observer = new IntersectionObserver((entries) => {
479
+ for (const entry of entries) {
480
+ if (entry.isIntersecting) {
481
+ prefetch(url);
482
+ observer.disconnect();
483
+ break;
484
+ }
485
+ }
486
+ }, { threshold });
487
+ observer.observe(element);
488
+ cleanup = () => observer.disconnect();
489
+ break;
490
+ }
491
+ case "eager": {
492
+ prefetch(url);
493
+ break;
494
+ }
495
+ case "none":
496
+ default:
497
+ break;
498
+ }
499
+ return () => {
500
+ if (cleanup)
501
+ cleanup();
502
+ };
503
+ }
504
+ function setupAutoPrefetch(options = {}) {
505
+ if (typeof document === "undefined")
506
+ return () => {};
507
+ const cleanups = [];
508
+ const links = document.querySelectorAll('a[href^="/"]');
509
+ links.forEach((link) => {
510
+ cleanups.push(setupLinkPrefetch(link, options));
511
+ });
512
+ const observer = new MutationObserver((mutations) => {
513
+ for (const mutation of mutations) {
514
+ for (const node of mutation.addedNodes) {
515
+ if (node instanceof HTMLAnchorElement && node.href.startsWith("/")) {
516
+ cleanups.push(setupLinkPrefetch(node, options));
517
+ }
518
+ if (node instanceof Element) {
519
+ const newLinks = node.querySelectorAll('a[href^="/"]');
520
+ newLinks.forEach((link) => {
521
+ cleanups.push(setupLinkPrefetch(link, options));
522
+ });
523
+ }
524
+ }
525
+ }
526
+ });
527
+ observer.observe(document.body, { childList: true, subtree: true });
528
+ return () => {
529
+ observer.disconnect();
530
+ cleanups.forEach((cleanup) => cleanup());
531
+ };
532
+ }
533
+ async function prefetchAll(urls) {
534
+ await Promise.all(urls.map(prefetch));
535
+ }
536
+ function isPrefetching(url) {
537
+ const entry = prefetchCache.get(url);
538
+ return entry?.loading ?? false;
539
+ }
540
+ function isPrefetched(url) {
541
+ const entry = prefetchCache.get(url);
542
+ return entry?.data !== undefined;
543
+ }
544
+ // src/hooks.ts
545
+ import {
546
+ createContext,
547
+ useContext,
548
+ useState,
549
+ useCallback,
550
+ useMemo,
551
+ createElement
552
+ } from "react";
553
+ var LoaderDataContext = createContext(null);
554
+ var ActionDataContext = createContext(null);
555
+ var NavigationContext = createContext(null);
556
+ var ErrorContext = createContext(null);
557
+ function useLoaderData() {
558
+ const context = useContext(LoaderDataContext);
559
+ if (context === null) {
560
+ throw new Error("useLoaderData must be used within an EreoProvider. " + "Make sure your component is wrapped with <EreoProvider>.");
561
+ }
562
+ return context.data;
563
+ }
564
+ function useActionData() {
565
+ const context = useContext(ActionDataContext);
566
+ if (context === null) {
567
+ throw new Error("useActionData must be used within an EreoProvider. " + "Make sure your component is wrapped with <EreoProvider>.");
568
+ }
569
+ return context.data;
570
+ }
571
+ function useNavigation() {
572
+ const context = useContext(NavigationContext);
573
+ if (context === null) {
574
+ throw new Error("useNavigation must be used within an EreoProvider. " + "Make sure your component is wrapped with <EreoProvider>.");
575
+ }
576
+ return context.state;
577
+ }
578
+ function useError() {
579
+ const context = useContext(ErrorContext);
580
+ if (context === null) {
581
+ throw new Error("useError must be used within an EreoProvider. " + "Make sure your component is wrapped with <EreoProvider>.");
582
+ }
583
+ return context.error;
584
+ }
585
+ function LoaderDataProvider({
586
+ children,
587
+ initialData
588
+ }) {
589
+ const [data, setData] = useState(initialData);
590
+ const value = useMemo(() => ({ data, setData }), [data]);
591
+ return createElement(LoaderDataContext.Provider, { value }, children);
592
+ }
593
+ function ActionDataProvider({
594
+ children,
595
+ initialData
596
+ }) {
597
+ const [data, setData] = useState(initialData);
598
+ const clearData = useCallback(() => {
599
+ setData(undefined);
600
+ }, []);
601
+ const value = useMemo(() => ({ data, setData, clearData }), [data, clearData]);
602
+ return createElement(ActionDataContext.Provider, { value }, children);
603
+ }
604
+ var defaultNavigationState = {
605
+ status: "idle"
606
+ };
607
+ function NavigationProvider({
608
+ children,
609
+ initialState = defaultNavigationState
610
+ }) {
611
+ const [state, setState] = useState(initialState);
612
+ const startLoading = useCallback((location) => {
613
+ setState({
614
+ status: "loading",
615
+ location
616
+ });
617
+ }, []);
618
+ const startSubmitting = useCallback((options) => {
619
+ setState({
620
+ status: "submitting",
621
+ location: options.location,
622
+ formData: options.formData,
623
+ formMethod: options.formMethod,
624
+ formAction: options.formAction
625
+ });
626
+ }, []);
627
+ const complete = useCallback(() => {
628
+ setState({ status: "idle" });
629
+ }, []);
630
+ const value = useMemo(() => ({
631
+ state,
632
+ setState,
633
+ startLoading,
634
+ startSubmitting,
635
+ complete
636
+ }), [state, startLoading, startSubmitting, complete]);
637
+ return createElement(NavigationContext.Provider, { value }, children);
638
+ }
639
+ function ErrorProvider({
640
+ children,
641
+ initialError
642
+ }) {
643
+ const [error, setError] = useState(initialError);
644
+ const clearError = useCallback(() => {
645
+ setError(undefined);
646
+ }, []);
647
+ const value = useMemo(() => ({ error, setError, clearError }), [error, clearError]);
648
+ return createElement(ErrorContext.Provider, { value }, children);
649
+ }
650
+ function EreoProvider({
651
+ children,
652
+ loaderData,
653
+ actionData,
654
+ navigationState,
655
+ error
656
+ }) {
657
+ return createElement(ErrorProvider, { initialError: error, children: createElement(NavigationProvider, { initialState: navigationState, children: createElement(ActionDataProvider, { initialData: actionData, children: createElement(LoaderDataProvider, { initialData: loaderData, children }) }) }) });
658
+ }
659
+ function useLoaderDataContext() {
660
+ const context = useContext(LoaderDataContext);
661
+ if (context === null) {
662
+ throw new Error("useLoaderDataContext must be used within an EreoProvider");
663
+ }
664
+ return context;
665
+ }
666
+ function useActionDataContext() {
667
+ const context = useContext(ActionDataContext);
668
+ if (context === null) {
669
+ throw new Error("useActionDataContext must be used within an EreoProvider");
670
+ }
671
+ return context;
672
+ }
673
+ function useNavigationContext() {
674
+ const context = useContext(NavigationContext);
675
+ if (context === null) {
676
+ throw new Error("useNavigationContext must be used within an EreoProvider");
677
+ }
678
+ return context;
679
+ }
680
+ function useErrorContext() {
681
+ const context = useContext(ErrorContext);
682
+ if (context === null) {
683
+ throw new Error("useErrorContext must be used within an EreoProvider");
684
+ }
685
+ return context;
686
+ }
687
+ // src/link.tsx
688
+ import * as React from "react";
689
+ import { jsxDEV } from "react/jsx-dev-runtime";
690
+ function isExternalUrl(url) {
691
+ if (typeof window === "undefined")
692
+ return false;
693
+ if (url.startsWith("/") && !url.startsWith("//") || url.startsWith(".")) {
694
+ return false;
695
+ }
696
+ try {
697
+ const parsed = new URL(url, window.location.origin);
698
+ return parsed.origin !== window.location.origin;
699
+ } catch {
700
+ return false;
701
+ }
702
+ }
703
+ function shouldNavigate(event) {
704
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
705
+ return false;
706
+ }
707
+ if (event.button !== 0) {
708
+ return false;
709
+ }
710
+ if (event.defaultPrevented) {
711
+ return false;
712
+ }
713
+ return true;
714
+ }
715
+ var Link = React.forwardRef(function Link2({
716
+ to,
717
+ prefetch: prefetchStrategy = "intent",
718
+ replace = false,
719
+ preventScrollReset = false,
720
+ state,
721
+ reloadDocument = false,
722
+ onClick,
723
+ onMouseEnter,
724
+ onFocus,
725
+ children,
726
+ ...rest
727
+ }, ref) {
728
+ const internalRef = React.useRef(null);
729
+ const resolvedRef = ref || internalRef;
730
+ const hasPrefetched = React.useRef(false);
731
+ const isExternal = isExternalUrl(to);
732
+ const triggerPrefetch = React.useCallback(() => {
733
+ if (hasPrefetched.current || isExternal || prefetchStrategy === "none") {
734
+ return;
735
+ }
736
+ hasPrefetched.current = true;
737
+ prefetch(to);
738
+ }, [to, isExternal, prefetchStrategy]);
739
+ const handleClick = React.useCallback((event) => {
740
+ onClick?.(event);
741
+ if (isExternal || reloadDocument) {
742
+ return;
743
+ }
744
+ if (!shouldNavigate(event)) {
745
+ return;
746
+ }
747
+ const target = event.currentTarget.getAttribute("target");
748
+ if (target && target !== "_self") {
749
+ return;
750
+ }
751
+ event.preventDefault();
752
+ navigate(to, { replace, state });
753
+ if (!preventScrollReset && typeof window !== "undefined") {
754
+ window.scrollTo(0, 0);
755
+ }
756
+ }, [onClick, to, replace, state, isExternal, reloadDocument, preventScrollReset]);
757
+ const handleMouseEnter = React.useCallback((event) => {
758
+ onMouseEnter?.(event);
759
+ if (prefetchStrategy === "intent") {
760
+ triggerPrefetch();
761
+ }
762
+ }, [onMouseEnter, prefetchStrategy, triggerPrefetch]);
763
+ const handleFocus = React.useCallback((event) => {
764
+ onFocus?.(event);
765
+ if (prefetchStrategy === "intent") {
766
+ triggerPrefetch();
767
+ }
768
+ }, [onFocus, prefetchStrategy, triggerPrefetch]);
769
+ React.useEffect(() => {
770
+ if (prefetchStrategy === "render") {
771
+ triggerPrefetch();
772
+ }
773
+ }, [prefetchStrategy, triggerPrefetch]);
774
+ React.useEffect(() => {
775
+ if (prefetchStrategy !== "viewport" || typeof IntersectionObserver === "undefined") {
776
+ return;
777
+ }
778
+ const element = resolvedRef.current;
779
+ if (!element)
780
+ return;
781
+ const observer = new IntersectionObserver((entries) => {
782
+ for (const entry of entries) {
783
+ if (entry.isIntersecting) {
784
+ triggerPrefetch();
785
+ observer.disconnect();
786
+ break;
787
+ }
788
+ }
789
+ }, { threshold: 0 });
790
+ observer.observe(element);
791
+ return () => {
792
+ observer.disconnect();
793
+ };
794
+ }, [prefetchStrategy, triggerPrefetch, resolvedRef]);
795
+ return /* @__PURE__ */ jsxDEV("a", {
796
+ ...rest,
797
+ ref: resolvedRef,
798
+ href: to,
799
+ onClick: handleClick,
800
+ onMouseEnter: handleMouseEnter,
801
+ onFocus: handleFocus,
802
+ children
803
+ }, undefined, false, undefined, this);
804
+ });
805
+ var NavLink = React.forwardRef(function NavLink2({ className, style, end = false, to, ...rest }, ref) {
806
+ const [navigationState, setNavigationState] = React.useState(() => {
807
+ if (typeof window !== "undefined") {
808
+ return router.getState();
809
+ }
810
+ return { pathname: "/", search: "", hash: "" };
811
+ });
812
+ React.useEffect(() => {
813
+ const unsubscribe = onNavigate((event) => {
814
+ setNavigationState(event.to);
815
+ });
816
+ return unsubscribe;
817
+ }, []);
818
+ const isActive = React.useMemo(() => {
819
+ const toPath = to.split("?")[0].split("#")[0];
820
+ if (end) {
821
+ return navigationState.pathname === toPath;
822
+ }
823
+ return navigationState.pathname.startsWith(toPath) && (toPath === "/" ? navigationState.pathname === "/" : true);
824
+ }, [to, end, navigationState.pathname]);
825
+ const isPending = false;
826
+ const activeProps = { isActive, isPending };
827
+ const resolvedClassName = typeof className === "function" ? className(activeProps) : className;
828
+ const resolvedStyle = typeof style === "function" ? style(activeProps) : style;
829
+ return /* @__PURE__ */ jsxDEV(Link, {
830
+ ...rest,
831
+ ref,
832
+ to,
833
+ className: resolvedClassName,
834
+ style: resolvedStyle,
835
+ "aria-current": isActive ? "page" : undefined
836
+ }, undefined, false, undefined, this);
837
+ });
838
+ function useIsActive(path, end = false) {
839
+ const [pathname, setPathname] = React.useState(() => {
840
+ if (typeof window !== "undefined") {
841
+ return window.location.pathname;
842
+ }
843
+ return "/";
844
+ });
845
+ React.useEffect(() => {
846
+ const unsubscribe = onNavigate((event) => {
847
+ setPathname(event.to.pathname);
848
+ });
849
+ return unsubscribe;
850
+ }, []);
851
+ const toPath = path.split("?")[0].split("#")[0];
852
+ if (end) {
853
+ return pathname === toPath;
854
+ }
855
+ return pathname.startsWith(toPath) && (toPath === "/" ? pathname === "/" : true);
856
+ }
857
+ // src/typed-link.tsx
858
+ import * as React2 from "react";
859
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
860
+ function buildPathWithParams(pattern, params) {
861
+ if (!params)
862
+ return pattern;
863
+ let result = pattern;
864
+ for (const [key, value] of Object.entries(params)) {
865
+ if (value === undefined)
866
+ continue;
867
+ if (result.includes(`[...${key}]`)) {
868
+ const arrayValue = Array.isArray(value) ? value : [value];
869
+ result = result.replace(`[...${key}]`, arrayValue.join("/"));
870
+ continue;
871
+ }
872
+ if (result.includes(`[[${key}]]`)) {
873
+ result = result.replace(`[[${key}]]`, Array.isArray(value) ? value[0] : value);
874
+ continue;
875
+ }
876
+ if (result.includes(`[${key}]`)) {
877
+ result = result.replace(`[${key}]`, Array.isArray(value) ? value[0] : value);
878
+ }
879
+ }
880
+ result = result.replace(/\/?\[\[[^\]]+\]\]/g, "");
881
+ return result;
882
+ }
883
+ function buildSearchString(search) {
884
+ if (!search)
885
+ return "";
886
+ const params = new URLSearchParams;
887
+ for (const [key, value] of Object.entries(search)) {
888
+ if (value === undefined || value === null)
889
+ continue;
890
+ if (Array.isArray(value)) {
891
+ for (const v of value) {
892
+ params.append(key, String(v));
893
+ }
894
+ } else {
895
+ params.set(key, String(value));
896
+ }
897
+ }
898
+ const queryString = params.toString();
899
+ return queryString ? `?${queryString}` : "";
900
+ }
901
+ function buildHashString(hash) {
902
+ if (!hash)
903
+ return "";
904
+ const params = new URLSearchParams;
905
+ for (const [key, value] of Object.entries(hash)) {
906
+ if (value === undefined || value === null)
907
+ continue;
908
+ params.set(key, String(value));
909
+ }
910
+ const hashString = params.toString();
911
+ return hashString ? `#${hashString}` : "";
912
+ }
913
+ function buildUrl(pattern, options = {}) {
914
+ const { params, search, hash } = options;
915
+ const path = buildPathWithParams(pattern, params);
916
+ const searchString = buildSearchString(search);
917
+ const hashString = buildHashString(hash);
918
+ return `${path}${searchString}${hashString}`;
919
+ }
920
+ function isExternalUrl2(url) {
921
+ if (typeof window === "undefined")
922
+ return false;
923
+ if (url.startsWith("/") && !url.startsWith("//") || url.startsWith(".")) {
924
+ return false;
925
+ }
926
+ try {
927
+ const parsed = new URL(url, window.location.origin);
928
+ return parsed.origin !== window.location.origin;
929
+ } catch {
930
+ return false;
931
+ }
932
+ }
933
+ function shouldNavigate2(event) {
934
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
935
+ return false;
936
+ }
937
+ if (event.button !== 0) {
938
+ return false;
939
+ }
940
+ if (event.defaultPrevented) {
941
+ return false;
942
+ }
943
+ return true;
944
+ }
945
+ var TypedLink = React2.forwardRef(function TypedLink2(props, ref) {
946
+ const {
947
+ to,
948
+ params,
949
+ search,
950
+ hash,
951
+ prefetch: prefetchStrategy = "intent",
952
+ replace = false,
953
+ preventScrollReset = false,
954
+ state,
955
+ reloadDocument = false,
956
+ onClick,
957
+ onMouseEnter,
958
+ onFocus,
959
+ children,
960
+ ...rest
961
+ } = props;
962
+ const internalRef = React2.useRef(null);
963
+ const resolvedRef = ref || internalRef;
964
+ const hasPrefetched = React2.useRef(false);
965
+ const href = React2.useMemo(() => {
966
+ return buildUrl(to, {
967
+ params,
968
+ search,
969
+ hash
970
+ });
971
+ }, [to, params, search, hash]);
972
+ const isExternal = isExternalUrl2(href);
973
+ const triggerPrefetch = React2.useCallback(() => {
974
+ if (hasPrefetched.current || isExternal || prefetchStrategy === "none") {
975
+ return;
976
+ }
977
+ hasPrefetched.current = true;
978
+ prefetch(href);
979
+ }, [href, isExternal, prefetchStrategy]);
980
+ const handleClick = React2.useCallback((event) => {
981
+ onClick?.(event);
982
+ if (isExternal || reloadDocument) {
983
+ return;
984
+ }
985
+ if (!shouldNavigate2(event)) {
986
+ return;
987
+ }
988
+ const target = event.currentTarget.getAttribute("target");
989
+ if (target && target !== "_self") {
990
+ return;
991
+ }
992
+ event.preventDefault();
993
+ navigate(href, { replace, state });
994
+ if (!preventScrollReset && typeof window !== "undefined") {
995
+ window.scrollTo(0, 0);
996
+ }
997
+ }, [onClick, href, replace, state, isExternal, reloadDocument, preventScrollReset]);
998
+ const handleMouseEnter = React2.useCallback((event) => {
999
+ onMouseEnter?.(event);
1000
+ if (prefetchStrategy === "intent") {
1001
+ triggerPrefetch();
1002
+ }
1003
+ }, [onMouseEnter, prefetchStrategy, triggerPrefetch]);
1004
+ const handleFocus = React2.useCallback((event) => {
1005
+ onFocus?.(event);
1006
+ if (prefetchStrategy === "intent") {
1007
+ triggerPrefetch();
1008
+ }
1009
+ }, [onFocus, prefetchStrategy, triggerPrefetch]);
1010
+ React2.useEffect(() => {
1011
+ if (prefetchStrategy === "render") {
1012
+ triggerPrefetch();
1013
+ }
1014
+ }, [prefetchStrategy, triggerPrefetch]);
1015
+ React2.useEffect(() => {
1016
+ if (prefetchStrategy !== "viewport" || typeof IntersectionObserver === "undefined") {
1017
+ return;
1018
+ }
1019
+ const element = resolvedRef.current;
1020
+ if (!element)
1021
+ return;
1022
+ const observer = new IntersectionObserver((entries) => {
1023
+ for (const entry of entries) {
1024
+ if (entry.isIntersecting) {
1025
+ triggerPrefetch();
1026
+ observer.disconnect();
1027
+ break;
1028
+ }
1029
+ }
1030
+ }, { threshold: 0 });
1031
+ observer.observe(element);
1032
+ return () => {
1033
+ observer.disconnect();
1034
+ };
1035
+ }, [prefetchStrategy, triggerPrefetch, resolvedRef]);
1036
+ return /* @__PURE__ */ jsxDEV2("a", {
1037
+ ...rest,
1038
+ ref: resolvedRef,
1039
+ href,
1040
+ onClick: handleClick,
1041
+ onMouseEnter: handleMouseEnter,
1042
+ onFocus: handleFocus,
1043
+ children
1044
+ }, undefined, false, undefined, this);
1045
+ });
1046
+ var TypedNavLink = React2.forwardRef(function TypedNavLink2(props, ref) {
1047
+ const {
1048
+ className,
1049
+ style,
1050
+ end = false,
1051
+ to,
1052
+ params,
1053
+ search,
1054
+ hash,
1055
+ ...rest
1056
+ } = props;
1057
+ const [navigationState, setNavigationState] = React2.useState(() => {
1058
+ if (typeof window !== "undefined") {
1059
+ return router.getState();
1060
+ }
1061
+ return { pathname: "/", search: "", hash: "" };
1062
+ });
1063
+ React2.useEffect(() => {
1064
+ const unsubscribe = onNavigate((event) => {
1065
+ setNavigationState(event.to);
1066
+ });
1067
+ return unsubscribe;
1068
+ }, []);
1069
+ const targetPath = React2.useMemo(() => {
1070
+ return buildPathWithParams(to, params).split("?")[0].split("#")[0];
1071
+ }, [to, params]);
1072
+ const isActive = React2.useMemo(() => {
1073
+ if (end) {
1074
+ return navigationState.pathname === targetPath;
1075
+ }
1076
+ return navigationState.pathname.startsWith(targetPath) && (targetPath === "/" ? navigationState.pathname === "/" : true);
1077
+ }, [targetPath, end, navigationState.pathname]);
1078
+ const isPending = false;
1079
+ const activeProps = { isActive, isPending };
1080
+ const resolvedClassName = typeof className === "function" ? className(activeProps) : className;
1081
+ const resolvedStyle = typeof style === "function" ? style(activeProps) : style;
1082
+ const href = React2.useMemo(() => {
1083
+ return buildUrl(to, {
1084
+ params,
1085
+ search,
1086
+ hash
1087
+ });
1088
+ }, [to, params, search, hash]);
1089
+ return /* @__PURE__ */ jsxDEV2("a", {
1090
+ ...rest,
1091
+ ref,
1092
+ href,
1093
+ className: resolvedClassName,
1094
+ style: resolvedStyle,
1095
+ "aria-current": isActive ? "page" : undefined
1096
+ }, undefined, false, undefined, this);
1097
+ });
1098
+ function useIsRouteActive(path, options = {}) {
1099
+ const { params, end = false } = options;
1100
+ const [pathname, setPathname] = React2.useState(() => {
1101
+ if (typeof window !== "undefined") {
1102
+ return window.location.pathname;
1103
+ }
1104
+ return "/";
1105
+ });
1106
+ React2.useEffect(() => {
1107
+ const unsubscribe = onNavigate((event) => {
1108
+ setPathname(event.to.pathname);
1109
+ });
1110
+ return unsubscribe;
1111
+ }, []);
1112
+ const targetPath = buildPathWithParams(path, params).split("?")[0].split("#")[0];
1113
+ if (end) {
1114
+ return pathname === targetPath;
1115
+ }
1116
+ return pathname.startsWith(targetPath) && (targetPath === "/" ? pathname === "/" : true);
1117
+ }
1118
+ // src/typed-navigate.ts
1119
+ function buildPathWithParams2(pattern, params) {
1120
+ if (!params)
1121
+ return pattern;
1122
+ let result = pattern;
1123
+ for (const [key, value] of Object.entries(params)) {
1124
+ if (value === undefined)
1125
+ continue;
1126
+ if (result.includes(`[...${key}]`)) {
1127
+ const arrayValue = Array.isArray(value) ? value : [value];
1128
+ result = result.replace(`[...${key}]`, arrayValue.join("/"));
1129
+ continue;
1130
+ }
1131
+ if (result.includes(`[[${key}]]`)) {
1132
+ result = result.replace(`[[${key}]]`, Array.isArray(value) ? value[0] : value);
1133
+ continue;
1134
+ }
1135
+ if (result.includes(`[${key}]`)) {
1136
+ result = result.replace(`[${key}]`, Array.isArray(value) ? value[0] : value);
1137
+ }
1138
+ }
1139
+ result = result.replace(/\/?\[\[[^\]]+\]\]/g, "");
1140
+ return result;
1141
+ }
1142
+ function buildSearchString2(search) {
1143
+ if (!search)
1144
+ return "";
1145
+ const params = new URLSearchParams;
1146
+ for (const [key, value] of Object.entries(search)) {
1147
+ if (value === undefined || value === null)
1148
+ continue;
1149
+ if (Array.isArray(value)) {
1150
+ for (const v of value) {
1151
+ params.append(key, String(v));
1152
+ }
1153
+ } else {
1154
+ params.set(key, String(value));
1155
+ }
1156
+ }
1157
+ const queryString = params.toString();
1158
+ return queryString ? `?${queryString}` : "";
1159
+ }
1160
+ function buildHashString2(hash) {
1161
+ if (!hash)
1162
+ return "";
1163
+ const params = new URLSearchParams;
1164
+ for (const [key, value] of Object.entries(hash)) {
1165
+ if (value === undefined || value === null)
1166
+ continue;
1167
+ params.set(key, String(value));
1168
+ }
1169
+ const hashString = params.toString();
1170
+ return hashString ? `#${hashString}` : "";
1171
+ }
1172
+ function buildTypedUrl(pattern, options = {}) {
1173
+ const { params, search, hash } = options;
1174
+ const path = buildPathWithParams2(pattern, params);
1175
+ const searchString = buildSearchString2(search);
1176
+ const hashString = buildHashString2(hash);
1177
+ return `${path}${searchString}${hashString}`;
1178
+ }
1179
+ async function typedNavigate(path, options) {
1180
+ const url = buildTypedUrl(path, {
1181
+ params: options?.params,
1182
+ search: options?.search,
1183
+ hash: options?.hash
1184
+ });
1185
+ await navigate(url, {
1186
+ replace: options?.replace,
1187
+ state: options?.state
1188
+ });
1189
+ if (options?.scroll !== false && typeof window !== "undefined") {
1190
+ window.scrollTo(0, 0);
1191
+ }
1192
+ }
1193
+ function useTypedNavigate() {
1194
+ const navigateFunction = (pathOrDelta, options) => {
1195
+ if (typeof pathOrDelta === "number") {
1196
+ router.go(pathOrDelta);
1197
+ return;
1198
+ }
1199
+ return typedNavigate(pathOrDelta, options);
1200
+ };
1201
+ return navigateFunction;
1202
+ }
1203
+ function typedRedirect(path, options) {
1204
+ const url = buildTypedUrl(path, {
1205
+ params: options?.params,
1206
+ search: options?.search,
1207
+ hash: options?.hash
1208
+ });
1209
+ const status = options?.status ?? 302;
1210
+ return new Response(null, {
1211
+ status,
1212
+ headers: {
1213
+ Location: url,
1214
+ ...options?.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options?.headers
1215
+ }
1216
+ });
1217
+ }
1218
+ var redirect = typedRedirect;
1219
+ function parseTypedSearchParams(url) {
1220
+ const urlObj = typeof url === "string" ? new URL(url) : url;
1221
+ const result = {};
1222
+ urlObj.searchParams.forEach((value, key) => {
1223
+ if (result[key]) {
1224
+ const existing = result[key];
1225
+ result[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
1226
+ } else {
1227
+ result[key] = value;
1228
+ }
1229
+ });
1230
+ return result;
1231
+ }
1232
+ function parseTypedHashParams(url) {
1233
+ const urlObj = typeof url === "string" ? new URL(url) : url;
1234
+ if (!urlObj.hash) {
1235
+ return {};
1236
+ }
1237
+ const hashParams = new URLSearchParams(urlObj.hash.slice(1));
1238
+ const result = {};
1239
+ hashParams.forEach((value, key) => {
1240
+ result[key] = value;
1241
+ });
1242
+ return result;
1243
+ }
1244
+ function goBack2() {
1245
+ router.back();
1246
+ }
1247
+ function goForward2() {
1248
+ router.forward();
1249
+ }
1250
+ function go(delta) {
1251
+ router.go(delta);
1252
+ }
1253
+ function isCurrentPath(path, options) {
1254
+ if (typeof window === "undefined")
1255
+ return false;
1256
+ const targetPath = buildPathWithParams2(path, options?.params).split("?")[0].split("#")[0];
1257
+ const currentPath = window.location.pathname;
1258
+ if (options?.exact) {
1259
+ return currentPath === targetPath;
1260
+ }
1261
+ return currentPath.startsWith(targetPath) && (targetPath === "/" ? currentPath === "/" : true);
1262
+ }
1263
+ async function preloadRoute(path, options) {
1264
+ const url = buildTypedUrl(path, {
1265
+ params: options?.params,
1266
+ search: options?.search
1267
+ });
1268
+ try {
1269
+ await fetch(url, {
1270
+ headers: {
1271
+ Accept: "application/json",
1272
+ "X-Ereo-Preload": "true"
1273
+ }
1274
+ });
1275
+ } catch {}
1276
+ }
1277
+ // src/form.ts
1278
+ import { createElement as createElement2, useCallback as useCallback4, useRef as useRef3, useState as useState4, useEffect as useEffect3, useContext as useContext2, createContext as createContext2 } from "react";
1279
+ var FormContext = createContext2(null);
1280
+ function FormProvider({
1281
+ children,
1282
+ initialActionData
1283
+ }) {
1284
+ const [actionData, setActionData] = useState4(initialActionData);
1285
+ const [state, setState] = useState4("idle");
1286
+ return createElement2(FormContext.Provider, { value: { actionData, state, setActionData, setState } }, children);
1287
+ }
1288
+ function useFormContext() {
1289
+ return useContext2(FormContext);
1290
+ }
1291
+ function Form({
1292
+ method = "post",
1293
+ action,
1294
+ onSubmitStart,
1295
+ onSubmitEnd,
1296
+ replace = false,
1297
+ preventScrollReset = false,
1298
+ encType = "application/x-www-form-urlencoded",
1299
+ fetcherKey,
1300
+ children,
1301
+ onSubmit,
1302
+ ...props
1303
+ }) {
1304
+ const formRef = useRef3(null);
1305
+ const formContext = useFormContext();
1306
+ const resolvedAction = action || (typeof window !== "undefined" ? window.location.pathname : "/");
1307
+ const handleSubmit = useCallback4(async (event) => {
1308
+ if (onSubmit) {
1309
+ onSubmit(event);
1310
+ if (event.defaultPrevented) {
1311
+ return;
1312
+ }
1313
+ }
1314
+ if (typeof window === "undefined") {
1315
+ return;
1316
+ }
1317
+ event.preventDefault();
1318
+ const form = event.currentTarget;
1319
+ const formData = new FormData(form);
1320
+ if (formContext) {
1321
+ formContext.setState("submitting");
1322
+ }
1323
+ if (onSubmitStart) {
1324
+ onSubmitStart();
1325
+ }
1326
+ try {
1327
+ let body;
1328
+ if (encType === "application/x-www-form-urlencoded") {
1329
+ body = new URLSearchParams;
1330
+ formData.forEach((value, key) => {
1331
+ if (typeof value === "string") {
1332
+ body.append(key, value);
1333
+ }
1334
+ });
1335
+ } else {
1336
+ body = formData;
1337
+ }
1338
+ if (method.toLowerCase() === "get") {
1339
+ const url = new URL(resolvedAction, window.location.origin);
1340
+ const params = new URLSearchParams;
1341
+ formData.forEach((value, key) => {
1342
+ if (typeof value === "string") {
1343
+ params.append(key, value);
1344
+ }
1345
+ });
1346
+ url.search = params.toString();
1347
+ await router.navigate(url.pathname + url.search, { replace });
1348
+ const result2 = {
1349
+ status: 200,
1350
+ ok: true
1351
+ };
1352
+ if (formContext) {
1353
+ formContext.setState("idle");
1354
+ }
1355
+ if (onSubmitEnd) {
1356
+ onSubmitEnd(result2);
1357
+ }
1358
+ return;
1359
+ }
1360
+ const response = await fetch(resolvedAction, {
1361
+ method: method.toUpperCase(),
1362
+ body,
1363
+ headers: {
1364
+ Accept: "application/json",
1365
+ ...encType === "application/x-www-form-urlencoded" && {
1366
+ "Content-Type": "application/x-www-form-urlencoded"
1367
+ }
1368
+ }
1369
+ });
1370
+ const result = {
1371
+ status: response.status,
1372
+ ok: response.ok
1373
+ };
1374
+ try {
1375
+ const data = await response.json();
1376
+ result.data = data;
1377
+ if (formContext) {
1378
+ formContext.setActionData(data);
1379
+ }
1380
+ } catch {}
1381
+ if (response.ok && !fetcherKey) {
1382
+ const redirectUrl = response.headers.get("X-Redirect-Url");
1383
+ if (redirectUrl) {
1384
+ await router.navigate(redirectUrl, { replace });
1385
+ } else if (!preventScrollReset && typeof window !== "undefined") {
1386
+ window.scrollTo(0, 0);
1387
+ }
1388
+ }
1389
+ if (formContext) {
1390
+ formContext.setState("idle");
1391
+ }
1392
+ if (onSubmitEnd) {
1393
+ onSubmitEnd(result);
1394
+ }
1395
+ } catch (error) {
1396
+ const result = {
1397
+ error: error instanceof Error ? error : new Error(String(error)),
1398
+ status: 0,
1399
+ ok: false
1400
+ };
1401
+ if (formContext) {
1402
+ formContext.setState("error");
1403
+ }
1404
+ if (onSubmitEnd) {
1405
+ onSubmitEnd(result);
1406
+ }
1407
+ }
1408
+ }, [method, resolvedAction, onSubmitStart, onSubmitEnd, replace, preventScrollReset, encType, fetcherKey, formContext, onSubmit]);
1409
+ const formMethod = method.toLowerCase() === "get" ? "get" : "post";
1410
+ return createElement2("form", {
1411
+ ref: formRef,
1412
+ method: formMethod,
1413
+ action: resolvedAction,
1414
+ encType,
1415
+ onSubmit: handleSubmit,
1416
+ ...props
1417
+ }, method !== "get" && method !== "post" ? createElement2("input", {
1418
+ type: "hidden",
1419
+ name: "_method",
1420
+ value: method.toUpperCase()
1421
+ }) : null, children);
1422
+ }
1423
+ function useSubmit() {
1424
+ const formContext = useFormContext();
1425
+ const submit = useCallback4(async (target, options = {}) => {
1426
+ const {
1427
+ method = "post",
1428
+ action,
1429
+ replace = false,
1430
+ preventScrollReset = false,
1431
+ encType = "application/x-www-form-urlencoded",
1432
+ fetcherKey
1433
+ } = options;
1434
+ if (typeof window === "undefined") {
1435
+ return { status: 0, ok: false, error: new Error("Not in browser environment") };
1436
+ }
1437
+ const resolvedAction = action || window.location.pathname;
1438
+ let formData;
1439
+ if (target instanceof HTMLFormElement) {
1440
+ formData = new FormData(target);
1441
+ } else if (target instanceof FormData) {
1442
+ formData = target;
1443
+ } else if (target instanceof URLSearchParams) {
1444
+ formData = new FormData;
1445
+ target.forEach((value, key) => {
1446
+ formData.append(key, value);
1447
+ });
1448
+ } else {
1449
+ formData = new FormData;
1450
+ for (const [key, value] of Object.entries(target)) {
1451
+ formData.append(key, value);
1452
+ }
1453
+ }
1454
+ if (formContext) {
1455
+ formContext.setState("submitting");
1456
+ }
1457
+ try {
1458
+ if (method.toLowerCase() === "get") {
1459
+ const url = new URL(resolvedAction, window.location.origin);
1460
+ const params = new URLSearchParams;
1461
+ formData.forEach((value, key) => {
1462
+ if (typeof value === "string") {
1463
+ params.append(key, value);
1464
+ }
1465
+ });
1466
+ url.search = params.toString();
1467
+ await router.navigate(url.pathname + url.search, { replace });
1468
+ if (formContext) {
1469
+ formContext.setState("idle");
1470
+ }
1471
+ return { status: 200, ok: true };
1472
+ }
1473
+ let body;
1474
+ if (encType === "application/x-www-form-urlencoded") {
1475
+ body = new URLSearchParams;
1476
+ formData.forEach((value, key) => {
1477
+ if (typeof value === "string") {
1478
+ body.append(key, value);
1479
+ }
1480
+ });
1481
+ } else {
1482
+ body = formData;
1483
+ }
1484
+ const response = await fetch(resolvedAction, {
1485
+ method: method.toUpperCase(),
1486
+ body,
1487
+ headers: {
1488
+ Accept: "application/json",
1489
+ ...encType === "application/x-www-form-urlencoded" && {
1490
+ "Content-Type": "application/x-www-form-urlencoded"
1491
+ }
1492
+ }
1493
+ });
1494
+ const result = {
1495
+ status: response.status,
1496
+ ok: response.ok
1497
+ };
1498
+ try {
1499
+ const data = await response.json();
1500
+ result.data = data;
1501
+ if (formContext) {
1502
+ formContext.setActionData(data);
1503
+ }
1504
+ } catch {}
1505
+ if (response.ok && !fetcherKey) {
1506
+ const redirectUrl = response.headers.get("X-Redirect-Url");
1507
+ if (redirectUrl) {
1508
+ await router.navigate(redirectUrl, { replace });
1509
+ } else if (!preventScrollReset) {
1510
+ window.scrollTo(0, 0);
1511
+ }
1512
+ }
1513
+ if (formContext) {
1514
+ formContext.setState("idle");
1515
+ }
1516
+ return result;
1517
+ } catch (error) {
1518
+ const result = {
1519
+ error: error instanceof Error ? error : new Error(String(error)),
1520
+ status: 0,
1521
+ ok: false
1522
+ };
1523
+ if (formContext) {
1524
+ formContext.setState("error");
1525
+ }
1526
+ return result;
1527
+ }
1528
+ }, [formContext]);
1529
+ return submit;
1530
+ }
1531
+ function useFetcher(key) {
1532
+ const [state, setStateInternal] = useState4("idle");
1533
+ const [data, setData] = useState4(undefined);
1534
+ const [error, setError] = useState4(undefined);
1535
+ const [formData, setFormData] = useState4(undefined);
1536
+ const [formMethod, setFormMethod] = useState4(undefined);
1537
+ const [formAction, setFormAction] = useState4(undefined);
1538
+ const mountedRef = useRef3(true);
1539
+ useEffect3(() => {
1540
+ mountedRef.current = true;
1541
+ return () => {
1542
+ mountedRef.current = false;
1543
+ };
1544
+ }, []);
1545
+ const safeSetState = useCallback4((newState) => {
1546
+ if (mountedRef.current) {
1547
+ setStateInternal(newState);
1548
+ }
1549
+ }, []);
1550
+ const reset = useCallback4(() => {
1551
+ if (mountedRef.current) {
1552
+ setStateInternal("idle");
1553
+ setData(undefined);
1554
+ setError(undefined);
1555
+ setFormData(undefined);
1556
+ setFormMethod(undefined);
1557
+ setFormAction(undefined);
1558
+ }
1559
+ }, []);
1560
+ const submit = useCallback4(async (target, options = {}) => {
1561
+ const {
1562
+ method = "post",
1563
+ action,
1564
+ encType = "application/x-www-form-urlencoded"
1565
+ } = options;
1566
+ if (typeof window === "undefined") {
1567
+ return;
1568
+ }
1569
+ const resolvedAction = action || window.location.pathname;
1570
+ let newFormData;
1571
+ if (target instanceof HTMLFormElement) {
1572
+ newFormData = new FormData(target);
1573
+ } else if (target instanceof FormData) {
1574
+ newFormData = target;
1575
+ } else if (target instanceof URLSearchParams) {
1576
+ newFormData = new FormData;
1577
+ target.forEach((value, key2) => {
1578
+ newFormData.append(key2, value);
1579
+ });
1580
+ } else {
1581
+ newFormData = new FormData;
1582
+ for (const [key2, value] of Object.entries(target)) {
1583
+ newFormData.append(key2, value);
1584
+ }
1585
+ }
1586
+ setFormData(newFormData);
1587
+ setFormMethod(method.toUpperCase());
1588
+ setFormAction(resolvedAction);
1589
+ safeSetState("submitting");
1590
+ try {
1591
+ if (method.toLowerCase() === "get") {
1592
+ const url = new URL(resolvedAction, window.location.origin);
1593
+ const params = new URLSearchParams;
1594
+ newFormData.forEach((value, key2) => {
1595
+ if (typeof value === "string") {
1596
+ params.append(key2, value);
1597
+ }
1598
+ });
1599
+ url.search = params.toString();
1600
+ const response2 = await fetch(url.toString(), {
1601
+ method: "GET",
1602
+ headers: { Accept: "application/json" }
1603
+ });
1604
+ if (mountedRef.current) {
1605
+ try {
1606
+ const responseData = await response2.json();
1607
+ setData(responseData);
1608
+ } catch {}
1609
+ safeSetState("idle");
1610
+ }
1611
+ return;
1612
+ }
1613
+ let body;
1614
+ if (encType === "application/x-www-form-urlencoded") {
1615
+ body = new URLSearchParams;
1616
+ newFormData.forEach((value, key2) => {
1617
+ if (typeof value === "string") {
1618
+ body.append(key2, value);
1619
+ }
1620
+ });
1621
+ } else {
1622
+ body = newFormData;
1623
+ }
1624
+ const response = await fetch(resolvedAction, {
1625
+ method: method.toUpperCase(),
1626
+ body,
1627
+ headers: {
1628
+ Accept: "application/json",
1629
+ ...encType === "application/x-www-form-urlencoded" && {
1630
+ "Content-Type": "application/x-www-form-urlencoded"
1631
+ }
1632
+ }
1633
+ });
1634
+ if (mountedRef.current) {
1635
+ try {
1636
+ const responseData = await response.json();
1637
+ setData(responseData);
1638
+ } catch {}
1639
+ safeSetState("idle");
1640
+ }
1641
+ } catch (err) {
1642
+ if (mountedRef.current) {
1643
+ setError(err instanceof Error ? err : new Error(String(err)));
1644
+ safeSetState("error");
1645
+ }
1646
+ }
1647
+ }, [safeSetState]);
1648
+ const load = useCallback4(async (href) => {
1649
+ if (typeof window === "undefined") {
1650
+ return;
1651
+ }
1652
+ setFormAction(href);
1653
+ setFormMethod("GET");
1654
+ safeSetState("loading");
1655
+ try {
1656
+ const response = await fetch(href, {
1657
+ method: "GET",
1658
+ headers: { Accept: "application/json" }
1659
+ });
1660
+ if (mountedRef.current) {
1661
+ try {
1662
+ const responseData = await response.json();
1663
+ setData(responseData);
1664
+ } catch {}
1665
+ safeSetState("idle");
1666
+ }
1667
+ } catch (err) {
1668
+ if (mountedRef.current) {
1669
+ setError(err instanceof Error ? err : new Error(String(err)));
1670
+ safeSetState("error");
1671
+ }
1672
+ }
1673
+ }, [safeSetState]);
1674
+ const FetcherForm = useCallback4((formProps) => {
1675
+ return Form({
1676
+ ...formProps,
1677
+ fetcherKey: key || "fetcher",
1678
+ onSubmitStart: () => {
1679
+ safeSetState("submitting");
1680
+ if (formProps.onSubmitStart) {
1681
+ formProps.onSubmitStart();
1682
+ }
1683
+ },
1684
+ onSubmitEnd: (result) => {
1685
+ if (mountedRef.current) {
1686
+ if (result.ok) {
1687
+ setData(result.data);
1688
+ safeSetState("idle");
1689
+ } else {
1690
+ setError(result.error);
1691
+ safeSetState("error");
1692
+ }
1693
+ }
1694
+ if (formProps.onSubmitEnd) {
1695
+ formProps.onSubmitEnd(result);
1696
+ }
1697
+ }
1698
+ });
1699
+ }, [key, safeSetState]);
1700
+ return {
1701
+ state,
1702
+ data,
1703
+ error,
1704
+ formData,
1705
+ formMethod,
1706
+ formAction,
1707
+ Form: FetcherForm,
1708
+ submit,
1709
+ load,
1710
+ reset
1711
+ };
1712
+ }
1713
+ function useActionData2() {
1714
+ const context = useFormContext();
1715
+ return context?.actionData;
1716
+ }
1717
+ function useNavigation2() {
1718
+ const [navigationState, setNavigationState] = useState4(() => {
1719
+ if (typeof window === "undefined") {
1720
+ return { pathname: "/", search: "", hash: "" };
1721
+ }
1722
+ return router.getState();
1723
+ });
1724
+ const formContext = useFormContext();
1725
+ useEffect3(() => {
1726
+ return router.subscribe((event) => {
1727
+ setNavigationState(event.to);
1728
+ });
1729
+ }, []);
1730
+ return {
1731
+ ...navigationState,
1732
+ state: formContext?.state || "idle"
1733
+ };
1734
+ }
1735
+ function serializeFormData(formData) {
1736
+ const params = new URLSearchParams;
1737
+ formData.forEach((value, key) => {
1738
+ if (typeof value === "string") {
1739
+ params.append(key, value);
1740
+ }
1741
+ });
1742
+ return params.toString();
1743
+ }
1744
+ function parseFormData(data) {
1745
+ const formData = new FormData;
1746
+ const params = new URLSearchParams(data);
1747
+ params.forEach((value, key) => {
1748
+ formData.append(key, value);
1749
+ });
1750
+ return formData;
1751
+ }
1752
+ function formDataToObject(formData) {
1753
+ const result = {};
1754
+ formData.forEach((value, key) => {
1755
+ if (typeof value === "string") {
1756
+ if (key in result) {
1757
+ const existing = result[key];
1758
+ if (Array.isArray(existing)) {
1759
+ existing.push(value);
1760
+ } else {
1761
+ result[key] = [existing, value];
1762
+ }
1763
+ } else {
1764
+ result[key] = value;
1765
+ }
1766
+ }
1767
+ });
1768
+ return result;
1769
+ }
1770
+ function objectToFormData(obj) {
1771
+ const formData = new FormData;
1772
+ for (const [key, value] of Object.entries(obj)) {
1773
+ if (Array.isArray(value)) {
1774
+ value.forEach((v) => formData.append(key, String(v)));
1775
+ } else {
1776
+ formData.append(key, String(value));
1777
+ }
1778
+ }
1779
+ return formData;
1780
+ }
1781
+ // src/error-boundary.tsx
1782
+ import React3, {
1783
+ Component,
1784
+ useContext as useContext3
1785
+ } from "react";
1786
+ class ErrorBoundary extends Component {
1787
+ constructor(props) {
1788
+ super(props);
1789
+ this.state = {
1790
+ error: null,
1791
+ errorInfo: null
1792
+ };
1793
+ }
1794
+ static getDerivedStateFromError(error) {
1795
+ return { error };
1796
+ }
1797
+ componentDidCatch(error, errorInfo) {
1798
+ this.setState({ errorInfo });
1799
+ if (this.props.onError) {
1800
+ this.props.onError(error, errorInfo);
1801
+ }
1802
+ }
1803
+ reset = () => {
1804
+ this.setState({
1805
+ error: null,
1806
+ errorInfo: null
1807
+ });
1808
+ };
1809
+ render() {
1810
+ const { error } = this.state;
1811
+ const { fallback, children } = this.props;
1812
+ if (error) {
1813
+ if (typeof fallback === "function") {
1814
+ return fallback(error, this.reset);
1815
+ }
1816
+ if (fallback !== undefined) {
1817
+ return fallback;
1818
+ }
1819
+ return React3.createElement("div", {
1820
+ style: {
1821
+ padding: "20px",
1822
+ border: "1px solid #f5c6cb",
1823
+ borderRadius: "4px",
1824
+ backgroundColor: "#f8d7da",
1825
+ color: "#721c24"
1826
+ }
1827
+ }, React3.createElement("h2", null, "Something went wrong"), React3.createElement("p", null, error.message), React3.createElement("button", {
1828
+ onClick: this.reset,
1829
+ style: {
1830
+ padding: "8px 16px",
1831
+ backgroundColor: "#721c24",
1832
+ color: "white",
1833
+ border: "none",
1834
+ borderRadius: "4px",
1835
+ cursor: "pointer"
1836
+ }
1837
+ }, "Try again"));
1838
+ }
1839
+ return children;
1840
+ }
1841
+ }
1842
+ function useErrorBoundary() {
1843
+ const context = useContext3(ErrorContext);
1844
+ if (!context) {
1845
+ throw new Error("useErrorBoundary must be used within an EreoProvider or ErrorProvider. " + "Make sure your component is wrapped with the appropriate provider.");
1846
+ }
1847
+ return {
1848
+ error: context.error,
1849
+ reset: context.clearError,
1850
+ showBoundary: (error) => context.setError(error)
1851
+ };
1852
+ }
1853
+ function useRouteError() {
1854
+ const context = useContext3(ErrorContext);
1855
+ return context?.error;
1856
+ }
1857
+
1858
+ class RouteErrorBoundary extends Component {
1859
+ constructor(props) {
1860
+ super(props);
1861
+ this.state = {
1862
+ error: null,
1863
+ errorInfo: null,
1864
+ retryCount: 0
1865
+ };
1866
+ }
1867
+ static getDerivedStateFromError(error) {
1868
+ return { error };
1869
+ }
1870
+ componentDidCatch(error, errorInfo) {
1871
+ this.setState({ errorInfo });
1872
+ const { errorConfig, routeId } = this.props;
1873
+ if (errorConfig?.reportError) {
1874
+ errorConfig.reportError(error, {
1875
+ route: routeId,
1876
+ phase: "render"
1877
+ });
1878
+ }
1879
+ if (errorConfig?.onError === "silent") {
1880
+ console.error(`[Route ${routeId}] Error:`, error);
1881
+ } else if (errorConfig?.onError === "redirect") {
1882
+ console.error(`[Route ${routeId}] Error (redirect mode):`, error);
1883
+ } else if (errorConfig?.onError === "toast") {
1884
+ console.error(`[Route ${routeId}] Error (toast mode):`, error);
1885
+ }
1886
+ }
1887
+ reset = () => {
1888
+ const { errorConfig } = this.props;
1889
+ const { retryCount } = this.state;
1890
+ if (errorConfig?.retry && retryCount < errorConfig.retry.count) {
1891
+ setTimeout(() => {
1892
+ this.setState((prevState) => ({
1893
+ error: null,
1894
+ errorInfo: null,
1895
+ retryCount: prevState.retryCount + 1
1896
+ }));
1897
+ }, errorConfig.retry.delay);
1898
+ } else {
1899
+ this.setState({
1900
+ error: null,
1901
+ errorInfo: null,
1902
+ retryCount: 0
1903
+ });
1904
+ }
1905
+ };
1906
+ render() {
1907
+ const { error } = this.state;
1908
+ const { children, fallbackComponent: FallbackComponent, errorConfig, params = {} } = this.props;
1909
+ if (error) {
1910
+ if (errorConfig?.maxCaptures !== undefined && this.state.retryCount >= errorConfig.maxCaptures) {
1911
+ return React3.createElement("div", { style: { padding: "20px", color: "#721c24" } }, "Error limit exceeded. Please refresh the page.");
1912
+ }
1913
+ if (FallbackComponent) {
1914
+ return React3.createElement(FallbackComponent, { error, params });
1915
+ }
1916
+ if (errorConfig?.fallback) {
1917
+ return React3.createElement(errorConfig.fallback, {});
1918
+ }
1919
+ return React3.createElement("div", {
1920
+ style: {
1921
+ padding: "20px",
1922
+ border: "1px solid #f5c6cb",
1923
+ borderRadius: "4px",
1924
+ backgroundColor: "#f8d7da",
1925
+ color: "#721c24"
1926
+ }
1927
+ }, React3.createElement("h2", null, "Route Error"), React3.createElement("p", null, error.message), React3.createElement("button", {
1928
+ onClick: this.reset,
1929
+ style: {
1930
+ padding: "8px 16px",
1931
+ backgroundColor: "#721c24",
1932
+ color: "white",
1933
+ border: "none",
1934
+ borderRadius: "4px",
1935
+ cursor: "pointer"
1936
+ }
1937
+ }, "Retry"));
1938
+ }
1939
+ return children;
1940
+ }
1941
+ }
1942
+ function isRouteErrorResponse(error) {
1943
+ return typeof error === "object" && error !== null && "status" in error && "statusText" in error && "data" in error && typeof error.status === "number" && typeof error.statusText === "string";
1944
+ }
1945
+ function createRouteErrorResponse(status, statusText, data = null) {
1946
+ return {
1947
+ status,
1948
+ statusText,
1949
+ data,
1950
+ internal: false
1951
+ };
1952
+ }
1953
+ function withErrorBoundary(WrappedComponent, errorBoundaryProps) {
1954
+ const displayName = WrappedComponent.displayName || WrappedComponent.name || "Component";
1955
+ const ComponentWithErrorBoundary = (props) => {
1956
+ const boundaryProps = {
1957
+ ...errorBoundaryProps || {},
1958
+ children: React3.createElement(WrappedComponent, props)
1959
+ };
1960
+ return React3.createElement(ErrorBoundary, boundaryProps);
1961
+ };
1962
+ ComponentWithErrorBoundary.displayName = `withErrorBoundary(${displayName})`;
1963
+ return ComponentWithErrorBoundary;
1964
+ }
1965
+
1966
+ class RouteError extends Error {
1967
+ status;
1968
+ statusText;
1969
+ data;
1970
+ constructor(status, statusText, data) {
1971
+ super(`${status} ${statusText}`);
1972
+ this.name = "RouteError";
1973
+ this.status = status;
1974
+ this.statusText = statusText;
1975
+ this.data = data;
1976
+ }
1977
+ toResponse() {
1978
+ return {
1979
+ status: this.status,
1980
+ statusText: this.statusText,
1981
+ data: this.data
1982
+ };
1983
+ }
1984
+ }
1985
+
1986
+ // src/index.ts
1987
+ function initClient() {
1988
+ initializeIslands();
1989
+ setupScrollRestoration();
1990
+ setupAutoPrefetch({ strategy: "hover" });
1991
+ }
1992
+ export {
1993
+ withErrorBoundary,
1994
+ useTypedNavigate,
1995
+ useSubmit,
1996
+ useRouteError,
1997
+ useNavigationContext,
1998
+ useNavigation,
1999
+ useLoaderDataContext,
2000
+ useLoaderData,
2001
+ useIsRouteActive,
2002
+ useIsActive,
2003
+ useNavigation2 as useFormNavigation,
2004
+ useFormContext,
2005
+ useActionData2 as useFormActionData,
2006
+ useFetcher,
2007
+ useErrorContext,
2008
+ useErrorBoundary,
2009
+ useError,
2010
+ useActionDataContext,
2011
+ useActionData,
2012
+ typedRedirect,
2013
+ typedNavigate,
2014
+ goForward2 as typedGoForward,
2015
+ goBack2 as typedGoBack,
2016
+ submitAction,
2017
+ stripHydrationProps,
2018
+ shouldHydrate,
2019
+ setupScrollRestoration,
2020
+ setupLinkPrefetch,
2021
+ setupAutoPrefetch,
2022
+ serializeFormData,
2023
+ router,
2024
+ resetIslandCounter,
2025
+ registerIslandComponents,
2026
+ registerIslandComponent,
2027
+ redirect,
2028
+ preloadRoute,
2029
+ prefetchAll,
2030
+ prefetch,
2031
+ parseTypedSearchParams,
2032
+ parseTypedHashParams,
2033
+ parseHydrationDirective,
2034
+ parseFormData,
2035
+ onNavigate,
2036
+ objectToFormData,
2037
+ navigate,
2038
+ islandRegistry,
2039
+ isRouteErrorResponse,
2040
+ isPrefetching,
2041
+ isPrefetched,
2042
+ isCurrentPath,
2043
+ initializeIslands,
2044
+ initClient,
2045
+ hydrateIslands,
2046
+ goForward,
2047
+ goBack,
2048
+ go,
2049
+ getPrefetchedData,
2050
+ getNavigationState,
2051
+ getIslandCount,
2052
+ getIslandComponent,
2053
+ generateIslandId,
2054
+ formDataToObject,
2055
+ fetchLoaderData,
2056
+ createRouteErrorResponse,
2057
+ createIsland,
2058
+ createHydrationTrigger,
2059
+ clearPrefetchCache,
2060
+ cleanupIslands,
2061
+ buildUrl,
2062
+ buildTypedUrl,
2063
+ TypedNavLink,
2064
+ TypedLink,
2065
+ RouteErrorBoundary,
2066
+ RouteError,
2067
+ NavigationProvider,
2068
+ NavigationContext,
2069
+ NavLink,
2070
+ LoaderDataProvider,
2071
+ LoaderDataContext,
2072
+ Link,
2073
+ FormProvider,
2074
+ Form,
2075
+ ErrorProvider,
2076
+ ErrorContext,
2077
+ ErrorBoundary,
2078
+ EreoProvider,
2079
+ ActionDataProvider,
2080
+ ActionDataContext
2081
+ };