Merge branch 'tech/overhaul-ui' of https://github.com/storybooks/storybook into tech/overhaul-ui

This commit is contained in:
Heather Roberts 2018-10-11 17:55:59 +11:00
commit 8674a24484
22 changed files with 725 additions and 335 deletions

View File

@ -0,0 +1,164 @@
import React, { Component, Fragment } from 'react';
import { document } from 'global';
import styled from '@emotion/styled';
import { Popout, Item, Icons, Icon, IconButton, Title, List } from '@storybook/components';
const storybookIframe = 'storybook-preview-iframe';
const ColorIcon = styled.span(
{
background: 'linear-gradient(to right, #F44336, #FF9800, #FFEB3B, #8BC34A, #2196F3, #9C27B0)',
},
({ filter }) => ({
filter: filter === 'mono' ? 'grayscale(100%)' : `url('#${filter}')`,
})
);
const Hidden = styled.div(() => ({
display: 'none',
}));
class ColorBlindness extends Component {
state = {
filter: false,
};
componentDidMount() {
this.iframe = document.getElementById(storybookIframe);
}
setFilter = filter => {
if (!this.iframe) {
throw new Error('Cannot find Storybook iframe');
}
this.iframe.style.filter = filter === 'mono' ? 'grayscale(100%)' : `url('#${filter}')`;
this.setState({
filter,
});
};
render() {
const { filter } = this.state;
return (
<Fragment>
<Hidden>
<svg key="svg">
<defs>
<filter id="protanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.567, 0.433, 0, 0, 0 0.558, 0.442, 0, 0, 0 0, 0.242, 0.758, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="protanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.817, 0.183, 0, 0, 0 0.333, 0.667, 0, 0, 0 0, 0.125, 0.875, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.625, 0.375, 0, 0, 0 0.7, 0.3, 0, 0, 0 0, 0.3, 0.7, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.8, 0.2, 0, 0, 0 0.258, 0.742, 0, 0, 0 0, 0.142, 0.858, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.95, 0.05, 0, 0, 0 0, 0.433, 0.567, 0, 0 0, 0.475, 0.525, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.967, 0.033, 0, 0, 0 0, 0.733, 0.267, 0, 0 0, 0.183, 0.817, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatopsia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.618, 0.320, 0.062, 0, 0 0.163, 0.775, 0.062, 0, 0 0.163, 0.320, 0.516, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
</defs>
</svg>
</Hidden>
<Popout key="filters">
<IconButton key="filter" active={!!filter} title="Color Blindness Emulation">
<Icons icon="mirror" />
</IconButton>
{({ hide }) => (
<List>
{[
'protanopia',
'protanomaly',
'deuteranopia',
'deuteranomaly',
'tritanopia',
'tritanomaly',
'achromatopsia',
'achromatomaly',
].map(i => (
<Item
key={i}
onClick={() => {
this.setFilter(filter === i ? null : i);
hide();
}}
>
<Icon type={<ColorIcon filter={i} />} />
<Title>{i}</Title>
</Item>
))}
<Item
onClick={() => {
this.setFilter(filter === 'mono' ? null : 'mono');
hide();
}}
>
<Icon type={<ColorIcon filter="mono" />} />
<Title>mono</Title>
</Item>
<Item
onClick={() => {
this.setFilter(null);
hide();
}}
>
<Icon type={<ColorIcon />} />
<Title>Off</Title>
</Item>
</List>
)}
</Popout>
</Fragment>
);
}
}
export default ColorBlindness;

View File

@ -1,13 +1,22 @@
import React from 'react';
import addons from '@storybook/addons';
import addons, { types } from '@storybook/addons';
import Panel from './components/Panel';
import ColorBlindness from './components/ColorBlindness';
import { ADDON_ID, PANEL_ID } from './shared';
function init() {
addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
addons.addPanel(PANEL_ID, {
addons.add(PANEL_ID, {
type: types.TOOL,
render: () => <ColorBlindness />,
});
addons.add(PANEL_ID, {
type: types.PANEL,
title: 'Accessibility',
// eslint-disable-next-line react/prop-types
render: ({ active }) => <Panel channel={channel} api={api} active={active} />,

View File

@ -60,7 +60,7 @@ const Instructions = () => (
</Wrapper>
);
export default class BackgroundPanel extends Component {
export default class Panel extends Component {
constructor(props) {
super(props);
@ -138,7 +138,7 @@ export default class BackgroundPanel extends Component {
);
}
}
BackgroundPanel.propTypes = {
Panel.propTypes = {
active: PropTypes.bool.isRequired,
api: PropTypes.shape({
getQueryParam: PropTypes.func,
@ -150,6 +150,6 @@ BackgroundPanel.propTypes = {
removeListener: PropTypes.func,
}),
};
BackgroundPanel.defaultProps = {
Panel.defaultProps = {
channel: undefined,
};

View File

@ -0,0 +1,132 @@
import { document } from 'global';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { Popout, Item, Icons, Icon, IconButton, Title, List } from '@storybook/components';
import Events from './constants';
const storybookIframe = 'storybook-preview-background';
const style = {
iframe: {
transition: 'background 0.25s ease-in-out',
},
};
const ColorIcon = styled.span(({ background }) => ({
background,
}));
const defaultBackground = {
name: 'default',
value: 'transparent',
};
export default class Tool extends Component {
constructor(props) {
super(props);
this.state = { backgrounds: [] };
}
componentDidMount() {
const { api, channel } = this.props;
channel.on(Events.SET, data => {
this.iframe = document.getElementById(storybookIframe);
if (!this.iframe) {
return;
// throw new Error('Cannot find Storybook iframe');
}
Object.keys(style.iframe).forEach(prop => {
this.iframe.style[prop] = style.iframe[prop];
});
const backgrounds = [...data];
this.setState({ backgrounds });
const current = api.getQueryParam('background');
const defaultOrFirst = backgrounds.find(x => x.default) || backgrounds[0];
if (current && backgrounds.find(bg => bg.value === current)) {
this.updateIframe(current);
} else if (defaultOrFirst) {
this.updateIframe(defaultOrFirst.value);
api.setQueryParams({ background: defaultOrFirst.value });
}
});
channel.on(Events.UNSET, () => {
if (!this.iframe) {
return;
// throw new Error('Cannot find Storybook iframe');
}
this.setState({ backgrounds: [] });
this.updateIframe('none');
});
}
setBackgroundFromSwatch = background => {
const { api } = this.props;
this.updateIframe(background);
api.setQueryParams({ background });
};
updateIframe(background) {
this.iframe.style.background = background;
}
render() {
const { backgrounds = [] } = this.state;
if (!backgrounds.length) {
// we should just disable the button
}
const hasDefault = backgrounds.filter(x => x.default).length;
if (!hasDefault) backgrounds.push(defaultBackground);
return (
<Fragment>
<Popout key="backgrounds">
<IconButton key="background" title="Backgrounds">
<Icons icon="photo" />
</IconButton>
{({ hide }) => (
<List>
{backgrounds.map(({ value, name }) => (
<Item
key={`${name} ${value}`}
onClick={() => {
this.setBackgroundFromSwatch(value);
hide();
}}
>
<Icon type={<ColorIcon background={value} />} />
<Title>{value}</Title>
</Item>
))}
</List>
)}
</Popout>
</Fragment>
);
}
}
Tool.propTypes = {
api: PropTypes.shape({
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
channel: PropTypes.shape({
emit: PropTypes.func,
on: PropTypes.func,
removeListener: PropTypes.func,
}),
};
Tool.defaultProps = {
channel: undefined,
};

View File

@ -2,7 +2,7 @@ import React from 'react';
import { shallow, mount } from 'enzyme';
import EventEmitter from 'events';
import BackgroundPanel from '../BackgroundPanel';
import Panel from '../Panel';
import Events from '../constants';
const backgrounds = [
@ -30,26 +30,26 @@ jest.mock('global', () => ({
describe('Background Panel', () => {
it('should exist', () => {
const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} active />);
const BackgroundPanel = shallow(<Panel channel={channel} api={mockedApi} active />);
expect(backgroundPanel).toBeDefined();
expect(BackgroundPanel).toBeDefined();
});
it('should have a default background value of transparent', () => {
const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} active />);
const BackgroundPanel = shallow(<Panel 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', () => {
const backgroundPanel = shallow(<BackgroundPanel channel={channel} api={mockedApi} active />);
const BackgroundPanel = shallow(<Panel 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', () => {
const SpiedChannel = new EventEmitter();
mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />);
mount(<Panel channel={SpiedChannel} api={mockedApi} active />);
SpiedChannel.emit(Events.SET, backgrounds);
expect(mockedApi.getQueryParam).toBeCalledWith('background');
@ -57,7 +57,7 @@ describe('Background Panel', () => {
it('should not unset the query string', () => {
const SpiedChannel = new EventEmitter();
mount(<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />);
mount(<Panel channel={SpiedChannel} api={mockedApi} active />);
SpiedChannel.emit(Events.UNSET, []);
expect(mockedApi.setQueryParams).not.toHaveBeenCalled();
@ -65,36 +65,30 @@ describe('Background Panel', () => {
it('should accept colors through channel and render the correct swatches with a default swatch', () => {
const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
const BackgroundPanel = mount(<Panel channel={SpiedChannel} api={mockedApi} active />);
SpiedChannel.emit(Events.SET, backgrounds);
expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds);
expect(BackgroundPanel.state('backgrounds')).toEqual(backgrounds);
});
it('should allow setting a default swatch', () => {
const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
const BackgroundPanel = mount(<Panel channel={SpiedChannel} api={mockedApi} active />);
const [head, ...tail] = backgrounds;
const localBgs = [{ ...head, default: true }, ...tail];
SpiedChannel.emit(Events.SET, localBgs);
expect(backgroundPanel.state('backgrounds')).toEqual(localBgs);
backgroundPanel.setState({ backgrounds: localBgs }); // force re-render
expect(BackgroundPanel.state('backgrounds')).toEqual(localBgs);
BackgroundPanel.setState({ backgrounds: localBgs }); // force re-render
// check to make sure the default bg was added
const headings = backgroundPanel.find('h4');
const headings = BackgroundPanel.find('h4');
expect(headings).toHaveLength(8);
});
it('should allow the default swatch become the background color', () => {
const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
const BackgroundPanel = mount(<Panel channel={SpiedChannel} api={mockedApi} active />);
const [head, second, ...tail] = backgrounds;
const localBgs = [head, { ...second, default: true }, ...tail];
SpiedChannel.on('background', bg => {
@ -102,41 +96,36 @@ describe('Background Panel', () => {
});
SpiedChannel.emit(Events.SET, localBgs);
expect(backgroundPanel.state('backgrounds')).toEqual(localBgs);
backgroundPanel.setState({ backgrounds: localBgs }); // force re-render
expect(BackgroundPanel.state('backgrounds')).toEqual(localBgs);
BackgroundPanel.setState({ backgrounds: localBgs }); // force re-render
// check to make sure the default bg was added
const headings = backgroundPanel.find('h4');
const headings = BackgroundPanel.find('h4');
expect(headings).toHaveLength(8);
});
it('should unset all swatches on receiving the background-unset message', () => {
const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
const BackgroundPanel = mount(<Panel channel={SpiedChannel} api={mockedApi} active />);
SpiedChannel.emit(Events.SET, backgrounds);
expect(backgroundPanel.state('backgrounds')).toEqual(backgrounds);
backgroundPanel.setState({ backgrounds }); // force re-render
expect(BackgroundPanel.state('backgrounds')).toEqual(backgrounds);
BackgroundPanel.setState({ backgrounds }); // force re-render
SpiedChannel.emit(Events.UNSET);
expect(backgroundPanel.state('backgrounds')).toHaveLength(0);
expect(BackgroundPanel.state('backgrounds')).toHaveLength(0);
});
it('should set iframe background', () => {
const SpiedChannel = new EventEmitter();
const backgroundPanel = mount(
<BackgroundPanel channel={SpiedChannel} api={mockedApi} active />
);
backgroundPanel.setState({ backgrounds }); // force re-render
const BackgroundPanel = mount(<Panel channel={SpiedChannel} api={mockedApi} active />);
BackgroundPanel.setState({ backgrounds }); // force re-render
backgroundPanel
.find('h4')
BackgroundPanel.find('h4')
.first()
.simulate('click');
expect(backgroundPanel.instance().iframe.style).toMatchObject({
expect(BackgroundPanel.instance().iframe.style).toMatchObject({
background: backgrounds[0].value,
});
});

View File

@ -1,14 +1,26 @@
import React from 'react';
import addons from '@storybook/addons';
import addons, { types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
import BackgroundPanel from './BackgroundPanel';
import Panel from './Panel';
import Tool from './Tool';
addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
addons.addPanel(PANEL_ID, {
title: 'Backgrounds',
// eslint-disable-next-line react/prop-types
render: ({ active }) => <BackgroundPanel channel={channel} api={api} active={active} />,
function init() {
addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
addons.add(PANEL_ID, {
type: types.TOOL,
render: () => <Tool channel={channel} api={api} />,
});
addons.add(PANEL_ID, {
type: types.PANEL,
title: 'Backgrounds',
// eslint-disable-next-line react/prop-types
render: ({ active }) => <Panel channel={channel} api={api} active={active} />,
});
});
});
}
export { init };

78
addons/notes/src/Panel.js Normal file
View File

@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { SyntaxHighlighter, Placeholder } from '@storybook/components';
import Markdown from 'markdown-to-jsx';
const Panel = styled.div({
padding: 10,
boxSizing: 'border-box',
width: '100%',
});
export default class NotesPanel extends React.Component {
state = {
markdown: '',
};
// use our SyntaxHighlighter component in place of a <code> element when
// converting markdown to react elements
markdownOpts = { overrides: { code: SyntaxHighlighter } };
componentDidMount() {
this.mounted = true;
const { channel, api } = this.props;
// Clear the current notes on every story change.
this.stopListeningOnStory = api.onStory(() => {
const { text } = this.state;
if (this.mounted && text !== '') {
this.onAddNotes('');
}
});
channel.on('storybook/notes/add_notes', this.onAddNotes);
}
componentWillUnmount() {
this.mounted = false;
const { channel } = this.props;
this.stopListeningOnStory();
channel.removeListener('storybook/notes/add_notes', this.onAddNotes);
}
onAddNotes = markdown => {
this.setState({ markdown });
};
render() {
const { active } = this.props;
const { markdown } = this.state;
if (!active) {
return null;
}
return markdown ? (
<Panel className="addon-notes-container">
<Markdown options={this.markdownOpts}>{markdown}</Markdown>
</Panel>
) : (
<Placeholder>There is no info/note</Placeholder>
);
}
}
NotesPanel.propTypes = {
active: PropTypes.bool.isRequired,
channel: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
api: PropTypes.shape({
onStory: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
};

View File

@ -1,103 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import addons from '@storybook/addons';
import { SyntaxHighlighter, Placeholder } from '@storybook/components';
import Markdown from 'markdown-to-jsx';
import addons, { types } from '@storybook/addons';
import Panel from './Panel';
import { ADDON_ID, PANEL_ID } from './shared';
const Panel = styled.div({
padding: 10,
boxSizing: 'border-box',
width: '100%',
});
export class NotesPanel extends React.Component {
state = {
markdown: '',
};
// use our SyntaxHighlighter component in place of a <code> element when
// converting markdown to react elements
markdownOpts = { overrides: { code: SyntaxHighlighter } };
componentDidMount() {
this.mounted = true;
const { channel, api } = this.props;
// Clear the current notes on every story change.
this.stopListeningOnStory = api.onStory(() => {
const { text } = this.state;
if (this.mounted && text !== '') {
this.onAddNotes('');
}
});
channel.on('storybook/notes/add_notes', this.onAddNotes);
}
componentWillUnmount() {
this.mounted = false;
const { channel } = this.props;
this.stopListeningOnStory();
channel.removeListener('storybook/notes/add_notes', this.onAddNotes);
}
onAddNotes = markdown => {
this.setState({ markdown });
};
render() {
const { active } = this.props;
const { markdown } = this.state;
if (!active) {
return null;
}
return markdown ? (
<Panel className="addon-notes-container">
<Markdown options={this.markdownOpts}>{markdown}</Markdown>
</Panel>
) : (
<Placeholder>There is no info/note</Placeholder>
);
}
}
NotesPanel.propTypes = {
active: PropTypes.bool.isRequired,
channel: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
api: PropTypes.shape({
onStory: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
};
addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
// eslint-disable-next-line react/prop-types
const render = ({ active }) => <NotesPanel channel={channel} api={api} active={active} />;
const render = ({ active }) => <Panel channel={channel} api={api} active={active} />;
const title = 'Notes';
const type = 'tab';
if (addons.addMain) {
addons.addMain(PANEL_ID, {
type,
title,
route: '/info/',
render,
});
} else {
addons.addPanel(PANEL_ID, {
title,
render,
});
}
addons.add(PANEL_ID, {
type: types.TAB,
title,
route: '/info/',
render,
});
addons.add(PANEL_ID, {
type: types.PANEL,
title,
route: '/info/',
render,
});
});

View File

@ -0,0 +1,191 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { document } from 'global';
import styled from '@emotion/styled';
import { Popout, Item, Icons, IconButton, Title, List } from '@storybook/components';
import { resetViewport, viewportsTransformer } from './viewportInfo';
import {
SET_STORY_DEFAULT_VIEWPORT_EVENT_ID,
CONFIGURE_VIEWPORT_EVENT_ID,
UPDATE_VIEWPORT_EVENT_ID,
VIEWPORT_CHANGED_EVENT_ID,
INITIAL_VIEWPORTS,
DEFAULT_VIEWPORT,
} from '../../shared';
const storybookIframe = 'storybook-preview-iframe';
const Container = styled.div({
padding: 15,
width: '100%',
boxSizing: 'border-box',
});
Container.displayName = 'Container';
const getDefaultViewport = (viewports, candidateViewport) =>
candidateViewport in viewports ? candidateViewport : Object.keys(viewports)[0];
const getViewports = viewports =>
Object.keys(viewports).length > 0 ? viewports : INITIAL_VIEWPORTS;
export default class ViewportTool extends Component {
static defaultOptions = {
viewports: INITIAL_VIEWPORTS,
defaultViewport: DEFAULT_VIEWPORT,
};
iframe = undefined;
previousViewport = DEFAULT_VIEWPORT;
static propTypes = {
api: PropTypes.shape({
selectStory: PropTypes.func.isRequired,
}).isRequired,
channel: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
};
state = {
viewport: DEFAULT_VIEWPORT,
defaultViewport: DEFAULT_VIEWPORT,
viewports: viewportsTransformer(INITIAL_VIEWPORTS),
};
componentDidMount() {
this.mounted = true;
const { channel, api } = this.props;
const { defaultViewport } = this.state;
channel.on(UPDATE_VIEWPORT_EVENT_ID, this.changeViewport);
channel.on(CONFIGURE_VIEWPORT_EVENT_ID, this.configure);
channel.on(SET_STORY_DEFAULT_VIEWPORT_EVENT_ID, this.setStoryDefaultViewport);
this.unsubscribeFromOnStory = api.onStory(() => {
const { storyDefaultViewport } = this.state;
if (this.mounted && !storyDefaultViewport === defaultViewport) {
this.setStoryDefaultViewport(defaultViewport);
}
});
}
componentWillUnmount() {
this.mounted = false;
const { channel } = this.props;
this.unsubscribeFromOnStory();
channel.removeListener(UPDATE_VIEWPORT_EVENT_ID, this.changeViewport);
channel.removeListener(CONFIGURE_VIEWPORT_EVENT_ID, this.configure);
channel.removeListener(SET_STORY_DEFAULT_VIEWPORT_EVENT_ID, this.setStoryDefaultViewport);
}
setStoryDefaultViewport = viewport => {
const { viewports } = this.state;
const defaultViewport = getDefaultViewport(viewports, viewport);
this.setState({
storyDefaultViewport: defaultViewport,
});
this.changeViewport(defaultViewport);
};
configure = (options = ViewportTool.defaultOptions) => {
this.iframe = document.getElementById(storybookIframe);
const viewports = getViewports(options.viewports);
const defaultViewport = getDefaultViewport(viewports, options.defaultViewport);
this.setState(
{
defaultViewport,
viewport: defaultViewport,
viewports: viewportsTransformer(viewports),
},
this.updateIframe
);
};
changeViewport = viewport => {
const { viewport: previousViewport } = this.state;
if (previousViewport !== viewport) {
this.setState(
{
viewport,
},
() => {
this.updateIframe();
this.emitViewportChanged();
}
);
}
};
emitViewportChanged = () => {
const { channel } = this.props;
const { viewport, viewports } = this.state;
if (!this.shouldNotify()) {
return;
}
this.previousViewport = viewport;
channel.emit(VIEWPORT_CHANGED_EVENT_ID, {
viewport: viewports[viewport],
});
};
shouldNotify = () => {
const { viewport } = this.state;
return this.previousViewport !== viewport;
};
updateIframe = () => {
const { viewports, viewport: viewportKey } = this.state;
const viewport = viewports[viewportKey] || resetViewport;
if (!this.iframe) {
throw new Error('Cannot find Storybook iframe');
}
Object.keys(viewport.styles).forEach(prop => {
this.iframe.style[prop] = viewport.styles[prop];
});
};
render() {
const { viewports } = this.state;
return (
<Fragment>
<Popout key="viewports">
<IconButton key="viewport" title="Change Viewport">
<Icons icon="browser" />
</IconButton>
{({ hide }) => (
<List>
{Object.entries(viewports).map(([key, { name }]) => (
<Item
key={key}
onClick={() => {
this.changeViewport(key);
hide();
}}
>
<Title>{name}</Title>
</Item>
))}
</List>
)}
</Popout>
</Fragment>
);
}
}

View File

@ -1,13 +1,21 @@
import React from 'react';
import addons from '@storybook/addons';
import addons, { types } from '@storybook/addons';
import Tool from './components/Tool';
import Panel from './components/Panel';
import { ADDON_ID, PANEL_ID } from '../shared';
const addChannel = api => {
const channel = addons.getChannel();
addons.addPanel(PANEL_ID, {
addons.add(PANEL_ID, {
type: types.TOOL,
render: () => <Tool channel={channel} api={api} />,
});
addons.add(PANEL_ID, {
type: types.PANEL,
title: 'Viewport',
// eslint-disable-next-line react/prop-types
render: ({ active }) => <Panel channel={channel} api={api} active={active} />,

View File

@ -38,10 +38,6 @@ export default class ReactProvider extends Provider {
}
}
getPanels() {
return addons.getPanels();
}
renderPreview(kind, story) {
this.selection = { kind, story };
this.channel.emit(Events.SET_CURRENT_STORY, { kind, story });

View File

@ -1,15 +1,18 @@
// Resolves to window in browser and to global in node
import global from 'global';
import { types, isSupportedType } from './types';
// import { TabWrapper } from '@storybook/components';
export mockChannel from './storybook-channel-mock';
export { makeDecorator } from './make-decorator';
export { types, isSupportedType };
export class AddonStore {
constructor() {
this.loaders = {};
this.panels = {};
this.mains = {};
this.elements = {};
this.channel = null;
this.preview = null;
this.database = null;
@ -49,30 +52,24 @@ export class AddonStore {
this.database = database;
}
getPanels() {
return this.panels;
}
addPanel(name, panel) {
// supporting legacy addons, which have not migrated to the active-prop
// const original = panel.render;
// if (original && original.toString() && !original.toString().match(/active/)) {
// this.panels[name] = {
// ...panel,
// render: ({ active }) => TabWrapper({ active, render: original }),
// };
// } else {
this.panels[name] = panel;
// }
this.add(name, {
type: types.PANEL,
...panel,
});
}
// might reconsider using something more generic and adding a 'type'
getMains() {
return this.mains;
add(name, options) {
const { type } = options;
// assert isSupportedType
if (!this.elements[type]) {
this.elements[type] = [];
}
this.elements[type].push(options);
}
addMain(name, panel) {
this.mains[name] = panel;
getElements(type) {
return this.elements[type] || [];
}
register(name, loader) {

9
lib/addons/src/types.js Normal file
View File

@ -0,0 +1,9 @@
export const types = {
TAB: 'tab',
PANEL: 'panel',
TOOL: 'tool',
};
export function isSupportedType(type) {
return Object.key(types).find(typeKey => types[typeKey] === type);
}

View File

@ -30,6 +30,7 @@
"@emotion/styled": "^0.10.6",
"@reach/router": "^1.1.1",
"@storybook/core-events": "4.0.0-alpha.23",
"@storybook/addons": "4.0.0-alpha.23",
"global": "^4.3.2",
"immer": "^1.5.0",
"js-beautify": "^1.8.6",

View File

@ -20,6 +20,7 @@ export { default as Placeholder } from './placeholder/placeholder';
export { Explorer } from './explorer/explorer';
export { Preview } from './preview/preview';
export { IconButton, Separator, Toolbar } from './preview/toolbar';
export { default as Heading } from './heading/heading';

View File

@ -3,8 +3,7 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import Events from '@storybook/core-events';
import { Popout, Item, Icon, Title, List } from '../menu/menu';
import { types } from '@storybook/addons';
import { IconButton, Toolbar, Separator } from './toolbar';
import Icons from '../icon/icon';
@ -90,25 +89,17 @@ const Frame = styled.div({
},
});
const FrameWrap = styled.div(
({ offset }) => ({
position: 'absolute',
overflow: 'auto',
left: 0,
right: 0,
bottom: 0,
top: offset,
zIndex: 3,
height: offset ? `calc(100% - ${offset}px)` : '100%',
background: 'transparent',
}),
({ filter }) =>
filter
? {
filter: filter === 'mono' ? 'grayscale(100%)' : `url('#${filter}')`,
}
: {}
);
const FrameWrap = styled.div(({ offset }) => ({
position: 'absolute',
overflow: 'auto',
left: 0,
right: 0,
bottom: 0,
top: offset,
zIndex: 3,
height: offset ? `calc(100% - ${offset}px)` : '100%',
background: 'transparent',
}));
const UnstyledLink = styled(Link)({
color: 'inherit',
@ -116,24 +107,14 @@ const UnstyledLink = styled(Link)({
display: 'inline-block',
});
const ColorIcon = styled.span(
{
background: 'linear-gradient(to right, #F44336, #FF9800, #FFEB3B, #8BC34A, #2196F3, #9C27B0)',
},
({ filter }) => ({
filter: filter === 'mono' ? 'grayscale(100%)' : `url('#${filter}')`,
})
);
// eslint-disable-next-line react/no-multi-comp
class Preview extends Component {
state = {
zoom: 1,
grid: false,
filter: false,
};
shouldComponentUpdate({ location, toolbar, options }, { zoom, grid, filter }) {
shouldComponentUpdate({ location, toolbar, options }, { zoom, grid }) {
const { props, state } = this;
return (
@ -141,7 +122,6 @@ class Preview extends Component {
location !== props.location ||
toolbar !== props.toolbar ||
zoom !== state.zoom ||
filter !== state.filter ||
grid !== state.grid
);
}
@ -154,77 +134,19 @@ class Preview extends Component {
}
render() {
const { id, toolbar = true, location = '/', panels, actions, options } = this.props;
const { zoom, grid, filter } = this.state;
const { id, toolbar = true, location = '/', getElements, actions, options } = this.props;
const { zoom, grid } = this.state;
const panels = getElements(types.PANEL);
const toolbarHeight = toolbar ? 40 : 0;
const panelList = Object.entries(panels).map(([key, value]) => ({ ...value, key }));
const tabsList = panelList.length
? [{ route: '/components/', title: 'Canvas', key: 'canvas' }].concat(panelList)
: [];
const tabsList = [{ route: '/components/', title: 'Canvas', key: 'canvas' }].concat(
getElements(types.TAB)
);
const toolList = getElements(types.TOOL);
return (
<Fragment>
<svg key="svg">
<defs>
<filter id="protanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.567, 0.433, 0, 0, 0 0.558, 0.442, 0, 0, 0 0, 0.242, 0.758, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="protanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.817, 0.183, 0, 0, 0 0.333, 0.667, 0, 0, 0 0, 0.125, 0.875, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.625, 0.375, 0, 0, 0 0.7, 0.3, 0, 0, 0 0, 0.3, 0.7, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.8, 0.2, 0, 0, 0 0.258, 0.742, 0, 0, 0 0, 0.142, 0.858, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.95, 0.05, 0, 0, 0 0, 0.433, 0.567, 0, 0 0, 0.475, 0.525, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.967, 0.033, 0, 0, 0 0, 0.733, 0.267, 0, 0 0, 0.183, 0.817, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatopsia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.618, 0.320, 0.062, 0, 0 0.163, 0.775, 0.062, 0, 0 0.163, 0.320, 0.516, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
</defs>
</svg>
{toolbar ? (
<Toolbar
key="toolbar"
@ -250,54 +172,11 @@ class Preview extends Component {
<IconButton active={!!grid} key="grid" onClick={() => this.setState({ grid: !grid })}>
<Icons icon="grid" />
</IconButton>,
<Popout key="filters">
<IconButton key="filter" active={!!filter}>
<Icons icon="mirror" />
</IconButton>
{({ hide }) => (
<List>
{[
'protanopia',
'protanomaly',
'deuteranopia',
'deuteranomaly',
'tritanopia',
'tritanomaly',
'achromatopsia',
'achromatomaly',
].map(i => (
<Item
key={i}
onClick={() => {
this.setState({ filter: filter === i ? null : i });
hide();
}}
>
<Icon type={<ColorIcon filter={i} />} />
<Title>{i}</Title>
</Item>
))}
<Item
onClick={() => {
this.setState({ filter: filter === 'mono' ? null : 'mono' });
hide();
}}
>
<Icon type={<ColorIcon filter="mono" />} />
<Title>mono</Title>
</Item>
<Item
onClick={() => {
this.setState({ filter: null });
hide();
}}
>
<Icon type={<ColorIcon />} />
<Title>Off</Title>
</Item>
</List>
)}
</Popout>,
<Fragment>
{toolList.map(t => (
<Fragment>{t.render()}</Fragment>
))}
</Fragment>,
]}
right={[
<Separator key="1" />,
@ -311,7 +190,7 @@ class Preview extends Component {
]}
/>
) : null}
<FrameWrap key="frame" offset={toolbarHeight} filter={filter || undefined}>
<FrameWrap key="frame" offset={toolbarHeight}>
<Route path="components" startsWith hideOnly>
<Frame
style={{
@ -348,7 +227,7 @@ Preview.propTypes = {
removeListener: PropTypes.func,
}).isRequired,
location: PropTypes.string.isRequired,
panels: PropTypes.shape({}).isRequired,
getElements: PropTypes.func.isRequired,
options: PropTypes.shape({
isFullscreen: PropTypes.bool,
}).isRequired,

View File

@ -12,12 +12,8 @@ export default class ReactProvider extends Provider {
this.channel.emit(Events.CHANNEL_CREATED);
}
getPanels() {
return addons.getPanels();
}
getMains() {
return addons.getMains();
getElements(type) {
return addons.getElements(type);
}
handleAPI(api) {

View File

@ -36,7 +36,7 @@ import React from 'react';
import { Provider } from '@storybook/ui';
export default class MyProvider extends Provider {
getPanels() {
getElements(type) {
return {};
}

View File

@ -29,6 +29,7 @@
"@emotion/provider": "^0.11.2",
"@emotion/styled": "^0.10.6",
"@reach/router": "^1.1.1",
"@storybook/addons": "4.0.0-alpha.23",
"@storybook/components": "4.0.0-alpha.23",
"@storybook/core-events": "4.0.0-alpha.23",
"@storybook/mantra-core": "^1.7.2",

View File

@ -13,7 +13,7 @@ describe('manager.ui.containers.panel', () => {
test2: {},
sdp: {},
};
const getPanels = () => panels;
const getElements = () => panels;
const props = {};
const env = {
@ -24,7 +24,7 @@ describe('manager.ui.containers.panel', () => {
}),
context: () => ({
provider: {
getPanels,
getElements,
},
}),
};

View File

@ -4,7 +4,7 @@ import { Preview } from '@storybook/components';
export function mapper({ store, uiStore }) {
return {
channel: store.channel,
panels: store.mains,
getElements: store.getElements,
actions: {
toggleFullscreen: () => uiStore.toggleFullscreen(),
},

View File

@ -1,6 +1,7 @@
import { observable, action, set } from 'mobx';
import pick from 'lodash.pick';
import { themes } from '@storybook/components';
import { types } from '@storybook/addons';
import qs from 'qs';
export function ensurePanel(panels, selectedPanel, currentPanel) {
@ -47,21 +48,25 @@ const createStore = ({ provider, history }) => {
],
selectedPanelValue: null,
get selectedPanel() {
return ensurePanel(this.panels, this.selectedPanelValue, this.selectedPanelValue);
},
set selectedPanel(value) {
this.selectedPanelValue = value;
},
get panels() {
return provider.getPanels();
},
get channel() {
return provider.channel;
},
get mains() {
return provider.getMains();
get panels() {
return this.getElements(types.PANEL);
},
getElements(type) {
return provider.getElements(type);
},
setOptions(changes) {