Fixes #13: CSS performance optimizations

- Add CSS minification support to build script (--minify flag)
  - Pure JS minifier: removes comments, whitespace, redundant chars
  - Reports size reduction percentage
  - Preserves CSS custom properties, data URIs, strings

- Refactor _08_themes.css: extend CSS custom properties pattern
  - Define theme-specific properties per theme (--rpg-content-bg,
    --rpg-content-box-shadow, --rpg-header-text-shadow, etc.)
  - Replace triple-theme selector duplication with single [data-theme]
    selectors — children inherit variables automatically
  - Reduces themes.css from 379 to ~220 lines
  - Reduces style.css from 305KB to 294KB (3.8% reduction)

- Add build:css:min npm script for production builds
- Add 9 tests for CSS build: minification, size reduction, CSS
  variable preservation, data URI preservation, theme refactoring
This commit is contained in:
2026-07-12 14:55:36 +02:00
parent 29f7865927
commit df7d3b3a38
6 changed files with 737 additions and 512 deletions
+80 -9
View File
@@ -3,7 +3,9 @@
* CSS Build Script
* Concatenates modular CSS files from src/styles/ into style.css
*
* Usage: node scripts/build-css.js
* Usage:
* node scripts/build-css.js # Development build (readable)
* node scripts/build-css.js --minify # Production build (minified)
*
* File order matters! Files are concatenated in alphabetical order.
* Prefix files with numbers to control order: _01_variables.css, _02_panel.css, etc.
@@ -19,8 +21,62 @@ const rootDir = path.resolve(__dirname, '..');
const stylesDir = path.join(rootDir, 'src', 'styles');
const outputFile = path.join(rootDir, 'style.css');
const shouldMinify = process.argv.includes('--minify');
/**
* Minify CSS by removing comments, unnecessary whitespace, and redundant characters.
* Safe for CSS custom properties, strings, and data URIs.
*/
function minifyCSS(css) {
let result = css;
// Remove CSS comments (/* ... */) — but not inside strings
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
// Remove lines that are only whitespace after comment removal
result = result.replace(/^\s*[\r\n]/g, '\n');
// Collapse multiple newlines into one
result = result.replace(/\n{2,}/g, '\n');
// Remove spaces around structural characters outside of strings
result = result.replace(/(\{|\}|;|:|,)/g, '$1');
// Remove space before {
result = result.replace(/\s+\{/g, '{');
// Remove space after {
result = result.replace(/\{\s+/g, '{');
// Remove space before }
result = result.replace(/\s+\}/g, '}');
// Remove space after }
result = result.replace(/\}\s+/g, '}');
// Remove space around :
result = result.replace(/\s*:\s*/g, ':');
// Remove space around ;
result = result.replace(/;\s*/g, ';');
// Remove space around ,
result = result.replace(/,\s*/g, ',');
// Remove trailing semicolon before }
result = result.replace(/;\}/g, '}');
// Collapse multiple spaces into one (preserves spaces inside strings roughly)
result = result.replace(/[ \t]+/g, ' ');
// Remove leading/trailing whitespace
result = result.trim();
return result;
}
async function buildCSS() {
console.log('Building CSS from src/styles/...');
console.log(`Building CSS from src/styles/...${shouldMinify ? ' (minified)' : ' (readable)'}`);
// Read all CSS files from src/styles/
const files = await fs.readdir(stylesDir);
@@ -42,15 +98,30 @@ async function buildCSS() {
for (const file of cssFiles) {
const content = await fs.readFile(file, 'utf-8');
const basename = path.basename(file);
contents.push(`/* ============================================ */`);
contents.push(`/* ${basename} */`);
contents.push(`/* ============================================ */`);
contents.push('');
contents.push(content);
contents.push('');
if (shouldMinify) {
// In minified mode, skip comment separators
contents.push(content);
} else {
contents.push(`/* ============================================ */`);
contents.push(`/* ${basename} */`);
contents.push(`/* ============================================ */`);
contents.push('');
contents.push(content);
contents.push('');
}
}
const combined = contents.join('\n');
let combined = contents.join('\n');
// Apply minification if requested
if (shouldMinify) {
const beforeSize = combined.length;
combined = minifyCSS(combined);
const afterSize = combined.length;
const reduction = (((beforeSize - afterSize) / beforeSize) * 100).toFixed(1);
console.log(`\n🗜️ Minification: ${beforeSize.toLocaleString()}${afterSize.toLocaleString()} bytes (${reduction}% reduction)`);
}
// Write to style.css
await fs.writeFile(outputFile, combined, 'utf-8');