mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 23:02:00 +08:00
Merge branch 'tech/overhaul-ui' of https://github.com/storybooks/storybook into tech/overhaul-ui
This commit is contained in:
commit
31cf0df734
@ -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);
|
||||
}
|
||||
|
@ -15,11 +15,4 @@ addons.register(ADDON_ID, api => {
|
||||
route: '/info/',
|
||||
render,
|
||||
});
|
||||
|
||||
addons.add(PANEL_ID, {
|
||||
type: types.PANEL,
|
||||
title,
|
||||
route: '/info/',
|
||||
render,
|
||||
});
|
||||
});
|
||||
|
@ -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" />,
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
38
lib/ui/src/core/context.js
Normal file
38
lib/ui/src/core/context.js
Normal 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;
|
39
lib/ui/src/core/initial-state.js
Normal file
39
lib/ui/src/core/initial-state.js
Normal 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,
|
||||
});
|
35
lib/ui/src/core/manager.js
Normal file
35
lib/ui/src/core/manager.js
Normal 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;
|
48
lib/ui/src/core/options.js
Normal file
48
lib/ui/src/core/options.js
Normal 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
43
lib/ui/src/core/panels.js
Normal 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
33
lib/ui/src/core/store.js
Normal 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
182
lib/ui/src/core/stories.js
Normal 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
75
lib/ui/src/core/ui.js
Normal 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,
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
@ -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 === '/') {
|
||||
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default as createUiStore } from './ui';
|
@ -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;
|
@ -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;
|
||||
}, {}),
|
||||
});
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user