Rustelo/scripts/build/build-design-system.js
Jesús Pérez 7cab57b645
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
chore: update layout and files
2026-02-08 20:18:46 +00:00

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 };