@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "3.4.3",
3
+ "version": "3.5.1",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- // 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
- }
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
- // Memoize the usage code extraction
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 : extractUsageCode(sourceCode);
304
- }, [sourceCode, showFullCode]);
429
+ return showFullCode ? sourceCode : usageCode;
430
+ }, [sourceCode, showFullCode, usageCode]);
305
431
 
306
- // Get the current story ID
307
- const currentStoryId = state?.storyId;
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
- cachedCode = stored[currentStoryId];
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}>