├── .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 | 34 | ), 35 | code: ``, 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 | [![NPM version](https://badgen.net/npm/v/demoboard)](https://npmjs.com/package/demoboard) [![NPM downloads](https://badgen.net/npm/dm/demoboard)](https://npmjs.com/package/demoboard) [![CircleCI](https://badgen.net/circleci/github/egoist/demoboard/master)](https://circleci.com/gh/egoist/demoboard/tree/master) [![donate](https://badgen.net/badge/support%20me/donate/ff69b4)](https://patreon.com/egoist) [![chat](https://badgen.net/badge/chat%20on/discord/7289DA)](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 | [![Edit Demoboard Example](https://codesandbox.io/static/img/play-codesandbox.svg)](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 | --------------------------------------------------------------------------------