// XBD Collective — shared components const { useState, useEffect, useRef, useMemo } = React; // ===== Logo (inline SVG, currentColor-aware via path fill from CSS) function Logo({ height = 30 }) { // Simplified mark — XBD wordmark — uses fg via CSS .nav-logo svg path return ( ); } // ===== Image with lazy fade-in function Img({ src, alt, ratio, className = "", style = {}, focus = "center" }) { const [loaded, setLoaded] = useState(false); const wrapStyle = { ...style }; if (ratio) wrapStyle.aspectRatio = ratio; return (
{alt setLoaded(true)} style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: focus, opacity: loaded ? 1 : 0, transform: loaded ? "scale(1)" : "scale(1.02)", transition: "opacity 700ms cubic-bezier(.2,.6,.2,1), transform 700ms cubic-bezier(.2,.6,.2,1), filter 600ms" }} />
); } // ===== Project card (used on Home featured + Projects index) function ProjectCard({ project, t, size = "default", onClick, ar = false }) { const [hover, setHover] = useState(false); const sectorLabel = (window.XBD.sectors.find(s => s.id === project.sector) || {}).label; const scopes = project.scope.map(sid => (window.XBD.scopes.find(x => x.id === sid) || {}).label).join(" · "); return ( { e.preventDefault(); onClick && onClick(project); }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{ display: "block", color: "inherit", cursor: "pointer" }} >
{project.name} {project.visualisation && (
Visualisation
)} {project.outcome && project.outcome.includes("Award") || project.outcome && project.outcome.includes("Commended") || project.outcome && project.outcome.toLowerCase().includes("highly commended") ? (
Awarded
) : null}
{project.name}
{project.location}
{project.year}
{sectorLabel}{scopes ? " · " + scopes : ""}
); } // ===== Sectional chip function FilterChip({ label, active, onClick, accent = false }) { return ( ); } // ===== Scroll-reveal wrapper (lightweight intersection observer) function Reveal({ children, delay = 0, y = 16, style = {} }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { if (!ref.current) return; const obs = new IntersectionObserver( ([e]) => { if (e.isIntersecting) { setShown(true); obs.disconnect(); } }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" } ); obs.observe(ref.current); return () => obs.disconnect(); }, []); return (
{children}
); } // ===== Quiet number / counter function StatBlock({ value, label, sub }) { return (
{value}
{label}
{sub ?
{sub}
: null}
); } // ===== Section header function SectionHeader({ eyebrow, title, kicker }) { return (
{eyebrow ?
{eyebrow}
: null}

{title}

{kicker ?
{kicker}
: null}
); } // ===== Footer function Footer({ t, onNavigate, ar = false }) { const F = window.XBD.firm; return ( ); } Object.assign(window, { Logo, Img, ProjectCard, FilterChip, Reveal, StatBlock, SectionHeader, Footer });