#!/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 };