Merge branch 'main' into code-block-title

pull/5007/head
bluwy 2 weeks ago
commit 7f1052d72b
No known key found for this signature in database

@ -46,4 +46,4 @@ jobs:
cache: pnpm
- run: pnpm install
- run: pnpm build
- run: npx pkg-pr-new publish --compact --no-template --pnpm
- run: npx pkg-pr-new publish --compact --no-template --pnpm --packageManager=pnpm,npm,yarn

@ -65,6 +65,11 @@ const nav: DefaultTheme.Config['nav'] = [
]
}
]
},
{
text: 'Active Match',
link: '/markdown-extensions/',
activeMatch: '^/home'
}
]
@ -132,6 +137,44 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
link: '/markdown-extensions/foo'
}
]
},
{
text: 'Team & Sponsors',
items: [
{
text: 'Doc Layout',
link: '/team-and-sponsors/doc'
},
{
text: 'Page Layout',
link: '/team-and-sponsors/page'
},
{
text: 'Home Layout',
link: '/team-and-sponsors/home'
},
{
text: 'Home Layout (raw markdown)',
link: '/team-and-sponsors/home-no-markdown-styles'
}
]
},
{
text: 'Sidebar Hash',
items: [
{
text: 'Overview',
link: '/sidebar-hash/'
},
{
text: 'Section One',
link: '/sidebar-hash/#section-one'
},
{
text: 'Section Two',
link: '/sidebar-hash/#section-two'
}
]
}
],
'/multi-sidebar/': [
@ -154,6 +197,7 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
export default defineConfig({
title: 'Example',
description: 'An example app using VitePress.',
srcExclude: ['**/parts/**'],
markdown: {
image: { lazyLoad: true }
},

@ -5,12 +5,13 @@ const props = defineProps<{
options: string[]
defaultOption: string
screenMenu?: boolean
menu?: boolean
}>()
// reactivity isn't needed for props here
const key = removeSpaces(`api-preference-${props.options.join('-')}`)
const name = key + (props.screenMenu ? '-screen-menu' : '')
const name = key + (props.screenMenu ? '-screen-menu' : props.menu ? '-menu' : '')
const selected = useLocalStorage(key, () => props.defaultOption)
@ -25,8 +26,11 @@ function removeSpaces(str: string) {
</script>
<template>
<div class="VPApiPreference" :class="{ 'screen-menu': screenMenu }">
<template v-for="option in optionsWithKeys" :key="option">
<div
class="VPApiPreference"
:class="{ 'screen-menu': screenMenu, 'in-menu': menu }"
>
<template v-for="option in optionsWithKeys" :key="option.key">
<input
type="radio"
:id="option.key"
@ -42,10 +46,10 @@ function removeSpaces(str: string) {
<style scoped>
.VPApiPreference {
display: flex;
margin: 12px 0;
margin: 0.75rem 0;
border: 1px solid var(--vp-c-border);
border-radius: 6px;
font-size: 14px;
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--vp-c-text-1);
}
@ -57,8 +61,13 @@ function removeSpaces(str: string) {
margin-bottom: 0;
}
.VPApiPreference.in-menu {
margin: 0.5rem 0.75rem;
}
.VPApiPreference.screen-menu {
margin: 12px 0 0 12px;
margin: 0.75rem 0 0 0.75rem;
font-size: 1rem;
}
.VPApiPreference input[type='radio'] {
@ -69,10 +78,10 @@ function removeSpaces(str: string) {
.VPApiPreference label {
flex: 1;
margin: 2px;
padding: 4px 12px;
margin: 0.125rem;
padding: 0.25rem 0.75rem;
cursor: pointer;
border-radius: 4px;
border-radius: 0.25rem;
text-align: center;
}

@ -1,5 +1,6 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import HomeHeroCopy from './HomeHeroCopy.vue'
const INSTALL_COMMAND = 'npx vitepress init'

@ -49,14 +49,7 @@
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<rect
height="14"
rx="2"
ry="2"
width="14"
x="8"
y="8"
/>
<rect height="14" rx="2" ry="2" width="14" x="8" y="8" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
<svg

@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vitepress'
import VPNavBarMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue'
import VPNavScreenMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue'
import VPNavMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavMenuGroup.vue'
import { computed } from 'vue'
const props = defineProps<{
versions: { text: string; link: string }[]
screenMenu?: boolean
menu?: boolean
}>()
const route = useRoute()
@ -26,15 +26,10 @@ const currentVersion = computed(() => {
</script>
<template>
<VPNavBarMenuGroup
v-if="!screenMenu"
<VPNavMenuGroup
:item="{ text: currentVersion, items: versions }"
class="VPNavVersion"
/>
<VPNavScreenMenuGroup
v-else
:text="currentVersion"
:items="versions"
:screen="screenMenu"
:menu="menu"
class="VPNavVersion"
/>
</template>

@ -1,7 +1,8 @@
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './components/CustomLayout.vue'
import ApiPreference from './components/ApiPreference.vue'
import CustomLayout from './components/CustomLayout.vue'
import NavVersion from './components/NavVersion.vue'
export default {

@ -1,4 +1,5 @@
import fs from 'node:fs'
import { defineLoader } from 'vitepress'
type Data = Record<string, boolean>[]

@ -1,9 +1,10 @@
import { defineRoutes } from 'vitepress'
import paths from './paths'
export default defineRoutes({
async paths(watchedFiles: string[]) {
// console.log('watchedFiles', watchedFiles)
async paths(_watchedFiles: string[]) {
// console.log('watchedFiles', _watchedFiles)
return paths
},
watch: ['../data-loading/**/*.json'],

@ -124,13 +124,15 @@ describe('Table of Contents', () => {
})
describe('Custom Containers', () => {
enum CustomBlocks {
Info = 'INFO',
Tip = 'TIP',
Warning = 'WARNING',
Danger = 'DANGER',
Details = 'Details'
}
const CustomBlocks = {
Info: 'INFO',
Tip: 'TIP',
Warning: 'WARNING',
Danger: 'DANGER',
Details: 'Details'
} as const
type CustomBlocks = (typeof CustomBlocks)[keyof typeof CustomBlocks]
const classnameMap = {
[CustomBlocks.Info]: 'info',

@ -15,7 +15,9 @@ describe('test multi sidebar sort root', () => {
'Data Loading',
'Multi Sidebar Test',
'Dynamic Routes',
'Markdown Extensions'
'Markdown Extensions',
'Team & Sponsors',
'Sidebar Hash'
])
})
})

@ -0,0 +1,239 @@
const ariaCurrent = (selector: string) =>
page.locator(selector).getAttribute('aria-current')
describe('navigation accessibility', () => {
beforeEach(async () => {
await page.setViewportSize({ width: 1280, height: 720 })
})
test('marks direct nav links to the current page', async () => {
await goto('/')
expect(await ariaCurrent('.VPNavBarMenuLink[href="/"]')).toBe('page')
await page.setViewportSize({ width: 375, height: 667 })
await page.locator('.VPNavBarHamburger').click()
expect(await ariaCurrent('.VPNavScreenMenuLink[href="/"]')).toBe('page')
})
test('marks nested nav links to the current page', async () => {
await goto('/home')
expect(await ariaCurrent('.VPMenuLink a[href="/home.html"]')).toBe('page')
expect(await ariaCurrent('.VPNavBarMenuLink[href="/"]')).toBeNull()
await page.setViewportSize({ width: 375, height: 667 })
await page.locator('.VPNavBarHamburger').click()
expect(
await ariaCurrent('.VPNavScreenMenuGroupLink[href="/home.html"]')
).toBe('page')
})
test('does not mark broad activeMatch links as current', async () => {
await goto('/home')
const sectionLink = page.locator(
'.VPNavBarMenuLink[href="/markdown-extensions/"]'
)
expect(await sectionLink.getAttribute('class')).toContain('active')
expect(await sectionLink.getAttribute('aria-current')).toBeNull()
})
test('marks only exact sidebar links, including fragments', async () => {
const overview = '.VPSidebarItem .link[href="/sidebar-hash/"]'
const sectionOne = '.VPSidebarItem .link[href="/sidebar-hash/#section-one"]'
const sectionTwo = '.VPSidebarItem .link[href="/sidebar-hash/#section-two"]'
await goto('/sidebar-hash/')
// wait for hydration to replace the hash-agnostic server-rendered state
await page.waitForFunction(
() => document.querySelectorAll('.VPSidebarItem.is-active').length === 1
)
expect(await ariaCurrent(overview)).toBe('page')
expect(await ariaCurrent(sectionOne)).toBeNull()
expect(await ariaCurrent(sectionTwo)).toBeNull()
await page.locator(sectionTwo).click()
await page.waitForSelector(`${sectionTwo}[aria-current="page"]`)
expect(await ariaCurrent(sectionOne)).toBeNull()
await page.locator(sectionOne).click()
await page.waitForSelector(`${sectionOne}[aria-current="page"]`)
expect(await ariaCurrent(sectionTwo)).toBeNull()
})
test('nav landmarks and controls have accessible names', async () => {
await goto('/')
expect(await page.locator('.VPNavBarMenu').getAttribute('aria-label')).toBe(
'Main Navigation'
)
await page.setViewportSize({ width: 375, height: 667 })
expect(
await page.locator('.VPNavBarHamburger').getAttribute('aria-label')
).toBe('Menu')
})
test('dropdown group is a keyboard-dismissible disclosure', async () => {
await goto('/')
const button = page.locator('.VPNavBarMenuGroup').first().locator('button')
expect(await button.getAttribute('aria-expanded')).toBe('false')
await button.click()
expect(await button.getAttribute('aria-expanded')).toBe('true')
await page.keyboard.press('Escape')
expect(await button.getAttribute('aria-expanded')).toBe('false')
// focus returned to the trigger
expect(
await page.evaluate(() => document.activeElement?.textContent)
).toContain('API Reference')
})
test('dropdown closes after navigating via a menu item', async () => {
await goto('/')
const group = page.locator('.VPNavBarMenuGroup').first()
await group.locator('button').click()
await group.locator('a[href="/home.html"]').click()
await page.waitForFunction(() => location.pathname.endsWith('/home.html'))
// the route watcher closes it on the post-navigation tick
await page.waitForSelector(
'.VPNavBarMenuGroup button[aria-expanded="false"]'
)
})
test('overflowing nav items move into the extra menu instead of clipping', async () => {
await goto('/')
// everything fits at 1280, so the ⋯ menu isn't rendered at all
expect(await page.locator('.VPNavBarExtra').count()).toBe(0)
// inflate the items so none of them can possibly fit
const style = await page.addStyleTag({
content: '.VPNavBarMenu .list > li > * { padding: 0 500px !important }'
})
await page.waitForSelector('.VPNavBarExtra')
// every control that stays in the bar remains fully within the viewport
// (no clipped/unreachable items — the failure mode of #2842)
expect(
await page.evaluate(() => {
const targets = [
...document.querySelectorAll('.VPNavBarMenu .list > li'),
document.querySelector('.VPNavBarSearch button'),
document.querySelector('.VPNavBarExtra > button')
].filter((el): el is HTMLElement => !!el)
return targets.every((el) => {
const rect = el.getBoundingClientRect()
return rect.left >= -1 && rect.right <= innerWidth + 1
})
})
).toBe(true)
// collapsed items and social links are reachable through the ⋯ menu
// (the scoped selector targets the moved "Home" item itself — the
// version-switcher component in the menu also links to "/")
await page.locator('.VPNavBarExtra > button').click()
await page.waitForSelector(
'.VPNavBarExtra .overflow-items > .VPMenuLink a[href="/"]'
)
await page.waitForSelector('.VPNavBarExtra .social-links')
// component items render menu-native: a titled group whose links are
// visible in place, not the screen accordion or a nested flyout
await page.waitForSelector(
'.VPNavBarExtra .overflow-items > .VPNavVersion .title'
)
expect(
await page.locator('.VPNavBarExtra .VPNavScreenMenuGroup').count()
).toBe(0)
// widening back restores the inline items and removes the ⋯ menu
await style.evaluate((el) => (el as HTMLStyleElement).remove())
await page.waitForSelector('.VPNavBarExtra', { state: 'detached' })
await page.waitForSelector('.VPNavBarMenuLink[href="/"]')
})
test('nav screen manages focus and inert state', async () => {
await page.setViewportSize({ width: 375, height: 667 })
await goto('/')
const hamburger = page.locator('.VPNavBarHamburger')
expect(await hamburger.getAttribute('aria-expanded')).toBe('false')
await hamburger.click()
await page.waitForSelector('#VPNavScreen')
expect(await hamburger.getAttribute('aria-expanded')).toBe('true')
// the covered page content is inert while the screen is open
expect(
await page.evaluate(() =>
document.getElementById('VPContent')!.hasAttribute('inert')
)
).toBe(true)
await page.keyboard.press('Escape')
await page.waitForSelector('#VPNavScreen', { state: 'detached' })
expect(
await page.evaluate(() =>
document.getElementById('VPContent')!.hasAttribute('inert')
)
).toBe(false)
// focus returned to the hamburger
expect(
await page.evaluate(() => document.activeElement?.className)
).toContain('VPNavBarHamburger')
})
test('screen menu group is a real disclosure', async () => {
await page.setViewportSize({ width: 375, height: 667 })
await goto('/')
await page.locator('.VPNavBarHamburger').click()
const group = page.locator('.VPNavScreenMenuGroup').first()
const button = group.locator('button').first()
expect(await button.getAttribute('aria-expanded')).toBe('false')
// collapsed content is hidden from view and the tab order
expect(await group.locator('a[href="/home.html"]').isVisible()).toBe(false)
await button.click()
expect(await button.getAttribute('aria-expanded')).toBe('true')
await page.waitForSelector('.VPNavScreenMenuGroup a[href="/home.html"]')
// aria-controls points at the panel it toggles
const controls = await button.getAttribute('aria-controls')
expect(await group.locator(`ul[id="${controls}"]`).count()).toBe(1)
})
test.runIf(process.env.VITE_TEST_BUILD)(
'omits aria-current for fragment links in server-rendered HTML',
async () => {
const response = await page.request.get(
`http://localhost:${process.env['PORT']}/sidebar-hash/`
)
const anchors = (
(await response.text()).match(/<a\b[^>]*>/g) ?? []
).filter((anchor) => anchor.includes('/sidebar-hash/'))
expect(
anchors.filter((anchor) => anchor.includes('#section-')).length
).toBeGreaterThanOrEqual(2)
expect(
anchors.filter((anchor) => anchor.includes('aria-current'))
).toEqual([expect.stringContaining('href="/sidebar-hash/"')])
}
)
})

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="32">
<rect width="96" height="32" rx="6" fill="#3c3c43" />
<text x="48" y="21" font-family="sans-serif" font-size="14" font-weight="600" fill="#ffffff" text-anchor="middle">SPONSOR</text>
</svg>

After

Width:  |  Height:  |  Size: 258 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<rect width="32" height="32" fill="#3451b2" />
<circle cx="16" cy="12" r="5" fill="#ffffff" />
<path d="M6 26c2-6 18-6 20 0z" fill="#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 221 B

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="128">
<rect width="192" height="128" fill="#18794e" />
<rect width="32" height="128" fill="#f9b44e" />
<rect x="160" width="32" height="128" fill="#f9b44e" />
<circle cx="96" cy="64" r="40" fill="#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 283 B

@ -0,0 +1,11 @@
# Sidebar Hash
A page whose sidebar entries point at fragments of the same page.
## Section One
Content for the first section.
## Section Two
Content for the second section.

@ -0,0 +1,36 @@
describe('sidebar', () => {
beforeAll(async () => {
await goto('/frontmatter/multiple-levels-outline')
})
test('collapsible group renders a heading and a single toggle button', async () => {
const group = page.locator('.VPSidebarItem.level-0.collapsible').first()
const caret = group.locator('.caret').first()
expect(await page.locator('.VPSidebarItem [role="button"]').count()).toBe(0)
expect(await caret.evaluate((el) => el.tagName)).toBe('BUTTON')
expect(await caret.getAttribute('aria-expanded')).toBe('true')
})
test('group toggles with keyboard, caret and heading', async () => {
const group = page.locator('.VPSidebarItem.level-0.collapsible').first()
const caret = group.locator('.caret').first()
const isCollapsed = () =>
group.evaluate((el) => el.classList.contains('collapsed'))
await caret.focus()
await page.keyboard.press('Enter')
expect(await isCollapsed()).toBe(true)
expect(await caret.getAttribute('aria-expanded')).toBe('false')
await page.keyboard.press('Space')
expect(await isCollapsed()).toBe(false)
expect(await caret.getAttribute('aria-expanded')).toBe('true')
await caret.click()
expect(await isCollapsed()).toBe(true)
await group.locator('.text').first().click()
expect(await isCollapsed()).toBe(false)
})
})

@ -0,0 +1,89 @@
import type { DefaultTheme } from 'vitepress/theme'
export const members: DefaultTheme.TeamMember[] = [
{
// smaller than the rendered avatar, checks that it still fills the circle
avatar: '/team-avatar-small.svg',
name: 'Alice Example',
title: 'Creator',
org: 'Example Org',
orgLink: 'https://example.com',
desc: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
links: [
{ icon: 'github', link: 'https://example.com' },
{ icon: 'x', link: 'https://example.com' }
],
sponsor: 'https://example.com'
},
{
// non-square, checks object-fit (side stripes must stay cropped out)
avatar: '/team-avatar-wide.svg',
name: 'Bob Example',
title: 'Maintainer',
links: [{ icon: 'github', link: 'https://example.com' }]
},
{
avatar: '/team-avatar-small.svg',
name: 'Carol Example',
title: 'Partner'
}
]
export const partners: DefaultTheme.TeamMember[] = [
{
avatar: '/team-avatar-wide.svg',
name: 'Dave Example',
title: 'Partner',
links: [{ icon: 'github', link: 'https://example.com' }]
},
{
avatar: '/team-avatar-small.svg',
name: 'Eve Example',
title: 'Partner'
},
{
avatar: '/team-avatar-wide.svg',
name: 'Frank Example',
title: 'Partner'
}
]
interface Sponsor {
name: string
img: string
url: string
}
const sponsor = (name: string): Sponsor => ({
name,
img: '/sponsor-logo.svg',
url: 'https://example.com'
})
export const sponsors: {
tier: string
size?: 'medium' | 'big'
items: Sponsor[]
}[] = [
{
tier: 'Platinum Sponsors',
size: 'big',
items: [sponsor('Sponsor One'), sponsor('Sponsor Two')]
},
{
tier: 'Gold Sponsors',
size: 'medium',
items: [
sponsor('Sponsor Three'),
sponsor('Sponsor Four'),
sponsor('Sponsor Five'),
sponsor('Sponsor Six')
]
}
]
export const friends: Sponsor[] = [
sponsor('Friend One'),
sponsor('Friend Two'),
sponsor('Friend Three')
]

@ -0,0 +1,3 @@
# Team & Sponsors in Doc Layout
<!--@include: ./parts/doc-page-body.md-->

@ -0,0 +1,30 @@
---
layout: home
title: Team & Sponsors in Home Layout (no markdown styles)
markdownStyles: false
hero:
name: Team & Sponsors
text: Home layout without markdown styles
tagline: Reference rendering of the components
actions:
- theme: brand
text: Doc Layout
link: ./doc
image:
src: /vitepress.png
alt: VitePress
features:
- icon: 📝
title: Feature A
details: Some details about feature A.
- icon: ⚡
title: Feature B
details: Some details about feature B.
- icon: 🚀
title: Feature C
details: Some details about feature C.
---
<!--@include: ./parts/home-body.md-->

@ -0,0 +1,29 @@
---
layout: home
title: Team & Sponsors in Home Layout
hero:
name: Team & Sponsors
text: Home layout with markdown styles
tagline: Compare with the no-markdown-styles variant
actions:
- theme: brand
text: Doc Layout
link: ./doc
image:
src: /vitepress.png
alt: VitePress
features:
- icon: 📝
title: Feature A
details: Some details about feature A.
- icon: ⚡
title: Feature B
details: Some details about feature B.
- icon: 🚀
title: Feature C
details: Some details about feature C.
---
<!--@include: ./parts/home-body.md-->

@ -0,0 +1,8 @@
---
layout: page
title: Team & Sponsors in Page Layout
---
# Team & Sponsors in Page Layout
<!--@include: ./parts/doc-page-body.md-->

@ -0,0 +1,42 @@
<script setup>
import { VPTeamMembers, VPSponsors } from 'vitepress/theme'
import { members, friends, sponsors } from './data'
</script>
Regular paragraph with a [link](https://example.com) and `inline code`.
## Small, One Member
<VPTeamMembers size="small" :members="members.slice(0, 1)" />
## Small, Two Members
<VPTeamMembers size="small" :members="members.slice(0, 2)" />
## Small, Three Members
<VPTeamMembers size="small" :members />
## Medium, One Member
<VPTeamMembers size="medium" :members="members.slice(0, 1)" />
## Medium, Two Members
<VPTeamMembers size="medium" :members="members.slice(0, 2)" />
## Medium, Three Members
<VPTeamMembers size="medium" :members />
## Sponsors, Tiered
<VPSponsors :data="sponsors" />
## Sponsors, Single Tier
<VPSponsors tier="Friends" size="mini" :data="friends" />
## After Components
Trailing markdown content after the components.

@ -0,0 +1,47 @@
<script setup>
import {
VPTeamPage,
VPTeamPageTitle,
VPTeamPageSection,
VPTeamMembers,
VPHomeSponsors
} from 'vitepress/theme'
import { members, partners, sponsors } from './data'
</script>
## Markdown Section
Some paragraph text with a [link](https://example.com) and `inline code` to
verify how markdown content is styled in this variant.
- List item one
- List item two
<VPTeamPage>
<VPTeamPageTitle>
<template #title>Our Team</template>
<template #lead>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt.
</template>
</VPTeamPageTitle>
<VPTeamMembers :members="members.slice(0, 2)" />
<VPTeamPageSection>
<template #title>Partners</template>
<template #lead>Lorem ipsum dolor sit amet.</template>
<template #members>
<VPTeamMembers size="small" :members="partners" />
</template>
</VPTeamPageSection>
</VPTeamPage>
<VPHomeSponsors
message="Made possible by our generous sponsors"
:data="sponsors"
action-text="Become a sponsor"
action-link="https://example.com"
/>
## After Components
Trailing markdown content after the components.

@ -1,5 +1,6 @@
import getPort from 'get-port'
import type { Server } from 'node:net'
import getPort from 'get-port'
import { chromium, type BrowserServer } from 'playwright-chromium'
import type { ViteDevServer } from 'vite'
import { build, createServer, serve } from 'vitepress'

@ -1,8 +1,9 @@
import getPort from 'get-port'
import { nanoid } from 'nanoid'
import { rm } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import getPort from 'get-port'
import { nanoid } from 'nanoid'
import { chromium } from 'playwright-chromium'
import { createServer, scaffold, ScaffoldThemeType } from 'vitepress'

@ -0,0 +1,135 @@
import {
computeNavFit,
type NavFitInput
} from 'client/theme-default/composables/nav-overflow'
function fit(input: Partial<NavFitInput>) {
return computeNavFit({
itemWidths: [],
translations: null,
appearance: null,
socialLinks: null,
available: 0,
extraWidth: 40,
...input
})
}
describe('client/theme-default/composables/nav-overflow', () => {
describe('computeNavFit', () => {
test('keeps everything when it fits', () => {
expect(
fit({
itemWidths: [100, 100],
translations: 50,
appearance: 60,
socialLinks: 90,
available: 400
})
).toEqual({
visibleItemCount: Infinity,
translations: true,
appearance: true,
socialLinks: true
})
})
test('collapses social links first', () => {
expect(
fit({
itemWidths: [100, 100],
translations: 50,
appearance: 60,
socialLinks: 90,
available: 390
})
).toEqual({
visibleItemCount: Infinity,
translations: true,
appearance: true,
socialLinks: false
})
})
test('collapses the cluster cascade in order', () => {
// items (200) + translations (50) fit in the 260 budget after
// reserving the extra button, appearance (60) does not — social links
// must follow appearance out even though they'd fit alone
expect(
fit({
itemWidths: [100, 100],
translations: 50,
appearance: 60,
socialLinks: 5,
available: 300
})
).toEqual({
visibleItemCount: Infinity,
translations: true,
appearance: false,
socialLinks: false
})
})
test('collapses menu items right-to-left after the cluster', () => {
expect(
fit({
itemWidths: [100, 100, 100],
translations: 50,
available: 250
})
).toEqual({
visibleItemCount: 2,
translations: false,
appearance: true,
socialLinks: true
})
})
test('ignores unconfigured cluster units', () => {
expect(
fit({
itemWidths: [100],
socialLinks: 90,
available: 150
})
).toEqual({
visibleItemCount: Infinity,
translations: true,
appearance: true,
socialLinks: false
})
})
test('collapses everything when nothing fits', () => {
expect(
fit({
itemWidths: [100, 100],
translations: 50,
available: 30
})
).toEqual({
visibleItemCount: 0,
translations: false,
// unconfigured units just stay "not collapsed"
appearance: true,
socialLinks: true
})
})
test('a lone overwide item still collapses instead of clipping', () => {
expect(fit({ itemWidths: [500], available: 400 }).visibleItemCount).toBe(
0
)
})
test('handles an empty nav', () => {
expect(fit({ socialLinks: 90, appearance: 60, available: 80 })).toEqual({
visibleItemCount: Infinity,
translations: true,
appearance: false,
socialLinks: false
})
})
})
})

@ -1,9 +1,10 @@
import { resolveConfig } from 'node/config'
import { createContentLoader } from 'node/contentLoader'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { resolveConfig } from 'node/config'
import { createContentLoader } from 'node/contentLoader'
describe('node/contentLoader', () => {
let root: string | undefined

@ -1,6 +1,7 @@
import path from 'node:path'
import { MarkdownItAsync } from 'markdown-it-async'
import { attrs as attrsPlugin } from '@mdit/plugin-attrs'
import { MarkdownItAsync } from 'markdown-it-async'
import { imagePlugin, type Options } from 'node/markdown/plugins/image'
const srcDir = path.resolve(import.meta.dirname, '../../../../e2e')

@ -1,6 +1,7 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
createMarkdownRenderer,
disposeMdItInstance,

@ -1,6 +1,7 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
createMarkdownRenderer,
disposeMdItInstance,

@ -1,9 +1,10 @@
import { resolveConfig } from 'node/config'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { resolveConfig } from 'node/config'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
describe('node/markdownToVue', () => {
let root: string | undefined

@ -1,11 +1,12 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import MiniSearch from 'minisearch'
import { resolveConfig } from 'node/config'
import { disposeMdItInstance } from 'node/markdown/markdown'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
import { localSearchPlugin } from 'node/plugins/localSearchPlugin'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
describe('node/plugins/localSearchPlugin', () => {
let root: string | undefined
@ -46,7 +47,7 @@ describe('node/plugins/localSearchPlugin', () => {
'# Chinese home\n\nlocaleonlytoken\n'
)
await writeFile(
path.join(configDir, 'config.ts'),
path.join(configDir, 'config.mjs'),
[
'export default {',
' rewrites: {',
@ -120,7 +121,7 @@ describe('node/plugins/localSearchPlugin', () => {
`# Chinese home\n\n${content}`
)
await writeFile(
path.join(configDir, 'config.ts'),
path.join(configDir, 'config.mjs'),
[
'export default {',
' locales: {',
@ -195,7 +196,7 @@ describe('node/plugins/localSearchPlugin', () => {
'# Broken\n\n<!-- @include: ./missing.md -->\n'
)
await writeFile(
path.join(configDir, 'config.ts'),
path.join(configDir, 'config.mjs'),
"export default { themeConfig: { search: { provider: 'local' } } }"
)

@ -1,6 +1,7 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { readFile, readTextFile, readTextFileSync } from 'node/utils/fs'
describe('node/utils/fs', () => {

@ -0,0 +1,56 @@
import { mergeHead, type HeadConfig } from 'shared/shared'
describe('shared/shared', () => {
describe('mergeHead', () => {
test('replaces meta tags with the same key in place', () => {
expect(
mergeHead(
[
['meta', { property: 'og:image', content: '/site.png' }],
['meta', { name: 'keywords', content: 'site' }]
],
[['meta', { content: '/page.png', property: 'og:image' }]]
)
).toEqual([
['meta', { content: '/page.png', property: 'og:image' }],
['meta', { name: 'keywords', content: 'site' }]
])
})
test('ignores content when keying meta tags', () => {
const head: HeadConfig[] = [
['meta', { content: 'a', name: 'name1' }],
['meta', { content: 'a', name: 'name2' }]
]
expect(mergeHead(head)).toEqual(head)
})
test('keys any element by id regardless of attribute order', () => {
expect(
mergeHead(
[
['meta', { name: 'author', content: 'a', id: 'author-a' }],
['meta', { name: 'author', content: 'b', id: 'author-b' }],
['script', { id: 'sw' }, 'old']
],
[
['meta', { id: 'author-a', name: 'author', content: 'c' }],
['script', { id: 'sw' }, 'new']
]
)
).toEqual([
['meta', { id: 'author-a', name: 'author', content: 'c' }],
['meta', { name: 'author', content: 'b', id: 'author-b' }],
['script', { id: 'sw' }, 'new']
])
})
test('appends elements without a key', () => {
const head: HeadConfig[] = [
['link', { rel: 'stylesheet', href: '/a.css' }],
['link', { rel: 'stylesheet', href: '/a.css' }]
]
expect(mergeHead(head, head)).toEqual([...head, ...head])
})
})
})

@ -1,6 +1,7 @@
import vue from '@vitejs/plugin-vue'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vitest/config'
const dir = dirname(fileURLToPath(import.meta.url))

@ -9,13 +9,14 @@ import {
localIconLoader
} from 'vitepress-plugin-group-icons'
import llmstxt from 'vitepress-plugin-llms'
import { markdown as esMarkdown } from '../es/config'
import { markdown as faMarkdown } from '../fa/config'
import { markdown as jaMarkdown } from '../ja/config'
import { markdown as koMarkdown } from '../ko/config'
import { markdown as ptMarkdown } from '../pt/config'
import { markdown as ruMarkdown } from '../ru/config'
import { markdown as zhMarkdown } from '../zh/config'
import { markdown as esMarkdown } from '../es/config.ts'
import { markdown as faMarkdown } from '../fa/config.ts'
import { markdown as jaMarkdown } from '../ja/config.ts'
import { markdown as koMarkdown } from '../ko/config.ts'
import { markdown as ptMarkdown } from '../pt/config.ts'
import { markdown as ruMarkdown } from '../ru/config.ts'
import { markdown as zhMarkdown } from '../zh/config.ts'
const prod = !!process.env.NETLIFY
const siteUrl = 'https://vitepress.dev'

@ -1,4 +1,7 @@
/// <reference types="vitepress/client" />
import Theme from 'vitepress/theme'
import 'virtual:group-icons.css'
import './styles.css'

@ -40,13 +40,13 @@
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
@media (min-width: 40rem) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
@media (min-width: 60rem) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
@ -54,7 +54,7 @@
.VPHero .VPImage {
filter: drop-shadow(-2px 4px 6px rgba(0, 0, 0, 0.2));
padding: 18px;
padding: 1.125rem;
}
.VPFeature .icon span {

@ -38,23 +38,23 @@ const showModal = ref(false)
}
.modal-container {
width: 300px;
width: 18.75rem;
margin: auto;
padding: 20px 30px;
padding: 1.25rem 1.875rem;
background-color: var(--vp-c-bg);
border-radius: 2px;
border-radius: 0.125rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.model-footer {
margin-top: 8px;
margin-top: 0.5rem;
text-align: right;
}
.modal-button {
padding: 4px 8px;
border-radius: 4px;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);

@ -1,8 +1,5 @@
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export default defineAdditionalConfig({
description: 'Vite & Vue powered static site generator.',
@ -40,7 +37,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -38,7 +38,12 @@ interface Theme {
*/
enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>
/**
* Extend another theme, calling its `enhanceApp` before ours
* Runs inside the root component's `setup()`
* @optional
*/
setup?: () => void
/**
* Extend another theme, calling its `enhanceApp` and `setup` before ours
* @optional
*/
extends?: Theme
@ -88,6 +93,26 @@ export default {
Return `false` from `onBeforeRouteChange` or `onBeforePageLoad` to cancel navigation.
The `setup` hook runs inside the root component's `setup()`, so Composition API calls (`onMounted`, `watch`, composables, ...) work there without wrapping the layout component:
```ts [.vitepress/theme/index.ts]
import { watch } from 'vue'
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
export default {
extends: DefaultTheme,
setup() {
const { page } = useData()
watch(() => page.value.relativePath, (path) => {
console.log('now viewing', path)
})
}
}
```
With `extends`, each theme's `setup` runs base-first, like `enhanceApp`. It also runs during SSR/SSG rendering, so keep browser-only work inside `onMounted`.
The default export is the only contract for a custom theme, and only the `Layout` property is required. So technically, a VitePress theme can be as simple as a single Vue component.
Inside your layout component, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./ssr-compat).

@ -292,62 +292,77 @@ You can deploy your VitePress project with [CloudRay](https://cloudray.io/) by f
You can deploy your VitePress project with [Hostinger](https://www.hostinger.com/web-apps-hosting) by following these [instructions](https://www.hostinger.com/support/how-to-deploy-a-nodejs-website-in-hostinger/). While configuring build settings, choose VitePress as the framework and adjust the root directory to `./docs`.
### Kinsta
You can deploy your VitePress website on [Kinsta](https://kinsta.com/static-site-hosting/) by following these [instructions](https://kinsta.com/docs/vitepress-static-site-example/).
### Stormkit
You can deploy your VitePress project to [Stormkit](https://www.stormkit.io) by following these [instructions](https://stormkit.io/blog/how-to-deploy-vitepress).
### Surge
1. After running `npm run docs:build`, run this command to deploy:
After running `npm run docs:build`, run this command to deploy to [Surge](https://surge.sh):
```sh
npx surge docs/.vitepress/dist
```
### Nginx
### harvis
After running `npm run docs:build`, run this command to deploy to [harvis](https://harvis.dev):
```sh
npx harvis docs/.vitepress/dist
```
### nginx
Here is a example of an Nginx server block configuration. This setup includes gzip compression for common text-based assets, rules for serving your VitePress site's static files with proper caching headers as well as handling `cleanUrls: true`.
Here is a example of an nginx server block configuration. This setup includes gzip compression for common text-based assets, rules for serving your VitePress site's static files with proper caching headers as well as handling `cleanUrls: true`.
```nginx
server {
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
map $uri $cache_control {
~^/assets/ "public, max-age=31536000, immutable";
default "no-cache";
}
listen 80;
server {
listen 8080;
listen [::]:8080;
server_name _;
index index.html;
location / {
# content location
root /app;
root /usr/share/nginx/html;
index index.html;
charset utf-8;
server_tokens off;
# exact matches -> reverse clean urls -> folders -> not found
try_files $uri $uri.html $uri/ =404;
absolute_redirect off;
# non existent pages
error_page 404 /404.html;
gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types
application/javascript
application/json
application/manifest+json
image/svg+xml
text/css
text/javascript
text/plain;
add_header Cache-Control $cache_control always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# a folder without index.html raises 403 in this setup
error_page 403 /404.html;
location / {
try_files $uri $uri.html $uri/index.html =404;
}
# adjust caching headers
# files in the assets folder have hashes filenames
location ~* ^/assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
location ~ ^(?<page>.+)/$ {
if (-f $document_root$page.html) {
return 301 $page$is_args$args;
}
try_files $page/index.html =404;
}
error_page 404 /404.html;
}
```
This configuration assumes that your built VitePress site is located in the `/app` directory on your server. Adjust the `root` directive accordingly if your site's files are located elsewhere.
::: warning Do not default to index.html
The try_files resolution must not default to index.html like in other Vue applications. This would result in an invalid page state.
:::
Further information can be found in the [official nginx documentation](https://nginx.org/en/docs/), in these issues [#2837](https://github.com/vuejs/vitepress/discussions/2837), [#3235](https://github.com/vuejs/vitepress/issues/3235) as well as in this [blog post](https://blog.mehdi.cc/articles/vitepress-cleanurls-on-nginx-environment#readings) by Mehdi Merah.

@ -40,6 +40,46 @@ export default DefaultTheme
See [default theme CSS variables](https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css) that can be overridden.
### Navbar
The navbar draws a single background surface controlled by CSS variables, so its look can be changed without touching component internals:
```css
:root {
/* bar height and background */
--vp-nav-height: 4rem;
--vp-nav-bg-color: var(--vp-c-bg);
/* background while on top of the home page (unscrolled);
set to var(--vp-nav-bg-color) to opt out of the transparent treatment */
--vp-nav-home-bg-color: transparent;
/* filter applied to the content behind the bar */
--vp-nav-backdrop-filter: none;
/* the bar's bottom rule and the mobile menu background */
--vp-nav-divider-color: var(--vp-c-gutter);
--vp-nav-screen-bg-color: var(--vp-c-bg);
}
```
For example, a frosted-glass navbar:
```css
:root {
--vp-nav-bg-color: color-mix(in srgb, var(--vp-c-bg) 65%, transparent);
--vp-nav-backdrop-filter: saturate(180%) blur(8px);
}
```
The same treatment carries over to the local nav: `--vp-local-nav-bg-color` follows the navbar surface color by default, and where the two bars meet they share a single blurred surface, so the glass stays continuous across them.
::: warning
`backdrop-filter` has a measurable scroll performance cost, especially on large or high-DPI screens. When using a translucent bar, also check text contrast over your page content. Safari 17 and earlier don't apply variable-driven backdrop filters, so they show the translucent color without the blur.
:::
When the nav items don't fit the available width, they move into the `⋯` menu at the end of the navbar instead of being clipped, starting with the social links, the appearance switch and the locale switcher, followed by the nav items right-to-left. Its button label can be localized with [`extraMenuLabel`](../reference/default-theme-config#extramenulabel).
## Using Different Fonts
VitePress uses [Inter](https://rsms.me/inter/) as the default font, and will include the fonts in the build output. The font is also auto preloaded in production. However, this may not be desirable if you want to use a different main font.

@ -445,6 +445,8 @@ VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-
> [!CAUTION]
> Negative potential consequences of an action.
By default, alert colors match GitHub's, with caution and danger both rendering in red. Enable [`themeConfig.gradedContainers`](../reference/default-theme-config#gradedcontainers) to use a graded severity scale: danger (red), warning (orange), and caution (yellow). Note that `[!DANGER]` is a VitePress extension and will render as a regular blockquote on GitHub.
## Syntax Highlighting in Code Blocks
VitePress uses [Shiki](https://github.com/shikijs/shiki) to highlight language syntax in Markdown code blocks, using coloured text. Shiki supports a wide variety of programming languages. All you need to do is append a valid language alias to the beginning backticks for the code block:

@ -14,7 +14,7 @@ You may use the `Badge` component which is globally available.
### Title <Badge type="info" text="default" />
### Title <Badge type="tip" text="^1.9.0" />
### Title <Badge type="warning" text="beta" />
### Title <Badge type="danger" text="caution" />
### Title <Badge type="danger" text="deprecated" />
```
Code above renders like:
@ -22,7 +22,7 @@ Code above renders like:
### Title <Badge type="info" text="default" />
### Title <Badge type="tip" text="^1.9.0" />
### Title <Badge type="warning" text="beta" />
### Title <Badge type="danger" text="caution" />
### Title <Badge type="danger" text="deprecated" />
## Custom Children
@ -44,9 +44,21 @@ You can customize the style of badges by overriding css variables. The following
--vp-badge-info-text: var(--vp-c-text-2);
--vp-badge-info-bg: var(--vp-c-default-soft);
--vp-badge-note-border: transparent;
--vp-badge-note-text: var(--vp-c-note-1);
--vp-badge-note-bg: var(--vp-c-note-soft);
--vp-badge-tip-border: transparent;
--vp-badge-tip-text: var(--vp-c-brand-1);
--vp-badge-tip-bg: var(--vp-c-brand-soft);
--vp-badge-tip-text: var(--vp-c-tip-1);
--vp-badge-tip-bg: var(--vp-c-tip-soft);
--vp-badge-important-border: transparent;
--vp-badge-important-text: var(--vp-c-important-1);
--vp-badge-important-bg: var(--vp-c-important-soft);
--vp-badge-caution-border: transparent;
--vp-badge-caution-text: var(--vp-c-caution-1);
--vp-badge-caution-bg: var(--vp-c-caution-soft);
--vp-badge-warning-border: transparent;
--vp-badge-warning-text: var(--vp-c-warning-1);
@ -67,7 +79,7 @@ interface Props {
// When `<slot>` is passed, this value gets ignored.
text?: string
// Defaults to `tip`.
type?: 'info' | 'tip' | 'warning' | 'danger'
// Defaults to `tip`. Matches markdown containers/alerts colors.
type?: 'info' | 'note' | 'tip' | 'important' | 'caution' | 'warning' | 'danger'
}
```

@ -470,6 +470,27 @@ Can be used to customize the label of the return to top button. This label is on
Can be used to customize the aria-label of the language toggle button in navbar. This is only used if you're using [i18n](../guide/i18n).
## navMenuLabel
- Type: `string`
- Default: `Main Navigation`
Can be used to customize the accessible label of the main navigation landmarks (the navbar menu and the mobile menu).
## mobileMenuLabel
- Type: `string`
- Default: `Menu`
Can be used to customize the aria-label of the mobile menu (hamburger) button.
## extraMenuLabel
- Type: `string`
- Default: `More options`
Can be used to customize the aria-label of the `⋯` menu button in the navbar. That menu collects the nav items and controls that don't fit in the bar at the current viewport size.
## skipToContentLabel
- Type: `string`
@ -484,6 +505,13 @@ Can be used to customize the label of the skip to content link. This link is sho
Whether to show an external link icon next to external links in markdown.
## gradedContainers
- Type: `boolean`
- Default: `false`
Whether to color [custom containers](../guide/markdown#custom-containers), [GitHub-flavored alerts](../guide/markdown#github-flavored-alerts), and badges on a graded severity scale — danger red, warning orange, caution yellow. By default, colors match GitHub's alerts, where caution shares danger's red and warning is yellow.
## `useLayout` <Badge type="info" text="composable" />
Returns layout-related data. The returned object has the following type:

@ -216,5 +216,6 @@ export default {
Your component will be rendered in the navigation bar. VitePress will provide the following additional props to the component:
- `screenMenu`: an optional boolean indicating whether the component is inside mobile navigation menu
- `menu`: an optional boolean indicating whether the component is inside a dropdown panel — for example, the `⋯` menu that nav items collapse into when they don't fit the bar. In both these contexts, render a flat list instead of a floating flyout, which would end up nested inside the panel
You can check an example in the e2e tests [here](https://github.com/vuejs/vitepress/tree/main/__tests__/e2e/.vitepress).

@ -27,10 +27,11 @@ Example result:
Alternatively, you can use [Algolia DocSearch](#algolia-search) or some community plugins like:
- <https://www.npmjs.com/package/vitepress-plugin-search>
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
- <https://www.npmjs.com/package/vitepress-plugin-typesense>
- <https://npmx.dev/package/vitepress-plugin-pagefind>
- <https://npmx.dev/package/vitepress-plugin-typesense>
- <https://npmx.dev/package/vitepress-plugin-cloudflare-ai-search>
<!-- - <https://npmx.dev/package/@orama/plugin-vitepress> -- replace with zbsearch one when published -->
### i18n {#local-search-i18n}

@ -63,7 +63,7 @@ description: VitePress
- Type: `HeadConfig[]`
Specify extra head tags to be injected for the current page. Will be appended after head tags injected by site-level config.
Specify extra head tags to be injected for the current page. They are [merged](./site-config#head) with the head tags injected by site-level config.
```yaml
---

@ -248,6 +248,13 @@ type HeadConfig =
| [string, Record<string, string>, string]
```
Head entries from the site config, [locale config](../guide/i18n), [directory-level config](#directory-level-overrides), [frontmatter](./frontmatter-config#head) and [`transformHead`](#transformhead) are merged in that order. A later entry replaces an earlier one with the same key instead of being appended:
- Any element with an `id` attribute is keyed by its `id`.
- A `meta` element without an `id` is keyed by its first attribute other than `content` (e.g. `name`, `property`, `http-equiv`) and that attribute's value.
Other elements are never deduplicated. To render multiple `meta` tags that would share a key, like several `<meta name="author">`, give each of them a unique `id`.
#### Example: Adding a favicon
```ts

@ -1,12 +1,9 @@
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export const markdown: MarkdownLocaleOptions = {
container: {
@ -92,7 +89,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/es/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -25,7 +25,7 @@ Resultado de ejemplo:
![captura de pantalla del modo de búsqueda](/search.png)
Alternativamente, puedes usar [Algolia DocSearch](#algolia-search) o algunos complementos comunitarios como <https://www.npmjs.com/package/vitepress-plugin-search> o <https://www.npmjs.com/package/vitepress-plugin-pagefind>.
Alternativamente, puedes usar [Algolia DocSearch](#algolia-search) o algunos complementos comunitarios como <https://www.npmjs.com/package/vitepress-plugin-search>, <https://www.npmjs.com/package/vitepress-plugin-pagefind> o <https://www.npmjs.com/package/vitepress-plugin-cloudflare-ai-search>.
### i18n {#local-search-i18n}

@ -1,12 +1,9 @@
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export const markdown: MarkdownLocaleOptions = {
container: {
@ -100,7 +97,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -25,7 +25,7 @@ export default defineConfig({
![تصویر نمایشی از مودال جستجو](/search.png)
همچنین، می‌توانید از [Algolia DocSearch](#algolia-search) یا برخی افزونه‌های جامعه‌ای مانند <https://www.npmjs.com/package/vitepress-plugin-search> یا <https://www.npmjs.com/package/vitepress-plugin-pagefind> استفاده کنید.
همچنین، می‌توانید از [Algolia DocSearch](#algolia-search) یا برخی افزونه‌های جامعه‌ای مانند <https://www.npmjs.com/package/vitepress-plugin-search>، <https://www.npmjs.com/package/vitepress-plugin-pagefind> یا <https://www.npmjs.com/package/vitepress-plugin-cloudflare-ai-search> استفاده کنید.
### بین‌المللی‌سازی {#local-search-i18n}

@ -1,12 +1,9 @@
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export const markdown: MarkdownLocaleOptions = {
container: {
@ -63,7 +60,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -31,6 +31,7 @@ export default defineConfig({
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
- <https://www.npmjs.com/package/vitepress-plugin-typesense>
- <https://www.npmjs.com/package/vitepress-plugin-cloudflare-ai-search>
### i18n {#local-search-i18n}

@ -1,12 +1,9 @@
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export const markdown: MarkdownLocaleOptions = {
container: {
@ -92,7 +89,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/ko/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -25,7 +25,7 @@ export default defineConfig({
![검색 모달의 스크린샷](/search.png)
대안으로 [Algolia DocSearch](#algolia-search), <https://www.npmjs.com/package/vitepress-plugin-search>, <https://www.npmjs.com/package/vitepress-plugin-pagefind>와 같은 커뮤니티 플러그인을 사용할 수도 있습니다.
대안으로 [Algolia DocSearch](#algolia-search), <https://www.npmjs.com/package/vitepress-plugin-search>, <https://www.npmjs.com/package/vitepress-plugin-pagefind>, <https://www.npmjs.com/package/vitepress-plugin-cloudflare-ai-search>와 같은 커뮤니티 플러그인을 사용할 수도 있습니다.
### i18n {#local-search-i18n}

@ -1,12 +1,9 @@
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export const markdown: MarkdownLocaleOptions = {
container: {
@ -92,7 +89,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/pt/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -25,7 +25,7 @@ Exemplo de resultado:
![captura de tela do modal de pesquisa](/search.png)
Alternativamente, você pode usar [Algolia DocSearch](#algolia-search) ou alguns plugins da comunidade como <https://www.npmjs.com/package/vitepress-plugin-search> ou <https://www.npmjs.com/package/vitepress-plugin-pagefind>.
Alternativamente, você pode usar [Algolia DocSearch](#algolia-search) ou alguns plugins da comunidade como <https://www.npmjs.com/package/vitepress-plugin-search>, <https://www.npmjs.com/package/vitepress-plugin-pagefind> ou <https://www.npmjs.com/package/vitepress-plugin-cloudflare-ai-search>.
### i18n {#local-search-i18n}

@ -40,23 +40,23 @@ const showModal = ref(false)
}
.modal-container {
width: 300px;
width: 18.75rem;
margin: auto;
padding: 20px 30px;
padding: 1.25rem 1.875rem;
background-color: var(--vp-c-bg);
border-radius: 2px;
border-radius: 0.125rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.model-footer {
margin-top: 8px;
margin-top: 0.5rem;
text-align: right;
}
.modal-button {
padding: 4px 8px;
border-radius: 4px;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);

@ -1,12 +1,9 @@
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export const markdown: MarkdownLocaleOptions = {
container: {
@ -90,7 +87,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/ru/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -31,6 +31,7 @@ export default defineConfig({
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
- <https://www.npmjs.com/package/vitepress-plugin-typesense>
- <https://www.npmjs.com/package/vitepress-plugin-cloudflare-ai-search>
### i18n {#local-search-i18n}

@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"noEmit": true,
"types": ["node"]
},

@ -1,12 +1,9 @@
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
import { version } from 'vitepress/package.json' with { type: 'json' }
export const markdown: MarkdownLocaleOptions = {
container: {
@ -92,7 +89,7 @@ function nav(): DefaultTheme.NavItem[] {
activeMatch: '/zh/reference/'
},
{
text: pkg.version,
text: version,
items: [
{
text: '1.6.4',

@ -779,7 +779,7 @@ export default config
<!--@@include: ./parts/basics.md-->
```
**Part file** (`parts/basics.md`)
**部分文件** (`parts/basics.md`)
```md
Some getting started stuff.
@ -815,7 +815,7 @@ Can be created using `.foorc.json`.
<!--@@include: ./parts/basics.md{3,}-->
```
**Part file** (`parts/basics.md`)
**部分文件** (`parts/basics.md`)
```md
Some getting started stuff.

@ -25,7 +25,7 @@ export default defineConfig({
![搜索弹窗截图](/search.png)
或者,你可以使用 [Algolia DocSearch](#algolia-search) 或一些社区插件,例如:<https://www.npmjs.com/package/vitepress-plugin-search> 或者 <https://www.npmjs.com/package/vitepress-plugin-pagefind>
或者,你可以使用 [Algolia DocSearch](#algolia-search) 或一些社区插件,例如:<https://www.npmjs.com/package/vitepress-plugin-search><https://www.npmjs.com/package/vitepress-plugin-pagefind> 或者 <https://www.npmjs.com/package/vitepress-plugin-cloudflare-ai-search>
### i18n {#local-search-i18n}

@ -52,19 +52,12 @@
"lib"
],
"scripts": {
"clean": "node -e \"require('node:fs').rmSync('./dist',{recursive:!0,force:!0,maxRetries:10})\"",
"dev": "pnpm clean && pnpm dev:shared && pnpm dev:start",
"dev:start": "pnpm --stream '/^dev:(client|node|watch)$/'",
"dev:client": "tsc --sourcemap -w --preserveWatchOutput -p src/client",
"dev:node": "DEV=true pnpm build:node -w",
"dev:shared": "node scripts/copyShared.ts",
"dev:watch": "node scripts/watchAndCopy.ts",
"build": "pnpm build:prepare && pnpm build:client && pnpm build:node && node scripts/genWebTypes.ts",
"build:prepare": "pnpm clean && node scripts/copyShared.ts",
"build:client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient.ts",
"build:node": "tsc -p src/node --noEmit && rollup --config rollup.config.ts --configPlugin esbuild",
"dev": "tsdown --watch --sourcemap",
"build": "tsdown && pnpm typecheck && node scripts/genWebTypes.ts && pnpm build:check",
"build:check": "publint && attw --pack . --profile esm-only",
"typecheck": "tsc -p tsconfig.shared.json && vue-tsc -p tsconfig.client.json && tsc -p tsconfig.node.json",
"test": "pnpm --aggregate-output --reporter=append-only '/^test:(types|unit|e2e|init)$/'",
"test:types": "tsc -p __tests__/unit && tsc -p __tests__/e2e && tsc -p __tests__/init && vue-tsc -p docs",
"test:types": "tsc -p __tests__/unit && vue-tsc -p __tests__/e2e && tsc -p __tests__/init && vue-tsc -p docs",
"test:unit": "vitest run -r __tests__/unit",
"test:unit:watch": "vitest -r __tests__/unit",
"test:e2e": "pnpm test:e2e-dev && pnpm test:e2e-build",
@ -101,22 +94,20 @@
"@docsearch/css": "^4.7.0",
"@docsearch/js": "^4.7.0",
"@docsearch/sidepanel-js": "^4.7.0",
"@iconify-json/simple-icons": "^1.2.92",
"@shikijs/core": "^4.4.1",
"@shikijs/transformers": "^4.4.1",
"@shikijs/types": "^4.4.1",
"@iconify-json/simple-icons": "^1.2.93",
"@shikijs/transformers": "^4.4.3",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.8",
"@vue/devtools-api": "^8.2.1",
"@vue/shared": "^3.5.40",
"@vue/shared": "^3.5.41",
"@vueuse/core": "^14.4.0",
"@vueuse/integrations": "^14.4.0",
"focus-trap": "^8.2.2",
"mark.js": "8.11.1",
"minisearch": "^7.2.0",
"shiki": "^4.4.1",
"vite": "^8.2.0",
"vue": "^3.5.40"
"shiki": "^4.4.3",
"vite": "^8.2.1",
"vue": "^3.5.41"
},
"devDependencies": {
"@clack/prompts": "^1.7.0",
@ -134,25 +125,21 @@
"@mdit/plugin-emoji": "^1.1.1",
"@mdit/plugin-footnote": "^1.0.2",
"@mdit/plugin-tasklist": "^1.0.2",
"@arethetypeswrong/cli": "^0.18.5",
"@polka/compression": "^1.0.0-next.28",
"@rolldown/pluginutils": "^1.0.1",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@types/cross-spawn": "^6.0.6",
"@types/lodash.template": "^4.5.3",
"@types/mark.js": "^8.11.12",
"@types/minimist": "^1.2.5",
"@types/node": "^26.1.2",
"@types/node": "^26.2.0",
"@types/picomatch": "^4.0.3",
"@types/semver": "^7.7.1",
"chokidar": "^5.0.0",
"conventional-changelog": "^8.1.0",
"@types/semver": "^7.8.0",
"@volar/typescript": "^2.4.28",
"@vue/language-core": "^3.3.11",
"conventional-changelog": "^8.1.1",
"conventional-changelog-angular": "^9.2.1",
"cross-spawn": "^7.0.6",
"esbuild": "^0.27.7",
"get-port": "^7.2.0",
"gray-matter": "^4.0.3",
"image-size": "^2.0.2",
@ -164,7 +151,7 @@
"markdown-it-cjk-friendly": "^2.0.2",
"markdown-it-mathjax3": "^4.3.2",
"minimist": "^1.2.8",
"nanoid": "^6.0.0",
"nanoid": "^6.0.1",
"obug": "^2.1.4",
"ora": "^9.4.1",
"p-map": "^7.0.6",
@ -175,20 +162,20 @@
"playwright-chromium": "^1.62.1",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.4",
"postcss-selector-parser": "^7.1.5",
"prettier": "^3.9.6",
"punycode": "^2.3.1",
"rollup": "^4.62.3",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"publint": "^0.3.24",
"rolldown": "^1.2.5",
"semver": "^7.8.5",
"simple-git-hooks": "^2.13.1",
"sirv": "^3.0.2",
"sitemap": "^9.0.1",
"tinyglobby": "^0.2.17",
"typescript": "^5.9.3",
"tsdown": "^0.22.14",
"typescript": "^6.0.3",
"vitest": "^4.1.10",
"vue-tsc": "^3.3.9",
"vue-sfc-transformer": "^0.2.5",
"vue-tsc": "^3.3.11",
"wait-on": "^9.1.0"
},
"peerDependencies": {
@ -203,5 +190,5 @@
"optional": true
}
},
"packageManager": "pnpm@11.18.0"
"packageManager": "pnpm@11.21.0"
}

@ -0,0 +1,18 @@
diff --git a/dist/rolldown.mjs b/dist/rolldown.mjs
index 7e04d96c703656b81220193b1f01426778b6a42f..26e96378fc0ad985237302e673ed0ab07674bf41 100644
--- a/dist/rolldown.mjs
+++ b/dist/rolldown.mjs
@@ -196,10 +196,11 @@ function resolveCache(options) {
async function transpileScript(code, filename = "__sfc.ts") {
const result = await transform(filename, code, {
lang: "ts",
- sourcemap: false
+ sourcemap: false,
+ typescript: { onlyRemoveTypeImports: true }
});
if (result.errors.length) throw new AggregateError(result.errors, `[vue-sfc-transformer] failed to transpile script in ${filename}`);
- return result.code ?? code;
+ return (result.code ?? code).replace(/\n?export \{\};?[\s\n]*$/, "");
}
function vueSfcPlugin(pluginOptions) {
const cwd = pluginOptions.cwd ?? process.cwd();

File diff suppressed because it is too large Load Diff

@ -3,7 +3,6 @@ packages:
- __tests__/*
allowBuilds:
esbuild: true
playwright-chromium: true
simple-git-hooks: true
@ -12,11 +11,12 @@ autoInstallPeers: false
ignoreWorkspaceRootCheck: true
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- '@mdit/*'
- '@vueuse/*'
overrides:
esbuild: '-'
patchedDependencies:
vue-sfc-transformer: patches/vue-sfc-transformer.patch
vitepress-plugin-group-icons: patches/vitepress-plugin-group-icons.patch
shellEmulator: true

@ -1,115 +0,0 @@
import alias from '@rollup/plugin-alias'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import { rm } from 'node:fs/promises'
import { builtinModules, createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { type RollupOptions, defineConfig } from 'rollup'
import dts from 'rollup-plugin-dts'
import esbuild from 'rollup-plugin-esbuild'
const require = createRequire(import.meta.url)
const pkg = require('./package.json')
const DEV = !!process.env.DEV
const PROD = !DEV
const external = [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies),
...builtinModules.flatMap((m) =>
m.includes('punycode') ? [] : [m, `node:${m}`]
)
]
const plugins = [
alias({ entries: { 'readable-stream': 'stream' } }),
replace({
// polyfill broken browser check from bundled deps
'navigator.userAgentData': 'undefined',
'navigator.userAgent': 'undefined',
preventAssignment: true
}),
commonjs(),
nodeResolve({ preferBuiltins: false }),
esbuild({ target: 'node22' }),
json()
]
const esmBuild: RollupOptions = {
input: ['src/node/index.ts', 'src/node/cli.ts'],
output: {
format: 'esm',
entryFileNames: `[name].js`,
chunkFileNames: 'chunk-[hash].js',
dir: 'dist/node',
sourcemap: DEV
},
external,
plugins,
onwarn(warning, warn) {
if (warning.code !== 'EVAL') warn(warning)
}
}
// keep .d.ts files under the repo root (e.g. types/*) external so module
// augmentations in the bundle still target the same files users reference.
// compared on normalized resolved paths so this works regardless of the
// checkout location, path separators, or drive-letter casing.
const normalizePath = (id: string): string => {
const normalized = id.replaceAll('\\', '/')
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
}
const root = normalizePath(fileURLToPath(new URL('.', import.meta.url)))
const typesExternal = (id: string): boolean => {
if (external.includes(id) || /^markdown-it(?:\/|$)/.test(id)) return true
const normalized = normalizePath(id)
return (
normalized.endsWith('.d.ts') &&
normalized.startsWith(root) &&
!normalized.startsWith(`${root}dist/`) &&
!normalized.startsWith(`${root}node_modules/`)
)
}
const dtsNode = dts({
respectExternal: true,
tsconfig: 'src/node/tsconfig.json',
compilerOptions: { preserveSymlinks: false }
})
const nodeTypes: RollupOptions = {
input: 'src/node/index.ts',
output: {
format: 'esm',
file: 'dist/node/index.d.ts'
},
external: typesExternal,
plugins: [dtsNode]
}
const clientTypes: RollupOptions = {
input: 'dist/client-types/index.d.ts',
output: {
format: 'esm',
file: 'dist/client/index.d.ts'
},
external: typesExternal,
plugins: [
dts({ respectExternal: true }),
{
name: 'cleanup',
async closeBundle() {
if (PROD) {
await rm('dist/client-types', { recursive: true })
}
}
}
]
}
export default defineConfig([esmBuild, nodeTypes, clientTypes])

@ -1,11 +0,0 @@
import { cp } from 'node:fs/promises'
import { globSync } from 'tinyglobby'
function toDest(file: string) {
return file.replace(/^src\//, 'dist/')
}
globSync(['src/client/**']).forEach((file) => {
if (/(\.ts|tsconfig\.json)$/.test(file)) return
cp(file, toDest(file))
})

@ -1,9 +0,0 @@
import { cp } from 'node:fs/promises'
import { globSync } from 'tinyglobby'
globSync(['src/shared/**/*.ts']).forEach(async (file) => {
await Promise.all([
cp(file, file.replace(/^src\/shared\//, 'src/node/')),
cp(file, file.replace(/^src\/shared\//, 'src/client/'))
])
})

@ -1,16 +1,15 @@
import { spawn } from 'cross-spawn'
import type { SpawnOptions } from 'node:child_process'
import { once } from 'node:events'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import * as prompts from '@clack/prompts'
import { spawn } from 'cross-spawn'
import semver from 'semver'
const { version: currentVersion } = createRequire(import.meta.url)(
'../package.json'
)
import { version as currentVersion } from '../package.json' with { type: 'json' }
const { inc: _inc, valid } = semver
const versionIncrements = ['patch', 'minor', 'major'] as const

@ -1,37 +0,0 @@
import { watch } from 'chokidar'
import { cp, rm } from 'node:fs/promises'
import { normalizePath } from 'vite'
function toClientAndNode(method: 'copy' | 'remove', file: string) {
file = normalizePath(file)
if (method === 'copy') {
cp(file, file.replace(/^src\/shared\//, 'src/node/'))
cp(file, file.replace(/^src\/shared\//, 'src/client/'))
} else if (method === 'remove') {
rm(file.replace(/^src\/shared\//, 'src/node/'), { force: true })
rm(file.replace(/^src\/shared\//, 'src/client/'), { force: true })
}
}
function toDist(file: string) {
return normalizePath(file).replace(/^src\//, 'dist/')
}
// copy shared files to the client and node directory whenever they change.
watch('src/shared', {
ignored: (path, stats) => !!stats?.isFile() && !path.endsWith('.ts')
})
.on('change', (file) => toClientAndNode('copy', file))
.on('add', (file) => toClientAndNode('copy', file))
.on('unlink', (file) => toClientAndNode('remove', file))
// copy non ts files, such as an html or css, to the dist directory whenever
// they change.
watch('src/client', {
ignored: (path, stats) =>
!!stats?.isFile() &&
(path.endsWith('.ts') || path.endsWith('tsconfig.json'))
})
.on('change', (file) => cp(file, toDist(file)))
.on('add', (file) => cp(file, toDist(file)))
.on('unlink', (file) => rm(toDist(file), { force: true }))

@ -0,0 +1,13 @@
// Cross-environment globals that environment-neutral code may use, declared
// merge-compatibly with lib.dom and @types/node (interface merging plus an
// identically named var). Loaded only by projects without a DOM lib; keep
// members to what shared code actually touches.
interface Console {
debug(...data: unknown[]): void
warn(...data: unknown[]): void
}
declare var console: Console
interface Document {}
declare var document: Document

@ -1,5 +1,6 @@
import { useData, useRoute } from 'vitepress'
import { defineComponent, h, watch } from 'vue'
import { contentUpdatedCallbacks } from '../utils'
const runCbs = () => contentUpdatedCallbacks.forEach((fn) => fn())

@ -7,7 +7,8 @@ export function useCodeGroups() {
Array.from(el.children).forEach((child) => {
child.classList.remove('active')
})
activate(el.children[0])
const first = el.children[0]
if (first) activate(first)
})
})
}

@ -1,11 +1,12 @@
import { inBrowser } from 'vitepress'
import { isShell } from '../../shared'
const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'].join(', ')
export function useCopyCode() {
if (inBrowser) {
const timeoutIdMap: WeakMap<HTMLElement, NodeJS.Timeout> = new WeakMap()
const timeoutIdMap: WeakMap<HTMLElement, number> = new WeakMap()
window.addEventListener('click', (e) => {
const el = e.target as HTMLElement
if (el.matches('div[class*="language-"] > button.copy')) {
@ -34,7 +35,7 @@ export function useCopyCode() {
copyToClipboard(text).then(() => {
el.classList.add('copied')
clearTimeout(timeoutIdMap.get(el))
const timeoutId = setTimeout(() => {
const timeoutId = window.setTimeout(() => {
el.classList.remove('copied')
el.blur()
timeoutIdMap.delete(el)

@ -1,4 +1,5 @@
import { watchEffect, type Ref } from 'vue'
import {
createTitle,
mergeHead,
@ -81,8 +82,8 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
function createHeadElement([tag, attrs, innerHTML]: HeadConfig) {
const el = document.createElement(tag)
for (const key in attrs) {
el.setAttribute(key, attrs[key])
for (const [key, value] of Object.entries(attrs)) {
el.setAttribute(key, value)
}
if (innerHTML) {
el.innerHTML = innerHTML

@ -2,6 +2,7 @@
// https://github.com/GoogleChromeLabs/quicklink
import { onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from '../router'
import { inBrowser, pathToFile } from '../utils'

@ -9,6 +9,7 @@ import {
type InjectionKey,
type Ref
} from 'vue'
import {
APPEARANCE_KEY,
createTitle,

@ -1,5 +1,6 @@
import { setupDevToolsPlugin } from '@vue/devtools-api'
import type { App } from 'vue'
import type { VitePressData } from './data'
import type { Router } from './router'

@ -8,6 +8,7 @@ import {
watchEffect,
type App
} from 'vue'
import { ClientOnly } from './components/ClientOnly'
import { Content } from './components/Content'
import { useCodeGroups } from './composables/codeGroups'
@ -25,8 +26,12 @@ function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme {
...base,
...theme,
async enhanceApp(ctx) {
if (base.enhanceApp) await base.enhanceApp(ctx)
if (theme.enhanceApp) await theme.enhanceApp(ctx)
await base.enhanceApp?.(ctx)
await theme.enhanceApp?.(ctx)
},
setup() {
base.setup?.()
theme.setup?.()
}
}
}

@ -1,5 +1,6 @@
import type { Component, InjectionKey } from 'vue'
import { inject, markRaw, nextTick, reactive, readonly } from 'vue'
import type { Awaitable, PageData, PageDataPayload, Route } from '../shared'
import { notFoundPageData, treatAsHtml } from '../shared'
import { siteDataRef } from './data'

@ -1,5 +1,6 @@
// entry for SSR
import { renderToString } from 'vue/server-renderer'
import type { SSGContext } from '../shared'
import { createApp } from './index'

@ -1,4 +1,5 @@
import type { App, Component, Ref } from 'vue'
import type { Awaitable, SiteData } from '../shared'
import type { Router } from './router'
@ -14,7 +15,8 @@ export interface Theme {
extends?: Theme
/**
* @deprecated can be replaced by wrapping layout component
* Runs inside the root component's `setup()` (during SSR too). With
* `extends`, setups run base-first, like `enhanceApp`.
*/
setup?: () => void

@ -1,5 +1,6 @@
import { tryOnUnmounted } from '@vueuse/core'
import { h, onMounted, shallowRef, type AsyncComponentLoader } from 'vue'
import {
EXTERNAL_URL_RE,
inBrowser,

@ -1,3 +1,8 @@
// vite/client rather than vitepress/client: the .vue declaration emit runs
// outside a project and cannot resolve self-references, and client.d.ts would
// pull the built dist into the program, clashing with the sources
/// <reference types="vite/client" />
declare const __VP_HASH_MAP__: Record<string, string>
declare const __VP_LOCAL_SEARCH__: boolean
declare const __ALGOLIA__: boolean
@ -5,12 +10,6 @@ declare const __CARBON__: boolean
declare const __VUE_PROD_DEVTOOLS__: boolean
declare const __ASSETS_DIR__: string
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
declare module '@siteData' {
import type { SiteData } from 'vitepress'
const data: SiteData

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, provide, useSlots } from 'vue'
import VPBackdrop from './components/VPBackdrop.vue'
import VPContent from './components/VPContent.vue'
import VPFooter from './components/VPFooter.vue'
@ -9,6 +10,7 @@ import VPSidebar from './components/VPSidebar.vue'
import VPSkipLink from './components/VPSkipLink.vue'
import { useData } from './composables/data'
import { layoutInfoInjectionKey, registerWatchers } from './composables/layout'
import { useNav } from './composables/nav'
import { useSidebarControl } from './composables/sidebar'
const {
@ -17,9 +19,13 @@ const {
close: closeSidebar
} = useSidebarControl()
// everything behind the open nav screen is inert so keyboard and screen
// reader users can't reach the covered page content
const { isScreenOpen } = useNav()
registerWatchers({ closeSidebar })
const { frontmatter } = useData()
const { frontmatter, theme } = useData()
const slots = useSlots()
const heroImageSlotExists = computed(() => !!slots['home-hero-image'])
@ -31,10 +37,13 @@ provide(layoutInfoInjectionKey, { heroImageSlotExists })
<div
v-if="frontmatter.layout !== false"
class="Layout"
:class="frontmatter.pageClass"
:class="[
frontmatter.pageClass,
theme.gradedContainers && 'vp-graded-containers'
]"
>
<slot name="layout-top" />
<VPSkipLink />
<VPSkipLink :inert="isScreenOpen" />
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
<VPNav>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
@ -44,14 +53,14 @@ provide(layoutInfoInjectionKey, { heroImageSlotExists })
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
</VPNav>
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" :inert="isScreenOpen" />
<VPSidebar :open="isSidebarOpen">
<VPSidebar :open="isSidebarOpen" :inert="isScreenOpen">
<template #sidebar-nav-before><slot name="sidebar-nav-before" /></template>
<template #sidebar-nav-after><slot name="sidebar-nav-after" /></template>
</VPSidebar>
<VPContent>
<VPContent :inert="isScreenOpen">
<template #page-top><slot name="page-top" /></template>
<template #page-bottom><slot name="page-bottom" /></template>
@ -81,7 +90,7 @@ provide(layoutInfoInjectionKey, { heroImageSlotExists })
<template #aside-ads-after><slot name="aside-ads-after" /></template>
</VPContent>
<VPFooter />
<VPFooter :inert="isScreenOpen" />
<slot name="layout-bottom" />
</div>
<Content v-else />

@ -1,5 +1,6 @@
<script setup lang="ts">
import { withBase } from 'vitepress'
import { useData } from './composables/data'
import { useLangs } from './composables/langs'
@ -33,60 +34,58 @@ const { currentLang } = useLangs()
<style scoped>
.NotFound {
padding: 64px 24px 96px;
padding: 4rem 1.5rem 6rem;
text-align: center;
}
@media (min-width: 768px) {
@media (min-width: 48rem) {
.NotFound {
padding: 96px 32px 168px;
padding: 6rem 2rem 10.5rem;
}
}
.code {
line-height: 64px;
font-size: 64px;
line-height: 1;
font-size: 4rem;
font-weight: 600;
}
.title {
padding-top: 12px;
letter-spacing: 2px;
line-height: 20px;
font-size: 20px;
padding-top: 0.75rem;
letter-spacing: 0.1em;
line-height: 1;
font-size: 1.25rem;
font-weight: 700;
}
.divider {
margin: 24px auto 18px;
width: 64px;
margin: 1.5rem auto 1.125rem;
width: 4rem;
height: 1px;
background-color: var(--vp-c-divider);
}
.quote {
margin: 0 auto;
max-width: 256px;
font-size: 14px;
max-width: 16rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--vp-c-text-2);
}
.action {
padding-top: 20px;
padding-top: 1.25rem;
}
.link {
display: inline-block;
border: 1px solid var(--vp-c-brand-1);
border-radius: 16px;
padding: 3px 16px;
font-size: 14px;
border-radius: 1rem;
padding: 0.1875rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--vp-c-brand-1);
transition:
border-color 0.25s,
color 0.25s;
transition: border-color 0.25s, color 0.25s;
}
.link:hover {

@ -4,6 +4,7 @@ import type { SidepanelInstance } from '@docsearch/sidepanel-js'
import { inBrowser, useRouter } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, onUnmounted, watch } from 'vue'
import type { DocSearchAskAi } from '../../../../types/docsearch'
import { useData } from '../composables/data'
import {

@ -30,10 +30,10 @@ defineProps<{
}
.VPBackdrop.fade-leave-active {
transition-duration: .25s;
transition-duration: 0.25s;
}
@media (min-width: 1280px) {
@media (min-width: 80rem) {
.VPBackdrop {
display: none;
}

@ -1,9 +1,8 @@
<script setup lang="ts">
interface Props {
withDefaults(defineProps<{
text?: string
type?: 'info' | 'tip' | 'warning' | 'danger'
}
withDefaults(defineProps<Props>(), {
type?: 'info' | 'note' | 'tip' | 'important' | 'caution' | 'warning' | 'danger'
}>(), {
type: 'tip'
})
</script>
@ -17,22 +16,22 @@ withDefaults(defineProps<Props>(), {
<style>
.VPBadge {
display: inline-block;
margin-left: 2px;
margin-left: 0.125rem;
border: 1px solid transparent;
border-radius: 12px;
padding: 0 10px;
line-height: 22px;
font-size: 12px;
border-radius: 0.75rem;
padding: 0 0.625rem;
line-height: 1.8333333;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
transform: translateY(-2px);
transform: translateY(-0.125rem);
}
.VPBadge.small {
padding: 0 6px;
line-height: 18px;
font-size: 10px;
transform: translateY(-8px);
padding: 0 0.375rem;
line-height: 1.8;
font-size: 0.625rem;
transform: translateY(-0.5rem);
}
.VPDocFooter .VPBadge {
@ -41,12 +40,12 @@ withDefaults(defineProps<Props>(), {
.vp-doc h1 > .VPBadge,
.vp-doc h2 > .VPBadge {
margin: 0 0 0 2px;
margin: 0 0 0 0.125rem;
vertical-align: middle;
}
.vp-doc h2 > .VPBadge {
padding: 0 8px;
padding: 0 0.5rem;
}
.vp-doc h3 > .VPBadge {
@ -57,7 +56,7 @@ withDefaults(defineProps<Props>(), {
.vp-doc h5 > .VPBadge,
.vp-doc h6 > .VPBadge {
vertical-align: middle;
line-height: 18px;
line-height: 1.5;
}
.VPBadge.info {
@ -66,12 +65,30 @@ withDefaults(defineProps<Props>(), {
background-color: var(--vp-badge-info-bg);
}
.VPBadge.note {
border-color: var(--vp-badge-note-border);
color: var(--vp-badge-note-text);
background-color: var(--vp-badge-note-bg);
}
.VPBadge.tip {
border-color: var(--vp-badge-tip-border);
color: var(--vp-badge-tip-text);
background-color: var(--vp-badge-tip-bg);
}
.VPBadge.important {
border-color: var(--vp-badge-important-border);
color: var(--vp-badge-important-text);
background-color: var(--vp-badge-important-bg);
}
.VPBadge.caution {
border-color: var(--vp-badge-caution-border);
color: var(--vp-badge-caution-text);
background-color: var(--vp-badge-caution-bg);
}
.VPBadge.warning {
border-color: var(--vp-badge-warning-border);
color: var(--vp-badge-warning-text);

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { EXTERNAL_URL_RE } from '../../shared'
import { normalizeLink } from '../support/utils'
@ -9,8 +10,8 @@ interface Props {
theme?: 'brand' | 'alt' | 'sponsor'
text?: string
href?: string
target?: string;
rel?: string;
target?: string
rel?: string
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
@ -29,7 +30,7 @@ const component = computed(() => {
<template>
<component
:is="component"
class="VPButton"
class="VPButton no-icon"
:class="[size, theme]"
:href="href ? normalizeLink(href) : undefined"
:target="props.target ?? (isExternal ? '_blank' : undefined)"
@ -46,6 +47,7 @@ const component = computed(() => {
text-align: center;
font-weight: 600;
white-space: nowrap;
text-decoration: none;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
}
@ -54,17 +56,17 @@ const component = computed(() => {
}
.VPButton.medium {
border-radius: 20px;
padding: 0 20px;
line-height: 38px;
font-size: 14px;
border-radius: 1.25rem;
padding: 0 1.25rem;
line-height: 2.7142857;
font-size: 0.875rem;
}
.VPButton.big {
border-radius: 24px;
padding: 0 24px;
line-height: 46px;
font-size: 16px;
border-radius: 1.5rem;
padding: 0 1.5rem;
line-height: 2.875;
font-size: 1rem;
}
.VPButton.brand {

@ -1,8 +1,8 @@
<script setup lang="ts">
import { useMediaQuery } from '@vueuse/core'
import { useRoute } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { onMounted, ref, watch } from 'vue'
import { useAside } from '../composables/aside'
import { onMounted, useTemplateRef, watch } from 'vue'
const route = useRoute()
const props = defineProps<{
@ -11,8 +11,8 @@ const props = defineProps<{
const carbonOptions = props.carbonAds
const { isAsideEnabled } = useAside()
const container = ref()
const isAsideVisible = useMediaQuery('(min-width: 80rem)')
const container = useTemplateRef('container')
let isInitialized = false
@ -22,18 +22,18 @@ function init() {
const params = new URLSearchParams({
serve: carbonOptions.code,
placement: carbonOptions.placement,
format: carbonOptions?.format || 'classic',
format: carbonOptions?.format || 'classic'
})
const s = document.createElement('script')
s.id = '_carbonads_js'
s.src = `//cdn.carbonads.com/carbon.js?${params.toString()}`
s.async = true
container.value.appendChild(s)
container.value?.appendChild(s)
}
}
watch(() => route.data.relativePath, () => {
if (isInitialized && isAsideEnabled.value) {
if (isInitialized && isAsideVisible.value) {
;(window as any)._carbonads?.refresh()
}
})
@ -45,10 +45,10 @@ if (carbonOptions) {
// if the page is loaded when aside is active, load carbon directly.
// otherwise, only load it if the page resizes to wide enough. this avoids
// loading carbon at all on mobile where it's never shown
if (isAsideEnabled.value) {
if (isAsideVisible.value) {
init()
} else {
watch(isAsideEnabled, (wide) => wide && init())
watch(isAsideVisible, (visible) => visible && init())
}
})
}
@ -63,25 +63,25 @@ if (carbonOptions) {
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
border-radius: 12px;
min-height: 256px;
padding: 1.5rem;
border-radius: 0.75rem;
min-height: 16rem;
text-align: center;
line-height: 18px;
font-size: 12px;
line-height: 1.5;
font-size: 0.75rem;
font-weight: 500;
background-color: var(--vp-carbon-ads-bg-color);
}
.VPCarbonAds :deep(img) {
margin: 0 auto;
border-radius: 6px;
border-radius: 0.375rem;
}
.VPCarbonAds :deep(.carbon-text) {
display: block;
margin: 0 auto;
padding-top: 12px;
padding-top: 0.75rem;
color: var(--vp-carbon-ads-text-color);
transition: color 0.25s;
}
@ -92,8 +92,8 @@ if (carbonOptions) {
.VPCarbonAds :deep(.carbon-poweredby) {
display: block;
padding-top: 6px;
font-size: 11px;
padding-top: 0.375rem;
font-size: 0.6875rem;
font-weight: 500;
color: var(--vp-carbon-ads-poweredby-color);
text-transform: uppercase;

@ -1,8 +1,9 @@
<script setup lang="ts">
import { resolveDynamicComponent } from 'vue'
import NotFound from '../NotFound.vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
import NotFound from '../NotFound.vue'
import VPDoc from './VPDoc.vue'
import VPHome from './VPHome.vue'
import VPPage from './VPPage.vue'
@ -78,7 +79,7 @@ function isRegistered(component: string): boolean {
margin: 0;
}
@media (min-width: 960px) {
@media (min-width: 60rem) {
.VPContent {
padding-top: var(--vp-nav-height);
}
@ -89,7 +90,7 @@ function isRegistered(component: string): boolean {
}
}
@media (min-width: 1440px) {
@media (min-width: 90rem) {
.VPContent.has-sidebar {
padding-right: calc((100% - var(--vp-layout-max-width)) / 2);
padding-left: calc((100% - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));

@ -1,6 +1,7 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
import VPDocAside from './VPDocAside.vue'
@ -63,33 +64,33 @@ const pageName = computed(() =>
<style scoped>
.VPDoc {
padding: 32px 24px 96px;
padding: 2rem 1.5rem 6rem;
width: 100%;
}
@media (min-width: 768px) {
@media (min-width: 48rem) {
.VPDoc {
padding: 48px 32px 128px;
padding: 3rem 2rem 8rem;
}
}
@media (min-width: 960px) {
@media (min-width: 60rem) {
.VPDoc {
padding: 48px 32px 0;
padding: 3rem 2rem 0;
}
.VPDoc:not(.has-sidebar) .container {
display: flex;
justify-content: center;
max-width: 992px;
max-width: 62rem;
}
.VPDoc:not(.has-sidebar) .content {
max-width: 752px;
max-width: 47rem;
}
}
@media (min-width: 1280px) {
@media (min-width: 80rem) {
.VPDoc .container {
display: flex;
justify-content: center;
@ -100,13 +101,13 @@ const pageName = computed(() =>
}
}
@media (min-width: 1440px) {
@media (min-width: 90rem) {
.VPDoc:not(.has-sidebar) .content {
max-width: 784px;
max-width: 49rem;
}
.VPDoc:not(.has-sidebar) .container {
max-width: 1104px;
max-width: 69rem;
}
}
@ -120,22 +121,22 @@ const pageName = computed(() =>
display: none;
order: 2;
flex-grow: 1;
padding-left: 32px;
padding-left: 2rem;
width: 100%;
max-width: 256px;
max-width: 16rem;
}
.left-aside {
order: 1;
padding-left: unset;
padding-right: 32px;
padding-right: 2rem;
}
.aside-container {
position: fixed;
top: 0;
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);
width: 224px;
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 3rem);
width: 14rem;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
@ -150,8 +151,8 @@ const pageName = computed(() =>
position: fixed;
bottom: 0;
z-index: 10;
width: 224px;
height: 32px;
width: 14rem;
height: 2rem;
background: linear-gradient(transparent, var(--vp-c-bg) 70%);
pointer-events: none;
}
@ -159,8 +160,8 @@ const pageName = computed(() =>
.aside-content {
display: flex;
flex-direction: column;
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));
padding-bottom: 32px;
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 3rem));
padding-bottom: 2rem;
}
.content {
@ -169,17 +170,17 @@ const pageName = computed(() =>
width: 100%;
}
@media (min-width: 960px) {
@media (min-width: 60rem) {
.content {
padding: 0 32px 128px;
padding: 0 2rem 8rem;
}
}
@media (min-width: 1280px) {
@media (min-width: 80rem) {
.content {
order: 1;
margin: 0;
min-width: 640px;
min-width: 40rem;
}
}
@ -188,6 +189,6 @@ const pageName = computed(() =>
}
.VPDoc.has-aside .content-container {
max-width: 688px;
max-width: 43rem;
}
</style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save