├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── docs
└── assets
│ ├── code.png
│ └── playground.png
├── package.json
├── src
├── Checkbox.js
├── Radio.js
├── index.js
└── styles.css
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 | insert_final_newline = false
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true
5 | },
6 |
7 | "parserOptions": {
8 | "ecmaVersion": 2018,
9 | "sourceType": "module",
10 | "ecmaFeatures": {
11 | "jsx": true
12 | }
13 | },
14 | "settings": {
15 | "react": {
16 | "version": "16.8.6"
17 | }
18 | },
19 | "extends": [
20 | "eslint:recommended",
21 | "plugin:react/recommended",
22 | "plugin:prettier/recommended"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_STORE
2 | node_modules
3 | package-lock.json
4 | yarn.lock
5 | *.log*
6 | .vscode
7 | /dist
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) VAGAS Tecnologia.
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Describe
2 |
3 | `React Describe` generates a component playground with editable prop values and a live preview that you can include in your own docs.
4 |
5 |
6 |
7 |
8 |
9 | | Table of Contents |
10 | | :-------------------------------------- |
11 | | [Runnable examples](#runnable-examples) |
12 | | [Installation](#installation) |
13 | | [Usage](#usage) |
14 | | [Licensing](#licensing) |
15 |
16 | ## Runnable examples
17 |
18 | - [Basic](https://codesandbox.io/s/react-describe-example-cw9b8)
19 | - [Gatsby](https://codesandbox.io/s/react-describe-gatsby-example-6x1qw)
20 |
21 | ## Installation
22 |
23 | with Yarn:
24 |
25 | ```bash
26 | yarn add react-describe
27 | ```
28 |
29 | with npm:
30 |
31 | ```bash
32 | npm install react-describe
33 | ```
34 |
35 | ## Usage
36 |
37 | - Define `propTypes` and `defaultProps` for your component props
38 | - Add a comment above each prop to describe it
39 |
40 | ```js
41 | import React from "react";
42 | import PropTypes from "prop-types";
43 |
44 | /**
45 | * Button description...
46 | */
47 | export default function Button({ label, disabled, size, borderRadius }) {
48 | const sizes = {
49 | small: 32,
50 | medium: 48,
51 | large: 64
52 | };
53 |
54 | return (
55 |
71 | );
72 | }
73 |
74 | Button.defaultProps = {
75 | size: "medium",
76 | disabled: false,
77 | borderRadius: 0
78 | };
79 |
80 | Button.propTypes = {
81 | /**
82 | * label description...
83 | */
84 | label: PropTypes.string.isRequired,
85 |
86 | /**
87 | * disabled description...
88 | */
89 | disabled: PropTypes.bool,
90 |
91 | /**
92 | * size description...
93 | */
94 | size: PropTypes.oneOf(["small", "medium", "large"]),
95 |
96 | /**
97 | * borderRadius description...
98 | */
99 | borderRadius: PropTypes.number
100 | };
101 | ```
102 |
103 | - Import `react-describe`
104 | - Import your component and place it as child of `Describe`
105 | - Import your component as a string and pass it to `src`
106 |
107 | ```js
108 | import React from "react";
109 | import ReactDOM from "react-dom";
110 | import { Describe } from "react-describe";
111 | import Button from "./Button";
112 | import RawButton from "!raw-loader!./Button.js";
113 |
114 | function App() {
115 | return (
116 |
117 | {state => }
118 |
119 | );
120 | }
121 |
122 | const rootElement = document.getElementById("root");
123 | ReactDOM.render(, rootElement);
124 | ```
125 |
126 | - You can provide an initial state to override default prop values
127 |
128 | ```js
129 |
138 | {state => }
139 |
140 | ```
141 |
142 | ### Available playground inputs
143 |
144 | | PropType | input |
145 | | :------- | ------------------ |
146 | | string | text input |
147 | | number | number input |
148 | | bool | checkbox |
149 | | oneOf | radio-button group |
150 |
151 | ## Licensing
152 |
153 | React Describe is licensed under the [MIT License](LICENSE)
154 |
--------------------------------------------------------------------------------
/docs/assets/code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VAGAScom/react-describe/41db9ebb6199e4026a706b0f6b32e943a21e2ba5/docs/assets/code.png
--------------------------------------------------------------------------------
/docs/assets/playground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VAGAScom/react-describe/41db9ebb6199e4026a706b0f6b32e943a21e2ba5/docs/assets/playground.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-describe",
3 | "version": "0.1.3",
4 | "description": "An accessible React playground on top of react-docgen.",
5 | "repository": "https://github.com/VAGAScom/react-describe",
6 | "author": "Charbel Rami",
7 | "license": "MIT",
8 | "private": false,
9 | "peerDependencies": {
10 | "react": "^16.8.6"
11 | },
12 | "dependencies": {
13 | "prop-types": "^15.7.2",
14 | "react-docgen": "^4.1.1",
15 | "shortid": "^2.2.14"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.5.5",
19 | "@babel/preset-react": "^7.0.0",
20 | "babel-loader": "^8.0.6",
21 | "clean-webpack-plugin": "^3.0.0",
22 | "css-loader": "^3.1.0",
23 | "eslint": "^6.1.0",
24 | "eslint-config-prettier": "^6.0.0",
25 | "eslint-plugin-prettier": "^3.1.0",
26 | "eslint-plugin-react": "^7.14.2",
27 | "prettier": "^1.18.2",
28 | "style-loader": "^0.23.1",
29 | "webpack": "^4.36.1",
30 | "webpack-cli": "^3.3.6",
31 | "webpack-merge": "^4.2.1"
32 | },
33 | "main": "dist/react-describe.js",
34 | "files": [
35 | "dist",
36 | "src"
37 | ],
38 | "scripts": {
39 | "fix": "./node_modules/.bin/eslint src/*.js --fix",
40 | "watch": "webpack --watch --config webpack.dev.js",
41 | "build": "webpack --config webpack.prod.js",
42 | "prepare": "yarn build"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | export default function Checkbox({ label, checked, ...rest }) {
5 | return (
6 |
32 | );
33 | }
34 |
35 | Checkbox.propTypes = {
36 | label: PropTypes.string.isRequired,
37 | checked: PropTypes.bool.isRequired
38 | };
39 |
--------------------------------------------------------------------------------
/src/Radio.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | export default function Radio({ label, checked, ...rest }) {
5 | return (
6 |
18 | );
19 | }
20 |
21 | Radio.propTypes = {
22 | label: PropTypes.string.isRequired,
23 | checked: PropTypes.bool.isRequired
24 | };
25 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useReducer } from "react";
2 | import { string, object, func } from "prop-types";
3 | import { parse } from "react-docgen";
4 | import { generate as generateShortId } from "shortid";
5 |
6 | import Checkbox from "./Checkbox";
7 | import Radio from "./Radio";
8 |
9 | import "./styles.css";
10 |
11 | export function Describe({ src, initialState, children }) {
12 | const [parsedData, setParsedData] = useState();
13 | const [stringDescId] = useState(generateShortId());
14 | const [numberDescId] = useState(generateShortId());
15 | const [boolDescId] = useState(generateShortId());
16 | const [enumDescId] = useState(generateShortId());
17 | const [articleLabelId] = useState(generateShortId());
18 | const [articleDescId] = useState(generateShortId());
19 |
20 | function removeExtraQuotes(str) {
21 | try {
22 | return JSON.parse(str);
23 | } catch (error) {
24 | return str.replace(/'/g, "");
25 | }
26 | }
27 |
28 | function reducer(state, { label, val, defaultVal }) {
29 | return {
30 | ...state,
31 | [label]:
32 | val !== undefined && val !== null
33 | ? val
34 | : initialState && initialState[label]
35 | ? initialState[label]
36 | : defaultVal && removeExtraQuotes(defaultVal)
37 | };
38 | }
39 |
40 | const [state, dispatch] = useReducer(
41 | reducer,
42 | initialState ? initialState : {}
43 | );
44 |
45 | useEffect(() => {
46 | setParsedData(parse(src));
47 | }, [src]);
48 |
49 | useEffect(() => {
50 | parsedData &&
51 | parsedData.props &&
52 | Object.entries(parsedData.props).map(([k, v]) => {
53 | return dispatch({
54 | label: k,
55 | defaultVal: v.defaultValue && v.defaultValue.value
56 | });
57 | });
58 | }, [parsedData]);
59 |
60 | const renderInput = ({ type, label, val, required, description }) => {
61 | switch (type) {
62 | case "string":
63 | return (
64 |
65 |
75 | {required ? (
76 |
77 | Required
78 |
79 | ) : (
80 | description && (
81 |
82 | )
83 | )}
84 | {description && (
85 |
89 | {description}
90 |
91 | )}
92 |
93 | );
94 | case "number":
95 | return (
96 |
97 |
109 | {required ? (
110 |
111 | Required
112 |
113 | ) : (
114 | description && (
115 |
116 | )
117 | )}
118 | {description && (
119 |
123 | {description}
124 |
125 | )}
126 |
127 | );
128 | case "bool":
129 | return (
130 |
131 | dispatch({ label, val: ev.target.checked })}
134 | aria-describedby={boolDescId}
135 | label={label}
136 | />
137 | {required ? (
138 | Required
139 | ) : (
140 | description && (
141 |
142 | )
143 | )}
144 | {description && (
145 |
149 | {description}
150 |
151 | )}
152 |
153 | );
154 | case "enum":
155 | return (
156 |
157 | {label}
158 |
164 | {val.map((v, i) => (
165 |
169 | dispatch({ label, val: removeExtraQuotes(v.value) })
170 | }
171 | label={removeExtraQuotes(v.value)}
172 | />
173 | ))}
174 |
175 | {required ? (
176 | Required
177 | ) : (
178 | description && (
179 |
180 | )
181 | )}
182 | {description && (
183 |
187 | {description}
188 |
189 | )}
190 |
191 | );
192 | default:
193 | return;
194 | }
195 | };
196 |
197 | return parsedData ? (
198 |
203 |
215 |
216 |
220 | {children(state)}
221 |
222 |
223 |
250 |
251 |
255 |
256 |
257 | {`<${parsedData.displayName &&
258 | parsedData.displayName} ${Object.entries(state).map(([k, v]) =>
259 | v ? k + "={" + JSON.stringify(v) + "}" : ""
260 | )} />`
261 | .replace(/,/g, " ")
262 | .replace(/\s+/g, " ")}
263 |
264 |
265 |
266 |
267 | ) : null;
268 | }
269 |
270 | Describe.propTypes = {
271 | src: string.isRequired,
272 | initialState: object,
273 | children: func.isRequired
274 | };
275 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | @import url("https://rsms.me/inter/inter.css");
2 |
3 | .react-describe-root {
4 | font-family: "Inter", sans-serif;
5 | margin: 16px;
6 | padding: 32px;
7 | box-shadow: 0 25px 50px -15px rgba(0, 0, 0, 0.2);
8 | }
9 |
10 | @supports (font-variation-settings: normal) {
11 | .react-describe-root {
12 | font-family: "Inter var", sans-serif;
13 | }
14 | }
15 |
16 | .react-describe-checkbox-input,
17 | .react-describe-radio-input {
18 | clip: rect(1px, 1px, 1px, 1px);
19 | height: 1px;
20 | overflow: hidden;
21 | position: absolute !important;
22 | width: 1px;
23 | }
24 |
25 | .react-describe-checkbox,
26 | .react-describe-radio {
27 | position: relative;
28 | display: inline-block;
29 | cursor: default;
30 | font-size: 15px;
31 | margin: 8px;
32 | padding-left: 25px;
33 | }
34 |
35 | .react-describe-checkbox::before,
36 | .react-describe-radio::before {
37 | position: absolute;
38 | top: -3px;
39 | left: -5px;
40 | content: "";
41 | width: 25px;
42 | height: 25px;
43 | box-sizing: border-box;
44 | background-color: rgb(230, 234, 236);
45 | }
46 |
47 | .react-describe-checkbox::before {
48 | border-radius: 9px;
49 | }
50 |
51 | .react-describe-radio::before {
52 | border-radius: 50%;
53 | transition: background-color 300ms;
54 | }
55 |
56 | .react-describe-checkbox-input:focus + .react-describe-checkbox::after,
57 | .react-describe-radio-input:focus + .react-describe-radio::after {
58 | position: absolute;
59 | top: -9px;
60 | left: -11px;
61 | content: "";
62 | width: 37px;
63 | height: 37px;
64 | background-color: rgba(25, 176, 232, 37%);
65 | }
66 |
67 | .react-describe-checkbox-input:focus + .react-describe-checkbox::after {
68 | border-radius: 14px;
69 | }
70 |
71 | .react-describe-radio-input:focus + .react-describe-radio::after {
72 | border-radius: 50%;
73 | }
74 |
75 | .react-describe-checkbox > svg {
76 | z-index: 1;
77 | position: absolute;
78 | left: 0;
79 | top: -2px;
80 | width: 16px;
81 | stroke: rgb(56, 70, 78);
82 | stroke-width: 3px;
83 | opacity: 0;
84 | transition: opacity 300ms;
85 | }
86 |
87 | .react-describe-checkbox[data-checked="true"] > svg {
88 | opacity: 1;
89 | }
90 |
91 | .react-describe-radio[data-checked="true"]::before {
92 | background-color: rgb(90, 134, 204);
93 | }
94 |
95 | .react-describe-string-input,
96 | .react-describe-number-input {
97 | border: none;
98 | border-radius: 3px;
99 | box-sizing: border-box;
100 | height: 42px;
101 | background-color: rgb(230, 234, 236);
102 | margin-top: 2px;
103 | padding: 0 20px;
104 | }
105 |
106 | .react-describe-checkbox {
107 | font-size: 15px;
108 | letter-spacing: 0.05em;
109 | color: rgb(49, 58, 64);
110 | }
111 |
112 | .react-describe-radio,
113 | .react-describe-string-input,
114 | .react-describe-number-input {
115 | font-size: 15px;
116 | color: rgb(56, 70, 78);
117 | font-weight: 300;
118 | }
119 |
120 | .react-describe-string-input:focus,
121 | .react-describe-number-input:focus {
122 | outline: none;
123 | background-color: hsl(200, 14%, 84%);
124 | }
125 |
126 | .react-describe-string-container,
127 | .react-describe-number-container,
128 | .react-describe-bool-container,
129 | .react-describe-enum-container,
130 | .react-describe-string-label-container,
131 | .react-describe-number-label-container {
132 | display: flex;
133 | flex-direction: column;
134 | margin: 0;
135 | }
136 |
137 | .react-describe-string-container,
138 | .react-describe-number-container,
139 | .react-describe-bool-container,
140 | .react-describe-enum-container {
141 | padding: 24px;
142 | }
143 |
144 | .react-describe-string-required,
145 | .react-describe-number-required,
146 | .react-describe-bool-required,
147 | .react-describe-enum-required {
148 | align-self: flex-end;
149 | font-size: 12px;
150 | font-weight: 500;
151 | line-height: 22px;
152 | margin-right: 2px;
153 | }
154 |
155 | .react-describe-string-description,
156 | .react-describe-number-description,
157 | .react-describe-bool-description,
158 | .react-describe-enum-description {
159 | padding-left: 12px;
160 | font-size: 12px;
161 | line-height: 22px;
162 | letter-spacing: 0.05em;
163 | font-style: italic;
164 | color: rgb(78, 90, 99);
165 | }
166 |
167 | .react-describe-editor-sidebar-form > :nth-child(2n) {
168 | background-color: hsl(0, 0%, 98%);
169 | }
170 |
171 | .react-describe-string-label-text,
172 | .react-describe-number-label-text,
173 | .react-describe-enum-label-text {
174 | font-size: 15px;
175 | line-height: 23px;
176 | letter-spacing: 0.05em;
177 | color: rgb(49, 58, 64);
178 | }
179 |
180 | .react-describe-enum-required-spacing {
181 | display: block;
182 | height: 18px;
183 | }
184 |
185 | .react-describe-title,
186 | .react-describe-description {
187 | margin: 0;
188 | }
189 |
190 | .react-describe-title {
191 | font-size: 52px;
192 | font-weight: 900;
193 | }
194 |
195 | .react-describe-description {
196 | font-size: 20px;
197 | font-weight: 300;
198 | margin-top: 8px;
199 | margin-bottom: 16px;
200 | }
201 |
202 | .react-describe-editor-container {
203 | display: flex;
204 | flex-wrap: wrap;
205 | margin-bottom: 16px;
206 | }
207 |
208 | .react-describe-editor-preview {
209 | min-width: 370px;
210 | flex: 2;
211 | overflow-x: auto;
212 | border-radius: 2px;
213 | padding: 32px;
214 | display: flex;
215 | align-items: center;
216 | justify-content: center;
217 | background-color: hsl(0, 0%, 97%);
218 | }
219 |
220 | .react-describe-editor-sidebar {
221 | width: 33vw;
222 | min-width: 250px;
223 | flex: 1;
224 | overflow-wrap: break-word;
225 | display: flex;
226 | align-items: center;
227 | }
228 |
229 | .react-describe-editor-sidebar-form {
230 | flex: 1;
231 | }
232 |
233 | .react-describe-output-pre {
234 | margin: 0;
235 | padding: 16px;
236 | background-color: hsla(0, 0%, 98%, 1);
237 | overflow-x: auto;
238 | }
239 |
240 | .react-describe-output-code {
241 | font-family: SFMono-Regular, Consolas, monospace;
242 | white-space: normal;
243 | }
244 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
3 |
4 | module.exports = {
5 | entry: "./src/index.js",
6 | output: {
7 | path: path.resolve(__dirname, "dist"),
8 | filename: "react-describe.js",
9 | library: "reactDescribe",
10 | libraryTarget: "umd"
11 | },
12 | externals: {
13 | react: "react"
14 | },
15 | node: {
16 | fs: "empty"
17 | },
18 | plugins: [new CleanWebpackPlugin()],
19 | module: {
20 | rules: [
21 | {
22 | test: /\.css$/,
23 | use: ["style-loader", "css-loader"]
24 | },
25 | {
26 | test: /\.js$/,
27 | exclude: /node_modules/,
28 | use: {
29 | loader: "babel-loader",
30 | options: {
31 | presets: ["@babel/preset-react"]
32 | }
33 | }
34 | }
35 | ]
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge");
2 | const common = require("./webpack.common.js");
3 |
4 | module.exports = merge(common, {
5 | mode: "development",
6 | devtool: "inline-source-map"
7 | });
8 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge");
2 | const common = require("./webpack.common.js");
3 |
4 | module.exports = merge(common, {
5 | mode: "production",
6 | devtool: "source-map"
7 | });
8 |
--------------------------------------------------------------------------------