/* ═══════════════════════════════════════════════════════════════
FOXBOT PRO — MOBILE SHELL
App container · Header · Bottom Nav · Bottom Sheet · Haptic
═══════════════════════════════════════════════════════════════ */
const { useState: useStateM, useEffect: useEffectM, useRef: useRefM, useCallback: useCallbackM } = React;
/* ─── Haptic helper (silently noop on unsupported) ─── */
function haptic(pattern = 10) {
try {
if (navigator.vibrate) navigator.vibrate(pattern);
} catch (e) { /* noop */ }
}
const HAPTIC = {
tap: () => haptic(8),
press: () => haptic(18),
toggle: () => haptic(12),
snap: () => haptic([8, 4, 8]),
success: () => haptic([22, 14, 22]),
warn: () => haptic([35, 22, 35]),
err: () => haptic([50, 30, 50, 30, 50]),
};
window.HAPTIC = HAPTIC;
/* ─── Mobile detection hook ─── */
function useIsMobile(breakpoint = 768) {
const [is, setIs] = useStateM(() =>
typeof window !== "undefined" && window.matchMedia(`(max-width: ${breakpoint}px)`).matches
);
useEffectM(() => {
const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);
const onChange = (e) => setIs(e.matches);
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, [breakpoint]);
useEffectM(() => {
document.documentElement.dataset.mobile = is ? "true" : "false";
}, [is]);
return is;
}
window.useIsMobile = useIsMobile;
/* ─── Brand mark (small fox glyph) ─── */
function MBrandMark() {
return (
);
}
/* ─── Header ─── */
const ROUTE_TITLES = {
dashboard: "Dashboard",
trading: "Trading",
signaux: "Signaux",
charts: "Charts",
more: "Plus",
journal: "Journal",
stats: "Statistiques",
settings: "Paramètres",
volatile: "Volatiles",
};
function MobileHeader({ route, mode, onToggleMode, onNotif }) {
const [scrolled, setScrolled] = useStateM(false);
useEffectM(() => {
const onScroll = () => setScrolled(window.scrollY > 6);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
// Notif-dot uniquement si vraie raison de l'allumer :
// - un signal éligible récent (≥ 7/10 actif)
// - une position dont SL/TP est proche (< 0.5%)
const store = window.useStore ? window.useStore() : (window.MOCK || {});
const hasNotif = (() => {
const sigs = Array.isArray(store.SIGNALS) ? store.SIGNALS : [];
if (sigs.some(s => s && s.state === "active" && (s.score || 0) >= 8)) return true;
const pos = Array.isArray(store.POSITIONS) ? store.POSITIONS : [];
return pos.some(p => {
if (!p) return false;
const px = (store.PRICES && store.PRICES[p.pair]) || p.current;
const slDist = p.sl ? Math.abs((p.sl - px) / px * 100) : 99;
const tp1Dist = p.tp1 ? Math.abs((p.tp1 - px) / px * 100) : 99;
return slDist < 0.5 || tp1Dist < 0.5;
});
})();
return (
);
}
/* ─── Bottom Nav ─── */
function MobileBottomNav({ route, onRoute, onMore }) {
// Compteurs RÉELS depuis le store
const store = window.useStore ? window.useStore() : (window.MOCK || {});
const posCount = Array.isArray(store.POSITIONS) ? store.POSITIONS.length : 0;
const signalsCount = Array.isArray(store.SIGNALS)
? store.SIGNALS.filter(s => s && (s.state === "active" || (s.score >= 7 && s.direction))).length
: 0;
const items = [
{ id: "dashboard", i: Icon.Home, l: "Home" },
{ id: "trading", i: Icon.Activity, l: "Trading", badge: posCount || null },
{ id: "signaux", i: Icon.Zap, l: "Signaux", badge: signalsCount || null },
{ id: "charts", i: Icon.Chart, l: "Charts" },
{ id: "more", i: MoreIcon, l: "Plus" },
];
return (
);
}
function MoreIcon({ size = 22 }) {
return (
);
}
/* ─── Bottom Sheet ─── */
function BottomSheet({ open, onClose, title, children, height }) {
const [closing, setClosing] = useStateM(false);
const [mounted, setMounted] = useStateM(open);
useEffectM(() => {
if (open) { setMounted(true); setClosing(false); }
else if (mounted) {
setClosing(true);
const t = setTimeout(() => { setMounted(false); setClosing(false); }, 200);
return () => clearTimeout(t);
}
}, [open]);
useEffectM(() => {
if (!mounted) return;
document.body.style.overflow = "hidden";
return () => { document.body.style.overflow = ""; };
}, [mounted]);
if (!mounted) return null;
return (
{ HAPTIC.tap(); onClose(); }}>
{title &&
{title}
}
{children}
);
}
window.BottomSheet = BottomSheet;
/* ─── More Sheet ─── */
function MoreSheet({ open, onClose, onRoute }) {
const items = [
{ id: "journal", i: Icon.Book, l: "Journal de trading" },
{ id: "stats", i: Icon.Stats, l: "Statistiques" },
{ id: "volatile", i: Icon.Flame, l: "Cryptos volatiles" },
{ id: "settings", i: Icon.Settings, l: "Paramètres" },
{ id: "alerts", i: Icon.Bell, l: "Notifications" },
{ id: "help", i: Icon.AlertTriangle, l: "Aide & Support" },
];
return (
{items.map(it => (
{ HAPTIC.tap(); onRoute(it.id); onClose(); }}>
{it.l}
))}
);
}
window.MoreSheet = MoreSheet;
/* ─── MobileApp — main shell ─── */
function MobileApp({ tweaks, setTweak, onAction }) {
const [route, setRoute] = useStateM(() => {
const h = (window.location.hash || "").replace("#/", "");
return ROUTE_TITLES[h] ? h : "dashboard";
});
const [moreOpen, setMoreOpen] = useStateM(false);
useEffectM(() => { window.location.hash = "#/" + route; }, [route]);
function handleRoute(id) {
if (id === route) return;
HAPTIC.tap();
setRoute(id);
}
return (
setTweak({ mode: tweaks.mode === "LIVE" ? "PAPER" : "LIVE" })}
/>
{route === "dashboard" && window.MobileDashboard && }
{route === "trading" && window.MobileTrading && }
{route === "trading" && !window.MobileTrading && }
{route === "signaux" && window.MobileSignaux && }
{route === "signaux" && !window.MobileSignaux && }
{route === "charts" && window.MobileCharts && }
{route === "charts" && !window.MobileCharts && }
{(route === "journal" || route === "stats" || route === "settings" || route === "volatile") && (
window.MobileMore ? :
)}
{ HAPTIC.tap(); setMoreOpen(true); }}/>
setMoreOpen(false)} onRoute={handleRoute}/>
);
}
function MobileSoon({ name }) {
return (
{name} arrive
Cet écran sera refait dans l'étape suivante. Tu peux valider le shell (header, nav, sheets) en attendant.
);
}
Object.assign(window, { MobileApp, MobileHeader, MobileBottomNav, MoreSheet, MBrandMark, HAPTIC });