├── .editorconfig ├── .eslintrc ├── .github └── workflows │ └── tests.js.yml ├── .gitignore ├── .lintstagedrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── PLAN.md ├── README.md ├── case.js ├── components ├── button.js ├── case.js ├── checkbox.js ├── error.js ├── events.js ├── go-to-top.js ├── header.js ├── index.js ├── loader.js ├── notice.js ├── page-select.js └── select.js ├── controllers ├── frame.js ├── go-to-top.js ├── index.js └── wrapper.js ├── docs ├── advanced-controller.gif ├── configure.md ├── controller.md ├── events.png ├── examples.md ├── install.md ├── install_craco.md ├── nesting.png ├── text-edit-mode.gif ├── troubleshooting.md └── uibook.gif ├── lib ├── combine-objects.js ├── fix-click.js ├── stringify.js └── throttler.js ├── package.json ├── plugin.js ├── src ├── template.html └── uibook.css ├── starter.js └── yarn.lock /.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 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@logux/eslint-config", 3 | "plugins": [ 4 | "prettierx" 5 | ], 6 | "rules": { 7 | "security/detect-possible-timing-attacks": "off", 8 | "unicorn/prefer-optional-catch-binding": "off", 9 | "node/no-missing-require": ["error", { 10 | "allowModules": ["webpack"] 11 | }], 12 | "unicorn/prefer-includes": "off", 13 | "prefer-let/prefer-let": "off", 14 | "prefer-arrow-callback": "off", 15 | "prefer-rest-params": "off", 16 | "multiline-ternary": [ 17 | "error", 18 | "always-multiline" 19 | ], 20 | "prettierx/options": "off", 21 | "object-shorthand": "off", 22 | "func-style": ["error", "expression"], 23 | "no-console": "error", 24 | "no-var": "off" 25 | }, 26 | "env": { 27 | "browser": true, 28 | "es6": false 29 | }, 30 | "overrides": [ 31 | { 32 | "files": [ "plugin.js" ], 33 | "rules": { 34 | "es5/no-block-scoping": "off", 35 | "es5/no-es6-methods": "off", 36 | "es5/no-classes": "off", 37 | "prefer-const": "off" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/tests.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, and run tests 2 | 3 | name: Tests 4 | 5 | on: 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | test: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [15.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: yarn 27 | - run: yarn test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __MACOSX/* 3 | .DS_Store 4 | .vscode 5 | *.log 6 | .env 7 | *~ 8 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": "eslint" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | node_modules/ 3 | 4 | *.log 5 | yarn.lock 6 | .gitignore 7 | .npmignore 8 | .travis.yml 9 | npm-debug.log 10 | .editorconfig 11 | .vscode 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - node 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | This project adheres to [Semantic Versioning](http://semver.org/). 3 | 4 | ## 0.6.1 5 | * Fixed link in deprecation warning 6 | 7 | ## 0.6.0 8 | * `name` in Case is deprecated. It was the same as the key in Uibook Controller, 9 | so it was unnecessary duplication. You should set Page Name in 10 | the Uibook Controller now. 11 | * Fixed overlapping of a Case content and the header bar 12 | 13 | ## 0.5.8 14 | * Added support of Cyrillic component names 15 | * Fixed converting of CamelCase to space separated words in Page Selector 16 | * Fixed unchangable Select 17 | 18 | ## 0.5.7 19 | * Added "Go to Top" button 20 | * Sticky header by default 21 | * `white` background is deprecated. Please use `light` instead 22 | * Fixed support of entrypoints as objects 23 | (@helsi-pro https://github.com/vrizo/uibook/pull/9) 24 | 25 | ## 0.5.6 26 | * Fixed HMR module loading if it is disabled in `devServer` 27 | 28 | ## 0.5.5 29 | * Added Hot Reload feature 30 | * Update dependencies 31 | * Added pre-commit tests 32 | * Use the first locale if it is absent in URL hash 33 | 34 | ## 0.5.4 35 | * Update dependencies 36 | * Fixed entrypoints iteration if `compilation.entrypoints` is object 37 | 38 | ## 0.5.3 39 | * Updated dependencies to resolve security vulnerabilities 40 | * Fixed entrypoints iteration 41 | 42 | ## 0.5.2 43 | * Fixed files importing in HTML 44 | * Added white page troubleshooting in Create React App 45 | 46 | ## 0.5.1 47 | * Fixed events toast z-index 48 | * Fixed HtmlWebpackPlugin detector 49 | * Fixed strinfying of ReactComponents/ReactElements 50 | * Added delay before scrollTo() after Page switching 51 | * Fixed blur() after click by mouse on Checkbox and Select 52 | 53 | ## 0.5 54 | * Initial release. 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vitalii Rizo 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PLAN.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - add docs how to add to Create React App 4 | - fix synthetic reuse error 5 | - stylize code examples 6 | - create a landing page with pros, docs and videos 7 | - stringify JSX mode (create examples in JSX style, not a hyperscript) 8 | - generate correct examples for Immutable in lib/stringify 9 | - add tests 10 | - write a hack for `create-react-app`, because it doesn’t allow to modify 11 | webpack config without ejection 12 | - add PropTypes 13 | - add complex example in docs like `desktop-popups.uibook.js` in Amplifr 14 | - custom props are stringified in iframe (integer becomes string). 15 | Pass prop type to iframe? 16 | 17 | ### Archive 18 | 19 | + исправить null в хеше 20 | + открывать первую страницу при запуске 21 | + исправить ошибки из-за key в консоли 22 | + inline стили передедать на css по бэму 23 | + добавить hover/focus на ссылку 24 | + добавить fixClick 25 | + добавить THEMES 26 | + добавить UibookEvents 27 | + выбирать в селекте компонент из хеша 28 | + приделать стрелочки в Header 29 | + группировка в списке 30 | + вынести stringify() в lib 31 | + перенести uibook в подпапку /uibook/ 32 | + добавить custom title страницы (в опициях `title`) 33 | + переименовать uikit в uibook 34 | + инициализировать package.json 35 | + прикрутить линтер 36 | + реализовать iframe 37 | + return '/uibook/?page=' + page + '&case=' + index + '&locale=' + locale 38 | + '&iframe=true' 39 | сюда подставлять пользовательский адрес 40 | + создать репозиторий 41 | + Object.assign не поддерживается в IE, заменить на combineObjects() 42 | + конфигурируемый output path (с фильтрацией слэшей) 43 | + добавить индикацию загрузки iframe 44 | + добавить возможность добавлять свой стор, провайдер и т.п. (Wrapper) 45 | + добавить возможность переключать пользовательские параметры в контексте 46 | + проброс текущей локали в контекст 47 | + больше локалей из конфига (ru/en) 48 | + дизайн верхней полоски 49 | + no pages view 50 | + no cases view 51 | + режим редактирования текста 52 | + добавить проверку, удалось ли найти main в iframe. Если нет, 53 | то рендерить ошибку 54 | + избавиться от пропа text, переделать на ребенка 55 | + переделать мобильные кейсы, чтоб всё в едином стиле было 56 | + проверить случай, когда компонент — функция, но нет локали 57 | (var component = i(this.state.locale)) 58 | + написать документацию 59 | + отключать горячие клавиши, когда включен режим редактирования текста 60 | + обработка случая мобильного вида без `body` (обрабатывается ошибкой айфрема) 61 | + добавить сообщение, что нужно сделать excludeChunks 62 | + зафиксировать ширину PageSelect 63 | + перенести Амплифер на плагин, проверить в ИЕ 64 | + исправить stringify of null 65 | + проверить, что еще случайно из ES6 (легко — yarn build) 66 | + предотвращать проскролливание до autofocus инпутов при смене страниц 67 | + еще подумать над page: null при загрузке 68 | + заменять `\n` на `
` в example 69 | + Node.js script to create structure 70 | + разместить ссылки на документацию в ошибках 71 | + add lint-staged 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sponsored by Amplifr 5 | 6 | 7 | 8 | # Uibook 9 | 10 | Uibook is a tool for visual testing of React components. It let you quickly 11 | check both desktop and mobile view of your components with real media queries 12 | and different combinations of props. 13 | 14 | The Key Features: 15 | - Responsive Testing for developers to play with **real media queries** 16 | - Live Text Editing for designers and editors to check their content 17 | - Installed as a Webpack Plugin, it doesn’t require any additional builder. 18 | 19 | Uibook key features 20 | 21 | :triangular_flag_on_post: Check amazing Uibook example here: 22 | [https://amplifr.com/uikit](https://amplifr.com/uikit). 23 | 24 | ## Usage 25 | 26 | ### Quick Install :hatching_chick: 27 | 28 | We’ve designed Uibook for seamless integration to your project. 29 | Install it as a webpack plugin, and you’re all set: 30 | Uibook doesn’t require any separate bundler. 31 | 32 | _webpack.config.js_ 33 | ```js 34 | const UibookPlugin = require('uibook/plugin') 35 | 36 | module.exports = { 37 | … 38 | plugins: [ 39 | new UibookPlugin({ 40 | outputPath: '/uibook', 41 | controller: path.join(__dirname, '../src/uibook-controller.js'), 42 | hot: true 43 | }) 44 | ], 45 | } 46 | ``` 47 | 48 | [Read more about installation →](docs/install.md) 49 | [Install in Create React App using CRACO →](docs/install_craco.md) 50 | 51 | ### Describe components in Pages :hatched_chick: 52 | 53 | You should define two things only: 54 | 55 | 1. The Page — simple object with test component name and cases; 56 | 2. The Case — set of props and callbacks to the component. 57 | 58 | _button.uibook.js_ 59 | ```js 60 | import UibookCase from 'uibook/case' 61 | import Button from '../src/button' 62 | 63 | export default { 64 | component: Button, 65 | cases: [ 66 | () => Button, 67 | () => Small button 68 | ] 69 | } 70 | ``` 71 | 72 | [Read more about configuration →](docs/configure.md) 73 | 74 | ### Pass Pages to the Controller :baby_chick: 75 | 76 | As soon as you finished your first Uibook Page, you’re ready 77 | to write Uibook Controller. This is a place where all 78 | Pages included and passed to UibookStarter :sparkles: 79 | 80 | _uibook-controller.js_ 81 | ```js 82 | import UibookStarter from 'uibook/starter' 83 | import ButtonUibook from './button.uibook' 84 | 85 | export default UibookStarter({ 86 | pages: { 87 | Button: ButtonUibook 88 | } 89 | }) 90 | ``` 91 | 92 | [Read more about Controller →](docs/controller.md) 93 | 94 | ### Launch :rocket: 95 | 96 | There is no need in any additional servers or webpack instances. 97 | Uibook integrates into your project, so just run your bundler 98 | and open `/uibook` (or your custom address — `outputPath`) in a browser. 99 | 100 | ### More information 101 | 102 | - **[Troubleshooting](docs/troubleshooting.md)** 103 | - [Examples](docs/examples.md) 104 | - [Ask me](https://twitter.com/vitaliirizo) 105 | 106 | ## Live Text Editing 107 | 108 | This mode enables `contentEditable` for each case, allowing managers, 109 | designers and interface editors to preview content in components. 110 | 111 | Text Edit Mode 112 | 113 | ## Special thanks 114 | 115 | - [@ai](https://github.com/ai) 116 | - [@demiazz](https://github.com/demiazz) 117 | - [@marfitsin](https://github.com/marfitsin) 118 | - [@iadramelk](https://github.com/iadramelk) 119 | - [@ikowalsker](https://www.facebook.com/ikowalsker) 120 | - [@antiflasher](https://github.com/antiflasher) 121 | - [@HellSquirrel](https://github.com/HellSquirrel) 122 | 123 | Anyone is welcomed to contribute, you can check current tasks 124 | in [PLAN.md](PLAN.md). I would be glad to answer your questions 125 | about the project. 126 | -------------------------------------------------------------------------------- /case.js: -------------------------------------------------------------------------------- 1 | var UibookCase = require('./components/case') 2 | 3 | module.exports = UibookCase 4 | -------------------------------------------------------------------------------- /components/button.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class') 2 | var React = require('react') 3 | var h = React.createElement 4 | 5 | var fixClick = require('../lib/fix-click') 6 | 7 | var UibookButton = createReactClass({ 8 | click: function (e) { 9 | if (this.props.onClick) this.props.onClick(e) 10 | fixClick(e) 11 | }, 12 | 13 | render: function () { 14 | var atts = { 15 | className: 'uibook-button', 16 | disabled: !!this.props.disabled, 17 | onClick: this.click 18 | } 19 | 20 | if (this.props.isSecondary) atts.className += ' is-secondary' 21 | if (this.props.href) atts.href = this.props.href 22 | 23 | return h(this.props.href ? 'a' : 'button', atts, this.props.children) 24 | } 25 | }) 26 | 27 | module.exports = UibookButton 28 | -------------------------------------------------------------------------------- /components/case.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var stringify = require('../lib/stringify.js') 5 | 6 | var Case = function (props) { 7 | return h('section', { className: 'uibook-case' }, props.children) 8 | } 9 | 10 | var UibookCase = function (props) { 11 | var example = props.example 12 | var children = props.children 13 | var component = props.component 14 | var componentProps = props.props || {} 15 | var name = props.name 16 | var text = props.text 17 | componentProps.key = name 18 | 19 | if (typeof example === 'undefined' && name) { 20 | example = 'h(' + name 21 | if (componentProps) example += ', ' + stringify(componentProps) 22 | if (text) example += ', [' + stringify(text) + ']' 23 | example += ')' 24 | } else { 25 | example = example.split('\\n').map(function (item, index) { 26 | return h('span', { key: 'example' + index }, [ 27 | item, 28 | h('br', { key: 'br' + index }) 29 | ]) 30 | }) 31 | } 32 | 33 | if (!children && component) { 34 | children = h(component, componentProps, text) 35 | } else if (children && component) { 36 | children = h(component, componentProps, children) 37 | } 38 | 39 | return props.isFrame 40 | ? children 41 | : h(Case, null, [ 42 | example 43 | ? h('code', { 44 | className: 'uibook-code', 45 | key: 'example' 46 | }, example) 47 | : null, 48 | h('div', { 49 | suppressContentEditableWarning: true, 50 | contentEditable: props.isEditable, 51 | className: 'uibook-content', 52 | key: 'content' 53 | }, children) 54 | ]) 55 | } 56 | 57 | UibookCase.event = function (name) { 58 | return function () { 59 | var args = Array.prototype.slice.call(arguments) 60 | var event = new CustomEvent('track', { 61 | detail: { 62 | name: name, 63 | args: args 64 | } 65 | }) 66 | if (window.parent) { 67 | window.parent.document.body.dispatchEvent(event) 68 | } else { 69 | document.body.dispatchEvent(event) 70 | } 71 | } 72 | } 73 | 74 | module.exports = UibookCase 75 | -------------------------------------------------------------------------------- /components/checkbox.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class') 2 | var React = require('react') 3 | var h = React.createElement 4 | 5 | var UibookCheckbox = createReactClass({ 6 | spaceToggle: function (e) { 7 | if (e.keyCode === 32 && this.props.onChange) { 8 | e.preventDefault() 9 | this.props.onChange(e) 10 | } 11 | }, 12 | 13 | click: function (e) { 14 | if (e.screenX > 0 && e.target && e.target.parentNode) { 15 | e.target.parentNode.blur() 16 | } 17 | }, 18 | 19 | change: function (e) { 20 | if (this.props.onChange) this.props.onChange(e) 21 | }, 22 | 23 | render: function () { 24 | var atts = { 25 | className: 'uibook-checkbox', 26 | disabled: !!this.props.disabled, 27 | onChange: this.change, 28 | tabIndex: -1, 29 | checked: !!this.props.checked, 30 | onClick: this.click, 31 | type: 'checkbox', 32 | key: 'input' 33 | } 34 | 35 | return h('label', { 36 | className: 'uibook-checkbox__wrapper', 37 | onKeyDown: this.spaceToggle, 38 | tabIndex: 0 39 | }, [ 40 | h('input', atts), 41 | h('div', { className: 'uibook-checkbox__fake', key: 'fake' }, 42 | h('div', { className: 'uibook-checkbox__crop' }, 43 | h('svg', null, 44 | h('path', { 45 | d: 'M4.21 6.95L3 8.163l3.76 3.761 6.208-6.207L11.75 4.5l-4.99 5z' 46 | }) 47 | ) 48 | ) 49 | ), 50 | this.props.children 51 | ]) 52 | } 53 | }) 54 | 55 | module.exports = UibookCheckbox 56 | -------------------------------------------------------------------------------- /components/error.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var UibookButton = require('./button') 5 | 6 | var UibookError = function (props) { 7 | return h('div', { className: 'uibook-error' }, [ 8 | h('div', { className: 'uibook-error__icon', key: 'icon' }, '🙁'), 9 | h('div', { className: 'uibook-error__desc', key: 'desc' }, props.desc), 10 | h(UibookButton, { 11 | href: props.actionUrl, 12 | key: 'action' 13 | }, props.actionText) 14 | ]) 15 | } 16 | 17 | module.exports = UibookError 18 | -------------------------------------------------------------------------------- /components/events.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class') 2 | var React = require('react') 3 | var h = React.createElement 4 | 5 | var stringify = require('../lib/stringify.js') 6 | 7 | var UibookEvents = createReactClass({ 8 | format: function (args) { 9 | return args.map(function (i) { 10 | if (typeof i.isPropagationStopped !== 'undefined') { 11 | return 'Event' 12 | } else { 13 | return stringify(i) 14 | } 15 | }).join(', ') 16 | }, 17 | 18 | render: function () { 19 | return h('div', { className: 'uibook-event__position' }, [ 20 | this.props.events.map(function (i) { 21 | return h('div', { className: 'uibook-event', key: i.id }, [ 22 | h('strong', { 23 | className: 'uibook-event__name', 24 | key: 'name' 25 | }, [i.name + ': ']), 26 | this.format(i.args) 27 | ]) 28 | }.bind(this)) 29 | ]) 30 | } 31 | }) 32 | 33 | module.exports = UibookEvents 34 | -------------------------------------------------------------------------------- /components/go-to-top.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var fixClick = require('../lib/fix-click') 5 | 6 | var t = { 7 | top: 'Go to top', 8 | return: 'Go back' 9 | } 10 | 11 | var UibookGoToTop = function (props) { 12 | var isVisible = props.isVisible 13 | var isReturn = props.isReturn 14 | var click = function (e) { 15 | props.onClick() 16 | fixClick(e) 17 | } 18 | 19 | return h('button', { 20 | className: 'uibook-go-to-top uibook-button is-secondary', 21 | onClick: click, 22 | title: isReturn ? t.return : t.top, 23 | style: { 24 | transform: isReturn ? 'rotate(180deg)' : 'rotate(0)', 25 | display: isVisible ? 'block' : 'none' 26 | } 27 | }, '↑') 28 | } 29 | 30 | module.exports = UibookGoToTop 31 | -------------------------------------------------------------------------------- /components/header.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var UibookPageSelect = require('./page-select') 5 | var UibookCheckbox = require('./checkbox') 6 | var UibookSelect = require('./select') 7 | var UibookButton = require('./button') 8 | 9 | var THEMES = { 10 | default: '#e6e6e6', 11 | white: '#f2f2f2', 12 | light: '#f2f2f2', 13 | dark: '#d2d2d2' 14 | } 15 | 16 | var t = { 17 | textEdit: 'Text Edit' 18 | } 19 | 20 | var Header = function (props) { 21 | var atts = { 22 | className: 'uibook-header', 23 | style: { background: THEMES[props.background] } 24 | } 25 | 26 | if (props.isFixed) atts.className += ' is-fixed' 27 | 28 | return h('header', atts, props.children) 29 | } 30 | 31 | var UibookHeader = function (props) { 32 | var selectors = [] 33 | 34 | if (props.values) { 35 | for (var key in props.values) { 36 | if (props.values[key]) { 37 | selectors.push({ 38 | key: key, 39 | values: props.values[key] 40 | }) 41 | } 42 | } 43 | } 44 | 45 | return h(Header, { 46 | background: props.background, 47 | isFixed: props.isFixed, 48 | key: 'header' 49 | }, [ 50 | h('nav', { className: 'uibook-nav', key: 'nav' }, [ 51 | h(UibookButton, { 52 | isSecondary: true, 53 | disabled: !props.page || props.isEditable, 54 | onClick: props.onPrevPage, 55 | key: '←' 56 | }, '←'), 57 | h(UibookPageSelect, { 58 | onPageChange: props.onPageChange, 59 | disabled: props.isEditable, 60 | pages: props.pages, 61 | page: props.page, 62 | key: 'select' 63 | }), 64 | h(UibookButton, { 65 | isSecondary: true, 66 | disabled: !props.page || props.isEditable, 67 | onClick: props.onNextPage, 68 | key: '→' 69 | }, '→') 70 | ]), 71 | selectors.length > 0 72 | ? selectors.map(function (selector) { 73 | return h(UibookSelect, { 74 | onChange: props.onValueChange, 75 | value: props.state[selector.key], 76 | key: selector.key, 77 | id: selector.key 78 | }, selector.values.map(function (value) { 79 | return h('option', { 80 | value: value, 81 | key: selector.key + value 82 | }, value) 83 | })) 84 | }) 85 | : null, 86 | h('div', { className: 'uibook-spacer', key: 'spacer' }), 87 | h('div', { className: 'uibook-editable', key: 'edit' }, 88 | h(UibookCheckbox, { 89 | onChange: props.onEditableSwitch, 90 | checked: props.isEditable 91 | }, t.textEdit) 92 | ) 93 | ]) 94 | } 95 | 96 | module.exports = UibookHeader 97 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var UibookGoToTop = require('../controllers/go-to-top') 5 | var UibookNotice = require('./notice') 6 | var UibookHeader = require('./header') 7 | var UibookEvents = require('./events') 8 | 9 | var THEMES = { 10 | default: '#f2f2f2', 11 | white: '#fff', 12 | light: '#fff', 13 | dark: '#ddd' 14 | } 15 | 16 | var Page = function (props) { 17 | return h('div', { 18 | className: 'uibook-page', 19 | style: { background: THEMES[props.background] } 20 | }, props.children) 21 | } 22 | 23 | var Uibook = function (props) { 24 | var atts = { 25 | className: 'uibook-container', 26 | key: 'body' 27 | } 28 | 29 | if (props.isFixedHeader) atts.className += ' is-fixed' 30 | if (props.isEditable) atts.className += ' is-editable' 31 | 32 | return h(Page, { background: props.background }, [ 33 | h('div', { className: 'uibook-top', key: 'top' }, 34 | h(UibookEvents, { events: props.events }) 35 | ), 36 | h(UibookGoToTop, { key: 'go-top' }), 37 | props.notice 38 | ? h(UibookNotice, { type: props.notice, key: 'notice' }) 39 | : null, 40 | h(UibookHeader, { 41 | onEditableSwitch: props.onEditableSwitch, 42 | onValueChange: props.onValueChange, 43 | onPageChange: props.onPageChange, 44 | background: props.background, 45 | isEditable: props.isEditable, 46 | onNextPage: props.onNextPage, 47 | onPrevPage: props.onPrevPage, 48 | isFixed: props.isFixedHeader, 49 | values: props.values, 50 | pages: props.pages, 51 | state: props.state, 52 | page: props.page, 53 | key: 'header' 54 | }), 55 | h('main', atts, props.children) 56 | ]) 57 | } 58 | 59 | module.exports = Uibook 60 | -------------------------------------------------------------------------------- /components/loader.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var Position = function (props) { 5 | return h('div', { className: 'uibook-loader__position' }, props.children) 6 | } 7 | 8 | var Loader = function () { 9 | return h('div', { className: 'uibook-loader' }) 10 | } 11 | 12 | var UibookLoader = function (props) { 13 | var isLoading = props.isLoading 14 | var children = props.children 15 | 16 | return h(Position, null, [ 17 | h('div', { 18 | style: { opacity: isLoading ? 0 : 1 }, 19 | key: 'content' 20 | }, children), 21 | isLoading ? h(Loader, { key: 'loader' }) : null 22 | ]) 23 | } 24 | 25 | module.exports = UibookLoader 26 | -------------------------------------------------------------------------------- /components/notice.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var DOCS_URL = 'https://github.com/vrizo/uibook/blob/master/docs/' 5 | 6 | var t = { 7 | hotUrl: 'troubleshooting.md#hot-reload-warning', 8 | hotText: 'Failed to activate hot reload', 9 | hotAction: 'Read more', 10 | chunkUrl: 'troubleshooting.md#exclude-chunk-warning', 11 | chunkText: 'Exclude Uibook chunk from HtmlWebpackPlugin', 12 | chunkAction: 'Read how' 13 | } 14 | 15 | var Uibook = function (props) { 16 | var type = props.type 17 | 18 | return h('a', { 19 | className: 'uibook-notice', 20 | href: DOCS_URL + t[type + 'Url'] 21 | }, [ 22 | h('div', { 23 | className: 'uibook-notice__text', 24 | key: 'message' 25 | }, t[type + 'Text']), 26 | h('div', { 27 | className: 'uibook-notice__action', 28 | key: 'link' 29 | }, t[type + 'Action']) 30 | ]) 31 | } 32 | 33 | module.exports = Uibook 34 | -------------------------------------------------------------------------------- /components/page-select.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class') 2 | var React = require('react') 3 | var h = React.createElement 4 | 5 | var UibookSelect = require('./select') 6 | 7 | var t = { 8 | noPages: 'No pages' 9 | } 10 | 11 | var unprepareName = function (name) { 12 | return name.replace(/ /g, '') 13 | } 14 | 15 | var prepareName = function (name) { 16 | return name.replace(/([a-zа-яёђѓєѕіїјљњћќўџґ])([A-ZЁЂЃЄЅІЇЈЉЊЋЌЎЏА-ЯҐ])/g, 17 | '$1 $2') 18 | } 19 | 20 | var UibookPageSelect = createReactClass({ 21 | change: function (e) { 22 | var name = unprepareName(e.target.value) 23 | 24 | this.props.onPageChange(name) 25 | e.target.blur() 26 | }, 27 | 28 | optionFromPage: function (page) { 29 | var preparedName = prepareName(page.name) 30 | 31 | return h('option', { key: page.name, value: preparedName }, preparedName) 32 | }, 33 | 34 | render: function () { 35 | var selectChildren = Object.keys(this.props.pages).map(function (i) { 36 | if (this.props.pages[i].name) { 37 | return this.optionFromPage(this.props.pages[i]) 38 | } 39 | 40 | return h('optgroup', { 41 | label: i, 42 | key: i 43 | }, [ 44 | Object.keys(this.props.pages[i]).map(function (j) { 45 | return this.optionFromPage(this.props.pages[i][j]) 46 | }.bind(this)) 47 | ]) 48 | }.bind(this)) 49 | 50 | return h(UibookSelect, { 51 | disabled: selectChildren.length === 0 || this.props.disabled, 52 | onChange: this.change, 53 | isAccent: true, 54 | value: prepareName(this.props.page || t.noPages), 55 | id: 'uibook-page-select' 56 | }, selectChildren) 57 | } 58 | }) 59 | 60 | module.exports = UibookPageSelect 61 | -------------------------------------------------------------------------------- /components/select.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var h = React.createElement 3 | 4 | var Wrapper = function (props) { 5 | var className = 'uibook-select__wrapper' 6 | if (props.isAccent) className += ' is-accent' 7 | if (props.disabled) className += ' is-disabled' 8 | 9 | return h('div', { className: className }, props.children) 10 | } 11 | 12 | var Label = function (props) { 13 | var className = 'uibook-select__label' 14 | if (props.isAccent) className += ' is-accent' 15 | 16 | return h('label', { 17 | className: className, 18 | htmlFor: props.htmlFor 19 | }, props.children) 20 | } 21 | 22 | var UibookSelect = function (props) { 23 | var change = function (event) { 24 | if (props.onChange) props.onChange(event) 25 | event.currentTarget.blur() 26 | } 27 | 28 | var atts = { 29 | className: 'uibook-select', 30 | disabled: props.disabled, 31 | onChange: change, 32 | value: props.value, 33 | key: props.id, 34 | id: props.id 35 | } 36 | 37 | return h(Wrapper, { isAccent: props.isAccent, disabled: props.disabled }, [ 38 | h('select', atts, props.children), 39 | h(Label, { 40 | isAccent: props.isAccent, 41 | htmlFor: props.id, 42 | key: 'label' + props.id 43 | }, props.value) 44 | ]) 45 | } 46 | 47 | module.exports = UibookSelect 48 | -------------------------------------------------------------------------------- /controllers/frame.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class') 2 | var React = require('react') 3 | var h = React.createElement 4 | 5 | var combineObjects = require('../lib/combine-objects') 6 | var UibookWrapper = require('../controllers/wrapper') 7 | var UibookCase = require('../components/case') 8 | 9 | var UibookFrameController = createReactClass({ 10 | pages: [], 11 | 12 | getPage: function (name) { 13 | var pages = this.props.pages 14 | 15 | if (pages[name]) return pages[name] 16 | for (var key in pages) { 17 | if (pages[key].name) continue 18 | if (pages[key][name]) return pages[key][name] 19 | } 20 | return {} 21 | }, 22 | 23 | atts: function () { 24 | var result = {} 25 | 26 | location.search.slice(1).split('&').forEach(function (i) { 27 | var name = i.split('=')[0] 28 | var value = i.split('=')[1] 29 | result[name] = decodeURI(value) 30 | }) 31 | return result 32 | }, 33 | 34 | render: function () { 35 | var atts = this.atts() 36 | var page = this.getPage(atts.page) 37 | var currCase = page.cases[atts.case] 38 | if (typeof currCase.body !== 'function') { 39 | return null 40 | } 41 | 42 | var component = currCase.body(atts.locale) 43 | 44 | var combinedProps = combineObjects( 45 | { component: page.component, name: page.name }, component.props 46 | ) 47 | if (component.type === UibookCase) { 48 | combinedProps = combineObjects({ isFrame: true }, combinedProps) 49 | } 50 | var children = component.children || component.props.children 51 | var content = h(component.type, combinedProps, children) 52 | 53 | return h(UibookWrapper, { 54 | wrapper: this.props.wrapper, 55 | values: this.props.values, 56 | state: atts 57 | }, h('main', { 58 | suppressContentEditableWarning: true, 59 | contentEditable: atts.editable, 60 | className: 'uibook-content' 61 | }, content)) 62 | } 63 | }) 64 | 65 | module.exports = UibookFrameController 66 | -------------------------------------------------------------------------------- /controllers/go-to-top.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class') 2 | var React = require('react') 3 | var h = React.createElement 4 | 5 | var UibookGoToTop = require('../components/go-to-top') 6 | var throttler = require('../lib/throttler') 7 | 8 | var scrollInterval = null 9 | var scrolling = false 10 | 11 | var UibookGoToTopController = createReactClass({ 12 | getInitialState: function () { 13 | return { 14 | prevScrollTop: 0, 15 | scrollTop: 0 16 | } 17 | }, 18 | 19 | componentDidMount: function () { 20 | document.addEventListener('scroll', this.scrollOrResize) 21 | window.addEventListener('resize', this.scrollOrResize) 22 | }, 23 | 24 | getScrollTop: function () { 25 | return document.getElementsByTagName('html')[0].scrollTop 26 | }, 27 | 28 | setScrollTop: function (scrollTop) { 29 | var startScrollTop = this.getScrollTop() 30 | var counter = 0 31 | scrolling = true 32 | 33 | scrollInterval = setInterval(function () { 34 | var easing = (Math.cos(Math.PI * counter / 5) + 1) / 2 35 | var scroll = scrollTop - ((scrollTop - startScrollTop) * easing) 36 | document.getElementsByTagName('html')[0].scrollTop = scroll 37 | counter += 1 38 | 39 | if (counter === 6) { 40 | clearInterval(scrollInterval) 41 | scrolling = false 42 | } 43 | }, 25) 44 | }, 45 | 46 | scrollOrResize: function () { 47 | throttler(function () { 48 | this.setState({ 49 | scrollTop: this.getScrollTop() 50 | }) 51 | }.bind(this)) 52 | }, 53 | 54 | click: function () { 55 | if (scrolling) return 56 | 57 | if (this.getScrollTop() === 0 && this.state.prevScrollTop) { 58 | this.setScrollTop(this.state.prevScrollTop) 59 | } else { 60 | this.setState({ 61 | prevScrollTop: this.getScrollTop() 62 | }) 63 | this.setScrollTop(0) 64 | } 65 | }, 66 | 67 | render: function () { 68 | var container = document.getElementsByClassName('uibook-page')[0] 69 | var hasScroll = container && 70 | container.offsetHeight - window.innerHeight > 50 71 | var isVisible = hasScroll && 72 | (this.state.prevScrollTop || this.state.scrollTop > 50) 73 | var isReturn = this.state.prevScrollTop && this.state.scrollTop === 0 74 | 75 | return h(UibookGoToTop, { 76 | isVisible: isVisible, 77 | isReturn: isReturn, 78 | onClick: this.click 79 | }) 80 | } 81 | }) 82 | 83 | module.exports = UibookGoToTopController 84 | -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class') 2 | var React = require('react') 3 | var h = React.createElement 4 | 5 | var combineObjects = require('../lib/combine-objects') 6 | var UibookWrapper = require('../controllers/wrapper') 7 | var UibookLoader = require('../components/loader') 8 | var UibookError = require('../components/error') 9 | var UibookCase = require('../components/case') 10 | var Uibook = require('../components/index') 11 | 12 | var DOCS_URL = 'https://github.com/vrizo/uibook/blob/master/docs/' 13 | 14 | var lastEventID = 0 15 | 16 | var t = { 17 | backgroundDeprecated: '\'white\' background is deprecated. ' + 18 | 'Please use \'light\' instead', 19 | noPagesAction: 'How to add pages', 20 | noPagesDesc: 'No pages in configuration file', 21 | noPagesUrl: DOCS_URL + 'troubleshooting.md#no-pages', 22 | noCasesDesc: function (page) { 23 | return page + ' contains no cases. Add them in config' 24 | }, 25 | noCasesAction: 'How to add case', 26 | noCasesUrl: DOCS_URL + 'troubleshooting.md#no-cases', 27 | iframeErrorAction: 'Possible reasons', 28 | iframeErrorUrl: DOCS_URL + 'troubleshooting.md#iframe-error', 29 | iframeErrorDesc: function (page) { 30 | return page + ' failed to load in iframe' 31 | } 32 | } 33 | 34 | var UibookController = createReactClass({ 35 | pages: [], 36 | 37 | getInitialState: function () { 38 | var values = this.props.values 39 | var locale 40 | 41 | if (values && values.locale) { 42 | locale = values.locale[0] 43 | } 44 | 45 | var state = { 46 | isEditable: false, 47 | settings: this.settings(), 48 | notices: this.notice(), 49 | errored: {}, 50 | locale: locale, 51 | isInit: false, 52 | height: {}, 53 | loaded: {}, 54 | events: [], 55 | page: null 56 | } 57 | 58 | if (values) { 59 | for (var key in values) { 60 | if (values[key]) { 61 | state[key] = values[key][0] 62 | } 63 | } 64 | } 65 | 66 | return state 67 | }, 68 | 69 | componentDidMount: function () { 70 | for (var key in this.props.pages) { 71 | if (this.props.pages[key].name) { 72 | this.pages.push(key) 73 | } else { 74 | for (var innerKey in this.props.pages[key]) { 75 | this.pages.push(innerKey) 76 | } 77 | } 78 | } 79 | document.body.addEventListener('track', this.track, false) 80 | window.addEventListener('hashchange', this.hashChange, false) 81 | window.addEventListener('keyup', this.keyup, false) 82 | this.hashChange() 83 | this.setState({ isInit: true }) 84 | }, 85 | 86 | componentDidUpdate: function (prevProps, prevState) { 87 | var locale = this.state.locale 88 | var page = this.state.page 89 | 90 | this.changeHash() 91 | if (prevState.page !== page || prevState.locale !== locale) { 92 | this.setState({ loaded: {} }, function () { 93 | setTimeout(function () { 94 | window.scrollTo(0, 0) 95 | }, 50) 96 | }) 97 | } 98 | }, 99 | 100 | componentWillUnmount: function () { 101 | document.body.removeEventListener('track', this.track, false) 102 | window.removeEventListener('hashchange', this.hashChange, false) 103 | window.removeEventListener('keyup', this.keyup, false) 104 | }, 105 | 106 | getPage: function (name) { 107 | var pages = this.props.pages 108 | 109 | if (pages[name]) return pages[name] 110 | for (var key in pages) { 111 | if (pages[key].name) continue 112 | if (pages[key][name]) return pages[key][name] 113 | } 114 | return {} 115 | }, 116 | 117 | track: function (e) { 118 | var event = { 119 | name: e.detail.name, 120 | args: e.detail.args, 121 | id: ++lastEventID 122 | } 123 | 124 | this.setState(function (prevState) { 125 | return { 126 | events: prevState.events.concat([event]) 127 | } 128 | }) 129 | setTimeout(function () { 130 | this.setState(function (prevState) { 131 | return { 132 | events: prevState.events.filter(function (i) { 133 | return i !== event 134 | }) 135 | } 136 | }) 137 | }.bind(this), 3000) 138 | }, 139 | 140 | changeValue: function (event) { 141 | var state = this.state 142 | 143 | if (typeof this.props.values[event.target.id][0] === 'number') { 144 | state[event.target.id] = parseInt(event.target.value) 145 | } else { 146 | state[event.target.id] = event.target.value 147 | } 148 | this.setState(state) 149 | }, 150 | 151 | changePage: function (page) { 152 | this.setState({ page: page }) 153 | }, 154 | 155 | changeEditable: function () { 156 | this.setState({ isEditable: !this.state.isEditable }) 157 | }, 158 | 159 | nextPage: function () { 160 | var pages = this.pages 161 | var current = pages.indexOf(this.state.page) 162 | var next = current + 1 163 | 164 | if (next >= pages.length) next = 0 165 | this.changePage(pages[next]) 166 | }, 167 | 168 | prevPage: function () { 169 | var pages = this.pages 170 | var current = pages.indexOf(this.state.page) 171 | var prev = current - 1 172 | 173 | if (prev < 0) prev = pages.length - 1 174 | this.changePage(pages[prev]) 175 | }, 176 | 177 | hashChange: function () { 178 | var locales = this.props.values && this.props.values.locale 179 | var pages = this.pages 180 | 181 | var location = window.location.href.split('#') 182 | var hash = location[1] || '' 183 | hash = hash.split(':') 184 | 185 | var page = decodeURIComponent(hash[0]) 186 | var locale = hash[1] 187 | 188 | if (this.state.page !== page || this.state.locale !== locale) { 189 | if (pages.indexOf(page) === -1) { 190 | this.changeHash() 191 | } else if (locales && !locale) { 192 | if (pages.indexOf(page) !== -1) { 193 | this.setState({ page: page }) 194 | } 195 | this.changeHash() 196 | } else { 197 | this.setState({ page: page, locale: locale }) 198 | } 199 | } 200 | }, 201 | 202 | keyup: function (e) { 203 | if (this.state.isEditable) return 204 | if (e.keyCode === 39) { 205 | this.nextPage() 206 | } else if (e.keyCode === 37) { 207 | this.prevPage() 208 | } 209 | }, 210 | 211 | loaded: function (index, e) { 212 | var key = this.state.page + index 213 | var main = e.target.contentDocument.querySelector('main') 214 | 215 | if (!main) { 216 | this.setState(function (prevState) { 217 | var errored = combineObjects(prevState.errored, {}) 218 | errored[key] = true 219 | return { errored: errored } 220 | }) 221 | return 222 | } 223 | var mainHeight = main.offsetHeight 224 | 225 | this.setState(function (prevState) { 226 | var loaded = combineObjects(prevState.loaded, {}) 227 | loaded[key] = true 228 | var height = combineObjects(prevState.height, {}) 229 | height[key + prevState.locale] = mainHeight 230 | 231 | return { 232 | loaded: loaded, 233 | height: height 234 | } 235 | }) 236 | }, 237 | 238 | changeHash: function () { 239 | var locale = this.state.locale ? ':' + this.state.locale : '' 240 | var hash = '' 241 | 242 | if (this.state.page) { 243 | hash = '#' + this.state.page + locale 244 | } else if (this.pages[0]) { 245 | hash = '#' + this.pages[0] + locale 246 | } 247 | if (location.hash !== hash) location.hash = hash 248 | }, 249 | 250 | frameUrl: function (index) { 251 | var params = [ 252 | 'editable=' + this.state.isEditable, 253 | 'page=' + this.state.page, 254 | 'case=' + index, 255 | 'iframe=true' 256 | ] 257 | 258 | for (var prop in this.props.values) { 259 | if (this.state.hasOwnProperty(prop)) { /* eslint-disable-line */ 260 | var key = encodeURIComponent(prop) 261 | var value = encodeURIComponent(this.state[prop]) 262 | params.push(key + '=' + value) 263 | } 264 | } 265 | 266 | return location.pathname + '/?' + params.join('&') 267 | }, 268 | 269 | height: function (caseObj, index) { 270 | if (caseObj.height) { 271 | return caseObj.height 272 | } else { 273 | var key = this.state.page + index 274 | return this.state.height[key + this.state.locale] || 150 275 | } 276 | }, 277 | 278 | notice: function () { 279 | var notices = [] 280 | var body = document.getElementsByTagName('body')[0] 281 | 282 | try { 283 | notices = JSON.parse(body.dataset.uibookNotices) 284 | } catch (error) { 285 | console.log('JSON parsing error: ', error) /* eslint-disable-line */ 286 | } 287 | 288 | return notices[0] 289 | }, 290 | 291 | settings: function () { 292 | var settings = {} 293 | var body = document.getElementsByTagName('body')[0] 294 | 295 | try { 296 | settings = JSON.parse(body.dataset.uibookSettings) 297 | } catch (error) { 298 | console.log('JSON parsing error: ', error) /* eslint-disable-line */ 299 | } 300 | 301 | return settings 302 | }, 303 | 304 | render: function () { 305 | var content 306 | var page = this.getPage(this.state.page || this.pages[0]) 307 | 308 | if (page.background === 'white') { 309 | console.warn(t.backgroundDeprecated) /* eslint-disable-line */ 310 | } 311 | 312 | if (!this.state.isInit) { 313 | content = h(UibookLoader, { isLoading: true }) 314 | } else if (!page.name) { 315 | content = h(UibookError, { 316 | actionText: t.noPagesAction, 317 | actionUrl: t.noPagesUrl, 318 | desc: t.noPagesDesc 319 | }) 320 | } else if (!page.cases || page.cases.length === 0) { 321 | content = h(UibookError, { 322 | actionText: t.noCasesAction, 323 | actionUrl: t.noCasesUrl, 324 | desc: t.noCasesDesc(page.name) 325 | }) 326 | } else { 327 | content = page.cases.map(function (i, index) { 328 | var key = this.state.page + index 329 | if (typeof i === 'function') { 330 | var component = i(this.state.locale) 331 | var combinedProps = combineObjects( 332 | { 333 | isEditable: this.state.isEditable, 334 | component: page.component, 335 | name: page.name 336 | }, component.props 337 | ) 338 | return h('div', { key: key }, h(component.type, combinedProps)) 339 | } else { 340 | return h(UibookCase, { key: key, example: i.example || '' }, [ 341 | !this.state.errored[key] 342 | ? h(UibookLoader, { 343 | isLoading: !this.state.loaded[key], 344 | key: 'loader' + key 345 | }, [ 346 | h('iframe', { 347 | className: 'uibook-iframe', 348 | onLoad: this.loaded.bind(this, index), 349 | style: { 350 | height: this.height(i, index), 351 | width: i.width || '100%' 352 | }, 353 | src: this.frameUrl(index), 354 | key: 'iframe' + key 355 | }) 356 | ]) 357 | : h(UibookError, { 358 | actionText: t.iframeErrorAction, 359 | actionUrl: t.iframeErrorUrl, 360 | desc: t.iframeErrorDesc(page.name), 361 | key: 'error' + key 362 | }) 363 | ]) 364 | } 365 | }.bind(this)) 366 | } 367 | 368 | return h(UibookWrapper, { 369 | wrapper: this.props.wrapper, 370 | values: this.props.values, 371 | state: this.state 372 | }, h(Uibook, { 373 | onEditableSwitch: this.changeEditable, 374 | isFixedHeader: this.state.settings.isFixedHeader, 375 | onValueChange: this.changeValue, 376 | onPageChange: this.changePage, 377 | background: page.background || 'default', 378 | isEditable: this.state.isEditable, 379 | onNextPage: this.nextPage, 380 | onPrevPage: this.prevPage, 381 | events: this.state.events, 382 | values: this.props.values, 383 | notice: this.state.notice, 384 | pages: this.props.pages, 385 | state: this.state, 386 | page: this.state.page 387 | }, content)) 388 | } 389 | }) 390 | 391 | module.exports = UibookController 392 | -------------------------------------------------------------------------------- /controllers/wrapper.js: -------------------------------------------------------------------------------- 1 | var UibookWrapper = function (props) { 2 | if (props.wrapper) { 3 | var values = {} 4 | for (var key in props.values) { 5 | values[key] = props.state[key] 6 | } 7 | return props.wrapper(props.children, values) 8 | } else { 9 | return props.children 10 | } 11 | } 12 | 13 | module.exports = UibookWrapper 14 | -------------------------------------------------------------------------------- /docs/advanced-controller.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vrizo/uibook/769c91dca637edab0fd32f599ac73ce5e49de4f3/docs/advanced-controller.gif -------------------------------------------------------------------------------- /docs/configure.md: -------------------------------------------------------------------------------- 1 | # Configure :hatched_chick: 2 | 3 | - [Pages](#pages) 4 | - [Cases](#cases) 5 | - [Responsive Cases](#responsive-cases) 6 | 7 | ## Pages 8 | 9 | You should describe each test component. One component — one page. 10 | 11 | First of all, let’s create a Page. 12 | It’s easier to follow the following file structure: 13 | 14 | ``` 15 | your-project 16 | ├── uibook 17 | │ ├── button.uibook.js 18 | │ ├── field.uibook.js 19 | │ └── uibook-controller.js 20 | ├── src 21 | │ ├── button.js 22 | │ └── field.js 23 | ├── webpack.config.js 24 | └── package.json 25 | ``` 26 | 27 | :triangular_flag_on_post: Tip: run `$ npm init uibook` 28 | to create example Uibook files. 29 | 30 | 1. Create a new js file in `uibook/` folder naming it as a test component 31 | 2. Open the file you just created, import `UibookCase` and your Component 32 | 3. Then return an object like in an example below 33 | 34 | _button.uibook.js_ 35 | ```js 36 | import UibookCase from 'uibook/case' 37 | import Button from '../src/button' 38 | 39 | export default { 40 | component: Button, 41 | name: 'Button', 42 | cases: [...] 43 | } 44 | ``` 45 | 46 | where: 47 | - `component` — pass the Test Component here 48 | - `name` — a name of the Page 49 | - `cases` — an array of Cases 50 | 51 | ## Cases 52 | 53 | A case is a single set of props passed to the Component. 54 | 55 | Each case is a function returning `UibookCase` with the following parameters: 56 | 57 | _button.uibook.js_ 58 | ```js 59 | export default { 60 | background: 'light', 61 | component: Button, 62 | cases: [ 63 | () => Button, 64 | () => 65 | Small Button 66 | 67 | ] 68 | } 69 | ``` 70 | 71 | where: 72 | - `background` _(optional)_ — page background: `light`/`dark`/`default` 73 | - `example` _(optional)_ — code example or your comments, it will be rendered 74 | before Test Component 75 | - `props` — set of necessary props 76 | – `text` — string child of the Component 77 | 78 | :triangular_flag_on_post: There is an optional argument `locale` if you’re 79 | using custom parameters. Please refer to 80 | [Advanced Controller](controller.md#advanced-controller). 81 | 82 | ## Responsive Cases 83 | 84 | Uibook render Responsive Cases in an iframe to use real media queries. 85 | 86 | You can add Responsive Case by wrapping a Case with 87 | an object with width, height or both values 88 | 89 | _button.uibook.js_ 90 | ```js 91 | cases: [ 92 | () => Button, 93 | { 94 | width: 320, 95 | body: () => Mobile Button 96 | } 97 | ] 98 | ``` 99 | 100 | ## Events testing 101 | 102 | Events bubble 103 | 104 | You can pass fake events to test callbacks: 105 | 106 | _button.uibook.js_ 107 | ```js 108 | cases: [ 109 | () => Clickable Button, 112 | ] 113 | ``` 114 | 115 | Congratulations! You’ve finished the most challenging part. 116 | It’s time to pass your first Page to the [Controller](controller.md). 117 | 118 | --- 119 | 120 | [← Back to the installation guide](install.md) 121 | 122 | **[Next: Controller →](controller.md)** 123 | -------------------------------------------------------------------------------- /docs/controller.md: -------------------------------------------------------------------------------- 1 | 2 | # Basic Controller :baby_chick: 3 | 4 | Once you finished your first [Uibook Page](configure.md), you’re ready 5 | to write Uibook Controller. This is a place where all 6 | Pages included and passed to UibookStarter :sparkles: 7 | 8 | Let’s start with a Basic Controller. You can add Redux, Context, etc 9 | later in [Advanced Controller](#advanced-controller) section. 10 | 11 | 1. Create `uibook-controller.js` file in `uibook/` folder 12 | 2. Import `UibookStarter` and all your Pages 13 | 3. Then export `UibookStarter` with the following arguments 14 | 15 | _uibook-controller.js_ 16 | ```js 17 | import UibookStarter from 'uibook/starter' 18 | 19 | import CheckboxUibook from './checkbox.uibook' 20 | import ButtonUibook from './button.uibook' 21 | import PopupUibook from './popup.uibook' 22 | 23 | export default UibookStarter({ 24 | pages: { 25 | Button: ButtonUibook, 26 | Checkbox: CheckboxUibook, 27 | Popup: PopupUibook 28 | } 29 | }) 30 | ``` 31 | 32 | :triangular_flag_on_post: The key represents Page Name. 33 | You can use any Name in CamelCase: 34 | 35 | _uibook-controller.js_ 36 | ```js 37 | { 38 | TheBestButton: ButtonUibook, 39 | Agree: CheckboxUibook, 40 | ВсплывающееОкно: PopupUibook 41 | } 42 | ``` 43 | 44 | :triangular_flag_on_post: You can use Pages nesting: 45 | 46 | Nesting in Pages 47 | 48 | _uibook-controller.js_ 49 | ```js 50 | pages: { 51 | Button: ButtonUibook, 52 | Checkbox: CheckboxUibook, 53 | Popups: { 54 | Popup: PopupUibook 55 | } 56 | } 57 | ``` 58 | 59 | Amazing! You’ve finished your Basic Controller, and now **you can start 60 | Uibook with your project**. 61 | 62 | Uibook integrates into your project, so just run your bundler 63 | and open `/uibook` (or your custom address) in a browser. 64 | 65 | # Advanced Controller 66 | 67 | This section describes how to add Wrappers, for example, Redux, Context, etc 68 | with **your switchable values**. 69 | 70 | Advanced Controller 71 | 72 | For example, wrap the component with a new React Context API and 73 | pass custom values: `locale` and `theme`. Uibook shows custom selectors 74 | in the top bar. 75 | 76 | ```js 77 | … 78 | import Context from './Context' 79 | 80 | export default UibookStarter({ 81 | wrapper: (children, props) => 82 | 83 | { children } 84 | , 85 | values: { 86 | locale: ['ru', 'en'], 87 | theme: ['dark', 'light'] 88 | }, 89 | pages: { 90 | Button: ButtonUibook, 91 | Checkbox: CheckboxUibook, 92 | Popup: PopupUibook 93 | } 94 | }) 95 | ``` 96 | 97 | :triangular_flag_on_post: `locale` is the only prop shown in URL. 98 | Also, it is passed to Case function: 99 | 100 | _button.uibook.js_ 101 | ``` 102 | cases: [ 103 | locale => locale === 'de' ? 'Hund' : 'Dog', 104 | ] 105 | ``` 106 | 107 | --- 108 | 109 | [← Back to the configuration guide](configure.md) 110 | 111 | **[Next: Troubleshooting →](troubleshooting.md)** 112 | -------------------------------------------------------------------------------- /docs/events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vrizo/uibook/769c91dca637edab0fd32f599ac73ce5e49de4f3/docs/events.png -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Simple Example with New Context API 4 | 5 | ``` 6 | react-app 7 | ├── uibook 8 | │ ├── checkbox.uibook.js 9 | │ ├── button.uibook.js 10 | │ ├── field.uibook.js 11 | │ └── uibook-controller.js 12 | ├── controllers 13 | │ └── context.js 14 | ├── src 15 | │ ├── checkbox.js 16 | │ ├── button.js 17 | │ └── field.js 18 | ├── webpack.config.js 19 | └── package.json 20 | ``` 21 | 22 | _webpack.config.js_ 23 | ```js 24 | const Uibook = require('uibook') 25 | plugins: [ 26 | new HtmlWebpackPlugin({ 27 | inject: true, 28 | excludeChunks: ['uibook'], 29 | template: paths.appHtml, 30 | }), 31 | new Uibook({ 32 | outputPath: '/uikitties', 33 | controller: path.join(__dirname, '../src/uibook-controller.js'), 34 | title: 'Uikitties' 35 | }), 36 | ], 37 | ``` 38 | 39 | _uibook-controller.js_ 40 | ```js 41 | import UibookStarter from 'uibook/uibook' 42 | import Context from '../controllers/context' 43 | 44 | import CheckboxUibook from './checkbox.uibook' 45 | import ButtonUibook from './button.uibook' 46 | import FieldUibook from './field.uibook' 47 | import PopupUibook from './popup.uibook' 48 | 49 | export default UibookStarter({ 50 | wrapper: (children, props) => 51 | 52 | { children } 53 | , 54 | values: { 55 | locale: ['ru', 'en'], 56 | theme: ['dark', 'light'] 57 | }, 58 | pages: { 59 | Checkbox: CheckboxUibook, 60 | Button: ButtonUibook, 61 | Field: FieldUibook, 62 | Popups: { 63 | Popup: PopupUibook 64 | } 65 | } 66 | }) 67 | ``` 68 | 69 | _button.uibook.js_ 70 | ```js 71 | import UibookCase from 'uibook/case' 72 | 73 | const ButtonUibook = { 74 | component: Button, 75 | cases: [ 76 | () => 77 | First Button 78 | , 79 | () => 80 | Large Button 81 | , 82 | () => 83 | Disabled 84 | 85 | ] 86 | } 87 | 88 | export default ButtonUibook 89 | ``` 90 | 91 | _checkbox.uibook.js_ 92 | ```js 93 | var CheckboxUibook = { 94 | background: 'dark', 95 | component: Checkbox, 96 | cases: [ 97 | () => First, 98 | () => Checked, 99 | () => Disabled 100 | ] 101 | } 102 | ``` 103 | 104 | --- 105 | 106 | [← Back to the Troubleshooting](troubleshooting.md) 107 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Install :hatching_chick: 2 | 3 | Step 1. Install the package using your favorite package manager 4 | 5 | ```bash 6 | $ yarn add uibook 7 | ``` 8 | 9 | Step 2. Add Uibook in `webpack.config.js` 10 | 11 | _webpack.config.js_ 12 | ```js 13 | let UibookPlugin = require('uibook/plugin') 14 | 15 | module.exports = { 16 | … 17 | plugins: [ 18 | new UibookPlugin({ 19 | isFixedHeader: true, 20 | controller: path.join(__dirname, '../src/uibook-controller.js'), 21 | outputPath: '/uibook', 22 | title: 'Uibook', 23 | hot: true 24 | }) 25 | ], 26 | } 27 | ``` 28 | 29 | where: 30 | 31 | - `controller` — **path to the Uibook Controller** (we’ll create it 32 | on the next step) 33 | - `isFixedHeader` _(optional)_ — enables or disables sticky header, 34 | the default is `true` 35 | - `outputPath` _(optional)_ — directory to build Uibook files, 36 | the default is `uibook` 37 | - `title` _(optional)_ — Uibook title in a browser 38 | - `hot` _(optional)_ — enable `webpack-dev-server` hot reload feature 39 | 40 | :warning: If you’re using HtmlWebpackPlugin, it’s necessary to exclude `uibook`: 41 | 42 | _webpack.config.js_ 43 | ```js 44 | new HtmlWebpackPlugin({ 45 | excludeChunks: ['uibook'] 46 | }) 47 | ``` 48 | 49 | Nice work! You’ve installed Uibook just now. 50 | Now we can [configure it](configure.md). 51 | 52 | --- 53 | 54 | [← Back to the main page](../README.md) 55 | 56 | **[Next: Configuration →](configure.md)** 57 | -------------------------------------------------------------------------------- /docs/install_craco.md: -------------------------------------------------------------------------------- 1 | # Install in create-react-app using [CRACO](https://github.com/gsoft-inc/craco) :hatching_chick: 2 | 3 | Step 1. Install the package using your favorite package manager 4 | 5 | ```bash 6 | $ yarn add uibook 7 | ``` 8 | 9 | Step 2. Add Uibook in `craco.config.js` 10 | 11 | _craco.config.js_ 12 | ```js 13 | let isProduction = process.env.NODE_ENV === 'production' 14 | 15 | module.exports = { 16 | webpack: { 17 | configure: { 18 | output: { 19 | filename: isProduction 20 | ? 'static/js/[name].[contenthash:8].js' 21 | : 'static/js/[name].js', 22 | }, 23 | }, 24 | plugins: [ 25 | new UibookPlugin({ 26 | // JSX is transformed only in `src/` folder 27 | controller: path.join(__dirname, 'src/uibook/uibook-controller.js'), 28 | isFixedHeader: true, 29 | outputPath: '/uibook', 30 | title: 'Uibook', 31 | hot: true 32 | }), 33 | ], 34 | }, 35 | }; 36 | ``` 37 | 38 | where: 39 | 40 | - `controller` — **path to the Uibook Controller** (we’ll create it 41 | on the next step) 42 | - `isFixedHeader` _(optional)_ — enables or disables sticky header, 43 | the default is `true` 44 | - `outputPath` _(optional)_ — directory to build Uibook files, 45 | the default is `uibook` 46 | - `title` _(optional)_ — Uibook title in a browser 47 | - `hot` _(optional)_ — enable `webpack-dev-server` hot reload feature 48 | 49 | :warning: If you’re using HtmlWebpackPlugin, it’s necessary to exclude `uibook`: 50 | 51 | _webpack.config.js_ 52 | ```js 53 | new HtmlWebpackPlugin({ 54 | excludeChunks: ['uibook'] 55 | }) 56 | ``` 57 | 58 | Nice work! You’ve installed Uibook in Create React App just now. 59 | Now we can [configure it](configure.md). 60 | 61 | _Thanks to :octocat: [@Grawl](https://github.com/Grawl) for help with this section._ 62 | 63 | --- 64 | 65 | [← Back to the main page](../README.md) 66 | 67 | **[Next: Configuration →](configure.md)** 68 | -------------------------------------------------------------------------------- /docs/nesting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vrizo/uibook/769c91dca637edab0fd32f599ac73ce5e49de4f3/docs/nesting.png -------------------------------------------------------------------------------- /docs/text-edit-mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vrizo/uibook/769c91dca637edab0fd32f599ac73ce5e49de4f3/docs/text-edit-mode.gif -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## White page (while using Create React App) 4 | 5 | It is supposed that Create React App will generate one main bundle only. 6 | You should replace `bundle.js` with `[name].js` to separate main app 7 | and Uibook: 8 | 9 | ```js 10 | filename: isEnvProduction 11 | ? 'static/js/[name].[contenthash:8].js' 12 | : isEnvDevelopment && 'static/js/bundle.js', 13 | ^^^^^^ 14 | ``` 15 | 16 | Replace with: 17 | 18 | ```js 19 | : isEnvDevelopment && 'static/js/[name].js', 20 | ``` 21 | _webpack.config.js_ 22 | 23 | Any ideas how to improve this? Please PR. 24 | 25 | ## No Pages 26 | 27 | You should describe each test component. 28 | Please read [about configuring](configure.md). 29 | 30 | ## No Cases 31 | 32 | You forget to pass Cases to your Page. 33 | Read more about Cases [here](configure.md#cases). 34 | 35 | ## Iframe Error 36 | 37 | Uibook renders Responsive Cases in a `