├── .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 |
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 |
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 |
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 |
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 |
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 |
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 `