// ui.jsx — shared UI primitives for NOVA Résidence
// Loaded BEFORE app.jsx so its components and styles are available.
// ──────────────────────────────────────────────────────────────────────────
// PLACEHOLDER (striped image stand-in)
// ──────────────────────────────────────────────────────────────────────────
function Placeholder({ label, ratio = "16/9", tone = "warm", radius = 0, style = {}, children, src }) {
const theme = window.useTheme ? window.useTheme() : null;
const showLabel = theme ? theme.t.showPlaceholderLabels : true;
const accent = theme ? theme.palette.accent : "#C5A572";
const ink = theme ? theme.palette.ink : "#1F2A33";
const bg = tone === "dark" ?
theme ? theme.palette.deep : "#1F2A33" :
tone === "ink" ?
ink :
theme ? theme.palette.bg2 : "#EBE6DA";
const stripeColor = tone === "dark" || tone === "ink" ?
"rgba(255,255,255,0.06)" :
"rgba(31,42,51,0.06)";
// If a real image src is provided, render it; placeholder label only shown if no src OR theme.showPlaceholderLabels is true AND we want overlay.
return (
{src &&

}
{!src && showLabel &&
◇
{label || "image"}
}
{children}
);
}
// ──────────────────────────────────────────────────────────────────────────
// LOGO — wordmark + monogram (text-based since no logo file provided yet)
// ──────────────────────────────────────────────────────────────────────────
function Logo({ size = 32, variant = "full", dark = false, onClick }) {
// Use the horizontal brand mark when full, the round monogram when compact
const src = variant === "mark" ?
dark ? "assets/logo-mono-dore.png" : "assets/logo-rond.png" :
dark ? "assets/logo-fond-sombre.png" : "assets/logo-horizontal.png";
const h = variant === "mark" ? size : Math.round(size * 1.05);
return (
{if (onClick) {e.preventDefault();onClick();}}}
style={{ display: "inline-flex", alignItems: "center", textDecoration: "none" }}>
);
}
// ──────────────────────────────────────────────────────────────────────────
// BUTTONS
// ──────────────────────────────────────────────────────────────────────────
function Button({ children, variant = "primary", onClick, href, size = "md", icon, style = {} }) {
const sizing = size === "lg" ?
{ padding: "16px 28px", fontSize: 14 } :
size === "sm" ?
{ padding: "8px 14px", fontSize: 12 } :
{ padding: "12px 22px", fontSize: 13 };
const variantStyle = variant === "primary" ?
{ background: "var(--accent)", color: "#FAF6EC", border: "1px solid var(--accent)" } :
variant === "dark" ?
{ background: "var(--ink)", color: "var(--bg)", border: "1px solid var(--ink)" } :
variant === "ghost" ?
{ background: "transparent", color: "var(--ink)", border: "1px solid var(--line)" } :
variant === "link" ?
{ background: "transparent", color: "var(--ink)", border: 0, padding: 0, textDecoration: "underline", textUnderlineOffset: 4 } :
{};
const Tag = href ? "a" : "button";
return (
{if (variant === "primary") e.currentTarget.style.background = "var(--accent-dark)";}}
onMouseLeave={(e) => {if (variant === "primary") e.currentTarget.style.background = "var(--accent)";}}>
{children}
{icon && {icon}}
);
}
// ──────────────────────────────────────────────────────────────────────────
// SECTION SHELL
// ──────────────────────────────────────────────────────────────────────────
function Section({ children, bg = "bg", id, style = {}, fullBleed = false, label }) {
return (
{label && {label}}
{children}
);
}
function Eyebrow({ children, style = {} }) {
return (
{children}
);
}
// ──────────────────────────────────────────────────────────────────────────
// TOP BAR
// ──────────────────────────────────────────────────────────────────────────
function TopBar({ route, navigate }) {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
// Close the mobile menu whenever the route changes.
useEffect(() => {setMenuOpen(false);}, [route]);
const links = [
{ id: "residence", label: "La Résidence" },
{ id: "appartements", label: "Appartements" },
{ id: "orange", label: "Orange" },
{ id: "habiter", label: "Habiter" },
{ id: "investir", label: "Investir" },
{ id: "contact", label: "Contact" }];
const go = (id) => {navigate(id);setMenuOpen(false);};
return (
navigate("accueil")} />
{ if (window.plausible) { plausible('Clic WhatsApp'); } }}
style={{ display: "none", alignItems: "center", justifyContent: "center", width: 40, height: 40, borderRadius: "50%", background: "#25D366", textDecoration: "none", flexShrink: 0, marginLeft: "auto", marginRight: 8 }}>
{/* Mobile dropdown menu */}
);
}
// ──────────────────────────────────────────────────────────────────────────
// FOOTER
// ──────────────────────────────────────────────────────────────────────────
function Footer({ navigate }) {
const [plaquetteOpen, setPlaquetteOpen] = React.useState(false);
return (
);
}
// ──────────────────────────────────────────────────────────────────────────
// COOKIE CONSENT — bandeau RGPD + chargement conditionnel du pixel Meta
// ──────────────────────────────────────────────────────────────────────────
const CONSENT_KEY = "nova_cookie_consent";
function loadMetaPixel() {
if (window.__novaMetaPixelLoaded) return;
window.__novaMetaPixelLoaded = true;
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
window.fbq('init', '860717183426332');
window.fbq('track', 'PageView');
}
function loadGoogleAds() {
if (window.__novaGoogleAdsLoaded) return;
window.__novaGoogleAdsLoaded = true;
var gs = document.createElement('script');
gs.async = true;
gs.src = 'https://www.googletagmanager.com/gtag/js?id=AW-18202499337';
document.head.appendChild(gs);
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
window.gtag = gtag;
gtag('js', new Date());
gtag('config', 'AW-18202499337');
}
function loadConsentedTags() {
loadMetaPixel();
loadGoogleAds();
}
function CookieBanner({ navigate }) {
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
let choice = null;
try { choice = localStorage.getItem(CONSENT_KEY); } catch (e) {}
if (choice === "accepted") loadConsentedTags();
else if (choice !== "refused") setVisible(true);
const reopen = () => setVisible(true);
window.addEventListener("nova:open-cookie-settings", reopen);
return () => window.removeEventListener("nova:open-cookie-settings", reopen);
}, []);
const choose = (value) => {
try { localStorage.setItem(CONSENT_KEY, value); } catch (e) {}
setVisible(false);
if (value === "accepted") loadConsentedTags();
};
if (!visible) return null;
return (
);
}
function cookieBtn(primary) {
return {
fontFamily: "var(--font-b)", fontWeight: 500, fontSize: 13,
letterSpacing: "0.04em", textTransform: "uppercase",
padding: "12px 26px", cursor: "pointer", borderRadius: 0,
border: primary ? "1px solid var(--accent)" : "1px solid rgba(242,235,221,.4)",
background: primary ? "var(--accent)" : "transparent",
color: primary ? "#1F2A33" : "var(--bg)",
};
}
function FooterCol({ title, links, navigate }) {
return (
);
}
// ──────────────────────────────────────────────────────────────────────────
// FORMAT HELPERS
// ──────────────────────────────────────────────────────────────────────────
const fmtPrice = (n) => new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(n);
const fmtN = (n) => new Intl.NumberFormat("fr-FR").format(Math.round(n));
// expose
const WHATSAPP_URL = "https://wa.me/33766461865?text=Bonjour%2C%20je%20suis%20int%C3%A9ress%C3%A9(e)%20par%20la%20R%C3%A9sidence%20NOVA%20%C3%A0%20Orange.";
function WhatsAppFab() {
const onClick = () => { if (window.plausible) { plausible('Clic WhatsApp'); } };
return (
);
}
Object.assign(window, {
Placeholder, Logo, Button, Section, Eyebrow,
TopBar, Footer, FooterCol, CookieBanner, WhatsAppFab, WHATSAPP_URL, fmtPrice, fmtN
});