@tpitre/story-ui 1.7.1 → 2.0.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.
Files changed (35) hide show
  1. package/.env.sample +3 -1
  2. package/README.md +160 -606
  3. package/dist/cli/index.js +23 -24
  4. package/dist/cli/setup.js +295 -36
  5. package/dist/mcp-server/index.js +67 -0
  6. package/dist/mcp-server/routes/generateStory.js +323 -56
  7. package/dist/story-generator/componentBlacklist.js +181 -0
  8. package/dist/story-generator/componentDiscovery.js +9 -2
  9. package/dist/story-generator/configLoader.js +109 -39
  10. package/dist/story-generator/considerationsLoader.js +204 -0
  11. package/dist/story-generator/documentation-sources.js +36 -0
  12. package/dist/story-generator/documentationLoader.js +214 -0
  13. package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
  14. package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
  15. package/dist/story-generator/generateStory.js +7 -3
  16. package/dist/story-generator/postProcessStory.js +71 -0
  17. package/dist/story-generator/promptGenerator.js +286 -37
  18. package/dist/story-generator/storyHistory.js +118 -0
  19. package/dist/story-generator/storyTracker.js +33 -18
  20. package/dist/story-generator/storyValidator.js +39 -0
  21. package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
  22. package/dist/story-generator/validateStory.js +82 -7
  23. package/dist/story-ui.config.js +12 -5
  24. package/package.json +11 -6
  25. package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
  26. package/templates/StoryUI/StoryUIPanel.tsx +489 -359
  27. package/templates/react-import-rule.json +36 -0
  28. package/templates/story-generation-rules.json +29 -0
  29. package/templates/story-ui-considerations.json +156 -0
  30. package/templates/story-ui-considerations.md +109 -0
  31. package/templates/story-ui-docs-README.md +55 -0
  32. package/dist/scripts/test-validation.js +0 -81
  33. package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
  34. package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
  35. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Post-process generated stories to fix common issues
3
+ * This module is completely design-system agnostic
4
+ */
5
+ export function postProcessStory(code, libraryPath) {
6
+ console.log(`🔧 Post-processing story for library: ${libraryPath}`);
7
+ let processedCode = code;
8
+ // Fix ANY component with children prop - ALWAYS convert to render function
9
+ if (processedCode.includes('children: (')) {
10
+ console.log('🚨 Detected children prop in args - converting to render function');
11
+ processedCode = convertLayoutToRenderFunction(processedCode);
12
+ }
13
+ // Leave inline styles as-is - let the AI use the available components naturally
14
+ // Post-processing should be design-system agnostic
15
+ if (processedCode.includes('style={{')) {
16
+ console.log('â„šī¸ Inline styles detected - keeping as-is for design system agnosticism');
17
+ }
18
+ return processedCode;
19
+ }
20
+ /**
21
+ * Convert any layout story with children prop to use render function
22
+ */
23
+ function convertLayoutToRenderFunction(code) {
24
+ // First, remove the component from meta if it's a layout
25
+ let processedCode = code;
26
+ // Find the meta object
27
+ const metaMatch = code.match(/const meta = {([^}]+)}/s);
28
+ if (metaMatch) {
29
+ const metaContent = metaMatch[1];
30
+ // If it has a component field, remove it for layouts
31
+ if (metaContent.includes('component:')) {
32
+ const newMetaContent = metaContent
33
+ .split('\n')
34
+ .filter(line => !line.includes('component:'))
35
+ .join('\n');
36
+ // Also fix the satisfies Meta type
37
+ const metaWithType = `const meta = {${newMetaContent}} satisfies Meta;`;
38
+ processedCode = code.replace(/const meta = {[^}]+} satisfies Meta(?:<[^>]+>)?;/s, metaWithType);
39
+ console.log('✅ Removed component from meta object');
40
+ }
41
+ }
42
+ // Extract all stories with children prop
43
+ const storyRegex = /export const (\w+): Story = {\s*args:\s*{\s*children:\s*\(\s*([\s\S]*?)\s*\)\s*}\s*};/g;
44
+ let match;
45
+ while ((match = storyRegex.exec(processedCode)) !== null) {
46
+ const storyName = match[1];
47
+ const childrenContent = match[2];
48
+ // Build new story with render function
49
+ const newStory = `export const ${storyName}: Story = {\n render: () => (\n${childrenContent}\n )\n};`;
50
+ // Replace the old story with the new one
51
+ processedCode = processedCode.replace(match[0], newStory);
52
+ console.log(`✅ Converted ${storyName} from children prop to render function`);
53
+ }
54
+ return processedCode;
55
+ }
56
+ /**
57
+ * Convert a story with Alert children to multiple story exports
58
+ */
59
+ function convertAlertChildrenToExports(code) {
60
+ // For now, return the code as-is
61
+ console.log('Alert conversion not yet implemented');
62
+ return code;
63
+ }
64
+ /**
65
+ * Convert a story with Toast children to multiple story exports
66
+ */
67
+ function convertToastChildrenToExports(code) {
68
+ // For now, return the code as-is
69
+ console.log('Toast conversion not yet implemented');
70
+ return code;
71
+ }
@@ -1,3 +1,5 @@
1
+ import { loadConsiderations, considerationsToPrompt } from './considerationsLoader.js';
2
+ import { DocumentationLoader } from './documentationLoader.js';
1
3
  /**
2
4
  * Generates a comprehensive AI prompt based on the configuration and discovered components
3
5
  */
@@ -25,7 +27,84 @@ function generateSystemPrompt(config) {
25
27
  const componentSystemName = config.componentPrefix ?
26
28
  `${config.componentPrefix.replace(/^[A-Z]+/, '')} design system` :
27
29
  'component library';
28
- return `You are an expert UI developer creating Storybook stories. Use ONLY the React components from the ${componentSystemName} listed below.`;
30
+ return `🚨 CRITICAL: EVERY STORY MUST START WITH "import React from 'react';" AS THE FIRST LINE 🚨
31
+
32
+ 🔴 CRITICAL RULE: NEVER use children in args for ANY component or layout. Always use render functions. 🔴
33
+
34
+ You are an expert UI developer creating Storybook stories. Use ONLY the React components from the ${componentSystemName} listed below.
35
+
36
+ 🔴 MANDATORY FIRST LINE - NO EXCEPTIONS:
37
+ The VERY FIRST LINE of every story file MUST be:
38
+ import React from 'react';
39
+
40
+ CRITICAL IMPORT RULES - MUST FOLLOW EXACTLY:
41
+ 1. **LINE 1: import React from 'react';** (MANDATORY - NEVER SKIP THIS)
42
+ 2. **LINE 2: import type { StoryObj } from '@storybook/[framework]';**
43
+ 3. **LINE 3: import { ComponentName } from '[your-import-path]';**
44
+
45
+ âš ī¸ WITHOUT "import React from 'react';" THE STORY WILL FAIL WITH "React is not defined" ERROR âš ī¸
46
+
47
+ 🚨 COMPONENT IMPORT VALIDATION - CRITICAL 🚨
48
+ You can ONLY import components that are explicitly listed in the "Available components" section below.
49
+ ANY component not in that list DOES NOT EXIST and will cause import errors.
50
+ Before importing any component, verify it exists in the Available components list.
51
+ If a component is not listed, DO NOT use it - choose an alternative from the available list.
52
+
53
+ 🔴 IMPORT PATH RULE - MANDATORY 🔴
54
+ ALWAYS use the EXACT import path shown in parentheses after each component name.
55
+ For example: If the Available components list shows "Button (import from 'antd')",
56
+ you MUST use: import { Button } from 'antd';
57
+ NEVER use the main package import if a specific path is shown.
58
+ This is critical for proper component resolution.
59
+
60
+ Example correct order:
61
+ import React from 'react';
62
+ import type { StoryObj } from '@storybook/[framework]';
63
+ import { ComponentName } from '[your-import-path]';
64
+
65
+ 2. Use the correct Storybook framework import for your environment
66
+ 3. ONLY import components that are explicitly listed in the "Available components" section below
67
+ 4. Do NOT create or import any components that are not in the list
68
+ 5. Do NOT import story exports from other story files
69
+ 6. When in doubt, use the basic components listed below
70
+
71
+ REQUIRED STORY STRUCTURE:
72
+ Every story MUST start with these three imports in this order:
73
+ 1. import React from 'react';
74
+ 2. import type { StoryObj } from '@storybook/[framework]';
75
+ 3. import { ComponentName } from '[library-path]';
76
+
77
+ GENERAL COMPONENT RULES:
78
+ - StoryUIPanel is the Story UI interface, not a design system component - never import it
79
+ - Do not import components that end with Story, Example, Demo, or that appear to be story exports
80
+ - Only use components explicitly listed in the available components section
81
+
82
+ CRITICAL STORY FORMAT RULES:
83
+ - Use ES modules syntax for exports: "export default meta;" NOT "module.exports = meta;"
84
+ - Every story file MUST have a default export with the meta object
85
+ - Follow the Component Story Format (CSF) 3.0 standard
86
+
87
+ IMPORTANT IMAGE RULES:
88
+ - When using image components or <img> tags, ALWAYS include a src attribute
89
+ - Use Lorem Picsum for all placeholder images: https://picsum.photos/[width]/[height] (e.g., https://picsum.photos/300/200)
90
+ - You can add random variation with: https://picsum.photos/300/200?random=1
91
+ - Never create <img> tags without a src attribute
92
+
93
+ STORY STRUCTURE RULES:
94
+ - NEVER pass children through args for ANY component - this breaks story rendering
95
+ - Always use render functions: render: () => (<YourLayout />)
96
+ - For layouts with multiple components, DO NOT set component in meta
97
+ - Only set component in meta when showcasing a SINGLE component's variations
98
+ - Examples of what NOT to do:
99
+ ❌ args: { children: <div>content</div> }
100
+ ❌ args: { children: (<><Component1 /><Component2 /></>) }
101
+ ✅ render: () => (<div><Component1 /><Component2 /></div>)
102
+
103
+ SPACING AND LAYOUT RULES:
104
+ - Use the layout components provided in the component library when available
105
+ - If no layout components are available, use appropriate HTML elements with inline styles
106
+ - Follow the design system's spacing and styling conventions
107
+ - Use the component library's design tokens and spacing system when available`;
29
108
  }
30
109
  /**
31
110
  * Generates component reference documentation
@@ -66,6 +145,10 @@ function generateComponentReference(components, config) {
66
145
  */
67
146
  function formatComponentReference(component, config) {
68
147
  let reference = `- ${component.name}`;
148
+ // Add import path information if available
149
+ if (component.__componentPath) {
150
+ reference += ` (import from '${component.__componentPath}')`;
151
+ }
69
152
  if (component.props && component.props.length > 0) {
70
153
  reference += `: Props: ${component.props.join(', ')}`;
71
154
  }
@@ -76,7 +159,7 @@ function formatComponentReference(component, config) {
76
159
  reference += ` - ${component.description}`;
77
160
  }
78
161
  // Add specific usage notes for layout components
79
- if (component.category === 'layout') {
162
+ if (component.category === 'layout' && component.name && typeof component.name === 'string') {
80
163
  if (component.name.toLowerCase().includes('layout') && !component.name.toLowerCase().includes('section')) {
81
164
  reference += ' - Use as main wrapper for multi-column layouts';
82
165
  }
@@ -95,15 +178,21 @@ function generateLayoutInstructions(config) {
95
178
  const layoutRules = config.layoutRules;
96
179
  if (layoutRules.multiColumnWrapper && layoutRules.columnComponent) {
97
180
  instructions.push('CRITICAL LAYOUT RULES:');
98
- instructions.push(`- For ANY multi-column layout (2, 3, or more columns), use CSS Grid with ${layoutRules.multiColumnWrapper} elements`);
181
+ instructions.push(`- For ANY multi-column layout (2, 3, or more columns), use ${layoutRules.multiColumnWrapper} components`);
99
182
  instructions.push(`- Each column must be wrapped in its own ${layoutRules.columnComponent} element`);
100
- instructions.push(`- Structure: <${layoutRules.multiColumnWrapper} style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}><${layoutRules.columnComponent}>column 1</${layoutRules.columnComponent}><${layoutRules.columnComponent}>column 2</${layoutRules.columnComponent}></${layoutRules.multiColumnWrapper}>`);
101
- instructions.push(`- Use inline styles for CSS Grid layouts since the design system lacks proper multi-column layout components`);
102
- instructions.push(`- The grid container ${layoutRules.multiColumnWrapper} should be the main component in your story, not individual cards`);
183
+ instructions.push(`- Structure: <${layoutRules.multiColumnWrapper}><${layoutRules.columnComponent}>column 1</${layoutRules.columnComponent}><${layoutRules.columnComponent}>column 2</${layoutRules.columnComponent}></${layoutRules.multiColumnWrapper}>`);
184
+ instructions.push(`- Use component library styling approach (className, style props, or design tokens as appropriate)`);
185
+ instructions.push(`- NEVER use CSS properties as props (like display="grid" or gridTemplateColumns) - these are not valid props`);
186
+ instructions.push(`- For grid-like layouts, use Flex with wrap prop and appropriate gap, NOT CSS Grid`);
187
+ instructions.push(`- The ${layoutRules.multiColumnWrapper} should be the main component in your story for multi-column layouts`);
103
188
  }
104
189
  if (layoutRules.prohibitedElements && layoutRules.prohibitedElements.length > 0) {
105
- instructions.push(`- Do NOT use plain HTML ${layoutRules.prohibitedElements.join(', ')} elements for layout - use the provided layout components`);
190
+ instructions.push(`- NEVER use plain HTML ${layoutRules.prohibitedElements.join(', ')} elements - ALWAYS use the provided design system components`);
106
191
  }
192
+ // Generic layout instructions for all design systems
193
+ instructions.push(`- Use semantic heading components from your design system instead of raw <h1>-<h6> tags`);
194
+ instructions.push(`- Use the design system's layout components and spacing tokens instead of inline styles`);
195
+ instructions.push(`- Prefer design system components over plain HTML elements for consistent styling`);
107
196
  return instructions;
108
197
  }
109
198
  /**
@@ -130,15 +219,96 @@ function generateExamples(config) {
130
219
  examples.push(layoutExamples.grid);
131
220
  examples.push('');
132
221
  }
222
+ // Add image-specific examples
223
+ examples.push('Image usage examples:');
224
+ examples.push('// Always include src attribute with placeholder images:');
225
+ examples.push('<img src="https://picsum.photos/300/200" alt="Placeholder image" />');
226
+ examples.push('// For different random images:');
227
+ examples.push('<img src="https://picsum.photos/400/300?random=1" alt="Random image" style={{width: "100%", height: "auto"}} />');
228
+ examples.push('');
229
+ // Add proper story structure examples
230
+ examples.push('Proper story structure examples:');
231
+ examples.push('');
232
+ examples.push('// CORRECT - Layout with multiple components:');
233
+ examples.push('const meta = {');
234
+ examples.push(' title: "Generated/Homepage Hero",');
235
+ examples.push(' parameters: { layout: "fullscreen" },');
236
+ examples.push(' // NO component field for layouts!');
237
+ examples.push('} satisfies Meta;');
238
+ examples.push('');
239
+ examples.push('export const Default: Story = {');
240
+ examples.push(' render: () => (');
241
+ examples.push(' <div>');
242
+ examples.push(' <Banner title="Sale!" variant="success" />');
243
+ examples.push(' <div className="hero-section">');
244
+ examples.push(' <h1>Welcome</h1>');
245
+ examples.push(' </div>');
246
+ examples.push(' </div>');
247
+ examples.push(' )');
248
+ examples.push('};');
249
+ examples.push('');
250
+ examples.push('// WRONG - Never use children in args:');
251
+ examples.push('export const Wrong: Story = {');
252
+ examples.push(' args: {');
253
+ examples.push(' children: ( // ❌ NEVER DO THIS');
254
+ examples.push(' <div>content</div>');
255
+ examples.push(' )');
256
+ examples.push(' }');
257
+ examples.push('};');
258
+ examples.push('');
259
+ examples.push('// CORRECT - Single component showcase:');
260
+ examples.push('const meta = {');
261
+ examples.push(' title: "Generated/Banner Variations",');
262
+ examples.push(' component: Banner, // ✓ OK for single component');
263
+ examples.push('} satisfies Meta<typeof Banner>;');
264
+ examples.push('');
265
+ examples.push('export const InfoBanner: Story = {');
266
+ examples.push(' args: {');
267
+ examples.push(' title: "Information",');
268
+ examples.push(' variant: "info"');
269
+ examples.push(' }');
270
+ examples.push('};');
271
+ examples.push('');
133
272
  }
134
273
  return examples;
135
274
  }
275
+ /**
276
+ * Generates import statements, using individual import paths if available
277
+ */
278
+ function generateImportStatements(config, components, componentNames) {
279
+ const importMap = new Map();
280
+ for (const componentName of componentNames) {
281
+ // Find the component in our discovered components to get its specific import path
282
+ const component = components.find(c => c.name === componentName);
283
+ if (component && typeof component === 'object' && '__componentPath' in component) {
284
+ // Use the discovered component's specific import path
285
+ const importPath = component.__componentPath;
286
+ if (!importMap.has(importPath)) {
287
+ importMap.set(importPath, []);
288
+ }
289
+ importMap.get(importPath).push(componentName);
290
+ }
291
+ else {
292
+ // Fallback to the main package import path
293
+ if (!importMap.has(config.importPath)) {
294
+ importMap.set(config.importPath, []);
295
+ }
296
+ importMap.get(config.importPath).push(componentName);
297
+ }
298
+ }
299
+ // Generate import statements
300
+ const importLines = [];
301
+ for (const [importPath, components] of importMap) {
302
+ importLines.push(`import { ${components.join(', ')} } from '${importPath}';`);
303
+ }
304
+ return importLines.join('\n');
305
+ }
136
306
  /**
137
307
  * Generates a default sample story if none provided
138
308
  */
139
309
  function generateDefaultSampleStory(config, components) {
140
- const layoutComponent = components.find(c => c.category === 'layout' && !c.name.toLowerCase().includes('section'));
141
- const sectionComponent = components.find(c => c.category === 'layout' && c.name.toLowerCase().includes('section'));
310
+ const layoutComponent = components.find(c => c.category === 'layout' && c.name && typeof c.name === 'string' && !c.name.toLowerCase().includes('section'));
311
+ const sectionComponent = components.find(c => c.category === 'layout' && c.name && typeof c.name === 'string' && c.name.toLowerCase().includes('section'));
142
312
  const contentComponent = components.find(c => c.category === 'content');
143
313
  const mainComponent = layoutComponent?.name || contentComponent?.name || components[0]?.name || 'div';
144
314
  const imports = [mainComponent];
@@ -146,56 +316,135 @@ function generateDefaultSampleStory(config, components) {
146
316
  imports.push(sectionComponent.name);
147
317
  if (contentComponent && contentComponent.name !== mainComponent)
148
318
  imports.push(contentComponent.name);
149
- const importStatement = `import { ${imports.join(', ')} } from '${config.importPath}';`;
150
- let children = '';
319
+ const importStatement = generateImportStatements(config, components, imports);
320
+ let renderContent = '';
151
321
  if (layoutComponent && sectionComponent) {
152
- children = `
153
- <${layoutComponent.name}>
154
- <${sectionComponent.name}>
155
- ${contentComponent ? `<${contentComponent.name}>Sample content</${contentComponent.name}>` : 'Sample content'}
156
- </${sectionComponent.name}>
157
- </${layoutComponent.name}>`;
322
+ renderContent = `
323
+ <${layoutComponent.name}>
324
+ <${sectionComponent.name}>
325
+ ${contentComponent ? `<${contentComponent.name}>Sample content</${contentComponent.name}>` : 'Sample content'}
326
+ </${sectionComponent.name}>
327
+ </${layoutComponent.name}>`;
158
328
  }
159
329
  else if (contentComponent) {
160
- children = `<${contentComponent.name}>Sample content</${contentComponent.name}>`;
330
+ renderContent = `<${contentComponent.name}>Sample content</${contentComponent.name}>`;
161
331
  }
162
332
  else {
163
- children = '<div>Sample content</div>';
333
+ renderContent = '<div>Sample content</div>';
334
+ }
335
+ const storybookFramework = config.storybookFramework || '@storybook/react';
336
+ // For layouts, don't set a component in meta
337
+ const isLayout = layoutComponent || renderContent.includes('<div');
338
+ if (isLayout) {
339
+ return `import React from 'react';
340
+ import type { Meta, StoryObj } from '${storybookFramework}';
341
+ ${importStatement}
342
+
343
+ const meta = {
344
+ title: 'Generated/Sample Layout',
345
+ parameters: {
346
+ layout: 'centered',
347
+ },
348
+ } satisfies Meta;
349
+
350
+ export default meta;
351
+ type Story = StoryObj<typeof meta>;
352
+
353
+ export const Default: Story = {
354
+ render: () => (${renderContent}
355
+ )
356
+ };`;
164
357
  }
165
- return `import type { StoryObj } from '@storybook/react-webpack5';
358
+ else {
359
+ return `import React from 'react';
360
+ import type { Meta, StoryObj } from '${storybookFramework}';
166
361
  ${importStatement}
167
362
 
168
- export default {
169
- title: 'Layouts/Sample Layout',
363
+ const meta = {
364
+ title: 'Generated/Sample Component',
170
365
  component: ${mainComponent},
171
- subcomponents: { ${imports.slice(1).join(', ')} },
172
- };
366
+ parameters: {
367
+ layout: 'centered',
368
+ },
369
+ } satisfies Meta<typeof ${mainComponent}>;
173
370
 
174
- export const Default: StoryObj<typeof ${mainComponent}> = {
371
+ export default meta;
372
+ type Story = StoryObj<typeof meta>;
373
+
374
+ export const Default: Story = {
175
375
  args: {
176
- children: (${children}
177
- )
376
+ // Add component props here
178
377
  }
179
378
  };`;
379
+ }
180
380
  }
181
381
  /**
182
382
  * Builds the complete Claude prompt
183
383
  */
184
- export function buildClaudePrompt(userPrompt, config, components) {
384
+ export async function buildClaudePrompt(userPrompt, config, components) {
185
385
  const generated = generatePrompt(config, components);
186
386
  const promptParts = [
187
387
  generated.systemPrompt,
188
388
  '',
189
- ...generated.layoutInstructions,
190
- '',
191
- 'Available components:',
192
- generated.componentReference,
193
- ...generated.examples,
194
389
  ];
195
- // Add critical structure instructions for multi-column layouts
196
- if (config.layoutRules.multiColumnWrapper && config.layoutRules.columnComponent) {
197
- promptParts.push(`CRITICAL: For multi-column layouts, the children prop must contain a SINGLE ${config.layoutRules.multiColumnWrapper} with CSS Grid styling wrapping all ${config.layoutRules.columnComponent} components.`, `WRONG: children: (<><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}></>)`, `CORRECT: children: (<${config.layoutRules.multiColumnWrapper} style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}></${config.layoutRules.multiColumnWrapper}>)`, '');
390
+ // Load documentation - try new directory-based approach first
391
+ const projectRoot = config.considerationsPath ?
392
+ config.considerationsPath.replace(/\/story-ui-considerations\.(md|json)$/, '') :
393
+ process.cwd();
394
+ const docLoader = new DocumentationLoader(projectRoot);
395
+ let documentationAdded = false;
396
+ if (docLoader.hasDocumentation()) {
397
+ const docs = await docLoader.loadDocumentation();
398
+ if (docs.sources.length > 0) {
399
+ const docPrompt = docLoader.formatForPrompt(docs);
400
+ if (docPrompt) {
401
+ promptParts.push(docPrompt);
402
+ promptParts.push('');
403
+ documentationAdded = true;
404
+ }
405
+ }
406
+ }
407
+ // Fall back to legacy considerations file if no directory-based docs
408
+ if (!documentationAdded) {
409
+ const considerations = loadConsiderations(config.considerationsPath);
410
+ if (considerations) {
411
+ const considerationsPrompt = considerationsToPrompt(considerations);
412
+ if (considerationsPrompt) {
413
+ promptParts.push(considerationsPrompt);
414
+ promptParts.push('');
415
+ }
416
+ }
417
+ }
418
+ promptParts.push(...generated.layoutInstructions, '', 'Available components:', generated.componentReference, ...generated.examples);
419
+ // Add additional imports information if configured
420
+ if (config.additionalImports && config.additionalImports.length > 0) {
421
+ promptParts.push('');
422
+ promptParts.push('ADDITIONAL IMPORT EXAMPLES - COPY THESE EXACTLY:');
423
+ config.additionalImports.forEach(additionalImport => {
424
+ // For each import path, show the exact syntax
425
+ const componentExamples = additionalImport.components.map(componentName => {
426
+ // Check if this component has specific import type information
427
+ // Look in both components and layoutComponents arrays
428
+ let componentConfig = config.components?.find(c => c.name === componentName);
429
+ if (!componentConfig) {
430
+ componentConfig = config.layoutComponents?.find(c => c.name === componentName);
431
+ }
432
+ // Use runtime check for importType since it may not be in TypeScript interface
433
+ if (componentConfig && componentConfig.importType === 'default') {
434
+ return `import ${componentName} from '${additionalImport.path}';`;
435
+ }
436
+ else {
437
+ return `import { ${componentName} } from '${additionalImport.path}';`;
438
+ }
439
+ });
440
+ componentExamples.forEach(example => {
441
+ promptParts.push(`- ${example}`);
442
+ });
443
+ });
198
444
  }
199
- promptParts.push(`Output a complete Storybook story file in TypeScript. Import components from "${config.importPath}". Use the following sample as a template. Respond ONLY with a single code block containing the full file, and nothing else.`, '', 'Sample story format:', generated.sampleStory, '', 'User request:', userPrompt);
445
+ // Icons and other specific imports should be handled through additionalImports or considerations
446
+ // Reinforce NO children in args rule
447
+ promptParts.push('', '🔴 CRITICAL REMINDER: NEVER use children in args 🔴', 'Always use render functions for any layout or component composition.', '');
448
+ promptParts.push(`Output a complete Storybook story file in TypeScript. Import components as shown in the sample template below. Use the following sample as a template. Respond ONLY with a single code block containing the full file, and nothing else.`, '', '<rules>', '🚨 FINAL CRITICAL REMINDERS 🚨', "🔴 FIRST LINE MUST BE: import React from 'react';", '🔴 WITHOUT THIS IMPORT, THE STORY WILL BREAK!', '', 'OTHER CRITICAL RULES:', '- Story title MUST always start with "Generated/" (e.g., title: "Generated/Recipe Card")', '- Do NOT use prefixes like "Content/", "Components/", or any other section name', '- ONLY import components that are listed in the "Available components" section', '- ALWAYS use the exact import path shown in parentheses after each component', '- NEVER use main package imports when specific subpath imports are shown', '- Do NOT import story exports - these are NOT real components', '- Check every import against the Available components list before using it', '- FORBIDDEN: Any component not explicitly listed in the Available components section', '- FORBIDDEN: Theme setup components (providers should be configured at the app level, not in individual stories)', '- All images MUST have a src attribute with placeholder URLs (use https://picsum.photos/)', '- Never create <img> tags without src attributes', '- MUST use ES modules syntax: "export default meta;" NOT "module.exports = meta;"', '- The file MUST have a default export for the meta object', '- Keep the story concise and focused - avoid overly complex layouts that might exceed token limits', '- Ensure all JSX tags are properly closed', '- Story must be complete and syntactically valid', '- CRITICAL: Never put ANY content in args.children - always use render function', '- Use render functions for ALL layouts and component compositions', '- For layouts: DO NOT set component in meta', '- Only set component in meta when showcasing a SINGLE component', '- Use appropriate styling for the component library (design tokens, className, or inline styles as needed)', '</rules>', '', 'Sample story format:', generated.sampleStory, '', 'User request:', userPrompt);
200
449
  return promptParts.join('\n');
201
450
  }
@@ -0,0 +1,118 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ export class StoryHistoryManager {
5
+ constructor(projectRoot) {
6
+ this.histories = new Map();
7
+ this.historyDir = path.join(projectRoot, '.story-ui-history');
8
+ this.ensureHistoryDir();
9
+ this.loadHistories();
10
+ }
11
+ ensureHistoryDir() {
12
+ if (!fs.existsSync(this.historyDir)) {
13
+ fs.mkdirSync(this.historyDir, { recursive: true });
14
+ }
15
+ }
16
+ loadHistories() {
17
+ if (!fs.existsSync(this.historyDir))
18
+ return;
19
+ const files = fs.readdirSync(this.historyDir);
20
+ for (const file of files) {
21
+ if (file.endsWith('.json')) {
22
+ try {
23
+ const content = fs.readFileSync(path.join(this.historyDir, file), 'utf-8');
24
+ const history = JSON.parse(content);
25
+ this.histories.set(history.storyId, history);
26
+ }
27
+ catch (e) {
28
+ console.warn(`Failed to load history file ${file}:`, e);
29
+ }
30
+ }
31
+ }
32
+ }
33
+ saveHistory(history) {
34
+ const filePath = path.join(this.historyDir, `${history.storyId}.json`);
35
+ fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
36
+ this.histories.set(history.storyId, history);
37
+ }
38
+ /**
39
+ * Create a new story history or add a version to existing history
40
+ */
41
+ addVersion(fileName, prompt, code, parentVersionId) {
42
+ // Extract story ID from filename (remove hash and extension)
43
+ const storyId = fileName.replace(/-[a-f0-9]+\.stories\.tsx$/, '');
44
+ let history = this.histories.get(storyId);
45
+ const newVersion = {
46
+ id: crypto.randomBytes(8).toString('hex'),
47
+ timestamp: Date.now(),
48
+ prompt,
49
+ code,
50
+ fileName,
51
+ parentId: parentVersionId
52
+ };
53
+ if (!history) {
54
+ // Create new history
55
+ history = {
56
+ storyId,
57
+ title: this.titleFromFileName(fileName),
58
+ versions: [newVersion],
59
+ currentVersionId: newVersion.id
60
+ };
61
+ }
62
+ else {
63
+ // Add version to existing history
64
+ history.versions.push(newVersion);
65
+ history.currentVersionId = newVersion.id;
66
+ }
67
+ this.saveHistory(history);
68
+ return newVersion;
69
+ }
70
+ /**
71
+ * Get the current version of a story by filename
72
+ */
73
+ getCurrentVersion(fileName) {
74
+ const storyId = fileName.replace(/-[a-f0-9]+\.stories\.tsx$/, '');
75
+ const history = this.histories.get(storyId);
76
+ if (!history)
77
+ return null;
78
+ return history.versions.find(v => v.id === history.currentVersionId) || null;
79
+ }
80
+ /**
81
+ * Get all versions for a story
82
+ */
83
+ getHistory(fileName) {
84
+ const storyId = fileName.replace(/-[a-f0-9]+\.stories\.tsx$/, '');
85
+ return this.histories.get(storyId) || null;
86
+ }
87
+ /**
88
+ * Find a story by partial filename match
89
+ */
90
+ findStoryByPartialName(partialName) {
91
+ for (const history of this.histories.values()) {
92
+ if (history.title.toLowerCase().includes(partialName.toLowerCase()) ||
93
+ history.storyId.toLowerCase().includes(partialName.toLowerCase())) {
94
+ return history;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ titleFromFileName(fileName) {
100
+ return fileName
101
+ .replace(/-[a-f0-9]+\.stories\.tsx$/, '')
102
+ .replace(/-/g, ' ')
103
+ .replace(/\b\w/g, c => c.toUpperCase());
104
+ }
105
+ /**
106
+ * Clean up old versions, keeping only the last N versions
107
+ */
108
+ pruneHistory(maxVersionsPerStory = 10) {
109
+ for (const history of this.histories.values()) {
110
+ if (history.versions.length > maxVersionsPerStory) {
111
+ // Sort by timestamp and keep only the latest versions
112
+ history.versions.sort((a, b) => b.timestamp - a.timestamp);
113
+ history.versions = history.versions.slice(0, maxVersionsPerStory);
114
+ this.saveHistory(history);
115
+ }
116
+ }
117
+ }
118
+ }