/* Bluehaus — App root: routing, Tweaks panel */
const { useState: useStateApp, useEffect: useEffectApp } = React;
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"paperPalette": ["#F2EFE8", "#0E2A47", "#0A0F16"],
"density": "regular",
"fontSize": 16,
"showMarquee": true,
"accentTreatment": "navy"
}/*EDITMODE-END*/;
const PALETTE_OPTIONS = [
["#F2EFE8", "#0E2A47", "#0A0F16"], // warm cream + brand navy (default)
["#FAFAF7", "#10202F", "#0A0F16"], // cool paper + slate navy
["#EDE7DA", "#1B3A5C", "#1A1410"], // sand + ink blue
["#F4F1EC", "#2B4358", "#0A0F16"], // softer
["#0A0F16", "#F2EFE8", "#F2EFE8"], // INVERTED dark mode
];
function App() {
const [route, setRoute] = useStateApp(parseRoute(window.location.hash));
const [t, setTweak] = (window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}]);
// Hash routing
useEffectApp(() => {
const onHash = () => {
setRoute(parseRoute(window.location.hash));
// scroll to top on route change unless anchor
window.scrollTo({ top: 0, behavior: 'instant' });
};
window.addEventListener('hashchange', onHash);
return () => window.removeEventListener('hashchange', onHash);
}, []);
const navigate = (path) => {
window.location.hash = '#' + path;
};
// apply palette
useEffectApp(() => {
const [paper, brand, ink] = t.paperPalette;
const r = document.documentElement;
const isDark = t.paperPalette === PALETTE_OPTIONS[4];
r.style.setProperty('--paper', paper);
r.style.setProperty('--blue', brand);
r.style.setProperty('--ink', ink);
r.style.setProperty('--paper-soft', isDark ? '#11161F' : shade(paper, -6));
r.style.setProperty('--surface', isDark ? '#171C26' : shade(paper, 4));
r.style.setProperty('--ink-soft', isDark ? '#D8D2C5' : shade(ink, 14));
r.style.setProperty('--muted', isDark ? '#9098A2' : '#5F6770');
r.style.setProperty('--line', isDark ? 'rgba(255,255,255,0.12)' : shade(paper, -16));
r.style.setProperty('--line-soft', isDark ? 'rgba(255,255,255,0.06)' : shade(paper, -10));
r.style.setProperty('--blue-deep', isDark ? '#04080F' : shade(brand, -32));
r.style.setProperty('--azure', isDark ? '#5A8AD8' : shade(brand, 30));
r.style.setProperty('--azure-bright', isDark ? '#7AA8E8' : shade(brand, 50));
document.body.style.background = paper;
document.body.style.color = ink;
}, [t.paperPalette]);
// apply density
useEffectApp(() => {
const r = document.documentElement;
if (t.density === 'compact') { r.style.setProperty('--gutter', 'clamp(16px, 3vw, 40px)'); }
if (t.density === 'regular') { r.style.setProperty('--gutter', 'clamp(20px, 4vw, 56px)'); }
if (t.density === 'comfy') { r.style.setProperty('--gutter', 'clamp(24px, 5vw, 80px)'); }
}, [t.density]);
// apply font size
useEffectApp(() => {
document.body.style.fontSize = t.fontSize + 'px';
}, [t.fontSize]);
const page = route.page;
const param = route.param;
const query = route.query;
return (
<>
{page === 'home' && }
{page === 'projects' && !param && }
{page === 'projects' && param && }
{page === 'sectors' && }
{page === 'services' && }
{page === 'about' && }
{page === 'awards' && }
{page === 'contact' && }
{/* TWEAKS */}
{window.TweaksPanel && (
setTweak('paperPalette', v)} />
setTweak('density', v)} />
setTweak('fontSize', v)} />
)}
>
);
}
function parseRoute(hash) {
const h = (hash || '').replace(/^#/, '') || 'home';
const [pathPart, queryPart] = h.split('?');
const parts = pathPart.split('/').filter(Boolean);
const page = parts[0] || 'home';
const param = parts[1] || null;
const query = {};
if (queryPart) {
queryPart.split('&').forEach(kv => {
const [k, v] = kv.split('=');
query[k] = decodeURIComponent(v || '');
});
}
return { page, param, query };
}
// Color helper — lighten/darken hex
function shade(hex, percent) {
const h = hex.replace('#', '');
const num = parseInt(h, 16);
let r = (num >> 16) & 0xff;
let g = (num >> 8) & 0xff;
let b = num & 0xff;
const f = percent / 100;
r = Math.max(0, Math.min(255, Math.round(r + (percent < 0 ? r : (255 - r)) * f)));
g = Math.max(0, Math.min(255, Math.round(g + (percent < 0 ? g : (255 - g)) * f)));
b = Math.max(0, Math.min(255, Math.round(b + (percent < 0 ? b : (255 - b)) * f)));
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();