Add storybook

This commit is contained in:
Alfred Melch 2020-03-07 17:45:25 +01:00
parent 4656f5b280
commit a81a5a2458
17 changed files with 5675 additions and 50 deletions

13
.storybook/main.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
stories: ['../src/components/**/*.stories.js'],
addons: [
'@storybook/addon-knobs/register',
'@storybook/addon-actions',
'@storybook/addon-links'
],
webpackFinal: async config => {
// do mutation to the config
return config
}
}

8
.storybook/preview.js Normal file
View File

@ -0,0 +1,8 @@
import { addDecorator } from '@storybook/react'
import { withInfo } from '@storybook/addon-info'
import { withKnobs } from '@storybook/addon-knobs'
import { withConsole } from '@storybook/addon-console'
addDecorator(withInfo({ inline: true }))
addDecorator(withKnobs)
addDecorator((storyFn, context) => withConsole()(storyFn)(context))

View File

@ -0,0 +1,23 @@
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: true
}
},
{
loader: 'postcss-loader'
}
]
}
]
}
}

5418
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@
"test": "tests", "test": "tests",
"lint": "eslint src", "lint": "eslint src",
"format": "prettier --write src/**/*.js", "format": "prettier --write src/**/*.js",
"graph": "mkdir -p build && madge src --exclude '.css$' --image build/dep-graph.svg" "graph": "mkdir -p build && madge src --exclude '.css$' --image build/dep-graph.svg",
"storybook": "start-storybook -p 6006 --ci",
"build-storybook": "build-storybook"
}, },
"author": "Alfred Melch (dev@melch.pro)", "author": "Alfred Melch (dev@melch.pro)",
"license": "ISC", "license": "ISC",
@ -34,6 +36,13 @@
"@babel/preset-env": "^7.8.4", "@babel/preset-env": "^7.8.4",
"@babel/preset-react": "^7.8.3", "@babel/preset-react": "^7.8.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.1.3",
"@storybook/addon-actions": "^5.3.14",
"@storybook/addon-console": "^1.2.1",
"@storybook/addon-info": "^5.3.14",
"@storybook/addon-knobs": "^5.3.14",
"@storybook/addon-links": "^5.3.14",
"@storybook/addons": "^5.3.14",
"@storybook/react": "^5.3.14",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"css-loader": "^3.4.2", "css-loader": "^3.4.2",
@ -55,6 +64,7 @@
"postcss-normalize": "^8.0.1", "postcss-normalize": "^8.0.1",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"prop-types": "^15.7.2",
"react-refresh": "^0.7.2", "react-refresh": "^0.7.2",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"webpack": "^4.41.5", "webpack": "^4.41.5",

View File

@ -1,5 +1,5 @@
.wrapper { .indicator {
overflow: hidden; text-align: center;
} }
.border { .border {

View File

@ -0,0 +1,41 @@
import React from 'react'
import styles from './Indicator.css'
export const Indicator = ({ type = 'border', children }) => {
const IndicatorType = indicatorByType(type)
return (
<div className={styles.indicator}>
<IndicatorType>{children}</IndicatorType>
</div>
)
}
const indicatorByType = type => {
switch (type) {
case 'border':
return BorderIndicator
case 'pipe':
return PipeIndicator
default:
throw Error(`Indicator of type ${type} is not defined`)
}
}
const BorderIndicator = ({ children }) => (
<>
<div className={styles.border}></div>
<div className={styles.marker}></div>
{children}
<div className={styles.marker}></div>
<div className={styles.border}></div>
</>
)
const PipeIndicator = ({ children }) => (
<>
<div>|</div>
{children}
<div>|</div>
</>
)

View File

@ -0,0 +1,25 @@
import React from 'react'
import { Indicator } from './Indicator'
import { Segment } from './Segment'
import { radios } from '@storybook/addon-knobs'
export default {
component: Indicator,
title: 'Indicator'
}
const typeKnob = () =>
radios('Type', { Border: 'border', Pipe: 'pipe' }, 'border')
export const HappyPath = () => {
return <Indicator type={typeKnob()}>Hello World</Indicator>
}
export const WithSegment = () => {
return (
<Indicator type={typeKnob()}>
<Segment>Hello World</Segment>
</Indicator>
)
}

23
src/components/Offset.js Normal file
View File

@ -0,0 +1,23 @@
import React from 'react'
import PropTypes from 'prop-types'
/**
* Wrapper that is of double width while hiding its overflow.
* Place children with offset (from -50 to 50, 0 is center)
*/
export const Offset = ({ offset = 0, children }) => {
offset = (offset - 50) / 2
return (
<div style={{ overflow: 'hidden' }}>
<div style={{ width: '200%' }}>
<div style={{ position: 'relative', left: `${offset}%` }}>
{children}
</div>
</div>
</div>
)
}
Offset.propTypes = {
offset: PropTypes.number
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import { number } from '@storybook/addon-knobs'
import { Offset } from './Offset'
import { Indicator } from './Indicator'
import { Segment } from './Segment'
export default {
component: Offset,
title: 'Offset'
}
const offsetKnob = () =>
number('Offset', 0, {
range: true,
min: -50,
max: 50,
step: 1
})
export const BlockElement = () => (
<Offset offset={offsetKnob()}>
<div style={{ textAlign: 'center' }}>This text is centered</div>
</Offset>
)
export const InlineElement = () => (
<Offset offset={offsetKnob()}>
This a long text to see that inline elements get pushed far to the left. To
be precise they get pushed half the container with to the left. Use
text-align: center for better display.
</Offset>
)
export const WithIndicator = () => (
<Offset offset={offsetKnob()}>
<Indicator>
<Segment>Hello World</Segment>
</Indicator>
</Offset>
)

View File

@ -1,28 +0,0 @@
import React from 'react'
import { useSelector } from '../store'
import styles from './PivotMarker.css'
import { selectOffset } from '../store/selectors'
export const PipeMarker = ({ children }) => (
<div>
<div style={{ textAlign: 'center' }}>|</div>
{children}
<div style={{ textAlign: 'center' }}>|</div>
</div>
)
export const BorderMarker = ({ children }) => {
const offset = useSelector(selectOffset)
return (
<div>
<div className={styles.border}></div>
<div className={styles.wrapper}>
<div className={styles.marker} style={{ left: offset + '%' }}></div>
{children}
<div className={styles.marker} style={{ left: offset + '%' }}></div>
</div>
<div className={styles.border}></div>
</div>
)
}

View File

@ -7,11 +7,16 @@ import { Options } from './Options'
import { PlayerControl } from './PlayerControl' import { PlayerControl } from './PlayerControl'
import styles from './RsvpReader.css' import styles from './RsvpReader.css'
import { BorderMarker } from './PivotMarker'
import { Progress } from './Progress' import { Progress } from './Progress'
import { TotalTime } from './TotalTime' import { TotalTime } from './TotalTime'
import { Indicator } from './Indicator'
import { Offset } from './Offset'
import { useSelector } from '../store'
import { selectCurrentSegment, selectOffset } from '../store/selectors'
export const RsvpReader = () => { export const RsvpReader = () => {
const segment = useSelector(selectCurrentSegment)
const offset = useSelector(selectOffset)
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.item}> <div className={styles.item}>
@ -19,9 +24,11 @@ export const RsvpReader = () => {
</div> </div>
<div className={styles.mainItem}> <div className={styles.mainItem}>
<Progress /> <Progress />
<BorderMarker> <Offset offset={offset}>
<Segment /> <Indicator>
</BorderMarker> <Segment>{segment}</Segment>
</Indicator>
</Offset>
<div className={styles.controls}> <div className={styles.controls}>
<SegmentControl> <SegmentControl>
<PlayerControl /> <PlayerControl />

View File

@ -16,4 +16,5 @@
} }
.suffix { .suffix {
flex: 1; flex: 1;
text-align: left;
} }

View File

@ -1,19 +1,20 @@
import React from 'react' import React from 'react'
import { useSelector } from '../store' import PropTypes from 'prop-types'
import { selectPivotizedSegment, selectOffset } from '../store/selectors'
import styles from './Segment.css' import styles from './Segment.css'
import { pivotize } from '../lib/pivotize'
export const Segment = () => { export const Segment = ({ children = '' }) => {
const [prefix, pivot, suffix] = useSelector(selectPivotizedSegment) const [prefix, pivot, suffix] = pivotize(children)
const offset = useSelector(selectOffset)
return ( return (
<div className={styles.wrapper}> <div className={styles.container}>
<div className={styles.container} style={{ left: offset + '%' }}> <span className={styles.prefix}>{prefix}</span>
<span className={styles.prefix}>{prefix}</span> <span className={styles.pivot}>{pivot}</span>
<span className={styles.pivot}>{pivot}</span> <span className={styles.suffix}>{suffix}</span>
<span className={styles.suffix}>{suffix}</span>
</div>
</div> </div>
) )
} }
Segment.propTypes = {
children: PropTypes.string.isRequired
}

View File

@ -0,0 +1,13 @@
import React from 'react'
import { text } from '@storybook/addon-knobs'
import { Segment } from './Segment'
export default {
component: Segment,
title: 'Segment'
}
export const HappyPath = () => {
return <Segment>{text('Text', 'Hello')}</Segment>
}

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import styles from './Slider.css' import styles from './Slider.css'
@ -8,14 +9,15 @@ export const Slider = ({
value, value,
defaultValue, defaultValue,
name, name,
min, min = 0,
max, max = 100,
step, step = 1,
onChange onChange
}) => { }) => {
value = value || defaultValue || (max - min) / 2 + min
// use ref for onChange to not trigger state updates on rerenders of parent // use ref for onChange to not trigger state updates on rerenders of parent
const onChangeRef = useRef(null) const onChangeRef = useRef(null)
const [internalValue, setInternalValue] = useState(value || defaultValue) const [internalValue, setInternalValue] = useState(value)
// trigger external update on internal change // trigger external update on internal change
useEffect(() => { useEffect(() => {
@ -61,3 +63,15 @@ export const Slider = ({
} }
const calcWidth = num => `${(String(num).length + 1) * 12}px` const calcWidth = num => `${(String(num).length + 1) * 12}px`
Slider.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
value: PropTypes.number,
defaultValue: PropTypes.number,
name: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
onChange: PropTypes.func
}

View File

@ -0,0 +1,15 @@
import React from 'react'
import { action } from '@storybook/addon-actions'
import { Slider } from './Slider'
export default {
component: Slider,
title: 'Slider'
}
export const Default = () => <Slider onChange={action()} />
export const CustomRange = () => <Slider onChange={action()} min={5} max={15} />
export const CustomStep = () => <Slider onChange={action()} step={5} />