@tpitre/story-ui 3.4.3 → 3.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "3.4.3",
3
+ "version": "3.5.0",
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,101 @@ 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 generate JSX from args
43
+ const generateJsxFromArgs = (argsStr: string, componentName: string): string | null => {
44
+ try {
45
+ // Extract children if present
46
+ const childrenMatch = argsStr.match(/children:\s*['"`]([^'"`]+)['"`]/);
47
+ const children = childrenMatch ? childrenMatch[1] : '';
48
+
49
+ // Extract other props (simplified)
50
+ const propsStr = argsStr
51
+ .replace(/children:\s*['"`][^'"`]*['"`],?/, '') // Remove children
52
+ .replace(/^\{|\}$/g, '') // Remove braces
53
+ .trim();
54
+
55
+ if (children) {
56
+ if (propsStr) {
57
+ return `<${componentName} ${propsStr.replace(/,\s*$/, '')}>${children}</${componentName}>`;
58
+ }
59
+ return `<${componentName}>${children}</${componentName}>`;
60
+ } else if (propsStr) {
61
+ return `<${componentName} ${propsStr.replace(/,\s*$/, '')} />`;
62
+ }
63
+ return `<${componentName} />`;
64
+ } catch {
65
+ return null;
66
+ }
67
+ };
68
+
69
+ // Get the component name from meta
70
+ const componentMatch = fullStoryCode.match(/component:\s*([A-Z][A-Za-z0-9]*)/);
71
+ const componentName = componentMatch ? componentMatch[1] : null;
72
+
73
+ // If we have a variant name, try to find that specific variant's args or render
74
+ if (variantName) {
75
+ // Normalize variant name for matching:
76
+ // - "primary" -> "Primary"
77
+ // - "full-width" -> "FullWidth" (kebab-case to PascalCase)
78
+ const normalizedVariant = variantName
79
+ .split('-')
80
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
81
+ .join('');
82
+
83
+ // Pattern A: export const Primary: Story = { args: {...} }
84
+ // Match the specific variant's export block
85
+ const variantExportRegex = new RegExp(
86
+ `export\\s+const\\s+${normalizedVariant}\\s*(?::\\s*Story)?\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*;`,
87
+ 'i'
88
+ );
89
+ const variantExportMatch = fullStoryCode.match(variantExportRegex);
90
+
91
+ if (variantExportMatch) {
92
+ const variantBlock = variantExportMatch[1];
93
+
94
+ // Try to extract render function from this variant
95
+ const renderWithParensMatch = variantBlock.match(/render:\s*\([^)]*\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*[,}]/);
96
+ if (renderWithParensMatch) {
97
+ return renderWithParensMatch[1].trim().replace(/,\s*$/, '');
98
+ }
99
+
100
+ const renderNoParensMatch = variantBlock.match(/render:\s*\([^)]*\)\s*=>\s*(<[A-Z][^,}]*(?:\/>|<\/[A-Za-z.]+>))/s);
101
+ if (renderNoParensMatch) {
102
+ return renderNoParensMatch[1].trim();
103
+ }
104
+
105
+ // Try to extract args from this variant
106
+ const argsMatch = variantBlock.match(/args:\s*(\{[\s\S]*?\})\s*[,}]/);
107
+ if (argsMatch && componentName) {
108
+ const result = generateJsxFromArgs(argsMatch[1], componentName);
109
+ if (result) return result;
110
+ }
111
+ }
112
+
113
+ // Pattern B: Arrow function variant: export const Primary = () => <Component...>
114
+ const arrowVariantRegex = new RegExp(
115
+ `export\\s+const\\s+${normalizedVariant}\\s*=\\s*\\(\\)\\s*=>\\s*\\(\\s*([\\s\\S]*?)\\s*\\)\\s*;`,
116
+ 'i'
117
+ );
118
+ const arrowVariantMatch = fullStoryCode.match(arrowVariantRegex);
119
+ if (arrowVariantMatch) {
120
+ return arrowVariantMatch[1].trim().replace(/,\s*$/, '');
121
+ }
122
+
123
+ // Pattern C: Arrow function without parens: export const Primary = () => <Component...>;
124
+ const arrowNoParensRegex = new RegExp(
125
+ `export\\s+const\\s+${normalizedVariant}\\s*=\\s*\\(\\)\\s*=>\\s*(<[A-Z][^;]*(?:\\/>|<\\/[A-Za-z.]+>))\\s*;`,
126
+ 'is'
127
+ );
128
+ const arrowNoParensMatch = fullStoryCode.match(arrowNoParensRegex);
129
+ if (arrowNoParensMatch) {
130
+ return arrowNoParensMatch[1].trim();
131
+ }
132
+ }
133
+
134
+ // Fallback: Try generic patterns (for Default or when variant not specified)
135
+
42
136
  // Try to extract JSX from render function: render: () => (<JSX>) or render: () => <JSX>
43
137
  // Pattern 1: render: () => (\n <Component...>\n)
44
138
  const renderWithParensMatch = fullStoryCode.match(/render:\s*\([^)]*\)\s*=>\s*\(\s*([\s\S]*?)\s*\)\s*[,}]/);
@@ -70,37 +164,9 @@ const extractUsageCode = (fullStoryCode: string): string => {
70
164
  // Pattern 5: Look for args-based stories with component prop spreading
71
165
  // e.g., args: { children: 'Click me', color: 'blue' }
72
166
  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
- }
167
+ if (argsMatch && componentName) {
168
+ const result = generateJsxFromArgs(argsMatch[1], componentName);
169
+ if (result) return result;
104
170
  }
105
171
 
106
172
  // Pattern 6: Look for any JSX block starting with < and ending with /> or </Component>
@@ -297,14 +363,33 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
297
363
  const [copied, setCopied] = useState(false);
298
364
  const [showFullCode, setShowFullCode] = useState(false);
299
365
 
300
- // Memoize the usage code extraction
366
+ // Get the current story ID
367
+ const currentStoryId = state?.storyId;
368
+
369
+ // Extract variant name from story ID (e.g., "generated-button--primary" -> "primary", "generated-button--full-width" -> "full-width")
370
+ const currentVariant = useMemo(() => {
371
+ if (!currentStoryId) return undefined;
372
+ // Match everything after the last -- (variant can contain hyphens like "full-width")
373
+ const variantMatch = currentStoryId.match(/--([a-z0-9-]+)$/i);
374
+ return variantMatch ? variantMatch[1] : undefined;
375
+ }, [currentStoryId]);
376
+
377
+ // Memoize the usage code extraction with variant awareness
378
+ const usageCode = useMemo(() => {
379
+ if (!sourceCode) return '';
380
+ return extractUsageCode(sourceCode, currentVariant);
381
+ }, [sourceCode, currentVariant]);
382
+
301
383
  const displayCode = useMemo(() => {
302
384
  if (!sourceCode) return '';
303
- return showFullCode ? sourceCode : extractUsageCode(sourceCode);
304
- }, [sourceCode, showFullCode]);
385
+ return showFullCode ? sourceCode : usageCode;
386
+ }, [sourceCode, showFullCode, usageCode]);
305
387
 
306
- // Get the current story ID
307
- const currentStoryId = state?.storyId;
388
+ // Check if there's different usage code (for showing toggle button)
389
+ // This should remain true even when showing full code
390
+ const hasUsageCode = useMemo(() => {
391
+ return sourceCode && usageCode && usageCode !== sourceCode;
392
+ }, [sourceCode, usageCode]);
308
393
 
309
394
  // Try to get source code from the story
310
395
  useEffect(() => {
@@ -340,11 +425,81 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
340
425
  let cachedCode = topWindow.__STORY_UI_GENERATED_CODE__?.[currentStoryId] ||
341
426
  window.__STORY_UI_GENERATED_CODE__?.[currentStoryId];
342
427
 
428
+ console.log('[Source Code Panel DEBUG] Looking for code:', {
429
+ currentStoryId,
430
+ foundInWindowCache: !!cachedCode,
431
+ windowCacheKeys: Object.keys(topWindow.__STORY_UI_GENERATED_CODE__ || {}),
432
+ });
433
+
343
434
  // If not in memory cache, check localStorage (survives page navigation)
344
435
  if (!cachedCode) {
345
436
  try {
346
437
  const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
347
- cachedCode = stored[currentStoryId];
438
+
439
+ console.log('[Source Code Panel DEBUG] localStorage lookup:', {
440
+ localStorageKeys: Object.keys(stored),
441
+ localStorageKeyCount: Object.keys(stored).length,
442
+ });
443
+
444
+ // Try multiple key formats since Storybook IDs differ from our storage keys
445
+ // Storybook ID format: "generated-componentname--variant" or "generated/componentname--variant"
446
+ // Our storage keys: "ComponentName", "ComponentName.stories.tsx", "story-hash123", etc.
447
+ const keysToTry: string[] = [currentStoryId];
448
+
449
+ // Extract component name and base story ID from Storybook ID
450
+ // e.g., "generated-simple-test-button--primary" -> baseId: "generated-simple-test-button--default", component: "simpletestbutton"
451
+ const match = currentStoryId.match(/^(generated[-\/]?.+?)(?:--(.*))?$/i);
452
+ if (match) {
453
+ const baseId = match[1];
454
+ const variant = match[2];
455
+
456
+ // Try base story ID with --default variant (this is what we store)
457
+ if (variant && variant !== 'default') {
458
+ keysToTry.push(`${baseId}--default`);
459
+ }
460
+ // Also try just the base ID without any variant
461
+ keysToTry.push(baseId);
462
+
463
+ // Extract component name (e.g., "generated-simple-test-button" -> "simpletestbutton")
464
+ const componentMatch = baseId.match(/^generated[-\/]?(.+)$/i);
465
+ if (componentMatch) {
466
+ const componentNameLower = componentMatch[1].replace(/-/g, '');
467
+ keysToTry.push(componentNameLower);
468
+ // Try PascalCase version (e.g., "simpletestbutton" -> "Simpletestbutton")
469
+ const pascalCase = componentNameLower.charAt(0).toUpperCase() + componentNameLower.slice(1);
470
+ keysToTry.push(pascalCase);
471
+ keysToTry.push(`${pascalCase}.stories.tsx`);
472
+
473
+ // Try with spaces converted to title case (e.g., "Simple Test Button" -> "SimpleTestButton")
474
+ const words = componentMatch[1].split('-');
475
+ if (words.length > 1) {
476
+ const titleCase = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
477
+ keysToTry.push(titleCase);
478
+ // Also try with space-separated title (what we store)
479
+ const spacedTitle = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
480
+ keysToTry.push(spacedTitle);
481
+ }
482
+ }
483
+ }
484
+
485
+ // Also try the story title if available
486
+ if (storyTitle) {
487
+ keysToTry.push(storyTitle);
488
+ keysToTry.push(storyTitle.replace(/\s+/g, ''));
489
+ keysToTry.push(`${storyTitle.replace(/\s+/g, '')}.stories.tsx`);
490
+ }
491
+
492
+ console.log('[Source Code Panel DEBUG] trying keys:', keysToTry);
493
+
494
+ // Try each key format
495
+ for (const key of keysToTry) {
496
+ if (stored[key]) {
497
+ cachedCode = stored[key];
498
+ console.log('[Source Code Panel DEBUG] found code with key:', key, 'codeLength:', cachedCode?.length);
499
+ break;
500
+ }
501
+ }
502
+
348
503
  // Restore to memory cache if found
349
504
  if (cachedCode) {
350
505
  if (!topWindow.__STORY_UI_GENERATED_CODE__) {
@@ -357,6 +512,11 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
357
512
  }
358
513
  }
359
514
 
515
+ console.log('[Source Code Panel DEBUG] final result:', {
516
+ foundCode: !!cachedCode,
517
+ codeLength: cachedCode?.length,
518
+ });
519
+
360
520
  if (cachedCode) {
361
521
  setSourceCode(cachedCode);
362
522
  } else {
@@ -441,9 +601,6 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
441
601
  );
442
602
  }
443
603
 
444
- // Check if extraction was successful (displayCode is different from sourceCode)
445
- const hasUsageCode = displayCode !== sourceCode;
446
-
447
604
  return (
448
605
  <div style={styles.container}>
449
606
  <div style={styles.header}>