// ============================================================
// Monarch — Socle partagé des simulateurs (window.MSC)
// Chargé AVANT les fichiers de simulateurs. Aucune dépendance externe.
// ============================================================
(function () {
const { useState, useEffect, useRef } = React;
const C = {
blanc: "#FFFFFF", albatre: "#FAF9F6", encre: "#1B1D1C",
ardoise: "#5B7C99", ardoiseF: "#3D5468",
sauge: "#5C6F5C", saugeF: "#48584A",
gris: "#737373", grisC: "#9A9A95", trait: "#E4E1DA"
};
const SERIF = "'Fraunces', Georgia, serif";
const BODY = "'Archivo', system-ui, sans-serif";
const CAL_LINK = "hugo-groslambert/diagnostic1h";
const CAL_ORIGIN = "https://cal.eu";
// ---- fiscalité partagée ----
const IS_SEUIL = 42500;
function IS(base) {
const bas = Math.min(Math.max(base, 0), IS_SEUIL);
const haut = Math.max(0, base - IS_SEUIL);
return bas * 0.15 + haut * 0.25;
}
const flatTax = (m) => m * 0.314;
// Barème progressif DMTG en ligne directe (par part, après abattement)
const DMTG_TRANCHES = [
[8072, 0.05], [12109, 0.10], [15932, 0.15],
[552324, 0.20], [902838, 0.30], [1805677, 0.40], [Infinity, 0.45]
];
function dmtg(baseTaxable) {
let reste = Math.max(0, baseTaxable), prev = 0, tax = 0;
for (const [plafond, taux] of DMTG_TRANCHES) {
const part = Math.min(reste, plafond - prev);
if (part <= 0) break;
tax += part * taux;
reste -= part; prev = plafond;
if (reste <= 0) break;
}
return tax;
}
// ---- format ----
const euro0 = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 });
const fmt = (n) => euro0.format(Math.round(n || 0));
const pct = (n, b) => b > 0 ? Math.round((n / b) * 100) : 0;
// ---- CountUp ----
function useCountUp(target, run, duration) {
const [val, setVal] = useState(0);
useEffect(() => {
if (!run) { setVal(0); return; }
const d = duration || 1100; let raf, start;
const ease = (t) => 1 - Math.pow(1 - t, 3);
const tick = (ts) => { if (!start) start = ts; const p = Math.min(1, (ts - start) / d); setVal(target * ease(p)); if (p < 1) raf = requestAnimationFrame(tick); };
raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf);
}, [target, run, duration]);
return val;
}
function CountEuro({ value, run, style }) { return {fmt(useCountUp(value, run))}; }
// ---- styles partagés ----
function injectCore() {
if (document.getElementById("msc-styles")) return;
const s = document.createElement("style");
s.id = "msc-styles";
s.textContent = `
.msc-grid2 { display:grid; grid-template-columns:1fr 1fr; gap:22px; align-items:stretch; }
.msc-inputs { display:grid; grid-template-columns:1fr 1fr; gap:24px 34px; }
.msc-line { stroke-dasharray:760; stroke-dashoffset:760; transition:stroke-dashoffset 1s ease; }
.msc-line.on { stroke-dashoffset:0; }
.msc-soft { opacity:0; transition:opacity .8s ease; }
.msc-soft.on { opacity:1; }
.msc-slideup { opacity:0; transform:translateY(22px); transition:opacity .6s ease, transform .6s cubic-bezier(.2,.7,.3,1); }
.msc-slideup.on { opacity:1; transform:none; }
.msc-lockbar { transition: stroke-dashoffset 1.1s ease .2s; }
@media (max-width:820px){ .msc-grid2{grid-template-columns:1fr !important;} .msc-inputs{grid-template-columns:1fr !important; gap:20px !important;} }
`;
document.head.appendChild(s);
}
// ---- atomes UI ----
function Eyebrow({ children, color }) {
return {children};
}
function Label({ children }) {
return ;
}
function MoneyInput({ value, onChange, min, max, step }) {
return (
{ const n = e.target.value.replace(/[^\d]/g, ""); onChange(n === "" ? "" : Number(n)); }}
style={{ fontFamily: SERIF, fontSize: "clamp(26px,3.4vw,40px)", fontWeight: 500, color: C.encre, border: "none", background: "transparent", outline: "none", width: "100%", padding: 0 }} />
€
{max != null &&
onChange(Number(e.target.value))}
style={{ width: "100%", marginTop: 14, accentColor: C.sauge }} />}
);
}
function IntInput({ value, onChange, min, max }) {
return (
{value}
);
}
const stepBtn = { fontFamily: BODY, fontSize: 20, width: 46, border: "none", background: "transparent", color: C.encre, cursor: "pointer" };
function Toggle({ options, value, onChange, full }) {
return (
{options.map((o) => {
const on = o.id === value;
return ;
})}
);
}
// Enveloppe de section (cohérente avec le simulateur d'extraction)
function Shell({ eyebrow, title, intro, children }) {
return (
{eyebrow}
{title}
{intro &&
{intro}
}
{children}
);
}
function InputsCard({ children, onCompute, computing, busyLabel, label }) {
return (
{children}
);
}
// Encart doctrine — fond anthracite, apparition slide-up
function Doctrine({ open, eyebrow, title, children, accent }) {
return (
{eyebrow &&
{eyebrow}}
{title}
{children}
);
}
// Alerte doctrinale claire (fond clair, bordure gauche)
function Alert({ open, title, children }) {
return (
Vigilance juridique
{title}
{children}
);
}
// CTA + portail email (modale) -> ouvre Cal
function CTAEmail({ note }) {
const book = (e) => {
if (window.Cal) { if (e) e.preventDefault(); window.Cal("modal", { calLink: CAL_LINK, config: { layout: "month_view" } }); }
};
return (
);
}
// Hook : déclenche `reveal` après montage des résultats
function useReveal(active, delay) {
const [on, setOn] = useState(false);
useEffect(() => { if (active) { const id = setTimeout(() => setOn(true), delay || 200); return () => clearTimeout(id); } else setOn(false); }, [active, delay]);
return on;
}
// Jauge horizontale (valeur conservée vs perte hachurée)
function Gauge({ segments, base, run, height }) {
const [w, setW] = useState(false);
useEffect(() => { if (run) { const id = setTimeout(() => setW(true), 60); return () => clearTimeout(id); } else setW(false); }, [run]);
return (
{segments.map((s, i) =>
)}
);
}
function Stat({ label, value, run, color, sub, small }) {
return (
);
}
// ---- Capture de leads (email) ----
const LEAD_KEY = "monarch.lead.email";
const LEAD_FULL_KEY = "monarch.lead.full";
function getLeadEmail() { try { return localStorage.getItem(LEAD_KEY) || ""; } catch (e) { return ""; } }
function getLeadFull() { try { return JSON.parse(localStorage.getItem(LEAD_FULL_KEY) || "null"); } catch (e) { return null; } }
function hasFullProfile() { const f = getLeadFull(); return !!(f && f.nom && f.prenom && f.telephone && f.email); }
function leadCapture(info, source) {
const data = typeof info === "string" ? { email: info } : (info || {});
if (data.email) { try { localStorage.setItem(LEAD_KEY, data.email); } catch (e) {} }
if (data.nom && data.prenom && data.telephone && data.email) {
try { localStorage.setItem(LEAD_FULL_KEY, JSON.stringify({ nom: data.nom, prenom: data.prenom, telephone: data.telephone, email: data.email })); } catch (e) {}
}
try {
fetch("lead.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nom: data.nom || "", prenom: data.prenom || "", telephone: data.telephone || "", email: data.email || "", source: source || "" }) }).catch(() => {});
} catch (e) {}
}
// Modale d'accès — `full` => nom/prénom/téléphone/email ; sinon email seul
function LeadModal({ open, onUnlock, onClose, source, title, subtitle, cta, full }) {
const saved = full ? (getLeadFull() || {}) : {};
const [nom, setNom] = useState(saved.nom || "");
const [prenom, setPrenom] = useState(saved.prenom || "");
const [telephone, setTelephone] = useState(saved.telephone || "");
const [email, setEmail] = useState(saved.email || getLeadEmail() || "");
if (!open) return null;
const submit = (e) => {
e.preventDefault();
if (!/.+@.+\..+/.test(email)) return;
if (full && (!nom.trim() || !prenom.trim() || telephone.replace(/[^\d]/g, "").length < 8)) return;
const payload = full
? { nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim(), email: email.trim() }
: { email: email.trim() };
leadCapture(payload, source || "acces");
onUnlock(email);
};
const field = { fontFamily: BODY, fontSize: 15, color: C.encre, background: C.blanc, border: `1px solid ${C.trait}`, borderRadius: 2, padding: "14px 14px", outline: "none", width: "100%" };
return (
onClose && onClose()} style={{ position: "fixed", inset: 0, zIndex: 300, display: "flex", alignItems: "center", justifyContent: "center", padding: 20, background: "rgba(244,242,238,.55)", backdropFilter: "blur(7px)", WebkitBackdropFilter: "blur(7px)" }}>
e.stopPropagation()} style={{ position: "relative", background: C.blanc, maxWidth: 480, width: "100%", padding: "clamp(28px,5vw,44px)", borderTop: `3px solid ${C.sauge}`, boxShadow: "0 30px 80px -30px rgba(0,0,0,.4)" }}>
{onClose &&
}
Accès réservé
{title || "Accédez aux simulateurs"}
{subtitle || "Renseignez votre adresse professionnelle pour ouvrir l'ensemble des outils d'ingénierie patrimoniale."}
Confidentiel · vos données ne sont jamais cédées · vous pourrez vous désinscrire à tout moment.
);
}
// Bloc pédagogique « comment ça marche » (étapes simples)
function Explainer({ title, steps }) {
return (
Comment ça marche
{title || "En quelques mots"}
);
}
// Avantages / Inconvénients — présentation honnête
function ProsCons({ open, pour, contre, note }) {
const tick = (color) => —;
const col = (titre, items, color, bg) => (
{titre}
{items.map((it, i) =>
{tick(color)}
{it}
)}
);
return (
En toute transparence
{col("Avantages", pour, C.saugeF, C.albatre)}
{col("Inconvénients & limites", contre, "#a23b3b", C.blanc)}
{note &&
{note}
}
);
}
window.MSC = {
C, SERIF, BODY, CAL_LINK, CAL_ORIGIN,
IS, flatTax, dmtg, fmt, pct,
useCountUp, CountEuro, injectCore, useReveal,
Eyebrow, Label, MoneyInput, IntInput, Toggle, Shell, InputsCard, Doctrine, Alert, CTAEmail, Gauge, Stat, Explainer, ProsCons,
getLeadEmail, leadCapture, LeadModal, getLeadFull, hasFullProfile
};
})();