@zenithbuild/router 1.0.1
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/README.md +127 -0
- package/index.d.ts +38 -0
- package/index.js +5 -0
- package/package.json +65 -0
- package/src/index.ts +108 -0
- package/src/lib.rs +18 -0
- package/src/manifest.rs +189 -0
- package/src/manifest.ts +208 -0
- package/src/navigation/ZenLink.zen +231 -0
- package/src/navigation/index.ts +79 -0
- package/src/navigation/zen-link.ts +585 -0
- package/src/render.rs +8 -0
- package/src/resolve.rs +53 -0
- package/src/runtime.ts +163 -0
- package/src/runtime_gen.rs +14 -0
- package/src/types.rs +46 -0
- package/src/types.ts +63 -0
- package/tsconfig.json +30 -0
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import native from "../index.js"
|
|
4
|
+
import {
|
|
5
|
+
type RouteDefinition,
|
|
6
|
+
type ParsedSegment,
|
|
7
|
+
SegmentType
|
|
8
|
+
} from "./types"
|
|
9
|
+
|
|
10
|
+
const { generateRouteManifestNative } = native
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scoring constants for route ranking
|
|
14
|
+
*/
|
|
15
|
+
const SEGMENT_SCORES = {
|
|
16
|
+
[SegmentType.Static]: 10,
|
|
17
|
+
[SegmentType.Dynamic]: 5,
|
|
18
|
+
[SegmentType.CatchAll]: 1,
|
|
19
|
+
[SegmentType.OptionalCatchAll]: 0
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Discover all .zen files in the pages directory
|
|
24
|
+
*/
|
|
25
|
+
export function discoverPages(pagesDir: string): string[] {
|
|
26
|
+
const pages: string[] = []
|
|
27
|
+
|
|
28
|
+
function walk(dir: string): void {
|
|
29
|
+
if (!fs.existsSync(dir)) return
|
|
30
|
+
|
|
31
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const fullPath = path.join(dir, entry.name)
|
|
35
|
+
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
walk(fullPath)
|
|
38
|
+
} else if (entry.isFile() && entry.name.endsWith(".zen")) {
|
|
39
|
+
pages.push(fullPath)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
walk(pagesDir)
|
|
45
|
+
return pages
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a file path to a route path
|
|
50
|
+
*/
|
|
51
|
+
export function filePathToRoutePath(filePath: string, pagesDir: string): string {
|
|
52
|
+
const relativePath = path.relative(pagesDir, filePath)
|
|
53
|
+
const withoutExt = relativePath.replace(/\.zen$/, "")
|
|
54
|
+
const segmentsList = withoutExt.split(path.sep)
|
|
55
|
+
const routeSegments: string[] = []
|
|
56
|
+
|
|
57
|
+
for (const segment of segmentsList) {
|
|
58
|
+
if (segment === "index") continue
|
|
59
|
+
|
|
60
|
+
const optionalCatchAllMatch = segment.match(/^\[\[\.\.\.(\w+)\]\]$/)
|
|
61
|
+
if (optionalCatchAllMatch) {
|
|
62
|
+
routeSegments.push(`*${optionalCatchAllMatch[1]}?`)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const catchAllMatch = segment.match(/^\[\.\.\.(\w+)\]$/)
|
|
67
|
+
if (catchAllMatch) {
|
|
68
|
+
routeSegments.push(`*${catchAllMatch[1]}`)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const dynamicMatch = segment.match(/^\[(\w+)\]$/)
|
|
73
|
+
if (dynamicMatch) {
|
|
74
|
+
routeSegments.push(`:${dynamicMatch[1]}`)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
routeSegments.push(segment)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const routePath = "/" + routeSegments.join("/")
|
|
82
|
+
return routePath === "/" ? "/" : routePath.replace(/\/$/, "")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse a route path into segments
|
|
87
|
+
*/
|
|
88
|
+
export function parseRouteSegments(routePath: string): ParsedSegment[] {
|
|
89
|
+
if (routePath === "/") return []
|
|
90
|
+
|
|
91
|
+
const segmentsList = routePath.slice(1).split("/")
|
|
92
|
+
const parsed: ParsedSegment[] = []
|
|
93
|
+
|
|
94
|
+
for (const segment of segmentsList) {
|
|
95
|
+
if (segment.startsWith("*") && segment.endsWith("?")) {
|
|
96
|
+
parsed.push({ segmentType: SegmentType.OptionalCatchAll, paramName: segment.slice(1, -1), raw: segment })
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if (segment.startsWith("*")) {
|
|
100
|
+
parsed.push({ segmentType: SegmentType.CatchAll, paramName: segment.slice(1), raw: segment })
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (segment.startsWith(":")) {
|
|
104
|
+
parsed.push({ segmentType: SegmentType.Dynamic, paramName: segment.slice(1), raw: segment })
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
parsed.push({ segmentType: SegmentType.Static, raw: segment })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return parsed
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calculate route score
|
|
115
|
+
*/
|
|
116
|
+
export function calculateRouteScore(segments: ParsedSegment[]): number {
|
|
117
|
+
if (segments.length === 0) return 100
|
|
118
|
+
let score = 0
|
|
119
|
+
for (const segment of segments) {
|
|
120
|
+
score += SEGMENT_SCORES[segment.segmentType]
|
|
121
|
+
}
|
|
122
|
+
const staticCount = segments.filter(s => s.segmentType === SegmentType.Static).length
|
|
123
|
+
score += staticCount * 2
|
|
124
|
+
return score
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extract parameter names
|
|
129
|
+
*/
|
|
130
|
+
export function extractParamNames(segments: ParsedSegment[]): string[] {
|
|
131
|
+
return segments
|
|
132
|
+
.filter(s => s.paramName !== undefined)
|
|
133
|
+
.map(s => s.paramName!)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convert route path to regex pattern
|
|
138
|
+
*/
|
|
139
|
+
export function routePathToRegex(routePath: string): RegExp {
|
|
140
|
+
if (routePath === "/") return /^\/$/
|
|
141
|
+
|
|
142
|
+
const segmentsList = routePath.slice(1).split("/")
|
|
143
|
+
const regexParts: string[] = []
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < segmentsList.length; i++) {
|
|
146
|
+
const segment = segmentsList[i]
|
|
147
|
+
if (!segment) continue
|
|
148
|
+
|
|
149
|
+
if (segment.startsWith("*") && segment.endsWith("?")) {
|
|
150
|
+
regexParts.push("(?:\\/(.*))?")
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (segment.startsWith("*")) {
|
|
154
|
+
regexParts.push("\\/(.+)")
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (segment.startsWith(":")) {
|
|
158
|
+
regexParts.push("\\/([^/]+)")
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
const escaped = segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
162
|
+
regexParts.push(`\\/${escaped}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return new RegExp(`^${regexParts.join("")}\\/?$`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate a route definition from a file path
|
|
170
|
+
*/
|
|
171
|
+
export function generateRouteDefinition(filePath: string, pagesDir: string): RouteDefinition {
|
|
172
|
+
const routePath = filePathToRoutePath(filePath, pagesDir)
|
|
173
|
+
const segments = parseRouteSegments(routePath)
|
|
174
|
+
const paramNames = extractParamNames(segments)
|
|
175
|
+
const score = calculateRouteScore(segments)
|
|
176
|
+
|
|
177
|
+
// Note: RouteDefinition extends RouteRecord, which no longer has segments
|
|
178
|
+
return {
|
|
179
|
+
path: routePath,
|
|
180
|
+
paramNames,
|
|
181
|
+
score,
|
|
182
|
+
filePath,
|
|
183
|
+
regex: routePathToRegex(routePath)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function generateRouteManifest(pagesDir: string): RouteDefinition[] {
|
|
188
|
+
// Optional: use native generator if available, but for now we keep the TS one for build-time safety
|
|
189
|
+
const pages = discoverPages(pagesDir)
|
|
190
|
+
const definitions = pages.map(filePath => generateRouteDefinition(filePath, pagesDir))
|
|
191
|
+
definitions.sort((a, b) => b.score - a.score)
|
|
192
|
+
return definitions
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function generateRouteManifestCode(definitions: RouteDefinition[]): string {
|
|
196
|
+
const routeEntries = definitions.map(def => {
|
|
197
|
+
const regex = routePathToRegex(def.path)
|
|
198
|
+
return ` {
|
|
199
|
+
path: ${JSON.stringify(def.path)},
|
|
200
|
+
regex: ${regex.toString()},
|
|
201
|
+
paramNames: ${JSON.stringify(def.paramNames)},
|
|
202
|
+
score: ${def.score},
|
|
203
|
+
filePath: ${JSON.stringify(def.filePath)}
|
|
204
|
+
}`
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return `// Auto-generated route manifest\n// Do not edit directly\n\nexport const routeManifest = [\n${routeEntries.join(",\n")}\n];\n`
|
|
208
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// Props extend HTMLAnchorElement properties + custom ZenLink attributes
|
|
3
|
+
// Standard anchor attributes: href, target, rel, download, hreflang, type, ping, referrerPolicy, etc.
|
|
4
|
+
// Custom ZenLink attributes: preload, exact, onClick
|
|
5
|
+
type Props = {
|
|
6
|
+
// Standard HTMLAnchorElement attributes
|
|
7
|
+
href?: string
|
|
8
|
+
target?: '_blank' | '_self' | '_parent' | '_top' | string
|
|
9
|
+
rel?: string
|
|
10
|
+
download?: string | boolean
|
|
11
|
+
hreflang?: string
|
|
12
|
+
type?: string
|
|
13
|
+
ping?: string
|
|
14
|
+
referrerPolicy?: string
|
|
15
|
+
class?: string
|
|
16
|
+
id?: string
|
|
17
|
+
title?: string
|
|
18
|
+
ariaLabel?: string
|
|
19
|
+
role?: string
|
|
20
|
+
tabIndex?: number | string
|
|
21
|
+
// Custom ZenLink attributes
|
|
22
|
+
preload?: boolean
|
|
23
|
+
exact?: boolean
|
|
24
|
+
onClick?: (event?: MouseEvent) => void | boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handle link click - prevents default and uses SPA navigation
|
|
29
|
+
* Respects target="_blank" and other standard anchor behaviors
|
|
30
|
+
*/
|
|
31
|
+
function handleClick(event, el) {
|
|
32
|
+
// Ensure attributes are set from props
|
|
33
|
+
if (el) {
|
|
34
|
+
ensureAttributes(el)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get target from the element attribute (more reliable than prop)
|
|
38
|
+
const linkTarget = el ? el.getAttribute('target') : (typeof target !== 'undefined' ? target : null)
|
|
39
|
+
|
|
40
|
+
// If target is _blank, _parent, or _top, let browser handle it (opens in new tab/window)
|
|
41
|
+
if (linkTarget === '_blank' || linkTarget === '_parent' || linkTarget === '_top') {
|
|
42
|
+
// Let browser handle standard navigation
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Allow modifier keys for native behavior (Cmd/Ctrl+click, etc.)
|
|
47
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get href from element or prop
|
|
52
|
+
const linkHref = el ? el.getAttribute('href') : (typeof href !== 'undefined' ? href : null)
|
|
53
|
+
if (!linkHref) return
|
|
54
|
+
|
|
55
|
+
// Check if external URL (http://, https://, //, mailto:, tel:, etc.)
|
|
56
|
+
if (linkHref.startsWith('http://') ||
|
|
57
|
+
linkHref.startsWith('https://') ||
|
|
58
|
+
linkHref.startsWith('//') ||
|
|
59
|
+
linkHref.startsWith('mailto:') ||
|
|
60
|
+
linkHref.startsWith('tel:') ||
|
|
61
|
+
linkHref.startsWith('javascript:')) {
|
|
62
|
+
// External/special link - open in new tab if target not specified
|
|
63
|
+
if (!linkTarget) {
|
|
64
|
+
el?.setAttribute('target', '_blank')
|
|
65
|
+
el?.setAttribute('rel', 'noopener noreferrer')
|
|
66
|
+
}
|
|
67
|
+
// Let browser handle it
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Prevent default navigation for internal SPA links
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
event.stopPropagation()
|
|
74
|
+
|
|
75
|
+
// Call onClick prop if provided
|
|
76
|
+
if (typeof onClick === 'function') {
|
|
77
|
+
const result = onClick(event)
|
|
78
|
+
// If onClick returns false, cancel navigation
|
|
79
|
+
if (result === false) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Normalize path for comparison
|
|
85
|
+
const normalizedHref = linkHref === '' ? '/' : linkHref
|
|
86
|
+
const currentPath = window.location.pathname === '' ? '/' : window.location.pathname
|
|
87
|
+
|
|
88
|
+
// Only navigate if path is different (idempotent navigation)
|
|
89
|
+
if (normalizedHref !== currentPath) {
|
|
90
|
+
console.log('[ZenLink] Navigating to:', linkHref)
|
|
91
|
+
// Navigate using SPA router
|
|
92
|
+
if (window.__zenith_router && window.__zenith_router.navigate) {
|
|
93
|
+
console.log('[ZenLink] Using router.navigate')
|
|
94
|
+
window.__zenith_router.navigate(linkHref)
|
|
95
|
+
} else {
|
|
96
|
+
console.log('[ZenLink] Using fallback history API')
|
|
97
|
+
// Fallback to history API
|
|
98
|
+
window.history.pushState(null, '', linkHref)
|
|
99
|
+
window.dispatchEvent(new PopStateEvent('popstate'))
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.log('[ZenLink] Already on route:', linkHref, '- skipping navigation')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle mouse enter for preloading
|
|
108
|
+
*/
|
|
109
|
+
function handleMouseEnter(event, el) {
|
|
110
|
+
// Ensure attributes are set
|
|
111
|
+
if (el) {
|
|
112
|
+
ensureAttributes(el)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const shouldPreload = typeof preload !== 'undefined' ? preload : false
|
|
116
|
+
console.log('[ZenLink] handleMouseEnter called, preload:', shouldPreload)
|
|
117
|
+
if (!shouldPreload) {
|
|
118
|
+
console.log('[ZenLink] Preload disabled, returning early')
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const linkHref = el ? el.getAttribute('href') : (typeof href !== 'undefined' ? href : null)
|
|
123
|
+
if (!linkHref) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Skip external URLs
|
|
128
|
+
if (linkHref.startsWith('http://') || linkHref.startsWith('https://') || linkHref.startsWith('//')) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('[ZenLink] Prefetch triggered on hover:', linkHref)
|
|
133
|
+
|
|
134
|
+
// Prefetch the route
|
|
135
|
+
if (window.__zenith_router && window.__zenith_router.prefetch) {
|
|
136
|
+
console.log('[ZenLink] Calling router.prefetch for:', linkHref)
|
|
137
|
+
window.__zenith_router.prefetch(linkHref).then(() => {
|
|
138
|
+
console.log('[ZenLink] Prefetch complete for:', linkHref)
|
|
139
|
+
}).catch((error) => {
|
|
140
|
+
console.warn('[ZenLink] Prefetch failed for:', linkHref, error)
|
|
141
|
+
})
|
|
142
|
+
} else {
|
|
143
|
+
console.warn('[ZenLink] Router prefetch not available')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Apply attributes on mount
|
|
148
|
+
if (typeof zenOnMount !== 'undefined') {
|
|
149
|
+
zenOnMount(() => {
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
// Find all ZenLink anchor elements and apply attributes
|
|
152
|
+
document.querySelectorAll('a[data-zen-component="Zenlink"]').forEach(el => {
|
|
153
|
+
ensureAttributes(el)
|
|
154
|
+
})
|
|
155
|
+
}, 0)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Apply standard anchor attributes from props to the element
|
|
161
|
+
* Called when the element is clicked to ensure attributes are set
|
|
162
|
+
*/
|
|
163
|
+
function ensureAttributes(el) {
|
|
164
|
+
if (!el) return
|
|
165
|
+
|
|
166
|
+
// Set attributes from props (only if they exist and aren't already set)
|
|
167
|
+
const attrs = {
|
|
168
|
+
target: typeof target !== 'undefined' ? target : null,
|
|
169
|
+
rel: typeof rel !== 'undefined' ? rel : null,
|
|
170
|
+
download: typeof download !== 'undefined' ? download : null,
|
|
171
|
+
hreflang: typeof hreflang !== 'undefined' ? hreflang : null,
|
|
172
|
+
type: typeof type !== 'undefined' ? type : null,
|
|
173
|
+
ping: typeof ping !== 'undefined' ? ping : null,
|
|
174
|
+
referrerPolicy: typeof referrerPolicy !== 'undefined' ? referrerPolicy : null,
|
|
175
|
+
id: typeof id !== 'undefined' ? id : null,
|
|
176
|
+
title: typeof title !== 'undefined' ? title : null,
|
|
177
|
+
ariaLabel: typeof ariaLabel !== 'undefined' ? ariaLabel : null,
|
|
178
|
+
role: typeof role !== 'undefined' ? role : null,
|
|
179
|
+
tabIndex: typeof tabIndex !== 'undefined' ? tabIndex : null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Map to HTML attribute names
|
|
183
|
+
const htmlAttrs = {
|
|
184
|
+
target: 'target',
|
|
185
|
+
rel: 'rel',
|
|
186
|
+
download: 'download',
|
|
187
|
+
hreflang: 'hreflang',
|
|
188
|
+
type: 'type',
|
|
189
|
+
ping: 'ping',
|
|
190
|
+
referrerPolicy: 'referrerpolicy',
|
|
191
|
+
id: 'id',
|
|
192
|
+
title: 'title',
|
|
193
|
+
ariaLabel: 'aria-label',
|
|
194
|
+
role: 'role',
|
|
195
|
+
tabIndex: 'tabindex'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Set attributes that have values
|
|
199
|
+
for (const [prop, value] of Object.entries(attrs)) {
|
|
200
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
201
|
+
const htmlAttr = htmlAttrs[prop]
|
|
202
|
+
if (htmlAttr && !el.hasAttribute(htmlAttr)) {
|
|
203
|
+
el.setAttribute(htmlAttr, String(value))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<style>
|
|
212
|
+
.zen-link {
|
|
213
|
+
color: inherit;
|
|
214
|
+
text-decoration: none;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.zen-link:hover {
|
|
219
|
+
text-decoration: underline;
|
|
220
|
+
}
|
|
221
|
+
</style>
|
|
222
|
+
|
|
223
|
+
<a
|
|
224
|
+
href="{ href }"
|
|
225
|
+
class="zen-link { class }"
|
|
226
|
+
onclick="handleClick"
|
|
227
|
+
onmouseenter="handleMouseEnter"
|
|
228
|
+
style="cursor: pointer;"
|
|
229
|
+
>
|
|
230
|
+
<slot />
|
|
231
|
+
</a>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Navigation System
|
|
3
|
+
*
|
|
4
|
+
* Provides SPA navigation utilities and the ZenLink API.
|
|
5
|
+
*
|
|
6
|
+
* @package @zenithbuild/router
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { navigate, isActive, prefetch, zenLink } from '@zenithbuild/router/navigation'
|
|
11
|
+
*
|
|
12
|
+
* // Programmatic navigation
|
|
13
|
+
* navigate('/about')
|
|
14
|
+
*
|
|
15
|
+
* // Check active state
|
|
16
|
+
* if (isActive('/blog')) {
|
|
17
|
+
* console.log('On blog section')
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* // Prefetch for faster navigation
|
|
21
|
+
* prefetch('/dashboard')
|
|
22
|
+
*
|
|
23
|
+
* // Create link programmatically
|
|
24
|
+
* const link = zenLink({ href: '/contact', children: 'Contact' })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Export all navigation utilities
|
|
29
|
+
export {
|
|
30
|
+
// Navigation API
|
|
31
|
+
zenNavigate,
|
|
32
|
+
navigate,
|
|
33
|
+
zenBack,
|
|
34
|
+
back,
|
|
35
|
+
zenForward,
|
|
36
|
+
forward,
|
|
37
|
+
zenGo,
|
|
38
|
+
go,
|
|
39
|
+
|
|
40
|
+
// Active state
|
|
41
|
+
zenIsActive,
|
|
42
|
+
isActive,
|
|
43
|
+
|
|
44
|
+
// Prefetching
|
|
45
|
+
zenPrefetch,
|
|
46
|
+
prefetch,
|
|
47
|
+
zenIsPrefetched,
|
|
48
|
+
isPrefetched,
|
|
49
|
+
|
|
50
|
+
// Transitions API
|
|
51
|
+
setGlobalTransition,
|
|
52
|
+
getGlobalTransition,
|
|
53
|
+
createTransitionContext,
|
|
54
|
+
|
|
55
|
+
// Route state
|
|
56
|
+
zenGetRoute,
|
|
57
|
+
getRoute,
|
|
58
|
+
zenGetParam,
|
|
59
|
+
getParam,
|
|
60
|
+
zenGetQuery,
|
|
61
|
+
getQuery,
|
|
62
|
+
|
|
63
|
+
// ZenLink factory
|
|
64
|
+
createZenLink,
|
|
65
|
+
zenLink,
|
|
66
|
+
|
|
67
|
+
// Utilities
|
|
68
|
+
isExternalUrl,
|
|
69
|
+
shouldUseSPANavigation,
|
|
70
|
+
normalizePath
|
|
71
|
+
} from './zen-link'
|
|
72
|
+
|
|
73
|
+
// Export types
|
|
74
|
+
export type {
|
|
75
|
+
ZenLinkProps,
|
|
76
|
+
TransitionContext,
|
|
77
|
+
TransitionHandler,
|
|
78
|
+
NavigateOptions
|
|
79
|
+
} from './zen-link'
|