@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.
- package/.env.sample +3 -1
- package/README.md +160 -606
- package/dist/cli/index.js +23 -24
- package/dist/cli/setup.js +295 -36
- package/dist/mcp-server/index.js +67 -0
- package/dist/mcp-server/routes/generateStory.js +323 -56
- package/dist/story-generator/componentBlacklist.js +181 -0
- package/dist/story-generator/componentDiscovery.js +9 -2
- package/dist/story-generator/configLoader.js +109 -39
- package/dist/story-generator/considerationsLoader.js +204 -0
- package/dist/story-generator/documentation-sources.js +36 -0
- package/dist/story-generator/documentationLoader.js +214 -0
- package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
- package/dist/story-generator/generateStory.js +7 -3
- package/dist/story-generator/postProcessStory.js +71 -0
- package/dist/story-generator/promptGenerator.js +286 -37
- package/dist/story-generator/storyHistory.js +118 -0
- package/dist/story-generator/storyTracker.js +33 -18
- package/dist/story-generator/storyValidator.js +39 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
- package/dist/story-generator/validateStory.js +82 -7
- package/dist/story-ui.config.js +12 -5
- package/package.json +11 -6
- package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
- package/templates/StoryUI/StoryUIPanel.tsx +489 -359
- package/templates/react-import-rule.json +36 -0
- package/templates/story-generation-rules.json +29 -0
- package/templates/story-ui-considerations.json +156 -0
- package/templates/story-ui-considerations.md +109 -0
- package/templates/story-ui-docs-README.md +55 -0
- package/dist/scripts/test-validation.js +0 -81
- package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
- package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
- 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
|
|
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
|
|
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}
|
|
101
|
-
instructions.push(`- Use
|
|
102
|
-
instructions.push(`-
|
|
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(`-
|
|
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 =
|
|
150
|
-
let
|
|
319
|
+
const importStatement = generateImportStatements(config, components, imports);
|
|
320
|
+
let renderContent = '';
|
|
151
321
|
if (layoutComponent && sectionComponent) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
330
|
+
renderContent = `<${contentComponent.name}>Sample content</${contentComponent.name}>`;
|
|
161
331
|
}
|
|
162
332
|
else {
|
|
163
|
-
|
|
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
|
-
|
|
358
|
+
else {
|
|
359
|
+
return `import React from 'react';
|
|
360
|
+
import type { Meta, StoryObj } from '${storybookFramework}';
|
|
166
361
|
${importStatement}
|
|
167
362
|
|
|
168
|
-
|
|
169
|
-
title: '
|
|
363
|
+
const meta = {
|
|
364
|
+
title: 'Generated/Sample Component',
|
|
170
365
|
component: ${mainComponent},
|
|
171
|
-
|
|
172
|
-
|
|
366
|
+
parameters: {
|
|
367
|
+
layout: 'centered',
|
|
368
|
+
},
|
|
369
|
+
} satisfies Meta<typeof ${mainComponent}>;
|
|
173
370
|
|
|
174
|
-
export
|
|
371
|
+
export default meta;
|
|
372
|
+
type Story = StoryObj<typeof meta>;
|
|
373
|
+
|
|
374
|
+
export const Default: Story = {
|
|
175
375
|
args: {
|
|
176
|
-
|
|
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
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
+
}
|