├── .gitattributes
├── .gitignore
├── demo
├── poi.config.js
└── index.js
├── .prettierrc
├── babel.config.js
├── bili.config.ts
├── .editorconfig
├── src
├── utils
│ ├── useMedia.js
│ ├── createMarkdown.js
│ ├── findItem.js
│ └── markdownStyle.js
├── renderers
│ └── Heading.js
├── CodeBlock.js
├── Resizebar.js
├── Main.js
├── index.js
├── App.js
├── Panel.js
├── Board.js
└── Sidebar.js
├── circle.yml
├── LICENSE
├── package.json
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 |
--------------------------------------------------------------------------------
/demo/poi.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: 'demo/index.js'
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "bracketSpacing": true
5 | }
6 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['poi/babel', '@emotion/babel-preset-css-prop']
3 | }
4 |
--------------------------------------------------------------------------------
/bili.config.ts:
--------------------------------------------------------------------------------
1 | import { Config } from 'bili'
2 |
3 | const config: Config = {
4 | input: './src/index.js',
5 | output: {
6 | moduleName: 'demoboard',
7 | format: ['cjs', 'esm']
8 | }
9 | }
10 |
11 | export default config
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/src/utils/useMedia.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const useMedia = (query, defaultState = false) => {
4 | const [state, setState] = React.useState(defaultState)
5 |
6 | React.useEffect(() => {
7 | const mql = window.matchMedia(query)
8 | setState(mql.matches)
9 |
10 | const onChange = () => setState(mql.matches)
11 | mql.addListener(onChange)
12 |
13 | return () => mql.removeListener(onChange)
14 | }, [query])
15 |
16 | return [state, setState]
17 | }
18 |
--------------------------------------------------------------------------------
/src/renderers/Heading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import slugo from 'slugo'
3 |
4 | function flatten(text, child) {
5 | return typeof child === 'string'
6 | ? text + child
7 | : React.Children.toArray(child.props.children).reduce(flatten, text)
8 | }
9 |
10 | export const HeadingRenderer = props => {
11 | const children = React.Children.toArray(props.children)
12 | const text = children.reduce(flatten, '')
13 | const slug = slugo(text)
14 | return React.createElement(`h${props.level}`, { id: slug }, props.children)
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/createMarkdown.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactMarkdown from 'react-markdown'
3 | import { CodeBlock } from '../CodeBlock'
4 | import { HeadingRenderer } from '../renderers/Heading'
5 | import { markdownStyle } from './markdownStyle'
6 |
7 | export const createMarkdown = source => {
8 | return (
9 | <>
10 |
11 |
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:latest
6 | branches:
7 | ignore:
8 | - gh-pages # list of branches to ignore
9 | - /release\/.*/ # or ignore regexes
10 | steps:
11 | - checkout
12 | - restore_cache:
13 | key: dependency-cache-{{ checksum "yarn.lock" }}
14 | - run:
15 | name: install dependences
16 | command: yarn
17 | - save_cache:
18 | key: dependency-cache-{{ checksum "yarn.lock" }}
19 | paths:
20 | - ./node_modules
21 | - run:
22 | name: test
23 | command: yarn test
24 | - run:
25 | name: release
26 | command: npx semantic-release
27 |
--------------------------------------------------------------------------------
/src/utils/findItem.js:
--------------------------------------------------------------------------------
1 | export const findItems = (boards, query) => {
2 | let board
3 | let item
4 |
5 | // Find matched board
6 | for (let i = 0; i < boards.length; i++) {
7 | if (query.board && query.board === boards[i].title) {
8 | board = boards[i]
9 | } else if (!query.board) {
10 | board = boards[0]
11 | }
12 | }
13 |
14 | for (let i = 0; i < board.sections.length; i++) {
15 | const section = board.sections[i]
16 | if (section.title === query.section) {
17 | for (let i = 0; i < section.items.length; i++) {
18 | if (section.items[i].title === query.item) {
19 | item = section.items[i]
20 | break
21 | }
22 | }
23 | }
24 | }
25 |
26 | return item && board.applyDecorators(item)
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) EGOIST <0x142857@gmail.com> (https://github.com/egoist)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/CodeBlock.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from '@emotion/core'
3 | import Highlight, { defaultProps } from 'prism-react-renderer'
4 | import prismTheme from 'prism-react-renderer/themes/nightOwlLight'
5 |
6 | prismTheme.plain.backgroundColor = undefined
7 |
8 | export const CodeBlock = ({ value, language }) => {
9 | return (
10 |
16 | {({ className, style, tokens, getLineProps, getTokenProps }) => (
17 |
18 | {tokens.map((line, i) => (
19 |
20 | {line.map((token, key) => (
21 |
22 | ))}
23 |
24 | ))}
25 |
26 | )}
27 |
28 | )
29 | }
30 |
31 | const styles = {
32 | code: css`
33 | margin: 0;
34 | font-family: var(--font-code);
35 | white-space: pre-wrap;
36 | word-break: normal;
37 | font-size: 0.875rem;
38 | `
39 | }
40 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { create, mount } from '../src'
3 | // eslint-disable-next-line
4 | import readme from '!raw-loader!../README.md'
5 |
6 | const demoboard = create()
7 |
8 | demoboard.addDecorator(item => {
9 | const Component = item.options.component
10 | item.options.component = () => (
11 |
12 |
13 |
14 | )
15 | })
16 |
17 | demoboard
18 | .section('Buttons')
19 | .add('Primary Button', {
20 | component: () => ,
21 | code: ``
22 | })
23 | .add('Pink Button', {
24 | component: () => ,
25 | code: ``
26 | })
27 |
28 | demoboard.section('Lists').add('Unordered List', {
29 | component: () => (
30 |
31 | - First item
32 | - Second item
33 |
34 | ),
35 | code: `
36 | - First item
37 | - Second item
38 |
`,
39 | readme: `
40 | \`ul\` tag is used for unordered lists.
41 | `
42 | })
43 |
44 | demoboard.section('Other').add('Blockquote', {
45 | component: () => lorem
46 | })
47 |
48 | mount(demoboard, '#app', {
49 | readme
50 | })
51 |
--------------------------------------------------------------------------------
/src/Resizebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from '@emotion/core'
3 |
4 | export const Resizebar = ({ axis, rootSelector, bounds }) => {
5 | const checkPosition = e => {
6 | if (e.touches) e = e.touches[0]
7 |
8 | const barPosition = axis === 'x' ? e.pageX : window.innerHeight - e.pageY
9 |
10 | if (bounds && (bounds.min >= barPosition || bounds.max <= barPosition))
11 | return false
12 |
13 | document.documentElement.style.setProperty(rootSelector, barPosition + 'px')
14 | }
15 |
16 | const dragstart = () => {
17 | document.onselectstart = () => false
18 |
19 | window.addEventListener('mousemove', checkPosition)
20 | window.addEventListener('mouseup', () => {
21 | window.removeEventListener('mousemove', checkPosition)
22 | dragend()
23 | })
24 | window.addEventListener('touchmove', checkPosition)
25 | window.addEventListener('touchend', () => {
26 | window.removeEventListener('touchmove', checkPosition)
27 | dragend()
28 | })
29 | }
30 |
31 | const dragend = () => {
32 | document.onselectstart = () => true
33 | }
34 |
35 | return (
36 |
41 | )
42 | }
43 |
44 | const styles = {
45 | x: css`
46 | height: 100vh;
47 | width: 10px;
48 | position: absolute;
49 | bottom: 0;
50 | right: -5px;
51 | cursor: ew-resize;
52 | `,
53 | y: css`
54 | height: 10px;
55 | width: 100%;
56 | position: absolute;
57 | top: -5px;
58 | left: 0;
59 | cursor: ns-resize;
60 | `
61 | }
62 |
--------------------------------------------------------------------------------
/src/utils/markdownStyle.js:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/core'
2 |
3 | export const markdownStyle = css`
4 | line-height: 1.5;
5 |
6 | & > *:last-child {
7 | margin-bottom: 0;
8 | }
9 |
10 | & a {
11 | color: #0366d6;
12 | text-decoration: none;
13 | &:hover {
14 | text-decoration: underline;
15 | }
16 | }
17 |
18 | & pre {
19 | background-color: #f6f8fa;
20 | border-radius: 3px;
21 | padding: 16px;
22 | font-family: var(--font-code);
23 | margin: 15px 0;
24 | }
25 |
26 | & *:not(pre) > code {
27 | background-color: #f6f8fa;
28 | border-radius: 3px;
29 | padding: 3px 5px;
30 | font-size: 0.875rem;
31 | font-family: var(--font-code);
32 | }
33 |
34 | & h1,
35 | & h2,
36 | & h3,
37 | & h4,
38 | & h5 {
39 | font-weight: 500;
40 | }
41 |
42 | & h1 {
43 | font-size: 32px;
44 | border-bottom: 1px solid var(--border-color);
45 | padding-bottom: 0.3em;
46 | margin-bottom: 20px;
47 | }
48 |
49 | & h2 {
50 | font-size: 24px;
51 | border-bottom: 1px solid var(--border-color);
52 | padding-bottom: 0.3em;
53 | }
54 |
55 | & h3 {
56 | font-size: 20px;
57 | }
58 |
59 | & h4 {
60 | font-size: 16px;
61 | }
62 |
63 | & h5 {
64 | font-size: 14px;
65 | }
66 |
67 | & h5,
68 | & h6 {
69 | font-weight: 600;
70 | }
71 |
72 | & h6 {
73 | font-size: 12px;
74 | }
75 |
76 | & p {
77 | margin: 15px 0;
78 | }
79 |
80 | & ul,
81 | & ol {
82 | padding-left: 20px;
83 | }
84 |
85 | & blockquote {
86 | border-left: 0.25em solid #dfe2e5;
87 | color: #6a737d;
88 | padding: 0 1em;
89 | margin: 15px 0;
90 | }
91 | `
92 |
--------------------------------------------------------------------------------
/src/Main.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from '@emotion/core'
3 | import { Panel } from './Panel'
4 | import { createMarkdown } from './utils/createMarkdown'
5 |
6 | export const Main = props => {
7 | const { readme, showMenu, currentItem } = props
8 |
9 | // Let's show stuff when `showMenu` is set
10 | // To prevent from content flashing
11 | if (showMenu === null) {
12 | return null
13 | }
14 |
15 | if (currentItem) {
16 | const panel = {
17 | code: currentItem.options.code,
18 | readme: currentItem.options.readme
19 | }
20 | const hasPanel = panel.code || panel.readme
21 | return (
22 |
23 |
24 |
25 |
26 | {hasPanel &&
}
27 |
28 | )
29 | }
30 |
31 | return (
32 |
33 | {readme &&
{createMarkdown(readme)}
}
34 |
35 | )
36 | }
37 |
38 | const styles = {
39 | main: css`
40 | position: absolute;
41 | top: var(--header-height);
42 | left: 0;
43 | bottom: 0;
44 | width: 100%;
45 | overflow-x: hidden;
46 | @media (min-width: 992px) {
47 | padding-left: var(--sidebar-width);
48 | top: 0;
49 | }
50 | `,
51 | component: ({ hasPanel }) => css`
52 | padding: 10px;
53 | height: ${hasPanel ? `calc(100% - var(--panel-height))` : `100%`};
54 | position: absolute;
55 | top: 0;
56 | left: 0;
57 | right: 0;
58 | overflow: auto;
59 |
60 | @media (min-width: 992px) {
61 | padding-left: calc(var(--sidebar-width) + 10px);
62 | }
63 | `,
64 | readme: css`
65 | padding: 10px;
66 | max-width: 800px;
67 |
68 | @media (min-width: 992px) {
69 | padding: 10px 20px;
70 | }
71 | `
72 | }
73 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { BrowserRouter as Router, Route } from 'react-router-dom'
4 | import { App } from './App'
5 | import { createMarkdown } from './utils/createMarkdown'
6 |
7 | const uid = () =>
8 | Math.random()
9 | .toString(36)
10 | .substring(7)
11 |
12 | class Demoboard {
13 | constructor() {
14 | this.id = uid()
15 | this.sections = []
16 | this.decorators = []
17 | }
18 |
19 | /**
20 | * Create a section in this demoboard
21 | */
22 | section(title, options = {}) {
23 | const section = new Section(title, options)
24 | this.sections.push(section)
25 | return section
26 | }
27 |
28 | addDecorator(decorator) {
29 | this.decorators.push(decorator)
30 | return this
31 | }
32 |
33 | applyDecorators(item) {
34 | if (this.decorators.length === 0) {
35 | return item
36 | }
37 |
38 | const newItem = { ...item, options: { ...item.options } }
39 | this.decorators.forEach(deco => deco(newItem, this))
40 | return newItem
41 | }
42 | }
43 |
44 | class Section {
45 | constructor(title, options) {
46 | this.title = title
47 | this.options = options
48 | this.items = []
49 | this.id = uid()
50 | }
51 |
52 | /**
53 | * Add a demo to this section
54 | */
55 | add(title, options = {}) {
56 | this.items.push({
57 | title,
58 | options,
59 | id: uid()
60 | })
61 | return this
62 | }
63 | }
64 |
65 | function create(opts) {
66 | return new Demoboard(opts)
67 | }
68 |
69 | function mount(_boards, target, options) {
70 | const boards = [].concat(_boards)
71 |
72 | render(
73 |
74 | (
77 |
78 | )}
79 | />
80 | ,
81 | typeof target === 'string' ? document.querySelector(target) : target
82 | )
83 | }
84 |
85 | export { create, mount }
86 | export { CodeBlock } from './CodeBlock'
87 |
88 | export const Markdown = ({ source }) => createMarkdown(source)
89 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import querystring from 'querystring'
2 | import React from 'react'
3 | import { Global, css } from '@emotion/core'
4 | import { withRouter } from 'react-router'
5 | import { Sidebar } from './Sidebar'
6 | import { Main } from './Main'
7 | import { useMedia } from './utils/useMedia'
8 | import { findItems } from './utils/findItem'
9 |
10 | export const App = withRouter(({ boards, options, location }) => {
11 | // Show sidebar menu when the view port is at least 992px (tablet-landscape) wide
12 | const [isWide] = useMedia('(min-width:992px)', null)
13 | const [showMenu, setShowMenu] = useMedia('(min-width:992px)', null)
14 | const query = querystring.parse(location.search.slice(1))
15 | const currentItem = findItems(boards, query)
16 |
17 | const { title = 'Demoboard' } = options
18 |
19 | const pageTitle = currentItem ? `${currentItem.title} - ${title}` : title
20 | React.useEffect(() => {
21 | document.title = pageTitle
22 | }, [pageTitle])
23 |
24 | return (
25 |
26 |
58 |
65 |
71 |
72 | )
73 | })
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demoboard",
3 | "version": "0.3.1",
4 | "description": "Demonstrating your React/Vue components with ease.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.esm.js",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "test": "npm run lint",
12 | "lint": "xo",
13 | "demo": "poi --config demo/poi.config.js -s",
14 | "build:demo": "poi --config demo/poi.config.js --prod",
15 | "toc": "markdown-toc -i README.md",
16 | "build": "bili",
17 | "prepublishOnly": "npm run build"
18 | },
19 | "repository": {
20 | "url": "egoist/demoboard",
21 | "type": "git"
22 | },
23 | "author": "egoist<0x142857@gmail.com>",
24 | "license": "MIT",
25 | "dependencies": {
26 | "@emotion/core": "^10.0.14",
27 | "prism-react-renderer": "^0.1.7",
28 | "react": "^16.8.6",
29 | "react-dom": "^16.8.6",
30 | "react-markdown": "^4.1.0",
31 | "react-router": "^5.0.1",
32 | "react-router-dom": "^5.0.1",
33 | "slugo": "^0.2.3"
34 | },
35 | "devDependencies": {
36 | "@emotion/babel-preset-css-prop": "^10.0.14",
37 | "bili": "^4.8.0",
38 | "eslint-config-prettier": "^6.0.0",
39 | "eslint-config-rem": "^4.0.0",
40 | "eslint-plugin-prettier": "^3.0.0",
41 | "eslint-plugin-react": "^7.14.2",
42 | "husky": "^3.0.0",
43 | "lint-staged": "^9.1.0",
44 | "markdown-toc": "^1.2.0",
45 | "poi": "^12.7.1-canary.585.f015dbf.0",
46 | "prettier": "^1.15.2",
47 | "raw-loader": "^3.0.0",
48 | "rollup-plugin-vue": "^5.0.1",
49 | "vue-template-compiler": "^2.6.10",
50 | "xo": "^0.24.0"
51 | },
52 | "xo": {
53 | "extends": [
54 | "rem",
55 | "plugin:react/recommended",
56 | "plugin:prettier/recommended"
57 | ],
58 | "envs": [
59 | "browser"
60 | ],
61 | "rules": {
62 | "unicorn/filename-case": "off",
63 | "react/prop-types": "off",
64 | "react/display-name": "off",
65 | "import/no-unassigned-import": "off",
66 | "import/no-webpack-loader-syntax": "off",
67 | "unicorn/no-abusive-eslint-disable": "off"
68 | }
69 | },
70 | "husky": {
71 | "hooks": {
72 | "pre-commit": "lint-staged"
73 | }
74 | },
75 | "lint-staged": {
76 | "*.js": [
77 | "xo --fix",
78 | "git add"
79 | ],
80 | "*.{json,md}": [
81 | "prettier --write",
82 | "git add"
83 | ],
84 | "README.md": [
85 | "markdown-toc -i",
86 | "git add"
87 | ]
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Panel.js:
--------------------------------------------------------------------------------
1 | import querystring from 'querystring'
2 | import React from 'react'
3 | import { css } from '@emotion/core'
4 | import { withRouter } from 'react-router'
5 | import { Link } from 'react-router-dom'
6 | import { Resizebar } from './Resizebar'
7 | import { CodeBlock } from './CodeBlock'
8 | import { createMarkdown } from './utils/createMarkdown'
9 |
10 | export const Panel = withRouter(({ panel, location }) => {
11 | const query = querystring.parse(location.search.slice(1))
12 | const tabs = [
13 | panel.readme && {
14 | name: 'readme',
15 | displayName: 'Readme',
16 | content: () => createMarkdown(panel.readme)
17 | },
18 | panel.code && {
19 | name: 'code',
20 | displayName: 'Code',
21 | content: () =>
22 | }
23 | ]
24 | .filter(Boolean)
25 | .map((tab, index) => {
26 | if (query.tab === tab.name) {
27 | tab.active = true
28 | } else if (!query.tab && index === 0) {
29 | tab.active = true
30 | }
31 |
32 | return tab
33 | })
34 |
35 | const resizeBar = (
36 |
41 | )
42 |
43 | return (
44 |
45 | {resizeBar}
46 |
47 | {tabs.map(tab => {
48 | const href = `?${querystring.stringify({ ...query, tab: tab.name })}`
49 | return (
50 |
55 | {tab.displayName}
56 |
57 | )
58 | })}
59 |
60 | {tabs.map(
61 | tab =>
62 | tab.active && (
63 |
64 |
65 |
66 | )
67 | )}
68 |
69 | )
70 | })
71 |
72 | const styles = {
73 | panel: css`
74 | position: absolute;
75 | height: var(--panel-height);
76 | bottom: 0;
77 | left: 0;
78 | width: 100%;
79 | border-top: 1px solid var(--border-color);
80 |
81 | @media (min-width: 992px) {
82 | padding-left: var(--sidebar-width);
83 | }
84 | `,
85 | tabHeader: css`
86 | height: 30px;
87 | display: flex;
88 | align-items: center;
89 | border-bottom: 1px solid var(--border-color);
90 | `,
91 | tabTitle: css`
92 | color: #999;
93 | text-decoration: none;
94 | padding: 0 10px;
95 | display: flex;
96 | height: 100%;
97 | align-items: center;
98 | &:hover {
99 | color: #555;
100 | }
101 | `,
102 | tabTitleActive: css`
103 | color: var(--theme-color) !important;
104 | box-shadow: inset 0 -1px 0 0 var(--theme-color);
105 | background-color: var(--panel-title-bg);
106 | `,
107 | content: css`
108 | overflow: auto;
109 | padding: 10px;
110 | height: calc(100% - 30px);
111 | `
112 | }
113 |
--------------------------------------------------------------------------------
/src/Board.js:
--------------------------------------------------------------------------------
1 | import querystring from 'querystring'
2 | import React from 'react'
3 | import { withRouter } from 'react-router'
4 | import { Link } from 'react-router-dom'
5 | import { css } from '@emotion/core'
6 |
7 | export const Board = withRouter(({ board, location, keyword, hideMenu }) => {
8 | const query = querystring.parse(location.search.slice(1))
9 | const [closedSections, setClosedSections] = React.useState([])
10 | const toggleSection = id => {
11 | const index = closedSections.indexOf(id)
12 | if (index === -1) {
13 | setClosedSections([...closedSections, id])
14 | } else {
15 | setClosedSections(closedSections.filter(sectionId => sectionId !== id))
16 | }
17 | }
18 |
19 | const checkKeyword = title => {
20 | if (!keyword) {
21 | return true
22 | }
23 |
24 | return title.toLowerCase().indexOf(keyword) > -1
25 | }
26 |
27 | return (
28 |
29 | {board.sections.map(section => {
30 | return (
31 |
32 |
toggleSection(section.id)}
35 | >
36 | {section.title}
37 |
38 | {closedSections.indexOf(section.id) === -1 &&
39 | section.items.map(item => {
40 | const currentHref = querystring.stringify({
41 | section: query.section,
42 | item: query.item
43 | })
44 | const href = querystring.stringify({
45 | section: section.title,
46 | item: item.title
47 | })
48 | return (
49 | checkKeyword(item.title) && (
50 |
59 | {item.title}
60 |
61 | )
62 | )
63 | })}
64 |
65 | )
66 | })}
67 |
68 | )
69 | })
70 |
71 | const styles = {
72 | board: css`
73 | font-size: 0.9rem;
74 | &:not(:last-child) {
75 | border-bottom: 1px solid #e2e2e2;
76 | padding-bottom: 30px;
77 | margin-bottom: 30px;
78 | }
79 | `,
80 | sectionTitle: css`
81 | padding: 5px 10px;
82 | font-size: 0.9rem;
83 | display: flex;
84 | align-items: center;
85 | cursor: pointer;
86 | user-select: none;
87 | text-decoration: none;
88 | color: #999;
89 | font-weight: 500;
90 |
91 | & svg {
92 | width: 1em;
93 | height: 1em;
94 | fill: #999;
95 | margin-right: 2px;
96 | }
97 |
98 | &:hover {
99 | color: inherit;
100 | }
101 | `,
102 | itemTitle: css`
103 | display: flex;
104 | padding: 5px 10px 5px 20px;
105 | text-decoration: none;
106 | font-size: 0.875rem;
107 | color: inherit;
108 |
109 | &:hover {
110 | color: var(--menu-item-active-color);
111 | }
112 | `,
113 | itemTitleActive: css`
114 | color: var(--menu-item-active-color) !important;
115 | &:hover {
116 | text-decoration: underline !important;
117 | }
118 | `
119 | }
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Demoboard
2 |
3 | [](https://npmjs.com/package/demoboard) [](https://npmjs.com/package/demoboard) [](https://circleci.com/gh/egoist/demoboard/tree/master) [](https://patreon.com/egoist) [](https://chat.egoist.moe)
4 |
5 | ## Table of Contents
6 |
7 |
8 |
9 | - [Install](#install)
10 | - [Basic Usage](#basic-usage)
11 | - [Guide](#guide)
12 | - [React Components](#react-components)
13 | - [Vue Components](#vue-components)
14 | - [Customize Homepage](#customize-homepage)
15 | - [Customize Site Title](#customize-site-title)
16 | - [Display Source Code](#display-source-code)
17 | - [Display README for Each Demo](#display-readme-for-each-demo)
18 | - [Contributing](#contributing)
19 | - [Author](#author)
20 |
21 |
22 |
23 | ## Install
24 |
25 | ```bash
26 | yarn add demoboard
27 | ```
28 |
29 | ## Basic Usage
30 |
31 | [](https://codesandbox.io/s/demoboard-example-eplue?fontsize=14)
32 |
33 | ```js
34 | import React from 'react'
35 | import { create, mount } from 'demoboard'
36 |
37 | // A component that you want to demonstrate
38 | import Button from './Button'
39 |
40 | // Create a demoboard instance
41 | const demoboard = create()
42 |
43 | // Add a section to demonstrate the `Button` component
44 | demoboard
45 | .section('Buttons')
46 | .add('Primary Button', {
47 | // `component` should be a React component
48 | component: () =>
49 | })
50 | .add('Success Button', {
51 | component: () =>
52 | })
53 |
54 | // Mount the demoboard to given selector
55 | mount(demoboard, '#app')
56 | ```
57 |
58 | ## Guide
59 |
60 | ### React Components
61 |
62 | It just works™.
63 |
64 | ### Vue Components
65 |
66 | Just convert your Vue component into React component with [@egoist/vue-to-react](https://github.com/egoist/vue-to-react):
67 |
68 | ```js
69 | import toReact from '@egoist/vue-to-react'
70 | import Button from './Button.vue'
71 |
72 | demoboard.addDecorator(item => {
73 | const Component = item.options.component
74 | item.options.component = toReact(Component)
75 | })
76 |
77 | demoboard.section('Buttons').add('Primary Button', {
78 | component: Button
79 | })
80 | ```
81 |
82 | ### Customize Homepage
83 |
84 | ```js
85 | import { create, mount } from 'demoboard'
86 |
87 | const demoboard = create()
88 |
89 | const readme = `Hello
`
90 | // Or
91 | // const readme = () => Hello
92 |
93 | mount(demoboard, '#app', {
94 | readme
95 | })
96 | ```
97 |
98 | `readme` should be a Markdown string.
99 |
100 | ### Customize Site Title
101 |
102 | ```js
103 | import { create, mount } from 'demoboard'
104 |
105 | const demoboard = create()
106 |
107 | mount(demoboard, '#app', {
108 | title: 'My Demo'
109 | })
110 | ```
111 |
112 | Then `title` defaults to `Demoboard`.
113 |
114 | ### Display Source Code
115 |
116 | ```js
117 | demoboard.section('Buttons')
118 | .add('Primary Button', {
119 | component: () => ,
120 | code: `,`
121 | // Optional, default to `js`
122 | codeLang: 'js'
123 | })
124 | ```
125 |
126 | ### Display README for Each Demo
127 |
128 | ```js
129 | demoboard.section('Buttons').add('Danger Button', {
130 | component: () => ,
131 | readme: `Used to trigger a dangerous operation.`
132 | })
133 | ```
134 |
135 | `readme` should be a Markdown string.
136 |
137 | ## Contributing
138 |
139 | 1. Fork it!
140 | 2. Create your feature branch: `git checkout -b my-new-feature`
141 | 3. Commit your changes: `git commit -am 'Add some feature'`
142 | 4. Push to the branch: `git push origin my-new-feature`
143 | 5. Submit a pull request :D
144 |
145 | ## Author
146 |
147 | **demoboard** © [EGOIST](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
148 | Authored and maintained by EGOIST with help from contributors ([list](https://github.com/egoist/demoboard/contributors)).
149 |
150 | > [github.com/egoist](https://github.com/egoist) · GitHub [@EGOIST](https://github.com/egoist) · Twitter [@\_egoistlily](https://twitter.com/_egoistlily)
151 |
--------------------------------------------------------------------------------
/src/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from '@emotion/core'
3 | import { Link } from 'react-router-dom'
4 | import { Board } from './Board'
5 | import { Resizebar } from './Resizebar'
6 |
7 | export const Sidebar = ({ title, boards, showMenu, setShowMenu, isWide }) => {
8 | if (showMenu === null) {
9 | return null
10 | }
11 |
12 | const [keyword, setKeyword] = React.useState('')
13 |
14 | const toggleMenu = () => {
15 | setShowMenu(!showMenu)
16 | }
17 |
18 | const handleSearch = e => {
19 | setKeyword(e.target.value)
20 | }
21 |
22 | const hideMenu = () => !isWide && setShowMenu(false)
23 |
24 | const Boards = boards.map((board, index) => (
25 |
26 | ))
27 |
28 | const menuIcon = isWide ? null : showMenu ? (
29 |
42 | ) : (
43 |
56 | )
57 |
58 | const resizeBar = isWide ? (
59 |
64 | ) : (
65 |
70 | )
71 |
72 | return (
73 |
74 |
75 |
76 | {menuIcon}
77 |
78 |
79 | {title}
80 |
81 |
82 |
83 | {showMenu && (
84 |
85 | {resizeBar}
86 |
87 |
93 |
94 | {Boards}
95 |
96 | )}
97 |
98 | )
99 | }
100 |
101 | const styles = {
102 | sidebar: css`
103 | background: var(--sidebar-bg);
104 | `,
105 | header: css`
106 | height: var(--header-height);
107 | border-bottom: 1px solid var(--border-color);
108 | display: flex;
109 | align-items: center;
110 | padding: 0 10px;
111 | position: fixed;
112 | top: 0;
113 | left: 0;
114 | width: 100%;
115 | background: var(--sidebar-bg);
116 | border-right: 1px solid var(--border-color);
117 | z-index: 1000;
118 | @media (min-width: 992px) {
119 | width: var(--sidebar-width);
120 | }
121 | `,
122 | menu: css`
123 | position: fixed;
124 | top: var(--header-height);
125 | left: 0;
126 | bottom: 0;
127 | width: var(--sidebar-width);
128 | background: var(--sidebar-bg);
129 | border-right: 1px solid var(--border-color);
130 | z-index: 1000;
131 | `,
132 | headerLeft: css`
133 | display: flex;
134 | align-items: center;
135 | user-select: none;
136 | & svg {
137 | width: 1em;
138 | height: 1em;
139 | margin-right: 5px;
140 | color: #9a9898;
141 | cursor: pointer;
142 | transition: color 0.3s ease;
143 | &:hover {
144 | color: inherit;
145 | }
146 | }
147 | `,
148 | siteTitle: css`
149 | font-weight: 500;
150 | font-size: 1.1rem;
151 | & a {
152 | color: inherit;
153 | text-decoration: none;
154 | }
155 | `,
156 | searchWrapper: css`
157 | padding: 10px;
158 | `,
159 | search: css`
160 | border: 1px solid var(--border-color);
161 | border-radius: 3px;
162 | width: 100%;
163 | padding: 8px 10px;
164 | font-size: 0.9rem;
165 | `
166 | }
167 |
--------------------------------------------------------------------------------