@tpitre/story-ui 2.8.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,8 @@
1
+ declare global {
2
+ interface Window {
3
+ __STORY_UI_GENERATED_CODE__?: Record<string, string>;
4
+ }
5
+ }
1
6
  declare function StoryUIPanel(): import("react/jsx-runtime").JSX.Element;
2
7
  export default StoryUIPanel;
3
8
  export { StoryUIPanel };
@@ -1 +1 @@
1
- {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AA85CA,iBAAS,YAAY,4CA8sCpB;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAsSA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,2BAA2B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACtD;CACF;AA6oCD,iBAAS,YAAY,4CA8sCpB;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -121,12 +121,27 @@ const titleToStoryPath = (title) => {
121
121
  const kebabTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
122
122
  return `generated-${kebabTitle}--default`;
123
123
  };
124
+ // Helper to store generated code for the Source Code panel to display
125
+ const storeGeneratedCode = (title, code) => {
126
+ const storyPath = titleToStoryPath(title);
127
+ const topWindow = window.top || window;
128
+ // Store code in the top window so it's accessible from manager frame
129
+ if (!topWindow.__STORY_UI_GENERATED_CODE__) {
130
+ topWindow.__STORY_UI_GENERATED_CODE__ = {};
131
+ }
132
+ topWindow.__STORY_UI_GENERATED_CODE__[storyPath] = code;
133
+ console.log(`[Story UI] Stored code for story "${storyPath}" in window cache`);
134
+ };
124
135
  // Helper to navigate to a newly created story after generation completes
125
136
  // In dev mode with HMR, this prevents the "Couldn't find story after HMR" error
126
137
  // In all modes, this provides a better UX by auto-navigating to the new story
127
- const navigateToNewStory = (title, delayMs = 4000) => {
138
+ const navigateToNewStory = (title, code, delayMs = 1500) => {
128
139
  const storyPath = titleToStoryPath(title);
129
140
  console.log(`[Story UI] Will navigate to story "${storyPath}" in ${delayMs}ms...`);
141
+ // Store the code for the Source Code panel if provided
142
+ if (code) {
143
+ storeGeneratedCode(title, code);
144
+ }
130
145
  setTimeout(() => {
131
146
  // Navigate the TOP window (parent Storybook UI), not the iframe
132
147
  // The Story UI panel runs inside an iframe, so we need window.top to escape it
@@ -1438,7 +1453,7 @@ function StoryUIPanel() {
1438
1453
  // Auto-navigate to the newly created story after HMR processes the file
1439
1454
  // This prevents the "Couldn't find story after HMR" error by refreshing
1440
1455
  // after the file system has been updated and HMR has processed the change
1441
- navigateToNewStory(chatTitle);
1456
+ navigateToNewStory(chatTitle, completion.code);
1442
1457
  }
1443
1458
  }, [activeChatId, activeTitle, conversation.length]);
1444
1459
  const handleSend = async (e) => {
@@ -1631,7 +1646,7 @@ function StoryUIPanel() {
1631
1646
  saveChats(chats);
1632
1647
  setRecentChats(chats);
1633
1648
  // Auto-navigate to the newly created story
1634
- navigateToNewStory(chatTitle);
1649
+ navigateToNewStory(chatTitle, data.code);
1635
1650
  }
1636
1651
  }
1637
1652
  catch (fallbackErr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "2.8.1",
3
+ "version": "3.1.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -291,13 +291,38 @@ const titleToStoryPath = (title: string): string => {
291
291
  return `generated-${kebabTitle}--default`;
292
292
  };
293
293
 
294
+ // Extend window to include our code cache for the Source Code panel
295
+ declare global {
296
+ interface Window {
297
+ __STORY_UI_GENERATED_CODE__?: Record<string, string>;
298
+ }
299
+ }
300
+
301
+ // Helper to store generated code for the Source Code panel to display
302
+ const storeGeneratedCode = (title: string, code: string) => {
303
+ const storyPath = titleToStoryPath(title);
304
+ const topWindow = window.top || window;
305
+
306
+ // Store code in the top window so it's accessible from manager frame
307
+ if (!topWindow.__STORY_UI_GENERATED_CODE__) {
308
+ topWindow.__STORY_UI_GENERATED_CODE__ = {};
309
+ }
310
+ topWindow.__STORY_UI_GENERATED_CODE__[storyPath] = code;
311
+ console.log(`[Story UI] Stored code for story "${storyPath}" in window cache`);
312
+ };
313
+
294
314
  // Helper to navigate to a newly created story after generation completes
295
315
  // In dev mode with HMR, this prevents the "Couldn't find story after HMR" error
296
316
  // In all modes, this provides a better UX by auto-navigating to the new story
297
- const navigateToNewStory = (title: string, delayMs: number = 4000) => {
317
+ const navigateToNewStory = (title: string, code?: string, delayMs: number = 1500) => {
298
318
  const storyPath = titleToStoryPath(title);
299
319
  console.log(`[Story UI] Will navigate to story "${storyPath}" in ${delayMs}ms...`);
300
320
 
321
+ // Store the code for the Source Code panel if provided
322
+ if (code) {
323
+ storeGeneratedCode(title, code);
324
+ }
325
+
301
326
  setTimeout(() => {
302
327
  // Navigate the TOP window (parent Storybook UI), not the iframe
303
328
  // The Story UI panel runs inside an iframe, so we need window.top to escape it
@@ -1927,7 +1952,7 @@ function StoryUIPanel() {
1927
1952
  // Auto-navigate to the newly created story after HMR processes the file
1928
1953
  // This prevents the "Couldn't find story after HMR" error by refreshing
1929
1954
  // after the file system has been updated and HMR has processed the change
1930
- navigateToNewStory(chatTitle);
1955
+ navigateToNewStory(chatTitle, completion.code);
1931
1956
  }
1932
1957
  }, [activeChatId, activeTitle, conversation.length]);
1933
1958
 
@@ -2137,7 +2162,7 @@ function StoryUIPanel() {
2137
2162
  setRecentChats(chats);
2138
2163
 
2139
2164
  // Auto-navigate to the newly created story
2140
- navigateToNewStory(chatTitle);
2165
+ navigateToNewStory(chatTitle, data.code);
2141
2166
  }
2142
2167
  } catch (fallbackErr: unknown) {
2143
2168
  const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
@@ -1,264 +1,495 @@
1
1
  /**
2
2
  * Story UI Storybook Manager Addon
3
3
  *
4
- * This addon integrates with Storybook's sidebar to show generated stories.
5
- * In Edge mode, it fetches stories from the Edge Worker and adds them to the sidebar.
4
+ * This addon adds a "Source Code" panel to Storybook that displays
5
+ * the source code of the currently viewed story with syntax highlighting.
6
+ *
7
+ * Works in both local development and production deployments.
6
8
  */
7
9
 
8
- import React, { useEffect, useState, useCallback } from 'react';
9
- import { addons, types } from '@storybook/manager-api';
10
- import { useStorybookApi, useChannel } from '@storybook/manager-api';
11
- import { IconButton } from '@storybook/components';
12
- import { styled } from '@storybook/theming';
10
+ import { addons, types, useStorybookApi, useStorybookState } from 'storybook/manager-api';
11
+ import React, { useEffect, useState, useCallback, useMemo } from 'react';
13
12
 
14
13
  // Addon identifier
15
14
  const ADDON_ID = 'story-ui';
16
- const PANEL_ID = `${ADDON_ID}/generated-stories`;
17
- const TOOL_ID = `${ADDON_ID}/tool`;
18
-
19
- // Event channels for communication between panel and manager
20
- export const EVENTS = {
21
- STORY_GENERATED: `${ADDON_ID}/story-generated`,
22
- REFRESH_STORIES: `${ADDON_ID}/refresh-stories`,
23
- SELECT_GENERATED_STORY: `${ADDON_ID}/select-generated-story`,
15
+ const PANEL_ID = `${ADDON_ID}/source-code`;
16
+
17
+ // Event channel for receiving generated code from StoryUIPanel
18
+ const EVENTS = {
19
+ CODE_GENERATED: `${ADDON_ID}/code-generated`,
20
+ STORY_SELECTED: `${ADDON_ID}/story-selected`,
24
21
  };
25
22
 
26
- // Types
27
- interface GeneratedStory {
28
- id: string;
29
- title: string;
30
- createdAt: number;
31
- framework?: string;
23
+ // Extend Window to include generated stories cache
24
+ declare global {
25
+ interface Window {
26
+ __STORY_UI_GENERATED_CODE__?: Record<string, string>;
27
+ }
32
28
  }
33
29
 
34
- // Styled components
35
- const SidebarContainer = styled.div`
36
- padding: 10px;
37
- `;
38
-
39
- const SectionTitle = styled.div`
40
- font-size: 11px;
41
- font-weight: 700;
42
- color: ${({ theme }) => theme.color.mediumdark};
43
- text-transform: uppercase;
44
- letter-spacing: 0.35em;
45
- padding: 10px 0;
46
- border-bottom: 1px solid ${({ theme }) => theme.appBorderColor};
47
- margin-bottom: 8px;
48
- display: flex;
49
- justify-content: space-between;
50
- align-items: center;
51
- `;
52
-
53
- const StoryList = styled.ul`
54
- list-style: none;
55
- padding: 0;
56
- margin: 0;
57
- `;
58
-
59
- const StoryItem = styled.li<{ isActive?: boolean }>`
60
- padding: 8px 12px;
61
- cursor: pointer;
62
- border-radius: 4px;
63
- font-size: 13px;
64
- color: ${({ theme, isActive }) => isActive ? theme.color.secondary : theme.color.defaultText};
65
- background: ${({ theme, isActive }) => isActive ? theme.background.hoverable : 'transparent'};
66
-
67
- &:hover {
68
- background: ${({ theme }) => theme.background.hoverable};
30
+ /**
31
+ * Extract clean component usage JSX from a full Storybook story file.
32
+ *
33
+ * Transforms:
34
+ * import { Button } from '@mantine/core';
35
+ * export default { title: 'Generated/Button' };
36
+ * export const Default: Story = { render: () => <Button>Click</Button> };
37
+ *
38
+ * Into:
39
+ * <Button>Click</Button>
40
+ */
41
+ const extractUsageCode = (fullStoryCode: string): string => {
42
+ // Try to extract JSX from render function: render: () => (<JSX>) or render: () => <JSX>
43
+ // Pattern 1: render: () => (\n <Component...>\n)
44
+ const renderWithParensMatch = fullStoryCode.match(/render:\s*\([^)]*\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*[,}]/);
45
+ if (renderWithParensMatch) {
46
+ const jsx = renderWithParensMatch[1].trim();
47
+ // Clean up any trailing commas or extra whitespace
48
+ return jsx.replace(/,\s*$/, '').trim();
49
+ }
50
+
51
+ // Pattern 2: render: () => <Component...> (no parentheses, single line)
52
+ const renderNoParensMatch = fullStoryCode.match(/render:\s*\([^)]*\)\s*=>\s*(<[A-Z][^,}]*(?:\/>|<\/[A-Za-z.]+>))/s);
53
+ if (renderNoParensMatch) {
54
+ return renderNoParensMatch[1].trim();
55
+ }
56
+
57
+ // Pattern 3: Arrow function story: () => (<JSX>)
58
+ const arrowWithParensMatch = fullStoryCode.match(/export\s+const\s+\w+\s*=\s*\(\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*;/);
59
+ if (arrowWithParensMatch) {
60
+ const jsx = arrowWithParensMatch[1].trim();
61
+ return jsx.replace(/,\s*$/, '').trim();
69
62
  }
70
63
 
71
- display: flex;
72
- align-items: center;
73
- gap: 8px;
74
- `;
75
-
76
- const StoryIcon = styled.span`
77
- font-size: 14px;
78
- `;
79
-
80
- const EmptyState = styled.div`
81
- padding: 20px;
82
- text-align: center;
83
- color: ${({ theme }) => theme.color.mediumdark};
84
- font-size: 12px;
85
- `;
86
-
87
- const RefreshButton = styled.button`
88
- background: none;
89
- border: none;
90
- cursor: pointer;
91
- padding: 4px;
92
- color: ${({ theme }) => theme.color.mediumdark};
93
- font-size: 14px;
94
-
95
- &:hover {
96
- color: ${({ theme }) => theme.color.secondary};
64
+ // Pattern 4: Arrow function story: () => <Component...>
65
+ const arrowNoParensMatch = fullStoryCode.match(/export\s+const\s+\w+\s*=\s*\(\)\s*=>\s*(<[A-Z][^;]*(?:\/>|<\/[A-Za-z.]+>))\s*;/s);
66
+ if (arrowNoParensMatch) {
67
+ return arrowNoParensMatch[1].trim();
97
68
  }
98
- `;
69
+
70
+ // Pattern 5: Look for args-based stories with component prop spreading
71
+ // e.g., args: { children: 'Click me', color: 'blue' }
72
+ const argsMatch = fullStoryCode.match(/args:\s*(\{[\s\S]*?\})\s*[,}]/);
73
+ if (argsMatch) {
74
+ // Try to find the component from the meta
75
+ const componentMatch = fullStoryCode.match(/component:\s*([A-Z][A-Za-z0-9]*)/);
76
+ if (componentMatch) {
77
+ const componentName = componentMatch[1];
78
+ try {
79
+ // Parse the args to generate JSX
80
+ const argsStr = argsMatch[1];
81
+ // Extract children if present
82
+ const childrenMatch = argsStr.match(/children:\s*['"`]([^'"`]+)['"`]/);
83
+ const children = childrenMatch ? childrenMatch[1] : '';
84
+
85
+ // Extract other props (simplified)
86
+ const propsStr = argsStr
87
+ .replace(/children:\s*['"`][^'"`]*['"`],?/, '') // Remove children
88
+ .replace(/^\{|\}$/g, '') // Remove braces
89
+ .trim();
90
+
91
+ if (children) {
92
+ if (propsStr) {
93
+ return `<${componentName} ${propsStr.replace(/,\s*$/, '')}>${children}</${componentName}>`;
94
+ }
95
+ return `<${componentName}>${children}</${componentName}>`;
96
+ } else if (propsStr) {
97
+ return `<${componentName} ${propsStr.replace(/,\s*$/, '')} />`;
98
+ }
99
+ return `<${componentName} />`;
100
+ } catch {
101
+ // Fall through to return full code
102
+ }
103
+ }
104
+ }
105
+
106
+ // Pattern 6: Look for any JSX block starting with < and ending with /> or </Component>
107
+ // This is a fallback for any JSX we can find
108
+ const jsxBlockMatch = fullStoryCode.match(/(<[A-Z][a-zA-Z0-9.]*[\s\S]*?(?:\/>|<\/[A-Za-z.]+>))/);
109
+ if (jsxBlockMatch) {
110
+ // Don't return if it looks like an import or type definition
111
+ const match = jsxBlockMatch[1];
112
+ if (!match.includes('import') && !match.includes('Meta<') && !match.includes('StoryObj<')) {
113
+ return match.trim();
114
+ }
115
+ }
116
+
117
+ // If no patterns matched, return the original code
118
+ // (better than showing nothing)
119
+ return fullStoryCode;
120
+ };
99
121
 
100
122
  /**
101
- * Get the Edge URL from environment variable.
102
- * This must be configured via VITE_STORY_UI_EDGE_URL environment variable.
103
- * No hardcoded URLs - each deployment must configure their own backend.
123
+ * Simple Prism-like syntax highlighting for JSX/TSX
124
+ * Uses inline styles for portability (no external CSS needed)
104
125
  */
105
- function getEdgeUrl(): string {
106
- // Check for Vite env variable - this is the ONLY source for the Edge URL
107
- if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_STORY_UI_EDGE_URL) {
108
- return import.meta.env.VITE_STORY_UI_EDGE_URL;
126
+ const tokenize = (code: string): Array<{ type: string; value: string }> => {
127
+ const tokens: Array<{ type: string; value: string }> = [];
128
+ let remaining = code;
129
+
130
+ const patterns: Array<{ type: string; regex: RegExp }> = [
131
+ // Comments
132
+ { type: 'comment', regex: /^(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/ },
133
+ // Strings (double, single, template)
134
+ { type: 'string', regex: /^("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'|`[^`\\]*(?:\\.[^`\\]*)*`)/ },
135
+ // JSX tags
136
+ { type: 'tag', regex: /^(<\/?[A-Z][a-zA-Z0-9.]*|<\/?[a-z][a-z0-9-]*)/ },
137
+ // Closing tag bracket
138
+ { type: 'punctuation', regex: /^(\/>|>)/ },
139
+ // Keywords
140
+ { type: 'keyword', regex: /^(const|let|var|function|return|export|default|import|from|if|else|for|while|class|extends|new|this|typeof|instanceof|async|await|try|catch|throw|finally)\b/ },
141
+ // Booleans and null
142
+ { type: 'boolean', regex: /^(true|false|null|undefined)\b/ },
143
+ // Numbers
144
+ { type: 'number', regex: /^-?\d+\.?\d*(e[+-]?\d+)?/ },
145
+ // Props/attributes (word followed by =)
146
+ { type: 'attr-name', regex: /^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?==)/ },
147
+ // Function names
148
+ { type: 'function', regex: /^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/ },
149
+ // Identifiers
150
+ { type: 'plain', regex: /^[a-zA-Z_$][a-zA-Z0-9_$]*/ },
151
+ // Operators
152
+ { type: 'operator', regex: /^(=>|===|!==|==|!=|<=|>=|&&|\|\||[+\-*/%=<>!&|^~?:])/ },
153
+ // Punctuation
154
+ { type: 'punctuation', regex: /^[{}[\]();,.]/ },
155
+ // Whitespace
156
+ { type: 'whitespace', regex: /^\s+/ },
157
+ ];
158
+
159
+ while (remaining.length > 0) {
160
+ let matched = false;
161
+
162
+ for (const { type, regex } of patterns) {
163
+ const match = remaining.match(regex);
164
+ if (match) {
165
+ tokens.push({ type, value: match[0] });
166
+ remaining = remaining.slice(match[0].length);
167
+ matched = true;
168
+ break;
169
+ }
170
+ }
171
+
172
+ if (!matched) {
173
+ // Unknown character, add as plain
174
+ tokens.push({ type: 'plain', value: remaining[0] });
175
+ remaining = remaining.slice(1);
176
+ }
109
177
  }
110
178
 
111
- // No fallback - environment variable must be configured
112
- return '';
113
- }
179
+ return tokens;
180
+ };
114
181
 
115
182
  /**
116
- * Check if we're in Edge mode
183
+ * Color scheme for syntax highlighting (VS Code-like dark theme)
117
184
  */
118
- function isEdgeMode(): boolean {
119
- const edgeUrl = getEdgeUrl();
120
- if (edgeUrl) return true;
121
-
122
- // Check if we're on a production domain
123
- if (typeof window !== 'undefined') {
124
- const hostname = window.location.hostname;
125
- return hostname !== 'localhost' && hostname !== '127.0.0.1';
126
- }
185
+ const tokenColors: Record<string, React.CSSProperties> = {
186
+ comment: { color: '#6A9955', fontStyle: 'italic' },
187
+ string: { color: '#CE9178' },
188
+ tag: { color: '#569CD6' },
189
+ keyword: { color: '#C586C0' },
190
+ boolean: { color: '#569CD6' },
191
+ number: { color: '#B5CEA8' },
192
+ 'attr-name': { color: '#9CDCFE' },
193
+ function: { color: '#DCDCAA' },
194
+ operator: { color: '#D4D4D4' },
195
+ punctuation: { color: '#D4D4D4' },
196
+ plain: { color: '#D4D4D4' },
197
+ whitespace: {},
198
+ };
127
199
 
128
- return false;
129
- }
200
+ /**
201
+ * Syntax Highlighter Component
202
+ */
203
+ const SyntaxHighlighter: React.FC<{ code: string }> = ({ code }) => {
204
+ const tokens = useMemo(() => tokenize(code), [code]);
205
+
206
+ return (
207
+ <pre
208
+ style={{
209
+ margin: 0,
210
+ padding: '16px',
211
+ background: '#1E1E1E',
212
+ borderRadius: '4px',
213
+ overflow: 'auto',
214
+ fontSize: '13px',
215
+ lineHeight: '1.5',
216
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
217
+ }}
218
+ >
219
+ <code>
220
+ {tokens.map((token, index) => (
221
+ <span key={index} style={tokenColors[token.type] || {}}>
222
+ {token.value}
223
+ </span>
224
+ ))}
225
+ </code>
226
+ </pre>
227
+ );
228
+ };
130
229
 
131
230
  /**
132
- * Generated Stories Sidebar Component
231
+ * Inline styles for the panel
133
232
  */
134
- const GeneratedStoriesSidebar: React.FC = () => {
135
- const [stories, setStories] = useState<GeneratedStory[]>([]);
136
- const [loading, setLoading] = useState(false);
137
- const [activeStoryId, setActiveStoryId] = useState<string | null>(null);
138
-
139
- // Listen for story generation events from the panel
140
- useChannel({
141
- [EVENTS.STORY_GENERATED]: (data: GeneratedStory) => {
142
- setStories(prev => [data, ...prev.filter(s => s.id !== data.id)]);
143
- },
144
- [EVENTS.REFRESH_STORIES]: () => {
145
- fetchStories();
146
- },
147
- });
233
+ const styles = {
234
+ container: {
235
+ height: '100%',
236
+ display: 'flex',
237
+ flexDirection: 'column' as const,
238
+ background: '#1E1E1E',
239
+ color: '#D4D4D4',
240
+ },
241
+ header: {
242
+ display: 'flex',
243
+ justifyContent: 'space-between',
244
+ alignItems: 'center',
245
+ padding: '8px 12px',
246
+ borderBottom: '1px solid #3C3C3C',
247
+ background: '#252526',
248
+ },
249
+ title: {
250
+ fontSize: '12px',
251
+ fontWeight: 600,
252
+ color: '#CCCCCC',
253
+ textTransform: 'uppercase' as const,
254
+ letterSpacing: '0.5px',
255
+ },
256
+ copyButton: {
257
+ background: '#0E639C',
258
+ color: 'white',
259
+ border: 'none',
260
+ borderRadius: '4px',
261
+ padding: '4px 10px',
262
+ fontSize: '11px',
263
+ cursor: 'pointer',
264
+ fontWeight: 500,
265
+ },
266
+ codeContainer: {
267
+ flex: 1,
268
+ overflow: 'auto',
269
+ padding: '0',
270
+ },
271
+ emptyState: {
272
+ display: 'flex',
273
+ flexDirection: 'column' as const,
274
+ alignItems: 'center',
275
+ justifyContent: 'center',
276
+ height: '100%',
277
+ color: '#888',
278
+ fontSize: '13px',
279
+ textAlign: 'center' as const,
280
+ padding: '20px',
281
+ },
282
+ storyInfo: {
283
+ fontSize: '11px',
284
+ color: '#888',
285
+ marginTop: '4px',
286
+ },
287
+ };
288
+
289
+ /**
290
+ * Source Code Panel Component
291
+ */
292
+ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
293
+ const api = useStorybookApi();
294
+ const state = useStorybookState();
295
+ const [sourceCode, setSourceCode] = useState<string>('');
296
+ const [storyTitle, setStoryTitle] = useState<string>('');
297
+ const [copied, setCopied] = useState(false);
298
+ const [showFullCode, setShowFullCode] = useState(false);
299
+
300
+ // Memoize the usage code extraction
301
+ const displayCode = useMemo(() => {
302
+ if (!sourceCode) return '';
303
+ return showFullCode ? sourceCode : extractUsageCode(sourceCode);
304
+ }, [sourceCode, showFullCode]);
305
+
306
+ // Get the current story ID
307
+ const currentStoryId = state?.storyId;
308
+
309
+ // Try to get source code from the story
310
+ useEffect(() => {
311
+ if (!currentStoryId || !active) return;
312
+
313
+ // Get story data from the API
314
+ const story = api.getData(currentStoryId);
315
+
316
+ // Check if this is a generated story based on ID (works even if story doesn't exist in Storybook yet)
317
+ const isGeneratedStory = currentStoryId.includes('generated');
318
+
319
+ if (story) {
320
+ setStoryTitle(story.title || '');
148
321
 
149
- const fetchStories = useCallback(async () => {
150
- if (!isEdgeMode()) return;
322
+ // Try to get source from story parameters
323
+ const storySource = (story as any)?.parameters?.docs?.source?.code ||
324
+ (story as any)?.parameters?.storySource?.source ||
325
+ (story as any)?.parameters?.source?.code;
151
326
 
152
- setLoading(true);
153
- try {
154
- const edgeUrl = getEdgeUrl();
155
- const response = await fetch(`${edgeUrl}/story-ui/stories`);
156
- if (response.ok) {
157
- const data = await response.json();
158
- setStories(Array.isArray(data) ? data : []);
327
+ if (storySource) {
328
+ setSourceCode(storySource);
329
+ return;
159
330
  }
160
- } catch (error) {
161
- console.error('Failed to fetch generated stories:', error);
162
- } finally {
163
- setLoading(false);
331
+ } else {
332
+ // Story doesn't exist in Storybook yet, set title from story ID
333
+ setStoryTitle(currentStoryId);
164
334
  }
165
- }, []);
166
335
 
167
- useEffect(() => {
168
- fetchStories();
169
- }, [fetchStories]);
336
+ // For generated stories (whether or not they exist in Storybook), try to get from cache/localStorage
337
+ if (isGeneratedStory) {
338
+ // Try to get from window cache first
339
+ const topWindow = window.top || window;
340
+ let cachedCode = topWindow.__STORY_UI_GENERATED_CODE__?.[currentStoryId] ||
341
+ window.__STORY_UI_GENERATED_CODE__?.[currentStoryId];
342
+
343
+ // If not in memory cache, check localStorage (survives page navigation)
344
+ if (!cachedCode) {
345
+ try {
346
+ const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
347
+ cachedCode = stored[currentStoryId];
348
+ // Restore to memory cache if found
349
+ if (cachedCode) {
350
+ if (!topWindow.__STORY_UI_GENERATED_CODE__) {
351
+ topWindow.__STORY_UI_GENERATED_CODE__ = {};
352
+ }
353
+ topWindow.__STORY_UI_GENERATED_CODE__[currentStoryId] = cachedCode;
354
+ }
355
+ } catch (e) {
356
+ console.warn('[Story UI] Failed to read from localStorage:', e);
357
+ }
358
+ }
170
359
 
171
- const handleStoryClick = useCallback((story: GeneratedStory) => {
172
- setActiveStoryId(story.id);
360
+ if (cachedCode) {
361
+ setSourceCode(cachedCode);
362
+ } else {
363
+ setSourceCode('');
364
+ }
365
+ } else {
366
+ setSourceCode('');
367
+ }
368
+ }, [currentStoryId, active, api]);
173
369
 
174
- // Emit event for the Story UI panel to display the story
370
+ // Listen for code generated events from StoryUIPanel
371
+ useEffect(() => {
175
372
  const channel = addons.getChannel();
176
- channel.emit(EVENTS.SELECT_GENERATED_STORY, story);
177
- }, []);
373
+ const topWindow = window.top || window;
374
+
375
+ const handleCodeGenerated = (data: { storyId: string; code: string }) => {
376
+ // Store in window cache (both local and top window)
377
+ if (!window.__STORY_UI_GENERATED_CODE__) {
378
+ window.__STORY_UI_GENERATED_CODE__ = {};
379
+ }
380
+ window.__STORY_UI_GENERATED_CODE__[data.storyId] = data.code;
381
+
382
+ if (topWindow !== window) {
383
+ if (!topWindow.__STORY_UI_GENERATED_CODE__) {
384
+ topWindow.__STORY_UI_GENERATED_CODE__ = {};
385
+ }
386
+ topWindow.__STORY_UI_GENERATED_CODE__[data.storyId] = data.code;
387
+ }
388
+
389
+ // If this is the current story, update the display
390
+ if (data.storyId === currentStoryId) {
391
+ setSourceCode(data.code);
392
+ }
393
+ };
394
+
395
+ channel.on(EVENTS.CODE_GENERATED, handleCodeGenerated);
396
+
397
+ return () => {
398
+ channel.off(EVENTS.CODE_GENERATED, handleCodeGenerated);
399
+ };
400
+ }, [currentStoryId]);
401
+
402
+ const handleCopy = useCallback(() => {
403
+ if (displayCode) {
404
+ navigator.clipboard.writeText(displayCode).then(() => {
405
+ setCopied(true);
406
+ setTimeout(() => setCopied(false), 2000);
407
+ });
408
+ }
409
+ }, [displayCode]);
178
410
 
179
- if (!isEdgeMode()) {
411
+ if (!active) return null;
412
+
413
+ // No story selected
414
+ if (!currentStoryId) {
180
415
  return (
181
- <SidebarContainer>
182
- <EmptyState>
183
- Generated stories appear here in Edge mode
184
- </EmptyState>
185
- </SidebarContainer>
416
+ <div style={styles.container}>
417
+ <div style={styles.emptyState}>
418
+ <span>Select a story to view its source code</span>
419
+ </div>
420
+ </div>
186
421
  );
187
422
  }
188
423
 
189
- return (
190
- <SidebarContainer>
191
- <SectionTitle>
192
- Generated Stories
193
- <RefreshButton onClick={fetchStories} title="Refresh stories">
194
- {loading ? '...' : '\u21bb'}
195
- </RefreshButton>
196
- </SectionTitle>
197
-
198
- {stories.length === 0 ? (
199
- <EmptyState>
200
- {loading ? 'Loading...' : 'No generated stories yet'}
201
- </EmptyState>
202
- ) : (
203
- <StoryList>
204
- {stories.map((story) => (
205
- <StoryItem
206
- key={story.id}
207
- isActive={activeStoryId === story.id}
208
- onClick={() => handleStoryClick(story)}
209
- >
210
- <StoryIcon>&#128214;</StoryIcon>
211
- {story.title}
212
- </StoryItem>
213
- ))}
214
- </StoryList>
215
- )}
216
- </SidebarContainer>
217
- );
218
- };
219
-
220
- /**
221
- * Toolbar button to toggle generated stories panel
222
- */
223
- const GeneratedStoriesToolbar: React.FC = () => {
224
- const api = useStorybookApi();
424
+ // No source code available
425
+ if (!sourceCode) {
426
+ return (
427
+ <div style={styles.container}>
428
+ <div style={styles.header}>
429
+ <span style={styles.title}>Source Code</span>
430
+ </div>
431
+ <div style={styles.emptyState}>
432
+ <span>No source code available for this story</span>
433
+ <span style={styles.storyInfo}>
434
+ {storyTitle || currentStoryId}
435
+ </span>
436
+ <span style={{ ...styles.storyInfo, marginTop: '12px', maxWidth: '280px' }}>
437
+ Generate a story using the Story UI panel to see the code here.
438
+ </span>
439
+ </div>
440
+ </div>
441
+ );
442
+ }
225
443
 
226
- if (!isEdgeMode()) return null;
444
+ // Check if extraction was successful (displayCode is different from sourceCode)
445
+ const hasUsageCode = displayCode !== sourceCode;
227
446
 
228
447
  return (
229
- <IconButton
230
- key={TOOL_ID}
231
- title="Generated Stories"
232
- onClick={() => {
233
- api.togglePanel(true);
234
- api.setSelectedPanel(PANEL_ID);
235
- }}
236
- >
237
- <span style={{ fontSize: '14px' }}>&#10024;</span>
238
- </IconButton>
448
+ <div style={styles.container}>
449
+ <div style={styles.header}>
450
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
451
+ <span style={styles.title}>{showFullCode ? 'Full Story' : 'Usage Code'}</span>
452
+ {hasUsageCode && (
453
+ <button
454
+ style={{
455
+ background: 'transparent',
456
+ color: '#888',
457
+ border: '1px solid #555',
458
+ borderRadius: '4px',
459
+ padding: '2px 8px',
460
+ fontSize: '10px',
461
+ cursor: 'pointer',
462
+ fontWeight: 400,
463
+ }}
464
+ onClick={() => setShowFullCode(!showFullCode)}
465
+ >
466
+ {showFullCode ? 'Show Usage' : 'Show Full'}
467
+ </button>
468
+ )}
469
+ </div>
470
+ <button
471
+ style={{
472
+ ...styles.copyButton,
473
+ background: copied ? '#16825D' : '#0E639C',
474
+ }}
475
+ onClick={handleCopy}
476
+ >
477
+ {copied ? 'Copied!' : 'Copy'}
478
+ </button>
479
+ </div>
480
+ <div style={styles.codeContainer}>
481
+ <SyntaxHighlighter code={displayCode} />
482
+ </div>
483
+ </div>
239
484
  );
240
485
  };
241
486
 
242
- // Register the addon
243
- addons.register(ADDON_ID, (api) => {
244
- // Only register in Edge mode
245
- if (!isEdgeMode()) return;
246
-
247
- // Register the toolbar button
248
- addons.add(TOOL_ID, {
249
- type: types.TOOL,
250
- title: 'Generated Stories',
251
- match: ({ viewMode }) => viewMode === 'story' || viewMode === 'docs',
252
- render: () => <GeneratedStoriesToolbar />,
253
- });
254
-
255
- // Register the sidebar panel
487
+ // Register the addon - always register, not just in Edge mode
488
+ addons.register(ADDON_ID, () => {
256
489
  addons.add(PANEL_ID, {
257
490
  type: types.PANEL,
258
- title: 'Generated',
491
+ title: 'Source Code',
259
492
  match: ({ viewMode }) => viewMode === 'story' || viewMode === 'docs',
260
- render: ({ active }) => active ? <GeneratedStoriesSidebar /> : null,
493
+ render: ({ active }) => <SourceCodePanel active={active} />,
261
494
  });
262
495
  });
263
-
264
- export default {};