- Create frontend/colors.mjs as SSOT for all design colors - Create scripts/generate-css-vars.mjs to auto-generate CSS variables - Update tailwind.config.ts to import colors from colors.mjs - Update frontend/package.json: add 'generate-css-vars' script to build pipeline - Update start_servers.py: include CSS variable generation before npm build - All color changes now require only one edit in colors.mjs - Build process auto-syncs CSS variables and Tailwind colors Eliminates DRY violation where colors were duplicated in: - tailwind.config.ts (Tailwind utility classes) - globals.css (CSS custom properties) Single source of truth approach ensures: ✓ No inconsistency between Tailwind and CSS variables ✓ One place to update colors for all contexts ✓ Automated CSS variable generation on every build Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
125 lines
3.8 KiB
JavaScript
125 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Generate CSS variables from colors.mjs into globals.css
|
|
* Run before npm build to ensure globals.css is in sync with colors.mjs
|
|
*
|
|
* Usage: node scripts/generate-css-vars.mjs
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { colors, spacing } from '../colors.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const globalsPath = path.join(__dirname, '../app/globals.css');
|
|
|
|
function flattenColorObject(obj, prefix = '') {
|
|
const result = {};
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const varName = prefix ? `${prefix}-${key}` : key;
|
|
|
|
if (typeof value === 'object' && value !== null && !key.includes('-')) {
|
|
// Nested object like primary: { DEFAULT: ..., container: ... }
|
|
Object.assign(result, flattenColorObject(value, varName));
|
|
} else if (typeof value === 'string') {
|
|
// Flat string value
|
|
result[varName] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function generateCSSVariables() {
|
|
const cssVars = flattenColorObject(colors);
|
|
|
|
let css = ' :root {\n';
|
|
css += ' /* Base colors from DESIGN.md - AUTO-GENERATED from colors.mjs */\n';
|
|
|
|
// Group variables by category for readability
|
|
const categories = {
|
|
'Base': ['background', 'on-background', 'foreground'],
|
|
'Surface': Object.keys(cssVars).filter(k => k.startsWith('surface')),
|
|
'Primary': Object.keys(cssVars).filter(k => k.startsWith('primary')),
|
|
'Secondary': Object.keys(cssVars).filter(k => k.startsWith('secondary')),
|
|
'Tertiary': Object.keys(cssVars).filter(k => k.startsWith('tertiary')),
|
|
'Error': Object.keys(cssVars).filter(k => k.startsWith('error')),
|
|
'Outline': Object.keys(cssVars).filter(k => k.startsWith('outline')),
|
|
'Inverse': Object.keys(cssVars).filter(k => k.startsWith('inverse')),
|
|
'Semantic': Object.keys(cssVars).filter(k =>
|
|
['border', 'muted', 'info', 'success', 'warning', 'alert'].includes(k)
|
|
),
|
|
};
|
|
|
|
for (const [category, keys] of Object.entries(categories)) {
|
|
if (keys.length > 0) {
|
|
css += `\n /* ${category} colors from DESIGN.md */\n`;
|
|
for (const key of keys) {
|
|
css += ` --${key}: ${cssVars[key]};\n`;
|
|
}
|
|
}
|
|
}
|
|
|
|
css += '\n /* Semantic spacing tokens */\n';
|
|
for (const [key, value] of Object.entries(spacing)) {
|
|
css += ` --spacing-${key}: ${value};\n`;
|
|
}
|
|
|
|
css += ' }\n';
|
|
return css;
|
|
}
|
|
|
|
function updateGlobalsCSS() {
|
|
const content = fs.readFileSync(globalsPath, 'utf-8');
|
|
|
|
// Find the start of @layer base and the :root closing brace
|
|
const layerBaseStart = content.indexOf('@layer base {');
|
|
const rootStart = content.indexOf(':root {', layerBaseStart);
|
|
|
|
if (layerBaseStart === -1 || rootStart === -1) {
|
|
console.error('❌ Could not find @layer base or :root in globals.css');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Find the matching closing brace for :root
|
|
let braceCount = 0;
|
|
let rootEnd = -1;
|
|
|
|
for (let i = rootStart; i < content.length; i++) {
|
|
if (content[i] === '{') braceCount++;
|
|
if (content[i] === '}') {
|
|
braceCount--;
|
|
if (braceCount === 0) {
|
|
rootEnd = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rootEnd === -1) {
|
|
console.error('❌ Could not find closing brace for :root');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Generate new CSS variables
|
|
const newCSSVars = generateCSSVariables();
|
|
|
|
// Reconstruct: everything before :root + generated :root + everything after
|
|
const before = content.substring(0, rootStart);
|
|
const after = content.substring(rootEnd + 1);
|
|
const newContent = before + newCSSVars + after;
|
|
|
|
fs.writeFileSync(globalsPath, newContent, 'utf-8');
|
|
console.log('✓ Generated CSS variables in globals.css from colors.mjs');
|
|
}
|
|
|
|
try {
|
|
updateGlobalsCSS();
|
|
} catch (error) {
|
|
console.error('❌ Error generating CSS variables:', error.message);
|
|
process.exit(1);
|
|
}
|