Skip to content

Commit 126cda9

Browse files
authored
feat: add excessive animation when switching themes (#24)
1 parent 5522a83 commit 126cda9

File tree

4 files changed

+126
-3
lines changed

4 files changed

+126
-3
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"homepage": "https://github.com/tolking/vitepress-theme-ououe#readme",
5555
"lint-staged": {
5656
"*.{ts,vue,js,tsx,jsx}": [
57-
"prettier --write --no-verify ",
57+
"prettier --write --no-verify",
5858
"eslint --fix"
5959
],
6060
"*.{html,css,md,json}": "prettier --write"

src/components/VPAppearance.vue

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<script lang="ts" setup>
2+
import { computed, ref, nextTick } from 'vue'
3+
import { useData } from 'vitepress'
4+
import VPSwitch from 'vitepress/dist/client/theme-default/components/VPSwitch.vue'
5+
6+
const { isDark, theme } = useData()
7+
const switchRef = ref()
8+
9+
const switchTitle = computed(() => {
10+
return isDark.value
11+
? theme.value.lightModeSwitchTitle || 'Switch to light theme'
12+
: theme.value.darkModeSwitchTitle || 'Switch to dark theme'
13+
})
14+
15+
function toggleAppearance() {
16+
const isAppearanceTransition =
17+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
18+
// @ts-expect-error
19+
document.startViewTransition &&
20+
!window.matchMedia('(prefers-reduced-motion: reduce)').matches
21+
if (!isAppearanceTransition) {
22+
isDark.value = !isDark.value
23+
return
24+
}
25+
26+
const switchElement = switchRef.value?.$el
27+
const rect = switchElement.getBoundingClientRect()
28+
const x = rect.left + rect.width / 2
29+
const y = rect.top + rect.height / 2
30+
31+
const endRadius = Math.hypot(
32+
Math.max(x, innerWidth - x),
33+
Math.max(y, innerHeight - y),
34+
)
35+
// @ts-expect-error: Transition API
36+
const transition = document.startViewTransition(async () => {
37+
isDark.value = !isDark.value
38+
await nextTick()
39+
})
40+
transition.ready.then(() => {
41+
const clipPath = [
42+
`circle(0px at ${x}px ${y}px)`,
43+
`circle(${endRadius}px at ${x}px ${y}px)`,
44+
]
45+
document.documentElement.animate(
46+
{
47+
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
48+
},
49+
{
50+
duration: 400,
51+
easing: 'ease-in',
52+
pseudoElement: isDark.value
53+
? '::view-transition-old(root)'
54+
: '::view-transition-new(root)',
55+
},
56+
)
57+
})
58+
}
59+
</script>
60+
61+
<template>
62+
<VPSwitch
63+
ref="switchRef"
64+
:title="switchTitle"
65+
:aria-checked="isDark"
66+
class="VPSwitchAppearance"
67+
@click="toggleAppearance"
68+
>
69+
<span class="vpi-sun sun" />
70+
<span class="vpi-moon moon" />
71+
</VPSwitch>
72+
</template>
73+
74+
<style scoped>
75+
.sun {
76+
opacity: 1;
77+
}
78+
79+
.moon {
80+
opacity: 0;
81+
}
82+
83+
.dark .sun {
84+
opacity: 0;
85+
}
86+
87+
.dark .moon {
88+
opacity: 1;
89+
}
90+
91+
.dark .VPSwitchAppearance :deep(.check) {
92+
/*rtl:ignore*/
93+
transform: translateX(18px);
94+
}
95+
</style>

src/components/VPHeader.vue

+16-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import VPImage from 'vitepress/dist/client/theme-default/components/VPImage.vue'
66
import VPNavBarMenu from 'vitepress/dist/client/theme-default/components/VPNavBarMenu.vue'
77
import VPNavBarSearch from 'vitepress/dist/client/theme-default/components/VPNavBarSearch.vue'
88
import VPNavBarTranslations from 'vitepress/dist/client/theme-default/components/VPNavBarTranslations.vue'
9-
import VPNavBarAppearance from 'vitepress/dist/client/theme-default/components/VPNavBarAppearance.vue'
109
import VPNavBarSocialLinks from 'vitepress/dist/client/theme-default/components/VPNavBarSocialLinks.vue'
1110
import VPNavBarHamburger from 'vitepress/dist/client/theme-default/components/VPNavBarHamburger.vue'
11+
import VPAppearance from './VPAppearance.vue'
1212
import type { HeaderSlots, Theme } from '../types/index'
1313
1414
defineProps<{
@@ -51,7 +51,12 @@ const homeLink = computed(() => {
5151
<slot name="header-search-before" />
5252
<VPNavBarSearch class="search" />
5353
<VPNavBarTranslations class="translations" />
54-
<VPNavBarAppearance class="appearance" />
54+
<div
55+
v-if="site.appearance && site.appearance !== 'force-dark'"
56+
class="VPNavBarAppearance"
57+
>
58+
<VPAppearance />
59+
</div>
5560
<VPNavBarSocialLinks class="social-links" />
5661
<VPNavBarHamburger
5762
:active="isScreenOpen"
@@ -109,10 +114,19 @@ const homeLink = computed(() => {
109114
.header .header-content .social-links {
110115
margin-left: var(--vp-size-space);
111116
}
117+
.VPNavBarAppearance {
118+
display: none;
119+
margin-left: var(--vp-size-space);
120+
}
112121
113122
@media (min-width: 768px) {
114123
.header .header-content .header-logo {
115124
flex-grow: 0;
116125
}
126+
127+
.VPNavBarAppearance {
128+
display: flex;
129+
align-items: center;
130+
}
117131
}
118132
</style>

src/styles/public.css

+14
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,17 @@
104104
.scale-leave-active {
105105
position: absolute;
106106
}
107+
108+
::view-transition-old(root),
109+
::view-transition-new(root) {
110+
animation: none;
111+
mix-blend-mode: normal;
112+
}
113+
::view-transition-old(root),
114+
.dark::view-transition-new(root) {
115+
z-index: 1;
116+
}
117+
::view-transition-new(root),
118+
.dark::view-transition-old(root) {
119+
z-index: 999999999;
120+
}

0 commit comments

Comments
 (0)