Skip to content

Commit 6f95d74

Browse files
committed
feat: View_Transitions_API
1 parent 60e4ae8 commit 6f95d74

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<script lang="ts" setup>
2+
import { ref, onMounted, watch } from 'vue'
3+
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
4+
import { APPEARANCE_KEY } from 'vitepress/dist/client/shared'
5+
import VPSwitch from 'vitepress/dist/client/theme-default/components/VPSwitch.vue'
6+
import VPIconSun from 'vitepress/dist/client/theme-default/components/icons/VPIconSun.vue'
7+
import VPIconMoon from 'vitepress/dist/client/theme-default/components/icons/VPIconMoon.vue'
8+
9+
const { site, isDark } = useData()
10+
const checked = ref(false)
11+
const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {}
12+
13+
onMounted(() => {
14+
checked.value = document.documentElement.classList.contains('dark')
15+
})
16+
17+
// @ts-expect-error: Transition API
18+
const isAppearanceTransition = document.startViewTransition &&
19+
!window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
20+
21+
function useAppearance() {
22+
const query = window.matchMedia('(prefers-color-scheme: dark)')
23+
const classList = document.documentElement.classList
24+
25+
let userPreference = localStorage.getItem(APPEARANCE_KEY)
26+
27+
let isDark =
28+
(site.value.appearance === 'dark' && userPreference == null) ||
29+
(userPreference === 'auto' || userPreference == null
30+
? query.matches
31+
: userPreference === 'dark')
32+
33+
query.onchange = (e) => {
34+
if (userPreference === 'auto') {
35+
setClass((isDark = e.matches))
36+
}
37+
}
38+
39+
function toggle(event: MouseEvent) {
40+
if (!isAppearanceTransition) {
41+
setClass((isDark = !isDark))
42+
43+
userPreference = isDark
44+
? query.matches ? 'auto' : 'dark'
45+
: query.matches ? 'light' : 'auto'
46+
47+
localStorage.setItem(APPEARANCE_KEY, userPreference)
48+
49+
return
50+
}
51+
52+
const x = event.clientX
53+
const y = event.clientY
54+
const endRadius = Math.hypot(
55+
Math.max(x, innerWidth - x),
56+
Math.max(y, innerHeight - y),
57+
)
58+
59+
// @ts-expect-error: Transition API
60+
const transition = document.startViewTransition(() => {
61+
setClass((isDark = !isDark))
62+
63+
userPreference = isDark
64+
? query.matches ? 'auto' : 'dark'
65+
: query.matches ? 'light' : 'auto'
66+
67+
localStorage.setItem(APPEARANCE_KEY, userPreference)
68+
})
69+
70+
transition.ready.then(() => {
71+
const clipPath = [
72+
`circle(0px at ${x}px ${y}px)`,
73+
`circle(${endRadius}px at ${x}px ${y}px)`,
74+
]
75+
76+
document.documentElement.animate(
77+
{
78+
clipPath: isDark ? clipPath : [...clipPath].reverse(),
79+
},
80+
{
81+
duration: 300,
82+
easing: 'ease-in',
83+
pseudoElement: isDark ? '::view-transition-new(root)' : '::view-transition-old(root)',
84+
},
85+
)
86+
})
87+
}
88+
89+
function setClass(dark: boolean): void {
90+
const css = document.createElement('style')
91+
css.type = 'text/css'
92+
css.appendChild(
93+
document.createTextNode(
94+
`:not(.VPSwitchAppearance):not(.VPSwitchAppearance *) {
95+
-webkit-transition: none !important;
96+
-moz-transition: none !important;
97+
-o-transition: none !important;
98+
-ms-transition: none !important;
99+
transition: none !important;
100+
}`
101+
)
102+
)
103+
document.head.appendChild(css)
104+
105+
checked.value = dark
106+
classList[dark ? 'add' : 'remove']('dark')
107+
108+
const _ = window.getComputedStyle(css).opacity
109+
document.head.removeChild(css)
110+
}
111+
112+
return toggle
113+
}
114+
115+
watch(checked, (newIsDark) => {
116+
isDark.value = newIsDark
117+
})
118+
</script>
119+
120+
<template>
121+
<label title="toggle dark mode">
122+
<VPSwitch
123+
class="VPSwitchAppearance"
124+
:class="{ 'VPSwitchAppearanceTransition': isAppearanceTransition }"
125+
:aria-checked="checked"
126+
@click="toggle"
127+
>
128+
<VPIconSun class="sun" />
129+
<VPIconMoon class="moon" />
130+
</VPSwitch>
131+
</label>
132+
</template>
133+
134+
<style scoped>
135+
.sun {
136+
opacity: 1;
137+
}
138+
139+
.moon {
140+
opacity: 0;
141+
}
142+
143+
.dark .sun {
144+
opacity: 0;
145+
}
146+
147+
.dark .moon {
148+
opacity: 1;
149+
}
150+
151+
.VPSwitchAppearance.VPSwitchAppearanceTransition {
152+
width: 22px;
153+
}
154+
155+
.dark .VPSwitchAppearance:not(.VPSwitchAppearanceTransition) :deep(.check) {
156+
/*rtl:ignore*/
157+
transform: translateX(18px);
158+
}
159+
</style>
160+
161+
<style>
162+
::view-transition-old(root),
163+
::view-transition-new(root) {
164+
animation: none;
165+
mix-blend-mode: normal;
166+
}
167+
168+
::view-transition-old(root) {
169+
z-index: 9999;
170+
}
171+
172+
::view-transition-new(root) {
173+
z-index: 1;
174+
}
175+
176+
.dark::view-transition-old(root) {
177+
z-index: 1;
178+
}
179+
180+
.dark::view-transition-new(root) {
181+
z-index: 9999;
182+
}
183+
</style>

.vitepress/config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import PanguPlugin from 'markdown-it-pangu'
77
import { createWriteStream } from 'node:fs'
88
import { resolve } from 'node:path'
99
import { SitemapStream } from 'sitemap'
10+
import { fileURLToPath, URL } from 'node:url'
1011

1112
const links = []
1213

@@ -175,5 +176,17 @@ export default withMermaid({
175176
sitemap.end()
176177
await new Promise((r) => writeStream.on('finish', r))
177178
},
179+
vite: {
180+
resolve: {
181+
alias: [
182+
{
183+
find: /^.*\/VPSwitchAppearance\.vue$/,
184+
replacement: fileURLToPath(
185+
new URL('./components/CustomSwitchAppearance.vue', import.meta.url)
186+
)
187+
}
188+
]
189+
}
190+
}
178191
})
179192

0 commit comments

Comments
 (0)