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 >
0 commit comments