@zenithbuild/core 0.6.2 → 1.1.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/cli/commands/dev.ts +107 -48
- package/compiler/discovery/componentDiscovery.ts +75 -11
- package/compiler/output/types.ts +15 -1
- package/compiler/parse/parseTemplate.ts +29 -0
- package/compiler/runtime/dataExposure.ts +27 -12
- package/compiler/runtime/generateDOM.ts +12 -3
- package/compiler/runtime/transformIR.ts +39 -3
- package/compiler/runtime/wrapExpression.ts +32 -13
- package/compiler/runtime/wrapExpressionWithLoop.ts +24 -10
- package/compiler/ssg-build.ts +71 -7
- package/compiler/test/component-stacking.test.ts +365 -0
- package/compiler/transform/componentResolver.ts +42 -4
- package/compiler/transform/fragmentLowering.ts +153 -1
- package/compiler/transform/generateBindings.ts +31 -10
- package/compiler/transform/transformNode.ts +114 -1
- package/core/config/index.ts +5 -3
- package/core/config/types.ts +67 -37
- package/core/plugins/bridge.ts +193 -0
- package/core/plugins/registry.ts +51 -6
- package/dist/cli.js +10 -0
- package/dist/zen-build.js +673 -1723
- package/dist/zen-dev.js +673 -1723
- package/dist/zen-preview.js +673 -1723
- package/dist/zenith.js +673 -1723
- package/package.json +11 -3
- package/runtime/bundle-generator.ts +36 -17
- package/runtime/client-runtime.ts +21 -1
- package/cli/utils/content.ts +0 -112
- package/router/manifest.ts +0 -314
- package/router/navigation/ZenLink.zen +0 -231
- package/router/navigation/index.ts +0 -78
- package/router/navigation/zen-link.ts +0 -584
- package/router/runtime.ts +0 -458
- package/router/types.ts +0 -168
|
@@ -1,584 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ZenLink Runtime Module
|
|
3
|
-
*
|
|
4
|
-
* Provides programmatic navigation and ZenLink utilities.
|
|
5
|
-
* This module can be imported in `.zen` files or TypeScript.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { navigate, zenLink, isActive } from 'zenith/core'
|
|
10
|
-
*
|
|
11
|
-
* // Programmatic navigation
|
|
12
|
-
* navigate('/about')
|
|
13
|
-
*
|
|
14
|
-
* // Check active state
|
|
15
|
-
* if (isActive('/blog')) {
|
|
16
|
-
* console.log('On blog section')
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// ============================================
|
|
22
|
-
// Types
|
|
23
|
-
// ============================================
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Props for ZenLink component
|
|
27
|
-
*/
|
|
28
|
-
export interface ZenLinkProps {
|
|
29
|
-
/** Target URL path */
|
|
30
|
-
href: string
|
|
31
|
-
/** Optional CSS class(es) */
|
|
32
|
-
class?: string
|
|
33
|
-
/** Link target (_blank, _self, etc.) */
|
|
34
|
-
target?: '_blank' | '_self' | '_parent' | '_top'
|
|
35
|
-
/** Click handler (called before navigation) */
|
|
36
|
-
onClick?: (event: MouseEvent) => void | boolean
|
|
37
|
-
/** Preload the linked page on hover */
|
|
38
|
-
preload?: boolean
|
|
39
|
-
/** Future: Transition configuration */
|
|
40
|
-
onTransition?: TransitionHandler
|
|
41
|
-
/** Future: Disable page transition animation */
|
|
42
|
-
noTransition?: boolean
|
|
43
|
-
/** Match exact path for active state */
|
|
44
|
-
exact?: boolean
|
|
45
|
-
/** Additional aria attributes */
|
|
46
|
-
ariaLabel?: string
|
|
47
|
-
/** Replace history instead of push */
|
|
48
|
-
replace?: boolean
|
|
49
|
-
/** Link content (children) */
|
|
50
|
-
children?: string | HTMLElement | HTMLElement[]
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Transition context for Transitions API
|
|
55
|
-
*/
|
|
56
|
-
export interface TransitionContext {
|
|
57
|
-
/** Current page element */
|
|
58
|
-
currentPage: HTMLElement | null
|
|
59
|
-
/** Next page element (after load) */
|
|
60
|
-
nextPage: HTMLElement | null
|
|
61
|
-
/** Between/loading page element */
|
|
62
|
-
betweenPage: HTMLElement | null
|
|
63
|
-
/** Navigation direction */
|
|
64
|
-
direction: 'forward' | 'back'
|
|
65
|
-
/** Origin path */
|
|
66
|
-
fromPath: string
|
|
67
|
-
/** Destination path */
|
|
68
|
-
toPath: string
|
|
69
|
-
/** Route params */
|
|
70
|
-
params: Record<string, string>
|
|
71
|
-
/** Query params */
|
|
72
|
-
query: Record<string, string>
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Transition handler function
|
|
77
|
-
*/
|
|
78
|
-
export type TransitionHandler = (context: TransitionContext) => void | Promise<void>
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Navigation options
|
|
82
|
-
*/
|
|
83
|
-
export interface NavigateOptions {
|
|
84
|
-
/** Replace current history entry instead of pushing */
|
|
85
|
-
replace?: boolean
|
|
86
|
-
/** Scroll to top after navigation */
|
|
87
|
-
scrollToTop?: boolean
|
|
88
|
-
/** Transition handler for this navigation */
|
|
89
|
-
onTransition?: TransitionHandler
|
|
90
|
-
/** Skip transition animation */
|
|
91
|
-
noTransition?: boolean
|
|
92
|
-
/** State to pass to the next page */
|
|
93
|
-
state?: Record<string, unknown>
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ============================================
|
|
97
|
-
// Internal State
|
|
98
|
-
// ============================================
|
|
99
|
-
|
|
100
|
-
/** Prefetched routes cache */
|
|
101
|
-
const prefetchedRoutes = new Set<string>()
|
|
102
|
-
|
|
103
|
-
/** Global transition handler (set at layout level) */
|
|
104
|
-
let globalTransitionHandler: TransitionHandler | null = null
|
|
105
|
-
|
|
106
|
-
/** Navigation in progress flag */
|
|
107
|
-
let isNavigating = false
|
|
108
|
-
|
|
109
|
-
// ============================================
|
|
110
|
-
// Utilities
|
|
111
|
-
// ============================================
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Check if URL is external (different origin)
|
|
115
|
-
*/
|
|
116
|
-
export function isExternalUrl(url: string): boolean {
|
|
117
|
-
if (!url) return false
|
|
118
|
-
|
|
119
|
-
// Protocol-relative or absolute URLs with different origin
|
|
120
|
-
if (url.startsWith('//') || url.startsWith('http://') || url.startsWith('https://')) {
|
|
121
|
-
try {
|
|
122
|
-
const linkUrl = new URL(url, window.location.origin)
|
|
123
|
-
return linkUrl.origin !== window.location.origin
|
|
124
|
-
} catch {
|
|
125
|
-
return true
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// mailto:, tel:, javascript:, etc.
|
|
130
|
-
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return false
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Check if link should use SPA navigation
|
|
139
|
-
*/
|
|
140
|
-
export function shouldUseSPANavigation(href: string, target?: string): boolean {
|
|
141
|
-
// Don't use SPA for external links
|
|
142
|
-
if (isExternalUrl(href)) return false
|
|
143
|
-
|
|
144
|
-
// Don't use SPA if target is set (except _self)
|
|
145
|
-
if (target && target !== '_self') return false
|
|
146
|
-
|
|
147
|
-
// Don't use SPA for hash-only links on same page
|
|
148
|
-
if (href.startsWith('#')) return false
|
|
149
|
-
|
|
150
|
-
// Don't use SPA for download links or special protocols
|
|
151
|
-
if (href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:')) {
|
|
152
|
-
return false
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return true
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Normalize a path
|
|
160
|
-
*/
|
|
161
|
-
export function normalizePath(path: string): string {
|
|
162
|
-
// Ensure path starts with /
|
|
163
|
-
if (!path.startsWith('/')) {
|
|
164
|
-
const currentDir = window.location.pathname.split('/').slice(0, -1).join('/')
|
|
165
|
-
path = currentDir + '/' + path
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Remove trailing slash (except for root)
|
|
169
|
-
if (path !== '/' && path.endsWith('/')) {
|
|
170
|
-
path = path.slice(0, -1)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return path
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ============================================
|
|
177
|
-
// Navigation API
|
|
178
|
-
// ============================================
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Navigate to a new URL (SPA navigation)
|
|
182
|
-
*
|
|
183
|
-
* This is the primary API for programmatic navigation.
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* ```ts
|
|
187
|
-
* // Simple navigation
|
|
188
|
-
* navigate('/about')
|
|
189
|
-
*
|
|
190
|
-
* // With options
|
|
191
|
-
* navigate('/dashboard', { replace: true })
|
|
192
|
-
*
|
|
193
|
-
* // With transition
|
|
194
|
-
* navigate('/gallery', {
|
|
195
|
-
* onTransition: async (ctx) => {
|
|
196
|
-
* await animateOut(ctx.currentPage)
|
|
197
|
-
* await animateIn(ctx.nextPage)
|
|
198
|
-
* }
|
|
199
|
-
* })
|
|
200
|
-
* ```
|
|
201
|
-
*/
|
|
202
|
-
export async function zenNavigate(
|
|
203
|
-
to: string,
|
|
204
|
-
options: NavigateOptions = {}
|
|
205
|
-
): Promise<void> {
|
|
206
|
-
// Prevent concurrent navigations
|
|
207
|
-
if (isNavigating) {
|
|
208
|
-
console.warn('[ZenLink] Navigation already in progress')
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
isNavigating = true
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
// Access global router
|
|
216
|
-
const router = (window as any).__zenith_router
|
|
217
|
-
|
|
218
|
-
if (router && router.navigate) {
|
|
219
|
-
// Use router's navigate function
|
|
220
|
-
await router.navigate(to, options)
|
|
221
|
-
} else {
|
|
222
|
-
// Fallback: use History API directly
|
|
223
|
-
const normalizedPath = normalizePath(to)
|
|
224
|
-
|
|
225
|
-
if (options.replace) {
|
|
226
|
-
window.history.replaceState(options.state || null, '', normalizedPath)
|
|
227
|
-
} else {
|
|
228
|
-
window.history.pushState(options.state || null, '', normalizedPath)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Dispatch popstate to trigger route resolution
|
|
232
|
-
window.dispatchEvent(new PopStateEvent('popstate'))
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Scroll to top if requested (default: true)
|
|
236
|
-
if (options.scrollToTop !== false) {
|
|
237
|
-
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
238
|
-
}
|
|
239
|
-
} finally {
|
|
240
|
-
isNavigating = false
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Clean alias
|
|
245
|
-
export const navigate = zenNavigate
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Navigate back in history
|
|
249
|
-
*/
|
|
250
|
-
export function zenBack(): void {
|
|
251
|
-
window.history.back()
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export const back = zenBack
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Navigate forward in history
|
|
258
|
-
*/
|
|
259
|
-
export function zenForward(): void {
|
|
260
|
-
window.history.forward()
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export const forward = zenForward
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Navigate to a specific history index
|
|
267
|
-
*/
|
|
268
|
-
export function zenGo(delta: number): void {
|
|
269
|
-
window.history.go(delta)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export const go = zenGo
|
|
273
|
-
|
|
274
|
-
// ============================================
|
|
275
|
-
// Active State
|
|
276
|
-
// ============================================
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Check if a path is currently active
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* ```ts
|
|
283
|
-
* // Check if on blog section
|
|
284
|
-
* if (isActive('/blog')) {
|
|
285
|
-
* addClass(link, 'active')
|
|
286
|
-
* }
|
|
287
|
-
*
|
|
288
|
-
* // Exact match only
|
|
289
|
-
* if (isActive('/blog', true)) {
|
|
290
|
-
* addClass(link, 'active-exact')
|
|
291
|
-
* }
|
|
292
|
-
* ```
|
|
293
|
-
*/
|
|
294
|
-
export function zenIsActive(path: string, exact: boolean = false): boolean {
|
|
295
|
-
const router = (window as any).__zenith_router
|
|
296
|
-
|
|
297
|
-
if (router && router.isActive) {
|
|
298
|
-
return router.isActive(path, exact)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Fallback: compare with current pathname
|
|
302
|
-
const currentPath = window.location.pathname
|
|
303
|
-
const normalizedPath = normalizePath(path)
|
|
304
|
-
|
|
305
|
-
if (exact) {
|
|
306
|
-
return currentPath === normalizedPath
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Root path special case
|
|
310
|
-
if (normalizedPath === '/') {
|
|
311
|
-
return currentPath === '/'
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return currentPath.startsWith(normalizedPath)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export const isActive = zenIsActive
|
|
318
|
-
|
|
319
|
-
// ============================================
|
|
320
|
-
// Prefetching
|
|
321
|
-
// ============================================
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Prefetch a route for faster navigation
|
|
325
|
-
*
|
|
326
|
-
* @example
|
|
327
|
-
* ```ts
|
|
328
|
-
* // Prefetch on hover
|
|
329
|
-
* element.addEventListener('mouseenter', () => {
|
|
330
|
-
* prefetch('/about')
|
|
331
|
-
* })
|
|
332
|
-
* ```
|
|
333
|
-
*/
|
|
334
|
-
export async function zenPrefetch(path: string): Promise<void> {
|
|
335
|
-
// Normalize path
|
|
336
|
-
const normalizedPath = normalizePath(path)
|
|
337
|
-
|
|
338
|
-
// Don't prefetch if already done
|
|
339
|
-
if (prefetchedRoutes.has(normalizedPath)) {
|
|
340
|
-
return
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Mark as prefetched
|
|
344
|
-
prefetchedRoutes.add(normalizedPath)
|
|
345
|
-
|
|
346
|
-
// Try router prefetch first
|
|
347
|
-
const router = (window as any).__zenith_router
|
|
348
|
-
|
|
349
|
-
if (router && router.prefetch) {
|
|
350
|
-
try {
|
|
351
|
-
await router.prefetch(normalizedPath)
|
|
352
|
-
} catch {
|
|
353
|
-
// Silently ignore prefetch errors
|
|
354
|
-
}
|
|
355
|
-
return
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Fallback: use link preload hint
|
|
359
|
-
try {
|
|
360
|
-
const link = document.createElement('link')
|
|
361
|
-
link.rel = 'prefetch'
|
|
362
|
-
link.href = normalizedPath
|
|
363
|
-
link.as = 'document'
|
|
364
|
-
document.head.appendChild(link)
|
|
365
|
-
} catch {
|
|
366
|
-
// Ignore errors
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export const prefetch = zenPrefetch
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Check if a route has been prefetched
|
|
374
|
-
*/
|
|
375
|
-
export function zenIsPrefetched(path: string): boolean {
|
|
376
|
-
return prefetchedRoutes.has(normalizePath(path))
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export const isPrefetched = zenIsPrefetched
|
|
380
|
-
|
|
381
|
-
// ============================================
|
|
382
|
-
// Transitions API (Future Extension)
|
|
383
|
-
// ============================================
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Set global transition handler
|
|
387
|
-
*
|
|
388
|
-
* This allows setting a layout-level transition that applies to all navigations.
|
|
389
|
-
*
|
|
390
|
-
* @example
|
|
391
|
-
* ```ts
|
|
392
|
-
* // In layout component
|
|
393
|
-
* setGlobalTransition(async (ctx) => {
|
|
394
|
-
* ctx.currentPage?.classList.add('fade-out')
|
|
395
|
-
* await delay(300)
|
|
396
|
-
* ctx.nextPage?.classList.add('fade-in')
|
|
397
|
-
* })
|
|
398
|
-
* ```
|
|
399
|
-
*/
|
|
400
|
-
export function setGlobalTransition(handler: TransitionHandler | null): void {
|
|
401
|
-
globalTransitionHandler = handler
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Get current global transition handler
|
|
406
|
-
*/
|
|
407
|
-
export function getGlobalTransition(): TransitionHandler | null {
|
|
408
|
-
return globalTransitionHandler
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Create a transition context
|
|
413
|
-
*/
|
|
414
|
-
export function createTransitionContext(
|
|
415
|
-
fromPath: string,
|
|
416
|
-
toPath: string,
|
|
417
|
-
direction: 'forward' | 'back' = 'forward'
|
|
418
|
-
): TransitionContext {
|
|
419
|
-
return {
|
|
420
|
-
currentPage: document.querySelector('[data-zen-page]') as HTMLElement | null,
|
|
421
|
-
nextPage: null,
|
|
422
|
-
betweenPage: null,
|
|
423
|
-
direction,
|
|
424
|
-
fromPath,
|
|
425
|
-
toPath,
|
|
426
|
-
params: {},
|
|
427
|
-
query: {}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// ============================================
|
|
432
|
-
// Route State
|
|
433
|
-
// ============================================
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Get current route state
|
|
437
|
-
*/
|
|
438
|
-
export function zenGetRoute(): {
|
|
439
|
-
path: string
|
|
440
|
-
params: Record<string, string>
|
|
441
|
-
query: Record<string, string>
|
|
442
|
-
} {
|
|
443
|
-
const router = (window as any).__zenith_router
|
|
444
|
-
|
|
445
|
-
if (router && router.getRoute) {
|
|
446
|
-
return router.getRoute()
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Fallback
|
|
450
|
-
const query: Record<string, string> = {}
|
|
451
|
-
const params = new URLSearchParams(window.location.search)
|
|
452
|
-
params.forEach((value, key) => {
|
|
453
|
-
query[key] = value
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
path: window.location.pathname,
|
|
458
|
-
params: {},
|
|
459
|
-
query
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export const getRoute = zenGetRoute
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Get a route parameter
|
|
467
|
-
*/
|
|
468
|
-
export function zenGetParam(name: string): string | undefined {
|
|
469
|
-
return zenGetRoute().params[name]
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
export const getParam = zenGetParam
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Get a query parameter
|
|
476
|
-
*/
|
|
477
|
-
export function zenGetQuery(name: string): string | undefined {
|
|
478
|
-
return zenGetRoute().query[name]
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export const getQuery = zenGetQuery
|
|
482
|
-
|
|
483
|
-
// ============================================
|
|
484
|
-
// ZenLink Factory (for programmatic creation)
|
|
485
|
-
// ============================================
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Create a ZenLink element programmatically
|
|
489
|
-
*
|
|
490
|
-
* @example
|
|
491
|
-
* ```ts
|
|
492
|
-
* const link = createZenLink({
|
|
493
|
-
* href: '/about',
|
|
494
|
-
* class: 'nav-link',
|
|
495
|
-
* children: 'About Us'
|
|
496
|
-
* })
|
|
497
|
-
* container.appendChild(link)
|
|
498
|
-
* ```
|
|
499
|
-
*/
|
|
500
|
-
export function createZenLink(props: ZenLinkProps): HTMLAnchorElement {
|
|
501
|
-
const link = document.createElement('a')
|
|
502
|
-
|
|
503
|
-
// Set href
|
|
504
|
-
link.href = props.href
|
|
505
|
-
|
|
506
|
-
// Set class
|
|
507
|
-
const classes = ['zen-link']
|
|
508
|
-
if (props.class) classes.push(props.class)
|
|
509
|
-
if (zenIsActive(props.href, props.exact)) classes.push('zen-link-active')
|
|
510
|
-
if (isExternalUrl(props.href)) classes.push('zen-link-external')
|
|
511
|
-
link.className = classes.join(' ')
|
|
512
|
-
|
|
513
|
-
// Set target
|
|
514
|
-
if (props.target) {
|
|
515
|
-
link.target = props.target
|
|
516
|
-
} else if (isExternalUrl(props.href)) {
|
|
517
|
-
link.target = '_blank'
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Set rel for security
|
|
521
|
-
if (isExternalUrl(props.href) || props.target === '_blank') {
|
|
522
|
-
link.rel = 'noopener noreferrer'
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Set aria-label
|
|
526
|
-
if (props.ariaLabel) {
|
|
527
|
-
link.setAttribute('aria-label', props.ariaLabel)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Set content
|
|
531
|
-
if (props.children) {
|
|
532
|
-
if (typeof props.children === 'string') {
|
|
533
|
-
link.textContent = props.children
|
|
534
|
-
} else if (Array.isArray(props.children)) {
|
|
535
|
-
props.children.forEach(child => link.appendChild(child))
|
|
536
|
-
} else {
|
|
537
|
-
link.appendChild(props.children)
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Click handler
|
|
542
|
-
link.addEventListener('click', (event: MouseEvent) => {
|
|
543
|
-
// Allow modifier keys for native behavior
|
|
544
|
-
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
545
|
-
return
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Check if we should use SPA navigation
|
|
549
|
-
if (!shouldUseSPANavigation(props.href, props.target)) {
|
|
550
|
-
return
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Prevent default
|
|
554
|
-
event.preventDefault()
|
|
555
|
-
|
|
556
|
-
// Call user's onClick handler
|
|
557
|
-
if (props.onClick) {
|
|
558
|
-
const result = props.onClick(event)
|
|
559
|
-
if (result === false) return
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Navigate
|
|
563
|
-
zenNavigate(props.href, {
|
|
564
|
-
replace: props.replace,
|
|
565
|
-
onTransition: props.onTransition,
|
|
566
|
-
noTransition: props.noTransition
|
|
567
|
-
})
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
// Preload on hover
|
|
571
|
-
if (props.preload) {
|
|
572
|
-
link.addEventListener('mouseenter', () => {
|
|
573
|
-
if (shouldUseSPANavigation(props.href, props.target)) {
|
|
574
|
-
zenPrefetch(props.href)
|
|
575
|
-
}
|
|
576
|
-
})
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return link
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Alias
|
|
583
|
-
export const zenLink = createZenLink
|
|
584
|
-
|