// app.jsx — Tweaks panel + dynamic bits for Salha Hakim site
// The bulk of the site is in static HTML (index.html). This script
// powers the Tweaks panel (accent / font / density / dark / hero layout),
// scroll reveals, and the floating theme toggle.
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accent": "purple",
"density": "regular",
"hero": "left",
"fontPair": "instrument",
"dark": false,
"grain": true
}/*EDITMODE-END*/;
const FONT_PAIRS = {
instrument: {
display: '"Instrument Serif", Georgia, serif',
body: '"Manrope", ui-sans-serif, system-ui, sans-serif',
label: "Instrument × Manrope",
},
newsreader: {
display: '"Newsreader", Georgia, serif',
body: '"Geist", ui-sans-serif, system-ui, sans-serif',
label: "Newsreader × Geist",
},
serif_mono: {
display: '"DM Serif Display", Georgia, serif',
body: '"IBM Plex Sans", ui-sans-serif, system-ui, sans-serif',
label: "DM Serif × Plex",
},
};
function applyTweaks(t) {
const root = document.documentElement;
root.setAttribute("data-accent", t.accent);
root.setAttribute("data-density", t.density);
root.setAttribute("data-hero", t.hero);
root.setAttribute("data-theme", t.dark ? "dark" : "light");
root.style.setProperty("--font-display", FONT_PAIRS[t.fontPair].display);
root.style.setProperty("--font-body", FONT_PAIRS[t.fontPair].body);
document.body.classList.toggle("no-grain", !t.grain);
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
React.useEffect(() => { applyTweaks(t); }, [t]);
// Bind floating theme toggle (always present, even when tweaks off)
React.useEffect(() => {
const btn = document.getElementById("theme-toggle");
if (!btn) return;
const handler = () => setTweak("dark", !t.dark);
btn.addEventListener("click", handler);
return () => btn.removeEventListener("click", handler);
}, [t.dark]);
return (
{
const map = { "#542d84": "purple", "#eb6624": "orange", "#e43226": "red", "#de1b51": "magenta", "#f1c334": "yellow" };
setTweak("accent", map[v]);
}}
/>
setTweak("dark", v)} />
setTweak("grain", v)} />
({ value: k, label: FONT_PAIRS[k].label }))}
onChange={(v) => setTweak("fontPair", v)}
/>
setTweak("density", v)}
/>
setTweak("hero", v)}
/>
);
}
// Grain toggle CSS
const grainStyle = document.createElement("style");
grainStyle.textContent = `.no-grain::before { display: none !important; }`;
document.head.appendChild(grainStyle);
// Mount
const rootEl = document.getElementById("tweaks-root");
if (rootEl) ReactDOM.createRoot(rootEl).render();
// ── Scroll reveals (vanilla, no React dependency)
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add("in");
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: "0px 0px -60px 0px" });
document.querySelectorAll(".reveal").forEach((el) => io.observe(el));
// ── Nav scroll state
const nav = document.querySelector(".nav");
const onScroll = () => nav?.classList.toggle("scrolled", window.scrollY > 12);
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();