// 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);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const links = [
{ id: "residence", label: "La Résidence" },
{ id: "appartements", label: "Appartements" },
{ id: "orange", label: "Orange" },
{ id: "investir", label: "Investir" },
{ id: "contact", label: "Contact" },
];
return (
);
}
// ──────────────────────────────────────────────────────────────────────────
// FOOTER
// ──────────────────────────────────────────────────────────────────────────
function Footer({ navigate }) {
return (
);
}
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
Object.assign(window, {
Placeholder, Logo, Button, Section, Eyebrow,
TopBar, Footer, FooterCol, fmtPrice, fmtN,
});