@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.
- package/dist/avl/index.cjs +349 -11
- package/dist/avl/index.js +349 -11
- package/dist/components/index.cjs +352 -11
- package/dist/components/index.d.cts +119 -5
- package/dist/components/index.d.ts +119 -5
- package/dist/components/index.js +353 -12
- package/dist/providers/index.cjs +349 -11
- package/dist/providers/index.js +349 -11
- package/dist/runtime/index.cjs +349 -11
- package/dist/runtime/index.js +349 -11
- package/package.json +6 -5
- package/scripts/audit-tailwind-safelist.ts +237 -0
- package/scripts/export-static.js +61 -0
- package/scripts/generate-design-system.ts +344 -0
- package/scripts/generate-theme-from-schema.ts +480 -0
- package/scripts/generate.ts +835 -0
- package/scripts/strip-base-tokens-from-themes.mjs +179 -0
- package/scripts/suggest-components.ts +508 -0
- package/scripts/theme-contrast-audit.mjs +132 -0
- package/scripts/types.ts +282 -0
- package/themes/_contract.md +1 -1
|
@@ -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
|
+
}
|