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

This commit is contained in:
Jessica Koch 2018-10-25 20:02:01 -07:00
commit 31cf0df734
27 changed files with 669 additions and 3628 deletions

View File

@ -23,15 +23,8 @@ export default class NotesPanel extends React.Component {
componentDidMount() {
this.mounted = true;
const { channel, api } = this.props;
const { channel } = 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(EVENT_ID, this.onAddNotes);
channel.on(Events.SET_CURRENT_STORY, this.clearNotes);
}

View File

@ -15,11 +15,4 @@ addons.register(ADDON_ID, api => {
route: '/info/',
render,
});
addons.add(PANEL_ID, {
type: types.PANEL,
title,
route: '/info/',
render,
});
});

View File

@ -93,7 +93,7 @@ class Preview extends Component {
key={t.key || index}
to={location.replace(/^\/(components|info)\//, t.route)}
>
<TabButton>{t.title}</TabButton>
<TabButton active={location.indexOf(t.route) === 0}>{t.title}</TabButton>
</S.UnstyledLink>
))}
</TabBar>,
@ -108,7 +108,7 @@ class Preview extends Component {
<IconButton active={!!grid} key="grid" onClick={() => this.setState({ grid: !grid })}>
<Icons icon="grid" />
</IconButton>,
...toolList.map((t, index) => <Fragment key={`t${index}`}>{t.render()}</Fragment>),
...toolList.map((t, index) => <Fragment key={t.key || index}>{t.render()}</Fragment>),
]}
right={[
<Separator key="1" />,

View File

@ -17,9 +17,6 @@ export default class ReactProvider extends Provider {
}
handleAPI(api) {
api.onStory((kind, story) => {
this.channel.emit(Events.SET_CURRENT_STORY, { kind, story });
});
this.channel.on(Events.SET_STORIES, data => {
api.setStories(data.stories);
});

View File

@ -46,8 +46,6 @@
"lodash.sortby": "^4.7.0",
"lodash.throttle": "^4.1.1",
"memoizerific": "^1.11.3",
"mobx": "^5.0.3",
"mobx-react": "^5.2.3",
"prop-types": "^15.6.2",
"qs": "^6.5.2",
"react-draggable": "^3.0.5",

View File

@ -1,13 +1,13 @@
import React from 'react';
import styled from '@emotion/styled';
import ResizeDetector from 'react-resize-detector';
import { inject, observer } from 'mobx-react';
import { Mobile } from './components/layout/mobile';
import { Desktop } from './components/layout/desktop';
import Nav from './containers/nav';
import Preview from './containers/preview';
import Panel from './containers/panel';
import { Consumer } from './core/context';
const Reset = styled.div(({ theme }) => ({
fontFamily: theme.mainTextFace,
@ -26,28 +26,31 @@ const Reset = styled.div(({ theme }) => ({
overflow: 'hidden',
}));
const App = ({ uiStore }) => {
const props = {
Nav,
Preview,
Panel,
options: {
...uiStore,
},
};
const App = () => (
<Consumer>
{({ state }) => {
const props = {
Nav,
Preview,
Panel,
options: {
...state.ui,
},
};
return (
<Reset>
<ResizeDetector handleWidth>
{width => {
if (width === 0) return <div />;
if (width < 600) return <Mobile {...props} />;
return <Desktop {...props} />;
}}
</ResizeDetector>
</Reset>
);
};
return (
<Reset>
<ResizeDetector handleWidth>
{width => {
if (width === 0) return <div />;
if (width < 600) return <Mobile {...props} />;
return <Desktop {...props} />;
}}
</ResizeDetector>
</Reset>
);
}}
</Consumer>
);
// TODO: use a uiStore and observer in the layout
export default inject('uiStore')(observer(App));
export default App;

View File

@ -1,56 +1,58 @@
import React from 'react';
import { inject } from 'mobx-react';
import { Badge } from '@storybook/components';
import { controlOrMetaKey, optionOrAlt } from '../../../components/src/treeview/utils';
import Nav from '../components/nav/nav';
export const mapper = ({ store, uiStore }) => {
import { Consumer } from '../core/context';
export const mapper = ({ state, manager }) => {
const {
uiOptions: { name, url },
notifications,
} = store;
ui: { isFullscreen, showPanel, showNav, panelPosition },
} = state;
return {
title: name,
url,
notifications,
stories: store.storiesHash,
stories: state.storiesHash,
menu: [
{
id: 'about',
title: 'About your storybook',
action: () => store.navigate('/settings/about'),
action: () => manager.navigate('/settings/about'),
detail: <Badge>Update</Badge>,
icon: '',
},
{
id: 'F',
title: 'Go Fullscreen',
action: () => uiStore.toggleFullscreen(),
action: () => manager.toggleFullscreen(),
detail: 'F',
icon: uiStore.isFullscreen ? 'check' : '',
icon: isFullscreen ? 'check' : '',
},
{
id: 'S',
title: 'Toggle Panel',
action: () => uiStore.togglePanel(),
action: () => manager.togglePanel(),
detail: 'D',
icon: uiStore.showPanel ? 'check' : '',
icon: showPanel ? 'check' : '',
},
{
id: 'D',
title: 'Toggle Panel Position',
action: () => uiStore.togglePanelPosition(),
action: () => manager.togglePanelPosition(),
detail: 'G',
icon: uiStore.panelPosition === 'bottom' ? 'bottombar' : 'sidebaralt',
icon: panelPosition === 'bottom' ? 'bottombar' : 'sidebaralt',
},
{
id: 'A',
title: 'Toggle Navigation',
action: () => uiStore.toggleNav(),
action: () => manager.toggleNav(),
detail: 'S',
icon: uiStore.showNav ? 'check' : '',
icon: showNav ? 'check' : '',
},
{
id: '/',
@ -62,35 +64,35 @@ export const mapper = ({ store, uiStore }) => {
{
id: 'up',
title: 'Previous component',
action: () => store.jumpToComponent(-1),
action: () => manager.jumpToComponent(-1),
detail: `${optionOrAlt()}`,
icon: '',
},
{
id: 'down',
title: 'Next component',
action: () => store.jumpToComponent(1),
action: () => manager.jumpToComponent(1),
detail: `${optionOrAlt()}`,
icon: '',
},
{
id: 'prev',
title: 'Previous story',
action: () => store.jumpToStory(-1),
action: () => manager.jumpToStory(-1),
detail: `${optionOrAlt()}`,
icon: '',
},
{
id: 'next',
title: 'Next story',
action: () => store.jumpToStory(1),
action: () => manager.jumpToStory(1),
detail: `${optionOrAlt()}`,
icon: '',
},
{
id: 'shortcuts',
title: 'Customize Storybook Hotkeys',
action: () => store.navigate('/settings/shortcuts'),
action: () => manager.navigate('/settings/shortcuts'),
detail: `${controlOrMetaKey()} ,`,
icon: 'wrench',
},
@ -98,4 +100,6 @@ export const mapper = ({ store, uiStore }) => {
};
};
export default inject(({ store, uiStore }) => mapper({ store, uiStore }))(Nav);
export default props => (
<Consumer>{({ state, manager }) => <Nav {...props} {...mapper({ state, manager })} />}</Consumer>
);

View File

@ -1,19 +1,22 @@
import { inject } from 'mobx-react';
import { types } from '@storybook/addons';
import React from 'react';
import AddonPanel from '../components/panel/panel';
import { Consumer } from '../core/context';
export function mapper({ store, uiStore }) {
return {
panels: store.getElements(types.PANEL),
selectedPanel: store.selectedPanel,
panelPosition: uiStore.panelPosition,
actions: {
onSelect: panel => store.selectPanel(panel),
toggleVisibility: () => uiStore.togglePanel(),
togglePosition: () => uiStore.togglePanelPosition(),
},
};
}
export default props => (
<Consumer>
{({ state, manager }) => {
const customProps = {
panels: manager.getPanels(),
selectedPanel: manager.getSelectedPanel(),
panelPosition: state.ui.panelPosition,
actions: {
onSelect: panel => manager.selectPanel(panel),
toggleVisibility: () => manager.togglePanel(),
togglePosition: () => manager.togglePanelPosition(),
},
};
export default inject(({ store, uiStore }) => mapper({ store, uiStore }))(AddonPanel);
return <AddonPanel {...props} {...customProps} />;
}}
</Consumer>
);

View File

@ -1,17 +1,21 @@
import { inject } from 'mobx-react';
import React from 'react';
import { Preview } from '@storybook/components';
import { Consumer } from '../core/context';
export function mapper({ store, uiStore }) {
return {
channel: store.channel,
getElements: store.getElements,
actions: {
toggleFullscreen: () => uiStore.toggleFullscreen(),
},
options: {
...uiStore,
},
};
}
export default inject(({ store, uiStore }) => mapper({ store, uiStore }))(Preview);
export default props => (
<Consumer>
{({ state, manager }) => {
const customProps = {
channel: manager.getChannel(),
getElements: manager.getElements,
actions: {
toggleFullscreen: () => manager.toggleFullscreen(),
},
options: {
...state.ui,
},
};
return <Preview {...props} {...customProps} />;
}}
</Consumer>
);

View File

@ -1,14 +1,7 @@
import React from 'react';
import ThemeProvider from '@emotion/provider';
import { Consumer } from '../core/context';
import { inject } from 'mobx-react';
export default inject(stores => {
const state = stores.store;
const {
uiOptions: { theme },
} = state;
return {
theme,
};
})(ThemeProvider);
export default props => (
<Consumer>{({ state }) => <ThemeProvider {...props} theme={state.uiOptions.theme} />}</Consumer>
);

View File

@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
const ctx = React.createContext();
export class Provider extends React.Component {
static propTypes = {
manager: PropTypes.shape().isRequired,
children: PropTypes.node.isRequired,
};
componentDidMount() {
const { manager } = this.props;
this.unsubscribe = manager.store.subscribe(() => {
this.setState({});
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { manager, children } = this.props;
return (
<ctx.Provider
value={{
state: manager.store.getState(),
manager,
}}
>
{children}
</ctx.Provider>
);
}
}
export const { Consumer } = ctx;

View File

@ -0,0 +1,39 @@
import { themes } from '@storybook/components';
// Returns the initialState of the app
export default () => ({
stories: [],
storiesHash: {},
selectedId: null,
shortcutOptions: {
full: false,
nav: true,
panel: 'right',
enableShortcuts: true,
},
uiOptions: {
name: 'STORYBOOK',
url: 'https://github.com/storybooks/storybook',
sortStoriesByKind: false,
sidebarAnimations: true,
theme: themes.normal,
},
ui: {
isFullscreen: false,
showPanel: true,
showNav: true,
panelPosition: 'bottom',
},
customQueryParams: {},
notifications: [
{
id: 'update',
level: 2,
link: '/settings/about',
icon: '🎉',
content: `There's a new version available: 4.0.0`,
},
],
selectedPanelValue: null,
});

View File

@ -0,0 +1,35 @@
import createStore from './store';
import getInitialState from './initial-state';
import initPanels from './panels';
import initStories from './stories';
import initUi from './ui';
import initOptions from './options';
// app state manager
const createManager = ({ history, provider }) => {
const store = createStore(getInitialState());
// ctx to pass to all managers
const ctx = {
store,
history,
provider,
};
return {
store,
getChannel() {
return provider.channel;
},
getElements(type) {
return provider.getElements(type);
},
...initOptions(ctx),
...initUi(ctx),
...initPanels(ctx),
...initStories(ctx),
};
};
export default createManager;

View File

@ -0,0 +1,48 @@
import pick from 'lodash.pick';
import { ensurePanel } from './panels';
export default function({ store }) {
return {
setOptions(changes) {
const { uiOptions: oldOptions, selectedPanel, panels } = store.getState();
const { newSelectedPanel } = changes;
const options = pick(changes, Object.keys(oldOptions));
store.setState({
uiOptions: {
...oldOptions,
...options,
},
});
if (newSelectedPanel && newSelectedPanel !== selectedPanel) {
store.setState({
selectedPanel: ensurePanel(panels, newSelectedPanel, selectedPanel),
});
}
},
setShortcutsOptions(options) {
const { shortcutOptions } = store.getState();
store.setState({
shortcutOptions: {
...shortcutOptions,
...pick(options, Object.keys(shortcutOptions)),
},
});
},
setQueryParams(customQueryParams) {
const state = store.getState();
store.setState({
customQueryParams: {
...state.customQueryParams,
...Object.keys(customQueryParams).reduce((acc, key) => {
if (customQueryParams[key] !== null) acc[key] = customQueryParams[key];
return acc;
}, {}),
},
});
},
};
}

43
lib/ui/src/core/panels.js Normal file
View File

@ -0,0 +1,43 @@
import { types } from '@storybook/addons';
export function ensurePanel(panels, selectedPanel, currentPanel) {
const keys = Object.keys(panels);
if (keys.indexOf(selectedPanel) >= 0) {
return selectedPanel;
}
if (keys.length) {
return keys[0];
}
return currentPanel;
}
export default function initPanels({ store, provider }) {
// getters
function getPanels() {
return provider.getElements(types.PANEL);
}
function getSelectedPanel() {
const { selectedPanelValue } = store.getState();
const panels = getPanels();
return ensurePanel(panels, selectedPanelValue, selectedPanelValue);
}
// setters
function setSelectedPanel(value) {
store.setState({ selectedPanelValue: value });
}
function selectPanel(panelName) {
store.setState({ selectedPanel: panelName });
}
return {
getSelectedPanel,
setSelectedPanel,
getPanels,
selectPanel,
};
}

33
lib/ui/src/core/store.js Normal file
View File

@ -0,0 +1,33 @@
// simple store implementation
const createStore = initialState => {
let state = initialState;
const listeners = [];
function notify() {
listeners.forEach(l => l());
}
function setState(patch) {
if (typeof patch === 'function') {
state = { ...state, ...patch(state) };
} else {
state = { ...state, ...patch };
}
notify();
}
return {
getState() {
return state;
},
setState,
subscribe(listener) {
listeners.push(listener);
return function unsubscribe(l) {
listeners.splice(listeners.indexOf(l), 1);
};
},
};
};
export default createStore;

182
lib/ui/src/core/stories.js Normal file
View File

@ -0,0 +1,182 @@
import qs from 'qs';
/**
* Gets the current component from the current location
* @param {Object} options
* * @param {Object} options.history History handler
*/
function getUrlData({ history }) {
const { search } = history.location;
const { path = '' } = qs.parse(search, { ignoreQueryPrefix: true });
const [, p1, p2] = path.match(/\/([^/]+)\/([^/]+)?/) || [];
const result = {};
if (p1 && p1.match(/(components|info)/)) {
Object.assign(result, {
componentRoot: p1,
component: p2,
});
}
return result;
}
export default function initStories({ store, history }) {
return {
jumpToStory(direction) {
const state = store.getState();
const { storiesHash } = state;
const { componentRoot, component } = getUrlData({ history });
let selectedId = component;
const lookupList = Object.keys(storiesHash).filter(
k => !(storiesHash[k].children || Array.isArray(storiesHash[k]))
);
if (!selectedId || !storiesHash[selectedId]) {
selectedId = state.selectedId || Object.keys(storiesHash)[0];
}
const index = lookupList.indexOf(selectedId);
// cannot navigate beyond fist or last
if (index === lookupList.length - 1 && direction > 0) {
return;
}
if (index === 0 && direction < 0) {
return;
}
if (direction === 0) {
return;
}
const result = lookupList[index + direction];
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${result}`);
store.setState({ selectedId: result });
}
},
jumpToComponent(direction) {
const state = store.getState();
const { storiesHash } = state;
const { componentRoot, component } = getUrlData({ history });
let selectedId = component;
const lookupList = Object.keys(storiesHash).filter(
k =>
(!storiesHash[k].children || Array.isArray(storiesHash[k])) &&
(storiesHash[k].kind !== storiesHash[selectedId].kind || k === selectedId)
);
console.log(lookupList);
const dirs = Object.entries(storiesHash).reduce((acc, i) => {
const key = i[0];
const value = i[1];
if (value.isComponent) {
acc[key] = [...i[1].children];
}
return acc;
}, []);
console.log('dirs', dirs);
const idx = Object.values(dirs).findIndex(i => i.includes(selectedId));
console.log('idx', idx);
if (!selectedId || !storiesHash[selectedId]) {
selectedId = state.selectedId || Object.keys(storiesHash)[0];
}
// const index = dirs.indexOf(selectedId);
// cannot navigate beyond fist or last
if (idx === dirs.length - 1 && direction > 0) {
return;
}
if (idx === 0 && direction < 0) {
return;
}
if (direction === 0) {
return;
}
const result = dirs[idx + direction];
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${result}`);
store.setState({ selectedId: result });
}
},
navigate(path) {
history.navigate(`?path=${path}`);
},
getUrlState() {
const state = store.getState();
return {
selectedKind: state.selectedKind,
selectedStory: state.selectedStory,
selectedPanel: state.selectedPanel,
full: Number(Boolean(state.shortcutOptions.full)),
panel:
state.shortcutOptions.panel === 'right' || state.shortcutOptions.panel === 'bottom'
? state.shortcutOptions.panel
: false,
nav: Number(Boolean(state.shortcutOptions.nav)),
...state.customQueryParams,
};
},
setStories(storiesHash) {
const state = store.getState();
store.setState({ storiesHash });
const { componentRoot, component } = getUrlData({ history });
// when there's no selectedId or the selectedId item doesn't exist
// we try to resolve from state or pick the first leaf and navigate
if (!component || !storiesHash[component]) {
// find first leaf
const selectedId =
state.selectedId || Object.values(storiesHash).find(s => !s.children).path;
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${selectedId}`);
}
store.setState({ selectedId });
} else {
store.setState({ selectedId: component });
}
},
selectStory(kind, story, { location } = {}) {
let selectedId;
const state = store.getState();
if (location) {
const { storiesHash } = state;
const { componentRoot, component } = getUrlData({ history });
selectedId = component;
if (!selectedId || !storiesHash[selectedId]) {
selectedId = state.selectedId || Object.keys(storiesHash)[0];
}
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${component}`);
}
store.setState({ selectedId });
} else {
throw new Error('NOT ALLOWED, must use location!');
}
},
selectInCurrentKind(name) {
const { componentRoot, component } = getUrlData({ history });
const selectedId = component.replace(/([^-]+)$/, name);
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${selectedId}`);
}
store.setState({ selectedId });
},
};
}

75
lib/ui/src/core/ui.js Normal file
View File

@ -0,0 +1,75 @@
export default function({ store }) {
return {
toggleFullscreen(toggled) {
if (typeof toggled !== 'undefined') {
return store.setState(state => ({
ui: {
...state.ui,
isFullscreen: toggled,
},
}));
}
return store.setState(state => ({
ui: {
...state.ui,
isFullscreen: !state.ui.isFullscreen,
},
}));
},
togglePanel(toggled) {
if (typeof toggled !== 'undefined') {
return store.setState(state => ({
ui: {
...state.ui,
showPanel: toggled,
},
}));
}
return store.setState(state => ({
ui: {
...state.ui,
showPanel: !state.ui.showPanel,
},
}));
},
togglePanelPosition(position) {
if (typeof position !== 'undefined') {
return store.setState(state => ({
ui: {
...state.ui,
panelPosition: position,
},
}));
}
return store.setState(state => ({
ui: {
...state.ui,
panelPosition: state.ui.panelPosition === 'right' ? 'bottom' : 'right',
},
}));
},
toggleNav(toggled) {
if (typeof toggled !== 'undefined') {
return store.setState(state => ({
ui: {
...state.ui,
showNav: toggled,
},
}));
}
return store.setState(state => ({
ui: {
...state.ui,
showNav: !state.ui.showNav,
},
}));
},
};
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider as MobxProvider } from 'mobx-react';
import ReactModal from 'react-modal';
import { window } from 'global';
@ -12,10 +11,9 @@ import Provider from './provider';
import initProviderApi from './init-provider-api';
import handleHistoryLoad from './onload-history';
import initKeyHandler from './init-key-handler';
import createStore from './store';
import { createUiStore } from './stores';
// const history = createBrowserHistory();
import createManager from './core/manager';
import { Provider as ManagerProvider } from './core/context';
function renderStorybookUI(domNode, provider) {
if (!(provider instanceof Provider)) {
@ -25,23 +23,19 @@ function renderStorybookUI(domNode, provider) {
// init history
const history = createHistory(window);
// init stores
const stores = {
store: createStore({ provider, history }),
uiStore: createUiStore(),
};
const manager = createManager({ history, provider });
// Init event handle to stores
const eventHandler = initKeyHandler(stores);
const eventHandler = initKeyHandler({ manager });
// listen to events
eventHandler.bind();
/** Init external interaction with the state */
initProviderApi({ provider, stores, eventHandler });
initProviderApi({ provider, manager, eventHandler });
/** parse old url params to set state and redirect to new routing scheme */
handleHistoryLoad(history, stores);
handleHistoryLoad(history, manager);
// const Preview = () => provider.renderPreview(store.selectedKind, store.selectedStory);
@ -51,13 +45,13 @@ function renderStorybookUI(domNode, provider) {
const Container = process.env.STORYBOOK_EXAMPLE_APP ? React.StrictMode : 'div';
const root = (
<Container>
<MobxProvider {...stores}>
<ManagerProvider manager={manager}>
<ThemeProvider>
<LocationProvider history={history}>
<App />
</LocationProvider>
</ThemeProvider>
</MobxProvider>
</ManagerProvider>
</Container>
);

View File

@ -1,25 +1,29 @@
import { document } from 'global';
import keyEvents, { features } from './libs/key_events';
export default ({ store, uiStore }) => {
export default ({ manager }) => {
const handle = eventType => {
const {
ui: { isFullscreen, showNav, showPanel },
} = manager.store.getState();
switch (eventType) {
case features.ESCAPE: {
if (uiStore.isFullscreen) {
uiStore.toggleFullscreen();
} else if (!uiStore.showNav) {
uiStore.toggleNav();
if (isFullscreen) {
manager.toggleFullscreen();
} else if (!showNav) {
manager.toggleNav();
}
document.activeElement.blur();
break;
}
case features.FOCUS_NAV: {
if (uiStore.isFullscreen) {
uiStore.toggleFullscreen();
if (isFullscreen) {
manager.toggleFullscreen();
}
if (!uiStore.showNav) {
uiStore.toggleNav();
if (!showNav) {
manager.toggleNav();
}
const element = document.getElementById('storybook-explorer-menu');
@ -30,11 +34,11 @@ export default ({ store, uiStore }) => {
}
case features.FOCUS_SEARCH: {
if (uiStore.isFullscreen) {
uiStore.toggleFullscreen();
if (isFullscreen) {
manager.toggleFullscreen();
}
if (!uiStore.showNav) {
uiStore.toggleNav();
if (!showNav) {
manager.toggleNav();
}
const element = document.getElementById('storybook-explorer-searchfield');
@ -59,11 +63,11 @@ export default ({ store, uiStore }) => {
}
case features.FOCUS_PANEL: {
if (uiStore.isFullscreen) {
uiStore.toggleFullscreen();
if (isFullscreen) {
manager.toggleFullscreen();
}
if (!uiStore.showPanel) {
uiStore.togglePanel();
if (!showPanel) {
manager.togglePanel();
}
const element = document.getElementById('storybook-panel-root');
@ -74,67 +78,67 @@ export default ({ store, uiStore }) => {
}
case features.NEXT_STORY: {
store.jumpToStory(1);
manager.jumpToStory(1);
break;
}
case features.PREV_STORY: {
store.jumpToStory(-1);
manager.jumpToStory(-1);
break;
}
case features.NEXT_COMPONENT: {
store.jumpToComponent(1);
manager.jumpToComponent(1);
break;
}
case features.PREV_COMPONENT: {
store.jumpToComponent(-1);
manager.jumpToComponent(-1);
break;
}
case features.FULLSCREEN: {
uiStore.toggleFullscreen();
manager.toggleFullscreen();
break;
}
case features.PANEL_VISIBILITY: {
if (uiStore.isFullscreen) {
uiStore.toggleFullscreen();
if (isFullscreen) {
manager.toggleFullscreen();
}
uiStore.togglePanel();
manager.togglePanel();
break;
}
case features.NAV_VISIBILITY: {
if (uiStore.isFullscreen) {
uiStore.toggleFullscreen();
if (isFullscreen) {
manager.toggleFullscreen();
}
uiStore.toggleNav();
manager.toggleNav();
break;
}
case features.PANEL_POSITION: {
if (uiStore.isFullscreen) {
uiStore.toggleFullscreen();
if (isFullscreen) {
manager.toggleFullscreen();
}
if (!uiStore.showPanel) {
uiStore.togglePanel();
if (!showPanel) {
manager.togglePanel();
}
uiStore.togglePanelPosition();
manager.togglePanelPosition();
break;
}
case features.ABOUT: {
store.navigate('/settings/about');
manager.navigate('/settings/about');
break;
}
case features.SHORTCUTS: {
store.navigate('/settings/shortcuts');
manager.navigate('/settings/shortcuts');
break;
}

View File

@ -1,12 +1,10 @@
import { reaction } from 'mobx';
import { EventEmitter } from 'events';
import qs from 'qs';
import { STORY_RENDERED } from '@storybook/core-events';
export default ({ provider, stores, eventHandler }) => {
export default ({ provider, manager, eventHandler }) => {
const onStoryListeners = new EventEmitter();
const { store } = stores;
const api = {
on(type, cb, peer = true) {
@ -29,32 +27,33 @@ export default ({ provider, stores, eventHandler }) => {
onStory(cb) {
return this.on(STORY_RENDERED, cb);
},
setStories: store.setStories.bind(store),
selectInCurrentKind: store.selectInCurrentKind.bind(store),
selectStory: store.selectStory.bind(store),
handleShortcut: eventHandler.handle.bind(store),
setStories: manager.setStories,
selectInCurrentKind: manager.selectInCurrentKind,
selectStory: manager.selectStory,
handleShortcut: eventHandler.handle,
setOptions(options) {
store.setOptions(options);
store.setShortcutsOptions(options);
manager.setOptions(options);
manager.setShortcutsOptions(options);
},
setQueryParams: store.setQueryParams.bind(store),
setQueryParams: manager.setQueryParams,
getQueryParam(key) {
if (store.customQueryParams) {
return store.customQueryParams[key];
const { customQueryParams } = manager.store.getState();
if (customQueryParams) {
return customQueryParams[key];
}
return undefined;
},
getUrlState(overrideParams) {
const url = qs.stringify(
{
...store.urlState,
...manager.getUrlState(),
...overrideParams,
},
{ encode: false, addQueryPrefix: true }
);
return {
...store.urlState,
...manager.getUrlState(),
url,
};
},
@ -62,14 +61,18 @@ export default ({ provider, stores, eventHandler }) => {
provider.handleAPI(api);
reaction(
() => ({
selectedKind: store.selectedKind,
selectedStory: store.selectedStory,
}),
() => {
if (!store.selectedKind) return;
onStoryListeners.emit('story', store.selectedKind, store.selectedStory);
let prevStory;
let prevKind;
manager.store.subscribe(() => {
const { selectedKind, selectedStory } = manager.store.getState();
if (!selectedKind) return;
if (selectedKind !== prevKind || selectedStory !== prevStory) {
onStoryListeners.emit('story', selectedKind, selectedStory);
}
);
prevStory = selectedStory;
prevKind = selectedKind;
});
};

View File

@ -1,26 +1,26 @@
import { parseQuery, stringifyQuery } from './router';
export default function handleHistoryLoad({ location, navigate }, { store, uiStore }) {
export default function handleHistoryLoad({ location, navigate }, manager) {
const query = parseQuery(location);
if (query.full === '1') {
uiStore.toggleFullscreen(true);
manager.toggleFullscreen(true);
}
if (query.panel) {
if (['right', 'bottom'].includes(query.panel)) {
uiStore.togglePanelPosition(query.panel);
manager.togglePanelPosition(query.panel);
} else if (query.panel === '0') {
uiStore.togglePanel(false);
manager.togglePanel(false);
}
}
if (query.nav === '0') {
uiStore.toggleNav(false);
manager.toggleNav(false);
}
if (query.selectedKind && query.selectedStory) {
store.selectStory(query.selectedKind, query.selectedStory);
manager.selectStory(query.selectedKind, query.selectedStory);
}
if (!query.path || query.path === '/') {

View File

@ -1,287 +0,0 @@
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) {
const keys = Object.keys(panels);
if (keys.indexOf(selectedPanel) >= 0) {
return selectedPanel;
}
if (keys.length) {
return keys[0];
}
return currentPanel;
}
const createStore = ({ provider, history }) => {
const store = observable(
{
stories: [],
storiesHash: {},
selectedId: null,
shortcutOptions: {
full: false,
nav: true,
panel: 'right',
enableShortcuts: true,
},
uiOptions: {
name: 'STORYBOOK',
url: 'https://github.com/storybooks/storybook',
sortStoriesByKind: false,
sidebarAnimations: true,
theme: themes.normal,
},
customQueryParams: {},
notifications: [
{
id: 'update',
level: 2,
link: '/settings/about',
icon: '🎉',
content: `There's a new version available: 4.0.0`,
},
],
selectedPanelValue: null,
get selectedPanel() {
return ensurePanel(this.panels, this.selectedPanelValue, this.selectedPanelValue);
},
set selectedPanel(value) {
this.selectedPanelValue = value;
},
get channel() {
return provider.channel;
},
get panels() {
return this.getElements(types.PANEL);
},
getElements(type) {
return provider.getElements(type);
},
setOptions(changes) {
const { selectedPanel } = changes;
const oldOptions = this.uiOptions;
const options = pick(changes, Object.keys(oldOptions));
set(oldOptions, options);
if (selectedPanel && selectedPanel !== this.selectedPanel) {
this.selectedPanel = ensurePanel(this.panels, selectedPanel, this.selectedPanel);
}
},
setShortcutsOptions(options) {
set(this.shortcutOptions, pick(options, Object.keys(this.shortcutOptions)));
},
jumpToStory(direction) {
const { storiesHash } = this;
const { componentRoot, component } = this.urlData;
let selectedId = component;
const lookupList = Object.keys(storiesHash).filter(
k => !(storiesHash[k].children || Array.isArray(storiesHash[k]))
);
if (!selectedId || !storiesHash[selectedId]) {
selectedId = this.selectedId || Object.keys(storiesHash)[0];
}
const index = lookupList.indexOf(selectedId);
// cannot navigate beyond fist or last
if (index === lookupList.length - 1 && direction > 0) {
return;
}
if (index === 0 && direction < 0) {
return;
}
if (direction === 0) {
return;
}
const result = lookupList[index + direction];
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${result}`);
this.selectedId = result;
}
},
jumpToComponent(direction) {
const { storiesHash } = this;
const { componentRoot, component } = this.urlData;
let selectedId = component;
const lookupList = Object.keys(storiesHash).filter(
k =>
(!storiesHash[k].children || Array.isArray(storiesHash[k])) &&
(storiesHash[k].kind !== storiesHash[selectedId].kind || k === selectedId)
);
console.log(lookupList);
const dirs = Object.entries(storiesHash).reduce((acc, i) => {
const key = i[0];
const value = i[1];
if (value.isComponent) {
acc[key] = [...i[1].children];
}
return acc;
}, []);
console.log('dirs', dirs);
const idx = Object.values(dirs).findIndex(i => i.includes(selectedId));
console.log('idx', idx);
if (!selectedId || !storiesHash[selectedId]) {
selectedId = this.selectedId || Object.keys(storiesHash)[0];
}
// const index = dirs.indexOf(selectedId);
// cannot navigate beyond fist or last
if (idx === dirs.length - 1 && direction > 0) {
return;
}
if (idx === 0 && direction < 0) {
return;
}
if (direction === 0) {
return;
}
const result = dirs[idx + direction];
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${result}`);
this.selectedId = result;
}
},
navigate(path) {
history.navigate(`?path=${path}`);
},
get urlState() {
return {
selectedKind: this.selectedKind,
selectedStory: this.selectedStory,
selectedPanel: this.selectedPanel,
full: Number(Boolean(this.shortcutOptions.full)),
panel:
this.shortcutOptions.panel === 'right' || this.shortcutOptions.panel === 'bottom'
? this.shortcutOptions.panel
: false,
nav: Number(Boolean(this.shortcutOptions.nav)),
...this.customQueryParams,
};
},
get urlData() {
const { search } = history.location;
const { path = '' } = qs.parse(search, { ignoreQueryPrefix: true });
const [, p1, p2] = path.match(/\/([^/]+)\/([^/]+)?/) || [];
const result = {};
if (p1 && p1.match(/(components|info)/)) {
Object.assign(result, {
componentRoot: p1,
component: p2,
});
}
return result;
},
selectPanel(panelName) {
this.selectedPanel = panelName;
},
setStories(storiesHash) {
this.storiesHash = storiesHash;
const { componentRoot, component } = this.urlData;
// when there's no selectedId or the selectedId item doesn't exist
// we try to resolve from state or pick the first leaf and navigate
if (!component || !storiesHash[component]) {
// find first leaf
const selectedId =
this.selectedId || Object.values(storiesHash).find(s => !s.children).path;
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${selectedId}`);
}
this.selectedId = selectedId;
} else {
this.selectedId = component;
}
},
selectStory(kind, story, { location } = {}) {
let selectedId;
if (location) {
const { storiesHash } = this;
const { componentRoot, component } = this.urlData;
selectedId = component;
if (!selectedId || !storiesHash[selectedId]) {
selectedId = this.selectedId || Object.keys(storiesHash)[0];
}
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${component}`);
}
this.selectedId = selectedId;
} else {
throw new Error('NOT ALLOWED, must use location!');
}
},
selectInCurrentKind(name) {
const { componentRoot, component } = this.urlData;
const selectedId = component.replace(/([^-]+)$/, name);
if (componentRoot) {
history.navigate(`?path=/${componentRoot}/${selectedId}`);
}
this.selectedId = selectedId;
},
setQueryParams(customQueryParams) {
set(
this.customQueryParams,
Object.keys(customQueryParams).reduce((acc, key) => {
if (customQueryParams[key] !== null) acc[key] = customQueryParams[key];
return acc;
}, {})
);
},
},
{
setOptions: action,
navigate: action,
setShortcutsOptions: action,
jumpToStory: action,
selectPanel: action,
setStories: action,
selectStory: action,
selectInCurrentKind: action,
setQueryParams: action,
}
);
return store;
};
export default createStore;

View File

@ -1 +0,0 @@
export { default as createUiStore } from './ui';

View File

@ -1,41 +0,0 @@
import { observable } from 'mobx';
import { bindActions } from './utils';
const initialState = {
isFullscreen: false,
showPanel: true,
showNav: true,
panelPosition: 'bottom',
};
const actions = {
toggleFullscreen(toggled) {
if (typeof toggled !== 'undefined') this.isFullscreen = toggled;
else this.isFullscreen = !this.isFullscreen;
},
togglePanel(toggled) {
if (typeof toggled !== 'undefined') this.showPanel = toggled;
else this.showPanel = !this.showPanel;
},
togglePanelPosition(position) {
if (typeof position !== 'undefined') this.panelPosition = position;
else this.panelPosition = this.panelPosition === 'bottom' ? 'right' : 'bottom';
},
toggleNav(toggled) {
if (typeof toggled !== 'undefined') this.showNav = toggled;
else this.showNav = !this.showNav;
},
};
const createUiStore = () => {
const store = observable(initialState);
bindActions(store, actions);
return store;
};
export default createUiStore;

View File

@ -1,10 +0,0 @@
import { action, extendObservable } from 'mobx';
export const bindActions = (store, actions) => {
extendObservable(store, actions, {
...Object.keys(actions).reduce((acc, key) => {
acc[key] = action;
return acc;
}, {}),
});
};

3104
yarn.lock

File diff suppressed because it is too large Load Diff