feat: make appearance configurable

pull/632/head
Kia Ishii 3 years ago
parent 73bbc6143a
commit 9250460b3f

@ -5,28 +5,10 @@ export default defineConfig({
title: 'VitePress', title: 'VitePress',
description: 'Vite & Vue powered static site generator.', description: 'Vite & Vue powered static site generator.',
// TODO: Do something about this.
head: [
[
'script',
{},
`
;(() => {
const saved = localStorage.getItem('vitepress-theme-appearance')
const prefereDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (!saved || saved === 'auto' ? prefereDark : saved === 'dark') {
document.documentElement.classList.add('dark')
}
})()
`
]
],
themeConfig: { themeConfig: {
nav: [ nav: [
{ text: 'Guide', link: '/guide/what-is-vitepress' }, { text: 'Guide', link: '/guide/what-is-vitepress' },
{ text: 'Config', link: '/config/app-basics' }, { text: 'Configs', link: '/config/app-configs' },
{ {
text: 'Release Notes', text: 'Release Notes',
link: 'https://github.com/vuejs/vitepress/releases' link: 'https://github.com/vuejs/vitepress/releases'
@ -70,8 +52,11 @@ function getGuideSidebar() {
function getConfigSidebar() { function getConfigSidebar() {
return [ return [
{ {
text: 'App Config', text: 'Config',
items: [{ text: 'Basics', link: '/config/app-basics' }] items: [
{ text: 'App Configs', link: '/config/app-configs' },
{ text: 'Theme Configs', link: '/config/theme-configs' }
]
} }
] ]
} }

@ -0,0 +1,82 @@
# App Configs
App configs are where you can define the global settings of the site. App configs define fundamental settings that are not only limited to the theme configs such as configuration for "base directory", or the "title" of the site.
```ts
export default {
// These are app level configs.
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
...
}
```
## base
- Type: `string`
- Default: `/`
The base URL the site will be deployed at. You will need to set this if you plan to deploy your site under a sub path, for example, GitHub pages. If you plan to deploy your site to `https://foo.github.io/bar/`, then you should set base to `'/bar/'`. It should always start and end with a slash.
The base is automatically prepended to all the URLs that start with / in other options, so you only need to specify it once.
```ts
export default {
base: '/base/'
}
```
## lang
- Type: `string`
- Default: `en-US`
The lang attribute for the site. This will render as a `<html lang="en-US">` tag in the page HTML.
```ts
export default {
lang: 'en-US'
}
```
## title
- Type: `string`
- Default: `VitePress`
Title for the site. This will be the suffix for all page titles, and displayed in the nav bar.
```ts
export default {
title: 'VitePress'
}
```
## description
- Type: `string`
- Default: `A VitePress site`
Description for the site. This will render as a `<meta>` tag in the page HTML.
```ts
export default {
description: 'A VitePress site'
}
```
## appearance
- Type: `boolean`
- Default: `true`
Whether to enable "Dark Mode" or not. If the option is set to `true`, it adds `.dark` class to the `<html>` tag.
It also injects inline script that tries to read users settings from local storage by `vitepress-theme-appearance` key and restores users preferred color mode.
```ts
export default {
appearance: true
}
```

@ -0,0 +1,3 @@
# Theme Configs
Coming soon...

@ -84,6 +84,7 @@ defineEmits<{
.menu + .translations::before, .menu + .translations::before,
.menu + .appearance::before, .menu + .appearance::before,
.menu + .social-links::before,
.translations + .appearance::before, .translations + .appearance::before,
.appearance + .social-links::before { .appearance + .social-links::before {
margin-right: 8px; margin-right: 8px;

@ -1,9 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress'
import VPSwitchAppearance from './VPSwitchAppearance.vue' import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site } = useData()
</script> </script>
<template> <template>
<div class="VPNavBarAppearance"> <div v-if="site.appearance" class="VPNavBarAppearance">
<VPSwitchAppearance /> <VPSwitchAppearance />
</div> </div>
</template> </template>

@ -4,7 +4,7 @@ import VPFlyout from './VPFlyout.vue'
import VPSwitchAppearance from './VPSwitchAppearance.vue' import VPSwitchAppearance from './VPSwitchAppearance.vue'
import VPSocialLinks from './VPSocialLinks.vue' import VPSocialLinks from './VPSocialLinks.vue'
const { theme } = useData() const { site, theme } = useData()
</script> </script>
<template> <template>
@ -23,7 +23,7 @@ const { theme } = useData()
</div> </div>
</div> </div>
<div class="group"> <div v-if="site.appearance" class="group">
<div class="item"> <div class="item">
<p class="label">Appearance</p> <p class="label">Appearance</p>
<div class="appearance-action"> <div class="appearance-action">

@ -1,9 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress'
import VPSwitchAppearance from './VPSwitchAppearance.vue' import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site } = useData()
</script> </script>
<template> <template>
<div class="VPNavScreenAppearance"> <div v-if="site.appearance" class="VPNavScreenAppearance">
<p class="text">Appearance</p> <p class="text">Appearance</p>
<VPSwitchAppearance /> <VPSwitchAppearance />
</div> </div>

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { APPEARANCE_KEY } from '../../shared'
import VPSwitch from './VPSwitch.vue' import VPSwitch from './VPSwitch.vue'
import VPIconSun from './icons/VPIconSun.vue' import VPIconSun from './icons/VPIconSun.vue'
import VPIconMoon from './icons/VPIconMoon.vue' import VPIconMoon from './icons/VPIconMoon.vue'
@ -6,11 +7,10 @@ import VPIconMoon from './icons/VPIconMoon.vue'
const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {} const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {}
function useAppearance() { function useAppearance() {
const storageKey = 'vitepress-theme-appearance'
const query = window.matchMedia('(prefers-color-scheme: dark)') const query = window.matchMedia('(prefers-color-scheme: dark)')
const classList = document.documentElement.classList const classList = document.documentElement.classList
let userPreference = localStorage.getItem(storageKey) || 'auto' let userPreference = localStorage.getItem(APPEARANCE_KEY) || 'auto'
let isDark = userPreference === 'auto' let isDark = userPreference === 'auto'
? query.matches ? query.matches
@ -29,7 +29,7 @@ function useAppearance() {
? query.matches ? 'auto' : 'dark' ? query.matches ? 'auto' : 'dark'
: query.matches ? 'light' : 'auto' : query.matches ? 'light' : 'auto'
localStorage.setItem(storageKey, userPreference) localStorage.setItem(APPEARANCE_KEY, userPreference)
} }
function setClass(dark: boolean): void { function setClass(dark: boolean): void {

@ -184,7 +184,7 @@
.vp-doc :not(pre) > code { .vp-doc :not(pre) > code {
border-radius: 4px; border-radius: 4px;
padding: 4px 6px; padding: 3px 6px;
color: var(--vp-c-text-code); color: var(--vp-c-text-code);
background-color: var(--vp-c-bg-mute); background-color: var(--vp-c-bg-mute);
transition: color 0.5s, background-color 0.5s; transition: color 0.5s, background-color 0.5s;

@ -97,6 +97,8 @@ export async function renderPage(
? `${pageData.title} | ${siteData.title}` ? `${pageData.title} | ${siteData.title}`
: siteData.title : siteData.title
const description: string = pageData.description || siteData.description
const head = addSocialTags( const head = addSocialTags(
title, title,
...siteData.head, ...siteData.head,
@ -127,9 +129,7 @@ export async function renderPage(
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title}</title> <title>${title}</title>
<meta name="description" content="${ <meta name="description" content="${description}">
pageData.description || siteData.description
}">
${stylesheetLink} ${stylesheetLink}
${preloadLinksString} ${preloadLinksString}
${prefetchLinkString} ${prefetchLinkString}

@ -14,26 +14,27 @@ import {
SiteData, SiteData,
HeadConfig, HeadConfig,
LocaleConfig, LocaleConfig,
createLangDictionary, DefaultTheme,
DefaultTheme APPEARANCE_KEY,
createLangDictionary
} from './shared' } from './shared'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias' import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug' import _debug from 'debug'
export { resolveSiteDataByRoute } from './shared' export { resolveSiteDataByRoute } from './shared'
export type { MarkdownOptions }
const debug = _debug('vitepress:config') const debug = _debug('vitepress:config')
export type { MarkdownOptions }
export interface UserConfig<ThemeConfig = any> { export interface UserConfig<ThemeConfig = any> {
extends?: RawConfigExports<ThemeConfig> extends?: RawConfigExports<ThemeConfig>
lang?: string
base?: string base?: string
lang?: string
title?: string title?: string
description?: string description?: string
head?: HeadConfig[] head?: HeadConfig[]
appearance?: boolean
themeConfig?: ThemeConfig themeConfig?: ThemeConfig
locales?: Record<string, LocaleConfig> locales?: Record<string, LocaleConfig>
markdown?: MarkdownOptions markdown?: MarkdownOptions
@ -243,15 +244,41 @@ export async function resolveSiteData(
mode = 'development' mode = 'development'
): Promise<SiteData> { ): Promise<SiteData> {
userConfig = userConfig || (await resolveUserConfig(root, command, mode))[0] userConfig = userConfig || (await resolveUserConfig(root, command, mode))[0]
return { return {
lang: userConfig.lang || 'en-US', lang: userConfig.lang || 'en-US',
title: userConfig.title || 'VitePress', title: userConfig.title || 'VitePress',
description: userConfig.description || 'A VitePress site', description: userConfig.description || 'A VitePress site',
base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/', base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/',
head: userConfig.head || [], head: resolveSiteDataHead(userConfig),
appearance: userConfig.appearance ?? true,
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
langs: createLangDictionary(userConfig), langs: createLangDictionary(userConfig),
scrollOffset: userConfig.scrollOffset || 90 scrollOffset: userConfig.scrollOffset || 90
} }
} }
function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] {
const head = userConfig?.head ?? []
// add inline script to apply dark mode, if user enables the feature.
// this is required to prevent "flush" on initial page load.
if (userConfig?.appearance ?? true) {
head.push([
'script',
{},
`
;(() => {
const saved = localStorage.getItem('${APPEARANCE_KEY}')
const prefereDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (!saved || saved === 'auto' ? prefereDark : saved === 'dark') {
document.documentElement.classList.add('dark')
}
})()
`
])
}
return head
}

@ -10,6 +10,7 @@ export type {
} from '../../types/shared' } from '../../types/shared'
export const EXTERNAL_URL_RE = /^https?:/i export const EXTERNAL_URL_RE = /^https?:/i
export const APPEARANCE_KEY = 'vitepress-theme-appearance'
// @ts-ignore // @ts-ignore
export const inBrowser = typeof window !== 'undefined' export const inBrowser = typeof window !== 'undefined'

17
types/shared.d.ts vendored

@ -11,19 +11,30 @@ export interface PageData {
lastUpdated?: number lastUpdated?: number
} }
export interface Header {
level: number
title: string
slug: string
}
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
base: string base: string
/** /**
* Language of the site as it should be set on the `html` element. * Language of the site as it should be set on the `html` element.
*
* @example `en-US`, `zh-CN` * @example `en-US`, `zh-CN`
*/ */
lang: string lang: string
title: string title: string
description: string description: string
head: HeadConfig[] head: HeadConfig[]
appearance: boolean
themeConfig: ThemeConfig themeConfig: ThemeConfig
scrollOffset: number | string scrollOffset: number | string
locales: Record<string, LocaleConfig> locales: Record<string, LocaleConfig>
/** /**
* Available locales for the site when it has defined `locales` in its * Available locales for the site when it has defined `locales` in its
* `themeConfig`. This object is otherwise empty. Keys are paths like `/` or * `themeConfig`. This object is otherwise empty. Keys are paths like `/` or
@ -50,12 +61,6 @@ export type HeadConfig =
| [string, Record<string, string>] | [string, Record<string, string>]
| [string, Record<string, string>, string] | [string, Record<string, string>, string]
export interface Header {
level: number
title: string
slug: string
}
export interface LocaleConfig { export interface LocaleConfig {
lang: string lang: string
title?: string title?: string

Loading…
Cancel
Save