mirror of https://github.com/vuejs/vitepress
parent
b198380618
commit
73bbc6143a
@ -1,134 +0,0 @@
|
||||
import {
|
||||
isSideBarEmpty,
|
||||
getSideBarConfig,
|
||||
getFlatSideBarLinks
|
||||
} from 'client/theme-default/support/sideBar'
|
||||
|
||||
describe('client/theme-default/support/sideBar', () => {
|
||||
it('checks if the given sidebar is empty', () => {
|
||||
expect(isSideBarEmpty(undefined)).toBe(true)
|
||||
expect(isSideBarEmpty(false)).toBe(true)
|
||||
expect(isSideBarEmpty([])).toBe(true)
|
||||
|
||||
expect(isSideBarEmpty('auto')).toBe(false)
|
||||
expect(isSideBarEmpty([{ text: 'a', link: '/a' }])).toBe(false)
|
||||
})
|
||||
|
||||
it('gets the correct sidebar items', () => {
|
||||
expect(getSideBarConfig(false, '')).toEqual(false)
|
||||
expect(getSideBarConfig('auto', '')).toEqual('auto')
|
||||
|
||||
const sidebar = [{ text: 'Title 01', link: 'title-01' }]
|
||||
const expected = [{ text: 'Title 01', link: 'title-01' }]
|
||||
|
||||
expect(getSideBarConfig(sidebar, '')).toEqual(expected)
|
||||
})
|
||||
|
||||
it('gets the correct sidebar items from the given path', () => {
|
||||
const sidebar = {
|
||||
'/guide/': [{ text: 'G', link: 'g' }],
|
||||
'/': [{ text: 'R', link: 'r' }]
|
||||
}
|
||||
|
||||
expect(getSideBarConfig(sidebar, '/')).toEqual(sidebar['/'])
|
||||
expect(getSideBarConfig(sidebar, '/guide/')).toEqual(sidebar['/guide/'])
|
||||
})
|
||||
|
||||
it('gets the correct sidebar items with various combination', () => {
|
||||
const s = {
|
||||
'/guide/': [{ text: 'G', link: 'g' }],
|
||||
api: [{ text: 'A', link: 'a' }]
|
||||
}
|
||||
|
||||
expect(getSideBarConfig(s, '/guide/')).toEqual(s['/guide/'])
|
||||
// no ending slash should not match
|
||||
expect(getSideBarConfig(s, '/guide')).not.toEqual(s['/guide/'])
|
||||
expect(getSideBarConfig(s, 'guide/')).toEqual(s['/guide/'])
|
||||
expect(getSideBarConfig(s, 'guide/nested')).toEqual(s['/guide/'])
|
||||
expect(getSideBarConfig(s, '/guide/nested')).toEqual(s['/guide/'])
|
||||
expect(getSideBarConfig(s, 'guide/nested/')).toEqual(s['/guide/'])
|
||||
expect(getSideBarConfig(s, '/api/')).toEqual(s['api'])
|
||||
expect(getSideBarConfig(s, '/api')).toEqual(s['api'])
|
||||
expect(getSideBarConfig(s, 'api/')).toEqual(s['api'])
|
||||
expect(getSideBarConfig(s, 'api/nested')).toEqual(s['api'])
|
||||
expect(getSideBarConfig(s, '/api/nested')).toEqual(s['api'])
|
||||
expect(getSideBarConfig(s, 'api/nested/')).toEqual(s['api'])
|
||||
expect(getSideBarConfig(s, '/')).toEqual('auto')
|
||||
})
|
||||
|
||||
it('creates flat sidebar links', () => {
|
||||
const sidebar = [
|
||||
{ text: 'Title 01', link: '/title-01' },
|
||||
{ text: 'Title 02', link: '/title-02' },
|
||||
{ text: 'Title 03', link: '/title-03' }
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{ text: 'Title 01', link: '/title-01' },
|
||||
{ text: 'Title 02', link: '/title-02' },
|
||||
{ text: 'Title 03', link: '/title-03' }
|
||||
]
|
||||
|
||||
expect(getFlatSideBarLinks(sidebar)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('creates flat sidebar links with mixed sidebar group', () => {
|
||||
const sidebar = [
|
||||
{
|
||||
text: 'Title 01',
|
||||
link: '/title-01',
|
||||
children: [
|
||||
{ text: 'Children 01', link: '/children-01' },
|
||||
{ text: 'Children 02', link: '/children-02' }
|
||||
]
|
||||
},
|
||||
{ text: 'Title 02', link: '/title-02' },
|
||||
{ text: 'Title 03', link: '/title-03' }
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{ text: 'Title 01', link: '/title-01' },
|
||||
{ text: 'Children 01', link: '/children-01' },
|
||||
{ text: 'Children 02', link: '/children-02' },
|
||||
{ text: 'Title 02', link: '/title-02' },
|
||||
{ text: 'Title 03', link: '/title-03' }
|
||||
]
|
||||
|
||||
expect(getFlatSideBarLinks(sidebar)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('ignores any items with no `link` property', () => {
|
||||
const sidebar = [
|
||||
{
|
||||
text: 'Title 01',
|
||||
children: [
|
||||
{ text: 'Children 01', link: '/children-01' },
|
||||
{ text: 'Children 02', link: '/children-02' }
|
||||
]
|
||||
},
|
||||
{ text: 'Title 02', link: '/title-02' }
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{ text: 'Children 01', link: '/children-01' },
|
||||
{ text: 'Children 02', link: '/children-02' },
|
||||
{ text: 'Title 02', link: '/title-02' }
|
||||
]
|
||||
|
||||
expect(getFlatSideBarLinks(sidebar)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('removes `.md` or `.html` extention', () => {
|
||||
const sidebar = [
|
||||
{ text: 'Title 01', link: '/title-01.md' },
|
||||
{ text: 'Title 02', link: '/title-02.html' }
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{ text: 'Title 01', link: '/title-01' },
|
||||
{ text: 'Title 02', link: '/title-02' }
|
||||
]
|
||||
|
||||
expect(getFlatSideBarLinks(sidebar)).toEqual(expected)
|
||||
})
|
||||
})
|
@ -1,41 +0,0 @@
|
||||
import * as Utils from 'client/theme-default/utils'
|
||||
|
||||
describe('client/theme-default/utils', () => {
|
||||
describe('ensureStartingSlash', () => {
|
||||
it('should add slash to the beginning of the given path', () => {
|
||||
expect(Utils.ensureStartingSlash('path')).toBe('/path')
|
||||
expect(Utils.ensureStartingSlash('path/nested')).toBe('/path/nested')
|
||||
expect(Utils.ensureStartingSlash('/path')).toBe('/path')
|
||||
expect(Utils.ensureStartingSlash('/path/nested')).toBe('/path/nested')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureEndingSlash', () => {
|
||||
it('should add slash to the end of the given path', () => {
|
||||
expect(Utils.ensureEndingSlash('path')).toBe('path/')
|
||||
expect(Utils.ensureEndingSlash('path/nested')).toBe('path/nested/')
|
||||
expect(Utils.ensureEndingSlash('path/')).toBe('path/')
|
||||
expect(Utils.ensureEndingSlash('path/nested/')).toBe('path/nested/')
|
||||
expect(Utils.ensureEndingSlash('path/page.html')).toBe('path/page.html')
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeExtention', () => {
|
||||
it('removes `.md` or `.html` extention from the path', () => {
|
||||
expect(Utils.removeExtention('/')).toBe('/')
|
||||
expect(Utils.removeExtention('index')).toBe('/')
|
||||
expect(Utils.removeExtention('index.md')).toBe('/')
|
||||
expect(Utils.removeExtention('index.html')).toBe('/')
|
||||
expect(Utils.removeExtention('/index')).toBe('/')
|
||||
expect(Utils.removeExtention('/index.md')).toBe('/')
|
||||
expect(Utils.removeExtention('/index.html')).toBe('/')
|
||||
expect(Utils.removeExtention('path')).toBe('path')
|
||||
expect(Utils.removeExtention('path.md')).toBe('path')
|
||||
expect(Utils.removeExtention('path.html')).toBe('path')
|
||||
expect(Utils.removeExtention('path/')).toBe('path/')
|
||||
expect(Utils.removeExtention('path/nested.md')).toBe('path/nested')
|
||||
expect(Utils.removeExtention('path/nested.html')).toBe('path/nested')
|
||||
expect(Utils.removeExtention('path/nested/index')).toBe('path/nested/')
|
||||
})
|
||||
})
|
||||
})
|
@ -1,4 +1,3 @@
|
||||
// re-export vite client types
|
||||
// with strict installers like pnpm, user won't be able to reference vite/client
|
||||
// in project root
|
||||
// re-export vite client types. with strict installers like pnpm, user won't
|
||||
// be able to reference vite/client in project root.
|
||||
/// <reference types="vite/client" />
|
||||
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<span>⚡</span>
|
||||
</template>
|
@ -1,53 +0,0 @@
|
||||
# Theme Config: Algolia Search
|
||||
|
||||
The `themeConfig.algolia` option allows you to use [Algolia DocSearch](https://docsearch.algolia.com). To enable it, you need to provide at least appId, apiKey and indexName:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
algolia: {
|
||||
appId: 'your_app_id',
|
||||
apiKey: 'your_api_key',
|
||||
indexName: 'index_name'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For more options, check out [Algolia DocSearch's documentation](https://docsearch.algolia.com/docs/api/). You can pass any extra option alongside other options, e.g. passing `searchParameters`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
algolia: {
|
||||
appId: 'your_app_id',
|
||||
apiKey: 'your_api_key',
|
||||
indexName: 'index_name',
|
||||
searchParameters: {
|
||||
facetFilters: ['tags:guide,api']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
If you have multiple locales in your documentation and you have defined a `locales` object in your `themeConfig`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
locales: {
|
||||
// ...
|
||||
},
|
||||
algolia: {
|
||||
appId: 'your_app_id',
|
||||
apiKey: 'your_api_key',
|
||||
indexName: 'index_name'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
VitePress will automatically add a `lang` _facetFilter_ to the `searchParameters.facetFilter` array with the correct language value. Algolia automatically adds the correct facet filter based on the `lang` attribute on the `<html>` tag. This will match search results with the currently viewed language of the page.
|
@ -1,59 +0,0 @@
|
||||
# App Config: Basics
|
||||
|
||||
::: tip
|
||||
The config reference is incomplete since the config format may still receive further changes. For a complete reference of the current available options, refer to [config.ts](https://github.com/vuejs/vitepress/blob/45b65ce8b63bd54f345bfc3383eb2416b6769dc9/src/node/config.ts#L30-L65).
|
||||
:::
|
||||
|
||||
## base
|
||||
|
||||
- Type: `string`
|
||||
- Default: `/`
|
||||
|
||||
The base URL the site will be deployed at. You will need to set this if you plan to deploy your site under a sub path, for example, GitHub pages. If you plan to deploy your site to `https://foo.github.io/bar/`, then you should set base to `'/bar/'`. It should always start and end with a slash.
|
||||
|
||||
The `base` is automatically prepended to all the URLs that start with `/` in other options, so you only need to specify it once.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
base: '/base/'
|
||||
}
|
||||
```
|
||||
|
||||
## lang
|
||||
|
||||
- Type: `string`
|
||||
- Default: `en-US`
|
||||
|
||||
The `lang` attribute for the site. This will render as a `<html lang="en-US">` tag in the page HTML.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
lang: 'en-US'
|
||||
}
|
||||
```
|
||||
|
||||
## title
|
||||
|
||||
- Type: `string`
|
||||
- Default: `VitePress`
|
||||
|
||||
Title for the site. This will be the suffix for all page titles, and displayed in the navbar.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
title: 'VitePress'
|
||||
}
|
||||
```
|
||||
|
||||
## description
|
||||
|
||||
- Type: `string`
|
||||
- Default: `A VitePress site`
|
||||
|
||||
Description for the site. This will render as a `<meta>` tag in the page HTML.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
description: 'A VitePress site'
|
||||
}
|
||||
```
|
@ -1,15 +0,0 @@
|
||||
# Theme Config: Carbon Ads
|
||||
|
||||
VitePress has built in native support for [Carbon Ads](https://www.carbonads.net). By defining the Carbon Ads credentials in config, VitePress will display ads on the page.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
carbonAds: {
|
||||
carbon: 'your-carbon-key',
|
||||
custom: 'your-carbon-custom',
|
||||
placement: 'your-carbon-placement'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -1,23 +0,0 @@
|
||||
# Theme Config: Homepage
|
||||
|
||||
VitePress provides a homepage layout. To use it, specify `home: true` plus some other metadata in your root `index.md`'s [YAML frontmatter](../guide/frontmatter). This is an example of how it works:
|
||||
|
||||
```yaml
|
||||
---
|
||||
home: true
|
||||
heroImage: /logo.png
|
||||
heroAlt: Logo image
|
||||
heroText: Hero Title
|
||||
tagline: Hero subtitle
|
||||
actionText: Get Started
|
||||
actionLink: /guide/
|
||||
features:
|
||||
- title: Simplicity First
|
||||
details: Minimal setup with markdown-centered project structure helps you focus on writing.
|
||||
- title: Vue-Powered
|
||||
details: Enjoy the dev experience of Vue + webpack, use Vue components in markdown, and develop custom themes with Vue.
|
||||
- title: Performant
|
||||
details: VitePress generates pre-rendered static HTML for each page, and runs as an SPA once a page is loaded.
|
||||
footer: MIT Licensed | Copyright © 2019-present Evan You
|
||||
---
|
||||
```
|
@ -1,95 +0,0 @@
|
||||
# API Reference
|
||||
|
||||
## Helper Methods
|
||||
|
||||
The following methods are globally importable from `vitepress` and are typically used in custom theme Vue components. However, they are also usable inside `.md` pages because markdown files are compiled into Vue single-file components.
|
||||
|
||||
Methods that start with `use*` indicates that it is a [Vue 3 Composition API](https://vuejs.org/guide/introduction.html#composition-api) function that can only be used inside `setup()` or `<script setup>`.
|
||||
|
||||
### `useData`
|
||||
|
||||
Returns page-specific data. The returned object has the following type:
|
||||
|
||||
```ts
|
||||
interface VitePressData {
|
||||
site: Ref<SiteData>
|
||||
page: Ref<PageData>
|
||||
theme: Ref<any> // themeConfig from .vitepress/config.js
|
||||
frontmatter: Ref<PageData['frontmatter']>
|
||||
title: Ref<string>
|
||||
description: Ref<string>
|
||||
lang: Ref<string>
|
||||
localePath: Ref<string>
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ theme.heroText }}</h1>
|
||||
</template>
|
||||
```
|
||||
|
||||
### `useRoute`
|
||||
|
||||
Returns the current route object with the following type:
|
||||
|
||||
```ts
|
||||
interface Route {
|
||||
path: string
|
||||
data: PageData
|
||||
component: Component | null
|
||||
}
|
||||
```
|
||||
|
||||
### `useRouter`
|
||||
|
||||
Returns the VitePress router instance so you can programmatically navigate to another page.
|
||||
|
||||
```ts
|
||||
interface Router {
|
||||
route: Route
|
||||
go: (href?: string) => Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### `withBase`
|
||||
|
||||
- **Type**: `(path: string) => string`
|
||||
|
||||
Appends the configured [`base`](../config/basics#base) to a given URL path. Also see [Base URL](./assets#base-url).
|
||||
|
||||
## Global Components
|
||||
|
||||
VitePress comes with few built-in component that can be used globally. You may use these components in your markdown or your custom theme configuration.
|
||||
|
||||
### `<Content/>`
|
||||
|
||||
The `<Content/>` component displays the rendered markdown contents. Useful [when creating your own theme](./theming).
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<h1>Custom Layout!</h1>
|
||||
<Content />
|
||||
</template>
|
||||
```
|
||||
|
||||
### `<ClientOnly/>`
|
||||
|
||||
The `<ClientOnly/>` component renders its slot only at client side.
|
||||
|
||||
Because VitePress applications are server-rendered in Node.js when generating static builds, any Vue usage must conform to the universal code requirements. In short, make sure to only access Browser / DOM APIs in beforeMount or mounted hooks.
|
||||
|
||||
If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the `ClientOnly` component.
|
||||
|
||||
```html
|
||||
<ClientOnly>
|
||||
<NonSSRFriendlyComponent />
|
||||
</ClientOnly>
|
||||
```
|
@ -1,55 +0,0 @@
|
||||
# Asset Handling
|
||||
|
||||
All Markdown files are compiled into Vue components and processed by [Vite](https://github.com/vitejs/vite). You can, **and should**, reference any assets using relative URLs:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
You can reference static assets in your markdown files, your `*.vue` components in the theme, styles and plain `.css` files either using absolute public paths (based on project root) or relative paths (based on your file system). The latter is similar to the behavior you are used to if you have used `vue-cli` or webpack's `file-loader`.
|
||||
|
||||
Common image, media, and font filetypes are detected and included as assets automatically.
|
||||
|
||||
All referenced assets, including those using absolute paths, will be copied to the dist folder with a hashed file name in the production build. Never-referenced assets will not be copied. Similar to `vue-cli`, image assets smaller than 4kb will be base64 inlined.
|
||||
|
||||
All **static** path references, including absolute paths, should be based on your working directory structure.
|
||||
|
||||
## Public Files
|
||||
|
||||
Sometimes you may need to provide static assets that are not directly referenced in any of your Markdown or theme components (for example, favicons and PWA icons). The `public` directory under project root can be used as an escape hatch to provide static assets that either are never referenced in source code (e.g. `robots.txt`), or must retain the exact same file name (without hashing).
|
||||
|
||||
Assets placed in `public` will be copied to the root of the dist directory as-is.
|
||||
|
||||
Note that you should reference files placed in `public` using root absolute path - for example, `public/icon.png` should always be referenced in source code as `/icon.png`.
|
||||
|
||||
## Base URL
|
||||
|
||||
If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash).
|
||||
|
||||
All your static asset paths are automatically processed to adjust for different `base` config values. For example, if you have an absolute reference to an asset under `public` in your markdown:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
You do **not** need to update it when you change the `base` config value in this case.
|
||||
|
||||
However, if you are authoring a theme component that links to assets dynamically, e.g. an image whose `src` is based on a theme config value:
|
||||
|
||||
```vue
|
||||
<img :src="theme.logoPath" />
|
||||
```
|
||||
|
||||
In this case it is recommended to wrap the path with the [`withBase` helper](./api#withbase) provided by VitePress:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { withBase, useData } from 'vitepress'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img :src="withBase(theme.logoPath)" />
|
||||
</template>
|
||||
```
|
@ -1,119 +0,0 @@
|
||||
---
|
||||
sidebarDepth: 2
|
||||
---
|
||||
|
||||
# Differences from VuePress
|
||||
|
||||
VitePress and VuePress have different [design goals](../index.md). Both projects share similar config naming conventions. VitePress aims to have the bare minimum features needed for authoring docs. Other features are pushed to Themes. On the other hand, VuePress has more features out-of-the-box or enabled by its ecosystem of plugins.
|
||||
|
||||
::: tip
|
||||
If you are using VuePress, there is no need to migrate to VitePress. Both projects are going to continue to co-exist for the foreseeable future.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Note this is early WIP! Currently, the focus is on making Vite stable and feature-complete first. It is not recommended to use this for anything serious yet.
|
||||
:::
|
||||
|
||||
In case you decide to move your project to VitePress, this is a list of differences from [VuePress v1.7.1](https://github.com/vuejs/vuepress/releases/tag/v1.7.1) that you need to take into account.
|
||||
|
||||
## General
|
||||
|
||||
- Missing
|
||||
- YAML and TOML are not supported formats for site config. Only javascript is supported for `.vitepress/config.js`
|
||||
- [Plugins](https://vuepress.vuejs.org/plugin/) support, features are implemented in themes
|
||||
- [permalink support](https://vuepress.vuejs.org/guide/permalinks.html)
|
||||
- `.vitepress/templates`
|
||||
- Components in `.vitepress/components` [are not auto registered as global components](https://vuepress.vuejs.org)
|
||||
- Differences
|
||||
- [Public files](https://vuepress.vuejs.org/guide/assets.html#public-files) that are directly copied to dist root moved from `.vitepress/public/` is `public/`
|
||||
- [styling](https://vuepress.vuejs.org/config/#styling) `.vitepress/styles/index.styl` and `.vitepress/styles/palette.styl` is not supported. See [Customizing CSS](./theming#customizing-css).
|
||||
- [App Level Enhancements](https://vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements) API, app enhancements `.vitepress/enhanceApp.js` is now done in `.vitepress/theme/index.js`. See [Extending the Default Theme](./theming#extending-the-default-theme).
|
||||
|
||||
## Markdown
|
||||
|
||||
- Missing
|
||||
- Support for [toml in frontmatter](https://vuepress.vuejs.org/guide/frontmatter.html#alternative-frontmatter-formats)
|
||||
- [details block](https://vuepress.vuejs.org/guide/markdown.html#custom-containers)
|
||||
- [markdown slots](https://vuepress.vuejs.org/guide/markdown-slot.html)
|
||||
- `~` prefix to explicitly specify a url is a [webpack module request](https://vuepress.vuejs.org/guide/assets.html#relative-urls)
|
||||
|
||||
## Site Config
|
||||
|
||||
- Missing
|
||||
- `temp`
|
||||
- `dest`
|
||||
- [`theme` from a dependency](https://vuepress.vuejs.org/theme/using-a-theme.html#using-a-theme-from-a-dependency)
|
||||
- `permalink`
|
||||
- [`port`](https://vuepress.vuejs.org/config/#port)
|
||||
- [`shouldPrefetch`](https://vuepress.vuejs.org/config/#shouldprefetch)
|
||||
- [`cache`](https://vuepress.vuejs.org/config/#cache)
|
||||
- [`extraWatchFiles`](https://vuepress.vuejs.org/config/#extrawatchfiles)
|
||||
- [`patterns`](https://vuepress.vuejs.org/config/#patterns)
|
||||
- [`plugins`](https://vuepress.vuejs.org/config/#pluggable)
|
||||
- [`markdown.pageSuffix`](https://vuepress.vuejs.org/config/#markdown-pagesuffix)
|
||||
- [`markdown.slugify`](https://vuepress.vuejs.org/config/#markdown-slugify)
|
||||
- [`markdown.plugins`](https://vuepress.vuejs.org/config/#markdown-plugins)
|
||||
- [`markdown.extractHeaders`](https://vuepress.vuejs.org/config/#markdown-extractheaders)
|
||||
- `markdown.extendMarkdown` to `markdown.config`
|
||||
- `configureWebpack`, `chainWebpack`, `postcss`, `Stylus`, `scss`, `Sass`, `less` configs
|
||||
- [`evergreen`](https://vuepress.vuejs.org/config/#evergreen)
|
||||
|
||||
## Default Theme Config
|
||||
|
||||
- Missing
|
||||
- [`smoothScroll`](https://vuepress.vuejs.org/theme/default-theme-config.html#smooth-scrolling)
|
||||
- [`displayAllHeaders`](https://vuepress.vuejs.org/theme/default-theme-config.html#displaying-header-links-of-all-pages)
|
||||
- [`activeHeaderLinks`](https://vuepress.vuejs.org/theme/default-theme-config.html#active-header-links)
|
||||
- `sidebarDepth` and `initialOpenGroupIndex` for [sidebar groups](https://vuepress.vuejs.org/theme/default-theme-config.html#sidebar-groups)
|
||||
- Differences
|
||||
- `searchMaxSuggestions` is `search.maxSuggestions`
|
||||
- `algolia` is `search.algolia`
|
||||
- `searchPlaceholder` is `search.placeholder`
|
||||
|
||||
## Default Theme
|
||||
|
||||
- Missing
|
||||
- [`<code-group>` and `<code-block>`](https://vuepress.vuejs.org/theme/default-theme-config.html#code-groups-and-code-blocks)
|
||||
|
||||
## Computed Globals
|
||||
|
||||
- Missing
|
||||
- `$lang`
|
||||
- `$localePath`
|
||||
|
||||
## Frontmatter Predefined Variables
|
||||
|
||||
- Missing
|
||||
- `description`
|
||||
- [`meta`](https://vuepress.vuejs.org/guide/frontmatter.html#meta)
|
||||
- [`metaTitle`](https://vuepress.vuejs.org/guide/frontmatter.html#predefined-variables)
|
||||
- `lang`
|
||||
- [`layout`](https://vuepress.vuejs.org/guide/frontmatter.html#layout)
|
||||
- [`permalink`](https://vuepress.vuejs.org/guide/frontmatter.html#predefined-variables)
|
||||
- [`canonicalUrl`](https://vuepress.vuejs.org/guide/frontmatter.html#predefined-variables)
|
||||
|
||||
## Frontmatter Default Theme Variables
|
||||
|
||||
- Missing
|
||||
- `prev`, `next`
|
||||
- [`search`](https://vuepress.vuejs.org/guide/frontmatter.html#search)
|
||||
- [`tags`](https://vuepress.vuejs.org/guide/frontmatter.html#tags)
|
||||
- [`pageClass`](https://vuepress.vuejs.org/theme/default-theme-config.html#custom-page-class)
|
||||
- [`layout`](https://vuepress.vuejs.org/theme/default-theme-config.html#custom-layout-for-specific-pages)
|
||||
|
||||
## siteData
|
||||
|
||||
- Missing
|
||||
- [`pages`](https://vuepress.vuejs.org/theme/writing-a-theme.html#site-and-page-metadata)
|
||||
|
||||
## pageData
|
||||
|
||||
- Missing
|
||||
- `key`
|
||||
- `path`
|
||||
- `regularPath`
|
||||
|
||||
## Global Components
|
||||
|
||||
- Missing
|
||||
- [`<Badge>`](https://vuepress.vuejs.org/guide/using-vue.html#badge)
|
@ -1,51 +0,0 @@
|
||||
# Getting Started
|
||||
|
||||
This section will help you build a basic VitePress documentation site from ground up. If you already have an existing project and would like to keep documentation inside the project, start from Step 3.
|
||||
|
||||
- **Step. 1:** Create and change into a new directory.
|
||||
|
||||
```bash
|
||||
$ mkdir vitepress-starter && cd vitepress-starter
|
||||
```
|
||||
|
||||
- **Step. 2:** Initialize with your preferred package manager.
|
||||
|
||||
```bash
|
||||
$ yarn init
|
||||
```
|
||||
|
||||
- **Step. 3:** Install VitePress locally.
|
||||
|
||||
```bash
|
||||
$ yarn add --dev vitepress
|
||||
```
|
||||
|
||||
- **Step. 4:** Create your first document.
|
||||
|
||||
```bash
|
||||
$ mkdir docs && echo '# Hello VitePress' > docs/index.md
|
||||
```
|
||||
|
||||
- **Step. 5:** Add some scripts to `package.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:serve": "vitepress serve docs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Step. 6:** Serve the documentation site in the local server.
|
||||
|
||||
```bash
|
||||
$ yarn docs:dev
|
||||
```
|
||||
|
||||
VitePress will start a hot-reloading development server at `http://localhost:3000`.
|
||||
|
||||
By now, you should have a basic but functional VitePress documentation site.
|
||||
|
||||
When your documentation site starts to take shape, be sure to read the [deployment guide](./deploy).
|
@ -1,32 +0,0 @@
|
||||
# Global Component
|
||||
|
||||
VitePress comes with few built-in component that can be used globally. You may use these components in your markdown or your custom theme configuration.
|
||||
|
||||
## Content
|
||||
|
||||
The `Content` component displays the rendered markdown contents. Useful [when creating your own theme](./theming).
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<h1>Custom Layout!</h1>
|
||||
<Content />
|
||||
</template>
|
||||
```
|
||||
|
||||
## ClientOnly
|
||||
|
||||
The `ClientOnly` component renderes its slot only at client side.
|
||||
|
||||
Because VitePress applications are server-rendered in Node.js when generating static builds, any Vue usage must conform to the universal code requirements. In short, make sure to only access Browser / DOM APIs in beforeMount or mounted hooks.
|
||||
|
||||
If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the `ClientOnly` component.
|
||||
|
||||
```html
|
||||
<ClientOnly>
|
||||
<NonSSRFriendlyComponent />
|
||||
</ClientOnly>
|
||||
```
|
||||
|
||||
## OutboundLink
|
||||
|
||||
The indicator `OutboundLink` is used to denote external links. In VitePress, this component has been followed by every external link.
|
@ -1,155 +0,0 @@
|
||||
# Theming
|
||||
|
||||
## Using a Custom Theme
|
||||
|
||||
You can enable a custom theme by adding the `.vitepress/theme/index.js` file (the "theme entry file").
|
||||
|
||||
```bash
|
||||
.
|
||||
├─ docs
|
||||
│ ├─ .vitepress
|
||||
│ │ ├─ theme
|
||||
│ │ │ └─ index.js
|
||||
│ │ └─ config.js
|
||||
│ └─ index.md
|
||||
└─ package.json
|
||||
```
|
||||
|
||||
A VitePress custom theme is simply an object containing three properties and is defined as follows:
|
||||
|
||||
```ts
|
||||
interface Theme {
|
||||
Layout: Component // Vue 3 component
|
||||
NotFound?: Component
|
||||
enhanceApp?: (ctx: EnhanceAppContext) => void
|
||||
}
|
||||
|
||||
interface EnhanceAppContext {
|
||||
app: App // Vue 3 app instance
|
||||
router: Router // VitePress router instance
|
||||
siteData: Ref<SiteData>
|
||||
}
|
||||
```
|
||||
|
||||
The theme entry file should export the theme as its default export:
|
||||
|
||||
```js
|
||||
// .vitepress/theme/index.js
|
||||
import Layout from './Layout.vue'
|
||||
|
||||
export default {
|
||||
Layout,
|
||||
NotFound: () => 'custom 404', // <- this is a Vue 3 functional component
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// app is the Vue 3 app instance from `createApp()`. router is VitePress'
|
||||
// custom router. `siteData` is a `ref` of current site-level metadata.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
...where the `Layout` component could look like this:
|
||||
|
||||
```vue
|
||||
<!-- .vitepress/theme/Layout.vue -->
|
||||
<template>
|
||||
<h1>Custom Layout!</h1>
|
||||
<Content /><!-- this is where markdown content will be rendered -->
|
||||
</template>
|
||||
```
|
||||
|
||||
The default export is the only contract for a custom theme. Inside your custom theme, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./using-vue#browser-api-access-restrictions).
|
||||
|
||||
To distribute a theme, simply export the object in your package entry. To consume an external theme, import and re-export it from the custom theme entry:
|
||||
|
||||
```js
|
||||
// .vitepress/theme/index.js
|
||||
import Theme from 'awesome-vitepress-theme'
|
||||
export default Theme
|
||||
```
|
||||
|
||||
## Extending the Default Theme
|
||||
|
||||
If you want to extend and customize the default theme, you can import it from `vitepress/theme` and augment it in a custom theme entry. Here are some examples of common customizations:
|
||||
|
||||
### Registering Global Components
|
||||
|
||||
```js
|
||||
// .vitepress/theme/index.js
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
|
||||
export default {
|
||||
...DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
// register global components
|
||||
app.component('MyGlobalComponent' /* ... */)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since we are using Vite, you can also leverage Vite's [glob import feature](https://vitejs.dev/guide/features.html#glob-import) to auto register a directory of components.
|
||||
|
||||
### Customizing CSS
|
||||
|
||||
The default theme CSS is customizable by overriding root level CSS variables:
|
||||
|
||||
```js
|
||||
// .vitepress/theme/index.js
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './custom.css'
|
||||
|
||||
export default DefaultTheme
|
||||
```
|
||||
|
||||
```css
|
||||
/* .vitepress/theme/custom.css */
|
||||
:root {
|
||||
--c-brand: #646cff;
|
||||
--c-brand-light: #747bff;
|
||||
}
|
||||
```
|
||||
|
||||
See [default theme CSS variables](https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css) that can be overridden.
|
||||
|
||||
### Layout Slots
|
||||
|
||||
The default theme's `<Layout/>` component has a few slots that can be used to inject content at certain locations of the page. Here's an example of injecting a component into the top of the sidebar:
|
||||
|
||||
```js
|
||||
// .vitepress/theme/index.js
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import MyLayout from './MyLayout.vue'
|
||||
|
||||
export default {
|
||||
...DefaultTheme,
|
||||
// override the Layout with a wrapper component that injects the slots
|
||||
Layout: MyLayout
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!--.vitepress/theme/MyLayout.vue-->
|
||||
<script setup>
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
const { Layout } = DefaultTheme
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template #sidebar-top>My custom sidebar top content</template>
|
||||
</Layout>
|
||||
</template>
|
||||
```
|
||||
|
||||
Full list of slots available in the default theme layout:
|
||||
|
||||
- `navbar-search`
|
||||
- `sidebar-top`
|
||||
- `sidebar-bottom`
|
||||
- `page-top-ads`
|
||||
- `page-top`
|
||||
- `page-bottom`
|
||||
- `page-bottom-ads`
|
||||
- Only when `home: true` is enabled via frontmatter:
|
||||
- `home-hero`
|
||||
- `home-features`
|
||||
- `home-footer`
|
@ -0,0 +1,3 @@
|
||||
# What is VitePress?
|
||||
|
||||
Coming soon...
|
Before Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 223 KiB |
Before Width: | Height: | Size: 37 KiB |
@ -1,7 +0,0 @@
|
||||
// #region snippet
|
||||
function foo() {
|
||||
// ..
|
||||
}
|
||||
// #endregion snippet
|
||||
|
||||
export default foo
|
@ -1,3 +0,0 @@
|
||||
export default function () {
|
||||
// ..
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { ref, onMounted, defineComponent } from 'vue'
|
||||
import { defineComponent, ref, onMounted } from 'vue'
|
||||
|
||||
export const ClientOnly = defineComponent({
|
||||
setup(_, { slots }) {
|
||||
const show = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
show.value = true
|
||||
})
|
||||
|
||||
return () => (show.value && slots.default ? slots.default() : null)
|
||||
}
|
||||
})
|
||||
|
@ -1,188 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import '@docsearch/css'
|
||||
import docsearch from '@docsearch/js'
|
||||
import { useRoute, useRouter, useData } from 'vitepress'
|
||||
import { getCurrentInstance, onMounted, watch } from 'vue'
|
||||
import type { DefaultTheme } from '../config'
|
||||
import type { DocSearchHit } from '@docsearch/react/dist/esm/types'
|
||||
|
||||
const props = defineProps<{
|
||||
options: DefaultTheme.AlgoliaSearchOptions
|
||||
multilang?: boolean
|
||||
}>()
|
||||
|
||||
const vm = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
(value) => {
|
||||
update(value)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initialize(props.options)
|
||||
})
|
||||
|
||||
function isSpecialClick(event: MouseEvent) {
|
||||
return (
|
||||
event.button === 1 ||
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey
|
||||
)
|
||||
}
|
||||
|
||||
function getRelativePath(absoluteUrl: string) {
|
||||
const { pathname, hash } = new URL(absoluteUrl)
|
||||
|
||||
return pathname + hash
|
||||
}
|
||||
|
||||
function update(options: any) {
|
||||
if (vm && vm.vnode.el) {
|
||||
vm.vnode.el.innerHTML =
|
||||
'<div class="algolia-search-box" id="docsearch"></div>'
|
||||
initialize(options)
|
||||
}
|
||||
}
|
||||
|
||||
const { lang } = useData()
|
||||
|
||||
// if the user has multiple locales, the search results should be filtered
|
||||
// based on the language
|
||||
const facetFilters: string[] = props.multilang ? ['lang:' + lang.value] : []
|
||||
|
||||
if (props.options.searchParameters?.facetFilters) {
|
||||
facetFilters.push(...props.options.searchParameters.facetFilters)
|
||||
}
|
||||
|
||||
watch(lang, (newLang, oldLang) => {
|
||||
const index = facetFilters.findIndex((filter) => filter === 'lang:' + oldLang)
|
||||
if (index > -1) {
|
||||
facetFilters.splice(index, 1, 'lang:' + newLang)
|
||||
}
|
||||
})
|
||||
|
||||
function initialize(userOptions: any) {
|
||||
docsearch(
|
||||
Object.assign({}, userOptions, {
|
||||
container: '#docsearch',
|
||||
|
||||
searchParameters: Object.assign({}, userOptions.searchParameters, {
|
||||
// pass a custom lang facetFilter to allow multiple language search
|
||||
// https://github.com/algolia/docsearch-configs/pull/3942
|
||||
facetFilters
|
||||
}),
|
||||
|
||||
navigator: {
|
||||
navigate: ({ itemUrl }: { itemUrl: string }) => {
|
||||
const { pathname: hitPathname } = new URL(
|
||||
window.location.origin + itemUrl
|
||||
)
|
||||
|
||||
// Router doesn't handle same-page navigation so we use the native
|
||||
// browser location API for anchor navigation
|
||||
if (route.path === hitPathname) {
|
||||
window.location.assign(window.location.origin + itemUrl)
|
||||
} else {
|
||||
router.go(itemUrl)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transformItems: (items: DocSearchHit[]) => {
|
||||
return items.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
url: getRelativePath(item.url)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
hitComponent: ({
|
||||
hit,
|
||||
children
|
||||
}: {
|
||||
hit: DocSearchHit
|
||||
children: any
|
||||
}) => {
|
||||
const relativeHit = hit.url.startsWith('http')
|
||||
? getRelativePath(hit.url as string)
|
||||
: hit.url
|
||||
|
||||
return {
|
||||
type: 'a',
|
||||
ref: undefined,
|
||||
constructor: undefined,
|
||||
key: undefined,
|
||||
props: {
|
||||
href: hit.url,
|
||||
onClick: (event: MouseEvent) => {
|
||||
if (isSpecialClick(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
// we rely on the native link scrolling when user is already on
|
||||
// the right anchor because Router doesn't support duplicated
|
||||
// history entries
|
||||
if (route.path === relativeHit) {
|
||||
return
|
||||
}
|
||||
|
||||
// if the hits goes to another page, we prevent the native link
|
||||
// behavior to leverage the Router loading feature
|
||||
if (route.path !== relativeHit) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
router.go(relativeHit)
|
||||
},
|
||||
children
|
||||
},
|
||||
__v: null
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="algolia-search-box" id="docsearch" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.algolia-search-box {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.algolia-search-box {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 751px) {
|
||||
.algolia-search-box {
|
||||
min-width: 176.3px; /* avoid layout shift */
|
||||
}
|
||||
|
||||
.algolia-search-box .DocSearch-Button-Placeholder {
|
||||
padding-left: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--c-brand);
|
||||
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
|
||||
--docsearch-text-color: var(--c-text-light);
|
||||
--docsearch-muted-color: var(--c-text-lighter);
|
||||
--docsearch-searchbox-background: #f2f2f2;
|
||||
}
|
||||
</style>
|
@ -1,152 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
// global _bsa
|
||||
const ID = 'bsa-cpc-script'
|
||||
|
||||
declare global {
|
||||
var _bsa: BSA | undefined
|
||||
|
||||
interface BSA {
|
||||
init(
|
||||
name: string,
|
||||
code: string,
|
||||
placement: string,
|
||||
options: {
|
||||
target: string
|
||||
align: string
|
||||
disable_css?: 'true' | 'false'
|
||||
}
|
||||
): void
|
||||
}
|
||||
}
|
||||
|
||||
const { code, placement } = defineProps<{
|
||||
code: string
|
||||
placement: string
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
if (!document.getElementById(ID)) {
|
||||
const s = document.createElement('script')
|
||||
|
||||
s.id = ID
|
||||
s.src = '//m.servedby-buysellads.com/monetization.js'
|
||||
|
||||
document.head.appendChild(s)
|
||||
|
||||
s.onload = () => {
|
||||
load()
|
||||
}
|
||||
} else {
|
||||
load()
|
||||
}
|
||||
})
|
||||
|
||||
function load() {
|
||||
if (typeof _bsa !== 'undefined' && _bsa) {
|
||||
const parent = document.querySelector('.bsa-cpc')!
|
||||
// cleanup any existing ad to avoid them stacking
|
||||
parent.innerHTML = ''
|
||||
|
||||
_bsa.init('default', code, `placement:${placement}`, {
|
||||
target: '.bsa-cpc',
|
||||
align: 'horizontal',
|
||||
disable_css: 'true'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="buy-sell-ads">
|
||||
<div class="bsa-cpc" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.buy-sell-ads {
|
||||
margin: 0 auto;
|
||||
padding-top: 2rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.bsa-cpc {
|
||||
border-radius: 6px;
|
||||
background-color: var(--c-bg-accent);
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(a._default_) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
color: var(--c-text-light);
|
||||
}
|
||||
|
||||
@media (min-width: 512px) {
|
||||
.bsa-cpc ::v-deep(a._default_) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(.default-ad) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(a._default_ .default-image) {
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(a._default_ .default-image img) {
|
||||
border-radius: 4px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(._default_::after) {
|
||||
border: 1px solid #1c90f3;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
margin-left: 36px;
|
||||
padding: 0 8px;
|
||||
line-height: 22px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: #1c90f3;
|
||||
content: 'Sponsored';
|
||||
}
|
||||
|
||||
@media (min-width: 512px) {
|
||||
.bsa-cpc ::v-deep(._default_::after) {
|
||||
margin-top: 0px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(.default-text) {
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
width: calc(100% - 36px);
|
||||
}
|
||||
|
||||
@media (min-width: 512px) {
|
||||
.bsa-cpc ::v-deep(.default-text) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(.default-title) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bsa-cpc ::v-deep(.default-description) {
|
||||
padding-left: 8px;
|
||||
}
|
||||
</style>
|
@ -1,99 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const { code, placement } = defineProps<{
|
||||
code: string
|
||||
placement: string
|
||||
}>()
|
||||
|
||||
const el = ref()
|
||||
|
||||
onMounted(() => {
|
||||
const s = document.createElement('script')
|
||||
s.id = '_carbonads_js'
|
||||
s.src = `//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}`
|
||||
el.value.appendChild(s)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="carbon-ads" ref="el" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.carbon-ads {
|
||||
border-radius: 4px;
|
||||
margin: 0 auto;
|
||||
padding: 8px;
|
||||
max-width: 280px;
|
||||
font-size: 0.75rem;
|
||||
background-color: var(--c-bg-accent);
|
||||
min-height: 105.38px; /* avoid layout shift on mobile */
|
||||
}
|
||||
|
||||
.carbon-ads::after {
|
||||
clear: both;
|
||||
display: block;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.carbon-ads {
|
||||
z-index: 1;
|
||||
float: right;
|
||||
margin: -8px -8px 24px 24px;
|
||||
width: 146px;
|
||||
max-width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.carbon-ads {
|
||||
right: 8px;
|
||||
float: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.carbon-ads :deep(.carbon-img) {
|
||||
float: left;
|
||||
margin-right: 0.75rem;
|
||||
max-width: 110px;
|
||||
border: 1px solid var(--c-divider);
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.carbon-ads :deep(.carbon-img) {
|
||||
float: none;
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
max-width: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
.carbon-ads :deep(.carbon-img img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.carbon-ads :deep(.carbon-text) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.carbon-ads :deep(.carbon-text) {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
color: var(--c-text-light);
|
||||
}
|
||||
|
||||
.carbon-ads :deep(.carbon-poweredby) {
|
||||
display: block;
|
||||
padding-top: 2px;
|
||||
font-weight: 400;
|
||||
color: var(--c-text-lighter);
|
||||
}
|
||||
</style>
|
@ -1,38 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useEditLink } from '../composables/editLink'
|
||||
import OutboundLink from './icons/OutboundLink.vue'
|
||||
|
||||
const { url, text } = useEditLink()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="edit-link">
|
||||
<a
|
||||
v-if="url"
|
||||
class="link"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ text }} <OutboundLink class="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--c-text-light);
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: none;
|
||||
color: var(--c-brand);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
@ -1,31 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import HomeHero from './HomeHero.vue'
|
||||
import HomeFeatures from './HomeFeatures.vue'
|
||||
import HomeFooter from './HomeFooter.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="home" aria-labelledby="main-title">
|
||||
<HomeHero />
|
||||
<slot name="hero" />
|
||||
<HomeFeatures />
|
||||
<div class="home-content">
|
||||
<Content />
|
||||
</div>
|
||||
<slot name="features" />
|
||||
<HomeFooter />
|
||||
<slot name="footer" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
padding-top: var(--header-height);
|
||||
}
|
||||
|
||||
.home-content {
|
||||
max-width: 960px;
|
||||
margin: 0px auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
</style>
|
@ -1,143 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { frontmatter } = useData()
|
||||
|
||||
const hasFeatures = computed(() => {
|
||||
return frontmatter.value.features && frontmatter.value.features.length > 0
|
||||
})
|
||||
|
||||
interface Feature {
|
||||
title?: string
|
||||
details?: string
|
||||
}
|
||||
|
||||
const features = computed<Feature[]>(() => {
|
||||
return frontmatter.value.features ? frontmatter.value.features : []
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasFeatures" class="home-features">
|
||||
<div class="wrapper">
|
||||
<div class="container">
|
||||
<div class="features">
|
||||
<section
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
class="feature"
|
||||
>
|
||||
<h2 class="title" v-if="feature.title">{{ feature.title }}</h2>
|
||||
<p class="details" v-if="feature.details">{{ feature.details }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-features {
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 0 2.75rem;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.home-hero + .home-features {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.home-features {
|
||||
padding: 3.25rem 0 3.5rem;
|
||||
}
|
||||
|
||||
.home-hero + .home-features {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.home-features {
|
||||
padding-right: 1.5rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.home-hero + .home-features .wrapper {
|
||||
border-top: 1px solid var(--c-divider);
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.home-hero + .home-features .wrapper {
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.wrapper {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 392px;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -20px -24px;
|
||||
}
|
||||
|
||||
.feature {
|
||||
flex-shrink: 0;
|
||||
padding: 20px 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.feature {
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
border-bottom: 0;
|
||||
line-height: 1.4;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
color: var(--c-text-light);
|
||||
}
|
||||
|
||||
.title + .details {
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
</style>
|
@ -1,50 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="frontmatter.footer" class="footer">
|
||||
<div class="container">
|
||||
<p class="text">{{ frontmatter.footer }}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
margin: 0 auto;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.footer {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2rem 1.5rem 2.25rem;
|
||||
}
|
||||
|
||||
.home-hero + .footer .container,
|
||||
.home-features + .footer .container,
|
||||
.home-content + .footer .container {
|
||||
border-top: 1px solid var(--c-divider);
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.container {
|
||||
padding: 3rem 1.5rem 3.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
font-size: 0.9rem;
|
||||
color: var(--c-text-light);
|
||||
}
|
||||
</style>
|
@ -1,161 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData, withBase } from 'vitepress'
|
||||
import NavLink from './NavLink.vue'
|
||||
|
||||
const { site, frontmatter } = useData()
|
||||
|
||||
const showHero = computed(() => {
|
||||
const { heroImage, heroText, tagline, actionLink, actionText } =
|
||||
frontmatter.value
|
||||
return heroImage || heroText || tagline || (actionLink && actionText)
|
||||
})
|
||||
|
||||
const heroText = computed(() => frontmatter.value.heroText || site.value.title)
|
||||
const tagline = computed(
|
||||
() => frontmatter.value.tagline || site.value.description
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header v-if="showHero" class="home-hero">
|
||||
<figure v-if="frontmatter.heroImage" class="figure">
|
||||
<img
|
||||
class="image"
|
||||
:src="withBase(frontmatter.heroImage)"
|
||||
:alt="frontmatter.heroAlt"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<h1 v-if="heroText" id="main-title" class="title">{{ heroText }}</h1>
|
||||
<p v-if="tagline" class="tagline">{{ tagline }}</p>
|
||||
|
||||
<NavLink
|
||||
v-if="frontmatter.actionLink && frontmatter.actionText"
|
||||
:item="{ link: frontmatter.actionLink, text: frontmatter.actionText }"
|
||||
class="action"
|
||||
/>
|
||||
|
||||
<NavLink
|
||||
v-if="frontmatter.altActionLink && frontmatter.altActionText"
|
||||
:item="{
|
||||
link: frontmatter.altActionLink,
|
||||
text: frontmatter.altActionText
|
||||
}"
|
||||
class="action alt"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero {
|
||||
margin: 2.5rem 0 2.75rem;
|
||||
padding: 0 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.home-hero {
|
||||
margin: 3.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.home-hero {
|
||||
margin: 4rem 0 4.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.figure {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 280px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.title {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tagline {
|
||||
margin: 0;
|
||||
margin-top: 0.25rem;
|
||||
line-height: 1.3;
|
||||
font-size: 1.2rem;
|
||||
color: var(--c-text-light);
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.tagline {
|
||||
line-height: 1.2;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 1.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.action.alt {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.action {
|
||||
margin-top: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.action :deep(.item) {
|
||||
display: inline-block;
|
||||
border-radius: 6px;
|
||||
padding: 0 20px;
|
||||
line-height: 44px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--c-bg);
|
||||
background-color: var(--c-brand);
|
||||
border: 2px solid var(--c-brand);
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
.action.alt :deep(.item) {
|
||||
background-color: var(--c-bg);
|
||||
color: var(--c-brand);
|
||||
}
|
||||
|
||||
.action :deep(.item:hover) {
|
||||
text-decoration: none;
|
||||
color: var(--c-bg);
|
||||
background-color: var(--c-brand-light);
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.action :deep(.item) {
|
||||
padding: 0 24px;
|
||||
line-height: 52px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,60 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { theme, page } = useData()
|
||||
|
||||
const hasLastUpdated = computed(() => {
|
||||
const lu = theme.value.lastUpdated
|
||||
|
||||
return lu !== undefined && lu !== false && page.value.lastUpdated !== 0
|
||||
})
|
||||
|
||||
const prefix = computed(() => {
|
||||
const p = theme.value.lastUpdated
|
||||
return p === true ? 'Last Updated' : p
|
||||
})
|
||||
|
||||
const datetime = ref('')
|
||||
onMounted(() => {
|
||||
watchEffect(() => {
|
||||
// locale string might be different based on end user
|
||||
// and will lead to potential hydration mismatch if calculated at build time
|
||||
datetime.value = new Date(page.value.lastUpdated!).toLocaleString('en-US')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p v-if="hasLastUpdated" class="last-updated">
|
||||
<span class="prefix">{{ prefix }}:</span>
|
||||
<span class="datetime">{{ datetime }}</span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.last-updated {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
font-size: 0.9rem;
|
||||
color: var(--c-text-light);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.last-updated {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.prefix {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
@ -1,60 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import NavBarTitle from './NavBarTitle.vue'
|
||||
import NavLinks from './NavLinks.vue'
|
||||
import ToggleSideBarButton from './ToggleSideBarButton.vue'
|
||||
|
||||
defineEmits(['toggle'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="nav-bar">
|
||||
<ToggleSideBarButton @toggle="$emit('toggle')" />
|
||||
|
||||
<NavBarTitle />
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
||||
<div class="nav">
|
||||
<NavLinks />
|
||||
</div>
|
||||
|
||||
<slot name="search" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-index-navbar);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--c-divider);
|
||||
padding: 0.7rem 1.5rem 0.7rem 4rem;
|
||||
height: var(--header-height);
|
||||
background-color: var(--c-bg);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.nav-bar {
|
||||
padding: 0.7rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.nav {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,41 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { withBase, useData } from 'vitepress'
|
||||
const { site, theme, localePath } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="nav-bar-title"
|
||||
:href="localePath"
|
||||
:aria-label="`${site.title}, back to home`"
|
||||
>
|
||||
<img
|
||||
v-if="theme.logo"
|
||||
class="logo"
|
||||
:src="withBase(theme.logo)"
|
||||
alt="Logo"
|
||||
/>
|
||||
{{ site.title }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav-bar-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-bar-title:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: 0.75rem;
|
||||
height: 1.3rem;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
</style>
|
@ -1,135 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vitepress'
|
||||
import type { DefaultTheme } from '../config'
|
||||
import NavDropdownLinkItem from './NavDropdownLinkItem.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithChildren
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
open.value = false
|
||||
}
|
||||
)
|
||||
|
||||
function toggle() {
|
||||
open.value = !open.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="nav-dropdown-link" :class="{ open }">
|
||||
<button class="button" :aria-label="item.ariaLabel" @click="toggle">
|
||||
<span class="button-text">{{ item.text }}</span>
|
||||
<span class="button-arrow" :class="open ? 'down' : 'right'" />
|
||||
</button>
|
||||
|
||||
<ul class="dialog">
|
||||
<li v-for="item in item.items" :key="item.text" class="dialog-item">
|
||||
<NavDropdownLinkItem :item="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav-dropdown-link {
|
||||
position: relative;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.nav-dropdown-link {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.nav-dropdown-link:hover .dialog {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-dropdown-link.open {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
border: 0;
|
||||
padding: 0 1.5rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
line-height: 36px;
|
||||
font-family: var(--font-family-base);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text);
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.button {
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 0;
|
||||
line-height: 24px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.button-arrow {
|
||||
display: inline-block;
|
||||
margin-top: -1px;
|
||||
margin-left: 8px;
|
||||
border-top: 6px solid #ccc;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 0;
|
||||
border-left: 4px solid transparent;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.button-arrow.right {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.button-arrow.right {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.dialog {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
right: -8px;
|
||||
border-radius: 6px;
|
||||
padding: 12px 0;
|
||||
min-width: 128px;
|
||||
background-color: var(--c-bg);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,76 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from 'vue'
|
||||
import type { DefaultTheme } from '../config'
|
||||
import { useNavLink } from '../composables/navLink'
|
||||
import OutboundLink from './icons/OutboundLink.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: DefaultTheme.NavItemWithLink
|
||||
}>()
|
||||
|
||||
const propsRefs = toRefs(props)
|
||||
|
||||
const { props: linkProps, isExternal } = useNavLink(propsRefs.item)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="nav-dropdown-link-item">
|
||||
<a class="item" v-bind="linkProps">
|
||||
<span class="arrow" />
|
||||
<span class="text">{{ item.text }}</span>
|
||||
<span class="icon"><OutboundLink v-if="isExternal" /></span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
display: block;
|
||||
padding: 0 1.5rem 0 2.5rem;
|
||||
line-height: 32px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--c-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.item {
|
||||
padding: 0 24px 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--c-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item.active .arrow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item:hover,
|
||||
.item.active {
|
||||
text-decoration: none;
|
||||
color: var(--c-brand);
|
||||
}
|
||||
|
||||
.item.external:hover {
|
||||
border-bottom-color: transparent;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.arrow {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
border-top: 6px solid #ccc;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 0;
|
||||
border-left: 4px solid transparent;
|
||||
vertical-align: middle;
|
||||
opacity: 0;
|
||||
transform: translateY(-2px) rotate(-90deg);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,61 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from 'vue'
|
||||
import type { DefaultTheme } from '../config'
|
||||
import { useNavLink } from '../composables/navLink'
|
||||
import OutboundLink from './icons/OutboundLink.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: DefaultTheme.NavItemWithLink
|
||||
}>()
|
||||
|
||||
const propsRefs = toRefs(props)
|
||||
|
||||
const { props: linkProps, isExternal } = useNavLink(propsRefs.item)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="nav-link">
|
||||
<a class="item" v-bind="linkProps">
|
||||
{{ item.text }} <OutboundLink v-if="isExternal" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
display: block;
|
||||
padding: 0 1.5rem;
|
||||
line-height: 36px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item:hover,
|
||||
.item.active {
|
||||
text-decoration: none;
|
||||
color: var(--c-brand);
|
||||
}
|
||||
|
||||
.item.external:hover {
|
||||
border-bottom-color: transparent;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.item {
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 0;
|
||||
line-height: 24px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item:hover,
|
||||
.item.active {
|
||||
border-bottom-color: var(--c-brand);
|
||||
color: var(--c-text);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,52 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { useLanguageLinks } from '../composables/nav'
|
||||
import { useRepo } from '../composables/repo'
|
||||
import NavLink from './NavLink.vue'
|
||||
import NavDropdownLink from './NavDropdownLink.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
const localeLinks = useLanguageLinks()
|
||||
const repo = useRepo()
|
||||
const show = computed(() => theme.value.nav || repo.value || localeLinks.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="show" class="nav-links">
|
||||
<template v-if="theme.nav">
|
||||
<div v-for="item in theme.nav" :key="item.text" class="item">
|
||||
<NavDropdownLink v-if="item.items" :item="item" />
|
||||
<NavLink v-else :item="item" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="localeLinks" class="item">
|
||||
<NavDropdownLink :item="localeLinks" />
|
||||
</div>
|
||||
|
||||
<div v-if="repo" class="item">
|
||||
<NavLink :item="repo" />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav-links {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--c-divider);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.nav-links {
|
||||
display: flex;
|
||||
padding: 6px 0 0;
|
||||
align-items: center;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,88 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { withBase } from 'vitepress'
|
||||
import { useNextAndPrevLinks } from '../composables/nextAndPrevLinks'
|
||||
import ArrowLeft from './icons/ArrowLeft.vue'
|
||||
import ArrowRight from './icons/ArrowRight.vue'
|
||||
|
||||
const { hasLinks, prev, next } = useNextAndPrevLinks()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasLinks" class="next-and-prev-link">
|
||||
<div class="container">
|
||||
<div class="prev">
|
||||
<a v-if="prev" class="link" :href="withBase(prev.link)">
|
||||
<ArrowLeft class="icon icon-prev" />
|
||||
<span class="text">{{ prev.text }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="next">
|
||||
<a v-if="next" class="link" :href="withBase(next.link)">
|
||||
<span class="text">{{ next.text }}</span>
|
||||
<ArrowRight class="icon icon-next" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.next-and-prev-link {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid var(--c-divider);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.prev,
|
||||
.next {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.prev {
|
||||
justify-content: flex-start;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.next {
|
||||
justify-content: flex-end;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: var(--c-text);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.icon-prev {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.icon-next {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
@ -1,53 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import PageFooter from './PageFooter.vue'
|
||||
import NextAndPrevLinks from './NextAndPrevLinks.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="page">
|
||||
<div class="container">
|
||||
<slot name="top" />
|
||||
|
||||
<Content class="content" />
|
||||
<PageFooter />
|
||||
<NextAndPrevLinks />
|
||||
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding-top: var(--header-height);
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.page {
|
||||
margin-left: 16.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.page {
|
||||
margin-left: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem 4rem;
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.content {
|
||||
/* fix carbon ads display */
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,44 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import EditLink from './EditLink.vue'
|
||||
import LastUpdated from './LastUpdated.vue'
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { page } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="page-footer">
|
||||
<div class="edit">
|
||||
<EditLink />
|
||||
</div>
|
||||
<div class="updated">
|
||||
<LastUpdated v-if="page.lastUpdated" />
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-footer {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.page-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.updated {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.updated {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,60 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import NavLinks from './NavLinks.vue'
|
||||
import SideBarLinks from './SideBarLinks.vue'
|
||||
|
||||
defineProps<{ open: boolean }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="sidebar" :class="{ open }">
|
||||
<NavLinks class="nav" />
|
||||
|
||||
<slot name="sidebar-top" />
|
||||
|
||||
<SideBarLinks />
|
||||
|
||||
<slot name="sidebar-bottom" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-index-sidebar);
|
||||
border-right: 1px solid var(--c-divider);
|
||||
width: 16.4rem;
|
||||
background-color: var(--c-bg);
|
||||
overflow-y: auto;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.sidebar {
|
||||
width: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,100 +0,0 @@
|
||||
import { FunctionalComponent, h, VNode } from 'vue'
|
||||
import { useRoute, useData } from 'vitepress'
|
||||
import { Header } from '../../shared'
|
||||
import { DefaultTheme } from '../config'
|
||||
import { joinUrl, isActive } from '../utils'
|
||||
|
||||
interface HeaderWithChildren extends Header {
|
||||
children?: Header[]
|
||||
}
|
||||
|
||||
export const SideBarLink: FunctionalComponent<{
|
||||
item: DefaultTheme.SideBarItem
|
||||
depth?: number
|
||||
}> = (props) => {
|
||||
const route = useRoute()
|
||||
const { site, frontmatter } = useData()
|
||||
const depth = props.depth || 1
|
||||
const maxDepth = frontmatter.value.sidebarDepth || Infinity
|
||||
|
||||
const headers = route.data.headers
|
||||
const text = props.item.text
|
||||
const link = resolveLink(site.value.base, props.item.link)
|
||||
const children = (props.item as DefaultTheme.SideBarGroup).children
|
||||
const active = isActive(route, props.item.link)
|
||||
const childItems =
|
||||
depth < maxDepth
|
||||
? createChildren(active, children, headers, depth + 1)
|
||||
: null
|
||||
|
||||
return h('li', { class: 'sidebar-link' }, [
|
||||
h(
|
||||
link ? 'a' : 'p',
|
||||
{
|
||||
class: { 'sidebar-link-item': true, active },
|
||||
href: link
|
||||
},
|
||||
text
|
||||
),
|
||||
childItems
|
||||
])
|
||||
}
|
||||
|
||||
function resolveLink(base: string, path?: string): string | undefined {
|
||||
if (path === undefined) {
|
||||
return path
|
||||
}
|
||||
|
||||
// keep relative hash to the same page
|
||||
if (path.startsWith('#')) {
|
||||
return path
|
||||
}
|
||||
|
||||
return joinUrl(base, path)
|
||||
}
|
||||
|
||||
function createChildren(
|
||||
active: boolean,
|
||||
children?: DefaultTheme.SideBarItem[],
|
||||
headers?: Header[],
|
||||
depth = 1
|
||||
): VNode | null {
|
||||
if (children && children.length > 0) {
|
||||
return h(
|
||||
'ul',
|
||||
{ class: 'sidebar-links' },
|
||||
children.map((c) => {
|
||||
return h(SideBarLink, { item: c, depth })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return active && headers
|
||||
? createChildren(false, resolveHeaders(headers), undefined, depth)
|
||||
: null
|
||||
}
|
||||
|
||||
function resolveHeaders(headers: Header[]): DefaultTheme.SideBarItem[] {
|
||||
return mapHeaders(groupHeaders(headers))
|
||||
}
|
||||
|
||||
function groupHeaders(headers: Header[]): HeaderWithChildren[] {
|
||||
headers = headers.map((h) => Object.assign({}, h))
|
||||
let lastH2: HeaderWithChildren
|
||||
headers.forEach((h) => {
|
||||
if (h.level === 2) {
|
||||
lastH2 = h
|
||||
} else if (lastH2) {
|
||||
;(lastH2.children || (lastH2.children = [])).push(h)
|
||||
}
|
||||
})
|
||||
return headers.filter((h) => h.level === 2)
|
||||
}
|
||||
|
||||
function mapHeaders(headers: HeaderWithChildren[]): DefaultTheme.SideBarItem[] {
|
||||
return headers.map((header) => ({
|
||||
text: header.title,
|
||||
link: `#${header.slug}`,
|
||||
children: header.children ? mapHeaders(header.children) : undefined
|
||||
}))
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useSideBar } from '../composables/sideBar'
|
||||
import { SideBarLink } from './SideBarLink'
|
||||
|
||||
const items = useSideBar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul v-if="items.length > 0" class="sidebar-links">
|
||||
<SideBarLink v-for="item of items" :item="item" />
|
||||
</ul>
|
||||
</template>
|
@ -1,46 +0,0 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
emits: ['toggle']
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-button" @click="$emit('toggle')">
|
||||
<svg
|
||||
class="icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"
|
||||
class
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.sidebar-button {
|
||||
position: absolute;
|
||||
top: 0.6rem;
|
||||
left: 1rem;
|
||||
display: none;
|
||||
padding: 0.6rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-button .icon {
|
||||
display: block;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 719px) {
|
||||
.sidebar-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import docsearch from '@docsearch/js'
|
||||
import { DocSearchHit } from '@docsearch/react/dist/esm/types'
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter, useRoute, useData } from 'vitepress'
|
||||
import { DefaultTheme } from '../config'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { theme } = useData()
|
||||
|
||||
onMounted(() => {
|
||||
initialize(theme.value.algolia)
|
||||
setTimeout(poll, 16)
|
||||
})
|
||||
|
||||
function poll() {
|
||||
// programmatically open the search box after initialize
|
||||
const e = new Event('keydown') as any
|
||||
|
||||
e.key = 'k'
|
||||
e.metaKey = true
|
||||
|
||||
window.dispatchEvent(e)
|
||||
|
||||
setTimeout(() => {
|
||||
if (!document.querySelector('.DocSearch-Modal')) {
|
||||
poll()
|
||||
}
|
||||
}, 16)
|
||||
}
|
||||
|
||||
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
|
||||
// note: multi-lang search support is removed since the theme
|
||||
// doesn't support multiple locales as of now.
|
||||
const options = Object.assign({}, userOptions, {
|
||||
container: '#docsearch',
|
||||
|
||||
navigator: {
|
||||
navigate({ itemUrl }: { itemUrl: string }) {
|
||||
const { pathname: hitPathname } = new URL(
|
||||
window.location.origin + itemUrl
|
||||
)
|
||||
|
||||
// router doesn't handle same-page navigation so we use the native
|
||||
// browser location API for anchor navigation
|
||||
if (route.path === hitPathname) {
|
||||
window.location.assign(window.location.origin + itemUrl)
|
||||
} else {
|
||||
router.go(itemUrl)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transformItems(items: DocSearchHit[]) {
|
||||
return items.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
url: getRelativePath(item.url)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
hitComponent({ hit, children }: { hit: DocSearchHit, children: any }) {
|
||||
const relativeHit = hit.url.startsWith('http')
|
||||
? getRelativePath(hit.url as string)
|
||||
: hit.url
|
||||
|
||||
return {
|
||||
__v: null,
|
||||
type: 'a',
|
||||
ref: undefined,
|
||||
constructor: undefined,
|
||||
key: undefined,
|
||||
|
||||
props: {
|
||||
href: hit.url,
|
||||
|
||||
onClick(event: MouseEvent) {
|
||||
if (isSpecialClick(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
// we rely on the native link scrolling when user is already on
|
||||
// the right anchor because Router doesn't support duplicated
|
||||
// history entries.
|
||||
if (route.path === relativeHit) {
|
||||
return
|
||||
}
|
||||
|
||||
// if the hits goes to another page, we prevent the native link
|
||||
// behavior to leverage the Router loading feature.
|
||||
if (route.path !== relativeHit) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
router.go(relativeHit)
|
||||
},
|
||||
|
||||
children
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
docsearch(options)
|
||||
}
|
||||
|
||||
function isSpecialClick(event: MouseEvent) {
|
||||
return (
|
||||
event.button === 1 ||
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey
|
||||
)
|
||||
}
|
||||
|
||||
function getRelativePath(absoluteUrl: string) {
|
||||
const { pathname, hash } = new URL(absoluteUrl)
|
||||
|
||||
return pathname + hash
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="docsearch" />
|
||||
</template>
|
@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="show" class="VPBackdrop" />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPBackdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--vp-z-backdrop);
|
||||
background: rgba(0, 0, 0, .6);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.VPBackdrop.fade-enter-from,
|
||||
.VPBackdrop.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.VPBackdrop.fade-leave-active {
|
||||
transition-duration: .25s;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPBackdrop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vitepress'
|
||||
import { useSidebar } from '../composables/sidebar'
|
||||
import NotFound from '../NotFound.vue'
|
||||
import VPContentDoc from './VPContentDoc.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const { hasSidebar } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPContent"
|
||||
id="VPContent"
|
||||
:class="{ 'has-sidebar': hasSidebar }"
|
||||
>
|
||||
<NotFound v-if="route.component === NotFound" />
|
||||
<VPContentDoc v-else :class="{ 'has-sidebar': hasSidebar }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media (max-width: 768px) {
|
||||
.VPContent {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPContent {
|
||||
padding-top: var(--vp-nav-height-desktop);
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
padding-left: var(--vp-sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPContent.has-sidebar {
|
||||
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import VPContentDocOutline from './VPContentDocOutline.vue'
|
||||
import VPContentDocFooter from './VPContentDocFooter.vue'
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
const pageName = computed(() => {
|
||||
return page.value.relativePath.slice(0, page.value.relativePath.indexOf('/'))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPContentDoc has-aside">
|
||||
<div class="container">
|
||||
<div class="aside">
|
||||
<div class="aside-container">
|
||||
<div class="aside-curtain" />
|
||||
<div class="aside-content">
|
||||
<VPContentDocOutline v-if="page.headers" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<main class="main">
|
||||
<Content class="vp-doc" :class="pageName" />
|
||||
</main>
|
||||
|
||||
<VPContentDocFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPContentDoc {
|
||||
padding: 32px 24px 96px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPContentDoc {
|
||||
padding: 48px 32px 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPContentDoc {
|
||||
padding: 32px 64px 96px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPContentDoc {
|
||||
padding: 32px 0 128px 64px;
|
||||
}
|
||||
|
||||
.VPContentDoc:not(.has-sidebar.has-aside) {
|
||||
padding-left: calc((100vw - 688px) / 2);
|
||||
}
|
||||
|
||||
.VPContentDoc.has-aside:not(.has-sidebar) {
|
||||
padding-left: calc((100vw - 688px - 320px) / 2);
|
||||
}
|
||||
|
||||
.VPContentDoc:not(.has-aside) .content {
|
||||
min-width: 688px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPContentDoc {
|
||||
padding: 32px 0 128px 96px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.aside {
|
||||
position: relative;
|
||||
display: none;
|
||||
order: 2;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
padding-left: 64px;
|
||||
padding-right: 32px;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.aside {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.aside {
|
||||
padding-left: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
.aside-container {
|
||||
position: fixed;
|
||||
top: var(--vp-nav-height-desktop);
|
||||
bottom: 0;
|
||||
padding-top: 32px;
|
||||
width: 224px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.aside-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.aside-container {
|
||||
width: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
.aside-curtain {
|
||||
position: fixed;
|
||||
top: var(--vp-nav-height-desktop);
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: linear-gradient(var(--vp-c-bg-content), transparent);
|
||||
}
|
||||
|
||||
.aside-content {
|
||||
padding-bottom: 96px;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
max-width: 688px;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.content {
|
||||
order: 1;
|
||||
margin: 0;
|
||||
min-width: 632px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
margin: 0 0 32px;
|
||||
}
|
||||
|
||||
.edit-link .vt-link {
|
||||
font-size: 14px;
|
||||
color: var(--vt-c-brand);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vt-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--vt-c-brand);
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import { normalizeLink } from '../support/utils'
|
||||
import { useEditLink } from '../composables/edit-link'
|
||||
import { usePrevNext } from '../composables/prev-next'
|
||||
import VPIconEdit from './icons/VPIconEdit.vue'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
const { theme, frontmatter } = useData()
|
||||
|
||||
const editLink = useEditLink()
|
||||
const control = usePrevNext()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="control.prev || control.next" class="VPContentDocFooter">
|
||||
<div v-if="theme.editLink && frontmatter.editLink !== false" class="edit-link">
|
||||
<VPLink class="edit-link-button" :href="editLink.url" :no-icon="true">
|
||||
<VPIconEdit class="edit-link-icon" />
|
||||
{{ editLink.text }}
|
||||
</VPLink>
|
||||
</div>
|
||||
|
||||
<div class="prev-next">
|
||||
<div class="pager">
|
||||
<a v-if="control.prev" class="pager-link prev" :href="normalizeLink(control.prev.link)">
|
||||
<span class="desc">Previous page</span>
|
||||
<span class="title">{{ control.prev.text }} </span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pager" :class="{ 'has-prev': control.prev }">
|
||||
<a v-if="control.next" class="pager-link next" :href="normalizeLink(control.next.link)">
|
||||
<span class="desc">Next page</span>
|
||||
<span class="title">{{ control.next.text }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPContentDocFooter {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.edit-link-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
padding: 10px 0;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.edit-link-button:hover {
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.edit-link-icon {
|
||||
margin-right: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.prev-next {
|
||||
border-top: 1px solid var(--vp-c-divider-light);
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prev-next {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.pager.has-prev {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.pager {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.pager.has-prev {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.pager-link {
|
||||
display: block;
|
||||
border: 1px solid var(--vp-c-divider-light);
|
||||
border-radius: 8px;
|
||||
padding: 11px 16px 13px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.pager-link:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.pager-link:hover .title {
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.pager-link.next {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { resolveHeaders, useActiveAnchor } from '../composables/outline'
|
||||
|
||||
const { page, frontmatter } = useData()
|
||||
|
||||
const container = ref()
|
||||
const marker = ref()
|
||||
|
||||
useActiveAnchor(container, marker)
|
||||
|
||||
const resolvedHeaders = computed(() => {
|
||||
return resolveHeaders(page.value.headers)
|
||||
})
|
||||
|
||||
function handleClick({ target: el }: Event) {
|
||||
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
|
||||
const heading = document.querySelector(id) as HTMLAnchorElement
|
||||
heading?.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPContentDocOutline" ref="container">
|
||||
<div class="outline-marker" ref="marker" />
|
||||
|
||||
<div class="outline-title">On this page</div>
|
||||
|
||||
<nav aria-labelledby="doc-outline-aria-label">
|
||||
<span class="visually-hidden" id="doc-outline-aria-label">
|
||||
Table of Contents for current page
|
||||
</span>
|
||||
|
||||
<ul class="root">
|
||||
<li
|
||||
v-for="{ text, link, children, hidden } in resolvedHeaders"
|
||||
v-show="!hidden"
|
||||
>
|
||||
<a class="outline-link" :href="link" @click="handleClick">
|
||||
{{ text }}
|
||||
</a>
|
||||
<ul v-if="children && frontmatter.outline === 'deep'">
|
||||
<li v-for="{ text, link, hidden } in children" v-show="!hidden">
|
||||
<a class="outline-link nested" :href="link" @click="handleClick">
|
||||
{{ text }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPContentDocOutline {
|
||||
position: relative;
|
||||
border-left: 1px solid var(--vp-c-divider-light);
|
||||
padding-left: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.outline-marker {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -1px;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background-color: var(--vp-c-brand);
|
||||
transition: top 0.25s cubic-bezier(0, 1, 0.5, 1), background-color 0.5s, opacity 0.25s;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 28px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.outline-link {
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.outline-link:hover,
|
||||
.outline-link.active {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.outline-link.nested {
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,149 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
import { useFlyout } from '../composables/flyout'
|
||||
import VPIconChevronDown from './icons/VPIconChevronDown.vue'
|
||||
import VPIconMoreHorizontal from './icons/VPIconMoreHorizontal.vue'
|
||||
import VPMenu from './VPMenu.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
icon?: any
|
||||
button?: string
|
||||
label?: string
|
||||
items?: any[]
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const el = ref<HTMLElement>()
|
||||
|
||||
useFlyout({ el, onBlur })
|
||||
|
||||
function onBlur() {
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPFlyout"
|
||||
ref="el"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="open"
|
||||
:aria-label="label"
|
||||
@click="open = !open"
|
||||
>
|
||||
<span v-if="button || icon" class="text">
|
||||
<component v-if="icon" :is="icon" class="option-icon" />
|
||||
{{ button }}
|
||||
<VPIconChevronDown class="text-icon" />
|
||||
</span>
|
||||
|
||||
<VPIconMoreHorizontal v-else class="icon" />
|
||||
</button>
|
||||
|
||||
<div class="menu">
|
||||
<VPMenu :items="items">
|
||||
<slot />
|
||||
</VPMenu>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPFlyout {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VPFlyout:hover {
|
||||
color: var(--vp-c-bland);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPFlyout:hover .text {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPFlyout:hover .icon {
|
||||
fill: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPFlyout:hover .menu,
|
||||
.button[aria-expanded="true"] + .menu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: var(--vp-nav-height-mobile);
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.button {
|
||||
height: var(--vp-nav-height-desktop);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: var(--vp-nav-height-mobile);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.text {
|
||||
line-height: var(--vp-nav-height-desktop);
|
||||
}
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
margin-right: 0px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
margin-left: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
transition: fill 0.25s;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: calc(var(--vp-nav-height-mobile) / 2 + 20px);
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.25s, visibility 0.25s, transform 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.menu {
|
||||
top: calc(var(--vp-nav-height-desktop) / 2 + 20px);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import VPIconExternalLink from './icons/VPIconExternalLink.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
href?: string
|
||||
noIcon?: boolean
|
||||
}>()
|
||||
|
||||
const isExternal = computed(() => props.href && /^[a-z]+:/i.test(props.href))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="href ? 'a' : 'span'"
|
||||
class="VPLink"
|
||||
:class="{ link: href }"
|
||||
:href="href"
|
||||
:target="isExternal ? '_blank' : undefined"
|
||||
:rel="isExternal ? 'noopener noreferrer' : undefined"
|
||||
>
|
||||
<slot />
|
||||
<VPIconExternalLink v-if="isExternal && !noIcon" class="icon" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: inline-block;
|
||||
margin-top: -1px;
|
||||
margin-left: 4px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
fill: var(--vp-c-text-3);
|
||||
transition: fill 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,106 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSidebar } from '../composables/sidebar'
|
||||
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'open-menu'): void
|
||||
}>()
|
||||
|
||||
const { hasSidebar } = useSidebar()
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasSidebar" class="VPLocalNav">
|
||||
<button
|
||||
class="menu"
|
||||
:aria-expanded="open"
|
||||
aria-controls="VPSidebarNav"
|
||||
@click="$emit('open-menu')"
|
||||
>
|
||||
<VPIconAlignLeft class="menu-icon" />
|
||||
<span class="menu-text">Menu</span>
|
||||
</button>
|
||||
|
||||
<a class="top-link" href="#" @click="scrollToTop">
|
||||
Return to top
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPLocalNav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-local-nav);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--vp-c-divider-light);
|
||||
width: 100%;
|
||||
background-color: var(--vp-c-bg-content);
|
||||
transition: border-color 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPLocalNav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
line-height: 47px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.menu:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menu {
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.top-link {
|
||||
padding: 0 24px;
|
||||
line-height: 47px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.top-link:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.top-link {
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
import VPMenuLink from './VPMenuLink.vue'
|
||||
import VPMenuGroup from './VPMenuGroup.vue'
|
||||
|
||||
defineProps<{
|
||||
items?: any[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPMenu">
|
||||
<div v-if="items" class="items">
|
||||
<template v-for="item in items" :key="item.text">
|
||||
<VPMenuLink v-if="'link' in item" :item="item" />
|
||||
<VPMenuGroup v-else :text="item.text" :items="item.items" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPMenu {
|
||||
border-radius: 8px;
|
||||
padding: 12px 0;
|
||||
min-width: 192px;
|
||||
border: 1px solid transparent;
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
.dark .VPMenu {
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
border: 1px solid var(--vp-c-divider-light);
|
||||
}
|
||||
|
||||
.VPMenu :deep(.items) {
|
||||
transition: border-color .5s;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group) {
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group + .group) {
|
||||
border-top: 1px solid var(--vp-c-divider-light);
|
||||
padding: 11px 0 12px;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group:last-child) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.group + .item) {
|
||||
border-top: 1px solid var(--vp-c-divider-light);
|
||||
padding: 11px 16px 0;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.item) {
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.label) {
|
||||
flex-grow: 1;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color .5s;
|
||||
}
|
||||
|
||||
.VPMenu :deep(.action) {
|
||||
padding-left: 24px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import VPMenuLink from './VPMenuLink.vue'
|
||||
|
||||
defineProps<{
|
||||
text?: string
|
||||
items: any[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPMenuGroup">
|
||||
<p v-if="text" class="title">{{ text }}</p>
|
||||
|
||||
<template v-for="item in items">
|
||||
<VPMenuLink v-if="'link' in item" :item="item" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPMenuGroup + .VPMenuGroup {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--vp-c-divider-light);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 16px;
|
||||
line-height: 28px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
item: any
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink class="VPMenuLink" :href="item.link">
|
||||
{{ item.text }}
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPMenuLink {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
line-height: 28px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPMenuLink:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { provide } from 'vue'
|
||||
import { useNav } from '../composables/nav'
|
||||
import VPNavBar from './VPNavBar.vue'
|
||||
import VPNavScreen from './VPNavScreen.vue'
|
||||
|
||||
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
|
||||
|
||||
provide('close-screen', closeScreen)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="VPNav">
|
||||
<VPNavBar :is-screen-open="isScreenOpen" @toggle-screen="toggleScreen" />
|
||||
<VPNavScreen :open="isScreenOpen" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNav {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-nav);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNav {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,109 @@
|
||||
<script lang="ts" setup>
|
||||
import VPNavBarTitle from './VPNavBarTitle.vue'
|
||||
import VPNavBarSearch from './VPNavBarSearch.vue'
|
||||
import VPNavBarMenu from './VPNavBarMenu.vue'
|
||||
import VPNavBarTranslations from './VPNavBarTranslations.vue'
|
||||
import VPNavBarAppearance from './VPNavBarAppearance.vue'
|
||||
import VPNavBarSocialLinks from './VPNavBarSocialLinks.vue'
|
||||
import VPNavBarExtra from './VPNavBarExtra.vue'
|
||||
import VPNavBarHamburger from './VPNavBarHamburger.vue'
|
||||
|
||||
defineProps<{
|
||||
isScreenOpen: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'toggle-screen'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavBar">
|
||||
<div class="container">
|
||||
<VPNavBarTitle />
|
||||
|
||||
<div class="content">
|
||||
<VPNavBarSearch class="search" />
|
||||
<VPNavBarMenu class="menu" />
|
||||
<VPNavBarTranslations class="translations" />
|
||||
<VPNavBarAppearance class="appearance" />
|
||||
<VPNavBarSocialLinks class="social-links" />
|
||||
<VPNavBarExtra class="extra" />
|
||||
<VPNavBarHamburger
|
||||
class="hamburger"
|
||||
:active="isScreenOpen"
|
||||
@click="$emit('toggle-screen')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBar {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--vp-c-divider-light);
|
||||
padding: 0 8px 0 24px;
|
||||
height: var(--vp-nav-height-mobile);
|
||||
background-color: var(--vt-c-bg);
|
||||
transition: border-color 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBar {
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar {
|
||||
height: var(--vp-nav-height-desktop);
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
max-width: var(--vp-layout-max-width);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.content {
|
||||
backdrop-filter: saturate(50%) blur(8px);
|
||||
}
|
||||
}
|
||||
|
||||
.menu + .translations::before,
|
||||
.menu + .appearance::before,
|
||||
.translations + .appearance::before,
|
||||
.appearance + .social-links::before {
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--vp-c-divider-light);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.menu + .appearance::before,
|
||||
.translations + .appearance::before {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.appearance + .social-links::before {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
margin-right: -8px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import VPSwitchAppearance from './VPSwitchAppearance.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavBarAppearance">
|
||||
<VPSwitchAppearance />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarAppearance {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarAppearance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,107 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from 'vitepress'
|
||||
import VPFlyout from './VPFlyout.vue'
|
||||
import VPSwitchAppearance from './VPSwitchAppearance.vue'
|
||||
import VPSocialLinks from './VPSocialLinks.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFlyout class="VPNavBarExtra" label="extra navigation">
|
||||
<div v-if="theme.localeLinks" class="group">
|
||||
<div class="trans">
|
||||
<p class="trans-title">
|
||||
{{ theme.localeLinks.text }}
|
||||
</p>
|
||||
|
||||
<ul class="trans-list">
|
||||
<li v-for="locale in theme.localeLinks.items" :key="locale.link" class="trans-item">
|
||||
<a class="trans-link" :href="locale.link">{{ locale.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="item">
|
||||
<p class="label">Appearance</p>
|
||||
<div class="appearance-action">
|
||||
<VPSwitchAppearance />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="theme.socialLinks" class="group">
|
||||
<div class="item">
|
||||
<VPSocialLinks class="social-links" :links="theme.socialLinks" />
|
||||
</div>
|
||||
</div>
|
||||
</VPFlyout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarExtra {
|
||||
display: none;
|
||||
margin-right: -12px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarExtra {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarExtra {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.trans {
|
||||
padding: 2px 16px 0;
|
||||
}
|
||||
|
||||
.trans-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.trans-icon {
|
||||
margin-right: 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.trans-list {
|
||||
padding: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.trans-link {
|
||||
line-height: 28px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.trans-link:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.appearance-action {
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
margin: -4px -8px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
active: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="VPNavBarHamburger"
|
||||
:class="{ active }"
|
||||
aria-label="mobile navigation"
|
||||
:aria-expanded="active"
|
||||
aria-controls="VPNavScreen"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<span class="container">
|
||||
<span class="top" />
|
||||
<span class="middle" />
|
||||
<span class="bottom" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarHamburger {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 48px;
|
||||
height: var(--vp-nav-height);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarHamburger {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container:hover .top { top: 0; left: 0; transform: translateX(4px); }
|
||||
.container:hover .middle { top: 6; left: 0; transform: translateX(0); }
|
||||
.container:hover .bottom { top: 12px; left: 0; transform: translateX(8px); }
|
||||
|
||||
.container.active .top { top: 6px; transform: translateX(0) rotate(225deg); }
|
||||
.container.active .middle { top: 6px; transform: translateX(16px); }
|
||||
.container.active .bottom { top: 6px; transform: translateX(0) rotate(135deg); }
|
||||
|
||||
.container.active:hover .top,
|
||||
.container.active:hover .middle,
|
||||
.container.active:hover .bottom {
|
||||
background-color: var(--vt-c-text-2);
|
||||
transition: top .25s, background-color .25s, transform .25s;
|
||||
}
|
||||
|
||||
.top,
|
||||
.middle,
|
||||
.bottom {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
background-color: var(--vp-c-text-1);
|
||||
transition: top .25s, background-color .5s, transform .25s;
|
||||
}
|
||||
|
||||
.top { top: 0; left: 0; transform: translateX(0); }
|
||||
.middle { top: 6px; left: 0; transform: translateX(8px); }
|
||||
.bottom { top: 12px; left: 0; transform: translateX(4px); }
|
||||
</style>
|
@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from 'vitepress'
|
||||
import VPNavBarMenuLink from './VPNavBarMenuLink.vue'
|
||||
import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="theme.nav" aria-labelledby="main-nav-aria-label" class="VPNavBarMenu">
|
||||
<span id="main-nav-aria-label" class="visually-hidden">Main Navigation</span>
|
||||
<template v-for="item in theme.nav" :key="item.text">
|
||||
<VPNavBarMenuLink v-if="'link' in item" :item="item" />
|
||||
<VPNavBarMenuGroup v-else :item="item" />
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarMenu {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { DefaultTheme, useData } from 'vitepress'
|
||||
import { isActive } from '../support/utils'
|
||||
import VPFlyout from './VPFlyout.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithChildren
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFlyout
|
||||
:button="item.text"
|
||||
:items="item.items"
|
||||
/>
|
||||
</template>
|
@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { DefaultTheme, useData } from 'vitepress'
|
||||
import { isActive } from '../support/utils'
|
||||
import { NavItemWithLink } from '../config'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithLink
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink
|
||||
:class="{
|
||||
VPNavBarMenuLink: true,
|
||||
active: isActive(
|
||||
page.relativePath,
|
||||
item.activeMatch || item.link,
|
||||
!!item.activeMatch
|
||||
)
|
||||
}"
|
||||
:href="item.link"
|
||||
:noIcon="true"
|
||||
>
|
||||
{{ item.text }}
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarMenuLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
line-height: var(--vp-nav-height-mobile);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarMenuLink {
|
||||
line-height: var(--vp-nav-height-desktop);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,269 @@
|
||||
<script lang="ts" setup>
|
||||
import '@docsearch/css'
|
||||
import { useData } from 'vitepress'
|
||||
import { defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const VPAlgoliaSearchBox = defineAsyncComponent(
|
||||
() => import('./VPAlgoliaSearchBox.vue')
|
||||
)
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
// to avoid loading the docsearch js upfront (which is more than 1/3 of the
|
||||
// payload), we delay initializing it until the user has actually clicked or
|
||||
// hit the hotkey to invoke it.
|
||||
const loaded = ref(false)
|
||||
|
||||
const metaKey = ref()
|
||||
|
||||
onMounted(() => {
|
||||
if (!theme.value.algolia) {
|
||||
return
|
||||
}
|
||||
|
||||
// meta key detect (same logic as in @docsearch/js)
|
||||
metaKey.value.textContent = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
|
||||
? '⌘'
|
||||
: 'Ctrl'
|
||||
|
||||
const handleSearchHotKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
load()
|
||||
remove()
|
||||
}
|
||||
}
|
||||
|
||||
const remove = () => {
|
||||
window.removeEventListener('keydown', handleSearchHotKey)
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleSearchHotKey)
|
||||
|
||||
onUnmounted(remove)
|
||||
})
|
||||
|
||||
function load() {
|
||||
if (!loaded.value) {
|
||||
loaded.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="theme.algolia" class="VPNavBarSearch">
|
||||
<VPAlgoliaSearchBox v-if="loaded" />
|
||||
|
||||
<div v-else id="docsearch" @click="load">
|
||||
<button
|
||||
type="button"
|
||||
class="DocSearch DocSearch-Button"
|
||||
aria-label="Search"
|
||||
>
|
||||
<span class="DocSearch-Button-Container">
|
||||
<svg
|
||||
class="DocSearch-Search-Icon"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="DocSearch-Button-Placeholder">Search</span>
|
||||
</span>
|
||||
<span class="DocSearch-Button-Keys">
|
||||
<span class="DocSearch-Button-Key" ref="metaKey">Meta</span>
|
||||
<span class="DocSearch-Button-Key">K</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.VPNavBarSearch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarSearch {
|
||||
flex-grow: 1;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBarSearch {
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand);
|
||||
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||
--docsearch-text-color: var(--vp-c-text-1);
|
||||
--docsearch-muted-color: var(--vp-c-text-2);
|
||||
--docsearch-searchbox-shadow: none;
|
||||
--docsearch-searchbox-focus-background: transparent;
|
||||
--docsearch-key-gradient: transparent;
|
||||
--docsearch-key-shadow: none;
|
||||
--docsearch-modal-background: var(--vp-c-bg-soft);
|
||||
--docsearch-footer-background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.dark .DocSearch {
|
||||
--docsearch-modal-shadow: none;
|
||||
--docsearch-footer-shadow: none;
|
||||
--docsearch-logo-color: var(--vp-c-text-2);
|
||||
--docsearch-hit-background: var(--vp-c-bg-mute);
|
||||
--docsearch-hit-color: var(--vp-c-text-2);
|
||||
--docsearch-hit-shadow: none;
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 55px;
|
||||
background: transparent;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.DocSearch-Button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.DocSearch-Button:focus:not(:focus-visible) {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px 0 12px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-sidebar);
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Search-Icon {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
fill: currentColor;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Search-Icon {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Search-Icon {
|
||||
top: 1px;
|
||||
margin-right: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Placeholder {
|
||||
display: none;
|
||||
margin-top: 2px;
|
||||
padding: 0 16px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Button-Placeholder {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Keys {
|
||||
display: none;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Button-Keys {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key {
|
||||
display: block;
|
||||
margin: 2px 0 0 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-right: none;
|
||||
border-radius: 4px 0 0 4px;
|
||||
padding-left: 6px;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: color 0.5s, border-color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
|
||||
border-right: 1px solid var(--vp-c-divider);
|
||||
border-left: none;
|
||||
border-radius: 0 4px 4px 0;
|
||||
padding-left: 2px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.dark .DocSearch-Footer {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.DocSearch-Form {
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
background-color: var(--vp-c-white);
|
||||
}
|
||||
|
||||
.dark .DocSearch-Form {
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from 'vitepress'
|
||||
import VPSocialLinks from './VPSocialLinks.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPSocialLinks
|
||||
v-if="theme.socialLinks"
|
||||
class="VPNavBarSocialLinks"
|
||||
:links="theme.socialLinks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarSocialLinks {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarSocialLinks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a class="VPNavBarTitle" href="/">
|
||||
<img v-if="theme.logo" class="logo" :src="theme.logo" :alt="site.title">
|
||||
{{ site.title }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--vp-nav-height-mobile);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: background-color 0.5s, opacity 0.25s;
|
||||
}
|
||||
|
||||
.VPNavBarTitle:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBarTitle {
|
||||
flex-shrink: 0;
|
||||
margin-right: 32px;
|
||||
border-bottom: 1px solid var(--vp-c-divider-light);
|
||||
width: calc(var(--vp-sidebar-width) - 64px);
|
||||
height: var(--vp-nav-height-desktop);
|
||||
background-color: var(--vp-c-bg-sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: 8px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import VPIconLanguages from './icons/VPIconLanguages.vue'
|
||||
import VPFlyout from './VPFlyout.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
function toggle() {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFlyout
|
||||
v-if="theme.localeLinks"
|
||||
class="VPNavBarTranslations"
|
||||
:icon="VPIconLanguages"
|
||||
>
|
||||
<div class="container">
|
||||
<p class="title">
|
||||
{{ theme.localeLinks.text }}
|
||||
</p>
|
||||
|
||||
<ul class="list">
|
||||
<li v-for="locale in theme.localeLinks.items" :key="locale.link" class="lang">
|
||||
<a class="link" :href="locale.link">{{ locale.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</VPFlyout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarTranslations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarTranslations {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.lang {
|
||||
padding: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
line-height: 28px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { disableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock'
|
||||
import VPNavScreenMenu from './VPNavScreenMenu.vue'
|
||||
import VPNavScreenAppearance from './VPNavScreenAppearance.vue'
|
||||
import VPNavScreenTranslations from './VPNavScreenTranslations.vue'
|
||||
import VPNavScreenSocialLinks from './VPNavScreenSocialLinks.vue'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
const screen = ref<HTMLElement | null>(null)
|
||||
|
||||
function lockBodyScroll() {
|
||||
disableBodyScroll(screen.value!, { reserveScrollBarGap: true })
|
||||
}
|
||||
|
||||
function unlockBodyScroll() {
|
||||
clearAllBodyScrollLocks()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
name="fade"
|
||||
@enter="lockBodyScroll"
|
||||
@after-leave="unlockBodyScroll"
|
||||
>
|
||||
<div v-if="open" class="VPNavScreen" ref="screen">
|
||||
<div class="container">
|
||||
<VPNavScreenMenu class="menu" />
|
||||
<VPNavScreenTranslations class="translations" />
|
||||
<VPNavScreenAppearance class="appearance" />
|
||||
<VPNavScreenSocialLinks class="social-links" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreen {
|
||||
position: fixed;
|
||||
top: var(--vp-nav-height-mobile);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0 32px;
|
||||
width: 100%;
|
||||
background-color: var(--vp-c-bg);
|
||||
overflow-y: auto;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
.VPNavScreen.fade-enter-active,
|
||||
.VPNavScreen.fade-leave-active {
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.VPNavScreen.fade-enter-active .container,
|
||||
.VPNavScreen.fade-leave-active .container {
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.VPNavScreen.fade-enter-from,
|
||||
.VPNavScreen.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.VPNavScreen.fade-enter-from .container,
|
||||
.VPNavScreen.fade-leave-to .container {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavScreen {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
padding: 24px 0 96px;
|
||||
max-width: 288px;
|
||||
}
|
||||
|
||||
.menu + .translations,
|
||||
.menu + .appearance,
|
||||
.translations + .appearance {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.menu + .social-links {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.appearance + .social-links {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import VPSwitchAppearance from './VPSwitchAppearance.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavScreenAppearance">
|
||||
<p class="text">Appearance</p>
|
||||
<VPSwitchAppearance />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenAppearance {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px 12px 16px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
.text {
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from 'vitepress'
|
||||
import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue'
|
||||
import VPNavScreenMenuGroup from './VPNavScreenMenuGroup.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="theme.nav" class="VPNavScreenMenu">
|
||||
<template v-for="item in theme.nav" :key="item.text">
|
||||
<VPNavScreenMenuLink
|
||||
v-if="'link' in item"
|
||||
:text="item.text"
|
||||
:link="item.link"
|
||||
/>
|
||||
<VPNavScreenMenuGroup
|
||||
v-else
|
||||
:text="item.text || ''"
|
||||
:items="item.items"
|
||||
/>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
import VPIconPlus from './icons/VPIconPlus.vue'
|
||||
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
|
||||
import VPNavScreenMenuGroupSection from './VPNavScreenMenuGroupSection.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
items: DefaultTheme.NavItemChild[]
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const groupId = computed(() =>
|
||||
`NavScreenGroup-${props.text.replace(' ', '-').toLowerCase()}`
|
||||
)
|
||||
|
||||
function toggle() {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavScreenMenuGroup" :class="{ open: isOpen }">
|
||||
<button
|
||||
class="button"
|
||||
:aria-controls="groupId"
|
||||
:aria-expanded="isOpen"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="button-text">{{ text }}</span>
|
||||
<VPIconPlus class="button-icon" />
|
||||
</button>
|
||||
|
||||
<div :id="groupId" class="items">
|
||||
<template v-for="item in items" :key="item.text">
|
||||
<div v-if="'link' in item" :key="item.text" class="item">
|
||||
<VPNavScreenMenuGroupLink
|
||||
:text="item.text"
|
||||
:link="item.link"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="group">
|
||||
<VPNavScreenMenuGroupSection
|
||||
:text="item.text"
|
||||
:items="item.items"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenMenuGroup {
|
||||
border-bottom: 1px solid var(--vp-c-divider-light);
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.5s;
|
||||
}
|
||||
|
||||
.VPNavScreenMenuGroup .items {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.VPNavScreenMenuGroup.open .items {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.VPNavScreenMenuGroup.open {
|
||||
padding-bottom: 10px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.VPNavScreenMenuGroup.open .button {
|
||||
padding-bottom: 6px;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.VPNavScreenMenuGroup.open .button-icon {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 4px 11px 0;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: var(--vp-c-text-2);
|
||||
transition: fill 0.5s, transform 0.25s;
|
||||
}
|
||||
|
||||
.group:first-child {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.group + .group,
|
||||
.group + .item {
|
||||
padding-top: 4px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
text: string
|
||||
link: string
|
||||
}>()
|
||||
|
||||
const closeScreen = inject('close-screen') as () => void
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink class="VPNavScreenMenuGroupLink" :href="link" @click="closeScreen">
|
||||
{{ text }}
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenMenuGroupLink {
|
||||
display: block;
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.VPNavScreenMenuGroupLink:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
|
||||
|
||||
defineProps<{
|
||||
text?: string
|
||||
items: DefaultTheme.NavItemWithLink[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavScreenMenuGroupSection">
|
||||
<p v-if="text" class="title">{{ text }}</p>
|
||||
<VPNavScreenMenuGroupLink
|
||||
v-for="item in items"
|
||||
:key="item.text"
|
||||
:text="item.text"
|
||||
:link="item.link"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenMenuGroupSection {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue'
|
||||
import VPLink from './VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
text: string
|
||||
link: string
|
||||
}>()
|
||||
|
||||
const closeScreen = inject('close-screen') as () => void
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink class="VPNavScreenMenuLink" :href="link" @click="closeScreen">
|
||||
{{ text }}
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenMenuLink {
|
||||
display: block;
|
||||
border-bottom: 1px solid var(--vp-c-divider-light);
|
||||
padding: 12px 0 11px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: border-color 0.5s, color 0.25s;
|
||||
}
|
||||
|
||||
.VPNavScreenMenuLink:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from 'vitepress'
|
||||
import VPSocialLinks from './VPSocialLinks.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPSocialLinks
|
||||
v-if="theme.socialLinks"
|
||||
class="VPNavScreenSocialLinks"
|
||||
:links="theme.socialLinks"
|
||||
/>
|
||||
</template>
|
@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import VPIconChevronDown from './icons/VPIconChevronDown.vue'
|
||||
import VPIconLanguages from './icons/VPIconLanguages.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
function toggle() {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="theme.localeLinks" class="VPNavScreenTranslations" :class="{ open: isOpen }">
|
||||
<button class="title" @click="toggle">
|
||||
<VPIconLanguages class="icon lang" />
|
||||
{{ theme.localeLinks.text }}
|
||||
<VPIconChevronDown class="icon chevron" />
|
||||
</button>
|
||||
|
||||
<ul class="list">
|
||||
<li v-for="locale in theme.localeLinks.items" :key="locale.link" class="item">
|
||||
<a class="link" :href="locale.link">{{ locale.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenTranslations {
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.VPNavScreenTranslations.open {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.icon.lang {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.icon.chevron {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 4px 0 0 24px;
|
||||
}
|
||||
|
||||
.link {
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watchPostEffect, nextTick } from 'vue'
|
||||
import { useSidebar } from '../composables/sidebar'
|
||||
import VPSidebarGroup from './VPSidebarGroup.vue'
|
||||
|
||||
const { sidebar, hasSidebar } = useSidebar()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
// a11y: focus Nav element when menu has opened
|
||||
let navEl = ref<(Element & { focus(): void }) | null>(null)
|
||||
|
||||
watchPostEffect(async () => {
|
||||
if (props.open) {
|
||||
await nextTick()
|
||||
navEl.value?.focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
v-if="hasSidebar"
|
||||
class="VPSidebar"
|
||||
:class="{ open }"
|
||||
ref="navEl"
|
||||
@click.stop
|
||||
>
|
||||
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
|
||||
<span class="visually-hidden" id="sidebar-aria-label">
|
||||
Sidebar Navigation
|
||||
</span>
|
||||
|
||||
<div v-for="group in sidebar" :key="group.text" class="group">
|
||||
<VPSidebarGroup :text="group.text" :items="group.items" />
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-sidebar);
|
||||
padding: 32px 32px 96px;
|
||||
width: calc(100vw - 64px);
|
||||
max-width: 320px;
|
||||
background-color: var(--vp-c-bg);
|
||||
opacity: 0;
|
||||
box-shadow: var(--vp-c-shadow-3);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
transform: translateX(-100%);
|
||||
transition: background-color 0.5s, opacity 0.5s, transform 0.25s ease;
|
||||
}
|
||||
|
||||
.VPSidebar.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(0);
|
||||
transition: background-color 0.5s,
|
||||
opacity 0.25s,
|
||||
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
.dark .VPSidebar {
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPSidebar {
|
||||
z-index: 1;
|
||||
padding-top: var(--vp-nav-height-desktop);
|
||||
padding-bottom: 128px;
|
||||
width: var(--vp-sidebar-width);
|
||||
max-width: 100%;
|
||||
width: var(--vp-sidebar-width);
|
||||
background-color: var(--vp-c-bg-sidebar);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
box-shadow: none;
|
||||
transform: translateX(0);
|
||||
transition: border-color 0.5s, background-color 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPSidebar {
|
||||
padding-left: calc((100% - var(--vp-layout-max-width)) / 2);
|
||||
width: calc((100% - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
margin-top: 32px;
|
||||
border-top: 1px solid var(--vp-c-divider-light);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.group {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import { useData } from 'vitepress'
|
||||
import { DefaultTheme } from '../config'
|
||||
import VPSidebarLink from './VPSidebarLink.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
items: DefaultTheme.SidebarItem[]
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="VPSidebarGroup">
|
||||
<div class="title">
|
||||
<h2 class="title-text">{{ text }}</h2>
|
||||
</div>
|
||||
|
||||
<template v-for="item in items" :key="item.link">
|
||||
<VPSidebarLink :item="item" />
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.title {
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title-text {
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,57 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { DefaultTheme } from '../config'
|
||||
import { isActive, normalizeLink } from '../support/utils'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.SidebarItem
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
const closeSideBar = inject('close-sidebar') as () => void
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="link"
|
||||
:class="{ active: isActive(page.relativePath, item.link) }"
|
||||
:href="normalizeLink(item.link)"
|
||||
@click="closeSideBar"
|
||||
>
|
||||
<p class="link-text">{{ item.text }}</p>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
display: block;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.link:hover .link-text {
|
||||
color: var(--vp-c-brand);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.link.active .link-text {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.link {
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vitepress'
|
||||
|
||||
const route = useRoute()
|
||||
const backToTop = ref()
|
||||
|
||||
watch(() => route.path, () => backToTop.value.focus())
|
||||
|
||||
function focusOnTargetAnchor({ target }: Event) {
|
||||
const el = document.querySelector(
|
||||
(target as HTMLAnchorElement).hash!
|
||||
) as HTMLAnchorElement
|
||||
|
||||
if (el) {
|
||||
const removeTabIndex = () => {
|
||||
el.removeAttribute('tabindex')
|
||||
el.removeEventListener('blur', removeTabIndex)
|
||||
}
|
||||
|
||||
el.setAttribute('tabindex', '-1')
|
||||
el.addEventListener('blur', removeTabIndex)
|
||||
el.focus()
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span ref="backToTop" tabindex="-1" />
|
||||
<a
|
||||
href="#VPContent"
|
||||
class="VPSkipLink visually-hidden"
|
||||
@click="focusOnTargetAnchor"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSkipLink {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 8px 16px;
|
||||
z-index: 999;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: var(--vp-c-brand);
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.VPSkipLink:focus {
|
||||
height: auto;
|
||||
width: auto;
|
||||
clip: auto;
|
||||
clip-path: none;
|
||||
}
|
||||
|
||||
.dark .VPSkipLink {
|
||||
color: var(--vp-c-green);
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPSkipLink {
|
||||
top: 14px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,63 @@
|
||||
<script lang="ts" setup>
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
import VPIconDiscord from './icons/VPIconDiscord.vue'
|
||||
import VPIconFacebook from './icons/VPIconFacebook.vue'
|
||||
import VPIconGitHub from './icons/VPIconGitHub.vue'
|
||||
import VPIconLinkedIn from './icons/VPIconLinkedIn.vue'
|
||||
import VPIconInstagram from './icons/VPIconInstagram.vue'
|
||||
import VPIconSlack from './icons/VPIconSlack.vue'
|
||||
import VPIconTwitter from './icons/VPIconTwitter.vue'
|
||||
import VPIconYouTube from './icons/VPIconYouTube.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
icon: DefaultTheme.SocialLinkIcon
|
||||
link: string
|
||||
}>()
|
||||
|
||||
const icons = {
|
||||
discord: VPIconDiscord,
|
||||
facebook: VPIconFacebook,
|
||||
github: VPIconGitHub,
|
||||
instagram: VPIconInstagram,
|
||||
linkedin: VPIconLinkedIn,
|
||||
slack: VPIconSlack,
|
||||
twitter: VPIconTwitter,
|
||||
youtube: VPIconYouTube
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="VPSocialLink"
|
||||
:href="link"
|
||||
:title="icon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<component :is="icons[icon]" class="icon" />
|
||||
<span class="visually-hidden">{{ icon }}</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSocialLink {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color .5s;
|
||||
}
|
||||
|
||||
.VPSocialLink:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color .25s;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
import VPSocialLink from './VPSocialLink.vue'
|
||||
|
||||
defineProps<{
|
||||
links: DefaultTheme.SocialLink[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPSocialLinks">
|
||||
<VPSocialLink
|
||||
v-for="{ link, icon } in links"
|
||||
:key="link"
|
||||
:icon="icon"
|
||||
:link="link"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSocialLinks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<button class="VPSwitch" type="button" role="switch">
|
||||
<span class="check">
|
||||
<span class="icon" v-if="$slots.default">
|
||||
<slot />
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSwitch {
|
||||
position: relative;
|
||||
border-radius: 11px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
transition: border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPSwitch:hover {
|
||||
border-color: var(--vp-c-gray);
|
||||
}
|
||||
|
||||
.check {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vp-c-white);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
transition: background-color 0.25s, transform 0.25s;
|
||||
}
|
||||
|
||||
.dark .check {
|
||||
background-color: var(--vp-c-black);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon :deep(svg) {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.dark .icon :deep(svg) {
|
||||
fill: var(--vp-c-text-1);
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import VPSwitch from './VPSwitch.vue'
|
||||
import VPIconSun from './icons/VPIconSun.vue'
|
||||
import VPIconMoon from './icons/VPIconMoon.vue'
|
||||
|
||||
const toggle = typeof localStorage !== 'undefined' ? useAppearance() : () => {}
|
||||
|
||||
function useAppearance() {
|
||||
const storageKey = 'vitepress-theme-appearance'
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const classList = document.documentElement.classList
|
||||
|
||||
let userPreference = localStorage.getItem(storageKey) || 'auto'
|
||||
|
||||
let isDark = userPreference === 'auto'
|
||||
? query.matches
|
||||
: userPreference === 'dark'
|
||||
|
||||
query.onchange = (e) => {
|
||||
if (userPreference === 'auto') {
|
||||
setClass((isDark = e.matches))
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
setClass((isDark = !isDark))
|
||||
|
||||
userPreference = isDark
|
||||
? query.matches ? 'auto' : 'dark'
|
||||
: query.matches ? 'light' : 'auto'
|
||||
|
||||
localStorage.setItem(storageKey, userPreference)
|
||||
}
|
||||
|
||||
function setClass(dark: boolean): void {
|
||||
classList[dark ? 'add' : 'remove']('dark')
|
||||
}
|
||||
|
||||
return toggle
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPSwitch
|
||||
class="VPSwitchAppearance"
|
||||
aria-label="toggle dark mode"
|
||||
@click="toggle"
|
||||
>
|
||||
<VPIconSun class="sun" />
|
||||
<VPIconMoon class="moon" />
|
||||
</VPSwitch>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sun {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.moon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dark .sun {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dark .moon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dark .VPSwitchAppearance :deep(.check) {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue