├── pnpm-workspace.yaml ├── .husky ├── pre-commit └── commit-msg ├── docs ├── .vuepress │ ├── configs │ │ ├── navbar │ │ │ ├── index.ts │ │ │ ├── en.ts │ │ │ └── zh.ts │ │ ├── sidebar │ │ │ ├── index.ts │ │ │ ├── en.ts │ │ │ └── zh.ts │ │ ├── head.ts │ │ ├── index.ts │ │ └── meta.ts │ └── config.ts ├── zh │ ├── guide │ │ ├── README.md │ │ ├── getting-started.md │ │ ├── markdown.md │ │ ├── frontmatter.md │ │ └── config.md │ └── README.md ├── package.json ├── guide │ ├── getting-started.md │ ├── README.md │ ├── markdown.md │ ├── frontmatter.md │ └── config.md └── README.md ├── .eslintignore ├── packages └── vuepress-theme-mix │ ├── src │ ├── shared │ │ ├── index.ts │ │ ├── page.ts │ │ ├── nav.ts │ │ └── options.ts │ ├── node │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── assignDefaultLocaleOptions.ts │ │ │ └── resolveContainerPluginOptions.ts │ │ ├── index.ts │ │ └── mixTheme.ts │ └── client │ │ ├── utils │ │ ├── index.ts │ │ ├── throttleAndDebounce.ts │ │ ├── shouldShowPageToc.ts │ │ └── isActiveSidebarItem.ts │ │ ├── shim.d.ts │ │ ├── components │ │ ├── global │ │ │ ├── index.ts │ │ │ ├── CodeGroupItem.vue │ │ │ └── CodeGroup.ts │ │ ├── Sidebar.vue │ │ ├── ToggleScreenButton.vue │ │ ├── NavbarFeatures.vue │ │ ├── Navbar.vue │ │ ├── SocialLinks.vue │ │ ├── NavbarBrand.vue │ │ ├── NavbarItems.vue │ │ ├── SidebarItems.vue │ │ ├── PageToc.vue │ │ ├── NavbarScreen.vue │ │ ├── LocalNav.vue │ │ ├── Page.vue │ │ ├── AutoLink.vue │ │ ├── SidebarItem.vue │ │ ├── PageNav.vue │ │ ├── LanguageDropdown.vue │ │ ├── NavbarScreenDropdown.vue │ │ ├── Home.vue │ │ ├── NavbarDropdown.vue │ │ ├── PageMeta.vue │ │ └── ToggleColorModeButton.vue │ │ ├── composables │ │ ├── index.ts │ │ ├── useThemeData.ts │ │ ├── useScrollPromise.ts │ │ ├── useNavLink.ts │ │ ├── useResolveRouteWithRedirect.ts │ │ ├── useActiveAnchor.ts │ │ └── useSidebarItems.ts │ │ ├── config.ts │ │ ├── layouts │ │ ├── NotFound.vue │ │ └── Layout.vue │ │ └── styles │ │ └── index.css │ ├── tsconfig.build.json │ ├── templates │ └── build.html │ ├── package.json │ └── tailwind.config.cjs ├── .prettierrc ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── tsconfig.base.json ├── .commitlintrc.cjs ├── .eslintrc.cjs ├── LICENSE ├── README.md ├── CHANGELOG.md └── package.json /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | - packages/* 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/navbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './en.js' 2 | export * from './zh.js' 3 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './en.js' 2 | export * from './zh.js' 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.vuepress/ 2 | !.*.js 3 | .cache/ 4 | .temp/ 5 | node_modules/ 6 | lib/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 5 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/head.ts: -------------------------------------------------------------------------------- 1 | import type { HeadConfig } from '@vuepress/core' 2 | 3 | export const head: HeadConfig[] = [] 4 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './head.js' 2 | export * from './navbar/index.js' 3 | export * from './sidebar/index.js' 4 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nav.js' 2 | export * from './options.js' 3 | export * from './page.js' 4 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/node/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './assignDefaultLocaleOptions.js' 2 | export * from './resolveContainerPluginOptions.js' 3 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './isActiveSidebarItem.js' 2 | export * from './shouldShowPageToc.js' 3 | export * from './throttleAndDebounce.js' 4 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { ComponentOptions } from 'vue' 3 | 4 | const comp: ComponentOptions 5 | export default comp 6 | } 7 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/global/index.ts: -------------------------------------------------------------------------------- 1 | import { CodeGroup } from './CodeGroup.js' 2 | import CodeGroupItem from './CodeGroupItem.vue' 3 | 4 | export { CodeGroup, CodeGroupItem } 5 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/node/index.ts: -------------------------------------------------------------------------------- 1 | import { mixTheme } from './mixTheme.js' 2 | 3 | export * from '../shared/index.js' 4 | export * from './mixTheme.js' 5 | export * from './utils/index.js' 6 | export default mixTheme 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "editorconfig": true, 3 | "singleQuote": true, 4 | "semi": false, 5 | "trailingComma": "es5", 6 | "arrowParens": "avoid", 7 | "plugins": ["prettier-plugin-tailwindcss"], 8 | "pluginSearchDirs": false 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | # Configurable Prettier behaviors 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 2 12 | max_line_length = 80 13 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/meta.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | 3 | import { fs } from '@vuepress/utils' 4 | 5 | const require = createRequire(import.meta.url) 6 | 7 | export const version = fs.readJsonSync( 8 | require.resolve('vuepress-theme-mix/package.json') 9 | ).version 10 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useActiveAnchor.js' 2 | export * from './useNavLink.js' 3 | export * from './useResolveRouteWithRedirect.js' 4 | export * from './useScrollPromise.js' 5 | export * from './useSidebarItems.js' 6 | export * from './useThemeData.js' 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "vuepress-theme-mix": ["./packages/vuepress-theme-mix/src"] 6 | } 7 | }, 8 | "include": ["**/.vuepress/**/*", "packages/**/*"], 9 | "exclude": ["node_modules", ".temp", "lib", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/sidebar/en.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarConfig } from 'vuepress-theme-mix' 2 | 3 | export const sidebarEn: SidebarConfig = { 4 | '/guide/': [ 5 | '/guide/', 6 | '/guide/getting-started.md', 7 | '/guide/config.md', 8 | '/guide/frontmatter.md', 9 | '/guide/markdown.md', 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/sidebar/zh.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarConfig } from 'vuepress-theme-mix' 2 | 3 | export const sidebarZh: SidebarConfig = { 4 | '/zh/guide/': [ 5 | '/zh/guide/', 6 | '/zh/guide/getting-started.md', 7 | '/zh/guide/config.md', 8 | '/zh/guide/frontmatter.md', 9 | '/zh/guide/markdown.md', 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VuePress files 2 | docs/.vuepress/.temp/ 3 | docs/.vuepress/.cache/ 4 | docs/.vuepress/dist/ 5 | 6 | # Dist files 7 | lib/ 8 | /public/ 9 | 10 | # Node modules 11 | node_modules/ 12 | 13 | # MacOS Desktop Services Store 14 | .DS_Store 15 | 16 | # Log files 17 | *.log 18 | 19 | # Typescript build info 20 | *.tsbuildinfo 21 | 22 | # IDE files 23 | .idea 24 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig-vuepress/base.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "lib": ["DOM", "ES2020"], 6 | "module": "ESNext", 7 | "moduleResolution": "NodeNext", 8 | "noEmitOnError": true, 9 | "noImplicitAny": false, 10 | "skipLibCheck": true, 11 | "target": "ES2020" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "./src", 6 | "outDir": "./lib", 7 | "baseUrl": ".", 8 | "paths": { 9 | "@theme/*": ["./src/client/components/*"] 10 | }, 11 | "types": ["@vuepress/client/types"] 12 | }, 13 | "include": ["./src"] 14 | } 15 | -------------------------------------------------------------------------------- /.commitlintrc.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const getSubDirectories = dir => 5 | fs 6 | .readdirSync(dir) 7 | .filter(item => fs.statSync(path.join(dir, item)).isDirectory()) 8 | const corePackages = getSubDirectories(path.resolve(__dirname, 'packages')) 9 | 10 | module.exports = { 11 | extends: ['@commitlint/config-conventional'], 12 | rules: { 13 | 'scope-enum': [2, 'always', [...corePackages]], 14 | 'footer-max-line-length': [0], 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/utils/throttleAndDebounce.ts: -------------------------------------------------------------------------------- 1 | export function throttleAndDebounce(fn: () => void, delay: number): () => void { 2 | let timeoutId: NodeJS.Timeout 3 | let called = false 4 | 5 | return () => { 6 | if (timeoutId) { 7 | clearTimeout(timeoutId) 8 | } 9 | 10 | if (!called) { 11 | fn() 12 | called = true 13 | setTimeout(() => { 14 | called = false 15 | }, delay) 16 | } else { 17 | timeoutId = setTimeout(fn, delay) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/navbar/en.ts: -------------------------------------------------------------------------------- 1 | import type { NavbarConfig } from 'vuepress-theme-mix' 2 | 3 | import { version } from '../meta.js' 4 | 5 | export const navbarEn: NavbarConfig = [ 6 | { 7 | text: 'Guide', 8 | link: '/guide/', 9 | }, 10 | { 11 | text: 'VuePress 2', 12 | link: 'https://v2.vuepress.vuejs.org/', 13 | }, 14 | { 15 | text: `v${version}`, 16 | children: [ 17 | { 18 | text: 'Changelog', 19 | link: 'https://github.com/gavinliu6/vuepress-theme-mix/blob/main/CHANGELOG.md', 20 | }, 21 | ], 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/navbar/zh.ts: -------------------------------------------------------------------------------- 1 | import type { NavbarConfig } from 'vuepress-theme-mix' 2 | 3 | import { version } from '../meta.js' 4 | 5 | export const navbarZh: NavbarConfig = [ 6 | { 7 | text: '指南', 8 | link: '/zh/guide/', 9 | }, 10 | { 11 | text: 'VuePress 2', 12 | link: 'https://v2.vuepress.vuejs.org/zh/', 13 | }, 14 | { 15 | text: `v${version}`, 16 | children: [ 17 | { 18 | text: '更新日志', 19 | link: 'https://github.com/gavinliu6/vuepress-theme-mix/blob/main/CHANGELOG.md', 20 | }, 21 | ], 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/composables/useThemeData.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ThemeDataRef, 3 | ThemeLocaleDataRef, 4 | } from '@vuepress/plugin-theme-data/client' 5 | import { 6 | useThemeData as _useThemeData, 7 | useThemeLocaleData as _useThemeLocaleData, 8 | } from '@vuepress/plugin-theme-data/client' 9 | 10 | import type { MixThemeData } from '../../shared/index.js' 11 | 12 | export const useThemeData = (): ThemeDataRef => 13 | _useThemeData() 14 | export const useThemeLocaleData = (): ThemeLocaleDataRef => 15 | _useThemeLocaleData() 16 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/global/CodeGroupItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/composables/useScrollPromise.ts: -------------------------------------------------------------------------------- 1 | export interface ScrollPromise { 2 | wait(): Promise | null 3 | pending: () => void 4 | resolve: () => void 5 | } 6 | 7 | let promise: Promise | null = null 8 | let promiseResolve: (() => void) | null = null 9 | 10 | const scrollPromise: ScrollPromise = { 11 | wait: () => promise, 12 | pending: () => { 13 | promise = new Promise(resolve => (promiseResolve = resolve)) 14 | }, 15 | resolve: () => { 16 | promiseResolve?.() 17 | promise = null 18 | promiseResolve = null 19 | }, 20 | } 21 | 22 | export const useScrollPromise = (): ScrollPromise => scrollPromise 23 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'vue-eslint-parser', 4 | parserOptions: { 5 | parser: '@typescript-eslint/parser', 6 | sourceType: 'module', 7 | }, 8 | extends: [ 9 | 'eslint-config-standard', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:vue/vue3-recommended', 12 | 'plugin:prettier/recommended', 13 | ], 14 | plugins: ['@typescript-eslint', 'simple-import-sort'], 15 | rules: { 16 | 'comma-dangle': 'off', 17 | 'import/newline-after-import': 'error', 18 | 'simple-import-sort/imports': 'error', 19 | 'simple-import-sort/exports': 'error', 20 | 'vue/multi-word-component-names': 'off', 21 | 'vue/no-v-html': 'off', 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /docs/zh/guide/README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | VuePress Theme Mix 是一款为 [VuePress 2](https://v2.vuepress.vuejs.org/) 开发的第三方主题,它是以 VuePress 2 的 [默认主题](https://v2.vuepress.vuejs.org/zh/reference/default-theme/config.html) 为基础,在布局和配色等方面深度“魔改”的一款混搭主题,这也是主题名称 _Mix(ture)_ 的由来。 4 | 5 | 在使用本主题前,请务必先阅读下列说明再做决定。 6 | 7 | ::: warning 说明 8 | 首先需要说明的是,作为本主题的作者,我并非一位专业的前端开发者,在开始这个项目时,我对很多前端知识也都不甚了解,其中大多都是我在开发过程中的“现学现卖”,再加之本人水平有限,你在使用该主题的过程中很可能会遇到一些问题。 9 | 10 | 另外,VuePress Theme Mix 只是我工作之余的一个 Side Project,不管是你在 [GitHub](https://github.com/gavinliu6/vuepress-theme-mix/issues) 上提的 issue,还是直接给我发邮件咨询的问题,我可能都不能及时修复或解决。如果你有前端开发经验,并且时间允许,我更加期望你向该项目提交 PR 以使其趋于完善。 11 | 12 | 综上,**作者并不保证本主题在生产环境下的稳定性**,用户需要自己权衡。 13 | ::: 14 | 15 | 如前所述,Mix 是基于 VuePress 2 的默认主题,尽管扩展了一些功能特性,但是在使用配置方面还是保持了很大的一致性。然而,使用本主题并不意味着你需要事先阅读了解官方默认主题的相关知识,后续文档会涵盖 Mix 所有的配置项和用法说明,即使有相当一部分配置项是相同的。如果你已经打算使用 Mix,那就请继续阅读下去吧。 16 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/utils/shouldShowPageToc.ts: -------------------------------------------------------------------------------- 1 | import { pageData, usePageFrontmatter } from '@vuepress/client' 2 | import { computed } from 'vue' 3 | 4 | import type { MixThemePageFrontmatter } from '../../shared/index.js' 5 | import { useThemeLocaleData } from '../composables/index.js' 6 | 7 | export const shouldShowPageToc = computed(() => { 8 | const themeLocale = useThemeLocaleData() 9 | const frontmatter = usePageFrontmatter() 10 | 11 | if (frontmatter.value.home === true) { 12 | if (frontmatter.value.toc === true) { 13 | return true 14 | } 15 | return false 16 | } 17 | 18 | if (frontmatter.value.toc === false) { 19 | return false 20 | } 21 | 22 | if (themeLocale.value.toc === false) { 23 | return false 24 | } 25 | 26 | return pageData.value.headers.length > 0 27 | }) 28 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "docs:build": "vuepress build --clean-cache --clean-temp", 7 | "docs:dev": "vuepress dev --clean-cache --clean-temp", 8 | "docs:serve": "anywhere -s -h localhost -d .vuepress/dist" 9 | }, 10 | "dependencies": { 11 | "@vuepress/bundler-vite": "2.0.0-beta.64", 12 | "@vuepress/bundler-webpack": "2.0.0-beta.64", 13 | "@vuepress/cli": "2.0.0-beta.64", 14 | "@vuepress/client": "2.0.0-beta.64", 15 | "@vuepress/core": "2.0.0-beta.64", 16 | "@vuepress/plugin-docsearch": "2.0.0-beta.64", 17 | "@vuepress/plugin-google-analytics": "2.0.0-beta.64", 18 | "@vuepress/utils": "2.0.0-beta.64", 19 | "anywhere": "^1.6.0", 20 | "vue": "^3.3.4", 21 | "vuepress": "2.0.0-beta.64", 22 | "vuepress-theme-mix": "workspace:*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/templates/build.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/zh/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | ## 安装 4 | 5 | 首先,在你的 VuePress 2 项目中执行下面的命令以安装该主题: 6 | 7 | :::: code-group 8 | ::: code-group-item npm 9 | 10 | ```bash 11 | npm install -D vuepress-theme-mix@next 12 | ``` 13 | 14 | ::: 15 | 16 | ::: code-group-item yarn 17 | 18 | ```bash 19 | yarn add -D vuepress-theme-mix@next 20 | ``` 21 | 22 | ::: 23 | 24 | ::: code-group-item pnpm 25 | 26 | ```bash 27 | pnpm add -D vuepress-theme-mix@next 28 | ``` 29 | 30 | ::: 31 | :::: 32 | 33 | ## 使用与配置 34 | 35 | 安装完成后,你就可以像使用其他 VuePress 主题那样在你的配置文件中通过 `theme` 配置项来设置要使用的主题: 36 | 37 | ```ts 38 | import { defineUserConfig } from 'vuepress' 39 | import mixTheme from 'vuepress-theme-mix' 40 | 41 | export default defineUserConfig({ 42 | // ... 43 | 44 | theme: mixTheme({ 45 | // 在这里配置主题 46 | }), 47 | 48 | // ... 49 | }) 50 | ``` 51 | 52 | 接下来,你就可以按照文档描述配置主题,然后开始编写你的内容了。 53 | 54 | 🎉 55 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/composables/useNavLink.ts: -------------------------------------------------------------------------------- 1 | import type { NavLink } from '../../shared/index.js' 2 | import { useResolveRouteWithRedirect } from './useResolveRouteWithRedirect.js' 3 | 4 | declare module 'vue-router' { 5 | interface RouteMeta { 6 | title?: string 7 | } 8 | } 9 | 10 | /** 11 | * Resolve NavLink props from string 12 | * 13 | * @example 14 | * - Input: '/README.md' 15 | * - Output: { text: 'Home', link: '/' } 16 | */ 17 | export const useNavLink = (item: string): NavLink => { 18 | // the route path of vue-router is url-encoded, and we expect users are using 19 | // non-url-encoded string in theme config, so we need to url-encode it first to 20 | // resolve the route correctly 21 | const resolved = useResolveRouteWithRedirect(encodeURI(item)) 22 | return { 23 | text: resolved.meta.title || item, 24 | link: resolved.name === '404' ? item : resolved.fullPath, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/ToggleScreenButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/composables/useResolveRouteWithRedirect.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isString } from '@vuepress/shared' 2 | import type { Router } from 'vue-router' 3 | import { useRouter } from 'vue-router' 4 | 5 | /** 6 | * Resolve a route with redirection 7 | */ 8 | export const useResolveRouteWithRedirect = ( 9 | ...args: Parameters 10 | ): ReturnType => { 11 | const router = useRouter() 12 | const route = router.resolve(...args) 13 | const lastMatched = route.matched[route.matched.length - 1] 14 | if (!lastMatched?.redirect) { 15 | return route 16 | } 17 | const { redirect } = lastMatched 18 | const resolvedRedirect = isFunction(redirect) ? redirect(route) : redirect 19 | const resolvedRedirectObj = isString(resolvedRedirect) 20 | ? { path: resolvedRedirect } 21 | : resolvedRedirect 22 | return useResolveRouteWithRedirect({ 23 | hash: route.hash, 24 | query: route.query, 25 | params: route.params, 26 | ...resolvedRedirectObj, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/NavbarFeatures.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/utils/isActiveSidebarItem.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalizedLoaded } from 'vue-router' 2 | 3 | import type { ResolvedSidebarItem } from '../../shared/index.js' 4 | 5 | const normalizePath = (path: string): string => 6 | decodeURI(path) 7 | .replace(/#.*$/, '') 8 | .replace(/(index)?\.(md|html)$/, '') 9 | 10 | const isActiveLink = ( 11 | link: string, 12 | route: RouteLocationNormalizedLoaded 13 | ): boolean => { 14 | if (route.hash === link) { 15 | return true 16 | } 17 | const currentPath = normalizePath(route.path) 18 | const targetPath = normalizePath(link) 19 | return currentPath === targetPath 20 | } 21 | 22 | export const isActiveSidebarItem = ( 23 | item: ResolvedSidebarItem, 24 | route: RouteLocationNormalizedLoaded 25 | ): boolean => { 26 | if (item.link && isActiveLink(item.link, route)) { 27 | return true 28 | } 29 | 30 | if (item.children) { 31 | return item.children.some(child => isActiveSidebarItem(child, route)) 32 | } 33 | 34 | return false 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present Gavin Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | > The following is translated from Chinese by ChatGPT. 4 | 5 | ## Installation 6 | 7 | First, execute the following command in your VuePress 2 project to install the theme: 8 | 9 | :::: code-group 10 | ::: code-group-item npm 11 | 12 | ```bash 13 | npm install -D vuepress-theme-mix@next 14 | ``` 15 | 16 | ::: 17 | 18 | ::: code-group-item yarn 19 | 20 | ```bash 21 | yarn add -D vuepress-theme-mix@next 22 | ``` 23 | 24 | ::: 25 | 26 | ::: code-group-item pnpm 27 | 28 | ```bash 29 | pnpm add -D vuepress-theme-mix@next 30 | ``` 31 | 32 | ::: 33 | :::: 34 | 35 | ## Usage and Configuration 36 | 37 | After installation, you can use the `theme` configuration option in your configuration file, just like using other VuePress themes, to set the theme to use: 38 | 39 | ```ts 40 | import { defineUserConfig } from 'vuepress' 41 | import mixTheme from 'vuepress-theme-mix' 42 | 43 | export default defineUserConfig({ 44 | // ... 45 | 46 | theme: mixTheme({ 47 | // configure the theme here 48 | }), 49 | 50 | // ... 51 | }) 52 | ``` 53 | 54 | Next, you can configure the theme according to the documentation and start writing your content. 55 | 56 | 🎉 57 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/shared/page.ts: -------------------------------------------------------------------------------- 1 | import type { GitPluginPageData } from '@vuepress/plugin-git' 2 | 3 | import type { NavLink, SidebarConfig } from './nav.js' 4 | 5 | export interface MixThemePageData extends GitPluginPageData { 6 | filePathRelative: string | null 7 | } 8 | 9 | export interface MixThemePageFrontmatter { 10 | home?: boolean 11 | navbar?: boolean 12 | pageClass?: string 13 | toc?: boolean 14 | tocDepth?: number 15 | } 16 | 17 | export interface MixThemeHomePageFrontmatter extends MixThemePageFrontmatter { 18 | home: true 19 | heroText?: string | null 20 | tagline?: string | null 21 | actions?: { 22 | text: string 23 | link: string 24 | type?: 'primary' | 'secondary' 25 | }[] 26 | features?: { 27 | title: string 28 | details: string 29 | }[] 30 | footer?: string 31 | toc?: true 32 | } 33 | 34 | export interface MixThemeNormalPageFrontmatter extends MixThemePageFrontmatter { 35 | home?: false 36 | editLink?: false | string | ((relativePath: string) => string) 37 | lastUpdated?: boolean 38 | contributors?: boolean 39 | sidebar?: 'auto' | false | SidebarConfig 40 | sidebarDepth?: number 41 | prev?: string | NavLink 42 | next?: string | NavLink 43 | toc?: false 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuepress-theme-mix 2 | 3 | [![npm](https://badgen.net/npm/v/vuepress-theme-mix/next)](https://www.npmjs.com/package/vuepress-theme-mix) 4 | [![](https://img.shields.io/badge/vuepress-next-blue)](https://v2.vuepress.vuejs.org) 5 | [![GitHub license](https://img.shields.io/github/license/gavinliu6/vuepress-theme-mix)](https://github.com/gavinliu6/vuepress-theme-mix/blob/main/LICENSE) 6 | [![Netlify Status](https://api.netlify.com/api/v1/badges/1c7f6ca5-685b-463c-ab12-66b4d89c2eb7/deploy-status)](https://app.netlify.com/sites/vuepress-theme-mix/deploys) 7 | [![npm](https://img.shields.io/npm/dt/vuepress-theme-mix)](https://www.npmjs.com/package/vuepress-theme-mix) 8 | 9 | ## About the Project 10 | 11 | VuePress Theme Mix is a third-party theme developed for [VuePress 2](https://v2.vuepress.vuejs.org/). Based on the [default theme](https://v2.vuepress.vuejs.org/reference/default-theme/config.html) of VuePress 2, it is a hybrid theme deeply customized in terms of layout and color matching, hence the name _Mix(ture)_. 12 | 13 | ## Document 14 | 15 | Multiple Deployments: 16 | 17 | - https://vuepress-theme-mix.netlify.app 18 | - https://vuepress-theme-mix.vercel.app 19 | 20 | ## License 21 | 22 | The project is distributed under the MIT License. Check out [LICENSE](https://github.com/vuepress/vuepress-theme-mix/blob/main/LICENSE) for more information. 23 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/config.ts: -------------------------------------------------------------------------------- 1 | import './styles/index.css' 2 | 3 | import { defineClientConfig } from '@vuepress/client' 4 | import { h } from 'vue' 5 | 6 | import { CodeGroup, CodeGroupItem } from './components/global/index.js' 7 | import { setupSidebarItems, useScrollPromise } from './composables/index.js' 8 | import Layout from './layouts/Layout.vue' 9 | import NotFound from './layouts/NotFound.vue' 10 | 11 | export default defineClientConfig({ 12 | layouts: { 13 | Layout, 14 | NotFound, 15 | }, 16 | 17 | enhance({ app, router }) { 18 | app.component('CodeGroup', CodeGroup) 19 | app.component('CodeGroupItem', CodeGroupItem) 20 | 21 | // compat with @vuepress/plugin-docsearch and @vuepress/plugin-search 22 | app.component('NavbarSearch', () => { 23 | const SearchComponent = 24 | app.component('Docsearch') || app.component('SearchBox') 25 | if (SearchComponent) { 26 | return h(SearchComponent) 27 | } 28 | return null 29 | }) 30 | 31 | // handle scrollBehavior with transition 32 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 33 | const scrollBehavior = router.options.scrollBehavior! 34 | router.options.scrollBehavior = async (...args) => { 35 | await useScrollPromise().wait() 36 | return scrollBehavior(...args) 37 | } 38 | }, 39 | 40 | setup() { 41 | setupSidebarItems() 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/shared/nav.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base nav item, displayed as text 3 | */ 4 | export interface NavItem { 5 | text: string 6 | ariaLabel?: string 7 | } 8 | 9 | /** 10 | * Base nav group, has nav items children 11 | */ 12 | export interface NavGroup extends NavItem { 13 | children: T[] 14 | } 15 | 16 | /** 17 | * Props for `` 18 | */ 19 | export interface NavLink extends NavItem { 20 | link: string 21 | rel?: string 22 | target?: string 23 | activeMatch?: string 24 | } 25 | 26 | /** 27 | * Navbar types 28 | */ 29 | // user config 30 | export type NavbarItem = NavLink 31 | export type NavbarGroup = NavGroup 32 | export type NavbarConfig = (NavbarItem | NavbarGroup | string)[] 33 | // resolved 34 | export type ResolvedNavbarItem = NavbarItem | NavGroup 35 | 36 | /** 37 | * Sidebar types 38 | */ 39 | // user config 40 | export type SidebarItem = NavItem & Partial 41 | export type SidebarGroup = SidebarItem & 42 | NavGroup & { 43 | collapsed?: boolean 44 | } 45 | export type SidebarConfigArray = (SidebarItem | SidebarGroup | string)[] 46 | export type SidebarConfigObject = Record 47 | export type SidebarConfig = SidebarConfigArray | SidebarConfigObject 48 | // resolved 49 | export type ResolvedSidebarItem = SidebarItem & 50 | Partial> & { 51 | collapsed?: boolean 52 | } 53 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/SocialLinks.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/layouts/NotFound.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 43 | -------------------------------------------------------------------------------- /docs/zh/guide/markdown.md: -------------------------------------------------------------------------------- 1 | # Markdown 2 | 3 | ## markdown-it 简介 4 | 5 | VuePress 2 使用 [markdown-it](https://github.com/markdown-it/markdown-it) 作为 Markdown 格式内容的解析器,该解析器遵循 [CommonMark Spec](https://spec.commonmark.org/) 规范,并提供了许多扩展特性。 6 | 7 | 除了内置的 [Tables](https://help.github.com/articles/organizing-information-with-tables/) (GFM) 和 [Strikethrough](https://help.github.com/articles/basic-writing-and-formatting-syntax/#styling-text) (GFM) 功能,markdown-it 的插件式设计为其他扩展语法或特性提供了丰富的选择,[这里](https://www.npmjs.com/search?q=keywords:markdown-it-plugin) 列出了 npm 上可用的相关插件。 8 | 9 | ## VuePress 2 的扩展 10 | 11 | 得益于 VuePress 良好的基础设施,我们可以使用配置项 [markdown.*](https://v2.vuepress.vuejs.org/zh/reference/config.html#markdown) 对 markdown-it 的 [所有配置项](https://github.com/markdown-it/markdown-it#init-with-presets-and-options) 以及 VuePress 2 的扩展配置项进行配置。 12 | 13 | 这些配置项独立于任何主题,且本主题为它们提供了风格一致的样式,因此,你可以放心地使用它们。 14 | 15 | ## 自定义容器 16 | 17 | VuePress 2 通过插件 [@vuepress/container](https://v2.vuepress.vuejs.org/zh/reference/plugin/container.html) 封装了 markdown-it 的 [markdown-it-container](https://github.com/markdown-it/markdown-it-container) 插件,以使其使用更加方便。本主题也使用了这个插件,支持以下 4 种自定义容器。 18 | 19 | ```md 20 | ::: tip 21 | 这是一条提示信息。 22 | ::: 23 | 24 | ::: warning 25 | 这是一条警告信息。 26 | ::: 27 | 28 | ::: danger 29 | 这是一条危险信息。 30 | ::: 31 | 32 | ::: details 33 | 这是一个 details 标签,在 IE / Edge 中不生效。 34 | ::: 35 | ``` 36 | 37 | 渲染结果如下: 38 | 39 | ::: tip 40 | 这是一条提示信息。 41 | ::: 42 | 43 | ::: warning 44 | 这是一条警告信息。 45 | ::: 46 | 47 | ::: danger 48 | 这是一条危险信息。 49 | ::: 50 | 51 | ::: details 52 | 这是一个 details 标签,在 IE / Edge 中不生效。 53 | ::: 54 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/node/utils/assignDefaultLocaleOptions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MixThemeLocaleData, 3 | MixThemeLocaleOptions, 4 | } from '../../shared/index.js' 5 | 6 | export const DEFAULT_LOCALE_OPTIONS: MixThemeLocaleOptions = { 7 | // color mode 8 | colorMode: 'auto', 9 | colorModeSwitch: true, 10 | toggleColorModeAriaLabel: 'Toggle color mode', 11 | 12 | // navbar 13 | navbar: [], 14 | logo: null, 15 | selectLanguageAriaLabel: 'Select language', 16 | socialLinks: [], 17 | 18 | // page meta 19 | editLinkText: 'Edit this page', 20 | lastUpdated: true, 21 | lastUpdatedText: 'Last Updated', 22 | contributors: true, 23 | contributorsText: 'Contributors', 24 | tocTitle: 'On this page', 25 | 26 | // 404 page messags 27 | notFound: 'This page could not be found.', 28 | backToHome: 'Take me home', 29 | 30 | // a11y 31 | toggleSidebarTitle: 'toggle sidebar', 32 | } 33 | 34 | export const DEFAULT_LOCALE_DATA: MixThemeLocaleData = { 35 | // navbar 36 | selectLanguageName: 'English', 37 | } 38 | 39 | /** 40 | * Assign default options 41 | */ 42 | export const assignDefaultLocaleOptions = ( 43 | localeOptions: MixThemeLocaleOptions 44 | ): void => { 45 | if (!localeOptions.locales) { 46 | localeOptions.locales = {} 47 | } 48 | 49 | if (!localeOptions.locales['/']) { 50 | localeOptions.locales['/'] = {} 51 | } 52 | 53 | Object.assign(localeOptions, { 54 | ...DEFAULT_LOCALE_OPTIONS, 55 | ...localeOptions, 56 | }) 57 | 58 | Object.assign(localeOptions.locales['/'], { 59 | ...DEFAULT_LOCALE_DATA, 60 | ...localeOptions.locales['/'], 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.0.0-alpha.6](https://github.com/gavinliu6/vuepress-theme-mix/compare/v2.0.0-alpha.5...v2.0.0-alpha.6) (2023-07-08) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * toc curtain ([cad6222](https://github.com/gavinliu6/vuepress-theme-mix/commit/cad6222291002648e2a366b83c20a3ca362bb5b9)) 7 | 8 | 9 | 10 | # [2.0.0-alpha.5](https://github.com/gavinliu6/vuepress-theme-mix/compare/v2.0.0-alpha.4...v2.0.0-alpha.5) (2023-04-20) 11 | 12 | 13 | 14 | # [2.0.0-alpha.4](https://github.com/gavinliu6/vuepress-theme-mix/compare/v2.0.0-alpha.3...v2.0.0-alpha.4) (2023-04-19) 15 | 16 | 17 | ### Features 18 | 19 | * add 404 page ([986b9db](https://github.com/gavinliu6/vuepress-theme-mix/commit/986b9dbe37d64882685d99257960f2d8dd4aa125)) 20 | 21 | 22 | 23 | # [2.0.0-alpha.3](https://github.com/gavinliu6/vuepress-theme-mix/compare/v2.0.0-alpha.2...v2.0.0-alpha.3) (2023-04-17) 24 | 25 | 26 | ### Features 27 | 28 | * the open/collapsed state of SidebarGroup by default is controlled ([fca93cf](https://github.com/gavinliu6/vuepress-theme-mix/commit/fca93cfafaa985da3f12325328116ac6e63b6e2e)) 29 | 30 | 31 | ### BREAKING CHANGES 32 | 33 | * remove configuration item `collapsible` in SidebarGroup 34 | 35 | 36 | 37 | # [2.0.0-alpha.2](https://github.com/gavinliu6/vuepress-theme-mix/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2023-04-16) 38 | 39 | 40 | ### Performance Improvements 41 | 42 | * just use simple SVG from https://tabler-icons.io ([2837677](https://github.com/gavinliu6/vuepress-theme-mix/commit/283767707b8b4626a31edbf3c8fb333a98433b61)) 43 | 44 | 45 | 46 | # [2.0.0-alpha.1](https://github.com/gavinliu6/vuepress-theme-mix/compare/v1.4.1...v2.0.0-alpha.1) (2023-04-16) 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/NavbarBrand.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 56 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/NavbarItems.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 58 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuepress-theme-mix", 3 | "version": "2.0.0-alpha.6", 4 | "description": "A VuePress theme with a mix of features", 5 | "keywords": [ 6 | "vuepress-theme", 7 | "vuepress", 8 | "theme", 9 | "mix" 10 | ], 11 | "homepage": "https://github.com/gavinliu6/vuepress-theme-mix", 12 | "bugs": { 13 | "url": "https://github.com/gavinliu6/vuepress-theme-mix/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/gavinliu6/vuepress-theme-mix.git" 18 | }, 19 | "license": "MIT", 20 | "author": "Gavin Liu (https://gavinliu.me)", 21 | "type": "module", 22 | "main": "lib/node/index.js", 23 | "types": "lib/node/index.d.ts", 24 | "files": [ 25 | "lib", 26 | "templates" 27 | ], 28 | "scripts": { 29 | "build": "tsc --build tsconfig.build.json", 30 | "clean": "rimraf lib *.tsbuildinfo", 31 | "copy": "cpx \"src/**/*.{d.ts,vue}\" lib", 32 | "style": "tailwindcss -i src/client/styles/index.css -o lib/client/styles/index.css" 33 | }, 34 | "dependencies": { 35 | "@vuepress/client": "2.0.0-beta.64", 36 | "@vuepress/core": "2.0.0-beta.64", 37 | "@vuepress/plugin-container": "2.0.0-beta.64", 38 | "@vuepress/plugin-git": "2.0.0-beta.64", 39 | "@vuepress/plugin-medium-zoom": "2.0.0-beta.64", 40 | "@vuepress/plugin-nprogress": "2.0.0-beta.64", 41 | "@vuepress/plugin-shiki": "2.0.0-beta.64", 42 | "@vuepress/plugin-theme-data": "2.0.0-beta.64", 43 | "@vuepress/shared": "2.0.0-beta.64", 44 | "@vuepress/utils": "2.0.0-beta.64", 45 | "@vueuse/core": "^10.2.1", 46 | "clsx": "^1.2.1", 47 | "vue": "^3.3.4", 48 | "vue-router": "^4.2.4" 49 | }, 50 | "publishConfig": { 51 | "access": "public", 52 | "registry": "https://registry.npmjs.org/" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: 'VuePress Theme Mix' 4 | tagline: 一款用于制作精美文档或笔记的 VuePress 2 主题 5 | actions: 6 | - text: 快速上手 7 | link: /zh/guide/getting-started.html 8 | type: primary 9 | - text: 在 GitHub 中打开 10 | link: https://github.com/gavinliu6/vuepress-theme-mix 11 | type: secondary 12 | footer: MIT Licensed | Copyright © 2021-present Gavin Liu 13 | --- 14 | 15 |

16 | npm{{ ' ' }} 17 | vuepress@next{{ ' ' }} 18 | GitHub license{{ ' ' }} 19 | Netlify Status{{ ' ' }} 20 | npm 21 |

22 | 23 | ### 使用案例 24 | 25 | > 如果你使用了本主题,并且已经部署在了公网中,那么你可以通过在 GitHub 上 [编辑本页](https://github.com/gavinliu6/vuepress-theme-mix/edit/main/docs/zh/README.md) 来在这里展示你的站点,也可以直接 联系作者,由作者代为添加。 26 | 27 | 39 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | > The following is translated from Chinese by ChatGPT. 4 | 5 | VuePress Theme Mix is a third-party theme developed for [VuePress 2](https://v2.vuepress.vuejs.org/). Based on the [default theme](https://v2.vuepress.vuejs.org/reference/default-theme/config.html) of VuePress 2, it is a hybrid theme deeply customized in terms of layout and color matching, hence the name _Mix(ture)_. 6 | 7 | Before using this theme, please make sure to read the following instructions carefully before making a decision. 8 | 9 | ::: warning 10 | First of all, it should be noted that as the author of this theme, I am not a professional front-end developer. When I started this project, I had limited knowledge of many front-end concepts, most of which were learned on-the-fly during development. In addition, due to my limited skills, you may encounter some issues when using this theme. 11 | 12 | Furthermore, VuePress Theme Mix is only a side project of mine that I work on in my spare time. I may not be able to fix or address issues in a timely manner, whether they are reported on [GitHub](https://github.com/gavinliu6/vuepress-theme-mix/issues) issues or sent to me via email. If you have front-end development experience and time allows, I would greatly appreciate it if you could submit a PR to improve the project. 13 | 14 | In summary, **the author does not guarantee the stability of this theme in a production environment**, and users need to weigh the risks themselves. 15 | ::: 16 | 17 | As mentioned earlier, Mix is based on the default theme of VuePress 2, and although it has added some new features and functionality, it still maintains a high degree of consistency in terms of usage and configuration. However, using this theme does not require prior knowledge of the default theme of VuePress 2. The documentation will cover all configuration options and usage instructions for Mix, even if a significant portion of the configuration options are the same. If you have already decided to use Mix, then please continue reading. 18 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/SidebarItems.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 59 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: 'VuePress Theme Mix' 4 | tagline: A VuePress 2 Theme for Crafting beautiful Documents or Notes 5 | actions: 6 | - text: Getting Started 7 | link: /guide/getting-started.html 8 | type: primary 9 | - text: Open on GitHub 10 | link: https://github.com/gavinliu6/vuepress-theme-mix 11 | type: secondary 12 | footer: MIT Licensed | Copyright © 2021-present Gavin Liu 13 | --- 14 | 15 |

16 | npm{{ ' ' }} 17 | vuepress@next{{ ' ' }} 18 | GitHub license{{ ' ' }} 19 | Netlify Status{{ ' ' }} 20 | npm 21 |

22 | 23 | ### Showcases 24 | 25 | > If you use this theme and your site has been deployed on the public network, you can display your site here by [editing this page](https://github.com/gavinliu6/vuepress-theme-mix/edit/main/docs/README.md) on GitHub, or you can contact the author directly, and the author will add it on his behalf. 26 | 27 | 39 | -------------------------------------------------------------------------------- /docs/guide/markdown.md: -------------------------------------------------------------------------------- 1 | # Markdown 2 | 3 | > The following is translated from Chinese by ChatGPT. 4 | 5 | ## About markdown-it 6 | 7 | VuePress 2 uses [markdown-it](https://github.com/markdown-it/markdown-it) as the parser for Markdown-formatted content. This parser follows the [CommonMark Spec](https://spec.commonmark.org/) specification and provides many extension features. 8 | 9 | In addition to the built-in [Tables](https://help.github.com/articles/organizing-information-with-tables/) (GFM) and [Strikethrough](https://help.github.com/articles/basic-writing-and-formatting-syntax/#styling-text) (GFM) features, markdown-it's plugin-based design provides rich choices for other extended syntax or features. [Here](https://www.npmjs.com/search?q=keywords:markdown-it-plugin) is a list of related plugins available on npm. 10 | 11 | ## VuePress 2 Extensions 12 | 13 | Thanks to VuePress's good infrastructure, we can use the [markdown.\*](https://v2.vuepress.vuejs.org/zh/reference/config.html#markdown) configuration option to configure all markdown-it configuration options and VuePress 2's extension configuration options. 14 | 15 | These configuration options are independent of any theme and this theme provides them with a consistent style, so you can use them with confidence. 16 | 17 | ## Custom Containers 18 | 19 | VuePress 2 encapsulates the [markdown-it-container](https://github.com/markdown-it/markdown-it-container) plugin of markdown-it through the [@vuepress/container](https://v2.vuepress.vuejs.org/zh/reference/plugin/container.html) plugin to make it more convenient to use. This theme also uses this plugin to support the following 4 custom containers. 20 | 21 | ```md 22 | ::: tip 23 | This is a tip. 24 | ::: 25 | 26 | ::: warning 27 | This is a warning. 28 | ::: 29 | 30 | ::: danger 31 | This is a danger. 32 | ::: 33 | 34 | ::: details 35 | This is a details tag, which doesn't work in IE/Edge. 36 | ::: 37 | ``` 38 | 39 | The rendered result is as follows: 40 | 41 | ::: tip 42 | This is a tip. 43 | ::: 44 | 45 | ::: warning 46 | This is a warning. 47 | ::: 48 | 49 | ::: danger 50 | This is a danger. 51 | ::: 52 | 53 | ::: details 54 | This is a details tag, which doesn't work in IE/Edge. 55 | ::: 56 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/PageToc.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 75 | -------------------------------------------------------------------------------- /docs/zh/guide/frontmatter.md: -------------------------------------------------------------------------------- 1 | # Frontmatter 2 | 3 | 除了 VuePress 2 内置的 [Frontmatter](https://v2.vuepress.vuejs.org/zh/reference/frontmatter.html) 外,本主题还提供了下列这些。 4 | 5 | ## 首页 6 | 7 | ### home 8 | 9 | - 类型:`boolean` 10 | - 默认值:`false` 11 | 12 | 指示该页面是否是首页。 13 | 14 | ### heroText 15 | 16 | - 类型:`string | null` 17 | - 默认值:站点的 [title](https://v2.vuepress.vuejs.org/zh/reference/config.html#title) 18 | 19 | 首页的大标题,设置为 `null` 可以禁用它。 20 | 21 | ### tagline 22 | 23 | - 类型:`string | null` 24 | - 默认值:站点的 [title](https://v2.vuepress.vuejs.org/zh/reference/config.html#description) 25 | 26 | 首页大标题下的描述性标语,设置为 `null` 可以禁用它。 27 | 28 | ### actions 29 | 30 | - 类型: 31 | ```ts 32 | Array<{ 33 | text: string 34 | link: string 35 | type?: 'primary' | 'secondary' 36 | }> 37 | ``` 38 | 39 | 首页按钮。 40 | 41 | 示例: 42 | 43 | ```md 44 | --- 45 | actions: 46 | - text: 快速上手 47 | link: /zh/guide/getting-started.html 48 | type: primary 49 | - text: 在 GitHub 中打开 50 | link: https://github.com/gavinliu6/vuepress-theme-mix 51 | type: secondary 52 | --- 53 | ``` 54 | 55 | ### features 56 | 57 | - 类型: 58 | ```ts 59 | Array<{ 60 | title: string 61 | details: string 62 | }> 63 | ``` 64 | 65 | 描述你的站点特性。 66 | 67 | ### footer 68 | 69 | - 类型:`string` 70 | - 默认值:无 71 | 72 | 首页页脚。 73 | 74 | ## 其他页面 75 | 76 | ### toc 77 | 78 | 同主题配置项 [toc](/zh/guide/config.html#toc) ,其优先级更高。 79 | 80 | ### editLink 81 | 82 | 同主题配置项 [editLink](/zh/guide/config.html#editlink) ,其优先级更高。 83 | 84 | ### lastUpdated 85 | 86 | 同主题配置项 [lastUpdated](/zh/guide/config.html#lastupdated) ,其优先级更高。 87 | 88 | ### contributors 89 | 90 | 同主题配置项 [contributors](/zh/guide/config.html#contributors) ,其优先级更高。 91 | 92 | ### sidebar 93 | 94 | 同主题配置项 [sidebar](/zh/guide/config.html#sidebar) ,其优先级更高。 95 | 96 | ### sidebarDepth 97 | 98 | 同主题配置项 [sidebarDepth](/zh/guide/config.html#sidebardepth) ,其优先级更高。 99 | 100 | ### prev & next 101 | 102 | - 类型:`string | NavLink` 103 | 104 | ```ts 105 | // 类型定义 106 | 107 | interface NavLink { 108 | text: string 109 | ariaLabel?: string 110 | link: string 111 | rel?: string 112 | target?: string 113 | activeMatch?: string 114 | } 115 | ``` 116 | 117 | 页面底部上/下一个页面的链接,如果不设置这两个选项,该链接会自动根据侧边栏配置进行推断。 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0-alpha.6", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "pnpm --filter=\"./packages/**\" -r --workspace-concurrency=1 --stream build", 7 | "clean": "pnpm --parallel --stream clean", 8 | "copy": "pnpm --parallel --stream copy", 9 | "docs:build": "pnpm --filter=docs docs:build", 10 | "docs:dev": "pnpm --filter=docs docs:dev", 11 | "docs:release": "pnpm clean && pnpm build && pnpm style && pnpm copy && pnpm docs:build", 12 | "docs:serve": "pnpm --filter=docs docs:serve", 13 | "lint": "eslint --ext .cjs,.js,.ts,.vue .", 14 | "prepare": "husky install", 15 | "release": "pnpm release:check && pnpm release:version && pnpm release:publish", 16 | "release:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 17 | "release:check": "pnpm lint && pnpm clean && pnpm build && pnpm style && pnpm copy", 18 | "release:publish": "pnpm -r publish --tag next", 19 | "release:version": "bumpp package.json packages/*/package.json --execute=\"pnpm release:changelog\" --commit \"build: publish v%s\" --all", 20 | "style": "pnpm --parallel --stream style" 21 | }, 22 | "lint-staged": { 23 | "*.{cjs,js,ts,vue}": "eslint --fix", 24 | "*.{json,yml,css}": "prettier --write", 25 | "package.json": "sort-package-json" 26 | }, 27 | "devDependencies": { 28 | "@commitlint/cli": "^17.6.6", 29 | "@commitlint/config-conventional": "^17.6.6", 30 | "@tailwindcss/typography": "^0.5.9", 31 | "@typescript-eslint/eslint-plugin": "^5.61.0", 32 | "@typescript-eslint/parser": "^5.61.0", 33 | "bumpp": "^9.1.1", 34 | "conventional-changelog-cli": "^3.0.0", 35 | "cpx2": "^5.0.0", 36 | "eslint": "^8.44.0", 37 | "eslint-config-prettier": "^8.8.0", 38 | "eslint-config-standard": "^17.1.0", 39 | "eslint-plugin-import": "^2.27.5", 40 | "eslint-plugin-n": "^16.0.1", 41 | "eslint-plugin-prettier": "^4.2.1", 42 | "eslint-plugin-promise": "^6.1.1", 43 | "eslint-plugin-simple-import-sort": "^10.0.0", 44 | "eslint-plugin-vue": "^9.15.1", 45 | "husky": "^8.0.3", 46 | "lint-staged": "^13.2.3", 47 | "prettier": "^2.8.8", 48 | "prettier-plugin-tailwindcss": "^0.3.0", 49 | "rimraf": "^5.0.1", 50 | "sort-package-json": "^2.5.0", 51 | "tailwindcss": "^3.3.2", 52 | "tsconfig-vuepress": "^4.2.0", 53 | "typescript": "^5.1.6" 54 | }, 55 | "packageManager": "pnpm@8.6.6" 56 | } 57 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/NavbarScreen.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 80 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./templates/build.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | darkMode: 'class', 5 | theme: { 6 | screens: { 7 | // >= 640px 8 | mobile: '640px', 9 | 10 | // <= 639px 11 | 'mobile-reverse': { max: '639px' }, 12 | 13 | // >= 768px 14 | tablet: '768px', 15 | 16 | // <= 767px 17 | 'tablet-reverse': { max: '767px' }, 18 | 19 | // >= 1024px 20 | laptop: '1024px', 21 | 22 | // <= 1023px 23 | 'laptop-reverse': { max: '1023px' }, 24 | 25 | // >= 1280px 26 | desktop: '1280px', 27 | 28 | // <= 1279px 29 | 'desktop-reverse': { max: '1279px' }, 30 | }, 31 | extend: { 32 | colors: { 33 | theme: 'rgb(var(--color-theme) / )', 34 | }, 35 | textColor: { 36 | default: 'var(--color-text-default)', 37 | muted: 'var(--color-text-muted)', 38 | subtle: 'var(--color-text-subtle)', 39 | tip: 'var(--color-text-tip)', 40 | warning: 'var(--color-text-warning)', 41 | danger: 'var(--color-text-danger)', 42 | 'muted-in-code-block': 'var(--color-text-muted-in-code-block)', 43 | }, 44 | backgroundColor: { 45 | default: 'rgb(var(--color-bg-default) / )', 46 | contrast: 'var(--color-bg-contrast)', 47 | divider: 'var(--color-bg-divider)', 48 | overlay: 'var(--color-bg-overlay)', 49 | tip: 'var(--color-bg-tip)', 50 | warning: 'var(--color-bg-warning)', 51 | danger: 'var(--color-bg-danger)', 52 | 'code-block': 'var(--color-bg-code-block)', 53 | highlight: 'var(--color-bg-highlight)', 54 | 'code-group-bar': 'var(--color-bg-code-group-bar)', 55 | }, 56 | borderColor: { 57 | default: 'var(--color-border-default)', 58 | 'line-numbers': 'var(--color-border-line-numbers)', 59 | }, 60 | maxWidth: { 61 | '8xl': '90rem', 62 | }, 63 | typography: { 64 | DEFAULT: { 65 | css: { 66 | 'h2, h3, h4, h5, h6': { 67 | 'scroll-margin-top': 'var(--scroll-mt)', 68 | }, 69 | a: { 70 | color: 'var(--text-default)', 71 | }, 72 | pre: { 73 | 'font-size': '85%', 74 | 'line-height': '1.45', 75 | padding: '16px', 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | plugins: [require('@tailwindcss/typography')], 83 | } 84 | -------------------------------------------------------------------------------- /docs/guide/frontmatter.md: -------------------------------------------------------------------------------- 1 | # Frontmatter 2 | 3 | > The following is translated from Chinese by ChatGPT. 4 | 5 | In addition to the [Frontmatter](https://v2.vuepress.vuejs.org/en/reference/frontmatter.html) built into VuePress 2, this theme also provides the following options. 6 | 7 | ## Homepage 8 | 9 | ### home 10 | 11 | - Type: `boolean` 12 | - Default: `false` 13 | 14 | Indicates whether the page is the homepage. 15 | 16 | ### heroText 17 | 18 | - Type: `string | null` 19 | - Default: The site's [title](https://v2.vuepress.vuejs.org/en/reference/config.html#title) 20 | 21 | The main title of the homepage, set to `null` to disable it. 22 | 23 | ### tagline 24 | 25 | - Type: `string | null` 26 | - Default: The site's [description](https://v2.vuepress.vuejs.org/en/reference/config.html#description) 27 | 28 | The descriptive slogan below the main title of the homepage, set to `null` to disable it. 29 | 30 | ### actions 31 | 32 | - Type: 33 | 34 | ```ts 35 | Array<{ 36 | text: string 37 | link: string 38 | type?: 'primary' | 'secondary' 39 | }> 40 | ``` 41 | 42 | Buttons on the homepage. 43 | 44 | Example: 45 | 46 | ```md 47 | --- 48 | actions: 49 | - text: Get Started 50 | link: /guide/getting-started.html 51 | type: primary 52 | - text: View on GitHub 53 | link: https://github.com/gavinliu6/vuepress-theme-mix 54 | type: secondary 55 | --- 56 | ``` 57 | 58 | ### features 59 | 60 | - Type: 61 | 62 | ```ts 63 | Array<{ 64 | title: string 65 | details: string 66 | }> 67 | ``` 68 | 69 | Describes the features of your site. 70 | 71 | ### footer 72 | 73 | - Type: `string` 74 | - Default: None 75 | 76 | Footer of the homepage. 77 | 78 | ## General pages 79 | 80 | ### toc 81 | 82 | Same as the theme configuration option [toc](/guide/config.html#toc), with higher priority. 83 | 84 | ### editLink 85 | 86 | Same as the theme configuration option [editLink](/guide/config.html#editlink), with higher priority. 87 | 88 | ### lastUpdated 89 | 90 | Same as the theme configuration option [lastUpdated](/guide/config.html#lastupdated), with higher priority. 91 | 92 | ### contributors 93 | 94 | Same as the theme configuration option [contributors](/guide/config.html#contributors), with higher priority. 95 | 96 | ### sidebar 97 | 98 | Same as the theme configuration option [sidebar](/guide/config.html#sidebar), with higher priority. 99 | 100 | ### sidebarDepth 101 | 102 | Same as the theme configuration option [sidebarDepth](/guide/config.html#sidebardepth), with higher priority. 103 | 104 | ### prev & next 105 | 106 | - Type: `string | NavLink` 107 | 108 | ```ts 109 | // Type definitions 110 | 111 | interface NavLink { 112 | text: string 113 | ariaLabel?: string 114 | link: string 115 | rel?: string 116 | target?: string 117 | activeMatch?: string 118 | } 119 | ``` 120 | 121 | Links to the previous and next pages at the bottom of the page. If these two options are not set, the link will be automatically inferred based on the sidebar configuration. 122 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/LocalNav.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 105 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/layouts/Layout.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 111 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/Page.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 105 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/composables/useActiveAnchor.ts: -------------------------------------------------------------------------------- 1 | import { useElementBounding } from '@vueuse/core' 2 | import type { Ref } from 'vue' 3 | import { onMounted, onUnmounted, onUpdated } from 'vue' 4 | 5 | import { shouldShowPageToc, throttleAndDebounce } from '../utils/index.js' 6 | 7 | export function useActiveAnchor( 8 | container: Ref, 9 | marker: Ref 10 | ) { 11 | const onScroll = throttleAndDebounce(setActiveLink, 100) 12 | let prevActiveLink: HTMLAnchorElement | null = null 13 | onMounted(() => { 14 | requestAnimationFrame(setActiveLink) 15 | window.addEventListener('scroll', onScroll) 16 | onScroll() 17 | }) 18 | onUpdated(() => { 19 | // sidebar update means a route change 20 | activateLink(location.hash) 21 | }) 22 | onUnmounted(() => { 23 | window.removeEventListener('scroll', onScroll) 24 | }) 25 | 26 | function setActiveLink() { 27 | if (!shouldShowPageToc.value) { 28 | return 29 | } 30 | 31 | const links = [].slice.call( 32 | container.value.querySelectorAll('.aside .toc-item') 33 | ) as HTMLAnchorElement[] 34 | const headings = [].slice 35 | .call( 36 | document.querySelectorAll( 37 | '.md-container h2, .md-container h3, .md-container h4, .md-container h5' 38 | ) 39 | ) 40 | .filter((head: HTMLHeadingElement) => { 41 | return links.some(link => { 42 | return ( 43 | decodeURIComponent(link.hash) === `#${head.id}` && 44 | head.offsetParent !== null 45 | ) 46 | }) 47 | }) as HTMLHeadingElement[] 48 | 49 | // const scrollY = window.scrollY 50 | // const innerHeight = window.innerHeight 51 | // const offsetHeight = document.body.offsetHeight 52 | // const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1 53 | // // page bottom - highlight last one 54 | // if (headings.length && isBottom) { 55 | // activateLink(headings[headings.length - 1].id) 56 | // return 57 | // } 58 | 59 | const sortedHeadings = headings.concat([]).sort((a, b) => { 60 | const rectA = useElementBounding(a) 61 | const rectB = useElementBounding(b) 62 | 63 | return rectA.top.value - rectB.top.value 64 | }) 65 | 66 | const style = window.getComputedStyle(document.documentElement) 67 | let scrollMt = parseFloat( 68 | style.getPropertyValue('--scroll-mt').match(/[\d.]+/)?.[0] ?? '0' 69 | ) 70 | const fontSize = parseFloat(style.fontSize.match(/[\d.]+/)?.[0] ?? '16') 71 | scrollMt = scrollMt * fontSize 72 | const top = scrollMt + 1 73 | 74 | let current = sortedHeadings[0] 75 | for (let i = 0; i < sortedHeadings.length; i++) { 76 | const rect = useElementBounding(sortedHeadings[i]) 77 | if (top >= rect.top.value) { 78 | current = sortedHeadings[i] 79 | } 80 | } 81 | 82 | current && activateLink(current.id) 83 | } 84 | 85 | function activateLink(hash: string | null) { 86 | if (prevActiveLink) { 87 | prevActiveLink.classList.remove('text-default') 88 | } 89 | if (hash !== null) { 90 | prevActiveLink = container.value.querySelector( 91 | `a[href="#${decodeURIComponent(hash)}"]` 92 | ) 93 | } 94 | const activeLink = prevActiveLink 95 | if (activeLink) { 96 | activeLink.classList.add('text-default') 97 | marker.value.style.top = activeLink.offsetTop + 40 + 'px' 98 | marker.value.style.opacity = '1' 99 | } else { 100 | marker.value.style.top = '40px' 101 | marker.value.style.opacity = '0' 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/AutoLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 93 | 94 | 117 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/node/utils/resolveContainerPluginOptions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ContainerPluginOptions, 3 | MarkdownItContainerRenderFunction, 4 | RenderPlaceFunction, 5 | } from '@vuepress/plugin-container' 6 | import { ensureLeadingSlash, resolveLocalePath } from '@vuepress/shared' 7 | 8 | import type { MixThemeData } from '../../shared/index.js' 9 | 10 | /** 11 | * Resolve options for @vuepress/plugin-container 12 | * 13 | * For custom containers default title 14 | */ 15 | export const resolveContainerPluginOptions = ( 16 | localeOptions: MixThemeData, 17 | type: 'tip' | 'warning' | 'danger' 18 | ): ContainerPluginOptions => { 19 | const locales = Object.entries(localeOptions.locales || {}).reduce( 20 | (result, [key, value]) => { 21 | result[key] = { 22 | defaultInfo: value?.[type] ?? localeOptions[type], 23 | } 24 | return result 25 | }, 26 | {} 27 | ) 28 | 29 | const typeIcons = { 30 | tip: '', 31 | warning: 32 | '', 33 | danger: 34 | '', 35 | } 36 | 37 | // custom render 38 | const renderBefore: RenderPlaceFunction = (info: string): string => 39 | `
${ 40 | info 41 | ? `

${typeIcons[type]} ${info}

` 42 | : '' 43 | }\n` 44 | const renderAfter: RenderPlaceFunction = (): string => '
\n' 45 | 46 | // token info stack 47 | const infoStack: string[] = [] 48 | 49 | const render: MarkdownItContainerRenderFunction = ( 50 | tokens, 51 | index, 52 | opts, 53 | env 54 | ): string => { 55 | const token = tokens[index] 56 | 57 | if (token.nesting === 1) { 58 | // `before` tag 59 | 60 | // resolve info (title) 61 | let info = token.info.trim().slice(type.length).trim() 62 | 63 | if (!info && locales) { 64 | // locale 65 | const { filePathRelative } = env 66 | const relativePath = ensureLeadingSlash(filePathRelative ?? '') 67 | 68 | const localePath = resolveLocalePath(locales, relativePath) 69 | const localeData = locales[localePath] ?? {} 70 | 71 | if (localeData.defaultInfo) { 72 | info = localeData.defaultInfo 73 | } else { 74 | info = type.replace(/^\S/, s => s.toUpperCase()) 75 | } 76 | } 77 | 78 | // push the info to stack 79 | infoStack.push(info) 80 | 81 | // render 82 | return renderBefore(info) 83 | } else { 84 | // `after` tag 85 | 86 | // pop the info from stack 87 | const info = infoStack.pop() || '' 88 | 89 | // render 90 | return renderAfter(info) 91 | } 92 | } 93 | 94 | return { 95 | type, 96 | locales, 97 | render, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 137 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/PageNav.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 143 | -------------------------------------------------------------------------------- /docs/.vuepress/config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { viteBundler } from '@vuepress/bundler-vite' 4 | import { webpackBundler } from '@vuepress/bundler-webpack' 5 | import { defineUserConfig } from '@vuepress/cli' 6 | import { docsearchPlugin } from '@vuepress/plugin-docsearch' 7 | import { googleAnalyticsPlugin } from '@vuepress/plugin-google-analytics' 8 | import mixTheme from 'vuepress-theme-mix' 9 | 10 | import { 11 | head, 12 | navbarEn, 13 | navbarZh, 14 | sidebarEn, 15 | sidebarZh, 16 | } from './configs/index.js' 17 | 18 | export default defineUserConfig({ 19 | // set site base to default value 20 | base: '/', 21 | 22 | // site-level locales config 23 | locales: { 24 | '/': { 25 | lang: 'en-US', 26 | title: 'VuePress Theme Mix', 27 | description: 'A mash-up theme for VuePress 2', 28 | }, 29 | '/zh/': { 30 | lang: 'zh-CN', 31 | title: 'VuePress Theme Mix', 32 | description: '一款适用于 VuePress 2 的混搭主题', 33 | }, 34 | }, 35 | 36 | // extra tags in `` 37 | head, 38 | 39 | // specify bundler via environment variable 40 | bundler: 41 | process.env.DOCS_BUNDLER === 'webpack' ? webpackBundler() : viteBundler(), 42 | 43 | // markdown config 44 | markdown: { 45 | code: { 46 | lineNumbers: false, 47 | }, 48 | }, 49 | 50 | theme: mixTheme({ 51 | editLink: 52 | 'https://github.com/gavinliu6/vuepress-theme-mix/edit/main/docs/:path', 53 | 54 | socialLinks: [ 55 | { 56 | icon: 'github', 57 | link: 'https://github.com/gavinliu6/vuepress-theme-mix', 58 | ariaLabel: 'github', 59 | }, 60 | ], 61 | 62 | sidebarDepth: 0, 63 | 64 | // theme-level locales config 65 | locales: { 66 | /** 67 | * English locale config 68 | * 69 | * As the default locale of @vuepress/theme-default is English, 70 | * we don't need to set all of the locale fields 71 | */ 72 | '/': { 73 | navbar: navbarEn, 74 | sidebar: sidebarEn, 75 | }, 76 | 77 | /** 78 | * Chinese locale config 79 | */ 80 | '/zh/': { 81 | navbar: navbarZh, 82 | sidebar: sidebarZh, 83 | selectLanguageAriaLabel: '选择语言', 84 | selectLanguageName: '简体中文', 85 | tocTitle: '本页目录', 86 | editLinkText: '在 GitHub 上编辑本页', 87 | lastUpdatedText: '最近更新时间', 88 | contributorsText: '贡献者', 89 | }, 90 | }, 91 | }), 92 | 93 | // use plugins 94 | plugins: [ 95 | docsearchPlugin({ 96 | appId: 'YEDIHRHK0I', 97 | apiKey: '193117ed50436656af493cd03005595e', 98 | indexName: 'vuepress-theme-mix', 99 | locales: { 100 | '/zh/': { 101 | placeholder: '搜索文档', 102 | translations: { 103 | button: { 104 | buttonText: '搜索文档', 105 | buttonAriaLabel: '搜索文档', 106 | }, 107 | modal: { 108 | searchBox: { 109 | resetButtonTitle: '清除查询条件', 110 | resetButtonAriaLabel: '清除查询条件', 111 | cancelButtonText: '取消', 112 | cancelButtonAriaLabel: '取消', 113 | }, 114 | startScreen: { 115 | recentSearchesTitle: '搜索历史', 116 | noRecentSearchesText: '没有搜索历史', 117 | saveRecentSearchButtonTitle: '保存至搜索历史', 118 | removeRecentSearchButtonTitle: '从搜索历史中移除', 119 | favoriteSearchesTitle: '收藏', 120 | removeFavoriteSearchButtonTitle: '从收藏中移除', 121 | }, 122 | errorScreen: { 123 | titleText: '无法获取结果', 124 | helpText: '你可能需要检查你的网络连接', 125 | }, 126 | footer: { 127 | selectText: '选择', 128 | navigateText: '切换', 129 | closeText: '关闭', 130 | searchByText: '搜索提供者', 131 | }, 132 | noResultsScreen: { 133 | noResultsText: '无法找到相关结果', 134 | suggestedQueryText: '你可以尝试查询', 135 | reportMissingResultsText: '你认为该查询应该有结果?', 136 | reportMissingResultsLinkText: '点击反馈', 137 | }, 138 | }, 139 | }, 140 | }, 141 | }, 142 | }), 143 | 144 | googleAnalyticsPlugin({ 145 | // we have multiple deployments, which would use different id 146 | id: process.env.GA_ID ?? '', 147 | }), 148 | ], 149 | }) 150 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/LanguageDropdown.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 126 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/global/CodeGroup.ts: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx' 2 | import type { Component, VNode } from 'vue' 3 | import { defineComponent, h, onBeforeUpdate, ref } from 'vue' 4 | 5 | export const CodeGroup = defineComponent({ 6 | name: 'CodeGroup', 7 | 8 | setup(_, { slots }) { 9 | // index of current active item 10 | const activeIndex = ref(-1) 11 | 12 | // refs of the tab buttons 13 | const tabRefs = ref([]) 14 | 15 | if (__VUEPRESS_DEV__) { 16 | // after removing a code-group-item, we need to clear the ref 17 | // of the removed item to avoid issues caused by HMR 18 | onBeforeUpdate(() => { 19 | tabRefs.value = [] 20 | }) 21 | } 22 | 23 | // activate next tab 24 | const activateNext = (i = activeIndex.value): void => { 25 | if (i < tabRefs.value.length - 1) { 26 | activeIndex.value = i + 1 27 | } else { 28 | activeIndex.value = 0 29 | } 30 | tabRefs.value[activeIndex.value].focus() 31 | } 32 | 33 | // activate previous tab 34 | const activatePrev = (i = activeIndex.value): void => { 35 | if (i > 0) { 36 | activeIndex.value = i - 1 37 | } else { 38 | activeIndex.value = tabRefs.value.length - 1 39 | } 40 | tabRefs.value[activeIndex.value].focus() 41 | } 42 | 43 | // handle keyboard event 44 | const keyboardHandler = (event: KeyboardEvent, i: number): void => { 45 | if (event.key === ' ' || event.key === 'Enter') { 46 | event.preventDefault() 47 | activeIndex.value = i 48 | } else if (event.key === 'ArrowRight') { 49 | event.preventDefault() 50 | activateNext(i) 51 | } else if (event.key === 'ArrowLeft') { 52 | event.preventDefault() 53 | activatePrev(i) 54 | } 55 | } 56 | 57 | return () => { 58 | // NOTICE: here we put the `slots.default()` inside the render function to make 59 | // the slots reactive, otherwise the slot content won't be changed once the 60 | // `setup()` function of current component is called 61 | 62 | // get children code-group-item 63 | const items = (slots.default?.() || []) 64 | .filter(vnode => (vnode.type as Component).name === 'CodeGroupItem') 65 | .map(vnode => { 66 | if (vnode.props === null) { 67 | vnode.props = {} 68 | } 69 | return vnode as VNode & { props: Exclude } 70 | }) 71 | 72 | // do not render anything if there is no code-group-item 73 | if (items.length === 0) { 74 | return null 75 | } 76 | 77 | if (activeIndex.value < 0 || activeIndex.value > items.length - 1) { 78 | // if `activeIndex` is invalid 79 | 80 | // find the index of the code-group-item with `active` props 81 | activeIndex.value = items.findIndex( 82 | vnode => vnode.props.active === '' || vnode.props.active === true 83 | ) 84 | 85 | // if there is no `active` props on code-group-item, set the first item active 86 | if (activeIndex.value === -1) { 87 | activeIndex.value = 0 88 | } 89 | } else { 90 | // set the active item 91 | items.forEach((vnode, i) => { 92 | vnode.props.active = i === activeIndex.value 93 | }) 94 | } 95 | 96 | return h('div', { class: 'code-group' }, [ 97 | h( 98 | 'div', 99 | { class: 'bg-code-group-bar rounded-t-lg -mb-1.5 pb-1.5' }, 100 | h( 101 | 'div', 102 | { class: 'flex' }, 103 | items.map((vnode, i) => { 104 | const isActive = i === activeIndex.value 105 | 106 | return h( 107 | 'button', 108 | { 109 | ref: element => { 110 | if (element) { 111 | tabRefs.value[i] = element as HTMLButtonElement 112 | } 113 | }, 114 | class: clsx( 115 | 'mx-2 py-2.5 first:ml-4', 116 | isActive ? 'text-white' : 'text-white/70 hover:text-white' 117 | ), 118 | ariaPressed: isActive, 119 | ariaExpanded: isActive, 120 | onClick: () => (activeIndex.value = i), 121 | onKeydown: e => keyboardHandler(e, i), 122 | }, 123 | vnode.props.title 124 | ) 125 | }) 126 | ) 127 | ), 128 | items, 129 | ]) 130 | } 131 | }, 132 | }) 133 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/NavbarScreenDropdown.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 145 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/node/mixTheme.ts: -------------------------------------------------------------------------------- 1 | import type { Page, Theme } from '@vuepress/core' 2 | import { containerPlugin } from '@vuepress/plugin-container' 3 | import { gitPlugin } from '@vuepress/plugin-git' 4 | import { mediumZoomPlugin } from '@vuepress/plugin-medium-zoom' 5 | import { nprogressPlugin } from '@vuepress/plugin-nprogress' 6 | import { shikiPlugin } from '@vuepress/plugin-shiki' 7 | import { themeDataPlugin } from '@vuepress/plugin-theme-data' 8 | import { fs, getDirname, path } from '@vuepress/utils' 9 | 10 | import type { 11 | MixThemeLocaleOptions, 12 | MixThemePageData, 13 | MixThemePluginsOptions, 14 | } from '../shared/index.js' 15 | import { 16 | assignDefaultLocaleOptions, 17 | resolveContainerPluginOptions, 18 | } from './utils/index.js' 19 | 20 | const __dirname = getDirname(import.meta.url) 21 | 22 | export interface MixThemeOptions extends MixThemeLocaleOptions { 23 | /** 24 | * To avoid confusion with the root `plugins` option, 25 | * we use `themePlugins` 26 | */ 27 | themePlugins?: MixThemePluginsOptions 28 | } 29 | 30 | export const mixTheme = ({ 31 | themePlugins = {}, 32 | ...localeOptions 33 | }: MixThemeOptions): Theme => { 34 | assignDefaultLocaleOptions(localeOptions) 35 | 36 | return { 37 | name: 'vuepress-theme-mix', 38 | 39 | templateBuild: path.resolve(__dirname, '../../templates/build.html'), 40 | 41 | alias: { 42 | // use alias to make all components replaceable 43 | ...Object.fromEntries( 44 | fs 45 | .readdirSync(path.resolve(__dirname, '../client/components')) 46 | .filter(file => file.endsWith('.vue')) 47 | .map(file => [ 48 | `@theme/${file}`, 49 | path.resolve(__dirname, '../client/components', file), 50 | ]) 51 | ), 52 | }, 53 | 54 | clientConfigFile: path.resolve(__dirname, '../client/config.js'), 55 | 56 | extendsPage: (page: Page>) => { 57 | // save relative file path into page data to generate edit link 58 | page.data.filePathRelative = page.filePathRelative 59 | // save title into route meta to generate navbar and sidebar 60 | page.routeMeta.title = page.title 61 | }, 62 | 63 | plugins: [ 64 | // @vuepress/plugin-container 65 | themePlugins.container?.tip !== false 66 | ? containerPlugin(resolveContainerPluginOptions(localeOptions, 'tip')) 67 | : [], 68 | themePlugins.container?.warning !== false 69 | ? containerPlugin( 70 | resolveContainerPluginOptions(localeOptions, 'warning') 71 | ) 72 | : [], 73 | themePlugins.container?.danger !== false 74 | ? containerPlugin( 75 | resolveContainerPluginOptions(localeOptions, 'danger') 76 | ) 77 | : [], 78 | themePlugins.container?.details !== false 79 | ? containerPlugin({ 80 | type: 'details', 81 | before: info => 82 | `
${ 83 | info ? `${info}` : '' 84 | }\n`, 85 | after: () => '
\n', 86 | }) 87 | : [], 88 | themePlugins.container?.codeGroup !== false 89 | ? containerPlugin({ 90 | type: 'code-group', 91 | before: () => `\n`, 92 | after: () => '\n', 93 | }) 94 | : [], 95 | themePlugins.container?.codeGroupItem !== false 96 | ? containerPlugin({ 97 | type: 'code-group-item', 98 | before: info => `\n`, 99 | after: () => '\n', 100 | }) 101 | : [], 102 | 103 | // @vuepress/plugin-git 104 | themePlugins.git !== false 105 | ? gitPlugin({ 106 | createdTime: false, 107 | updatedTime: localeOptions.lastUpdated !== false, 108 | contributors: localeOptions.contributors !== false, 109 | }) 110 | : [], 111 | 112 | // @vuepress/plugin-medium-zoom 113 | themePlugins.mediumZoom !== false 114 | ? mediumZoomPlugin({ 115 | selector: '.md-container > img, .md-container :not(a) > img', 116 | zoomOptions: {}, 117 | // should greater than page transition duration 118 | delay: 300, 119 | }) 120 | : [], 121 | 122 | // @vuepress/plugin-nprogress 123 | themePlugins.nprogress !== false ? nprogressPlugin() : [], 124 | 125 | // @vuepress/plugin-theme-data 126 | themeDataPlugin({ themeData: localeOptions }), 127 | 128 | // @vuepress/plugin-shiki 129 | shikiPlugin({ 130 | theme: themePlugins.shikiTheme ?? 'one-dark-pro', 131 | }), 132 | ], 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/Home.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 158 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/NavbarDropdown.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 172 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/composables/useSidebarItems.ts: -------------------------------------------------------------------------------- 1 | import type { PageHeader } from '@vuepress/client' 2 | import { usePageData, usePageFrontmatter } from '@vuepress/client' 3 | import { 4 | isArray, 5 | isPlainObject, 6 | isString, 7 | resolveLocalePath, 8 | } from '@vuepress/shared' 9 | import type { ComputedRef, InjectionKey } from 'vue' 10 | import { computed, inject, provide } from 'vue' 11 | import { useRoute } from 'vue-router' 12 | 13 | import type { 14 | MixThemeData, 15 | MixThemeNormalPageFrontmatter, 16 | ResolvedSidebarItem, 17 | SidebarConfigArray, 18 | SidebarConfigObject, 19 | SidebarItem, 20 | } from '../../shared/index.js' 21 | import { useNavLink } from './useNavLink.js' 22 | import { useThemeLocaleData } from './useThemeData.js' 23 | 24 | export type SidebarItemsRef = ComputedRef 25 | 26 | export const sidebarItemsSymbol: InjectionKey = 27 | Symbol('sidebarItems') 28 | 29 | /** 30 | * Inject sidebar items global computed 31 | */ 32 | export const useSidebarItems = (): SidebarItemsRef => { 33 | const sidebarItems = inject(sidebarItemsSymbol) 34 | if (!sidebarItems) { 35 | throw new Error('useSidebarItems() is called without provider.') 36 | } 37 | return sidebarItems 38 | } 39 | 40 | /** 41 | * Create sidebar items ref and provide as global computed in setup 42 | */ 43 | export const setupSidebarItems = (): void => { 44 | const themeLocale = useThemeLocaleData() 45 | const frontmatter = usePageFrontmatter() 46 | const sidebarItems = computed(() => 47 | resolveSidebarItems(frontmatter.value, themeLocale.value) 48 | ) 49 | 50 | provide(sidebarItemsSymbol, sidebarItems) 51 | } 52 | 53 | /** 54 | * Resolve sidebar items global computed 55 | * 56 | * It should only be resolved and provided once 57 | */ 58 | export const resolveSidebarItems = ( 59 | frontmatter: MixThemeNormalPageFrontmatter, 60 | themeLocale: MixThemeData 61 | ): ResolvedSidebarItem[] => { 62 | // get sidebar config from frontmatter > theme data 63 | const sidebarConfig = frontmatter.sidebar ?? themeLocale.sidebar ?? 'auto' 64 | const sidebarDepth = frontmatter.sidebarDepth ?? themeLocale.sidebarDepth ?? 2 65 | 66 | // resolve sidebar items according to the config 67 | if (frontmatter.home || sidebarConfig === false) { 68 | return [] 69 | } 70 | 71 | if (sidebarConfig === 'auto') { 72 | return resolveAutoSidebarItems(sidebarDepth) 73 | } 74 | 75 | if (isArray(sidebarConfig)) { 76 | return resolveArraySidebarItems(sidebarConfig, sidebarDepth) 77 | } 78 | 79 | if (isPlainObject(sidebarConfig)) { 80 | return resolveMultiSidebarItems(sidebarConfig, sidebarDepth) 81 | } 82 | 83 | return [] 84 | } 85 | 86 | /** 87 | * Util to transform page header to sidebar item 88 | */ 89 | export const headerToSidebarItem = ( 90 | header: PageHeader, 91 | sidebarDepth: number 92 | ): ResolvedSidebarItem => ({ 93 | text: header.title, 94 | link: header.link, 95 | children: headersToSidebarItemChildren(header.children, sidebarDepth), 96 | }) 97 | 98 | export const headersToSidebarItemChildren = ( 99 | headers: PageHeader[], 100 | sidebarDepth: number 101 | ): ResolvedSidebarItem[] => 102 | sidebarDepth > 0 103 | ? headers.map(header => headerToSidebarItem(header, sidebarDepth - 1)) 104 | : [] 105 | 106 | /** 107 | * Resolve sidebar items if the config is `auto` 108 | */ 109 | export const resolveAutoSidebarItems = ( 110 | sidebarDepth: number 111 | ): ResolvedSidebarItem[] => { 112 | const page = usePageData() 113 | 114 | return [ 115 | { 116 | text: page.value.title, 117 | children: headersToSidebarItemChildren(page.value.headers, sidebarDepth), 118 | }, 119 | ] 120 | } 121 | 122 | /** 123 | * Resolve sidebar items if the config is an array 124 | */ 125 | export const resolveArraySidebarItems = ( 126 | sidebarConfig: SidebarConfigArray, 127 | sidebarDepth: number 128 | ): ResolvedSidebarItem[] => { 129 | const route = useRoute() 130 | const page = usePageData() 131 | 132 | const handleChildItem = ( 133 | item: ResolvedSidebarItem | SidebarItem | string 134 | ): ResolvedSidebarItem => { 135 | let childItem: ResolvedSidebarItem 136 | if (isString(item)) { 137 | childItem = useNavLink(item) 138 | } else { 139 | childItem = item as ResolvedSidebarItem 140 | } 141 | 142 | if (childItem.children) { 143 | return { 144 | ...childItem, 145 | children: childItem.children.map(item => handleChildItem(item)), 146 | } 147 | } 148 | 149 | // if the sidebar item is current page and children is not set 150 | // use headers of current page as children 151 | if (childItem.link === route.path) { 152 | // skip h1 header 153 | const headers = 154 | page.value.headers[0]?.level === 1 155 | ? page.value.headers[0].children 156 | : page.value.headers 157 | return { 158 | ...childItem, 159 | children: headersToSidebarItemChildren(headers, sidebarDepth), 160 | } 161 | } 162 | 163 | return childItem 164 | } 165 | 166 | return sidebarConfig.map(item => handleChildItem(item)) 167 | } 168 | 169 | /** 170 | * Resolve sidebar items if the config is a key -> value (path-prefix -> array) object 171 | */ 172 | export const resolveMultiSidebarItems = ( 173 | sidebarConfig: SidebarConfigObject, 174 | sidebarDepth: number 175 | ): ResolvedSidebarItem[] => { 176 | const route = useRoute() 177 | const sidebarPath = resolveLocalePath(sidebarConfig, route.path) 178 | const matchedSidebarConfig = sidebarConfig[sidebarPath] ?? [] 179 | 180 | return resolveArraySidebarItems(matchedSidebarConfig, sidebarDepth) 181 | } 182 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/PageMeta.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 180 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/shared/options.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeData } from '@vuepress/plugin-theme-data' 2 | import type { LocaleData } from '@vuepress/shared' 3 | 4 | import type { NavbarConfig, SidebarConfig } from './nav.js' 5 | 6 | export type ColorMode = 'auto' | 'light' | 'dark' 7 | 8 | export type SocialLinkIcon = 'github' | 'twitter' | { svg: string } 9 | 10 | export type SocialLink = { 11 | icon: SocialLinkIcon 12 | link: string 13 | ariaLabel?: string 14 | } 15 | 16 | export interface MixThemePluginsOptions { 17 | /** 18 | * Enable @vuepress/plugin-container or not 19 | */ 20 | container?: { 21 | tip?: boolean 22 | warning?: boolean 23 | danger?: boolean 24 | details?: boolean 25 | codeGroup?: boolean 26 | codeGroupItem?: boolean 27 | } 28 | 29 | /** 30 | * Enable @vuepress/plugin-git or not 31 | */ 32 | git?: boolean 33 | 34 | /** 35 | * Enable @vuepress/plugin-medium-zoom or not 36 | */ 37 | mediumZoom?: boolean 38 | 39 | /** 40 | * Enable @vuepress/plugin-nprogress or not 41 | */ 42 | nprogress?: boolean 43 | 44 | /** 45 | * Custom shiki theme 46 | */ 47 | shikiTheme?: string 48 | } 49 | 50 | export interface MixThemeLocaleData extends LocaleData { 51 | /** 52 | * Default theme mode 53 | * 54 | * @default 'auto' 55 | */ 56 | colorMode?: ColorMode 57 | 58 | /** 59 | * Enable color mode switching and display a button in navbar or not 60 | * 61 | * @default true 62 | */ 63 | colorModeSwitch?: boolean 64 | 65 | /** 66 | * Navbar color mode config 67 | * 68 | * Aria label of of the language selection dropdown 69 | */ 70 | toggleColorModeAriaLabel?: string 71 | 72 | /** 73 | * Home path of current locale 74 | * 75 | * Used as the link of back-to-home and navbar logo 76 | */ 77 | home?: string 78 | 79 | /** 80 | * Navbar config 81 | * 82 | * Set to `false` to disable navbar in current locale 83 | */ 84 | navbar?: false | NavbarConfig 85 | 86 | /** 87 | * Navbar logo config 88 | * 89 | * Logo to display in navbar 90 | */ 91 | logo?: null | string 92 | 93 | /** 94 | * Navbar logo config for dark mode 95 | * 96 | * Logo to display in navbar in dark mode 97 | */ 98 | logoDark?: null | string 99 | 100 | /** 101 | * Navbar language selection config 102 | * 103 | * Aria label of of the language selection dropdown 104 | */ 105 | selectLanguageAriaLabel?: string 106 | 107 | /** 108 | * Navbar language selection config 109 | * 110 | * Language name of current locale 111 | * 112 | * Displayed inside the language selection dropdown 113 | */ 114 | selectLanguageName?: string 115 | 116 | /** 117 | * Navbar social links config 118 | * 119 | * Show your social account links with icons in nav 120 | */ 121 | socialLinks?: SocialLink[] 122 | 123 | /** 124 | * Sidebar config 125 | * 126 | * Set to `false` to disable sidebar in current locale 127 | */ 128 | sidebar?: 'auto' | false | SidebarConfig 129 | 130 | /** 131 | * Sidebar depth 132 | * 133 | * - Set to `0` to disable all levels 134 | * - Set to `1` to include `

` 135 | * - Set to `2` to include `

` and `

` 136 | * - ... 137 | * 138 | * The max value depends on which headers you have extracted 139 | * via `markdown.headers.level`. 140 | * 141 | * The default value of `markdown.headers.level` is `[2, 3]`, 142 | * so the default max value of `sidebarDepth` is `2` 143 | */ 144 | sidebarDepth?: number 145 | 146 | /** 147 | * Page meta - edit link config 148 | * 149 | * Pattern of edit link 150 | * 151 | * @example 'https://github.com/gavinliu6/vuepress-theme-mix/edit/main/docs/:path' 152 | */ 153 | editLink?: string | ((relativePath: string) => string) 154 | 155 | /** 156 | * Page meta - edit link config 157 | * 158 | * The text to replace the default "Edit this page" 159 | */ 160 | editLinkText?: string 161 | 162 | /** 163 | * A11y text for sidebar toggle button 164 | */ 165 | toggleSidebarTitle?: string 166 | 167 | /** 168 | * Page toc config 169 | * 170 | * Set to `false` to disable toc in page aside 171 | */ 172 | toc?: false 173 | 174 | /** 175 | * Toc depth 176 | * 177 | * - Set to `1` to include `

` 178 | * - Set to `2` to include `

` and `

` 179 | * - ... 180 | * 181 | * The max value depends on which headers you have extracted 182 | * via `markdown.headers.level`. 183 | * 184 | * The default value of `markdown.headers.level` is `[2, 3]`, 185 | * so the default max value of `tocDepth` is `2` 186 | */ 187 | tocDepth?: number 188 | 189 | /** 190 | * A11y text for toc title 191 | */ 192 | tocTitle?: string 193 | 194 | /** 195 | * Page meta - last updated config 196 | * 197 | * Whether to show "Last Updated" or not 198 | */ 199 | lastUpdated?: boolean 200 | 201 | /** 202 | * Page meta - last updated config 203 | * 204 | * The text to replace the default "Last Updated" 205 | */ 206 | lastUpdatedText?: string 207 | 208 | /** 209 | * Page meta - contributors config 210 | * 211 | * Whether to show "Contributors" or not 212 | */ 213 | contributors?: boolean 214 | 215 | /** 216 | * Page meta - contributors config 217 | * 218 | * The text to replace the default "Contributors" 219 | */ 220 | contributorsText?: string 221 | 222 | /** 223 | * Custom block config 224 | * 225 | * Default title of TIP custom block 226 | */ 227 | tip?: string 228 | 229 | /** 230 | * Custom block config 231 | * 232 | * Default title of WARNING custom block 233 | */ 234 | warning?: string 235 | 236 | /** 237 | * Custom block config 238 | * 239 | * Default title of DANGER custom block 240 | */ 241 | danger?: string 242 | 243 | /** 244 | * 404 page config 245 | * 246 | * Not Found messages for 404 page 247 | */ 248 | notFound?: string 249 | 250 | /** 251 | * 404 page config 252 | * 253 | * Text of back-to-home link in 404 page 254 | */ 255 | backToHome?: string 256 | } 257 | 258 | export type MixThemeData = ThemeData 259 | 260 | export type MixThemeLocaleOptions = MixThemeData 261 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/components/ToggleColorModeButton.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 178 | -------------------------------------------------------------------------------- /packages/vuepress-theme-mix/src/client/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html { 7 | @apply tablet-reverse:[--scroll-mt:4.5rem] scroll-smooth [--scroll-mt:5.5rem]; 8 | } 9 | 10 | html, 11 | body { 12 | @apply bg-default relative min-h-screen transition-colors; 13 | } 14 | 15 | body { 16 | @apply text-default min-h-screen text-sm; 17 | } 18 | 19 | :root { 20 | --color-theme: 255 87 88; 21 | 22 | --color-text-default: rgb(17, 24, 39); 23 | --color-text-muted: rgba(17, 24, 39, 0.75); 24 | --color-text-subtle: rgba(17, 24, 39, 0.33); 25 | --color-text-tip: #2ce23f; 26 | --color-text-warning: #cba408; 27 | --color-text-danger: #ff3b10; 28 | --color-text-muted-in-code-block: rgba(235, 235, 245, 0.38); 29 | 30 | --color-bg-default: 255 255 255; 31 | --color-bg-contrast: #f6f6f7; 32 | --color-bg-divider: rgba(15, 23, 42, 0.1); 33 | --color-bg-overlay: rgba(0, 0, 0, 0.6); 34 | --color-bg-tip: #faf9f9; 35 | --color-bg-warning: #fffeeb; 36 | --color-bg-danger: #fff9f8; 37 | --color-bg-code-block: #292b30; 38 | --color-bg-highlight: rgba(0, 0, 0, 0.5); 39 | --color-bg-code-group-bar: #1b1e22; 40 | 41 | --color-border-default: rgba(15, 23, 42, 0.1); 42 | --color-border-line-numbers: rgba(202, 212, 234, 0.1); 43 | } 44 | 45 | .dark { 46 | --color-theme: 255 87 88; 47 | 48 | --color-text-default: rgb(209, 213, 219); 49 | --color-text-muted: rgba(209, 213, 219, 0.6); 50 | --color-text-subtle: rgba(209, 213, 219, 0.38); 51 | --color-text-tip: #36b844; 52 | --color-text-warning: #ddc45e; 53 | --color-text-danger: #ffa692; 54 | --color-text-muted-in-code-block: rgba(235, 235, 245, 0.38); 55 | 56 | --color-bg-default: 22 22 24; 57 | --color-bg-contrast: #242424; 58 | --color-bg-divider: rgba(82, 82, 89, 0.32); 59 | --color-bg-overlay: rgba(0, 0, 0, 0.6); 60 | --color-bg-tip: #1a1a1a; 61 | --color-bg-warning: #1b1500; 62 | --color-bg-danger: #1c0301; 63 | --color-bg-code-block: #292b30; 64 | --color-bg-highlight: rgba(0, 0, 0, 0.5); 65 | --color-bg-code-group-bar: #30343b; 66 | 67 | --color-border-default: rgba(82, 82, 89, 0.32); 68 | --color-border-line-numbers: rgba(82, 82, 89, 0.32); 69 | } 70 | 71 | .navbar-select-language a.router-link-active { 72 | @apply text-theme bg-contrast; 73 | } 74 | 75 | .md-container .header-anchor { 76 | @apply text-theme -ml-[0.87em] !border-none !no-underline opacity-0; 77 | } 78 | 79 | .md-container h1 .header-anchor { 80 | @apply hidden; 81 | } 82 | 83 | .md-container h2:hover .header-anchor, 84 | .md-container h3:hover .header-anchor, 85 | .md-container h4:hover .header-anchor, 86 | .md-container h5:hover .header-anchor, 87 | .md-container h6:hover .header-anchor { 88 | @apply opacity-100; 89 | } 90 | 91 | /** 92 | * Inline code 93 | */ 94 | :not(pre) > code { 95 | @apply bg-contrast inline-flex rounded px-1 py-0.5 align-middle !font-normal; 96 | } 97 | 98 | :not(pre):not(a) > code { 99 | @apply !text-muted; 100 | } 101 | 102 | a > code { 103 | @apply !text-theme/80 hover:!text-theme; 104 | } 105 | } 106 | 107 | /** 108 | * fade-slide-y 109 | */ 110 | .fade-slide-y-enter-active { 111 | transition: all 0.2s ease; 112 | } 113 | .fade-slide-y-leave-active { 114 | transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); 115 | } 116 | .fade-slide-y-enter-from, 117 | .fade-slide-y-leave-to { 118 | transform: translateY(10px); 119 | opacity: 0; 120 | } 121 | 122 | /** 123 | * plugins 124 | */ 125 | #nprogress { 126 | --nprogress-color: rgb(var(--color-theme)); 127 | } 128 | 129 | .DocSearch { 130 | --docsearch-primary-color: rgb(var(--color-theme)); 131 | --docsearch-highlight-color: rgb(var(--color-theme)); 132 | --docsearch-searchbox-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 133 | --docsearch-searchbox-background: var(--color-bg-contrast); 134 | --docsearch-text-color: var(--color-text-muted); 135 | --docsearch-searchbox-focus-background: var(--color-bg-contrast); 136 | --docsearch-modal-background: rgb(var(--color-bg-default)); 137 | --docsearch-footer-background: rgb(var(--color-bg-default)); 138 | --docsearch-modal-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 139 | --docsearch-footer-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 140 | --docsearch-hit-background: var(--color-bg-contrast); 141 | --docsearch-hit-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 142 | } 143 | 144 | .custom-container { 145 | @apply my-[1.25em] rounded-md p-[1em]; 146 | } 147 | 148 | .custom-container 149 | *:not( 150 | .custom-container-title, 151 | .custom-container-title > svg, 152 | .custom-container-title > svg *, 153 | a, 154 | pre * 155 | ) { 156 | @apply !text-muted; 157 | } 158 | 159 | .custom-container > :last-child { 160 | @apply mb-0 mt-2; 161 | } 162 | 163 | .custom-container .custom-container-title { 164 | @apply my-0 flex items-center; 165 | } 166 | 167 | .custom-container .custom-container-title > svg { 168 | @apply mr-2; 169 | } 170 | 171 | .custom-container.tip { 172 | @apply bg-tip; 173 | } 174 | 175 | .custom-container.tip .custom-container-title { 176 | @apply text-tip; 177 | } 178 | 179 | .custom-container.warning { 180 | @apply bg-warning; 181 | } 182 | 183 | .custom-container.warning .custom-container-title { 184 | @apply text-warning; 185 | } 186 | 187 | .custom-container.danger { 188 | @apply bg-danger; 189 | } 190 | 191 | .custom-container.danger .custom-container-title { 192 | @apply text-danger; 193 | } 194 | 195 | .custom-container.details { 196 | @apply bg-tip; 197 | } 198 | 199 | .code-group pre { 200 | @apply !mt-0 !rounded-b-lg !rounded-t-none; 201 | } 202 | 203 | div[class*='language-'] { 204 | @apply bg-code-block relative rounded-md; 205 | } 206 | 207 | div[class*='language-']:before { 208 | content: attr(data-ext); 209 | position: absolute; 210 | top: 0.25em; 211 | right: 1em; 212 | font-size: 0.75rem; 213 | font-weight: 500; 214 | color: var(--color-text-muted-in-code-block); 215 | z-index: 1; 216 | } 217 | 218 | div[class*='language-'] pre { 219 | @apply relative z-10 !bg-transparent; 220 | } 221 | 222 | div[class*='language-'].line-numbers-mode pre { 223 | @apply pl-11; 224 | } 225 | 226 | div[class*='language-'].line-numbers-mode .line-numbers { 227 | @apply border-r-line-numbers absolute bottom-0 left-0 top-0 w-8 border-r px-2 pt-4 text-center; 228 | counter-reset: line-number; 229 | } 230 | 231 | div[class*='language-'].line-numbers-mode .line-numbers .line-number { 232 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 233 | 'Liberation Mono', 'Courier New', monospace; 234 | font-size: 85%; 235 | line-height: 1.45; 236 | } 237 | 238 | div[class*='language-'].line-numbers-mode .line-numbers .line-number:before { 239 | counter-increment: line-number; 240 | content: counter(line-number); 241 | color: var(--color-text-muted-in-code-block); 242 | } 243 | 244 | div[class*='language-'] .highlight-lines { 245 | @apply absolute left-0 top-0 w-full select-none pt-4; 246 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 247 | 'Liberation Mono', 'Courier New', monospace; 248 | font-size: 85%; 249 | line-height: 1.45; 250 | } 251 | 252 | div[class*='language-'] .highlight-lines .highlight-line { 253 | @apply bg-highlight; 254 | } 255 | -------------------------------------------------------------------------------- /docs/zh/guide/config.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | ::: tip 4 | 本页内容仅描述主题级别配置项,其他独立于主题的站点配置项请参阅官方文档 [参考 > VuePress > 配置](https://v2.vuepress.vuejs.org/zh/reference/config.html) 。 5 | ::: 6 | 7 | ## 基础配置 8 | 9 | ### locales 10 | 11 | 与 VuePress 2 的官方默认主题一样,Mix 也支持多语言配置,你可以使用配置项 `locales` 针对不同语言应用不同的配置值。 12 | 13 | ```ts 14 | export default { 15 | theme: mixTheme({ 16 | locales: { 17 | '/': { 18 | // 默认语言的配置项 19 | }, 20 | 21 | '/klingon/': { 22 | // 克林贡语的配置项 23 | }, 24 | }, 25 | }), 26 | } 27 | ``` 28 | 29 | 除了 [插件配置](#插件配置) 外,本主题的所有其他配置项都支持多语言配置。 30 | 31 | ::: warning 注意 32 | 不要将这里主题级别的 `locales` 配置项与 VuePress 2 站点级别的配置项 [locales](https://v2.vuepress.vuejs.org/zh/reference/config.html#locales) 搞混了。 33 | ::: 34 | 35 | ## 外观 36 | 37 | ### colorMode 38 | 39 | - 类型:`'auto' | 'light' | 'dark'` 40 | - 默认值:`auto` 41 | 42 | 你可以根据个人喜好选择主题的外观颜色,默认的 `auto` 表示主题颜色跟随系统设置动态变化。另外,你也可以将此配置项设置为 `light` 或 `dark` 仅使用单一的颜色模式。 43 | 44 | ### colorModeSwitch 45 | 46 | - 类型:`boolean` 47 | - 默认值:`true` 48 | 49 | 默认情况下,顶部导航栏中会显示颜色模式切换菜单,将 `colorModeSwitch` 设置为 `false` 可隐藏它。 50 | 51 | ### toggleColorModeAriaLabel 52 | 53 | - 类型:`string` 54 | - 默认值:`'Toggle color mode'` 55 | 56 | 指定颜色模式切换菜单的 `aria-label` 属性,这主要是为了站点的可访问性 (a11y)。 57 | 58 | ## 导航栏 59 | 60 | ### logo 61 | 62 | - 类型:`null|string` 63 | - 默认值:`null` 64 | 65 | 指定顶部导航栏左侧的 Logo 图片地址,既可以是 [Public 文件路径](https://v2.vuepress.vuejs.org/zh/guide/assets.html#public-文件),也可以是 HTTP URL 地址。 66 | 67 | 示例: 68 | 69 | ```ts 70 | export default { 71 | theme: mixTheme({ 72 | // Public 文件路径 73 | logo: '/images/logo.png', 74 | // URL 75 | logo: 'https://vuejs.org/images/logo.png', 76 | }), 77 | } 78 | ``` 79 | 80 | ### logoDark 81 | 82 | 同配置项 `logo`,只不过它是用在暗黑模式下的 Logo 图片。 83 | 84 | ### navbar 85 | 86 | - 类型:`false | (NavbarItem | NavbarGroup | string)[]` 87 | - 默认值:`[]` 88 | 89 | ```ts 90 | // 类型定义 91 | 92 | interface NavItem { 93 | text: string 94 | ariaLabel?: string 95 | } 96 | 97 | interface NavGroup extends NavItem { 98 | children: T[] 99 | } 100 | 101 | interface NavbarItem extends NavItem { 102 | link: string 103 | rel?: string 104 | target?: string 105 | activeMatch?: string 106 | } 107 | 108 | type NavbarGroup = NavGroup 109 | ``` 110 | 111 | 示例 1: 112 | 113 | ```ts 114 | export default { 115 | theme: mixTheme({ 116 | navbar: [ 117 | // NavbarItem 118 | { 119 | text: 'Foo', 120 | link: '/foo/', 121 | }, 122 | // NavbarGroup 123 | { 124 | text: 'Group', 125 | children: ['/group/foo.md', '/group/bar.md'], 126 | }, 127 | // 字符串 - 页面文件路径 128 | '/bar/README.md', 129 | ], 130 | }), 131 | } 132 | ``` 133 | 134 | 示例 2: 135 | 136 | ```ts 137 | export default { 138 | theme: mixTheme({ 139 | navbar: [ 140 | // 嵌套 Group - 最大深度为 2 141 | { 142 | text: 'Group', 143 | children: [ 144 | { 145 | text: 'SubGroup', 146 | children: ['/group/sub/foo.md', '/group/sub/bar.md'], 147 | }, 148 | ], 149 | }, 150 | // 控制元素何时被激活 151 | { 152 | text: 'Group 2', 153 | children: [ 154 | { 155 | text: 'Always active', 156 | link: '/', 157 | // 该元素将一直处于激活状态 158 | activeMatch: '/', 159 | }, 160 | { 161 | text: 'Active on /foo/', 162 | link: '/not-foo/', 163 | // 该元素在当前路由路径是 /foo/ 开头时激活 164 | // 支持正则表达式 165 | activeMatch: '^/foo/', 166 | }, 167 | ], 168 | }, 169 | ], 170 | }), 171 | } 172 | ``` 173 | 174 | 另外,如果你将此配置项设置为 `false` ,整个顶部导航栏将会被隐藏。 175 | 176 | ### socialLinks 177 | 178 | - 类型:`SocialLink[]` 179 | - 默认值:`[]` 180 | 181 | ```ts 182 | // 定义 183 | 184 | type SocialLinkIcon = 'github' | 'twitter' | { svg: string } 185 | 186 | type SocialLink = { 187 | icon: SocialLinkIcon 188 | link: string 189 | ariaLabel?: string 190 | } 191 | ``` 192 | 193 | 除了上述的导航菜单外,你还可以在导航栏中添加自己的社交账号,目前内置的菜单 Icon 有 GitHub 和 Twitter,你可以自己提供 SVG 字符串来添加你想要的菜单 Icon。 194 | 195 | 示例: 196 | 197 | ```ts 198 | export default { 199 | theme: mixTheme({ 200 | socialLinks: [ 201 | { 202 | icon: 'github', 203 | link: 'https://github.com/gavinliu6/vuepress-theme-mix', 204 | }, 205 | { icon: 'twitter', link: '...' }, 206 | { 207 | icon: { 208 | svg: '...', 209 | }, 210 | link: '...', 211 | }, 212 | ], 213 | }), 214 | } 215 | ``` 216 | 217 | ## 侧边栏 218 | 219 | ### sidebar 220 | 221 | - 类型:`false | 'auto' | SidebarConfigArray | SidebarConfigObject` 222 | - 默认值:`auto` 223 | 224 | ```ts 225 | // 类型定义 226 | 227 | interface NavItem { 228 | text: string 229 | ariaLabel?: string 230 | } 231 | 232 | interface NavGroup extends NavItem { 233 | children: T[] 234 | } 235 | 236 | interface NavLink extends NavItem { 237 | link: string 238 | rel?: string 239 | target?: string 240 | activeMatch?: string 241 | } 242 | 243 | type SidebarItem = NavItem & Partial 244 | type SidebarGroup = SidebarItem & 245 | NavGroup & { 246 | collapsible?: boolean 247 | } 248 | 249 | type SidebarConfigArray = (SidebarItem | SidebarGroup | string)[] 250 | type SidebarConfigObject = Record 251 | ``` 252 | 253 | 设置为 `false` 时,禁用侧边导航栏;设置为 'auto',侧边栏会根据页面标题自动生成。 254 | 255 | 当某一项的配置类型是 `SidebarGroup` 时,你还可以使用 `collapsed` 来控制它的默认开闭状态,其默认值是 `false`,即默认情况下,所有 SidebarGroup 都是打开的。 256 | 257 | 数组风格的侧边栏配置会使得全站共用一套侧边导航栏,当你的文档结构较为简单,且关联性较强时,你也许更喜欢这种方式。 258 | 259 | 示例 1: 260 | 261 | ```ts 262 | export default { 263 | theme: mixTheme({ 264 | sidebar: [ 265 | // SidebarItem 266 | { 267 | text: 'Foo', 268 | link: '/foo/', 269 | children: [ 270 | // SidebarItem 271 | { 272 | text: 'github', 273 | link: 'https://github.com', 274 | children: [], 275 | }, 276 | // 字符串 - 页面文件路径 277 | '/foo/bar.md', 278 | ], 279 | }, 280 | // 字符串 - 页面文件路径 281 | '/bar/README.md', 282 | ], 283 | }), 284 | } 285 | ``` 286 | 287 | 当你的文档数量众多,并且需要划分不同的结构时,对象风格的侧边栏配置就派上用场了。这样,不同的子路径将会使用各自独立的侧边栏。 288 | 289 | 示例 2: 290 | 291 | ```ts 292 | export default { 293 | theme: mixTheme({ 294 | // 侧边栏对象 295 | // 不同子路径下的页面会使用不同的侧边栏 296 | sidebar: { 297 | '/guide/': [ 298 | { 299 | text: 'Guide', 300 | children: ['/guide/README.md', '/guide/getting-started.md'], 301 | }, 302 | ], 303 | '/reference/': [ 304 | { 305 | text: 'Reference', 306 | children: ['/reference/cli.md', '/reference/config.md'], 307 | }, 308 | ], 309 | }, 310 | }), 311 | } 312 | ``` 313 | 314 | ### sidebarDepth 315 | 316 | - 类型:`number` 317 | - 默认值:`2` 318 | 319 | 设置根据页面标题自动生成的侧边栏的最大深度,设置为 `0` 时关闭此功能,设置为 `1` 时仅显示 `

` 标题,设置为 `2` 时显示 `

`、`

` 标题,以此类推。 320 | 321 | ## 页面配置 322 | 323 | ### home 324 | 325 | - 类型:`string` 326 | - 默认值:`'/'` 327 | 328 | 指定首页的路径,此值会用在导航栏的最左侧链接,以及 404 页面的返回首页链接。 329 | 330 | ### editLink 331 | 332 | - 类型:`string | ((relativePath: string) => string)` 333 | 334 | 页面(不包括首页)底部编辑本页的链接模式。 335 | 336 | 示例 1: 337 | 338 | ```ts 339 | export default { 340 | theme: mixTheme({ 341 | editLink: 342 | 'https://github.com/gavinliu6/vuepress-theme-mix/edit/main/docs/:path', 343 | }), 344 | } 345 | ``` 346 | 347 | 示例 2: 348 | 349 | ```ts 350 | export default { 351 | theme: mixTheme({ 352 | editLink: ({ relativePath }) => { 353 | if (relativePath.startsWith('packages/')) { 354 | return `https://github.com/acme/monorepo/edit/main/${relativePath}` 355 | } else { 356 | return `https://github.com/acme/monorepo/edit/main/docs/${relativePath}` 357 | } 358 | }, 359 | }), 360 | } 361 | ``` 362 | 363 | ### editLinkText 364 | 365 | - 类型:`string` 366 | - 默认值:`Edit this page` 367 | 368 | 页面(不包括首页)底部编辑本页的链接文字。 369 | 370 | ### toc 371 | 372 | - 类型:`false` 373 | 374 | 页面目录默认显示,你可以将此配置项设置为 `false` 来关闭它。 375 | 376 | ### tocDepth 377 | 378 | - 类型:`number` 379 | - 默认值:`2` 380 | 381 | 设置页面目录显示级别,设置为 `1` 时仅显示 `

` 标题,设置为 `2` 时显示 `

`、`

` 标题,以此类推。 382 | 383 | ### tocTitle 384 | 385 | - 类型:`string` 386 | - 默认值:`Table of Contents` 387 | 388 | 页面目录的指示性标题。 389 | 390 | ### lastUpdated 391 | 392 | - 类型:`boolean` 393 | - 默认值:`true` 394 | 395 | 是否在页面底部显示文档最近更新时间。 396 | 397 | ### lastUpdatedText 398 | 399 | - 类型:`string` 400 | - 默认值:`'Last Updated'` 401 | 402 | 最近更新时间的标签文字。 403 | 404 | ### contributors 405 | 406 | - 类型:`boolean` 407 | - 默认值:`true` 408 | 409 | 是否在页面底部显示文档贡献者列表。 410 | 411 | ### contributorsText 412 | 413 | - 类型:`string` 414 | - 默认值:`'Contributors'` 415 | 416 | 文档贡献者的标签文字。 417 | 418 | ## 其他 419 | 420 | ### tip 421 | 422 | - 类型:`string` 423 | - 默认值:`Tip` 424 | 425 | 自定义容器 [tip](/zh/guide/markdown.html#自定义容器) 的标题。 426 | 427 | ### warning 428 | 429 | - 类型:`string` 430 | - 默认值:`Warning` 431 | 432 | 自定义容器 [warning](/zh/guide/markdown.html#自定义容器) 的标题。 433 | 434 | ### danger 435 | 436 | - 类型:`string` 437 | - 默认值:`Danger` 438 | 439 | 自定义容器 [danger](/zh/guide/markdown.html#自定义容器) 的标题。 440 | 441 | ### selectLanguageAriaLabel 442 | 443 | - 类型:`string` 444 | - 默认值:`'Select language'` 445 | 446 | 指定导航栏中选择语言菜单的 `aria-label` 属性,它主要是为了站点的可访问性 (a11y)。 447 | 448 | ### selectLanguageName 449 | 450 | - 类型:`string` 451 | - 默认值:无 452 | 453 | 指定选择语言菜单的下拉列表中显示的语言名称,请注意该配置项仅在主题配置项 [locales](#locales) 内部有效。 454 | 455 | ### notFound 456 | 457 | - 类型:`string` 458 | - 默认值:`'This page could not be found.'` 459 | 460 | 404 页面的提示信息。 461 | 462 | ### backToHome 463 | 464 | - 类型:`string` 465 | - 默认值:`Back to home` 466 | 467 | 404 页面中返回首页链接的文字。 468 | 469 | ## 插件配置 470 | 471 | Mix 主题默认使用了多个插件,通过修改下面的配置你可以有选择地关闭它们,这些配置项都包裹在 `themePlugins` 中。 472 | 473 | ### git 474 | 475 | - 类型:`boolean` 476 | - 默认值:`true` 477 | 478 | 是否启用插件 [@vuepress/plugin-git](https://v2.vuepress.vuejs.org/zh/reference/plugin/git.html) 。 479 | 480 | ### mediumZoom 481 | 482 | - 类型:`boolean` 483 | - 默认值:`true` 484 | 485 | 是否启用插件 [@vuepress/plugin-medium-zoom](https://v2.vuepress.vuejs.org/zh/reference/plugin/medium-zoom.html) 。 486 | 487 | ### nprogress 488 | 489 | - 类型:`boolean` 490 | - 默认值:`true` 491 | 492 | 是否启用插件 [@vuepress/plugin-nprogress](https://v2.vuepress.vuejs.org/zh/reference/plugin/nprogress.html) 。 493 | 494 | ### shikiTheme 495 | 496 | - 类型:`string` 497 | - 默认值:`one-dark-pro` 498 | 499 | Mix 主题使用插件 [@vuepress/plugin-shiki](https://v2.vuepress.vuejs.org/zh/reference/plugin/shiki.html) 高亮代码,且不可关闭,但是你可以指定 Shiki 的主题。[这里](https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes) 列出了所有可用的 Shiki 主题。 500 | -------------------------------------------------------------------------------- /docs/guide/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | > The following is translated from Chinese by ChatGPT. 4 | 5 | ::: tip 6 | This page only describes theme-level configuration options. For other site-level configuration options independent of the theme, please refer to the official documentation [Reference > VuePress > Configuration](https://v2.vuepress.vuejs.org/reference/config.html). 7 | ::: 8 | 9 | ## Basic Configuration 10 | 11 | ### locales 12 | 13 | Like the official default theme for VuePress 2, Mix also supports multi-language configurations. You can use the `locales` option to apply different configuration values for different languages. 14 | 15 | ```ts 16 | export default { 17 | theme: mixTheme({ 18 | locales: { 19 | '/': { 20 | // Default language configuration item 21 | }, 22 | 23 | '/klingon/': { 24 | // Configuration item for the Klingon language 25 | }, 26 | }, 27 | }), 28 | } 29 | ``` 30 | 31 | In addition to [plugin configuration](#plugin-configuration), all other configuration options for this theme support multi-language configuration. 32 | 33 | ::: warning Note 34 | Do not confuse the theme-level `locales` configuration option here with the site-level `locales` configuration option in VuePress 2. Please refer to [Reference > VuePress > Configuration](https://v2.vuepress.vuejs.org/reference/config.html#locales) for more information. 35 | ::: 36 | 37 | ## Appearance 38 | 39 | ### colorMode 40 | 41 | - Type: `'auto' | 'light' | 'dark'` 42 | - Default: `auto` 43 | 44 | You can choose the appearance color of the theme according to your personal preference. The default value `auto` means that the theme color changes dynamically with the system settings. Additionally, you can set this option to `light` or `dark` to use a single color mode. 45 | 46 | ### colorModeSwitch 47 | 48 | - Type: `boolean` 49 | - Default: `true` 50 | 51 | By default, the color mode switch menu is displayed in the top navigation bar. You can hide it by setting `colorModeSwitch` to `false`. 52 | 53 | ### toggleColorModeAriaLabel 54 | 55 | - Type: `string` 56 | - Default: `'Toggle color mode'` 57 | 58 | Specify the `aria-label` attribute of the color mode switch menu. This is mainly for site accessibility (a11y). 59 | 60 | ## Navbar 61 | 62 | ### logo 63 | 64 | - Type: `null|string` 65 | - Default: `null` 66 | 67 | Specifies the logo image URL on the left side of the top navigation bar. It can be either a [Public file path](https://v2.vuepress.vuejs.org/guide/assets.html#public-files) or an HTTP URL. 68 | 69 | Example: 70 | 71 | ```ts 72 | export default { 73 | theme: mixTheme({ 74 | // Public file path 75 | logo: '/images/logo.png', 76 | // URL 77 | logo: 'https://vuejs.org/images/logo.png', 78 | }), 79 | } 80 | ``` 81 | 82 | ### logoDark 83 | 84 | Same as the `logo` option, but used for the logo image in dark mode. 85 | 86 | ### navbar 87 | 88 | - Type: `false | (NavbarItem | NavbarGroup | string)[]` 89 | - Default: `[]` 90 | 91 | ```ts 92 | // Type definitions 93 | 94 | interface NavItem { 95 | text: string 96 | ariaLabel?: string 97 | } 98 | 99 | interface NavGroup extends NavItem { 100 | children: T[] 101 | } 102 | 103 | interface NavbarItem extends NavItem { 104 | link: string 105 | rel?: string 106 | target?: string 107 | activeMatch?: string 108 | } 109 | 110 | type NavbarGroup = NavGroup 111 | ``` 112 | 113 | Example 1: 114 | 115 | ```ts 116 | export default { 117 | theme: mixTheme({ 118 | navbar: [ 119 | // NavbarItem 120 | { 121 | text: 'Foo', 122 | link: '/foo/', 123 | }, 124 | // NavbarGroup 125 | { 126 | text: 'Group', 127 | children: ['/group/foo.md', '/group/bar.md'], 128 | }, 129 | // String - page file path 130 | '/bar/README.md', 131 | ], 132 | }), 133 | } 134 | ``` 135 | 136 | Example 2: 137 | 138 | ```ts 139 | export default { 140 | theme: defaultTheme({ 141 | navbar: [ 142 | // nested group - max depth is 2 143 | { 144 | text: 'Group', 145 | children: [ 146 | { 147 | text: 'SubGroup', 148 | children: ['/group/sub/foo.md', '/group/sub/bar.md'], 149 | }, 150 | ], 151 | }, 152 | // control when should the item be active 153 | { 154 | text: 'Group 2', 155 | children: [ 156 | { 157 | text: 'Always active', 158 | link: '/', 159 | // this item will always be active 160 | activeMatch: '/', 161 | }, 162 | { 163 | text: 'Active on /foo/', 164 | link: '/not-foo/', 165 | // this item will be active when current route path starts with /foo/ 166 | // regular expression is supported 167 | activeMatch: '^/foo/', 168 | }, 169 | ], 170 | }, 171 | ], 172 | }), 173 | } 174 | ``` 175 | 176 | In addition, if you set this configuration item to `false`, the entire top navigation bar will be hidden. 177 | 178 | ### socialLinks 179 | 180 | - Type: `SocialLink[]` 181 | - Default: `[]` 182 | 183 | ```ts 184 | // Type Definition 185 | 186 | type SocialLinkIcon = 'github' | 'twitter' | { svg: string } 187 | 188 | type SocialLink = { 189 | icon: SocialLinkIcon 190 | link: string 191 | ariaLabel?: string 192 | } 193 | ``` 194 | 195 | In addition to the above navigation menu, you can also add your own social accounts to the navigation bar. The currently built-in menu icons are GitHub and Twitter, but you can provide an SVG string to add the menu icon you want. 196 | 197 | ## Sidebar 198 | 199 | ### sidebar 200 | 201 | - Type: `false | 'auto' | SidebarConfigArray | SidebarConfigObject` 202 | - Default: `'auto'` 203 | 204 | ```ts 205 | // Type definitions 206 | 207 | interface NavItem { 208 | text: string 209 | ariaLabel?: string 210 | } 211 | 212 | interface NavGroup extends NavItem { 213 | children: T[] 214 | } 215 | 216 | interface NavLink extends NavItem { 217 | link: string 218 | rel?: string 219 | target?: string 220 | activeMatch?: string 221 | } 222 | 223 | type SidebarItem = NavItem & Partial 224 | type SidebarGroup = SidebarItem & 225 | NavGroup & { 226 | collapsed?: boolean 227 | } 228 | 229 | type SidebarConfigArray = (SidebarItem | SidebarGroup | string)[] 230 | type SidebarConfigObject = Record 231 | ``` 232 | 233 | Setting it to `false` disables the sidebar navigation. Setting it to `'auto'` generates the sidebar based on page titles. 234 | 235 | When the configuration type of an item is SidebarGroup, you can also use `collapsed` to control whether it is collapsed by default, and its default value is `false`. 236 | 237 | The array-style sidebar configuration will make the entire site use the same sidebar navigation. When your document structure is relatively simple and closely related, you may prefer this approach. 238 | 239 | Example 1: 240 | 241 | ```ts 242 | export default { 243 | theme: mixTheme({ 244 | sidebar: [ 245 | // SidebarItem 246 | { 247 | text: 'Foo', 248 | link: '/foo/', 249 | children: [ 250 | // SidebarItem 251 | { 252 | text: 'github', 253 | link: 'https://github.com', 254 | children: [], 255 | }, 256 | // String - Page file path 257 | '/foo/bar.md', 258 | ], 259 | }, 260 | // String - Page file path 261 | '/bar/README.md', 262 | ], 263 | }), 264 | } 265 | ``` 266 | 267 | When you have a large number of documents and need to divide them into different structures, the object-style sidebar configuration comes in handy. In this way, different sub-paths will use their own independent sidebar navigation. 268 | 269 | Example 2: 270 | 271 | ```ts 272 | export default { 273 | theme: mixTheme({ 274 | // Sidebar object 275 | // Different sub-paths will use different sidebar navigations 276 | sidebar: { 277 | '/guide/': [ 278 | { 279 | text: 'Guide', 280 | children: ['/guide/README.md', '/guide/getting-started.md'], 281 | }, 282 | ], 283 | '/reference/': [ 284 | { 285 | text: 'Reference', 286 | children: ['/reference/cli.md', '/reference/config.md'], 287 | }, 288 | ], 289 | }, 290 | }), 291 | } 292 | ``` 293 | 294 | ### sidebarDepth 295 | 296 | - Type: `number` 297 | - Default: `2` 298 | 299 | Set the maximum depth of the sidebar automatically generated according to the page title, set to `0` to disable this function, set to `1` to show only `

` title, set to `2` to show `

`, `

` title, and so on. 300 | 301 | ## Page Configuration 302 | 303 | ### home 304 | 305 | - Type: `string` 306 | - Default: `'/'` 307 | 308 | Specifies the path of the homepage. This value is used in the leftmost link of the navigation bar and the return to homepage link on the 404 page. 309 | 310 | ### editLink 311 | 312 | - Type: `string | ((relativePath: string) => string)` 313 | 314 | The link pattern for editing the page (excluding the homepage) at the bottom of the page. 315 | 316 | Example 1: 317 | 318 | ```ts 319 | export default { 320 | theme: mixTheme({ 321 | editLink: 322 | 'https://github.com/gavinliu6/vuepress-theme-mix/edit/main/docs/:path', 323 | }), 324 | } 325 | ``` 326 | 327 | Example 2: 328 | 329 | ```ts 330 | export default { 331 | theme: mixTheme({ 332 | editLink: ({ relativePath }) => { 333 | if (relativePath.startsWith('packages/')) { 334 | return `https://github.com/acme/monorepo/edit/main/${relativePath}` 335 | } else { 336 | return `https://github.com/acme/monorepo/edit/main/docs/${relativePath}` 337 | } 338 | }, 339 | }), 340 | } 341 | ``` 342 | 343 | ### editLinkText 344 | 345 | - Type: `string` 346 | - Default: `Edit this page` 347 | 348 | The link text for editing the page (excluding the homepage) at the bottom of the page. 349 | 350 | ### toc 351 | 352 | - Type: `false` 353 | 354 | Whether to display the table of contents on the page by default. You can set this option to `false` to turn it off. 355 | 356 | ### tocDepth 357 | 358 | - Type: `number` 359 | - Default: `2` 360 | 361 | Sets the display depth of the table of contents. Setting it to `1` will only show the `

` title, setting it to `2` will show the `

` and `

` titles, and so on. 362 | 363 | ### tocTitle 364 | 365 | - Type: `string` 366 | - Default: `Table of Contents` 367 | 368 | The indicative title of the table of contents. 369 | 370 | ### lastUpdated 371 | 372 | - Type: `boolean` 373 | - Default: `true` 374 | 375 | Whether to display the last update time of the document at the bottom of the page. 376 | 377 | ### lastUpdatedText 378 | 379 | - Type: `string` 380 | - Default: `'Last Updated'` 381 | 382 | The label text for the last update time. 383 | 384 | ### contributors 385 | 386 | - Type: `boolean` 387 | - Default: `true` 388 | 389 | Whether to display a list of document contributors at the bottom of the page. 390 | 391 | ### contributorsText 392 | 393 | - Type: `string` 394 | - Default: `'Contributors'` 395 | 396 | The label text for document contributors. 397 | 398 | ## Others 399 | 400 | ### tip 401 | 402 | - Type: `string` 403 | - Default: `Tip` 404 | 405 | The title of the custom container [tip](/guide/markdown.html#custom-containers). 406 | 407 | ### warning 408 | 409 | - Type: `string` 410 | - Default: `Warning` 411 | 412 | The title of the custom container [warning](/guide/markdown.html#custom-containers). 413 | 414 | ### danger 415 | 416 | - Type: `string` 417 | - Default: `Danger` 418 | 419 | The title of the custom container [danger](/guide/markdown.html#custom-containers). 420 | 421 | ### selectLanguageAriaLabel 422 | 423 | - Type: `string` 424 | - Default: `'Select language'` 425 | 426 | Specifies the `aria-label` attribute of the language selection menu in the navigation bar, mainly for site accessibility (a11y). 427 | 428 | ### selectLanguageName 429 | 430 | - Type: `string` 431 | - Default: none 432 | 433 | Specifies the language name displayed in the drop-down list of the language selection menu. Note that this option is only valid within the [locales](#locales) configuration option of the theme. 434 | 435 | ### notFound 436 | 437 | - Type: `string` 438 | - Default: `'This page could not be found.'` 439 | 440 | Specify the messages of the 404 page. 441 | 442 | ### backToHome 443 | 444 | - Type: `string` 445 | - Default: `Back to home` 446 | 447 | Specify the text of the back to home link in the 404 page. 448 | 449 | ## Plugin Configuration 450 | 451 | The Mix theme uses multiple plugins by default. You can selectively disable them by modifying the following configuration options, all of which are wrapped in `themePlugins`. 452 | 453 | ### git 454 | 455 | - Type: `boolean` 456 | - Default: `true` 457 | 458 | Enables the [@vuepress/plugin-git](https://v2.vuepress.vuejs.org/plugin/official/plugin-git.html) plugin. 459 | 460 | ### mediumZoom 461 | 462 | - Type: `boolean` 463 | - Default: `true` 464 | 465 | Enables the [@vuepress/plugin-medium-zoom](https://v2.vuepress.vuejs.org/plugin/official/plugin-medium-zoom.html) plugin. 466 | 467 | ### nprogress 468 | 469 | - Type: `boolean` 470 | - Default: `true` 471 | 472 | Enables the [@vuepress/plugin-nprogress](https://v2.vuepress.vuejs.org/plugin/official/plugin-nprogress.html) plugin. 473 | 474 | ### shikiTheme 475 | 476 | - Type: `string` 477 | - Default: `'one-dark-pro'` 478 | 479 | The Mix theme uses the [@vuepress/plugin-shiki](https://v2.vuepress.vuejs.org/plugin/official/plugin-shiki.html) plugin to highlight code, which cannot be disabled, but you can specify the Shiki theme to use. [Here](https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes) is a list of all available Shiki themes. 480 | --------------------------------------------------------------------------------