@almadar/ui 5.128.0 → 5.129.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,835 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Design System Generator
4
+ *
5
+ * Reads a config file from a project folder and outputs a customized
6
+ * design system to that project.
7
+ *
8
+ * Usage:
9
+ * npx tsx generate.ts --config <path-to-config>
10
+ * npm run generate -- --config ../../projects/winning-11/design-system.config.ts
11
+ */
12
+
13
+ import * as fs from "fs";
14
+ import * as path from "path";
15
+ import { execSync } from "child_process";
16
+ import type {
17
+ DesignSystemConfig,
18
+ ThemeConfig,
19
+ CustomComponentConfig,
20
+ ShadowConfig,
21
+ RadiusConfig,
22
+ } from "./types";
23
+
24
+ // Paths
25
+ const DESIGN_SYSTEM_ROOT = path.join(__dirname, "..");
26
+ const COMPONENTS_SRC = path.join(DESIGN_SYSTEM_ROOT, "components");
27
+ const THEMES_SRC = path.join(DESIGN_SYSTEM_ROOT, "themes");
28
+ const LIB_SRC = path.join(DESIGN_SYSTEM_ROOT, "lib");
29
+ const HOOKS_SRC = path.join(DESIGN_SYSTEM_ROOT, "hooks");
30
+ const CONTEXTS_SRC = path.join(DESIGN_SYSTEM_ROOT, "contexts");
31
+ const CLIENTS_SRC = path.join(DESIGN_SYSTEM_ROOT, "clients");
32
+
33
+ // Shadow presets
34
+ const SHADOW_PRESETS: Record<string, ShadowConfig> = {
35
+ "soft-organic": {
36
+ main: "0 4px 12px -2px rgba(21, 128, 61, 0.15), 0 2px 6px -2px rgba(21, 128, 61, 0.1)",
37
+ sm: "0 2px 4px -1px rgba(21, 128, 61, 0.1)",
38
+ lg: "0 16px 32px -8px rgba(21, 128, 61, 0.2), 0 8px 16px -4px rgba(21, 128, 61, 0.1)",
39
+ hover: "0 12px 24px -6px rgba(21, 128, 61, 0.25)",
40
+ },
41
+ minimal: {
42
+ main: "0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06)",
43
+ sm: "0 1px 2px rgba(0, 0, 0, 0.04)",
44
+ lg: "0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
45
+ hover: "0 10px 40px -10px rgba(0, 0, 0, 0.15)",
46
+ },
47
+ corporate: {
48
+ main: "0 2px 8px -2px rgba(15, 23, 42, 0.12)",
49
+ sm: "0 1px 3px rgba(15, 23, 42, 0.08)",
50
+ lg: "0 12px 28px -6px rgba(15, 23, 42, 0.18)",
51
+ hover: "0 16px 36px -8px rgba(15, 23, 42, 0.22)",
52
+ },
53
+ playful: {
54
+ main: "0 4px 14px -3px rgba(124, 58, 237, 0.2)",
55
+ sm: "0 2px 6px rgba(124, 58, 237, 0.12)",
56
+ lg: "0 18px 36px -10px rgba(124, 58, 237, 0.25)",
57
+ hover: "0 20px 40px -12px rgba(124, 58, 237, 0.3)",
58
+ },
59
+ dramatic: {
60
+ main: "0 4px 20px -4px rgba(0, 0, 0, 0.5)",
61
+ sm: "0 2px 8px rgba(0, 0, 0, 0.4)",
62
+ lg: "0 20px 50px -12px rgba(0, 0, 0, 0.6)",
63
+ hover: "0 24px 60px -16px rgba(0, 0, 0, 0.7)",
64
+ },
65
+ soft: {
66
+ main: "0 2px 8px -2px rgba(37, 99, 235, 0.15)",
67
+ sm: "0 1px 4px rgba(37, 99, 235, 0.1)",
68
+ lg: "0 12px 28px -6px rgba(37, 99, 235, 0.2)",
69
+ hover: "0 16px 36px -8px rgba(37, 99, 235, 0.25)",
70
+ },
71
+ };
72
+
73
+ // Radius presets
74
+ const RADIUS_PRESETS: Record<string, RadiusConfig> = {
75
+ organic: { sm: "6px", md: "12px", lg: "16px", xl: "24px" },
76
+ gentle: { sm: "4px", md: "8px", lg: "12px", xl: "16px" },
77
+ sharp: { sm: "2px", md: "4px", lg: "6px", xl: "8px" },
78
+ rounded: { sm: "8px", md: "16px", lg: "24px", xl: "32px" },
79
+ friendly: { sm: "6px", md: "10px", lg: "14px", xl: "20px" },
80
+ };
81
+
82
+ /**
83
+ * Main generator function
84
+ */
85
+ async function generate(configPath: string): Promise<void> {
86
+ console.log("🎨 Orbital Design System Generator\n");
87
+
88
+ // Resolve config path
89
+ const absoluteConfigPath = path.resolve(configPath);
90
+ if (!fs.existsSync(absoluteConfigPath)) {
91
+ throw new Error(`Config file not found: ${absoluteConfigPath}`);
92
+ }
93
+
94
+ // Load config
95
+ console.log(`📋 Loading config: ${absoluteConfigPath}`);
96
+ const configModule = await import(absoluteConfigPath);
97
+ const config: DesignSystemConfig = configModule.default || configModule;
98
+
99
+ // Resolve output directory relative to config file
100
+ const configDir = path.dirname(absoluteConfigPath);
101
+ const outputDir = path.resolve(configDir, config.outputDir);
102
+
103
+ console.log(`📁 Output directory: ${outputDir}`);
104
+ console.log(`🏷️ Project: ${config.projectName}\n`);
105
+
106
+ // Create output directory
107
+ if (fs.existsSync(outputDir)) {
108
+ console.log(" Cleaning existing output directory...");
109
+ fs.rmSync(outputDir, { recursive: true });
110
+ }
111
+ fs.mkdirSync(outputDir, { recursive: true });
112
+
113
+ // Step 1: Copy base components
114
+ console.log("1️⃣ Copying base components...");
115
+ copyDirectory(
116
+ COMPONENTS_SRC,
117
+ path.join(outputDir, "components"),
118
+ config.includeComponents,
119
+ );
120
+ console.log(" ✅ Components copied\n");
121
+
122
+ // Step 2: Copy lib utilities, hooks, and contexts
123
+ console.log("2️⃣ Copying utilities...");
124
+ copyDirectory(LIB_SRC, path.join(outputDir, "lib"));
125
+ if (fs.existsSync(HOOKS_SRC)) {
126
+ copyDirectory(HOOKS_SRC, path.join(outputDir, "hooks"));
127
+ }
128
+ if (fs.existsSync(CONTEXTS_SRC)) {
129
+ copyDirectory(CONTEXTS_SRC, path.join(outputDir, "contexts"));
130
+ }
131
+ console.log(" ✅ Utilities, hooks, and contexts copied\n");
132
+
133
+ // Step 3: Generate theme CSS
134
+ console.log("3️⃣ Generating theme...");
135
+ const themesDir = path.join(outputDir, "themes");
136
+ fs.mkdirSync(themesDir, { recursive: true });
137
+
138
+ // Copy base themes
139
+ for (const file of ["wireframe.css", "minimalist.css"]) {
140
+ const src = path.join(THEMES_SRC, file);
141
+ if (fs.existsSync(src)) {
142
+ fs.copyFileSync(src, path.join(themesDir, file));
143
+ }
144
+ }
145
+
146
+ // Generate custom theme
147
+ const themeCSS = generateThemeCSS(config.theme);
148
+ const themePath = path.join(themesDir, `${config.theme.name}.css`);
149
+ fs.writeFileSync(themePath, themeCSS);
150
+
151
+ // Create theme index
152
+ const themeIndex = generateThemeIndex(config.theme.name);
153
+ fs.writeFileSync(path.join(themesDir, "index.css"), themeIndex);
154
+ console.log(` ✅ Theme generated: ${config.theme.name}\n`);
155
+
156
+ // Step 4: Copy client-specific implementations (if they exist)
157
+ const clientDir = path.join(CLIENTS_SRC, config.projectName);
158
+ const outputClientsDir = path.join(outputDir, "clients", config.projectName);
159
+ if (fs.existsSync(clientDir)) {
160
+ console.log("4️⃣ Copying client implementations...");
161
+ copyDirectory(clientDir, outputClientsDir);
162
+ console.log(` ✅ Client implementations copied: ${config.projectName}\n`);
163
+ }
164
+
165
+ // Step 5: Generate custom components (re-exports or stubs)
166
+ if (config.customComponents && config.customComponents.length > 0) {
167
+ console.log("5️⃣ Generating custom components...");
168
+ generateCustomComponents(
169
+ config.customComponents,
170
+ path.join(outputDir, "components"),
171
+ config.projectName,
172
+ fs.existsSync(clientDir),
173
+ );
174
+ console.log(
175
+ ` ✅ Generated ${config.customComponents.length} custom components\n`,
176
+ );
177
+ }
178
+
179
+ // Step 6: Generate index.css
180
+ console.log("6️⃣ Generating index.css...");
181
+ const indexCSS = generateIndexCSS(config.theme.name);
182
+ fs.writeFileSync(path.join(outputDir, "index.css"), indexCSS);
183
+ console.log(" ✅ index.css generated\n");
184
+
185
+ // Step 7: Generate package.json
186
+ console.log("7️⃣ Generating package.json...");
187
+ const packageJson = generatePackageJson(config);
188
+ fs.writeFileSync(
189
+ path.join(outputDir, "package.json"),
190
+ JSON.stringify(packageJson, null, 2),
191
+ );
192
+ console.log(" ✅ package.json generated\n");
193
+
194
+ // Step 8: Copy config files
195
+ console.log("8️⃣ Copying config files...");
196
+ copyConfigFiles(outputDir, config);
197
+ console.log(" ✅ Config files copied\n");
198
+
199
+ // Step 9: Generate Storybook config
200
+ if (config.storybook?.build !== false) {
201
+ console.log("9️⃣ Generating Storybook config...");
202
+ generateStorybookConfig(outputDir, config);
203
+ console.log(" ✅ Storybook config generated\n");
204
+ }
205
+
206
+ // Step 10: Generate manifest
207
+ console.log("🔟 Generating manifest...");
208
+ const manifest = generateManifest(config);
209
+ fs.writeFileSync(
210
+ path.join(outputDir, "manifest.json"),
211
+ JSON.stringify(manifest, null, 2),
212
+ );
213
+ console.log(" ✅ Manifest generated\n");
214
+
215
+ // Summary
216
+ console.log("🎉 Design System Generated Successfully!");
217
+ console.log("=========================================");
218
+ console.log(` Location: ${outputDir}`);
219
+ console.log(` Theme: ${config.theme.name}`);
220
+ console.log(` Custom Components: ${config.customComponents?.length || 0}`);
221
+ console.log("");
222
+ console.log("Next steps:");
223
+ console.log(` cd ${outputDir}`);
224
+ console.log(" npm install");
225
+ console.log(" npm run storybook");
226
+ console.log("");
227
+ }
228
+
229
+ /**
230
+ * Copy directory with optional filtering
231
+ */
232
+ function copyDirectory(
233
+ src: string,
234
+ dest: string,
235
+ filter?: DesignSystemConfig["includeComponents"],
236
+ ): void {
237
+ if (!fs.existsSync(dest)) {
238
+ fs.mkdirSync(dest, { recursive: true });
239
+ }
240
+
241
+ const entries = fs.readdirSync(src, { withFileTypes: true });
242
+
243
+ for (const entry of entries) {
244
+ const srcPath = path.join(src, entry.name);
245
+ const destPath = path.join(dest, entry.name);
246
+
247
+ if (entry.isDirectory()) {
248
+ // Check category filter
249
+ if (
250
+ filter?.categories &&
251
+ !filter.categories.some((c) => c === entry.name)
252
+ ) {
253
+ continue;
254
+ }
255
+ copyDirectory(srcPath, destPath, filter);
256
+ } else {
257
+ // Check include/exclude filters
258
+ const componentName = path.basename(entry.name, path.extname(entry.name));
259
+ if (filter?.exclude?.includes(componentName)) {
260
+ continue;
261
+ }
262
+ if (filter?.include && !filter.include.includes(componentName)) {
263
+ continue;
264
+ }
265
+ fs.copyFileSync(srcPath, destPath);
266
+ }
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Generate theme CSS from config
272
+ */
273
+ function generateThemeCSS(theme: ThemeConfig): string {
274
+ const shadows =
275
+ typeof theme.shadows === "string"
276
+ ? SHADOW_PRESETS[theme.shadows] || SHADOW_PRESETS.minimal
277
+ : theme.shadows || SHADOW_PRESETS.minimal;
278
+
279
+ const radius =
280
+ typeof theme.radius === "string"
281
+ ? RADIUS_PRESETS[theme.radius] || RADIUS_PRESETS.gentle
282
+ : theme.radius || RADIUS_PRESETS.gentle;
283
+
284
+ const { colors } = theme;
285
+
286
+ let css = `/**
287
+ * ${theme.displayName || theme.name} Theme
288
+ * ${theme.metaphor ? `Metaphor: ${theme.metaphor}` : ""}
289
+ * Generated: ${new Date().toISOString()}
290
+ */
291
+
292
+ [data-design-theme="${theme.name}"] {
293
+ /* === Colors === */
294
+ --color-primary: ${colors.primary};
295
+ --color-primary-hover: ${colors.primaryHover || adjustBrightness(colors.primary, -10)};
296
+ --color-primary-foreground: ${colors.primaryForeground || "#ffffff"};
297
+
298
+ --color-secondary: ${colors.secondary};
299
+ --color-secondary-hover: ${colors.secondaryHover || adjustBrightness(colors.secondary, -5)};
300
+ --color-secondary-foreground: ${colors.secondaryForeground || colors.foreground};
301
+
302
+ --color-accent: ${colors.accent};
303
+ --color-accent-foreground: ${colors.accentForeground || "#ffffff"};
304
+
305
+ --color-muted: ${colors.muted};
306
+ --color-muted-foreground: ${colors.mutedForeground || adjustBrightness(colors.foreground, 30)};
307
+
308
+ --color-background: ${colors.background};
309
+ --color-foreground: ${colors.foreground};
310
+ --color-card: ${colors.card || "#ffffff"};
311
+ --color-card-foreground: ${colors.cardForeground || colors.foreground};
312
+ --color-border: ${colors.border};
313
+ --color-input: ${colors.input || colors.border};
314
+ --color-ring: ${colors.ring || colors.primary};
315
+
316
+ /* Semantic Colors */
317
+ --color-success: ${colors.success || "#22c55e"};
318
+ --color-warning: ${colors.warning || "#f59e0b"};
319
+ --color-error: ${colors.error || "#dc2626"};
320
+ --color-info: ${colors.info || "#0ea5e9"};
321
+
322
+ /* === Shadows === */
323
+ --shadow-main: ${shadows.main};
324
+ --shadow-sm: ${shadows.sm};
325
+ --shadow-lg: ${shadows.lg};
326
+ --shadow-hover: ${shadows.hover};
327
+ --shadow-inner: ${shadows.inner || "inset 0 2px 4px rgba(0, 0, 0, 0.05)"};
328
+ --shadow-active: ${shadows.active || shadows.sm};
329
+ --shadow-none: none;
330
+
331
+ /* === Border Radius === */
332
+ --radius-none: 0px;
333
+ --radius-sm: ${radius.sm};
334
+ --radius-md: ${radius.md};
335
+ --radius-lg: ${radius.lg};
336
+ --radius-xl: ${radius.xl};
337
+ --radius-full: 9999px;
338
+
339
+ /* === Border Width === */
340
+ --border-width: 1px;
341
+ --border-width-thin: 1px;
342
+ --border-width-thick: 2px;
343
+
344
+ /* === Typography === */
345
+ --font-family: ${theme.typography?.fontFamily || "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"};
346
+ --font-weight-normal: ${theme.typography?.fontWeightNormal || 400};
347
+ --font-weight-medium: ${theme.typography?.fontWeightMedium || 500};
348
+ --font-weight-bold: ${theme.typography?.fontWeightBold || 600};
349
+ --letter-spacing: -0.01em;
350
+ --line-height: 1.6;
351
+
352
+ /* === Transitions === */
353
+ --transition-fast: 150ms;
354
+ --transition-normal: 250ms;
355
+ --transition-slow: 400ms;
356
+ --transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
357
+
358
+ /* === Transforms === */
359
+ --hover-scale: 1.02;
360
+ --hover-translate-y: -2px;
361
+ --active-scale: 0.98;
362
+
363
+ /* === Focus === */
364
+ --focus-ring-width: 2px;
365
+ --focus-ring-offset: 2px;
366
+ --focus-ring-color: ${colors.ring || colors.primary};
367
+
368
+ /* === Card === */
369
+ --card-hover-shadow: ${shadows.hover};
370
+ --card-hover-transform: translateY(-2px) scale(1.01);
371
+
372
+ /* === Button === */
373
+ --button-active-transform: scale(0.98);
374
+ `;
375
+
376
+ // Add custom colors
377
+ if (colors.custom) {
378
+ css += "\n /* === Custom Colors === */\n";
379
+ for (const [name, value] of Object.entries(colors.custom)) {
380
+ css += ` --color-${name}: ${value};\n`;
381
+ }
382
+ }
383
+
384
+ css += "}\n";
385
+
386
+ // Dark mode
387
+ if (theme.darkMode) {
388
+ css += `
389
+ /* Dark mode */
390
+ .dark[data-design-theme="${theme.name}"],
391
+ [data-design-theme="${theme.name}"].dark {
392
+ `;
393
+ for (const [key, value] of Object.entries(theme.darkMode)) {
394
+ if (value) {
395
+ const cssVar = key.replace(/([A-Z])/g, "-$1").toLowerCase();
396
+ css += ` --color-${cssVar}: ${value};\n`;
397
+ }
398
+ }
399
+ css += "}\n";
400
+ }
401
+
402
+ return css;
403
+ }
404
+
405
+ /**
406
+ * Generate theme index CSS
407
+ */
408
+ function generateThemeIndex(themeName: string): string {
409
+ return `/**
410
+ * Theme Index
411
+ */
412
+ @import './wireframe.css';
413
+ @import './minimalist.css';
414
+ @import './${themeName}.css';
415
+ `;
416
+ }
417
+
418
+ /**
419
+ * Generate index.css
420
+ */
421
+ function generateIndexCSS(themeName: string): string {
422
+ return `/* Design System - ${themeName} */
423
+ @import './themes/index.css';
424
+
425
+ @tailwind base;
426
+ @tailwind components;
427
+ @tailwind utilities;
428
+
429
+ /* Base styles */
430
+ :root {
431
+ --font-family: 'Inter', system-ui, -apple-system, sans-serif;
432
+ }
433
+
434
+ html {
435
+ font-family: var(--font-family);
436
+ }
437
+
438
+ body {
439
+ background-color: var(--color-background);
440
+ color: var(--color-foreground);
441
+ }
442
+ `;
443
+ }
444
+
445
+ /**
446
+ * Generate custom component files
447
+ * If client implementations exist, generates re-exports; otherwise generates stubs
448
+ */
449
+ function generateCustomComponents(
450
+ components: CustomComponentConfig[],
451
+ outputDir: string,
452
+ projectName: string,
453
+ hasClientImplementations: boolean,
454
+ ): void {
455
+ // Group components by category for index generation
456
+ const componentsByCategory: Record<string, CustomComponentConfig[]> = {};
457
+
458
+ for (const component of components) {
459
+ const categoryDir = path.join(outputDir, component.category, "custom");
460
+ if (!fs.existsSync(categoryDir)) {
461
+ fs.mkdirSync(categoryDir, { recursive: true });
462
+ }
463
+
464
+ // Track by category
465
+ if (!componentsByCategory[component.category]) {
466
+ componentsByCategory[component.category] = [];
467
+ }
468
+ componentsByCategory[component.category].push(component);
469
+
470
+ // Check if client implementation exists
471
+ const clientImplPath = path.join(
472
+ CLIENTS_SRC,
473
+ projectName,
474
+ component.category,
475
+ `${component.name}.tsx`,
476
+ );
477
+ const hasImplementation =
478
+ hasClientImplementations && fs.existsSync(clientImplPath);
479
+
480
+ if (hasImplementation) {
481
+ // Generate re-export from client implementation
482
+ const reexportCode = generateReexportCode(component, projectName);
483
+ fs.writeFileSync(
484
+ path.join(categoryDir, `${component.name}.tsx`),
485
+ reexportCode,
486
+ );
487
+
488
+ // Don't copy stories here - Storybook will pick them up from clients folder directly
489
+ // This avoids duplicate story warnings
490
+ const clientStoryPath = path.join(
491
+ CLIENTS_SRC,
492
+ projectName,
493
+ component.category,
494
+ `${component.name}.stories.tsx`,
495
+ );
496
+ if (!fs.existsSync(clientStoryPath)) {
497
+ // Only generate a story if the client doesn't have one
498
+ const storyCode = generateStoryCode(component);
499
+ fs.writeFileSync(
500
+ path.join(categoryDir, `${component.name}.stories.tsx`),
501
+ storyCode,
502
+ );
503
+ }
504
+ } else {
505
+ // Generate stub component file
506
+ const componentCode = generateComponentCode(component);
507
+ fs.writeFileSync(
508
+ path.join(categoryDir, `${component.name}.tsx`),
509
+ componentCode,
510
+ );
511
+
512
+ // Generate story file
513
+ const storyCode = generateStoryCode(component);
514
+ fs.writeFileSync(
515
+ path.join(categoryDir, `${component.name}.stories.tsx`),
516
+ storyCode,
517
+ );
518
+ }
519
+ }
520
+
521
+ // Generate index for each category's custom folder
522
+ for (const [category, categoryComponents] of Object.entries(
523
+ componentsByCategory,
524
+ )) {
525
+ const categoryIndexContent = categoryComponents
526
+ .map((c) => `export * from './${c.name}';`)
527
+ .join("\n");
528
+ const categoryDir = path.join(outputDir, category, "custom");
529
+ fs.writeFileSync(
530
+ path.join(categoryDir, "index.ts"),
531
+ `/**\n * Custom ${category} - ${projectName}\n */\n\n${categoryIndexContent}\n`,
532
+ );
533
+ }
534
+
535
+ // Generate main index for custom components
536
+ const indexContent = components
537
+ .map((c) => `export * from './${c.category}/custom/${c.name}';`)
538
+ .join("\n");
539
+ fs.writeFileSync(path.join(outputDir, "custom-components.ts"), indexContent);
540
+ }
541
+
542
+ /**
543
+ * Generate re-export code for components with client implementations
544
+ */
545
+ function generateReexportCode(
546
+ component: CustomComponentConfig,
547
+ projectName: string,
548
+ ): string {
549
+ return `/**
550
+ * ${component.name}
551
+ *
552
+ * Re-exports from client-specific implementation
553
+ */
554
+
555
+ export * from '../../../clients/${projectName}/${component.category}/${component.name}';
556
+ `;
557
+ }
558
+
559
+ /**
560
+ * Generate component code
561
+ */
562
+ function generateComponentCode(component: CustomComponentConfig): string {
563
+ const propsInterface = component.props
564
+ .map(
565
+ (p) =>
566
+ ` /** ${p.description} */\n ${p.name}${p.required ? "" : "?"}: ${p.type};`,
567
+ )
568
+ .join("\n");
569
+
570
+ return `/**
571
+ * ${component.name}
572
+ *
573
+ * ${component.description}
574
+ * ${component.reasoning ? `\nReasoning: ${component.reasoning}` : ""}
575
+ */
576
+
577
+ import React from 'react';
578
+ import { cn } from '../../../lib/cn';
579
+
580
+ export interface ${component.name}Props {
581
+ ${propsInterface}
582
+ }
583
+
584
+ export function ${component.name}({ ${component.props.map((p) => p.name).join(", ")} }: ${component.name}Props) {
585
+ // TODO: Implement ${component.name}
586
+ return (
587
+ <div className={cn('${component.name.toLowerCase()}')}>
588
+ <p>${component.name} - Not yet implemented</p>
589
+ {/* Implementation goes here */}
590
+ </div>
591
+ );
592
+ }
593
+ `;
594
+ }
595
+
596
+ /**
597
+ * Generate Storybook story code
598
+ */
599
+ function generateStoryCode(component: CustomComponentConfig): string {
600
+ return `import type { Meta, StoryObj } from '@storybook/react';
601
+ import { ${component.name} } from './${component.name}';
602
+
603
+ const meta: Meta<typeof ${component.name}> = {
604
+ title: 'Custom/${component.category}/${component.name}',
605
+ component: ${component.name},
606
+ parameters: {
607
+ layout: 'centered',
608
+ },
609
+ tags: ['autodocs'],
610
+ };
611
+
612
+ export default meta;
613
+ type Story = StoryObj<typeof meta>;
614
+
615
+ export const Default: Story = {
616
+ args: {
617
+ // Add default props here
618
+ },
619
+ };
620
+ `;
621
+ }
622
+
623
+ /**
624
+ * Generate package.json
625
+ */
626
+ function generatePackageJson(config: DesignSystemConfig): object {
627
+ return {
628
+ name: `@${config.projectName}/design-system`,
629
+ version: "1.0.0",
630
+ private: true,
631
+ scripts: {
632
+ storybook: "storybook dev -p 6006",
633
+ "build-storybook": "storybook build -o storybook-static",
634
+ },
635
+ dependencies: {
636
+ clsx: "^2.1.0",
637
+ "lucide-react": "^0.344.0",
638
+ react: "^18.2.0",
639
+ "react-dom": "^18.2.0",
640
+ "react-router-dom": "^6.22.0",
641
+ "tailwind-merge": "^2.2.0",
642
+ },
643
+ devDependencies: {
644
+ "@storybook/addon-essentials": "^8.0.0",
645
+ "@storybook/addon-interactions": "^8.0.0",
646
+ "@storybook/addon-links": "^8.0.0",
647
+ "@storybook/addon-themes": "^8.0.0",
648
+ "@storybook/blocks": "^8.0.0",
649
+ "@storybook/react": "^8.0.0",
650
+ "@storybook/react-vite": "^8.0.0",
651
+ "@storybook/test": "^8.0.0",
652
+ "@types/react": "^18.2.0",
653
+ "@types/react-dom": "^18.2.0",
654
+ "@vitejs/plugin-react": "^4.2.0",
655
+ autoprefixer: "^10.4.17",
656
+ postcss: "^8.4.35",
657
+ storybook: "^8.0.0",
658
+ tailwindcss: "^3.4.1",
659
+ typescript: "^5.3.0",
660
+ vite: "^5.0.0",
661
+ },
662
+ };
663
+ }
664
+
665
+ /**
666
+ * Copy config files (tailwind, postcss, tsconfig, vite)
667
+ */
668
+ function copyConfigFiles(outputDir: string, config: DesignSystemConfig): void {
669
+ // tailwind.config.js
670
+ const tailwindConfig = `/** @type {import('tailwindcss').Config} */
671
+ export default {
672
+ content: [
673
+ './components/**/*.{js,ts,jsx,tsx}',
674
+ './clients/**/*.{js,ts,jsx,tsx}',
675
+ './.storybook/**/*.{js,ts,jsx,tsx}',
676
+ ],
677
+ darkMode: 'class',
678
+ theme: { extend: {} },
679
+ plugins: [],
680
+ };
681
+ `;
682
+ fs.writeFileSync(path.join(outputDir, "tailwind.config.js"), tailwindConfig);
683
+
684
+ // postcss.config.js
685
+ fs.writeFileSync(
686
+ path.join(outputDir, "postcss.config.js"),
687
+ "export default { plugins: { tailwindcss: {}, autoprefixer: {} } };",
688
+ );
689
+
690
+ // tsconfig.json
691
+ const tsconfig = {
692
+ compilerOptions: {
693
+ target: "ES2020",
694
+ useDefineForClassFields: true,
695
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
696
+ module: "ESNext",
697
+ skipLibCheck: true,
698
+ moduleResolution: "bundler",
699
+ allowImportingTsExtensions: true,
700
+ resolveJsonModule: true,
701
+ isolatedModules: true,
702
+ noEmit: true,
703
+ jsx: "react-jsx",
704
+ strict: true,
705
+ esModuleInterop: true,
706
+ allowSyntheticDefaultImports: true,
707
+ },
708
+ include: ["components", "clients", ".storybook"],
709
+ };
710
+ fs.writeFileSync(
711
+ path.join(outputDir, "tsconfig.json"),
712
+ JSON.stringify(tsconfig, null, 2),
713
+ );
714
+
715
+ // vite.config.ts
716
+ fs.writeFileSync(
717
+ path.join(outputDir, "vite.config.ts"),
718
+ "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nexport default defineConfig({ plugins: [react()] });",
719
+ );
720
+ }
721
+
722
+ /**
723
+ * Generate Storybook config
724
+ */
725
+ function generateStorybookConfig(
726
+ outputDir: string,
727
+ config: DesignSystemConfig,
728
+ ): void {
729
+ const sbDir = path.join(outputDir, ".storybook");
730
+ fs.mkdirSync(sbDir, { recursive: true });
731
+
732
+ // main.ts
733
+ const mainTs = `import type { StorybookConfig } from '@storybook/react-vite';
734
+
735
+ const config: StorybookConfig = {
736
+ stories: [
737
+ '../components/**/*.stories.@(js|jsx|ts|tsx)',
738
+ '../clients/**/*.stories.@(js|jsx|ts|tsx)',
739
+ ],
740
+ addons: [
741
+ '@storybook/addon-links',
742
+ '@storybook/addon-essentials',
743
+ '@storybook/addon-interactions',
744
+ '@storybook/addon-themes',
745
+ ],
746
+ framework: { name: '@storybook/react-vite', options: {} },
747
+ docs: { autodocs: 'tag' },
748
+ };
749
+
750
+ export default config;
751
+ `;
752
+ fs.writeFileSync(path.join(sbDir, "main.ts"), mainTs);
753
+
754
+ // preview.tsx
755
+ const previewTsx = `import type { Preview } from '@storybook/react';
756
+ import { withThemeByDataAttribute } from '@storybook/addon-themes';
757
+ import '../index.css';
758
+
759
+ const preview: Preview = {
760
+ parameters: {
761
+ actions: { argTypesRegex: '^on[A-Z].*' },
762
+ controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } },
763
+ },
764
+ decorators: [
765
+ withThemeByDataAttribute({
766
+ themes: {
767
+ wireframe: 'wireframe',
768
+ minimalist: 'minimalist',
769
+ '${config.theme.name}': '${config.theme.name}',
770
+ },
771
+ defaultTheme: '${config.theme.name}',
772
+ attributeName: 'data-design-theme',
773
+ }),
774
+ (Story) => <div style={{ padding: '1rem' }}><Story /></div>,
775
+ ],
776
+ };
777
+
778
+ export default preview;
779
+ `;
780
+ fs.writeFileSync(path.join(sbDir, "preview.tsx"), previewTsx);
781
+ }
782
+
783
+ /**
784
+ * Generate manifest
785
+ */
786
+ function generateManifest(config: DesignSystemConfig): object {
787
+ return {
788
+ name: config.projectName,
789
+ generatedAt: new Date().toISOString(),
790
+ theme: {
791
+ name: config.theme.name,
792
+ displayName: config.theme.displayName,
793
+ metaphor: config.theme.metaphor,
794
+ },
795
+ customComponents: (config.customComponents || []).map((c) => ({
796
+ name: c.name,
797
+ category: c.category,
798
+ priority: c.priority,
799
+ })),
800
+ };
801
+ }
802
+
803
+ /**
804
+ * Adjust color brightness
805
+ */
806
+ function adjustBrightness(hex: string, percent: number): string {
807
+ const num = parseInt(hex.replace("#", ""), 16);
808
+ const amt = Math.round(2.55 * percent);
809
+ const R = Math.max(0, Math.min(255, (num >> 16) + amt));
810
+ const G = Math.max(0, Math.min(255, ((num >> 8) & 0x00ff) + amt));
811
+ const B = Math.max(0, Math.min(255, (num & 0x0000ff) + amt));
812
+ return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
813
+ }
814
+
815
+ // CLI
816
+ const args = process.argv.slice(2);
817
+ const configIndex = args.indexOf("--config");
818
+
819
+ if (configIndex === -1 || !args[configIndex + 1]) {
820
+ console.log(`
821
+ Orbital Design System Generator
822
+
823
+ Usage:
824
+ npx tsx generate.ts --config <path-to-config>
825
+
826
+ Example:
827
+ npx tsx generate.ts --config ../../projects/winning-11/design-system.config.ts
828
+ `);
829
+ process.exit(1);
830
+ }
831
+
832
+ generate(args[configIndex + 1]).catch((err) => {
833
+ console.error("Error:", err.message);
834
+ process.exit(1);
835
+ });