diff --git a/README.md b/README.md
index 4ccc0e5b5..52c549d84 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,23 @@
# **Vant React**
-[](https://badge.fury.io/js/vant-react)
-[](LICENSE)
-
-[](https://vant.bctc.io)
-[](https://vant.bctc.io)
+
+
+
+
+
Lightweight Mobile UI Components built on Typescript and React in under 2kb!
-## **Features**
+### **Features**
- Support Typescript
- 60+ Reusable components
- 100% Storybook coverage: [https://vant.bctc.io](https://vant.bctc.io)
- Extensive documentation and demos
-## Install
+### Install
-```text
+``` bash
# Using npm
npm i vant-react -S
@@ -25,9 +25,9 @@ npm i vant-react -S
yarn add vant-react
```
-## Quickstart
+### Quickstart
-```text
+``` js
import React from 'react';
import { Button } from 'vant-react';
import 'vant-react/dist/index.css';
diff --git a/docs/changelog.zh-CN.md b/docs/changelog.zh-CN.md
new file mode 100644
index 000000000..2d291ae49
--- /dev/null
+++ b/docs/changelog.zh-CN.md
@@ -0,0 +1 @@
+## 更新日志
\ No newline at end of file
diff --git a/docs/contribute.zh-CN.md b/docs/contribute.zh-CN.md
new file mode 100644
index 000000000..cfbca5ac4
--- /dev/null
+++ b/docs/contribute.zh-CN.md
@@ -0,0 +1 @@
+## 如何参与
\ No newline at end of file
diff --git a/docs/locale.zh-CN.md b/docs/locale.zh-CN.md
new file mode 100644
index 000000000..fe57d0e98
--- /dev/null
+++ b/docs/locale.zh-CN.md
@@ -0,0 +1 @@
+## 国际化
diff --git a/docs/markdown.zh-CN.md b/docs/markdown.zh-CN.md
new file mode 100644
index 000000000..bf130bfc7
--- /dev/null
+++ b/docs/markdown.zh-CN.md
@@ -0,0 +1,23 @@
+## 组件文档如何编写
+
+### 介绍
+
+文档网站分为文档展示和demo展示,文档通过收集组件目录下的 `README.[locale].md`, ` locale ` 的值是国际化的标识,和 ` ./site/configs/nav.config.js ` 中的 ` key `保持一致。
+
+### 使用
+
+文档网站替换了storybook 自带的文档生成,在运行 ` yarn storybook ` 自动收集文档和demo的信息,生成对应的页面,并且自动在浏览器中打开页面,对于开发过程中的使用基本上没有太大的影响
+
+### 添加文档配置
+
+在` ./site/configs/nav.config.js ` 添加新的文档页面配置,包括标题和` path `,标题会在左侧导航展示,` path`用来生成页面路由.
+
+### 添加文档文件
+
+在组件目录中新增 `README.zh-CN.md`,用来展示组件文档,使用 ` markdown ` 文件语法。
+
+### 添加文档 demo 文件
+
+在组件目录中新增 ` demo/index.tsx ` 作为文档的页面,然后就可以愉快的编写 demo 了。
+由于每次在运行` yarn storybook `时才会收集所有文档信息生成文档配置,所以每次新增的文档文件需要重新运行` yarn storybook `。
+
diff --git a/docs/quickstart.zh-CN.md b/docs/quickstart.zh-CN.md
new file mode 100644
index 000000000..1e8aa0de9
--- /dev/null
+++ b/docs/quickstart.zh-CN.md
@@ -0,0 +1,24 @@
+## 快速上手
+
+### Install
+
+``` bash
+# Using npm
+npm i vant-react -S
+
+# Using yarn
+yarn add vant-react
+```
+
+### Quickstart
+
+``` js
+import React from 'react';
+import { Button } from 'vant-react';
+import 'vant-react/dist/index.css';
+
+const App = () => {
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/package.json b/package.json
index e7822944e..2b75b8338 100644
--- a/package.json
+++ b/package.json
@@ -20,11 +20,10 @@
"test:lint": "run-s lint",
"test:unit": "cross-env CI=1 react-scripts test --env=jsdom --passWithNoTests",
"test:watch": "react-scripts test --env=jsdom",
- "predeploy": "cd example && yarn install && yarn run build",
- "deploy": "gh-pages -d example/build",
+ "deploy": "cd site && yarn deploy",
"lint": "eslint --ext .tsx ./src",
"lint:watch": "esw --watch --fix --ext .tsx ./src",
- "storybook": "start-storybook -p 9009",
+ "storybook": "cd site && yarn dev && start-storybook",
"build-storybook": "build-storybook"
},
"peerDependencies": {
diff --git a/site/.editorconfig b/site/.editorconfig
new file mode 100644
index 000000000..39cb692c8
--- /dev/null
+++ b/site/.editorconfig
@@ -0,0 +1,23 @@
+root = true
+
+[*]
+indent_style = space
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.js]
+indent_size = 2
+
+[*.vue]
+indent_size = 2
+
+[*.css]
+indent_size = 2
+
+[Makefile]
+indent_style = tab
diff --git a/site/.eslintrc.js b/site/.eslintrc.js
new file mode 100644
index 000000000..fef4a2e8b
--- /dev/null
+++ b/site/.eslintrc.js
@@ -0,0 +1,8 @@
+module.exports = {
+ root: true,
+ env: {
+ browser: true,
+ },
+ extends: ['../eslintrc.react.js.base.js'],
+ ignorePatterns: ['node_modules/', 'dist/', 'src/nav.js'],
+};
diff --git a/site/.gitignore b/site/.gitignore
new file mode 100644
index 000000000..84582ce4f
--- /dev/null
+++ b/site/.gitignore
@@ -0,0 +1,5 @@
+dist
+node_modules
+configs/site-desktop-shared.js
+configs/site-mobile-shared.js
+.awcache
diff --git a/site/README.md b/site/README.md
new file mode 100644
index 000000000..f3fde3329
--- /dev/null
+++ b/site/README.md
@@ -0,0 +1,19 @@
+## 开发
+
+```bash
+yarn
+
+npm run dev
+```
+
+浏览器打开[http://127.0.0.1:4396](http://127.0.0.1:4396)
+
+## 部署
+
+`yarn run deploy`
+
+默认情况下生成的静态文件会部署到当前仓库的 `gh-pages` 分支上,环境变量来修改仓库。
+
+## Notes
+
+Github doesn't support SPAs, the `404.html` is a workaround. Do NOT remove it.
diff --git a/site/assets/zanui-logo.png b/site/assets/zanui-logo.png
new file mode 100644
index 000000000..ffbbde2b5
Binary files /dev/null and b/site/assets/zanui-logo.png differ
diff --git a/site/babel.config.js b/site/babel.config.js
new file mode 100644
index 000000000..a22e85ee5
--- /dev/null
+++ b/site/babel.config.js
@@ -0,0 +1,40 @@
+module.exports = function(api) {
+ api.cache(true);
+
+ const envSpecificPlugins =
+ process.env.NODE_ENV === 'development'
+ ? ['react-loadable/babel', 'react-hot-loader/babel']
+ : [];
+
+ return {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ useBuiltIns: 'entry',
+ corejs: 3,
+ },
+ ],
+ '@babel/preset-react',
+ ],
+
+ plugins: [
+ 'transform-property-literals',
+ 'transform-member-expression-literals',
+ '@babel/plugin-proposal-export-namespace-from',
+ '@babel/plugin-proposal-export-default-from',
+ '@babel/plugin-proposal-class-properties',
+ '@babel/plugin-syntax-dynamic-import',
+ [
+ '@babel/plugin-transform-runtime',
+ {
+ corejs: false,
+ helpers: true,
+ regenerator: true,
+ useESModules: false,
+ },
+ ],
+ ...envSpecificPlugins,
+ ],
+ };
+};
diff --git a/site/configs/doc.config.js b/site/configs/doc.config.js
new file mode 100644
index 000000000..6b8f567de
--- /dev/null
+++ b/site/configs/doc.config.js
@@ -0,0 +1,6 @@
+import pkgJson from '../../package.json';
+
+export const { version } = pkgJson;
+
+export const versions = [version];
+
diff --git a/site/configs/nav.config.js b/site/configs/nav.config.js
new file mode 100644
index 000000000..8c0c8da96
--- /dev/null
+++ b/site/configs/nav.config.js
@@ -0,0 +1,119 @@
+
+const github = 'https://github.com/mxdi9i7/vant-react';
+
+module.exports = {
+ 'zh-CN': {
+ header: {
+ logo: {
+ image: 'https://img.yzcdn.cn/vant/logo.png',
+ title: 'Vant React',
+ href: '#/'
+ },
+ nav: {
+ lang: {
+ text: 'En',
+ from: 'zh-CN',
+ to: 'en-US'
+ },
+ logoLink: [
+ {
+ image: 'https://b.yzcdn.cn/vant/logo/github.svg',
+ url: 'github'
+ }
+ ]
+ }
+ },
+
+ nav: [
+ {
+ name: '开发指南',
+ list: [
+ {
+ title: '快速上手',
+ path: 'quickstart',
+ },
+ {
+ title: '更新日志',
+ path: 'changelog',
+ },
+ {
+ title: '如何参与',
+ path: 'contribute',
+ },
+ {
+ title: '文档规范',
+ path: 'markdown',
+ },
+ {
+ title: '国际化',
+ path: 'locale',
+ }
+ ],
+ },
+ {
+ name: '基础组件',
+ list: [
+ {
+ title: 'Button 按钮',
+ path: 'button',
+ },
+ {
+ title: 'Cell 单元格',
+ path: 'cell',
+ },
+ {
+ title: 'Field 输入框',
+ path: 'field',
+ },
+ {
+ title: 'Icon 图标',
+ path: 'icons',
+ },
+ {
+ title: 'Navbar 导航栏',
+ path: 'navbar',
+ },
+ {
+ title: 'Popup 弹出层',
+ path: 'popup',
+ },
+ {
+ title: 'Rate 评分',
+ path: 'rate',
+ },
+ {
+ title: 'Search 搜索',
+ path: 'search',
+ },
+ {
+ title: 'Tag 标记',
+ path: 'tag',
+ }
+ ],
+ },
+ ]
+ },
+ 'en-US': {
+ header: {
+ logo: {
+ image:
+ 'https://img.yzcdn.cn/vant/logo.png',
+ title: 'Vant React',
+ href: '#/'
+ },
+ nav: {
+ lang: {
+ text: '中文',
+ from: 'en-US',
+ to: 'zh-CN'
+ },
+ logoLink: [
+ {
+ image: 'https://b.yzcdn.cn/vant/logo/github.svg',
+ url: 'github'
+ }
+ ]
+ }
+ },
+ }
+};
\ No newline at end of file
diff --git a/site/constants.js b/site/constants.js
new file mode 100644
index 000000000..294b170d9
--- /dev/null
+++ b/site/constants.js
@@ -0,0 +1,8 @@
+exports.prefix = getPrefix();
+
+function getPrefix() {
+ if (process.env.NODE_ENV === 'production') {
+ return '/vant-react/';
+ }
+ return '/';
+}
diff --git a/site/markdown-loader/card-wrapper.js b/site/markdown-loader/card-wrapper.js
new file mode 100644
index 000000000..6216cdef9
--- /dev/null
+++ b/site/markdown-loader/card-wrapper.js
@@ -0,0 +1,16 @@
+module.exports = function cardWrapper(html) {
+ const group = html
+ .replace(/
{
+ if (fragment.indexOf('${fragment}`;
+ }
+
+ return fragment;
+ })
+ .join('');
+};
diff --git a/site/markdown-loader/highlight.js b/site/markdown-loader/highlight.js
new file mode 100644
index 000000000..9adb9eecf
--- /dev/null
+++ b/site/markdown-loader/highlight.js
@@ -0,0 +1,9 @@
+const hljs = require('highlight.js');
+
+module.exports = function highlight(str, lang) {
+ if (lang && hljs.getLanguage(lang)) {
+ return hljs.highlight(lang, str, true).value;
+ }
+
+ return '';
+};
diff --git a/site/markdown-loader/index.js b/site/markdown-loader/index.js
new file mode 100644
index 000000000..827e395de
--- /dev/null
+++ b/site/markdown-loader/index.js
@@ -0,0 +1,67 @@
+const loaderUtils = require('loader-utils');
+const MarkdownIt = require('markdown-it');
+const markdownItAnchor = require('markdown-it-anchor');
+const markdownCheckbox = require('markdown-it-checkbox');
+const frontMatter = require('front-matter');
+const highlight = require('./highlight');
+const cardWrapper = require('./card-wrapper');
+const linkOpen = require('./link-open');
+const { slugify } = require('transliteration');
+
+
+function wrapper(content) {
+ content = cardWrapper(content);
+ content = escape(content);
+
+ return `
+ import * as React from 'react';
+
+ function RawHtmlRenderer(props) {
+ return ;
+ }
+
+ class ReactMarkdownComponent extends React.PureComponent {
+
+ render() {
+ return
+ }
+ }
+
+ export default ReactMarkdownComponent;
+`;
+}
+
+const parser = new MarkdownIt({
+ html: true,
+ highlight,
+}).use(markdownItAnchor, {
+ level: 2,
+ slugify,
+}).use(markdownCheckbox, {
+ divClass: 'van-doc-checkbox'
+});
+
+module.exports = function(source) {
+ let options = loaderUtils.getOptions(this) || {};
+ this.cacheable && this.cacheable();
+
+ options = {
+ wrapper,
+ linkOpen: true,
+ ...options,
+ };
+
+ let fm;
+
+ if (options.enableMetaData) {
+ fm = frontMatter(source);
+ source = fm.body;
+ }
+
+ if (options.linkOpen) {
+ linkOpen(parser);
+ }
+
+ return options.wrapper(parser.render(source), fm);
+};
+
diff --git a/site/markdown-loader/link-open.js b/site/markdown-loader/link-open.js
new file mode 100644
index 000000000..0e513226a
--- /dev/null
+++ b/site/markdown-loader/link-open.js
@@ -0,0 +1,18 @@
+// add target="_blank" to all links
+module.exports = function linkOpen(md) {
+ const defaultRender =
+ md.renderer.rules.link_open ||
+ function(tokens, idx, options, env, self) {
+ return self.renderToken(tokens, idx, options);
+ };
+
+ md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
+ const aIndex = tokens[idx].attrIndex('target');
+
+ if (aIndex < 0) {
+ tokens[idx].attrPush(['target', '_blank']); // add new attribute
+ }
+
+ return defaultRender(tokens, idx, options, env, self);
+ };
+};
diff --git a/site/package.json b/site/package.json
new file mode 100644
index 000000000..89f32b1cc
--- /dev/null
+++ b/site/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "vant-docs",
+ "version": "0.0.1",
+ "description": "doc site generator for vant react",
+ "private": true,
+ "keywords": [
+ "docs",
+ "react",
+ "vant"
+ ],
+ "license": "MIT",
+ "scripts": {
+ "prepare": "node ./scripts/gen-shared.js",
+ "prepare-build": "rm -rf dist && yarn prepare",
+ "dev": "yarn prepare && cross-env NODE_ENV=development node scripts/dev.js",
+ "webpack": "cross-env NODE_ENV=production webpack --progress --hide-modules --config webpack/webpack.prd.config.js",
+ "build": "yarn prepare-build && yarn webpack",
+ "deploy": "yarn install && yarn build && ./scripts/deploy.sh"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.1.5",
+ "classnames": "^2.2.5",
+ "core-js": "3",
+ "fibers": "^4.0.2",
+ "fuse.js": "^3.2.0",
+ "highlight.js": "^10.1.1",
+ "lodash": "^4.17.15",
+ "prismjs": "1.8.4",
+ "prop-types": "^15.6.0",
+ "react": "16.11.x",
+ "react-beautiful-dnd": "^10.0.4",
+ "react-dom": "16.11.x",
+ "react-loadable": "^5.3.0",
+ "react-router-dom": "^4.1.1",
+ "transliteration": "^2.1.11"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.1.6",
+ "@babel/parser": "^7.1.6",
+ "@babel/plugin-proposal-class-properties": "^7.1.0",
+ "@babel/plugin-proposal-export-default-from": "^7.0.0",
+ "@babel/plugin-proposal-export-namespace-from": "^7.0.0",
+ "@babel/plugin-syntax-dynamic-import": "^7.0.0",
+ "@babel/plugin-transform-runtime": "^7.1.0",
+ "@babel/preset-env": "^7.1.6",
+ "@babel/preset-react": "^7.0.0",
+ "@babel/types": "^7.1.6",
+ "@hot-loader/react-dom": "16.11.x",
+ "@types/react": "^16.9.41",
+ "@types/react-dom": "^16.9.8",
+ "autoprefixer": "^9.4.6",
+ "awesome-typescript-loader": "^5.2.1",
+ "babel-loader": "^8.0.5",
+ "babel-plugin-module-resolver": "^3.1.1",
+ "babel-plugin-transform-member-expression-literals": "^6.9.4",
+ "babel-plugin-transform-property-literals": "^6.9.4",
+ "babel-plugin-transform-remove-console": "^6.9.4",
+ "babel-plugin-transform-remove-debugger": "^6.9.4",
+ "cache-loader": "^4.1.0",
+ "colors": "^1.1.2",
+ "coveralls": "^3.0.3",
+ "cross-env": "^4.0.0",
+ "css-loader": "^1.0.1",
+ "favicons-webpack-plugin": "^0.0.9",
+ "front-matter": "^4.0.2",
+ "gh-pages": "1.2.0",
+ "html-loader": "^0.5.5",
+ "html-webpack-plugin": "^3.2.0",
+ "lint-staged": "^8.0.5",
+ "loader-utils": "^2.0.0",
+ "markdown-doc-loader": "2.1.3",
+ "markdown-it": "^11.0.0",
+ "markdown-it-anchor": "^5.3.0",
+ "markdown-it-checkbox": "^1.1.0",
+ "mini-css-extract-plugin": "^0.4.5",
+ "postcss-loader": "^3.0.0",
+ "progress-bar-webpack-plugin": "^1.11.0",
+ "ramda": "^0.25.0",
+ "react-hot-loader": "^4.6.3",
+ "react-markdown-doc-loader": "^3.0.0",
+ "sass": "^1.22.7",
+ "sass-lint": "^1.12.1",
+ "sass-loader": "^7.1.0",
+ "style-loader": "^0.23.1",
+ "thread-loader": "^2.1.2",
+ "typescript": "~3.7.2",
+ "url-loader": "^1.1.2",
+ "webpack": "^4.26.0",
+ "webpack-cli": "^3.1.2",
+ "webpack-dev-server": "^3.1.10",
+ "webpack-merge": "^4.1.4"
+ }
+}
diff --git a/site/postcss.config.js b/site/postcss.config.js
new file mode 100644
index 000000000..4732bdac6
--- /dev/null
+++ b/site/postcss.config.js
@@ -0,0 +1,7 @@
+/* eslint-disable global-require */
+
+module.exports = {
+ plugins: [
+ require('autoprefixer')(),
+ ],
+};
diff --git a/site/scripts/constants.js b/site/scripts/constants.js
new file mode 100644
index 000000000..088afdb26
--- /dev/null
+++ b/site/scripts/constants.js
@@ -0,0 +1,15 @@
+const path = require('path');
+
+const SRC_DIR = path.join(__dirname, '../../src/components');
+const SITE_MOBILE_SHARED_FILE = path.join(__dirname, '../configs/site-mobile-shared.js')
+const DOCS_DIR = path.join(__dirname, '../../docs')
+
+
+const SITE_DESKTOP_SHARED_FILE = path.join(__dirname, '../configs/site-desktop-shared.js')
+
+module.exports = {
+ SRC_DIR,
+ DOCS_DIR,
+ SITE_MOBILE_SHARED_FILE,
+ SITE_DESKTOP_SHARED_FILE
+}
diff --git a/site/scripts/deploy.sh b/site/scripts/deploy.sh
new file mode 100755
index 000000000..10628851b
--- /dev/null
+++ b/site/scripts/deploy.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+gh-pages -d dist
diff --git a/site/scripts/dev.js b/site/scripts/dev.js
new file mode 100644
index 000000000..9c5951164
--- /dev/null
+++ b/site/scripts/dev.js
@@ -0,0 +1,40 @@
+const path = require('path');
+const webpack = require('webpack');
+const WebpackDevServer = require('webpack-dev-server');
+const cp = require('child_process');
+
+const webpackConfig = require('../webpack/webpack.dev.config.js');
+
+const cmds = {
+ wind32: 'start',
+ linux: 'xdg-open',
+ darwin: 'open',
+};
+
+let onceMark = true;
+
+const compiler = webpack(webpackConfig);
+
+compiler.plugin('done', () => {
+ if (onceMark) {
+ cp.exec(`${cmds[process.platform]} http://127.0.0.1:4396`);
+ }
+ onceMark = false;
+});
+
+const server = new WebpackDevServer(compiler, {
+ stats: {
+ colors: true,
+ },
+ hot: true,
+ contentBase: path.resolve(__dirname, '../dist'),
+ publicPath: '/',
+ disableHostCheck: true,
+ inline: true,
+ historyApiFallback: true,
+});
+
+server.listen(4396, '0.0.0.0', () => {
+ // eslint-disable-next-line
+ console.log('\n Starting server on http://localhost:4396 \n');
+});
diff --git a/site/scripts/gen-desktop-shared.js b/site/scripts/gen-desktop-shared.js
new file mode 100644
index 000000000..3a1b8a685
--- /dev/null
+++ b/site/scripts/gen-desktop-shared.js
@@ -0,0 +1,68 @@
+const glob = require('fast-glob');
+const path = require('path');
+const { existsSync, readdirSync } = require('fs-extra');
+const { SRC_DIR, DOCS_DIR, SITE_DESKTOP_SHARED_FILE } = require('./constants');
+const { join, normalize } = require('path');
+const configs = require('../configs/nav.config');
+const { pascalize, normalizePath, smartOutputFile } = require('./utils');
+
+function formatName(component, lang) {
+ component = pascalize(component);
+
+ if (lang) {
+ return `${component}_${lang.replace('-', '_')}`;
+ }
+
+ return component;
+}
+
+function resolveDocuments(components) {
+ const docs = [];
+
+ const langs = Object.keys(configs);
+
+ langs.forEach(lang => {
+ const fileName = `README.${lang}.md`;
+ components.forEach(component => {
+ docs.push({
+ name: formatName(component, lang),
+ path: join(SRC_DIR, component, fileName),
+ })
+ })
+
+ })
+
+ const staticDocs = glob.sync(normalizePath(path.join(DOCS_DIR, '**/*.md'))).map(p => {
+ const pairs = path.parse(p).name.split('.');
+ return {
+ name: formatName(pairs[0], pairs[1] || 'zh-CN'),
+ path: p,
+ }
+ });
+
+ return [
+ ...staticDocs,
+ ...docs.filter(item => existsSync(item.path))
+ ]
+}
+
+function genImports(documents) {
+ return documents.map(item => `import ${item.name} from '${normalizePath(item.path)}';`).join('\n');
+}
+
+function genExport(documents) {
+ return `export const documents = {
+ ${documents.map(item => item.name).join(',\n ')}
+};`
+}
+
+function genSiteDesktopShared() {
+ const dirs = readdirSync(SRC_DIR);
+ const documents = resolveDocuments(dirs);
+ const code = `${genImports(documents)}
+${genExport(documents)}
+`;
+ smartOutputFile(SITE_DESKTOP_SHARED_FILE, code);
+}
+
+module.exports = genSiteDesktopShared;
diff --git a/site/scripts/gen-mobile-shared.js b/site/scripts/gen-mobile-shared.js
new file mode 100644
index 000000000..621908d79
--- /dev/null
+++ b/site/scripts/gen-mobile-shared.js
@@ -0,0 +1,97 @@
+const path = require('path');
+const { existsSync, readdirSync, readFileSync, outputFileSync } = require('fs-extra');
+const configs = require('../configs/nav.config');
+const { SRC_DIR, SITE_MOBILE_SHARED_FILE } = require('./constants');
+
+
+const camelizeRE = /-(\w)/g;
+const pascalizeRE = /(\w)(\w*)/g;
+
+function camelize(str) {
+ return str.replace(camelizeRE, (_, c) => c.toUpperCase());
+}
+
+function pascalize(str) {
+ return camelize(str).replace(
+ pascalizeRE,
+ (_, c1, c2) => c1.toUpperCase() + c2
+ );
+}
+
+function normalizePath(path) {
+ return path.replace(/\\/g, '/');
+}
+
+function decamelize(str, sep = '-') {
+ return str
+ .replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2')
+ .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2')
+ .toLowerCase();
+}
+
+function removeExt(path) {
+ return path.replace('.tsx', '');
+}
+
+function smartOutputFile(filePath, content) {
+ if (existsSync(filePath)) {
+ const previousContent = readFileSync(filePath, 'utf-8');
+
+ if (previousContent === content) {
+ return;
+ }
+ }
+
+ outputFileSync(filePath, content);
+}
+
+function genImports(demos) {
+ return demos.map(item => `import ${item.name} from '${removeExt(normalizePath(item.path))}';`).join('\n');
+}
+
+function genExports(demos) {
+ return `export const demos = {\n ${demos
+ .map(item => item.name)
+ .join(',\n ')}\n};`;
+}
+
+function genConfigs(demos) {
+ const demoNames = demos.map(item => decamelize(item.name));
+
+ function demoFilter(nav) {
+ return nav.filter(group => {
+ group.list = group.list.filter((item) =>
+ demoNames.includes(item.path)
+ );
+ return group.list.length;
+ });
+ }
+ Object.keys(configs).forEach(lang => {
+ if (configs[lang].nav) {
+ configs[lang].nav = demoFilter(configs[lang].nav)
+ }
+ })
+
+ return `export const config = ${JSON.stringify(configs, null, 2)}`;
+}
+
+function genCode(components) {
+ const demos = components.map(component => ({
+ component,
+ name: pascalize(component),
+ path: path.join(SRC_DIR, component, 'demo/index.tsx')
+ })).filter(item => existsSync(item.path));
+ return `${genImports(demos)}
+ ${genExports(demos)}
+ ${genConfigs(demos)}
+ `;
+}
+
+function genSiteMobileShared() {
+ const dirs = readdirSync(SRC_DIR);
+ const code = genCode(dirs);
+
+ smartOutputFile(SITE_MOBILE_SHARED_FILE, code);
+}
+
+module.exports = genSiteMobileShared;
\ No newline at end of file
diff --git a/site/scripts/gen-shared.js b/site/scripts/gen-shared.js
new file mode 100644
index 000000000..daf260b70
--- /dev/null
+++ b/site/scripts/gen-shared.js
@@ -0,0 +1,6 @@
+const genSiteDesktopShared = require('./gen-desktop-shared');
+const genSiteMobileShared = require('./gen-mobile-shared');
+
+genSiteDesktopShared();
+genSiteMobileShared();
+
diff --git a/site/scripts/utils.js b/site/scripts/utils.js
new file mode 100644
index 000000000..ccd6bcdbe
--- /dev/null
+++ b/site/scripts/utils.js
@@ -0,0 +1,47 @@
+const { existsSync, readFileSync, outputFileSync } = require('fs-extra');
+
+const camelizeRE = /-(\w)/g;
+const pascalizeRE = /(\w)(\w*)/g;
+
+function camelize(str) {
+ return str.replace(camelizeRE, (_, c) => c.toUpperCase());
+}
+
+function pascalize(str) {
+ return camelize(str).replace(
+ pascalizeRE,
+ (_, c1, c2) => c1.toUpperCase() + c2
+ );
+}
+
+function normalizePath(path) {
+ return path.replace(/\\/g, '/');
+}
+
+function decamelize(str, sep = '-') {
+ return str
+ .replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2')
+ .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2')
+ .toLowerCase();
+}
+
+function smartOutputFile(filePath, content) {
+ if (existsSync(filePath)) {
+ const previousContent = readFileSync(filePath, 'utf-8');
+
+ if (previousContent === content) {
+ return;
+ }
+ }
+
+ outputFileSync(filePath, content);
+}
+
+
+module.exports = {
+ decamelize,
+ camelize,
+ pascalize,
+ normalizePath,
+ smartOutputFile,
+}
diff --git a/site/src/.eslintrc b/site/src/.eslintrc
new file mode 100644
index 000000000..e5a34aec6
--- /dev/null
+++ b/site/src/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "env": {
+ "browser": true
+ }
+}
diff --git a/site/src/common/styles/base.scss b/site/src/common/styles/base.scss
new file mode 100644
index 000000000..4d36e60eb
--- /dev/null
+++ b/site/src/common/styles/base.scss
@@ -0,0 +1,48 @@
+@import './variable.scss';
+
+body {
+ min-width: 1100px;
+ margin: 0;
+ overflow-x: auto;
+ color: $van-doc-black;
+ font-size: 16px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
+ Segoe UI, Arial, Roboto, 'PingFang SC', 'Hiragino Sans GB',
+ 'Microsoft Yahei', sans-serif;
+ background-color: $van-doc-background-color;
+ -webkit-font-smoothing: antialiased;
+}
+
+p {
+ margin: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0;
+ font-size: inherit;
+}
+
+ul,
+ol {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+a {
+ text-decoration: none;
+}
+
+.van-doc-row {
+ width: 100%;
+
+ @media (min-width: $van-doc-row-max-width) {
+ width: $van-doc-row-max-width;
+ margin: 0 auto;
+ }
+}
diff --git a/site/src/common/styles/highlight.scss b/site/src/common/styles/highlight.scss
new file mode 100644
index 000000000..684386b55
--- /dev/null
+++ b/site/src/common/styles/highlight.scss
@@ -0,0 +1,82 @@
+@import './variable.scss';
+
+code {
+ position: relative;
+ display: block;
+ overflow-x: auto;
+ color: $van-doc-code-color;
+ font-weight: 400;
+ font-size: 13.4px;
+ font-family: $van-doc-code-font-family;
+ line-height: 26px;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ -webkit-font-smoothing: auto;
+}
+
+pre {
+ margin: 20px 0 0;
+
+ + p {
+ margin-top: 20px;
+ }
+}
+
+.hljs {
+ display: block;
+ padding: 0.5em;
+ overflow-x: auto;
+ background: #fff;
+}
+
+.hljs-subst {
+ color: $van-doc-code-color;
+}
+
+.hljs-string,
+.hljs-meta,
+.hljs-symbol,
+.hljs-template-tag,
+.hljs-template-variable,
+.hljs-addition {
+ color: $van-doc-green;
+}
+
+.hljs-comment,
+.hljs-quote {
+ color: #999;
+}
+
+.hljs-params,
+.hljs-keyword,
+.hljs-attribute {
+ color: $van-doc-purple;
+}
+
+.hljs-deletion,
+.hljs-variable,
+.hljs-number,
+.hljs-regexp,
+.hljs-literal,
+.hljs-bullet,
+.hljs-link {
+ color: #eb6f6f;
+}
+
+.hljs-attr,
+.hljs-selector-tag,
+.hljs-title,
+.hljs-section,
+.hljs-built_in,
+.hljs-doctag,
+.hljs-type,
+.hljs-name,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-strong {
+ color: #4994df;
+}
+
+.hljs-emphasis {
+ font-style: italic;
+}
diff --git a/site/src/common/styles/index.scss b/site/src/common/styles/index.scss
new file mode 100644
index 000000000..1d234f02c
--- /dev/null
+++ b/site/src/common/styles/index.scss
@@ -0,0 +1,17 @@
+@import './variable.scss';
+@import './base.scss';
+@import './highlight.scss';
+
+.van-doc-container {
+ overflow: hidden;
+ box-sizing: border-box;
+ padding-left: $van-doc-nav-width;
+
+ &--with-simulator {
+ padding-right: calc($van-doc-simulator-width + $van-doc-padding);
+
+ @media (max-width: 1300px) {
+ padding-right: calc($van-doc-simulator-small-width + $van-doc-padding);
+ }
+ }
+}
\ No newline at end of file
diff --git a/site/src/common/styles/variable.scss b/site/src/common/styles/variable.scss
new file mode 100644
index 000000000..d45b0ac4c
--- /dev/null
+++ b/site/src/common/styles/variable.scss
@@ -0,0 +1,29 @@
+$van-doc-black: #323233;
+$van-doc-blue: #1989fa;
+$van-doc-purple: #8080ff;
+$van-doc-fuchsia: #a7419e;
+$van-doc-green: #4fc08d;
+$van-doc-text-color: #34495e;
+$van-doc-text-light-blue: rgba(69, 90, 100, 0.6);
+$van-doc-background-color: #f7f8fa;
+$van-doc-grey: #999;
+$van-doc-dark-grey: #666;
+$van-doc-light-grey: #ccc;
+$van-doc-border-color: #f1f4f8;
+$van-doc-code-color: #58727e;
+$van-doc-code-background-color: #f1f4f8;
+$van-doc-code-font-family: 'Source Code Pro', 'Monaco', 'Inconsolata', monospace;
+$van-doc-padding: 30px;
+$van-doc-row-max-width: 1680px;
+$van-doc-nav-width: 220px;
+$van-doc-border-radius: 12px;
+
+// header
+$van-doc-header-top-height: 60px;
+$van-doc-header-bottom-height: 50px;
+
+// simulator
+$van-doc-simulator-width: 360px;
+$van-doc-simulator-small-width: 320px;
+$van-doc-simulator-height: 620px;
+$van-doc-simulator-small-height: 560px;
diff --git a/site/src/desktop/App.js b/site/src/desktop/App.js
new file mode 100644
index 000000000..a9df97042
--- /dev/null
+++ b/site/src/desktop/App.js
@@ -0,0 +1,53 @@
+import React, { Component } from 'react';
+import {
+ HashRouter as Router,
+ Route,
+ Switch,
+ Redirect,
+} from 'react-router-dom';
+
+import ScrollToTop from './components/scroll-to-top';
+import { version as pkgVersion } from '../../configs/doc.config';
+import navConfig from '../../configs/nav.config';
+import { prefix } from '../../constants';
+import DocContent from './components/content';
+import getRoutes from './router.config';
+
+const routes = getRoutes();
+export default class App extends Component {
+ state = {
+ i18n: 'zh-CN',
+ };
+
+ changeI18N = target => {
+ this.setState({
+ i18n: target,
+ });
+ };
+
+ render() {
+ const { i18n } = this.state;
+ const sideNavData = navConfig[i18n].nav;
+ const passthrough = i18nStr => ({
+ version: pkgVersion,
+ sideNavData: sideNavData,
+ changeI18N: this.changeI18N,
+ prefix,
+ i18n,
+ });
+ return (
+
+
+
+
+ {
+ routes.map(item => )
+ }
+
+
+
+
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/site/src/desktop/components/content/index.js b/site/src/desktop/components/content/index.js
new file mode 100644
index 000000000..cd654f251
--- /dev/null
+++ b/site/src/desktop/components/content/index.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import { withRouter } from "react-router-dom";
+import classnames from 'classnames';
+
+import PageHeader from '../header';
+import SideNav from '../side-nav';
+import { prefix } from '../../../../constants';
+import './styles.scss';
+
+
+function Layout({
+ i18n,
+ children,
+ version,
+ sideNavData,
+ sideNavRef,
+ location,
+}) {
+ const { pathname } = location;
+ const withSimulator = true;
+
+ const [ innerHeight, setInnerHeight ] = React.useState(window.innerHeight);
+ const [ scrollY, setScrollY ] = React.useState(window.scrollY);
+ const iframeRef = React.useRef(null);
+
+ const simulatorStyles = React.useMemo(() => {
+ const height = Math.min(640, innerHeight - 90);
+ return {
+ height: height + 'px',
+ }
+ }, [innerHeight])
+
+ const isFixed = React.useMemo(() => {
+ return scrollY > 60;
+ }, [scrollY])
+
+ React.useEffect(() => {
+ window.addEventListener('scroll', () => {
+ setScrollY(window.scrollY);
+ });
+ window.addEventListener('resize', () => {
+ setInnerHeight(window.innerHeight);
+ });
+ }, [])
+
+ return (
+
+
+
+
+
+ {withSimulator &&
+
+
}
+
+
+ );
+}
+
+export default withRouter(Layout);
diff --git a/site/src/desktop/components/content/styles.scss b/site/src/desktop/components/content/styles.scss
new file mode 100644
index 000000000..5df47c8d0
--- /dev/null
+++ b/site/src/desktop/components/content/styles.scss
@@ -0,0 +1,260 @@
+@import '../../../common/styles/variable.scss';
+
+.van-doc-content {
+ position: relative;
+ flex: 1;
+ padding: 0 0 75px;
+
+ .card {
+ margin-bottom: 24px;
+ padding: 24px;
+ background-color: #fff;
+ border-radius: $van-doc-border-radius;
+ box-shadow: 0 8px 12px #ebedf0;
+ }
+
+ a {
+ margin: 0 1px;
+ color: $van-doc-blue;
+ -webkit-font-smoothing: auto;
+
+ &:hover {
+ color: darken($van-doc-blue, 10%);
+ }
+
+ &:active {
+ color: darken($van-doc-blue, 20%);
+ }
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: $van-doc-black;
+ font-weight: normal;
+ line-height: 1.5;
+
+ &[id] {
+ cursor: pointer;
+ }
+ }
+
+ h1 {
+ margin: 0 0 30px;
+ font-size: 30px;
+ cursor: default;
+ }
+
+ h2 {
+ margin: 45px 0 20px;
+ font-size: 22px;
+ }
+
+ h3 {
+ margin-bottom: 16px;
+ font-weight: 500;
+ font-size: 18px;
+ }
+
+ h4 {
+ margin: 24px 0 12px;
+ font-weight: 500;
+ font-size: 15px;
+ }
+
+ h5 {
+ margin: 24px 0 12px;
+ font-weight: 500;
+ font-size: 14px;
+ }
+
+ p {
+ color: $van-doc-text-color;
+ font-size: 14px;
+ line-height: 26px;
+ }
+
+ table {
+ width: 100%;
+ margin-top: 12px;
+ color: $van-doc-text-color;
+ font-size: 13px;
+ line-height: 1.5;
+ border-collapse: collapse;
+
+ th {
+ padding: 8px 10px;
+ font-weight: 500;
+ text-align: left;
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+
+ td {
+ padding: 8px;
+ border-top: 1px solid $van-doc-code-background-color;
+
+ &:first-child {
+ padding-left: 0;
+
+ // version tag
+ code {
+ margin: 0;
+ padding: 2px 6px;
+ color: $van-doc-blue;
+ font-weight: 500;
+ font-size: 10px;
+ background-color: fade($van-doc-blue, 10%);
+ border-radius: 20px;
+ }
+ }
+
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+
+ em {
+ color: $van-doc-green;
+ font-size: 12.5px;
+ font-family: $van-doc-code-font-family;
+ font-style: normal;
+ -webkit-font-smoothing: auto;
+ }
+ }
+
+ ul li,
+ ol li {
+ position: relative;
+ margin: 5px 0 5px 10px;
+ padding-left: 15px;
+ color: $van-doc-text-color;
+ font-size: 14px;
+ line-height: 26px;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ box-sizing: border-box;
+ width: 6px;
+ height: 6px;
+ margin-top: 10px;
+ border: 1px solid $van-doc-dark-grey;
+ border-radius: 50%;
+ content: '';
+ }
+ }
+
+ hr {
+ margin: 30px 0;
+ border: 0 none;
+ border-top: 1px solid #eee;
+ }
+
+ p > code,
+ li > code,
+ table code {
+ display: inline;
+ margin: 2px 3px;
+ padding: 2px 5px;
+ font-size: 13px;
+ font-family: inherit;
+ word-break: keep-all;
+ background-color: #f0f2f5;
+ border-radius: 4px;
+ -webkit-font-smoothing: antialiased;
+ }
+
+ p > code {
+ font-size: 14px;
+ }
+
+ section {
+ padding: 30px;
+ overflow: hidden;
+ }
+
+ blockquote {
+ margin: 20px 0 0;
+ padding: 16px;
+ color: rgba(52, 73, 94, 0.8);
+ font-weight: 500;
+ font-size: 14px;
+ background-color: #ecf9ff;
+ border-radius: $van-doc-border-radius;
+ }
+
+ img {
+ width: 100%;
+ margin: 16px 0;
+ border-radius: $van-doc-border-radius;
+ }
+
+ &--changelog {
+ strong {
+ display: block;
+ margin: 24px 0 12px;
+ font-weight: 500;
+ font-size: 15px;
+ }
+
+ h3 {
+ + p code {
+ margin: 0;
+ }
+
+ a {
+ color: inherit;
+ font-size: 20px;
+ }
+ }
+ }
+}
+
+.van-doc-simulator {
+ position: absolute;
+ top: $van-doc-padding + $van-doc-header-top-height;
+ right: $van-doc-padding;
+ z-index: 1;
+ box-sizing: border-box;
+ width: $van-doc-simulator-width;
+ min-width: $van-doc-simulator-width;
+ overflow: hidden;
+ background: #fafafa;
+ border-radius: $van-doc-border-radius;
+ box-shadow: #ebedf0 0 4px 12px;
+
+ @media (max-width: 1100px) {
+ right: auto;
+ left: 750px;
+ }
+
+ @media (min-width: $van-doc-row-max-width) {
+ right: 50%;
+ margin-right: -$van-doc-row-max-width / 2 + 40px;
+ }
+
+ &-fixed {
+ position: fixed;
+ top: $van-doc-padding;
+ }
+
+ iframe {
+ display: block;
+ width: 100%;
+ }
+}
+
+.van-doc-nav-fixed {
+ top: 0;
+}
\ No newline at end of file
diff --git a/site/src/desktop/components/header/index.js b/site/src/desktop/components/header/index.js
new file mode 100644
index 000000000..7e194e931
--- /dev/null
+++ b/site/src/desktop/components/header/index.js
@@ -0,0 +1,80 @@
+import React, { Component } from 'react';
+import { withRouter } from 'react-router-dom';
+import SearchBox from '../search-box';
+import { versions } from '../../../../configs/doc.config';
+import navConfigs from '../../../../configs/nav.config';
+
+import './style.scss';
+
+const CONTROLLS = {
+ 'zh-CN': 'EN',
+ 'en-US': '中文',
+};
+
+class Header extends Component {
+
+ toggle = () => {
+ const { replace } = this.context.router.history;
+ const path = this.context.router.route.location.pathname.split('/');
+ if (path[1] === 'en') {
+ path[1] = 'zh';
+ } else {
+ path[1] = 'en';
+ }
+ replace(path.join('/'));
+ };
+
+ render() {
+ const { i18n, sideNavData } = this.props;
+ const { header } = navConfigs[i18n];
+
+ const { nav, logo } = header;
+
+ return (
+
+ );
+ }
+}
+
+export default withRouter(Header);
\ No newline at end of file
diff --git a/site/src/desktop/components/header/style.scss b/site/src/desktop/components/header/style.scss
new file mode 100644
index 000000000..a0ecb8a07
--- /dev/null
+++ b/site/src/desktop/components/header/style.scss
@@ -0,0 +1,134 @@
+@import '../../../common/styles/variable.scss';
+
+.van-doc-header {
+ width: 100%;
+ background-color: #001938;
+ user-select: none;
+
+ &__top {
+ display: flex;
+ align-items: center;
+ height: $van-doc-header-top-height;
+ padding: 0 $van-doc-padding;
+ line-height: $van-doc-header-top-height;
+
+ &-nav {
+ flex: 1;
+ font-size: 0;
+ text-align: right;
+
+ > li {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ &-item {
+ margin-left: 20px;
+ }
+
+ &-title {
+ display: block;
+ font-size: 15px;
+ }
+ }
+ }
+
+ &__cube {
+ position: relative;
+ display: block;
+ padding: 0 12px;
+ color: #fff;
+ font-size: 14px;
+ font-family: 'Helvetica Neue', Arial, sans-serif;
+ line-height: 24px;
+ text-align: center;
+ border: 1px solid rgba(255, 255, 255, 0.7);
+ border-radius: 20px;
+ cursor: pointer;
+ transition: 0.3s ease-in-out;
+ }
+
+ &__version {
+ padding-right: 20px;
+
+ &::after {
+ position: absolute;
+ top: 7px;
+ right: 7px;
+ width: 5px;
+ height: 5px;
+ color: rgba(255, 255, 255, 0.9);
+ border: 1px solid;
+ border-color: transparent transparent currentColor currentColor;
+ transform: rotate(-45deg);
+ content: '';
+ }
+
+ &-pop {
+ position: absolute;
+ top: 30px;
+ right: 0;
+ left: 0;
+ z-index: 99;
+ color: #333;
+ line-height: 36px;
+ text-align: left;
+ background-color: #fff;
+ border-radius: $van-doc-border-radius;
+ box-shadow: 0 4px 12px #ebedf0;
+ transform-origin: top;
+ transition: 0.2s cubic-bezier(0.215, 0.61, 0.355, 1);
+
+ &-item {
+ padding-left: 12px;
+ transition: 0.2s;
+
+ &:hover {
+ color: $van-doc-blue;
+ }
+ }
+ }
+ }
+
+ &__logo {
+ display: block;
+
+ img,
+ span {
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ img {
+ width: 24px;
+ margin-right: 10px;
+ }
+
+ span {
+ color: #fff;
+ font-size: 22px;
+ }
+ }
+
+ &__logo-link {
+ img {
+ display: block;
+ width: 26px;
+ height: 26px;
+ transition: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+ &:hover {
+ transform: scale(1.2);
+ }
+ }
+ }
+}
+
+.van-doc-dropdown {
+ &-enter,
+ &-leave-active {
+ transform: scaleY(0);
+ opacity: 0;
+ }
+}
\ No newline at end of file
diff --git a/site/src/desktop/components/scroll-to-top/index.js b/site/src/desktop/components/scroll-to-top/index.js
new file mode 100644
index 000000000..6ab06d5fc
--- /dev/null
+++ b/site/src/desktop/components/scroll-to-top/index.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react';
+import { withRouter } from 'react-router-dom';
+
+class ScrollToTop extends Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.location !== prevProps.location) {
+ window.scrollTo(0, 0);
+ }
+ }
+
+ render() {
+ return
{this.props.children}
;
+ }
+}
+
+export default withRouter(ScrollToTop);
diff --git a/site/src/desktop/components/search-box/ResultList.js b/site/src/desktop/components/search-box/ResultList.js
new file mode 100644
index 000000000..1288c217a
--- /dev/null
+++ b/site/src/desktop/components/search-box/ResultList.js
@@ -0,0 +1,123 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import isEmpty from 'lodash/isEmpty';
+import cx from 'classnames';
+
+import { SKIP_SCROLL } from './constants';
+
+const i18n = {
+ 'zh-CN': {
+ notFound: '未找到结果',
+ },
+ 'en-US': {
+ notFound: 'No results found',
+ },
+};
+
+export default class ResultList extends Component {
+ static propTypes = {
+ matches: PropTypes.array,
+ locale: PropTypes.string.isRequired,
+ activeIndex: PropTypes.number,
+ redirectToResult: PropTypes.func.isRequired,
+ clearActiveIndex: PropTypes.func.isRequired,
+ };
+
+ componentDidUpdate() {
+ this.scrollActiveElementToViewport();
+ }
+
+ render() {
+ const { matches, locale, activeIndex, redirectToResult } = this.props;
+
+ if (isEmpty(matches)) {
+ return (
+
+ {i18n[locale].notFound}
+
+ );
+ }
+
+ return (
+
+ {matches.map((item, idx) => {
+ const { title, subtitle, path } = item;
+
+ return (
+ - redirectToResult(item)}
+ >
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ );
+ })}
+
+ );
+ }
+
+ saveListNode = node => {
+ this.list = node;
+ };
+
+ getListItemHeight() {
+ if (this.list && !this.itemHeight) {
+ const itemNode = this.list.querySelector(
+ '.van-doc-search-box-result-item'
+ );
+ if (itemNode) {
+ this.itemHeight = itemNode.scrollHeight;
+ }
+ }
+
+ return this.itemHeight || 0;
+ }
+
+ scrollActiveElementToViewport() {
+ const { activeIndex } = this.props;
+ const { list } = this;
+ if (!list || activeIndex === SKIP_SCROLL) {
+ return;
+ }
+
+ const { scrollTop, offsetHeight } = list;
+ const itemHeight = this.getListItemHeight();
+ const activeElementPosition = itemHeight * (activeIndex + 1);
+ const actualPosition = scrollTop + offsetHeight;
+
+ // 如果高亮节点不在可见区域就滚动
+ const bottomOverflow = activeElementPosition > actualPosition;
+ const topOverflow =
+ activeElementPosition - itemHeight < actualPosition - offsetHeight;
+ if (bottomOverflow || topOverflow) {
+ let scrollY;
+
+ if (bottomOverflow) {
+ scrollY =
+ Math.ceil((activeElementPosition - offsetHeight) / itemHeight) *
+ itemHeight;
+ } else {
+ scrollY =
+ Math.ceil((activeElementPosition - itemHeight) / itemHeight) *
+ itemHeight;
+ }
+
+ // scroll(this.list, 0, scrollY, 100);
+ }
+ }
+}
diff --git a/site/src/desktop/components/search-box/constants.js b/site/src/desktop/components/search-box/constants.js
new file mode 100644
index 000000000..edc4eda8e
--- /dev/null
+++ b/site/src/desktop/components/search-box/constants.js
@@ -0,0 +1,2 @@
+// 用于鼠标东时重置键盘选中的下标,此时不应该滚动位置
+export const SKIP_SCROLL = -100;
diff --git a/site/src/desktop/components/search-box/index.js b/site/src/desktop/components/search-box/index.js
new file mode 100644
index 000000000..a6d483be3
--- /dev/null
+++ b/site/src/desktop/components/search-box/index.js
@@ -0,0 +1,218 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import isEqual from 'lodash/isEqual';
+import { withRouter } from 'react-router-dom';
+import isEmpty from 'lodash/isEmpty';
+
+import ResultList from './ResultList';
+import makeSearcher from './search';
+import { SKIP_SCROLL } from './constants';
+
+import './style.scss';
+
+const i18n = {
+ 'zh-CN': {
+ placeholder: '搜索组件...',
+ },
+
+ 'en-US': {
+ placeholder: 'Search components...',
+ },
+};
+
+class SearchBox extends Component {
+ static propTypes = {
+ locale: PropTypes.string.isRequired,
+ navData: PropTypes.array.isRequired,
+ };
+
+ state = {
+ keyword: '',
+ activeIndex: SKIP_SCROLL,
+ matches: [],
+ resultVisible: false,
+ };
+
+ constructor(props) {
+ super(props);
+ this.buildLUT(props.navData);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!isEqual(nextProps.navData, this.props.navData)) {
+ this.buildLUT();
+ }
+ }
+
+ render() {
+ const { keyword, matches, activeIndex, resultVisible } = this.state;
+ const { locale } = this.props;
+
+ return (
+
+ //
+ //
+ //
+ //
+
+ //
+ //
+ //
+ //
+ );
+ }
+
+ // react-hot-loader rewrites this function into an infinite loop... lol
+ // if we use arrow function here
+ onKeywordChange(evt) {
+ const keyword = evt.target.value;
+ if (keyword !== this.state.keyword) {
+ this.search(keyword);
+ }
+ }
+
+ onKeydown(evt) {
+ const { key } = evt;
+
+ if (key === 'Enter') {
+ return setTimeout(() => {
+ const { matches } = this.state;
+ if (!isEmpty(matches)) {
+ let { activeIndex } = this.state;
+ if (activeIndex < 0) {
+ activeIndex = 0;
+ }
+ this.redirectToResult(matches[activeIndex]);
+ }
+ }, 0);
+ }
+
+ if (key !== 'ArrowDown' && key !== 'ArrowUp') {
+ return;
+ }
+
+ const { resultVisible } = this.state;
+ if (!resultVisible) {
+ this.setState({
+ activeIndex: 0,
+ resultVisible: true,
+ });
+ return;
+ }
+
+ // Scroll list with arrow keys
+ let { activeIndex } = this.state;
+ if (activeIndex === SKIP_SCROLL) {
+ activeIndex = -1;
+ }
+
+ if (key === 'ArrowDown') {
+ activeIndex++;
+ } else if (key === 'ArrowUp') {
+ activeIndex--;
+ } else {
+ return;
+ }
+ const { matches } = this.state;
+ const maxIndex = matches.length - 1;
+ if (activeIndex < 0) {
+ activeIndex = maxIndex;
+ } else if (activeIndex > maxIndex) {
+ activeIndex = 0;
+ }
+
+ this.setState({
+ activeIndex,
+ resultVisible: true,
+ });
+ }
+
+ onResultVisibleChange(visible) {
+ this.setState({
+ resultVisible: visible,
+ });
+ }
+
+ onInputClick() {
+ this.search(this.state.keyword);
+ }
+
+ clearActiveIndex() {
+ this.setState({
+ activeIndex: SKIP_SCROLL,
+ });
+ }
+
+ buildLUT(navData) {
+ // Only include components
+ const { list } = navData[1];
+ const data = list.reduce(
+ // eslint-disable-next-line
+ (lut, item) => {
+ lut.push(item);
+ return lut;
+ }, []
+ );
+
+ data.sort((a, b) => {
+ if (a.title > b.title) {
+ return 1;
+ }
+
+ if (a.title === b.title) {
+ return 0;
+ }
+
+ return -1;
+ });
+
+ this.lut = makeSearcher(data);
+ }
+
+ search(keyword) {
+ if (!this.lut) {
+ return;
+ }
+
+ const matches = this.lut.search(keyword);
+
+ this.setState({
+ keyword,
+ resultVisible: true,
+ matches,
+ activeIndex: 0,
+ });
+ }
+
+ redirectToResult(item) {
+ const { path } = item;
+ const { history, locale } = this.props;
+ const prefix = locale.split('-')[0];
+
+ history.replace(`/${prefix}/${path}`);
+ this.onResultVisibleChange(false);
+ }
+}
+
+export default withRouter(SearchBox);
diff --git a/site/src/desktop/components/search-box/search.js b/site/src/desktop/components/search-box/search.js
new file mode 100644
index 000000000..cf6281d85
--- /dev/null
+++ b/site/src/desktop/components/search-box/search.js
@@ -0,0 +1,25 @@
+import Fuse from 'fuse.js';
+
+const options = {
+ shouldSort: true,
+ threshold: 0.5,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: ['title', 'subtitle'],
+};
+
+export default function makeSearcher(list) {
+ const fuse = new Fuse(list, options);
+
+ return {
+ search(keyword) {
+ if (!keyword) {
+ return list;
+ }
+
+ return fuse.search(keyword);
+ },
+ };
+}
diff --git a/site/src/desktop/components/search-box/style.scss b/site/src/desktop/components/search-box/style.scss
new file mode 100644
index 000000000..fa5bbd169
--- /dev/null
+++ b/site/src/desktop/components/search-box/style.scss
@@ -0,0 +1,59 @@
+@import '../../../common/styles/variable.scss';
+
+
+.van-doc-search {
+ width: 200px;
+ height: 60px;
+ margin-left: 140px;
+ color: #fff;
+ font-size: 14px;
+ background-color: transparent;
+ border: none;
+
+ &:focus {
+ outline: none;
+ }
+
+ &::placeholder {
+ color: #fff;
+ opacity: 0.7;
+ }
+}
+
+.ds-dropdown-menu {
+ line-height: 1.8;
+}
+
+.algolia-autocomplete {
+ .algolia-docsearch-suggestion--highlight {
+ color: $van-doc-blue;
+ background-color: transparent;
+ }
+
+ .algolia-docsearch-suggestion--title {
+ font-weight: 500;
+ }
+
+ .algolia-docsearch-suggestion--text {
+ .algolia-docsearch-suggestion--highlight {
+ box-shadow: inset 0 -1px 0 0 $van-doc-blue;
+ }
+ }
+
+ .algolia-docsearch-suggestion--category-header {
+ border-bottom-color: #eee;
+ }
+
+ .ds-dropdown-menu [class^='ds-dataset-'] {
+ border: none;
+ }
+
+ .ds-dropdown-menu {
+ top: 80% !important;
+ box-shadow: 0 4px 12px #ebedf0;
+
+ &::before {
+ display: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/site/src/desktop/components/side-nav/index.js b/site/src/desktop/components/side-nav/index.js
new file mode 100644
index 000000000..f2c0767d5
--- /dev/null
+++ b/site/src/desktop/components/side-nav/index.js
@@ -0,0 +1,58 @@
+import React, { Component } from 'react';
+import { NavLink } from 'react-router-dom';
+import classnames from 'classnames';
+
+import { prefix } from '../../../../constants';
+
+import './style.scss';
+
+export default class SideNav extends Component {
+
+ parseData = (item, index) => (
+
+
+ {item.name}
+
+ {item.list && item.list.map(this.parseList)}
+
+ );
+
+ parseList = (navItem, index) => {
+ const { title, subtitle, hidden, link } = navItem;
+
+ if (hidden) {
+ return null;
+ }
+
+ const linkTitle = subtitle ? (
+
+ {title} {subtitle}
+
+ ) : (
+ title
+ );
+
+ return navItem.disabled ? null : (
+
+
+ {linkTitle}
+
+
+ );
+ };
+
+ render() {
+ const { data, className } = this.props;
+ return (
+
+ {data.map(this.parseData)}
+
+ );
+ }
+}
diff --git a/site/src/desktop/components/side-nav/style.scss b/site/src/desktop/components/side-nav/style.scss
new file mode 100644
index 000000000..66b9132ec
--- /dev/null
+++ b/site/src/desktop/components/side-nav/style.scss
@@ -0,0 +1,84 @@
+@import '../../../common/styles/variable.scss';
+
+.van-doc-nav {
+ position: fixed;
+ top: 60px;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ min-width: $van-doc-nav-width;
+ max-width: $van-doc-nav-width;
+ padding: 24px 0 72px;
+ overflow-y: scroll;
+ background-color: #fff;
+ box-shadow: 0 8px 12px #ebedf0;
+
+ @media (min-width: $van-doc-row-max-width) {
+ left: 50%;
+ margin-left: -($van-doc-row-max-width / 2);
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ background-color: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: transparent;
+ border-radius: 6px;
+ }
+
+ &:hover::-webkit-scrollbar-thumb {
+ background-color: rgba(69, 90, 100, 0.2);
+ }
+
+ &__group {
+ margin-bottom: 16px;
+ }
+
+ &__title {
+ padding: 8px 0 8px $van-doc-padding;
+ color: #455a64;
+ font-weight: 500;
+ font-size: 15px;
+ line-height: 28px;
+ }
+
+ &__item {
+ a {
+ display: block;
+ margin: 0;
+ padding: 8px 0 8px $van-doc-padding;
+ color: #455a64;
+ font-size: 14px;
+ line-height: 28px;
+ transition: color 0.2s;
+
+ &:hover,
+ &.active {
+ color: $van-doc-green;
+ }
+
+ &.active {
+ -webkit-font-smoothing: auto;
+ }
+
+ span {
+ font-size: 13px;
+ }
+ }
+ }
+
+ @media (max-width: 1300px) {
+ &__item {
+ a {
+ font-size: 13px;
+ }
+
+ &:active {
+ font-size: 14px;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/site/src/desktop/index.html b/site/src/desktop/index.html
new file mode 100644
index 000000000..9b30ea073
--- /dev/null
+++ b/site/src/desktop/index.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+ Vant React
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/site/src/desktop/index.js b/site/src/desktop/index.js
new file mode 100644
index 000000000..b8057e57d
--- /dev/null
+++ b/site/src/desktop/index.js
@@ -0,0 +1,25 @@
+import 'core-js/stable';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { AppContainer } from 'react-hot-loader';
+
+import './styles/index.scss'
+
+import App from './App';
+
+const render = ChildComponent => {
+ ReactDOM.render(
+
+
+ ,
+ document.getElementById('app-container')
+ );
+};
+
+render(App);
+
+if (module.hot) {
+ module.hot.accept('./App', () => {
+ render(App);
+ });
+}
diff --git a/site/src/desktop/router.config.js b/site/src/desktop/router.config.js
new file mode 100644
index 000000000..fe28f6f4b
--- /dev/null
+++ b/site/src/desktop/router.config.js
@@ -0,0 +1,50 @@
+import configs from '../../configs/nav.config';
+import { documents } from '../../configs/site-desktop-shared';
+
+function decamelize(str, sep = '-') {
+ return str
+ .replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2')
+ .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2')
+ .toLowerCase();
+}
+
+function parseName(name) {
+ if (name.indexOf('_') !== -1) {
+ const pairs = name.split('_');
+ const component = pairs.shift();
+
+ return {
+ component: `${decamelize(component)}`,
+ lang: pairs.join('-'),
+ };
+ }
+
+ return {
+ component: `${decamelize(name)}`,
+ lang: '',
+ };
+}
+
+
+function getRoutes() {
+ const routes = [];
+ const names = Object.keys(documents);
+
+
+
+ names.forEach(name => {
+ const { component, lang } = parseName(name);
+
+ if (lang) {
+ routes.push({
+ name: `${lang}/${component}`,
+ path: `/${lang}/${component}`,
+ component: documents[name],
+ })
+ }
+ })
+
+ return routes;
+}
+
+export default getRoutes;
diff --git a/site/src/desktop/styles/docs.scss b/site/src/desktop/styles/docs.scss
new file mode 100644
index 000000000..2ef781fd4
--- /dev/null
+++ b/site/src/desktop/styles/docs.scss
@@ -0,0 +1,20 @@
+@import '../../common/styles/index.scss';
+
+.van-doc-row {
+ width: 100%;
+
+ @media(min-width: $van-doc-row-max-width) {
+ width: $van-doc-row-max-width;
+ margin: 0 auto;
+ }
+}
+
+.van-doc-container {
+ box-sizing: border-box;
+ padding-left: $van-doc-nav-width;
+ overflow: hidden;
+
+ &--with-simulator {
+ padding-right: $van-doc-simulator-width + $van-doc-padding;
+ }
+}
diff --git a/site/src/desktop/styles/index.scss b/site/src/desktop/styles/index.scss
new file mode 100644
index 000000000..2721c1f33
--- /dev/null
+++ b/site/src/desktop/styles/index.scss
@@ -0,0 +1 @@
+@import './docs.scss'
\ No newline at end of file
diff --git a/site/src/simulator/App.js b/site/src/simulator/App.js
new file mode 100644
index 000000000..b74490f20
--- /dev/null
+++ b/site/src/simulator/App.js
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import {
+ HashRouter as Router,
+ Route,
+ Switch,
+ Redirect
+} from 'react-router-dom';
+import { getRoutes } from './utils';
+import DemoHome from './pages/DemoHome';
+
+const routes = getRoutes();
+
+const App = () => {
+ return (
+
+
+
+ {
+ routes.map(item => )
+ }
+
+
+
+ );
+}
+
+export default App;
diff --git a/site/src/simulator/index.html b/site/src/simulator/index.html
new file mode 100644
index 000000000..c34f8886a
--- /dev/null
+++ b/site/src/simulator/index.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+ Vant React
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/site/src/simulator/index.js b/site/src/simulator/index.js
new file mode 100644
index 000000000..793584fc6
--- /dev/null
+++ b/site/src/simulator/index.js
@@ -0,0 +1,30 @@
+import 'core-js/stable';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { AppContainer } from 'react-hot-loader';
+
+import App from './App';
+
+const render = ChildComponent => {
+ ReactDOM.render(
+
+
+ ,
+ document.getElementById('app-container') // eslint-disable-line
+ );
+};
+
+// Add a delay in dev mode to ensure styles are loaded before executing any JavaScript code
+if (process.env.NODE_ENV !== 'production') {
+ setTimeout(() => {
+ render(App);
+ }, 500);
+} else {
+ render(App);
+}
+
+if (module.hot) {
+ module.hot.accept('./App', () => {
+ render(App);
+ });
+}
diff --git a/site/src/simulator/pages/DemoHome.tsx b/site/src/simulator/pages/DemoHome.tsx
new file mode 100644
index 000000000..d4ff89a24
--- /dev/null
+++ b/site/src/simulator/pages/DemoHome.tsx
@@ -0,0 +1,7 @@
+import * as React from 'react';
+
+function DemoHome() {
+ return DemoHome
;
+}
+
+export default DemoHome;
\ No newline at end of file
diff --git a/site/src/simulator/utils.js b/site/src/simulator/utils.js
new file mode 100644
index 000000000..530d86466
--- /dev/null
+++ b/site/src/simulator/utils.js
@@ -0,0 +1,37 @@
+import { demos, config } from '../../configs/site-mobile-shared';
+import { decamelize } from '../../utils';
+
+export function getRoutes() {
+ const routes = [];
+ const names = Object.keys(demos);
+ const langs = config ? Object.keys(config) : [];
+
+ names.forEach(name => {
+ const component = decamelize(name);
+
+ if (langs.length) {
+ langs.forEach(lang => {
+ routes.push({
+ name: `${lang}/${component}`,
+ path: `/${lang}/${component}`,
+ component: demos[name],
+ meta: {
+ name,
+ lang,
+ },
+ });
+ });
+ } else {
+ routes.push({
+ name,
+ path: `/${component}`,
+ component: demos[name],
+ meta: {
+ name,
+ },
+ });
+ }
+ });
+
+ return routes;
+}
diff --git a/site/tsconfig.json b/site/tsconfig.json
new file mode 100644
index 000000000..c73ab144f
--- /dev/null
+++ b/site/tsconfig.json
@@ -0,0 +1,38 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "module": "esnext",
+ "lib": [
+ "dom",
+ "esnext"
+ ],
+ "moduleResolution": "node",
+ "jsx": "react",
+ "sourceMap": true,
+ "declaration": true,
+ "esModuleInterop": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": false,
+ "strictNullChecks": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "allowSyntheticDefaultImports": true,
+ "target": "es5",
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": [
+ "src"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ ]
+}
diff --git a/site/utils/index.js b/site/utils/index.js
new file mode 100644
index 000000000..257487164
--- /dev/null
+++ b/site/utils/index.js
@@ -0,0 +1,10 @@
+function decamelize(str, sep = '-') {
+ return str
+ .replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2')
+ .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2')
+ .toLowerCase();
+}
+
+module.exports = {
+ decamelize,
+}
diff --git a/site/webpack/webpack.config.js b/site/webpack/webpack.config.js
new file mode 100644
index 000000000..bef559d2c
--- /dev/null
+++ b/site/webpack/webpack.config.js
@@ -0,0 +1,200 @@
+const webpack = require('webpack');
+const Fiber = require('fibers');
+const sass = require('sass');
+const os = require('os');
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const ProgressBarPlugin = require('progress-bar-webpack-plugin');
+const { prefix } = require('../constants');
+
+const DEV = process.env.NODE_ENV !== 'production';
+
+
+module.exports = {
+ mode: process.env.NODE_ENV,
+
+ output: {
+ path: path.resolve(__dirname, '../dist'),
+ filename: '[name]-[hash].js',
+ publicPath: prefix,
+ },
+
+ resolve: {
+ extensions: ['.tsx', '.ts', '.js', '.md', '.json'],
+ alias: Object.assign({
+ vant$: path.resolve(__dirname, '../../src'),
+ }),
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/,
+ use: 'url-loader',
+ },
+ {
+ test: /\.html$/,
+ use: 'html-loader',
+ },
+ {
+ test: /\.s?css$/,
+ use: [
+ DEV ? 'style-loader' : MiniCssExtractPlugin.loader,
+ 'cache-loader',
+ {
+ loader: 'css-loader',
+ options: {
+ importLoaders: 1,
+ sourceMap: DEV,
+ },
+ },
+ {
+ loader: 'postcss-loader',
+ options: {
+ sourceMap: DEV,
+ config: {
+ path: path.resolve(__dirname, '..'),
+ },
+ },
+ },
+ {
+ loader: 'sass-loader',
+ options: {
+ sourceMap: DEV,
+ implementation: sass,
+ fiber: Fiber,
+ },
+ },
+ ],
+ },
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: true,
+ },
+ },
+ ],
+ },
+ {
+ test: /\.md$/,
+ use: [
+ {
+ loader: 'thread-loader',
+ // loaders with equal options will share worker pools
+ options: {
+ // the number of spawned workers, defaults to (number of cpus - 1) or
+ // fallback to 1 when require('os').cpus() is undefined
+ workers: os.cpus() - 1,
+
+ // number of jobs a worker processes in parallel
+ // defaults to 20
+ workerParallelJobs: 10,
+
+ // additional node.js arguments
+ workerNodeArgs: ['--max-old-space-size=1024'],
+
+ // Allow to respawn a dead worker pool
+ // respawning slows down the entire compilation
+ // and should be set to false for development
+ poolRespawn: !DEV,
+
+ // timeout for killing the worker processes when idle
+ // defaults to 500 (ms)
+ // can be set to Infinity for watching builds to keep workers alive
+ poolTimeout: DEV ? Infinity : 500,
+
+ // number of jobs the poll distributes to the workers
+ // defaults to 200
+ // decrease of less efficient but more fair distribution
+ poolParallelJobs: 50,
+
+ // name of the pool
+ // can be used to create different pools with elsewise identical options
+ name: 'md-pool',
+ },
+ },
+ 'babel-loader',
+ {
+ loader: path.resolve(__dirname, '../markdown-loader')
+ },
+ // {
+ // loader: 'react-markdown-doc-loader',
+ // options: {
+ // jsTemplate: path.resolve(__dirname, '../react-template.jstpl'),
+ // renderers: {
+ // markdown: 'Markdown',
+ // style: 'Style',
+ // demo: 'Demo',
+ // },
+ // },
+ // },
+ // 'markdown-doc-loader',
+ ],
+ },
+ {
+ test: /\.tsx?$/,
+ use: [
+ {
+ loader: 'awesome-typescript-loader',
+ options: {
+ useCache: true,
+ getCustomTransformers: program => ({
+ before: [],
+ }),
+ },
+ },
+ ],
+ },
+ ],
+ },
+
+ plugins: [
+ new webpack.EnvironmentPlugin({
+ NODE_ENV: 'development', // use 'development' unless process.env.NODE_ENV is defined
+ VERSION: 'release',
+ }),
+
+ new ProgressBarPlugin(),
+
+ new HtmlWebpackPlugin({
+ template: 'src/desktop/index.html',
+ filename: 'index.html',
+ chunks: ['vendor', 'docs'],
+ inject: 'body',
+ }),
+
+ new HtmlWebpackPlugin({
+ template: 'src/simulator/index.html',
+ chunks: ['vendor', 'simulator'],
+ filename: 'simulator.html',
+ inject: 'body',
+ }),
+
+ new MiniCssExtractPlugin({
+ filename: DEV ? '[name].css' : '[name]-[contenthash].css',
+ chunkFilename: DEV ? '[id].css' : '[id].[contenthash].css',
+ }),
+ ],
+
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendor',
+ chunks: 'all',
+ },
+ },
+ },
+ },
+
+ node: {
+ fs: 'empty',
+ net: 'empty',
+ },
+};
diff --git a/site/webpack/webpack.dev.config.js b/site/webpack/webpack.dev.config.js
new file mode 100644
index 000000000..f491bf752
--- /dev/null
+++ b/site/webpack/webpack.dev.config.js
@@ -0,0 +1,32 @@
+const merge = require('webpack-merge');
+const webpack = require('webpack');
+
+const base = require('./webpack.config');
+
+module.exports = merge.smart(base, {
+ entry: {
+ docs: [
+ './src/desktop/index.js',
+ ],
+ simulator: [
+ './src/simulator/index.js'
+ ]
+ },
+
+ devServer: {
+ host: 'localhost',
+ port: 4396,
+ hot: true,
+ open: true,
+ },
+
+ devtool: 'inline-cheap-module-source-map',
+
+ resolve: {
+ alias: {
+ 'react-dom': '@hot-loader/react-dom',
+ },
+ },
+
+ plugins: [new webpack.HotModuleReplacementPlugin()],
+});
diff --git a/site/webpack/webpack.prd.config.js b/site/webpack/webpack.prd.config.js
new file mode 100644
index 000000000..7d2a542fb
--- /dev/null
+++ b/site/webpack/webpack.prd.config.js
@@ -0,0 +1,47 @@
+const merge = require('webpack-merge');
+const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
+
+const base = require('./webpack.config');
+
+module.exports = merge.smart(base, {
+ entry: {
+ docs: './src/desktop/index.js',
+ simulator: './src/simulator/index.js'
+ },
+
+ plugins: [
+ new FaviconsWebpackPlugin({
+ // Your source logo
+ logo: './assets/zanui-logo.png',
+ // The prefix for all image files (might be a folder or a name)
+ prefix: 'favico-[hash]-',
+ // Emit all stats of the generated icons
+ // emitStats: false,
+ // The name of the json containing all favicon information
+ // statsFilename: 'iconstats-[hash].json',
+ // Generate a cache file with control hashes and
+ // don't rebuild the favicons until those hashes change
+ persistentCache: true,
+ // Inject the html into the html-webpack-plugin
+ inject: true,
+ // favicon background color (see https://github.com/haydenbleasel/favicons#usage)
+ background: '#fff',
+ // favicon app title (see https://github.com/haydenbleasel/favicons#usage)
+ title: 'Vant React',
+
+ // which icons should be generated (see https://github.com/haydenbleasel/favicons#usage)
+ icons: {
+ android: true,
+ appleIcon: true,
+ appleStartup: true,
+ coast: false,
+ favicons: true,
+ firefox: true,
+ opengraph: false,
+ twitter: false,
+ yandex: false,
+ windows: false,
+ },
+ }),
+ ],
+});
diff --git a/src/components/Button/README.zh-CN.md b/src/components/Button/README.zh-CN.md
new file mode 100644
index 000000000..0ac75fea2
--- /dev/null
+++ b/src/components/Button/README.zh-CN.md
@@ -0,0 +1,155 @@
+# Button 按钮
+
+### 引入
+
+```js
+import { Button } from 'vant-react';
+
+```
+
+## 代码演示
+
+### 按钮类型
+
+支持`default`、`primary`、`info`、`warning`、`danger`五种类型,默认为`default`
+
+```html
+
+
+
+
+
+```
+
+### 朴素按钮
+
+通过`plain`属性将按钮设置为朴素按钮,朴素按钮的文字为按钮颜色,背景为白色。
+
+```html
+
+
+```
+
+### 细边框
+
+设置`hairline`属性可以开启 0.5px 边框,基于伪类实现
+
+```html
+
+
+```
+
+### 禁用状态
+
+通过`disabled`属性来禁用按钮,禁用状态下按钮不可点击
+
+```html
+
+
+```
+
+### 加载状态
+
+通过`loading`属性设置按钮为加载状态,加载状态下默认会隐藏按钮文字,可以通过`loading-text`设置加载状态下的文字
+
+```html
+
+
+
+```
+
+### 按钮形状
+
+通过`square`设置方形按钮,通过`round`设置圆形按钮
+
+```html
+
+
+```
+
+### 图标按钮
+
+通过`icon`属性设置按钮图标,支持 Icon 组件里的所有图标,也可以传入图标 URL
+
+```html
+
+
+
+```
+
+### 按钮尺寸
+
+支持`large`、`normal`、`small`、`mini`四种尺寸,默认为`normal`
+
+```html
+
+
+
+
+```
+
+### 块级元素
+
+按钮在默认情况下为行内块级元素,通过`block`属性可以将按钮的元素类型设置为块级元素
+
+```html
+
+```
+
+### 页面导航
+
+可以通过`url`属性进行 URL 跳转,或通过`to`属性进行路由跳转
+
+```html
+
+
+```
+
+### 自定义颜色
+
+通过`color`属性可以自定义按钮的颜色
+
+```html
+
+
+
+```
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| type | 类型,可选值为 `primary` `info` `warning` `danger` | _string_ | `default` |
+| size | 尺寸,可选值为 `large` `small` `mini` | _string_ | `normal` |
+| text | 按钮文字 | _string_ | - |
+| color `v2.1.8` | 按钮颜色,支持传入`linear-gradient`渐变色 | _string_ | - |
+| icon | 左侧[图标名称](#/zh-CN/icon)或图片链接 | _string_ | - |
+| icon-prefix `v2.6.0` | 图标类名前缀,同 Icon 组件的 [class-prefix 属性](#/zh-CN/icon#props) | _string_ | `van-icon` |
+| tag | 根节点的 HTML 标签 | _string_ | `button` |
+| native-type | 原生 button 标签的 type 属性 | _string_ | - |
+| block | 是否为块级元素 | _boolean_ | `false` |
+| plain | 是否为朴素按钮 | _boolean_ | `false` |
+| square | 是否为方形按钮 | _boolean_ | `false` |
+| round | 是否为圆形按钮 | _boolean_ | `false` |
+| disabled | 是否禁用按钮 | _boolean_ | `false` |
+| hairline | 是否使用 0.5px 边框 | _boolean_ | `false` |
+| loading | 是否显示为加载状态 | _boolean_ | `false` |
+| loading-text | 加载状态提示文字 | _string_ | - |
+| loading-type | [加载图标类型](#/zh-CN/loading),可选值为`spinner` | _string_ | `circular` |
+| loading-size | 加载图标大小 | _string_ | `20px` |
+| url | 点击后跳转的链接地址 | _string_ | - |
+| to | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) | _string \| object_ | - |
+| replace | 是否在跳转时替换当前页面历史 | _boolean_ | `false` |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+| ---------- | ---------------------------------------- | ------------------- |
+| click | 点击按钮,且按钮状态不为加载或禁用时触发 | _event: Event_ |
+| touchstart | 开始触摸按钮时触发 | _event: TouchEvent_ |
diff --git a/src/components/Cell/README.zh-CN.md b/src/components/Cell/README.zh-CN.md
new file mode 100644
index 000000000..e956ba282
--- /dev/null
+++ b/src/components/Cell/README.zh-CN.md
@@ -0,0 +1 @@
+# Cell 单元格
\ No newline at end of file
diff --git a/src/components/Field/README.zh-CN.md b/src/components/Field/README.zh-CN.md
new file mode 100644
index 000000000..0eeea5d5e
--- /dev/null
+++ b/src/components/Field/README.zh-CN.md
@@ -0,0 +1 @@
+# Field 输入框
\ No newline at end of file
diff --git a/src/components/Icons/README.zh-CN.md b/src/components/Icons/README.zh-CN.md
new file mode 100644
index 000000000..ae377af89
--- /dev/null
+++ b/src/components/Icons/README.zh-CN.md
@@ -0,0 +1 @@
+# Icon 图标
\ No newline at end of file
diff --git a/src/components/Navbar/README.zh-CN.md b/src/components/Navbar/README.zh-CN.md
new file mode 100644
index 000000000..99b99cb0e
--- /dev/null
+++ b/src/components/Navbar/README.zh-CN.md
@@ -0,0 +1 @@
+# Navbar 导航栏
\ No newline at end of file
diff --git a/src/components/Popup/README.zh-CN.md b/src/components/Popup/README.zh-CN.md
new file mode 100644
index 000000000..23d8a72c2
--- /dev/null
+++ b/src/components/Popup/README.zh-CN.md
@@ -0,0 +1 @@
+# Popup 弹出层
\ No newline at end of file
diff --git a/src/components/Rate/README.zh-CN.md b/src/components/Rate/README.zh-CN.md
new file mode 100644
index 000000000..0593d74da
--- /dev/null
+++ b/src/components/Rate/README.zh-CN.md
@@ -0,0 +1 @@
+# Rate 评分
\ No newline at end of file
diff --git a/src/components/Search/README.zh-CN.md b/src/components/Search/README.zh-CN.md
new file mode 100644
index 000000000..03502e87e
--- /dev/null
+++ b/src/components/Search/README.zh-CN.md
@@ -0,0 +1 @@
+# Search 搜索
diff --git a/src/components/Tag/README.zh-CN.md b/src/components/Tag/README.zh-CN.md
new file mode 100644
index 000000000..e2b417eed
--- /dev/null
+++ b/src/components/Tag/README.zh-CN.md
@@ -0,0 +1 @@
+# Tag 标签
diff --git a/tsconfig.json b/tsconfig.json
index c079083e9..90d6838cd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -34,6 +34,7 @@
"exclude": [
"node_modules",
"dist",
- "demo"
+ "demo",
+ "site"
]
}