@timber-js/app 0.2.0-alpha.66 → 0.2.0-alpha.68
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/LICENSE +8 -0
- package/dist/client/history.d.ts +19 -4
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +105 -23
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/nav-link-store.d.ts +36 -0
- package/dist/client/nav-link-store.d.ts.map +1 -0
- package/dist/client/navigation-api-types.d.ts +90 -0
- package/dist/client/navigation-api-types.d.ts.map +1 -0
- package/dist/client/navigation-api.d.ts +115 -0
- package/dist/client/navigation-api.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +11 -0
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/nuqs-adapter.d.ts.map +1 -1
- package/dist/client/router.d.ts +45 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/package.json +6 -7
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +77 -8
- package/src/client/history.ts +26 -4
- package/src/client/link.tsx +29 -7
- package/src/client/nav-link-store.ts +47 -0
- package/src/client/navigation-api-types.ts +112 -0
- package/src/client/navigation-api.ts +305 -0
- package/src/client/navigation-context.ts +20 -0
- package/src/client/nuqs-adapter.tsx +16 -3
- package/src/client/router.ts +148 -16
- package/src/client/rsc-fetch.ts +4 -3
- package/src/client/top-loader.tsx +10 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/client/link.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/client/link.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAiCrF,MAAM,MAAM,eAAe,GAAG;IAC5B,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;AAE7D;;GAEG;AACH,UAAU,aAAc,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACnF,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;OAYG;IACH,oBAAoB,CAAC,EAAE,IAAI,GAAG,MAAM,EAAE,CAAC;IACvC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE;QACb,UAAU,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1D;;OAEG;IACH,YAAY,CAAC,EAAE;QACb,UAAU,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH;AAED,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAUhE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOnD;AAID,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAWpD;AAID;;;;;;;;GAQG;AACH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAE3D;AAqED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC,GACjD,MAAM,CAOR;AAID;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC,EACnD,YAAY,CAAC,EAAE;IACb,UAAU,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,GACA,MAAM,CAyBR;AAID,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IACpD,YAAY,CAAC,EAAE;QACb,UAAU,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH,GACA,eAAe,CAIjB;AAkCD;;;;;;;;;;;GAWG;AACH,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,gBAAgB,EAC9B,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,SAAS,2CA0IX"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Link Store — passes per-link metadata from Link's onClick
|
|
3
|
+
* to the Navigation API's navigate event handler.
|
|
4
|
+
*
|
|
5
|
+
* When the Navigation API is active, Link does NOT call event.preventDefault()
|
|
6
|
+
* or router.navigate(). Instead it stores metadata (scroll option, link
|
|
7
|
+
* pending instance) here, and lets the <a> click propagate naturally.
|
|
8
|
+
* The navigate event handler reads this metadata to configure the RSC
|
|
9
|
+
* navigation with the correct options.
|
|
10
|
+
*
|
|
11
|
+
* This store is consumed once per navigation — after reading, the metadata
|
|
12
|
+
* is cleared. If no metadata is present (e.g., a plain <a> tag without
|
|
13
|
+
* our Link component), the navigate handler uses default options.
|
|
14
|
+
*
|
|
15
|
+
* See design/19-client-navigation.md §"Navigation API Integration"
|
|
16
|
+
*/
|
|
17
|
+
import type { LinkPendingInstance } from './link-pending-store.js';
|
|
18
|
+
export interface NavLinkMetadata {
|
|
19
|
+
/** Whether to scroll to top after navigation. Default: true. */
|
|
20
|
+
scroll: boolean;
|
|
21
|
+
/** The Link's pending state instance for per-link status tracking. */
|
|
22
|
+
linkInstance: LinkPendingInstance | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Store metadata from Link's onClick for the next navigate event.
|
|
26
|
+
* Called synchronously in the click handler — the navigate event
|
|
27
|
+
* fires synchronously after onClick returns.
|
|
28
|
+
*/
|
|
29
|
+
export declare function setNavLinkMetadata(metadata: NavLinkMetadata): void;
|
|
30
|
+
/**
|
|
31
|
+
* Consume the stored metadata. Returns null if no Link onClick
|
|
32
|
+
* preceded this navigation (e.g., plain <a> tag, programmatic nav).
|
|
33
|
+
* Clears the store after reading.
|
|
34
|
+
*/
|
|
35
|
+
export declare function consumeNavLinkMetadata(): NavLinkMetadata | null;
|
|
36
|
+
//# sourceMappingURL=nav-link-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nav-link-store.d.ts","sourceRoot":"","sources":["../../src/client/nav-link-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,EAAE,OAAO,CAAC;IAChB,sEAAsE;IACtE,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC1C;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAElE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,GAAG,IAAI,CAI/D"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient type declarations for the Navigation API.
|
|
3
|
+
*
|
|
4
|
+
* The Navigation API is not yet in TypeScript's standard lib. These types
|
|
5
|
+
* are used internally via type assertions — we never import Navigation API
|
|
6
|
+
* types unconditionally. Progressive enhancement only: the API is feature-
|
|
7
|
+
* detected at runtime.
|
|
8
|
+
*
|
|
9
|
+
* See https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API
|
|
10
|
+
*/
|
|
11
|
+
export interface NavigationHistoryEntry {
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly url: string | null;
|
|
15
|
+
readonly index: number;
|
|
16
|
+
readonly sameDocument: boolean;
|
|
17
|
+
getState(): unknown;
|
|
18
|
+
addEventListener(type: string, listener: EventListener): void;
|
|
19
|
+
removeEventListener(type: string, listener: EventListener): void;
|
|
20
|
+
}
|
|
21
|
+
export interface NavigationDestination {
|
|
22
|
+
readonly url: string;
|
|
23
|
+
readonly key: string | null;
|
|
24
|
+
readonly id: string | null;
|
|
25
|
+
readonly index: number;
|
|
26
|
+
readonly sameDocument: boolean;
|
|
27
|
+
getState(): unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface NavigateEvent extends Event {
|
|
30
|
+
readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';
|
|
31
|
+
readonly destination: NavigationDestination;
|
|
32
|
+
readonly canIntercept: boolean;
|
|
33
|
+
readonly userInitiated: boolean;
|
|
34
|
+
readonly hashChange: boolean;
|
|
35
|
+
readonly signal: AbortSignal;
|
|
36
|
+
readonly formData: FormData | null;
|
|
37
|
+
readonly downloadRequest: string | null;
|
|
38
|
+
readonly info: unknown;
|
|
39
|
+
intercept(options?: NavigateInterceptOptions): void;
|
|
40
|
+
scroll(): void;
|
|
41
|
+
}
|
|
42
|
+
export interface NavigateInterceptOptions {
|
|
43
|
+
handler?: () => Promise<void>;
|
|
44
|
+
focusReset?: 'after-transition' | 'manual';
|
|
45
|
+
scroll?: 'after-transition' | 'manual';
|
|
46
|
+
}
|
|
47
|
+
export interface NavigationTransition {
|
|
48
|
+
readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';
|
|
49
|
+
readonly from: NavigationHistoryEntry;
|
|
50
|
+
readonly finished: Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
export interface NavigationResult {
|
|
53
|
+
committed: Promise<NavigationHistoryEntry>;
|
|
54
|
+
finished: Promise<NavigationHistoryEntry>;
|
|
55
|
+
}
|
|
56
|
+
export interface NavigationApi {
|
|
57
|
+
readonly currentEntry: NavigationHistoryEntry | null;
|
|
58
|
+
readonly transition: NavigationTransition | null;
|
|
59
|
+
readonly canGoBack: boolean;
|
|
60
|
+
readonly canGoForward: boolean;
|
|
61
|
+
entries(): NavigationHistoryEntry[];
|
|
62
|
+
navigate(url: string, options?: NavigationNavigateOptions): NavigationResult;
|
|
63
|
+
reload(options?: NavigationReloadOptions): NavigationResult;
|
|
64
|
+
traverseTo(key: string, options?: NavigationOptions): NavigationResult;
|
|
65
|
+
back(options?: NavigationOptions): NavigationResult;
|
|
66
|
+
forward(options?: NavigationOptions): NavigationResult;
|
|
67
|
+
updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void;
|
|
68
|
+
addEventListener(type: 'navigate', listener: (event: NavigateEvent) => void): void;
|
|
69
|
+
addEventListener(type: 'navigatesuccess', listener: (event: Event) => void): void;
|
|
70
|
+
addEventListener(type: 'navigateerror', listener: (event: Event) => void): void;
|
|
71
|
+
addEventListener(type: 'currententrychange', listener: (event: Event) => void): void;
|
|
72
|
+
addEventListener(type: string, listener: EventListener): void;
|
|
73
|
+
removeEventListener(type: string, listener: EventListener): void;
|
|
74
|
+
}
|
|
75
|
+
export interface NavigationNavigateOptions {
|
|
76
|
+
state?: unknown;
|
|
77
|
+
history?: 'auto' | 'push' | 'replace';
|
|
78
|
+
info?: unknown;
|
|
79
|
+
}
|
|
80
|
+
export interface NavigationReloadOptions {
|
|
81
|
+
state?: unknown;
|
|
82
|
+
info?: unknown;
|
|
83
|
+
}
|
|
84
|
+
export interface NavigationOptions {
|
|
85
|
+
info?: unknown;
|
|
86
|
+
}
|
|
87
|
+
export interface NavigationUpdateCurrentEntryOptions {
|
|
88
|
+
state: unknown;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=navigation-api-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation-api-types.d.ts","sourceRoot":"","sources":["../../src/client/navigation-api-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation API integration — progressive enhancement for client navigation.
|
|
3
|
+
*
|
|
4
|
+
* When the Navigation API (`window.navigation`) is available, this module
|
|
5
|
+
* provides an intercept-based navigation model that replaces the separate
|
|
6
|
+
* popstate + click handler approach with a single navigate event listener.
|
|
7
|
+
*
|
|
8
|
+
* Key benefits:
|
|
9
|
+
* - Intercepts ALL navigations (link clicks, form submissions, back/forward)
|
|
10
|
+
* - Built-in AbortSignal per navigation (auto-aborts in-flight fetches)
|
|
11
|
+
* - Per-entry state via NavigationHistoryEntry.getState()
|
|
12
|
+
* - navigation.transition for progress tracking
|
|
13
|
+
*
|
|
14
|
+
* When unavailable, all functions are no-ops and the History API fallback
|
|
15
|
+
* in browser-entry.ts handles navigation.
|
|
16
|
+
*
|
|
17
|
+
* See design/19-client-navigation.md
|
|
18
|
+
*/
|
|
19
|
+
import type { NavigationApi } from './navigation-api-types.js';
|
|
20
|
+
/**
|
|
21
|
+
* Returns true if the Navigation API is available in the current environment.
|
|
22
|
+
* Feature-detected at runtime — no polyfill.
|
|
23
|
+
*/
|
|
24
|
+
export declare function hasNavigationApi(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Get the Navigation API instance. Returns null if unavailable.
|
|
27
|
+
* Uses type assertion — we never import Navigation API types unconditionally.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getNavigationApi(): NavigationApi | null;
|
|
30
|
+
/**
|
|
31
|
+
* Callbacks for the Navigation API event handler.
|
|
32
|
+
*
|
|
33
|
+
* When the Navigation API intercepts a navigation, it delegates to these
|
|
34
|
+
* callbacks which run the RSC fetch + render pipeline.
|
|
35
|
+
*/
|
|
36
|
+
export interface NavigationApiCallbacks {
|
|
37
|
+
/**
|
|
38
|
+
* Handle a push/replace navigation intercepted by the Navigation API.
|
|
39
|
+
* This covers both Link <a> clicks (user-initiated, with metadata from
|
|
40
|
+
* nav-link-store) and external navigations (plain <a> tags, programmatic).
|
|
41
|
+
* The Navigation API handles the URL update via event.intercept().
|
|
42
|
+
*/
|
|
43
|
+
onExternalNavigate: (url: string, options: {
|
|
44
|
+
replace: boolean;
|
|
45
|
+
signal: AbortSignal;
|
|
46
|
+
scroll?: boolean;
|
|
47
|
+
}) => Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Handle a traversal (back/forward button). The Navigation API intercepts
|
|
50
|
+
* the traversal and delegates to us for RSC replay/fetch.
|
|
51
|
+
*/
|
|
52
|
+
onTraverse: (url: string, scrollY: number, signal: AbortSignal) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Controller returned by setupNavigationApi. Provides methods to
|
|
56
|
+
* coordinate between the router and the navigate event listener.
|
|
57
|
+
*/
|
|
58
|
+
export interface NavigationApiController {
|
|
59
|
+
/**
|
|
60
|
+
* Set the router-navigating flag. When `true`, the next navigate event
|
|
61
|
+
* (from pushState/replaceState) is recognized as router-initiated. The
|
|
62
|
+
* handler still intercepts it — but ties the browser's native loading
|
|
63
|
+
* state to a deferred promise instead of running the RSC pipeline again.
|
|
64
|
+
*
|
|
65
|
+
* This means `navigation.transition` is active for the full duration of
|
|
66
|
+
* every router-initiated navigation, giving the browser a native loading
|
|
67
|
+
* indicator (tab spinner, address bar) aligned with the TopLoader.
|
|
68
|
+
*
|
|
69
|
+
* Must be called synchronously around pushState/replaceState:
|
|
70
|
+
* controller.setRouterNavigating(true);
|
|
71
|
+
* history.pushState(...); // navigate event fires, intercepted
|
|
72
|
+
* controller.setRouterNavigating(false); // flag off, deferred stays open
|
|
73
|
+
*/
|
|
74
|
+
setRouterNavigating: (value: boolean) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the deferred promise created by setRouterNavigating(true),
|
|
77
|
+
* clearing the browser's native loading state. Call this when the
|
|
78
|
+
* navigation fully completes — aligned with when the TopLoader's
|
|
79
|
+
* pendingUrl clears (same finally block in router.navigate).
|
|
80
|
+
*/
|
|
81
|
+
completeRouterNavigation: () => void;
|
|
82
|
+
/**
|
|
83
|
+
* Initiate a navigation via the Navigation API (`navigation.navigate()`).
|
|
84
|
+
* Unlike `history.pushState()`, this fires the navigate event BEFORE
|
|
85
|
+
* committing the URL — allowing Chrome to show its native loading
|
|
86
|
+
* indicator while the intercept handler runs.
|
|
87
|
+
*
|
|
88
|
+
* Must be called with setRouterNavigating(true) active so the handler
|
|
89
|
+
* recognizes it as router-initiated and uses the deferred promise.
|
|
90
|
+
*/
|
|
91
|
+
navigate: (url: string, replace: boolean) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Save scroll position into the current navigation entry's state.
|
|
94
|
+
* Uses navigation.updateCurrentEntry() for per-entry scroll storage.
|
|
95
|
+
*/
|
|
96
|
+
saveScrollPosition: (scrollY: number) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Check if the Navigation API has an active transition.
|
|
99
|
+
* Returns the transition object if available, null otherwise.
|
|
100
|
+
*/
|
|
101
|
+
hasActiveTransition: () => boolean;
|
|
102
|
+
/** Remove the navigate event listener. */
|
|
103
|
+
cleanup: () => void;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set up the Navigation API navigate event listener.
|
|
107
|
+
*
|
|
108
|
+
* Intercepts same-origin navigations and delegates to the provided callbacks.
|
|
109
|
+
* Router-initiated navigations (pushState from router.navigate) are detected
|
|
110
|
+
* via a synchronous flag and NOT intercepted — the router already handles them.
|
|
111
|
+
*
|
|
112
|
+
* Returns a controller for coordinating with the router.
|
|
113
|
+
*/
|
|
114
|
+
export declare function setupNavigationApi(callbacks: NavigationApiCallbacks): NavigationApiController;
|
|
115
|
+
//# sourceMappingURL=navigation-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation-api.d.ts","sourceRoot":"","sources":["../../src/client/navigation-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,2BAA2B,CAAC;AAK9E;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAM1C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAGvD;AAID;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,kBAAkB,EAAE,CAClB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KACjE,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnB;;;OAGG;IACH,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAE9C;;;;;OAKG;IACH,wBAAwB,EAAE,MAAM,IAAI,CAAC;IAErC;;;;;;;;OAQG;IACH,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAElD;;;OAGG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAE9C;;;OAGG;IACH,mBAAmB,EAAE,MAAM,OAAO,CAAC;IAEnC,0CAA0C;IAC1C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,sBAAsB,GAAG,uBAAuB,CAsK7F"}
|
|
@@ -73,4 +73,15 @@ export declare function PendingNavigationProvider({ value, children, }: {
|
|
|
73
73
|
value: string | null;
|
|
74
74
|
children?: ReactNode;
|
|
75
75
|
}): React.ReactElement;
|
|
76
|
+
/**
|
|
77
|
+
* Check if the browser's Navigation API has an active transition.
|
|
78
|
+
*
|
|
79
|
+
* When the Navigation API is available and a navigation has been intercepted
|
|
80
|
+
* via event.intercept(), `navigation.transition` is non-null until the
|
|
81
|
+
* handler resolves. This provides browser-native progress tracking that
|
|
82
|
+
* can be used alongside the existing pendingUrl mechanism.
|
|
83
|
+
*
|
|
84
|
+
* Returns false when Navigation API is unavailable or no transition is active.
|
|
85
|
+
*/
|
|
86
|
+
export declare function hasNativeNavigationTransition(): boolean;
|
|
76
87
|
//# sourceMappingURL=navigation-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-context.d.ts","sourceRoot":"","sources":["../../src/client/navigation-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuCD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,GAAG,IAAI,CAM7D;AAMD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,GACT,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CAO9C;AA6BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAE/D;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AA8BD;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAKvD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,GAAG,KAAK,CAAC,YAAY,CAMrB"}
|
|
1
|
+
{"version":3,"file":"navigation-context.d.ts","sourceRoot":"","sources":["../../src/client/navigation-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuCD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,GAAG,IAAI,CAM7D;AAMD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,GACT,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CAO9C;AA6BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAE/D;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AA8BD;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAKvD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,GAAG,KAAK,CAAC,YAAY,CAMrB;AAMD;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAIvD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nuqs-adapter.d.ts","sourceRoot":"","sources":["../../src/client/nuqs-adapter.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"nuqs-adapter.d.ts","sourceRoot":"","sources":["../../src/client/nuqs-adapter.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAgGrE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAatE"}
|
package/dist/client/router.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ export interface NavigationOptions {
|
|
|
8
8
|
scroll?: boolean;
|
|
9
9
|
/** Use replaceState instead of pushState (replaces current history entry) */
|
|
10
10
|
replace?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* @internal AbortSignal from the Navigation API's NavigateEvent.
|
|
13
|
+
* When provided, the signal is linked to the router's per-navigation
|
|
14
|
+
* AbortController so in-flight RSC fetches are cancelled when a new
|
|
15
|
+
* navigation starts.
|
|
16
|
+
*/
|
|
17
|
+
_signal?: AbortSignal;
|
|
18
|
+
/**
|
|
19
|
+
* @internal Skip pushState/replaceState — the Navigation API has already
|
|
20
|
+
* updated the URL via event.intercept(). Used for external navigations
|
|
21
|
+
* intercepted by the navigate event handler.
|
|
22
|
+
*/
|
|
23
|
+
_skipHistory?: boolean;
|
|
11
24
|
}
|
|
12
25
|
/**
|
|
13
26
|
* Function that decodes an RSC Flight stream into a React element tree.
|
|
@@ -62,6 +75,37 @@ export interface RouterDeps {
|
|
|
62
75
|
* If not provided (tests), the router falls back to renderRoot.
|
|
63
76
|
*/
|
|
64
77
|
navigateTransition?: (pendingUrl: string, perform: (wrapPayload: (payload: unknown, navState: NavigationState) => unknown) => Promise<unknown>) => Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Whether the Navigation API is active and handling traversals.
|
|
80
|
+
* When true, the popstate handler is a no-op — the Navigation API's
|
|
81
|
+
* navigate event covers back/forward button presses.
|
|
82
|
+
*/
|
|
83
|
+
navigationApiActive?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Called around pushState/replaceState to set a flag that prevents
|
|
86
|
+
* the Navigation API's navigate listener from double-handling
|
|
87
|
+
* router-initiated navigations.
|
|
88
|
+
*/
|
|
89
|
+
setRouterNavigating?: (value: boolean) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Save scroll position via the Navigation API's per-entry state.
|
|
92
|
+
* When provided, used instead of history.replaceState for scroll storage.
|
|
93
|
+
*/
|
|
94
|
+
saveNavigationEntryScroll?: (scrollY: number) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Signal that a router-initiated navigation has completed. Resolves the
|
|
97
|
+
* deferred promise that ties the browser's native loading state to the
|
|
98
|
+
* navigation lifecycle. Called in the finally block of navigate/refresh,
|
|
99
|
+
* aligned with when the TopLoader's pendingUrl clears.
|
|
100
|
+
*/
|
|
101
|
+
completeRouterNavigation?: () => void;
|
|
102
|
+
/**
|
|
103
|
+
* Initiate a navigation via the Navigation API (`navigation.navigate()`).
|
|
104
|
+
* Fires the navigate event BEFORE committing the URL, allowing Chrome
|
|
105
|
+
* to show its native loading indicator. Falls back to pushState when
|
|
106
|
+
* unavailable.
|
|
107
|
+
*/
|
|
108
|
+
navigationNavigate?: (url: string, replace: boolean) => void;
|
|
65
109
|
}
|
|
66
110
|
export interface RouterInstance {
|
|
67
111
|
/** Navigate to a new URL (forward navigation) */
|
|
@@ -69,7 +113,7 @@ export interface RouterInstance {
|
|
|
69
113
|
/** Full re-render of the current URL — no state tree sent */
|
|
70
114
|
refresh(): Promise<void>;
|
|
71
115
|
/** Handle a popstate event (back/forward button). scrollY is read from history.state. */
|
|
72
|
-
handlePopState(url: string, scrollY?: number): Promise<void>;
|
|
116
|
+
handlePopState(url: string, scrollY?: number, externalSignal?: AbortSignal): Promise<void>;
|
|
73
117
|
/** Whether a navigation is currently in flight */
|
|
74
118
|
isPending(): boolean;
|
|
75
119
|
/** The URL currently being navigated to, or null if idle */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/client/router.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAoB,MAAM,iBAAiB,CAAC;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,yBAAyB,CAAC;AAYjC,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/client/router.ts"],"names":[],"mappings":"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"}
|
|
@@ -111,5 +111,5 @@ export declare class ServerErrorResponse extends Error {
|
|
|
111
111
|
*/
|
|
112
112
|
export declare function fetchRscPayload(url: string, deps: RouterDeps, stateTree?: {
|
|
113
113
|
segments: string[];
|
|
114
|
-
}, currentUrl?: string): Promise<FetchResult>;
|
|
114
|
+
}, currentUrl?: string, signal?: AbortSignal): Promise<FetchResult>;
|
|
115
115
|
//# sourceMappingURL=rsc-fetch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc-fetch.d.ts","sourceRoot":"","sources":["../../src/client/rsc-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,uFAAuF;IACvF,WAAW,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAClC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IACjD,+EAA+E;IAC/E,eAAe,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAClC;AAID,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAoCnD,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE7D;AAED,oCAAoC;AACpC,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAErD;AAID,sEAAsE;AACtE,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C,kDAAkD;AAClD,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAE7D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAE7D;AAID,wBAAgB,eAAe,CAC7B,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,EAC7C,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBxB;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,GAAG,IAAI,CAQ5E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,GAAG,IAAI,CAQ3E;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,EAAE,GAAG,IAAI,CAS1E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAQ1F;AAID;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBACjB,GAAG,EAAE,MAAM;CAIxB;AAED;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;;CAI1C;AAED;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBACT,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;CAKxC;AAID;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,UAAU,EAChB,SAAS,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,EAClC,UAAU,CAAC,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"rsc-fetch.d.ts","sourceRoot":"","sources":["../../src/client/rsc-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,uFAAuF;IACvF,WAAW,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAClC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IACjD,+EAA+E;IAC/E,eAAe,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAClC;AAID,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAoCnD,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE7D;AAED,oCAAoC;AACpC,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAErD;AAID,sEAAsE;AACtE,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C,kDAAkD;AAClD,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAE7D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAE7D;AAID,wBAAgB,eAAe,CAC7B,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,EAC7C,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBxB;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,GAAG,IAAI,CAQ5E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,GAAG,IAAI,CAQ3E;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,EAAE,GAAG,IAAI,CAS1E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAQ1F;AAID;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBACjB,GAAG,EAAE,MAAM;CAIxB;AAED;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;;CAI1C;AAED;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBACT,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;CAKxC;AAID;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,UAAU,EAChB,SAAS,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,EAClC,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAoEtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"top-loader.d.ts","sourceRoot":"","sources":["../../src/client/top-loader.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AASH,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAmDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,eAAe,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"top-loader.d.ts","sourceRoot":"","sources":["../../src/client/top-loader.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AASH,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAmDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,eAAe,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAmG7F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.68",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -88,11 +88,6 @@
|
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
90
90
|
},
|
|
91
|
-
"scripts": {
|
|
92
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
93
|
-
"typecheck": "tsgo --noEmit",
|
|
94
|
-
"prepublishOnly": "pnpm run build"
|
|
95
|
-
},
|
|
96
91
|
"dependencies": {
|
|
97
92
|
"@opentelemetry/api": "^1.9.1",
|
|
98
93
|
"@opentelemetry/context-async-hooks": "^2.6.1",
|
|
@@ -131,5 +126,9 @@
|
|
|
131
126
|
},
|
|
132
127
|
"engines": {
|
|
133
128
|
"node": ">=22.12.0"
|
|
129
|
+
},
|
|
130
|
+
"scripts": {
|
|
131
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
132
|
+
"typecheck": "tsgo --noEmit"
|
|
134
133
|
}
|
|
135
|
-
}
|
|
134
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
|
@@ -76,6 +76,11 @@ import {
|
|
|
76
76
|
DEPLOYMENT_ID_HEADER,
|
|
77
77
|
RELOAD_HEADER,
|
|
78
78
|
} from './rsc-fetch.js';
|
|
79
|
+
import {
|
|
80
|
+
hasNavigationApi,
|
|
81
|
+
setupNavigationApi,
|
|
82
|
+
type NavigationApiController,
|
|
83
|
+
} from './navigation-api.js';
|
|
79
84
|
|
|
80
85
|
// ─── Server Action Dispatch ──────────────────────────────────────
|
|
81
86
|
|
|
@@ -257,6 +262,12 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
257
262
|
// Assigned inside initRouter() which is called in both branches.
|
|
258
263
|
let router!: RouterInstance;
|
|
259
264
|
|
|
265
|
+
// Navigation API controller — initialized when the API is available.
|
|
266
|
+
// Declared here (before the hydration if/else) because initRouter()
|
|
267
|
+
// is called from runPreHydration() inside both branches, and it
|
|
268
|
+
// assigns to this variable. Must be in scope before first use.
|
|
269
|
+
let navApiController: NavigationApiController | null = null;
|
|
270
|
+
|
|
260
271
|
if (timberChunks) {
|
|
261
272
|
const encoder = new TextEncoder();
|
|
262
273
|
|
|
@@ -480,10 +491,16 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
480
491
|
// the initial render. renderRoot uses transitionRender which is set
|
|
481
492
|
// by the TransitionRoot component during hydration.
|
|
482
493
|
function initRouter(): void {
|
|
494
|
+
// Feature-detect Navigation API. When available, the navigate event
|
|
495
|
+
// replaces popstate for back/forward and catches external navigations.
|
|
496
|
+
// See design/19-client-navigation.md §"Navigation API Integration"
|
|
497
|
+
const useNavApi = hasNavigationApi();
|
|
498
|
+
|
|
483
499
|
const deps: RouterDeps = {
|
|
484
500
|
fetch: (url, init) => window.fetch(url, init),
|
|
485
501
|
pushState: (data, unused, url) => window.history.pushState(data, unused, url),
|
|
486
502
|
replaceState: (data, unused, url) => window.history.replaceState(data, unused, url),
|
|
503
|
+
navigationApiActive: useNavApi,
|
|
487
504
|
scrollTo: (x, y) => {
|
|
488
505
|
// Scroll the document viewport.
|
|
489
506
|
window.scrollTo(x, y);
|
|
@@ -584,6 +601,37 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
584
601
|
|
|
585
602
|
router = createRouter(deps);
|
|
586
603
|
setGlobalRouter(router);
|
|
604
|
+
|
|
605
|
+
// Set up Navigation API integration after router is created.
|
|
606
|
+
// The navigate event listener delegates to router.navigate and
|
|
607
|
+
// router.handlePopState for external navigations and traversals.
|
|
608
|
+
if (useNavApi) {
|
|
609
|
+
navApiController = setupNavigationApi({
|
|
610
|
+
onExternalNavigate: async (url, { replace, signal, scroll }) => {
|
|
611
|
+
// Navigation intercepted by the Navigation API. Covers both
|
|
612
|
+
// Link <a> clicks (user-initiated) and external navigations.
|
|
613
|
+
// The Navigation API handles the URL update via intercept(),
|
|
614
|
+
// so pass _skipHistory to avoid double pushState.
|
|
615
|
+
await router.navigate(url, {
|
|
616
|
+
replace,
|
|
617
|
+
scroll,
|
|
618
|
+
_signal: signal,
|
|
619
|
+
_skipHistory: true,
|
|
620
|
+
});
|
|
621
|
+
},
|
|
622
|
+
onTraverse: async (url, scrollY, signal) => {
|
|
623
|
+
// Back/forward — delegate to the router's popstate handler.
|
|
624
|
+
await router.handlePopState(url, scrollY, signal);
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Wire the router-navigating flag into RouterDeps.
|
|
629
|
+
// This must be done after setupNavigationApi returns the controller.
|
|
630
|
+
deps.setRouterNavigating = (v) => navApiController!.setRouterNavigating(v);
|
|
631
|
+
deps.saveNavigationEntryScroll = (y) => navApiController!.saveScrollPosition(y);
|
|
632
|
+
deps.completeRouterNavigation = () => navApiController!.completeRouterNavigation();
|
|
633
|
+
deps.navigationNavigate = (url, replace) => navApiController!.navigate(url, replace);
|
|
634
|
+
}
|
|
587
635
|
}
|
|
588
636
|
|
|
589
637
|
// ── Pre-hydration sequence ──────────────────────────────────────────
|
|
@@ -619,9 +667,17 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
619
667
|
headElements: null, // SSR already set the correct head
|
|
620
668
|
});
|
|
621
669
|
|
|
622
|
-
// Initialize
|
|
623
|
-
//
|
|
624
|
-
|
|
670
|
+
// Initialize scroll state for the initial entry.
|
|
671
|
+
// When Navigation API is available, use per-entry state.
|
|
672
|
+
// Otherwise fall back to history.state.
|
|
673
|
+
// Note: navApiController is assigned inside initRouter() which runs
|
|
674
|
+
// synchronously before this point via runPreHydration().
|
|
675
|
+
const navApi = navApiController as NavigationApiController | null;
|
|
676
|
+
if (navApi) {
|
|
677
|
+
navApi.saveScrollPosition(0);
|
|
678
|
+
} else {
|
|
679
|
+
window.history.replaceState({ timber: true, scrollY: 0 }, '');
|
|
680
|
+
}
|
|
625
681
|
|
|
626
682
|
// Populate the segment cache from server-embedded segment metadata.
|
|
627
683
|
// This enables state tree diffing from the very first client navigation.
|
|
@@ -653,27 +709,40 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
653
709
|
}
|
|
654
710
|
|
|
655
711
|
// Register popstate handler for back/forward navigation.
|
|
712
|
+
// When Navigation API is active, the navigate event covers traversals —
|
|
713
|
+
// popstate is a no-op. When unavailable, popstate handles back/forward.
|
|
714
|
+
//
|
|
656
715
|
// Use pathname+search (not full href) to match the URL format used by
|
|
657
716
|
// navigate() — Link hrefs are relative paths like "/scroll-test/page-a".
|
|
658
717
|
// Read scrollY from history.state — the browser maintains per-entry state
|
|
659
718
|
// so duplicate URLs in history each have their own scroll position.
|
|
660
719
|
window.addEventListener('popstate', () => {
|
|
720
|
+
// Navigation API handles traversals via the navigate event.
|
|
721
|
+
if (navApiController) return;
|
|
722
|
+
|
|
661
723
|
const state = window.history.state;
|
|
662
724
|
const scrollY = state && typeof state.scrollY === 'number' ? state.scrollY : 0;
|
|
663
725
|
void router.handlePopState(window.location.pathname + window.location.search, scrollY);
|
|
664
726
|
});
|
|
665
727
|
|
|
666
|
-
// Keep
|
|
728
|
+
// Keep scroll position up to date as the user scrolls.
|
|
667
729
|
// This ensures that when the user presses back/forward, the departing
|
|
668
730
|
// page's scroll position is already saved in its history entry.
|
|
669
|
-
//
|
|
731
|
+
// When Navigation API is available, uses per-entry state via
|
|
732
|
+
// navigation.updateCurrentEntry(). Otherwise falls back to history.state.
|
|
733
|
+
// Debounced to avoid excessive state updates during smooth scrolling.
|
|
670
734
|
let scrollTimer: ReturnType<typeof setTimeout>;
|
|
671
735
|
function saveScrollPosition(): void {
|
|
672
736
|
clearTimeout(scrollTimer);
|
|
673
737
|
scrollTimer = setTimeout(() => {
|
|
674
|
-
const
|
|
675
|
-
if (
|
|
676
|
-
|
|
738
|
+
const y = getScrollY();
|
|
739
|
+
if (navApiController) {
|
|
740
|
+
navApiController.saveScrollPosition(y);
|
|
741
|
+
} else {
|
|
742
|
+
const state = window.history.state;
|
|
743
|
+
if (state && typeof state === 'object') {
|
|
744
|
+
window.history.replaceState({ ...state, scrollY: y }, '');
|
|
745
|
+
}
|
|
677
746
|
}
|
|
678
747
|
}, 100);
|
|
679
748
|
}
|
package/src/client/history.ts
CHANGED
|
@@ -23,20 +23,42 @@ export interface HistoryEntry {
|
|
|
23
23
|
* On forward navigation, the new page's payload is pushed onto the stack.
|
|
24
24
|
* On popstate, the cached payload is replayed instantly.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Supports two keying modes:
|
|
27
|
+
* - **URL-keyed** (default): entries keyed by pathname + search.
|
|
28
|
+
* Used with the History API fallback.
|
|
29
|
+
* - **Entry-key + URL**: when the Navigation API is available,
|
|
30
|
+
* entries can also be stored by Navigation entry key for
|
|
31
|
+
* disambiguation of duplicate URLs in the history stack.
|
|
32
|
+
* Falls back to URL lookup when entry key is not found.
|
|
33
|
+
*
|
|
34
|
+
* Scroll positions are stored in history.state or Navigation API entry
|
|
35
|
+
* state, not in this stack — see design/19-client-navigation.md §Scroll Restoration.
|
|
28
36
|
*
|
|
29
37
|
* Entries persist for the session duration (no expiry) and are cleared
|
|
30
38
|
* when the tab is closed — matching browser back-button behavior.
|
|
31
39
|
*/
|
|
32
40
|
export class HistoryStack {
|
|
33
41
|
private entries = new Map<string, HistoryEntry>();
|
|
42
|
+
/** Entries keyed by Navigation API entry key for duplicate URL disambiguation. */
|
|
43
|
+
private entryKeyMap = new Map<string, HistoryEntry>();
|
|
34
44
|
|
|
35
|
-
push(url: string, entry: HistoryEntry): void {
|
|
45
|
+
push(url: string, entry: HistoryEntry, entryKey?: string): void {
|
|
36
46
|
this.entries.set(url, entry);
|
|
47
|
+
if (entryKey) {
|
|
48
|
+
this.entryKeyMap.set(entryKey, entry);
|
|
49
|
+
}
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Get an entry. When an entry key is provided (Navigation API),
|
|
54
|
+
* tries the entry-key map first for accurate disambiguation of
|
|
55
|
+
* duplicate URLs, then falls back to URL lookup.
|
|
56
|
+
*/
|
|
57
|
+
get(url: string, entryKey?: string): HistoryEntry | undefined {
|
|
58
|
+
if (entryKey) {
|
|
59
|
+
const byKey = this.entryKeyMap.get(entryKey);
|
|
60
|
+
if (byKey) return byKey;
|
|
61
|
+
}
|
|
40
62
|
return this.entries.get(url);
|
|
41
63
|
}
|
|
42
64
|
|