specnav-next 0.2.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/CHANGELOG.md +38 -0
- package/dist/index.cjs +552 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +547 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +20 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +5 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +14 -0
- package/dist/server.js.map +1 -0
- package/package.json +82 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Strategy, CacheConfig, PrefetchConfig, MorphConfig, GraphConfig, CacheLayer } from 'specnav-core';
|
|
3
|
+
|
|
4
|
+
interface ProgressConfig {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
color: string;
|
|
7
|
+
height: number;
|
|
8
|
+
showSpinner: boolean;
|
|
9
|
+
position: "top" | "bottom";
|
|
10
|
+
minimum: number;
|
|
11
|
+
easing: string;
|
|
12
|
+
speed: number;
|
|
13
|
+
trickle: boolean;
|
|
14
|
+
trickleSpeed: number;
|
|
15
|
+
}
|
|
16
|
+
interface TransitionConfig {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
duration: number;
|
|
19
|
+
easing: string;
|
|
20
|
+
}
|
|
21
|
+
interface NavigateProviderProps {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
strategy?: Strategy;
|
|
24
|
+
cache?: Partial<CacheConfig>;
|
|
25
|
+
prefetch?: Partial<PrefetchConfig>;
|
|
26
|
+
progress?: Partial<ProgressConfig>;
|
|
27
|
+
morph?: Partial<MorphConfig>;
|
|
28
|
+
transitions?: Partial<TransitionConfig>;
|
|
29
|
+
graph?: Partial<GraphConfig>;
|
|
30
|
+
onNavigateStart?: (href: string) => void;
|
|
31
|
+
onNavigateEnd?: (href: string, durationMs: number) => void;
|
|
32
|
+
onCacheHit?: (href: string, layer: CacheLayer) => void;
|
|
33
|
+
}
|
|
34
|
+
interface NavigateOptions {
|
|
35
|
+
replace?: boolean;
|
|
36
|
+
scroll?: boolean;
|
|
37
|
+
morph?: boolean;
|
|
38
|
+
cache?: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
41
|
+
href: string;
|
|
42
|
+
prefetch?: "trajectory" | "hover" | "eager" | false;
|
|
43
|
+
morph?: boolean;
|
|
44
|
+
cache?: boolean;
|
|
45
|
+
replace?: boolean;
|
|
46
|
+
scroll?: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface UseNavigateReturn {
|
|
49
|
+
navigate: (href: string, options?: NavigateOptions) => void;
|
|
50
|
+
back: () => void;
|
|
51
|
+
forward: () => void;
|
|
52
|
+
prefetch: (href: string) => Promise<void>;
|
|
53
|
+
clearCache: (href?: string) => void;
|
|
54
|
+
isNavigating: boolean;
|
|
55
|
+
pendingHref: string | null;
|
|
56
|
+
}
|
|
57
|
+
interface UseNavigationStateReturn {
|
|
58
|
+
isNavigating: boolean;
|
|
59
|
+
pendingHref: string | null;
|
|
60
|
+
previousHref: string | null;
|
|
61
|
+
cacheSize: number;
|
|
62
|
+
cacheHitRate: number;
|
|
63
|
+
lastNavigationMs: number;
|
|
64
|
+
progress: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare function NavigateProvider({ children, strategy, cache: cacheConfig, prefetch: prefetchConfig, progress: progressConfig, morph: morphConfig, graph: graphConfig, onNavigateStart, onNavigateEnd, onCacheHit, }: NavigateProviderProps): react_jsx_runtime.JSX.Element;
|
|
68
|
+
|
|
69
|
+
declare function Link({ href, prefetch, morph, cache, replace, scroll, children, ...props }: LinkProps): react_jsx_runtime.JSX.Element;
|
|
70
|
+
|
|
71
|
+
declare function useNavigate(): UseNavigateReturn;
|
|
72
|
+
|
|
73
|
+
declare function useNavigationState(): UseNavigationStateReturn;
|
|
74
|
+
|
|
75
|
+
export { Link, type LinkProps, type NavigateOptions, NavigateProvider, type NavigateProviderProps, type ProgressConfig, type TransitionConfig, type UseNavigateReturn, type UseNavigationStateReturn, useNavigate, useNavigationState };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Strategy, CacheConfig, PrefetchConfig, MorphConfig, GraphConfig, CacheLayer } from 'specnav-core';
|
|
3
|
+
|
|
4
|
+
interface ProgressConfig {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
color: string;
|
|
7
|
+
height: number;
|
|
8
|
+
showSpinner: boolean;
|
|
9
|
+
position: "top" | "bottom";
|
|
10
|
+
minimum: number;
|
|
11
|
+
easing: string;
|
|
12
|
+
speed: number;
|
|
13
|
+
trickle: boolean;
|
|
14
|
+
trickleSpeed: number;
|
|
15
|
+
}
|
|
16
|
+
interface TransitionConfig {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
duration: number;
|
|
19
|
+
easing: string;
|
|
20
|
+
}
|
|
21
|
+
interface NavigateProviderProps {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
strategy?: Strategy;
|
|
24
|
+
cache?: Partial<CacheConfig>;
|
|
25
|
+
prefetch?: Partial<PrefetchConfig>;
|
|
26
|
+
progress?: Partial<ProgressConfig>;
|
|
27
|
+
morph?: Partial<MorphConfig>;
|
|
28
|
+
transitions?: Partial<TransitionConfig>;
|
|
29
|
+
graph?: Partial<GraphConfig>;
|
|
30
|
+
onNavigateStart?: (href: string) => void;
|
|
31
|
+
onNavigateEnd?: (href: string, durationMs: number) => void;
|
|
32
|
+
onCacheHit?: (href: string, layer: CacheLayer) => void;
|
|
33
|
+
}
|
|
34
|
+
interface NavigateOptions {
|
|
35
|
+
replace?: boolean;
|
|
36
|
+
scroll?: boolean;
|
|
37
|
+
morph?: boolean;
|
|
38
|
+
cache?: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
41
|
+
href: string;
|
|
42
|
+
prefetch?: "trajectory" | "hover" | "eager" | false;
|
|
43
|
+
morph?: boolean;
|
|
44
|
+
cache?: boolean;
|
|
45
|
+
replace?: boolean;
|
|
46
|
+
scroll?: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface UseNavigateReturn {
|
|
49
|
+
navigate: (href: string, options?: NavigateOptions) => void;
|
|
50
|
+
back: () => void;
|
|
51
|
+
forward: () => void;
|
|
52
|
+
prefetch: (href: string) => Promise<void>;
|
|
53
|
+
clearCache: (href?: string) => void;
|
|
54
|
+
isNavigating: boolean;
|
|
55
|
+
pendingHref: string | null;
|
|
56
|
+
}
|
|
57
|
+
interface UseNavigationStateReturn {
|
|
58
|
+
isNavigating: boolean;
|
|
59
|
+
pendingHref: string | null;
|
|
60
|
+
previousHref: string | null;
|
|
61
|
+
cacheSize: number;
|
|
62
|
+
cacheHitRate: number;
|
|
63
|
+
lastNavigationMs: number;
|
|
64
|
+
progress: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare function NavigateProvider({ children, strategy, cache: cacheConfig, prefetch: prefetchConfig, progress: progressConfig, morph: morphConfig, graph: graphConfig, onNavigateStart, onNavigateEnd, onCacheHit, }: NavigateProviderProps): react_jsx_runtime.JSX.Element;
|
|
68
|
+
|
|
69
|
+
declare function Link({ href, prefetch, morph, cache, replace, scroll, children, ...props }: LinkProps): react_jsx_runtime.JSX.Element;
|
|
70
|
+
|
|
71
|
+
declare function useNavigate(): UseNavigateReturn;
|
|
72
|
+
|
|
73
|
+
declare function useNavigationState(): UseNavigationStateReturn;
|
|
74
|
+
|
|
75
|
+
export { Link, type LinkProps, type NavigateOptions, NavigateProvider, type NavigateProviderProps, type ProgressConfig, type TransitionConfig, type UseNavigateReturn, type UseNavigationStateReturn, useNavigate, useNavigationState };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { createContext, useState, useRef, useEffect, useContext } from 'react';
|
|
2
|
+
import { createAdaptiveMode, createCacheManager, createMorpher, createSpeculativeRenderer, createNavigationGraphLearner, createTrajectoryEngine } from 'specnav-core';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/prefetch.ts
|
|
16
|
+
var prefetch_exports = {};
|
|
17
|
+
__export(prefetch_exports, {
|
|
18
|
+
inflightRequests: () => inflightRequests,
|
|
19
|
+
prefetchPage: () => prefetchPage
|
|
20
|
+
});
|
|
21
|
+
async function prefetchPage(href, cache, speculator) {
|
|
22
|
+
if (inflightRequests.has(href)) {
|
|
23
|
+
await inflightRequests.get(href);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const fetchPromise = fetch(href, {
|
|
28
|
+
headers: { "x-specnav": "1" },
|
|
29
|
+
signal: controller.signal
|
|
30
|
+
}).then((res) => {
|
|
31
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
32
|
+
return res.text();
|
|
33
|
+
}).then((html) => {
|
|
34
|
+
if (cache) cache.set(href, html);
|
|
35
|
+
if (speculator) speculator.speculate(href, html);
|
|
36
|
+
return html;
|
|
37
|
+
}).finally(() => {
|
|
38
|
+
inflightRequests.delete(href);
|
|
39
|
+
});
|
|
40
|
+
inflightRequests.set(href, fetchPromise);
|
|
41
|
+
await fetchPromise;
|
|
42
|
+
}
|
|
43
|
+
var inflightRequests;
|
|
44
|
+
var init_prefetch = __esm({
|
|
45
|
+
"src/prefetch.ts"() {
|
|
46
|
+
inflightRequests = /* @__PURE__ */ new Map();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
var NavigationContext = createContext(null);
|
|
50
|
+
function useNavigationContext() {
|
|
51
|
+
const ctx = useContext(NavigationContext);
|
|
52
|
+
if (!ctx) throw new Error("useNavigationContext must be used within NavigateProvider");
|
|
53
|
+
return ctx;
|
|
54
|
+
}
|
|
55
|
+
function NavigateProvider({
|
|
56
|
+
children,
|
|
57
|
+
strategy = "auto",
|
|
58
|
+
cache: cacheConfig,
|
|
59
|
+
prefetch: prefetchConfig,
|
|
60
|
+
progress: progressConfig,
|
|
61
|
+
morph: morphConfig,
|
|
62
|
+
graph: graphConfig,
|
|
63
|
+
onNavigateStart,
|
|
64
|
+
onNavigateEnd,
|
|
65
|
+
onCacheHit
|
|
66
|
+
}) {
|
|
67
|
+
const [isNavigating, setIsNavigating] = useState(false);
|
|
68
|
+
const [pendingHref, setPendingHref] = useState(null);
|
|
69
|
+
const [progress, setProgress] = useState(0);
|
|
70
|
+
const [cacheHits, setCacheHits] = useState(0);
|
|
71
|
+
const [cacheMisses, setCacheMisses] = useState(0);
|
|
72
|
+
const navigationStartTime = useRef(null);
|
|
73
|
+
const [trajectory, setTrajectory] = useState(null);
|
|
74
|
+
const [cache, setCache] = useState(null);
|
|
75
|
+
const [morpher, setMorpher] = useState(null);
|
|
76
|
+
const [speculator, setSpeculator] = useState(null);
|
|
77
|
+
const [graph, setGraph] = useState(null);
|
|
78
|
+
const [adaptive, setAdaptive] = useState(null);
|
|
79
|
+
const wrappedSetNavigating = (href) => {
|
|
80
|
+
if (href) {
|
|
81
|
+
navigationStartTime.current = Date.now();
|
|
82
|
+
onNavigateStart?.(href);
|
|
83
|
+
} else if (navigationStartTime.current) {
|
|
84
|
+
const duration = Date.now() - navigationStartTime.current;
|
|
85
|
+
if (pendingHref) {
|
|
86
|
+
onNavigateEnd?.(pendingHref, duration);
|
|
87
|
+
}
|
|
88
|
+
navigationStartTime.current = null;
|
|
89
|
+
}
|
|
90
|
+
setIsNavigating(!!href);
|
|
91
|
+
setPendingHref(href);
|
|
92
|
+
};
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const adaptiveEngine = createAdaptiveMode();
|
|
95
|
+
const cacheEngine = createCacheManager(cacheConfig, {
|
|
96
|
+
onCacheHit: (href, layer) => {
|
|
97
|
+
setCacheHits((prev) => prev + 1);
|
|
98
|
+
onCacheHit?.(href, layer);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const morpherEngine = createMorpher(morphConfig);
|
|
102
|
+
const speculatorEngine = createSpeculativeRenderer(3);
|
|
103
|
+
const graphEngine = createNavigationGraphLearner(graphConfig);
|
|
104
|
+
adaptiveEngine.waitForInit().then(() => {
|
|
105
|
+
setAdaptive(adaptiveEngine);
|
|
106
|
+
setCache(cacheEngine);
|
|
107
|
+
setMorpher(morpherEngine);
|
|
108
|
+
setSpeculator(speculatorEngine);
|
|
109
|
+
setGraph(graphEngine);
|
|
110
|
+
const effectiveStrategy = adaptiveEngine.getStrategy(strategy);
|
|
111
|
+
if (effectiveStrategy !== "off" && prefetchConfig?.mode === "trajectory") {
|
|
112
|
+
const trajectoryEngine = createTrajectoryEngine(prefetchConfig.trajectory, {
|
|
113
|
+
onPrediction: async (href) => {
|
|
114
|
+
if (!adaptiveEngine?.shouldPrefetch()) return;
|
|
115
|
+
const { prefetchPage: prefetchPage2 } = await Promise.resolve().then(() => (init_prefetch(), prefetch_exports));
|
|
116
|
+
await prefetchPage2(href, cacheEngine, speculatorEngine);
|
|
117
|
+
},
|
|
118
|
+
onCancel: (href) => {
|
|
119
|
+
speculatorEngine?.cancel(href);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
trajectoryEngine.start();
|
|
123
|
+
setTrajectory(trajectoryEngine);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const handlePopState = async (e) => {
|
|
127
|
+
if (!e.state?.specnav) return;
|
|
128
|
+
const href = window.location.pathname;
|
|
129
|
+
wrappedSetNavigating(href);
|
|
130
|
+
setProgress(0.3);
|
|
131
|
+
try {
|
|
132
|
+
const html = await cache?.get(href);
|
|
133
|
+
if (html && morpher) {
|
|
134
|
+
setProgress(0.7);
|
|
135
|
+
const parser = new DOMParser();
|
|
136
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
137
|
+
const newContent = doc.body;
|
|
138
|
+
morpher.morph(document.body, newContent);
|
|
139
|
+
setProgress(1);
|
|
140
|
+
setTimeout(() => wrappedSetNavigating(null), 100);
|
|
141
|
+
} else {
|
|
142
|
+
window.location.reload();
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("Popstate navigation failed:", error);
|
|
146
|
+
window.location.reload();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
window.addEventListener("popstate", handlePopState);
|
|
150
|
+
return () => {
|
|
151
|
+
trajectory?.stop();
|
|
152
|
+
cache?.destroy();
|
|
153
|
+
speculator?.cancelAll();
|
|
154
|
+
window.removeEventListener("popstate", handlePopState);
|
|
155
|
+
};
|
|
156
|
+
}, [strategy, cacheConfig, prefetchConfig, morphConfig, graphConfig, onCacheHit, onNavigateEnd]);
|
|
157
|
+
const value = {
|
|
158
|
+
trajectory,
|
|
159
|
+
cache,
|
|
160
|
+
morpher,
|
|
161
|
+
speculator,
|
|
162
|
+
graph,
|
|
163
|
+
adaptive,
|
|
164
|
+
isNavigating,
|
|
165
|
+
pendingHref,
|
|
166
|
+
progress,
|
|
167
|
+
cacheHits,
|
|
168
|
+
cacheMisses,
|
|
169
|
+
setNavigating: wrappedSetNavigating,
|
|
170
|
+
setProgress,
|
|
171
|
+
incrementCacheMiss: () => setCacheMisses((prev) => prev + 1)
|
|
172
|
+
};
|
|
173
|
+
return /* @__PURE__ */ jsxs(NavigationContext.Provider, { value, children: [
|
|
174
|
+
progressConfig?.enabled !== false && /* @__PURE__ */ jsx(ProgressBar, { config: progressConfig, progress, isNavigating }),
|
|
175
|
+
children
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
178
|
+
function ProgressBar({
|
|
179
|
+
config,
|
|
180
|
+
progress,
|
|
181
|
+
isNavigating
|
|
182
|
+
}) {
|
|
183
|
+
const color = config?.color ?? "#6366f1";
|
|
184
|
+
const height = config?.height ?? 3;
|
|
185
|
+
const position = config?.position ?? "top";
|
|
186
|
+
return /* @__PURE__ */ jsx(
|
|
187
|
+
"div",
|
|
188
|
+
{
|
|
189
|
+
style: {
|
|
190
|
+
position: "fixed",
|
|
191
|
+
[position]: 0,
|
|
192
|
+
left: 0,
|
|
193
|
+
right: 0,
|
|
194
|
+
height: `${height}px`,
|
|
195
|
+
zIndex: 9999,
|
|
196
|
+
pointerEvents: "none"
|
|
197
|
+
},
|
|
198
|
+
children: /* @__PURE__ */ jsx(
|
|
199
|
+
"div",
|
|
200
|
+
{
|
|
201
|
+
style: {
|
|
202
|
+
height: "100%",
|
|
203
|
+
width: `${progress * 100}%`,
|
|
204
|
+
background: color,
|
|
205
|
+
transition: isNavigating ? "width 200ms ease" : "opacity 200ms ease",
|
|
206
|
+
opacity: isNavigating ? 1 : 0
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
init_prefetch();
|
|
214
|
+
function Link({
|
|
215
|
+
href,
|
|
216
|
+
prefetch = "trajectory",
|
|
217
|
+
morph = true,
|
|
218
|
+
cache = true,
|
|
219
|
+
replace = false,
|
|
220
|
+
scroll = true,
|
|
221
|
+
children,
|
|
222
|
+
...props
|
|
223
|
+
}) {
|
|
224
|
+
const ctx = useNavigationContext();
|
|
225
|
+
const linkRef = useRef(null);
|
|
226
|
+
const abortControllerRef = useRef(null);
|
|
227
|
+
const isSafeUrl = (url) => {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = new URL(url, window.location.origin);
|
|
230
|
+
return parsed.origin === window.location.origin;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!linkRef.current || !ctx.trajectory || prefetch === false) return;
|
|
237
|
+
const element = linkRef.current;
|
|
238
|
+
ctx.trajectory.registerLink(href, element);
|
|
239
|
+
return () => {
|
|
240
|
+
ctx.trajectory?.unregisterLink(element);
|
|
241
|
+
abortControllerRef.current?.abort();
|
|
242
|
+
};
|
|
243
|
+
}, [href, ctx.trajectory, prefetch]);
|
|
244
|
+
const handleMouseEnter = async () => {
|
|
245
|
+
if (prefetch === "hover" && ctx.adaptive?.shouldPrefetch()) {
|
|
246
|
+
await prefetchPage2(href);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const handleMouseLeave = () => {
|
|
250
|
+
};
|
|
251
|
+
const handleFocus = async () => {
|
|
252
|
+
if (ctx.adaptive?.shouldPrefetch()) {
|
|
253
|
+
await prefetchPage2(href);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const prefetchPage2 = async (url) => {
|
|
257
|
+
if (!isSafeUrl(url) || !ctx.cache) return;
|
|
258
|
+
const cached = await ctx.cache.get(url);
|
|
259
|
+
if (cached) return;
|
|
260
|
+
if (inflightRequests.has(url)) {
|
|
261
|
+
await inflightRequests.get(url);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
abortControllerRef.current = new AbortController();
|
|
265
|
+
const fetchPromise = fetch(url, {
|
|
266
|
+
headers: { "x-specnav": "1" },
|
|
267
|
+
signal: abortControllerRef.current.signal
|
|
268
|
+
}).then((res) => res.text()).then((html) => {
|
|
269
|
+
ctx.cache?.set(url, html);
|
|
270
|
+
if (ctx.speculator && ctx.adaptive?.shouldSpeculate()) {
|
|
271
|
+
ctx.speculator.speculate(url, html);
|
|
272
|
+
}
|
|
273
|
+
return html;
|
|
274
|
+
}).catch((err) => {
|
|
275
|
+
if (err.name !== "AbortError") {
|
|
276
|
+
console.warn("Prefetch failed:", err);
|
|
277
|
+
}
|
|
278
|
+
throw err;
|
|
279
|
+
}).finally(() => {
|
|
280
|
+
inflightRequests.delete(url);
|
|
281
|
+
});
|
|
282
|
+
inflightRequests.set(url, fetchPromise);
|
|
283
|
+
await fetchPromise;
|
|
284
|
+
};
|
|
285
|
+
const handleClick = async (e) => {
|
|
286
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
if (!isSafeUrl(href)) {
|
|
289
|
+
window.location.href = href;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!morph || !ctx.morpher || !ctx.cache) {
|
|
293
|
+
window.location.href = href;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
ctx.setNavigating(href);
|
|
297
|
+
ctx.setProgress(0.3);
|
|
298
|
+
const timeoutId = setTimeout(() => {
|
|
299
|
+
console.error("Navigation timeout");
|
|
300
|
+
ctx.setNavigating(null);
|
|
301
|
+
window.location.href = href;
|
|
302
|
+
}, 1e4);
|
|
303
|
+
try {
|
|
304
|
+
let newContent = ctx.speculator?.get(href);
|
|
305
|
+
if (!newContent) {
|
|
306
|
+
let html = cache ? await ctx.cache.get(href) : null;
|
|
307
|
+
if (!html) {
|
|
308
|
+
ctx.incrementCacheMiss();
|
|
309
|
+
ctx.setProgress(0.5);
|
|
310
|
+
if (inflightRequests.has(href)) {
|
|
311
|
+
html = await inflightRequests.get(href);
|
|
312
|
+
} else {
|
|
313
|
+
const controller = new AbortController();
|
|
314
|
+
const fetchPromise = fetch(href, {
|
|
315
|
+
headers: { "x-specnav": "1" },
|
|
316
|
+
signal: controller.signal
|
|
317
|
+
}).then((res) => {
|
|
318
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
319
|
+
return res.text();
|
|
320
|
+
});
|
|
321
|
+
inflightRequests.set(href, fetchPromise);
|
|
322
|
+
html = await fetchPromise;
|
|
323
|
+
inflightRequests.delete(href);
|
|
324
|
+
}
|
|
325
|
+
if (cache) {
|
|
326
|
+
await ctx.cache.set(href, html);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
ctx.setProgress(0.7);
|
|
330
|
+
const parser = new DOMParser();
|
|
331
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
332
|
+
newContent = doc.body;
|
|
333
|
+
}
|
|
334
|
+
ctx.setProgress(0.9);
|
|
335
|
+
if ("startViewTransition" in document && ctx.adaptive?.shouldUseTransitions()) {
|
|
336
|
+
await document.startViewTransition(() => {
|
|
337
|
+
ctx.morpher?.morph(document.body, newContent);
|
|
338
|
+
}).finished;
|
|
339
|
+
} else {
|
|
340
|
+
ctx.morpher?.morph(document.body, newContent);
|
|
341
|
+
}
|
|
342
|
+
const currentHref = window.location.pathname;
|
|
343
|
+
if (replace) {
|
|
344
|
+
window.history.replaceState({ specnav: true }, "", href);
|
|
345
|
+
} else {
|
|
346
|
+
window.history.pushState({ specnav: true }, "", href);
|
|
347
|
+
}
|
|
348
|
+
ctx.graph?.recordNavigation(currentHref, href);
|
|
349
|
+
if (scroll) {
|
|
350
|
+
window.scrollTo(0, 0);
|
|
351
|
+
}
|
|
352
|
+
clearTimeout(timeoutId);
|
|
353
|
+
ctx.setProgress(1);
|
|
354
|
+
setTimeout(() => ctx.setNavigating(null), 200);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
clearTimeout(timeoutId);
|
|
357
|
+
console.error("Navigation failed:", error);
|
|
358
|
+
ctx.setNavigating(null);
|
|
359
|
+
window.location.href = href;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
return /* @__PURE__ */ jsx(
|
|
363
|
+
"a",
|
|
364
|
+
{
|
|
365
|
+
ref: linkRef,
|
|
366
|
+
href,
|
|
367
|
+
onClick: handleClick,
|
|
368
|
+
onMouseEnter: handleMouseEnter,
|
|
369
|
+
onMouseLeave: handleMouseLeave,
|
|
370
|
+
onFocus: handleFocus,
|
|
371
|
+
...props,
|
|
372
|
+
children
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/useNavigate.ts
|
|
378
|
+
init_prefetch();
|
|
379
|
+
function useNavigate() {
|
|
380
|
+
const ctx = useNavigationContext();
|
|
381
|
+
const isSafeUrl = (url) => {
|
|
382
|
+
try {
|
|
383
|
+
const parsed = new URL(url, window.location.origin);
|
|
384
|
+
return parsed.origin === window.location.origin;
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const navigate = async (href, options = {}) => {
|
|
390
|
+
const {
|
|
391
|
+
replace = false,
|
|
392
|
+
scroll = true,
|
|
393
|
+
morph = true,
|
|
394
|
+
cache: useCache = true
|
|
395
|
+
} = options;
|
|
396
|
+
if (!isSafeUrl(href)) {
|
|
397
|
+
window.location.href = href;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!morph || !ctx.morpher || !ctx.cache) {
|
|
401
|
+
window.location.href = href;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
ctx.setNavigating(href);
|
|
405
|
+
ctx.setProgress(0.3);
|
|
406
|
+
try {
|
|
407
|
+
let newContent = ctx.speculator?.get(href);
|
|
408
|
+
if (!newContent) {
|
|
409
|
+
let html = useCache ? await ctx.cache.get(href) : null;
|
|
410
|
+
if (!html) {
|
|
411
|
+
ctx.setProgress(0.5);
|
|
412
|
+
if (inflightRequests.has(href)) {
|
|
413
|
+
html = await inflightRequests.get(href);
|
|
414
|
+
} else {
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
const fetchPromise = fetch(href, {
|
|
417
|
+
headers: { "x-specnav": "1" },
|
|
418
|
+
signal: controller.signal
|
|
419
|
+
}).then((res) => {
|
|
420
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
421
|
+
return res.text();
|
|
422
|
+
});
|
|
423
|
+
inflightRequests.set(href, fetchPromise);
|
|
424
|
+
html = await fetchPromise;
|
|
425
|
+
inflightRequests.delete(href);
|
|
426
|
+
}
|
|
427
|
+
if (useCache) {
|
|
428
|
+
await ctx.cache.set(href, html);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
ctx.setProgress(0.7);
|
|
432
|
+
const parser = new DOMParser();
|
|
433
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
434
|
+
newContent = doc.body;
|
|
435
|
+
}
|
|
436
|
+
ctx.setProgress(0.9);
|
|
437
|
+
if ("startViewTransition" in document && ctx.adaptive?.shouldUseTransitions()) {
|
|
438
|
+
await document.startViewTransition(() => {
|
|
439
|
+
ctx.morpher?.morph(document.body, newContent);
|
|
440
|
+
}).finished;
|
|
441
|
+
} else {
|
|
442
|
+
ctx.morpher?.morph(document.body, newContent);
|
|
443
|
+
}
|
|
444
|
+
const currentHref = window.location.pathname;
|
|
445
|
+
if (replace) {
|
|
446
|
+
window.history.replaceState({ specnav: true }, "", href);
|
|
447
|
+
} else {
|
|
448
|
+
window.history.pushState({ specnav: true }, "", href);
|
|
449
|
+
}
|
|
450
|
+
ctx.graph?.recordNavigation(currentHref, href);
|
|
451
|
+
if (scroll) {
|
|
452
|
+
window.scrollTo(0, 0);
|
|
453
|
+
}
|
|
454
|
+
ctx.setProgress(1);
|
|
455
|
+
setTimeout(() => ctx.setNavigating(null), 200);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error("Navigation failed:", error);
|
|
458
|
+
ctx.setNavigating(null);
|
|
459
|
+
window.location.href = href;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
const back = () => {
|
|
463
|
+
window.history.back();
|
|
464
|
+
};
|
|
465
|
+
const forward = () => {
|
|
466
|
+
window.history.forward();
|
|
467
|
+
};
|
|
468
|
+
const prefetch = async (href) => {
|
|
469
|
+
if (!ctx.cache || !isSafeUrl(href)) return;
|
|
470
|
+
try {
|
|
471
|
+
if (inflightRequests.has(href)) {
|
|
472
|
+
await inflightRequests.get(href);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const controller = new AbortController();
|
|
476
|
+
const fetchPromise = fetch(href, {
|
|
477
|
+
headers: { "x-specnav": "1" },
|
|
478
|
+
signal: controller.signal
|
|
479
|
+
}).then((res) => res.text()).then((html) => {
|
|
480
|
+
ctx.cache?.set(href, html);
|
|
481
|
+
if (ctx.speculator && ctx.adaptive?.shouldSpeculate()) {
|
|
482
|
+
ctx.speculator.speculate(href, html);
|
|
483
|
+
}
|
|
484
|
+
return html;
|
|
485
|
+
}).finally(() => {
|
|
486
|
+
inflightRequests.delete(href);
|
|
487
|
+
});
|
|
488
|
+
inflightRequests.set(href, fetchPromise);
|
|
489
|
+
await fetchPromise;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (error.name !== "AbortError") {
|
|
492
|
+
console.warn("Prefetch failed:", error);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const clearCache = (href) => {
|
|
497
|
+
ctx.cache?.clear(href);
|
|
498
|
+
if (href) {
|
|
499
|
+
ctx.speculator?.cancel(href);
|
|
500
|
+
} else {
|
|
501
|
+
ctx.speculator?.cancelAll();
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
return {
|
|
505
|
+
navigate,
|
|
506
|
+
back,
|
|
507
|
+
forward,
|
|
508
|
+
prefetch,
|
|
509
|
+
clearCache,
|
|
510
|
+
isNavigating: ctx.isNavigating,
|
|
511
|
+
pendingHref: ctx.pendingHref
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function useNavigationState() {
|
|
515
|
+
const ctx = useNavigationContext();
|
|
516
|
+
const [previousHref, setPreviousHref] = useState(null);
|
|
517
|
+
const [lastNavigationMs, setLastNavigationMs] = useState(0);
|
|
518
|
+
const [startTime, setStartTime] = useState(null);
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
if (ctx.isNavigating && ctx.pendingHref) {
|
|
521
|
+
setPreviousHref(window.location.pathname);
|
|
522
|
+
setStartTime(Date.now());
|
|
523
|
+
}
|
|
524
|
+
}, [ctx.isNavigating, ctx.pendingHref]);
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
if (!ctx.isNavigating && startTime !== null) {
|
|
527
|
+
setLastNavigationMs(Date.now() - startTime);
|
|
528
|
+
setStartTime(null);
|
|
529
|
+
}
|
|
530
|
+
}, [ctx.isNavigating, startTime]);
|
|
531
|
+
const cacheSize = ctx.cache?.getSize() ?? 0;
|
|
532
|
+
const totalRequests = ctx.cacheHits + ctx.cacheMisses;
|
|
533
|
+
const cacheHitRate = totalRequests > 0 ? ctx.cacheHits / totalRequests : 0;
|
|
534
|
+
return {
|
|
535
|
+
isNavigating: ctx.isNavigating,
|
|
536
|
+
pendingHref: ctx.pendingHref,
|
|
537
|
+
previousHref,
|
|
538
|
+
cacheSize,
|
|
539
|
+
cacheHitRate,
|
|
540
|
+
lastNavigationMs,
|
|
541
|
+
progress: ctx.progress
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export { Link, NavigateProvider, useNavigate, useNavigationState };
|
|
546
|
+
//# sourceMappingURL=index.js.map
|
|
547
|
+
//# sourceMappingURL=index.js.map
|