{
133 | if (props.parentSize) {
134 | const parent = container?.parentElement;
135 | if (parent) {
136 | parent.style.height = props.parentSize;
137 | }
138 | }
139 | stateRef.editorContainer = container || undefined;
140 | resolveContainer(container);
141 | }}
142 | style={{ height: '100%' }}
143 | className="sb-unstyled"
144 | />
145 | );
146 | }
147 |
--------------------------------------------------------------------------------
/src/Editor/getMonacoOverflowContainer.ts:
--------------------------------------------------------------------------------
1 | export function getMonacoOverflowContainer(id: string) {
2 | let container = document.getElementById(id);
3 |
4 | if (container) {
5 | return container;
6 | }
7 |
8 | container = document.createElement('div');
9 | container.id = id;
10 | container.classList.add('monaco-editor', 'sb-unstyled');
11 |
12 | document.body.appendChild(container);
13 |
14 | return container;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Editor/monacoLoader.ts:
--------------------------------------------------------------------------------
1 | import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api';
2 |
3 | function injectScript(url: string) {
4 | return new Promise
((resolve, reject) => {
5 | const script = document.createElement('script');
6 | script.src = url;
7 | script.defer = true;
8 | script.onload = resolve;
9 | script.onerror = reject;
10 | document.head.append(script);
11 | });
12 | }
13 |
14 | export function monacoLoader(): Promise {
15 | const relativeLoaderScriptPath = 'monaco-editor/min/vs/loader.js';
16 | return injectScript(relativeLoaderScriptPath).then((e) => {
17 | const loaderScriptSrc: string = (e.target as any)?.src || window.location.origin + '/';
18 | const baseUrl = loaderScriptSrc.replace(relativeLoaderScriptPath, '');
19 | return new Promise((resolve) => {
20 | (window as any).require.config({ paths: { vs: `${baseUrl}monaco-editor/min/vs` } });
21 | (window as any).require(['vs/editor/editor.main'], resolve);
22 | });
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/Editor/reactTypesLoader.ts:
--------------------------------------------------------------------------------
1 | export function reactTypesLoader() {
2 | const typeLibs = ['@types/react/index.d.ts', '@types/react/jsx-runtime.d.ts'];
3 |
4 | return Promise.all(
5 | typeLibs.map((typeLib) => {
6 | return fetch(typeLib, { headers: { accept: 'text/plain' } }).then(
7 | async (resp) => [typeLib, await resp.text()],
8 | () => {},
9 | );
10 | }),
11 | ).then((typeLibs) => {
12 | return typeLibs.filter((l) => Array.isArray(l));
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/Editor/setupMonaco.ts:
--------------------------------------------------------------------------------
1 | import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api';
2 | import { createStore } from '../createStore';
3 |
4 | interface MonacoSetup {
5 | monacoEnvironment?: Monaco.Environment;
6 | onMonacoLoad?: (monaco: typeof Monaco) => any;
7 | }
8 |
9 | const store = createStore();
10 |
11 | export function setupMonaco(options: MonacoSetup) {
12 | store.setValue('monacoSetup', options);
13 | }
14 |
15 | export function getMonacoSetup() {
16 | return store.getValue('monacoSetup') || {};
17 | }
18 |
--------------------------------------------------------------------------------
/src/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | interface ErrorBoundaryProps {
4 | resetRef: React.MutableRefObject<(() => void) | undefined>;
5 | children: React.ReactNode;
6 | }
7 |
8 | export const errorStyle = {
9 | backgroundColor: '#f8d7da',
10 | borderRadius: '5px',
11 | color: '#721c24',
12 | fontFamily: 'monospace',
13 | margin: '0',
14 | padding: '20px',
15 | };
16 |
17 | class ErrorBoundary extends React.Component {
18 | state = { error: undefined };
19 |
20 | constructor(props: ErrorBoundaryProps) {
21 | super(props);
22 | this.props.resetRef.current = this.setState.bind(this, this.state);
23 | }
24 |
25 | static getDerivedStateFromError(error: unknown) {
26 | return { error };
27 | }
28 |
29 | render() {
30 | if (this.state.error) {
31 | return {String(this.state.error)}
;
32 | }
33 |
34 | return this.props.children;
35 | }
36 | }
37 |
38 | export default ErrorBoundary;
39 |
--------------------------------------------------------------------------------
/src/Preview.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { EsModules, evalModule } from './evalModule';
3 | import { errorStyle } from './ErrorBoundary';
4 |
5 | interface PreviewProps {
6 | availableImports: EsModules;
7 | code: string;
8 | componentProps?: any;
9 | }
10 |
11 | export default function Preview({ availableImports, code, componentProps }: PreviewProps) {
12 | let DefaultExport: any;
13 |
14 | try {
15 | DefaultExport = code ? evalModule(code, availableImports).default : undefined;
16 | const isObject = DefaultExport && typeof DefaultExport === 'object';
17 | const isFunction = typeof DefaultExport === 'function';
18 | if (!isObject && !isFunction) {
19 | throw new TypeError('Default export is not a React component');
20 | }
21 | } catch (error) {
22 | return {String(error)}
;
23 | }
24 |
25 | return ;
26 | }
27 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const paramId = 'liveCodeEditor';
2 | export const addonId = 'liveCodeEditorAddon';
3 | export const panelId = 'liveCodeEditorPanel';
4 |
--------------------------------------------------------------------------------
/src/createStore.ts:
--------------------------------------------------------------------------------
1 | // This provides shared state and the ability to subscribe to changes
2 | // between the manager and preview iframes.
3 | // Attempted to use Storybook's `addons.getChannel()` but it doesn't emit
4 | // across iframes.
5 |
6 | type Callback = (newValue: T) => any;
7 |
8 | interface Store {
9 | onChange(callback: Callback): () => void;
10 | getValue(): T | undefined;
11 | setValue(newValue: T): void;
12 | }
13 |
14 | function newStore(initialValue?: T): Store {
15 | const callbacks = new Set>();
16 | let value = initialValue;
17 |
18 | return {
19 | onChange(callback) {
20 | callbacks.add(callback);
21 | return () => {
22 | callbacks.delete(callback);
23 | };
24 | },
25 | getValue: () => value,
26 | setValue(newValue) {
27 | value = newValue;
28 | callbacks.forEach((callback) => {
29 | try {
30 | callback(newValue);
31 | } catch (error) {
32 | console.error(error);
33 | }
34 | });
35 | },
36 | };
37 | }
38 |
39 | interface KeyStore {
40 | onChange(key: string, callback: Callback): () => void;
41 | getValue(key: string): T | undefined;
42 | setValue(key: string, newValue: T): void;
43 | }
44 |
45 | function newKeyStore(): KeyStore {
46 | const stores: Record> = {};
47 |
48 | return {
49 | onChange: (key, callback) => (stores[key] ||= newStore()).onChange(callback),
50 | getValue: (key) => (stores[key] ||= newStore()).getValue(),
51 | setValue: (key, newValue) => (stores[key] ||= newStore()).setValue(newValue),
52 | };
53 | }
54 |
55 | export function createStore(): KeyStore {
56 | const getStore = (managerWindow: any) =>
57 | (managerWindow._addon_code_editor_store ||= newKeyStore());
58 | try {
59 | // This will throw in the manager if the storybook site is in an iframe.
60 | return getStore(window.parent);
61 | } catch {
62 | return getStore(window);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/declarations.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@babel/standalone';
2 |
--------------------------------------------------------------------------------
/src/evalModule.ts:
--------------------------------------------------------------------------------
1 | import { transform } from '@babel/standalone';
2 |
3 | export type EsModules = Record>;
4 |
5 | export function evalModule(
6 | moduleCode: string,
7 | availableImports: EsModules,
8 | ): Record {
9 | const { code } = transform(moduleCode, {
10 | filename: 'index.tsx',
11 | presets: ['typescript', 'react'],
12 | plugins: ['transform-modules-commonjs'],
13 | });
14 | const setExports = new Function('require', 'exports', code);
15 | const require = (moduleId: string) => {
16 | const module = availableImports[moduleId];
17 | if (!module) {
18 | throw new TypeError(`Failed to resolve module specifier "${moduleId}"`);
19 | }
20 | return module;
21 | };
22 | const exports = {};
23 |
24 | setExports(require, exports);
25 |
26 | return exports;
27 | }
28 |
--------------------------------------------------------------------------------
/src/getStaticDirs.ts:
--------------------------------------------------------------------------------
1 | import { createRequire } from 'node:module';
2 | import path from 'node:path';
3 |
4 | // Why not use `__filename` or `import.meta.filename`? Because this file gets compiled to both
5 | // CommonJS and ES module. `import.meta.filename` is a syntax error in CommonJS and `__filename`
6 | // is not available in ES modules. We can't use `import.meta.url` at all so we need a workaround.
7 | function getFileNameFromStack() {
8 | const isWindows = process.platform === 'win32';
9 | const fullPathRegex = isWindows
10 | ? /[a-zA-Z]:\\.*\\getStaticDirs\.[cm]?js/
11 | : /\/.*\/getStaticDirs\.[cm]?js/;
12 | const match = fullPathRegex.exec(new Error().stack || '');
13 | if (!match) {
14 | throw new Error('Could not get the file path of storybook-addon-code-editor/getStaticDirs');
15 | }
16 | return match[0];
17 | }
18 |
19 | const filename = typeof __filename === 'string' ? __filename : getFileNameFromStack();
20 |
21 | function resolve(reqFn: NodeRequire, packageName: string) {
22 | try {
23 | return reqFn.resolve(`${packageName}/package.json`);
24 | } catch (err) {
25 | return reqFn.resolve(packageName);
26 | }
27 | }
28 |
29 | function resolvePackagePath(reqFn: NodeRequire, packageName: string) {
30 | let error: Error | undefined;
31 | let result: string | undefined;
32 | try {
33 | const packageEntryFile = resolve(reqFn, packageName);
34 | const namePosition = packageEntryFile.indexOf(`${path.sep}${packageName}${path.sep}`);
35 | if (namePosition === -1) {
36 | error = new Error(
37 | `Cannot resolve package path for: '${packageName}'.\nEntry file: ${packageEntryFile}`,
38 | );
39 | } else {
40 | result = `${packageEntryFile.slice(0, namePosition)}${path.sep}${packageName}${path.sep}`;
41 | }
42 | } catch (err: any) {
43 | // Sometimes the require function can't find the entry file but knows the path.
44 | result =
45 | /Source path: (.+)/.exec(err?.message)?.[1] ||
46 | /main defined in (.+?)[\\/]package\.json/.exec(err?.message)?.[1];
47 | if (!result) {
48 | error = err;
49 | }
50 | }
51 | if (result) {
52 | return result;
53 | }
54 | throw error;
55 | }
56 |
57 | export function getExtraStaticDir(specifier: string, relativeToFile = filename) {
58 | const specifierParts = specifier.split('/');
59 | const isScopedPackage = specifier.startsWith('@') && !!specifierParts[1];
60 | const pathParts = isScopedPackage ? specifierParts.slice(2) : specifierParts.slice(1);
61 | const packageName = isScopedPackage
62 | ? `${specifierParts[0]}/${specifierParts[1]}`
63 | : specifierParts[0];
64 | const require = createRequire(relativeToFile);
65 | const packageDir = resolvePackagePath(require, packageName);
66 |
67 | return {
68 | from: path.join(packageDir, ...pathParts),
69 | to: specifier,
70 | };
71 | }
72 |
73 | function tryGetStaticDir(packageName: string, relativeToFile?: string) {
74 | try {
75 | return getExtraStaticDir(packageName, relativeToFile);
76 | } catch (err) {}
77 | }
78 |
79 | export function getCodeEditorStaticDirs(relativeToFile?: string) {
80 | const result = [getExtraStaticDir('monaco-editor/min', filename)];
81 | const reactTypesDir = tryGetStaticDir('@types/react', relativeToFile);
82 | if (reactTypesDir) {
83 | result.push(reactTypesDir);
84 | }
85 | return result;
86 | }
87 |
--------------------------------------------------------------------------------
/src/index.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import * as React from 'react';
3 | import { afterEach, describe, expect, test } from 'vitest';
4 | import { createStore } from './createStore';
5 | import { getCodeEditorStaticDirs, getExtraStaticDir } from './getStaticDirs';
6 | import { makeLiveEditStory, Playground, setupMonaco, type StoryState } from './index';
7 |
8 | type StorybookStory = {
9 | render: (...args: any[]) => React.ReactNode;
10 | parameters: {
11 | liveCodeEditor: {
12 | disable: boolean;
13 | id: string;
14 | };
15 | };
16 | [key: string]: unknown;
17 | };
18 |
19 | const originalConsoleError = console.error;
20 |
21 | afterEach(() => {
22 | // Some tests silence console.error to quiet logs.
23 | console.error = originalConsoleError;
24 | document.body.innerHTML = '';
25 | });
26 |
27 | describe('makeLiveEditStory', () => {
28 | test('is a function', () => {
29 | expect(typeof makeLiveEditStory).toBe('function');
30 | });
31 |
32 | test('renders error', async () => {
33 | const Story = {} as StorybookStory;
34 | makeLiveEditStory(Story, { code: '' });
35 |
36 | render(Story.render());
37 |
38 | await screen.findByText('TypeError: Default export is not a React component');
39 | });
40 |
41 | test("renders eval'd code", async () => {
42 | const Story = {} as StorybookStory;
43 | makeLiveEditStory(Story, { code: 'export default () => Hello
' });
44 |
45 | render(Story.render());
46 |
47 | await screen.findByText('Hello');
48 | });
49 |
50 | test('allows import React', async () => {
51 | const Story = {} as StorybookStory;
52 | makeLiveEditStory(Story, {
53 | code: `import React from 'react';
54 | export default () => Hello
;`,
55 | });
56 |
57 | render(Story.render());
58 |
59 | await screen.findByText('Hello');
60 | });
61 |
62 | test('allows import * as React', async () => {
63 | const Story = {} as StorybookStory;
64 | makeLiveEditStory(Story, {
65 | code: `import * as React from 'react';
66 | export default () => Hello
;`,
67 | });
68 |
69 | render(Story.render());
70 |
71 | await screen.findByText('Hello');
72 | });
73 |
74 | test("allows import { named } from 'react'", async () => {
75 | const Story = {} as StorybookStory;
76 | makeLiveEditStory(Story, {
77 | code: `import { useState } from 'react';
78 | export default () => Hello
;`,
79 | });
80 |
81 | render(Story.render());
82 |
83 | await screen.findByText('Hello');
84 | });
85 |
86 | test("allows import React, { named } from 'react'", async () => {
87 | const Story = {} as StorybookStory;
88 | makeLiveEditStory(Story, {
89 | code: `import React, { useState } from 'react';
90 | export default () => Hello
;`,
91 | });
92 |
93 | render(Story.render());
94 |
95 | await screen.findByText('Hello');
96 | });
97 |
98 | test('adds available imports', async () => {
99 | const Story = {} as StorybookStory;
100 | makeLiveEditStory(Story, {
101 | availableImports: { a: { b: 'c' } },
102 | code: `import { b } from 'a';
103 | export default () => {b}
;`,
104 | });
105 |
106 | render(Story.render());
107 |
108 | await screen.findByText('c');
109 | });
110 |
111 | test('passes props to evaluated component', async () => {
112 | const Story = {} as StorybookStory;
113 | makeLiveEditStory(Story, { code: 'export default (props) => {props.a}
;' });
114 |
115 | render(Story.render({ a: 'b' }));
116 |
117 | await screen.findByText('b');
118 | });
119 |
120 | test('recovers from syntax errors', async () => {
121 | const Story = {} as StorybookStory;
122 | makeLiveEditStory(Story, { code: '] this is not valid code [' });
123 |
124 | render(Story.render());
125 |
126 | await screen.findByText('SyntaxError', { exact: false });
127 |
128 | createStore().setValue(Story.parameters.liveCodeEditor.id, {
129 | code: 'export default () => Hello
',
130 | });
131 |
132 | await screen.findByText('Hello');
133 | });
134 |
135 | test('recovers from runtime errors', async () => {
136 | const Story = {} as StorybookStory;
137 | makeLiveEditStory(Story, { code: 'window.thisIsNot.defined' });
138 |
139 | render(Story.render());
140 |
141 | await screen.findByText("TypeError: Cannot read properties of undefined (reading 'defined')");
142 |
143 | createStore().setValue(Story.parameters.liveCodeEditor.id, {
144 | code: 'export default () => Hello
',
145 | });
146 |
147 | await screen.findByText('Hello');
148 | });
149 |
150 | test('recovers from runtime errors in the default export function', async () => {
151 | // React's error boundaries use console.error and make this test noisy.
152 | console.error = () => {};
153 |
154 | const Story = {} as StorybookStory;
155 | makeLiveEditStory(Story, {
156 | code: 'export default () => {window.thisIsNot.defined}
',
157 | });
158 |
159 | render(Story.render());
160 |
161 | await screen.findByText("TypeError: Cannot read properties of undefined (reading 'defined')");
162 |
163 | createStore().setValue(Story.parameters.liveCodeEditor.id, {
164 | code: 'export default () => Hello
',
165 | });
166 |
167 | await screen.findByText('Hello');
168 | });
169 | });
170 |
171 | describe('Playground', () => {
172 | test('is a function', () => {
173 | expect(typeof Playground).toBe('function');
174 | });
175 | });
176 |
177 | describe('setupMonaco', () => {
178 | test('is a function', () => {
179 | expect(typeof setupMonaco).toBe('function');
180 | });
181 | });
182 |
183 | describe('getCodeEditorStaticDirs', () => {
184 | test('is a function', () => {
185 | expect(typeof getCodeEditorStaticDirs).toBe('function');
186 | });
187 |
188 | test('returns an array of objects', () => {
189 | const result = getCodeEditorStaticDirs();
190 | expect(Array.isArray(result)).toBe(true);
191 | expect(typeof result[0].to).toBe('string');
192 | expect(typeof result[0].from).toBe('string');
193 | });
194 | });
195 |
196 | describe('getExtraStaticDir', () => {
197 | test('is a function', () => {
198 | expect(typeof getExtraStaticDir).toBe('function');
199 | });
200 |
201 | test('returns an object', () => {
202 | const result = getExtraStaticDir('typescript');
203 | expect(typeof result.to).toBe('string');
204 | expect(typeof result.from).toBe('string');
205 | });
206 | });
207 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createStore } from './createStore';
3 | import Editor, { EditorOptions } from './Editor/Editor';
4 | import ErrorBoundary from './ErrorBoundary';
5 | import Preview from './Preview';
6 | export { setupMonaco } from './Editor/setupMonaco';
7 |
8 | export interface StoryState {
9 | code: string;
10 | availableImports?: Record>;
11 | modifyEditor?: React.ComponentProps['modifyEditor'];
12 | defaultEditorOptions?: EditorOptions;
13 | }
14 |
15 | const store = createStore();
16 | const hasReactRegex = /import\s+(\*\s+as\s+)?React[,\s]/;
17 | const noop = () => {};
18 |
19 | function LivePreview({ storyId, storyArgs }: { storyId: string; storyArgs?: any }) {
20 | const [state, setState] = React.useState(store.getValue(storyId));
21 | const errorBoundaryResetRef = React.useRef(noop);
22 | const fullCode = hasReactRegex.test(state!.code)
23 | ? state!.code
24 | : "import * as React from 'react';" + state!.code;
25 |
26 | React.useEffect(() => {
27 | return store.onChange(storyId, (newState) => {
28 | setState(newState);
29 | errorBoundaryResetRef.current();
30 | });
31 | }, [storyId]);
32 |
33 | return (
34 |
35 |
40 |
41 | );
42 | }
43 |
44 | type AnyFn = (...args: any[]) => unknown;
45 |
46 | // Only define the types from Storybook that are used in makeLiveEditStory.
47 | // This allows us to support multiple versions of Storybook.
48 | type MinimalStoryObj = {
49 | tags?: string[];
50 | parameters?: {
51 | liveCodeEditor?: {
52 | disable: boolean;
53 | id: string;
54 | };
55 | docs?: {
56 | source?: Record;
57 | [k: string]: any;
58 | };
59 | [k: string]: any;
60 | };
61 | render?: AnyFn;
62 | [k: string]: any;
63 | };
64 |
65 | // A story can be a function or an object.
66 | type MinimalStory = MinimalStoryObj | (AnyFn & MinimalStoryObj);
67 |
68 | /**
69 | * Returns a story with live editing capabilities.
70 | *
71 | * @deprecated Use the {@link makeLiveEditStory} function instead.
72 | */
73 | export function createLiveEditStory({
74 | code,
75 | availableImports,
76 | modifyEditor,
77 | defaultEditorOptions,
78 | ...storyOptions
79 | }: StoryState & T) {
80 | const id = `id_${Math.random()}`;
81 |
82 | store.setValue(id, { code, availableImports, modifyEditor, defaultEditorOptions });
83 |
84 | return {
85 | ...storyOptions,
86 | parameters: {
87 | ...storyOptions.parameters,
88 | liveCodeEditor: { disable: false, id },
89 | docs: {
90 | ...storyOptions.parameters?.docs,
91 | source: {
92 | ...storyOptions.parameters?.docs?.source,
93 | transform: (code: string) => store.getValue(id)?.code ?? code,
94 | },
95 | },
96 | },
97 | render: (props: any) => ,
98 | } as unknown as T;
99 | }
100 |
101 | /**
102 | * Modifies a story to include a live code editor addon panel.
103 | */
104 | export function makeLiveEditStory(
105 | story: T,
106 | { code, availableImports, modifyEditor, defaultEditorOptions }: StoryState,
107 | ): void {
108 | const id = `id_${Math.random()}`;
109 |
110 | store.setValue(id, { code, availableImports, modifyEditor, defaultEditorOptions });
111 |
112 | story.parameters = {
113 | ...story.parameters,
114 | liveCodeEditor: { disable: false, id },
115 | docs: {
116 | ...story.parameters?.docs,
117 | source: {
118 | ...story.parameters?.docs?.source,
119 | transform: (code: string) => store.getValue(id)?.code ?? code,
120 | },
121 | },
122 | };
123 |
124 | story.render = (props: any) => ;
125 | }
126 |
127 | const savedCode: Record = {};
128 |
129 | /**
130 | * React component containing a live code editor and preview.
131 | */
132 | export function Playground({
133 | availableImports,
134 | code,
135 | height = '200px',
136 | id,
137 | Container,
138 | ...editorProps
139 | }: Partial & {
140 | height?: string;
141 | id?: string;
142 | Container?: React.ComponentType<{ editor: React.ReactNode; preview: React.ReactNode }>;
143 | }) {
144 | let initialCode = code ?? '';
145 | if (id !== undefined) {
146 | savedCode[id] ??= initialCode;
147 | initialCode = savedCode[id];
148 | }
149 | const [currentCode, setCurrentCode] = React.useState(initialCode);
150 | const errorBoundaryResetRef = React.useRef(noop);
151 | const fullCode = hasReactRegex.test(currentCode)
152 | ? currentCode
153 | : "import * as React from 'react';" + currentCode;
154 |
155 | const editor = (
156 | {
159 | if (id !== undefined) {
160 | savedCode[id] = newCode;
161 | }
162 | setCurrentCode(newCode);
163 | errorBoundaryResetRef.current();
164 | }}
165 | value={currentCode}
166 | />
167 | );
168 |
169 | const preview = (
170 |
171 |
172 |
173 | );
174 |
175 | return Container ? (
176 |
177 | ) : (
178 |
179 |
180 | {preview}
181 |
182 |
183 | {editor}
184 |
185 |
186 | );
187 | }
188 |
--------------------------------------------------------------------------------
/src/manager.tsx:
--------------------------------------------------------------------------------
1 | import { addons, types } from '@storybook/manager-api';
2 | import { AddonPanel } from '@storybook/components';
3 | import * as React from 'react';
4 | import { addonId, panelId, paramId } from './constants';
5 | import { createStore } from './createStore';
6 | import Editor from './Editor/Editor';
7 | import type { StoryState } from './index';
8 |
9 | const store = createStore();
10 |
11 | addons.register(addonId, (api) => {
12 | const getCodeEditorStoryId = (): string | undefined =>
13 | (api.getCurrentStoryData()?.parameters as any)?.liveCodeEditor?.id;
14 |
15 | addons.add(panelId, {
16 | id: addonId,
17 | title: 'Live code editor',
18 | type: types.PANEL,
19 | disabled: () => !getCodeEditorStoryId(),
20 | render({ active }) {
21 | const storyId = getCodeEditorStoryId();
22 |
23 | if (!active || !storyId) {
24 | return null;
25 | }
26 |
27 | const storyState = store.getValue(storyId)!;
28 |
29 | return (
30 |
31 | {
34 | store.setValue(storyId, { ...storyState, code: newCode });
35 | }}
36 | value={storyState.code}
37 | parentSize="100%"
38 | />
39 |
40 | );
41 | },
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tsconfig-cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "moduleResolution": "Node10"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "declaration": true,
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "isolatedModules": true,
8 | "jsx": "react",
9 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
10 | "module": "Preserve",
11 | "moduleResolution": "Bundler",
12 | "noEmitOnError": true,
13 | "resolveJsonModule": true,
14 | "skipLibCheck": true,
15 | "strict": true,
16 | "target": "ES2023"
17 | },
18 | "include": ["src"],
19 | "exclude": ["src/**/*.test.*"]
20 | }
21 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | environment: 'happy-dom',
6 | },
7 | })
8 |
--------------------------------------------------------------------------------