Merge branch 'master' into a11y#3641

This commit is contained in:
Jason Miazga 2018-07-20 08:10:15 -04:00
commit 5d0ed30854
255 changed files with 5429 additions and 3734 deletions

View File

@ -1,3 +1,50 @@
# 4.0.0-alpha.14
2018-July-11
#### Bug Fixes
- Upgrade universal-dotenv to fix core-js dependency [#3874](https://github.com/storybooks/storybook/pull/3874)
# 4.0.0-alpha.13
2018-July-09
#### Features
- Refactor addon-jest to use a parameter-based pattern [#3678](https://github.com/storybooks/storybook/pull/3678)
#### Bug Fixes
- Upgrade universal-dotenv to fix babel-runtime [#3863](https://github.com/storybooks/storybook/pull/3863)
#### Maintenance
- Added a test for parameter combination [#3844](https://github.com/storybooks/storybook/pull/3844)
# 4.0.0-alpha.12
2018-July-03
#### Bug Fixes
- Fix non-polyfilled themed UI components [#3829](https://github.com/storybooks/storybook/pull/3829)
# 4.0.0-alpha.11
2018-July-02
#### Features
- Storybook UI theming [#3628](https://github.com/storybooks/storybook/pull/3628)
- Replaced 'dotenv-webpack' with 'universal-dotenv' to support multiple dot env files (like CRA) [#3744](https://github.com/storybooks/storybook/pull/3744)
- Support other type of webpack configs [#3785](https://github.com/storybooks/storybook/pull/3785)
#### Bug Fixes
- Marko: fix welcome component [#3796](https://github.com/storybooks/storybook/pull/3796)
- Addon-a11y: Run analysis on demand [#3690](https://github.com/storybooks/storybook/pull/3690)
# 4.0.0-alpha.10 # 4.0.0-alpha.10
2018-June-21 2018-June-21

View File

@ -331,7 +331,7 @@ yarn bootstrap --reset --core
```sh ```sh
# publish and tag the release # publish and tag the release
npm run publish -- --concurrency 1 --npm-tag=alpha --force-publish=* npm run publish:alpha
# update the release page # update the release page
open https://github.com/storybooks/storybook/releases open https://github.com/storybooks/storybook/releases
@ -355,7 +355,7 @@ git commit -m "Changelog for vX.Y"
yarn bootstrap --reset --core yarn bootstrap --reset --core
# publish and tag the release # publish and tag the release
npm run publish -- --concurrency 1 --force-publish=* npm run publish
# update the release page # update the release page
open https://github.com/storybooks/storybook/releases open https://github.com/storybooks/storybook/releases

View File

@ -67,6 +67,24 @@ storiesOf('button', module)
)); ));
``` ```
If you want to add a11y globally to your stories, you can use the global Storybook decorator in your *.storybook/config.js* file:
```js
import { configure, addDecorator } from '@storybook/react';
import { checkA11y } from '@storybook/addon-a11y';
// pick all stories.js files within the src/ folder
const req = require.context('../src', true, /stories\.js$/);
addDecorator(checkA11y);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);
```
## Roadmap ## Roadmap
* Make UI accessibile * Make UI accessibile

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-a11y", "name": "@storybook/addon-a11y",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "a11y addon for storybook", "description": "a11y addon for storybook",
"keywords": [ "keywords": [
"a11y", "a11y",
@ -25,13 +25,12 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/client-logger": "4.0.0-alpha.10", "@storybook/client-logger": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"@storybook/core-events": "4.0.0-alpha.10", "@storybook/core-events": "4.0.0-alpha.14",
"axe-core": "^3.0.3", "axe-core": "^3.0.3",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"emotion": "^9.1.3",
"global": "^4.3.2", "global": "^4.3.2",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-emotion": "^9.1.3", "react-emotion": "^9.1.3",

View File

@ -1,52 +1,76 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import addons from '@storybook/addons'; import PropTypes from 'prop-types';
import styled from 'react-emotion'; import styled from 'react-emotion';
import { CHECK_EVENT_ID } from '../shared'; import { STORY_RENDERED } from '@storybook/core-events';
import { ActionBar, ActionButton } from '@storybook/components';
import { CHECK_EVENT_ID, RERUN_EVENT_ID, REQUEST_CHECK_EVENT_ID } from '../shared';
import Tabs from './Tabs'; import Tabs from './Tabs';
import Report from './Report'; import Report from './Report';
const Passes = styled('span')({ const Passes = styled('span')(({ theme }) => ({
color: '#0D6731', color: theme.successColor,
}); }));
const Violations = styled('span')({ const Violations = styled('span')(({ theme }) => ({
color: '#AC2300', color: theme.failColor,
}); }));
class Panel extends Component { class Panel extends Component {
constructor(props, ...args) { static propTypes = {
super(props, ...args); active: PropTypes.bool.isRequired,
this.state = { channel: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
};
state = {
passes: [], passes: [],
violations: [], violations: [],
}; };
this.channel = addons.getChannel();
this.onUpdate = this.onUpdate.bind(this);
}
componentDidMount() { componentDidMount() {
this.channel.on(CHECK_EVENT_ID, this.onUpdate); this.props.channel.on(CHECK_EVENT_ID, this.onUpdate);
this.props.channel.on(STORY_RENDERED, this.requestCheck);
this.props.channel.on(RERUN_EVENT_ID, this.requestCheck);
}
componentDidUpdate(prevProps) {
if (!prevProps.active && this.props.active) {
this.requestCheck();
}
} }
componentWillUnmount() { componentWillUnmount() {
this.channel.removeListener(CHECK_EVENT_ID, this.onUpdate); this.props.channel.removeListener(CHECK_EVENT_ID, this.onUpdate);
this.props.channel.removeListener(STORY_RENDERED, this.requestCheck);
this.props.channel.removeListener(RERUN_EVENT_ID, this.requestCheck);
} }
onUpdate({ passes, violations }) { onUpdate = ({ passes, violations }) => {
this.setState({ this.setState({
passes, passes,
violations, violations,
}); });
};
requestCheck = () => {
if (this.props.active) {
this.props.channel.emit(REQUEST_CHECK_EVENT_ID);
} }
};
render() { render() {
const { passes, violations } = this.state; const { passes, violations } = this.state;
const { active } = this.props;
return ( return active ? (
<div>
<Tabs <Tabs
tabs={[ tabs={[
{ {
@ -59,7 +83,11 @@ class Panel extends Component {
}, },
]} ]}
/> />
); <ActionBar>
<ActionButton onClick={this.requestCheck}>RERUN TEST</ActionButton>
</ActionBar>
</div>
) : null;
} }
} }

View File

@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
import styled from 'react-emotion'; import styled from 'react-emotion';
const Wrapper = styled('div')({ const Wrapper = styled('div')(({ theme }) => ({
backgroundColor: 'rgb(234, 234, 234)', backgroundColor: theme.barFill,
padding: '12px', padding: '12px',
marginBottom: '10px', marginBottom: '10px',
}); }));
const Help = styled('p')({ const Help = styled('p')({
margin: '0 0 12px', margin: '0 0 12px',
}); });
const Link = styled('a')({ const Link = styled('a')({
marginTop: '12px', marginTop: '12px',
textDecoration: 'underline', textDecoration: 'underline',
color: 'rgb(130, 130, 130)', color: 'inherit',
display: 'block', display: 'block',
}); });

View File

@ -8,19 +8,28 @@ import Info from './Info';
import Tags from './Tags'; import Tags from './Tags';
import Elements from './Elements'; import Elements from './Elements';
const Wrapper = styled('div')({ const Wrapper = styled('div')(({ theme }) => ({
padding: '0 14px', padding: '0 14px',
cursor: 'pointer', cursor: 'pointer',
borderBottom: '1px solid rgb(234, 234, 234)', borderBottom: theme.mainBorder,
}); }));
const HeaderBar = styled('button')({ const HeaderBar = styled('button')(({ theme }) => ({
padding: '12px 0px', padding: '12px 0px',
display: 'block', display: 'block',
width: '100%', width: '100%',
border: 0, border: 0,
background: 'none', background: 'none',
}); color: 'inherit',
borderTop: '3px solid transparent',
borderBottom: '3px solid transparent',
'&:focus': {
outline: '0 none',
borderBottom: `3px solid ${theme.highlightColor}`,
},
}));
class Item extends Component { class Item extends Component {
static propTypes = { static propTypes = {

View File

@ -1,17 +0,0 @@
import styled from 'react-emotion';
const RerunButton = styled('button')({
position: 'absolute',
bottom: 0,
right: 0,
border: 'none',
borderTop: 'solid 1px rgba(0, 0, 0, 0.2)',
borderLeft: 'solid 1px rgba(0, 0, 0, 0.2)',
background: 'rgba(255, 255, 255, 0.5)',
padding: '5px 10px',
borderRadius: '4px 0 0 0',
color: 'rgba(0, 0, 0, 0.5)',
textTransform: 'uppercase',
});
export default RerunButton;

View File

@ -9,14 +9,12 @@ const Wrapper = styled('div')({
margin: '12px 0', margin: '12px 0',
}); });
const Item = styled('div')({ const Item = styled('div')(({ theme }) => ({
margin: '0 6px', margin: '0 6px',
padding: '5px', padding: '5px',
border: '1px solid rgb(234, 234, 234)', border: theme.mainBorder,
borderRadius: '2px', borderRadius: theme.mainBorderRadius,
color: 'rgb(130, 130, 130)', }));
fontSize: '12px',
});
function Tags({ tags }) { function Tags({ tags }) {
return <Wrapper>{tags.map(tag => <Item key={tag}>{tag}</Item>)}</Wrapper>; return <Wrapper>{tags.map(tag => <Item key={tag}>{tag}</Item>)}</Wrapper>;

View File

@ -1,18 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import addons from '@storybook/addons';
import { Placeholder } from '@storybook/components'; import { Placeholder } from '@storybook/components';
import { RERUN_EVENT_ID } from '../../shared';
import RerunButton from './RerunButton';
import Item from './Item'; import Item from './Item';
function onRerunClick() {
const channel = addons.getChannel();
channel.emit(RERUN_EVENT_ID);
}
const Report = ({ items, empty, passes }) => ( const Report = ({ items, empty, passes }) => (
<div> <div>
{items.length ? ( {items.length ? (
@ -20,7 +11,6 @@ const Report = ({ items, empty, passes }) => (
) : ( ) : (
<Placeholder>{empty}</Placeholder> <Placeholder>{empty}</Placeholder>
)} )}
<RerunButton onClick={onRerunClick}>Re-run tests</RerunButton>
</div> </div>
); );

View File

@ -6,13 +6,14 @@ import styled from 'react-emotion';
const Container = styled('div')({ const Container = styled('div')({
width: '100%', width: '100%',
position: 'relative', position: 'relative',
minHeight: '100%',
}); });
const List = styled('div')({ const List = styled('div')(({ theme }) => ({
borderBottom: '1px solid rgb(234, 234, 234)', borderBottom: theme.mainBorder,
flexWrap: 'wrap', flexWrap: 'wrap',
display: 'flex', display: 'flex',
}); }));
const Item = styled('button')( const Item = styled('button')(
({ active }) => ({ active }) =>
@ -22,9 +23,7 @@ const Item = styled('button')(
fontWeight: 600, fontWeight: 600,
} }
: {}, : {},
{ ({ theme }) => ({
color: 'rgb(68, 68, 68)',
fontSize: '11px',
textDecoration: 'none', textDecoration: 'none',
textTransform: 'uppercase', textTransform: 'uppercase',
padding: '10px 15px', padding: '10px 15px',
@ -33,9 +32,16 @@ const Item = styled('button')(
fontWeight: 500, fontWeight: 500,
opacity: 0.7, opacity: 0.7,
border: 'none', border: 'none',
borderTop: '3px solid transparent',
borderBottom: '3px solid transparent',
background: 'none', background: 'none',
flex: 1, flex: 1,
}
'&:focus': {
outline: '0 none',
borderBottom: `3px solid ${theme.highlightColor}`,
},
})
); );
class Tabs extends Component { class Tabs extends Component {

View File

@ -4,7 +4,7 @@ import addons from '@storybook/addons';
import Events from '@storybook/core-events'; import Events from '@storybook/core-events';
import { logger } from '@storybook/client-logger'; import { logger } from '@storybook/client-logger';
import { CHECK_EVENT_ID, RERUN_EVENT_ID } from './shared'; import { CHECK_EVENT_ID, REQUEST_CHECK_EVENT_ID } from './shared';
let axeOptions = {}; let axeOptions = {};
@ -23,11 +23,10 @@ const runA11yCheck = () => {
const a11ySubscription = () => { const a11ySubscription = () => {
const channel = addons.getChannel(); const channel = addons.getChannel();
channel.on(Events.STORY_RENDERED, runA11yCheck); channel.on(REQUEST_CHECK_EVENT_ID, runA11yCheck);
channel.on(RERUN_EVENT_ID, runA11yCheck);
return () => { return () => {
channel.removeListener(Events.STORY_RENDERED, runA11yCheck); channel.removeListener(REQUEST_CHECK_EVENT_ID, runA11yCheck);
channel.removeListener(RERUN_EVENT_ID, runA11yCheck);
}; };
}; };

View File

@ -5,12 +5,12 @@ import Panel from './components/Panel';
import { ADDON_ID, PANEL_ID } from './shared'; import { ADDON_ID, PANEL_ID } from './shared';
function init() { function init() {
addons.register(ADDON_ID, () => { addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
addons.addPanel(PANEL_ID, { addons.addPanel(PANEL_ID, {
title: 'Accessibility', title: 'Accessibility',
render() { // eslint-disable-next-line react/prop-types
return <Panel />; render: ({ active }) => <Panel channel={channel} api={api} active={active} />,
},
}); });
}); });
} }

View File

@ -3,5 +3,6 @@ const ADDON_ID = '@storybook/addon-a11y';
const PANEL_ID = `${ADDON_ID}/panel`; const PANEL_ID = `${ADDON_ID}/panel`;
const CHECK_EVENT_ID = `${ADDON_ID}/check`; const CHECK_EVENT_ID = `${ADDON_ID}/check`;
const RERUN_EVENT_ID = `${ADDON_ID}/rerun`; const RERUN_EVENT_ID = `${ADDON_ID}/rerun`;
const REQUEST_CHECK_EVENT_ID = `${ADDON_ID}/request-check`;
export { ADDON_ID, PANEL_ID, CHECK_EVENT_ID, RERUN_EVENT_ID }; export { ADDON_ID, PANEL_ID, CHECK_EVENT_ID, RERUN_EVENT_ID, REQUEST_CHECK_EVENT_ID };

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-actions", "name": "@storybook/addon-actions",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Action Logger addon for storybook", "description": "Action Logger addon for storybook",
"keywords": [ "keywords": [
"storybook" "storybook"
@ -20,12 +20,12 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"@storybook/core-events": "4.0.0-alpha.10", "@storybook/core-events": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"emotion": "^9.1.3", "emotion-theming": "^9.1.2",
"global": "^4.3.2", "global": "^4.3.2",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"make-error": "^1.3.4", "make-error": "^1.3.4",

View File

@ -1,20 +1,21 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React from 'react';
import Inspector from 'react-inspector'; import Inspector from 'react-inspector';
import { Actions, Action, Button, Wrapper, InspectorContainer, Countwrap, Counter } from './style'; import { withTheme } from 'emotion-theming';
class ActionLogger extends Component { import { ActionBar, ActionButton } from '@storybook/components';
getActionData() {
return this.props.actions.map(action => this.renderAction(action));
}
renderAction(action) { import { Actions, Action, Wrapper, InspectorContainer, Countwrap, Counter } from './style';
const counter = <Counter>{action.count}</Counter>;
return ( const ActionLogger = withTheme(({ actions, onClear, theme }) => (
<Wrapper>
<Actions>
{actions.map(action => (
<Action key={action.id}> <Action key={action.id}>
<Countwrap>{action.count > 1 && counter}</Countwrap> <Countwrap>{action.count > 1 && <Counter>{action.count}</Counter>}</Countwrap>
<InspectorContainer> <InspectorContainer>
<Inspector <Inspector
theme={theme.addonActionsTheme || 'chromeLight'}
sortObjectKeys sortObjectKeys
showNonenumerable={false} showNonenumerable={false}
name={action.data.name} name={action.data.name}
@ -22,27 +23,26 @@ class ActionLogger extends Component {
/> />
</InspectorContainer> </InspectorContainer>
</Action> </Action>
); ))}
} </Actions>
render() { <ActionBar>
return ( <ActionButton onClick={onClear}>CLEAR</ActionButton>
<Wrapper> </ActionBar>
<Actions>{this.getActionData()}</Actions>
<Button onClick={this.props.onClear}>Clear</Button>
</Wrapper> </Wrapper>
); ));
}
}
ActionLogger.propTypes = { ActionLogger.propTypes = {
onClear: PropTypes.func, onClear: PropTypes.func.isRequired,
// eslint-disable-next-line react/forbid-prop-types actions: PropTypes.arrayOf(
actions: PropTypes.array, PropTypes.shape({
}; count: PropTypes.node,
ActionLogger.defaultProps = { data: PropTypes.shape({
onClear: () => {}, name: PropTypes.node.isRequired,
actions: [], args: PropTypes.any,
}),
})
).isRequired,
}; };
export default ActionLogger; export default ActionLogger;

View File

@ -1,5 +1,4 @@
import styled from 'react-emotion'; import styled from 'react-emotion';
import { Button as BaseButton } from '@storybook/components';
export const Actions = styled('pre')({ export const Actions = styled('pre')({
flex: 1, flex: 1,
@ -12,24 +11,12 @@ export const Actions = styled('pre')({
export const Action = styled('div')({ export const Action = styled('div')({
display: 'flex', display: 'flex',
padding: '3px 3px 3px 0', padding: '3px 3px 3px 0',
borderLeft: '5px solid white', borderLeft: '5px solid transparent',
borderBottom: '1px solid #fafafa', borderBottom: '1px solid transparent',
transition: 'all 0.1s', transition: 'all 0.1s',
alignItems: 'start', alignItems: 'start',
}); });
export const Button = styled(BaseButton)({
position: 'absolute',
bottom: 0,
right: 0,
borderRadius: '4px 0 0 0',
textTransform: 'uppercase',
letterSpacing: 1,
paddingTop: 5,
paddingBottom: 5,
border: '0 none',
});
export const Counter = styled('div')({ export const Counter = styled('div')({
margin: '0 5px 0 5px', margin: '0 5px 0 5px',
backgroundColor: '#777777', backgroundColor: '#777777',
@ -51,4 +38,5 @@ export const Wrapper = styled('div')({
flex: 1, flex: 1,
display: 'flex', display: 'flex',
position: 'relative', position: 'relative',
minHeight: '100%',
}); });

View File

@ -56,21 +56,25 @@ export default class ActionLogger extends React.Component {
} }
render() { render() {
const { active } = this.props;
const props = { const props = {
actions: this.state.actions, actions: this.state.actions,
onClear: () => this.clearActions(), onClear: () => this.clearActions(),
}; };
return <ActionLoggerComponent {...props} />; return active ? <ActionLoggerComponent {...props} /> : null;
} }
} }
ActionLogger.propTypes = { ActionLogger.propTypes = {
// eslint-disable-next-line react/forbid-prop-types active: PropTypes.bool.isRequired,
channel: PropTypes.object, channel: PropTypes.shape({
emit: PropTypes.func,
on: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
api: PropTypes.shape({ api: PropTypes.shape({
onStory: PropTypes.func.isRequired, onStory: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired, }).isRequired,
}; };
ActionLogger.defaultProps = {
channel: {},
};

View File

@ -8,7 +8,8 @@ export function register() {
const channel = addons.getChannel(); const channel = addons.getChannel();
addons.addPanel(PANEL_ID, { addons.addPanel(PANEL_ID, {
title: 'Action Logger', title: 'Action Logger',
render: () => <ActionLogger channel={channel} api={api} />, // eslint-disable-next-line react/prop-types
render: ({ active }) => <ActionLogger channel={channel} api={api} active={active} />,
}); });
}); });
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-backgrounds", "name": "@storybook/addon-backgrounds",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "A storybook addon to show different backgrounds for your preview", "description": "A storybook addon to show different backgrounds for your preview",
"keywords": [ "keywords": [
"addon", "addon",
@ -24,10 +24,9 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/core-events": "4.0.0-alpha.10", "@storybook/core-events": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"emotion": "^9.1.3",
"global": "^4.3.2", "global": "^4.3.2",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-emotion": "^9.1.3", "react-emotion": "^9.1.3",

View File

@ -16,14 +16,13 @@ const Title = styled('h5')({
fontSize: 16, fontSize: 16,
}); });
const Pre = styled('pre')({ const Pre = styled('pre')(({ theme }) => ({
padding: '30px', padding: '30px',
display: 'block', display: 'block',
background: 'rgba(19,19,19,0.9)', background: theme.fillColor,
color: 'rgba(255,255,255,0.95)',
marginTop: '15px', marginTop: '15px',
lineHeight: '1.75em', lineHeight: '1.75em',
}); }));
const List = styled('div')({ const List = styled('div')({
display: 'inline-block', display: 'inline-block',
@ -47,13 +46,13 @@ const defaultBackground = {
}; };
const instructionsHtml = ` const instructionsHtml = `
import { storiesOf } from "@storybook/react"; import { storiesOf } from '@storybook/react';
import { withBackgrounds } from "@storybook/addon-backgrounds"; import { withBackgrounds } from '@storybook/addon-backgrounds';
storiesOf("First Component", module) storiesOf('First Component', module)
.addDecorator(withBackgrounds([ .addDecorator(withBackgrounds([
{ name: "twitter", value: "#00aced" }, { name: 'twitter', value: '#00aced' },
{ name: "facebook", value: "#3b5998" }, { name: 'facebook', value: '#3b5998" },
])) ]))
.add("First Button", () => <button>Click me</button>); .add("First Button", () => <button>Click me</button>);
`.trim(); `.trim();
@ -130,8 +129,11 @@ export default class BackgroundPanel extends Component {
} }
render() { render() {
const { active } = this.props;
const backgrounds = [...this.state.backgrounds]; const backgrounds = [...this.state.backgrounds];
if (!active) {
return null;
}
if (!backgrounds.length) return <Instructions />; if (!backgrounds.length) return <Instructions />;
const hasDefault = backgrounds.filter(x => x.default).length; const hasDefault = backgrounds.filter(x => x.default).length;
@ -149,6 +151,7 @@ export default class BackgroundPanel extends Component {
} }
} }
BackgroundPanel.propTypes = { BackgroundPanel.propTypes = {
active: PropTypes.bool.isRequired,
api: PropTypes.shape({ api: PropTypes.shape({
getQueryParam: PropTypes.func, getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func, setQueryParams: PropTypes.func,

View File

@ -3,24 +3,26 @@ import PropTypes from 'prop-types';
import styled from 'react-emotion'; import styled from 'react-emotion';
const Button = styled('button')({ const Button = styled('button')(({ theme }) => ({
listStyle: 'none', listStyle: 'none',
backgroundColor: '#fff', backgroundColor: theme.barFill,
textAlign: 'center', textAlign: 'center',
border: '1px solid rgba(0,0,0,0.1)', border: theme.mainBorder,
borderRadius: 4, borderRadius: theme.mainBorderRadius,
color: 'inherit',
cursor: 'pointer', cursor: 'pointer',
display: 'inline-block', display: 'inline-block',
width: 175, width: 175,
verticalAlign: 'top', verticalAlign: 'top',
wordWrap: 'break-word', wordWrap: 'break-word',
padding: 0, padding: 0,
}); overflow: 'hidden',
const Block = styled('div')(({ bg }) => ({ }));
const Block = styled('div')(({ bg, theme }) => ({
height: 80, height: 80,
borderRadius: '4px 4px 0 0',
transition: 'opacity 0.25s ease-in-out', transition: 'opacity 0.25s ease-in-out',
borderBottom: '1px solid rgba(0,0,0,0.1)', borderBottom: theme.mainBorder,
background: bg, background: bg,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',

View File

@ -30,26 +30,26 @@ jest.mock('global', () => ({
describe('Background Panel', () => { describe('Background Panel', () => {
it('should exist', () => { it('should exist', () => {
const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} />); const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} active />);
expect(backgroundPanel).toBeDefined(); expect(backgroundPanel).toBeDefined();
}); });
it('should have a default background value of transparent', () => { it('should have a default background value of transparent', () => {
const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} />); const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} active />);
expect(backgroundPanel.state().backgrounds).toHaveLength(0); expect(backgroundPanel.state().backgrounds).toHaveLength(0);
}); });
it('should show setup instructions if no colors provided', () => { it('should show setup instructions if no colors provided', () => {
const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} />); const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} active />);
expect(backgroundPanel.html().match(/Setup Instructions/gim).length).toBeGreaterThan(0); expect(backgroundPanel.html().match(/Setup Instructions/gim).length).toBeGreaterThan(0);
}); });
it('should set the query string', () => { it('should set the query string', () => {
const SpiedChannel = new EventEmitter(); const SpiedChannel = new EventEmitter();
mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />); mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />);
SpiedChannel.emit(Events.SET, backgrounds); SpiedChannel.emit(Events.SET, backgrounds);
expect(mockedApi.getQueryParam).toBeCalledWith('background'); expect(mockedApi.getQueryParam).toBeCalledWith('background');
@ -57,7 +57,7 @@ describe('Background Panel', () => {
it('should not unset the query string', () => { it('should not unset the query string', () => {
const SpiedChannel = new EventEmitter(); const SpiedChannel = new EventEmitter();
mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />); mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />);
SpiedChannel.emit(Events.UNSET, []); SpiedChannel.emit(Events.UNSET, []);
expect(mockedApi.setQueryParams).not.toHaveBeenCalled(); expect(mockedApi.setQueryParams).not.toHaveBeenCalled();
@ -65,7 +65,9 @@ describe('Background Panel', () => {
it('should accept colors through channel and render the correct swatches with a default swatch', () => { it('should accept colors through channel and render the correct swatches with a default swatch', () => {
const SpiedChannel = new EventEmitter(); const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />); const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
SpiedChannel.emit(Events.SET, backgrounds); SpiedChannel.emit(Events.SET, backgrounds);
expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds); expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds);
@ -73,7 +75,9 @@ describe('Background Panel', () => {
it('should allow setting a default swatch', () => { it('should allow setting a default swatch', () => {
const SpiedChannel = new EventEmitter(); const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />); const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
const [head, ...tail] = backgrounds; const [head, ...tail] = backgrounds;
const localBgs = [{ ...head, default: true }, ...tail]; const localBgs = [{ ...head, default: true }, ...tail];
SpiedChannel.emit(Events.SET, localBgs); SpiedChannel.emit(Events.SET, localBgs);
@ -88,7 +92,9 @@ describe('Background Panel', () => {
it('should allow the default swatch become the background color', () => { it('should allow the default swatch become the background color', () => {
const SpiedChannel = new EventEmitter(); const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />); const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
const [head, second, ...tail] = backgrounds; const [head, second, ...tail] = backgrounds;
const localBgs = [head, { ...second, default: true }, ...tail]; const localBgs = [head, { ...second, default: true }, ...tail];
SpiedChannel.on('background', bg => { SpiedChannel.on('background', bg => {
@ -106,7 +112,9 @@ describe('Background Panel', () => {
it('should unset all swatches on receiving the background-unset message', () => { it('should unset all swatches on receiving the background-unset message', () => {
const SpiedChannel = new EventEmitter(); const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />); const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
SpiedChannel.emit(Events.SET, backgrounds); SpiedChannel.emit(Events.SET, backgrounds);
expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds); expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds);
@ -118,7 +126,9 @@ describe('Background Panel', () => {
it('should set iframe background', () => { it('should set iframe background', () => {
const SpiedChannel = new EventEmitter(); const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} />); const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
backgroundPanel.setState({ backgrounds }); // force re-render backgroundPanel.setState({ backgrounds }); // force re-render
backgroundPanel backgroundPanel

View File

@ -15,6 +15,7 @@ export const withBackgrounds = makeDecorator({
name: 'backgrounds', name: 'backgrounds',
parameterName: 'backgrounds', parameterName: 'backgrounds',
skipIfNoParametersOrOptions: true, skipIfNoParametersOrOptions: true,
allowDeprecatedUsage: true,
wrapper: (getStory, context, { options, parameters }) => { wrapper: (getStory, context, { options, parameters }) => {
const backgrounds = parameters || options; const backgrounds = parameters || options;

View File

@ -10,6 +10,7 @@ addons.register(ADDON_ID, api => {
const channel = addons.getChannel(); const channel = addons.getChannel();
addons.addPanel(PANEL_ID, { addons.addPanel(PANEL_ID, {
title: 'Backgrounds', title: 'Backgrounds',
render: () => <BackgroundPanel channel={channel} api={api} />, // eslint-disable-next-line react/prop-types
render: ({ active }) => <BackgroundPanel channel={channel} api={api} active={active} />,
}); });
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-centered", "name": "@storybook/addon-centered",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook decorator to center components", "description": "Storybook decorator to center components",
"license": "MIT", "license": "MIT",
"author": "Muhammed Thanish <mnmtanish@gmail.com>", "author": "Muhammed Thanish <mnmtanish@gmail.com>",

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-events", "name": "@storybook/addon-events",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Add events to your Storybook stories.", "description": "Add events to your Storybook stories.",
"keywords": [ "keywords": [
"addon", "addon",
@ -19,10 +19,9 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/core-events": "4.0.0-alpha.10", "@storybook/core-events": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"emotion": "^9.1.3",
"format-json": "^1.0.3", "format-json": "^1.0.3",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-emotion": "^9.1.3", "react-emotion": "^9.1.3",

View File

@ -10,10 +10,12 @@ const Wrapper = styled('div')({
width: '100%', width: '100%',
boxSizing: 'border-box', boxSizing: 'border-box',
padding: '10px', padding: '10px',
minHeight: '100%',
}); });
export default class Events extends Component { export default class Events extends Component {
static propTypes = { static propTypes = {
active: PropTypes.bool.isRequired,
channel: PropTypes.shape({ channel: PropTypes.shape({
on: PropTypes.func, on: PropTypes.func,
emit: PropTypes.func, emit: PropTypes.func,
@ -43,10 +45,11 @@ export default class Events extends Component {
render() { render() {
const { events } = this.state; const { events } = this.state;
return ( const { active } = this.props;
return active ? (
<Wrapper> <Wrapper>
{events.map(event => <Event key={event.name} {...event} onEmit={this.onEmit} />)} {events.map(event => <Event key={event.name} {...event} onEmit={this.onEmit} />)}
</Wrapper> </Wrapper>
); ) : null;
} }
} }

View File

@ -6,9 +6,11 @@ import { ADDON_ID, PANEL_ID } from './constants';
export function register() { export function register() {
addons.register(ADDON_ID, () => { addons.register(ADDON_ID, () => {
const channel = addons.getChannel();
addons.addPanel(PANEL_ID, { addons.addPanel(PANEL_ID, {
title: 'Events', title: 'Events',
render: () => <Panel channel={addons.getChannel()} />, // eslint-disable-next-line react/prop-types
render: ({ active }) => <Panel channel={channel} active={active} />,
}); });
}); });
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-graphql", "name": "@storybook/addon-graphql",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook addon to display the GraphiQL IDE", "description": "Storybook addon to display the GraphiQL IDE",
"keywords": [ "keywords": [
"storybook" "storybook"

View File

@ -7,7 +7,7 @@
[![Storybook Slack](https://now-examples-slackin-rrirkqohko.now.sh/badge.svg)](https://now-examples-slackin-rrirkqohko.now.sh/) [![Storybook Slack](https://now-examples-slackin-rrirkqohko.now.sh/badge.svg)](https://now-examples-slackin-rrirkqohko.now.sh/)
[![Backers on Open Collective](https://opencollective.com/storybook/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/storybook/sponsors/badge.svg)](#sponsors) [![Backers on Open Collective](https://opencollective.com/storybook/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/storybook/sponsors/badge.svg)](#sponsors)
* * * ---
Storybook Info Addon will show additional information for your stories in [Storybook](https://storybook.js.org). Storybook Info Addon will show additional information for your stories in [Storybook](https://storybook.js.org).
Useful when you want to display usage or other types of documentation alongside your story. Useful when you want to display usage or other types of documentation alongside your story.
@ -25,6 +25,7 @@ npm i -D @storybook/addon-info
``` ```
## Basic usage ## Basic usage
Then, add `withInfo` as a decarator to your book of stories. Then, add `withInfo` as a decarator to your book of stories.
It is possible to add `info` by default to all or a subsection of stories by using a global or story decorator. It is possible to add `info` by default to all or a subsection of stories by using a global or story decorator.
@ -33,7 +34,9 @@ It is important to declare this decorator as **the first decorator**, otherwise
```js ```js
addDecorator(withInfo); // Globally in your .storybook/config.js. addDecorator(withInfo); // Globally in your .storybook/config.js.
``` ```
or or
```js ```js
storiesOf('Component', module) storiesOf('Component', module)
.addDecorator(withInfo) // At your stories directly. .addDecorator(withInfo) // At your stories directly.
@ -53,12 +56,13 @@ storiesOf('Component', module)
.addParameters({ .addParameters({
info: { info: {
// Your settings // Your settings
} },
}) })
.add('with some emoji', () => <Component/>); .add('with some emoji', () => <Component />);
``` ```
...or for each story individually: ...or for each story individually:
```js ```js
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
@ -67,12 +71,12 @@ import Component from './Component';
storiesOf('Component', module) storiesOf('Component', module)
.add( .add(
'with some emoji', 'with some emoji',
() => <Component emoji/>, () => <Component emoji />,
{ info : { inline: false, header: false } } // Make your component render inline with the additional info { info: { inline: true, header: false } } // Make your component render inline with the additional info
) )
.add( .add(
'with no emoji', 'with no emoji',
() => <Component/>, () => <Component />,
{ info: '☹️ no emojis' } // Add additional info text directly { info: '☹️ no emojis' } // Add additional info text directly
); );
``` ```
@ -86,41 +90,36 @@ import Component from './Component';
storiesOf('Component', module) storiesOf('Component', module)
.addParameters({ .addParameters({
info: { // Make a default for all stories in this book, info: {
// Make a default for all stories in this book,
inline: true, // where the components are inlined inline: true, // where the components are inlined
styles: { styles: {
header: { header: {
h1: { h1: {
color: 'red' // and the headers of the sections are red. color: 'red', // and the headers of the sections are red.
} },
} },
},
}, },
}
}) })
.add( .add('green version', () => <Component green />, {
'green version',
() => <Component green/>,
{
info: { info: {
styles: stylesheet => ({ // Setting the style with a function styles: stylesheet => ({
// Setting the style with a function
...stylesheet, ...stylesheet,
header: { header: {
...stylesheet.header, ...stylesheet.header,
h1: { h1: {
...stylesheet.header.h1, ...stylesheet.header.h1,
color: 'green' // Still inlined but with green headers! color: 'green', // Still inlined but with green headers!
} },
} },
}),
},
}) })
} .add('something else', () => <Component different />, {
}) info: 'This story has additional text added to the info!', // Still inlined and with red headers!
.add( });
'something else',
() => <Component different/>,
{
info: "This story has additional text added to the info!" // Still inlined and with red headers!
}
);
``` ```
It is also possible to disable the `info` addon entirely. It is also possible to disable the `info` addon entirely.
@ -135,16 +134,12 @@ Depending on the scope at which you want to disable the addon, pass the followin
``` ```
## Markdown ## Markdown
The `info` addon also supports markdown. The `info` addon also supports markdown.
To use markdown as additional textual documentation for your stories, either pass it directly as a String to the `info` parameters, or use the `text` option. To use markdown as additional textual documentation for your stories, either pass it directly as a String to the `info` parameters, or use the `text` option.
```js ```js
storiesOf('Button', module) storiesOf('Button', module).add('Button Component', () => <Button />, {
.add(
'Button Component',
() => <Button />,
{
info: { info: {
text: ` text: `
description or documentation about my component, supports markdown description or documentation about my component, supports markdown
@ -152,10 +147,9 @@ storiesOf('Button', module)
~~~js ~~~js
<Button>Click Here</Button> <Button>Click Here</Button>
~~~ ~~~
` `,
} },
} });
);
``` ```
## Setting Global Options ## Setting Global Options
@ -166,9 +160,11 @@ To configure default options for all usage of the info addon, pass a option obje
// config.js // config.js
import { withInfo } from '@storybook/addon-info'; import { withInfo } from '@storybook/addon-info';
addDecorator(withInfo({ addDecorator(
withInfo({
header: false, // Global configuration for the info addon across all of your stories. header: false, // Global configuration for the info addon across all of your stories.
})); })
);
``` ```
Configuration parameters can be set at 3 different locations: passed as default options along the `addDecorator` call, passed as an object of parameters to a book of stories to the `addParameters` call, and passed as direct parameters to each individual story. Configuration parameters can be set at 3 different locations: passed as default options along the `addDecorator` call, passed as an object of parameters to a book of stories to the `addParameters` call, and passed as direct parameters to each individual story.
@ -274,40 +270,40 @@ Example:
```js ```js
// button.js // button.js
// @flow // @flow
import React from 'react' import React from 'react';
const paddingStyles = { const paddingStyles = {
small: '4px 8px', small: '4px 8px',
medium: '8px 16px' medium: '8px 16px',
} };
const Button = ({ const Button = ({
size, size,
...rest ...rest
}: { }: {
/** The size of the button */ /** The size of the button */
size: 'small' | 'medium' size: 'small' | 'medium',
}) => { }) => {
const style = { const style = {
padding: paddingStyles[size] || '' padding: paddingStyles[size] || '',
} };
return <button style={style} {...rest} /> return <button style={style} {...rest} />;
} };
Button.defaultProps = { Button.defaultProps = {
size: 'medium' size: 'medium',
} };
export default Button export default Button;
``` ```
```js ```js
// stories.js // stories.js
import React from "react"; import React from 'react';
import { storiesOf } from "@storybook/react"; import { storiesOf } from '@storybook/react';
import { withInfo } from "@storybook/addon-info"; import Button from './button';
import Button from "./button";
const Red = props => <span style={{ color: "red" }} {...props} />; const Red = props => <span style={{ color: 'red' }} {...props} />;
const TableComponent = ({ propDefinitions }) => { const TableComponent = ({ propDefinitions }) => {
const props = propDefinitions.map( const props = propDefinitions.map(
@ -341,12 +337,11 @@ const TableComponent = ({ propDefinitions }) => {
); );
}; };
storiesOf("Button", module).add( storiesOf('Button', module).add('with text', () => <Button>Hello Button</Button>, {
"with text", info: {
withInfo({ TableComponent,
TableComponent },
})(() => <Button>Hello Button</Button>) });
);
``` ```
### React Docgen Integration ### React Docgen Integration
@ -359,10 +354,11 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
/** Button component description */ /** Button component description */
const DocgenButton = ({ disabled, label, style, onClick }) => const DocgenButton = ({ disabled, label, style, onClick }) => (
<button disabled={disabled} style={style} onClick={onClick}> <button disabled={disabled} style={style} onClick={onClick}>
{label} {label}
</button>; </button>
);
DocgenButton.defaultProps = { DocgenButton.defaultProps = {
disabled: false, disabled: false,

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-info", "name": "@storybook/addon-info",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "A Storybook addon to show additional information for your stories.", "description": "A Storybook addon to show additional information for your stories.",
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,12 +13,11 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/client-logger": "4.0.0-alpha.10", "@storybook/client-logger": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"core-js": "2.5.7", "core-js": "2.5.7",
"emotion": "^9.1.3",
"global": "^4.3.2", "global": "^4.3.2",
"marksy": "^6.0.3", "marksy": "^6.0.3",
"nested-object-assign": "^1.0.1", "nested-object-assign": "^1.0.1",

View File

@ -33,6 +33,9 @@ exports[`addon Info should render <Info /> and external markdown 1`] = `
-webkit-align-self: flex-start; -webkit-align-self: flex-start;
-ms-flex-item-align: start; -ms-flex-item-align: start;
align-self: flex-start; align-self: flex-start;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
} }
.emotion-1:hover { .emotion-1:hover {
@ -1335,6 +1338,9 @@ exports[`addon Info should render <Info /> and markdown 1`] = `
-webkit-align-self: flex-start; -webkit-align-self: flex-start;
-ms-flex-item-align: start; -ms-flex-item-align: start;
align-self: flex-start; align-self: flex-start;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
} }
.emotion-1:hover { .emotion-1:hover {

View File

@ -12,6 +12,7 @@ const Button = styled('button')(
fontSize: 13, fontSize: 13,
padding: '3px 10px', padding: '3px 10px',
alignSelf: 'flex-start', alignSelf: 'flex-start',
flexShrink: 0,
':hover': { ':hover': {
backgroundColor: '#f4f7fa', backgroundColor: '#f4f7fa',

View File

@ -87,6 +87,7 @@ function addInfo(storyFn, context, infoOptions) {
export const withInfo = makeDecorator({ export const withInfo = makeDecorator({
name: 'withInfo', name: 'withInfo',
parameterName: 'info', parameterName: 'info',
allowDeprecatedUsage: true,
wrapper: (getStory, context, { options, parameters }) => { wrapper: (getStory, context, { options, parameters }) => {
const storyOptions = parameters || options; const storyOptions = parameters || options;
const infoOptions = typeof storyOptions === 'string' ? { text: storyOptions } : storyOptions; const infoOptions = typeof storyOptions === 'string' ? { text: storyOptions } : storyOptions;

View File

@ -31,16 +31,18 @@ When running **Jest**, be sure to save the results in a json file:
``` ```
You may want to add it the result file to `.gitignore`, since it's a generated file: You may want to add it the result file to `.gitignore`, since it's a generated file:
``` ```
jest-test-results.json jest-test-results.json
``` ```
But much like lockfiles and snapshots checking-in generated files can have certain advantages as well. It's up to you. But much like lockfiles and snapshots checking-in generated files can have certain advantages as well. It's up to you.
We recommend to **do** check in the test results file so starting storybook from an clean git clone doesn't require running all tests first, We recommend to **do** check in the test results file so starting storybook from an clean git clone doesn't require running all tests first,
but this can mean you'll experience merge conflicts on this file in the future. (*re-generating this file is super easy though, just like lockfiles and snapshots*) but this can mean you'll experience merge conflicts on this file in the future. (_re-generating this file is super easy though, just like lockfiles and snapshots_)
## Generating the test results ## Generating the test results
You need to make sure the generated test-results file exists before you start storybook. You need to make sure the generated test-restuls file exists before you start storybook.
During development you will likely start jest in watch-mode During development you will likely start jest in watch-mode
and so the json file will be re-generated every time code or tests change. and so the json file will be re-generated every time code or tests change.
@ -53,6 +55,7 @@ This change will then be HMR (hot module reloaded) using webpack and displayed b
If you want to pre-run jest automaticly during development or a static build, If you want to pre-run jest automaticly during development or a static build,
you may need to consider that if your tests fail, the script receives a non-0 exit code and will exit. you may need to consider that if your tests fail, the script receives a non-0 exit code and will exit.
You could create a `prebuild:storybook` npm script, which will never fail by appending `|| true`: You could create a `prebuild:storybook` npm script, which will never fail by appending `|| true`:
```json ```json
"scripts": { "scripts": {
"test:generate-output": "jest --json --outputFile=.jest-test-results.json || true", "test:generate-output": "jest --json --outputFile=.jest-test-results.json || true",
@ -83,40 +86,57 @@ import results from '../.jest-test-results.json';
import { withTests } from '@storybook/addon-jest'; import { withTests } from '@storybook/addon-jest';
storiesOf('MyComponent', module) storiesOf('MyComponent', module)
.addDecorator(withTests({ results })('MyComponent', 'MyOtherComponent')) .addDecorator(withTests({ results }))
.add('This story shows test results from MyComponent.test.js and MyOtherComponent.test.js', () => ( .add(
<div>Jest results in storybook</div> 'This story shows test results from MyComponent.test.js and MyOtherComponent.test.js',
)); () => <div>Jest results in storybook</div>,
{
jest: ['MyComponent.test.js', 'MyOtherComponent.test.js'],
}
);
``` ```
Or in order to avoid importing `.jest-test-results.json` in each story, you can create a simple file `withTests.js`: Or in order to avoid importing `.jest-test-results.json` in each story, simply add the decorator in your `.storybook/config.js` and results will display for stories that you have set the `jest` parameter on:
```js ```js
import results from '../.jest-test-results.json'; import { addDecorator } from '@storybook/react'; // <- or your view layer
import { withTests } from '@storybook/addon-jest'; import { withTests } from '@storybook/addon-jest';
export default withTests({ import results from '../.jest-test-results.json';
addDecorator(
withTests({
results, results,
}); })
);
``` ```
Then in your story: Then in your story:
```js ```js
// import your file
import withTests from '.withTests';
storiesOf('MyComponent', module) storiesOf('MyComponent', module)
.addDecorator(withTests('MyComponent', 'MyOtherComponent')) // Use .addParameters if you want the same tests displayed for all stories of the component
.add('This story shows test results from MyComponent.test.js and MyOtherComponent.test.js', () => ( .addParameters({ jest: ['MyComponent', 'MyOtherComponent'] })
<div>Jest results in storybook</div> .add(
)); 'This story shows test results from MyComponent.test.js and MyOtherComponent.test.js',
() => <div>Jest results in storybook</div>
);
```
### Disabling
You can disable the addon for a single story by setting the `jest` parameter to `{disable: true}`:
```js
storiesOf('MyComponent', module).add('Story', () => <div>Jest results disabled here</div>, {
jest: { disable: true },
});
``` ```
### withTests(options) ### withTests(options)
- **options.results**: OBJECT jest output results. *mandatory* - **options.results**: OBJECT jest output results. _mandatory_
- **filesExt**: STRING test file extention. *optional*. This allow you to write "MyComponent" and not "MyComponent.test.js". It will be used as regex to find your file results. Default value is `((\\.specs?)|(\\.tests?))?(\\.js)?$`. That mean it will match: MyComponent.js, MyComponent.test.js, MyComponent.tests.js, MyComponent.spec.js, MyComponent.specs.js... - **filesExt**: STRING test file extention. _optional_. This allow you to write "MyComponent" and not "MyComponent.test.js". It will be used as regex to find your file results. Default value is `((\\.specs?)|(\\.tests?))?(\\.js)?$`. That mean it will match: MyComponent.js, MyComponent.test.js, MyComponent.tests.js, MyComponent.spec.js, MyComponent.specs.js...
## Usage with Angular ## Usage with Angular
@ -124,7 +144,7 @@ Assuming that you have created a test files `my.component.spec.ts` and `my-other
Configure Jest with [jest-preset-angular](https://www.npmjs.com/package/jest-preset-angular) Configure Jest with [jest-preset-angular](https://www.npmjs.com/package/jest-preset-angular)
In project`s `typings.d.ts` add In project`s`typings.d.ts` add
```ts ```ts
declare module '*.json' { declare module '*.json' {
@ -133,29 +153,31 @@ declare module '*.json' {
} }
``` ```
Create a simple file `withTests.ts`: In your `.storybook/config.ts`:
```ts ```ts
import * as results from '../.jest-test-results.json'; import { addDecorator } from '@storybook/angular';
import { withTests } from '@storybook/addon-jest'; import { withTests } from '@storybook/addon-jest';
export const wTests = withTests({ import * as results from '../.jest-test-results.json';
addDecorator(
withTests({
results, results,
filesExt: '((\\.specs?)|(\\.tests?))?(\\.ts)?$' filesExt: '((\\.specs?)|(\\.tests?))?(\\.ts)?$',
}); })
);
``` ```
Then in your story: Then in your story:
```js ```js
// import your file
import wTests from '.withTests';
storiesOf('MyComponent', module) storiesOf('MyComponent', module)
.addDecorator(wTests('my.component', 'my-other.component')) .addParameters({ jest: ['my.component', 'my-other.component'] })
.add('This story shows test results from my.component.spec.ts and my-other.component.spec.ts', () => ( .add(
<div>Jest results in storybook</div> 'This story shows test results from my.component.spec.ts and my-other.component.spec.ts',
)); () => <div>Jest results in storybook</div>
);
``` ```
##### Example [here](https://github.com/storybooks/storybook/tree/master/examples/angular-cli) ##### Example [here](https://github.com/storybooks/storybook/tree/master/examples/angular-cli)

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-jest", "name": "@storybook/addon-jest",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "React storybook addon that show component jest report", "description": "React storybook addon that show component jest report",
"keywords": [ "keywords": [
"addon", "addon",
@ -25,13 +25,13 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"emotion": "^9.1.3",
"global": "^4.3.2", "global": "^4.3.2",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-emotion": "^9.1.3" "react-emotion": "^9.1.3",
"util-deprecate": "^1.0.2"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"

View File

@ -2,8 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'react-emotion'; import styled from 'react-emotion';
import { baseFonts } from '@storybook/components';
import Indicator from './Indicator'; import Indicator from './Indicator';
import Result, { FailedResult } from './Result'; import Result, { FailedResult } from './Result';
import provideJestResult from '../hoc/provideJestResult'; import provideJestResult from '../hoc/provideJestResult';
@ -25,7 +23,6 @@ const Item = styled('li')({
const NoTests = styled('div')({ const NoTests = styled('div')({
padding: '10px 20px', padding: '10px 20px',
flex: 1, flex: 1,
...baseFonts,
}); });
const FileTitle = styled('h2')({ const FileTitle = styled('h2')({
@ -139,7 +136,6 @@ const Content = styled(({ tests, className }) => (
))({ ))({
padding: '10px 20px', padding: '10px 20px',
flex: '1 1 0%', flex: '1 1 0%',
...baseFonts,
}); });
const Panel = ({ tests }) => const Panel = ({ tests }) =>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const provideTests = Component => { const provideTests = Component =>
class TestProvider extends React.Component { class TestProvider extends React.Component {
static propTypes = { static propTypes = {
channel: PropTypes.shape({ channel: PropTypes.shape({
@ -11,21 +11,22 @@ const provideTests = Component => {
api: PropTypes.shape({ api: PropTypes.shape({
onStory: PropTypes.func, onStory: PropTypes.func,
}).isRequired, }).isRequired,
active: PropTypes.bool,
};
static defaultProps = {
active: true,
}; };
constructor(props) { state = {};
super(props);
this.state = {};
this.onAddTests = this.onAddTests.bind(this);
}
componentDidMount() { componentDidMount() {
this.stopListeningOnStory = this.props.api.onStory(() => { const { channel, api } = this.props;
this.stopListeningOnStory = api.onStory(() => {
this.onAddTests({}); this.onAddTests({});
}); });
this.props.channel.on('storybook/tests/add_tests', this.onAddTests); channel.on('storybook/tests/add_tests', this.onAddTests);
} }
componentWillUnmount() { componentWillUnmount() {
@ -35,16 +36,14 @@ const provideTests = Component => {
this.props.channel.removeListener('storybook/tests/add_tests', this.onAddTests); this.props.channel.removeListener('storybook/tests/add_tests', this.onAddTests);
} }
onAddTests({ kind, storyName, tests }) { onAddTests = ({ kind, storyName, tests }) => {
this.setState({ kind, storyName, tests }); this.setState({ kind, storyName, tests });
} };
render() { render() {
return <Component {...this.state} />; const { active } = this.props;
return active ? <Component {...this.state} /> : null;
} }
} };
return TestProvider;
};
export default provideTests; export default provideTests;

View File

@ -1,7 +1,8 @@
import addons from '@storybook/addons'; import addons from '@storybook/addons';
import deprecate from 'util-deprecate';
const findTestResults = (testFiles, jestTestResults, jestTestFilesExt) => const findTestResults = (testFiles, jestTestResults, jestTestFilesExt) =>
testFiles.map(name => { Array.from(testFiles).map(name => {
if (jestTestResults && jestTestResults.testResults) { if (jestTestResults && jestTestResults.testResults) {
return { return {
name, name,
@ -27,9 +28,27 @@ export const withTests = userOptions => {
}; };
const options = Object.assign({}, defaultOptions, userOptions); const options = Object.assign({}, defaultOptions, userOptions);
return (...testFiles) => (storyFn, { kind, story }) => { return (...args) => {
emitAddTests({ kind, story, testFiles, options }); if (typeof args[0] === 'string') {
return deprecate((story, { kind }) => {
emitAddTests({ kind, story, testFiles: args, options });
return storyFn(); return story();
}, 'Passing component filenames to the `@storybook/addon-jest` via `withTests` is deprecated. Instead, use the `jest` story parameter');
}
const [
story,
{
kind,
parameters: { jest: testFiles },
},
] = args;
if (testFiles && !testFiles.disable) {
emitAddTests({ kind, story, testFiles, options });
}
return story();
}; };
}; };

View File

@ -4,11 +4,11 @@ import addons from '@storybook/addons';
import PanelTitle from './components/PanelTitle'; import PanelTitle from './components/PanelTitle';
import Panel from './components/Panel'; import Panel from './components/Panel';
// Register the addon with a unique name.
addons.register('storybook/tests', api => { addons.register('storybook/tests', api => {
// Also need to set a unique name to the panel. const channel = addons.getChannel();
addons.addPanel('storybook/tests/panel', { addons.addPanel('storybook/tests/panel', {
title: <PanelTitle channel={addons.getChannel()} api={api} />, title: <PanelTitle channel={addons.getChannel()} api={api} />,
render: () => <Panel channel={addons.getChannel()} api={api} />, // eslint-disable-next-line react/prop-types
render: ({ active }) => <Panel channel={channel} api={api} active={active} />,
}); });
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-knobs", "name": "@storybook/addon-knobs",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook Addon Prop Editor Component", "description": "Storybook Addon Prop Editor Component",
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,22 +13,20 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"@storybook/core-events": "4.0.0-alpha.10", "@storybook/core-events": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"deep-equal": "^1.0.1", "copy-to-clipboard": "^3.0.8",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"fast-deep-equal": "^2.0.1",
"global": "^4.3.2", "global": "^4.3.2",
"insert-css": "^2.0.0",
"lodash.debounce": "^4.0.8",
"moment": "^2.22.2",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"qs": "^6.5.2",
"react-color": "^2.14.1", "react-color": "^2.14.1",
"react-datetime": "^2.14.0", "react-datetime": "^2.14.0",
"react-emotion": "^9.1.3", "react-emotion": "^9.1.3",
"react-lifecycles-compat": "^3.0.4", "react-lifecycles-compat": "^3.0.4",
"react-textarea-autosize": "^6.1.0",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,5 +1,5 @@
/* eslint no-underscore-dangle: 0 */ /* eslint no-underscore-dangle: 0 */
import deepEqual from 'deep-equal'; import deepEqual from 'fast-deep-equal';
import escape from 'escape-html'; import escape from 'escape-html';
import KnobStore from './KnobStore'; import KnobStore from './KnobStore';

View File

@ -1,3 +1,6 @@
const callArg = fn => fn();
const callAll = fns => fns.forEach(callArg);
export default class KnobStore { export default class KnobStore {
constructor() { constructor() {
this.store = {}; this.store = {};
@ -12,7 +15,12 @@ export default class KnobStore {
this.store[key] = value; this.store[key] = value;
this.store[key].used = true; this.store[key].used = true;
this.store[key].groupId = value.groupId; this.store[key].groupId = value.groupId;
this.callbacks.forEach(cb => cb());
// debounce the execution of the callbacks for 50 milliseconds
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(callAll, 50, this.callbacks);
} }
get(key) { get(key) {

View File

@ -1,110 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styled from 'react-emotion';
import { Placeholder } from '@storybook/components';
const Wrapper = styled('div')({
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
background: 'white',
borderRadius: 4,
border: 'solid 1px rgb(236, 236, 236)',
width: '100%',
});
const Bar = styled('div')({
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
borderBottom: 'solid 1px #eaeaea',
});
const Content = styled('div')({
flex: '1 1 0',
display: 'flex',
overflow: 'auto',
});
const Tab = styled('button')(({ active }) => ({
fontSize: 11,
letterSpacing: '1px',
padding: '10px 15px',
textTransform: 'uppercase',
transition: 'opacity 0.3s',
opacity: active ? 1 : 0.5,
maxHeight: 60,
overflow: 'hidden',
cursor: 'pointer',
background: 'transparent',
border: 'none',
}));
class GroupTabs extends Component {
renderTab(name, group) {
const onClick = e => {
e.preventDefault();
this.props.onGroupSelect(name);
};
let { title } = group;
if (typeof title === 'function') {
title = title();
}
return (
<Tab
type="button"
key={name}
onClick={onClick}
role="tab"
active={this.props.selectedGroup === name}
>
{title}
</Tab>
);
}
render() {
const entries = this.props.groups ? Object.entries(this.props.groups) : null;
return entries && entries.length ? (
<Wrapper>
<Bar role="tablist">{entries.map(([key, value]) => this.renderTab(key, value))}</Bar>
<Content>
{entries.map(([key, value]) => {
const groupStyle = { display: 'none' };
if (key === this.props.selectedGroup) {
Object.assign(groupStyle, { flex: 1, display: 'flex' });
}
return (
<div key={key} style={groupStyle}>
{value.render()}
</div>
);
})}
</Content>
</Wrapper>
) : (
<Placeholder>no groups available</Placeholder>
);
}
}
GroupTabs.defaultProps = {
groups: {},
onGroupSelect: () => {},
selectedGroup: null,
};
GroupTabs.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
groups: PropTypes.object,
onGroupSelect: PropTypes.func,
selectedGroup: PropTypes.string,
};
export default GroupTabs;

View File

@ -1,12 +1,14 @@
import React from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import qs from 'qs';
import { document } from 'global';
import styled from 'react-emotion'; import styled from 'react-emotion';
import copy from 'copy-to-clipboard';
import { Placeholder, TabWrapper, TabsState, ActionBar, ActionButton } from '@storybook/components';
import { Placeholder } from '@storybook/components';
import GroupTabs from './GroupTabs';
import PropForm from './PropForm';
import Types from './types'; import Types from './types';
import PropForm from './PropForm';
const getTimestamp = () => +new Date(); const getTimestamp = () => +new Date();
@ -16,49 +18,21 @@ const PanelWrapper = styled('div')({
width: '100%', width: '100%',
}); });
const PanelInner = styled('div')({ export default class Panel extends PureComponent {
padding: '5px',
width: 'auto',
position: 'relative',
});
const ResetButton = styled('button')({
position: 'absolute',
bottom: 11,
right: 10,
border: 'none',
borderTop: 'solid 1px rgba(0, 0, 0, 0.2)',
borderLeft: 'solid 1px rgba(0, 0, 0, 0.2)',
background: 'rgba(255, 255, 255, 0.5)',
padding: '5px 10px',
borderRadius: '4px 0 0 0',
color: 'rgba(0, 0, 0, 0.5)',
outline: 'none',
});
export default class Panel extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleChange = this.handleChange.bind(this); this.state = { knobs: {} };
this.handleClick = this.handleClick.bind(this);
this.setKnobs = this.setKnobs.bind(this);
this.reset = this.reset.bind(this);
this.setOptions = this.setOptions.bind(this);
this.onGroupSelect = this.onGroupSelect.bind(this);
this.state = { knobs: {}, groupId: DEFAULT_GROUP_ID };
this.options = {}; this.options = {};
this.lastEdit = getTimestamp(); this.lastEdit = getTimestamp();
this.loadedFromUrl = false; this.loadedFromUrl = false;
} }
componentDidMount() { componentDidMount() {
this.props.channel.on('addon:knobs:setKnobs', this.setKnobs); this.props.channel.on('addon:knobs:setKnobs', this.setKnobs);
this.props.channel.on('addon:knobs:setOptions', this.setOptions); this.props.channel.on('addon:knobs:setOptions', this.setOptions);
this.stopListeningOnStory = this.props.api.onStory(() => { this.stopListeningOnStory = this.props.api.onStory(() => {
this.setState({ knobs: [], groupId: DEFAULT_GROUP_ID }); this.setState({ knobs: {} });
this.props.channel.emit('addon:knobs:reset'); this.props.channel.emit('addon:knobs:reset');
}); });
} }
@ -68,15 +42,11 @@ export default class Panel extends React.Component {
this.stopListeningOnStory(); this.stopListeningOnStory();
} }
onGroupSelect(name) { setOptions = (options = { timestamps: false }) => {
this.setState({ groupId: name });
}
setOptions(options = { timestamps: false }) {
this.options = options; this.options = options;
} };
setKnobs({ knobs, timestamp }) { setKnobs = ({ knobs, timestamp }) => {
const queryParams = {}; const queryParams = {};
const { api, channel } = this.props; const { api, channel } = this.props;
@ -86,7 +56,6 @@ export default class Panel extends React.Component {
// For the first time, get values from the URL and set them. // For the first time, get values from the URL and set them.
if (!this.loadedFromUrl) { if (!this.loadedFromUrl) {
const urlValue = api.getQueryParam(`knob-${name}`); const urlValue = api.getQueryParam(`knob-${name}`);
if (urlValue !== undefined) { if (urlValue !== undefined) {
// If the knob value present in url // If the knob value present in url
knob.value = Types[knob.type].deserialize(urlValue); knob.value = Types[knob.type].deserialize(urlValue);
@ -94,48 +63,63 @@ export default class Panel extends React.Component {
} }
} }
queryParams[`knob-${name}`] = Types[knob.type].serialize(knob.value); // set all knobsquery params to be deleted from URL
queryParams[`knob-${name}`] = null;
}); });
this.loadedFromUrl = true;
api.setQueryParams(queryParams); api.setQueryParams(queryParams);
this.setState({ knobs }); this.setState({ knobs });
}
}
reset() { this.loadedFromUrl = true;
}
};
reset = () => {
this.props.channel.emit('addon:knobs:reset'); this.props.channel.emit('addon:knobs:reset');
} };
emitChange(changedKnob) { copy = () => {
this.props.channel.emit('addon:knobs:knobChange', changedKnob); const { location } = document;
} const query = qs.parse(location.search.replace('?', ''));
handleChange(changedKnob) {
this.lastEdit = getTimestamp();
const { api } = this.props;
const { knobs } = this.state; const { knobs } = this.state;
const { name, type, value } = changedKnob;
Object.entries(knobs).forEach(([name, knob]) => {
query[`knob-${name}`] = Types[knob.type].serialize(knob.value);
});
copy(`${location.origin + location.pathname}?${qs.stringify(query)}`);
// TODO: show some notification of this
};
emitChange = changedKnob => {
this.props.channel.emit('addon:knobs:knobChange', changedKnob);
};
handleChange = changedKnob => {
this.lastEdit = getTimestamp();
const { knobs } = this.state;
const { name } = changedKnob;
const newKnobs = { ...knobs }; const newKnobs = { ...knobs };
newKnobs[name] = { newKnobs[name] = {
...newKnobs[name], ...newKnobs[name],
...changedKnob, ...changedKnob,
}; };
this.setState({ knobs: newKnobs });
const queryParams = {};
queryParams[`knob-${name}`] = Types[type].serialize(value);
api.setQueryParams(queryParams);
this.setState({ knobs: newKnobs }, this.emitChange(changedKnob)); this.setState({ knobs: newKnobs }, this.emitChange(changedKnob));
} };
handleClick(knob) { handleClick = knob => {
this.props.channel.emit('addon:knobs:knobClick', knob); this.props.channel.emit('addon:knobs:knobClick', knob);
} };
render() { render() {
const { knobs, groupId } = this.state; const { knobs } = this.state;
const { active } = this.props;
if (!active) {
return null;
}
const groups = {}; const groups = {};
const groupIds = []; const groupIds = [];
@ -146,20 +130,23 @@ export default class Panel extends React.Component {
const knobKeyGroupId = knobs[key].groupId; const knobKeyGroupId = knobs[key].groupId;
groupIds.push(knobKeyGroupId); groupIds.push(knobKeyGroupId);
groups[knobKeyGroupId] = { groups[knobKeyGroupId] = {
render: () => <div id={knobKeyGroupId}>{knobKeyGroupId}</div>, render: ({ active: groupActive, selected }) => (
<TabWrapper active={groupActive || selected === DEFAULT_GROUP_ID}>
<PropForm
knobs={knobsArray.filter(knob => knob.groupId === knobKeyGroupId)}
onFieldChange={this.handleChange}
onFieldClick={this.handleClick}
/>
</TabWrapper>
),
title: knobKeyGroupId, title: knobKeyGroupId,
}; };
}); });
if (groupIds.length > 0) {
groups[DEFAULT_GROUP_ID] = { groups[DEFAULT_GROUP_ID] = {
render: () => <div id={DEFAULT_GROUP_ID}>{DEFAULT_GROUP_ID}</div>, render: () => null,
title: DEFAULT_GROUP_ID, title: DEFAULT_GROUP_ID,
}; };
if (groupId !== DEFAULT_GROUP_ID) {
knobsArray = knobsArray.filter(key => knobs[key].groupId === groupId);
}
}
knobsArray = knobsArray.map(key => knobs[key]); knobsArray = knobsArray.map(key => knobs[key]);
@ -169,33 +156,38 @@ export default class Panel extends React.Component {
return ( return (
<PanelWrapper> <PanelWrapper>
{groupIds.length > 0 && ( {groupIds.length > 0 ? (
<GroupTabs <TabsState>
groups={groups} {Object.entries(groups).map(([k, v]) => (
onGroupSelect={this.onGroupSelect} <div id={k} title={v.title}>
selectedGroup={this.state.groupId} {v.render}
/> </div>
)} ))}
<PanelInner> </TabsState>
) : (
<PropForm <PropForm
knobs={knobsArray} knobs={knobsArray}
onFieldChange={this.handleChange} onFieldChange={this.handleChange}
onFieldClick={this.handleClick} onFieldClick={this.handleClick}
/> />
</PanelInner> )}
<ResetButton onClick={this.reset}>RESET</ResetButton> <ActionBar>
<ActionButton onClick={this.copy}>COPY</ActionButton>
<ActionButton onClick={this.reset}>RESET</ActionButton>
</ActionBar>
</PanelWrapper> </PanelWrapper>
); );
} }
} }
Panel.propTypes = { Panel.propTypes = {
active: PropTypes.bool.isRequired,
onReset: PropTypes.object, // eslint-disable-line
channel: PropTypes.shape({ channel: PropTypes.shape({
emit: PropTypes.func, emit: PropTypes.func,
on: PropTypes.func, on: PropTypes.func,
removeListener: PropTypes.func, removeListener: PropTypes.func,
}).isRequired, }).isRequired,
onReset: PropTypes.object, // eslint-disable-line
api: PropTypes.shape({ api: PropTypes.shape({
onStory: PropTypes.func, onStory: PropTypes.func,
getQueryParam: PropTypes.func, getQueryParam: PropTypes.func,

View File

@ -1,43 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'react-emotion';
import TypeMap from './types';
const InvalidType = () => <span>Invalid Type</span>;
const Field = styled('div')({
display: 'table-row',
padding: '5px',
});
const Label = styled('label')({
display: 'table-cell',
boxSizing: 'border-box',
verticalAlign: 'top',
paddingRight: 5,
paddingTop: 5,
textAlign: 'right',
width: 80,
fontSize: 12,
color: 'rgb(68, 68, 68)',
fontWeight: 600,
});
export default function PropField({ onChange, onClick, knob }) {
const InputType = TypeMap[knob.type] || InvalidType;
return (
<Field>
<Label htmlFor={knob.name}>{!knob.hideLabel && `${knob.name}`}</Label>
<InputType knob={knob} onChange={onChange} onClick={onClick} />
</Field>
);
}
PropField.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
}).isRequired,
onChange: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
};

View File

@ -1,19 +1,19 @@
import React from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'react-emotion'; import styled from 'react-emotion';
import PropField from './PropField'; import { Field } from '@storybook/components';
import TypeMap from './types';
const Form = styled('form')({ const Form = styled('form')({
display: 'table',
boxSizing: 'border-box', boxSizing: 'border-box',
width: '100%', width: '100%',
borderCollapse: 'separate',
borderSpacing: '5px',
}); });
export default class propForm extends React.Component { const InvalidType = () => <span>Invalid Type</span>;
export default class PropForm extends Component {
makeChangeHandler(name, type) { makeChangeHandler(name, type) {
return value => { return value => {
const change = { name, type, value }; const change = { name, type, value };
@ -28,16 +28,12 @@ export default class propForm extends React.Component {
<Form> <Form>
{knobs.map(knob => { {knobs.map(knob => {
const changeHandler = this.makeChangeHandler(knob.name, knob.type); const changeHandler = this.makeChangeHandler(knob.name, knob.type);
const InputType = TypeMap[knob.type] || InvalidType;
return ( return (
<PropField <Field key={knob.name} label={!knob.hideLabel && `${knob.name}`}>
key={knob.name} <InputType knob={knob} onChange={changeHandler} onClick={this.props.onFieldClick} />
name={knob.name} </Field>
type={knob.type}
value={knob.value}
knob={knob}
onChange={changeHandler}
onClick={this.props.onFieldClick}
/>
); );
})} })}
</Form> </Form>
@ -45,19 +41,15 @@ export default class propForm extends React.Component {
} }
} }
propForm.displayName = 'propForm'; PropForm.displayName = 'PropForm';
propForm.defaultProps = { PropForm.propTypes = {
knobs: [],
};
propForm.propTypes = {
knobs: PropTypes.arrayOf( knobs: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
value: PropTypes.any, value: PropTypes.any,
}) })
), ).isRequired,
onFieldChange: PropTypes.func.isRequired, onFieldChange: PropTypes.func.isRequired,
onFieldClick: PropTypes.func.isRequired, onFieldClick: PropTypes.func.isRequired,
}; };

View File

@ -1,62 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import GroupTabs from '../GroupTabs';
describe('GroupTabs', () => {
test('should render only the selected group with display set other than "none"', () => {
const groups = {
test1: {
render() {
return <div id="test1">TEST 1</div>;
},
},
test2: {
render() {
return <div id="test2">TEST 2</div>;
},
},
};
const onGroupSelect = () => 'onGroupSelect';
const wrapper = shallow(
<GroupTabs groups={groups} onGroupSelect={onGroupSelect} selectedGroup="test2" />
);
expect(wrapper.find('#test1').parent()).toHaveStyle('display', 'none');
expect(wrapper.find('#test2').parent()).not.toHaveStyle('display', 'none');
});
test('should set onGroupSelected as onClick handlers of tabs', () => {
const groups = {
test1: {
title: 'test 1',
render() {
return <div>TEST 1</div>;
},
},
};
const onGroupSelect = jest.fn();
const preventDefault = jest.fn();
const wrapper = shallow(
<GroupTabs groups={groups} onGroupSelect={onGroupSelect} selectedGroup="test1" />
);
wrapper
.find('Styled(button)')
.dive()
.simulate('click', { preventDefault });
expect(onGroupSelect).toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalled();
});
describe('when no groups are given', () => {
test('should render "no groups available"', () => {
const groups = {};
const onGroupSelect = () => 'onGroupSelect';
const wrapper = shallow(<GroupTabs groups={groups} onGroupSelect={onGroupSelect} />);
expect(wrapper.contains('no groups available')).toBe(true);
});
});
});

View File

@ -6,14 +6,14 @@ describe('Panel', () => {
it('should subscribe to setKnobs event of channel', () => { it('should subscribe to setKnobs event of channel', () => {
const testChannel = { on: jest.fn() }; const testChannel = { on: jest.fn() };
const testApi = { onStory: jest.fn() }; const testApi = { onStory: jest.fn() };
shallow(<Panel channel={testChannel} api={testApi} />); shallow(<Panel channel={testChannel} api={testApi} active />);
expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:setKnobs', jasmine.any(Function)); expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:setKnobs', jasmine.any(Function));
}); });
it('should subscribe to onStory event', () => { it('should subscribe to onStory event', () => {
const testChannel = { on: jest.fn() }; const testChannel = { on: jest.fn() };
const testApi = { onStory: jest.fn() }; const testApi = { onStory: jest.fn() };
shallow(<Panel channel={testChannel} api={testApi} />); shallow(<Panel channel={testChannel} api={testApi} active />);
expect(testApi.onStory).toHaveBeenCalled(); expect(testApi.onStory).toHaveBeenCalled();
expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:setKnobs', jasmine.any(Function)); expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:setKnobs', jasmine.any(Function));
@ -41,7 +41,7 @@ describe('Panel', () => {
onStory: jest.fn(), onStory: jest.fn(),
}; };
shallow(<Panel channel={testChannel} api={testApi} />); shallow(<Panel channel={testChannel} api={testApi} active />);
const setKnobsHandler = handlers['addon:knobs:setKnobs']; const setKnobsHandler = handlers['addon:knobs:setKnobs'];
const knobs = { const knobs = {
@ -67,7 +67,7 @@ describe('Panel', () => {
expect(testChannel.emit).toHaveBeenCalledWith(e, knobFromUrl); expect(testChannel.emit).toHaveBeenCalledWith(e, knobFromUrl);
}); });
it('should set query params when url params are already read', () => { it('should remove query params when url params are already read', () => {
const handlers = {}; const handlers = {};
const testChannel = { const testChannel = {
@ -88,7 +88,7 @@ describe('Panel', () => {
onStory: jest.fn(), onStory: jest.fn(),
}; };
const wrapper = shallow(<Panel channel={testChannel} api={testApi} />); const wrapper = shallow(<Panel channel={testChannel} api={testApi} active />);
const setKnobsHandler = handlers['addon:knobs:setKnobs']; const setKnobsHandler = handlers['addon:knobs:setKnobs'];
const knobs = { const knobs = {
@ -109,8 +109,8 @@ describe('Panel', () => {
setKnobsHandler({ knobs, timestamp: +new Date() }); setKnobsHandler({ knobs, timestamp: +new Date() });
const knobFromStory = { const knobFromStory = {
'knob-foo': knobs.foo.value, 'knob-foo': null,
'knob-baz': knobs.baz.value, 'knob-baz': null,
}; };
expect(testApi.setQueryParams).toHaveBeenCalledWith(knobFromStory); expect(testApi.setQueryParams).toHaveBeenCalledWith(knobFromStory);
@ -130,7 +130,7 @@ describe('Panel', () => {
onStory: jest.fn(), onStory: jest.fn(),
}; };
const wrapper = shallow(<Panel channel={testChannel} api={testApi} />); const wrapper = shallow(<Panel channel={testChannel} api={testApi} active />);
const testChangedKnob = { const testChangedKnob = {
name: 'foo', name: 'foo',
@ -140,8 +140,8 @@ describe('Panel', () => {
wrapper.instance().handleChange(testChangedKnob); wrapper.instance().handleChange(testChangedKnob);
expect(testChannel.emit).toHaveBeenCalledWith('addon:knobs:knobChange', testChangedKnob); expect(testChannel.emit).toHaveBeenCalledWith('addon:knobs:knobChange', testChangedKnob);
const paramsChange = { 'knob-foo': 'changed text' }; // const paramsChange = { 'knob-foo': 'changed text' };
expect(testApi.setQueryParams).toHaveBeenCalledWith(paramsChange); // expect(testApi.setQueryParams).toHaveBeenCalledWith(paramsChange);
}); });
}); });
}); });

View File

@ -1,24 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import styled from 'react-emotion';
import Textarea from 'react-textarea-autosize'; import { Textarea } from '@storybook/components';
import debounce from 'lodash.debounce';
const StyledTextarea = styled(Textarea)({
display: 'table-cell',
boxSizing: 'border-box',
verticalAlign: 'middle',
height: '26px',
width: '100%',
maxWidth: '100%',
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: '5px',
color: '#555',
});
function formatArray(value, separator) { function formatArray(value, separator) {
if (value === '') { if (value === '') {
@ -28,34 +11,21 @@ function formatArray(value, separator) {
} }
class ArrayType extends React.Component { class ArrayType extends React.Component {
constructor(props, context) { shouldComponentUpdate(nextProps) {
super(props, context); return nextProps.knob.value !== this.props.knob.value;
this.state = {
value: props.knob.value.join(props.knob.separator),
};
this.onChange = debounce(this.props.onChange, 200);
}
componentWillUnmount() {
this.onChange.cancel();
} }
handleChange = e => { handleChange = e => {
const { knob } = this.props; const { knob } = this.props;
const { value } = e.target; const { value } = e.target;
const newVal = formatArray(value, knob.separator); const newVal = formatArray(value, knob.separator);
this.props.onChange(newVal);
this.setState({ value });
this.onChange(newVal);
}; };
render() { render() {
const { knob } = this.props; const { knob } = this.props;
const { value } = this.state;
return <StyledTextarea id={knob.name} value={value} onChange={this.handleChange} />; return <Textarea id={knob.name} value={knob.value} onChange={this.handleChange} size="flex" />;
} }
} }

View File

@ -1,26 +1,18 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import styled from 'react-emotion'; import { Button } from '@storybook/components';
const Button = styled('button')({
height: '26px',
});
const ButtonType = ({ knob, onClick }) => ( const ButtonType = ({ knob, onClick }) => (
<Button type="button" id={knob.name} onClick={() => onClick(knob)}> <Button type="button" onClick={() => onClick(knob)}>
{knob.name} {knob.name}
</Button> </Button>
); );
ButtonType.defaultProps = {
knob: {},
};
ButtonType.propTypes = { ButtonType.propTypes = {
knob: PropTypes.shape({ knob: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
}), }).isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
}; };

View File

@ -4,22 +4,15 @@ import React from 'react';
import { SketchPicker } from 'react-color'; import { SketchPicker } from 'react-color';
import styled from 'react-emotion'; import styled from 'react-emotion';
import debounce from 'lodash.debounce';
const SwatchButton = styled('button')({ import { Button } from '@storybook/components';
background: '#fff',
borderRadius: '1px',
border: '1px solid rgb(247, 244, 244)',
display: 'inline-block',
cursor: 'pointer',
width: '100%',
padding: 0,
});
const Swatch = styled('div')({ const Swatch = styled('div')({
width: 'auto', position: 'absolute',
height: '20px', top: 0,
borderRadius: '2px', bottom: 0,
margin: 5, right: 3,
width: 28,
}); });
const Popover = styled('div')({ const Popover = styled('div')({
position: 'absolute', position: 'absolute',
@ -27,23 +20,18 @@ const Popover = styled('div')({
}); });
class ColorType extends React.Component { class ColorType extends React.Component {
constructor(props, context) { state = {
super(props, context);
this.state = {
displayColorPicker: false, displayColorPicker: false,
value: props.knob.value,
}; };
this.onChange = debounce(props.onChange, 200);
}
componentDidMount() { componentDidMount() {
document.addEventListener('mousedown', this.handleWindowMouseDown); document.addEventListener('mousedown', this.handleWindowMouseDown);
} }
shouldComponentUpdate(nextProps) {
return nextProps.knob.value !== this.props.knob.value;
}
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('mousedown', this.handleWindowMouseDown); document.removeEventListener('mousedown', this.handleWindowMouseDown);
this.onChange.cancel();
} }
handleWindowMouseDown = e => { handleWindowMouseDown = e => {
@ -62,35 +50,30 @@ class ColorType extends React.Component {
}; };
handleChange = color => { handleChange = color => {
this.setState({ this.props.onChange(`rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`);
value: color,
});
this.onChange(`rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`);
}; };
render() { render() {
const { knob } = this.props; const { knob } = this.props;
const { displayColorPicker, value } = this.state; const { displayColorPicker } = this.state;
const colorStyle = { const colorStyle = {
background: knob.value, background: knob.value,
}; };
return ( return (
<div id={knob.name}> <Button type="button" onClick={this.handleClick} size="flex">
<SwatchButton type="button" onClick={this.handleClick}> {knob.value}
<Swatch style={colorStyle} /> <Swatch style={colorStyle} />
</SwatchButton>
{displayColorPicker ? ( {displayColorPicker ? (
<Popover <Popover
innerRef={e => { innerRef={e => {
this.popover = e; this.popover = e;
}} }}
> >
<SketchPicker color={value} onChange={this.handleChange} /> <SketchPicker color={knob.value} onChange={this.handleChange} />
</Popover> </Popover>
) : null} ) : null}
</div> </Button>
); );
} }
} }

View File

@ -1,73 +1,43 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import styled from 'react-emotion';
import Datetime from 'react-datetime'; import Datetime from 'react-datetime';
import insertCss from 'insert-css';
import debounce from 'lodash.debounce';
import style from './styles'; import style from './styles';
const customStyle = ` const DateInput = styled(Datetime)(style);
.rdt input {
outline: 0;
width: 100%;
border: 1px solid #f7f4f4;
border-radius: 2px;
font-size: 11px;
padding: 5px;
color: #555;
display: table-cell;
box-sizing: border-box;
}
`;
insertCss(style);
insertCss(customStyle);
class DateType extends React.Component { class DateType extends React.Component {
constructor(props, context) { shouldComponentUpdate(nextProps) {
super(props, context); return nextProps.knob.value !== this.props.knob.value;
this.state = {
value: props.knob.value,
};
this.onChange = debounce(props.onChange, 200);
} }
handleChange = date => { handleChange = date => {
const value = date.valueOf(); const value = date.valueOf();
this.setState({ value }); this.props.onChange(value);
this.onChange(value);
}; };
render() { render() {
const { knob } = this.props; const { knob } = this.props;
const { value } = this.state;
return ( return (
<div> <DateInput
<Datetime value={knob.value ? new Date(knob.value) : null}
id={knob.name}
value={value ? new Date(value) : null}
type="date" type="date"
onChange={this.handleChange} onChange={this.handleChange}
size="flex"
/> />
</div>
); );
} }
} }
DateType.defaultProps = {
knob: {},
onChange: value => value,
};
DateType.propTypes = { DateType.propTypes = {
knob: PropTypes.shape({ knob: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
value: PropTypes.number, value: PropTypes.number,
}), }).isRequired,
onChange: PropTypes.func, onChange: PropTypes.func.isRequired,
}; };
DateType.serialize = value => String(value); DateType.serialize = value => String(value);

View File

@ -1,219 +1,202 @@
export default ` import { Input } from '@storybook/components';
.rdt {
position: relative;
}
.rdtPicker {
display: none;
position: absolute;
width: 200px;
padding: 4px;
margin-top: 1px;
z-index: 99999 !important;
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,.1);
border: 1px solid #f9f9f9;
}
.rdtOpen .rdtPicker {
display: block;
}
.rdtStatic .rdtPicker {
box-shadow: none;
position: static;
}
.rdtPicker .rdtTimeToggle { export default ({ theme, size }) => ({
text-align: center; ...Input.sizes({ size, theme }),
font-size:11px; '&.rdt': {
} position: 'relative',
},
'& > input': {
width: '100%',
height: 32,
...Input.styles({ theme }),
...Input.alignment({ theme }),
},
'& .rdtPicker': {
display: 'none',
position: 'absolute',
width: 200,
padding: 4,
marginTop: 1,
zIndex: 99999,
background: theme.barFill,
},
'&.rdtOpen .rdtPicker': {
display: 'block',
},
'&.rdt .rdtPicker': {
boxShadow: 'none',
position: 'static',
},
.rdtPicker table { '& .rdtPicker .rdtTimeToggle': {
width: 100%; textAlign: 'center',
margin: 0; fontSize: 11,
} },
.rdtPicker td,
.rdtPicker th {
text-align: center;
height: 28px;
}
.rdtPicker td {
cursor: pointer;
}
.rdtPicker td.rdtDay:hover,
.rdtPicker td.rdtHour:hover,
.rdtPicker td.rdtMinute:hover,
.rdtPicker td.rdtSecond:hover,
.rdtPicker .rdtTimeToggle:hover {
background: #eeeeee;
cursor: pointer;
}
.rdtPicker td.rdtOld,
.rdtPicker td.rdtNew {
color: #999999;
}
.rdtPicker td.rdtToday {
position: relative;
}
.rdtPicker td.rdtToday:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-bottom: 7px solid #428bca;
border-top-color: rgba(0, 0, 0, 0.2);
position: absolute;
bottom: 4px;
right: 4px;
}
.rdtPicker td.rdtActive,
.rdtPicker td.rdtActive:hover {
background-color: #428bca;
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.rdtPicker td.rdtActive.rdtToday:before {
border-bottom-color: #fff;
}
.rdtPicker td.rdtDisabled,
.rdtPicker td.rdtDisabled:hover {
background: none;
color: #999999;
cursor: not-allowed;
}
.rdtPicker td span.rdtOld { '& .rdtPicker table': {
color: #999999; width: '100%',
} margin: 0,
.rdtPicker td span.rdtDisabled, },
.rdtPicker td span.rdtDisabled:hover { '& .rdtPicker td, & .rdtPicker th': {
background: none; textAlign: 'center',
color: #999999; height: 32,
cursor: not-allowed; boxSizing: 'border-box',
} },
.rdtPicker th { '& .rdtPicker td': {
border-bottom: 1px solid #f9f9f9; cursor: 'pointer',
} },
.rdtPicker .dow { '& .rdtPicker td.rdtDay:hover, & .rdtPicker td.rdtHour:hover, & .rdtPicker td.rdtMinute:hover, & .rdtPicker td.rdtSecond:hover, & .rdtPicker .rdtTimeToggle:hover': {
width: 14.2857%; color: theme.highlightColor,
font-size: 11px; textDecoration: 'underline',
border-bottom: none; cursor: 'pointer',
} },
.rdtPicker th.rdtSwitch { '& .rdtPicker td.rdtOld, & .rdtPicker td.rdtNew': {
width: 100px; color: '#999999',
font-size: 11px; },
} '& .rdtPicker td.rdtToday': {
.rdtPicker th.rdtNext, position: 'relative',
.rdtPicker th.rdtPrev { },
font-size: 11px; '& .rdtPicker td.rdtToday:before': {
vertical-align: top; content: '""',
} display: 'inline-block',
borderLeft: '7px solid transparent',
borderBottom: `7px solid ${theme.highlightColor}`,
borderTopColor: 'rgba(0, 0, 0, 0.2)',
position: 'absolute',
bottom: 4,
right: 4,
},
'& .rdtPicker td.rdtActive, & .rdtPicker td.rdtActive:hover': {
backgroundColor: theme.highlightColor,
color: '#fff',
textShadow:
'0 -1px 0 rgba(0,0,0,0.25), 0 1px 0 rgba(0,0,0,0.25), -1px 0 0 rgba(0,0,0,0.25), 1px 0 0 rgba(0,0,0,0.25)',
},
'& .rdtPicker td.rdtActive.rdtToday:before': {
borderBottomColor: '#fff',
},
'& .rdtPicker td.rdtDisabled, & .rdtPicker td.rdtDisabled:hover': {
background: 'none',
color: '#999999',
cursor: 'not-allowed',
},
.rdtPrev span, '& .rdtPicker td span.rdtOld': {
.rdtNext span { color: '#999999',
display: block; },
-webkit-touch-callout: none; /* iOS Safari */ '& .rdtPicker td span.rdtDisabled, & .rdtPicker td span.rdtDisabled:hover': {
-webkit-user-select: none; /* Chrome/Safari/Opera */ background: 'none',
-khtml-user-select: none; /* Konqueror */ color: '#999999',
-moz-user-select: none; /* Firefox */ cursor: 'not-allowed',
-ms-user-select: none; /* Internet Explorer/Edge */ },
user-select: none; '& .rdtPicker th': {
} borderBottom: `1px solid ${theme.highlightColor}`,
},
'& .rdtPicker .dow': {
width: '14.2857%',
fontSize: 11,
borderBottom: 'none',
},
'& .rdtPicker th.rdtSwitch': {
width: 100,
fontSize: 11,
},
'& .rdtPicker th.rdtNext, & .rdtPicker th.rdtPrev': {
fontSize: 11,
verticalAlign: 'middle',
},
.rdtPicker th.rdtDisabled, '& .rdtPrev span, & .rdtNext span': {
.rdtPicker th.rdtDisabled:hover { display: 'block',
background: none; userSelect: 'none',
color: #999999; },
cursor: not-allowed;
}
.rdtPicker thead tr:first-child th {
cursor: pointer;
}
.rdtPicker thead tr:first-child th:hover {
background: #eeeeee;
}
.rdtPicker tfoot { '& .rdtPicker th.rdtDisabled, & .rdtPicker th.rdtDisabled:hover': {
border-top: 1px solid #f9f9f9; background: 'none',
} color: '#999999',
cursor: 'not-allowed',
},
'& .rdtPicker thead tr:first-child th': {
cursor: 'pointer',
},
'& .rdtPicker thead tr:first-child th:hover': {
color: theme.highlightColor,
},
.rdtPicker button { '& .rdtPicker tfoot': {
border: none; borderTop: '1px solid #f9f9f9',
background: none; },
cursor: pointer;
}
.rdtPicker button:hover {
background-color: #eee;
}
.rdtPicker thead button { '& .rdtPicker button': {
width: 100%; border: 'none',
height: 100%; background: 'none',
} cursor: 'pointer',
},
'& .rdtPicker button:hover': {
color: theme.highlightColor,
},
td.rdtMonth, '& .rdtPicker thead button': {
td.rdtYear { width: '100%',
height: 50px; height: '100%',
width: 25%; },
cursor: pointer;
}
td.rdtMonth:hover,
td.rdtYear:hover {
background: #eee;
}
td.rdtDay { '& td.rdtMonth, & td.rdtYear': {
font-size: 11px height: 50,
} width: '25%',
cursor: 'pointer',
},
'& td.rdtMonth:hover, & td.rdtYear:hover': {
color: theme.highlightColor,
},
.rdtCounters { '& td.rdtDay': {
display: inline-block; fontSize: 11,
} },
.rdtCounters > div { '& .rdtCounters': {
float: left; display: 'inline-block',
} },
.rdtCounter { '& .rdtCounters > div': {
height: 100px; float: 'left',
} },
.rdtCounter { '& .rdtCounter': {
width: 40px; height: 100,
} width: 40,
},
.rdtCounterSeparator { '& .rdtCounterSeparator': {
line-height: 100px; lineHeight: '100px',
} },
.rdtCounter .rdtBtn { '& .rdtCounter .rdtBtn': {
height: 40%; height: '40%',
line-height: 40px; lineHeight: '40px',
cursor: pointer; cursor: 'pointer',
display: block; display: 'block',
font-size: 11px; fontSize: 11,
-webkit-touch-callout: none; /* iOS Safari */ userSelect: 'none',
-webkit-user-select: none; /* Chrome/Safari/Opera */ },
-khtml-user-select: none; /* Konqueror */ '& .rdtCounter .rdtBtn:hover': {
-moz-user-select: none; /* Firefox */ color: theme.highlightColor,
-ms-user-select: none; /* Internet Explorer/Edge */ },
user-select: none; '& .rdtCounter .rdtCount': {
} height: '20%',
.rdtCounter .rdtBtn:hover { fontSize: 11,
background: #eee; },
}
.rdtCounter .rdtCount {
height: 20%;
font-size: 11px;
}
.rdtMilli { '& .rdtMilli': {
vertical-align: middle; verticalSlign: 'middle',
padding-left: 8px; paddingLeft: 8,
width: 48px; width: 48,
} },
.rdtMilli input { '& .rdtMilli input': {
width: 100%; width: '100%',
font-size: 11px; fontSize: 11,
margin-top: 37px; marginTop: 37,
} },
`; });

View File

@ -1,22 +1,12 @@
import { FileReader } from 'global'; import { FileReader } from 'global';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import styled from 'react-emotion'; import styled from 'react-emotion';
const Input = styled('input')({ import { Input } from '@storybook/components';
display: 'table-cell',
boxSizing: 'border-box', const FileInput = styled(Input)({
verticalAlign: 'middle', paddingTop: 4,
height: '26px',
width: '100%',
maxWidth: '100%',
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: '5px',
color: '#555',
}); });
function fileReaderPromise(file) { function fileReaderPromise(file) {
@ -28,12 +18,12 @@ function fileReaderPromise(file) {
} }
const FilesType = ({ knob, onChange }) => ( const FilesType = ({ knob, onChange }) => (
<Input <FileInput
id={knob.name}
type="file" type="file"
multiple multiple
onChange={e => Promise.all(Array.from(e.target.files).map(fileReaderPromise)).then(onChange)} onChange={e => Promise.all(Array.from(e.target.files).map(fileReaderPromise)).then(onChange)}
accept={knob.accept} accept={knob.accept}
size="flex"
/> />
); );

View File

@ -2,7 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import styled from 'react-emotion'; import styled from 'react-emotion';
import debounce from 'lodash.debounce';
import { Input } from '@storybook/components';
const base = { const base = {
boxSizing: 'border-box', boxSizing: 'border-box',
@ -15,11 +16,6 @@ const base = {
color: '#444', color: '#444',
}; };
const TextInput = styled('input')(base, {
display: 'table-cell',
width: '100%',
verticalAlign: 'middle',
});
const RangeInput = styled('input')(base, { const RangeInput = styled('input')(base, {
display: 'table-cell', display: 'table-cell',
flexGrow: 1, flexGrow: 1,
@ -37,78 +33,62 @@ const RangeWrapper = styled('div')({
}); });
class NumberType extends React.Component { class NumberType extends React.Component {
constructor(props) { shouldComponentUpdate(nextProps) {
super(props); return nextProps.knob.value !== this.props.knob.value;
let { value } = props.knob;
if (value === null || value === undefined) {
value = '';
}
this.state = { value };
this.onChange = debounce(props.onChange, 400);
}
componentWillUnmount() {
this.onChange.cancel();
} }
handleChange = event => { handleChange = event => {
const { value } = event.target; const { value } = event.target;
this.setState({ value });
let parsedValue = Number(value); let parsedValue = Number(value);
if (Number.isNaN(parsedValue) || value === '') { if (Number.isNaN(parsedValue) || value === '') {
parsedValue = null; parsedValue = null;
} }
this.onChange(parsedValue); this.props.onChange(parsedValue);
}; };
render() { render() {
const { knob } = this.props; const { knob } = this.props;
const { value } = this.state;
return knob.range ? ( return knob.range ? (
<RangeWrapper> <RangeWrapper>
<RangeLabel>{knob.min}</RangeLabel> <RangeLabel>{knob.min}</RangeLabel>
<RangeInput <RangeInput
id={knob.name} value={knob.value}
value={value}
type="range" type="range"
min={knob.min} min={knob.min}
max={knob.max} max={knob.max}
step={knob.step} step={knob.step}
onChange={this.handleChange} onChange={this.handleChange}
/> />
<RangeLabel>{`${value} / ${knob.max}`}</RangeLabel> <RangeLabel>{`${knob.value} / ${knob.max}`}</RangeLabel>
</RangeWrapper> </RangeWrapper>
) : ( ) : (
<TextInput <Input
id={knob.name} value={knob.value}
value={value}
type="number" type="number"
min={knob.min} min={knob.min}
max={knob.max} max={knob.max}
step={knob.step} step={knob.step}
onChange={this.handleChange} onChange={this.handleChange}
size="flex"
/> />
); );
} }
} }
NumberType.defaultProps = {
knob: {},
onChange: value => value,
};
NumberType.propTypes = { NumberType.propTypes = {
knob: PropTypes.shape({ knob: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
value: PropTypes.number, value: PropTypes.number,
}), range: PropTypes.bool,
onChange: PropTypes.func, min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
}).isRequired,
onChange: PropTypes.func.isRequired,
}; };
NumberType.serialize = value => (value === null || value === undefined ? '' : String(value)); NumberType.serialize = value => (value === null || value === undefined ? '' : String(value));

View File

@ -1,58 +1,38 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import styled from 'react-emotion'; import PropTypes from 'prop-types';
import deepEqual from 'fast-deep-equal';
import Textarea from 'react-textarea-autosize'; import { polyfill } from 'react-lifecycles-compat';
import debounce from 'lodash.debounce'; import { Textarea } from '@storybook/components';
const StyledTextarea = styled(Textarea)({
display: 'table-cell',
boxSizing: 'border-box',
verticalAlign: 'middle',
width: '100%',
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: '5px',
color: '#555',
fontFamily: 'monospace',
});
class ObjectType extends Component { class ObjectType extends Component {
constructor(props, context) { static getDerivedStateFromProps(props, state) {
super(props, context); if (!state || !deepEqual(props.knob.value, state.json)) {
try { try {
this.state = { return {
value: JSON.stringify(props.knob.value, null, 2), value: JSON.stringify(props.knob.value, null, 2),
failed: false, failed: false,
json: props.knob.value,
}; };
} catch (e) { } catch (e) {
this.state = { return { value: 'Object cannot be stringified', failed: true };
// if it can't be JSON stringified, it's probably some weird stuff
value: 'Default object cannot not be JSON stringified',
failed: true,
};
} }
this.onChange = debounce(props.onChange, 200);
} }
return null;
componentWillUnmount() {
this.onChange.cancel();
} }
handleChange = e => { handleChange = e => {
const { value } = e.target; const { value } = e.target;
try { try {
const json = JSON.parse(e.target.value.trim()); const json = JSON.parse(value.trim());
this.onChange(json);
this.setState({ this.setState({
value, value,
json,
failed: false, failed: false,
}); });
if (deepEqual(this.props.knob.value, this.state.json)) {
this.props.onChange(json);
}
} catch (err) { } catch (err) {
this.setState({ this.setState({
value, value,
@ -62,40 +42,30 @@ class ObjectType extends Component {
}; };
render() { render() {
const { knob } = this.props;
const { value, failed } = this.state; const { value, failed } = this.state;
const extraStyle = {};
if (failed) {
extraStyle.border = '1px solid #fadddd';
extraStyle.backgroundColor = '#fff5f5';
}
return ( return (
<StyledTextarea <Textarea
id={knob.name} valid={failed ? 'error' : null}
style={extraStyle}
value={value} value={value}
onChange={this.handleChange} onChange={this.handleChange}
size="flex"
/> />
); );
} }
} }
ObjectType.defaultProps = {
knob: {},
onChange: value => value,
};
ObjectType.propTypes = { ObjectType.propTypes = {
knob: PropTypes.shape({ knob: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}), }).isRequired,
onChange: PropTypes.func, onChange: PropTypes.func.isRequired,
}; };
ObjectType.serialize = object => JSON.stringify(object); ObjectType.serialize = object => JSON.stringify(object);
ObjectType.deserialize = value => (value ? JSON.parse(value) : {}); ObjectType.deserialize = value => (value ? JSON.parse(value) : {});
polyfill(ObjectType);
export default ObjectType; export default ObjectType;

View File

@ -1,22 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react';
import styled from 'react-emotion';
const Select = styled('select')({ import { Select } from '@storybook/components';
display: 'table-cell',
boxSizing: 'border-box',
verticalAlign: 'middle',
height: '26px',
width: '100%',
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: '5px',
color: '#555',
});
class SelectType extends React.Component { class SelectType extends Component {
renderOptionList({ options }) { renderOptionList({ options }) {
if (Array.isArray(options)) { if (Array.isArray(options)) {
return options.map(val => this.renderOption(val, val)); return options.map(val => this.renderOption(val, val));
@ -34,7 +21,7 @@ class SelectType extends React.Component {
const { knob, onChange } = this.props; const { knob, onChange } = this.props;
return ( return (
<Select id={knob.name} value={knob.value} onChange={e => onChange(e.target.value)}> <Select value={knob.value} onChange={e => onChange(e.target.value)} size="flex">
{this.renderOptionList(knob)} {this.renderOptionList(knob)}
</Select> </Select>
); );

View File

@ -1,53 +1,22 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import styled from 'react-emotion';
import Textarea from 'react-textarea-autosize'; import { Textarea } from '@storybook/components';
import debounce from 'lodash.debounce';
const StyledTextarea = styled(Textarea)({
display: 'table-cell',
boxSizing: 'border-box',
verticalAlign: 'middle',
height: '26px',
width: '100%',
maxWidth: '100%',
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: '5px',
color: '#555',
});
class TextType extends React.Component { class TextType extends React.Component {
constructor(props, context) { shouldComponentUpdate(nextProps) {
super(props, context); return nextProps.knob.value !== this.props.knob.value;
this.state = {
value: props.knob.value,
};
this.onChange = debounce(props.onChange, 200);
}
componentWillUnmount() {
this.onChange.cancel();
} }
handleChange = event => { handleChange = event => {
const { value } = event.target; const { value } = event.target;
this.props.onChange(value);
this.setState({ value });
this.onChange(value);
}; };
render() { render() {
const { knob } = this.props; const { knob } = this.props;
const { value } = this.state;
return <StyledTextarea id={knob.name} value={value} onChange={this.handleChange} />; return <Textarea id={knob.name} value={knob.value} onChange={this.handleChange} size="flex" />;
} }
} }

View File

@ -77,6 +77,7 @@ export const withKnobs = makeDecorator({
name: 'withKnobs', name: 'withKnobs',
parameterName: 'knobs', parameterName: 'knobs',
skipIfNoParametersOrOptions: false, skipIfNoParametersOrOptions: false,
allowDeprecatedUsage: true,
wrapper: (getStory, context, { options, parameters }) => { wrapper: (getStory, context, { options, parameters }) => {
const storyOptions = parameters || options; const storyOptions = parameters || options;
const allOptions = { ...defaultOptions, ...storyOptions }; const allOptions = { ...defaultOptions, ...storyOptions };

View File

@ -4,9 +4,9 @@ import Panel from './components/Panel';
addons.register('storybooks/storybook-addon-knobs', api => { addons.register('storybooks/storybook-addon-knobs', api => {
const channel = addons.getChannel(); const channel = addons.getChannel();
addons.addPanel('storybooks/storybook-addon-knobs', { addons.addPanel('storybooks/storybook-addon-knobs', {
title: 'Knobs', title: 'Knobs',
render: () => <Panel channel={channel} api={api} key="knobs-panel" />, // eslint-disable-next-line react/prop-types
render: ({ active }) => <Panel channel={channel} api={api} key="knobs-panel" active={active} />,
}); });
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-links", "name": "@storybook/addon-links",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Story Links addon for storybook", "description": "Story Links addon for storybook",
"keywords": [ "keywords": [
"storybook" "storybook"
@ -20,9 +20,9 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"@storybook/core-events": "4.0.0-alpha.10", "@storybook/core-events": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"global": "^4.3.2", "global": "^4.3.2",
"prop-types": "^15.6.1" "prop-types": "^15.6.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-notes", "name": "@storybook/addon-notes",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Write notes for your Storybook stories.", "description": "Write notes for your Storybook stories.",
"keywords": [ "keywords": [
"addon", "addon",
@ -18,13 +18,11 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"emotion": "^9.1.3",
"marked": "^0.4.0", "marked": "^0.4.0",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-emotion": "^9.1.3", "react-emotion": "^9.1.3"
"util-deprecate": "^1.0.2"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"

View File

@ -10,6 +10,7 @@ export const withNotes = makeDecorator({
name: 'withNotes', name: 'withNotes',
parameterName: 'notes', parameterName: 'notes',
skipIfNoParametersOrOptions: true, skipIfNoParametersOrOptions: true,
allowDeprecatedUsage: true,
wrapper: (getStory, context, { options, parameters }) => { wrapper: (getStory, context, { options, parameters }) => {
const channel = addons.getChannel(); const channel = addons.getChannel();

View File

@ -44,6 +44,7 @@ export class Notes extends React.Component {
} }
render() { render() {
const { active } = this.props;
const { text } = this.state; const { text } = this.state;
const textAfterFormatted = text const textAfterFormatted = text
? text ? text
@ -52,31 +53,34 @@ export class Notes extends React.Component {
.replace(/\n/g, '<br />') .replace(/\n/g, '<br />')
: ''; : '';
return ( return active ? (
<Panel <Panel
className="addon-notes-container" className="addon-notes-container"
dangerouslySetInnerHTML={{ __html: textAfterFormatted }} dangerouslySetInnerHTML={{ __html: textAfterFormatted }}
/> />
); ) : null;
} }
} }
Notes.propTypes = { Notes.propTypes = {
// eslint-disable-next-line react/forbid-prop-types active: PropTypes.bool.isRequired,
channel: PropTypes.object, channel: PropTypes.shape({
// eslint-disable-next-line react/forbid-prop-types on: PropTypes.func,
api: PropTypes.object, emit: PropTypes.func,
}; removeListener: PropTypes.func,
Notes.defaultProps = { }).isRequired,
channel: {}, api: PropTypes.shape({
api: {}, onStory: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
}; };
// Register the addon with a unique name.
addons.register('storybook/notes', api => { addons.register('storybook/notes', api => {
// Also need to set a unique name to the panel. const channel = addons.getChannel();
addons.addPanel('storybook/notes/panel', { addons.addPanel('storybook/notes/panel', {
title: 'Notes', title: 'Notes',
render: () => <Notes channel={addons.getChannel()} api={api} />, // eslint-disable-next-line react/prop-types
render: ({ active }) => <Notes channel={channel} api={api} active={active} />,
}); });
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-options", "name": "@storybook/addon-options",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Options addon for storybook", "description": "Options addon for storybook",
"keywords": [ "keywords": [
"storybook" "storybook"
@ -19,7 +19,7 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0" "babel-runtime": "^6.26.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-storyshots", "name": "@storybook/addon-storyshots",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "StoryShots is a Jest Snapshot Testing Addon for Storybook.", "description": "StoryShots is a Jest Snapshot Testing Addon for Storybook.",
"repository": { "repository": {
"type": "git", "type": "git",
@ -16,7 +16,7 @@
"storybook": "start-storybook -p 6006" "storybook": "start-storybook -p 6006"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"glob": "^7.1.2", "glob": "^7.1.2",
"global": "^4.3.2", "global": "^4.3.2",
@ -24,10 +24,10 @@
"read-pkg-up": "^3.0.0" "read-pkg-up": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "4.0.0-alpha.10", "@storybook/addon-actions": "4.0.0-alpha.14",
"@storybook/addon-links": "4.0.0-alpha.10", "@storybook/addon-links": "4.0.0-alpha.14",
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/react": "4.0.0-alpha.10", "@storybook/react": "4.0.0-alpha.14",
"enzyme-to-json": "^3.3.4", "enzyme-to-json": "^3.3.4",
"react": "^16.4.0" "react": "^16.4.0"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-storyshots-puppeteer", "name": "@storybook/addon-storyshots-puppeteer",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Image snappshots addition to StoryShots base on puppeteer", "description": "Image snappshots addition to StoryShots base on puppeteer",
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,7 +13,7 @@
"prepare": "node ../../../scripts/prepare.js" "prepare": "node ../../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/node-logger": "4.0.0-alpha.10", "@storybook/node-logger": "4.0.0-alpha.14",
"jest-image-snapshot": "^2.4.2", "jest-image-snapshot": "^2.4.2",
"puppeteer": "^1.4.0" "puppeteer": "^1.4.0"
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-storysource", "name": "@storybook/addon-storysource",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Stories addon for storybook", "description": "Stories addon for storybook",
"keywords": [ "keywords": [
"storybook" "storybook"
@ -20,8 +20,8 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"estraverse": "^4.2.0", "estraverse": "^4.2.0",
"loader-utils": "^1.1.0", "loader-utils": "^1.1.0",

View File

@ -7,6 +7,7 @@ import SyntaxHighlighter, { registerLanguage } from 'react-syntax-highlighter/pr
import { createElement } from 'react-syntax-highlighter'; import { createElement } from 'react-syntax-highlighter';
import { EVENT_ID } from './'; import { EVENT_ID } from './';
// TODO: take from theme
const highlighterTheme = { const highlighterTheme = {
...darcula, ...darcula,
'pre[class*="language-"]': { 'pre[class*="language-"]': {
@ -175,7 +176,8 @@ export default class StoryPanel extends Component {
}; };
render() { render() {
return ( const { active } = this.props;
return active ? (
<SyntaxHighlighter <SyntaxHighlighter
language="jsx" language="jsx"
showLineNumbers="true" showLineNumbers="true"
@ -185,11 +187,12 @@ export default class StoryPanel extends Component {
> >
{this.state.source} {this.state.source}
</SyntaxHighlighter> </SyntaxHighlighter>
); ) : null;
} }
} }
StoryPanel.propTypes = { StoryPanel.propTypes = {
active: PropTypes.bool.isRequired,
api: PropTypes.shape({ api: PropTypes.shape({
selectStory: PropTypes.func.isRequired, selectStory: PropTypes.func.isRequired,
}).isRequired, }).isRequired,

View File

@ -8,7 +8,8 @@ export function register() {
const channel = addons.getChannel(); const channel = addons.getChannel();
addons.addPanel(PANEL_ID, { addons.addPanel(PANEL_ID, {
title: 'Story', title: 'Story',
render: () => <StoryPanel channel={channel} api={api} />, // eslint-disable-next-line react/prop-types
render: ({ active }) => <StoryPanel channel={channel} api={api} active={active} />,
}); });
}); });
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/addon-viewport", "name": "@storybook/addon-viewport",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook addon to change the viewport size to mobile", "description": "Storybook addon to change the viewport size to mobile",
"keywords": [ "keywords": [
"storybook" "storybook"
@ -11,13 +11,11 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/addons": "4.0.0-alpha.10", "@storybook/addons": "4.0.0-alpha.14",
"@storybook/components": "4.0.0-alpha.10", "@storybook/components": "4.0.0-alpha.14",
"@storybook/core-events": "4.0.0-alpha.10", "@storybook/core-events": "4.0.0-alpha.14",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"emotion": "^9.1.3",
"global": "^4.3.2", "global": "^4.3.2",
"lodash.debounce": "^4.0.8",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-emotion": "^9.1.3", "react-emotion": "^9.1.3",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"

View File

@ -1,13 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { document } from 'global'; import { document } from 'global';
import debounce from 'lodash.debounce';
import styled from 'react-emotion'; import styled from 'react-emotion';
import { ActionBar, ActionButton, Button, Select, Field } from '@storybook/components';
import { resetViewport, viewportsTransformer } from './viewportInfo'; import { resetViewport, viewportsTransformer } from './viewportInfo';
import { SelectViewport } from './SelectViewport';
import { RotateViewport } from './RotateViewport';
import { import {
SET_STORY_DEFAULT_VIEWPORT_EVENT_ID, SET_STORY_DEFAULT_VIEWPORT_EVENT_ID,
CONFIGURE_VIEWPORT_EVENT_ID, CONFIGURE_VIEWPORT_EVENT_ID,
@ -17,14 +15,13 @@ import {
DEFAULT_VIEWPORT, DEFAULT_VIEWPORT,
} from '../../shared'; } from '../../shared';
import { Button } from './styles';
const storybookIframe = 'storybook-preview-iframe'; const storybookIframe = 'storybook-preview-iframe';
const Container = styled('div')({ const Container = styled('div')({
padding: 15, padding: 15,
width: '100%', width: '100%',
boxSizing: 'border-box', boxSizing: 'border-box',
}); });
Container.displayName = 'Container';
const getDefaultViewport = (viewports, candidateViewport) => const getDefaultViewport = (viewports, candidateViewport) =>
candidateViewport in viewports ? candidateViewport : Object.keys(viewports)[0]; candidateViewport in viewports ? candidateViewport : Object.keys(viewports)[0];
@ -32,36 +29,30 @@ const getDefaultViewport = (viewports, candidateViewport) =>
const getViewports = viewports => const getViewports = viewports =>
Object.keys(viewports).length > 0 ? viewports : INITIAL_VIEWPORTS; Object.keys(viewports).length > 0 ? viewports : INITIAL_VIEWPORTS;
const setStoryDefaultViewportWait = 100;
export class Panel extends Component { export class Panel extends Component {
static defaultOptions = { static defaultOptions = {
viewports: INITIAL_VIEWPORTS, viewports: INITIAL_VIEWPORTS,
defaultViewport: DEFAULT_VIEWPORT, defaultViewport: DEFAULT_VIEWPORT,
}; };
static propTypes = { static propTypes = {
channel: PropTypes.shape({}).isRequired, active: PropTypes.bool.isRequired,
api: PropTypes.shape({}).isRequired, api: PropTypes.shape({
selectStory: PropTypes.func.isRequired,
}).isRequired,
channel: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
}; };
constructor(props, context) { state = {
super(props, context);
this.state = {
viewport: DEFAULT_VIEWPORT, viewport: DEFAULT_VIEWPORT,
defaultViewport: DEFAULT_VIEWPORT, defaultViewport: DEFAULT_VIEWPORT,
viewports: viewportsTransformer(INITIAL_VIEWPORTS), viewports: viewportsTransformer(INITIAL_VIEWPORTS),
isLandscape: false, isLandscape: false,
}; };
this.previousViewport = DEFAULT_VIEWPORT;
this.setStoryDefaultViewport = debounce(
this.setStoryDefaultViewport,
setStoryDefaultViewportWait
);
}
componentDidMount() { componentDidMount() {
const { channel, api } = this.props; const { channel, api } = this.props;
@ -113,6 +104,7 @@ export class Panel extends Component {
}; };
iframe = undefined; iframe = undefined;
previousViewport = DEFAULT_VIEWPORT;
changeViewport = viewport => { changeViewport = viewport => {
const { viewport: previousViewport } = this.state; const { viewport: previousViewport } = this.state;
@ -180,28 +172,39 @@ export class Panel extends Component {
viewport, viewport,
viewports, viewports,
} = this.state; } = this.state;
const { active } = this.props;
const disableDefault = viewport === storyDefaultViewport; const isResponsive = viewport === storyDefaultViewport;
return ( return active ? (
<Container> <Container>
<SelectViewport <Field label="Device">
viewports={viewports} <Select value={viewport} onChange={e => this.changeViewport(e.target.value)} size="flex">
defaultViewport={storyDefaultViewport} {Object.entries(viewports).map(([key, { name }]) => (
activeViewport={viewport} <option value={key} key={key}>
onChange={e => this.changeViewport(e.target.value)} {key === defaultViewport ? `${name} (Default)` : name}
/> </option>
))}
</Select>
</Field>
<RotateViewport {!isResponsive ? (
onClick={this.toggleLandscape} <Field label="Rotate">
disabled={disableDefault} <Button onClick={this.toggleLandscape} active={isLandscape} size="flex">
active={isLandscape} {isLandscape ? 'rotate to portrait' : 'rotate to landscape'}
/>
<Button onClick={() => this.changeViewport(storyDefaultViewport)} disabled={disableDefault}>
Reset Viewport
</Button> </Button>
</Field>
) : null}
<ActionBar>
<ActionButton
onClick={() => this.changeViewport(storyDefaultViewport)}
disabled={isResponsive}
>
RESET
</ActionButton>
</ActionBar>
</Container> </Container>
); ) : null;
} }
} }

View File

@ -1,24 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Label, Row, Button } from './styles';
export const RotateViewport = ({ active, ...props }) => (
<Row>
<Label htmlFor="rotate">Rotate</Label>
<Button id="rotate" {...props}>
{active ? 'Vertical' : 'Landscape'}
</Button>
</Row>
);
RotateViewport.propTypes = {
disabled: PropTypes.bool,
onClick: PropTypes.func.isRequired,
active: PropTypes.bool,
};
RotateViewport.defaultProps = {
disabled: true,
active: false,
};

View File

@ -1,26 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Label, Row, Select } from './styles';
export function SelectViewport({ viewports, defaultViewport, activeViewport, onChange }) {
return (
<Row>
<Label htmlFor="device">Device</Label>
<Select id="device" value={activeViewport} onChange={onChange}>
{Object.entries(viewports).map(([key, { name }]) => (
<option value={key} key={key}>
{key === defaultViewport ? `(Default) ${name}` : name}
</option>
))}
</Select>
</Row>
);
}
SelectViewport.propTypes = {
onChange: PropTypes.func.isRequired,
activeViewport: PropTypes.string.isRequired,
viewports: PropTypes.shape({}).isRequired,
defaultViewport: PropTypes.string.isRequired,
};

View File

@ -1,36 +0,0 @@
import styled from 'react-emotion';
export const Row = styled('div')({
width: '100%',
display: 'flex',
marginBottom: 15,
});
export const Label = styled('label')({
width: 80,
marginRight: 15,
});
const actionColor = 'rgb(247, 247, 247)';
const basebutton = {
color: 'rgb(85, 85, 85)',
width: '100%',
border: `1px solid ${actionColor}`,
backgroundColor: actionColor,
borderRadius: 4,
padding: 10,
};
export const Button = styled('button')(
basebutton,
({ disabled }) =>
disabled
? {
opacity: '0.5',
cursor: 'not-allowed',
}
: {}
);
export const Select = styled('select')(basebutton);

View File

@ -2,6 +2,8 @@ import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { document } from 'global'; import { document } from 'global';
import { ActionButton, Select } from '@storybook/components';
import { Panel } from '../Panel'; import { Panel } from '../Panel';
import { resetViewport, viewportsTransformer } from '../viewportInfo'; import { resetViewport, viewportsTransformer } from '../viewportInfo';
import { DEFAULT_VIEWPORT, INITIAL_VIEWPORTS } from '../../../shared'; import { DEFAULT_VIEWPORT, INITIAL_VIEWPORTS } from '../../../shared';
@ -9,8 +11,6 @@ import { DEFAULT_VIEWPORT, INITIAL_VIEWPORTS } from '../../../shared';
const initialViewportAt = index => Object.keys(INITIAL_VIEWPORTS)[index]; const initialViewportAt = index => Object.keys(INITIAL_VIEWPORTS)[index];
const transformedInitialViewports = viewportsTransformer(INITIAL_VIEWPORTS); const transformedInitialViewports = viewportsTransformer(INITIAL_VIEWPORTS);
jest.mock('lodash.debounce', () => jest.fn(fn => fn));
describe('Viewport/Panel', () => { describe('Viewport/Panel', () => {
const props = { const props = {
channel: { channel: {
@ -20,7 +20,9 @@ describe('Viewport/Panel', () => {
}, },
api: { api: {
onStory: jest.fn(), onStory: jest.fn(),
selectStory: jest.fn(),
}, },
active: true,
}; };
let subject; let subject;
@ -324,16 +326,18 @@ describe('Viewport/Panel', () => {
beforeEach(() => { beforeEach(() => {
subject.instance().changeViewport = jest.fn(); subject.instance().changeViewport = jest.fn();
resetBtn = subject.find('Styled(button)'); resetBtn = subject.find(ActionButton);
}); });
it('enables the reset button if not default', () => { it('enables the reset button if not default', () => {
subject.setState({ viewport: 'responsive' }); subject.setState({ viewport: 'responsive' });
resetBtn = subject.find('Styled(button)');
resetBtn = subject.find(ActionButton);
expect(resetBtn).toHaveProp('disabled', true); expect(resetBtn).toHaveProp('disabled', true);
subject.setState({ viewport: 'iphone6' }); subject.setState({ viewport: 'iphone6' });
resetBtn = subject.find('Styled(button)');
resetBtn = subject.find(ActionButton);
expect(resetBtn).toHaveProp('disabled', false); expect(resetBtn).toHaveProp('disabled', false);
}); });
@ -347,32 +351,16 @@ describe('Viewport/Panel', () => {
let select; let select;
beforeEach(() => { beforeEach(() => {
select = subject.find('SelectViewport'); select = subject.find(Select);
subject.instance().changeViewport = jest.fn(); subject.instance().changeViewport = jest.fn();
}); });
it('passes the activeViewport', () => { it('passes the value', () => {
expect(select.props()).toEqual( expect(select.props().value).toEqual('responsive');
expect.objectContaining({
activeViewport: DEFAULT_VIEWPORT,
})
);
}); });
it('passes the defaultViewport', () => { it('passes the children', () => {
expect(select.props()).toEqual( expect(select.props().children).toHaveLength(8);
expect.objectContaining({
defaultViewport: DEFAULT_VIEWPORT,
})
);
});
it('passes the INITIAL_VIEWPORTS', () => {
expect(select.props()).toEqual(
expect.objectContaining({
viewports: transformedInitialViewports,
})
);
}); });
it('onChange it updates the viewport', () => { it('onChange it updates the viewport', () => {
@ -386,34 +374,21 @@ describe('Viewport/Panel', () => {
let toggle; let toggle;
beforeEach(() => { beforeEach(() => {
toggle = subject.find('RotateViewport'); toggle = subject.find('Field[label="Rotate"]');
jest.spyOn(subject.instance(), 'toggleLandscape'); jest.spyOn(subject.instance(), 'toggleLandscape');
subject.instance().forceUpdate(); subject.instance().forceUpdate();
}); });
it('passes the active prop based on the state of the panel', () => { it('renders viewport is not default', () => {
expect(toggle.props().active).toEqual(subject.state('isLandscape'));
});
describe('is on the default viewport', () => {
beforeEach(() => {
subject.setState({ viewport: DEFAULT_VIEWPORT });
});
it('sets the disabled property', () => {
expect(toggle.props().disabled).toEqual(true);
});
});
describe('is on a non-default viewport', () => {
beforeEach(() => {
subject.setState({ viewport: 'iphone6' }); subject.setState({ viewport: 'iphone6' });
toggle = subject.find('RotateViewport'); toggle = subject.find({ label: 'Rotate' });
expect(toggle).toExist();
}); });
it('the disabled property is false', () => { it('hidden the viewport is default', () => {
expect(toggle.props().disabled).toEqual(false); subject.setState({ viewport: DEFAULT_VIEWPORT });
}); toggle = subject.find({ label: 'Rotate' });
expect(toggle).not.toExist();
}); });
}); });
}); });

View File

@ -1,51 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { RotateViewport } from '../RotateViewport';
const setup = addition => {
const props = {
onClick: jest.fn(),
disabled: false,
...addition,
};
const result = shallow(<RotateViewport {...props} />);
return { result, props };
};
describe('Viewport/RotateViewport', () => {
it('renders correctly', () => {
const { result } = setup();
expect(result).toMatchSnapshot();
});
it('has a click handler set via props', () => {
const { result, props } = setup();
const button = result.find('Styled(button)');
expect(button).toHaveProp('onClick');
button.simulate('click');
expect(props.onClick).toHaveBeenCalled();
});
it('renders the correctly if not-disabled', () => {
const { result } = setup({ disabled: false });
expect(result.find('Styled(button)')).toHaveProp('disabled', false);
});
it('renders the correctly if disabled', () => {
const { result } = setup({ disabled: true });
expect(result.find('Styled(button)')).toHaveProp('disabled', true);
});
it('renders the correctly if not-active', () => {
const { result } = setup({ active: false });
expect(result.html()).toContain('Landscape');
});
it('renders the correctly if active', () => {
const { result } = setup({ active: true });
expect(result.html()).toContain('Vertical');
});
});

View File

@ -1,44 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { SelectViewport } from '../SelectViewport';
import { INITIAL_VIEWPORTS, DEFAULT_VIEWPORT } from '../../../shared';
const setup = () => {
const props = {
onChange: jest.fn(),
activeViewport: DEFAULT_VIEWPORT,
viewports: INITIAL_VIEWPORTS,
defaultViewport: DEFAULT_VIEWPORT,
};
return { props, result: shallow(<SelectViewport {...props} />) };
};
describe('Viewport/SelectViewport', () => {
it('is correctly rendered', () => {
const { result } = setup();
expect(result).toMatchSnapshot();
});
it('has a default option first', () => {
const { result } = setup();
expect(result.find('Styled(select)').props().value).toEqual(DEFAULT_VIEWPORT);
});
it('has at least 1 option', () => {
const viewportKeys = Object.keys(INITIAL_VIEWPORTS);
expect(viewportKeys.length).toBeGreaterThan(0);
});
const viewportKeys = Object.keys(INITIAL_VIEWPORTS);
const { result } = setup();
viewportKeys.forEach(key => {
const { name } = INITIAL_VIEWPORTS[key];
const expectedText = key === DEFAULT_VIEWPORT ? `(Default) ${name}` : name;
it(`renders an option for ${name}`, () => {
const option = result.find(`option[value="${key}"]`);
expect(option.text()).toEqual(expectedText);
});
});
});

View File

@ -1,18 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Viewport/RotateViewport renders correctly 1`] = `
<Styled(div)>
<Styled(label)
htmlFor="rotate"
>
Rotate
</Styled(label)>
<Styled(button)
disabled={false}
id="rotate"
onClick={[MockFunction]}
>
Landscape
</Styled(button)>
</Styled(div)>
`;

View File

@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Viewport/SelectViewport is correctly rendered 1`] = `
<Styled(div)>
<Styled(label)
htmlFor="device"
>
Device
</Styled(label)>
<Styled(select)
id="device"
onChange={[MockFunction]}
value="responsive"
>
<option
key="responsive"
value="responsive"
>
(Default) Responsive
</option>
<option
key="iphone5"
value="iphone5"
>
iPhone 5
</option>
<option
key="iphone6"
value="iphone6"
>
iPhone 6
</option>
<option
key="iphone6p"
value="iphone6p"
>
iPhone 6 Plus
</option>
<option
key="ipad"
value="ipad"
>
iPad
</option>
<option
key="galaxys5"
value="galaxys5"
>
Galaxy S5
</option>
<option
key="nexus5x"
value="nexus5x"
>
Nexus 5X
</option>
<option
key="nexus6p"
value="nexus6p"
>
Nexus 6P
</option>
</Styled(select)>
</Styled(div)>
`;

View File

@ -7,12 +7,10 @@ import { ADDON_ID, PANEL_ID } from '../shared';
const addChannel = api => { const addChannel = api => {
const channel = addons.getChannel(); const channel = addons.getChannel();
addons.addPanel(PANEL_ID, { addons.addPanel(PANEL_ID, {
title: 'Viewport', title: 'Viewport',
render() { // eslint-disable-next-line react/prop-types
return <Panel channel={channel} api={api} />; render: ({ active }) => <Panel channel={channel} api={api} active={active} />,
},
}); });
}; };

View File

@ -33,6 +33,7 @@ const applyViewportOptions = (options = {}) => {
const withViewport = makeDecorator({ const withViewport = makeDecorator({
name: 'withViewport', name: 'withViewport',
parameterName: 'viewport', parameterName: 'viewport',
allowDeprecatedUsage: true,
wrapper: (getStory, context, { options, parameters }) => { wrapper: (getStory, context, { options, parameters }) => {
const storyOptions = parameters || options; const storyOptions = parameters || options;
const viewportOptions = const viewportOptions =

View File

@ -1,3 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
require('../dist/server/build'); require('../dist/server/build');

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/angular", "name": "@storybook/angular",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook for Angular: Develop Angular Components in isolation with Hot Reloading.", "description": "Storybook for Angular: Develop Angular Components in isolation with Hot Reloading.",
"homepage": "https://github.com/storybooks/storybook/tree/master/apps/angular", "homepage": "https://github.com/storybooks/storybook/tree/master/apps/angular",
"bugs": { "bugs": {
@ -22,8 +22,8 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/core": "4.0.0-alpha.10", "@storybook/core": "4.0.0-alpha.14",
"@storybook/node-logger": "4.0.0-alpha.10", "@storybook/node-logger": "4.0.0-alpha.14",
"angular2-template-loader": "^0.6.2", "angular2-template-loader": "^0.6.2",
"babel-runtime": "^6.23.0", "babel-runtime": "^6.23.0",
"core-js": "^2.5.7", "core-js": "^2.5.7",

View File

@ -10,7 +10,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
button { button {
border: 1px solid #eee; border: 1px solid #eee;
border-radius: 3px; border-radius: 3px;
background-color: #FFFFFF; background-color: #ffffff;
cursor: pointer; cursor: pointer;
font-size: 15px; font-size: 15px;
padding: 3px 10px; padding: 3px 10px;

View File

@ -49,17 +49,17 @@ import { Component, Output, EventEmitter } from '@angular/core';
main { main {
margin: 15px; margin: 15px;
max-width: 600; max-width: 600;
lineHeight: 1.4; line-height: 1.4;
fontFamily: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif; fontfamily: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, freesans, sans-serif;
} }
.note { .note {
opacity: 0.5, opacity: 0.5;
} }
.inline-code { .inline-code {
font-size: 15px; font-size: 15px;
fontWeight: 600; font-weight: 600;
padding: 2px 5px; padding: 2px 5px;
border: 1px solid #eae9e9; border: 1px solid #eae9e9;
border-radius: 4px; border-radius: 4px;

View File

@ -1,5 +1,4 @@
import { buildStatic } from '@storybook/core/server'; import { buildStatic } from '@storybook/core/server';
import options from './options'; import options from './options';
buildStatic(options); buildStatic(options);

View File

@ -1,3 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
require('../dist/server/build'); require('../dist/server/build');

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/html", "name": "@storybook/html",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.", "description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.",
"homepage": "https://github.com/storybooks/storybook/tree/master/apps/html", "homepage": "https://github.com/storybooks/storybook/tree/master/apps/html",
"bugs": { "bugs": {
@ -21,7 +21,7 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/core": "4.0.0-alpha.10", "@storybook/core": "4.0.0-alpha.14",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"global": "^4.3.2", "global": "^4.3.2",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",

View File

@ -1,5 +1,4 @@
import { buildStatic } from '@storybook/core/server'; import { buildStatic } from '@storybook/core/server';
import options from './options'; import options from './options';
buildStatic(options); buildStatic(options);

View File

@ -1,3 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
require('../dist/server/build'); require('../dist/server/build');

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/marko", "name": "@storybook/marko",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook for Marko: Develop Marko Component in isolation with Hot Reloading.", "description": "Storybook for Marko: Develop Marko Component in isolation with Hot Reloading.",
"homepage": "https://github.com/storybooks/storybook/tree/master/app/marko", "homepage": "https://github.com/storybooks/storybook/tree/master/app/marko",
"bugs": { "bugs": {
@ -22,7 +22,7 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/core": "4.0.0-alpha.10", "@storybook/core": "4.0.0-alpha.14",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"global": "^4.3.2", "global": "^4.3.2",
"marko-loader": "^1.3.3", "marko-loader": "^1.3.3",
@ -31,7 +31,7 @@
"react-dom": "^16.4.0" "react-dom": "^16.4.0"
}, },
"peerDependencies": { "peerDependencies": {
"marko": "^4", "marko": "^4.10.0",
"marko-widgets": "^7.0.1" "marko-widgets": "^7.0.1"
} }
} }

View File

@ -1,5 +1,4 @@
import { buildStatic } from '@storybook/core/server'; import { buildStatic } from '@storybook/core/server';
import options from './options'; import options from './options';
buildStatic(options); buildStatic(options);

View File

@ -1,3 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
require('../dist/server/build'); require('../dist/server/build');

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/mithril", "name": "@storybook/mithril",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook for Mithril: Develop Mithril Component in isolation.", "description": "Storybook for Mithril: Develop Mithril Component in isolation.",
"homepage": "https://github.com/storybooks/storybook/tree/master/app/mithril", "homepage": "https://github.com/storybooks/storybook/tree/master/app/mithril",
"bugs": { "bugs": {
@ -22,7 +22,7 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/core": "4.0.0-alpha.10", "@storybook/core": "4.0.0-alpha.14",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"global": "^4.3.2", "global": "^4.3.2",
"react": "^16.4.0", "react": "^16.4.0",

View File

@ -1,5 +1,4 @@
import { buildStatic } from '@storybook/core/server'; import { buildStatic } from '@storybook/core/server';
import options from './options'; import options from './options';
buildStatic(options); buildStatic(options);

View File

@ -1,3 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
require('../dist/server/build'); require('../dist/server/build');

View File

@ -1,6 +1,6 @@
{ {
"name": "@storybook/polymer", "name": "@storybook/polymer",
"version": "4.0.0-alpha.10", "version": "4.0.0-alpha.14",
"description": "Storybook for Polymer: Develop Polymer components in isolation with Hot Reloading.", "description": "Storybook for Polymer: Develop Polymer components in isolation with Hot Reloading.",
"homepage": "https://github.com/storybooks/storybook/tree/master/apps/polymer", "homepage": "https://github.com/storybooks/storybook/tree/master/apps/polymer",
"bugs": { "bugs": {
@ -21,7 +21,7 @@
"prepare": "node ../../scripts/prepare.js" "prepare": "node ../../scripts/prepare.js"
}, },
"dependencies": { "dependencies": {
"@storybook/core": "4.0.0-alpha.10", "@storybook/core": "4.0.0-alpha.14",
"@webcomponents/webcomponentsjs": "^1.2.0", "@webcomponents/webcomponentsjs": "^1.2.0",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",

Some files were not shown because too many files have changed in this diff Show More