@tpitre/story-ui 3.4.3 → 3.5.1
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/package.json +1 -1
- package/templates/StoryUI/manager.tsx +242 -41
package/package.json
CHANGED
|
@@ -38,7 +38,145 @@ declare global {
|
|
|
38
38
|
* Into:
|
|
39
39
|
* <Button>Click</Button>
|
|
40
40
|
*/
|
|
41
|
-
const extractUsageCode = (fullStoryCode: string): string => {
|
|
41
|
+
const extractUsageCode = (fullStoryCode: string, variantName?: string): string => {
|
|
42
|
+
// Helper function to convert object-style props to JSX attribute syntax
|
|
43
|
+
// e.g., "color: 'blue', variant: 'filled'" -> 'color="blue" variant="filled"'
|
|
44
|
+
const convertToJsxAttributes = (propsStr: string): string => {
|
|
45
|
+
if (!propsStr.trim()) return '';
|
|
46
|
+
|
|
47
|
+
const attributes: string[] = [];
|
|
48
|
+
// Match key: value pairs, handling strings, booleans, numbers, and expressions
|
|
49
|
+
// Pattern: key: 'value' or key: "value" or key: true/false or key: 123 or key: expression
|
|
50
|
+
const propRegex = /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|(\btrue\b|\bfalse\b)|(\d+(?:\.\d+)?)|(\{[^}]+\})|([^,}\s]+))/g;
|
|
51
|
+
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = propRegex.exec(propsStr)) !== null) {
|
|
54
|
+
const key = match[1];
|
|
55
|
+
const stringValueSingle = match[2]; // 'value'
|
|
56
|
+
const stringValueDouble = match[3]; // "value"
|
|
57
|
+
const boolValue = match[4]; // true/false
|
|
58
|
+
const numValue = match[5]; // 123 or 1.5
|
|
59
|
+
const objValue = match[6]; // {expression}
|
|
60
|
+
const otherValue = match[7]; // other expressions
|
|
61
|
+
|
|
62
|
+
if (stringValueSingle !== undefined) {
|
|
63
|
+
attributes.push(`${key}="${stringValueSingle}"`);
|
|
64
|
+
} else if (stringValueDouble !== undefined) {
|
|
65
|
+
attributes.push(`${key}="${stringValueDouble}"`);
|
|
66
|
+
} else if (boolValue !== undefined) {
|
|
67
|
+
if (boolValue === 'true') {
|
|
68
|
+
attributes.push(key); // Just the prop name for true (e.g., fullWidth)
|
|
69
|
+
}
|
|
70
|
+
// Skip false values - they're the default and don't need to be shown
|
|
71
|
+
} else if (numValue !== undefined) {
|
|
72
|
+
attributes.push(`${key}={${numValue}}`);
|
|
73
|
+
} else if (objValue !== undefined) {
|
|
74
|
+
attributes.push(`${key}=${objValue}`);
|
|
75
|
+
} else if (otherValue !== undefined) {
|
|
76
|
+
attributes.push(`${key}={${otherValue}}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return attributes.join(' ');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Helper function to generate JSX from args
|
|
84
|
+
const generateJsxFromArgs = (argsStr: string, componentName: string): string | null => {
|
|
85
|
+
try {
|
|
86
|
+
// Extract children if present
|
|
87
|
+
const childrenMatch = argsStr.match(/children:\s*['"`]([^'"`]+)['"`]/);
|
|
88
|
+
const children = childrenMatch ? childrenMatch[1] : '';
|
|
89
|
+
|
|
90
|
+
// Extract other props (remove children first)
|
|
91
|
+
const propsStr = argsStr
|
|
92
|
+
.replace(/children:\s*['"`][^'"`]*['"`],?/, '') // Remove children
|
|
93
|
+
.replace(/^\{|\}$/g, '') // Remove braces
|
|
94
|
+
.trim();
|
|
95
|
+
|
|
96
|
+
// Convert to JSX attribute syntax
|
|
97
|
+
const jsxAttributes = convertToJsxAttributes(propsStr);
|
|
98
|
+
|
|
99
|
+
if (children) {
|
|
100
|
+
if (jsxAttributes) {
|
|
101
|
+
return `<${componentName} ${jsxAttributes}>${children}</${componentName}>`;
|
|
102
|
+
}
|
|
103
|
+
return `<${componentName}>${children}</${componentName}>`;
|
|
104
|
+
} else if (jsxAttributes) {
|
|
105
|
+
return `<${componentName} ${jsxAttributes} />`;
|
|
106
|
+
}
|
|
107
|
+
return `<${componentName} />`;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Get the component name from meta
|
|
114
|
+
const componentMatch = fullStoryCode.match(/component:\s*([A-Z][A-Za-z0-9]*)/);
|
|
115
|
+
const componentName = componentMatch ? componentMatch[1] : null;
|
|
116
|
+
|
|
117
|
+
// If we have a variant name, try to find that specific variant's args or render
|
|
118
|
+
if (variantName) {
|
|
119
|
+
// Normalize variant name for matching:
|
|
120
|
+
// - "primary" -> "Primary"
|
|
121
|
+
// - "full-width" -> "FullWidth" (kebab-case to PascalCase)
|
|
122
|
+
const normalizedVariant = variantName
|
|
123
|
+
.split('-')
|
|
124
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
125
|
+
.join('');
|
|
126
|
+
|
|
127
|
+
// Pattern A: export const Primary: Story = { args: {...} }
|
|
128
|
+
// Match the specific variant's export block
|
|
129
|
+
const variantExportRegex = new RegExp(
|
|
130
|
+
`export\\s+const\\s+${normalizedVariant}\\s*(?::\\s*Story)?\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*;`,
|
|
131
|
+
'i'
|
|
132
|
+
);
|
|
133
|
+
const variantExportMatch = fullStoryCode.match(variantExportRegex);
|
|
134
|
+
|
|
135
|
+
if (variantExportMatch) {
|
|
136
|
+
const variantBlock = variantExportMatch[1];
|
|
137
|
+
|
|
138
|
+
// Try to extract render function from this variant
|
|
139
|
+
const renderWithParensMatch = variantBlock.match(/render:\s*\([^)]*\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*[,}]/);
|
|
140
|
+
if (renderWithParensMatch) {
|
|
141
|
+
return renderWithParensMatch[1].trim().replace(/,\s*$/, '');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const renderNoParensMatch = variantBlock.match(/render:\s*\([^)]*\)\s*=>\s*(<[A-Z][^,}]*(?:\/>|<\/[A-Za-z.]+>))/s);
|
|
145
|
+
if (renderNoParensMatch) {
|
|
146
|
+
return renderNoParensMatch[1].trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Try to extract args from this variant
|
|
150
|
+
const argsMatch = variantBlock.match(/args:\s*(\{[\s\S]*?\})\s*[,}]/);
|
|
151
|
+
if (argsMatch && componentName) {
|
|
152
|
+
const result = generateJsxFromArgs(argsMatch[1], componentName);
|
|
153
|
+
if (result) return result;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Pattern B: Arrow function variant: export const Primary = () => <Component...>
|
|
158
|
+
const arrowVariantRegex = new RegExp(
|
|
159
|
+
`export\\s+const\\s+${normalizedVariant}\\s*=\\s*\\(\\)\\s*=>\\s*\\(\\s*([\\s\\S]*?)\\s*\\)\\s*;`,
|
|
160
|
+
'i'
|
|
161
|
+
);
|
|
162
|
+
const arrowVariantMatch = fullStoryCode.match(arrowVariantRegex);
|
|
163
|
+
if (arrowVariantMatch) {
|
|
164
|
+
return arrowVariantMatch[1].trim().replace(/,\s*$/, '');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Pattern C: Arrow function without parens: export const Primary = () => <Component...>;
|
|
168
|
+
const arrowNoParensRegex = new RegExp(
|
|
169
|
+
`export\\s+const\\s+${normalizedVariant}\\s*=\\s*\\(\\)\\s*=>\\s*(<[A-Z][^;]*(?:\\/>|<\\/[A-Za-z.]+>))\\s*;`,
|
|
170
|
+
'is'
|
|
171
|
+
);
|
|
172
|
+
const arrowNoParensMatch = fullStoryCode.match(arrowNoParensRegex);
|
|
173
|
+
if (arrowNoParensMatch) {
|
|
174
|
+
return arrowNoParensMatch[1].trim();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Fallback: Try generic patterns (for Default or when variant not specified)
|
|
179
|
+
|
|
42
180
|
// Try to extract JSX from render function: render: () => (<JSX>) or render: () => <JSX>
|
|
43
181
|
// Pattern 1: render: () => (\n <Component...>\n)
|
|
44
182
|
const renderWithParensMatch = fullStoryCode.match(/render:\s*\([^)]*\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*[,}]/);
|
|
@@ -70,37 +208,9 @@ const extractUsageCode = (fullStoryCode: string): string => {
|
|
|
70
208
|
// Pattern 5: Look for args-based stories with component prop spreading
|
|
71
209
|
// e.g., args: { children: 'Click me', color: 'blue' }
|
|
72
210
|
const argsMatch = fullStoryCode.match(/args:\s*(\{[\s\S]*?\})\s*[,}]/);
|
|
73
|
-
if (argsMatch) {
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
}
|
|
211
|
+
if (argsMatch && componentName) {
|
|
212
|
+
const result = generateJsxFromArgs(argsMatch[1], componentName);
|
|
213
|
+
if (result) return result;
|
|
104
214
|
}
|
|
105
215
|
|
|
106
216
|
// Pattern 6: Look for any JSX block starting with < and ending with /> or </Component>
|
|
@@ -297,14 +407,33 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
|
|
|
297
407
|
const [copied, setCopied] = useState(false);
|
|
298
408
|
const [showFullCode, setShowFullCode] = useState(false);
|
|
299
409
|
|
|
300
|
-
//
|
|
410
|
+
// Get the current story ID
|
|
411
|
+
const currentStoryId = state?.storyId;
|
|
412
|
+
|
|
413
|
+
// Extract variant name from story ID (e.g., "generated-button--primary" -> "primary", "generated-button--full-width" -> "full-width")
|
|
414
|
+
const currentVariant = useMemo(() => {
|
|
415
|
+
if (!currentStoryId) return undefined;
|
|
416
|
+
// Match everything after the last -- (variant can contain hyphens like "full-width")
|
|
417
|
+
const variantMatch = currentStoryId.match(/--([a-z0-9-]+)$/i);
|
|
418
|
+
return variantMatch ? variantMatch[1] : undefined;
|
|
419
|
+
}, [currentStoryId]);
|
|
420
|
+
|
|
421
|
+
// Memoize the usage code extraction with variant awareness
|
|
422
|
+
const usageCode = useMemo(() => {
|
|
423
|
+
if (!sourceCode) return '';
|
|
424
|
+
return extractUsageCode(sourceCode, currentVariant);
|
|
425
|
+
}, [sourceCode, currentVariant]);
|
|
426
|
+
|
|
301
427
|
const displayCode = useMemo(() => {
|
|
302
428
|
if (!sourceCode) return '';
|
|
303
|
-
return showFullCode ? sourceCode :
|
|
304
|
-
}, [sourceCode, showFullCode]);
|
|
429
|
+
return showFullCode ? sourceCode : usageCode;
|
|
430
|
+
}, [sourceCode, showFullCode, usageCode]);
|
|
305
431
|
|
|
306
|
-
//
|
|
307
|
-
|
|
432
|
+
// Check if there's different usage code (for showing toggle button)
|
|
433
|
+
// This should remain true even when showing full code
|
|
434
|
+
const hasUsageCode = useMemo(() => {
|
|
435
|
+
return sourceCode && usageCode && usageCode !== sourceCode;
|
|
436
|
+
}, [sourceCode, usageCode]);
|
|
308
437
|
|
|
309
438
|
// Try to get source code from the story
|
|
310
439
|
useEffect(() => {
|
|
@@ -340,11 +469,81 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
|
|
|
340
469
|
let cachedCode = topWindow.__STORY_UI_GENERATED_CODE__?.[currentStoryId] ||
|
|
341
470
|
window.__STORY_UI_GENERATED_CODE__?.[currentStoryId];
|
|
342
471
|
|
|
472
|
+
console.log('[Source Code Panel DEBUG] Looking for code:', {
|
|
473
|
+
currentStoryId,
|
|
474
|
+
foundInWindowCache: !!cachedCode,
|
|
475
|
+
windowCacheKeys: Object.keys(topWindow.__STORY_UI_GENERATED_CODE__ || {}),
|
|
476
|
+
});
|
|
477
|
+
|
|
343
478
|
// If not in memory cache, check localStorage (survives page navigation)
|
|
344
479
|
if (!cachedCode) {
|
|
345
480
|
try {
|
|
346
481
|
const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
|
|
347
|
-
|
|
482
|
+
|
|
483
|
+
console.log('[Source Code Panel DEBUG] localStorage lookup:', {
|
|
484
|
+
localStorageKeys: Object.keys(stored),
|
|
485
|
+
localStorageKeyCount: Object.keys(stored).length,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Try multiple key formats since Storybook IDs differ from our storage keys
|
|
489
|
+
// Storybook ID format: "generated-componentname--variant" or "generated/componentname--variant"
|
|
490
|
+
// Our storage keys: "ComponentName", "ComponentName.stories.tsx", "story-hash123", etc.
|
|
491
|
+
const keysToTry: string[] = [currentStoryId];
|
|
492
|
+
|
|
493
|
+
// Extract component name and base story ID from Storybook ID
|
|
494
|
+
// e.g., "generated-simple-test-button--primary" -> baseId: "generated-simple-test-button--default", component: "simpletestbutton"
|
|
495
|
+
const match = currentStoryId.match(/^(generated[-\/]?.+?)(?:--(.*))?$/i);
|
|
496
|
+
if (match) {
|
|
497
|
+
const baseId = match[1];
|
|
498
|
+
const variant = match[2];
|
|
499
|
+
|
|
500
|
+
// Try base story ID with --default variant (this is what we store)
|
|
501
|
+
if (variant && variant !== 'default') {
|
|
502
|
+
keysToTry.push(`${baseId}--default`);
|
|
503
|
+
}
|
|
504
|
+
// Also try just the base ID without any variant
|
|
505
|
+
keysToTry.push(baseId);
|
|
506
|
+
|
|
507
|
+
// Extract component name (e.g., "generated-simple-test-button" -> "simpletestbutton")
|
|
508
|
+
const componentMatch = baseId.match(/^generated[-\/]?(.+)$/i);
|
|
509
|
+
if (componentMatch) {
|
|
510
|
+
const componentNameLower = componentMatch[1].replace(/-/g, '');
|
|
511
|
+
keysToTry.push(componentNameLower);
|
|
512
|
+
// Try PascalCase version (e.g., "simpletestbutton" -> "Simpletestbutton")
|
|
513
|
+
const pascalCase = componentNameLower.charAt(0).toUpperCase() + componentNameLower.slice(1);
|
|
514
|
+
keysToTry.push(pascalCase);
|
|
515
|
+
keysToTry.push(`${pascalCase}.stories.tsx`);
|
|
516
|
+
|
|
517
|
+
// Try with spaces converted to title case (e.g., "Simple Test Button" -> "SimpleTestButton")
|
|
518
|
+
const words = componentMatch[1].split('-');
|
|
519
|
+
if (words.length > 1) {
|
|
520
|
+
const titleCase = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
521
|
+
keysToTry.push(titleCase);
|
|
522
|
+
// Also try with space-separated title (what we store)
|
|
523
|
+
const spacedTitle = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
524
|
+
keysToTry.push(spacedTitle);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Also try the story title if available
|
|
530
|
+
if (storyTitle) {
|
|
531
|
+
keysToTry.push(storyTitle);
|
|
532
|
+
keysToTry.push(storyTitle.replace(/\s+/g, ''));
|
|
533
|
+
keysToTry.push(`${storyTitle.replace(/\s+/g, '')}.stories.tsx`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
console.log('[Source Code Panel DEBUG] trying keys:', keysToTry);
|
|
537
|
+
|
|
538
|
+
// Try each key format
|
|
539
|
+
for (const key of keysToTry) {
|
|
540
|
+
if (stored[key]) {
|
|
541
|
+
cachedCode = stored[key];
|
|
542
|
+
console.log('[Source Code Panel DEBUG] found code with key:', key, 'codeLength:', cachedCode?.length);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
348
547
|
// Restore to memory cache if found
|
|
349
548
|
if (cachedCode) {
|
|
350
549
|
if (!topWindow.__STORY_UI_GENERATED_CODE__) {
|
|
@@ -357,6 +556,11 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
|
|
|
357
556
|
}
|
|
358
557
|
}
|
|
359
558
|
|
|
559
|
+
console.log('[Source Code Panel DEBUG] final result:', {
|
|
560
|
+
foundCode: !!cachedCode,
|
|
561
|
+
codeLength: cachedCode?.length,
|
|
562
|
+
});
|
|
563
|
+
|
|
360
564
|
if (cachedCode) {
|
|
361
565
|
setSourceCode(cachedCode);
|
|
362
566
|
} else {
|
|
@@ -441,9 +645,6 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
|
|
|
441
645
|
);
|
|
442
646
|
}
|
|
443
647
|
|
|
444
|
-
// Check if extraction was successful (displayCode is different from sourceCode)
|
|
445
|
-
const hasUsageCode = displayCode !== sourceCode;
|
|
446
|
-
|
|
447
648
|
return (
|
|
448
649
|
<div style={styles.container}>
|
|
449
650
|
<div style={styles.header}>
|