@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"
|
|
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 =
|
|
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
|
@@ -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 =
|
|
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
|
|
5
|
-
*
|
|
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
|
|
9
|
-
import {
|
|
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}/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
*
|
|
102
|
-
*
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
179
|
+
return tokens;
|
|
180
|
+
};
|
|
114
181
|
|
|
115
182
|
/**
|
|
116
|
-
*
|
|
183
|
+
* Color scheme for syntax highlighting (VS Code-like dark theme)
|
|
117
184
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
231
|
+
* Inline styles for the panel
|
|
133
232
|
*/
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
360
|
+
if (cachedCode) {
|
|
361
|
+
setSourceCode(cachedCode);
|
|
362
|
+
} else {
|
|
363
|
+
setSourceCode('');
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
setSourceCode('');
|
|
367
|
+
}
|
|
368
|
+
}, [currentStoryId, active, api]);
|
|
173
369
|
|
|
174
|
-
|
|
370
|
+
// Listen for code generated events from StoryUIPanel
|
|
371
|
+
useEffect(() => {
|
|
175
372
|
const channel = addons.getChannel();
|
|
176
|
-
|
|
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 (!
|
|
411
|
+
if (!active) return null;
|
|
412
|
+
|
|
413
|
+
// No story selected
|
|
414
|
+
if (!currentStoryId) {
|
|
180
415
|
return (
|
|
181
|
-
<
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
</
|
|
185
|
-
</
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<
|
|
194
|
-
{
|
|
195
|
-
</
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
onClick={() => handleStoryClick(story)}
|
|
209
|
-
>
|
|
210
|
-
<StoryIcon>📖</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 (
|
|
444
|
+
// Check if extraction was successful (displayCode is different from sourceCode)
|
|
445
|
+
const hasUsageCode = displayCode !== sourceCode;
|
|
227
446
|
|
|
228
447
|
return (
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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, (
|
|
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: '
|
|
491
|
+
title: 'Source Code',
|
|
259
492
|
match: ({ viewMode }) => viewMode === 'story' || viewMode === 'docs',
|
|
260
|
-
render: ({ active }) =>
|
|
493
|
+
render: ({ active }) => <SourceCodePanel active={active} />,
|
|
261
494
|
});
|
|
262
495
|
});
|
|
263
|
-
|
|
264
|
-
export default {};
|