@tpitre/story-ui 3.6.2 → 3.7.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.
- package/README.md +36 -32
- package/dist/cli/index.js +0 -5
- package/dist/cli/setup.js +1 -1
- package/dist/mcp-server/routes/generateStory.d.ts.map +1 -1
- package/dist/mcp-server/routes/generateStory.js +142 -87
- package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -1
- package/dist/mcp-server/routes/generateStoryStream.js +149 -31
- package/dist/story-generator/dynamicPackageDiscovery.d.ts +35 -2
- package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -1
- package/dist/story-generator/dynamicPackageDiscovery.js +332 -6
- package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -1
- package/dist/story-generator/enhancedComponentDiscovery.js +149 -2
- package/dist/story-generator/framework-adapters/base-adapter.d.ts +1 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/base-adapter.js +12 -2
- package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/react-adapter.js +2 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/svelte-adapter.js +53 -7
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/vue-adapter.js +21 -1
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/web-components-adapter.js +4 -0
- package/dist/story-generator/llm-providers/openai-provider.js +2 -2
- package/dist/story-generator/promptGenerator.d.ts.map +1 -1
- package/dist/story-generator/promptGenerator.js +179 -26
- package/dist/story-generator/selfHealingLoop.d.ts +112 -0
- package/dist/story-generator/selfHealingLoop.d.ts.map +1 -0
- package/dist/story-generator/selfHealingLoop.js +202 -0
- package/dist/story-generator/validateStory.d.ts.map +1 -1
- package/dist/story-generator/validateStory.js +81 -12
- package/dist/story-ui.config.d.ts +2 -0
- package/dist/story-ui.config.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.d.ts +0 -5
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +411 -223
- package/package.json +4 -4
- package/templates/StoryUI/StoryUIPanel.mdx +84 -0
- package/templates/StoryUI/StoryUIPanel.tsx +493 -265
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +0 -18
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +0 -1
- package/dist/templates/StoryUI/StoryUIPanel.stories.js +0 -37
- package/templates/StoryUI/StoryUIPanel.stories.tsx +0 -44
- package/templates/StoryUI/manager.tsx +0 -859
|
@@ -1,859 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Story UI Storybook Manager Addon
|
|
3
|
-
*
|
|
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.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { addons, types, useStorybookApi, useStorybookState } from 'storybook/manager-api';
|
|
11
|
-
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
12
|
-
|
|
13
|
-
// Addon identifier
|
|
14
|
-
const ADDON_ID = 'story-ui';
|
|
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`,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get the API base URL for story operations.
|
|
25
|
-
* Works in both local development and production (Railway).
|
|
26
|
-
*/
|
|
27
|
-
const getApiBaseUrl = (): string => {
|
|
28
|
-
if (typeof window === 'undefined') return 'http://localhost:4001';
|
|
29
|
-
|
|
30
|
-
// Check for Railway production domain - use same-origin requests
|
|
31
|
-
const hostname = window.location.hostname;
|
|
32
|
-
if (hostname.includes('.railway.app')) {
|
|
33
|
-
return '';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Check for window overrides (local development)
|
|
37
|
-
const windowOverride = (window as any).__STORY_UI_PORT__;
|
|
38
|
-
if (windowOverride) return `http://localhost:${windowOverride}`;
|
|
39
|
-
|
|
40
|
-
const mcpOverride = (window as any).STORY_UI_MCP_PORT;
|
|
41
|
-
if (mcpOverride) return `http://localhost:${mcpOverride}`;
|
|
42
|
-
|
|
43
|
-
return 'http://localhost:4001';
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Extend Window to include generated stories cache
|
|
47
|
-
declare global {
|
|
48
|
-
interface Window {
|
|
49
|
-
__STORY_UI_GENERATED_CODE__?: Record<string, string>;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Extract clean component usage JSX from a full Storybook story file.
|
|
55
|
-
*
|
|
56
|
-
* Transforms:
|
|
57
|
-
* import { Button } from '@mantine/core';
|
|
58
|
-
* export default { title: 'Generated/Button' };
|
|
59
|
-
* export const Default: Story = { render: () => <Button>Click</Button> };
|
|
60
|
-
*
|
|
61
|
-
* Into:
|
|
62
|
-
* <Button>Click</Button>
|
|
63
|
-
*/
|
|
64
|
-
const extractUsageCode = (fullStoryCode: string, variantName?: string): string => {
|
|
65
|
-
// Helper function to convert object-style props to JSX attribute syntax
|
|
66
|
-
// e.g., "color: 'blue', variant: 'filled'" -> 'color="blue" variant="filled"'
|
|
67
|
-
const convertToJsxAttributes = (propsStr: string): string => {
|
|
68
|
-
if (!propsStr.trim()) return '';
|
|
69
|
-
|
|
70
|
-
const attributes: string[] = [];
|
|
71
|
-
// Match key: value pairs, handling strings, booleans, numbers, and expressions
|
|
72
|
-
// Pattern: key: 'value' or key: "value" or key: true/false or key: 123 or key: expression
|
|
73
|
-
const propRegex = /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|(\btrue\b|\bfalse\b)|(\d+(?:\.\d+)?)|(\{[^}]+\})|([^,}\s]+))/g;
|
|
74
|
-
|
|
75
|
-
let match;
|
|
76
|
-
while ((match = propRegex.exec(propsStr)) !== null) {
|
|
77
|
-
const key = match[1];
|
|
78
|
-
const stringValueSingle = match[2]; // 'value'
|
|
79
|
-
const stringValueDouble = match[3]; // "value"
|
|
80
|
-
const boolValue = match[4]; // true/false
|
|
81
|
-
const numValue = match[5]; // 123 or 1.5
|
|
82
|
-
const objValue = match[6]; // {expression}
|
|
83
|
-
const otherValue = match[7]; // other expressions
|
|
84
|
-
|
|
85
|
-
if (stringValueSingle !== undefined) {
|
|
86
|
-
attributes.push(`${key}="${stringValueSingle}"`);
|
|
87
|
-
} else if (stringValueDouble !== undefined) {
|
|
88
|
-
attributes.push(`${key}="${stringValueDouble}"`);
|
|
89
|
-
} else if (boolValue !== undefined) {
|
|
90
|
-
if (boolValue === 'true') {
|
|
91
|
-
attributes.push(key); // Just the prop name for true (e.g., fullWidth)
|
|
92
|
-
}
|
|
93
|
-
// Skip false values - they're the default and don't need to be shown
|
|
94
|
-
} else if (numValue !== undefined) {
|
|
95
|
-
attributes.push(`${key}={${numValue}}`);
|
|
96
|
-
} else if (objValue !== undefined) {
|
|
97
|
-
attributes.push(`${key}=${objValue}`);
|
|
98
|
-
} else if (otherValue !== undefined) {
|
|
99
|
-
attributes.push(`${key}={${otherValue}}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return attributes.join(' ');
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Helper function to generate JSX from args
|
|
107
|
-
const generateJsxFromArgs = (argsStr: string, componentName: string): string | null => {
|
|
108
|
-
try {
|
|
109
|
-
// Extract children if present
|
|
110
|
-
const childrenMatch = argsStr.match(/children:\s*['"`]([^'"`]+)['"`]/);
|
|
111
|
-
const children = childrenMatch ? childrenMatch[1] : '';
|
|
112
|
-
|
|
113
|
-
// Extract other props (remove children first)
|
|
114
|
-
const propsStr = argsStr
|
|
115
|
-
.replace(/children:\s*['"`][^'"`]*['"`],?/, '') // Remove children
|
|
116
|
-
.replace(/^\{|\}$/g, '') // Remove braces
|
|
117
|
-
.trim();
|
|
118
|
-
|
|
119
|
-
// Convert to JSX attribute syntax
|
|
120
|
-
const jsxAttributes = convertToJsxAttributes(propsStr);
|
|
121
|
-
|
|
122
|
-
if (children) {
|
|
123
|
-
if (jsxAttributes) {
|
|
124
|
-
return `<${componentName} ${jsxAttributes}>${children}</${componentName}>`;
|
|
125
|
-
}
|
|
126
|
-
return `<${componentName}>${children}</${componentName}>`;
|
|
127
|
-
} else if (jsxAttributes) {
|
|
128
|
-
return `<${componentName} ${jsxAttributes} />`;
|
|
129
|
-
}
|
|
130
|
-
return `<${componentName} />`;
|
|
131
|
-
} catch {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// Get the component name from meta
|
|
137
|
-
const componentMatch = fullStoryCode.match(/component:\s*([A-Z][A-Za-z0-9]*)/);
|
|
138
|
-
const componentName = componentMatch ? componentMatch[1] : null;
|
|
139
|
-
|
|
140
|
-
// If we have a variant name, try to find that specific variant's args or render
|
|
141
|
-
if (variantName) {
|
|
142
|
-
// Normalize variant name for matching:
|
|
143
|
-
// - "primary" -> "Primary"
|
|
144
|
-
// - "full-width" -> "FullWidth" (kebab-case to PascalCase)
|
|
145
|
-
const normalizedVariant = variantName
|
|
146
|
-
.split('-')
|
|
147
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
148
|
-
.join('');
|
|
149
|
-
|
|
150
|
-
// Pattern A: export const Primary: Story = { args: {...} }
|
|
151
|
-
// Match the specific variant's export block
|
|
152
|
-
const variantExportRegex = new RegExp(
|
|
153
|
-
`export\\s+const\\s+${normalizedVariant}\\s*(?::\\s*Story)?\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*;`,
|
|
154
|
-
'i'
|
|
155
|
-
);
|
|
156
|
-
const variantExportMatch = fullStoryCode.match(variantExportRegex);
|
|
157
|
-
|
|
158
|
-
if (variantExportMatch) {
|
|
159
|
-
const variantBlock = variantExportMatch[1];
|
|
160
|
-
|
|
161
|
-
// Try to extract render function from this variant
|
|
162
|
-
const renderWithParensMatch = variantBlock.match(/render:\s*\([^)]*\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*[,}]/);
|
|
163
|
-
if (renderWithParensMatch) {
|
|
164
|
-
return renderWithParensMatch[1].trim().replace(/,\s*$/, '');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const renderNoParensMatch = variantBlock.match(/render:\s*\([^)]*\)\s*=>\s*(<[A-Z][^,}]*(?:\/>|<\/[A-Za-z.]+>))/s);
|
|
168
|
-
if (renderNoParensMatch) {
|
|
169
|
-
return renderNoParensMatch[1].trim();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Try to extract args from this variant
|
|
173
|
-
const argsMatch = variantBlock.match(/args:\s*(\{[\s\S]*?\})\s*[,}]/);
|
|
174
|
-
if (argsMatch && componentName) {
|
|
175
|
-
const result = generateJsxFromArgs(argsMatch[1], componentName);
|
|
176
|
-
if (result) return result;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Pattern B: Arrow function variant: export const Primary = () => <Component...>
|
|
181
|
-
const arrowVariantRegex = new RegExp(
|
|
182
|
-
`export\\s+const\\s+${normalizedVariant}\\s*=\\s*\\(\\)\\s*=>\\s*\\(\\s*([\\s\\S]*?)\\s*\\)\\s*;`,
|
|
183
|
-
'i'
|
|
184
|
-
);
|
|
185
|
-
const arrowVariantMatch = fullStoryCode.match(arrowVariantRegex);
|
|
186
|
-
if (arrowVariantMatch) {
|
|
187
|
-
return arrowVariantMatch[1].trim().replace(/,\s*$/, '');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Pattern C: Arrow function without parens: export const Primary = () => <Component...>;
|
|
191
|
-
const arrowNoParensRegex = new RegExp(
|
|
192
|
-
`export\\s+const\\s+${normalizedVariant}\\s*=\\s*\\(\\)\\s*=>\\s*(<[A-Z][^;]*(?:\\/>|<\\/[A-Za-z.]+>))\\s*;`,
|
|
193
|
-
'is'
|
|
194
|
-
);
|
|
195
|
-
const arrowNoParensMatch = fullStoryCode.match(arrowNoParensRegex);
|
|
196
|
-
if (arrowNoParensMatch) {
|
|
197
|
-
return arrowNoParensMatch[1].trim();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Fallback: Try generic patterns (for Default or when variant not specified)
|
|
202
|
-
|
|
203
|
-
// Try to extract JSX from render function: render: () => (<JSX>) or render: () => <JSX>
|
|
204
|
-
// Pattern 1: render: () => (\n <Component...>\n)
|
|
205
|
-
const renderWithParensMatch = fullStoryCode.match(/render:\s*\([^)]*\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*[,}]/);
|
|
206
|
-
if (renderWithParensMatch) {
|
|
207
|
-
const jsx = renderWithParensMatch[1].trim();
|
|
208
|
-
// Clean up any trailing commas or extra whitespace
|
|
209
|
-
return jsx.replace(/,\s*$/, '').trim();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Pattern 2: render: () => <Component...> (no parentheses, single line)
|
|
213
|
-
const renderNoParensMatch = fullStoryCode.match(/render:\s*\([^)]*\)\s*=>\s*(<[A-Z][^,}]*(?:\/>|<\/[A-Za-z.]+>))/s);
|
|
214
|
-
if (renderNoParensMatch) {
|
|
215
|
-
return renderNoParensMatch[1].trim();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Pattern 3: Arrow function story: () => (<JSX>)
|
|
219
|
-
const arrowWithParensMatch = fullStoryCode.match(/export\s+const\s+\w+\s*=\s*\(\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*;/);
|
|
220
|
-
if (arrowWithParensMatch) {
|
|
221
|
-
const jsx = arrowWithParensMatch[1].trim();
|
|
222
|
-
return jsx.replace(/,\s*$/, '').trim();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Pattern 4: Arrow function story: () => <Component...>
|
|
226
|
-
const arrowNoParensMatch = fullStoryCode.match(/export\s+const\s+\w+\s*=\s*\(\)\s*=>\s*(<[A-Z][^;]*(?:\/>|<\/[A-Za-z.]+>))\s*;/s);
|
|
227
|
-
if (arrowNoParensMatch) {
|
|
228
|
-
return arrowNoParensMatch[1].trim();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Pattern 5: Look for args-based stories with component prop spreading
|
|
232
|
-
// e.g., args: { children: 'Click me', color: 'blue' }
|
|
233
|
-
const argsMatch = fullStoryCode.match(/args:\s*(\{[\s\S]*?\})\s*[,}]/);
|
|
234
|
-
if (argsMatch && componentName) {
|
|
235
|
-
const result = generateJsxFromArgs(argsMatch[1], componentName);
|
|
236
|
-
if (result) return result;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Pattern 6: Look for any JSX block starting with < and ending with /> or </Component>
|
|
240
|
-
// This is a fallback for any JSX we can find
|
|
241
|
-
const jsxBlockMatch = fullStoryCode.match(/(<[A-Z][a-zA-Z0-9.]*[\s\S]*?(?:\/>|<\/[A-Za-z.]+>))/);
|
|
242
|
-
if (jsxBlockMatch) {
|
|
243
|
-
// Don't return if it looks like an import or type definition
|
|
244
|
-
const match = jsxBlockMatch[1];
|
|
245
|
-
if (!match.includes('import') && !match.includes('Meta<') && !match.includes('StoryObj<')) {
|
|
246
|
-
return match.trim();
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// If no patterns matched, return the original code
|
|
251
|
-
// (better than showing nothing)
|
|
252
|
-
return fullStoryCode;
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Simple Prism-like syntax highlighting for JSX/TSX
|
|
257
|
-
* Uses inline styles for portability (no external CSS needed)
|
|
258
|
-
*/
|
|
259
|
-
const tokenize = (code: string): Array<{ type: string; value: string }> => {
|
|
260
|
-
const tokens: Array<{ type: string; value: string }> = [];
|
|
261
|
-
let remaining = code;
|
|
262
|
-
|
|
263
|
-
const patterns: Array<{ type: string; regex: RegExp }> = [
|
|
264
|
-
// Comments
|
|
265
|
-
{ type: 'comment', regex: /^(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/ },
|
|
266
|
-
// Strings (double, single, template)
|
|
267
|
-
{ type: 'string', regex: /^("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'|`[^`\\]*(?:\\.[^`\\]*)*`)/ },
|
|
268
|
-
// JSX tags
|
|
269
|
-
{ type: 'tag', regex: /^(<\/?[A-Z][a-zA-Z0-9.]*|<\/?[a-z][a-z0-9-]*)/ },
|
|
270
|
-
// Closing tag bracket
|
|
271
|
-
{ type: 'punctuation', regex: /^(\/>|>)/ },
|
|
272
|
-
// Keywords
|
|
273
|
-
{ 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/ },
|
|
274
|
-
// Booleans and null
|
|
275
|
-
{ type: 'boolean', regex: /^(true|false|null|undefined)\b/ },
|
|
276
|
-
// Numbers
|
|
277
|
-
{ type: 'number', regex: /^-?\d+\.?\d*(e[+-]?\d+)?/ },
|
|
278
|
-
// Props/attributes (word followed by =)
|
|
279
|
-
{ type: 'attr-name', regex: /^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?==)/ },
|
|
280
|
-
// Function names
|
|
281
|
-
{ type: 'function', regex: /^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/ },
|
|
282
|
-
// Identifiers
|
|
283
|
-
{ type: 'plain', regex: /^[a-zA-Z_$][a-zA-Z0-9_$]*/ },
|
|
284
|
-
// Operators
|
|
285
|
-
{ type: 'operator', regex: /^(=>|===|!==|==|!=|<=|>=|&&|\|\||[+\-*/%=<>!&|^~?:])/ },
|
|
286
|
-
// Punctuation
|
|
287
|
-
{ type: 'punctuation', regex: /^[{}[\]();,.]/ },
|
|
288
|
-
// Whitespace
|
|
289
|
-
{ type: 'whitespace', regex: /^\s+/ },
|
|
290
|
-
];
|
|
291
|
-
|
|
292
|
-
while (remaining.length > 0) {
|
|
293
|
-
let matched = false;
|
|
294
|
-
|
|
295
|
-
for (const { type, regex } of patterns) {
|
|
296
|
-
const match = remaining.match(regex);
|
|
297
|
-
if (match) {
|
|
298
|
-
tokens.push({ type, value: match[0] });
|
|
299
|
-
remaining = remaining.slice(match[0].length);
|
|
300
|
-
matched = true;
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (!matched) {
|
|
306
|
-
// Unknown character, add as plain
|
|
307
|
-
tokens.push({ type: 'plain', value: remaining[0] });
|
|
308
|
-
remaining = remaining.slice(1);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return tokens;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Color scheme for syntax highlighting (VS Code-like dark theme)
|
|
317
|
-
*/
|
|
318
|
-
const tokenColors: Record<string, React.CSSProperties> = {
|
|
319
|
-
comment: { color: '#6A9955', fontStyle: 'italic' },
|
|
320
|
-
string: { color: '#CE9178' },
|
|
321
|
-
tag: { color: '#569CD6' },
|
|
322
|
-
keyword: { color: '#C586C0' },
|
|
323
|
-
boolean: { color: '#569CD6' },
|
|
324
|
-
number: { color: '#B5CEA8' },
|
|
325
|
-
'attr-name': { color: '#9CDCFE' },
|
|
326
|
-
function: { color: '#DCDCAA' },
|
|
327
|
-
operator: { color: '#D4D4D4' },
|
|
328
|
-
punctuation: { color: '#D4D4D4' },
|
|
329
|
-
plain: { color: '#D4D4D4' },
|
|
330
|
-
whitespace: {},
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Syntax Highlighter Component
|
|
335
|
-
*/
|
|
336
|
-
const SyntaxHighlighter: React.FC<{ code: string }> = ({ code }) => {
|
|
337
|
-
const tokens = useMemo(() => tokenize(code), [code]);
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
<pre
|
|
341
|
-
style={{
|
|
342
|
-
margin: 0,
|
|
343
|
-
padding: '16px',
|
|
344
|
-
background: '#1E1E1E',
|
|
345
|
-
borderRadius: '4px',
|
|
346
|
-
overflow: 'auto',
|
|
347
|
-
fontSize: '13px',
|
|
348
|
-
lineHeight: '1.5',
|
|
349
|
-
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
350
|
-
}}
|
|
351
|
-
>
|
|
352
|
-
<code>
|
|
353
|
-
{tokens.map((token, index) => (
|
|
354
|
-
<span key={index} style={tokenColors[token.type] || {}}>
|
|
355
|
-
{token.value}
|
|
356
|
-
</span>
|
|
357
|
-
))}
|
|
358
|
-
</code>
|
|
359
|
-
</pre>
|
|
360
|
-
);
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Inline styles for the panel
|
|
365
|
-
*/
|
|
366
|
-
const styles = {
|
|
367
|
-
container: {
|
|
368
|
-
height: '100%',
|
|
369
|
-
display: 'flex',
|
|
370
|
-
flexDirection: 'column' as const,
|
|
371
|
-
background: '#1E1E1E',
|
|
372
|
-
color: '#D4D4D4',
|
|
373
|
-
},
|
|
374
|
-
header: {
|
|
375
|
-
display: 'flex',
|
|
376
|
-
justifyContent: 'space-between',
|
|
377
|
-
alignItems: 'center',
|
|
378
|
-
padding: '8px 12px',
|
|
379
|
-
borderBottom: '1px solid #3C3C3C',
|
|
380
|
-
background: '#252526',
|
|
381
|
-
},
|
|
382
|
-
title: {
|
|
383
|
-
fontSize: '12px',
|
|
384
|
-
fontWeight: 600,
|
|
385
|
-
color: '#CCCCCC',
|
|
386
|
-
textTransform: 'uppercase' as const,
|
|
387
|
-
letterSpacing: '0.5px',
|
|
388
|
-
},
|
|
389
|
-
copyButton: {
|
|
390
|
-
background: '#0E639C',
|
|
391
|
-
color: 'white',
|
|
392
|
-
border: 'none',
|
|
393
|
-
borderRadius: '4px',
|
|
394
|
-
padding: '4px 10px',
|
|
395
|
-
fontSize: '11px',
|
|
396
|
-
cursor: 'pointer',
|
|
397
|
-
fontWeight: 500,
|
|
398
|
-
},
|
|
399
|
-
codeContainer: {
|
|
400
|
-
flex: 1,
|
|
401
|
-
overflow: 'auto',
|
|
402
|
-
padding: '0',
|
|
403
|
-
},
|
|
404
|
-
emptyState: {
|
|
405
|
-
display: 'flex',
|
|
406
|
-
flexDirection: 'column' as const,
|
|
407
|
-
alignItems: 'center',
|
|
408
|
-
justifyContent: 'center',
|
|
409
|
-
height: '100%',
|
|
410
|
-
color: '#888',
|
|
411
|
-
fontSize: '13px',
|
|
412
|
-
textAlign: 'center' as const,
|
|
413
|
-
padding: '20px',
|
|
414
|
-
},
|
|
415
|
-
storyInfo: {
|
|
416
|
-
fontSize: '11px',
|
|
417
|
-
color: '#888',
|
|
418
|
-
marginTop: '4px',
|
|
419
|
-
},
|
|
420
|
-
deleteButton: {
|
|
421
|
-
background: 'transparent',
|
|
422
|
-
color: '#C75050',
|
|
423
|
-
border: '1px solid #C75050',
|
|
424
|
-
borderRadius: '4px',
|
|
425
|
-
padding: '4px 10px',
|
|
426
|
-
fontSize: '11px',
|
|
427
|
-
cursor: 'pointer',
|
|
428
|
-
fontWeight: 500,
|
|
429
|
-
},
|
|
430
|
-
deleteButtonHover: {
|
|
431
|
-
background: '#C75050',
|
|
432
|
-
color: 'white',
|
|
433
|
-
},
|
|
434
|
-
deleteButtonDeleting: {
|
|
435
|
-
background: '#555',
|
|
436
|
-
color: '#888',
|
|
437
|
-
border: '1px solid #555',
|
|
438
|
-
cursor: 'not-allowed',
|
|
439
|
-
},
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Source Code Panel Component
|
|
444
|
-
*/
|
|
445
|
-
const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
|
|
446
|
-
const api = useStorybookApi();
|
|
447
|
-
const state = useStorybookState();
|
|
448
|
-
const [sourceCode, setSourceCode] = useState<string>('');
|
|
449
|
-
const [storyTitle, setStoryTitle] = useState<string>('');
|
|
450
|
-
const [copied, setCopied] = useState(false);
|
|
451
|
-
const [showFullCode, setShowFullCode] = useState(false);
|
|
452
|
-
const [isDeleting, setIsDeleting] = useState(false);
|
|
453
|
-
const [deleteHover, setDeleteHover] = useState(false);
|
|
454
|
-
|
|
455
|
-
// Get the current story ID
|
|
456
|
-
const currentStoryId = state?.storyId;
|
|
457
|
-
|
|
458
|
-
// Extract variant name from story ID (e.g., "generated-button--primary" -> "primary", "generated-button--full-width" -> "full-width")
|
|
459
|
-
const currentVariant = useMemo(() => {
|
|
460
|
-
if (!currentStoryId) return undefined;
|
|
461
|
-
// Match everything after the last -- (variant can contain hyphens like "full-width")
|
|
462
|
-
const variantMatch = currentStoryId.match(/--([a-z0-9-]+)$/i);
|
|
463
|
-
return variantMatch ? variantMatch[1] : undefined;
|
|
464
|
-
}, [currentStoryId]);
|
|
465
|
-
|
|
466
|
-
// Memoize the usage code extraction with variant awareness
|
|
467
|
-
const usageCode = useMemo(() => {
|
|
468
|
-
if (!sourceCode) return '';
|
|
469
|
-
return extractUsageCode(sourceCode, currentVariant);
|
|
470
|
-
}, [sourceCode, currentVariant]);
|
|
471
|
-
|
|
472
|
-
const displayCode = useMemo(() => {
|
|
473
|
-
if (!sourceCode) return '';
|
|
474
|
-
return showFullCode ? sourceCode : usageCode;
|
|
475
|
-
}, [sourceCode, showFullCode, usageCode]);
|
|
476
|
-
|
|
477
|
-
// Check if there's different usage code (for showing toggle button)
|
|
478
|
-
// This should remain true even when showing full code
|
|
479
|
-
const hasUsageCode = useMemo(() => {
|
|
480
|
-
return sourceCode && usageCode && usageCode !== sourceCode;
|
|
481
|
-
}, [sourceCode, usageCode]);
|
|
482
|
-
|
|
483
|
-
// Check if this is a generated story (for showing delete button)
|
|
484
|
-
const isGeneratedStory = useMemo(() => {
|
|
485
|
-
return currentStoryId?.includes('generated') || false;
|
|
486
|
-
}, [currentStoryId]);
|
|
487
|
-
|
|
488
|
-
// Extract story file ID from the Storybook story ID
|
|
489
|
-
// e.g., "generated-simple-test-button--primary" -> "SimpleTestButton"
|
|
490
|
-
const getStoryFileId = useCallback((storyId: string): string => {
|
|
491
|
-
// Remove "generated-" prefix and variant suffix
|
|
492
|
-
const match = storyId.match(/^generated[-\/]?(.+?)(?:--.*)?$/i);
|
|
493
|
-
if (match) {
|
|
494
|
-
// Convert kebab-case to PascalCase: "simple-test-button" -> "SimpleTestButton"
|
|
495
|
-
const words = match[1].split('-');
|
|
496
|
-
return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
497
|
-
}
|
|
498
|
-
return storyId;
|
|
499
|
-
}, []);
|
|
500
|
-
|
|
501
|
-
// Handle delete story
|
|
502
|
-
const handleDelete = useCallback(async () => {
|
|
503
|
-
if (!currentStoryId || !isGeneratedStory || isDeleting) return;
|
|
504
|
-
|
|
505
|
-
const confirmed = window.confirm('Delete this story file and ALL its variants? This cannot be undone.');
|
|
506
|
-
if (!confirmed) return;
|
|
507
|
-
|
|
508
|
-
setIsDeleting(true);
|
|
509
|
-
|
|
510
|
-
try {
|
|
511
|
-
const apiBase = getApiBaseUrl();
|
|
512
|
-
const storyFileId = getStoryFileId(currentStoryId);
|
|
513
|
-
|
|
514
|
-
console.log('[Source Code Panel] Deleting story:', { currentStoryId, storyFileId, apiBase });
|
|
515
|
-
|
|
516
|
-
// Try the RESTful DELETE endpoint first
|
|
517
|
-
const response = await fetch(`${apiBase}/story-ui/stories/${storyFileId}`, {
|
|
518
|
-
method: 'DELETE',
|
|
519
|
-
headers: { 'Content-Type': 'application/json' },
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
if (response.ok) {
|
|
523
|
-
console.log('[Source Code Panel] Story deleted successfully');
|
|
524
|
-
|
|
525
|
-
// Clear local cache
|
|
526
|
-
const topWindow = window.top || window;
|
|
527
|
-
if (topWindow.__STORY_UI_GENERATED_CODE__) {
|
|
528
|
-
delete topWindow.__STORY_UI_GENERATED_CODE__[currentStoryId];
|
|
529
|
-
}
|
|
530
|
-
if (window.__STORY_UI_GENERATED_CODE__) {
|
|
531
|
-
delete window.__STORY_UI_GENERATED_CODE__[currentStoryId];
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Clear localStorage cache
|
|
535
|
-
try {
|
|
536
|
-
const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
|
|
537
|
-
delete stored[storyFileId];
|
|
538
|
-
delete stored[currentStoryId];
|
|
539
|
-
localStorage.setItem('storyui_generated_code', JSON.stringify(stored));
|
|
540
|
-
} catch (e) {
|
|
541
|
-
console.warn('[Source Code Panel] Failed to clear localStorage:', e);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Clear the source code display
|
|
545
|
-
setSourceCode('');
|
|
546
|
-
|
|
547
|
-
// Navigate to a different story (Story UI Generator default)
|
|
548
|
-
api.selectStory('story-ui-story-generator--default');
|
|
549
|
-
|
|
550
|
-
// Trigger Storybook refresh to update sidebar
|
|
551
|
-
window.location.reload();
|
|
552
|
-
} else {
|
|
553
|
-
const errorData = await response.json().catch(() => ({}));
|
|
554
|
-
console.error('[Source Code Panel] Delete failed:', errorData);
|
|
555
|
-
alert(`Failed to delete story: ${errorData.error || 'Unknown error'}`);
|
|
556
|
-
}
|
|
557
|
-
} catch (error) {
|
|
558
|
-
console.error('[Source Code Panel] Delete error:', error);
|
|
559
|
-
alert('Failed to delete story. Check console for details.');
|
|
560
|
-
} finally {
|
|
561
|
-
setIsDeleting(false);
|
|
562
|
-
}
|
|
563
|
-
}, [currentStoryId, isGeneratedStory, isDeleting, api, getStoryFileId]);
|
|
564
|
-
|
|
565
|
-
// Try to get source code from the story
|
|
566
|
-
useEffect(() => {
|
|
567
|
-
if (!currentStoryId || !active) return;
|
|
568
|
-
|
|
569
|
-
// Get story data from the API
|
|
570
|
-
const story = api.getData(currentStoryId);
|
|
571
|
-
|
|
572
|
-
// Check if this is a generated story based on ID (works even if story doesn't exist in Storybook yet)
|
|
573
|
-
const isGeneratedStory = currentStoryId.includes('generated');
|
|
574
|
-
|
|
575
|
-
if (story) {
|
|
576
|
-
setStoryTitle(story.title || '');
|
|
577
|
-
|
|
578
|
-
// Try to get source from story parameters
|
|
579
|
-
const storySource = (story as any)?.parameters?.docs?.source?.code ||
|
|
580
|
-
(story as any)?.parameters?.storySource?.source ||
|
|
581
|
-
(story as any)?.parameters?.source?.code;
|
|
582
|
-
|
|
583
|
-
if (storySource) {
|
|
584
|
-
setSourceCode(storySource);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
} else {
|
|
588
|
-
// Story doesn't exist in Storybook yet, set title from story ID
|
|
589
|
-
setStoryTitle(currentStoryId);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// For generated stories (whether or not they exist in Storybook), try to get from cache/localStorage
|
|
593
|
-
if (isGeneratedStory) {
|
|
594
|
-
// Try to get from window cache first
|
|
595
|
-
const topWindow = window.top || window;
|
|
596
|
-
let cachedCode = topWindow.__STORY_UI_GENERATED_CODE__?.[currentStoryId] ||
|
|
597
|
-
window.__STORY_UI_GENERATED_CODE__?.[currentStoryId];
|
|
598
|
-
|
|
599
|
-
console.log('[Source Code Panel DEBUG] Looking for code:', {
|
|
600
|
-
currentStoryId,
|
|
601
|
-
foundInWindowCache: !!cachedCode,
|
|
602
|
-
windowCacheKeys: Object.keys(topWindow.__STORY_UI_GENERATED_CODE__ || {}),
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
// If not in memory cache, check localStorage (survives page navigation)
|
|
606
|
-
if (!cachedCode) {
|
|
607
|
-
try {
|
|
608
|
-
const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
|
|
609
|
-
|
|
610
|
-
console.log('[Source Code Panel DEBUG] localStorage lookup:', {
|
|
611
|
-
localStorageKeys: Object.keys(stored),
|
|
612
|
-
localStorageKeyCount: Object.keys(stored).length,
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
// Try multiple key formats since Storybook IDs differ from our storage keys
|
|
616
|
-
// Storybook ID format: "generated-componentname--variant" or "generated/componentname--variant"
|
|
617
|
-
// Our storage keys: "ComponentName", "ComponentName.stories.tsx", "story-hash123", etc.
|
|
618
|
-
const keysToTry: string[] = [currentStoryId];
|
|
619
|
-
|
|
620
|
-
// Extract component name and base story ID from Storybook ID
|
|
621
|
-
// e.g., "generated-simple-test-button--primary" -> baseId: "generated-simple-test-button--default", component: "simpletestbutton"
|
|
622
|
-
const match = currentStoryId.match(/^(generated[-\/]?.+?)(?:--(.*))?$/i);
|
|
623
|
-
if (match) {
|
|
624
|
-
const baseId = match[1];
|
|
625
|
-
const variant = match[2];
|
|
626
|
-
|
|
627
|
-
// Try base story ID with --default variant (this is what we store)
|
|
628
|
-
if (variant && variant !== 'default') {
|
|
629
|
-
keysToTry.push(`${baseId}--default`);
|
|
630
|
-
}
|
|
631
|
-
// Also try just the base ID without any variant
|
|
632
|
-
keysToTry.push(baseId);
|
|
633
|
-
|
|
634
|
-
// Extract component name (e.g., "generated-simple-test-button" -> "simpletestbutton")
|
|
635
|
-
const componentMatch = baseId.match(/^generated[-\/]?(.+)$/i);
|
|
636
|
-
if (componentMatch) {
|
|
637
|
-
const componentNameLower = componentMatch[1].replace(/-/g, '');
|
|
638
|
-
keysToTry.push(componentNameLower);
|
|
639
|
-
// Try PascalCase version (e.g., "simpletestbutton" -> "Simpletestbutton")
|
|
640
|
-
const pascalCase = componentNameLower.charAt(0).toUpperCase() + componentNameLower.slice(1);
|
|
641
|
-
keysToTry.push(pascalCase);
|
|
642
|
-
keysToTry.push(`${pascalCase}.stories.tsx`);
|
|
643
|
-
|
|
644
|
-
// Try with spaces converted to title case (e.g., "Simple Test Button" -> "SimpleTestButton")
|
|
645
|
-
const words = componentMatch[1].split('-');
|
|
646
|
-
if (words.length > 1) {
|
|
647
|
-
const titleCase = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
648
|
-
keysToTry.push(titleCase);
|
|
649
|
-
// Also try with space-separated title (what we store)
|
|
650
|
-
const spacedTitle = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
651
|
-
keysToTry.push(spacedTitle);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Also try the story title if available
|
|
657
|
-
if (storyTitle) {
|
|
658
|
-
keysToTry.push(storyTitle);
|
|
659
|
-
keysToTry.push(storyTitle.replace(/\s+/g, ''));
|
|
660
|
-
keysToTry.push(`${storyTitle.replace(/\s+/g, '')}.stories.tsx`);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
console.log('[Source Code Panel DEBUG] trying keys:', keysToTry);
|
|
664
|
-
|
|
665
|
-
// Try each key format
|
|
666
|
-
for (const key of keysToTry) {
|
|
667
|
-
if (stored[key]) {
|
|
668
|
-
cachedCode = stored[key];
|
|
669
|
-
console.log('[Source Code Panel DEBUG] found code with key:', key, 'codeLength:', cachedCode?.length);
|
|
670
|
-
break;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Restore to memory cache if found
|
|
675
|
-
if (cachedCode) {
|
|
676
|
-
if (!topWindow.__STORY_UI_GENERATED_CODE__) {
|
|
677
|
-
topWindow.__STORY_UI_GENERATED_CODE__ = {};
|
|
678
|
-
}
|
|
679
|
-
topWindow.__STORY_UI_GENERATED_CODE__[currentStoryId] = cachedCode;
|
|
680
|
-
}
|
|
681
|
-
} catch (e) {
|
|
682
|
-
console.warn('[Story UI] Failed to read from localStorage:', e);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
console.log('[Source Code Panel DEBUG] final result:', {
|
|
687
|
-
foundCode: !!cachedCode,
|
|
688
|
-
codeLength: cachedCode?.length,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
if (cachedCode) {
|
|
692
|
-
setSourceCode(cachedCode);
|
|
693
|
-
} else {
|
|
694
|
-
setSourceCode('');
|
|
695
|
-
}
|
|
696
|
-
} else {
|
|
697
|
-
setSourceCode('');
|
|
698
|
-
}
|
|
699
|
-
}, [currentStoryId, active, api]);
|
|
700
|
-
|
|
701
|
-
// Listen for code generated events from StoryUIPanel
|
|
702
|
-
useEffect(() => {
|
|
703
|
-
const channel = addons.getChannel();
|
|
704
|
-
const topWindow = window.top || window;
|
|
705
|
-
|
|
706
|
-
const handleCodeGenerated = (data: { storyId: string; code: string }) => {
|
|
707
|
-
// Store in window cache (both local and top window)
|
|
708
|
-
if (!window.__STORY_UI_GENERATED_CODE__) {
|
|
709
|
-
window.__STORY_UI_GENERATED_CODE__ = {};
|
|
710
|
-
}
|
|
711
|
-
window.__STORY_UI_GENERATED_CODE__[data.storyId] = data.code;
|
|
712
|
-
|
|
713
|
-
if (topWindow !== window) {
|
|
714
|
-
if (!topWindow.__STORY_UI_GENERATED_CODE__) {
|
|
715
|
-
topWindow.__STORY_UI_GENERATED_CODE__ = {};
|
|
716
|
-
}
|
|
717
|
-
topWindow.__STORY_UI_GENERATED_CODE__[data.storyId] = data.code;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// If this is the current story, update the display
|
|
721
|
-
if (data.storyId === currentStoryId) {
|
|
722
|
-
setSourceCode(data.code);
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
|
|
726
|
-
channel.on(EVENTS.CODE_GENERATED, handleCodeGenerated);
|
|
727
|
-
|
|
728
|
-
return () => {
|
|
729
|
-
channel.off(EVENTS.CODE_GENERATED, handleCodeGenerated);
|
|
730
|
-
};
|
|
731
|
-
}, [currentStoryId]);
|
|
732
|
-
|
|
733
|
-
const handleCopy = useCallback(() => {
|
|
734
|
-
if (displayCode) {
|
|
735
|
-
navigator.clipboard.writeText(displayCode).then(() => {
|
|
736
|
-
setCopied(true);
|
|
737
|
-
setTimeout(() => setCopied(false), 2000);
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
}, [displayCode]);
|
|
741
|
-
|
|
742
|
-
if (!active) return null;
|
|
743
|
-
|
|
744
|
-
// No story selected
|
|
745
|
-
if (!currentStoryId) {
|
|
746
|
-
return (
|
|
747
|
-
<div style={styles.container}>
|
|
748
|
-
<div style={styles.emptyState}>
|
|
749
|
-
<span>Select a story to view its source code</span>
|
|
750
|
-
</div>
|
|
751
|
-
</div>
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// No source code available
|
|
756
|
-
if (!sourceCode) {
|
|
757
|
-
return (
|
|
758
|
-
<div style={styles.container}>
|
|
759
|
-
<div style={styles.header}>
|
|
760
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
761
|
-
<span style={styles.title}>Source Code</span>
|
|
762
|
-
</div>
|
|
763
|
-
{isGeneratedStory && (
|
|
764
|
-
<button
|
|
765
|
-
style={{
|
|
766
|
-
...styles.deleteButton,
|
|
767
|
-
...(isDeleting ? styles.deleteButtonDeleting : {}),
|
|
768
|
-
...(deleteHover && !isDeleting ? styles.deleteButtonHover : {}),
|
|
769
|
-
}}
|
|
770
|
-
onClick={handleDelete}
|
|
771
|
-
onMouseEnter={() => setDeleteHover(true)}
|
|
772
|
-
onMouseLeave={() => setDeleteHover(false)}
|
|
773
|
-
disabled={isDeleting}
|
|
774
|
-
title="Delete this story file and all its variants"
|
|
775
|
-
>
|
|
776
|
-
{isDeleting ? 'Deleting...' : 'Delete Story'}
|
|
777
|
-
</button>
|
|
778
|
-
)}
|
|
779
|
-
</div>
|
|
780
|
-
<div style={styles.emptyState}>
|
|
781
|
-
<span>No source code available for this story</span>
|
|
782
|
-
<span style={styles.storyInfo}>
|
|
783
|
-
{storyTitle || currentStoryId}
|
|
784
|
-
</span>
|
|
785
|
-
<span style={{ ...styles.storyInfo, marginTop: '12px', maxWidth: '280px' }}>
|
|
786
|
-
Generate a story using the Story UI panel to see the code here.
|
|
787
|
-
</span>
|
|
788
|
-
</div>
|
|
789
|
-
</div>
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
return (
|
|
794
|
-
<div style={styles.container}>
|
|
795
|
-
<div style={styles.header}>
|
|
796
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
797
|
-
<span style={styles.title}>{showFullCode ? 'Full Story' : 'Usage Code'}</span>
|
|
798
|
-
{hasUsageCode && (
|
|
799
|
-
<button
|
|
800
|
-
style={{
|
|
801
|
-
background: 'transparent',
|
|
802
|
-
color: '#888',
|
|
803
|
-
border: '1px solid #555',
|
|
804
|
-
borderRadius: '4px',
|
|
805
|
-
padding: '2px 8px',
|
|
806
|
-
fontSize: '10px',
|
|
807
|
-
cursor: 'pointer',
|
|
808
|
-
fontWeight: 400,
|
|
809
|
-
}}
|
|
810
|
-
onClick={() => setShowFullCode(!showFullCode)}
|
|
811
|
-
>
|
|
812
|
-
{showFullCode ? 'Show Usage' : 'Show Full'}
|
|
813
|
-
</button>
|
|
814
|
-
)}
|
|
815
|
-
</div>
|
|
816
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
817
|
-
<button
|
|
818
|
-
style={{
|
|
819
|
-
...styles.copyButton,
|
|
820
|
-
background: copied ? '#16825D' : '#0E639C',
|
|
821
|
-
}}
|
|
822
|
-
onClick={handleCopy}
|
|
823
|
-
>
|
|
824
|
-
{copied ? 'Copied!' : 'Copy'}
|
|
825
|
-
</button>
|
|
826
|
-
{isGeneratedStory && (
|
|
827
|
-
<button
|
|
828
|
-
style={{
|
|
829
|
-
...styles.deleteButton,
|
|
830
|
-
...(isDeleting ? styles.deleteButtonDeleting : {}),
|
|
831
|
-
...(deleteHover && !isDeleting ? styles.deleteButtonHover : {}),
|
|
832
|
-
}}
|
|
833
|
-
onClick={handleDelete}
|
|
834
|
-
onMouseEnter={() => setDeleteHover(true)}
|
|
835
|
-
onMouseLeave={() => setDeleteHover(false)}
|
|
836
|
-
disabled={isDeleting}
|
|
837
|
-
title="Delete this story file and all its variants"
|
|
838
|
-
>
|
|
839
|
-
{isDeleting ? 'Deleting...' : 'Delete Story'}
|
|
840
|
-
</button>
|
|
841
|
-
)}
|
|
842
|
-
</div>
|
|
843
|
-
</div>
|
|
844
|
-
<div style={styles.codeContainer}>
|
|
845
|
-
<SyntaxHighlighter code={displayCode} />
|
|
846
|
-
</div>
|
|
847
|
-
</div>
|
|
848
|
-
);
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
// Register the addon - always register, not just in Edge mode
|
|
852
|
-
addons.register(ADDON_ID, () => {
|
|
853
|
-
addons.add(PANEL_ID, {
|
|
854
|
-
type: types.PANEL,
|
|
855
|
-
title: 'Source Code',
|
|
856
|
-
match: ({ viewMode }) => viewMode === 'story' || viewMode === 'docs',
|
|
857
|
-
render: ({ active }) => <SourceCodePanel active={active} />,
|
|
858
|
-
});
|
|
859
|
-
});
|