@evgenest/vitepress-theme-contour 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/config.d.ts +19 -0
- package/config.js +146 -0
- package/package.json +41 -4
- package/theme/ThemeSwitch.vue +47 -0
- package/theme/custom.css +267 -0
- package/theme/index.d.ts +3 -0
- package/theme/index.ts +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 evgenest
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @evgenest/vitepress-theme-contour
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A theme for [VitePress](https://vitepress.dev) 2 that adds some variety to the stock look: a wider layout, framed code blocks and a few small tools for reading long code. It extends the default theme, so everything you already use in VitePress keeps working, including local search.
|
|
4
|
+
|
|
5
|
+
> **Requires VitePress 2**, which is still in alpha. The theme styles some of VitePress's internal class names, so a new VitePress version can break parts of it. Keep your lockfile committed and look over your site after upgrading.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
**Layout**
|
|
10
|
+
- Left sidebar pinned to the edge of the window, with the active page on a filled background
|
|
11
|
+
- Search box and page text aligned on one line
|
|
12
|
+
- A wider text column, up to 1100 px
|
|
13
|
+
- The "On this page" outline hugs the right edge and takes only the width its text needs
|
|
14
|
+
- A site header that stays visible while scrolling on narrow screens
|
|
15
|
+
|
|
16
|
+
**Code blocks**
|
|
17
|
+
- Frame, higher-contrast plain text and a styled but visible scrollbar
|
|
18
|
+
- Copy button that appears on hover and shows a green "Copied!" label
|
|
19
|
+
- Line-wrap button: one click applies to every code block on the site and is remembered in the browser
|
|
20
|
+
- Text never runs under the buttons: wrapped text flows around them, and an unwrapped first line fades out before them, then scrolls fully clear
|
|
21
|
+
|
|
22
|
+
**Navigation**
|
|
23
|
+
- Auto / Light / Dark switch in the header on wide screens
|
|
24
|
+
- Previous and next links as cards with arrows
|
|
25
|
+
- On narrow screens, the outline highlights the current section and closes after you pick an item
|
|
26
|
+
- `sidebarTree()` builds a nested sidebar from your folders
|
|
27
|
+
- Interface language: English or Russian
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
bun add -d vitepress@next @evgenest/vitepress-theme-contour
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
npm, pnpm and yarn work too.
|
|
36
|
+
|
|
37
|
+
## Set up
|
|
38
|
+
|
|
39
|
+
`.vitepress/theme/index.ts`:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export { default } from '@evgenest/vitepress-theme-contour'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`.vitepress/config.ts`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { contourConfig, sidebarTree } from '@evgenest/vitepress-theme-contour/config'
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
extends: contourConfig({ locale: 'en' }), // or 'ru'
|
|
52
|
+
title: 'My docs',
|
|
53
|
+
themeConfig: {
|
|
54
|
+
sidebar: sidebarTree(),
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Sidebar
|
|
60
|
+
|
|
61
|
+
`sidebarTree()` turns each folder into a nested group and each Markdown file into a link. All options are optional:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
sidebarTree({
|
|
65
|
+
srcDir: 'docs', // same as srcDir in your config
|
|
66
|
+
exclude: ['drafts.md'], // paths relative to srcDir
|
|
67
|
+
order: ['intro.md', 'guide'], // listed first, the rest follow alphabetically
|
|
68
|
+
titles: { 'intro.md': 'Welcome' }, // overrides the name in the menu
|
|
69
|
+
rootLabel: 'Docs', // wraps the tree in one top-level group
|
|
70
|
+
collapsed: false,
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The menu name comes from `titles`, then frontmatter `title`, then the first `#` heading, then the file name.
|
|
75
|
+
|
|
76
|
+
`exclude` only hides a page from the menu. To leave it off the site, also add it to `srcExclude`.
|
|
77
|
+
|
|
78
|
+
A `README.md` in the root of `srcDir` links to `/`, so add `rewrites: { 'README.md': 'index.md' }` if you use one as your home page.
|
|
79
|
+
|
|
80
|
+
## Your own `markdown.config`
|
|
81
|
+
|
|
82
|
+
You can set `markdown.config` as usual: VitePress runs it together with the theme's own, so the wrap buttons stay.
|
|
83
|
+
|
|
84
|
+
## Colors
|
|
85
|
+
|
|
86
|
+
Colors are CSS variables. Override them in your own stylesheet, imported from `.vitepress/theme/index.ts`:
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
:root { --vp-c-brand-1: #0f766e; }
|
|
90
|
+
.dark { --vp-c-brand-1: #5eead4; }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DefaultTheme, UserConfig } from 'vitepress'
|
|
2
|
+
|
|
3
|
+
export type ContourLocale = 'en' | 'ru'
|
|
4
|
+
|
|
5
|
+
export function contourConfig(options?: { locale?: ContourLocale }): UserConfig<DefaultTheme.Config>
|
|
6
|
+
|
|
7
|
+
export function sidebarTree(options?: {
|
|
8
|
+
/** Folder with the Markdown files, the same as `srcDir` in your config. */
|
|
9
|
+
srcDir?: string
|
|
10
|
+
/** Paths relative to `srcDir` to leave out of the menu. */
|
|
11
|
+
exclude?: string[]
|
|
12
|
+
/** Menu names by path. Otherwise: frontmatter `title`, then the first `#` heading, then the file name. */
|
|
13
|
+
titles?: Record<string, string>
|
|
14
|
+
/** Paths in the order they should appear; everything else follows alphabetically. */
|
|
15
|
+
order?: string[]
|
|
16
|
+
/** Wraps the whole tree in one top-level group. */
|
|
17
|
+
rootLabel?: string
|
|
18
|
+
collapsed?: boolean
|
|
19
|
+
}): DefaultTheme.SidebarItem[]
|
package/config.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const PACKAGE = '@evgenest/vitepress-theme-contour'
|
|
5
|
+
|
|
6
|
+
const LABELS = {
|
|
7
|
+
en: {
|
|
8
|
+
contour: { theme: 'Theme', auto: 'Auto', light: 'Light', dark: 'Dark', wrapOn: 'Wrap lines', wrapOff: 'Do not wrap lines' },
|
|
9
|
+
copy: 'Copy code',
|
|
10
|
+
copied: 'Copied!',
|
|
11
|
+
themeConfig: { docFooter: { prev: 'Previous', next: 'Next' } },
|
|
12
|
+
},
|
|
13
|
+
ru: {
|
|
14
|
+
contour: { theme: 'Тема', auto: 'Авто', light: 'Светлая', dark: 'Тёмная', wrapOn: 'Переносить строки', wrapOff: 'Не переносить строки' },
|
|
15
|
+
copy: 'Копировать',
|
|
16
|
+
copied: 'Скопировано!',
|
|
17
|
+
permalink: 'Ссылка на раздел',
|
|
18
|
+
themeConfig: {
|
|
19
|
+
search: {
|
|
20
|
+
provider: 'local',
|
|
21
|
+
options: {
|
|
22
|
+
translations: {
|
|
23
|
+
button: { buttonText: 'Поиск', buttonAriaLabel: 'Поиск' },
|
|
24
|
+
modal: {
|
|
25
|
+
noResultsText: 'Ничего не найдено по запросу',
|
|
26
|
+
displayDetails: 'Показать развёрнутые результаты',
|
|
27
|
+
resetButtonTitle: 'Очистить',
|
|
28
|
+
backButtonTitle: 'Закрыть',
|
|
29
|
+
footer: {
|
|
30
|
+
selectText: 'открыть',
|
|
31
|
+
selectKeyAriaLabel: 'Enter',
|
|
32
|
+
navigateText: 'выбрать',
|
|
33
|
+
navigateUpKeyAriaLabel: 'стрелка вверх',
|
|
34
|
+
navigateDownKeyAriaLabel: 'стрелка вниз',
|
|
35
|
+
closeText: 'закрыть',
|
|
36
|
+
closeKeyAriaLabel: 'Escape',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
outline: { label: 'На этой странице' },
|
|
43
|
+
sidebarMenuLabel: 'Меню',
|
|
44
|
+
returnToTopLabel: 'Наверх',
|
|
45
|
+
darkModeSwitchLabel: 'Тема',
|
|
46
|
+
lightModeSwitchTitle: 'Включить светлую тему',
|
|
47
|
+
darkModeSwitchTitle: 'Включить тёмную тему',
|
|
48
|
+
skipToContentLabel: 'Перейти к содержимому',
|
|
49
|
+
langMenuLabel: 'Сменить язык',
|
|
50
|
+
navMenuLabel: 'Навигация',
|
|
51
|
+
mobileMenuLabel: 'Меню',
|
|
52
|
+
extraMenuLabel: 'Ещё',
|
|
53
|
+
notFound: {
|
|
54
|
+
title: 'СТРАНИЦА НЕ НАЙДЕНА',
|
|
55
|
+
quote: 'Такой страницы здесь нет. Вернитесь на главную или воспользуйтесь поиском.',
|
|
56
|
+
linkLabel: 'на главную',
|
|
57
|
+
linkText: 'На главную',
|
|
58
|
+
},
|
|
59
|
+
docFooter: { prev: 'Предыдущая', next: 'Следующая' },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// markdown-it plugin: adds the wrap button to every code block.
|
|
65
|
+
function contourMarkdown(md, { locale = 'en' } = {}) {
|
|
66
|
+
const labels = LABELS[locale]
|
|
67
|
+
const fence = md.renderer.rules.fence
|
|
68
|
+
const label = labels.contour.wrapOn
|
|
69
|
+
md.renderer.rules.fence = (...args) =>
|
|
70
|
+
fence(...args).replace(
|
|
71
|
+
'<button',
|
|
72
|
+
`<button type="button" class="wrap" title="${label}" aria-label="${label}" aria-pressed="false"></button><button`,
|
|
73
|
+
)
|
|
74
|
+
if (labels.permalink) {
|
|
75
|
+
md.core.ruler.push('contour-permalink', (state) => {
|
|
76
|
+
for (const token of state.tokens) {
|
|
77
|
+
for (const child of token.children ?? []) {
|
|
78
|
+
if (child.type !== 'link_open') continue
|
|
79
|
+
const value = child.attrGet('aria-label')
|
|
80
|
+
if (value?.startsWith('Permalink to')) {
|
|
81
|
+
child.attrSet('aria-label', value.replace('Permalink to', labels.permalink))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Base config to pass to `extends`. */
|
|
90
|
+
export function contourConfig({ locale = 'en' } = {}) {
|
|
91
|
+
const labels = LABELS[locale]
|
|
92
|
+
if (!labels) throw new Error(`${PACKAGE}: unknown locale "${locale}", use one of: ${Object.keys(LABELS).join(', ')}`)
|
|
93
|
+
return {
|
|
94
|
+
lang: locale,
|
|
95
|
+
// Applied before first paint so code blocks do not jump on load.
|
|
96
|
+
head: [
|
|
97
|
+
['script', {}, "try{if(localStorage.getItem('code-wrap')==='1')document.documentElement.classList.add('code-wrap')}catch(e){}"],
|
|
98
|
+
],
|
|
99
|
+
vite: { ssr: { noExternal: [PACKAGE] } },
|
|
100
|
+
markdown: {
|
|
101
|
+
codeCopyButton: { tooltipText: labels.copy, copiedText: labels.copied },
|
|
102
|
+
config: (md) => contourMarkdown(md, { locale }),
|
|
103
|
+
},
|
|
104
|
+
themeConfig: {
|
|
105
|
+
search: { provider: 'local' },
|
|
106
|
+
...labels.themeConfig,
|
|
107
|
+
contour: labels.contour,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function pageTitle(file, fallback) {
|
|
113
|
+
const text = readFileSync(file, 'utf8')
|
|
114
|
+
const front = text.match(/^---\r?\n[\s\S]*?^title:\s*(.+)$[\s\S]*?^---/m)
|
|
115
|
+
const heading = text.match(/^# (.+)$/m)
|
|
116
|
+
return (front?.[1] ?? heading?.[1] ?? fallback).trim().replace(/^['"]|['"]$/g, '')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Builds a sidebar tree from the folders in `srcDir`. */
|
|
120
|
+
export function sidebarTree({ srcDir = '.', exclude = [], titles = {}, order = [], rootLabel, collapsed = false } = {}) {
|
|
121
|
+
const isHome = (path) => path === 'README.md' || path === 'index.md'
|
|
122
|
+
const rank = (path) => {
|
|
123
|
+
const i = order.indexOf(path)
|
|
124
|
+
if (i !== -1) return i
|
|
125
|
+
return isHome(path) ? -1 : order.length
|
|
126
|
+
}
|
|
127
|
+
const walk = (rel) =>
|
|
128
|
+
readdirSync(join(srcDir, rel), { withFileTypes: true })
|
|
129
|
+
.filter((e) => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'public')
|
|
130
|
+
.map((e) => ({ e, path: rel ? `${rel}/${e.name}` : e.name }))
|
|
131
|
+
.filter(({ e, path }) => e.isDirectory() || (e.name.endsWith('.md') && !exclude.includes(path)))
|
|
132
|
+
.sort((a, b) => rank(a.path) - rank(b.path) || a.path.localeCompare(b.path))
|
|
133
|
+
.map(({ e, path }) => {
|
|
134
|
+
if (e.isDirectory()) {
|
|
135
|
+
const items = walk(path)
|
|
136
|
+
return items.length ? { text: titles[path] ?? e.name, collapsed, items } : null
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
text: titles[path] ?? pageTitle(join(srcDir, path), e.name),
|
|
140
|
+
link: isHome(path) ? '/' : `/${path.replace(/\.md$/, '')}`,
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
const items = walk('')
|
|
145
|
+
return rootLabel ? [{ text: rootLabel, collapsed, items }] : items
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evgenest/vitepress-theme-contour",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A VitePress 2 theme with a calmer layout, framed code blocks, a global line-wrap toggle and an auto/light/dark switch.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vitepress",
|
|
7
|
+
"vitepress-theme",
|
|
8
|
+
"documentation",
|
|
9
|
+
"theme"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/evgenest/vitepress-theme-contour.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/evgenest/vitepress-theme-contour#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/evgenest/vitepress-theme-contour/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./theme/index.d.ts",
|
|
24
|
+
"default": "./theme/index.ts"
|
|
25
|
+
},
|
|
26
|
+
"./config": {
|
|
27
|
+
"types": "./config.d.ts",
|
|
28
|
+
"default": "./config.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"theme",
|
|
33
|
+
"config.js",
|
|
34
|
+
"config.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vitepress": "^2.0.0-alpha.21",
|
|
38
|
+
"vue": "^3.5.0"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
3
|
+
import { useData } from 'vitepress'
|
|
4
|
+
|
|
5
|
+
const KEY = 'vitepress-theme-appearance'
|
|
6
|
+
const { isDark, theme } = useData()
|
|
7
|
+
const ui = computed(() => (theme.value as any).contour ?? { theme: 'Theme', auto: 'Auto', light: 'Light', dark: 'Dark' })
|
|
8
|
+
const mode = ref('auto')
|
|
9
|
+
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
mode.value = localStorage.getItem(KEY) || 'auto'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
async function change() {
|
|
15
|
+
const v = mode.value
|
|
16
|
+
const system = matchMedia('(prefers-color-scheme: dark)').matches
|
|
17
|
+
isDark.value = v === 'auto' ? system : v === 'dark'
|
|
18
|
+
// VitePress collapses a choice equal to the system theme into auto,
|
|
19
|
+
// so the explicit choice is written after its own write.
|
|
20
|
+
await nextTick()
|
|
21
|
+
localStorage.setItem(KEY, v)
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<label class="theme-switch">
|
|
27
|
+
<span class="sr-only">{{ ui.theme }}</span>
|
|
28
|
+
<select v-model="mode" @change="change" :aria-label="ui.theme">
|
|
29
|
+
<option value="auto">{{ ui.auto }}</option>
|
|
30
|
+
<option value="light">{{ ui.light }}</option>
|
|
31
|
+
<option value="dark">{{ ui.dark }}</option>
|
|
32
|
+
</select>
|
|
33
|
+
</label>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style scoped>
|
|
37
|
+
.theme-switch { display: none; align-items: center; margin-left: 16px; }
|
|
38
|
+
@media (min-width: 1280px) { .theme-switch { display: flex; } }
|
|
39
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
|
40
|
+
select {
|
|
41
|
+
height: 32px; padding: 0 10px; font-size: 13px; cursor: pointer;
|
|
42
|
+
color: var(--vp-c-text-1); background: var(--vp-c-bg);
|
|
43
|
+
border: 1px solid var(--vp-c-border); border-radius: 6px;
|
|
44
|
+
appearance: auto;
|
|
45
|
+
}
|
|
46
|
+
select:hover { border-color: var(--vp-c-brand-1); }
|
|
47
|
+
</style>
|
package/theme/custom.css
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/* Palette and framing; font and inline code stay as in VitePress. */
|
|
2
|
+
:root {
|
|
3
|
+
--contour-header-bg: #f6f7f9;
|
|
4
|
+
--contour-on-accent: #ffffff;
|
|
5
|
+
--vp-c-bg: #ffffff;
|
|
6
|
+
--vp-c-bg-alt: #f6f7f9;
|
|
7
|
+
--vp-c-bg-soft: #f6f7f9;
|
|
8
|
+
--vp-c-text-1: #17181c;
|
|
9
|
+
--vp-c-text-2: #353841;
|
|
10
|
+
--vp-c-text-3: #6b6f7b;
|
|
11
|
+
--vp-c-divider: #e3e4e8;
|
|
12
|
+
--vp-c-gutter: #e3e4e8;
|
|
13
|
+
--vp-c-border: #c1c3c8;
|
|
14
|
+
--vp-c-brand-1: #3d50f5;
|
|
15
|
+
--vp-c-brand-2: #3344d6;
|
|
16
|
+
--vp-c-brand-3: #3d50f5;
|
|
17
|
+
--vp-c-brand-soft: rgba(61, 80, 245, 0.12);
|
|
18
|
+
--vp-code-color: #3451b2;
|
|
19
|
+
--vp-code-bg: rgba(142, 150, 170, 0.14);
|
|
20
|
+
--vp-code-block-bg: #f6f7f9;
|
|
21
|
+
--contour-code-text: #2b2d36;
|
|
22
|
+
--contour-scroll: #a9acb5;
|
|
23
|
+
--vp-sidebar-width: 300px;
|
|
24
|
+
--vp-nav-bg-color: #f6f7f9;
|
|
25
|
+
--vp-sidebar-bg-color: var(--vp-c-bg);
|
|
26
|
+
--contour-gutter: 36px;
|
|
27
|
+
}
|
|
28
|
+
.dark {
|
|
29
|
+
--contour-header-bg: #23262f;
|
|
30
|
+
--contour-on-accent: #17264f;
|
|
31
|
+
--vp-nav-bg-color: #23262f;
|
|
32
|
+
--vp-c-bg: #17181c;
|
|
33
|
+
--vp-c-bg-alt: #23262f;
|
|
34
|
+
--vp-c-bg-soft: #23262f;
|
|
35
|
+
--vp-c-bg-elv: #23262f;
|
|
36
|
+
--vp-c-text-1: #ffffff;
|
|
37
|
+
--vp-c-text-2: #c1c3c8;
|
|
38
|
+
--vp-c-text-3: #8b8e98;
|
|
39
|
+
--vp-c-divider: #353841;
|
|
40
|
+
--vp-c-gutter: #353841;
|
|
41
|
+
--vp-c-border: #4a4e5a;
|
|
42
|
+
--vp-c-brand-1: #b3c7ff;
|
|
43
|
+
--vp-c-brand-2: #9db5ff;
|
|
44
|
+
--vp-c-brand-3: #b3c7ff;
|
|
45
|
+
--vp-c-brand-soft: rgba(179, 199, 255, 0.14);
|
|
46
|
+
--vp-code-color: #a8b1ff;
|
|
47
|
+
--vp-code-bg: rgba(101, 117, 133, 0.16);
|
|
48
|
+
--vp-code-block-bg: #23262f;
|
|
49
|
+
--contour-code-text: #d6deeb;
|
|
50
|
+
--contour-scroll: #6a6e7b;
|
|
51
|
+
--vp-sidebar-bg-color: #23262f;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Full-width header. */
|
|
55
|
+
.VPNavBar, .VPNavBar.has-sidebar, .VPNavBar:not(.home) {
|
|
56
|
+
background: var(--contour-header-bg) !important;
|
|
57
|
+
border-bottom: 1px solid var(--vp-c-divider);
|
|
58
|
+
}
|
|
59
|
+
.VPNavBar .divider { display: none; }
|
|
60
|
+
.VPNavBar .content-body, .VPNavBar .title { background: transparent !important; backdrop-filter: none !important; }
|
|
61
|
+
.VPNavBarTitle .title { color: var(--vp-c-brand-1); font-size: 22px; font-weight: 600; border: 0; }
|
|
62
|
+
|
|
63
|
+
@media (min-width: 960px) {
|
|
64
|
+
/* Sidebar pinned to the left edge. */
|
|
65
|
+
.VPSidebar {
|
|
66
|
+
width: var(--vp-sidebar-width) !important;
|
|
67
|
+
padding: 80px 16px 48px !important;
|
|
68
|
+
background: var(--vp-sidebar-bg-color) !important;
|
|
69
|
+
border-right: 1px solid var(--vp-c-divider);
|
|
70
|
+
}
|
|
71
|
+
.VPSidebar .curtain { display: none; }
|
|
72
|
+
.VPContent.has-sidebar { padding-left: var(--vp-sidebar-width) !important; padding-right: 0 !important; }
|
|
73
|
+
.VPFooter { padding-left: var(--vp-sidebar-width) !important; }
|
|
74
|
+
|
|
75
|
+
.VPNavBar.has-sidebar .container { max-width: none !important; }
|
|
76
|
+
.VPNavBar.has-sidebar .wrapper { padding: 0 24px !important; }
|
|
77
|
+
.VPNavBar.has-sidebar .title { left: 0 !important; width: var(--vp-sidebar-width) !important; padding: 0 0 0 24px !important; position: absolute !important; }
|
|
78
|
+
.VPNavBar.has-sidebar .content { padding-left: calc(var(--vp-sidebar-width) - 24px) !important; padding-right: 0 !important; }
|
|
79
|
+
.VPNavBar .content-body { justify-content: flex-start !important; }
|
|
80
|
+
|
|
81
|
+
/* Search aligned with the page text. */
|
|
82
|
+
.VPNavBarSearch { flex-grow: 1; justify-content: flex-start; padding-left: var(--contour-gutter) !important; }
|
|
83
|
+
.VPNavBarSearchButton {
|
|
84
|
+
width: 352px; height: 40px; margin: 0; padding: 0 8px 0 12px; justify-content: flex-start;
|
|
85
|
+
background: var(--vp-c-bg) !important; border: 1px solid var(--vp-c-border); border-radius: 8px;
|
|
86
|
+
}
|
|
87
|
+
.VPNavBarSearchButton:hover { border-color: var(--vp-c-brand-1); }
|
|
88
|
+
.VPNavBarSearchButton .keys { margin-left: auto; }
|
|
89
|
+
.VPNavBarAppearance { display: none !important; }
|
|
90
|
+
.VPNavBarExtra { margin-left: auto; }
|
|
91
|
+
|
|
92
|
+
/* Text starts right after the sidebar. */
|
|
93
|
+
.VPDoc.has-sidebar, .VPDoc { padding: 40px 0 64px !important; }
|
|
94
|
+
.VPDoc .container { display: flex; justify-content: flex-start !important; max-width: none !important; margin: 0 !important; }
|
|
95
|
+
.VPDoc > .container > .content { flex: 1 1 0; min-width: 0; max-width: 1172px !important; padding: 0 var(--contour-gutter) !important; margin: 0 !important; order: 1; }
|
|
96
|
+
.VPDoc .content-container { max-width: none !important; margin: 0 !important; }
|
|
97
|
+
|
|
98
|
+
/* The right outline takes only the width its text needs. */
|
|
99
|
+
.VPDoc .aside { order: 2; flex: 0 0 auto; width: max-content; max-width: 360px !important; margin-left: auto; padding-left: 0 !important; padding-right: 24px; }
|
|
100
|
+
.VPDoc .aside-container { position: sticky; top: 104px; height: calc(100vh - 104px); padding-top: 0 !important; width: max-content !important; max-width: 100%; }
|
|
101
|
+
.VPDoc .aside-content { width: max-content !important; max-width: 100%; min-height: 0 !important; }
|
|
102
|
+
.VPDoc .aside-curtain { display: none; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Sidebar: active item on a filled background. */
|
|
106
|
+
.VPSidebarItem .indicator { display: none; }
|
|
107
|
+
.VPSidebarItem.is-active > .item {
|
|
108
|
+
background: var(--vp-c-brand-1);
|
|
109
|
+
border-radius: 4px;
|
|
110
|
+
}
|
|
111
|
+
.VPSidebarItem.is-active > .item .link-text,
|
|
112
|
+
.VPSidebarItem.is-active > .item .text { color: var(--contour-on-accent) !important; font-weight: 600; }
|
|
113
|
+
.VPSidebarItem .item { padding-left: 8px; padding-right: 8px; }
|
|
114
|
+
.VPSidebarItem.level-0 { padding-bottom: 0 !important; }
|
|
115
|
+
.VPSidebarItem.level-0 > .item > .text { font-weight: 600; }
|
|
116
|
+
.VPSidebarItem .items { border-left: 1px solid var(--vp-c-divider); margin-left: 8px; padding-left: 8px; }
|
|
117
|
+
.VPSidebarItem .text { color: var(--vp-c-text-2) !important; font-size: 14px; }
|
|
118
|
+
.VPSidebarItem .item:hover:not(:has(.is-active)) .text { color: var(--vp-c-brand-1) !important; }
|
|
119
|
+
|
|
120
|
+
/* Right outline: VitePress rail, active item in the accent color. */
|
|
121
|
+
.VPDocAsideOutline .outline-link.active { color: var(--vp-c-brand-1) !important; font-weight: 500; }
|
|
122
|
+
/* Narrow screens: current section in color; clicks on item text reach the link so the list closes. */
|
|
123
|
+
.VPLocalNavOutlineDropdown .outline-link.active { color: var(--vp-c-brand-1) !important; font-weight: 500; }
|
|
124
|
+
.VPLocalNavOutlineDropdown .outline-link bdi { pointer-events: none; }
|
|
125
|
+
.VPDocAsideOutline .outline-marker { background: var(--vp-c-brand-1); width: 2px; }
|
|
126
|
+
|
|
127
|
+
/* Headings. */
|
|
128
|
+
.vp-doc h1 { font-size: 36px; line-height: 1.2; color: var(--vp-c-text-1); letter-spacing: -0.02em; }
|
|
129
|
+
.vp-doc h1:not(:first-of-type) { font-size: 24px; margin-top: 16px; color: var(--vp-c-text-2); font-weight: 500; letter-spacing: -0.01em; }
|
|
130
|
+
.vp-doc h2 { color: var(--vp-c-text-1); }
|
|
131
|
+
.vp-doc p, .vp-doc li, .vp-doc td { color: var(--vp-c-text-2); }
|
|
132
|
+
|
|
133
|
+
/* Code blocks: frame, contrasting text, visible scrollbar. */
|
|
134
|
+
.vp-doc div[class*='language-'] {
|
|
135
|
+
background: var(--vp-code-block-bg) !important;
|
|
136
|
+
border: 1px solid var(--vp-c-border);
|
|
137
|
+
border-radius: 6px !important;
|
|
138
|
+
margin-left: 0 !important;
|
|
139
|
+
margin-right: 0 !important;
|
|
140
|
+
}
|
|
141
|
+
.vp-doc div[class*='language-'] code { font-size: 14px; }
|
|
142
|
+
.vp-doc div[class*='language-']:is(.language-, .language-txt, .language-text) .line span {
|
|
143
|
+
color: var(--contour-code-text) !important;
|
|
144
|
+
}
|
|
145
|
+
.vp-doc div[class*='language-'] > span.lang { display: none; }
|
|
146
|
+
.vp-doc div[class*='language-'] pre { scrollbar-width: thin; scrollbar-color: var(--contour-scroll) transparent; }
|
|
147
|
+
.vp-doc div[class*='language-'] pre::-webkit-scrollbar { height: 10px; }
|
|
148
|
+
.vp-doc div[class*='language-'] pre::-webkit-scrollbar-thumb {
|
|
149
|
+
background: var(--contour-scroll); border-radius: 5px;
|
|
150
|
+
border: 2px solid var(--vp-code-block-bg);
|
|
151
|
+
}
|
|
152
|
+
.vp-doc div[class*='language-'] pre::-webkit-scrollbar-track { background: transparent; }
|
|
153
|
+
:root {
|
|
154
|
+
--contour-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2353565f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='9' width='12' height='12' rx='2'/%3E%3Cpath d='M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1'/%3E%3C/svg%3E");
|
|
155
|
+
--contour-icon-wrap: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2353565f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18M3 12h15a3 3 0 0 1 0 6h-4M3 18h6'/%3E%3Cpath d='m16 16-2 2 2 2'/%3E%3C/svg%3E");
|
|
156
|
+
--contour-icon-wrap-on: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233d50f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18M3 12h15a3 3 0 0 1 0 6h-4M3 18h6'/%3E%3Cpath d='m16 16-2 2 2 2'/%3E%3C/svg%3E");
|
|
157
|
+
--contour-btn-hover: #dfe1e6;
|
|
158
|
+
}
|
|
159
|
+
.dark {
|
|
160
|
+
--contour-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23c1c3c8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='9' width='12' height='12' rx='2'/%3E%3Cpath d='M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1'/%3E%3C/svg%3E");
|
|
161
|
+
--contour-icon-wrap: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23c1c3c8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18M3 12h15a3 3 0 0 1 0 6h-4M3 18h6'/%3E%3Cpath d='m16 16-2 2 2 2'/%3E%3C/svg%3E");
|
|
162
|
+
--contour-icon-wrap-on: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b3c7ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18M3 12h15a3 3 0 0 1 0 6h-4M3 18h6'/%3E%3Cpath d='m16 16-2 2 2 2'/%3E%3C/svg%3E");
|
|
163
|
+
--contour-btn-hover: #3a3e4a;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Code block buttons appear on hover. */
|
|
167
|
+
.vp-doc div[class*='language-'] > button.copy,
|
|
168
|
+
.vp-doc div[class*='language-'] > button.wrap {
|
|
169
|
+
position: absolute; z-index: 3; top: 8px; width: 32px; height: 32px;
|
|
170
|
+
border: 0 !important; border-radius: 4px; cursor: pointer;
|
|
171
|
+
background-color: var(--vp-code-block-bg) !important;
|
|
172
|
+
background-position: 50%; background-size: 18px; background-repeat: no-repeat;
|
|
173
|
+
opacity: 0; transition: opacity 0.2s, background-color 0.2s;
|
|
174
|
+
}
|
|
175
|
+
.vp-doc div[class*='language-'] > button.copy { right: 44px; inset-inline-end: 44px; background-image: var(--contour-icon-copy) !important; }
|
|
176
|
+
.vp-doc div[class*='language-'] > button.wrap { right: 8px; background-image: var(--contour-icon-wrap); }
|
|
177
|
+
.vp-doc div[class*='language-']:hover > button.copy,
|
|
178
|
+
.vp-doc div[class*='language-']:hover > button.wrap,
|
|
179
|
+
.vp-doc div[class*='language-'] > button:focus-visible { opacity: 0.8; }
|
|
180
|
+
.vp-doc div[class*='language-'] > button.copy:hover,
|
|
181
|
+
.vp-doc div[class*='language-'] > button.wrap:hover {
|
|
182
|
+
opacity: 1; background-color: var(--contour-btn-hover) !important;
|
|
183
|
+
}
|
|
184
|
+
@media (hover: none) {
|
|
185
|
+
.vp-doc div[class*='language-'] > button.copy,
|
|
186
|
+
.vp-doc div[class*='language-'] > button.wrap { opacity: 0.8; }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Line wrapping on. */
|
|
190
|
+
:root.code-wrap .vp-doc div[class*='language-'] > button.wrap { background-image: var(--contour-icon-wrap-on); opacity: 1; }
|
|
191
|
+
:root.code-wrap .vp-doc div[class*='language-'] pre { overflow-x: hidden; }
|
|
192
|
+
:root.code-wrap .vp-doc div[class*='language-'] code { display: block; width: 100%; min-width: 0; white-space: pre-wrap; padding-right: 24px; }
|
|
193
|
+
:root.code-wrap .vp-doc div[class*='language-'] .line { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
194
|
+
|
|
195
|
+
/* Green copied label with an arrow to the button. */
|
|
196
|
+
.vp-doc div[class*='language-'] > button.copy.copied,
|
|
197
|
+
.vp-doc div[class*='language-'] > button.copy:hover.copied {
|
|
198
|
+
opacity: 1; border-radius: 4px !important; z-index: 5;
|
|
199
|
+
background-color: var(--contour-btn-hover) !important;
|
|
200
|
+
background-image: var(--contour-icon-copy) !important;
|
|
201
|
+
}
|
|
202
|
+
.vp-doc div[class*='language-'] > button.copy.copied::before,
|
|
203
|
+
.vp-doc div[class*='language-'] > button.copy:hover.copied::before {
|
|
204
|
+
position: absolute; top: 50%; right: calc(100% + 8px); left: auto;
|
|
205
|
+
transform: translateY(-50%);
|
|
206
|
+
display: block; width: max-content; height: auto; padding: 2px 12px;
|
|
207
|
+
border: 0; border-radius: 4px;
|
|
208
|
+
font-size: 14px; font-weight: 400; line-height: 24px;
|
|
209
|
+
color: #fff; background: #078662;
|
|
210
|
+
content: attr(data-copied);
|
|
211
|
+
}
|
|
212
|
+
.vp-doc div[class*='language-'] > button.copy.copied::after,
|
|
213
|
+
.vp-doc div[class*='language-'] > button.copy:hover.copied::after {
|
|
214
|
+
content: ''; position: absolute; top: 50%; right: calc(100% + 3px);
|
|
215
|
+
transform: translateY(-50%);
|
|
216
|
+
border: solid transparent; border-width: 5px 0 5px 5px;
|
|
217
|
+
border-left-color: #078662;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Footer navigation: cards with arrows. */
|
|
221
|
+
.VPDocFooter .pager-link {
|
|
222
|
+
position: relative; display: block; height: 100%; padding: 16px 16px 16px 52px;
|
|
223
|
+
border: 1px solid var(--vp-c-border); border-radius: 8px;
|
|
224
|
+
}
|
|
225
|
+
.VPDocFooter .pager-link.next { padding: 16px 52px 16px 16px; text-align: right; }
|
|
226
|
+
.VPDocFooter .pager-link:hover { border-color: var(--vp-c-brand-1); }
|
|
227
|
+
.VPDocFooter .pager-link::before {
|
|
228
|
+
content: ''; position: absolute; top: 50%; left: 16px; width: 24px; height: 24px;
|
|
229
|
+
transform: translateY(-50%); background: var(--vp-c-text-2);
|
|
230
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 12H5M11 6l-6 6 6 6'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
231
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 12H5M11 6l-6 6 6 6'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
232
|
+
}
|
|
233
|
+
.VPDocFooter .pager-link.next::before { left: auto; right: 16px; transform: translateY(-50%) scaleX(-1); }
|
|
234
|
+
.VPDocFooter .pager-link .desc { display: block; font-size: 14px; line-height: 20px; color: var(--vp-c-text-2); }
|
|
235
|
+
.VPDocFooter .pager-link .title { display: block; margin-top: 2px; font-size: 20px; line-height: 28px; font-weight: 500; color: var(--vp-c-text-1); }
|
|
236
|
+
|
|
237
|
+
@media (min-width: 1280px) { .VPNavBarExtra { display: none !important; } }
|
|
238
|
+
|
|
239
|
+
/* Narrow screens: header and local nav stay while scrolling. */
|
|
240
|
+
@media (max-width: 959.98px) {
|
|
241
|
+
.VPNav { position: sticky !important; top: 0; }
|
|
242
|
+
.VPLocalNav { top: var(--vp-nav-height) !important; }
|
|
243
|
+
.VPDoc .vp-doc :is(h1, h2, h3, h4, h5, h6) { scroll-margin-top: calc(var(--vp-nav-height) + 56px); }
|
|
244
|
+
.VPLocalNavOutlineDropdown .items { max-height: calc(100dvh - var(--vp-nav-height) - 72px) !important; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Wrapped first row flows around the buttons. */
|
|
248
|
+
:root.code-wrap .vp-doc div[class*='language-'] code::before {
|
|
249
|
+
content: ''; float: right; width: 60px; height: 22px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* No wrapping: the first line fades out before the buttons. */
|
|
253
|
+
:root:not(.code-wrap) .vp-doc div[class*='language-']::before {
|
|
254
|
+
content: ''; position: absolute; z-index: 2; top: 0; right: 0;
|
|
255
|
+
width: 132px; height: 44px; pointer-events: none;
|
|
256
|
+
border-top-right-radius: 6px;
|
|
257
|
+
background: linear-gradient(to right, transparent, var(--vp-code-block-bg) 48px);
|
|
258
|
+
opacity: 0; transition: opacity 0.2s;
|
|
259
|
+
}
|
|
260
|
+
:root:not(.code-wrap) .vp-doc div[class*='language-']:hover::before,
|
|
261
|
+
:root:not(.code-wrap) .vp-doc div[class*='language-']:focus-within::before { opacity: 1; }
|
|
262
|
+
@media (hover: none) {
|
|
263
|
+
:root:not(.code-wrap) .vp-doc div[class*='language-']::before { opacity: 1; }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Room on the right so a fully scrolled first line clears the fade. */
|
|
267
|
+
:root:not(.code-wrap) .vp-doc div[class*='language-'] .line:first-child { padding-right: 116px; }
|
package/theme/index.d.ts
ADDED
package/theme/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { h, onMounted } from 'vue'
|
|
2
|
+
import { inBrowser, onContentUpdated, useData } from 'vitepress'
|
|
3
|
+
import DefaultTheme from 'vitepress/theme'
|
|
4
|
+
import ThemeSwitch from './ThemeSwitch.vue'
|
|
5
|
+
import './custom.css'
|
|
6
|
+
|
|
7
|
+
const WRAP_KEY = 'code-wrap'
|
|
8
|
+
let wrapLabels = { on: 'Wrap lines', off: 'Do not wrap lines' }
|
|
9
|
+
|
|
10
|
+
// Keep every wrap button in step with the global state.
|
|
11
|
+
function syncWrapButtons() {
|
|
12
|
+
const on = document.documentElement.classList.contains(WRAP_KEY)
|
|
13
|
+
const label = on ? wrapLabels.off : wrapLabels.on
|
|
14
|
+
document.querySelectorAll("div[class*='language-'] > button.wrap").forEach((button) => {
|
|
15
|
+
button.setAttribute('aria-pressed', String(on))
|
|
16
|
+
button.setAttribute('aria-label', label)
|
|
17
|
+
button.setAttribute('title', label)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The stock mobile outline has no current-section highlight.
|
|
22
|
+
function markCurrentHeading() {
|
|
23
|
+
const links = [...document.querySelectorAll<HTMLAnchorElement>('.VPLocalNavOutlineDropdown .outline-link')]
|
|
24
|
+
const heading = (link: HTMLAnchorElement) => document.getElementById(decodeURIComponent(link.hash.slice(1)))
|
|
25
|
+
const atBottom = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 1
|
|
26
|
+
let current: HTMLAnchorElement | undefined = atBottom ? links.at(-1) : undefined
|
|
27
|
+
if (!current) {
|
|
28
|
+
for (const link of links) {
|
|
29
|
+
const el = heading(link)
|
|
30
|
+
if (!el) continue
|
|
31
|
+
const margin = Number.parseFloat(getComputedStyle(el).scrollMarginTop) || 0
|
|
32
|
+
if (el.getBoundingClientRect().top > margin + 4) break
|
|
33
|
+
current = link
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
links.forEach((link) => link.classList.toggle('active', link === current))
|
|
37
|
+
current?.scrollIntoView({ block: 'nearest' })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
extends: DefaultTheme,
|
|
42
|
+
Layout: () =>
|
|
43
|
+
h(DefaultTheme.Layout, null, {
|
|
44
|
+
'nav-bar-content-after': () => h(ThemeSwitch),
|
|
45
|
+
}),
|
|
46
|
+
setup() {
|
|
47
|
+
const { theme } = useData()
|
|
48
|
+
const ui = (theme.value as any).contour
|
|
49
|
+
if (ui) wrapLabels = { on: ui.wrapOn, off: ui.wrapOff }
|
|
50
|
+
onMounted(syncWrapButtons)
|
|
51
|
+
onContentUpdated(syncWrapButtons)
|
|
52
|
+
},
|
|
53
|
+
enhanceApp() {
|
|
54
|
+
if (!inBrowser) return
|
|
55
|
+
window.addEventListener('click', (e) => {
|
|
56
|
+
const button = e.target as HTMLElement
|
|
57
|
+
if (button.closest?.('.VPLocalNavOutlineDropdown > button')) {
|
|
58
|
+
requestAnimationFrame(markCurrentHeading)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if (!button.matches?.("div[class*='language-'] > button.wrap")) return
|
|
62
|
+
// Blocks above change height: keep the pressed button where it is on screen.
|
|
63
|
+
const top = button.getBoundingClientRect().top
|
|
64
|
+
const on = document.documentElement.classList.toggle(WRAP_KEY)
|
|
65
|
+
try {
|
|
66
|
+
localStorage.setItem(WRAP_KEY, on ? '1' : '0')
|
|
67
|
+
} catch {}
|
|
68
|
+
syncWrapButtons()
|
|
69
|
+
window.scrollBy({ top: button.getBoundingClientRect().top - top, behavior: 'instant' })
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
}
|