mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 02:01:48 +08:00
REFACTOR preview
This commit is contained in:
parent
5bfcbf2a0a
commit
867a662879
@ -16,9 +16,10 @@ import Panel from '../panel/panel';
|
||||
import { Preview } from '../preview/preview';
|
||||
|
||||
import { panels } from '../panel/panel.stories';
|
||||
import { previewProps } from '../preview/preview.stories';
|
||||
|
||||
import { previewProps } from '../preview/preview.mockdata';
|
||||
import { mockDataset } from '../sidebar/treeview/treeview.mockdata';
|
||||
|
||||
import { store } from './persist';
|
||||
|
||||
const realNavProps = {
|
||||
|
@ -7,7 +7,6 @@ export interface PreviewProps {
|
||||
storyId: string;
|
||||
viewMode: ViewMode;
|
||||
docsOnly: boolean;
|
||||
isLoading: boolean;
|
||||
options: {
|
||||
isFullscreen: boolean;
|
||||
isToolshown: boolean;
|
||||
|
@ -19,3 +19,10 @@ export const UnstyledLink = styled(Link)({
|
||||
textDecoration: 'inherit',
|
||||
display: 'inline-block',
|
||||
});
|
||||
|
||||
export const DesktopOnly = styled.span({
|
||||
// Hides full screen icon at mobile breakpoint defined in app.js
|
||||
'@media (max-width: 599px)': {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
@ -1,141 +0,0 @@
|
||||
import window from 'global';
|
||||
import React, { Fragment } from 'react';
|
||||
import memoize from 'memoizerific';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { State, API } from '@storybook/api';
|
||||
import { types, Addon } from '@storybook/addons';
|
||||
import { Icons, IconButton, TabButton, TabBar, Separator } from '@storybook/components';
|
||||
import { ZoomConsumer, Zoom } from './zoom';
|
||||
import { getElementList, DesktopOnly, stringifyQueryParams } from './preview';
|
||||
|
||||
import * as S from './components';
|
||||
|
||||
export const getTools = memoize(10)(
|
||||
(
|
||||
getElements: API['getElements'],
|
||||
queryParams: State['customQueryParams'],
|
||||
panels: Partial<Addon>[],
|
||||
api: API,
|
||||
options,
|
||||
storyId: string,
|
||||
viewMode: State['viewMode'],
|
||||
docsOnly: boolean,
|
||||
location: State['location'],
|
||||
path: string,
|
||||
baseUrl: string
|
||||
) => {
|
||||
const tools = getElementList(getElements, types.TOOL, [
|
||||
panels.filter(p => !p.hidden).length > 1
|
||||
? ({
|
||||
render: () => (
|
||||
<Fragment>
|
||||
<TabBar key="tabs">
|
||||
{panels
|
||||
.filter(p => !p.hidden)
|
||||
.map((t, index) => {
|
||||
const to = t.route({ storyId, viewMode, path, location });
|
||||
const isActive = path === to;
|
||||
return (
|
||||
<S.UnstyledLink key={t.id || `l${index}`} to={to}>
|
||||
<TabButton disabled={t.disabled} active={isActive}>
|
||||
{t.title}
|
||||
</TabButton>
|
||||
</S.UnstyledLink>
|
||||
);
|
||||
})}
|
||||
</TabBar>
|
||||
<Separator />
|
||||
</Fragment>
|
||||
),
|
||||
} as Partial<Addon>)
|
||||
: null,
|
||||
{
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<Fragment>
|
||||
<ZoomConsumer>
|
||||
{({ set, value }) => (
|
||||
<Zoom key="zoom" set={(v: number) => set(value * v)} reset={() => set(1)} />
|
||||
)}
|
||||
</ZoomConsumer>
|
||||
<Separator />
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
]);
|
||||
const extraTools = getElementList(getElements, types.TOOLEXTRA, [
|
||||
{
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<DesktopOnly>
|
||||
<IconButton
|
||||
key="full"
|
||||
onClick={api.toggleFullscreen as any}
|
||||
title={options.isFullscreen ? 'Exit full screen' : 'Go full screen'}
|
||||
>
|
||||
<Icons icon={options.isFullscreen ? 'close' : 'expand'} />
|
||||
</IconButton>
|
||||
</DesktopOnly>
|
||||
),
|
||||
},
|
||||
{
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<IconButton
|
||||
key="opener"
|
||||
href={`${baseUrl}?id=${storyId}${stringifyQueryParams(queryParams)}`}
|
||||
target="_blank"
|
||||
title="Open canvas in new tab"
|
||||
>
|
||||
<Icons icon="share" />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<IconButton
|
||||
key="copy"
|
||||
onClick={() =>
|
||||
copy(
|
||||
`${window.location.origin}${
|
||||
window.location.pathname
|
||||
}${baseUrl}?id=${storyId}${stringifyQueryParams(queryParams)}`
|
||||
)
|
||||
}
|
||||
title="Copy canvas link"
|
||||
>
|
||||
<Icons icon="copy" />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
]);
|
||||
// if its a docsOnly page, even the 'story' view mode is considered 'docs'
|
||||
const filter = (item: Partial<Addon>) =>
|
||||
item &&
|
||||
(!item.match ||
|
||||
item.match({
|
||||
storyId,
|
||||
viewMode: docsOnly && viewMode === 'story' ? 'docs' : viewMode,
|
||||
location,
|
||||
path,
|
||||
}));
|
||||
const displayItems = (list: Partial<Addon>[]) =>
|
||||
list.reduce(
|
||||
(acc, item, index) =>
|
||||
item ? (
|
||||
// @ts-ignore
|
||||
<Fragment key={item.id || item.key || `f-${index}`}>
|
||||
{acc}
|
||||
{item.render({}) || item}
|
||||
</Fragment>
|
||||
) : (
|
||||
acc
|
||||
),
|
||||
null
|
||||
);
|
||||
const left = displayItems(tools.filter(filter));
|
||||
const right = displayItems(extraTools.filter(filter));
|
||||
return { left, right };
|
||||
}
|
||||
);
|
35
lib/ui/src/components/preview/preview.mockdata.js
Normal file
35
lib/ui/src/components/preview/preview.mockdata.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { types } from '@storybook/addons';
|
||||
|
||||
export const previewProps = {
|
||||
id: 'string',
|
||||
api: {
|
||||
on: () => {},
|
||||
emit: () => {},
|
||||
off: () => {},
|
||||
},
|
||||
storyId: 'string',
|
||||
path: 'string',
|
||||
viewMode: 'story',
|
||||
location: {},
|
||||
baseUrl: 'http://example.com',
|
||||
queryParams: {},
|
||||
getElements: type =>
|
||||
type === types.TAB
|
||||
? [
|
||||
{
|
||||
id: 'notes',
|
||||
type: types.TAB,
|
||||
title: 'Notes',
|
||||
route: ({ storyId }) => `/info/${storyId}`,
|
||||
match: ({ viewMode }) => viewMode === 'info',
|
||||
render: () => null,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
options: {
|
||||
isFullscreen: false,
|
||||
isToolshown: true,
|
||||
},
|
||||
actions: {},
|
||||
withLoader: false,
|
||||
};
|
@ -1,46 +1,13 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { types } from '@storybook/addons';
|
||||
|
||||
import { Preview } from './preview';
|
||||
import { previewProps } from './preview.mockdata';
|
||||
|
||||
export const previewProps = {
|
||||
id: 'string',
|
||||
api: {
|
||||
on: () => {},
|
||||
emit: () => {},
|
||||
off: () => {},
|
||||
},
|
||||
storyId: 'string',
|
||||
path: 'string',
|
||||
viewMode: 'story',
|
||||
location: {},
|
||||
baseUrl: 'http://example.com',
|
||||
queryParams: {},
|
||||
getElements: type =>
|
||||
type === types.TAB
|
||||
? [
|
||||
{
|
||||
id: 'notes',
|
||||
type: types.TAB,
|
||||
title: 'Notes',
|
||||
route: ({ storyId }) => `/info/${storyId}`, // todo add type
|
||||
match: ({ viewMode }) => viewMode === 'info', // todo add type
|
||||
render: () => null,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
options: {
|
||||
isFullscreen: false,
|
||||
isToolshown: true,
|
||||
},
|
||||
actions: {},
|
||||
withLoader: false,
|
||||
export default {
|
||||
title: 'UI/Preview/Preview',
|
||||
component: Preview,
|
||||
};
|
||||
|
||||
storiesOf('UI/Preview/Preview', module)
|
||||
.addParameters({
|
||||
component: Preview,
|
||||
})
|
||||
.add('no tabs', () => <Preview {...previewProps} getElements={() => []} />)
|
||||
.add('with tabs', () => <Preview {...previewProps} />);
|
||||
export const noTabs = () => <Preview {...previewProps} getElements={() => []} />;
|
||||
|
||||
export const withTabs = () => <Preview {...previewProps} />;
|
||||
|
@ -1,15 +1,14 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import memoize from 'memoizerific';
|
||||
import { styled } from '@storybook/theming';
|
||||
import { API } from '@storybook/api';
|
||||
import { API, Consumer, Combo, State } from '@storybook/api';
|
||||
import { SET_CURRENT_STORY } from '@storybook/core-events';
|
||||
import addons, { types, Types, Addon } from '@storybook/addons';
|
||||
import addons, { types, Addon } from '@storybook/addons';
|
||||
import merge from '@storybook/api/dist/lib/merge';
|
||||
import { Loader } from '@storybook/components';
|
||||
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
|
||||
import { Toolbar } from './toolbar';
|
||||
import { Toolbar, defaultTools, defaultToolsExtra, createTabsTool } from './toolbar';
|
||||
|
||||
import * as S from './components';
|
||||
|
||||
@ -17,20 +16,9 @@ import { ZoomProvider, ZoomConsumer } from './zoom';
|
||||
|
||||
import { IFrame } from './iframe';
|
||||
import { PreviewProps, ApplyWrappersProps, IframeRenderer } from './PreviewProps';
|
||||
import { getTools } from './getTools';
|
||||
import { defaultWrappers, ApplyWrappers } from './ApplyWrappers';
|
||||
|
||||
export const DesktopOnly = styled.span({
|
||||
// Hides full screen icon at mobile breakpoint defined in app.js
|
||||
'@media (max-width: 599px)': {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
export const stringifyQueryParams = (queryParams: Record<string, string>) =>
|
||||
Object.entries(queryParams).reduce((acc, [k, v]) => {
|
||||
return `${acc}&${k}=${v}`;
|
||||
}, '');
|
||||
import { defaultWrappers, ApplyWrappers } from './wrappers';
|
||||
import { stringifyQueryParams } from './stringifyQueryParams';
|
||||
|
||||
export const renderIframe: IframeRenderer = (
|
||||
storyId,
|
||||
@ -50,16 +38,79 @@ export const renderIframe: IframeRenderer = (
|
||||
/>
|
||||
);
|
||||
|
||||
export const getElementList = memoize(
|
||||
10
|
||||
)((getFn: API['getElements'], type: Types, base: Partial<Addon>[]) =>
|
||||
base.concat(Object.values(getFn(type)))
|
||||
const getWrapper = memoize(1)((getFn: API['getElements']) =>
|
||||
Object.values(getFn<Addon>(types.PREVIEW))
|
||||
);
|
||||
const getTabs = memoize(1)((getFn: API['getElements']) => Object.values(getFn<Addon>(types.TAB)));
|
||||
const getTools = memoize(1)((getFn: API['getElements']) => Object.values(getFn<Addon>(types.TOOL)));
|
||||
const getToolsExtra = memoize(1)((getFn: API['getElements']) =>
|
||||
Object.values(getFn<Addon>(types.TOOLEXTRA))
|
||||
);
|
||||
|
||||
const getDocumentTitle = (description: string) => {
|
||||
return description ? `${description} ⋅ Storybook` : 'Storybook';
|
||||
};
|
||||
|
||||
const mapper = ({ state, api }: Combo) => ({
|
||||
storyId: state.storyId,
|
||||
viewMode: state.viewMode,
|
||||
customCanvas: api.renderPreview,
|
||||
queryParams: state.customQueryParams,
|
||||
getElements: api.getElements,
|
||||
isLoading: !state.storiesConfigured,
|
||||
});
|
||||
|
||||
const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): Addon => ({
|
||||
id: 'canvas',
|
||||
title: 'Canvas',
|
||||
route: p => `/story/${p.storyId}`,
|
||||
match: p => !!(p.viewMode && p.viewMode.match(/^(story|docs)$/)),
|
||||
render: p => {
|
||||
return (
|
||||
<Consumer filter={mapper}>
|
||||
{({
|
||||
customCanvas,
|
||||
storyId,
|
||||
viewMode,
|
||||
queryParams,
|
||||
getElements,
|
||||
isLoading,
|
||||
}: ReturnType<typeof mapper>) => (
|
||||
<ZoomConsumer>
|
||||
{({ value: scale }) => {
|
||||
const wrappers = [...defaultWrappers, ...getWrapper(getElements)];
|
||||
|
||||
const data = [storyId, viewMode, id, baseUrl, scale, queryParams] as Parameters<
|
||||
IframeRenderer
|
||||
>;
|
||||
|
||||
const content = customCanvas ? customCanvas(...data) : renderIframe(...data);
|
||||
const props = {
|
||||
viewMode,
|
||||
active: p.active,
|
||||
wrappers,
|
||||
id,
|
||||
storyId,
|
||||
baseUrl,
|
||||
queryParams,
|
||||
scale,
|
||||
customCanvas,
|
||||
} as ApplyWrappersProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
{withLoader && isLoading && <Loader id="preview-loader" role="progressbar" />}
|
||||
<ApplyWrappers {...props}>{content}</ApplyWrappers>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ZoomConsumer>
|
||||
)}
|
||||
</Consumer>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
class Preview extends Component<PreviewProps> {
|
||||
shouldComponentUpdate({
|
||||
storyId,
|
||||
@ -91,113 +142,35 @@ class Preview extends Component<PreviewProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const {
|
||||
id,
|
||||
location,
|
||||
queryParams,
|
||||
getElements,
|
||||
api,
|
||||
options,
|
||||
viewMode = undefined,
|
||||
docsOnly = false,
|
||||
storyId = undefined,
|
||||
path = undefined,
|
||||
description = undefined,
|
||||
baseUrl = 'iframe.html',
|
||||
customCanvas = undefined,
|
||||
parameters = undefined,
|
||||
isLoading = true,
|
||||
withLoader = true,
|
||||
} = this.props;
|
||||
const toolbarHeight = options.isToolshown ? 40 : 0;
|
||||
const wrappers = getElementList(getElements, types.PREVIEW, defaultWrappers);
|
||||
let panels = getElementList(getElements, types.TAB, [
|
||||
{
|
||||
route: p => `/story/${p.storyId}`,
|
||||
match: p => !!(p.viewMode && p.viewMode.match(/^(story|docs)$/)),
|
||||
render: p => (
|
||||
<ZoomConsumer>
|
||||
{({ value: scale }) => {
|
||||
const props = {
|
||||
viewMode,
|
||||
active: p.active,
|
||||
wrappers,
|
||||
id,
|
||||
storyId,
|
||||
baseUrl,
|
||||
queryParams,
|
||||
scale,
|
||||
customCanvas,
|
||||
} as ApplyWrappersProps;
|
||||
} = props;
|
||||
const viewMode = docsOnly && props.viewMode === 'story' ? 'docs' : props.viewMode;
|
||||
const { isToolshown } = options;
|
||||
|
||||
const data = [storyId, viewMode, id, baseUrl, scale, queryParams] as Parameters<
|
||||
IframeRenderer
|
||||
>;
|
||||
const allTabs = [createCanvas(id, baseUrl, withLoader), ...getTabs(getElements)];
|
||||
const tabs = filterTabs(allTabs, parameters);
|
||||
|
||||
const content = customCanvas ? customCanvas(...data) : renderIframe(...data);
|
||||
const tools = [...defaultTools, ...getTools(getElements)];
|
||||
const toolsExtra = [...defaultToolsExtra, ...getToolsExtra(getElements)];
|
||||
|
||||
return (
|
||||
<>
|
||||
{withLoader && isLoading && <Loader id="preview-loader" role="progressbar" />}
|
||||
<ApplyWrappers {...props}>{content}</ApplyWrappers>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ZoomConsumer>
|
||||
),
|
||||
title: 'Canvas',
|
||||
id: 'canvas',
|
||||
},
|
||||
]);
|
||||
const { previewTabs } = addons.getConfig();
|
||||
const parametersTabs = parameters ? parameters.previewTabs : undefined;
|
||||
if (previewTabs || parametersTabs) {
|
||||
// deep merge global and local settings
|
||||
const tabs = merge(previewTabs, parametersTabs);
|
||||
const arrTabs = Object.keys(tabs).map((key, index) => ({
|
||||
index,
|
||||
...(typeof tabs[key] === 'string' ? { title: tabs[key] } : tabs[key]),
|
||||
id: key,
|
||||
}));
|
||||
panels = panels
|
||||
.filter(panel => {
|
||||
const t = arrTabs.find(tab => tab.id === panel.id);
|
||||
return t === undefined || t.id === 'canvas' || !t.hidden;
|
||||
})
|
||||
.map((panel, index) => ({ ...panel, index }))
|
||||
.sort((p1, p2) => {
|
||||
const tab_1 = arrTabs.find(tab => tab.id === p1.id);
|
||||
const index_1 = tab_1 ? tab_1.index : arrTabs.length + p1.index;
|
||||
const tab_2 = arrTabs.find(tab => tab.id === p2.id);
|
||||
const index_2 = tab_2 ? tab_2.index : arrTabs.length + p2.index;
|
||||
return index_1 - index_2;
|
||||
})
|
||||
.map(panel => {
|
||||
const t = arrTabs.find(tab => tab.id === panel.id);
|
||||
if (t) {
|
||||
return {
|
||||
...panel,
|
||||
title: t.title || panel.title,
|
||||
disabled: t.disabled,
|
||||
hidden: t.hidden,
|
||||
};
|
||||
}
|
||||
return panel;
|
||||
});
|
||||
}
|
||||
const { left, right } = getTools(
|
||||
getElements,
|
||||
queryParams,
|
||||
panels,
|
||||
api,
|
||||
options,
|
||||
storyId,
|
||||
const { left, right } = filterTools(tools, toolsExtra, tabs, {
|
||||
viewMode,
|
||||
docsOnly,
|
||||
storyId,
|
||||
location,
|
||||
path,
|
||||
baseUrl
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<ZoomProvider>
|
||||
@ -208,15 +181,15 @@ class Preview extends Component<PreviewProps> {
|
||||
</Helmet>
|
||||
)}
|
||||
{(left || right) && (
|
||||
<Toolbar key="toolbar" shown={options.isToolshown} border>
|
||||
<Toolbar key="toolbar" shown={isToolshown} border>
|
||||
<Fragment key="left">{left}</Fragment>
|
||||
<Fragment key="right">{right}</Fragment>
|
||||
</Toolbar>
|
||||
)}
|
||||
<S.FrameWrap key="frame" offset={toolbarHeight}>
|
||||
{panels.map(p => (
|
||||
<S.FrameWrap key="frame" offset={isToolshown ? 40 : 0}>
|
||||
{tabs.map((p, i) => (
|
||||
// @ts-ignore
|
||||
<Fragment key={p.id || p.key}>
|
||||
<Fragment key={p.id || p.key || i}>
|
||||
{p.render({ active: p.match({ storyId, viewMode, location, path }) })}
|
||||
</Fragment>
|
||||
))}
|
||||
@ -228,3 +201,99 @@ class Preview extends Component<PreviewProps> {
|
||||
}
|
||||
|
||||
export { Preview };
|
||||
|
||||
function filterTabs(panels: Addon[], parameters: Record<string, any>) {
|
||||
const { previewTabs } = addons.getConfig();
|
||||
const parametersTabs = parameters ? parameters.previewTabs : undefined;
|
||||
|
||||
if (previewTabs || parametersTabs) {
|
||||
// deep merge global and local settings
|
||||
const tabs = merge(previewTabs, parametersTabs);
|
||||
const arrTabs = Object.keys(tabs).map((key, index) => ({
|
||||
index,
|
||||
...(typeof tabs[key] === 'string' ? { title: tabs[key] } : tabs[key]),
|
||||
id: key,
|
||||
}));
|
||||
return panels
|
||||
.filter(panel => {
|
||||
const t = arrTabs.find(tab => tab.id === panel.id);
|
||||
return t === undefined || t.id === 'canvas' || !t.hidden;
|
||||
})
|
||||
.map((panel, index) => ({ ...panel, index } as Addon))
|
||||
.sort((p1, p2) => {
|
||||
const tab_1 = arrTabs.find(tab => tab.id === p1.id);
|
||||
// @ts-ignore
|
||||
const index_1 = tab_1 ? tab_1.index : arrTabs.length + p1.index;
|
||||
const tab_2 = arrTabs.find(tab => tab.id === p2.id);
|
||||
// @ts-ignore
|
||||
const index_2 = tab_2 ? tab_2.index : arrTabs.length + p2.index;
|
||||
return index_1 - index_2;
|
||||
})
|
||||
.map(panel => {
|
||||
const t = arrTabs.find(tab => tab.id === panel.id);
|
||||
if (t) {
|
||||
return {
|
||||
...panel,
|
||||
title: t.title || panel.title,
|
||||
disabled: t.disabled,
|
||||
hidden: t.hidden,
|
||||
} as Addon;
|
||||
}
|
||||
return panel;
|
||||
});
|
||||
}
|
||||
return panels;
|
||||
}
|
||||
|
||||
function filterTools(
|
||||
tools: Addon[],
|
||||
toolsExtra: Addon[],
|
||||
tabs: Addon[],
|
||||
{
|
||||
viewMode,
|
||||
storyId,
|
||||
location,
|
||||
path,
|
||||
}: {
|
||||
viewMode: State['viewMode'];
|
||||
storyId: State['storyId'];
|
||||
location: State['location'];
|
||||
path: State['path'];
|
||||
}
|
||||
) {
|
||||
const tabsTool = createTabsTool(tabs);
|
||||
const toolsLeft = [tabs.filter(p => !p.hidden).length > 1 ? tabsTool : null, ...tools];
|
||||
|
||||
const toolsRight = [...toolsExtra];
|
||||
|
||||
// if its a docsOnly page, even the 'story' view mode is considered 'docs'
|
||||
const filter = (item: Partial<Addon>) =>
|
||||
item &&
|
||||
(!item.match ||
|
||||
item.match({
|
||||
storyId,
|
||||
viewMode,
|
||||
location,
|
||||
path,
|
||||
}));
|
||||
|
||||
const displayItems = (list: Partial<Addon>[]) =>
|
||||
list.reduce(
|
||||
(acc, item, index) =>
|
||||
item ? (
|
||||
// @ts-ignore
|
||||
<Fragment key={item.id || item.key || `f-${index}`}>
|
||||
{acc}
|
||||
{item.render({}) || item}
|
||||
</Fragment>
|
||||
) : (
|
||||
acc
|
||||
),
|
||||
null
|
||||
);
|
||||
|
||||
const left = displayItems(toolsLeft.filter(filter));
|
||||
const right = displayItems(toolsRight.filter(filter));
|
||||
|
||||
return { left, right };
|
||||
}
|
||||
|
4
lib/ui/src/components/preview/stringifyQueryParams.tsx
Normal file
4
lib/ui/src/components/preview/stringifyQueryParams.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export const stringifyQueryParams = (queryParams: Record<string, string>) =>
|
||||
Object.entries(queryParams).reduce((acc, [k, v]) => {
|
||||
return `${acc}&${k}=${v}`;
|
||||
}, '');
|
@ -1,7 +1,16 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import React, { FunctionComponent, Fragment } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import { FlexBar } from '@storybook/components';
|
||||
import { FlexBar, IconButton, Icons, Separator, TabButton, TabBar } from '@storybook/components';
|
||||
import { Consumer, Combo } from '@storybook/api';
|
||||
import { Addon } from '@storybook/addons';
|
||||
|
||||
import { stringifyQueryParams } from './stringifyQueryParams';
|
||||
import { ZoomConsumer, Zoom } from './zoom';
|
||||
|
||||
import * as S from './components';
|
||||
|
||||
const Bar: FunctionComponent<{ shown: boolean } & Record<string, any>> = ({ shown, ...props }) => (
|
||||
<FlexBar {...props} />
|
||||
@ -19,3 +28,134 @@ export const Toolbar = styled(Bar)(
|
||||
tranform: shown ? 'translateY(0px)' : 'translateY(-40px)',
|
||||
})
|
||||
);
|
||||
|
||||
const fullScreenMapper = ({ api, state }: Combo) => ({
|
||||
toggle: api.toggleFullscreen,
|
||||
value: state.layout.isFullscreen,
|
||||
});
|
||||
|
||||
export const fullScreenTool: Addon = {
|
||||
title: 'fullscreen',
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<Consumer filter={fullScreenMapper}>
|
||||
{({ toggle, value }: ReturnType<typeof fullScreenMapper>) => (
|
||||
<S.DesktopOnly>
|
||||
<IconButton
|
||||
key="full"
|
||||
onClick={toggle as any}
|
||||
title={value ? 'Exit full screen' : 'Go full screen'}
|
||||
>
|
||||
<Icons icon={value ? 'close' : 'expand'} />
|
||||
</IconButton>
|
||||
</S.DesktopOnly>
|
||||
)}
|
||||
</Consumer>
|
||||
),
|
||||
};
|
||||
|
||||
const copyMapper = ({ state }: Combo) => ({
|
||||
origin: state.location.origin,
|
||||
pathname: state.location.pathname,
|
||||
storyId: state.storyId,
|
||||
baseUrl: 'iframe.html',
|
||||
queryParams: state.customQueryParams,
|
||||
});
|
||||
|
||||
export const copyTool: Addon = {
|
||||
title: 'copy',
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<Consumer filter={copyMapper}>
|
||||
{({ baseUrl, storyId, origin, pathname, queryParams }: ReturnType<typeof copyMapper>) => (
|
||||
<IconButton
|
||||
key="copy"
|
||||
onClick={() =>
|
||||
copy(`${origin}${pathname}${baseUrl}?id=${storyId}${stringifyQueryParams(queryParams)}`)
|
||||
}
|
||||
title="Copy canvas link"
|
||||
>
|
||||
<Icons icon="copy" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Consumer>
|
||||
),
|
||||
};
|
||||
|
||||
const ejectMapper = ({ state }: Combo) => ({
|
||||
baseUrl: 'iframe.html',
|
||||
storyId: state.storyId,
|
||||
queryParams: state.customQueryParams,
|
||||
});
|
||||
|
||||
export const ejectTool: Addon = {
|
||||
title: 'eject',
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<Consumer filter={ejectMapper}>
|
||||
{({ baseUrl, storyId, queryParams }: ReturnType<typeof ejectMapper>) => (
|
||||
<IconButton
|
||||
key="opener"
|
||||
href={`${baseUrl}?id=${storyId}${stringifyQueryParams(queryParams)}`}
|
||||
target="_blank"
|
||||
title="Open canvas in new tab"
|
||||
>
|
||||
<Icons icon="share" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Consumer>
|
||||
),
|
||||
};
|
||||
|
||||
const zoomTool: Addon = {
|
||||
title: 'zoom',
|
||||
match: p => p.viewMode === 'story',
|
||||
render: () => (
|
||||
<Fragment>
|
||||
<ZoomConsumer>
|
||||
{({ set, value }) => (
|
||||
<Zoom key="zoom" set={(v: number) => set(value * v)} reset={() => set(1)} />
|
||||
)}
|
||||
</ZoomConsumer>
|
||||
<Separator />
|
||||
</Fragment>
|
||||
),
|
||||
};
|
||||
|
||||
const tabsMapper = ({ state }: Combo) => ({
|
||||
viewMode: state.docsOnly,
|
||||
storyId: state.storyId,
|
||||
path: state.path,
|
||||
location: state.location,
|
||||
});
|
||||
|
||||
export const createTabsTool = (tabs: Addon[]): Addon => ({
|
||||
title: 'title',
|
||||
render: () => (
|
||||
<Consumer filter={tabsMapper}>
|
||||
{({ viewMode, storyId, path, location }: ReturnType<typeof tabsMapper>) => (
|
||||
<Fragment>
|
||||
<TabBar key="tabs">
|
||||
{tabs
|
||||
.filter(p => !p.hidden)
|
||||
.map((t, index) => {
|
||||
const to = t.route({ storyId, viewMode, path, location });
|
||||
const isActive = path === to;
|
||||
return (
|
||||
<S.UnstyledLink key={t.id || `l${index}`} to={to}>
|
||||
<TabButton disabled={t.disabled} active={isActive}>
|
||||
{t.title}
|
||||
</TabButton>
|
||||
</S.UnstyledLink>
|
||||
);
|
||||
})}
|
||||
</TabBar>
|
||||
<Separator />
|
||||
</Fragment>
|
||||
)}
|
||||
</Consumer>
|
||||
),
|
||||
});
|
||||
|
||||
export const defaultTools: Addon[] = [zoomTool];
|
||||
export const defaultToolsExtra: Addon[] = [fullScreenTool, ejectTool, copyTool];
|
||||
|
@ -18,6 +18,7 @@ export const ApplyWrappers: FunctionComponent<ApplyWrappersProps> = ({
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const IframeWrapper = styled.div(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
@ -39,7 +39,6 @@ const mapper = ({ api, state }: Combo) => {
|
||||
docsOnly: (parameters && parameters.docsOnly) as boolean,
|
||||
location,
|
||||
parameters,
|
||||
isLoading: !state.storiesConfigured,
|
||||
};
|
||||
};
|
||||
|
||||
@ -58,11 +57,6 @@ const PreviewConnected = React.memo<{ id: string; withLoader: boolean }>(props =
|
||||
...props,
|
||||
baseUrl: getBaseUrl(),
|
||||
...fromState,
|
||||
...(fromState.api.renderPreview
|
||||
? {
|
||||
customCanvas: fromState.api.renderPreview,
|
||||
}
|
||||
: {}),
|
||||
} as PreviewProps;
|
||||
|
||||
return <Preview {...p} />;
|
||||
|
Loading…
x
Reference in New Issue
Block a user