← back to Dw Universe
scripts/apply-design-fixes.js
173 lines
// Apply the P0 + P1 design-review fixes to public/index.html in one pass:
// - P0: wrap script in IIFE
// - P1a: consolidate type scale (13 tiers → 6)
// - P1b: consolidate spacing scale (14 distinct → 4-base)
// - P1c: replace raw hex literals with var(--*) tokens
// - P1d: add IntersectionObserver pause for off-screen .node-inner float animations
//
// Idempotent: if IIFE wrap already present, skips. Token + size maps use word boundaries
// to avoid partial matches inside SVG fills, viewBox, etc.
const fs = require('fs');
const path = require('path');
const FILE = path.join(__dirname, '..', 'public', 'index.html');
let html = fs.readFileSync(FILE, 'utf8');
const before = html;
// ─── 1. Type scale consolidation ────────────────────────────────────────
// Map: existing size → nearest tier on 11 / 13 / 18 / 22 / 32 (clamp() heroes left alone)
const TYPE_MAP = {
'8px': '11px',
'9px': '11px',
'10px': '11px',
'11px': '11px',
'12px': '13px',
'13px': '13px',
'14px': '13px',
'16px': '18px',
'18px': '18px',
'20px': '22px',
'22px': '22px',
'24px': '22px',
'36px': '32px',
};
for (const [from, to] of Object.entries(TYPE_MAP)) {
// Only inside font-size declarations
const re = new RegExp(`(font-size:\\s*)${from.replace('.','\\.')}\\b`, 'g');
html = html.replace(re, `$1${to}`);
}
// ─── 2. Spacing scale consolidation ─────────────────────────────────────
// Map: existing → nearest 4-base step (4 / 8 / 12 / 16 / 24 / 32 / 48 / 64)
const SPACE_MAP = {
'6px': '8px',
'10px': '12px',
'14px': '16px',
'18px': '16px',
'22px': '24px',
'28px': '32px',
'36px': '32px',
'56px': '48px',
};
// Apply ONLY in margin/padding contexts (avoid breaking border-width, etc.)
for (const [from, to] of Object.entries(SPACE_MAP)) {
// multi-value forms: "padding: 12px 6px;", "margin-top: 6px;", "margin: 0 0 6px"
const re1 = new RegExp(`((?:padding|margin)(?:-(?:top|bottom|left|right))?:\\s*[^;]*?)\\b${from}\\b`, 'g');
// run twice to catch repeated occurrences in same declaration
html = html.replace(re1, `$1${to}`).replace(re1, `$1${to}`).replace(re1, `$1${to}`);
}
// ─── 3. Raw-hex → CSS var() replacement ─────────────────────────────────
// Only replace where the hex IS a brand token. Regex anchors to value-start with
// :, space, or ( to avoid e.g. inside #anchor-link.
const HEX_TO_TOKEN = {
'#f4ecd8': 'var(--ink)',
'#c9b687': 'var(--gold)',
'#1b1814': 'var(--bg)', // dark theme
'#fafaf6': 'var(--bg)', // light theme
'#0c0a08': 'var(--bg)', // dark theme alt
'#15110d': 'var(--bg-2)', // dark theme alt
'#a39888': 'var(--ink-mute)',
'#5a5246': 'var(--ink-faint)',
'#2a241c': 'var(--rule)',
'#e6cc88': 'var(--gold-warm)',
'#d36e4d': 'var(--signal)',
'#b86a4a': 'var(--c-decade)',
'#6b8e6f': 'var(--c-craft)',
'#4a6b8e': 'var(--c-use)',
};
for (const [hex, tok] of Object.entries(HEX_TO_TOKEN)) {
// Match the literal hex preceded by : space ( , (typical CSS-value contexts)
// and followed by ; ) , ! space or end-of-line. Skip occurrences inside :root { } block.
const re = new RegExp(`([:\\s(,])${hex.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}([\\s,;)!]|$)`, 'g');
html = html.replace(re, `$1${tok}$2`);
}
// But the :root token defs need their hex preserved. Restore those:
const ROOT_DEFS = {
'--bg: var(--bg)': '--bg: #0c0a08',
'--bg-2: var(--bg-2)': '--bg-2: #15110d',
'--ink: var(--ink)': '--ink: #f4ecd8',
'--ink-mute: var(--ink-mute)': '--ink-mute: #a39888',
'--ink-faint: var(--ink-faint)':'--ink-faint: #5a5246',
'--rule: var(--rule)': '--rule: #2a241c',
'--gold: var(--gold)': '--gold: #c9b687',
'--gold-warm: var(--gold-warm)':'--gold-warm: #e6cc88',
'--signal: var(--signal)': '--signal: #d36e4d',
'--c-material: var(--gold)': '--c-material: #c9b687',
'--c-decade: var(--c-decade)': '--c-decade: #b86a4a',
'--c-craft: var(--c-craft)': '--c-craft: #6b8e6f',
'--c-use: var(--c-use)': '--c-use: #4a6b8e',
};
for (const [from, to] of Object.entries(ROOT_DEFS)) {
html = html.split(from).join(to);
}
// Light-theme overrides too
html = html.replace(/--bg:\s*var\(--bg\)/g, '--bg: #0c0a08');
// Restore the [data-theme="light"] block
html = html.replace(/\[data-theme="light"\] \{[^}]+\}/, m => m
.replace(/var\(--bg\)/, '#fafaf6')
.replace(/var\(--bg-2\)/, '#f0ece1')
.replace(/var\(--ink\)/, '#1b1814')
.replace(/var\(--ink-mute\)/, '#6b6357')
.replace(/var\(--ink-faint\)/, '#b0a890')
.replace(/var\(--rule\)/, '#e5dfd0')
.replace(/var\(--gold\)/, '#8a7440')
.replace(/var\(--gold-warm\)/, '#a6884d')
);
// ─── 4. IntersectionObserver to pause off-screen .node-inner animations ─
const PAUSE_CSS = `
/* Off-screen pause — battery-friendly star-float */
svg#map.paused .node-inner { animation-play-state: paused }
`;
if (!html.includes('svg#map.paused .node-inner')) {
html = html.replace('/* Star float:', PAUSE_CSS + '\n/* Star float:');
}
const PAUSE_JS = `
// Pause star-float animations when constellation off-screen (perf + battery)
(function () {
if (!('IntersectionObserver' in window)) return;
const map = document.getElementById('map');
if (!map) return;
const io = new IntersectionObserver((entries) => {
for (const e of entries) map.classList.toggle('paused', !e.isIntersecting);
}, { threshold: 0 });
io.observe(map);
})();
`;
if (!html.includes('Pause star-float animations when constellation off-screen')) {
// Insert near end of script, just before the buildMap() call
html = html.replace(/buildMap\(\);\s*<\/script>/, `buildMap();\n${PAUSE_JS}\n</script>`);
}
// ─── 5. P0: IIFE wrap ───────────────────────────────────────────────────
// Wrap the LARGE main script block (not the tiny preconnect/font script).
// Identify by the presence of `const CAT_COLOR` near the top.
if (!html.includes('// IIFE wrap (P0)')) {
html = html.replace(
/<script>\s*(const CAT_COLOR = \{)/,
'<script>\n// IIFE wrap (P0) — keep all 11 features\' state out of the global scope.\n(function () {\n$1'
);
html = html.replace(
/buildMap\(\);\s*\n\s*\/\/ Pause star-float/,
`buildMap();\n\n// Pause star-float`
);
// Close the IIFE before the closing </script>
html = html.replace(/\}\);\s*\n<\/script>\s*<\/body>/, `});\n})();\n</script>\n</body>`);
}
// ─── Done ───────────────────────────────────────────────────────────────
if (html === before) {
console.log('no-op (already applied)');
} else {
fs.writeFileSync(FILE, html);
console.log('applied:');
console.log(' type-scale: 13 tiers → 6 (11/13/18/22/32 + clamp() heroes)');
console.log(' spacing: 14 distinct → 4-base (4/8/12/16/24/32/48/64)');
console.log(' hex→token: ~15 brand-color literals replaced');
console.log(' iobserver: off-screen .node-inner pause armed');
console.log(' iife: script body wrapped (closure)');
console.log(` size: ${before.length} → ${html.length} bytes (${html.length - before.length >= 0 ? '+' : ''}${html.length - before.length})`);
}