Some checks failed
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
CI/CD Pipeline / Cleanup (push) Has been cancelled
366 lines
12 KiB
JavaScript
Executable File
366 lines
12 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Design System Build Script
|
|
*
|
|
* Generates CSS variables and responsive utilities from comprehensive design system TOML
|
|
* Supports automatic dark mode, responsive breakpoints, and semantic components
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Simple TOML parser for our needs
|
|
function parseToml(content) {
|
|
const result = {};
|
|
let currentSection = result;
|
|
let sectionPath = [];
|
|
|
|
const lines = content.split('\n');
|
|
|
|
for (let line of lines) {
|
|
line = line.trim();
|
|
|
|
// Skip empty lines and comments
|
|
if (!line || line.startsWith('#')) continue;
|
|
|
|
// Handle sections
|
|
if (line.startsWith('[') && line.endsWith(']')) {
|
|
const section = line.slice(1, -1);
|
|
sectionPath = section.split('.');
|
|
|
|
currentSection = result;
|
|
for (let i = 0; i < sectionPath.length; i++) {
|
|
const key = sectionPath[i];
|
|
if (!currentSection[key]) {
|
|
currentSection[key] = {};
|
|
}
|
|
currentSection = currentSection[key];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Handle key-value pairs
|
|
if (line.includes('=')) {
|
|
const [key, ...valueParts] = line.split('=');
|
|
let value = valueParts.join('=').trim();
|
|
|
|
// Remove quotes and handle inline comments
|
|
if (value.startsWith('"') && value.includes('"', 1)) {
|
|
const endQuote = value.indexOf('"', 1);
|
|
value = value.slice(1, endQuote);
|
|
} else if (value.includes('#')) {
|
|
value = value.split('#')[0].trim();
|
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
}
|
|
|
|
currentSection[key.trim()] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
class DesignSystemBuilder {
|
|
constructor(designSystemPath) {
|
|
this.designSystemPath = designSystemPath;
|
|
this.designSystem = this.loadDesignSystem();
|
|
}
|
|
|
|
loadDesignSystem() {
|
|
try {
|
|
const content = fs.readFileSync(this.designSystemPath, 'utf8');
|
|
return parseToml(content);
|
|
} catch (error) {
|
|
console.error(`Error loading design system: ${error.message}`);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Generate CSS custom properties from design tokens
|
|
generateCSSVariables() {
|
|
const { colors, typography, spacing, radius, shadows, z_index, breakpoints, components } = this.designSystem;
|
|
|
|
let css = `/* Design System Variables */\n/* Generated from design-system.toml */\n/* Do not edit manually */\n\n`;
|
|
|
|
// Root variables (light theme)
|
|
css += `:root {\n`;
|
|
|
|
// Breakpoints (for JavaScript access)
|
|
if (breakpoints) {
|
|
css += ` /* Breakpoints */\n`;
|
|
Object.entries(breakpoints).forEach(([key, value]) => {
|
|
css += ` --breakpoint-${key}: ${value};\n`;
|
|
});
|
|
css += `\n`;
|
|
}
|
|
|
|
// Colors
|
|
if (colors) {
|
|
css += ` /* Colors */\n`;
|
|
Object.entries(colors).forEach(([key, value]) => {
|
|
if (key !== 'dark' && typeof value === 'string') {
|
|
css += ` --color-${key.replace(/_/g, '-')}: ${value};\n`;
|
|
}
|
|
});
|
|
css += `\n`;
|
|
}
|
|
|
|
// Typography
|
|
if (typography) {
|
|
css += ` /* Typography */\n`;
|
|
Object.entries(typography).forEach(([key, value]) => {
|
|
css += ` --${key.replace(/_/g, '-')}: ${value};\n`;
|
|
});
|
|
css += `\n`;
|
|
}
|
|
|
|
// Spacing
|
|
if (spacing) {
|
|
css += ` /* Spacing */\n`;
|
|
Object.entries(spacing).forEach(([key, value]) => {
|
|
css += ` --${key.replace(/_/g, '-')}: ${value};\n`;
|
|
});
|
|
css += `\n`;
|
|
}
|
|
|
|
// Border radius
|
|
if (radius) {
|
|
css += ` /* Border Radius */\n`;
|
|
Object.entries(radius).forEach(([key, value]) => {
|
|
css += ` --${key.replace(/_/g, '-')}: ${value};\n`;
|
|
});
|
|
css += `\n`;
|
|
}
|
|
|
|
// Shadows
|
|
if (shadows) {
|
|
css += ` /* Shadows */\n`;
|
|
Object.entries(shadows).forEach(([key, value]) => {
|
|
css += ` --${key.replace(/_/g, '-')}: ${value};\n`;
|
|
});
|
|
css += `\n`;
|
|
}
|
|
|
|
// Z-index
|
|
if (z_index) {
|
|
css += ` /* Z-Index */\n`;
|
|
Object.entries(z_index).forEach(([key, value]) => {
|
|
css += ` --${key.replace(/_/g, '-')}: ${value};\n`;
|
|
});
|
|
css += `\n`;
|
|
}
|
|
|
|
css += `}\n\n`;
|
|
|
|
// Dark theme variables
|
|
if (colors && colors.dark) {
|
|
css += `@media (prefers-color-scheme: dark) {\n :root {\n`;
|
|
css += ` /* Dark theme colors */\n`;
|
|
Object.entries(colors.dark).forEach(([key, value]) => {
|
|
css += ` --color-${key.replace(/_/g, '-')}: ${value};\n`;
|
|
});
|
|
css += ` }\n}\n\n`;
|
|
}
|
|
|
|
// Explicit dark mode class
|
|
if (colors && colors.dark) {
|
|
css += `.dark {\n`;
|
|
css += ` /* Dark theme colors (explicit) */\n`;
|
|
Object.entries(colors.dark).forEach(([key, value]) => {
|
|
css += ` --color-${key.replace(/_/g, '-')}: ${value};\n`;
|
|
});
|
|
css += `}\n\n`;
|
|
}
|
|
|
|
return css;
|
|
}
|
|
|
|
// Generate responsive breakpoint mixins for CSS
|
|
generateResponsiveUtilities() {
|
|
const { breakpoints } = this.designSystem;
|
|
if (!breakpoints) return '';
|
|
|
|
let css = `/* Responsive Utilities */\n\n`;
|
|
|
|
Object.entries(breakpoints).forEach(([key, value]) => {
|
|
css += `@media (min-width: ${value}) {\n`;
|
|
css += ` .${key}\\:container {\n`;
|
|
css += ` max-width: ${value};\n`;
|
|
css += ` margin-left: auto;\n`;
|
|
css += ` margin-right: auto;\n`;
|
|
css += ` padding-left: var(--space-4, 1rem);\n`;
|
|
css += ` padding-right: var(--space-4, 1rem);\n`;
|
|
css += ` }\n`;
|
|
css += `}\n\n`;
|
|
});
|
|
|
|
return css;
|
|
}
|
|
|
|
// Generate semantic component classes
|
|
generateComponentClasses() {
|
|
const { components, colors } = this.designSystem;
|
|
if (!components) return '';
|
|
|
|
let css = `/* Semantic Component Classes */\n\n`;
|
|
|
|
// Button components
|
|
if (components.button) {
|
|
const button = components.button;
|
|
|
|
css += `/* Button Base */\n`;
|
|
css += `.btn {\n`;
|
|
css += ` display: inline-flex;\n`;
|
|
css += ` align-items: center;\n`;
|
|
css += ` justify-content: center;\n`;
|
|
css += ` border-radius: var(--${button.border_radius?.replace(/_/g, '-')}, var(--radius-md));\n`;
|
|
css += ` font-weight: var(--${button.font_weight?.replace(/_/g, '-')}, var(--font-medium));\n`;
|
|
css += ` transition: ${button.transition || 'all 0.2s ease-in-out'};\n`;
|
|
css += ` border: none;\n`;
|
|
css += ` cursor: pointer;\n`;
|
|
css += ` text-decoration: none;\n`;
|
|
css += ` outline: none;\n`;
|
|
css += ` focus-visible: ring-2 ring-offset-2;\n`;
|
|
css += `}\n\n`;
|
|
|
|
// Button sizes
|
|
if (button.sizes) {
|
|
Object.entries(button.sizes).forEach(([size, config]) => {
|
|
css += `.btn-${size} {\n`;
|
|
css += ` padding: var(--${config.padding_y?.replace(/_/g, '-')}) var(--${config.padding_x?.replace(/_/g, '-')});\n`;
|
|
css += ` font-size: var(--${config.font_size?.replace(/_/g, '-')});\n`;
|
|
css += `}\n\n`;
|
|
});
|
|
}
|
|
|
|
// Button variants
|
|
if (button.variants) {
|
|
Object.entries(button.variants).forEach(([variant, config]) => {
|
|
css += `.btn-${variant} {\n`;
|
|
css += ` background-color: var(--color-${config.bg?.replace(/_/g, '-')});\n`;
|
|
css += ` color: var(--color-${config.text?.replace(/_/g, '-')});\n`;
|
|
if (config.hover_bg) {
|
|
css += `}\n`;
|
|
css += `.btn-${variant}:hover {\n`;
|
|
css += ` background-color: var(--color-${config.hover_bg?.replace(/_/g, '-')});\n`;
|
|
}
|
|
css += `}\n\n`;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Card component
|
|
if (components.card) {
|
|
const card = components.card;
|
|
css += `/* Card Component */\n`;
|
|
css += `.card {\n`;
|
|
css += ` background-color: var(--color-${card.background?.replace(/_/g, '-')});\n`;
|
|
css += ` border: 1px solid var(--color-${card.border?.replace(/_/g, '-')});\n`;
|
|
css += ` border-radius: var(--${card.border_radius?.replace(/_/g, '-')});\n`;
|
|
css += ` box-shadow: var(--${card.shadow?.replace(/_/g, '-')});\n`;
|
|
css += ` padding: var(--${card.padding?.replace(/_/g, '-')});\n`;
|
|
css += `}\n\n`;
|
|
|
|
if (card.dark) {
|
|
css += `@media (prefers-color-scheme: dark) {\n`;
|
|
css += ` .card {\n`;
|
|
css += ` background-color: var(--color-${card.dark.background?.replace(/_/g, '-')});\n`;
|
|
css += ` border-color: var(--color-${card.dark.border?.replace(/_/g, '-')});\n`;
|
|
css += ` }\n`;
|
|
css += `}\n\n`;
|
|
|
|
css += `.dark .card {\n`;
|
|
css += ` background-color: var(--color-${card.dark.background?.replace(/_/g, '-')});\n`;
|
|
css += ` border-color: var(--color-${card.dark.border?.replace(/_/g, '-')});\n`;
|
|
css += `}\n\n`;
|
|
}
|
|
}
|
|
|
|
// Input component
|
|
if (components.input) {
|
|
const input = components.input;
|
|
css += `/* Input Component */\n`;
|
|
css += `.input {\n`;
|
|
css += ` width: 100%;\n`;
|
|
css += ` background-color: var(--color-${input.background?.replace(/_/g, '-')});\n`;
|
|
css += ` border: 1px solid var(--color-${input.border?.replace(/_/g, '-')});\n`;
|
|
css += ` border-radius: var(--${input.border_radius?.replace(/_/g, '-')});\n`;
|
|
css += ` padding: var(--${input.padding_y?.replace(/_/g, '-')}) var(--${input.padding_x?.replace(/_/g, '-')});\n`;
|
|
css += ` font-size: var(--${input.font_size?.replace(/_/g, '-')});\n`;
|
|
css += ` transition: border-color 0.2s ease-in-out;\n`;
|
|
css += ` outline: none;\n`;
|
|
css += `}\n\n`;
|
|
|
|
css += `.input:focus {\n`;
|
|
css += ` border-color: var(--color-${input.focus_border?.replace(/_/g, '-')});\n`;
|
|
css += ` box-shadow: 0 0 0 3px var(--color-${input.focus_border?.replace(/_/g, '-')})20;\n`;
|
|
css += `}\n\n`;
|
|
|
|
if (input.dark) {
|
|
css += `@media (prefers-color-scheme: dark) {\n`;
|
|
css += ` .input {\n`;
|
|
css += ` background-color: var(--color-${input.dark.background?.replace(/_/g, '-')});\n`;
|
|
css += ` border-color: var(--color-${input.dark.border?.replace(/_/g, '-')});\n`;
|
|
css += ` }\n`;
|
|
css += `}\n\n`;
|
|
|
|
css += `.dark .input {\n`;
|
|
css += ` background-color: var(--color-${input.dark.background?.replace(/_/g, '-')});\n`;
|
|
css += ` border-color: var(--color-${input.dark.border?.replace(/_/g, '-')});\n`;
|
|
css += `}\n\n`;
|
|
}
|
|
}
|
|
|
|
return css;
|
|
}
|
|
|
|
// Generate complete design system CSS
|
|
generateFullCSS() {
|
|
const variables = this.generateCSSVariables();
|
|
const responsive = this.generateResponsiveUtilities();
|
|
const components = this.generateComponentClasses();
|
|
|
|
return variables + responsive + components;
|
|
}
|
|
|
|
// Build and save CSS file
|
|
build(outputPath) {
|
|
console.log('🎨 Building design system CSS...');
|
|
|
|
const css = this.generateFullCSS();
|
|
|
|
// Ensure output directory exists
|
|
const outputDir = path.dirname(outputPath);
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
fs.writeFileSync(outputPath, css);
|
|
|
|
const stats = fs.statSync(outputPath);
|
|
console.log(`✅ Design system built: ${outputPath} (${Math.round(stats.size / 1024)}KB)`);
|
|
|
|
return css;
|
|
}
|
|
}
|
|
|
|
// CLI handling
|
|
if (require.main === module) {
|
|
const designSystemPath = path.join(__dirname, '..', 'assets', 'styles', 'themes', 'design-system.toml');
|
|
const outputPath = path.join(__dirname, '..', 'public', 'styles', 'design-system.css');
|
|
|
|
const builder = new DesignSystemBuilder(designSystemPath);
|
|
builder.build(outputPath);
|
|
|
|
console.log('\\n💡 Usage in components:');
|
|
console.log('- Colors: var(--color-brand-primary), var(--color-neutral-500)');
|
|
console.log('- Spacing: var(--space-4), var(--space-lg)');
|
|
console.log('- Typography: var(--text-lg), var(--font-semibold)');
|
|
console.log('- Components: .btn.btn-md.btn-primary, .card, .input');
|
|
console.log('- Responsive: .sm:container, .md:container, .lg:container');
|
|
}
|
|
|
|
module.exports = { DesignSystemBuilder }; |