@almadar/ui 5.128.0 → 5.130.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,237 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Tailwind Safelist Audit
4
+ *
5
+ * Scans all @almadar/ui source files for Tailwind classes that need
6
+ * to be in the safelist (because consuming apps can't extract them
7
+ * from compiled dist/ files). Compares against the current safelist
8
+ * in tailwind-preset.cjs and reports missing entries.
9
+ *
10
+ * Classes that need safelisting:
11
+ * 1. Arbitrary value classes: bg-[var(--color-X)], min-h-[200px], etc.
12
+ * 2. Opacity modifiers on semantic colors: bg-primary/10, text-foreground/60
13
+ * 3. Responsive prefixes on dynamic values: sm:grid-cols-2
14
+ * 4. State prefixes on CSS vars: hover:bg-[var(--color-X)]
15
+ *
16
+ * Standard utility classes (flex, p-4, rounded-lg) are fine because
17
+ * the shell template's tailwind.config.js includes the dist/ path in
18
+ * content scanning. But CSS-variable-based arbitrary classes use bracket
19
+ * notation that Tailwind can't extract from minified JS.
20
+ *
21
+ * Usage:
22
+ * npx tsx scripts/audit-tailwind-safelist.ts
23
+ * npx tsx scripts/audit-tailwind-safelist.ts --fix (appends missing to preset)
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+
28
+ import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
29
+ import { join, relative } from 'path';
30
+
31
+ const ROOT = join(import.meta.dirname, '..');
32
+ const PRESET_PATH = join(ROOT, 'tailwind-preset.cjs');
33
+ const SCAN_DIRS = ['components', 'runtime', 'renderer', 'context', 'providers', 'hooks'];
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Regex patterns for classes that need safelisting
37
+ // ---------------------------------------------------------------------------
38
+
39
+ // Matches arbitrary value classes: bg-[var(...)], min-h-[200px], w-[300px], etc.
40
+ // Requires a hyphen before the bracket to avoid matching JS array access like arr[0]
41
+ const ARBITRARY_RE = /(?:^|\s|["'`])([a-z][a-z0-9]*(?::[a-z][a-z0-9-]*)+-\[[^\]]+\]|[a-z][a-z0-9]*-[a-z0-9-]*-\[[^\]]+\]|[a-z][a-z0-9]*-\[[^\]]+\])/g;
42
+
43
+ // Matches opacity modifiers on semantic colors: bg-primary/10, text-foreground/60
44
+ const OPACITY_RE = /(?:^|\s|["'`])((?:bg|text|border|ring|from|to|via|hover:bg|hover:text|hover:border|dark:bg|dark:hover:bg|group-hover:bg|group-hover:text|peer-focus:ring|focus:ring)-(?:primary|secondary|muted|accent|foreground|card|surface|background|border|error|success|warning|info|ring|input|muted-foreground|card-foreground|primary-foreground|secondary-foreground|error-foreground|success-foreground|warning-foreground|info-foreground)\/\d+)/g;
45
+
46
+ // Matches classes with [var(--...)] or [length:var(--...)]
47
+ const CSS_VAR_RE = /(?:^|\s|["'`])([a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)*-\[(?:length:|color:)?var\(--[a-z0-9-]+\)[^\]]*\])/g;
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // File scanner
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function getAllFiles(dir: string, ext = '.tsx'): string[] {
54
+ const results: string[] = [];
55
+ try {
56
+ for (const entry of readdirSync(dir)) {
57
+ const full = join(dir, entry);
58
+ const stat = statSync(full);
59
+ if (stat.isDirectory() && entry !== 'node_modules' && entry !== 'dist' && entry !== '.storybook') {
60
+ results.push(...getAllFiles(full, ext));
61
+ } else if (entry.endsWith(ext) || entry.endsWith('.ts')) {
62
+ results.push(full);
63
+ }
64
+ }
65
+ } catch {
66
+ // directory doesn't exist
67
+ }
68
+ return results;
69
+ }
70
+
71
+ function extractSafelistCandidates(content: string): Set<string> {
72
+ const candidates = new Set<string>();
73
+
74
+ // Find arbitrary value classes
75
+ for (const match of content.matchAll(ARBITRARY_RE)) {
76
+ const cls = match[1];
77
+ if (cls && !cls.startsWith('//') && !cls.startsWith('*')) {
78
+ candidates.add(cls);
79
+ }
80
+ }
81
+
82
+ // Find opacity modifier classes
83
+ for (const match of content.matchAll(OPACITY_RE)) {
84
+ if (match[1]) candidates.add(match[1]);
85
+ }
86
+
87
+ // Find CSS variable classes
88
+ for (const match of content.matchAll(CSS_VAR_RE)) {
89
+ if (match[1]) candidates.add(match[1]);
90
+ }
91
+
92
+ return candidates;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Safelist parser
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function getCurrentSafelist(): Set<string> {
100
+ const content = readFileSync(PRESET_PATH, 'utf-8');
101
+ const safelist = new Set<string>();
102
+
103
+ // Find the safelist array - match from 'safelist: [' to the matching ']'
104
+ const startIdx = content.indexOf('safelist:');
105
+ if (startIdx === -1) return safelist;
106
+
107
+ const bracketStart = content.indexOf('[', startIdx);
108
+ if (bracketStart === -1) return safelist;
109
+
110
+ // Find the matching closing bracket (handle nested brackets)
111
+ let depth = 0;
112
+ let bracketEnd = -1;
113
+ for (let i = bracketStart; i < content.length; i++) {
114
+ if (content[i] === '[') depth++;
115
+ if (content[i] === ']') {
116
+ depth--;
117
+ if (depth === 0) { bracketEnd = i; break; }
118
+ }
119
+ }
120
+ if (bracketEnd === -1) return safelist;
121
+
122
+ const safelistContent = content.slice(bracketStart + 1, bracketEnd);
123
+ const entries = safelistContent.matchAll(/['"]([^'"]+)['"]/g);
124
+ for (const m of entries) {
125
+ if (m[1]) safelist.add(m[1]);
126
+ }
127
+
128
+ return safelist;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Main
133
+ // ---------------------------------------------------------------------------
134
+
135
+ const fix = process.argv.includes('--fix');
136
+
137
+ console.log('Tailwind Safelist Audit');
138
+ console.log('='.repeat(60));
139
+ console.log(`Scanning: ${SCAN_DIRS.join(', ')}`);
140
+ console.log(`Preset: ${relative(ROOT, PRESET_PATH)}`);
141
+ console.log();
142
+
143
+ // Scan all source files
144
+ const allCandidates = new Map<string, Set<string>>(); // class -> set of files
145
+ let fileCount = 0;
146
+
147
+ for (const dir of SCAN_DIRS) {
148
+ const files = getAllFiles(join(ROOT, dir));
149
+ for (const file of files) {
150
+ if (file.includes('.stories.') || file.includes('.test.') || file.includes('.spec.')) continue;
151
+ fileCount++;
152
+ const content = readFileSync(file, 'utf-8');
153
+ const candidates = extractSafelistCandidates(content);
154
+ for (const cls of candidates) {
155
+ if (!allCandidates.has(cls)) allCandidates.set(cls, new Set());
156
+ allCandidates.get(cls)!.add(relative(ROOT, file));
157
+ }
158
+ }
159
+ }
160
+
161
+ console.log(`Scanned ${fileCount} files, found ${allCandidates.size} safelist candidates`);
162
+ console.log();
163
+
164
+ // Compare against current safelist
165
+ const currentSafelist = getCurrentSafelist();
166
+ const missing: Array<{ cls: string; files: string[] }> = [];
167
+ const alreadySafe: string[] = [];
168
+
169
+ for (const [cls, files] of allCandidates) {
170
+ if (currentSafelist.has(cls)) {
171
+ alreadySafe.push(cls);
172
+ } else {
173
+ missing.push({ cls, files: Array.from(files) });
174
+ }
175
+ }
176
+
177
+ // Also exclude standard utility classes that Tailwind extracts from dist/ scanning.
178
+ // Only arbitrary values ([...]), opacity modifiers (/N), and CSS var classes need safelisting.
179
+ const needsSafelist = (cls: string) =>
180
+ cls.includes('[') || cls.includes('/') || cls.includes('var(');
181
+
182
+ const filtered = missing.filter(m => needsSafelist(m.cls));
183
+ const skipped = missing.filter(m => !needsSafelist(m.cls));
184
+ missing.length = 0;
185
+ missing.push(...filtered);
186
+
187
+ // Report
188
+ console.log(`Current safelist: ${currentSafelist.size} entries`);
189
+ console.log(`Already safelisted: ${alreadySafe.length}`);
190
+ console.log(`Standard utilities (no safelist needed): ${skipped.length}`);
191
+ console.log(`Missing from safelist: ${missing.length}`);
192
+ console.log();
193
+
194
+ if (missing.length === 0) {
195
+ console.log('All safelist candidates are covered. No changes needed.');
196
+ process.exit(0);
197
+ }
198
+
199
+ // Sort missing by class name
200
+ missing.sort((a, b) => a.cls.localeCompare(b.cls));
201
+
202
+ console.log('Missing classes:');
203
+ for (const { cls, files } of missing) {
204
+ console.log(` ${cls}`);
205
+ for (const f of files.slice(0, 3)) {
206
+ console.log(` <- ${f}`);
207
+ }
208
+ if (files.length > 3) {
209
+ console.log(` ... and ${files.length - 3} more files`);
210
+ }
211
+ }
212
+
213
+ if (fix) {
214
+ console.log();
215
+ console.log('Appending missing classes to safelist...');
216
+
217
+ const presetContent = readFileSync(PRESET_PATH, 'utf-8');
218
+
219
+ // Find the end of the safelist array and insert before the closing ]
220
+ const safelistEndIdx = presetContent.indexOf('],', presetContent.indexOf('safelist:'));
221
+ if (safelistEndIdx === -1) {
222
+ console.error('Could not find safelist closing bracket in preset file');
223
+ process.exit(1);
224
+ }
225
+
226
+ const newEntries = missing.map(m => ` '${m.cls}',`).join('\n');
227
+ const comment = ` // Auto-added by audit-tailwind-safelist.ts (${new Date().toISOString().split('T')[0]})`;
228
+ const updated = presetContent.slice(0, safelistEndIdx) +
229
+ '\n' + comment + '\n' + newEntries + '\n' +
230
+ presetContent.slice(safelistEndIdx);
231
+
232
+ writeFileSync(PRESET_PATH, updated, 'utf-8');
233
+ console.log(`Added ${missing.length} classes to safelist.`);
234
+ } else {
235
+ console.log();
236
+ console.log('Run with --fix to add missing classes to the preset.');
237
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Export Static Design System
3
+ *
4
+ * After Storybook build, this script packages the design system
5
+ * into a client-ready deliverable (PDF or HTML bundle).
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const STORYBOOK_DIR = path.join(__dirname, '..', 'storybook-static');
12
+ const OUTPUT_DIR = path.join(__dirname, '..', 'exports');
13
+
14
+ async function exportDesignSystem() {
15
+ console.log('📦 Exporting design system...');
16
+
17
+ // Ensure output directory exists
18
+ if (!fs.existsSync(OUTPUT_DIR)) {
19
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
20
+ }
21
+
22
+ // Check if storybook build exists
23
+ if (!fs.existsSync(STORYBOOK_DIR)) {
24
+ console.error('❌ Storybook build not found. Run `npm run build-storybook` first.');
25
+ process.exit(1);
26
+ }
27
+
28
+ // Copy storybook-static to exports with timestamp
29
+ const timestamp = new Date().toISOString().split('T')[0];
30
+ const exportName = `design-system-${timestamp}`;
31
+ const exportPath = path.join(OUTPUT_DIR, exportName);
32
+
33
+ // Copy directory
34
+ fs.cpSync(STORYBOOK_DIR, exportPath, { recursive: true });
35
+
36
+ console.log(`✅ Design system exported to: ${exportPath}`);
37
+ console.log(` Open ${exportPath}/index.html in a browser to view.`);
38
+
39
+ // Create a manifest
40
+ const manifest = {
41
+ name: '@orbital/design-system',
42
+ version: '1.0.0',
43
+ exportedAt: new Date().toISOString(),
44
+ themes: ['wireframe', 'minimalist', 'almadar', 'winning-11'],
45
+ components: {
46
+ atoms: ['Avatar', 'Badge', 'Button', 'Card', 'Checkbox', 'Divider', 'Input', 'Radio', 'Select', 'Spinner', 'Typography'],
47
+ molecules: ['Accordion', 'Alert', 'Modal', 'Tabs', 'Toast'],
48
+ organisms: ['ConfirmDialog', 'DataTable', 'Header', 'Sidebar', 'StatCard', 'WizardContainer'],
49
+ templates: ['AuthLayout', 'CounterTemplate', 'DashboardLayout', 'GameTemplate', 'GenericAppTemplate'],
50
+ },
51
+ };
52
+
53
+ fs.writeFileSync(
54
+ path.join(exportPath, 'manifest.json'),
55
+ JSON.stringify(manifest, null, 2)
56
+ );
57
+
58
+ console.log(` Manifest created at: ${exportPath}/manifest.json`);
59
+ }
60
+
61
+ exportDesignSystem().catch(console.error);
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Unified Design System Generator
3
+ *
4
+ * Generates a complete client-specific design system from an .orb schema:
5
+ * 1. Generates custom theme CSS
6
+ * 2. Suggests domain-specific components
7
+ * 3. Generates component stubs
8
+ * 4. Builds Storybook documentation
9
+ * 5. Exports static design system package
10
+ */
11
+
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { execSync } from "child_process";
15
+ import { generateThemeFromSchema } from "./generate-theme-from-schema";
16
+ import {
17
+ suggestComponents,
18
+ generateComponentStubs,
19
+ } from "./suggest-components";
20
+
21
+ interface GeneratorOptions {
22
+ schemaPath: string;
23
+ outputDir: string;
24
+ clientName?: string;
25
+ buildStorybook?: boolean;
26
+ generateComponents?: boolean;
27
+ clientOnly?: boolean; // Only include client-specific components in Storybook
28
+ }
29
+
30
+ interface GeneratorResult {
31
+ themePath: string;
32
+ suggestedComponents: number;
33
+ generatedComponents: number;
34
+ storybookPath?: string;
35
+ }
36
+
37
+ /**
38
+ * Main generator function
39
+ */
40
+ export async function generateDesignSystem(
41
+ options: GeneratorOptions,
42
+ ): Promise<GeneratorResult> {
43
+ const {
44
+ schemaPath,
45
+ outputDir,
46
+ clientName,
47
+ buildStorybook = true,
48
+ generateComponents = true,
49
+ clientOnly = false,
50
+ } = options;
51
+
52
+ console.log("🎨 Orbital Design System Generator");
53
+ console.log("==================================\n");
54
+
55
+ // Validate schema exists
56
+ if (!fs.existsSync(schemaPath)) {
57
+ throw new Error(`Schema file not found: ${schemaPath}`);
58
+ }
59
+
60
+ // Read schema to get name
61
+ const schemaContent = fs.readFileSync(schemaPath, "utf-8");
62
+ const schema = JSON.parse(schemaContent);
63
+ const name =
64
+ clientName || schema.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
65
+
66
+ console.log(`📋 Processing schema: ${schema.name}`);
67
+ console.log(`📁 Output directory: ${outputDir}\n`);
68
+
69
+ // Create output directory
70
+ const clientOutputDir = path.join(outputDir, name);
71
+ if (!fs.existsSync(clientOutputDir)) {
72
+ fs.mkdirSync(clientOutputDir, { recursive: true });
73
+ }
74
+
75
+ // Step 1: Generate theme
76
+ console.log("1️⃣ Generating theme...");
77
+ const themePath = path.join(clientOutputDir, `${name}-theme.css`);
78
+ generateThemeFromSchema(schemaPath, themePath);
79
+ console.log(` ✅ Theme generated: ${themePath}\n`);
80
+
81
+ // Step 2: Suggest components
82
+ console.log("2️⃣ Analyzing schema for component suggestions...");
83
+ const { domains, suggestions } = suggestComponents(schemaPath);
84
+ console.log(` 📦 Detected domains: ${domains.join(", ") || "none"}`);
85
+ console.log(` 💡 Suggested components: ${suggestions.length}\n`);
86
+
87
+ // Step 3: Generate component stubs
88
+ let generatedCount = 0;
89
+ if (generateComponents && suggestions.length > 0) {
90
+ console.log("3️⃣ Generating component stubs...");
91
+ const componentsDir = path.join(clientOutputDir, "components");
92
+ generateComponentStubs(suggestions, componentsDir);
93
+ generatedCount = suggestions.length;
94
+ console.log(` ✅ Generated ${generatedCount} component stubs\n`);
95
+ }
96
+
97
+ // Step 4: Copy theme to design system themes folder for Storybook
98
+ const designSystemThemesDir = path.join(__dirname, "..", "themes");
99
+ const targetThemePath = path.join(designSystemThemesDir, `${name}.css`);
100
+ fs.copyFileSync(themePath, targetThemePath);
101
+ console.log(`4️⃣ Theme copied to design system: ${targetThemePath}\n`);
102
+
103
+ // Update index.css to include the new theme
104
+ const indexCssPath = path.join(__dirname, "..", "index.css");
105
+ const indexCss = fs.readFileSync(indexCssPath, "utf-8");
106
+ const importStatement = `@import './themes/${name}.css';`;
107
+
108
+ if (!indexCss.includes(importStatement)) {
109
+ const updatedCss = indexCss.replace(
110
+ "@tailwind base;",
111
+ `${importStatement}\n\n@tailwind base;`,
112
+ );
113
+ fs.writeFileSync(indexCssPath, updatedCss);
114
+ console.log(` Updated index.css with theme import\n`);
115
+ }
116
+
117
+ // Step 5: Update Storybook preview to include the new theme
118
+ const previewPath = path.join(__dirname, "..", ".storybook", "preview.tsx");
119
+ const previewContent = fs.readFileSync(previewPath, "utf-8");
120
+
121
+ if (!previewContent.includes(`'${name}':`)) {
122
+ const updatedPreview = previewContent.replace(
123
+ "defaultTheme: 'minimalist',",
124
+ `'${name}': '${name}',\n },\n defaultTheme: '${name}',`,
125
+ );
126
+ fs.writeFileSync(previewPath, updatedPreview);
127
+ console.log(` Updated Storybook preview with theme: ${name}\n`);
128
+ }
129
+
130
+ // Step 6: Build Storybook (optional)
131
+ let storybookPath: string | undefined;
132
+ if (buildStorybook) {
133
+ console.log("5️⃣ Building Storybook...");
134
+ const designSystemDir = path.join(__dirname, "..");
135
+
136
+ // Determine output path for storybook
137
+ // For client-only, output directly to the output directory (e.g., projects/winning-11/design-system/storybook-static)
138
+ // For full library, output to clientOutputDir/storybook
139
+ storybookPath = clientOnly
140
+ ? path.join(outputDir, "storybook-static")
141
+ : path.join(clientOutputDir, "storybook");
142
+
143
+ try {
144
+ // Install dependencies if needed
145
+ if (!fs.existsSync(path.join(designSystemDir, "node_modules"))) {
146
+ console.log(" Installing dependencies...");
147
+ execSync("npm install", { cwd: designSystemDir, stdio: "inherit" });
148
+ }
149
+
150
+ if (clientOnly) {
151
+ // Use the dedicated .storybook-client config and output directly to project
152
+ console.log(` 📦 Building client-only Storybook for: ${name}`);
153
+ console.log(` 📁 Output: ${storybookPath}`);
154
+
155
+ // Build using client-only config with custom output path
156
+ execSync(
157
+ `npx storybook build -c .storybook-client -o "${storybookPath}"`,
158
+ {
159
+ cwd: designSystemDir,
160
+ stdio: "inherit",
161
+ },
162
+ );
163
+ } else {
164
+ // Build full storybook
165
+ execSync("npm run build-storybook", {
166
+ cwd: designSystemDir,
167
+ stdio: "inherit",
168
+ });
169
+
170
+ // Copy to output
171
+ const storybookStaticDir = path.join(
172
+ designSystemDir,
173
+ "storybook-static",
174
+ );
175
+ if (fs.existsSync(storybookStaticDir)) {
176
+ fs.cpSync(storybookStaticDir, storybookPath, { recursive: true });
177
+ }
178
+ }
179
+
180
+ console.log(` ✅ Storybook built: ${storybookPath}\n`);
181
+ } catch (error) {
182
+ console.log(
183
+ " ⚠️ Storybook build skipped (run manually with npm run build-storybook)\n",
184
+ );
185
+ storybookPath = undefined;
186
+ }
187
+ }
188
+
189
+ // Step 7: Generate client portal and deployment files
190
+ console.log("6️⃣ Generating client portal and deployment files...");
191
+ const templatesDir = path.join(__dirname, "..", "..", "templates");
192
+ const projectRoot = path.join(outputDir, "..");
193
+
194
+ // Template variables
195
+ const projectDisplayName =
196
+ schema.name ||
197
+ name.replace(/-/g, " ").replace(/\b\w/g, (c: string) => c.toUpperCase());
198
+ const projectId = name.toLowerCase().replace(/[^a-z0-9-]/g, "");
199
+ const generatedDate = new Date().toISOString().split("T")[0];
200
+ const version = schema.version || "1.0.0";
201
+
202
+ const replaceTemplateVars = (content: string): string => {
203
+ return content
204
+ .replace(/\{\{PROJECT_NAME\}\}/g, projectDisplayName)
205
+ .replace(/\{\{PROJECT_ID\}\}/g, projectId)
206
+ .replace(/\{\{GENERATED_DATE\}\}/g, generatedDate)
207
+ .replace(/\{\{VERSION\}\}/g, version);
208
+ };
209
+
210
+ // Files to copy and process
211
+ const templateFiles = [
212
+ { src: "client-portal.html", dest: "index.html" },
213
+ { src: "firebase.json", dest: "firebase.json" },
214
+ { src: ".firebaserc", dest: ".firebaserc" },
215
+ { src: "deploy.sh", dest: "deploy.sh", executable: true },
216
+ ];
217
+
218
+ for (const file of templateFiles) {
219
+ const srcPath = path.join(templatesDir, file.src);
220
+ const destPath = path.join(projectRoot, file.dest);
221
+
222
+ if (fs.existsSync(srcPath)) {
223
+ let content = fs.readFileSync(srcPath, "utf-8");
224
+ content = replaceTemplateVars(content);
225
+ fs.writeFileSync(destPath, content);
226
+
227
+ if (file.executable) {
228
+ fs.chmodSync(destPath, "755");
229
+ }
230
+
231
+ console.log(` ✅ Generated: ${file.dest}`);
232
+ }
233
+ }
234
+ console.log("");
235
+
236
+ // Step 8: Generate manifest
237
+ const manifest = {
238
+ name: schema.name,
239
+ clientName: name,
240
+ generatedAt: new Date().toISOString(),
241
+ domains,
242
+ theme: {
243
+ path: `${name}-theme.css`,
244
+ variables: extractCssVariables(themePath),
245
+ },
246
+ components: {
247
+ suggested: suggestions.map((s) => ({
248
+ name: s.name,
249
+ category: s.category,
250
+ priority: s.priority,
251
+ })),
252
+ generated: generatedCount,
253
+ },
254
+ storybook: storybookPath ? "storybook/index.html" : null,
255
+ clientPortal: "../index.html",
256
+ };
257
+
258
+ const manifestPath = path.join(clientOutputDir, "manifest.json");
259
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
260
+ console.log(`📄 Manifest generated: ${manifestPath}\n`);
261
+
262
+ // Summary
263
+ console.log("🎉 Design System Generation Complete!");
264
+ console.log("=====================================");
265
+ console.log(` Theme: ${themePath}`);
266
+ console.log(` Suggested Components: ${suggestions.length}`);
267
+ console.log(` Generated Components: ${generatedCount}`);
268
+ if (storybookPath) {
269
+ console.log(` Storybook: ${storybookPath}/index.html`);
270
+ }
271
+ console.log(` Manifest: ${manifestPath}`);
272
+ console.log("");
273
+
274
+ return {
275
+ themePath,
276
+ suggestedComponents: suggestions.length,
277
+ generatedComponents: generatedCount,
278
+ storybookPath,
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Extract CSS variable names from a theme file
284
+ */
285
+ function extractCssVariables(themePath: string): string[] {
286
+ const content = fs.readFileSync(themePath, "utf-8");
287
+ const variablePattern = /--([a-z-]+):/g;
288
+ const variables: string[] = [];
289
+ let match;
290
+
291
+ while ((match = variablePattern.exec(content)) !== null) {
292
+ if (!variables.includes(`--${match[1]}`)) {
293
+ variables.push(`--${match[1]}`);
294
+ }
295
+ }
296
+
297
+ return variables;
298
+ }
299
+
300
+ // CLI usage
301
+ if (require.main === module) {
302
+ const args = process.argv.slice(2);
303
+
304
+ if (args.length < 1) {
305
+ console.log(`
306
+ Orbital Design System Generator
307
+
308
+ Usage:
309
+ npx tsx generate-design-system.ts <schema.orb> [options]
310
+
311
+ Options:
312
+ --output <dir> Output directory (default: ./exports)
313
+ --name <name> Client name override
314
+ --no-storybook Skip Storybook build
315
+ --no-components Skip component generation
316
+ --client-only Only include client-specific components in Storybook (not the full library)
317
+
318
+ Examples:
319
+ npx tsx generate-design-system.ts winning-11.orb
320
+ npx tsx generate-design-system.ts winning-11.orb --output ./clients
321
+ npx tsx generate-design-system.ts winning-11.orb --name garden-app --no-storybook
322
+ npx tsx generate-design-system.ts winning-11.orb --client-only
323
+ `);
324
+ process.exit(1);
325
+ }
326
+
327
+ const schemaPath = args[0];
328
+ const outputIndex = args.indexOf("--output");
329
+ const nameIndex = args.indexOf("--name");
330
+
331
+ const options: GeneratorOptions = {
332
+ schemaPath,
333
+ outputDir:
334
+ outputIndex !== -1
335
+ ? args[outputIndex + 1]
336
+ : path.join(__dirname, "..", "exports"),
337
+ clientName: nameIndex !== -1 ? args[nameIndex + 1] : undefined,
338
+ buildStorybook: !args.includes("--no-storybook"),
339
+ generateComponents: !args.includes("--no-components"),
340
+ clientOnly: args.includes("--client-only"),
341
+ };
342
+
343
+ generateDesignSystem(options).catch(console.error);
344
+ }