@tpitre/story-ui 1.0.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.
@@ -0,0 +1,482 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { DEFAULT_CONFIG, createStoryUIConfig } from '../story-ui.config.js';
4
+ // Config cache to prevent excessive loading
5
+ let cachedConfig = null;
6
+ let configLoadTime = 0;
7
+ const CONFIG_CACHE_TTL = 30000; // 30 seconds
8
+ /**
9
+ * Loads Story UI configuration from the user's project
10
+ * Looks for story-ui.config.js in the current working directory
11
+ * Uses caching to prevent excessive loading
12
+ */
13
+ export function loadUserConfig() {
14
+ const now = Date.now();
15
+ // Return cached config if still valid
16
+ if (cachedConfig && (now - configLoadTime) < CONFIG_CACHE_TTL) {
17
+ return cachedConfig;
18
+ }
19
+ const configPaths = [
20
+ path.join(process.cwd(), 'story-ui.config.js'),
21
+ path.join(process.cwd(), 'story-ui.config.ts'),
22
+ path.join(process.cwd(), '.storybook', 'story-ui.config.js'),
23
+ path.join(process.cwd(), '.storybook', 'story-ui.config.ts')
24
+ ];
25
+ for (const configPath of configPaths) {
26
+ if (fs.existsSync(configPath)) {
27
+ try {
28
+ console.log(`Loading Story UI config from: ${configPath}`);
29
+ // Read and evaluate the config file
30
+ const configContent = fs.readFileSync(configPath, 'utf-8');
31
+ // Simple evaluation for CommonJS modules
32
+ if (configContent.includes('module.exports')) {
33
+ // Create a temporary module context
34
+ const module = { exports: {} };
35
+ const exports = module.exports;
36
+ // Evaluate the config file content
37
+ eval(configContent);
38
+ const userConfig = module.exports;
39
+ const config = createStoryUIConfig(userConfig.default || userConfig);
40
+ // Cache the loaded config
41
+ cachedConfig = config;
42
+ configLoadTime = now;
43
+ return config;
44
+ }
45
+ }
46
+ catch (error) {
47
+ console.warn(`Failed to load config from ${configPath}:`, error);
48
+ }
49
+ }
50
+ }
51
+ // Only log warnings once per cache period
52
+ if (!cachedConfig || (now - configLoadTime) >= CONFIG_CACHE_TTL) {
53
+ console.warn('No story-ui.config.js found. Using default configuration.');
54
+ console.warn('Please create a story-ui.config.js file in your project root to configure Story UI for your design system.');
55
+ }
56
+ // Cache the default config
57
+ cachedConfig = DEFAULT_CONFIG;
58
+ configLoadTime = now;
59
+ return DEFAULT_CONFIG;
60
+ }
61
+ /**
62
+ * Validates that the configuration has the necessary paths and components
63
+ */
64
+ export function validateConfig(config) {
65
+ const errors = [];
66
+ // Check if generated stories path exists or can be created
67
+ if (!config.generatedStoriesPath) {
68
+ errors.push('generatedStoriesPath is required');
69
+ }
70
+ else {
71
+ const dir = path.dirname(config.generatedStoriesPath);
72
+ if (!fs.existsSync(dir)) {
73
+ try {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ }
76
+ catch (error) {
77
+ errors.push(`Cannot create generated stories directory: ${dir}`);
78
+ }
79
+ }
80
+ }
81
+ // Check if components can be discovered
82
+ if (!config.componentsPath && !config.componentsMetadataPath) {
83
+ errors.push('Either componentsPath or componentsMetadataPath must be specified');
84
+ }
85
+ if (config.componentsPath && !fs.existsSync(config.componentsPath)) {
86
+ errors.push(`Components path does not exist: ${config.componentsPath}`);
87
+ }
88
+ if (config.componentsMetadataPath && !fs.existsSync(config.componentsMetadataPath)) {
89
+ errors.push(`Components metadata path does not exist: ${config.componentsMetadataPath}`);
90
+ }
91
+ // Check import path
92
+ if (!config.importPath || config.importPath === 'your-component-library' || config.importPath.trim() === '') {
93
+ errors.push('importPath must be configured to point to your component library');
94
+ }
95
+ return {
96
+ isValid: errors.length === 0,
97
+ errors
98
+ };
99
+ }
100
+ /**
101
+ * Analyzes existing Storybook files to detect design system patterns
102
+ */
103
+ export function analyzeExistingStories(projectRoot = process.cwd()) {
104
+ const storyFiles = [];
105
+ const componentDirs = [];
106
+ const importPaths = [];
107
+ const componentPrefixes = [];
108
+ const layoutPatterns = [];
109
+ // Find all .stories.tsx/.stories.ts files
110
+ function findStoryFiles(dir, depth = 0) {
111
+ if (depth > 4)
112
+ return; // Limit recursion depth
113
+ try {
114
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
115
+ for (const entry of entries) {
116
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
117
+ continue;
118
+ const fullPath = path.join(dir, entry.name);
119
+ if (entry.isDirectory()) {
120
+ findStoryFiles(fullPath, depth + 1);
121
+ }
122
+ else if (entry.name.match(/\.stories\.(tsx?|jsx?)$/)) {
123
+ storyFiles.push(fullPath);
124
+ // Track component directory (parent of story file)
125
+ const componentDir = path.dirname(fullPath);
126
+ if (!componentDirs.includes(componentDir)) {
127
+ componentDirs.push(componentDir);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ catch (error) {
133
+ // Skip directories we can't read
134
+ }
135
+ }
136
+ findStoryFiles(projectRoot);
137
+ // Analyze story files for patterns
138
+ for (const storyFile of storyFiles) {
139
+ try {
140
+ const content = fs.readFileSync(storyFile, 'utf-8');
141
+ // Extract import statements
142
+ const importMatches = content.match(/import\s+{[^}]+}\s+from\s+['"]([^'"]+)['"]/g);
143
+ if (importMatches) {
144
+ for (const importMatch of importMatches) {
145
+ const pathMatch = importMatch.match(/from\s+['"]([^'"]+)['"]/);
146
+ if (pathMatch) {
147
+ const importPath = pathMatch[1];
148
+ // Skip relative imports and focus on package imports
149
+ if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
150
+ if (!importPaths.includes(importPath)) {
151
+ importPaths.push(importPath);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ // Extract component names to detect prefixes
158
+ const componentMatches = content.match(/<([A-Z][A-Za-z0-9]*)/g);
159
+ if (componentMatches) {
160
+ for (const match of componentMatches) {
161
+ const componentName = match.slice(1); // Remove '<'
162
+ // Detect common prefixes (2-3 characters)
163
+ const prefixMatch = componentName.match(/^([A-Z]{1,3})[A-Z]/);
164
+ if (prefixMatch) {
165
+ const prefix = prefixMatch[1];
166
+ if (!componentPrefixes.includes(prefix)) {
167
+ componentPrefixes.push(prefix);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ // Look for layout patterns
173
+ const layoutMatches = content.match(/<(Grid|Row|Col|Box|Stack|Flex|Layout|Container|Section)[^>]*>/g);
174
+ if (layoutMatches) {
175
+ for (const match of layoutMatches) {
176
+ if (!layoutPatterns.includes(match)) {
177
+ layoutPatterns.push(match);
178
+ }
179
+ }
180
+ }
181
+ }
182
+ catch (error) {
183
+ // Skip files we can't read
184
+ }
185
+ }
186
+ return {
187
+ storyFiles,
188
+ componentDirs,
189
+ importPaths,
190
+ componentPrefixes,
191
+ layoutPatterns
192
+ };
193
+ }
194
+ /**
195
+ * Auto-detects design system configuration by analyzing the project structure
196
+ */
197
+ export function autoDetectDesignSystem() {
198
+ const cwd = process.cwd();
199
+ const packageJsonPath = path.join(cwd, 'package.json');
200
+ if (!fs.existsSync(packageJsonPath)) {
201
+ return null;
202
+ }
203
+ try {
204
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
205
+ const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
206
+ // First, try to detect known design systems from package.json
207
+ const knownSystems = detectKnownDesignSystems(dependencies);
208
+ if (knownSystems) {
209
+ console.log(`🎨 Detected known design system: ${knownSystems.importPath}`);
210
+ }
211
+ // Analyze existing Storybook files for patterns
212
+ const analysis = analyzeExistingStories(cwd);
213
+ console.log(`📊 Analysis found: ${analysis.storyFiles.length} story files, ${analysis.componentDirs.length} component directories`);
214
+ // Determine the most likely component directory
215
+ const componentPath = findMostLikelyComponentDirectory(analysis.componentDirs, cwd);
216
+ // Determine the most likely import path
217
+ const importPath = findMostLikelyImportPath(analysis.importPaths, packageJson.name);
218
+ // Determine component prefix
219
+ const componentPrefix = findMostLikelyPrefix(analysis.componentPrefixes);
220
+ // Determine layout patterns
221
+ const layoutRules = detectLayoutPatterns(analysis.layoutPatterns, componentPrefix);
222
+ // Build configuration
223
+ const config = {
224
+ generatedStoriesPath: path.join(cwd, 'src/stories/generated/'),
225
+ componentsPath: componentPath,
226
+ importPath: importPath,
227
+ componentPrefix: componentPrefix,
228
+ layoutRules: layoutRules
229
+ };
230
+ // Merge with known system config if available
231
+ if (knownSystems) {
232
+ return { ...knownSystems, ...config };
233
+ }
234
+ return config;
235
+ }
236
+ catch (error) {
237
+ console.warn('Failed to auto-detect design system:', error);
238
+ return null;
239
+ }
240
+ }
241
+ /**
242
+ * Detects known design systems from package.json dependencies
243
+ */
244
+ function detectKnownDesignSystems(dependencies) {
245
+ // Material-UI detection
246
+ if (dependencies['@mui/material']) {
247
+ return {
248
+ importPath: '@mui/material',
249
+ componentPrefix: '',
250
+ layoutRules: {
251
+ multiColumnWrapper: 'Grid',
252
+ columnComponent: 'Grid',
253
+ containerComponent: 'Container',
254
+ layoutExamples: {
255
+ twoColumn: `<Grid container spacing={2}>
256
+ <Grid item xs={6}>
257
+ <Card>
258
+ <CardContent>
259
+ <Typography variant="h5">Left Card</Typography>
260
+ <Typography>Left content</Typography>
261
+ </CardContent>
262
+ </Card>
263
+ </Grid>
264
+ <Grid item xs={6}>
265
+ <Card>
266
+ <CardContent>
267
+ <Typography variant="h5">Right Card</Typography>
268
+ <Typography>Right content</Typography>
269
+ </CardContent>
270
+ </Card>
271
+ </Grid>
272
+ </Grid>`
273
+ }
274
+ }
275
+ };
276
+ }
277
+ // Chakra UI detection
278
+ if (dependencies['@chakra-ui/react']) {
279
+ return {
280
+ importPath: '@chakra-ui/react',
281
+ componentPrefix: '',
282
+ layoutRules: {
283
+ multiColumnWrapper: 'SimpleGrid',
284
+ columnComponent: 'Box',
285
+ containerComponent: 'Container'
286
+ }
287
+ };
288
+ }
289
+ // Ant Design detection
290
+ if (dependencies['antd']) {
291
+ return {
292
+ importPath: 'antd',
293
+ componentPrefix: '',
294
+ layoutRules: {
295
+ multiColumnWrapper: 'Row',
296
+ columnComponent: 'Col',
297
+ containerComponent: 'div'
298
+ }
299
+ };
300
+ }
301
+ // Mantine detection
302
+ if (dependencies['@mantine/core']) {
303
+ return {
304
+ importPath: '@mantine/core',
305
+ componentPrefix: '',
306
+ layoutRules: {
307
+ multiColumnWrapper: 'SimpleGrid',
308
+ columnComponent: 'div',
309
+ containerComponent: 'Container'
310
+ }
311
+ };
312
+ }
313
+ // ShadCN/UI detection
314
+ if (dependencies['@radix-ui/react-slot'] || dependencies['class-variance-authority']) {
315
+ return {
316
+ importPath: '@/components/ui',
317
+ componentPrefix: '',
318
+ layoutRules: {
319
+ multiColumnWrapper: 'div',
320
+ columnComponent: 'div',
321
+ containerComponent: 'div',
322
+ layoutExamples: {
323
+ twoColumn: `<div className="grid grid-cols-2 gap-4">
324
+ <div>
325
+ <Card>
326
+ <CardHeader>
327
+ <CardTitle>Left Card</CardTitle>
328
+ </CardHeader>
329
+ <CardContent>
330
+ <p>Left content</p>
331
+ </CardContent>
332
+ </Card>
333
+ </div>
334
+ <div>
335
+ <Card>
336
+ <CardHeader>
337
+ <CardTitle>Right Card</CardTitle>
338
+ </CardHeader>
339
+ <CardContent>
340
+ <p>Right content</p>
341
+ </CardContent>
342
+ </Card>
343
+ </div>
344
+ </div>`
345
+ }
346
+ }
347
+ };
348
+ }
349
+ return null;
350
+ }
351
+ /**
352
+ * Finds the most likely component directory based on story file locations
353
+ */
354
+ function findMostLikelyComponentDirectory(componentDirs, projectRoot) {
355
+ if (componentDirs.length === 0) {
356
+ // Fallback to common patterns
357
+ const commonPaths = [
358
+ path.join(projectRoot, 'src/components'),
359
+ path.join(projectRoot, 'components'),
360
+ path.join(projectRoot, 'lib/components'),
361
+ path.join(projectRoot, 'src/ui'),
362
+ path.join(projectRoot, 'ui')
363
+ ];
364
+ for (const commonPath of commonPaths) {
365
+ if (fs.existsSync(commonPath)) {
366
+ return commonPath;
367
+ }
368
+ }
369
+ return path.join(projectRoot, 'src/components');
370
+ }
371
+ // Find the common parent directory of most story files
372
+ const dirCounts = {};
373
+ for (const dir of componentDirs) {
374
+ // Count occurrences of parent directories
375
+ let currentDir = dir;
376
+ while (currentDir !== projectRoot && currentDir !== path.dirname(currentDir)) {
377
+ dirCounts[currentDir] = (dirCounts[currentDir] || 0) + 1;
378
+ currentDir = path.dirname(currentDir);
379
+ }
380
+ }
381
+ // Find the directory with the most story files
382
+ let maxCount = 0;
383
+ let bestDir = path.join(projectRoot, 'src/components');
384
+ for (const [dir, count] of Object.entries(dirCounts)) {
385
+ if (count > maxCount) {
386
+ maxCount = count;
387
+ bestDir = dir;
388
+ }
389
+ }
390
+ return bestDir;
391
+ }
392
+ /**
393
+ * Finds the most likely import path based on import analysis
394
+ */
395
+ function findMostLikelyImportPath(importPaths, packageName) {
396
+ if (importPaths.length === 0) {
397
+ return packageName || 'your-component-library';
398
+ }
399
+ // Count frequency of import paths
400
+ const pathCounts = {};
401
+ for (const importPath of importPaths) {
402
+ // Skip common non-component libraries
403
+ if (importPath.includes('react') || importPath.includes('storybook') ||
404
+ importPath.includes('testing') || importPath.includes('jest')) {
405
+ continue;
406
+ }
407
+ pathCounts[importPath] = (pathCounts[importPath] || 0) + 1;
408
+ }
409
+ // Find the most common import path
410
+ let maxCount = 0;
411
+ let bestPath = packageName || 'your-component-library';
412
+ for (const [importPath, count] of Object.entries(pathCounts)) {
413
+ if (count > maxCount) {
414
+ maxCount = count;
415
+ bestPath = importPath;
416
+ }
417
+ }
418
+ return bestPath;
419
+ }
420
+ /**
421
+ * Finds the most likely component prefix
422
+ */
423
+ function findMostLikelyPrefix(componentPrefixes) {
424
+ if (componentPrefixes.length === 0) {
425
+ return '';
426
+ }
427
+ // Count frequency of prefixes
428
+ const prefixCounts = {};
429
+ for (const prefix of componentPrefixes) {
430
+ prefixCounts[prefix] = (prefixCounts[prefix] || 0) + 1;
431
+ }
432
+ // Find the most common prefix
433
+ let maxCount = 0;
434
+ let bestPrefix = '';
435
+ for (const [prefix, count] of Object.entries(prefixCounts)) {
436
+ if (count > maxCount && count > 2) { // Only consider prefixes used multiple times
437
+ maxCount = count;
438
+ bestPrefix = prefix;
439
+ }
440
+ }
441
+ return bestPrefix;
442
+ }
443
+ /**
444
+ * Detects layout patterns from existing components
445
+ */
446
+ function detectLayoutPatterns(layoutPatterns, componentPrefix) {
447
+ const rules = {
448
+ multiColumnWrapper: 'div',
449
+ columnComponent: 'div',
450
+ containerComponent: 'div',
451
+ layoutExamples: {
452
+ twoColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}>
453
+ <div>Column 1 content</div>
454
+ <div>Column 2 content</div>
455
+ </div>`
456
+ },
457
+ prohibitedElements: []
458
+ };
459
+ // Analyze layout patterns to determine best components
460
+ for (const pattern of layoutPatterns) {
461
+ if (pattern.includes('Grid')) {
462
+ rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Grid` : 'Grid';
463
+ rules.columnComponent = componentPrefix ? `${componentPrefix}Grid` : 'Grid';
464
+ }
465
+ else if (pattern.includes('Row') && pattern.includes('Col')) {
466
+ rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Row` : 'Row';
467
+ rules.columnComponent = componentPrefix ? `${componentPrefix}Col` : 'Col';
468
+ }
469
+ else if (pattern.includes('Stack')) {
470
+ rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Stack` : 'Stack';
471
+ rules.columnComponent = componentPrefix ? `${componentPrefix}Box` : 'Box';
472
+ }
473
+ else if (pattern.includes('Layout')) {
474
+ rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Layout` : 'Layout';
475
+ rules.columnComponent = componentPrefix ? `${componentPrefix}LayoutSection` : 'LayoutSection';
476
+ }
477
+ if (pattern.includes('Container')) {
478
+ rules.containerComponent = componentPrefix ? `${componentPrefix}Container` : 'Container';
479
+ }
480
+ }
481
+ return rules;
482
+ }
@@ -0,0 +1,19 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { STORY_UI_CONFIG } from '../story-ui.config.js';
4
+ function slugify(str) {
5
+ return str
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, '-')
8
+ .replace(/^-+|-+$/g, '');
9
+ }
10
+ export function generateStory({ fileContents, fileName }) {
11
+ const outPath = path.join(STORY_UI_CONFIG.generatedStoriesPath, fileName);
12
+ fs.writeFileSync(outPath, fileContents, 'utf-8');
13
+ return outPath;
14
+ }
15
+ // Mock usage:
16
+ // generateStory({
17
+ // title: 'Login Form',
18
+ // jsx: '<al-input label="Email"></al-input>\n<al-input label="Password" type="password"></al-input>\n<al-button>Login</al-button>'
19
+ // });
@@ -0,0 +1,182 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Manages .gitignore entries for Story UI generated content
5
+ */
6
+ export class GitignoreManager {
7
+ constructor(config, projectRoot = process.cwd()) {
8
+ this.config = config;
9
+ this.projectRoot = projectRoot;
10
+ }
11
+ /**
12
+ * Ensures the generated stories directory is added to .gitignore
13
+ */
14
+ ensureGeneratedDirectoryIgnored() {
15
+ const gitignorePath = path.join(this.projectRoot, '.gitignore');
16
+ const generatedPath = this.getRelativeGeneratedPath();
17
+ if (!generatedPath) {
18
+ console.warn('Could not determine relative path for generated stories directory');
19
+ return;
20
+ }
21
+ // Create .gitignore if it doesn't exist
22
+ if (!fs.existsSync(gitignorePath)) {
23
+ this.createGitignore(gitignorePath, generatedPath);
24
+ return;
25
+ }
26
+ // Check if the path is already ignored
27
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
28
+ if (this.isPathIgnored(gitignoreContent, generatedPath)) {
29
+ console.log(`✅ Generated stories directory already ignored: ${generatedPath}`);
30
+ return;
31
+ }
32
+ // Add the ignore rule
33
+ this.addIgnoreRule(gitignorePath, generatedPath);
34
+ }
35
+ /**
36
+ * Gets the relative path from project root to generated stories directory
37
+ */
38
+ getRelativeGeneratedPath() {
39
+ try {
40
+ const absoluteGeneratedPath = path.resolve(this.config.generatedStoriesPath);
41
+ const absoluteProjectRoot = path.resolve(this.projectRoot);
42
+ // Get relative path from project root to generated directory
43
+ let relativePath = path.relative(absoluteProjectRoot, absoluteGeneratedPath);
44
+ // Normalize path separators for cross-platform compatibility
45
+ relativePath = relativePath.replace(/\\/g, '/');
46
+ // Ensure it starts with ./ if it's a relative path
47
+ if (!relativePath.startsWith('../') && !relativePath.startsWith('/')) {
48
+ relativePath = './' + relativePath;
49
+ }
50
+ return relativePath;
51
+ }
52
+ catch (error) {
53
+ console.error('Error calculating relative path:', error);
54
+ return null;
55
+ }
56
+ }
57
+ /**
58
+ * Creates a new .gitignore file with Story UI section
59
+ */
60
+ createGitignore(gitignorePath, generatedPath) {
61
+ const content = this.generateGitignoreSection(generatedPath);
62
+ fs.writeFileSync(gitignorePath, content);
63
+ console.log(`✅ Created .gitignore with Story UI generated directory: ${generatedPath}`);
64
+ }
65
+ /**
66
+ * Checks if the generated path is already ignored
67
+ */
68
+ isPathIgnored(gitignoreContent, generatedPath) {
69
+ const lines = gitignoreContent.split('\n').map(line => line.trim());
70
+ // Check for exact match or parent directory match
71
+ const pathVariations = [
72
+ generatedPath,
73
+ generatedPath.replace(/^\.\//, ''),
74
+ generatedPath + '/',
75
+ generatedPath.replace(/^\.\//, '') + '/',
76
+ generatedPath + '/**',
77
+ generatedPath.replace(/^\.\//, '') + '/**'
78
+ ];
79
+ return pathVariations.some(variation => lines.includes(variation) ||
80
+ lines.includes(variation.replace(/\/$/, '')));
81
+ }
82
+ /**
83
+ * Adds ignore rule to existing .gitignore
84
+ */
85
+ addIgnoreRule(gitignorePath, generatedPath) {
86
+ const existingContent = fs.readFileSync(gitignorePath, 'utf-8');
87
+ const newSection = this.generateGitignoreSection(generatedPath);
88
+ // Add with proper spacing
89
+ const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
90
+ const updatedContent = existingContent + separator + newSection;
91
+ fs.writeFileSync(gitignorePath, updatedContent);
92
+ console.log(`✅ Added Story UI generated directory to .gitignore: ${generatedPath}`);
93
+ }
94
+ /**
95
+ * Generates the gitignore section for Story UI
96
+ */
97
+ generateGitignoreSection(generatedPath) {
98
+ return `# Story UI - AI Generated Stories (ephemeral, not for version control)
99
+ # These are temporary stories for testing layouts and should not be committed
100
+ ${generatedPath}/
101
+ ${generatedPath}/**`;
102
+ }
103
+ /**
104
+ * Creates the generated directory if it doesn't exist
105
+ */
106
+ ensureGeneratedDirectoryExists() {
107
+ const generatedDir = this.config.generatedStoriesPath;
108
+ if (!fs.existsSync(generatedDir)) {
109
+ fs.mkdirSync(generatedDir, { recursive: true });
110
+ console.log(`✅ Created generated stories directory: ${generatedDir}`);
111
+ // Create a README to explain the purpose
112
+ this.createGeneratedDirectoryReadme(generatedDir);
113
+ }
114
+ }
115
+ /**
116
+ * Creates a README in the generated directory explaining its purpose
117
+ */
118
+ createGeneratedDirectoryReadme(generatedDir) {
119
+ const readmePath = path.join(generatedDir, 'README.md');
120
+ const readmeContent = `# AI Generated Stories
121
+
122
+ This directory contains stories generated by Story UI for testing and iteration purposes.
123
+
124
+ ## ⚠️ Important Notes
125
+
126
+ - **These stories are ephemeral** - they are meant for testing layouts and sharing with stakeholders
127
+ - **Do not commit these files** - they are automatically ignored by git
128
+ - **Stories are regenerated** - feel free to delete and regenerate as needed
129
+
130
+ ## Purpose
131
+
132
+ These stories are designed for:
133
+ - 🎨 **Layout Testing** - Test different component arrangements
134
+ - 👥 **Stakeholder Review** - Share layouts with product owners, designers, and project managers
135
+ - 🔄 **Rapid Iteration** - Quickly generate and modify layouts
136
+ - 📱 **Design Validation** - Validate designs before implementation
137
+
138
+ ## Usage
139
+
140
+ Stories in this directory will appear in Storybook under the "${this.config.storyPrefix}" section.
141
+
142
+ Generated by [Story UI](https://github.com/your-org/story-ui) - AI-powered Storybook story generator.
143
+ `;
144
+ fs.writeFileSync(readmePath, readmeContent);
145
+ console.log(`✅ Created README in generated directory`);
146
+ }
147
+ /**
148
+ * Cleans up old generated stories (optional utility)
149
+ */
150
+ cleanupOldStories(maxAge = 7 * 24 * 60 * 60 * 1000) {
151
+ const generatedDir = this.config.generatedStoriesPath;
152
+ if (!fs.existsSync(generatedDir)) {
153
+ return;
154
+ }
155
+ const files = fs.readdirSync(generatedDir);
156
+ const now = Date.now();
157
+ let cleanedCount = 0;
158
+ for (const file of files) {
159
+ if (!file.endsWith('.stories.tsx')) {
160
+ continue;
161
+ }
162
+ const filePath = path.join(generatedDir, file);
163
+ const stats = fs.statSync(filePath);
164
+ const age = now - stats.mtime.getTime();
165
+ if (age > maxAge) {
166
+ fs.unlinkSync(filePath);
167
+ cleanedCount++;
168
+ }
169
+ }
170
+ if (cleanedCount > 0) {
171
+ console.log(`🧹 Cleaned up ${cleanedCount} old generated stories`);
172
+ }
173
+ }
174
+ }
175
+ /**
176
+ * Convenience function to set up gitignore for Story UI
177
+ */
178
+ export function setupGitignoreForStoryUI(config, projectRoot) {
179
+ const manager = new GitignoreManager(config, projectRoot);
180
+ manager.ensureGeneratedDirectoryExists();
181
+ manager.ensureGeneratedDirectoryIgnored();
182
+ }