diff --git a/examples/external-docs/.babelrc b/examples/external-docs/.babelrc
new file mode 100644
index 00000000000..202d425a099
--- /dev/null
+++ b/examples/external-docs/.babelrc
@@ -0,0 +1,7 @@
+{
+ "presets": [
+ "@babel/preset-env",
+ "@babel/preset-react",
+ "@babel/preset-typescript"
+ ]
+}
diff --git a/examples/external-docs/README.md b/examples/external-docs/README.md
new file mode 100644
index 00000000000..5fe025e663f
--- /dev/null
+++ b/examples/external-docs/README.md
@@ -0,0 +1,3 @@
+# Storybook External Docs Example
+
+This example demostrates using Stories in an app built outside of SB's build process.
diff --git a/examples/external-docs/package.json b/examples/external-docs/package.json
new file mode 100644
index 00000000000..ad131088f16
--- /dev/null
+++ b/examples/external-docs/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@storybook/external-docs",
+ "version": "6.5.0-alpha.55",
+ "private": true,
+ "scripts": {
+ "build-storybook": "cross-env STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true build-storybook -c ./src/.storybook",
+ "debug": "cross-env NODE_OPTIONS=--inspect-brk STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true start-storybook -p 9011",
+ "sb": "node ../../lib/cli/bin/index.js",
+ "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start",
+ "storybook": "cross-env STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true start-storybook -p 9011 --no-manager-cache -c ./src/.storybook"
+ },
+ "dependencies": {
+ "@storybook/addon-essentials": "6.5.0-alpha.58",
+ "@storybook/components": "6.5.0-alpha.58",
+ "@storybook/csf": "0.0.2--canary.87bc651.0",
+ "@storybook/preview-web": "6.5.0-alpha.58",
+ "@storybook/react": "6.5.0-alpha.58",
+ "@storybook/store": "6.5.0-alpha.58",
+ "@storybook/theming": "6.5.0-alpha.58",
+ "formik": "^2.2.9",
+ "prop-types": "15.7.2",
+ "react": "16.14.0",
+ "react-dom": "16.14.0",
+ "react-scripts": "^4.0.2"
+ },
+ "devDependencies": {
+ "@babel/preset-env": "^7.12.11",
+ "@babel/preset-react": "^7.12.10",
+ "@babel/preset-typescript": "^7.12.7",
+ "@testing-library/dom": "^7.31.2",
+ "@testing-library/user-event": "^13.1.9",
+ "@types/babel__preset-env": "^7",
+ "@types/react": "^16.14.23",
+ "@types/react-dom": "^16.9.14",
+ "cross-env": "^7.0.3",
+ "typescript": "^3.9.7",
+ "webpack": "4"
+ }
+}
diff --git a/examples/external-docs/public/favicon.ico b/examples/external-docs/public/favicon.ico
new file mode 100644
index 00000000000..c2c86b859ea
Binary files /dev/null and b/examples/external-docs/public/favicon.ico differ
diff --git a/examples/external-docs/public/index.html b/examples/external-docs/public/index.html
new file mode 100644
index 00000000000..c240d2ca8b0
--- /dev/null
+++ b/examples/external-docs/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/examples/external-docs/public/logo192.png b/examples/external-docs/public/logo192.png
new file mode 100644
index 00000000000..fbdb05d4eb6
Binary files /dev/null and b/examples/external-docs/public/logo192.png differ
diff --git a/examples/external-docs/public/logo512.png b/examples/external-docs/public/logo512.png
new file mode 100644
index 00000000000..917458c29a8
Binary files /dev/null and b/examples/external-docs/public/logo512.png differ
diff --git a/examples/external-docs/public/manifest.json b/examples/external-docs/public/manifest.json
new file mode 100644
index 00000000000..080d6c77ac2
--- /dev/null
+++ b/examples/external-docs/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/examples/external-docs/public/robots.txt b/examples/external-docs/public/robots.txt
new file mode 100644
index 00000000000..01b0f9a1073
--- /dev/null
+++ b/examples/external-docs/public/robots.txt
@@ -0,0 +1,2 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
diff --git a/examples/external-docs/src/.storybook/main.ts b/examples/external-docs/src/.storybook/main.ts
new file mode 100644
index 00000000000..0465288c69c
--- /dev/null
+++ b/examples/external-docs/src/.storybook/main.ts
@@ -0,0 +1,34 @@
+import type { StorybookConfig } from '@storybook/react/types';
+
+const config: StorybookConfig = {
+ stories: [
+ {
+ directory: '../components',
+ titlePrefix: 'Demo',
+ },
+ ],
+ logLevel: 'debug',
+ addons: ['@storybook/addon-essentials'],
+ typescript: {
+ check: true,
+ checkOptions: {},
+ reactDocgenTypescriptOptions: {
+ propFilter: (prop) => ['label', 'disabled'].includes(prop.name),
+ },
+ },
+ core: {
+ builder: { name: 'webpack4' },
+ channelOptions: { allowFunction: false, maxDepth: 10 },
+ },
+ features: {
+ postcss: false,
+ // modernInlineRender: true,
+ storyStoreV7: !global.navigator?.userAgent?.match?.('jsdom'),
+ buildStoriesJson: true,
+ babelModeV7: true,
+ warnOnLegacyHierarchySeparator: false,
+ previewMdx2: true,
+ },
+ framework: '@storybook/react',
+};
+module.exports = config;
diff --git a/examples/external-docs/src/.storybook/preview.js b/examples/external-docs/src/.storybook/preview.js
new file mode 100644
index 00000000000..47aa5c189bf
--- /dev/null
+++ b/examples/external-docs/src/.storybook/preview.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import { ThemeProvider, convert, themes } from '@storybook/theming';
+
+export const parameters = {
+ options: {
+ // storySortV6: (a, b) => (
+ // a[1].kind === b[1].kind
+ // ? 0
+ // : a[1].id.localeCompare(b[1].id, undefined, { numeric: true });
+ // ),
+ // storySortV7: (a, b) => (
+ // a.title === b.title
+ // ? 0
+ // : a.id.localeCompare(b.id, undefined, { numeric: true });
+ // ),
+ storySort: {
+ order: ['Examples', 'Docs', 'Demo'],
+ },
+ },
+};
+
+export const decorators = [
+ (StoryFn) => (
+
+
+
+ ),
+];
diff --git a/examples/external-docs/src/SecondStoriesPage.tsx b/examples/external-docs/src/SecondStoriesPage.tsx
new file mode 100644
index 00000000000..e3b3bc3c201
--- /dev/null
+++ b/examples/external-docs/src/SecondStoriesPage.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { DocsProvider, Meta, Story } from './blocks';
+
+import meta, { Standard } from './components/AccountForm.stories';
+
+export default () => (
+
+
+
+
+
+
+
+);
diff --git a/examples/external-docs/src/StoriesPage.tsx b/examples/external-docs/src/StoriesPage.tsx
new file mode 100644
index 00000000000..75cd6b9126c
--- /dev/null
+++ b/examples/external-docs/src/StoriesPage.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { DocsProvider, Meta, Story } from './blocks';
+
+import meta, { WithArgs, Basic } from './components/button.stories';
+import EmojiMeta, { WithArgs as EmojiWithArgs } from './components/emoji-button.stories';
+
+export default () => (
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/examples/external-docs/src/blocks.tsx b/examples/external-docs/src/blocks.tsx
new file mode 100644
index 00000000000..9a4281a209d
--- /dev/null
+++ b/examples/external-docs/src/blocks.tsx
@@ -0,0 +1,171 @@
+import React, { createContext, useContext, useRef, useEffect } from 'react';
+
+import { Preview } from '@storybook/preview-web';
+import { Path, ModuleExports, StoryIndex } from '@storybook/store';
+import { toId, AnyFramework, ComponentTitle, StoryId } from '@storybook/csf';
+
+// @ts-ignore
+import * as reactAnnotations from '@storybook/react/dist/esm/client/preview/config';
+// @ts-ignore
+import * as previewAnnotations from './.storybook/preview';
+
+type StoryExport = any;
+type MetaExport = any;
+type ExportName = string;
+
+const projectAnnotations = {
+ ...reactAnnotations,
+ ...previewAnnotations,
+};
+
+const DocsContext = createContext<{
+ setMeta: (meta: MetaExport) => void;
+ addStory: (story: StoryExport, storyMeta?: MetaExport) => void;
+ renderStory: (story: StoryExport, element: HTMLElement) => void;
+}>({
+ setMeta: () => {},
+ addStory: () => {},
+ renderStory: () => {},
+});
+
+export const DocsProvider: React.FC = ({ children }) => {
+ let pageMeta: MetaExport;
+ const setMeta = (m: MetaExport) => {
+ pageMeta = m;
+ };
+
+ let nextImportPath = 0;
+ const importPaths = new Map();
+ const getImportPath = (meta: MetaExport) => {
+ if (!importPaths.has(meta)) {
+ importPaths.set(meta, `importPath-${nextImportPath}`);
+ nextImportPath += 1;
+ }
+ return importPaths.get(meta) as Path;
+ };
+
+ let nextTitle = 0;
+ const titles = new Map();
+ const getTitle = (meta: MetaExport) => {
+ if (!titles.has(meta)) {
+ titles.set(meta, `title-${nextTitle}`);
+ nextTitle += 1;
+ }
+ return titles.get(meta);
+ };
+
+ let nextExportName = 0;
+ const exportNames = new Map();
+ const getExportName = (story: StoryExport) => {
+ if (!exportNames.has(story)) {
+ exportNames.set(story, `export-${nextExportName}`);
+ nextExportName += 1;
+ }
+ return exportNames.get(story) as ExportName;
+ };
+
+ const storyIds = new Map();
+
+ const storyIndex: StoryIndex = { v: 3, stories: {} };
+ const knownCsfFiles: Record = {};
+
+ const addStory = (storyExport: StoryExport, storyMeta?: MetaExport) => {
+ const meta = storyMeta || pageMeta;
+ const importPath: Path = getImportPath(meta);
+ const title: ComponentTitle = meta.title || getTitle(meta);
+
+ const exportName = getExportName(storyExport);
+ const storyId = toId(title, exportName);
+ storyIds.set(storyExport, storyId);
+
+ if (!knownCsfFiles[importPath]) {
+ knownCsfFiles[importPath] = {
+ default: meta,
+ };
+ }
+ knownCsfFiles[importPath][exportName] = storyExport;
+
+ storyIndex.stories[storyId] = {
+ id: storyId,
+ importPath,
+ title,
+ name: 'Name',
+ };
+ };
+
+ let previewPromise: Promise>;
+ const getPreview = () => {
+ const importFn = (importPath: Path) => {
+ console.log(knownCsfFiles, importPath);
+ return Promise.resolve(knownCsfFiles[importPath]);
+ };
+
+ if (!previewPromise) {
+ previewPromise = (async () => {
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ if (window.preview) {
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ (window.preview as PreviewWeb).onStoriesChanged({
+ importFn,
+ storyIndex,
+ });
+ } else {
+ const preview = new Preview();
+ await preview.initialize({
+ getStoryIndex: () => storyIndex,
+ importFn,
+ getProjectAnnotations: () => projectAnnotations,
+ });
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ window.preview = preview;
+ }
+
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ return window.preview;
+ })();
+ }
+
+ return previewPromise;
+ };
+
+ const renderStory = async (storyExport: any, element: HTMLElement) => {
+ const preview = await getPreview();
+
+ const storyId = storyIds.get(storyExport);
+ if (!storyId) throw new Error(`Didn't find story id '${storyId}'`);
+ const story = await preview.storyStore.loadStory({ storyId });
+
+ console.log({ story });
+
+ preview.renderStoryToElement(story, element);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export function Meta({ of }: { of: any }) {
+ const { setMeta } = useContext(DocsContext);
+ setMeta(of);
+ return null;
+}
+
+export function Story({ of, meta }: { of: any; meta?: any }) {
+ const { addStory, renderStory } = useContext(DocsContext);
+
+ addStory(of, meta);
+
+ const ref = useRef(null);
+ useEffect(() => {
+ if (ref.current) renderStory(of, ref.current);
+ });
+
+ return ;
+}
diff --git a/examples/external-docs/src/components/AccountForm.stories.tsx b/examples/external-docs/src/components/AccountForm.stories.tsx
new file mode 100644
index 00000000000..46fe06ff649
--- /dev/null
+++ b/examples/external-docs/src/components/AccountForm.stories.tsx
@@ -0,0 +1,95 @@
+/* eslint-disable storybook/await-interactions */
+/* eslint-disable storybook/use-storybook-testing-library */
+// @TODO: use addon-interactions and remove the rule disable above
+import { ComponentStoryObj, ComponentMeta } from '@storybook/react';
+import { screen } from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
+import { AccountForm } from './AccountForm';
+
+export default {
+ // Title not needed due to CSF3 auto-title
+ // title: 'Demo/AccountForm',
+ component: AccountForm,
+ parameters: {
+ layout: 'centered',
+ },
+} as ComponentMeta;
+
+// export const Standard = (args: any) => ;
+// Standard.args = { passwordVerification: false };
+// Standard.play = () => userEvent.type(screen.getByTestId('email'), 'michael@chromatic.com');
+
+export const Standard: ComponentStoryObj = {
+ // render: (args: AccountFormProps) => ,
+ args: { passwordVerification: false },
+};
+
+export const StandardEmailFilled = {
+ ...Standard,
+ play: () => userEvent.type(screen.getByTestId('email'), 'michael@chromatic.com'),
+};
+
+export const StandardEmailFailed = {
+ ...Standard,
+ play: async () => {
+ await userEvent.type(screen.getByTestId('email'), 'michael@chromatic.com.com@com');
+ await userEvent.type(screen.getByTestId('password1'), 'testpasswordthatwontfail');
+ await userEvent.click(screen.getByTestId('submit'));
+ },
+};
+
+export const StandardPasswordFailed = {
+ ...Standard,
+ play: async () => {
+ await StandardEmailFilled.play();
+ await userEvent.type(screen.getByTestId('password1'), 'asdf');
+ await userEvent.click(screen.getByTestId('submit'));
+ },
+};
+
+const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
+export const StandardFailHover = {
+ ...StandardPasswordFailed,
+ play: async () => {
+ await StandardPasswordFailed.play();
+ await sleep(100);
+ await userEvent.hover(screen.getByTestId('password-error-info'));
+ },
+};
+
+export const Verification: ComponentStoryObj = {
+ args: { passwordVerification: true },
+};
+
+export const VerificationPasssword1 = {
+ ...Verification,
+ play: async () => {
+ await StandardEmailFilled.play();
+ await userEvent.type(screen.getByTestId('password1'), 'asdfasdf');
+ await userEvent.click(screen.getByTestId('submit'));
+ },
+};
+
+export const VerificationPasswordMismatch = {
+ ...Verification,
+ play: async () => {
+ await StandardEmailFilled.play();
+ await userEvent.type(screen.getByTestId('password1'), 'asdfasdf');
+ await userEvent.type(screen.getByTestId('password2'), 'asdf1234');
+ await userEvent.click(screen.getByTestId('submit'));
+ },
+};
+
+export const VerificationSuccess = {
+ ...Verification,
+ play: async () => {
+ await StandardEmailFilled.play();
+ await sleep(1000);
+ await userEvent.type(screen.getByTestId('password1'), 'asdfasdf', { delay: 50 });
+ await sleep(1000);
+ await userEvent.type(screen.getByTestId('password2'), 'asdfasdf', { delay: 50 });
+ await sleep(1000);
+ await userEvent.click(screen.getByTestId('submit'));
+ },
+};
diff --git a/examples/external-docs/src/components/AccountForm.tsx b/examples/external-docs/src/components/AccountForm.tsx
new file mode 100644
index 00000000000..d5e612931ac
--- /dev/null
+++ b/examples/external-docs/src/components/AccountForm.tsx
@@ -0,0 +1,552 @@
+import { keyframes, styled } from '@storybook/theming';
+import {
+ ErrorMessage,
+ Field as FormikInput,
+ Form as FormikForm,
+ Formik,
+ FormikProps,
+} from 'formik';
+import React, { FC, HTMLAttributes, useCallback, useState } from 'react';
+import { Icons, WithTooltip } from '@storybook/components';
+
+const errorMap = {
+ email: {
+ required: {
+ normal: 'Please enter your email address',
+ tooltip:
+ 'We do require an email address and a password as a minimum in order to be able to create an account for you to log in with',
+ },
+ format: {
+ normal: 'Please enter a correctly formatted email address',
+ tooltip:
+ 'Your email address is formatted incorrectly and is not correct - please double check for misspelling',
+ },
+ },
+ password: {
+ required: {
+ normal: 'Please enter a password',
+ tooltip: 'A password is requried to create an account',
+ },
+ length: {
+ normal: 'Please enter a password of minimum 6 characters',
+ tooltip:
+ 'For security reasons we enforce a password length of minimum 6 characters - but have no other requirements',
+ },
+ },
+ verifiedPassword: {
+ required: {
+ normal: 'Please verify your password',
+ tooltip:
+ 'Verification of your password is required to ensure no errors in the spelling of the password',
+ },
+ match: {
+ normal: 'Your passwords do not match',
+ tooltip:
+ 'Your verification password has to match your password to make sure you have not misspelled',
+ },
+ },
+};
+
+// https://emailregex.com/
+const email99RegExp = new RegExp(
+ // eslint-disable-next-line no-useless-escape
+ /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+);
+
+export interface AccountFormResponse {
+ success: boolean;
+}
+
+export interface AccountFormValues {
+ email: string;
+ password: string;
+}
+
+interface FormValues extends AccountFormValues {
+ verifiedPassword: string;
+}
+
+interface FormErrors {
+ email?: string;
+ emailTooltip?: string;
+ password?: string;
+ passwordTooltip?: string;
+ verifiedPassword?: string;
+ verifiedPasswordTooltip?: string;
+}
+
+export type AccountFormProps = {
+ passwordVerification?: boolean;
+ onSubmit?: (values: AccountFormValues) => void;
+ onTransactionStart?: (values: AccountFormValues) => void;
+ onTransactionEnd?: (values: AccountFormResponse) => void;
+};
+
+export const AccountForm: FC = ({
+ passwordVerification,
+ onSubmit,
+ onTransactionStart,
+ onTransactionEnd,
+}) => {
+ const [state, setState] = useState({
+ transacting: false,
+ transactionSuccess: false,
+ transactionFailure: false,
+ });
+
+ const handleFormSubmit = useCallback(
+ async ({ email, password }: FormValues, { setSubmitting, resetForm }) => {
+ if (onSubmit) {
+ onSubmit({ email, password });
+ }
+
+ if (onTransactionStart) {
+ onTransactionStart({ email, password });
+ }
+
+ setSubmitting(true);
+
+ setState({
+ ...state,
+ transacting: true,
+ });
+
+ await new Promise((r) => setTimeout(r, 2100));
+
+ const success = Math.random() < 1;
+
+ if (onTransactionEnd) {
+ onTransactionEnd({ success });
+ }
+
+ setSubmitting(false);
+ resetForm({ values: { email: '', password: '', verifiedPassword: '' } });
+
+ setState({
+ ...state,
+ transacting: false,
+ transactionSuccess: success === true,
+ transactionFailure: success === false,
+ });
+ },
+ [setState, onTransactionEnd, onTransactionStart]
+ );
+
+ return (
+
+
+
+ Storybook icon
+
+
+
+
+
+
+
+ Storybook
+
+
+
+
+
+ {!state.transactionSuccess && !state.transactionFailure && (
+ Create an account to join the Storybook community
+ )}
+
+ {state.transactionSuccess && !state.transactionFailure && (
+
+
+ Everything is perfect. Your account is ready and we should probably get you started!
+
+ So why don't you get started then?
+ {
+ setState({
+ transacting: false,
+ transactionSuccess: false,
+ transactionFailure: false,
+ });
+ }}
+ >
+ Go back
+
+
+ )}
+ {state.transactionFailure && !state.transactionSuccess && (
+
+ What a mess, this API is not working
+
+ Someone should probably have a stern talking to about this, but it won't be me - coz
+ I'm gonna head out into the nice weather
+
+ {
+ setState({
+ transacting: false,
+ transactionSuccess: false,
+ transactionFailure: false,
+ });
+ }}
+ >
+ Go back
+
+
+ )}
+ {!state.transactionSuccess && !state.transactionFailure && (
+ {
+ const errors: FormErrors = {};
+
+ if (!email) {
+ errors.email = errorMap.email.required.normal;
+ errors.emailTooltip = errorMap.email.required.tooltip;
+ } else {
+ const validEmail = email.match(email99RegExp);
+
+ if (validEmail === null) {
+ errors.email = errorMap.email.format.normal;
+ errors.emailTooltip = errorMap.email.format.tooltip;
+ }
+ }
+
+ if (!password) {
+ errors.password = errorMap.password.required.normal;
+ errors.passwordTooltip = errorMap.password.required.tooltip;
+ } else if (password.length < 6) {
+ errors.password = errorMap.password.length.normal;
+ errors.passwordTooltip = errorMap.password.length.tooltip;
+ }
+
+ if (passwordVerification && !verifiedPassword) {
+ errors.verifiedPassword = errorMap.verifiedPassword.required.normal;
+ errors.verifiedPasswordTooltip = errorMap.verifiedPassword.required.tooltip;
+ } else if (passwordVerification && password !== verifiedPassword) {
+ errors.verifiedPassword = errorMap.verifiedPassword.match.normal;
+ errors.verifiedPasswordTooltip = errorMap.verifiedPassword.match.tooltip;
+ }
+
+ return errors;
+ }}
+ >
+ {({ errors: _errors, isSubmitting, dirty }: FormikProps) => {
+ const errors = _errors as FormErrors;
+
+ return (
+
+ );
+ }}
+
+ )}
+
+
+ );
+};
+
+const Wrapper = styled.section(({ theme }) => ({
+ fontFamily: theme.typography.fonts.base,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ width: 450,
+ padding: 32,
+ backgroundColor: theme.background.content,
+ borderRadius: 7,
+}));
+
+const Brand = styled.div({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+});
+
+const Title = styled.svg({
+ height: 40,
+ zIndex: 1,
+ left: -32,
+ position: 'relative',
+});
+
+const logoAnimation = keyframes({
+ '0': {
+ transform: 'rotateY(0deg)',
+ transformOrigin: '50% 5% 0',
+ },
+ '100%': {
+ transform: 'rotateY(360deg)',
+ transformOrigin: '50% 5% 0',
+ },
+});
+
+interface LogoProps {
+ transacting: boolean;
+}
+
+const Logo = styled.svg(
+ ({ transacting }) =>
+ transacting && {
+ animation: `${logoAnimation} 1250ms both infinite`,
+ },
+ { height: 40, zIndex: 10, marginLeft: 32 }
+);
+
+const Introduction = styled.p({
+ marginTop: 20,
+ textAlign: 'center',
+});
+
+const Content = styled.div({
+ display: 'flex',
+ alignItems: 'flex-start',
+ justifyContent: 'center',
+ width: 350,
+ minHeight: 189,
+ marginTop: 8,
+});
+
+const Presentation = styled.div({
+ textAlign: 'center',
+});
+
+const Form = styled(FormikForm)({
+ width: '100%',
+ alignSelf: 'flex-start',
+ '&[aria-disabled="true"]': {
+ opacity: 0.6,
+ },
+});
+
+const FieldWrapper = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'stretch',
+ marginBottom: 10,
+});
+
+const Label = styled.label({
+ fontSize: 13,
+ fontWeight: 500,
+ marginBottom: 6,
+});
+
+const Input = styled.input(({ theme }) => ({
+ fontSize: 14,
+ color: theme.color.defaultText,
+ padding: '10px 15px',
+ borderRadius: 4,
+ appearance: 'none',
+ outline: 'none',
+ border: '0 none',
+ boxShadow: 'rgb(0 0 0 / 10%) 0px 0px 0px 1px inset',
+ '&:focus': {
+ boxShadow: 'rgb(30 167 253) 0px 0px 0px 1px inset',
+ },
+ '&:active': {
+ boxShadow: 'rgb(30 167 253) 0px 0px 0px 1px inset',
+ },
+ '&[aria-invalid="true"]': {
+ boxShadow: 'rgb(255 68 0) 0px 0px 0px 1px inset',
+ },
+}));
+
+const ErrorWrapper = styled.div({
+ display: 'flex',
+ alignItems: 'flex-start',
+ fontSize: 11,
+ marginTop: 6,
+ cursor: 'help',
+});
+
+const ErrorIcon = styled(Icons)(({ theme }) => ({
+ fill: theme.color.defaultText,
+ opacity: 0.8,
+ marginRight: 6,
+ marginLeft: 2,
+ marginTop: 1,
+}));
+
+const ErrorTooltip = styled.div(({ theme }) => ({
+ fontFamily: theme.typography.fonts.base,
+ fontSize: 13,
+ padding: 8,
+ maxWidth: 350,
+}));
+
+const Actions = styled.div({
+ alignSelf: 'stretch',
+ display: 'flex',
+ justifyContent: 'space-between',
+ marginTop: 24,
+});
+
+const Error = styled(ErrorMessage)({});
+
+interface ButtonProps {
+ dirty?: boolean;
+}
+
+const Button = styled.button({
+ backgroundColor: 'transparent',
+ border: '0 none',
+ outline: 'none',
+ appearance: 'none',
+ fontWeight: 500,
+ fontSize: 12,
+ flexBasis: '50%',
+ cursor: 'pointer',
+ padding: '11px 16px',
+ borderRadius: 4,
+ textTransform: 'uppercase',
+ '&:focus': {
+ textDecoration: 'underline',
+ fontWeight: 700,
+ },
+ '&:active': {
+ textDecoration: 'underline',
+ fontWeight: 700,
+ },
+ '&[aria-disabled="true"]': {
+ cursor: 'default',
+ },
+});
+
+const Submit = styled(Button)(({ theme, dirty }) => ({
+ marginRight: 8,
+ backgroundColor: theme.color.secondary,
+ color: theme.color.inverseText,
+ opacity: dirty ? 1 : 0.6,
+ boxShadow: 'rgb(30 167 253 / 10%) 0 0 0 1px inset',
+}));
+
+const Reset = styled(Button)(({ theme }) => ({
+ marginLeft: 8,
+ boxShadow: 'rgb(30 167 253) 0 0 0 1px inset',
+ color: theme.color.secondary,
+}));
diff --git a/examples/external-docs/src/components/button.stories.tsx b/examples/external-docs/src/components/button.stories.tsx
new file mode 100644
index 00000000000..7005dc92658
--- /dev/null
+++ b/examples/external-docs/src/components/button.stories.tsx
@@ -0,0 +1,42 @@
+/* eslint-disable storybook/await-interactions */
+/* eslint-disable storybook/use-storybook-testing-library */
+// @TODO: use addon-interactions and remove the rule disable above
+import React from 'react';
+import { Meta, ComponentStory } from '@storybook/react';
+import { screen } from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
+import { Button } from './button';
+
+export default {
+ component: Button,
+ title: 'Examples / Button',
+ argTypes: { onClick: { action: 'click ' } },
+ // render: () => <>hohoho>,
+} as Meta;
+
+export const WithArgs: ComponentStory = (args) => ;
+WithArgs.args = { label: 'With args' };
+
+export const Basic = () => ;
+
+export const StoryObject = {
+ render: () => <>hahaha>,
+};
+
+export const StoryNoRender = {
+ args: { label: 'magic!' },
+};
+
+export const StoryWithPlay = {
+ args: { label: 'play' },
+ play: () => {
+ console.log('play!!');
+ userEvent.click(screen.getByRole('button'));
+ },
+};
+
+export const CSF2StoryWithPlay = WithArgs.bind({});
+CSF2StoryWithPlay.play = () => {
+ console.log('play!!');
+ userEvent.click(screen.getByRole('button'));
+};
diff --git a/examples/external-docs/src/components/button.tsx b/examples/external-docs/src/components/button.tsx
new file mode 100644
index 00000000000..f3c9ee6b83e
--- /dev/null
+++ b/examples/external-docs/src/components/button.tsx
@@ -0,0 +1,14 @@
+import React, { ButtonHTMLAttributes } from 'react';
+
+export interface ButtonProps extends ButtonHTMLAttributes {
+ /**
+ * A label to show on the button
+ */
+ label: string;
+}
+
+export const Button = ({ label = 'Hello', ...props }: ButtonProps) => (
+
+);
diff --git a/examples/external-docs/src/components/emoji-button.stories.tsx b/examples/external-docs/src/components/emoji-button.stories.tsx
new file mode 100644
index 00000000000..9fdbd03f2c2
--- /dev/null
+++ b/examples/external-docs/src/components/emoji-button.stories.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { EmojiButton } from './emoji-button';
+
+export default { component: EmojiButton, title: 'Examples / Emoji Button' };
+
+export const WithArgs = (args: any) => ;
+WithArgs.args = { label: 'With args' };
+export const Basic = () => ;
diff --git a/examples/external-docs/src/components/emoji-button.tsx b/examples/external-docs/src/components/emoji-button.tsx
new file mode 100644
index 00000000000..f38ee88a0dd
--- /dev/null
+++ b/examples/external-docs/src/components/emoji-button.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export const EmojiButton = ({ label, ...props }: { label: string }) => (
+
+);
+
+EmojiButton.propTypes = {
+ /**
+ * A label to show on the button
+ */
+ label: PropTypes.string,
+};
+
+EmojiButton.defaultProps = {
+ label: 'Hello',
+};
diff --git a/examples/external-docs/src/index.tsx b/examples/external-docs/src/index.tsx
new file mode 100644
index 00000000000..ec04c046a88
--- /dev/null
+++ b/examples/external-docs/src/index.tsx
@@ -0,0 +1,24 @@
+/* global document */
+
+import React, { useState } from 'react';
+import ReactDOM from 'react-dom';
+import StoriesPage from './StoriesPage';
+import SecondStoriesPage from './SecondStoriesPage';
+
+const Router = ({ routes }: { routes: (() => JSX.Element)[] }) => {
+ const [routeNumber, setRoute] = useState(0);
+ const Route = routes[routeNumber];
+
+ console.log(routeNumber);
+ return (
+
+
+ {/* eslint-disable-next-line react/button-has-type */}
+
+
+ );
+};
+
+const App = () => ;
+
+ReactDOM.render(, document.getElementById('root'));
diff --git a/examples/external-docs/src/react-app-env.d.ts b/examples/external-docs/src/react-app-env.d.ts
new file mode 100644
index 00000000000..6431bc5fc6b
--- /dev/null
+++ b/examples/external-docs/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/external-docs/tsconfig.json b/examples/external-docs/tsconfig.json
new file mode 100644
index 00000000000..dc08a84bc28
--- /dev/null
+++ b/examples/external-docs/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "esModuleInterop": true,
+ "jsx": "react",
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "es5",
+ "lib": [
+ "dom",
+ "esnext"
+ ],
+ "allowJs": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": [ "src/*"]
+}
diff --git a/lib/cli/src/versions.ts b/lib/cli/src/versions.ts
index 931317e5a4b..35b535384de 100644
--- a/lib/cli/src/versions.ts
+++ b/lib/cli/src/versions.ts
@@ -1,59 +1,59 @@
// auto generated file, do not edit
export default {
- "@storybook/addon-a11y": "6.5.0-alpha.58",
- "@storybook/addon-actions": "6.5.0-alpha.58",
- "@storybook/addon-backgrounds": "6.5.0-alpha.58",
- "@storybook/addon-controls": "6.5.0-alpha.58",
- "@storybook/addon-docs": "6.5.0-alpha.58",
- "@storybook/addon-essentials": "6.5.0-alpha.58",
- "@storybook/addon-interactions": "6.5.0-alpha.58",
- "@storybook/addon-jest": "6.5.0-alpha.58",
- "@storybook/addon-links": "6.5.0-alpha.58",
- "@storybook/addon-measure": "6.5.0-alpha.58",
- "@storybook/addon-outline": "6.5.0-alpha.58",
- "@storybook/addon-storyshots": "6.5.0-alpha.58",
- "@storybook/addon-storyshots-puppeteer": "6.5.0-alpha.58",
- "@storybook/addon-storysource": "6.5.0-alpha.58",
- "@storybook/addon-toolbars": "6.5.0-alpha.58",
- "@storybook/addon-viewport": "6.5.0-alpha.58",
- "@storybook/addons": "6.5.0-alpha.58",
- "@storybook/angular": "6.5.0-alpha.58",
- "@storybook/api": "6.5.0-alpha.58",
- "@storybook/builder-webpack4": "6.5.0-alpha.58",
- "@storybook/builder-webpack5": "6.5.0-alpha.58",
- "@storybook/channel-postmessage": "6.5.0-alpha.58",
- "@storybook/channel-websocket": "6.5.0-alpha.58",
- "@storybook/channels": "6.5.0-alpha.58",
- "@storybook/cli": "6.5.0-alpha.58",
- "@storybook/client-api": "6.5.0-alpha.58",
- "@storybook/client-logger": "6.5.0-alpha.58",
- "@storybook/codemod": "6.5.0-alpha.58",
- "@storybook/components": "6.5.0-alpha.58",
- "@storybook/core": "6.5.0-alpha.58",
- "@storybook/core-client": "6.5.0-alpha.58",
- "@storybook/core-common": "6.5.0-alpha.58",
- "@storybook/core-events": "6.5.0-alpha.58",
- "@storybook/core-server": "6.5.0-alpha.58",
- "@storybook/csf-tools": "6.5.0-alpha.58",
- "@storybook/docs-tools": "6.5.0-alpha.58",
- "@storybook/ember": "6.5.0-alpha.58",
- "@storybook/html": "6.5.0-alpha.58",
- "@storybook/instrumenter": "6.5.0-alpha.58",
- "@storybook/manager-webpack4": "6.5.0-alpha.58",
- "@storybook/manager-webpack5": "6.5.0-alpha.58",
- "@storybook/node-logger": "6.5.0-alpha.58",
- "@storybook/postinstall": "6.5.0-alpha.58",
- "@storybook/preact": "6.5.0-alpha.58",
- "@storybook/preview-web": "6.5.0-alpha.58",
- "@storybook/react": "6.5.0-alpha.58",
- "@storybook/router": "6.5.0-alpha.58",
- "@storybook/server": "6.5.0-alpha.58",
- "@storybook/source-loader": "6.5.0-alpha.58",
- "@storybook/store": "6.5.0-alpha.58",
- "@storybook/svelte": "6.5.0-alpha.58",
- "@storybook/theming": "6.5.0-alpha.58",
- "@storybook/ui": "6.5.0-alpha.58",
- "@storybook/vue": "6.5.0-alpha.58",
- "@storybook/vue3": "6.5.0-alpha.58",
- "@storybook/web-components": "6.5.0-alpha.58"
-}
\ No newline at end of file
+ '@storybook/addon-a11y': '6.5.0-alpha.58',
+ '@storybook/addon-actions': '6.5.0-alpha.58',
+ '@storybook/addon-backgrounds': '6.5.0-alpha.58',
+ '@storybook/addon-controls': '6.5.0-alpha.58',
+ '@storybook/addon-docs': '6.5.0-alpha.58',
+ '@storybook/addon-essentials': '6.5.0-alpha.58',
+ '@storybook/addon-interactions': '6.5.0-alpha.58',
+ '@storybook/addon-jest': '6.5.0-alpha.58',
+ '@storybook/addon-links': '6.5.0-alpha.58',
+ '@storybook/addon-measure': '6.5.0-alpha.58',
+ '@storybook/addon-outline': '6.5.0-alpha.58',
+ '@storybook/addon-storyshots': '6.5.0-alpha.58',
+ '@storybook/addon-storyshots-puppeteer': '6.5.0-alpha.58',
+ '@storybook/addon-storysource': '6.5.0-alpha.58',
+ '@storybook/addon-toolbars': '6.5.0-alpha.58',
+ '@storybook/addon-viewport': '6.5.0-alpha.58',
+ '@storybook/addons': '6.5.0-alpha.58',
+ '@storybook/angular': '6.5.0-alpha.58',
+ '@storybook/api': '6.5.0-alpha.58',
+ '@storybook/builder-webpack4': '6.5.0-alpha.58',
+ '@storybook/builder-webpack5': '6.5.0-alpha.58',
+ '@storybook/channel-postmessage': '6.5.0-alpha.58',
+ '@storybook/channel-websocket': '6.5.0-alpha.58',
+ '@storybook/channels': '6.5.0-alpha.58',
+ '@storybook/cli': '6.5.0-alpha.58',
+ '@storybook/client-api': '6.5.0-alpha.58',
+ '@storybook/client-logger': '6.5.0-alpha.58',
+ '@storybook/codemod': '6.5.0-alpha.58',
+ '@storybook/components': '6.5.0-alpha.58',
+ '@storybook/core': '6.5.0-alpha.58',
+ '@storybook/core-client': '6.5.0-alpha.58',
+ '@storybook/core-common': '6.5.0-alpha.58',
+ '@storybook/core-events': '6.5.0-alpha.58',
+ '@storybook/core-server': '6.5.0-alpha.58',
+ '@storybook/csf-tools': '6.5.0-alpha.58',
+ '@storybook/docs-tools': '6.5.0-alpha.58',
+ '@storybook/ember': '6.5.0-alpha.58',
+ '@storybook/html': '6.5.0-alpha.58',
+ '@storybook/instrumenter': '6.5.0-alpha.58',
+ '@storybook/manager-webpack4': '6.5.0-alpha.58',
+ '@storybook/manager-webpack5': '6.5.0-alpha.58',
+ '@storybook/node-logger': '6.5.0-alpha.58',
+ '@storybook/postinstall': '6.5.0-alpha.58',
+ '@storybook/preact': '6.5.0-alpha.58',
+ '@storybook/preview-web': '6.5.0-alpha.58',
+ '@storybook/react': '6.5.0-alpha.58',
+ '@storybook/router': '6.5.0-alpha.58',
+ '@storybook/server': '6.5.0-alpha.58',
+ '@storybook/source-loader': '6.5.0-alpha.58',
+ '@storybook/store': '6.5.0-alpha.58',
+ '@storybook/svelte': '6.5.0-alpha.58',
+ '@storybook/theming': '6.5.0-alpha.58',
+ '@storybook/ui': '6.5.0-alpha.58',
+ '@storybook/vue': '6.5.0-alpha.58',
+ '@storybook/vue3': '6.5.0-alpha.58',
+ '@storybook/web-components': '6.5.0-alpha.58',
+};
diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts
index 57f6176cf95..d386611584e 100644
--- a/lib/preview-web/src/DocsRender.ts
+++ b/lib/preview-web/src/DocsRender.ts
@@ -4,15 +4,21 @@ import { Story, StoryStore, CSFFile } from '@storybook/store';
import { Channel } from '@storybook/addons';
import { DOCS_RENDERED } from '@storybook/core-events';
-import { DocsContextProps } from './types';
+import { Render, StoryRender } from './StoryRender';
+import type { DocsContextProps } from './types';
-export class DocsRender {
+export class DocsRender implements Render {
private canvasElement?: HTMLElement;
private context?: DocsContextProps;
public disableKeyListeners = false;
+ static fromStoryRender(storyRender: StoryRender) {
+ const { channel, store, id, story } = storyRender;
+ return new DocsRender(channel, store, id, story);
+ }
+
// eslint-disable-next-line no-useless-constructor
constructor(
private channel: Channel,
diff --git a/lib/preview-web/src/Preview.tsx b/lib/preview-web/src/Preview.tsx
new file mode 100644
index 00000000000..fb57fc374a7
--- /dev/null
+++ b/lib/preview-web/src/Preview.tsx
@@ -0,0 +1,343 @@
+import dedent from 'ts-dedent';
+import global from 'global';
+import { SynchronousPromise } from 'synchronous-promise';
+import Events from '@storybook/core-events';
+import { logger } from '@storybook/client-logger';
+import { addons, Channel } from '@storybook/addons';
+import { AnyFramework, StoryId, ProjectAnnotations, Args, Globals } from '@storybook/csf';
+import {
+ ModuleImportFn,
+ Story,
+ StoryStore,
+ StoryIndex,
+ PromiseLike,
+ WebProjectAnnotations,
+} from '@storybook/store';
+
+import { StoryRender } from './StoryRender';
+import { DocsRender } from './DocsRender';
+
+const { fetch } = global;
+
+type MaybePromise = Promise | T;
+
+const STORY_INDEX_PATH = './stories.json';
+
+export class Preview {
+ channel: Channel;
+
+ serverChannel?: Channel;
+
+ storyStore: StoryStore;
+
+ getStoryIndex?: () => StoryIndex;
+
+ importFn?: ModuleImportFn;
+
+ renderToDOM: WebProjectAnnotations['renderToDOM'];
+
+ storyRenders: StoryRender[] = [];
+
+ previewEntryError?: Error;
+
+ constructor() {
+ this.channel = addons.getChannel();
+ if (global.FEATURES?.storyStoreV7 && addons.hasServerChannel()) {
+ this.serverChannel = addons.getServerChannel();
+ }
+ this.storyStore = new StoryStore();
+ }
+
+ // INITIALIZATION
+
+ // NOTE: the reason that the preview and store's initialization code is written in a promise
+ // style and not `async-await`, and the use of `SynchronousPromise`s is in order to allow
+ // storyshots to immediately call `raw()` on the store without waiting for a later tick.
+ // (Even simple things like `Promise.resolve()` and `await` involve the callback happening
+ // in the next promise "tick").
+ // See the comment in `storyshots-core/src/api/index.ts` for more detail.
+ initialize({
+ getStoryIndex,
+ importFn,
+ getProjectAnnotations,
+ }: {
+ // In the case of the v6 store, we can only get the index from the facade *after*
+ // getProjectAnnotations has been run, thus this slightly awkward approach
+ getStoryIndex?: () => StoryIndex;
+ importFn: ModuleImportFn;
+ getProjectAnnotations: () => MaybePromise>;
+ }) {
+ // We save these two on initialization in case `getProjectAnnotations` errors,
+ // in which case we may need them later when we recover.
+ this.getStoryIndex = getStoryIndex;
+ this.importFn = importFn;
+
+ this.setupListeners();
+
+ return this.getProjectAnnotationsOrRenderError(getProjectAnnotations).then(
+ (projectAnnotations) => this.initializeWithProjectAnnotations(projectAnnotations)
+ );
+ }
+
+ setupListeners() {
+ this.serverChannel?.on(Events.STORY_INDEX_INVALIDATED, this.onStoryIndexChanged.bind(this));
+
+ this.channel.on(Events.UPDATE_GLOBALS, this.onUpdateGlobals.bind(this));
+ this.channel.on(Events.UPDATE_STORY_ARGS, this.onUpdateArgs.bind(this));
+ this.channel.on(Events.RESET_STORY_ARGS, this.onResetArgs.bind(this));
+ this.channel.on(Events.FORCE_RE_RENDER, this.onForceReRender.bind(this));
+ this.channel.on(Events.FORCE_REMOUNT, this.onForceRemount.bind(this));
+ }
+
+ getProjectAnnotationsOrRenderError(
+ getProjectAnnotations: () => MaybePromise>
+ ): PromiseLike> {
+ return SynchronousPromise.resolve()
+ .then(getProjectAnnotations)
+ .then((projectAnnotations) => {
+ this.renderToDOM = projectAnnotations.renderToDOM;
+ if (!this.renderToDOM) {
+ throw new Error(dedent`
+ Expected your framework's preset to export a \`renderToDOM\` field.
+
+ Perhaps it needs to be upgraded for Storybook 6.4?
+
+ More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field
+ `);
+ }
+ return projectAnnotations;
+ })
+ .catch((err) => {
+ // This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
+ // needs to be show to the user as a simple error
+ this.renderPreviewEntryError('Error reading preview.js:', err);
+ throw err;
+ });
+ }
+
+ // If initialization gets as far as project annotations, this function runs.
+ initializeWithProjectAnnotations(projectAnnotations: WebProjectAnnotations) {
+ this.storyStore.setProjectAnnotations(projectAnnotations);
+
+ this.setInitialGlobals();
+
+ let storyIndexPromise: PromiseLike;
+ if (global.FEATURES?.storyStoreV7) {
+ storyIndexPromise = this.getStoryIndexFromServer();
+ } else {
+ if (!this.getStoryIndex) {
+ throw new Error('No `getStoryIndex` passed defined in v6 mode');
+ }
+ storyIndexPromise = SynchronousPromise.resolve().then(this.getStoryIndex);
+ }
+
+ return storyIndexPromise
+ .then((storyIndex: StoryIndex) => this.initializeWithStoryIndex(storyIndex))
+ .catch((err) => {
+ this.renderPreviewEntryError('Error loading story index:', err);
+ throw err;
+ });
+ }
+
+ async setInitialGlobals() {
+ this.emitGlobals();
+ }
+
+ emitGlobals() {
+ this.channel.emit(Events.SET_GLOBALS, {
+ globals: this.storyStore.globals.get() || {},
+ globalTypes: this.storyStore.projectAnnotations.globalTypes || {},
+ });
+ }
+
+ async getStoryIndexFromServer() {
+ const result = await fetch(STORY_INDEX_PATH);
+ if (result.status === 200) return result.json() as StoryIndex;
+
+ throw new Error(await result.text());
+ }
+
+ // If initialization gets as far as the story index, this function runs.
+ initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike {
+ return this.storyStore.initialize({
+ storyIndex,
+ importFn: this.importFn,
+ cache: !global.FEATURES?.storyStoreV7,
+ });
+ }
+
+ // EVENT HANDLERS
+
+ // This happens when a config file gets reloaded
+ async onGetProjectAnnotationsChanged({
+ getProjectAnnotations,
+ }: {
+ getProjectAnnotations: () => MaybePromise>;
+ }) {
+ delete this.previewEntryError;
+
+ const projectAnnotations = await this.getProjectAnnotationsOrRenderError(getProjectAnnotations);
+ if (!this.storyStore.projectAnnotations) {
+ await this.initializeWithProjectAnnotations(projectAnnotations);
+ return;
+ }
+
+ await this.storyStore.setProjectAnnotations(projectAnnotations);
+ this.emitGlobals();
+ }
+
+ async onStoryIndexChanged() {
+ delete this.previewEntryError;
+
+ if (!this.storyStore.projectAnnotations) {
+ // We haven't successfully set project annotations yet,
+ // we need to do that before we can do anything else.
+ return;
+ }
+
+ try {
+ const storyIndex = await this.getStoryIndexFromServer();
+
+ // This is the first time the story index worked, let's load it into the store
+ if (!this.storyStore.storyIndex) {
+ await this.initializeWithStoryIndex(storyIndex);
+ }
+
+ // Update the store with the new stories.
+ await this.onStoriesChanged({ storyIndex });
+ } catch (err) {
+ this.renderPreviewEntryError('Error loading story index:', err);
+ throw err;
+ }
+ }
+
+ // This happens when a glob gets HMR-ed
+ async onStoriesChanged({
+ importFn,
+ storyIndex,
+ }: {
+ importFn?: ModuleImportFn;
+ storyIndex?: StoryIndex;
+ }) {
+ await this.storyStore.onStoriesChanged({ importFn, storyIndex });
+ }
+
+ async onUpdateGlobals({ globals }: { globals: Globals }) {
+ this.storyStore.globals.update(globals);
+
+ await Promise.all(this.storyRenders.map((r) => r.rerender()));
+
+ this.channel.emit(Events.GLOBALS_UPDATED, {
+ globals: this.storyStore.globals.get(),
+ initialGlobals: this.storyStore.globals.initialGlobals,
+ });
+ }
+
+ async onUpdateArgs({ storyId, updatedArgs }: { storyId: StoryId; updatedArgs: Args }) {
+ this.storyStore.args.update(storyId, updatedArgs);
+
+ await Promise.all(this.storyRenders.filter((r) => r.id === storyId).map((r) => r.rerender()));
+
+ this.channel.emit(Events.STORY_ARGS_UPDATED, {
+ storyId,
+ args: this.storyStore.args.get(storyId),
+ });
+ }
+
+ async onResetArgs({ storyId, argNames }: { storyId: string; argNames?: string[] }) {
+ // NOTE: we have to be careful here and avoid await-ing when updating a rendered's args.
+ // That's because below in `renderStoryToElement` we have also bound to this event and will
+ // render the story in the same tick.
+ // However, we can do that safely as the current story is available in `this.storyRenders`
+ const render = this.storyRenders.find((r) => r.id === storyId);
+ const story = render?.story || (await this.storyStore.loadStory({ storyId }));
+
+ const argNamesToReset = argNames || Object.keys(this.storyStore.args.get(storyId));
+ const updatedArgs = argNamesToReset.reduce((acc, argName) => {
+ acc[argName] = story.initialArgs[argName];
+ return acc;
+ }, {} as Partial);
+
+ await this.onUpdateArgs({ storyId, updatedArgs });
+ }
+
+ // ForceReRender does not include a story id, so we simply must
+ // re-render all stories in case they are relevant
+ async onForceReRender() {
+ await Promise.all(this.storyRenders.map((r) => r.rerender()));
+ }
+
+ async onForceRemount({ storyId }: { storyId: StoryId }) {
+ await Promise.all(this.storyRenders.filter((r) => r.id === storyId).map((r) => r.remount()));
+ }
+
+ // Used by docs' modernInlineRender to render a story to a given element
+ // Note this short-circuits the `prepare()` phase of the StoryRender,
+ // main to be consistent with the previous behaviour. In the future,
+ // we will change it to go ahead and load the story, which will end up being
+ // "instant", although async.
+ renderStoryToElement(story: Story, element: HTMLElement) {
+ const render = new StoryRender(
+ this.channel,
+ this.storyStore,
+ this.renderToDOM,
+ this.inlineStoryCallbacks(story.id),
+ story.id,
+ 'docs',
+ story
+ );
+ render.renderToElement(element);
+
+ this.storyRenders.push(render);
+
+ return async () => {
+ await this.teardownRender(render);
+ };
+ }
+
+ async teardownRender(
+ render: StoryRender | DocsRender,
+ { viewModeChanged }: { viewModeChanged?: boolean } = {}
+ ) {
+ this.storyRenders = this.storyRenders.filter((r) => r !== render);
+ await render?.teardown({ viewModeChanged });
+ }
+
+ // API
+ async extract(options?: { includeDocsOnly: boolean }) {
+ if (this.previewEntryError) {
+ throw this.previewEntryError;
+ }
+
+ if (!this.storyStore.projectAnnotations) {
+ // In v6 mode, if your preview.js throws, we never get a chance to initialize the preview
+ // or store, and the error is simply logged to the browser console. This is the best we can do
+ throw new Error(dedent`Failed to initialize Storybook.
+
+ Do you have an error in your \`preview.js\`? Check your Storybook's browser console for errors.`);
+ }
+
+ if (global.FEATURES?.storyStoreV7) {
+ await this.storyStore.cacheAllCSFFiles();
+ }
+
+ return this.storyStore.extract(options);
+ }
+
+ // UTILITIES
+ inlineStoryCallbacks(storyId: StoryId) {
+ return {
+ showMain: () => {},
+ showError: (err: { title: string; description: string }) =>
+ logger.error(`Error rendering docs story (${storyId})`, err),
+ showException: (err: Error) => logger.error(`Error rendering docs story (${storyId})`, err),
+ };
+ }
+
+ renderPreviewEntryError(reason: string, err: Error) {
+ this.previewEntryError = err;
+ logger.error(reason);
+ logger.error(err);
+ this.channel.emit(Events.CONFIG_ERROR, err);
+ }
+}
diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx
index 71c450cb1ea..0492c70b0f4 100644
--- a/lib/preview-web/src/PreviewWeb.tsx
+++ b/lib/preview-web/src/PreviewWeb.tsx
@@ -1,77 +1,52 @@
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import global from 'global';
-import { SynchronousPromise } from 'synchronous-promise';
import Events, { IGNORED_EXCEPTION } from '@storybook/core-events';
import { logger } from '@storybook/client-logger';
-import { addons, Channel } from '@storybook/addons';
import { AnyFramework, StoryId, ProjectAnnotations, Args, Globals } from '@storybook/csf';
import {
ModuleImportFn,
Selection,
Story,
- StoryStore,
StorySpecifier,
StoryIndex,
+ PromiseLike,
WebProjectAnnotations,
} from '@storybook/store';
+import { Preview } from './Preview';
+
import { UrlStore } from './UrlStore';
import { WebView } from './WebView';
-import { PREPARE_ABORTED, StoryRender } from './StoryRender';
+import { PREPARE_ABORTED, Render, StoryRender } from './StoryRender';
import { DocsRender } from './DocsRender';
-const { window: globalWindow, fetch } = global;
+const { window: globalWindow } = global;
function focusInInput(event: Event) {
const target = event.target as Element;
return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null;
}
-type PromiseLike = Promise | SynchronousPromise;
type MaybePromise = Promise | T;
-type StoryCleanupFn = () => MaybePromise;
-
-const STORY_INDEX_PATH = './stories.json';
-
-type HTMLStoryRender = StoryRender;
-
-export class PreviewWeb {
- channel: Channel;
-
- serverChannel?: Channel;
+export class PreviewWeb extends Preview {
urlStore: UrlStore;
- storyStore: StoryStore;
-
view: WebView;
- getStoryIndex?: () => StoryIndex;
-
- importFn?: ModuleImportFn;
-
- renderToDOM: WebProjectAnnotations['renderToDOM'];
-
previewEntryError?: Error;
currentSelection: Selection;
- currentRender: HTMLStoryRender | DocsRender;
-
- storyRenders: HTMLStoryRender[] = [];
-
- previousCleanup: StoryCleanupFn;
+ currentRender: Render;
constructor() {
- this.channel = addons.getChannel();
- if (global.FEATURES?.storyStoreV7 && addons.hasServerChannel()) {
- this.serverChannel = addons.getServerChannel();
- }
- this.view = new WebView();
+ super();
+ this.view = new WebView();
this.urlStore = new UrlStore();
- this.storyStore = new StoryStore();
+
// Add deprecated APIs for back-compat
// @ts-ignore
this.storyStore.getSelection = deprecate(
@@ -84,99 +59,19 @@ export class PreviewWeb {
);
}
- // INITIALIZATION
-
- // NOTE: the reason that the preview and store's initialization code is written in a promise
- // style and not `async-await`, and the use of `SynchronousPromise`s is in order to allow
- // storyshots to immediately call `raw()` on the store without waiting for a later tick.
- // (Even simple things like `Promise.resolve()` and `await` involve the callback happening
- // in the next promise "tick").
- // See the comment in `storyshots-core/src/api/index.ts` for more detail.
- initialize({
- getStoryIndex,
- importFn,
- getProjectAnnotations,
- }: {
- // In the case of the v6 store, we can only get the index from the facade *after*
- // getProjectAnnotations has been run, thus this slightly awkward approach
- getStoryIndex?: () => StoryIndex;
- importFn: ModuleImportFn;
- getProjectAnnotations: () => MaybePromise>;
- }) {
- // We save these two on initialization in case `getProjectAnnotations` errors,
- // in which case we may need them later when we recover.
- this.getStoryIndex = getStoryIndex;
- this.importFn = importFn;
-
- this.setupListeners();
-
- return this.getProjectAnnotationsOrRenderError(getProjectAnnotations).then(
- (projectAnnotations) => this.initializeWithProjectAnnotations(projectAnnotations)
- );
- }
-
setupListeners() {
- globalWindow.onkeydown = this.onKeydown.bind(this);
+ super.setupListeners();
- this.serverChannel?.on(Events.STORY_INDEX_INVALIDATED, this.onStoryIndexChanged.bind(this));
+ globalWindow.onkeydown = this.onKeydown.bind(this);
this.channel.on(Events.SET_CURRENT_STORY, this.onSetCurrentStory.bind(this));
this.channel.on(Events.UPDATE_QUERY_PARAMS, this.onUpdateQueryParams.bind(this));
- this.channel.on(Events.UPDATE_GLOBALS, this.onUpdateGlobals.bind(this));
- this.channel.on(Events.UPDATE_STORY_ARGS, this.onUpdateArgs.bind(this));
- this.channel.on(Events.RESET_STORY_ARGS, this.onResetArgs.bind(this));
- this.channel.on(Events.FORCE_RE_RENDER, this.onForceReRender.bind(this));
- this.channel.on(Events.FORCE_REMOUNT, this.onForceRemount.bind(this));
}
- getProjectAnnotationsOrRenderError(
- getProjectAnnotations: () => MaybePromise>
- ): PromiseLike> {
- return SynchronousPromise.resolve()
- .then(getProjectAnnotations)
- .then((projectAnnotations) => {
- this.renderToDOM = projectAnnotations.renderToDOM;
- if (!this.renderToDOM) {
- throw new Error(dedent`
- Expected your framework's preset to export a \`renderToDOM\` field.
-
- Perhaps it needs to be upgraded for Storybook 6.4?
-
- More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field
- `);
- }
- return projectAnnotations;
- })
- .catch((err) => {
- // This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
- // needs to be show to the user as a simple error
- this.renderPreviewEntryError('Error reading preview.js:', err);
- throw err;
- });
- }
-
- // If initialization gets as far as project annotations, this function runs.
initializeWithProjectAnnotations(projectAnnotations: WebProjectAnnotations) {
- this.storyStore.setProjectAnnotations(projectAnnotations);
-
- this.setInitialGlobals();
-
- let storyIndexPromise: PromiseLike;
- if (global.FEATURES?.storyStoreV7) {
- storyIndexPromise = this.getStoryIndexFromServer();
- } else {
- if (!this.getStoryIndex) {
- throw new Error('No `getStoryIndex` passed defined in v6 mode');
- }
- storyIndexPromise = SynchronousPromise.resolve().then(this.getStoryIndex);
- }
-
- return storyIndexPromise
- .then((storyIndex: StoryIndex) => this.initializeWithStoryIndex(storyIndex))
- .catch((err) => {
- this.renderPreviewEntryError('Error loading story index:', err);
- throw err;
- });
+ return super
+ .initializeWithProjectAnnotations(projectAnnotations)
+ .then(() => this.setInitialGlobals());
}
async setInitialGlobals() {
@@ -187,35 +82,15 @@ export class PreviewWeb {
this.emitGlobals();
}
- emitGlobals() {
- this.channel.emit(Events.SET_GLOBALS, {
- globals: this.storyStore.globals.get() || {},
- globalTypes: this.storyStore.projectAnnotations.globalTypes || {},
- });
- }
-
- async getStoryIndexFromServer() {
- const result = await fetch(STORY_INDEX_PATH);
- if (result.status === 200) return result.json() as StoryIndex;
-
- throw new Error(await result.text());
- }
-
// If initialization gets as far as the story index, this function runs.
- initializeWithStoryIndex(storyIndex: StoryIndex) {
- return this.storyStore
- .initialize({
- storyIndex,
- importFn: this.importFn,
- cache: !global.FEATURES?.storyStoreV7,
- })
- .then(() => {
- if (!global.FEATURES?.storyStoreV7) {
- this.channel.emit(Events.SET_STORIES, this.storyStore.getSetStoriesPayload());
- }
+ initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike {
+ return super.initializeWithStoryIndex(storyIndex).then(() => {
+ if (!global.FEATURES?.storyStoreV7) {
+ this.channel.emit(Events.SET_STORIES, this.storyStore.getSetStoriesPayload());
+ }
- return this.selectSpecifiedStory();
- });
+ return this.selectSpecifiedStory();
+ });
}
// Use the selection specifier to choose a story, then render it
@@ -269,44 +144,11 @@ export class PreviewWeb {
}: {
getProjectAnnotations: () => MaybePromise>;
}) {
- delete this.previewEntryError;
+ await super.onGetProjectAnnotationsChanged({ getProjectAnnotations });
- const projectAnnotations = await this.getProjectAnnotationsOrRenderError(getProjectAnnotations);
- if (!this.storyStore.projectAnnotations) {
- await this.initializeWithProjectAnnotations(projectAnnotations);
- return;
- }
-
- await this.storyStore.setProjectAnnotations(projectAnnotations);
- this.emitGlobals();
this.renderSelection();
}
- async onStoryIndexChanged() {
- delete this.previewEntryError;
-
- if (!this.storyStore.projectAnnotations) {
- // We haven't successfully set project annotations yet,
- // we need to do that before we can do anything else.
- return;
- }
-
- try {
- const storyIndex = await this.getStoryIndexFromServer();
-
- // This is the first time the story index worked, let's load it into the store
- if (!this.storyStore.storyIndex) {
- await this.initializeWithStoryIndex(storyIndex);
- }
-
- // Update the store with the new stories.
- await this.onStoriesChanged({ storyIndex });
- } catch (err) {
- this.renderPreviewEntryError('Error loading story index:', err);
- throw err;
- }
- }
-
// This happens when a glob gets HMR-ed
async onStoriesChanged({
importFn,
@@ -315,7 +157,8 @@ export class PreviewWeb {
importFn?: ModuleImportFn;
storyIndex?: StoryIndex;
}) {
- await this.storyStore.onStoriesChanged({ importFn, storyIndex });
+ super.onStoriesChanged({ importFn, storyIndex });
+
if (!global.FEATURES?.storyStoreV7) {
this.channel.emit(Events.SET_STORIES, await this.storyStore.getSetStoriesPayload());
}
@@ -349,22 +192,13 @@ export class PreviewWeb {
}
async onUpdateGlobals({ globals }: { globals: Globals }) {
- this.storyStore.globals.update(globals);
-
- await Promise.all(this.storyRenders.map((r) => r.rerender()));
+ super.onUpdateGlobals({ globals });
if (this.currentRender instanceof DocsRender) await this.currentRender.rerender();
-
- this.channel.emit(Events.GLOBALS_UPDATED, {
- globals: this.storyStore.globals.get(),
- initialGlobals: this.storyStore.globals.initialGlobals,
- });
}
async onUpdateArgs({ storyId, updatedArgs }: { storyId: StoryId; updatedArgs: Args }) {
- this.storyStore.args.update(storyId, updatedArgs);
-
- await Promise.all(this.storyRenders.filter((r) => r.id === storyId).map((r) => r.rerender()));
+ super.onUpdateArgs({ storyId, updatedArgs });
// NOTE: we aren't checking to see the story args are targetted at the "right" story.
// This is because we may render >1 story on the page and there is no easy way to keep track
@@ -372,40 +206,6 @@ export class PreviewWeb {
// However, in `modernInlineRender`, the individual stories track their own events as they
// each call `renderStoryToElement` below.
if (this.currentRender instanceof DocsRender) await this.currentRender.rerender();
-
- this.channel.emit(Events.STORY_ARGS_UPDATED, {
- storyId,
- args: this.storyStore.args.get(storyId),
- });
- }
-
- async onResetArgs({ storyId, argNames }: { storyId: string; argNames?: string[] }) {
- // NOTE: we have to be careful here and avoid await-ing when updating the current story's args.
- // That's because below in `renderStoryToElement` we have also bound to this event and will
- // render the story in the same tick.
- // However, we can do that safely as the current story is available in `this.currentRender.story`
- const { initialArgs } =
- storyId === this.currentRender?.id
- ? this.currentRender.story
- : await this.storyStore.loadStory({ storyId });
-
- const argNamesToReset = argNames || Object.keys(this.storyStore.args.get(storyId));
- const updatedArgs = argNamesToReset.reduce((acc, argName) => {
- acc[argName] = initialArgs[argName];
- return acc;
- }, {} as Partial);
-
- await this.onUpdateArgs({ storyId, updatedArgs });
- }
-
- // ForceReRender does not include a story id, so we simply must
- // re-render all stories in case they are relevant
- async onForceReRender() {
- await Promise.all(this.storyRenders.map((r) => r.rerender()));
- }
-
- async onForceRemount({ storyId }: { storyId: StoryId }) {
- await Promise.all(this.storyRenders.filter((r) => r.id === storyId).map((r) => r.remount()));
}
// RENDERING
@@ -447,10 +247,7 @@ export class PreviewWeb {
lastRender = null;
}
- const storyRender: PreviewWeb['currentRender'] = new StoryRender<
- HTMLElement,
- TFramework
- >(
+ const storyRender = new StoryRender(
this.channel,
this.storyStore,
(...args) => {
@@ -520,7 +317,7 @@ export class PreviewWeb {
}
if (selection.viewMode === 'docs' || parameters.docsOnly) {
- this.currentRender = storyRender.toDocsRender();
+ this.currentRender = DocsRender.fromStoryRender(storyRender);
this.currentRender.renderToElement(
this.view.prepareForDocs(),
this.renderStoryToElement.bind(this)
@@ -537,7 +334,7 @@ export class PreviewWeb {
// we will change it to go ahead and load the story, which will end up being
// "instant", although async.
renderStoryToElement(story: Story, element: HTMLElement) {
- const render = new StoryRender(
+ const render = new StoryRender(
this.channel,
this.storyStore,
this.renderToDOM,
@@ -556,7 +353,7 @@ export class PreviewWeb {
}
async teardownRender(
- render: HTMLStoryRender | DocsRender,
+ render: Render,
{ viewModeChanged }: { viewModeChanged?: boolean } = {}
) {
this.storyRenders = this.storyRenders.filter((r) => r !== render);
@@ -603,11 +400,8 @@ export class PreviewWeb {
}
renderPreviewEntryError(reason: string, err: Error) {
- this.previewEntryError = err;
- logger.error(reason);
- logger.error(err);
+ super.renderPreviewEntryError(reason, err);
this.view.showErrorDisplay(err);
- this.channel.emit(Events.CONFIG_ERROR, err);
}
renderMissingStory() {
diff --git a/lib/preview-web/src/StoryRender.ts b/lib/preview-web/src/StoryRender.ts
index 70d4e311570..d0ede7cc122 100644
--- a/lib/preview-web/src/StoryRender.ts
+++ b/lib/preview-web/src/StoryRender.ts
@@ -9,7 +9,6 @@ import {
import { Story, RenderContext, StoryStore } from '@storybook/store';
import { Channel } from '@storybook/addons';
import { STORY_RENDER_PHASE_CHANGED, STORY_RENDERED } from '@storybook/core-events';
-import { DocsRender } from './DocsRender';
const { AbortController } = global;
@@ -41,28 +40,34 @@ export type RenderContextCallbacks = Pick<
export const PREPARE_ABORTED = new Error('prepareAborted');
-export class StoryRender<
- CanvasElement extends HTMLElement | void,
- TFramework extends AnyFramework
-> {
+export interface Render {
+ id: StoryId;
+ story?: Story;
+ isPreparing: () => boolean;
+ disableKeyListeners: boolean;
+ teardown: (options: { viewModeChanged: boolean }) => Promise;
+ renderToElement: (canvasElement: HTMLElement, renderStoryToElement?: any) => Promise;
+}
+
+export class StoryRender implements Render {
public story?: Story;
public phase?: RenderPhase;
private abortController?: AbortController;
- private canvasElement?: CanvasElement;
+ private canvasElement?: HTMLElement;
private notYetRendered = true;
public disableKeyListeners = false;
constructor(
- private channel: Channel,
- private store: StoryStore,
+ public channel: Channel,
+ public store: StoryStore,
private renderToScreen: (
renderContext: RenderContext,
- canvasElement: CanvasElement
+ canvasElement: HTMLElement
) => void | Promise,
private callbacks: RenderContextCallbacks,
public id: StoryId,
@@ -104,7 +109,7 @@ export class StoryRender<
}
// The two story "renders" are equal and have both loaded the same story
- isEqual(other?: StoryRender | DocsRender) {
+ isEqual(other?: Render) {
return other && this.id === other.id && this.story && this.story === other.story;
}
@@ -116,15 +121,11 @@ export class StoryRender<
return ['rendering', 'playing'].includes(this.phase);
}
- toDocsRender() {
- return new DocsRender(this.channel, this.store, this.id, this.story);
- }
-
context() {
return this.store.getStoryContext(this.story);
}
- async renderToElement(canvasElement: CanvasElement) {
+ async renderToElement(canvasElement: HTMLElement) {
this.canvasElement = canvasElement;
// FIXME: this comment
diff --git a/lib/preview-web/src/index.ts b/lib/preview-web/src/index.ts
index d643864f3ab..d3abaf01141 100644
--- a/lib/preview-web/src/index.ts
+++ b/lib/preview-web/src/index.ts
@@ -1,6 +1,7 @@
// FIXME: breaks builder-vite, remove this in 7.0
export { composeConfigs } from '@storybook/store';
+export { Preview } from './Preview';
export { PreviewWeb } from './PreviewWeb';
export { simulatePageLoad, simulateDOMContentLoaded } from './simulate-pageload';
diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts
index 04b15b465a4..d76e91fa613 100644
--- a/lib/store/src/StoryStore.ts
+++ b/lib/store/src/StoryStore.ts
@@ -26,6 +26,7 @@ import type {
Path,
ExtractOptions,
BoundStory,
+ PromiseLike,
StoryIndex,
StoryIndexEntry,
V2CompatIndexEntry,
diff --git a/lib/store/src/types.ts b/lib/store/src/types.ts
index cd92d6c1606..d52a0f2588e 100644
--- a/lib/store/src/types.ts
+++ b/lib/store/src/types.ts
@@ -26,7 +26,7 @@ import type {
export type { StoryId, Parameters };
export type Path = string;
export type ModuleExports = Record;
-type PromiseLike = Promise | SynchronousPromise;
+export type PromiseLike = Promise | SynchronousPromise;
export type ModuleImportFn = (path: Path) => PromiseLike;
export type WebProjectAnnotations =
diff --git a/lib/ui/src/app.tsx b/lib/ui/src/app.tsx
index 1027819e944..9b0302fee3e 100644
--- a/lib/ui/src/app.tsx
+++ b/lib/ui/src/app.tsx
@@ -61,13 +61,7 @@ const App = React.memo(
if (!width || !height) {
content = ;
} else if (width < 600) {
- content =
- ;
+ content = ;
} else {
content = (
(
))}
-)) as FunctionComponent<{ active: ActiveTabsType; children: ReactNode, isFullscreen: boolean }>);
+)) as FunctionComponent<{ active: ActiveTabsType; children: ReactNode; isFullscreen: boolean }>);
Panels.displayName = 'Panels';
const PanelsContainer = styled.div<{ isFullscreen: boolean }>(
@@ -101,7 +101,8 @@ const PanelsContainer = styled.div<{ isFullscreen: boolean }>(
top: 0,
left: 0,
width: '100vw',
- }, ({ isFullscreen }) => ({
+ },
+ ({ isFullscreen }) => ({
height: isFullscreen ? '100vh' : 'calc(100% - 40px)',
})
);
@@ -192,7 +193,10 @@ class Mobile extends Component {
{!options.isFullscreen && (
- this.setState({ active: SIDEBAR })} active={active === SIDEBAR}>
+ this.setState({ active: SIDEBAR })}
+ active={active === SIDEBAR}
+ >
Sidebar
this.setState({ active: CANVAS })} active={active === CANVAS}>
@@ -202,7 +206,10 @@ class Mobile extends Component {
))}
{viewMode && !docsOnly ? (
- this.setState({ active: ADDONS })} active={active === ADDONS}>
+ this.setState({ active: ADDONS })}
+ active={active === ADDONS}
+ >
Addons
) : null}
diff --git a/nx.json b/nx.json
index 588fa7bfcf8..3545eaf1005 100644
--- a/nx.json
+++ b/nx.json
@@ -139,6 +139,10 @@
"@storybook/example-react-ts-webpack4": {
"implicitDependencies": []
},
+ "@storybook/external-docs": {
+ "implicitDependencies": []
+ },
+
"server-kitchen-sink": {
"implicitDependencies": []
},
diff --git a/workspace.json b/workspace.json
index 010773cb947..f02b363a625 100644
--- a/workspace.json
+++ b/workspace.json
@@ -161,6 +161,10 @@
"root": "examples/react-ts-webpack4",
"type": "library"
},
+ "@storybook/external-docs": {
+ "root": "examples/external-docs",
+ "type": "library"
+ },
"server-kitchen-sink": {
"root": "examples/server-kitchen-sink",
"type": "library"
diff --git a/yarn.lock b/yarn.lock
index 3ea752d6a42..e87284f01cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7735,6 +7735,36 @@ __metadata:
languageName: node
linkType: hard
+"@storybook/external-docs@workspace:examples/external-docs":
+ version: 0.0.0-use.local
+ resolution: "@storybook/external-docs@workspace:examples/external-docs"
+ dependencies:
+ "@babel/preset-env": ^7.12.11
+ "@babel/preset-react": ^7.12.10
+ "@babel/preset-typescript": ^7.12.7
+ "@storybook/addon-essentials": 6.5.0-alpha.58
+ "@storybook/components": 6.5.0-alpha.58
+ "@storybook/csf": 0.0.2--canary.87bc651.0
+ "@storybook/preview-web": 6.5.0-alpha.58
+ "@storybook/react": 6.5.0-alpha.58
+ "@storybook/store": 6.5.0-alpha.58
+ "@storybook/theming": 6.5.0-alpha.58
+ "@testing-library/dom": ^7.31.2
+ "@testing-library/user-event": ^13.1.9
+ "@types/babel__preset-env": ^7
+ "@types/react": ^16.14.23
+ "@types/react-dom": ^16.9.14
+ cross-env: ^7.0.3
+ formik: ^2.2.9
+ prop-types: 15.7.2
+ react: 16.14.0
+ react-dom: 16.14.0
+ react-scripts: ^4.0.2
+ typescript: ^3.9.7
+ webpack: 4
+ languageName: unknown
+ linkType: soft
+
"@storybook/html@6.5.0-alpha.58, @storybook/html@workspace:*, @storybook/html@workspace:app/html":
version: 0.0.0-use.local
resolution: "@storybook/html@workspace:app/html"