@fictjs/router 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2373 -3
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1136 -2
- package/dist/index.d.ts +1136 -2
- package/dist/index.js +2305 -3
- package/dist/index.js.map +1 -0
- package/package.json +33 -7
- package/src/components.tsx +926 -0
- package/src/context.ts +404 -0
- package/src/data.ts +545 -0
- package/src/history.ts +659 -0
- package/src/index.ts +217 -0
- package/src/lazy.tsx +242 -0
- package/src/link.tsx +601 -0
- package/src/scroll.ts +245 -0
- package/src/types.ts +447 -0
- package/src/utils.ts +570 -0
package/src/scroll.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Scroll restoration utilities for @fictjs/router
|
|
3
|
+
*
|
|
4
|
+
* This module provides scroll position management including:
|
|
5
|
+
* - Saving scroll positions per location key
|
|
6
|
+
* - Restoring scroll on back/forward navigation
|
|
7
|
+
* - Scrolling to top on new navigation
|
|
8
|
+
* - Hash scrolling support (#section)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Location } from './types'
|
|
12
|
+
import { isBrowser } from './utils'
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Scroll Position Storage
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/** Stored scroll positions keyed by location key */
|
|
19
|
+
const scrollPositions = new Map<string, { x: number; y: number }>()
|
|
20
|
+
|
|
21
|
+
/** Maximum number of positions to store to prevent memory leaks */
|
|
22
|
+
const MAX_STORED_POSITIONS = 100
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Save the current scroll position for a location
|
|
26
|
+
*/
|
|
27
|
+
export function saveScrollPosition(key: string): void {
|
|
28
|
+
if (!isBrowser()) return
|
|
29
|
+
|
|
30
|
+
scrollPositions.set(key, {
|
|
31
|
+
x: window.scrollX,
|
|
32
|
+
y: window.scrollY,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Evict oldest entries if we exceed the limit
|
|
36
|
+
if (scrollPositions.size > MAX_STORED_POSITIONS) {
|
|
37
|
+
const firstKey = scrollPositions.keys().next().value
|
|
38
|
+
if (firstKey) {
|
|
39
|
+
scrollPositions.delete(firstKey)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the saved scroll position for a location
|
|
46
|
+
*/
|
|
47
|
+
export function getSavedScrollPosition(key: string): { x: number; y: number } | undefined {
|
|
48
|
+
return scrollPositions.get(key)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear saved scroll position for a location
|
|
53
|
+
*/
|
|
54
|
+
export function clearScrollPosition(key: string): void {
|
|
55
|
+
scrollPositions.delete(key)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clear all saved scroll positions
|
|
60
|
+
*/
|
|
61
|
+
export function clearAllScrollPositions(): void {
|
|
62
|
+
scrollPositions.clear()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Scroll Actions
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Scroll to a specific position
|
|
71
|
+
*/
|
|
72
|
+
export function scrollTo(x: number, y: number, behavior: ScrollBehavior = 'auto'): void {
|
|
73
|
+
if (!isBrowser()) return
|
|
74
|
+
|
|
75
|
+
window.scrollTo({
|
|
76
|
+
left: x,
|
|
77
|
+
top: y,
|
|
78
|
+
behavior,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Scroll to top of the page
|
|
84
|
+
*/
|
|
85
|
+
export function scrollToTop(behavior: ScrollBehavior = 'auto'): void {
|
|
86
|
+
scrollTo(0, 0, behavior)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Scroll to an element by ID (hash navigation)
|
|
91
|
+
*/
|
|
92
|
+
export function scrollToHash(hash: string, behavior: ScrollBehavior = 'auto'): boolean {
|
|
93
|
+
if (!isBrowser() || !hash) return false
|
|
94
|
+
|
|
95
|
+
// Remove the leading #
|
|
96
|
+
const id = hash.startsWith('#') ? hash.slice(1) : hash
|
|
97
|
+
if (!id) return false
|
|
98
|
+
|
|
99
|
+
const element = document.getElementById(id)
|
|
100
|
+
if (element) {
|
|
101
|
+
element.scrollIntoView({ behavior })
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Restore scroll position for a location
|
|
110
|
+
* Returns true if position was restored
|
|
111
|
+
*/
|
|
112
|
+
export function restoreScrollPosition(key: string): boolean {
|
|
113
|
+
if (!isBrowser()) return false
|
|
114
|
+
|
|
115
|
+
const position = scrollPositions.get(key)
|
|
116
|
+
if (position) {
|
|
117
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
118
|
+
requestAnimationFrame(() => {
|
|
119
|
+
scrollTo(position.x, position.y)
|
|
120
|
+
})
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Scroll Restoration Manager
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
export interface ScrollRestorationOptions {
|
|
132
|
+
/** Whether scroll restoration is enabled */
|
|
133
|
+
enabled?: boolean
|
|
134
|
+
/** Whether to restore scroll on back/forward navigation */
|
|
135
|
+
restoreOnPop?: boolean
|
|
136
|
+
/** Whether to scroll to top on push navigation */
|
|
137
|
+
scrollToTopOnPush?: boolean
|
|
138
|
+
/** Default scroll behavior */
|
|
139
|
+
behavior?: ScrollBehavior
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const defaultOptions: Required<ScrollRestorationOptions> = {
|
|
143
|
+
enabled: true,
|
|
144
|
+
restoreOnPop: true,
|
|
145
|
+
scrollToTopOnPush: true,
|
|
146
|
+
behavior: 'auto',
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create a scroll restoration manager
|
|
151
|
+
*/
|
|
152
|
+
export function createScrollRestoration(options: ScrollRestorationOptions = {}) {
|
|
153
|
+
const config = { ...defaultOptions, ...options }
|
|
154
|
+
|
|
155
|
+
// Disable browser's native scroll restoration
|
|
156
|
+
if (isBrowser() && 'scrollRestoration' in history) {
|
|
157
|
+
history.scrollRestoration = 'manual'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handle navigation to save/restore scroll
|
|
162
|
+
*/
|
|
163
|
+
function handleNavigation(
|
|
164
|
+
from: Location | null,
|
|
165
|
+
to: Location,
|
|
166
|
+
action: 'PUSH' | 'REPLACE' | 'POP',
|
|
167
|
+
): void {
|
|
168
|
+
if (!config.enabled || !isBrowser()) return
|
|
169
|
+
|
|
170
|
+
// Save current position before navigating
|
|
171
|
+
if (from?.key) {
|
|
172
|
+
saveScrollPosition(from.key)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Determine what scroll action to take
|
|
176
|
+
if (action === 'POP' && config.restoreOnPop) {
|
|
177
|
+
// Back/forward navigation - try to restore position
|
|
178
|
+
if (!restoreScrollPosition(to.key)) {
|
|
179
|
+
// No saved position, handle hash or scroll to top
|
|
180
|
+
if (to.hash) {
|
|
181
|
+
requestAnimationFrame(() => {
|
|
182
|
+
if (!scrollToHash(to.hash, config.behavior)) {
|
|
183
|
+
scrollToTop(config.behavior)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
} else {
|
|
187
|
+
scrollToTop(config.behavior)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else if ((action === 'PUSH' || action === 'REPLACE') && config.scrollToTopOnPush) {
|
|
191
|
+
// New navigation - handle hash or scroll to top
|
|
192
|
+
requestAnimationFrame(() => {
|
|
193
|
+
if (to.hash) {
|
|
194
|
+
if (!scrollToHash(to.hash, config.behavior)) {
|
|
195
|
+
scrollToTop(config.behavior)
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
scrollToTop(config.behavior)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reset scroll restoration to browser defaults
|
|
206
|
+
*/
|
|
207
|
+
function reset(): void {
|
|
208
|
+
if (isBrowser() && 'scrollRestoration' in history) {
|
|
209
|
+
history.scrollRestoration = 'auto'
|
|
210
|
+
}
|
|
211
|
+
clearAllScrollPositions()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
handleNavigation,
|
|
216
|
+
saveScrollPosition,
|
|
217
|
+
restoreScrollPosition,
|
|
218
|
+
scrollToTop: () => scrollToTop(config.behavior),
|
|
219
|
+
scrollToHash: (hash: string) => scrollToHash(hash, config.behavior),
|
|
220
|
+
reset,
|
|
221
|
+
config,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Default scroll restoration instance
|
|
227
|
+
*/
|
|
228
|
+
let defaultScrollRestoration: ReturnType<typeof createScrollRestoration> | null = null
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get or create the default scroll restoration instance
|
|
232
|
+
*/
|
|
233
|
+
export function getScrollRestoration(): ReturnType<typeof createScrollRestoration> {
|
|
234
|
+
if (!defaultScrollRestoration) {
|
|
235
|
+
defaultScrollRestoration = createScrollRestoration()
|
|
236
|
+
}
|
|
237
|
+
return defaultScrollRestoration
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Configure the default scroll restoration
|
|
242
|
+
*/
|
|
243
|
+
export function configureScrollRestoration(options: ScrollRestorationOptions): void {
|
|
244
|
+
defaultScrollRestoration = createScrollRestoration(options)
|
|
245
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core type definitions for @fictjs/router
|
|
3
|
+
*
|
|
4
|
+
* This module defines the fundamental types used throughout the router.
|
|
5
|
+
* Designed to integrate seamlessly with Fict's reactive system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FictNode, Component } from '@fictjs/runtime'
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Location Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Represents a location in the router.
|
|
16
|
+
* Similar to window.location but with reactive support.
|
|
17
|
+
*/
|
|
18
|
+
export interface Location {
|
|
19
|
+
/** The pathname portion of the URL (e.g., "/users/123") */
|
|
20
|
+
pathname: string
|
|
21
|
+
/** The search/query portion of the URL (e.g., "?page=1") */
|
|
22
|
+
search: string
|
|
23
|
+
/** The hash portion of the URL (e.g., "#section") */
|
|
24
|
+
hash: string
|
|
25
|
+
/** State associated with this location */
|
|
26
|
+
state: unknown
|
|
27
|
+
/** Unique key for this location entry */
|
|
28
|
+
key: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Target for navigation - can be a string path or a partial location object
|
|
33
|
+
*/
|
|
34
|
+
export type To = string | Partial<Location>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Navigation intent type
|
|
38
|
+
*/
|
|
39
|
+
export type NavigationIntent = 'initial' | 'navigate' | 'native' | 'preload'
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Parameter Types
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Route parameters extracted from the URL
|
|
47
|
+
*/
|
|
48
|
+
export type Params<Key extends string = string> = Readonly<Record<Key, string | undefined>>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Search parameters from the query string
|
|
52
|
+
*/
|
|
53
|
+
export type SearchParams = URLSearchParams
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Match filter for validating route parameters
|
|
57
|
+
*/
|
|
58
|
+
export type MatchFilter<T = string> = RegExp | readonly T[] | ((value: string) => boolean)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Match filters for route parameters
|
|
62
|
+
*/
|
|
63
|
+
export type MatchFilters<P extends string = string> = Partial<Record<P, MatchFilter>>
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Route Definition Types
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Props passed to route components
|
|
71
|
+
*/
|
|
72
|
+
export interface RouteComponentProps<P extends string = string> {
|
|
73
|
+
/** Route parameters */
|
|
74
|
+
params: Params<P>
|
|
75
|
+
/** Current location */
|
|
76
|
+
location: Location
|
|
77
|
+
/** Preloaded data (if preload function is defined) */
|
|
78
|
+
data?: unknown
|
|
79
|
+
/** Children routes rendered via <Outlet /> */
|
|
80
|
+
children?: FictNode
|
|
81
|
+
/** Allow additional properties for component extensibility */
|
|
82
|
+
[key: string]: unknown
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Preload function arguments
|
|
87
|
+
*/
|
|
88
|
+
export interface PreloadArgs<P extends string = string> {
|
|
89
|
+
/** Route parameters */
|
|
90
|
+
params: Params<P>
|
|
91
|
+
/** Current location */
|
|
92
|
+
location: Location
|
|
93
|
+
/** Navigation intent */
|
|
94
|
+
intent: NavigationIntent
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Preload function type
|
|
99
|
+
*/
|
|
100
|
+
export type PreloadFunction<T = unknown, P extends string = string> = (
|
|
101
|
+
args: PreloadArgs<P>,
|
|
102
|
+
) => T | Promise<T>
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Route definition - user-facing configuration
|
|
106
|
+
*/
|
|
107
|
+
export interface RouteDefinition<P extends string = string> {
|
|
108
|
+
/** Path pattern (e.g., "/users/:id", "/items/:id?") */
|
|
109
|
+
path?: string
|
|
110
|
+
/** Component to render for this route */
|
|
111
|
+
component?: Component<RouteComponentProps<P>>
|
|
112
|
+
/** Element to render (alternative to component) */
|
|
113
|
+
element?: FictNode
|
|
114
|
+
/** Preload function for data loading */
|
|
115
|
+
preload?: PreloadFunction<unknown, P>
|
|
116
|
+
/** Nested child routes */
|
|
117
|
+
children?: RouteDefinition[]
|
|
118
|
+
/** Parameter validation filters */
|
|
119
|
+
matchFilters?: MatchFilters<P>
|
|
120
|
+
/** Whether this is an index route */
|
|
121
|
+
index?: boolean
|
|
122
|
+
/** Route key for caching/optimization */
|
|
123
|
+
key?: string
|
|
124
|
+
/** Catch-all error boundary element */
|
|
125
|
+
errorElement?: FictNode
|
|
126
|
+
/** Loading fallback element */
|
|
127
|
+
loadingElement?: FictNode
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Props for the Route component (JSX-based definition)
|
|
132
|
+
*/
|
|
133
|
+
export interface RouteProps<P extends string = string> extends Omit<
|
|
134
|
+
RouteDefinition<P>,
|
|
135
|
+
'children'
|
|
136
|
+
> {
|
|
137
|
+
/** JSX children (nested Route components) */
|
|
138
|
+
children?: FictNode
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Match Types
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Result of matching a route against a location
|
|
147
|
+
*/
|
|
148
|
+
export interface RouteMatch<P extends string = string> {
|
|
149
|
+
/** The matched route definition */
|
|
150
|
+
route: RouteDefinition<P>
|
|
151
|
+
/** The matched portion of the pathname */
|
|
152
|
+
pathname: string
|
|
153
|
+
/** Extracted parameters */
|
|
154
|
+
params: Params<P>
|
|
155
|
+
/** The pattern that matched */
|
|
156
|
+
pattern: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Internal compiled route with matcher function
|
|
161
|
+
*/
|
|
162
|
+
export interface CompiledRoute {
|
|
163
|
+
/** Original route definition */
|
|
164
|
+
route: RouteDefinition
|
|
165
|
+
/** Normalized path pattern */
|
|
166
|
+
pattern: string
|
|
167
|
+
/** Matcher function */
|
|
168
|
+
matcher: (pathname: string) => RouteMatch | null
|
|
169
|
+
/** Route score for ranking */
|
|
170
|
+
score: number
|
|
171
|
+
/** Child compiled routes */
|
|
172
|
+
children?: CompiledRoute[]
|
|
173
|
+
/** Unique key for this route */
|
|
174
|
+
key: string
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Branch of routes (for nested route matching)
|
|
179
|
+
*/
|
|
180
|
+
export interface RouteBranch {
|
|
181
|
+
/** Routes in this branch from root to leaf */
|
|
182
|
+
routes: CompiledRoute[]
|
|
183
|
+
/** Combined score for the branch */
|
|
184
|
+
score: number
|
|
185
|
+
/** Matcher for the entire branch */
|
|
186
|
+
matcher: (pathname: string) => RouteMatch[] | null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Navigation Types
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Options for navigation
|
|
195
|
+
*/
|
|
196
|
+
export interface NavigateOptions {
|
|
197
|
+
/** Replace current history entry instead of pushing */
|
|
198
|
+
replace?: boolean | undefined
|
|
199
|
+
/** State to pass with the navigation */
|
|
200
|
+
state?: unknown
|
|
201
|
+
/** Scroll to top after navigation */
|
|
202
|
+
scroll?: boolean | undefined
|
|
203
|
+
/** Resolve path relative to current route */
|
|
204
|
+
relative?: 'route' | 'path' | undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Navigation function type
|
|
209
|
+
*/
|
|
210
|
+
export interface NavigateFunction {
|
|
211
|
+
(to: To, options?: NavigateOptions): void
|
|
212
|
+
(delta: number): void
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Navigation state during transitions
|
|
217
|
+
*/
|
|
218
|
+
export interface Navigation {
|
|
219
|
+
/** Current navigation state */
|
|
220
|
+
state: 'idle' | 'loading' | 'submitting'
|
|
221
|
+
/** Target location (if loading) */
|
|
222
|
+
location?: Location
|
|
223
|
+
/** Form data (if submitting) */
|
|
224
|
+
formData?: FormData
|
|
225
|
+
/** Form action (if submitting) */
|
|
226
|
+
formAction?: string
|
|
227
|
+
/** Form method (if submitting) */
|
|
228
|
+
formMethod?: string
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Context Types
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Router context value
|
|
237
|
+
*/
|
|
238
|
+
export interface RouterContextValue {
|
|
239
|
+
/** Current location (reactive) */
|
|
240
|
+
location: () => Location
|
|
241
|
+
/** Route parameters (reactive) */
|
|
242
|
+
params: () => Params
|
|
243
|
+
/** Current matches (reactive) */
|
|
244
|
+
matches: () => RouteMatch[]
|
|
245
|
+
/** Navigate function */
|
|
246
|
+
navigate: NavigateFunction
|
|
247
|
+
/** Whether currently routing */
|
|
248
|
+
isRouting: () => boolean
|
|
249
|
+
/** Pending navigation target (if routing) */
|
|
250
|
+
pendingLocation: () => Location | null
|
|
251
|
+
/** Base path for the router */
|
|
252
|
+
base: string
|
|
253
|
+
/** Resolve a path relative to the current route */
|
|
254
|
+
resolvePath: (to: To) => string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Route context value (for nested routes)
|
|
259
|
+
*/
|
|
260
|
+
export interface RouteContextValue {
|
|
261
|
+
/** The current route match */
|
|
262
|
+
match: () => RouteMatch | undefined
|
|
263
|
+
/** Preloaded data */
|
|
264
|
+
data: () => unknown
|
|
265
|
+
/** Route error (if any) */
|
|
266
|
+
error?: () => unknown
|
|
267
|
+
/** Outlet function to render child route */
|
|
268
|
+
outlet: () => FictNode
|
|
269
|
+
/** Parent route context */
|
|
270
|
+
parent?: RouteContextValue
|
|
271
|
+
/** Resolve path relative to this route */
|
|
272
|
+
resolvePath: (to: To) => string
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// History Types
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* History action type
|
|
281
|
+
*/
|
|
282
|
+
export type HistoryAction = 'POP' | 'PUSH' | 'REPLACE'
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* History listener callback
|
|
286
|
+
*/
|
|
287
|
+
export type HistoryListener = (update: { action: HistoryAction; location: Location }) => void
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* History interface (browser, hash, or memory)
|
|
291
|
+
*/
|
|
292
|
+
export interface History {
|
|
293
|
+
/** Current action */
|
|
294
|
+
readonly action: HistoryAction
|
|
295
|
+
/** Current location */
|
|
296
|
+
readonly location: Location
|
|
297
|
+
/** Push a new entry */
|
|
298
|
+
push(to: To, state?: unknown): void
|
|
299
|
+
/** Replace the current entry */
|
|
300
|
+
replace(to: To, state?: unknown): void
|
|
301
|
+
/** Go forward or backward */
|
|
302
|
+
go(delta: number): void
|
|
303
|
+
/** Go back one entry */
|
|
304
|
+
back(): void
|
|
305
|
+
/** Go forward one entry */
|
|
306
|
+
forward(): void
|
|
307
|
+
/** Listen for location changes */
|
|
308
|
+
listen(listener: HistoryListener): () => void
|
|
309
|
+
/** Create an href from a To value */
|
|
310
|
+
createHref(to: To): string
|
|
311
|
+
/** Block navigation */
|
|
312
|
+
block(blocker: Blocker): () => void
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Blocker function for preventing navigation
|
|
317
|
+
*/
|
|
318
|
+
export type Blocker = (tx: { action: HistoryAction; location: Location; retry: () => void }) => void
|
|
319
|
+
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// BeforeLeave Types
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Arguments passed to beforeLeave handlers
|
|
326
|
+
*/
|
|
327
|
+
export interface BeforeLeaveEventArgs {
|
|
328
|
+
/** Target location */
|
|
329
|
+
to: Location
|
|
330
|
+
/** Current location */
|
|
331
|
+
from: Location
|
|
332
|
+
/** Whether this was prevented */
|
|
333
|
+
defaultPrevented: boolean
|
|
334
|
+
/** Prevent the navigation */
|
|
335
|
+
preventDefault: () => void
|
|
336
|
+
/** Retry the navigation */
|
|
337
|
+
retry: (force?: boolean) => void
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* BeforeLeave handler function
|
|
342
|
+
*/
|
|
343
|
+
export type BeforeLeaveHandler = (e: BeforeLeaveEventArgs) => void | Promise<void>
|
|
344
|
+
|
|
345
|
+
// ============================================================================
|
|
346
|
+
// Data Loading Types
|
|
347
|
+
// ============================================================================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Submission state
|
|
351
|
+
*/
|
|
352
|
+
export interface Submission<T = unknown> {
|
|
353
|
+
/** Unique submission key */
|
|
354
|
+
key: string
|
|
355
|
+
/** Form data being submitted */
|
|
356
|
+
formData: FormData
|
|
357
|
+
/** Submission state */
|
|
358
|
+
state: 'submitting' | 'loading' | 'idle'
|
|
359
|
+
/** Result data */
|
|
360
|
+
result?: T
|
|
361
|
+
/** Error if submission failed */
|
|
362
|
+
error?: unknown
|
|
363
|
+
/** Clear the submission */
|
|
364
|
+
clear: () => void
|
|
365
|
+
/** Retry the submission */
|
|
366
|
+
retry: () => void
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Action function type
|
|
371
|
+
*/
|
|
372
|
+
export type ActionFunction<T = unknown> = (
|
|
373
|
+
formData: FormData,
|
|
374
|
+
args: { params: Params; request: Request },
|
|
375
|
+
) => T | Promise<T>
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Action object returned by createAction
|
|
379
|
+
*/
|
|
380
|
+
export interface Action<T = unknown> {
|
|
381
|
+
/** Action URL */
|
|
382
|
+
url: string
|
|
383
|
+
/** Submit the action */
|
|
384
|
+
submit: (formData: FormData) => Promise<T>
|
|
385
|
+
/** Action name */
|
|
386
|
+
name?: string
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Query function type
|
|
391
|
+
*/
|
|
392
|
+
export type QueryFunction<T = unknown, Args extends unknown[] = unknown[]> = (
|
|
393
|
+
...args: Args
|
|
394
|
+
) => T | Promise<T>
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Query cache entry
|
|
398
|
+
*/
|
|
399
|
+
export interface QueryCacheEntry<T = unknown> {
|
|
400
|
+
/** Timestamp when cached */
|
|
401
|
+
timestamp: number
|
|
402
|
+
/** Cached promise */
|
|
403
|
+
promise: Promise<T>
|
|
404
|
+
/** Resolved result */
|
|
405
|
+
result?: T
|
|
406
|
+
/** Intent when fetched */
|
|
407
|
+
intent: NavigationIntent
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// Router Configuration Types
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Router configuration options
|
|
416
|
+
*/
|
|
417
|
+
export interface RouterOptions {
|
|
418
|
+
/** Base path for the router */
|
|
419
|
+
base?: string
|
|
420
|
+
/** Initial location (for SSR) */
|
|
421
|
+
url?: string
|
|
422
|
+
/** History implementation to use */
|
|
423
|
+
history?: History
|
|
424
|
+
/** Data preloaded on server */
|
|
425
|
+
hydrationData?: {
|
|
426
|
+
loaderData?: Record<string, unknown>
|
|
427
|
+
actionData?: Record<string, unknown>
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Memory router options
|
|
433
|
+
*/
|
|
434
|
+
export interface MemoryRouterOptions extends RouterOptions {
|
|
435
|
+
/** Initial entries in the history stack */
|
|
436
|
+
initialEntries?: string[]
|
|
437
|
+
/** Initial index in the history stack */
|
|
438
|
+
initialIndex?: number
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Hash router options
|
|
443
|
+
*/
|
|
444
|
+
export interface HashRouterOptions extends RouterOptions {
|
|
445
|
+
/** Hash type: "slash" for /#/path, "noslash" for /#path */
|
|
446
|
+
hashType?: 'slash' | 'noslash'
|
|
447
|
+
}
|