/* 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 (
{alt} {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 (
go("home")} aria-label="LW — home">
{openMobile && (
{items.map(([id, label]) => ( { go(id); setOpenMobile(false); }}>{label} ))}
)}
); } function Wordmark() { /* A restrained mono-line LW wordmark — placeholder until client supplies SVG. Marked as placeholder in the source. */ return ( LW DESIGN GROUP · DUBAI ); } 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 ( {p.name}
{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, });