@tanstack/vue-router 0.0.1 → 1.140.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.
Files changed (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -45
  3. package/dist/esm/Asset.d.ts +2 -0
  4. package/dist/esm/Asset.js +33 -0
  5. package/dist/esm/Asset.js.map +1 -0
  6. package/dist/esm/CatchBoundary.d.ts +19 -0
  7. package/dist/esm/CatchBoundary.js +135 -0
  8. package/dist/esm/CatchBoundary.js.map +1 -0
  9. package/dist/esm/ClientOnly.d.ts +67 -0
  10. package/dist/esm/HeadContent.d.ts +10 -0
  11. package/dist/esm/HeadContent.js +116 -0
  12. package/dist/esm/HeadContent.js.map +1 -0
  13. package/dist/esm/Match.d.ts +25 -0
  14. package/dist/esm/Match.js +262 -0
  15. package/dist/esm/Match.js.map +1 -0
  16. package/dist/esm/Matches.d.ts +39 -0
  17. package/dist/esm/Matches.js +186 -0
  18. package/dist/esm/Matches.js.map +1 -0
  19. package/dist/esm/RouterProvider.d.ts +33 -0
  20. package/dist/esm/RouterProvider.js +65 -0
  21. package/dist/esm/RouterProvider.js.map +1 -0
  22. package/dist/esm/SafeFragment.d.ts +4 -0
  23. package/dist/esm/ScriptOnce.d.ts +5 -0
  24. package/dist/esm/ScriptOnce.js +21 -0
  25. package/dist/esm/ScriptOnce.js.map +1 -0
  26. package/dist/esm/Scripts.d.ts +1 -0
  27. package/dist/esm/Scripts.js +46 -0
  28. package/dist/esm/Scripts.js.map +1 -0
  29. package/dist/esm/ScrollRestoration.d.ts +14 -0
  30. package/dist/esm/ScrollRestoration.js +36 -0
  31. package/dist/esm/ScrollRestoration.js.map +1 -0
  32. package/dist/esm/Transitioner.d.ts +2 -0
  33. package/dist/esm/Transitioner.js +154 -0
  34. package/dist/esm/Transitioner.js.map +1 -0
  35. package/dist/esm/awaited.d.ts +12 -0
  36. package/dist/esm/awaited.js +40 -0
  37. package/dist/esm/awaited.js.map +1 -0
  38. package/dist/esm/fileRoute.d.ts +54 -0
  39. package/dist/esm/fileRoute.js +103 -0
  40. package/dist/esm/fileRoute.js.map +1 -0
  41. package/dist/esm/history.d.ts +8 -0
  42. package/dist/esm/index.d.ts +51 -0
  43. package/dist/esm/index.js +138 -0
  44. package/dist/esm/index.js.map +1 -0
  45. package/dist/esm/lazyRouteComponent.d.ts +8 -0
  46. package/dist/esm/lazyRouteComponent.js +106 -0
  47. package/dist/esm/lazyRouteComponent.js.map +1 -0
  48. package/dist/esm/link.d.ts +61 -0
  49. package/dist/esm/link.js +376 -0
  50. package/dist/esm/link.js.map +1 -0
  51. package/dist/esm/matchContext.d.ts +20 -0
  52. package/dist/esm/matchContext.js +16 -0
  53. package/dist/esm/matchContext.js.map +1 -0
  54. package/dist/esm/not-found.d.ts +12 -0
  55. package/dist/esm/not-found.js +45 -0
  56. package/dist/esm/not-found.js.map +1 -0
  57. package/dist/esm/renderRouteNotFound.d.ts +11 -0
  58. package/dist/esm/renderRouteNotFound.js +19 -0
  59. package/dist/esm/renderRouteNotFound.js.map +1 -0
  60. package/dist/esm/route.d.ts +96 -0
  61. package/dist/esm/route.js +176 -0
  62. package/dist/esm/route.js.map +1 -0
  63. package/dist/esm/router.d.ts +69 -0
  64. package/dist/esm/router.js +14 -0
  65. package/dist/esm/router.js.map +1 -0
  66. package/dist/esm/routerContext.d.ts +21 -0
  67. package/dist/esm/routerContext.js +21 -0
  68. package/dist/esm/routerContext.js.map +1 -0
  69. package/dist/esm/scroll-restoration.d.ts +1 -0
  70. package/dist/esm/scroll-restoration.js +21 -0
  71. package/dist/esm/scroll-restoration.js.map +1 -0
  72. package/dist/esm/typePrimitives.d.ts +10 -0
  73. package/dist/esm/useBlocker.d.ts +66 -0
  74. package/dist/esm/useBlocker.js +295 -0
  75. package/dist/esm/useBlocker.js.map +1 -0
  76. package/dist/esm/useCanGoBack.d.ts +1 -0
  77. package/dist/esm/useCanGoBack.js +8 -0
  78. package/dist/esm/useCanGoBack.js.map +1 -0
  79. package/dist/esm/useLoaderData.d.ts +8 -0
  80. package/dist/esm/useLoaderData.js +14 -0
  81. package/dist/esm/useLoaderData.js.map +1 -0
  82. package/dist/esm/useLoaderDeps.d.ts +7 -0
  83. package/dist/esm/useLoaderDeps.js +17 -0
  84. package/dist/esm/useLoaderDeps.js.map +1 -0
  85. package/dist/esm/useLocation.d.ts +7 -0
  86. package/dist/esm/useLocation.js +10 -0
  87. package/dist/esm/useLocation.js.map +1 -0
  88. package/dist/esm/useMatch.d.ts +10 -0
  89. package/dist/esm/useMatch.js +39 -0
  90. package/dist/esm/useMatch.js.map +1 -0
  91. package/dist/esm/useNavigate.d.ts +5 -0
  92. package/dist/esm/useNavigate.js +29 -0
  93. package/dist/esm/useNavigate.js.map +1 -0
  94. package/dist/esm/useParams.d.ts +9 -0
  95. package/dist/esm/useParams.js +15 -0
  96. package/dist/esm/useParams.js.map +1 -0
  97. package/dist/esm/useRouteContext.d.ts +4 -0
  98. package/dist/esm/useRouteContext.js +11 -0
  99. package/dist/esm/useRouteContext.js.map +1 -0
  100. package/dist/esm/useRouter.d.ts +4 -0
  101. package/dist/esm/useRouter.js +12 -0
  102. package/dist/esm/useRouter.js.map +1 -0
  103. package/dist/esm/useRouterState.d.ts +8 -0
  104. package/dist/esm/useRouterState.js +20 -0
  105. package/dist/esm/useRouterState.js.map +1 -0
  106. package/dist/esm/useSearch.d.ts +9 -0
  107. package/dist/esm/useSearch.js +15 -0
  108. package/dist/esm/useSearch.js.map +1 -0
  109. package/dist/esm/utils.d.ts +40 -0
  110. package/dist/esm/utils.js +44 -0
  111. package/dist/esm/utils.js.map +1 -0
  112. package/dist/source/Asset.d.ts +2 -0
  113. package/dist/source/Asset.jsx +22 -0
  114. package/dist/source/Asset.jsx.map +1 -0
  115. package/dist/source/CatchBoundary.d.ts +19 -0
  116. package/dist/source/CatchBoundary.jsx +134 -0
  117. package/dist/source/CatchBoundary.jsx.map +1 -0
  118. package/dist/source/ClientOnly.d.ts +67 -0
  119. package/dist/source/ClientOnly.jsx +63 -0
  120. package/dist/source/ClientOnly.jsx.map +1 -0
  121. package/dist/source/HeadContent.d.ts +10 -0
  122. package/dist/source/HeadContent.jsx +133 -0
  123. package/dist/source/HeadContent.jsx.map +1 -0
  124. package/dist/source/Match.d.ts +25 -0
  125. package/dist/source/Match.jsx +316 -0
  126. package/dist/source/Match.jsx.map +1 -0
  127. package/dist/source/Matches.d.ts +39 -0
  128. package/dist/source/Matches.jsx +191 -0
  129. package/dist/source/Matches.jsx.map +1 -0
  130. package/dist/source/RouterProvider.d.ts +33 -0
  131. package/dist/source/RouterProvider.jsx +63 -0
  132. package/dist/source/RouterProvider.jsx.map +1 -0
  133. package/dist/source/SafeFragment.d.ts +4 -0
  134. package/dist/source/SafeFragment.jsx +10 -0
  135. package/dist/source/SafeFragment.jsx.map +1 -0
  136. package/dist/source/ScriptOnce.d.ts +5 -0
  137. package/dist/source/ScriptOnce.jsx +17 -0
  138. package/dist/source/ScriptOnce.jsx.map +1 -0
  139. package/dist/source/Scripts.d.ts +1 -0
  140. package/dist/source/Scripts.jsx +49 -0
  141. package/dist/source/Scripts.jsx.map +1 -0
  142. package/dist/source/ScrollRestoration.d.ts +14 -0
  143. package/dist/source/ScrollRestoration.jsx +37 -0
  144. package/dist/source/ScrollRestoration.jsx.map +1 -0
  145. package/dist/source/Transitioner.d.ts +2 -0
  146. package/dist/source/Transitioner.jsx +181 -0
  147. package/dist/source/Transitioner.jsx.map +1 -0
  148. package/dist/source/awaited.d.ts +12 -0
  149. package/dist/source/awaited.jsx +38 -0
  150. package/dist/source/awaited.jsx.map +1 -0
  151. package/dist/source/fileRoute.d.ts +54 -0
  152. package/dist/source/fileRoute.js +98 -0
  153. package/dist/source/fileRoute.js.map +1 -0
  154. package/dist/source/history.d.ts +8 -0
  155. package/dist/source/history.js +2 -0
  156. package/dist/source/history.js.map +1 -0
  157. package/dist/source/index.d.ts +51 -0
  158. package/dist/source/index.jsx +40 -0
  159. package/dist/source/index.jsx.map +1 -0
  160. package/dist/source/lazyRouteComponent.d.ts +8 -0
  161. package/dist/source/lazyRouteComponent.jsx +135 -0
  162. package/dist/source/lazyRouteComponent.jsx.map +1 -0
  163. package/dist/source/link.d.ts +61 -0
  164. package/dist/source/link.jsx +495 -0
  165. package/dist/source/link.jsx.map +1 -0
  166. package/dist/source/matchContext.d.ts +20 -0
  167. package/dist/source/matchContext.jsx +32 -0
  168. package/dist/source/matchContext.jsx.map +1 -0
  169. package/dist/source/not-found.d.ts +12 -0
  170. package/dist/source/not-found.jsx +48 -0
  171. package/dist/source/not-found.jsx.map +1 -0
  172. package/dist/source/renderRouteNotFound.d.ts +11 -0
  173. package/dist/source/renderRouteNotFound.jsx +24 -0
  174. package/dist/source/renderRouteNotFound.jsx.map +1 -0
  175. package/dist/source/route.d.ts +97 -0
  176. package/dist/source/route.js +167 -0
  177. package/dist/source/route.js.map +1 -0
  178. package/dist/source/router.d.ts +70 -0
  179. package/dist/source/router.js +10 -0
  180. package/dist/source/router.js.map +1 -0
  181. package/dist/source/routerContext.d.ts +21 -0
  182. package/dist/source/routerContext.jsx +37 -0
  183. package/dist/source/routerContext.jsx.map +1 -0
  184. package/dist/source/scroll-restoration.d.ts +1 -0
  185. package/dist/source/scroll-restoration.jsx +16 -0
  186. package/dist/source/scroll-restoration.jsx.map +1 -0
  187. package/dist/source/typePrimitives.d.ts +10 -0
  188. package/dist/source/typePrimitives.js +2 -0
  189. package/dist/source/typePrimitives.js.map +1 -0
  190. package/dist/source/useBlocker.d.ts +66 -0
  191. package/dist/source/useBlocker.jsx +308 -0
  192. package/dist/source/useBlocker.jsx.map +1 -0
  193. package/dist/source/useCanGoBack.d.ts +1 -0
  194. package/dist/source/useCanGoBack.js +5 -0
  195. package/dist/source/useCanGoBack.js.map +1 -0
  196. package/dist/source/useLoaderData.d.ts +8 -0
  197. package/dist/source/useLoaderData.jsx +11 -0
  198. package/dist/source/useLoaderData.jsx.map +1 -0
  199. package/dist/source/useLoaderDeps.d.ts +7 -0
  200. package/dist/source/useLoaderDeps.jsx +11 -0
  201. package/dist/source/useLoaderDeps.jsx.map +1 -0
  202. package/dist/source/useLocation.d.ts +7 -0
  203. package/dist/source/useLocation.jsx +7 -0
  204. package/dist/source/useLocation.jsx.map +1 -0
  205. package/dist/source/useMatch.d.ts +10 -0
  206. package/dist/source/useMatch.jsx +46 -0
  207. package/dist/source/useMatch.jsx.map +1 -0
  208. package/dist/source/useNavigate.d.ts +5 -0
  209. package/dist/source/useNavigate.jsx +18 -0
  210. package/dist/source/useNavigate.jsx.map +1 -0
  211. package/dist/source/useParams.d.ts +9 -0
  212. package/dist/source/useParams.jsx +12 -0
  213. package/dist/source/useParams.jsx.map +1 -0
  214. package/dist/source/useRouteContext.d.ts +4 -0
  215. package/dist/source/useRouteContext.js +8 -0
  216. package/dist/source/useRouteContext.js.map +1 -0
  217. package/dist/source/useRouter.d.ts +4 -0
  218. package/dist/source/useRouter.jsx +9 -0
  219. package/dist/source/useRouter.jsx.map +1 -0
  220. package/dist/source/useRouterState.d.ts +8 -0
  221. package/dist/source/useRouterState.jsx +19 -0
  222. package/dist/source/useRouterState.jsx.map +1 -0
  223. package/dist/source/useSearch.d.ts +9 -0
  224. package/dist/source/useSearch.jsx +12 -0
  225. package/dist/source/useSearch.jsx.map +1 -0
  226. package/dist/source/utils.d.ts +40 -0
  227. package/dist/source/utils.js +78 -0
  228. package/dist/source/utils.js.map +1 -0
  229. package/package.json +77 -7
  230. package/src/Asset.tsx +23 -0
  231. package/src/CatchBoundary.tsx +186 -0
  232. package/src/ClientOnly.tsx +75 -0
  233. package/src/HeadContent.tsx +159 -0
  234. package/src/Match.tsx +415 -0
  235. package/src/Matches.tsx +349 -0
  236. package/src/RouterProvider.tsx +117 -0
  237. package/src/SafeFragment.tsx +10 -0
  238. package/src/ScriptOnce.tsx +30 -0
  239. package/src/Scripts.tsx +65 -0
  240. package/src/ScrollRestoration.tsx +69 -0
  241. package/src/Transitioner.tsx +213 -0
  242. package/src/awaited.tsx +54 -0
  243. package/src/fileRoute.ts +271 -0
  244. package/src/history.ts +9 -0
  245. package/src/index.tsx +346 -0
  246. package/src/lazyRouteComponent.tsx +173 -0
  247. package/src/link.tsx +765 -0
  248. package/src/matchContext.tsx +41 -0
  249. package/src/not-found.tsx +55 -0
  250. package/src/renderRouteNotFound.tsx +35 -0
  251. package/src/route.ts +658 -0
  252. package/src/router.ts +103 -0
  253. package/src/routerContext.tsx +53 -0
  254. package/src/scroll-restoration.tsx +29 -0
  255. package/src/typePrimitives.ts +74 -0
  256. package/src/useBlocker.tsx +501 -0
  257. package/src/useCanGoBack.ts +5 -0
  258. package/src/useLoaderData.tsx +50 -0
  259. package/src/useLoaderDeps.tsx +46 -0
  260. package/src/useLocation.tsx +30 -0
  261. package/src/useMatch.tsx +127 -0
  262. package/src/useNavigate.tsx +40 -0
  263. package/src/useParams.tsx +71 -0
  264. package/src/useRouteContext.ts +31 -0
  265. package/src/useRouter.tsx +15 -0
  266. package/src/useRouterState.tsx +43 -0
  267. package/src/useSearch.tsx +71 -0
  268. package/src/utils.ts +111 -0
package/src/link.tsx ADDED
@@ -0,0 +1,765 @@
1
+ import * as Vue from 'vue'
2
+ import {
3
+ deepEqual,
4
+ exactPathTest,
5
+ preloadWarning,
6
+ removeTrailingSlash,
7
+ } from '@tanstack/router-core'
8
+
9
+ import { useRouterState } from './useRouterState'
10
+ import { useRouter } from './useRouter'
11
+ import { useIntersectionObserver } from './utils'
12
+ import { useMatches } from './Matches'
13
+
14
+ import type {
15
+ AnyRouter,
16
+ Constrain,
17
+ LinkCurrentTargetElement,
18
+ LinkOptions,
19
+ RegisteredRouter,
20
+ RoutePaths,
21
+ } from '@tanstack/router-core'
22
+ import type {
23
+ ValidateLinkOptions,
24
+ ValidateLinkOptionsArray,
25
+ } from './typePrimitives'
26
+
27
+ // Type definitions to replace missing Vue JSX types
28
+ type EventHandler<TEvent = Event> = (e: TEvent) => void
29
+ interface HTMLAttributes {
30
+ class?: string
31
+ style?: Record<string, string | number>
32
+ onClick?: EventHandler<MouseEvent>
33
+ onFocus?: EventHandler<FocusEvent>
34
+ // Vue 3's h() function expects lowercase event names after 'on' prefix
35
+ onMouseenter?: EventHandler<MouseEvent>
36
+ onMouseleave?: EventHandler<MouseEvent>
37
+ onMouseover?: EventHandler<MouseEvent>
38
+ onMouseout?: EventHandler<MouseEvent>
39
+ onTouchstart?: EventHandler<TouchEvent>
40
+ // Also accept the camelCase versions for external API compatibility
41
+ onMouseEnter?: EventHandler<MouseEvent>
42
+ onMouseLeave?: EventHandler<MouseEvent>
43
+ onMouseOver?: EventHandler<MouseEvent>
44
+ onMouseOut?: EventHandler<MouseEvent>
45
+ onTouchStart?: EventHandler<TouchEvent>
46
+ [key: string]: any
47
+ }
48
+
49
+ interface StyledProps {
50
+ class?: string
51
+ style?: Record<string, string | number>
52
+ [key: string]: any
53
+ }
54
+
55
+ export function useLinkProps<
56
+ TRouter extends AnyRouter = RegisteredRouter,
57
+ TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
58
+ TTo extends string = '',
59
+ TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
60
+ TMaskTo extends string = '',
61
+ >(
62
+ options: UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
63
+ ): HTMLAttributes {
64
+ const router = useRouter()
65
+ const isTransitioning = Vue.ref(false)
66
+ let hasRenderFetched = false
67
+
68
+ // Ensure router is defined before proceeding
69
+ if (!router) {
70
+ console.warn('useRouter must be used inside a <RouterProvider> component!')
71
+ return {}
72
+ }
73
+
74
+ // Determine if the link is external or internal
75
+ const type = Vue.computed(() => {
76
+ try {
77
+ new URL(`${options.to}`)
78
+ return 'external'
79
+ } catch {
80
+ return 'internal'
81
+ }
82
+ })
83
+
84
+ const currentSearch = useRouterState({
85
+ select: (s) => s.location.searchStr,
86
+ })
87
+
88
+ // when `from` is not supplied, use the leaf route of the current matches as the `from` location
89
+ const from = useMatches({
90
+ select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath,
91
+ })
92
+
93
+ const _options = Vue.computed(() => ({
94
+ ...options,
95
+ from: from.value,
96
+ }))
97
+
98
+ const next = Vue.computed(() => {
99
+ // Depend on search to rebuild when search changes
100
+ currentSearch.value
101
+ return router.buildLocation(_options.value as any)
102
+ })
103
+
104
+ const preload = Vue.computed(() => {
105
+ if (_options.value.reloadDocument) {
106
+ return false
107
+ }
108
+ return options.preload ?? router.options.defaultPreload
109
+ })
110
+
111
+ const preloadDelay = Vue.computed(
112
+ () => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0,
113
+ )
114
+
115
+ const isActive = useRouterState({
116
+ select: (s) => {
117
+ const activeOptions = options.activeOptions
118
+ if (activeOptions?.exact) {
119
+ const testExact = exactPathTest(
120
+ s.location.pathname,
121
+ next.value.pathname,
122
+ router.basepath,
123
+ )
124
+ if (!testExact) {
125
+ return false
126
+ }
127
+ } else {
128
+ const currentPathSplit = removeTrailingSlash(
129
+ s.location.pathname,
130
+ router.basepath,
131
+ ).split('/')
132
+ const nextPathSplit = removeTrailingSlash(
133
+ next.value?.pathname,
134
+ router.basepath,
135
+ )?.split('/')
136
+
137
+ const pathIsFuzzyEqual = nextPathSplit?.every(
138
+ (d, i) => d === currentPathSplit[i],
139
+ )
140
+ if (!pathIsFuzzyEqual) {
141
+ return false
142
+ }
143
+ }
144
+
145
+ if (activeOptions?.includeSearch ?? true) {
146
+ const searchTest = deepEqual(s.location.search, next.value.search, {
147
+ partial: !activeOptions?.exact,
148
+ ignoreUndefined: !activeOptions?.explicitUndefined,
149
+ })
150
+ if (!searchTest) {
151
+ return false
152
+ }
153
+ }
154
+
155
+ if (activeOptions?.includeHash) {
156
+ return s.location.hash === next.value.hash
157
+ }
158
+ return true
159
+ },
160
+ })
161
+
162
+ const doPreload = () =>
163
+ router.preloadRoute(_options.value as any).catch((err: any) => {
164
+ console.warn(err)
165
+ console.warn(preloadWarning)
166
+ })
167
+
168
+ const preloadViewportIoCallback = (
169
+ entry: IntersectionObserverEntry | undefined,
170
+ ) => {
171
+ if (entry?.isIntersecting) {
172
+ doPreload()
173
+ }
174
+ }
175
+
176
+ const ref = Vue.ref<Element | null>(null)
177
+
178
+ useIntersectionObserver(
179
+ ref,
180
+ preloadViewportIoCallback,
181
+ { rootMargin: '100px' },
182
+ { disabled: () => !!options.disabled || !(preload.value === 'viewport') },
183
+ )
184
+
185
+ Vue.effect(() => {
186
+ if (hasRenderFetched) {
187
+ return
188
+ }
189
+ if (!options.disabled && preload.value === 'render') {
190
+ doPreload()
191
+ hasRenderFetched = true
192
+ }
193
+ })
194
+
195
+ // Create safe props that can be spread
196
+ const getPropsSafeToSpread = () => {
197
+ const result: Record<string, any> = {}
198
+ for (const key in options) {
199
+ if (
200
+ ![
201
+ 'activeProps',
202
+ 'inactiveProps',
203
+ 'activeOptions',
204
+ 'to',
205
+ 'preload',
206
+ 'preloadDelay',
207
+ 'hashScrollIntoView',
208
+ 'replace',
209
+ 'startTransition',
210
+ 'resetScroll',
211
+ 'viewTransition',
212
+ 'children',
213
+ 'target',
214
+ 'disabled',
215
+ 'style',
216
+ 'class',
217
+ 'onClick',
218
+ 'onFocus',
219
+ 'onMouseEnter',
220
+ 'onMouseLeave',
221
+ 'onMouseOver',
222
+ 'onMouseOut',
223
+ 'onTouchStart',
224
+ 'ignoreBlocker',
225
+ 'params',
226
+ 'search',
227
+ 'hash',
228
+ 'state',
229
+ 'mask',
230
+ 'reloadDocument',
231
+ '_asChild',
232
+ 'from',
233
+ 'additionalProps',
234
+ ].includes(key)
235
+ ) {
236
+ result[key] = options[key]
237
+ }
238
+ }
239
+ return result
240
+ }
241
+
242
+ if (type.value === 'external') {
243
+ // External links just have simple props
244
+ const externalProps: HTMLAttributes = {
245
+ ...getPropsSafeToSpread(),
246
+ ref,
247
+ href: options.to,
248
+ target: options.target,
249
+ disabled: options.disabled,
250
+ style: options.style,
251
+ class: options.class,
252
+ onClick: options.onClick,
253
+ onFocus: options.onFocus,
254
+ onMouseEnter: options.onMouseEnter,
255
+ onMouseLeave: options.onMouseLeave,
256
+ onMouseOver: options.onMouseOver,
257
+ onMouseOut: options.onMouseOut,
258
+ onTouchStart: options.onTouchStart,
259
+ }
260
+
261
+ // Remove undefined values
262
+ Object.keys(externalProps).forEach((key) => {
263
+ if (externalProps[key] === undefined) {
264
+ delete externalProps[key]
265
+ }
266
+ })
267
+
268
+ return externalProps
269
+ }
270
+
271
+ // The click handler
272
+ const handleClick = (e: MouseEvent): void => {
273
+ // Check actual element's target attribute as fallback
274
+ const elementTarget = (
275
+ e.currentTarget as HTMLAnchorElement | SVGAElement
276
+ )?.getAttribute('target')
277
+ const effectiveTarget =
278
+ options.target !== undefined ? options.target : elementTarget
279
+
280
+ if (
281
+ !options.disabled &&
282
+ !isCtrlEvent(e) &&
283
+ !e.defaultPrevented &&
284
+ (!effectiveTarget || effectiveTarget === '_self') &&
285
+ e.button === 0
286
+ ) {
287
+ // Don't prevent default or handle navigation if reloadDocument is true
288
+ if (_options.value.reloadDocument) {
289
+ return
290
+ }
291
+
292
+ e.preventDefault()
293
+
294
+ isTransitioning.value = true
295
+
296
+ const unsub = router.subscribe('onResolved', () => {
297
+ unsub()
298
+ isTransitioning.value = false
299
+ })
300
+
301
+ // All is well? Navigate!
302
+ router.navigate({
303
+ ..._options.value,
304
+ replace: options.replace,
305
+ resetScroll: options.resetScroll,
306
+ hashScrollIntoView: options.hashScrollIntoView,
307
+ startTransition: options.startTransition,
308
+ viewTransition: options.viewTransition,
309
+ ignoreBlocker: options.ignoreBlocker,
310
+ } as any)
311
+ }
312
+ }
313
+
314
+ // The focus handler
315
+ const handleFocus = (_: FocusEvent) => {
316
+ if (options.disabled) return
317
+ if (preload.value) {
318
+ doPreload()
319
+ }
320
+ }
321
+
322
+ const handleTouchStart = (_: TouchEvent) => {
323
+ if (options.disabled) return
324
+ if (preload.value) {
325
+ doPreload()
326
+ }
327
+ }
328
+
329
+ const handleEnter = (e: MouseEvent) => {
330
+ if (options.disabled) return
331
+ // Use currentTarget (the element with the handler) instead of target (which may be a child)
332
+ const eventTarget = (e.currentTarget ||
333
+ e.target ||
334
+ {}) as LinkCurrentTargetElement
335
+
336
+ if (preload.value) {
337
+ if (eventTarget.preloadTimeout) {
338
+ return
339
+ }
340
+
341
+ eventTarget.preloadTimeout = setTimeout(() => {
342
+ eventTarget.preloadTimeout = null
343
+ doPreload()
344
+ }, preloadDelay.value)
345
+ }
346
+ }
347
+
348
+ const handleLeave = (e: MouseEvent) => {
349
+ if (options.disabled) return
350
+ // Use currentTarget (the element with the handler) instead of target (which may be a child)
351
+ const eventTarget = (e.currentTarget ||
352
+ e.target ||
353
+ {}) as LinkCurrentTargetElement
354
+
355
+ if (eventTarget.preloadTimeout) {
356
+ clearTimeout(eventTarget.preloadTimeout)
357
+ eventTarget.preloadTimeout = null
358
+ }
359
+ }
360
+
361
+ // Helper to compose event handlers - with explicit return type and better type handling
362
+ function composeEventHandlers<T extends Event>(
363
+ handlers: Array<EventHandler<T> | undefined>,
364
+ ): (e: T) => void {
365
+ return (event: T) => {
366
+ for (const handler of handlers) {
367
+ if (handler) {
368
+ handler(event)
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ // Get the active and inactive props
375
+ const resolvedActiveProps = Vue.computed<StyledProps>(() => {
376
+ const activeProps = options.activeProps || (() => ({ class: 'active' }))
377
+ const props = isActive.value
378
+ ? typeof activeProps === 'function'
379
+ ? activeProps()
380
+ : activeProps
381
+ : {}
382
+
383
+ return props || { class: undefined, style: undefined }
384
+ })
385
+
386
+ const resolvedInactiveProps = Vue.computed<StyledProps>(() => {
387
+ const inactiveProps = options.inactiveProps || (() => ({}))
388
+ const props = isActive.value
389
+ ? {}
390
+ : typeof inactiveProps === 'function'
391
+ ? inactiveProps()
392
+ : inactiveProps
393
+
394
+ return props || { class: undefined, style: undefined }
395
+ })
396
+
397
+ const resolvedClassName = Vue.computed(() => {
398
+ const classes = [
399
+ options.class,
400
+ resolvedActiveProps.value?.class,
401
+ resolvedInactiveProps.value?.class,
402
+ ].filter(Boolean)
403
+ return classes.length ? classes.join(' ') : undefined
404
+ })
405
+
406
+ const resolvedStyle = Vue.computed(() => {
407
+ const result: Record<string, string | number> = {}
408
+
409
+ // Merge styles from all sources
410
+ if (options.style) {
411
+ Object.assign(result, options.style)
412
+ }
413
+
414
+ if (resolvedActiveProps.value?.style) {
415
+ Object.assign(result, resolvedActiveProps.value.style)
416
+ }
417
+
418
+ if (resolvedInactiveProps.value?.style) {
419
+ Object.assign(result, resolvedInactiveProps.value.style)
420
+ }
421
+
422
+ return Object.keys(result).length > 0 ? result : undefined
423
+ })
424
+
425
+ const href = Vue.computed(() => {
426
+ if (options.disabled) {
427
+ return undefined
428
+ }
429
+ const nextLocation = next.value
430
+ const maskedLocation = nextLocation?.maskedLocation
431
+
432
+ let hrefValue: string
433
+ if (maskedLocation) {
434
+ hrefValue = maskedLocation.url
435
+ } else {
436
+ hrefValue = nextLocation?.url
437
+ }
438
+
439
+ // Handle origin stripping like Solid does
440
+ if (router.origin && hrefValue?.startsWith(router.origin)) {
441
+ hrefValue = router.history.createHref(
442
+ hrefValue.replace(router.origin, ''),
443
+ )
444
+ }
445
+
446
+ return hrefValue
447
+ })
448
+
449
+ // Create a reactive proxy that reads computed values on access
450
+ // This allows the returned object to stay reactive when used in templates
451
+ // Use shallowReactive to preserve the ref object without unwrapping it
452
+ const reactiveProps: HTMLAttributes = Vue.shallowReactive({
453
+ ...getPropsSafeToSpread(),
454
+ href: undefined as string | undefined,
455
+ ref,
456
+ onClick: composeEventHandlers<MouseEvent>([
457
+ options.onClick,
458
+ handleClick,
459
+ ]) as any,
460
+ onFocus: composeEventHandlers<FocusEvent>([
461
+ options.onFocus,
462
+ handleFocus,
463
+ ]) as any,
464
+ onMouseenter: composeEventHandlers<MouseEvent>([
465
+ options.onMouseEnter,
466
+ handleEnter,
467
+ ]) as any,
468
+ onMouseover: composeEventHandlers<MouseEvent>([
469
+ options.onMouseOver,
470
+ handleEnter,
471
+ ]) as any,
472
+ onMouseleave: composeEventHandlers<MouseEvent>([
473
+ options.onMouseLeave,
474
+ handleLeave,
475
+ ]) as any,
476
+ onMouseout: composeEventHandlers<MouseEvent>([
477
+ options.onMouseOut,
478
+ handleLeave,
479
+ ]) as any,
480
+ onTouchstart: composeEventHandlers<TouchEvent>([
481
+ options.onTouchStart,
482
+ handleTouchStart,
483
+ ]) as any,
484
+ disabled: !!options.disabled,
485
+ target: options.target,
486
+ })
487
+
488
+ // Watch computed values and update reactive props
489
+ Vue.watchEffect(() => {
490
+ // Update from resolved active/inactive props
491
+ const activeP = resolvedActiveProps.value
492
+ const inactiveP = resolvedInactiveProps.value
493
+
494
+ // Update href
495
+ reactiveProps.href = href.value
496
+
497
+ // Update style
498
+ if (resolvedStyle.value) {
499
+ reactiveProps.style = resolvedStyle.value
500
+ } else {
501
+ delete reactiveProps.style
502
+ }
503
+
504
+ // Update class
505
+ if (resolvedClassName.value) {
506
+ reactiveProps.class = resolvedClassName.value
507
+ } else {
508
+ delete reactiveProps.class
509
+ }
510
+
511
+ // Update disabled props
512
+ if (options.disabled) {
513
+ reactiveProps.role = 'link'
514
+ reactiveProps['aria-disabled'] = true
515
+ } else {
516
+ delete reactiveProps.role
517
+ delete reactiveProps['aria-disabled']
518
+ }
519
+
520
+ // Update active status
521
+ if (isActive.value) {
522
+ reactiveProps['data-status'] = 'active'
523
+ reactiveProps['aria-current'] = 'page'
524
+ } else {
525
+ delete reactiveProps['data-status']
526
+ delete reactiveProps['aria-current']
527
+ }
528
+
529
+ // Update transitioning status
530
+ if (isTransitioning.value) {
531
+ reactiveProps['data-transitioning'] = 'transitioning'
532
+ } else {
533
+ delete reactiveProps['data-transitioning']
534
+ }
535
+
536
+ // Merge active/inactive props (excluding class and style which are handled above)
537
+ for (const key of Object.keys(activeP)) {
538
+ if (key !== 'class' && key !== 'style') {
539
+ reactiveProps[key] = activeP[key]
540
+ }
541
+ }
542
+ for (const key of Object.keys(inactiveP)) {
543
+ if (key !== 'class' && key !== 'style') {
544
+ reactiveProps[key] = inactiveP[key]
545
+ }
546
+ }
547
+ })
548
+
549
+ return reactiveProps
550
+ }
551
+
552
+ // Type definitions
553
+ export type UseLinkPropsOptions<
554
+ TRouter extends AnyRouter = RegisteredRouter,
555
+ TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
556
+ TTo extends string | undefined = '.',
557
+ TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
558
+ TMaskTo extends string = '.',
559
+ > = ActiveLinkOptions<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
560
+ HTMLAttributes
561
+
562
+ export type ActiveLinkOptions<
563
+ TComp = 'a',
564
+ TRouter extends AnyRouter = RegisteredRouter,
565
+ TFrom extends string = string,
566
+ TTo extends string | undefined = '.',
567
+ TMaskFrom extends string = TFrom,
568
+ TMaskTo extends string = '.',
569
+ > = LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
570
+ ActiveLinkOptionProps<TComp>
571
+
572
+ type ActiveLinkProps<TComp> = Partial<
573
+ HTMLAttributes & {
574
+ [key: `data-${string}`]: unknown
575
+ }
576
+ >
577
+
578
+ export interface ActiveLinkOptionProps<TComp = 'a'> {
579
+ /**
580
+ * A function that returns additional props for the `active` state of this link.
581
+ * These props override other props passed to the link (`style`'s are merged, `class`'s are concatenated)
582
+ */
583
+ activeProps?: ActiveLinkProps<TComp> | (() => ActiveLinkProps<TComp>)
584
+ /**
585
+ * A function that returns additional props for the `inactive` state of this link.
586
+ * These props override other props passed to the link (`style`'s are merged, `class`'s are concatenated)
587
+ */
588
+ inactiveProps?: ActiveLinkProps<TComp> | (() => ActiveLinkProps<TComp>)
589
+ }
590
+
591
+ export type LinkProps<
592
+ TComp = 'a',
593
+ TRouter extends AnyRouter = RegisteredRouter,
594
+ TFrom extends string = string,
595
+ TTo extends string | undefined = '.',
596
+ TMaskFrom extends string = TFrom,
597
+ TMaskTo extends string = '.',
598
+ > = ActiveLinkOptions<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
599
+ LinkPropsChildren
600
+
601
+ export interface LinkPropsChildren {
602
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
603
+ children?:
604
+ | Vue.VNode
605
+ | ((state: { isActive: boolean; isTransitioning: boolean }) => Vue.VNode)
606
+ }
607
+
608
+ type LinkComponentVueProps<TComp> = TComp extends keyof HTMLElementTagNameMap
609
+ ? Omit<HTMLAttributes, keyof CreateLinkProps>
610
+ : TComp extends Vue.Component
611
+ ? Record<string, any>
612
+ : Record<string, any>
613
+
614
+ export type LinkComponentProps<
615
+ TComp = 'a',
616
+ TRouter extends AnyRouter = RegisteredRouter,
617
+ TFrom extends string = string,
618
+ TTo extends string | undefined = '.',
619
+ TMaskFrom extends string = TFrom,
620
+ TMaskTo extends string = '.',
621
+ > = LinkComponentVueProps<TComp> &
622
+ LinkProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
623
+
624
+ export type CreateLinkProps = LinkProps<
625
+ any,
626
+ any,
627
+ string,
628
+ string,
629
+ string,
630
+ string
631
+ >
632
+
633
+ export type LinkComponent<TComp> = <
634
+ TRouter extends AnyRouter = RegisteredRouter,
635
+ const TFrom extends string = string,
636
+ const TTo extends string | undefined = undefined,
637
+ const TMaskFrom extends string = TFrom,
638
+ const TMaskTo extends string = '',
639
+ >(
640
+ props: LinkComponentProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
641
+ ) => Vue.VNode
642
+
643
+ export function createLink<const TComp>(
644
+ Comp: Constrain<TComp, any, (props: CreateLinkProps) => Vue.VNode>,
645
+ ): LinkComponent<TComp> {
646
+ return Vue.defineComponent({
647
+ name: 'CreatedLink',
648
+ inheritAttrs: false,
649
+ setup(_, { attrs, slots }) {
650
+ return () => Vue.h(Link, { ...attrs, _asChild: Comp }, slots)
651
+ },
652
+ }) as any
653
+ }
654
+
655
+ const LinkImpl = Vue.defineComponent({
656
+ name: 'Link',
657
+ inheritAttrs: false,
658
+ props: [
659
+ '_asChild',
660
+ 'to',
661
+ 'preload',
662
+ 'preloadDelay',
663
+ 'activeProps',
664
+ 'inactiveProps',
665
+ 'activeOptions',
666
+ 'from',
667
+ 'search',
668
+ 'params',
669
+ 'hash',
670
+ 'state',
671
+ 'mask',
672
+ 'reloadDocument',
673
+ 'disabled',
674
+ 'additionalProps',
675
+ 'viewTransition',
676
+ 'resetScroll',
677
+ 'startTransition',
678
+ 'hashScrollIntoView',
679
+ 'replace',
680
+ 'ignoreBlocker',
681
+ 'target',
682
+ ],
683
+ setup(props, { attrs, slots }) {
684
+ // Call useLinkProps ONCE during setup with combined props and attrs
685
+ // The returned object includes computed values that update reactively
686
+ const allProps = { ...props, ...attrs }
687
+ const linkProps = useLinkProps(allProps as any)
688
+
689
+ return () => {
690
+ const Component = props._asChild || 'a'
691
+
692
+ const isActive = linkProps['data-status'] === 'active'
693
+ const isTransitioning =
694
+ linkProps['data-transitioning'] === 'transitioning'
695
+
696
+ // Create the slot content or empty array if no default slot
697
+ const slotContent = slots.default
698
+ ? slots.default({
699
+ isActive,
700
+ isTransitioning,
701
+ })
702
+ : []
703
+
704
+ // Special handling for SVG links - wrap an <a> inside the SVG
705
+ if (Component === 'svg') {
706
+ // Create props without class for svg link
707
+ const svgLinkProps = { ...linkProps }
708
+ delete (svgLinkProps as any).class
709
+ return Vue.h('svg', {}, [Vue.h('a', svgLinkProps, slotContent)])
710
+ }
711
+
712
+ // For custom functional components (non-string), pass children as a prop
713
+ // since they may expect children as a prop like in Solid
714
+ if (typeof Component !== 'string') {
715
+ return Vue.h(
716
+ Component,
717
+ { ...linkProps, children: slotContent },
718
+ slotContent,
719
+ )
720
+ }
721
+
722
+ // Return the component with props and children
723
+ return Vue.h(Component, linkProps, slotContent)
724
+ }
725
+ },
726
+ })
727
+
728
+ /**
729
+ * Link component with proper TypeScript generics support
730
+ */
731
+ export const Link = LinkImpl as unknown as {
732
+ <
733
+ TRouter extends AnyRouter = RegisteredRouter,
734
+ TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
735
+ TTo extends string | undefined = '.',
736
+ TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
737
+ TMaskTo extends string = '.',
738
+ >(
739
+ props: LinkComponentProps<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
740
+ ): Vue.VNode
741
+ }
742
+
743
+ function isCtrlEvent(e: MouseEvent) {
744
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
745
+ }
746
+
747
+ export type LinkOptionsFnOptions<
748
+ TOptions,
749
+ TComp,
750
+ TRouter extends AnyRouter = RegisteredRouter,
751
+ > =
752
+ TOptions extends ReadonlyArray<any>
753
+ ? ValidateLinkOptionsArray<TRouter, TOptions, string, TComp>
754
+ : ValidateLinkOptions<TRouter, TOptions, string, TComp>
755
+
756
+ export type LinkOptionsFn<TComp> = <
757
+ const TOptions,
758
+ TRouter extends AnyRouter = RegisteredRouter,
759
+ >(
760
+ options: LinkOptionsFnOptions<TOptions, TComp, TRouter>,
761
+ ) => TOptions
762
+
763
+ export const linkOptions: LinkOptionsFn<'a'> = (options) => {
764
+ return options as any
765
+ }