// Mounts ONLY the Tweaks panel. Page itself is static HTML. const TETRA_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": ["#b34a1f", "#efece4", "#16140f"], "dark": false, "displayFont": "Space Grotesk", "italicFont": "Fraunces", "density": "regular", "showAccent": true, "stickyHeader": true }/*EDITMODE-END*/; const FONT_PAIRS = { "Space Grotesk": { display: '"Space Grotesk", system-ui, sans-serif', italic: '"Fraunces", serif' }, "Inter Tight": { display: '"Inter Tight", system-ui, sans-serif', italic: '"Fraunces", serif' }, "Fraunces": { display: '"Fraunces", serif', italic: '"Fraunces", serif' }, "IBM Plex Sans": { display: '"IBM Plex Sans", system-ui, sans-serif', italic: '"Fraunces", serif' }, }; function TetraTweaks(){ const [t, setTweak] = useTweaks(TETRA_DEFAULTS); // apply palette → CSS vars React.useEffect(()=>{ const r = document.documentElement; const [accent, bg, ink] = t.palette; r.style.setProperty('--accent', accent); r.style.setProperty('--bg', bg); r.style.setProperty('--ink', ink); // derive supporting tones r.style.setProperty('--bg-2', mix(bg, ink, 0.06)); r.style.setProperty('--paper', mix(bg, '#ffffff', 0.35)); r.style.setProperty('--rule', mix(bg, ink, 0.18)); r.style.setProperty('--ink-2', mix(ink, bg, 0.18)); r.style.setProperty('--ink-3', mix(ink, bg, 0.50)); }, [t.palette]); React.useEffect(()=>{ document.documentElement.setAttribute('data-theme', t.dark ? 'dark' : 'light'); if(t.dark){ const r = document.documentElement; r.style.setProperty('--bg', '#121110'); r.style.setProperty('--bg-2', '#1c1a17'); r.style.setProperty('--paper', '#1a1916'); r.style.setProperty('--ink', '#f0ede4'); r.style.setProperty('--ink-2', '#cdc8b8'); r.style.setProperty('--ink-3', '#8a8674'); r.style.setProperty('--rule', '#2b2925'); } }, [t.dark, t.palette]); React.useEffect(()=>{ const pair = FONT_PAIRS[t.displayFont] || FONT_PAIRS["Space Grotesk"]; document.documentElement.style.setProperty('--font-display', pair.display); }, [t.displayFont]); React.useEffect(()=>{ const nav = document.querySelector('.nav'); if(nav) nav.style.position = t.stickyHeader ? 'sticky' : 'static'; }, [t.stickyHeader]); React.useEffect(()=>{ // density: tweak section padding const styleId = 'tetra-density-style'; let s = document.getElementById(styleId); if(!s){ s = document.createElement('style'); s.id = styleId; document.head.appendChild(s); } const map = { compact: '60px', regular: 'clamp(80px,11vw,140px)', spacious: 'clamp(110px,14vw,180px)' }; s.textContent = `section{padding-top:${map[t.density]};padding-bottom:${map[t.density]}}`; }, [t.density]); return ( setTweak('palette', v)} /> setTweak('dark', v)} /> setTweak('displayFont', v)} /> setTweak('density', v)} /> setTweak('stickyHeader', v)} /> ); } // utility: mix two hex colors function mix(a, b, t){ const pa = parseHex(a), pb = parseHex(b); if(!pa || !pb) return a; const r = Math.round(pa[0] + (pb[0]-pa[0])*t); const g = Math.round(pa[1] + (pb[1]-pa[1])*t); const bl = Math.round(pa[2] + (pb[2]-pa[2])*t); return `rgb(${r},${g},${bl})`; } function parseHex(h){ if(typeof h !== 'string') return null; if(h.startsWith('rgb')){ const m = h.match(/\d+/g); if(!m) return null; return [+m[0],+m[1],+m[2]]; } h = h.replace('#',''); if(h.length === 3) h = h.split('').map(c=>c+c).join(''); if(h.length !== 6) return null; return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)]; } ReactDOM.createRoot(document.getElementById('tweaks-root')).render();