// 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();