feat(theme): add appearance mode support with toggle functionality

pull/5159/head
HichemF 1 month ago
parent 4fc1db83d7
commit b5ece7ab5a

@ -1,5 +1,5 @@
import siteData from '@siteData'
import { useDark, usePreferredDark } from '@vueuse/core'
import { usePreferredDark } from '@vueuse/core'
import {
computed,
inject,
@ -7,8 +7,10 @@ import {
ref,
shallowRef,
watch,
watchEffect,
type InjectionKey,
type Ref
type Ref,
type WritableComputedRef
} from 'vue'
import {
APPEARANCE_KEY,
@ -20,6 +22,8 @@ import {
} from '../shared'
import type { Route } from './router'
export type AppearanceMode = 'auto' | 'light' | 'dark'
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
export interface VitePressData<T = any> {
@ -49,6 +53,10 @@ export interface VitePressData<T = any> {
dir: Ref<string>
localeIndex: Ref<string>
isDark: Ref<boolean>
/**
* The current appearance mode: 'auto' (follow system), 'light', or 'dark'
*/
appearanceMode: Ref<AppearanceMode>
/**
* Current location hash
*/
@ -67,18 +75,49 @@ export function initData(route: Route): VitePressData {
)
const appearance = site.value.appearance // fine with reactivity being lost here, config change triggers a restart
const isDark =
appearance === 'force-dark'
? ref(true)
: appearance === 'force-auto'
? usePreferredDark()
: appearance
? useDark({
storageKey: APPEARANCE_KEY,
initialValue: () => (appearance === 'dark' ? 'dark' : 'auto'),
...(typeof appearance === 'object' ? appearance : {})
})
: ref(false)
const prefersDark = appearance ? usePreferredDark() : ref(false)
let isDark: Ref<boolean>
let appearanceMode: Ref<AppearanceMode>
if (appearance === 'force-dark') {
isDark = ref(true)
appearanceMode = ref('dark')
} else if (appearance === 'force-auto') {
isDark = prefersDark
appearanceMode = ref('auto')
} else if (appearance) {
const defaultMode: AppearanceMode = appearance === 'dark' ? 'dark' : 'auto'
appearanceMode = ref<AppearanceMode>(
inBrowser
? (localStorage.getItem(APPEARANCE_KEY) as AppearanceMode) ||
defaultMode
: defaultMode
)
isDark = computed({
get: () => {
if (appearanceMode.value === 'auto') return prefersDark.value
return appearanceMode.value === 'dark'
},
set: (v: boolean) => {
appearanceMode.value = v ? 'dark' : 'light'
}
}) as WritableComputedRef<boolean>
if (inBrowser) {
watchEffect(() => {
document.documentElement.classList.toggle('dark', isDark.value)
})
watch(appearanceMode, (val) => {
localStorage.setItem(APPEARANCE_KEY, val)
})
}
} else {
isDark = ref(false)
appearanceMode = ref('light')
}
const hashRef = ref(inBrowser ? location.hash : '')
@ -109,6 +148,7 @@ export function initData(route: Route): VitePressData {
() => route.data.description || site.value.description
),
isDark,
appearanceMode,
hash: computed(() => hashRef.value)
}
}

@ -2,7 +2,7 @@
// so the user can do `import { useRoute, useData } from 'vitepress'`
// generic types
export type { VitePressData } from './app/data'
export type { AppearanceMode, VitePressData } from './app/data'
export type { Route, Router } from './app/router'
// theme types

@ -1,54 +1,79 @@
<script lang="ts" setup>
import { inject, ref, watchPostEffect } from 'vue'
import { useData } from '../composables/data'
import VPSwitch from './VPSwitch.vue'
import type { AppearanceMode } from 'vitepress'
const { isDark, theme } = useData()
const { theme, appearanceMode } = useData()
const modes: AppearanceMode[] = ['auto', 'light', 'dark']
const toggleAppearance = inject('toggle-appearance', () => {
isDark.value = !isDark.value
const current = modes.indexOf(appearanceMode.value)
appearanceMode.value = modes[(current + 1) % 3]
})
const switchTitle = ref('')
watchPostEffect(() => {
switchTitle.value = isDark.value
? theme.value.lightModeSwitchTitle || 'Switch to light theme'
: theme.value.darkModeSwitchTitle || 'Switch to dark theme'
const nextMode = modes[(modes.indexOf(appearanceMode.value) + 1) % 3]
const titles: Record<AppearanceMode, string> = {
auto: theme.value.autoModeSwitchTitle || 'Switch to system theme',
light: theme.value.lightModeSwitchTitle || 'Switch to light theme',
dark: theme.value.darkModeSwitchTitle || 'Switch to dark theme'
}
switchTitle.value = titles[nextMode]
})
</script>
<template>
<VPSwitch
<button
:title="switchTitle"
class="VPSwitchAppearance"
:aria-checked="isDark"
:class="`mode-${appearanceMode}`"
:aria-label="switchTitle"
@click="toggleAppearance"
>
<span class="vpi-sun sun" />
<span class="vpi-moon moon" />
</VPSwitch>
<span class="vpi-system system" />
</button>
</template>
<style scoped>
.sun {
opacity: 1;
.VPSwitchAppearance {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
color: var(--vp-c-text-2);
transition: color 0.5s;
cursor: pointer;
}
.moon {
opacity: 0;
.VPSwitchAppearance:hover {
color: var(--vp-c-text-1);
}
.dark .sun {
.sun,
.moon,
.system {
position: absolute;
width: 20px;
height: 20px;
opacity: 0;
transition: opacity 0.25s ease;
}
.dark .moon {
.mode-light .sun {
opacity: 1;
}
.dark .VPSwitchAppearance :deep(.check) {
/*rtl:ignore*/
transform: translateX(18px);
.mode-dark .moon {
opacity: 1;
}
.mode-auto .system {
opacity: 1;
}
</style>

@ -64,6 +64,9 @@
.vpi-moon {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 3a6 6 0 0 0 9 9a9 9 0 1 1-9-9'/%3E%3C/svg%3E");
}
.vpi-system {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='20' height='14' x='2' y='3' rx='2'/%3E%3Cline x1='8' x2='16' y1='21' y2='21'/%3E%3Cline x1='12' x2='12' y1='17' y2='21'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-more-horizontal {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/g%3E%3C/svg%3E");
}

@ -104,6 +104,11 @@ export namespace DefaultTheme {
*/
darkModeSwitchTitle?: string
/**
* @default 'Switch to system theme'
*/
autoModeSwitchTitle?: string
/**
* @default 'Menu'
*/

Loading…
Cancel
Save