/* LW Design Group — shared UI */
const { useState, useEffect, useRef, useMemo } = React;
/* -------- tiny helpers -------- */
function cx(...xs) { return xs.filter(Boolean).join(" "); }
function useT(lang) { return I18N[lang] || I18N.en; }
function MonoLabel({ children, className }) {
return {children};
}
function Rule({ wide }) { return
; }
/* -------- IMAGE with reference badge --------
Brief: NO ref photos may ship. We display them in the prototype but mark them. */
function Img({ src, alt, ratio, className, showRef = true, style }) {
return (
{showRef && REF}
);
}
/* -------- NAVBAR -------- */
function Navbar({ route, go, lang, setLang, density }) {
const t = useT(lang);
const [scrolled, setScrolled] = useState(false);
const [openMobile, setOpenMobile] = useState(false);
useEffect(() => {
const on = () => setScrolled(window.scrollY > 12);
on();
window.addEventListener("scroll", on, { passive: true });
return () => window.removeEventListener("scroll", on);
}, []);
const items = [
["projects", t.nav_projects],
["sectors", t.nav_sectors],
["studio", t.nav_studio],
["offices", t.nav_offices],
["awards", t.nav_awards],
["contact", t.nav_contact],
];
return (
);
}
function Wordmark() {
/* A restrained mono-line LW wordmark — placeholder until client supplies SVG.
Marked as placeholder in the source. */
return (
);
}
function LangSwitch({ lang, setLang }) {
return (
{["en", "ar"].map(L => (
))}
);
}
/* -------- FOOTER -------- */
function Footer({ go, lang }) {
const t = useT(lang);
return (
);
}
/* -------- PROJECT CARD -------- */
function ProjectCard({ p, size = "md", onClick, lang }) {
const sector = SECTORS.find(s => s.id === p.sector);
return (
{sector?.label}
{p.year}
{p.name}
{p.location}
);
}
/* -------- CTA BAND -------- */
function StartBand({ go, lang }) {
const t = useT(lang);
return (
Studio enquiry
A new brief, a new building, a new room — start here.
);
}
/* -------- REVEAL on scroll -------- */
function Reveal({ children, as: As = "div", delay = 0, className }) {
const ref = useRef(null);
const [seen, setSeen] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) { setSeen(true); io.disconnect(); }
}, { threshold: 0.08, rootMargin: "0px 0px -40px 0px" });
io.observe(el);
return () => io.disconnect();
}, []);
return (
{children}
);
}
/* -------- MARQUEE (infinite ticker) -------- */
function Marquee({ items, speed = 60, separator = "·" }) {
// duplicate items so the loop never shows a seam
const row = [...items, ...items];
return (
{row.map((item, i) => (
{typeof item === "string"
? {item}
: (
<>
{item.name}
{item.city && {item.city}}
{item.year && {item.year}}
>
)}
{separator}
))}
);
}
Object.assign(window, {
cx, useT, MonoLabel, Rule, Img, Navbar, Footer, ProjectCard, StartBand, Wordmark,
Reveal, Marquee,
});