@tpitre/story-ui 2.1.5 → 2.2.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.
@@ -1,19 +1,20 @@
1
+ import { logger } from './logger.js';
1
2
  /**
2
3
  * Post-process generated stories to fix common issues
3
4
  * This module is completely design-system agnostic
4
5
  */
5
6
  export function postProcessStory(code, libraryPath) {
6
- console.log(`🔧 Post-processing story for library: ${libraryPath}`);
7
+ logger.log(`🔧 Post-processing story for library: ${libraryPath}`);
7
8
  let processedCode = code;
8
9
  // Fix ANY component with children prop - ALWAYS convert to render function
9
10
  if (processedCode.includes('children: (')) {
10
- console.log('🚨 Detected children prop in args - converting to render function');
11
+ logger.log('🚨 Detected children prop in args - converting to render function');
11
12
  processedCode = convertLayoutToRenderFunction(processedCode);
12
13
  }
13
14
  // Leave inline styles as-is - let the AI use the available components naturally
14
15
  // Post-processing should be design-system agnostic
15
16
  if (processedCode.includes('style={{')) {
16
- console.log('â„šī¸ Inline styles detected - keeping as-is for design system agnosticism');
17
+ logger.log('â„šī¸ Inline styles detected - keeping as-is for design system agnosticism');
17
18
  }
18
19
  return processedCode;
19
20
  }
@@ -36,7 +37,7 @@ function convertLayoutToRenderFunction(code) {
36
37
  // Also fix the satisfies Meta type
37
38
  const metaWithType = `const meta = {${newMetaContent}} satisfies Meta;`;
38
39
  processedCode = code.replace(/const meta = {[^}]+} satisfies Meta(?:<[^>]+>)?;/s, metaWithType);
39
- console.log('✅ Removed component from meta object');
40
+ logger.log('✅ Removed component from meta object');
40
41
  }
41
42
  }
42
43
  // Extract all stories with children prop
@@ -49,7 +50,7 @@ function convertLayoutToRenderFunction(code) {
49
50
  const newStory = `export const ${storyName}: Story = {\n render: () => (\n${childrenContent}\n )\n};`;
50
51
  // Replace the old story with the new one
51
52
  processedCode = processedCode.replace(match[0], newStory);
52
- console.log(`✅ Converted ${storyName} from children prop to render function`);
53
+ logger.log(`✅ Converted ${storyName} from children prop to render function`);
53
54
  }
54
55
  return processedCode;
55
56
  }
@@ -58,7 +59,7 @@ function convertLayoutToRenderFunction(code) {
58
59
  */
59
60
  function convertAlertChildrenToExports(code) {
60
61
  // For now, return the code as-is
61
- console.log('Alert conversion not yet implemented');
62
+ logger.log('Alert conversion not yet implemented');
62
63
  return code;
63
64
  }
64
65
  /**
@@ -66,6 +67,6 @@ function convertAlertChildrenToExports(code) {
66
67
  */
67
68
  function convertToastChildrenToExports(code) {
68
69
  // For now, return the code as-is
69
- console.log('Toast conversion not yet implemented');
70
+ logger.log('Toast conversion not yet implemented');
70
71
  return code;
71
72
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { logger } from './logger.js';
3
4
  /**
4
5
  * Production-ready gitignore manager that handles both development and server environments
5
6
  */
@@ -55,7 +56,7 @@ export class ProductionGitignoreManager {
55
56
  * Production environment: Use in-memory story generation
56
57
  */
57
58
  handleProductionEnvironment() {
58
- console.log('🌐 Production environment detected - using in-memory story generation');
59
+ logger.log('🌐 Production environment detected - using in-memory story generation');
59
60
  // Validate that gitignore is already set up
60
61
  this.validateProductionSetup();
61
62
  // Set up temporary directory for story generation if needed
@@ -65,7 +66,7 @@ export class ProductionGitignoreManager {
65
66
  * Development environment: Full gitignore management
66
67
  */
67
68
  handleDevelopmentEnvironment() {
68
- console.log('🔧 Development environment - setting up gitignore integration');
69
+ logger.log('🔧 Development environment - setting up gitignore integration');
69
70
  this.ensureGeneratedDirectoryExists();
70
71
  this.ensureGeneratedDirectoryIgnored();
71
72
  this.createGeneratedDirectoryReadme();
@@ -86,7 +87,7 @@ export class ProductionGitignoreManager {
86
87
  console.warn(' Run "npx story-ui setup-gitignore" in development to fix this.');
87
88
  }
88
89
  else {
89
- console.log('✅ Production gitignore configuration validated');
90
+ logger.log('✅ Production gitignore configuration validated');
90
91
  }
91
92
  }
92
93
  /**
@@ -97,7 +98,7 @@ export class ProductionGitignoreManager {
97
98
  const tempDir = this.getProductionTempDirectory();
98
99
  if (!fs.existsSync(tempDir)) {
99
100
  fs.mkdirSync(tempDir, { recursive: true });
100
- console.log(`✅ Created temporary directory: ${tempDir}`);
101
+ logger.log(`✅ Created temporary directory: ${tempDir}`);
101
102
  }
102
103
  }
103
104
  catch (error) {
@@ -135,7 +136,7 @@ export class ProductionGitignoreManager {
135
136
  const generatedDir = this.config.generatedStoriesPath;
136
137
  if (!fs.existsSync(generatedDir)) {
137
138
  fs.mkdirSync(generatedDir, { recursive: true });
138
- console.log(`✅ Created generated stories directory: ${generatedDir}`);
139
+ logger.log(`✅ Created generated stories directory: ${generatedDir}`);
139
140
  }
140
141
  }
141
142
  /**
@@ -156,7 +157,7 @@ export class ProductionGitignoreManager {
156
157
  // Check if the path is already ignored
157
158
  const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
158
159
  if (this.isPathIgnored(gitignoreContent, generatedPath)) {
159
- console.log(`✅ Generated stories directory already ignored: ${generatedPath}`);
160
+ logger.log(`✅ Generated stories directory already ignored: ${generatedPath}`);
160
161
  return;
161
162
  }
162
163
  // Add the ignore rule
@@ -187,7 +188,7 @@ export class ProductionGitignoreManager {
187
188
  createGitignore(gitignorePath, generatedPath) {
188
189
  const content = this.generateGitignoreSection(generatedPath);
189
190
  fs.writeFileSync(gitignorePath, content);
190
- console.log(`✅ Created .gitignore with Story UI generated directory: ${generatedPath}`);
191
+ logger.log(`✅ Created .gitignore with Story UI generated directory: ${generatedPath}`);
191
192
  }
192
193
  /**
193
194
  * Checks if the generated path is already ignored
@@ -214,7 +215,7 @@ export class ProductionGitignoreManager {
214
215
  const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
215
216
  const updatedContent = existingContent + separator + newSection;
216
217
  fs.writeFileSync(gitignorePath, updatedContent);
217
- console.log(`✅ Added Story UI generated directory to .gitignore: ${generatedPath}`);
218
+ logger.log(`✅ Added Story UI generated directory to .gitignore: ${generatedPath}`);
218
219
  }
219
220
  /**
220
221
  * Generates the gitignore section for Story UI
@@ -260,7 +261,7 @@ Generated by [Story UI](https://github.com/your-org/story-ui) - AI-powered Story
260
261
  `;
261
262
  try {
262
263
  fs.writeFileSync(readmePath, readmeContent);
263
- console.log(`✅ Created README in generated directory`);
264
+ logger.log(`✅ Created README in generated directory`);
264
265
  }
265
266
  catch (error) {
266
267
  console.warn('âš ī¸ Could not create README in generated directory');
@@ -293,7 +294,7 @@ Generated by [Story UI](https://github.com/your-org/story-ui) - AI-powered Story
293
294
  }
294
295
  }
295
296
  if (cleanedCount > 0) {
296
- console.log(`🧹 Cleaned up ${cleanedCount} old generated stories from ${dir}`);
297
+ logger.log(`🧹 Cleaned up ${cleanedCount} old generated stories from ${dir}`);
297
298
  }
298
299
  }
299
300
  catch (error) {
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { logger } from './logger.js';
3
4
  export class StoryTracker {
4
5
  constructor(config) {
5
6
  this.mappingFile = path.join(path.dirname(config.generatedStoriesPath), '.story-mappings.json');
@@ -88,7 +89,7 @@ export class StoryTracker {
88
89
  // This prevents false positives like "card" matching "card layouts" vs "card animations"
89
90
  const similarityThreshold = Math.max(4, Math.floor(promptKeywords.length * 0.9));
90
91
  if (sharedKeywords.length >= similarityThreshold && promptKeywords.length >= 4) {
91
- console.log(`🔄 Found similar story: "${mapping.title}" (${sharedKeywords.length}/${promptKeywords.length} keywords match)`);
92
+ logger.log(`🔄 Found similar story: "${mapping.title}" (${sharedKeywords.length}/${promptKeywords.length} keywords match)`);
92
93
  return mapping;
93
94
  }
94
95
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { logger } from './logger.js';
3
4
  /**
4
5
  * Universal adapter to make Story UI work with any React design system
5
6
  */
@@ -14,7 +15,7 @@ export class UniversalDesignSystemAdapter {
14
15
  async detectDesignSystems() {
15
16
  const packageJsonPath = path.join(this.projectRoot, 'package.json');
16
17
  if (!fs.existsSync(packageJsonPath)) {
17
- console.log('đŸ“Ļ No package.json found for design system detection');
18
+ logger.log('đŸ“Ļ No package.json found for design system detection');
18
19
  return [];
19
20
  }
20
21
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
@@ -29,7 +30,7 @@ export class UniversalDesignSystemAdapter {
29
30
  this.checkForAntDesign(allDeps);
30
31
  this.checkForMantine(allDeps);
31
32
  this.checkForGenericReactComponents(allDeps);
32
- console.log(`🎨 Detected ${this.detectedSystems.length} design systems:`, this.detectedSystems.map(ds => ds.name));
33
+ logger.log(`🎨 Detected ${this.detectedSystems.length} design systems:`, this.detectedSystems.map(ds => ds.name));
33
34
  return this.detectedSystems;
34
35
  }
35
36
  /**
@@ -0,0 +1,140 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { logger } from './logger.js';
4
+ export class UrlRedirectService {
5
+ constructor(storageDir) {
6
+ this.redirects = new Map();
7
+ this.redirectsPath = path.join(storageDir, '.story-ui', 'redirects.json');
8
+ this.loadRedirects();
9
+ }
10
+ ensureDirectoryExists() {
11
+ const dir = path.dirname(this.redirectsPath);
12
+ if (!fs.existsSync(dir)) {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+ }
16
+ loadRedirects() {
17
+ try {
18
+ if (fs.existsSync(this.redirectsPath)) {
19
+ const data = fs.readFileSync(this.redirectsPath, 'utf-8');
20
+ const redirectArray = JSON.parse(data);
21
+ this.redirects = new Map(redirectArray.map(r => [r.oldUrl, r]));
22
+ logger.log(`📍 Loaded ${this.redirects.size} URL redirects`);
23
+ }
24
+ }
25
+ catch (error) {
26
+ logger.log('âš ī¸ Failed to load redirects, starting fresh:', error);
27
+ this.redirects = new Map();
28
+ }
29
+ }
30
+ saveRedirects() {
31
+ try {
32
+ this.ensureDirectoryExists();
33
+ const redirectArray = Array.from(this.redirects.values());
34
+ fs.writeFileSync(this.redirectsPath, JSON.stringify(redirectArray, null, 2));
35
+ logger.log(`💾 Saved ${redirectArray.length} URL redirects`);
36
+ }
37
+ catch (error) {
38
+ logger.log('❌ Failed to save redirects:', error);
39
+ }
40
+ }
41
+ addRedirect(oldUrl, newUrl, oldTitle, newTitle, storyId) {
42
+ // Don't create a redirect if URLs are the same
43
+ if (oldUrl === newUrl) {
44
+ return;
45
+ }
46
+ const redirect = {
47
+ oldUrl,
48
+ newUrl,
49
+ oldTitle,
50
+ newTitle,
51
+ timestamp: new Date().toISOString(),
52
+ storyId
53
+ };
54
+ this.redirects.set(oldUrl, redirect);
55
+ // Also handle redirect chains (if A->B exists and we add B->C, update A->C)
56
+ for (const [url, existingRedirect] of this.redirects.entries()) {
57
+ if (existingRedirect.newUrl === oldUrl) {
58
+ existingRedirect.newUrl = newUrl;
59
+ existingRedirect.newTitle = newTitle;
60
+ logger.log(`🔄 Updated redirect chain: ${url} → ${newUrl}`);
61
+ }
62
+ }
63
+ this.saveRedirects();
64
+ logger.log(`âžĄī¸ Added redirect: ${oldUrl} → ${newUrl}`);
65
+ }
66
+ getRedirect(oldUrl) {
67
+ return this.redirects.get(oldUrl) || null;
68
+ }
69
+ getAllRedirects() {
70
+ return Array.from(this.redirects.values());
71
+ }
72
+ // Get a JavaScript snippet that can be injected into Storybook
73
+ getRedirectScript() {
74
+ const redirectMap = Object.fromEntries(Array.from(this.redirects.entries()).map(([oldUrl, redirect]) => [
75
+ oldUrl,
76
+ { newUrl: redirect.newUrl, message: `Story updated: "${redirect.oldTitle}" → "${redirect.newTitle}"` }
77
+ ]));
78
+ return `
79
+ // Story UI URL Redirect Handler
80
+ (function() {
81
+ const redirects = ${JSON.stringify(redirectMap, null, 2)};
82
+
83
+ function checkForRedirect() {
84
+ const currentPath = window.location.pathname + window.location.search;
85
+
86
+ for (const [oldUrl, redirect] of Object.entries(redirects)) {
87
+ if (currentPath.includes(oldUrl)) {
88
+ console.log('[Story UI] Redirecting from', oldUrl, 'to', redirect.newUrl);
89
+
90
+ // Show a brief notification
91
+ const notification = document.createElement('div');
92
+ notification.style.cssText = \`
93
+ position: fixed;
94
+ top: 20px;
95
+ right: 20px;
96
+ background: #1890ff;
97
+ color: white;
98
+ padding: 12px 20px;
99
+ border-radius: 4px;
100
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
101
+ z-index: 9999;
102
+ font-family: sans-serif;
103
+ font-size: 14px;
104
+ \`;
105
+ notification.textContent = redirect.message;
106
+ document.body.appendChild(notification);
107
+
108
+ // Redirect after a brief delay
109
+ setTimeout(() => {
110
+ window.location.href = redirect.newUrl;
111
+ }, 1500);
112
+
113
+ // Remove notification after redirect
114
+ setTimeout(() => notification.remove(), 1400);
115
+
116
+ return true;
117
+ }
118
+ }
119
+ return false;
120
+ }
121
+
122
+ // Check on page load
123
+ if (window.location.pathname.includes('/story/')) {
124
+ checkForRedirect();
125
+ }
126
+
127
+ // Also check when navigation occurs in single-page app
128
+ let lastPath = window.location.pathname;
129
+ setInterval(() => {
130
+ if (window.location.pathname !== lastPath) {
131
+ lastPath = window.location.pathname;
132
+ if (window.location.pathname.includes('/story/')) {
133
+ checkForRedirect();
134
+ }
135
+ }
136
+ }, 100);
137
+ })();
138
+ `;
139
+ }
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "2.1.5",
3
+ "version": "2.2.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "build": "tsc",
20
20
  "start": "yarn build && node dist/mcp-server/index.js",
21
+ "mcp": "yarn build && node dist/mcp-server/mcp-stdio-server.js",
21
22
  "dev": "tsc --watch",
22
23
  "prepublishOnly": "npm run build",
23
24
  "test": "echo \"Tests coming soon\" && exit 0",
@@ -60,6 +61,10 @@
60
61
  "registry": "https://registry.npmjs.org/"
61
62
  },
62
63
  "dependencies": {
64
+ "@emotion/react": "^11.14.0",
65
+ "@emotion/styled": "^11.14.1",
66
+ "@modelcontextprotocol/sdk": "^0.5.0",
67
+ "@mui/material": "^7.2.0",
63
68
  "chalk": "^5.3.0",
64
69
  "commander": "^11.0.0",
65
70
  "cors": "^2.8.5",
@@ -68,7 +73,8 @@
68
73
  "glob": "^11.0.3",
69
74
  "inquirer": "^9.2.0",
70
75
  "node-fetch": "^2.6.7",
71
- "typescript": "^5.8.3"
76
+ "typescript": "^5.8.3",
77
+ "zod": "^3.22.4"
72
78
  },
73
79
  "devDependencies": {
74
80
  "@commitlint/cli": "^19.6.1",
@@ -107,5 +113,16 @@
107
113
  "commitizen": {
108
114
  "path": "./node_modules/cz-conventional-changelog"
109
115
  }
116
+ },
117
+ "mcp": {
118
+ "name": "story-ui",
119
+ "description": "AI-powered Storybook story generator with dynamic component discovery",
120
+ "command": "node",
121
+ "args": [
122
+ "dist/mcp-server/mcp-stdio-server.js"
123
+ ],
124
+ "env": {
125
+ "NODE_ENV": "production"
126
+ }
110
127
  }
111
128
  }
@@ -93,19 +93,28 @@ const syncWithActualStories = async (): Promise<ChatSession[]> => {
93
93
  // Load existing chats
94
94
  const existingChats = loadChats();
95
95
 
96
- // Create a map for quick lookup
96
+ // Create a map for quick lookup - using chat.id as the primary key
97
97
  const chatMap = new Map<string, ChatSession>();
98
98
  existingChats.forEach(chat => {
99
99
  chatMap.set(chat.id, chat);
100
- if (chat.fileName) {
101
- chatMap.set(chat.fileName, chat);
102
- }
103
100
  });
104
101
 
105
102
  // Update or add memory stories
106
103
  memoryStories.forEach((story: any) => {
107
104
  const storyId = story.storyId || story.fileName;
108
- const existingChat = chatMap.get(storyId) || chatMap.get(story.fileName);
105
+
106
+ // Look for existing chat by ID or by matching fileName
107
+ let existingChat = chatMap.get(storyId);
108
+
109
+ // If not found by ID, search by fileName
110
+ if (!existingChat && story.fileName) {
111
+ for (const [id, chat] of chatMap.entries()) {
112
+ if (chat.fileName === story.fileName) {
113
+ existingChat = chat;
114
+ break;
115
+ }
116
+ }
117
+ }
109
118
 
110
119
  if (existingChat) {
111
120
  // Update existing chat with latest info
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "story-ui": {
4
+ "command": "npx",
5
+ "args": ["@tpitre/story-ui", "mcp"],
6
+ "env": {
7
+ "CLAUDE_API_KEY": "your-claude-api-key-here"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,76 @@
1
+ # Story UI MCP Example
2
+
3
+ This example shows how to use Story UI with Claude Desktop via MCP.
4
+
5
+ ## Example Conversation
6
+
7
+ **You:** Use Story UI to generate a hero section with a gradient background, a main title "Welcome to Our Platform", a subtitle "Build amazing experiences", and two buttons - "Get Started" (primary) and "Learn More" (secondary).
8
+
9
+ **Claude (using Story UI MCP):** I'll help you create a hero section using Story UI. Let me generate that for you.
10
+
11
+ *[Claude would then use the generate-story tool]*
12
+
13
+ The story has been generated successfully! Here's what was created:
14
+
15
+ **Title:** Hero Section with Gradient Background
16
+ **ID:** hero-gradient-1234
17
+
18
+ The generated story includes:
19
+ - A hero section with a beautiful gradient background
20
+ - Main title: "Welcome to Our Platform"
21
+ - Subtitle: "Build amazing experiences"
22
+ - Two buttons with proper styling:
23
+ - "Get Started" button with primary variant
24
+ - "Learn More" button with secondary variant
25
+
26
+ You can now see this story in your Storybook instance under the "Generated" category.
27
+
28
+ **You:** Can you show me what components are available for building layouts?
29
+
30
+ **Claude:** Let me check what layout components are available in your design system.
31
+
32
+ *[Claude would use the list-components tool with category filter]*
33
+
34
+ I found the following layout components available:
35
+ - Grid: Flexible grid system for responsive layouts
36
+ - Container: Page container with max-width constraints
37
+ - Row: Horizontal layout component
38
+ - Column: Vertical layout component
39
+ - Stack: Stacks children vertically or horizontally with spacing
40
+ - Spacer: Adds space between components
41
+
42
+ **You:** Great! Now modify the hero section to use a Grid with two columns - put the text content on the left and add an illustration placeholder on the right.
43
+
44
+ **Claude:** I'll help you iterate on the existing hero section to add a two-column layout with the content on the left and space for an illustration on the right.
45
+
46
+ *[Claude would use generate-story again with the iteration prompt]*
47
+
48
+ The hero section has been updated! The story now features:
49
+ - A Grid component with two equal columns
50
+ - Left column: Contains the title, subtitle, and buttons
51
+ - Right column: Includes a placeholder for an illustration
52
+ - Responsive design that stacks on mobile devices
53
+
54
+ The iteration preserved your existing content while restructuring it into the requested layout.
55
+
56
+ ## Common Workflows
57
+
58
+ ### 1. Component Discovery
59
+ "What Button variants are available in my design system?"
60
+ "Show me all form-related components"
61
+ "What props does the Card component accept?"
62
+
63
+ ### 2. Story Generation
64
+ "Create a login form with email, password, and remember me checkbox"
65
+ "Build a pricing card with a featured badge"
66
+ "Generate a navigation bar with logo and menu items"
67
+
68
+ ### 3. Story Management
69
+ "List all stories I've created today"
70
+ "Show me the story with ID abc-123"
71
+ "Delete the old hero section story"
72
+
73
+ ### 4. Iterative Design
74
+ "Add a dark mode variant to the current story"
75
+ "Make the buttons larger and add icons"
76
+ "Change the color scheme to use brand colors"