@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,201 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getInMemoryStoryService } from './inMemoryStoryService.js';
4
+ import { setupProductionGitignore } from './productionGitignoreManager.js';
5
+ /**
6
+ * Story synchronization service that keeps chat interface, file system, and memory in sync
7
+ */
8
+ export class StorySyncService {
9
+ constructor(config) {
10
+ this.config = config;
11
+ const gitignoreManager = setupProductionGitignore(config);
12
+ this.isProduction = gitignoreManager.isProductionMode();
13
+ }
14
+ /**
15
+ * Gets all available stories from both file system and memory
16
+ */
17
+ async getAllStories() {
18
+ const stories = [];
19
+ if (this.isProduction) {
20
+ // Production: Get from memory
21
+ const memoryService = getInMemoryStoryService(this.config);
22
+ const memoryStories = memoryService.getAllStories();
23
+ stories.push(...memoryStories.map(story => ({
24
+ id: story.id,
25
+ title: story.title,
26
+ fileName: `${story.id}.stories.tsx`,
27
+ description: story.description,
28
+ createdAt: story.createdAt,
29
+ lastAccessed: story.lastAccessed,
30
+ source: 'memory',
31
+ content: story.content,
32
+ prompt: story.prompt
33
+ })));
34
+ }
35
+ else {
36
+ // Development: Get from file system
37
+ const fileSystemStories = await this.getFileSystemStories();
38
+ stories.push(...fileSystemStories);
39
+ // Also include any memory stories (for hybrid scenarios)
40
+ const memoryService = getInMemoryStoryService(this.config);
41
+ const memoryStories = memoryService.getAllStories();
42
+ stories.push(...memoryStories.map(story => ({
43
+ id: story.id,
44
+ title: story.title,
45
+ fileName: `${story.id}.stories.tsx`,
46
+ description: story.description,
47
+ createdAt: story.createdAt,
48
+ lastAccessed: story.lastAccessed,
49
+ source: 'memory',
50
+ content: story.content,
51
+ prompt: story.prompt
52
+ })));
53
+ }
54
+ // Sort by creation date (newest first)
55
+ return stories.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
56
+ }
57
+ /**
58
+ * Gets stories from the file system
59
+ */
60
+ async getFileSystemStories() {
61
+ const stories = [];
62
+ if (!fs.existsSync(this.config.generatedStoriesPath)) {
63
+ return stories;
64
+ }
65
+ const files = fs.readdirSync(this.config.generatedStoriesPath);
66
+ const storyFiles = files.filter(file => file.endsWith('.stories.tsx'));
67
+ for (const file of storyFiles) {
68
+ try {
69
+ const filePath = path.join(this.config.generatedStoriesPath, file);
70
+ const stats = fs.statSync(filePath);
71
+ const content = fs.readFileSync(filePath, 'utf-8');
72
+ // Extract title from file content - handle escaped quotes more robustly
73
+ let title = file.replace('.stories.tsx', ''); // fallback
74
+ // Find the title line - look for the pattern and extract until the closing quote + comma
75
+ const titleStart = content.indexOf("title: '");
76
+ if (titleStart !== -1) {
77
+ const startPos = titleStart + "title: '".length;
78
+ const endPos = content.indexOf("',", startPos);
79
+ if (endPos !== -1) {
80
+ title = content.substring(startPos, endPos);
81
+ }
82
+ }
83
+ // Remove the story prefix and unescape characters
84
+ title = title
85
+ .replace(this.config.storyPrefix, '')
86
+ .replace(/\\"/g, '"')
87
+ .replace(/\\'/g, "'")
88
+ .replace(/\\\\/g, '\\');
89
+ // Generate ID from filename
90
+ const id = file.replace('.stories.tsx', '');
91
+ stories.push({
92
+ id,
93
+ title,
94
+ fileName: file,
95
+ description: `Generated story: ${title}`,
96
+ createdAt: stats.birthtime,
97
+ lastAccessed: stats.atime,
98
+ source: 'filesystem',
99
+ content,
100
+ prompt: undefined
101
+ });
102
+ }
103
+ catch (error) {
104
+ console.warn(`Failed to read story file ${file}:`, error);
105
+ }
106
+ }
107
+ return stories;
108
+ }
109
+ /**
110
+ * Deletes a story from both file system and memory
111
+ */
112
+ async deleteStory(storyId) {
113
+ let deleted = false;
114
+ // Delete from memory
115
+ const memoryService = getInMemoryStoryService(this.config);
116
+ if (memoryService.deleteStory(storyId)) {
117
+ deleted = true;
118
+ }
119
+ // Delete from file system (if not production)
120
+ if (!this.isProduction) {
121
+ const files = fs.readdirSync(this.config.generatedStoriesPath);
122
+ const matchingFiles = files.filter(file => file.includes(storyId) || file.startsWith(storyId));
123
+ for (const file of matchingFiles) {
124
+ try {
125
+ const filePath = path.join(this.config.generatedStoriesPath, file);
126
+ fs.unlinkSync(filePath);
127
+ deleted = true;
128
+ }
129
+ catch (error) {
130
+ console.warn(`Failed to delete story file ${file}:`, error);
131
+ }
132
+ }
133
+ }
134
+ return deleted;
135
+ }
136
+ /**
137
+ * Gets a specific story by ID
138
+ */
139
+ async getStory(storyId) {
140
+ const allStories = await this.getAllStories();
141
+ return allStories.find(story => story.id === storyId) || null;
142
+ }
143
+ /**
144
+ * Clears all stories
145
+ */
146
+ async clearAllStories() {
147
+ // Clear memory
148
+ const memoryService = getInMemoryStoryService(this.config);
149
+ memoryService.clearAllStories();
150
+ // Clear file system (if not production)
151
+ if (!this.isProduction && fs.existsSync(this.config.generatedStoriesPath)) {
152
+ const files = fs.readdirSync(this.config.generatedStoriesPath);
153
+ const storyFiles = files.filter(file => file.endsWith('.stories.tsx'));
154
+ for (const file of storyFiles) {
155
+ try {
156
+ const filePath = path.join(this.config.generatedStoriesPath, file);
157
+ fs.unlinkSync(filePath);
158
+ }
159
+ catch (error) {
160
+ console.warn(`Failed to delete story file ${file}:`, error);
161
+ }
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Syncs localStorage chat history with actual stories
167
+ */
168
+ async syncChatHistory() {
169
+ const actualStories = await this.getAllStories();
170
+ // This would be called from the frontend to sync localStorage
171
+ return {
172
+ actualStories: actualStories.map(story => ({
173
+ id: story.id,
174
+ title: story.title,
175
+ fileName: story.fileName,
176
+ lastUpdated: story.createdAt.getTime()
177
+ })),
178
+ shouldClearOrphanedChats: true
179
+ };
180
+ }
181
+ /**
182
+ * Validates that a chat session corresponds to an actual story
183
+ */
184
+ async validateChatSession(chatId) {
185
+ const story = await this.getStory(chatId);
186
+ return story !== null;
187
+ }
188
+ }
189
+ /**
190
+ * Global story sync service instance
191
+ */
192
+ let globalStorySyncService = null;
193
+ /**
194
+ * Gets or creates the global story sync service
195
+ */
196
+ export function getStorySyncService(config) {
197
+ if (!globalStorySyncService) {
198
+ globalStorySyncService = new StorySyncService(config);
199
+ }
200
+ return globalStorySyncService;
201
+ }
@@ -0,0 +1,114 @@
1
+ import { fileURLToPath } from 'url';
2
+ import path from 'path';
3
+ const __filename = fileURLToPath(import.meta.url);
4
+ const __dirname = path.dirname(__filename);
5
+ // Default generic configuration
6
+ export const DEFAULT_CONFIG = {
7
+ generatedStoriesPath: path.resolve(process.cwd(), './src/stories/generated/'),
8
+ componentsPath: path.resolve(process.cwd(), './src/components'),
9
+ componentsMetadataPath: undefined,
10
+ storyPrefix: 'Generated/',
11
+ defaultAuthor: 'Story UI AI',
12
+ importPath: 'your-component-library',
13
+ componentPrefix: '',
14
+ components: [], // Will be populated dynamically
15
+ layoutRules: {
16
+ multiColumnWrapper: 'div',
17
+ columnComponent: 'div',
18
+ containerComponent: 'div',
19
+ layoutExamples: {
20
+ twoColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}>
21
+ <div>
22
+ <Card>
23
+ <h3>Left Card</h3>
24
+ <p>Left content</p>
25
+ </Card>
26
+ </div>
27
+ <div>
28
+ <Card>
29
+ <h3>Right Card</h3>
30
+ <p>Right content</p>
31
+ </Card>
32
+ </div>
33
+ </div>`,
34
+ threeColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem'}}>
35
+ <div>
36
+ <Card>
37
+ <h3>Column 1</h3>
38
+ <p>First column content</p>
39
+ </Card>
40
+ </div>
41
+ <div>
42
+ <Card>
43
+ <h3>Column 2</h3>
44
+ <p>Second column content</p>
45
+ </Card>
46
+ </div>
47
+ <div>
48
+ <Card>
49
+ <h3>Column 3</h3>
50
+ <p>Third column content</p>
51
+ </Card>
52
+ </div>
53
+ </div>`
54
+ },
55
+ prohibitedElements: []
56
+ },
57
+ sampleStory: `import type { StoryObj } from '@storybook/react-webpack5';
58
+ import { Card } from 'your-component-library';
59
+
60
+ export default {
61
+ title: 'Layouts/Sample Layout',
62
+ component: Card,
63
+ };
64
+
65
+ export const Default: StoryObj<typeof Card> = {
66
+ args: {
67
+ children: (
68
+ <Card>
69
+ <h3>Sample Card</h3>
70
+ <p>Sample content</p>
71
+ </Card>
72
+ )
73
+ }
74
+ };`
75
+ };
76
+ // Generic configuration template for other design systems
77
+ export const GENERIC_CONFIG_TEMPLATE = {
78
+ storyPrefix: 'Generated/',
79
+ defaultAuthor: 'Story UI AI',
80
+ componentPrefix: '',
81
+ layoutRules: {
82
+ multiColumnWrapper: 'div',
83
+ columnComponent: 'div',
84
+ layoutExamples: {
85
+ twoColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}>
86
+ <div>Column 1 content</div>
87
+ <div>Column 2 content</div>
88
+ </div>`,
89
+ threeColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem'}}>
90
+ <div>Column 1 content</div>
91
+ <div>Column 2 content</div>
92
+ <div>Column 3 content</div>
93
+ </div>`
94
+ },
95
+ prohibitedElements: []
96
+ }
97
+ };
98
+ // Default configuration - should be overridden by user's story-ui.config.js
99
+ export const STORY_UI_CONFIG = DEFAULT_CONFIG;
100
+ // Function to merge user config with defaults
101
+ export function createStoryUIConfig(userConfig) {
102
+ return {
103
+ ...DEFAULT_CONFIG,
104
+ ...userConfig,
105
+ layoutRules: {
106
+ ...DEFAULT_CONFIG.layoutRules,
107
+ ...userConfig.layoutRules,
108
+ layoutExamples: {
109
+ ...DEFAULT_CONFIG.layoutRules.layoutExamples,
110
+ ...userConfig.layoutRules?.layoutExamples
111
+ }
112
+ }
113
+ };
114
+ }
@@ -0,0 +1,205 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createStoryUIConfig, GENERIC_CONFIG_TEMPLATE } from './story-ui.config.js';
4
+ /**
5
+ * Loads Story UI configuration from various sources
6
+ */
7
+ export class StoryUIConfigLoader {
8
+ constructor() {
9
+ this.config = null;
10
+ }
11
+ static getInstance() {
12
+ if (!StoryUIConfigLoader.instance) {
13
+ StoryUIConfigLoader.instance = new StoryUIConfigLoader();
14
+ }
15
+ return StoryUIConfigLoader.instance;
16
+ }
17
+ /**
18
+ * Load configuration from a file
19
+ */
20
+ async loadFromFile(configPath) {
21
+ if (!fs.existsSync(configPath)) {
22
+ throw new Error(`Configuration file not found: ${configPath}`);
23
+ }
24
+ try {
25
+ // Support both .js and .json config files
26
+ let userConfig;
27
+ if (configPath.endsWith('.json')) {
28
+ const configContent = fs.readFileSync(configPath, 'utf-8');
29
+ userConfig = JSON.parse(configContent);
30
+ }
31
+ else {
32
+ // Dynamic import for .js/.ts files
33
+ const configModule = await import(configPath);
34
+ userConfig = configModule.default || configModule.config || configModule;
35
+ }
36
+ this.config = createStoryUIConfig(userConfig);
37
+ return this.config;
38
+ }
39
+ catch (error) {
40
+ throw new Error(`Failed to load configuration from ${configPath}: ${error}`);
41
+ }
42
+ }
43
+ /**
44
+ * Load configuration from package.json
45
+ */
46
+ loadFromPackageJson(packagePath = process.cwd()) {
47
+ const packageJsonPath = path.join(packagePath, 'package.json');
48
+ if (!fs.existsSync(packageJsonPath)) {
49
+ throw new Error(`package.json not found at ${packageJsonPath}`);
50
+ }
51
+ try {
52
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
53
+ const storyUIConfig = packageJson.storyUI || {};
54
+ // Auto-detect common paths if not specified
55
+ if (!storyUIConfig.generatedStoriesPath) {
56
+ const possiblePaths = [
57
+ path.join(packagePath, 'src/stories/generated'),
58
+ path.join(packagePath, 'stories/generated'),
59
+ path.join(packagePath, '.storybook/generated'),
60
+ path.join(packagePath, 'src/components/generated')
61
+ ];
62
+ for (const possiblePath of possiblePaths) {
63
+ if (fs.existsSync(path.dirname(possiblePath))) {
64
+ storyUIConfig.generatedStoriesPath = possiblePath;
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ // Auto-detect components path
70
+ if (!storyUIConfig.componentsPath) {
71
+ const possiblePaths = [
72
+ path.join(packagePath, 'src/components'),
73
+ path.join(packagePath, 'lib/components'),
74
+ path.join(packagePath, 'components'),
75
+ path.join(packagePath, 'src')
76
+ ];
77
+ for (const possiblePath of possiblePaths) {
78
+ if (fs.existsSync(possiblePath)) {
79
+ storyUIConfig.componentsPath = possiblePath;
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ // Auto-detect import path from package name
85
+ if (!storyUIConfig.importPath && packageJson.name) {
86
+ storyUIConfig.importPath = packageJson.name;
87
+ }
88
+ this.config = createStoryUIConfig(storyUIConfig);
89
+ return this.config;
90
+ }
91
+ catch (error) {
92
+ throw new Error(`Failed to load configuration from package.json: ${error}`);
93
+ }
94
+ }
95
+ /**
96
+ * Auto-detect configuration from project structure
97
+ */
98
+ autoDetectConfig(projectPath = process.cwd()) {
99
+ const config = { ...GENERIC_CONFIG_TEMPLATE };
100
+ // Try to detect from package.json first
101
+ try {
102
+ return this.loadFromPackageJson(projectPath);
103
+ }
104
+ catch (error) {
105
+ console.warn('Could not load from package.json, using auto-detection');
106
+ }
107
+ // Auto-detect paths
108
+ const possibleComponentPaths = [
109
+ path.join(projectPath, 'src/components'),
110
+ path.join(projectPath, 'lib/components'),
111
+ path.join(projectPath, 'components'),
112
+ path.join(projectPath, 'src')
113
+ ];
114
+ for (const possiblePath of possibleComponentPaths) {
115
+ if (fs.existsSync(possiblePath)) {
116
+ config.componentsPath = possiblePath;
117
+ break;
118
+ }
119
+ }
120
+ // Set generated stories path
121
+ config.generatedStoriesPath = path.join(projectPath, 'src/stories/generated');
122
+ // Try to detect component prefix from existing components
123
+ if (config.componentsPath) {
124
+ const componentDirs = fs.readdirSync(config.componentsPath, { withFileTypes: true })
125
+ .filter(d => d.isDirectory())
126
+ .map(d => d.name)
127
+ .slice(0, 5); // Check first 5 components
128
+ // Look for common prefixes
129
+ const prefixes = ['UI', 'App', 'My', 'Custom'];
130
+ for (const prefix of prefixes) {
131
+ if (componentDirs.some(name => name.startsWith(prefix))) {
132
+ config.componentPrefix = prefix;
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ this.config = createStoryUIConfig(config);
138
+ return this.config;
139
+ }
140
+ /**
141
+ * Get current configuration
142
+ */
143
+ getConfig() {
144
+ return this.config;
145
+ }
146
+ /**
147
+ * Set configuration directly
148
+ */
149
+ setConfig(config) {
150
+ this.config = config;
151
+ }
152
+ /**
153
+ * Generate a sample configuration file
154
+ */
155
+ generateSampleConfig(outputPath, type = 'json') {
156
+ const sampleConfig = {
157
+ generatedStoriesPath: './src/stories/generated',
158
+ componentsPath: './src/components',
159
+ storyPrefix: 'Generated/',
160
+ defaultAuthor: 'Story UI AI',
161
+ importPath: 'your-component-library',
162
+ componentPrefix: 'UI',
163
+ layoutRules: {
164
+ multiColumnWrapper: 'div',
165
+ columnComponent: 'div',
166
+ layoutExamples: {
167
+ twoColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}>
168
+ <div>Column 1 content</div>
169
+ <div>Column 2 content</div>
170
+ </div>`,
171
+ threeColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem'}}>
172
+ <div>Column 1 content</div>
173
+ <div>Column 2 content</div>
174
+ <div>Column 3 content</div>
175
+ </div>`
176
+ },
177
+ prohibitedElements: []
178
+ }
179
+ };
180
+ if (type === 'json') {
181
+ fs.writeFileSync(outputPath, JSON.stringify(sampleConfig, null, 2));
182
+ }
183
+ else {
184
+ const jsContent = `export default ${JSON.stringify(sampleConfig, null, 2)};`;
185
+ fs.writeFileSync(outputPath, jsContent);
186
+ }
187
+ console.log(`Sample configuration written to: ${outputPath}`);
188
+ }
189
+ }
190
+ // Convenience functions
191
+ export const configLoader = StoryUIConfigLoader.getInstance();
192
+ export function loadStoryUIConfig(configPath) {
193
+ if (configPath) {
194
+ return configLoader.loadFromFile(configPath);
195
+ }
196
+ else {
197
+ // Try auto-detection
198
+ try {
199
+ return Promise.resolve(configLoader.loadFromPackageJson());
200
+ }
201
+ catch {
202
+ return Promise.resolve(configLoader.autoDetectConfig());
203
+ }
204
+ }
205
+ }
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@tpitre/story-ui",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered Storybook story generator for any React component library",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "story-ui": "dist/cli/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "templates",
14
+ ".env.sample",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "start": "yarn build && node dist/mcp-server/index.js",
21
+ "dev": "tsc --watch",
22
+ "prepublishOnly": "npm run build",
23
+ "test": "echo \"Tests coming soon\" && exit 0"
24
+ },
25
+ "keywords": [
26
+ "storybook",
27
+ "ai",
28
+ "react",
29
+ "components",
30
+ "ui",
31
+ "design-system",
32
+ "claude",
33
+ "mcp",
34
+ "story-generation"
35
+ ],
36
+ "author": "Story UI Contributors",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/southleft/story-ui.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/southleft/story-ui/issues"
44
+ },
45
+ "homepage": "https://github.com/southleft/story-ui#readme",
46
+ "engines": {
47
+ "node": ">=16.0.0"
48
+ },
49
+ "dependencies": {
50
+ "express": "^4.18.2",
51
+ "cors": "^2.8.5",
52
+ "dotenv": "^16.3.1",
53
+ "node-fetch": "^2.6.7",
54
+ "commander": "^11.0.0",
55
+ "chalk": "^5.3.0",
56
+ "inquirer": "^9.2.0",
57
+ "concurrently": "^8.2.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/express": "^4.17.21",
61
+ "@types/node": "^20.4.2",
62
+ "@types/inquirer": "^9.0.0",
63
+ "@types/cors": "^2.8.17",
64
+ "@types/node-fetch": "^2.6.12",
65
+ "ts-node": "^10.9.2",
66
+ "typescript": "^5.2.2"
67
+ },
68
+ "peerDependencies": {
69
+ "react": ">=16.8.0",
70
+ "@storybook/react": ">=6.0.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "react": {
74
+ "optional": false
75
+ },
76
+ "@storybook/react": {
77
+ "optional": false
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,32 @@
1
+ # Story UI Templates
2
+
3
+ This directory contains template files that are copied to your project during the Story UI setup process.
4
+
5
+ ## Contents
6
+
7
+ ### StoryUI/
8
+ - `StoryUIPanel.tsx` - The main Story UI interface component
9
+ - `StoryUIPanel.stories.tsx` - Storybook story configuration
10
+ - `index.tsx` - Export file for cleaner imports
11
+
12
+ ## Installation
13
+
14
+ These files are automatically copied to your project when you run:
15
+
16
+ ```bash
17
+ npx story-ui init
18
+ ```
19
+
20
+ They will be installed to your configured stories directory (default: `./src/stories/generated/StoryUI/`).
21
+
22
+ ## Customization
23
+
24
+ After installation, you can customize these components to match your design system:
25
+
26
+ 1. Update the styles in `StoryUIPanel.tsx` to match your theme
27
+ 2. Modify the story configuration in `StoryUIPanel.stories.tsx`
28
+ 3. Add additional features or integrations as needed
29
+
30
+ ## Note
31
+
32
+ These files are automatically added to your `.gitignore` during setup since they're part of the Story UI installation and can be regenerated.
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+ import StoryUIPanel from './StoryUIPanel';
4
+
5
+ export default {
6
+ title: 'Story UI/Story Generator',
7
+ component: StoryUIPanel,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ docs: {
11
+ description: {
12
+ component: 'AI-powered story generator for creating complex UI layouts using your component library.'
13
+ }
14
+ }
15
+ }
16
+ } as Meta<typeof StoryUIPanel>;
17
+
18
+ const Template: StoryFn<typeof StoryUIPanel> = (args) => <StoryUIPanel {...args} />;
19
+
20
+ export const Default = Template.bind({});
21
+ Default.args = {};
22
+ Default.parameters = {
23
+ docs: {
24
+ description: {
25
+ story: 'Use natural language prompts to generate Storybook stories with your components.'
26
+ }
27
+ }
28
+ };