forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1/**
2 * Scoped View Transitions plugin.
3 *
4 * Only triggers the View Transition API when navigating between `/` and `/search`
5 * (the search-box morph animation). All other navigations are left untouched so
6 * they feel instant.
7 */
8export default defineNuxtPlugin(nuxtApp => {
9 if (!document.startViewTransition) return
10
11 let transition: ViewTransition | undefined
12 let finishTransition: (() => void) | undefined
13 let hasUAVisualTransition = false
14
15 const resetTransitionState = () => {
16 transition = undefined
17 finishTransition = undefined
18 hasUAVisualTransition = false
19 }
20
21 // Respect browser-initiated visual transitions (e.g. swipe-back)
22 window.addEventListener('popstate', event => {
23 hasUAVisualTransition =
24 (event as PopStateEvent & { hasUAVisualTransition?: boolean }).hasUAVisualTransition ?? false
25 if (hasUAVisualTransition) {
26 transition?.skipTransition()
27 }
28 })
29
30 const router = useRouter()
31
32 router.beforeResolve(async (to, from) => {
33 if (to.matched.length === 0) return
34
35 const toPath = to.path
36 const fromPath = from.path
37
38 // Only transition between / and /search
39 if (!isSearchTransition(toPath, fromPath)) return
40
41 // Respect prefers-reduced-motion
42 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
43
44 // Skip if browser already handled the visual transition
45 if (hasUAVisualTransition) return
46
47 const promise = new Promise<void>(resolve => {
48 finishTransition = resolve
49 })
50
51 let changeRoute: () => void
52 const ready = new Promise<void>(resolve => (changeRoute = resolve))
53
54 transition = document.startViewTransition(() => {
55 changeRoute!()
56 return promise
57 })
58
59 transition.finished.then(resetTransitionState)
60
61 await nuxtApp.callHook('page:view-transition:start', transition)
62
63 return ready
64 })
65
66 // Abort on errors
67 router.onError(() => {
68 finishTransition?.()
69 resetTransitionState()
70 })
71 nuxtApp.hook('app:error', () => {
72 finishTransition?.()
73 resetTransitionState()
74 })
75 nuxtApp.hook('vue:error', () => {
76 finishTransition?.()
77 resetTransitionState()
78 })
79
80 // Finish when page render completes
81 nuxtApp.hook('page:finish', () => {
82 finishTransition?.()
83 resetTransitionState()
84 })
85})
86
87/** Return true when navigating between `/` and `/search` (either direction). */
88function isSearchTransition(toPath: string, fromPath: string): boolean {
89 const paths = new Set([toPath, fromPath])
90 return paths.has('/') && paths.has('/search')
91}