@almadar/ui 5.127.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.
- package/dist/{UserContext-BKckAUv7.d.cts → UserContext-g_LcDiGN.d.cts} +6 -22
- package/dist/{UserContext-BKckAUv7.d.ts → UserContext-g_LcDiGN.d.ts} +6 -22
- package/dist/avl/index.cjs +297 -6
- package/dist/avl/index.js +297 -6
- package/dist/components/index.cjs +300 -6
- package/dist/components/index.d.cts +119 -1
- package/dist/components/index.d.ts +119 -1
- package/dist/components/index.js +301 -7
- package/dist/context/index.d.cts +2 -2
- package/dist/context/index.d.ts +2 -2
- package/dist/providers/index.cjs +394 -107
- package/dist/providers/index.d.cts +15 -4
- package/dist/providers/index.d.ts +15 -4
- package/dist/providers/index.js +393 -108
- package/dist/runtime/index.cjs +297 -6
- package/dist/runtime/index.js +297 -6
- package/package.json +7 -6
- 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,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
|
+
});
|