epic(website): refactor website (#3426)

* feat(website): init viteperss (#3162)

* init vitepress

* Finish homepage

* relayout page

* partial finish

* update

* update use lang

* remove font weight; change font size

* docs: optimize docs folder structure

* reorganize project

* fix ssr issue

* fix build issues

Co-authored-by: Kevin <sxzz@sxzz.moe>
Co-authored-by: zouhang <zouhang@didiglobal.com>

* feat(website) integrate with crowdin (#3218)

- Introduce Crowdin CLI
- Add script for fetching Crowdin token from env
- Add script for local development

* fix formatting

* update codeblocks (#3249)

* feat(docs): update website preview script

- Update azure preview build script
- Eliminate dead links

* bootstrap docs

* fix homepage layout issue

* fix formating

* Finish ToC and codepen integration

* reorganized files and fix issue that causes demo unavailable after build

* feat(docs): migration documentations (#3283)

* feat(docs): migration documentations

- Move documentations to docs/

* remove unused files

* docs: [popconfirm] migrate document (#3285)

* feat(docs): migrate documentation from space.md to upload.md (#3292)

- Upload documentations acorrodingly

* docs: [date-picker] migrate document  (#3289)

* docs: [date-picker] migrate document

* fix: typo

* docs: [slider] migrate document (#3287)

* docs: migrate documents (#3290)

* docs: migrate-datetime-picker

* docs: migrate descriptions

* docs: migrate dialog

* docs: migrate divider

* docs: migrate drawer

* docs: migrate drapdown

* docs: fix drapdown

* docs: migrate empty

* docs: migrate form

* docs: add scoped for style

* docs: simplify toRefs

* chore: update doc (#3297)

* chore: update doc

* chore: update doc

* chore: update doc

* feat(docs): migrate documentations from infinite-scroll to page-header (#3303)

- Update docs accordingly
- Update CodePen icon to match style
- Update component name to match others

* docs: migrate documentions (#3293)

migrate list:
* image
* popover
* scrollbar
* radio
* rate
* skeleton
* select
* select-v2
* reault
* progress
* pagination

* chore: update doc (#3305)

Co-authored-by: 0song <0song@gmail.com>

* Fix broken pipeline

* chore: update demo plugin

* website perfection

* fix hydration bug

* docs: update guide (#3342)

* WIP docs

* docs: update docs

* add dark mode

* make dev fetch components from local instead of node_modules

Co-authored-by: msidolphin <msidolphin@outlook.com>
Co-authored-by: Aex <spryti@qq.com>
Co-authored-by: 0song <82012629+0song@users.noreply.github.com>
Co-authored-by: 0song <0song@gmail.com>
Co-authored-by: zouhang <zouhang@didiglobal.com>
Co-authored-by: 三咲智子 <sxzz@sxzz.moe>

* fix(docs): fix codepen code example issue (#3380)

* fix(docs): fix codepen code example issue

- Add lang="ts" for all example files
- Fix codepen import error

* revert changes in resource.vue

* feat(docs): complete crowdin integration (#3408)

* Update mds for preparing the integration script

* deprecate old website

* update sponsors and even handler for resize

* update build script for preview

* fix preview-build error

* fix preview-build error

* fix preview-build error

* fix preview-build error

* fix preview-build error

* update deploy script and some bugs

* Fix existing issue

* chore(project): add dev preview workflow

* chore(project): rename dev to staging

* update the size of toc

* update staging-preview script

* update preview scripts

* enable website build for preview

* fix build error

* final commitment for update the details

* remove azure pipeline

* move docs ignores into docs and update date

Co-authored-by: Kevin <sxzz@sxzz.moe>
Co-authored-by: zouhang <zouhang@didiglobal.com>
Co-authored-by: msidolphin <msidolphin@outlook.com>
Co-authored-by: Aex <spryti@qq.com>
Co-authored-by: 0song <82012629+0song@users.noreply.github.com>
Co-authored-by: 0song <0song@gmail.com>
This commit is contained in:
jeremywu 2021-09-17 00:18:50 +08:00 committed by GitHub
parent 70ebeaaedc
commit c6bed151a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1040 changed files with 28144 additions and 138740 deletions

View File

@ -15,25 +15,41 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '12'
node-version: '16'
- name: Install dependencies
run: yarn bootstrap
- name: Build Website
run: yarn website-build
- name: Init docs
run: cd docs && yarn
- name: Build Indices
run: yarn build:indices
- name: Fetch Crowdin token for pulling languages
run: yarn init:crowdin
env:
ALGOLIA_KEY: ${{secrets.ALGOLIA_KEY}}
CROWDIN_TOKEN: ${{secrets.CROWDIN_TOKEN}}
- name: Pull Crowdin translations
run: cd docs && yarn crowdin download
- name: Generate common locale
run: yarn docs:gen-locale
- name: Build website
run: yarn docs:build
env:
DOC_ENV: production
# - name: Build Indices
# run: yarn build:indices
# env:
# ALGOLIA_KEY: ${{secrets.ALGOLIA_KEY}}
- name: Deploy
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: website-dist
FOLDER: docs/.vitepress/dist
GIT_CONFIG_NAME: ElementPlusBot
GIT_CONFIG_EMAIL: hello@element-plus.org
COMMIT_MESSAGE: website deploy

View File

@ -14,25 +14,33 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
- name: Cache
uses: actions/cache@v2
with:
path: node_modules
key: node_modules-${{ hashFiles('**/yarn.lock') }}
node-version: '16'
registry-url: https://registry.npmjs.com/
- name: Install dependencies
run: yarn bootstrap
- name: Init docs
run: cd docs && yarn
- name: Generate common locale
run: yarn docs:gen-locale
- name: Build Element Plus
run: sh scripts/build.sh
- name: Build website
run: yarn website-build
run: yarn docs:build
env:
DOC_ENV: preview
# share website dist
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: website
path: website-dist/
name: docs
path: docs/.vitepress/dist/
retention-days: 1
# write pr.txt for share

View File

@ -28,7 +28,7 @@ jobs:
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
workflow_conclusion: success
name: website
name: docs
- name: Deploy Site
run: |

55
.github/workflows/staging-preview.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Staging Preview
on:
push:
branches:
- 'dev'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: yarn bootstrap
- name: Compile Element Plus
run: sh scripts/build.sh
- name: Init docs
run: cd docs && yarn
- name: Init Crowdin token
run: yarn init:crowdin
env:
CROWDIN_TOKEN: ${{secrets.CROWDIN_TOKEN}}
- name: Upload source files
run: cd docs && yarn crowdin upload sources
- name: Pull Crowdin translations
run: cd docs && yarn crowdin download
- name: generate common locale
run: yarn docs:gen-locale
- name: Build website
run: yarn docs:build
env:
DOC_ENV: staging
- name: Deploy Site
run: |
export DEPLOY_DOMAIN=https://staging-preview-element-plus.surge.sh
echo "Deploy to $DEPLOY_DOMAIN"
npx surge --project ./docs/.vitepress/dist/ --domain $DEPLOY_DOMAIN --token $SURGE_TOKEN
env:
SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }}

View File

@ -12,11 +12,19 @@
<br>
</p>
<p align="center">Element Plus - A Vue.js 3.0 UI library</p>
<p align="center">Element Plus - A Vue.js 3 UI library</p>
- 💪 Vue 3.0 Composition API
- 💪 Vue 3 Composition API
- 🔥 Written in TypeScript
## Archived website
If you are looking for previous version website, here is the link.
[Element Plus Documentation Archived](https://github.com/element-plus/doc-archive)
The new website is launched at 17th Sep 2021.
## Status: Beta
This project is still under heavy development. Feel free to join us and make your first pull request.
@ -47,6 +55,11 @@ This project is still under heavy development. Feel free to join us and make you
---
## Translations
Element Plus is translated to multiple languages, you can click the badge to help up update the translation or apply to become
a proofreader [![Crowdin](https://badges.crowdin.net/element-plus/localized.svg)](https://crowdin.com/project/element-plus)
## Documentation
You can find for more details, API, and other docs on [https://element-plus.org](https://element-plus.org/)

View File

@ -5,8 +5,6 @@ import { epRoot } from './paths'
const { name, version } = require(path.resolve(epRoot, './package.json'))
import icon from '../website/icon.json'
const icons = icon.map((item) => 'el-icon-' + item).join('/')
const tagVer = process.env.TAG_VERSION
const _version = tagVer
? tagVer.startsWith('v')
@ -18,7 +16,7 @@ helper({
name,
version: _version,
entry:
'website/docs/en-US/!(custom-theme|datetime-picker|i18n|installation|message-box|message|migration-from-2.x|notification|quickstart|transition|typography).md',
'docs/en-US/!(custom-theme|datetime-picker|i18n|installation|message-box|message|migration-from-2.x|notification|quickstart|transition|typography).md',
outDir: 'dist/element-plus',
reComponentName,
reDocUrl,
@ -53,9 +51,7 @@ function reAttribute(value, key, item) {
const _value = value.match(/^\*\*(.*)\*\*$/)
const str = _value ? _value[1] : value
if (key === 'Accepted Values' && /icon/i.test(item[0])) {
return icons
} else if (key === 'Name' && /^(-|—)$/.test(str)) {
if (key === 'Name' && /^(-|—)$/.test(str)) {
return 'default'
} else if (str === '' || /^(-|—)$/.test(str)) {
return undefined

View File

@ -28,7 +28,9 @@ const langs = {
;['zh-CN', 'en-US', 'es', 'fr-FR', 'jp'].forEach((lang) => {
const indexName = langs[lang]
const index = client.initIndex(indexName)
index.clearObjects().then(() => {
index
.clearObjects()
.then(() => {
const files = fg.sync(`website/docs/${lang}/*.md`)
let indices: Index[] = []
files.forEach((file) => {
@ -49,30 +51,17 @@ const langs = {
.split('\n')
.filter((part) => !!part)
)
.map((match) => {
const length = match.length
if (length > 2) {
const desc = match.slice(1, length).join('')
return [match[0], desc]
}
return match
})
let i = 0
indices = indices.concat(
matches.map((match) => {
const title = match[0].replace(/#{2,4}/, '').trim()
const index = { component, title } as Index
index.anchor = slugify(title)
index.content = (match[1] || title).replace(/<[^>]+>/g, '')
index.path = path
index.sort = i++
return index
})
)
})
index.saveObjects(indices, {
index
.saveObjects(indices, {
autoGenerateObjectIDIfNotExist: true,
})
.catch((e) => {
console.log(e)
})
})
.catch((e) => {
console.log(e)
})
})

View File

@ -0,0 +1,24 @@
import path from 'path'
import fs from 'fs'
import chalk from 'chalk'
const credentialPlaceholder = 'API_TOKEN_PLACEHOLDER'
const CREDENTIAL = process.env.CROWDIN_TOKEN
;(async () => {
console.info(chalk.cyan('Fetching Crowdin credential'))
const configPath = path.resolve(__dirname, '../docs/crowdin.yml')
try {
const file = await fs.promises.readFile(configPath, {
encoding: 'utf-8',
})
await fs.promises.writeFile(
configPath,
file.replace(credentialPlaceholder, CREDENTIAL!)
)
console.info(chalk.green('Crowdin credential update successfully'))
} catch (e) {
throw e
}
})()

110
build/crowdin-generate.ts Normal file
View File

@ -0,0 +1,110 @@
/* eslint-disable */
import fs from 'fs'
import path from 'path'
import chalk from 'chalk'
import { docRoot } from './paths'
// NB: this file is only for generating files that enables developers to develop the website.
const componentLocaleRoot = path.resolve(docRoot, '.vitepress/crowdin')
const exists = 'File already exists'
async function main() {
const localeOutput = path.resolve(docRoot, '.vitepress/i18n')
if (fs.existsSync(localeOutput)) {
throw new Error(exists)
}
console.log(chalk.cyan('Starting for build doc for developing'))
// all language should be identical since it is mirrored from crowdin.
const dirs = await fs.promises.readdir(componentLocaleRoot, {
withFileTypes: true,
})
const languages = dirs.map((dir) => dir.name)
const langWithoutEn = languages.filter((l) => l !== 'en-US')
await fs.promises.mkdir(localeOutput)
// build lang.json for telling `header>language-select` how many languages are there
await fs.promises.writeFile(
path.resolve(localeOutput, 'lang.json'),
JSON.stringify(languages),
{
encoding: 'utf-8',
}
)
// loop through en-US
const enUS = path.resolve(componentLocaleRoot, 'en-US')
// we do not include en-US since we are currently using it as template
const languagePaths = langWithoutEn.map((l) => {
return {
name: l,
pathname: path.resolve(componentLocaleRoot, l),
}
})
console.log(languagePaths)
await traverseDir(enUS, languagePaths, localeOutput)
}
async function traverseDir(dir, paths, targetPath) {
const contents = await fs.promises.readdir(dir, { withFileTypes: true })
await Promise.all(
contents.map(async (c) => {
if (c.isDirectory()) {
await fs.promises.mkdir(path.resolve(targetPath, c.name), {
recursive: true,
})
return traverseDir(
path.resolve(dir, c.name),
paths.map((p) => {
return {
...p,
pathname: path.resolve(p.pathname, c.name),
}
}),
path.resolve(targetPath, c.name)
)
} else if (c.isFile()) {
const content = require(path.resolve(dir, c.name))
const contentToWrite = {
'en-US': content,
}
await Promise.all(
paths.map(async (p) => {
const content = require(path.resolve(p.pathname, c.name))
contentToWrite[p.name] = content
})
)
return fs.promises.writeFile(
path.resolve(targetPath, c.name),
JSON.stringify(contentToWrite, null, 2),
{
encoding: 'utf-8',
}
)
}
})
)
}
main()
.then(() => {
console.log(chalk.green('Locale for website development generated'))
})
.catch((e) => {
if (e.message === exists) {
// do nothing
} else {
console.log(chalk.red(e.message))
throw e
}
})

View File

@ -10,3 +10,4 @@ export const directiveRoot = path.resolve(pkgRoot, './directives')
export const epRoot = path.resolve(pkgRoot, './element-plus')
export const utilRoot = path.resolve(pkgRoot, './utils')
export const buildOutput = path.resolve(projRoot, './dist')
export const docRoot = path.resolve(projRoot, './docs')

12
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
.vitepress/*.js
.vitepress/i18n/*
temp
zh-CN
fr-FR
es-ES
ja-JP
.vitepress/crowdin/*
!.vitepress/crowdin/en-US

120
docs/.vitepress/config.js Normal file
View File

@ -0,0 +1,120 @@
/* eslint-disable */
const sidebars = require('./sidebars')
const nav = require('./nav')
const mdPlugin = require('./plugins')
const features = require('./features')
const transformer = () => {
return {
props: [],
needRuntime: true,
}
}
const buildTransformers = () => {
const transformers = {}
const directives = [
'infinite-scroll',
'loading',
'popover',
'click-outside',
'repeat-click',
'trap-focus',
'mousewheel',
'resize',
]
directives.forEach((k) => {
transformers[k] = transformer
})
return transformers
}
console.log(process.env.DOC_ENV)
module.exports = {
title: 'ElementPlus',
head: [
[
'link',
{
rel: 'icon',
href: '/favicon.ico',
},
],
[
'link',
{
rel: 'stylesheet',
href: '//fonts.loli.net/css?family=Inter:300,400,500,600|Open+Sans:400,600;display=swap',
},
],
features.theme
? [
'script',
{},
require('fs').readFileSync(
require('path').resolve(__dirname, './darkmode.js'),
'utf-8'
),
]
: [],
],
themeConfig: {
repo: 'element-plus/element-plus',
docsDir: 'docs',
editLinks: true,
editLinkText: 'Edit this page on GitHub',
lastUpdated: 'Last Updated',
logo: '/images/element-plus-logo.svg',
logoSmall: '/images/element-plus-logo-small.svg',
sidebars,
nav,
agolia: {
apiKey: 'e32c681af38f324039e81d81834e70b8',
appId: '7DCTSU0WBW',
},
features,
},
markdown: {
config: (md) => {
mdPlugin(md)
},
},
vue: {
template: {
ssr: true,
compilerOptions: {
directiveTransforms: buildTransformers(),
},
},
},
vite: {
sourcemap: true,
...(process.env.DOC_ENV === 'production'
? {}
: {
resolve: {
alias: [
{
find: /^element-plus$/,
replacement: require('path').resolve(
__dirname,
'../../dist/element-plus/es/index'
),
},
{
find: /^element-plus\/lib\/utils\/(.*)/,
replacement: require('path').resolve(
__dirname,
'../../dist/element-plus/es/utils/$1'
),
},
],
},
}),
},
}

View File

@ -0,0 +1,10 @@
{
"view-source": "View source",
"edit-on-github": "Edit on Github",
"edit-in-codepen": "Edit in Codepen.io",
"copy-button-text": "Copy",
"switch-button-option-text": "Switch to Options API",
"switch-button-setup-text": "Switch to Composition API",
"copy-success": "Copied",
"copy-error": "This browser does not support automatic copy"
}

View File

@ -0,0 +1,4 @@
{
"copy-success": "Copied",
"copy-error": "Your browser does not support copy :("
}

View File

@ -0,0 +1,3 @@
{
"title": "Last Updated"
}

View File

@ -0,0 +1,5 @@
{
"search": "Search",
"empty": "No results",
"index": "en"
}

View File

@ -0,0 +1,4 @@
{
"title": "Sponsors",
"sponsoredBy": "Sponsored by"
}

View File

@ -0,0 +1,12 @@
[
{
"name": "bit",
"img": "/images/bit.svg",
"url": "https://bit.dev/?from=element-ui"
},
{
"name": "renren.io",
"img": "/images/renren.png",
"url": "https://www.renren.io/?from=element-ui"
}
]

View File

@ -0,0 +1,3 @@
{
"help": "Help Translate 😉"
}

View File

@ -0,0 +1,13 @@
{
"links": "Links",
"repo": "GitHub",
"community": "Community",
"changelog": "Changelog",
"theme": "Online Theme Roller",
"faq": "FAQ",
"gitter": "Gitter",
"starter": "Starter kit",
"feedback": "Feedback",
"contribution": "Contribution",
"eleme": "Eleme"
}

View File

@ -0,0 +1,7 @@
{
"guide": "Guide",
"components": "Component",
"theme": "Theme",
"resource": "Resource",
"backToTop": "Back to top"
}

View File

@ -0,0 +1,302 @@
{
"basic": {
"text": "Basic",
"children": [
{
"link": "/border",
"text": "Border"
},
{
"link": "/button",
"text": "Button"
},
{
"link": "/color",
"text": "Color"
},
{
"link": "/container",
"text": "Layout Container"
},
{
"link": "/icon",
"text": "Icon"
},
{
"link": "/layout",
"text": "Layout"
},
{
"link": "/link",
"text": "Link"
},
{
"link": "/scrollbar",
"text": "Scrollbar"
},
{
"link": "/space",
"text": "Space"
},
{
"link": "/typography",
"text": "Typography"
}
]
},
"configuration": {
"text": "Configuration",
"children": [
{
"link": "/config-provider",
"text": "Config Provider"
}
]
},
"form": {
"text": "Form",
"children": [
{
"link": "/cascader",
"text": "Cascader"
},
{
"link": "/checkbox",
"text": "Checkbox"
},
{
"link": "/color-picker",
"text": "Color Picker"
},
{
"link": "/date-picker",
"text": "Date Picker"
},
{
"link": "/datetime-picker",
"text": "DateTime Picker"
},
{
"link": "/form",
"text": "Form"
},
{
"link": "/input",
"text": "Input"
},
{
"link": "/input-number",
"text": "Input Number"
},
{
"link": "/radio",
"text": "Radio"
},
{
"link": "/rate",
"text": "Rate"
},
{
"link": "/select",
"text": "Select"
},
{
"link": "/select-v2",
"text": "Virtualized Select"
},
{
"link": "/slider",
"text": "Slider"
},
{
"link": "/switch",
"text": "Switch"
},
{
"link": "/time-picker",
"text": "Time Picker"
},
{
"link": "/time-select",
"text": "Time Select"
},
{
"link": "/transfer",
"text": "Transfer"
},
{
"link": "/upload",
"text": "Upload"
}
]
},
"data": {
"text": "Data",
"children": [
{
"link": "/avatar",
"text": "Avatar"
},
{
"link": "/badge",
"text": "Badge"
},
{
"link": "/calendar",
"text": "Calendar"
},
{
"link": "/card",
"text": "Card"
},
{
"link": "/carousel",
"text": "Carousel"
},
{
"link": "/collapse",
"text": "Collapse"
},
{
"link": "/descriptions",
"text": "Descriptions"
},
{
"link": "/empty",
"text": "Empty"
},
{
"link": "/image",
"text": "Image"
},
{
"link": "/infinite-scroll",
"text": "Infinite Scroll"
},
{
"link": "/pagination",
"text": "Pagination"
},
{
"link": "/progress",
"text": "Progress"
},
{
"link": "/result",
"text": "Result"
},
{
"link": "/skeleton",
"text": "Skeleton"
},
{
"link": "/table",
"text": "Table"
},
{
"link": "/tag",
"text": "Tag"
},
{
"link": "/timeline",
"text": "Timeline"
},
{
"link": "/tree",
"text": "Tree"
}
]
},
"navigation": {
"text": "Navigation",
"children": [
{
"link": "/affix",
"text": "Affix"
},
{
"link": "/breadcrumb",
"text": "Breadcrumb"
},
{
"link": "/dropdown",
"text": "Dropdown"
},
{
"link": "/menu",
"text": "NavMenu"
},
{
"link": "/page-header",
"text": "Page Header"
},
{
"link": "/steps",
"text": "Steps"
},
{
"link": "/tabs",
"text": "Tabs"
}
]
},
"feedback": {
"text": "Feedback",
"children": [
{
"link": "/alert",
"text": "Alert"
},
{
"link": "/dialog",
"text": "Dialog"
},
{
"link": "/drawer",
"text": "Drawer"
},
{
"link": "/loading",
"text": "Loading"
},
{
"link": "/message",
"text": "Message"
},
{
"link": "/message-box",
"text": "Message Box"
},
{
"link": "/notification",
"text": "Notification"
},
{
"link": "/popconfirm",
"text": "Pop Confirm"
},
{
"link": "/popover",
"text": "Popover"
},
{
"link": "/tooltip",
"text": "Tooltip"
}
]
},
"others": {
"text": "Others",
"children": [
{
"link": "/backtop",
"text": "Backtop"
},
{
"link": "/divider",
"text": "Divider"
}
]
}
}

View File

@ -0,0 +1,26 @@
{
"intro": {
"text": "Basics",
"children": [
{ "text": "Design", "link": "/guide/design" },
{ "text": "Navigation", "link": "/guide/nav" },
{
"text": "Installation",
"link": "/guide/installation"
},
{
"text": "Quick Start",
"link": "/guide/quickstart"
}
]
},
"advanced": {
"text": "Advanced",
"children": [
{ "text": "i18n", "link": "/guide/i18n" },
{ "text": "Migration", "link": "/guide/migration" },
{ "text": "Theming", "link": "/guide/theming" },
{ "text": "Built-in Transitions", "link": "/guide/transitions" }
]
}
}

View File

@ -0,0 +1,19 @@
{
"1": "A Desktop UI Library",
"2": "Element Plus, a Vue 3.0 based component library for developers, designers and product managers",
"3": "Guide",
"4": "Understand the design guidelines, helping designers build product that's logically sound, reasonably structured and easy to use.",
"5": "View Detail",
"6": "Component",
"7": "Experience interaction details by strolling through component demos. Use encapsulated code to improve developing efficiency.",
"8": "Resource",
"9": "Download relevant design resources for shaping page prototype or visual draft, increasing design efficiency.",
"10": "Theme",
"11": "Online theme roller, visualize custom and manage site themes and component styles",
"12": "Theme customization is available!",
"13": "Click here",
"14": "Make your own theme",
"lang": "en-US",
"titleSize": "34",
"paraSize": "18"
}

View File

@ -0,0 +1,5 @@
{
"title": "Resource not found",
"button-title": "Back to Home",
"desc": "The page you requested did not exist"
}

View File

@ -0,0 +1,17 @@
[
{
"text": "Guide",
"link": "/guide/design",
"activeMatch": "/guide/"
},
{
"text": "Component",
"link": "/component/border",
"activeMatch": "/component/"
},
{
"text": "Resource",
"link": "/resource/",
"activeMatch": "/resource/"
}
]

View File

@ -0,0 +1,10 @@
;(() => {
const saved = localStorage.getItem('preferred_theme')
if (
saved === 'auto'
? window.matchMedia(`(prefers-color-scheme: dark)`).matches
: saved === 'dark'
) {
document.documentElement.classList.add('dark')
}
})()

View File

@ -0,0 +1,3 @@
module.exports = {
theme: false,
}

19
docs/.vitepress/nav.js Normal file
View File

@ -0,0 +1,19 @@
const navLocale = require('./i18n/pages/sidebar.json')
const { ensureLang } = require('./site-utils')
// Mapping the first sub link to the nav link to avoid 404 error.
function getNav() {
const nav = {}
Object.entries(navLocale).forEach(([lang, val]) => {
nav[lang] = Object.values(val).map((item) => {
return {
...item,
link: `${ensureLang(lang)}${item.link}`,
}
})
})
return nav
}
module.exports = getNav()

View File

@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const mdContainer = require('markdown-it-container')
const path = require('path')
const fs = require('fs')
const { highlight } = require('vitepress/dist/node/markdown/plugins/highlight')
const { parse } = require('@vue/compiler-sfc')
const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
module.exports = (md) => {
md.use(mdContainer, 'demo', {
validate(params) {
return params.trim().match(/^demo\s*(.*)$/)
},
render(tokens, idx) {
const data = md.__data
const hoistedTags = data.hoistedTags || (data.hoistedTags = [])
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
if (tokens[idx].nesting === 1) {
const description = m && m.length > 1 ? m[1] : ''
const content =
tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : ''
const sourceFileToken = tokens[idx + 2]
let source = ''
const sourceFile = sourceFileToken.children[0].content
if (sourceFileToken.type === 'inline') {
source = fs.readFileSync(
path.resolve(__dirname, '../examples', `${sourceFile}.vue`),
'utf-8'
)
const existingScriptIndex = hoistedTags.findIndex((tag) => {
return scriptSetupRE.test(tag)
})
if (existingScriptIndex === -1) {
hoistedTags.push(`
<script setup>
const demos = import.meta.globEager('../../examples/${
sourceFile.split('/')[0]
}/*.vue')
</script>
`)
}
}
if (!source) throw new Error(`Incorrect source file: ${sourceFile}`)
const { html, js, css, cssPreProcessor, jsPreProcessor } =
generateCodePenSnippet(source)
return `<Demo :demos="demos" source="${encodeURIComponent(
highlight(source, 'vue')
)}" path="${sourceFile}" html="${html}" js="${js}" css="${css}" css-pre-processor="${cssPreProcessor}" js-pre-processor="${jsPreProcessor}">
${description ? `` : ''}
<!--element-demo: ${content}:element-demo-->
`
}
return '</Demo>'
},
})
}
function generateCodePenSnippet(source) {
const { template, script, styles } = parse(source).descriptor
const css = (styles || [{ content: '' }]).pop()
return {
html: encodeURIComponent(template.content),
js: encodeURIComponent((script || { content: '' }).content),
css: encodeURIComponent(css?.content || ''),
cssPreProcessor: css?.lang || 'none',
jsPreProcessor: script?.lang || 'none',
}
}

View File

@ -0,0 +1,50 @@
const guideLocale = require('./i18n/pages/guide.json')
const componentLocale = require('./i18n/pages/component.json')
const { ensureLang } = require('./site-utils')
function getGuideSidebar() {
const guideSidebars = {}
Object.entries(guideLocale).forEach(([lang, val]) => {
guideSidebars[lang] = Object.values(val).map((item) => {
return mapPrefix(item, lang)
})
})
return guideSidebars
}
function getComponentsSideBar() {
const componentSidebar = {}
Object.entries(componentLocale).forEach(([lang, val]) => {
componentSidebar[lang] = Object.values(val).map((item) => {
return mapPrefix(item, lang, '/component')
})
})
return componentSidebar
}
// return sidebar with language configs.
// this might create duplicated data but the overhead is ignorable
const getSidebars = () => {
return {
'/guide/': getGuideSidebar(),
'/component/': getComponentsSideBar(),
}
}
function mapPrefix(item, lang, prefix = '') {
if (item.children && item.children.length > 0) {
return {
...item,
children: item.children.map((child) => {
return mapPrefix(child, lang, prefix)
}),
}
}
return {
...item,
link: `${ensureLang(lang)}${prefix}${item.link}`,
}
}
module.exports = getSidebars()

View File

@ -0,0 +1,5 @@
const ensureLang = (lang) => {
return `/${lang}`
}
exports.ensureLang = ensureLang

View File

@ -0,0 +1,16 @@
import ElementPlus from 'element-plus'
import VPApp, { globals, NotFound } from '../vitepress'
export default {
NotFound,
Layout: VPApp,
logo: '/images/element-plus-logo-small.svg',
enhanceApp: ({ app }) => {
app.use(ElementPlus)
globals.forEach(([name, Comp]) => {
app.component(name, Comp)
})
},
}

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { computed } from 'vue'
import ExternalLink from '../icons/external-link-icon.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="link-item"
:class="{ link: href }"
:href="href"
:target="isExternal ? '_blank' : undefined"
:rel="isExternal ? 'noopener noreferrer' : undefined"
>
<slot />
<ElIcon v-if="isExternal && !noIcon">
<ExternalLink class="link-icon" />
</ElIcon>
</component>
</template>
<style scoped>
.link-item {
display: flex;
align-items: center;
}
.el-icon {
margin-left: 4px;
}
</style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
// for now el-switch does not support customized icon in the dot
// we will implement a simple version of el-switch then update the switch
// component for this feature
</script>
<template>
<div class="switch" role="switch">
<div class="switch__action">
<div class="switch__icon">
<slot />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import VPSwitch from './vp-switch.vue'
import DarkIcon from '../icons/dark.vue'
import LightIcon from '../icons/light.vue'
</script>
<template>
<VPSwitch>
<ElIcon :size="13">
<DarkIcon class="dark-icon" />
<LightIcon class="light-icon" />
</ElIcon>
</VPSwitch>
</template>
<style lang="scss" scoped>
.el-icon {
cursor: pointer;
}
.dark-icon,
.light-icon {
transition: color var(--el-transition-duration),
opacity var(--el-transition-duration);
}
.light-icon {
opacity: 1;
position: absolute;
top: 0;
left: 0;
}
.dark-icon {
opacity: 0;
position: absolute;
top: 0;
left: 0;
}
@at-root .dark {
.dark-icon {
opacity: 1;
}
.light-icon {
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import { computed, ref, getCurrentInstance } from 'vue'
const vm = getCurrentInstance()
const props = defineProps({
css: {
type: String,
required: true,
},
cssPreProcessor: {
type: String,
required: true,
},
js: {
type: String,
required: true,
},
jsPreProcessor: {
type: String,
required: true,
},
html: {
type: String,
required: true,
},
})
const globalMapper = {
"'vue'": 'Vue',
"'element-plus'": 'ElementPlus',
}
const js = computed(() => {
const decodedJs = decodeURIComponent(props.js || '')
const imports = /(import*) ([^'\n]*) from ([^\n]*)/g
const globals = []
let match
while ((match = imports.exec(decodedJs))) {
const [_, __, members, target] = match
globals.push(`const ${members} = ${globalMapper[target]};`)
}
const componentRegex = decodedJs.includes('export default defineComponent')
? /export default defineComponent\({([\s\S]*)}\)/g
: /export default {([\s\S]*)}/g
let component = componentRegex.exec(decodedJs)
component = ((component && component[1]) || '')
.replace(/\n {2}/g, '\n')
.trim()
return `${globals.join('\n')}
var Main = {
${component}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app");
`
})
const data = computed(() => {
return JSON.stringify({
html: `<script src="//unpkg.com/vue@next"><${'/script'}>
<script src="//unpkg.com/element-plus/dist/index.full.js"><${'/script'}>
<div id="app">
${decodeURIComponent(props.html).trim()}
</div>
`,
css: `@import url("//unpkg.com/element-plus/dist/index.css");
${decodeURIComponent(props.css).trim()}
`,
js: js.value,
css_pre_processor: props.cssPreProcessor,
js_pre_processor:
props.jsPreProcessor === 'ts' ? ' typescript' : props.jsPreProcessor,
})
})
const formRef = ref(null)
const submit = () => {
formRef.value.submit?.()
}
defineExpose({
submit,
})
</script>
<template>
<form
ref="formRef"
action="https://codepen.io/pen/define/"
method="POST"
target="_blank"
>
<input type="hidden" name="data" :value="data" />
</form>
</template>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { withBase } from 'vitepress'
import { shallowRef, onMounted } from 'vue'
const props = defineProps({
file: {
type: String,
required: true,
},
demo: {
type: Object,
required: true,
},
})
</script>
<template>
<div class="example-showcase">
<ClientOnly>
<component v-if="demo" :is="demo" v-bind="$attrs" />
</ClientOnly>
</div>
</template>
<style lang="scss" scoped>
.example-showcase {
padding: 1rem;
margin: 0.5px;
background-color: var(--bg-color);
&.transparent-enabled {
background-image: linear-gradient(
45deg,
rgb(249, 249, 250) 25%,
transparent 25%
),
linear-gradient(135deg, rgb(249, 249, 250) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgb(249, 249, 250) 75%),
linear-gradient(135deg, transparent 75%, rgb(249, 249, 250) 75%);
background-size: 20px 20px;
background-position: 0px 0px, 10px 0px, 10px -10px, 0px 10px;
}
@at-root .dark .example-showcase {
background-image: unset;
background-color: var(--bg-color-soft);
}
}
</style>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
source: {
type: String,
required: true,
},
})
const decoded = computed(() => {
return decodeURIComponent(props.source)
})
</script>
<template>
<div class="example-source language-vue" v-html="decoded"></div>
</template>
<style scoped lang="scss">
.language-vue {
margin: 0;
border-radius: 0;
}
</style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { useEditLink } from '../../composables/edit-link'
import ExternalIcon from '../icons/external-link-icon.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 }}
<ElIcon :size="16" style="vertical-align: text-top; line-height: 24px">
<ExternalIcon />
</ElIcon>
</a>
</div>
</template>
<style scoped>
.link {
display: inline-block;
font-size: 1rem;
font-weight: 500;
color: var(--text-color-light);
}
.link:hover {
text-decoration: none;
color: var(--brand-color);
}
.icon {
margin-left: 4px;
}
</style>

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useData } from 'vitepress'
import { useLang } from '../../composables/lang'
import localeData from '../../../i18n/component/last-update-at.json'
const { theme, page } = useData()
const lang = useLang()
const prefix = computed(() => {
return localeData[lang.value].title
})
const datetime = ref('')
onMounted(() => {
datetime.value = new Date(page.value.lastUpdated).toLocaleString(lang.value)
})
</script>
<template>
<p class="last-updated">
<span class="prefix">{{ prefix }}:</span>
<span class="datetime">{{ datetime }}</span>
</p>
</template>
<style scoped lang="scss">
@import '../../styles/mixins';
.last-updated {
display: inline-block;
margin: 0;
line-height: 1.4;
font-size: 0.9rem;
color: var(--text-color-light);
.prefix {
display: inline-block;
font-weight: 500;
}
.datetime {
display: inline-block;
margin-left: 6px;
font-weight: 400;
}
}
@include respond-to('lg') {
.last-updated {
font-size: 1rem;
}
}
</style>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import VPEditLink from './vp-edit-link.vue'
import VPLastUpdatedAt from './vp-last-updated-at.vue'
</script>
<template>
<footer class="page-footer">
<div class="edit">
<VPEditLink />
</div>
<div class="updated">
<VPLastUpdatedAt />
</div>
</footer>
</template>
<style scoped lang="scss">
@import '../../styles/mixins';
.page-footer {
padding-top: 1rem;
padding-bottom: 1rem;
overflow: auto;
.updated {
padding-top: 4px;
}
}
@include respond-to('lg') {
.page-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.updated {
padding-top: 0;
}
}
</style>

View File

@ -0,0 +1,90 @@
<script setup lang="ts">
import { withBase } from 'vitepress'
import { ArrowLeft, ArrowRight } from '@element-plus/icons'
import { usePageNav } from '../../composables/page-nav'
const { hasLinks, prev, next } = usePageNav()
</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)">
<ElIcon>
<ArrowLeft />
</ElIcon>
<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>
<ElIcon>
<ArrowRight />
</ElIcon>
</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(--border-color);
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;
}
.el-icon {
display: block;
flex-shrink: 0;
font-size: 1rem;
color: var(--text-color);
transform: translateY(1px);
}
.icon-prev {
margin-right: 8px;
}
.icon-next {
margin-left: 8px;
}
</style>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { useRoute, useData } from 'vitepress'
import { ref, watch } from 'vue'
import { useToc } from '../../composables/use-toc'
import { useActiveSidebarLinks } from '../../composables/active-bar'
const headers = useToc()
const marker = ref()
const container = ref()
useActiveSidebarLinks(container, marker)
</script>
<template>
<aside ref="container" class="toc-wrapper">
<nav class="toc-content">
<h3 class="toc-content__heading">Contents</h3>
<ul class="toc-items">
<li v-for="{ link, text, children } in headers" class="toc-item">
<a class="toc-link" :href="link">{{ text }}</a>
<ul v-if="children">
<li v-for="{ link, text } in children" class="toc-item">
<a class="toc-link subitem" :href="link">{{ text }}</a>
</li>
</ul>
</li>
</ul>
<div ref="marker" class="toc-marker"></div>
</nav>
</aside>
</template>

View File

@ -0,0 +1,40 @@
<script lang="ts" setup>
import VPLink from '../common/vp-link.vue'
import type { Link } from '../../types'
defineProps<{
item: Link
}>()
</script>
<template>
<VPLink
:class="{
'is-menu-link': true,
}"
:href="item.link"
:no-icon="true"
>
{{ item.text }}
</VPLink>
</template>
<style scoped lang="scss">
.is-menu-link {
display: block;
font-size: 13px;
font-weight: 500;
line-height: 24px;
color: var(--text-color);
transition: color var(--el-transition-duration);
&.active {
border-bottom: 2px solid var(--brand-color);
}
&:hover {
color: var(--brand-color);
}
}
</style>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { useNav } from '../../composables/nav'
import VPMenuLink from './vp-menu-link.vue'
const navs = useNav()
defineEmits(['close'])
</script>
<template>
<nav v-if="navs" class="full-screen-menu">
<div v-for="item in navs" class="full-screen-menu__item">
<VPMenuLink :item="item" @click="$emit('close')" />
</div>
</nav>
</template>
<style lang="scss" scoped>
.full-screen-menu__item {
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
</style>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import CommonThemeToggler from '../common/vp-theme-toggler.vue'
import { useTheme } from '../../composables/theme'
const toggle = useTheme()
</script>
<template>
<div class="full-screen-theme-toggler">
<span> Theme </span>
<CommonThemeToggler @click="toggle" />
</div>
</template>
<style lang="scss" scoped>
.full-screen-theme-toggler {
display: flex;
padding: 12px 14px;
align-items: center;
justify-content: space-between;
margin-top: 16px;
font-size: 13px;
background-color: var(--bg-color-soft);
border-radius: 8px;
}
</style>

View File

@ -0,0 +1,82 @@
<script setup lang="ts">
import VPLink from '../common/vp-link.vue'
import { useTranslation } from '../../composables/translation'
import { useToggle } from '../../composables/toggle'
import ExpandIcon from '../icons/expand.vue'
const { languageMap, langs, lang, switchLang, helpTranslate } = useTranslation()
const [show, toggle] = useToggle()
</script>
<template>
<div class="full-screen-translation">
<ElButton
type="text"
@click="toggle"
style="width: 100%; color: var(--text-color)"
>
<div class="translation-toggler">
<span> Translations </span>
<ElIcon :size="14">
<ExpandIcon class="toggle-icon" :class="{ expanded: show }" />
</ElIcon>
</div>
</ElButton>
<div v-show="show" class="translation-items">
<p
v-for="l in langs"
class="translation-item"
:class="{ active: l === lang }"
@click="switchLang(l)"
>
{{ languageMap[l] }}
</p>
<p class="translation-item">
<VPLink href="https://crowdin.com/project/element-plus">
{{ helpTranslate }}
</VPLink>
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.full-screen-translation {
border-bottom: 1px solid var(--border-color);
}
.translation-toggler {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
line-height: 24px;
.toggle-icon {
transition: transform var(--el-transition-duration);
transform: rotate(180deg);
&.expanded {
transform: rotate(0deg);
}
}
}
.translation-items {
padding-bottom: 12px;
.translation-item {
cursor: pointer;
margin: 0;
font-size: 14px;
line-height: 32px;
&.active {
font-weight: 500;
color: var(--brand-color);
}
.link-item {
font-weight: 500;
}
}
}
</style>

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
import { computed, markRaw } from 'vue'
import { hyphenate } from '@vue/shared'
import clipboardCopy from 'clipboard-copy'
import { ElMessage } from 'element-plus'
import * as Icons from '@element-plus/icons'
import { useLang } from '../../composables/lang'
import localeData from '../../../i18n/component/icons.json'
const lang = useLang()
const locale = computed(() => localeData[lang.value])
const copySvgIcon = async (svg) => {
try {
await clipboardCopy(
`<el-icon>
<${hyphenate(svg)} />
</el-icon>`
)
ElMessage({
showClose: true,
message: locale.value['copy-success'],
type: 'success',
})
} catch (e) {
ElMessage({
showClose: true,
message: locale.value['copy-error'],
type: 'error',
})
}
}
</script>
<template>
<ul class="demo-icon-list">
<li
v-for="component in Icons"
:key="component"
class="icon-item"
@click="copySvgIcon(component.name)"
>
<span class="demo-svg-icon">
<ElIcon :size="20">
<component :is="component" />
</ElIcon>
<span class="icon-name">{{ component.name }}</span>
</span>
</li>
</ul>
</template>
<style scoped lang="scss">
.demo-icon-list {
overflow: hidden;
list-style: none;
padding: 0 !important;
border-top: 1px solid var(--el-border-color-base);
border-left: 1px solid var(--el-border-color-base);
border-radius: 4px;
display: grid;
grid-template-columns: repeat(6, 1fr);
.icon-item {
text-align: center;
color: var(--el-text-color-regular);
height: 120px;
font-size: 13px;
border-right: 1px solid var(--el-border-color-base);
border-bottom: 1px solid var(--el-border-color-base);
transition: background-color var(--el-transition-duration);
&:hover {
background-color: var(--el-border-color-extra-light);
.el-icon {
color: var(--brand-color-light);
}
color: var(--brand-color-light);
}
.demo-svg-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
.icon-name {
margin-top: 8px;
}
}
}
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<el-row :gutter="12">
<el-col :span="10" :xs="{ span: 12 }">
<div class="demo-color-box" :style="{ background: primary }">
Brand Color
<div class="value">#409EFF</div>
<div
class="bg-color-sub"
:style="{ background: tintColor(primary, 0.9) }"
>
<div
v-for="item in 9"
:key="item"
class="bg-blue-sub-item"
:style="{ background: tintColor(primary, (item + 1) / 10) }"
></div>
</div>
</div>
</el-col>
</el-row>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const primary = ref('#409EFF')
const tintColor = (c: string, tint: number) => {
const color = c.replace('#', '')
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) {
// when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
return `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}`
}
}
return {
tintColor,
primary,
}
},
})
</script>

View File

@ -0,0 +1,137 @@
<template>
<el-row :gutter="12">
<el-col :span="6" :xs="{ span: 12 }">
<div class="demo-color-box-group">
<div
v-for="(text, i) in textColors"
:key="i"
class="demo-color-box demo-color-box-other"
:style="{ background: getCssVarName('text-color', text.type) }"
>
{{ text.name || formatType(text.type) + ' Text' }}
<div class="value">
{{ getCssVarValue('text-color', text.type).toUpperCase() }}
</div>
</div>
</div>
</el-col>
<el-col :span="6" :xs="{ span: 12 }">
<div class="demo-color-box-group">
<div
v-for="(border, i) in borderColors"
:key="i"
class="demo-color-box demo-color-box-other demo-color-box-lite"
:style="{ background: `var(--el-border-color-${border.type})` }"
>
{{ formatType(border.type) + ' Border' }}
<div class="value">
{{ getColorValue(border.type).toUpperCase() }}
</div>
</div>
</div>
</el-col>
<el-col :span="6" :xs="{ span: 12 }">
<div class="demo-color-box-group">
<div
class="demo-color-box demo-color-box-other"
:style="{ background: black }"
>
Basic Black
<div class="value">{{ black }}</div>
</div>
<div
class="demo-color-box demo-color-box-other"
:style="{
background: white,
color: '#303133',
border: '1px solid #eee',
}"
>
Basic White
<div class="value">{{ white }}</div>
</div>
<div class="demo-color-box demo-color-box-other bg-transparent">
Transparent
<div class="value">Transparent</div>
</div>
</div>
</el-col>
</el-row>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue'
export default defineComponent({
setup() {
const getColorValue = (type: string) => {
return getComputedStyle(document.documentElement).getPropertyValue(
`--el-border-color-${type}`
)
}
const formatType = (type: string) => {
return type
.split('-')
.map((item) => item.charAt(0).toUpperCase() + item.slice(1))
.join(' ')
}
const getCssVarName = (namespace: string, type: string) => {
return `var(--el-${namespace}-${type})`
}
const getCssVarValue = (namespace, type) => {
return getComputedStyle(document.documentElement).getPropertyValue(
`--el-${namespace}-${type}`
)
}
return {
borderColors: markRaw([
{
type: 'base',
color: '#DCDFE6',
},
{
type: 'light',
color: '#E4E7ED',
},
{
type: 'lighter',
color: '#EBEEF5',
},
{
type: 'extra-light',
color: '#F2F6FC',
},
]),
textColors: markRaw([
{
name: 'Primary Text',
type: 'primary',
},
{
name: 'Regular Text',
type: 'regular',
},
{
name: 'Secondary Text',
type: 'secondary',
},
{
name: 'Placeholder Text',
type: 'placeholder',
},
]),
black: '#000000',
white: '#FFFFFF',
formatType,
getColorValue,
getCssVarName,
getCssVarValue,
}
},
})
</script>

View File

@ -0,0 +1,62 @@
<template>
<el-row :gutter="12">
<el-col
v-for="(type, i) in colorsType"
:key="type"
:span="6"
:xs="{ span: 12 }"
>
<div class="demo-color-box" :style="{ background: getColorValue(type) }">
{{ type.charAt(0).toUpperCase() + type.slice(1) }}
<div class="value">{{ getColorValue(type).toUpperCase() }}</div>
<div class="bg-color-sub">
<div
v-for="(_, key) in Array(2)"
:key="key"
class="bg-secondary-sub-item"
:style="{
background: tintColor(getColorValue(type), (key + 8) / 10),
}"
></div>
</div>
</div>
</el-col>
</el-row>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue'
export default defineComponent({
setup() {
const tintColor = (c: string, tint: number) => {
const color = c.trim().slice(1)
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) {
// when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
return `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}`
}
}
const getColorValue = (type: string) => {
return getComputedStyle(document.documentElement).getPropertyValue(
`--el-color-${type}`
)
}
return {
colorsType: markRaw(['success', 'warning', 'danger', 'info']),
getColorValue,
tintColor,
}
},
})
</script>

View File

@ -0,0 +1,147 @@
<script setup lang="ts">
import { computed, shallowRef, watch, toRef, ref } from 'vue'
import { useToggle } from '../composables/toggle'
import { useLang } from '../composables/lang'
import { useSourceCode } from '../composables/source-code'
import GithubIcon from './icons/github.vue'
import SourceCodeIcon from './icons/source-code.vue'
import CodepenIcon from './icons/codepen.vue'
import Example from './demo/vp-example.vue'
import SourceCode from './demo/vp-source-code.vue'
import Codepen from './demo/vp-codepen.vue'
import demoBlockLocale from '../../i18n/component/demo-block.json'
const props = defineProps({
// source is encoded via encodeURIComponent
source: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
css: {
type: String,
required: true,
},
cssPreProcessor: {
type: String,
required: true,
},
js: {
type: String,
required: true,
},
html: {
type: String,
required: true,
},
demos: {
type: Object,
required: true,
},
})
const formatPathDemos = computed(() => {
const demos = {}
Object.keys(props.demos).forEach((key) => {
demos[key.replace('../../examples/', '').replace('.vue', '')] =
props.demos[key].default
})
return demos
})
const loaded = shallowRef(false)
const hasError = shallowRef(false)
const dataOpt = shallowRef('')
const setupScript = shallowRef('')
const template = shallowRef('')
const [sourceVisible, setSourceVisible] = useToggle()
const hasSetup = computed(() => loaded.value && setupScript.value !== '')
const lang = useLang()
const locale = computed(() => demoBlockLocale[lang.value])
const onDemoLoaded = (content) => {
loaded.value = true
}
const demoSourceUrl = useSourceCode(toRef(props, 'path'))
const codepenRef = ref()
const onCodepenClicked = () => {
codepenRef.value.submit?.()
}
</script>
<template>
<div class="example">
<Codepen
:css="props.css"
:css-pre-processor="props.cssPreProcessor"
:html="props.html"
:js="props.js"
ref="codepenRef"
/>
<div class="op-btns">
<ElTooltip :content="locale['edit-in-codepen']" :visible-arrow="false">
<ElIcon :size="20" class="op-btn">
<CodepenIcon @click="onCodepenClicked" />
</ElIcon>
</ElTooltip>
<ElTooltip :content="locale['edit-on-github']" :visible-arrow="false">
<ElIcon :size="20" class="op-btn github">
<a :href="demoSourceUrl" rel="noreferrer noopener" target="_blank">
<GithubIcon />
</a>
</ElIcon>
</ElTooltip>
<ElTooltip :content="locale['view-source']" :visible-arrow="false">
<ElIcon :size="20" class="op-btn" @click="setSourceVisible">
<SourceCodeIcon />
</ElIcon>
</ElTooltip>
</div>
<ElDivider />
<Example :file="path" :demo="formatPathDemos[path]" />
<ElDivider v-if="sourceVisible" />
<el-collapse-transition>
<SourceCode v-show="sourceVisible" :source="source" />
</el-collapse-transition>
</div>
</template>
<style scoped lang="scss">
.example {
border: var(--el-border-base);
border-radius: var(--el-border-radius-base);
.op-btns {
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: flex-end;
height: 3rem;
line-height: 3rem;
.op-btn {
margin: 0 0.5rem;
cursor: pointer;
&.github a {
color: var(--text-color-lighter);
}
}
}
.el-divider {
margin: 0;
background-color: var(--border-color);
}
}
</style>

View File

@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 512 512">
<path
d="M256 48C141.13 48 48 141.13 48 256s93.13 208 208 208s208-93.13 208-208S370.87 48 256 48zm91.36 212.65a16 16 0 0 1-22.63.09L272 208.42V342a16 16 0 0 1-32 0V208.42l-52.73 52.32A16 16 0 1 1 164.73 238l80-79.39a16 16 0 0 1 22.54 0l80 79.39a16 16 0 0 1 .09 22.65z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,14 @@
<template>
<svg viewBox="0 0 24 24">
<path
d="M18.144 13.067v-2.134L16.55 12zm1.276 1.194a.628.628 0 0 1-.006.083l-.005.028l-.011.053l-.01.031a.443.443 0 0 1-.017.047l-.014.03a.78.78 0 0 1-.021.043l-.019.03a.57.57 0 0 1-.08.1l-.026.025a.602.602 0 0 1-.036.03l-.029.022l-.01.008l-6.782 4.522a.637.637 0 0 1-.708 0L4.864 14.79l-.01-.008a.599.599 0 0 1-.065-.052l-.026-.025l-.032-.034l-.021-.028a.588.588 0 0 1-.067-.11l-.014-.031a.644.644 0 0 1-.017-.047l-.01-.03c-.004-.018-.008-.036-.01-.054l-.006-.028a.628.628 0 0 1-.006-.083V9.739a.58.58 0 0 1 .006-.083l.005-.027l.011-.054l.01-.03a.574.574 0 0 1 .12-.217l.031-.034l.026-.025a.62.62 0 0 1 .065-.052l.01-.008l6.782-4.521a.638.638 0 0 1 .708 0l6.782 4.521l.01.008l.03.022l.035.03c.01.008.017.016.026.025a.545.545 0 0 1 .08.1l.019.03a.633.633 0 0 1 .021.043l.014.03c.007.016.012.032.017.047l.01.031c.004.018.008.036.01.054l.006.027a.619.619 0 0 1 .006.083zM12 0C5.373 0 0 5.372 0 12c0 6.627 5.373 12 12 12c6.628 0 12-5.372 12-12c0-6.627-5.372-12-12-12m0 10.492L9.745 12L12 13.51L14.255 12zm.638 4.124v2.975l4.996-3.33l-2.232-1.493zm-6.272-.356l4.996 3.33v-2.974l-2.764-1.849zm11.268-4.52l-4.996-3.33v2.974l2.764 1.85zm-6.272-.356V6.41L6.366 9.74l2.232 1.493zm-5.506 1.549v2.134L7.45 12z"
fill="currentColor"
></path>
</svg>
</template>
<script>
export default {
name: 'CodePenIcon',
}
</script>

View File

@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 24 24">
<path
d="M11.01 3.05C6.51 3.54 3 7.36 3 12a9 9 0 0 0 9 9c4.63 0 8.45-3.5 8.95-8c.09-.79-.78-1.42-1.54-.95A5.403 5.403 0 0 1 11.1 7.5c0-1.06.31-2.06.84-2.89c.45-.67-.04-1.63-.93-1.56z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 44 44">
<path
fill="currentColor"
d="M37.41,32.37c0,1.57-.83,1.93-.83,1.93L21.51,43A1.69,1.69,0,0,1,20,43S5.2,34.4,4.66,34a1.29,1.29,0,0,1-.55-1V15.24c0-.78,1-1.33,1-1.33L19.86,5.36a2,2,0,0,1,1.79,0l14.46,8.41a2.06,2.06,0,0,1,1.25,2.06V32.37Zm-5.9-17L21.35,9.5a1.59,1.59,0,0,0-1.41,0L8.33,16.15s-.77.46-.76,1.08,0,13.92,0,13.92A1,1,0,0,0,8,31.9c.43.3,12,7,12,7a1.31,1.31,0,0,0,1.19,0C21.91,38.5,33,32.11,33,32.11s.65-.28.65-1.51V27.13l-13,7.9V32a3.05,3.05,0,0,1,1-2.07L33.2,23a2.44,2.44,0,0,0,.55-1.46V18.43L20.64,26.35v-3.2a2.22,2.22,0,0,1,.83-1.79ZM41.07,4.22a.39.39,0,0,0-.37-.42H38V1.06c0-.16-.26-.22-.53-.22L36,1.08c-.18,0-.31.12-.31.23V3.8H33a.4.4,0,0,0-.36.37v2h3V9c0,.16.26.27.54.23l1.51-.25c.18,0,.29-.13.29-.23V6.14h3Z"
/>
</svg>
</template>

View File

@ -0,0 +1,24 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 153.71 38">
<g>
<g>
<path
fill="currentColor"
d="M142,26.16s.28,0,.82,0a.72.72,0,0,1,.69.41s1.08,2,1.37,2.48,0,.42-.12.41h0s-.31,0-3.45,0a4.93,4.93,0,0,1-4.54-4.54v-7H134.3V15.28c0-.36.41-.41.41-.41h2.07V12.25a.6.6,0,0,1,.41-.55l2.3-.66c.34-.1.59,0,.59.35V15h3.58c.34,0,.41.41.41.41V18h-4v6.06c0,1.76,1.93,2.07,1.93,2.07Zm-10.6,3h-2.2c-.43,0-.41-.55-.41-.55V18.45c0-.62-.83-.83-.83-.83h-4.54c-.68,0-.69.83-.69.83V28.77a.41.41,0,0,1-.41.42h-2.2c-.48,0-.41-.55-.41-.55V15.83c0-1,1.24-1.24,1.24-1.24h9.63c1,0,1.23,1.24,1.23,1.24V28.5c0,.72-.41.69-.41.69ZM115.73,23.4H107.2v2.07c0,.74,1,1,1,1H115a1.16,1.16,0,0,1,.82.42s.61,1.25.83,1.79-.41.55-.41.55H106c-1.24,0-1.51-1.52-1.51-1.52V16c0-.67,1-1,1-1h10.32c1,0,1.24,1.23,1.24,1.23v5.93c0,1-1.24,1.23-1.24,1.23Zm-1.52-4.95s-.08-.69-.68-.69h-5.65s-.68.18-.68.69V20a.69.69,0,0,0,.68.69h5.65a.9.9,0,0,0,.68-.83V18.45ZM101.28,29.19h-2.2c-.29,0-.41-.42-.41-.42V18.45c0-.64-.83-.83-.83-.83H95.78c-.58,0-.69.83-.69.83V28.77c0,.35-.41.42-.41.42h-2.2c-.31,0-.42-.42-.42-.42V18.45c0-.66-.82-.83-.82-.83H89.17c-.63,0-.68.83-.68.83V28.77a.39.39,0,0,1-.42.42h-2.2a.41.41,0,0,1-.41-.42V15.69c0-.75,1.1-1.1,1.1-1.1h13.76c1.1,0,1.37,1.38,1.37,1.38v12.8c0,.48-.41.42-.41.42Zm-20-5.79H72.8v2.07c0,.74,1,1,1,1h6.88a1.19,1.19,0,0,1,.83.42s.6,1.25.82,1.79-.41.55-.41.55H71.56c-1.24,0-1.51-1.52-1.51-1.52V16c0-.67,1-1,1-1H81.33c1,0,1.24,1.23,1.24,1.23v5.93c0,1-1.24,1.23-1.24,1.23Zm-1.51-4.95s-.09-.69-.69-.69H73.49s-.69.18-.69.69V20a.69.69,0,0,0,.69.69h5.64a.91.91,0,0,0,.69-.83V18.45ZM68,29.19H62.76a4.35,4.35,0,0,1-4.13-4c0-3.91,0-16.1,0-16.1h2.48a.79.79,0,0,1,.82.82V24.37A2.58,2.58,0,0,0,63.86,26h2.2s.72-.23,1.24.69l1.1,1.93s.08.55-.41.55Zm-26.56-.83V10.19a1,1,0,0,1,.69-1H55.05c.73,0,.42.83.42.83s-.41,1.12-.69,1.65a1,1,0,0,1-.83.55H45.56a.77.77,0,0,0-.83.69v4.54h9.5c.55,0,.27.69.27.69s-.71,1.52-1,1.93a1.05,1.05,0,0,1-.83.41h-8v4.82a.91.91,0,0,0,.69.83h8.81a.85.85,0,0,1,.82.41l1.24,1.93c.37.56-.14.69-.14.69H42.26C41.68,29.19,41.43,28.36,41.43,28.36Zm-8.14-1.14c0,1.57-.83,1.93-.83,1.93S18.32,37.31,17.4,37.83a1.68,1.68,0,0,1-1.52,0S1.09,29.25.55,28.87a1.29,1.29,0,0,1-.55-1s0-17,0-17.78S1,8.76,1,8.76L15.75.21a2,2,0,0,1,1.79,0S30.6,7.8,32,8.62a2.08,2.08,0,0,1,1.25,2.06s0,15.07,0,16.54Zm-5.9-17c-3-1.74-10.16-5.87-10.16-5.87a1.58,1.58,0,0,0-1.41,0L4.22,11s-.77.46-.76,1.08S3.46,26,3.46,26a1,1,0,0,0,.43.75c.43.3,12,7,12,7a1.3,1.3,0,0,0,1.19,0c.72-.4,11.82-6.79,11.82-6.79s.65-.28.65-1.51c0-.36,0-1.74,0-3.47L16.53,29.88v-3a3,3,0,0,1,1-2.07l11.56-7a2.49,2.49,0,0,0,.55-1.46c0-1.27,0-2.37,0-3.07L16.53,21.2V18a2.17,2.17,0,0,1,.83-1.79Z"
/>
<path
fill="currentColor"
d="M150.32,11.21h-2.24v-5c0-.11.12-.21.29-.24l1.44-.26c.26,0,.51.07.51.24Z"
/>
<path
fill="currentColor"
d="M148.08,9h2.24v5.11c0,.11-.11.21-.28.25l-1.45.26c-.26.05-.51-.07-.51-.24Z"
/>
<path
fill="currentColor"
d="M145.09,9h8.22a.4.4,0,0,1,.4.4v1.85a0,0,0,0,1,0,0h-9a0,0,0,0,1,0,0V9.36A.4.4,0,0,1,145.09,9Z"
/>
</g>
</g>
</svg>
</template>

View File

@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 24 24">
<path
d="M11.29 8.71L6.7 13.3a.996.996 0 1 0 1.41 1.41L12 10.83l3.88 3.88a.996.996 0 1 0 1.41-1.41L12.7 8.71a.996.996 0 0 0-1.41 0z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,16 @@
<template>
<svg
class="link-icon"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M853.333333 469.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v256a42.666667 42.666667 0 0 1-42.666667 42.666667H256a42.666667 42.666667 0 0 1-42.666667-42.666667V256a42.666667 42.666667 0 0 1 42.666667-42.666667h256a42.666667 42.666667 0 0 0 0-85.333333H256a128 128 0 0 0-128 128v512a128 128 0 0 0 128 128h512a128 128 0 0 0 128-128v-256a42.666667 42.666667 0 0 0-42.666667-42.666667z"
fill="currentColor"
></path>
<path
d="M682.666667 213.333333h67.413333l-268.373333 267.946667a42.666667 42.666667 0 0 0 0 60.586667 42.666667 42.666667 0 0 0 60.586666 0L810.666667 273.92V341.333333a42.666667 42.666667 0 0 0 42.666666 42.666667 42.666667 42.666667 0 0 0 42.666667-42.666667V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667h-170.666666a42.666667 42.666667 0 0 0 0 85.333333z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,15 @@
<template>
<svg viewBox="0 0 16 16">
<path
fillRule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59c.4.07.55-.17.55-.38c0-.19-.01-.82-.01-1.49c-2.01.37-2.53-.49-2.69-.94c-.09-.23-.48-.94-.82-1.13c-.28-.15-.68-.52-.01-.53c.63-.01 1.08.58 1.23.82c.72 1.21 1.87.87 2.33.66c.07-.52.28-.87.51-1.07c-1.78-.2-3.64-.89-3.64-3.95c0-.87.31-1.59.82-2.15c-.08-.2-.36-1.02.08-2.12c0 0 .67-.21 2.2.82c.64-.18 1.32-.27 2-.27c.68 0 1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82c.44 1.1.16 1.92.08 2.12c.51.56.82 1.27.82 2.15c0 3.07-1.87 3.75-3.65 3.95c.29.25.54.73.54 1.48c0 1.07-.01 1.93-.01 2.2c0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"
fill="currentColor"
></path>
</svg>
</template>
<script>
export default {
name: 'GithubIcon',
}
</script>

View File

@ -0,0 +1,8 @@
<template>
<svg viewBox="0 0 24 24">
<path
d="M6.05 4.14l-.39-.39a.993.993 0 0 0-1.4 0l-.01.01a.984.984 0 0 0 0 1.4l.39.39c.39.39 1.01.39 1.4 0l.01-.01a.984.984 0 0 0 0-1.4zM3.01 10.5H1.99c-.55 0-.99.44-.99.99v.01c0 .55.44.99.99.99H3c.56.01 1-.43 1-.98v-.01c0-.56-.44-1-.99-1zm9-9.95H12c-.56 0-1 .44-1 .99v.96c0 .55.44.99.99.99H12c.56.01 1-.43 1-.98v-.97c0-.55-.44-.99-.99-.99zm7.74 3.21c-.39-.39-1.02-.39-1.41-.01l-.39.39a.984.984 0 0 0 0 1.4l.01.01c.39.39 1.02.39 1.4 0l.39-.39a.984.984 0 0 0 0-1.4zm-1.81 15.1l.39.39a.996.996 0 1 0 1.41-1.41l-.39-.39a.993.993 0 0 0-1.4 0c-.4.4-.4 1.02-.01 1.41zM20 11.49v.01c0 .55.44.99.99.99H22c.55 0 .99-.44.99-.99v-.01c0-.55-.44-.99-.99-.99h-1.01c-.55 0-.99.44-.99.99zM12 5.5c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6s-2.69-6-6-6zm-.01 16.95H12c.55 0 .99-.44.99-.99v-.96c0-.55-.44-.99-.99-.99h-.01c-.55 0-.99.44-.99.99v.96c0 .55.44.99.99.99zm-7.74-3.21c.39.39 1.02.39 1.41 0l.39-.39a.993.993 0 0 0 0-1.4l-.01-.01a.996.996 0 0 0-1.41 0l-.39.39c-.38.4-.38 1.02.01 1.41z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,14 @@
<template>
<svg viewBox="0 0 24 24">
<path
d="M8.7 15.9L4.8 12l3.9-3.9a.984.984 0 0 0 0-1.4a.984.984 0 0 0-1.4 0l-4.59 4.59a.996.996 0 0 0 0 1.41l4.59 4.6c.39.39 1.01.39 1.4 0a.984.984 0 0 0 0-1.4zm6.6 0l3.9-3.9l-3.9-3.9a.984.984 0 0 1 0-1.4a.984.984 0 0 1 1.4 0l4.59 4.59c.39.39.39 1.02 0 1.41l-4.59 4.6a.984.984 0 0 1-1.4 0a.984.984 0 0 1 0-1.4z"
fill="currentColor"
></path>
</svg>
</template>
<script>
export default {
name: 'SourceCodeIcon',
}
</script>

View File

@ -0,0 +1,18 @@
<template>
<svg viewBox="0 0 24 24">
<g fill="none">
<path
d="M4 6a1 1 0 0 1 1-1h14a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1z"
fill="currentColor"
></path>
<path
d="M4 18a1 1 0 0 1 1-1h14a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1z"
fill="currentColor"
></path>
<path
d="M11 11a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2h-8z"
fill="currentColor"
></path>
</g>
</svg>
</template>

View File

@ -0,0 +1,12 @@
<template>
<svg viewBox="0 0 512 512">
<path
d="M478.33 433.6l-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362L368 281.65L401.17 362z"
fill="currentColor"
></path>
<path
d="M267.84 342.92a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73c39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36c-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93c.92 1.19 1.83 2.35 2.74 3.51c-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59c22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{
active: boolean
}>()
</script>
<template>
<div :class="{ 'menu-hamburger': true, active }" role="button">
<span class="hamburger-1" />
<span class="hamburger-2" />
<span class="hamburger-3" />
</div>
</template>

View File

@ -0,0 +1,50 @@
<script lang="ts" setup>
import { useData, useRoute } from 'vitepress'
import VPLink from '../common/vp-link.vue'
import { isActiveLink } from '../../utils'
import type { Link } from '../../types'
defineProps<{
item: Link
}>()
const route = useRoute()
</script>
<template>
<VPLink
:class="{
'is-menu-link': true,
active: isActiveLink(
route,
item.activeMatch || item.link,
!!item.activeMatch
),
}"
:href="item.link"
:no-icon="true"
>
{{ item.text }}
</VPLink>
</template>
<style scoped lang="scss">
.is-menu-link {
display: block;
padding: 0 12px;
line-height: calc(var(--nav-height) - 3px);
font-size: 14px;
font-weight: 500;
color: var(--text-color);
transition: color var(--el-transition-duration);
&.active {
border-bottom: 2px solid var(--brand-color);
}
&:hover {
color: var(--brand-color);
}
}
</style>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import VPMenuLink from './vp-menu-link.vue'
import { useNav } from '../../composables/nav'
const navs = useNav()
</script>
<template>
<nav v-if="navs" class="navbar-menu">
<VPMenuLink v-for="item in navs" :item="item" />
</nav>
</template>

View File

@ -0,0 +1,205 @@
<script setup lang="ts">
import '@docsearch/css'
import { ref, computed, watch, onMounted, getCurrentInstance } from 'vue'
import { useRouter, useRoute } from 'vitepress'
import docsearch from '@docsearch/js'
import { useLang } from '../../composables/lang'
// import type { DefaultTheme } from '../config'
import type { DocSearchHit } from '@docsearch/react/dist/esm/types'
const props = defineProps<{
options: any
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 searchIndexMap = {
'zh-CN': 'element-zh',
'en-US': 'element-en',
es: 'element-es',
'fr-FR': 'element-fr',
jp: 'element-jp',
}
const lang = useLang()
function initialize(userOptions: any) {
// if the user has multiple locales, the search results should be filtered
// based on the language
const facetFilters = props.multilang ? ['language:' + lang.value] : []
docsearch(
Object.assign({}, userOptions, {
container: '#docsearch',
indexName: searchIndexMap[lang.value],
searchParameters: Object.assign({}, userOptions.searchParameters, {
// pass a custom lang facetFilter to allow multiple language search
// https://github.com/algolia/docsearch-configs/pull/3942
facetFilters: facetFilters.concat(
userOptions.searchParameters?.facetFilters || []
),
}),
navigator: {
navigate: ({ suggestionUrl }: { suggestionUrl: string }) => {
const { pathname: hitPathname } = new URL(
window.location.origin + suggestionUrl
)
// 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 + suggestionUrl)
} else {
router.go(suggestionUrl)
}
},
},
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,
},
}
},
})
)
}
</script>
<template>
<div class="algolia-search-box" id="docsearch" />
</template>
<style lang="scss">
@import '../../styles/mixins';
.algolia-search-box {
// display: flex;
// align-items: center;
// line-height: var(--header-height);
// padding-left: 0.5rem;
// padding-top: 1px;
// margin-right: 12px;
// .search-box-placeholder,
// .search-box-key {
// display: flex;
// }
@include respond-to('md') {
min-width: 176.3px;
}
}
.DocSearch {
--docsearch-primary-color: var(--brand-color);
--docsearch-key-gradient: rgba(125, 125, 125, 0.1);
// --docsearch-key-shadow: rgba(125, 125, 125, 0.3);
--docsearch-footer-height: 44px;
--docsearch-footer-background: var(--bg-color);
--docsearch-footer-shadow: 0 -1px 0 0 #e0e3e8,
0 -3px 6px 0 rgba(69, 98, 155, 0.12);
--docsearch-searchbox-background: var(--bg-color-soft);
--docsearch-searchbox-focus-background: var(--bg-color-mute);
--docsearch-muted-color: var(--text-color-lighter);
--docsearch-text-color: var(--text-color-light);
--docsearch-modal-background: var(--bg-color-soft);
.dark & {
--docsearch-text-color: var(--text-color-light);
// --docsearch-searchbox-focus-background: var(--bg-color-mute);
.DocSearch-Button {
.DocSearch-Button-Key {
box-shadow: unset;
}
}
}
background-color: transparent;
@include respond-to('md') {
background-color: var(--docsearch-searchbox-background);
}
}
</style>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { Component } from 'vue'
defineProps<{
icon: Component
link: string
text: string
}>()
</script>
<template>
<a
:href="link"
:title="text"
target="_blank"
rel="noreferrer noopener"
class="social-link"
>
<ElIcon :size="20">
<component :is="icon" />
</ElIcon>
</a>
</template>
<style scoped lang="scss">
.social-link {
padding: 0 4px;
}
</style>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import VPSocialLink from './vp-social-link.vue'
import { useSocialLinks } from '../../composables/social-links'
const { theme } = useData()
const links = useSocialLinks()
</script>
<template>
<div class="social-links">
<VPSocialLink v-for="link in links" v-bind="link" />
</div>
</template>
<style scoped lang="scss">
@import '../../styles/mixins';
.social-links {
height: 20px;
padding: 0 8px;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { useTheme } from '../../composables/theme'
import CommonThemeToggler from '../common/vp-theme-toggler.vue'
const toggle = useTheme()
</script>
<template>
<div class="theme-toggler-content">
<CommonThemeToggler @click="toggle" />
</div>
</template>
<style scoped lang="scss">
@import '../../styles/mixins';
.theme-toggler-content {
@include with-bg;
display: none;
border-radius: 50%;
height: 20px;
padding: 0 8px;
@include respond-to('md') {
display: block;
}
}
</style>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import VPLink from '../common/vp-link.vue'
import { useTranslation } from '../../composables/translation'
import { PREFERRED_LANG_KEY, defaultLang } from '../../constant'
import TranslationIcon from '../icons/translation-icon.vue'
const { switchLang, languageMap, langs, lang, helpTranslate } = useTranslation()
</script>
<template>
<div class="translation-container">
<ClientOnly>
<ElPopover
:show-arrow="false"
trigger="hover"
popper-class="translation-popup"
>
<template #reference>
<ElIcon :size="20">
<TranslationIcon />
</ElIcon>
</template>
<div
v-for="l in langs"
:key="l"
@click="switchLang(l)"
:class="{ language: true, selected: l === lang }"
>
{{ languageMap[l] }}
</div>
<div class="language">
<VPLink href="https://crowdin.com/project/element-plus">
{{ helpTranslate }}
</VPLink>
</div>
</ElPopover>
</ClientOnly>
</div>
</template>
<style lang="scss" scoped>
@import '../../styles/mixins';
.translation-container {
display: none;
height: 20px;
padding: 0 8px;
@include respond-to('md') {
display: block;
}
@at-root .translation-popup.el-popper {
box-shadow: var(--el-box-shadow-base);
.language {
cursor: pointer;
padding: 0 16px;
line-height: 28px;
&.selected {
color: var(--brand-color);
}
.link-item {
font-weight: 500;
}
}
}
}
</style>

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { useRoute } from 'vitepress'
import { isActive } from '../../utils'
import type { Link } from '../../types'
defineProps<{
item: Link
}>()
defineEmits(['close'])
const route = useRoute()
</script>
<template>
<a
:class="{ link: true, active: isActive(route, item.link) }"
:href="item.link"
@click="$emit('close')"
>
<p class="link-text">{{ item.text }}</p>
</a>
</template>
<style scoped lang="scss">
.link {
display: block;
padding: 0.625rem 2rem 0.625rem 1.5rem;
line-height: 1.5;
font-size: 0.9rem;
margin: 0 8px;
border-radius: 8px;
.link-text {
margin: 0;
}
}
.link:hover .link-text {
color: var(--brand-color);
transition: color 0.25s;
}
.link.active {
background-color: var(--bg-brand-color);
.link-text {
font-weight: 600;
color: var(--brand-color);
transition: color 0.25s;
}
}
.link-text {
line-height: 20px;
font-size: 13px;
font-weight: 500;
color: var(--text-color-light);
transition: color 0.5s;
}
</style>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import ToggleButton from '../icons/toggle-button.vue'
</script>
<template>
<div class="sidebar-button">
<ElIcon :size="24">
<ToggleButton />
</ElIcon>
</div>
</template>
<style>
.sidebar-button {
cursor: pointer;
color: var(--text-color);
display: flex;
align-items: center;
}
.sidebar-button .icon {
display: block;
width: 1.25rem;
height: 1.25rem;
}
/* @media screen and (max-width: 1044px) {
.sidebar-button {
display: block;
}
} */
</style>

View File

@ -0,0 +1,59 @@
<script setup>
import VPOverlay from './vp-overlay.vue'
import VPNav from './vp-nav.vue'
import VPSubNav from './vp-subnav.vue'
import VPSidebar from './vp-sidebar.vue'
import VPContent from './vp-content.vue'
import VPSponsors from './vp-sponsors.vue'
import { useToggle } from '../composables/toggle'
import { useSidebar } from '../composables/sidebar'
import { useToggleWidgets } from '../composables/toggle-widgets'
import { breakpoints } from '../constant'
const [isSidebarOpen, toggleSidebar] = useToggle(false)
const { hasSidebar } = useSidebar()
useToggleWidgets(isSidebarOpen, () => {
if (window.outerWidth >= breakpoints.lg) {
toggleSidebar(false)
}
})
</script>
<template>
<div class="App">
<VPOverlay
class="overlay"
:show="isSidebarOpen"
@click="toggleSidebar(false)"
/>
<VPNav />
<VPSubNav v-if="hasSidebar" @open-menu="toggleSidebar(true)" />
<VPSidebar :open="isSidebarOpen" @close="toggleSidebar(false)">
<template #top>
<VPSponsors />
</template>
<template #bottom>
<slot name="sidebar-bottom" />
</template>
</VPSidebar>
<VPContent>
<template #content-top>
<slot name="content-top" />
</template>
<template #content-bottom>
<slot name="content-bottom" />
</template>
<template #aside-top>
<slot name="aside-top" />
</template>
<template #aside-mid>
<slot name="aside-mid" />
</template>
<template #aside-bottom>
<slot name="aside-bottom" />
</template>
</VPContent>
<Debug />
</div>
</template>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData, useRoute } from 'vitepress'
import { useSidebar } from '../composables/sidebar'
import VPHeroContent from './vp-hero-content.vue'
import VPDocContent from './vp-doc-content.vue'
import VPNotFound from './vp-not-found.vue'
const { frontmatter } = useData()
const route = useRoute()
const isNotFound = computed(() => route.component === VPNotFound)
const isHeroPost = computed(() => frontmatter.value.page === true)
const { hasSidebar } = useSidebar()
</script>
<template>
<main :class="{ 'page-content': true, 'has-sidebar': hasSidebar }">
<VPNotFound v-if="isNotFound" />
<VPHeroContent v-else-if="isHeroPost" />
<VPDocContent v-else>
<template #content-top><slot name="content-top" /></template>
<template #content-bottom><slot name="content-bottom" /></template>
</VPDocContent>
</main>
</template>

View File

@ -0,0 +1,154 @@
<script setup lang="ts">
import { computed, shallowRef, watch, toRef, ref } from 'vue'
import { useToggle } from '../composables/toggle'
import { useLang } from '../composables/lang'
import { useSourceCode } from '../composables/source-code'
import GithubIcon from './icons/github.vue'
import SourceCodeIcon from './icons/source-code.vue'
import CodepenIcon from './icons/codepen.vue'
import Example from './demo/vp-example.vue'
import SourceCode from './demo/vp-source-code.vue'
import Codepen from './demo/vp-codepen.vue'
import demoBlockLocale from '../../i18n/component/demo-block.json'
const props = defineProps({
// source is encoded via encodeURIComponent
source: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
css: {
type: String,
required: true,
},
cssPreProcessor: {
type: String,
required: true,
},
js: {
type: String,
required: true,
},
jsPreProcessor: {
type: String,
required: true,
},
html: {
type: String,
required: true,
},
demos: {
type: Object,
required: true,
},
})
const formatPathDemos = computed(() => {
const demos = {}
Object.keys(props.demos).forEach((key) => {
demos[key.replace('../../examples/', '').replace('.vue', '')] =
props.demos[key].default
})
return demos
})
const loaded = shallowRef(false)
const hasError = shallowRef(false)
const dataOpt = shallowRef('')
const setupScript = shallowRef('')
const template = shallowRef('')
const [sourceVisible, setSourceVisible] = useToggle()
const hasSetup = computed(() => loaded.value && setupScript.value !== '')
const lang = useLang()
const locale = computed(() => demoBlockLocale[lang.value])
const onDemoLoaded = (content) => {
loaded.value = true
}
const demoSourceUrl = useSourceCode(toRef(props, 'path'))
const codepenRef = ref()
const onCodepenClicked = () => {
codepenRef.value.submit?.()
}
</script>
<template>
<ClientOnly>
<div class="example">
<Codepen
ref="codepenRef"
:css="props.css"
:css-pre-processor="props.cssPreProcessor"
:html="props.html"
:js="props.js"
:js-pre-processor="props.jsPreProcessor"
/>
<div class="op-btns">
<ElTooltip :content="locale['edit-in-codepen']" :visible-arrow="false">
<ElIcon :size="20" class="op-btn">
<CodepenIcon @click="onCodepenClicked" />
</ElIcon>
</ElTooltip>
<ElTooltip :content="locale['edit-on-github']" :visible-arrow="false">
<ElIcon :size="20" class="op-btn github">
<a :href="demoSourceUrl" rel="noreferrer noopener" target="_blank">
<GithubIcon />
</a>
</ElIcon>
</ElTooltip>
<ElTooltip :content="locale['view-source']" :visible-arrow="false">
<ElIcon :size="20" class="op-btn" @click="setSourceVisible">
<SourceCodeIcon />
</ElIcon>
</ElTooltip>
</div>
<ElDivider />
<Example :file="path" :demo="formatPathDemos[path]" />
<ElDivider v-if="sourceVisible" />
<el-collapse-transition>
<SourceCode v-show="sourceVisible" :source="source" />
</el-collapse-transition>
</div>
</ClientOnly>
</template>
<style scoped lang="scss">
.example {
border: 1px solid var(--border-color-light);
border-radius: var(--el-border-radius-base);
.op-btns {
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: flex-end;
height: 3rem;
line-height: 3rem;
.op-btn {
margin: 0 0.5rem;
cursor: pointer;
color: var(--text-color);
&.github a {
color: var(--text-color);
}
}
}
.el-divider {
margin: 0;
}
}
</style>

View File

@ -0,0 +1,19 @@
<script lang="ts" setup>
import { useData } from 'vitepress'
import VPPageFooter from './doc-content/vp-page-footer.vue'
import VPPageNav from './doc-content/vp-page-nav.vue'
import VPTableOfContent from './doc-content/vp-table-of-content.vue'
const { page } = useData()
</script>
<template>
<div class="doc-content-wrapper">
<div class="doc-content-container">
<Content class="doc-content" />
<VPPageFooter />
<VPPageNav />
</div>
<VPTableOfContent v-if="page.headers" />
</div>
</template>

View File

@ -0,0 +1,73 @@
<template>
<header class="nav-bar">
<div class="nav-wrapper">
<div class="header-container">
<a :href="currentRoot" class="icon-link">
<slot name="header-logo"></slot>
<!-- <ElIcon class="logo">
<ElementPlusTextLogo class="nav-logo" />
</ElIcon> -->
</a>
<div class="nav-action-items">
<AlgoliaSearch :options="theme.agolia" />
<nav class="nav-menu">
<slot name="header-nav"></slot>
<!-- <nav-link
v-for="(navItem, key) in nav"
:key="key"
:item="navItem"
class="nav-item"
/> -->
</nav>
<div :class="{ 'theme-switcher': true, 'is-dark': isDark }">
<ElIcon class="header-icon" @click="$emit('toggle-dark')">
<Dark class="dark-icon" />
<Light class="light-icon" />
</ElIcon>
</div>
<div class="action-icons">
<div class="action-group translations">
<ElIcon class="header-icon translation-icon">
<TranslationIcon />
<div class="dropdown-content">
<ul class="languages-list">
<li
v-for="l in langs"
:key="l"
@click="switchLang(l)"
:class="{ language: true, selected: l === lang }"
>
{{ languageMap[l] }}
</li>
</ul>
</div>
</ElIcon>
</div>
<div class="header-socials" v-if></div>
<ElIcon class="header-icon social-icon">
<a
href="https://github.com/element-plus/element-plus"
target="_blank"
rel="noopener noreferrer"
>
<GithubIcon />
</a>
</ElIcon>
</div>
</div>
</div>
</div>
</header>
<div class="sub-nav">
<ToggleSidebarBtn v-if="hasSidebar" @toggle="$emit('toggle-sidebar')" />
<Transition name="shifting">
<ElButton
type="text"
:class="{ 'go-back-top': true, show: shouldShow }"
@click.prevent.stop="scrollToTop"
>{{ 'Back to top' }}</ElButton
>
</Transition>
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div class="hero-content">
<Content />
</div>
</template>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { ref } from 'vue'
import VPFullScreenMenu from './full-screen/vp-menu.vue'
import VPFullScreenTranslation from './full-screen/vp-translation.vue'
import VPFullScreenThemeToggler from './full-screen/vp-theme-toggler.vue'
import { useLockScreen } from '../composables/lock-screen'
import { useFeatureFlag } from '../composables/feature-flag'
defineProps<{
fullScreen: boolean
}>()
const { lock, cleanup } = useLockScreen()
const fullscreen = ref()
const themeEnabled = useFeatureFlag('theme')
</script>
<template>
<Transition name="el-fade-in" @enter="lock" @after-leave="cleanup">
<div v-if="fullScreen" ref="fullscreen">
<div class="full-screen-container">
<VPFullScreenMenu @close="$emit('close')" />
<VPFullScreenTranslation />
<VPFullScreenThemeToggler v-if="themeEnabled" />
</div>
</div>
</Transition>
</template>
<style lang="scss" scoped>
.full-screen {
position: fixed;
top: var(--nav-height);
right: 0;
bottom: 0;
left: 0;
padding: 0 32px;
width: 100%;
background-color: var(--bg-color);
transition: background-color 0.5s;
overflow-y: auto;
&.el-fade-in-enter-active,
&.el-fade-in-leave-active {
.full-screen-container {
transition: transform var(--el-transition-duration)
var(--el-transition-function-ease-in-out-bezier);
}
}
&.el-fade-in-enter-from,
&.el-fade-in-leave-to {
.full-screen-container {
transform: translateY(-8px);
}
}
.full-screen-container {
margin: 0 auto;
padding: 24px 0 96px;
max-width: 18rem;
}
}
</style>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useSidebar } from '../composables/sidebar'
import { useFullScreen } from '../composables/fullscreen'
import { useToggleWidgets } from '../composables/toggle-widgets'
import { breakpoints } from '../constant'
import VpNavbar from './vp-navbar.vue'
import VpNavFull from './vp-nav-full.vue'
const { hasSidebar } = useSidebar()
const { toggleFullScreen, isFullScreen } = useFullScreen()
const close = () => toggleFullScreen(false)
useToggleWidgets(isFullScreen, () => {
if (window.outerWidth >= breakpoints.md) {
close()
}
})
</script>
<template>
<header :class="{ navbar: true, 'has-sidebar': hasSidebar }">
<VpNavbar :full-screen="isFullScreen" @toggle="toggleFullScreen" />
<VpNavFull :full-screen="isFullScreen" @close="close" class="full-screen" />
</header>
</template>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import VPNavbarSearch from './navbar/vp-search.vue'
import VPNavbarMenu from './navbar/vp-menu.vue'
import VPNavbarThemeToggler from './navbar/vp-theme-toggler.vue'
import VPNavbarTranslation from './navbar/vp-translation.vue'
import VPNavbarSocialLinks from './navbar/vp-social-links.vue'
import VPNavbarHamburger from './navbar/vp-hamburger.vue'
import { useFeatureFlag } from '../composables/feature-flag'
defineProps<{
fullScreen: boolean
}>()
defineEmits(['toggle'])
const themeEnabled = useFeatureFlag('theme')
const { theme } = useData()
</script>
<template>
<div class="navbar-wrapper">
<div class="container">
<div class="logo-container">
<a href="/">
<img
class="logo"
src="/images/element-plus-logo.svg"
alt="Elemenet Plus Logo"
/>
</a>
</div>
<div class="content">
<VPNavbarSearch class="search" :options="theme.agolia" />
<VPNavbarMenu class="menu" />
<VPNavbarThemeToggler v-if="themeEnabled" class="theme-toggler" />
<VPNavbarTranslation class="translation" />
<VPNavbarSocialLinks class="social-links" />
<VPNavbarHamburger
:active="fullScreen"
class="hamburger"
@click="$emit('toggle')"
/>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.logo-container {
display: flex;
align-items: center;
height: var(--header-height);
> a {
height: 28px;
width: 128px;
}
.logo {
position: relative;
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useLang } from '../composables/lang'
import localeData from '../../i18n/pages/not-found.json'
const lang = useLang()
const locale = computed(() => localeData[lang.value])
const goHome = () => {
window.location.href = `/${lang.value}/`
}
</script>
<template>
<el-result icon="error" :title="locale.title" :sub-title="locale.desc">
<template #extra>
<el-button @click="goHome">{{ locale['button-title'] }}</el-button>
</template>
</el-result>
</template>
<style scoped>
.el-result {
height: 100vh;
width: 100vw;
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
defineProps<{
show: boolean
}>()
</script>
<template>
<Transition name="el-fade-in">
<div v-if="show" />
</Transition>
</template>

View File

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { computed, ref, markRaw, watch } from 'vue'
import { useRoute, withBase, useData } from 'vitepress'
import VPSidebarLink from './sidebar/vp-sidebar-link.vue'
import { useSidebar } from '../composables/sidebar'
import { useLang } from '../composables/lang'
import sponsorsData from '../../i18n/component/sponsor.json'
type SideNavItem = {
beta: boolean
text: string
link: string
activeMatch: string
children: Array<SideNavItem>
}
defineProps<{ open: boolean }>()
defineEmits(['close'])
// const isHome = useIsHome()
const { theme } = useData()
const route = useRoute()
const lang = useLang()
const { sidebars, hasSidebar } = useSidebar()
</script>
<template>
<aside v-if="hasSidebar" :class="{ sidebar: true, open }">
<slot name="top" />
<div class="sidebar-groups">
<section v-for="(item, key) of sidebars" class="sidebar-group">
<p class="sidebar-group__title">
{{ item.text }}
</p>
<VPSidebarLink
v-for="item in item.children"
:item="item"
@close="$emit('close')"
/>
</section>
</div>
<slot name="bottom" />
</aside>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
defineProps<{
item: {
name: string
img: string
url: string
}
}>()
</script>
<template>
<div class="sponsor-item">
<a :href="item.url" :title="item.name">
<img :src="item.img" :alt="item.name" />
</a>
</div>
</template>
<style scoped lang="scss">
.sponsor-item {
margin-right: 4px;
a {
display: inline-flex;
align-items: center;
img {
width: 36px;
height: 36px;
}
}
}
</style>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { computed } from 'vue'
import VPSponsor from './vp-sponsor.vue'
import sponsorsLocale from '../../i18n/component/sponsors.json'
import sponsorLocale from '../../i18n/component/sponsor.json'
import { useLang } from '../composables/lang'
import { defaultLang } from '../constant'
const lang = useLang()
const sponsors = computed(() => sponsorsLocale[lang.value])
const sponsor = computed(() => sponsorLocale[lang.value])
</script>
<template>
<div class="sponsors">
<p class="sponsors-title">{{ sponsor.sponsoredBy }}</p>
<div class="container">
<VPSponsor v-for="sponsor in sponsors" :item="sponsor" />
</div>
</div>
</template>
<style lang="scss" scoped>
.sponsors {
padding: 0.35rem 1.5rem 0.35rem 1.25rem;
.sponsors-title {
color: var(--text-color-lighter);
font-weight: 300;
font-size: 14px;
}
.container {
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import ToggleSidebarBtn from './subnav/toggle-sidebar-btn.vue'
import { useSidebar } from '../composables/sidebar'
import { useBackTop } from '../composables/back-top'
defineEmits(['open-menu'])
const { hasSidebar } = useSidebar()
const { shouldShow, scrollToTop } = useBackTop()
</script>
<template>
<div class="sub-nav">
<ToggleSidebarBtn v-if="hasSidebar" @click="$emit('open-menu')" />
<Transition name="shifting">
<ElButton
type="text"
:class="{ 'go-back-top': true, show: shouldShow }"
@click.prevent.stop="scrollToTop"
>{{ 'Back to top' }}</ElButton
>
</Transition>
</div>
</template>

View File

@ -0,0 +1,119 @@
import { computed, shallowRef, onMounted, onUnmounted, onUpdated } from 'vue'
import { throttleAndDebounce } from '../utils'
import type { Ref } from 'vue'
const BOUNDING_OFFSET = 100 // 56 the header height + margin-top 32
export function useActiveSidebarLinks(
container: Ref<HTMLElement>,
marker: Ref<HTMLElement>
) {
const onScroll = throttleAndDebounce(setActiveLink, 150)
function setActiveLink() {
const sidebarLinks = getSidebarLinks()
const anchors = getAnchors(sidebarLinks)
if (
anchors.length &&
window.scrollY + window.innerHeight === document.body.offsetHeight
) {
activateLink(anchors[anchors.length - 1].hash)
return
}
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i]
const nextAnchor = anchors[i + 1]
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
if (isActive) {
history.replaceState(
null,
document.title,
hash ? (hash as string) : ' '
)
activateLink(hash as string)
return
}
}
}
let prevActiveLink: HTMLAnchorElement | null = null
function activateLink(hash: string) {
deactiveLink(prevActiveLink)
const activeLink = (prevActiveLink =
hash == null
? null
: (container.value.querySelector(
`.toc-item a[href="${decodeURIComponent(hash)}"]`
) as HTMLAnchorElement))
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.opacity = '1'
marker.value.style.top = activeLink.offsetTop + 'px'
} else {
marker.value.style.opacity = '0'
marker.value.style.top = '33px'
}
}
function deactiveLink(link: HTMLElement) {
link && link.classList.remove('active')
}
onMounted(() => {
window.requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
activateLink(location.hash)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
}
function getSidebarLinks() {
return Array.from(
document.querySelectorAll('.toc-content .toc-link')
) as HTMLAnchorElement[]
}
function getAnchors(sidebarLinks: HTMLAnchorElement[]) {
return (
Array.from(
document.querySelectorAll('.doc-content .header-anchor')
) as HTMLAnchorElement[]
).filter((anchor) =>
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
)
}
function getPageOffset() {
return (document.querySelector('.navbar') as HTMLElement).offsetHeight
}
function getAnchorTop(anchor: HTMLAnchorElement) {
const pageOffset = getPageOffset()
try {
return anchor.parentElement.offsetTop - pageOffset - 15
} catch (e) {
return 0
}
}
function isAnchorActive(
index: number,
anchor: HTMLAnchorElement,
nextAnchor: HTMLAnchorElement
) {
const scrollTop = window.scrollY
if (index === 0 && scrollTop === 0) {
return [true, null]
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null]
}
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
return [true, decodeURIComponent(anchor.hash)]
}
return [false, null]
}

View File

@ -0,0 +1,61 @@
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { throttleAndDebounce } from '../utils'
const threshold = 960
const cubic = (value: number): number => Math.pow(value, 3)
const easeInOutCubic = (value: number): number =>
value < 0.5 ? cubic(value * 2) / 2 : 1 - cubic((1 - value) * 2) / 2
export const useBackTop = (offset = 200) => {
const shouldShow = ref(false)
const throttleResize = throttleAndDebounce(onResize, 300)
const throttleScroll = throttleAndDebounce(onScroll, 160)
onMounted(() => {
onResize()
onScroll()
window.addEventListener('resize', throttleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', throttleResize)
window.removeEventListener('scroll', throttleScroll)
})
const scrollToTop = () => {
const beginTime = Date.now()
const beginValue = document.documentElement.scrollTop
const rAF = window.requestAnimationFrame
const frameFunc = () => {
const progress = (Date.now() - beginTime) / 500
if (progress < 1) {
document.documentElement.scrollTop =
beginValue * (1 - easeInOutCubic(progress))
rAF(frameFunc)
} else {
document.documentElement.scrollTop = 0
}
}
rAF(frameFunc)
}
function onResize() {
const { clientWidth } = document.body
if (clientWidth < threshold) {
window.addEventListener('scroll', throttleScroll)
} else {
window.removeEventListener('scroll', throttleScroll)
}
}
function onScroll() {
shouldShow.value = document.documentElement.scrollTop > offset
}
return {
shouldShow,
scrollToTop,
}
}

View File

@ -0,0 +1,32 @@
import { computed } from 'vue'
import { useData } from 'vitepress'
import { createGitHubUrl } from '../utils'
export function useEditLink() {
const { page, theme, frontmatter } = useData()
const url = computed(() => {
const {
repo,
docsDir = '',
docsBranch = 'dev',
docsRepo = repo,
editLinks,
} = theme.value
const showEditLink =
frontmatter.value.editLink != null
? frontmatter.value.editLink
: editLinks
const { relativePath } = page.value
if (!showEditLink || !relativePath || !repo) {
return null
}
return createGitHubUrl(docsRepo, docsDir, docsBranch, relativePath)
})
const text = computed(() => {
return theme.value.editLinkText || 'Edit this page'
})
return {
url,
text,
}
}

View File

@ -0,0 +1,10 @@
import { computed } from 'vue'
import { useData } from 'vitepress'
export const useFeatureFlag = (flag: string) => {
const { theme } = useData()
return computed(() => {
return (theme.value.features || {})[flag]
})
}

View File

@ -0,0 +1,8 @@
import { useToggle } from './toggle'
export const useFullScreen = () => {
const [isFullScreen, toggleFullScreen] = useToggle()
return {
isFullScreen,
toggleFullScreen,
}
}

View File

@ -0,0 +1,19 @@
import { computed } from 'vue'
import { useRoute } from 'vitepress'
import { defaultLang } from '../constant'
export const useLang = () => {
const route = useRoute()
return computed(() => {
// the first part of the first slash
const path = route.data?.relativePath
let lang: string
if (path?.includes('/')) {
lang = path.split('/').shift()
} else {
lang = defaultLang
}
return lang
})
}

View File

@ -0,0 +1,58 @@
import { watch, onUnmounted } from 'vue'
import {
addClass,
hasClass,
getStyle,
removeClass,
} from 'element-plus/lib/utils/dom'
import getScrollBarWidth from 'element-plus/lib/utils/scrollbar-width'
import { isServer } from '../utils'
export const useLockScreen = () => {
let scrollBarWidth = 0
let withoutHiddenClass = false
let bodyPaddingRight = '0'
let computedBodyPaddingRight = 0
onUnmounted(() => {
cleanup()
})
const cleanup = () => {
if (isServer) return
removeClass(document.body, 'el-popup-parent--hidden')
if (withoutHiddenClass) {
document.body.style.paddingRight = bodyPaddingRight
}
}
const lock = () => {
if (isServer) return
withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden')
if (withoutHiddenClass) {
bodyPaddingRight = document.body.style.paddingRight
computedBodyPaddingRight = parseInt(
getStyle(document.body, 'paddingRight'),
10
)
}
scrollBarWidth = getScrollBarWidth()
const bodyHasOverflow =
document.documentElement.clientHeight < document.body.scrollHeight
const bodyOverflowY = getStyle(document.body, 'overflowY')
if (
scrollBarWidth > 0 &&
(bodyHasOverflow || bodyOverflowY === 'scroll') &&
withoutHiddenClass
) {
document.body.style.paddingRight =
computedBodyPaddingRight + scrollBarWidth + 'px'
}
addClass(document.body, 'el-popup-parent--hidden')
}
return {
lock,
cleanup,
}
}

View File

@ -0,0 +1,13 @@
import { computed } from 'vue'
import { useData } from 'vitepress'
import { useLang } from './lang'
export const useNav = () => {
const { theme } = useData()
const lang = useLang()
return computed(() => {
return theme.value.nav[lang.value]
})
}

View File

@ -0,0 +1,53 @@
import { computed } from 'vue'
import { useData } from 'vitepress'
import {
isArray,
ensureStartingSlash,
removeExtention as removeExtension,
} from '../utils'
import { useLang } from './lang'
import { getSidebarConfig, getFlatSideBarLinks } from './sidebar'
export function usePageNav() {
const { page, theme } = useData()
const lang = useLang()
const path = computed(() => {
return removeExtension(ensureStartingSlash(page.value.relativePath))
})
const candidates = computed(() => {
const config = getSidebarConfig(
theme.value.sidebars,
path.value,
lang.value
)
return isArray(config) ? getFlatSideBarLinks(config) : []
})
const index = computed(() => {
return candidates.value.findIndex((item) => {
return item.link === path.value
})
})
const next = computed(() => {
if (
theme.value.nextLinks !== false &&
index.value > -1 &&
index.value < candidates.value.length - 1
) {
return candidates.value[index.value + 1]
}
})
const prev = computed(() => {
if (theme.value.prevLinks !== false && index.value > 0) {
return candidates.value[index.value - 1]
}
})
const hasLinks = computed(() => !!next.value || !!prev.value)
return {
next,
prev,
hasLinks,
}
}

View File

@ -0,0 +1,86 @@
import { computed } from 'vue'
import { useRoute, useData } from 'vitepress'
import { isArray, ensureStartingSlash, removeExtention } from '../utils'
import { useLang } from './lang'
export const useSidebar = () => {
const route = useRoute()
const { site, page } = useData()
const lang = useLang()
if (!page.value) {
return {
sidebars: computed(() => []),
hasSidebar: computed(() => false),
}
}
const sidebars = computed(() => {
if (page.value.frontmatter.sidebar === false) return []
const sidebars = getSidebarConfig(
site.value.themeConfig.sidebars,
route.data.relativePath,
lang.value
)
return sidebars
})
return {
sidebars,
hasSidebar: computed(() => sidebars.value.length > 0),
}
}
export function isSideBarConfig(sidebar) {
return sidebar === false || sidebar === 'auto' || isArray(sidebar)
}
export function isSideBarGroup(item) {
return item.children !== undefined
}
export function isSideBarEmpty(sidebar) {
return isArray(sidebar) ? sidebar.length === 0 : !sidebar
}
type SidebarItem = {
text: string
link: string
}
type SidebarConfig = SidebarItem[]
type Sidebar =
| {
[key: string]: SidebarConfig
}
| false
| 'auto'
export function getSidebarConfig(sidebar: Sidebar, path: string, lang: string) {
if (sidebar === false || Array.isArray(sidebar) || sidebar === 'auto') {
return []
}
path = ensureStartingSlash(path)
for (const dir in sidebar) {
// make sure the multi sidebar key starts with slash too
if (path.startsWith(ensureStartingSlash(`${lang}${dir}`))) {
return sidebar[dir][lang]
}
}
return []
}
/**
* Get flat sidebar links from the sidebar items. This method is useful for
* creating the "next and prev link" feature. It will ignore any items that
* don't have `link` property and removes `.md` or `.html` extension if a
* link contains it.
*/
export function getFlatSideBarLinks(sidebar) {
return sidebar.reduce((links, item) => {
if (item.link) {
links.push({ text: item.text, link: removeExtention(item.link) })
}
if (isSideBarGroup(item)) {
links = [...links, ...getFlatSideBarLinks(item.children)]
}
return links
}, [])
}

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