2026-05-27 13:18:45 +02:00
"use client" ;
2026-05-28 23:04:07 +02:00
import { useState , useRef , useCallback } from "react" ;
import { createPortal } from "react-dom" ;
2026-06-02 23:01:51 +02:00
import { AlertTriangle } from "lucide-react" ;
2026-05-27 13:18:45 +02:00
import type { DriverData } from "@/lib/types" ;
interface Props { drivers : DriverData }
function fmt ( v : number | null , dec : number , unit = "" ) {
if ( v === null ) return "—" ;
return ` ${ v . toLocaleString ( "fr-FR" , { minimumFractionDigits : dec , maximumFractionDigits : dec } )} ${ unit } ` ;
}
2026-05-28 23:04:07 +02:00
interface TooltipState { x : number ; y : number }
2026-06-08 14:33:23 +02:00
function Tile ({ label , value , dec = 2 , unit = "" , delta , deltaPct , deltaDec , tooltip , accent } : {
2026-06-02 23:01:51 +02:00
label : string ;
value : number | null ;
dec? : number ;
unit? : string ;
delta? : number | null ;
deltaPct? : boolean ;
deltaDec? : number ;
tooltip? : string ;
2026-06-08 14:33:23 +02:00
accent ?: "red" | "green" | "amber" ;
2026-06-02 23:01:51 +02:00
}) {
2026-05-28 23:04:07 +02:00
const [ tip , setTip ] = useState < TooltipState | null >( null );
2026-06-02 23:01:51 +02:00
const ref = useRef < HTMLDivElement >( null );
2026-05-28 23:04:07 +02:00
const show = useCallback (() => {
if ( ! ref . current ) return ;
const r = ref . current . getBoundingClientRect ();
setTip ({ x : r.left + r . width / 2 , y : r.top });
}, []);
const hide = useCallback (() => setTip ( null ), []);
2026-06-02 23:01:51 +02:00
const pos = delta != null && delta > 0 ;
const neg = delta != null && delta < 0 ;
const dColor = pos ? "text-emerald-400" : neg ? "text-red-400" : "text-slate-500" ;
const dArrow = pos ? "▲" : neg ? "▼" : "" ;
const dFmt = delta != null
? ` ${ Math . abs ( delta ). toFixed ( deltaDec ?? dec ) }${ deltaPct ? "%" : "" } `
: null ;
2026-06-08 14:33:23 +02:00
const borderCls = accent === "red" ? "border-red-500/30 bg-red-500/5"
: accent === "green" ? "border-emerald-500/30 bg-emerald-500/5"
: accent === "amber" ? "border-amber-500/30 bg-amber-500/5"
: "border-slate-800/60 bg-slate-900/40" ;
2026-05-28 23:04:07 +02:00
return (
<>
2026-06-02 23:01:51 +02:00
< div
2026-05-28 23:04:07 +02:00
ref = { ref }
2026-06-02 23:01:51 +02:00
onMouseEnter = { tooltip ? show : undefined }
onMouseLeave = { tooltip ? hide : undefined }
2026-06-08 14:33:23 +02:00
className = { `flex flex-col gap-0.5 px-3 py-2 rounded-lg border ${ borderCls } ${ tooltip ? "cursor-help" : "" } min-w-[72px]` }
2026-05-28 23:04:07 +02:00
>
2026-06-08 14:33:23 +02:00
< span className = "text-slate-500 text-[10px] font-medium leading-none" >{ label }</ span >
< div className = "flex items-baseline gap-1" >
< span className = "text-slate-100 font-bold tabular-nums text-[13px] leading-none" >
{ fmt ( value , dec , unit )}
2026-06-02 23:01:51 +02:00
</ span >
2026-06-08 14:33:23 +02:00
{ dFmt && (
< span className = { `text-[10px] font-medium tabular-nums leading-none ${ dColor } ` }>
{ dArrow }{ dFmt }
</ span >
)}
</ div >
2026-06-02 23:01:51 +02:00
</ div >
2026-05-28 23:04:07 +02:00
{ tip && typeof document !== "undefined" && createPortal (
< div
2026-06-02 23:01:51 +02:00
className = "fixed z-[9999] bg-slate-800 border border-slate-700 text-slate-300 text-[10px] rounded-lg px-3 py-2 w-60 leading-snug shadow-xl pointer-events-none whitespace-pre-line"
style = {{ left : tip.x , top : tip.y - 8 , transform : "translate(-50%, -100%)" }}
2026-05-28 23:04:07 +02:00
>
{ tooltip }
2026-06-02 23:01:51 +02:00
< span className = "absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-slate-800" />
2026-05-28 23:04:07 +02:00
</ div >,
document . body ,
)}
</>
);
}
2026-06-08 14:33:23 +02:00
function GroupLabel ({ label } : { label : string }) {
return (
< span className = "text-slate-600 text-[9px] font-semibold uppercase tracking-widest self-center shrink-0 hidden sm:block" >
{ label }
</ span >
);
2026-05-27 13:18:45 +02:00
}
export default function DriversBar ({ drivers } : Props ) {
const {
vix , vixDelta ,
sp500 , sp500ChangePct ,
2026-06-27 14:12:05 +02:00
btc , btcDeltaPct , btcChange24h ,
2026-05-27 13:18:45 +02:00
hySpread , igSpread ,
2026-06-01 23:16:18 +02:00
us10y , us2y , curveSlope ,
2026-06-27 14:12:05 +02:00
gold , goldDeltaPct ,
silver , silverDeltaPct ,
brent , brentDelta , brentDeltaPct ,
wti , wtiDelta , wtiDeltaPct ,
2026-06-02 23:01:51 +02:00
} = drivers ;
2026-05-27 13:18:45 +02:00
2026-06-27 14:12:05 +02:00
const riskOff = ( vix ?? 0 ) > 25 || ( hySpread ?? 0 ) > 500 ;
2026-05-27 13:18:45 +02:00
return (
2026-06-08 14:33:23 +02:00
< div className = "mb-4 bg-slate-950/60 border border-slate-800 rounded-xl p-3 space-y-2" >
2026-05-27 13:18:45 +02:00
2026-06-08 14:33:23 +02:00
{ /* Ligne titre + alerte */ }
< div className = "flex items-center justify-between" >
< span className = "text-slate-500 font-semibold uppercase tracking-widest text-[10px]" >
Drivers Globaux
</ span >
{ riskOff && (
< div className = "flex items-center gap-1 bg-red-500/10 border border-red-500/20 rounded-full px-2 py-0.5" >
< AlertTriangle size = { 10 } className = "text-red-400" />
< span className = "text-[9px] font-semibold text-red-400" > Risk - Off </ span >
</ div >
)}
</ div >
2026-06-02 23:01:51 +02:00
2026-06-08 14:33:23 +02:00
{ /* Grille responsive — 2 lignes sur desktop, s'adapte sur mobile */ }
< div className = "flex flex-wrap gap-2" >
{ /* ── Sentiment ─────────────────────────────────────────── */ }
< GroupLabel label = "Sentiment" />
< Tile label = "VIX" value = { vix } dec = { 1 } delta = { vixDelta } deltaDec = { 1 }
accent = {( vix ?? 0 ) > 25 ? "red" : undefined }
tooltip = "Clôture actuelle − clôture précédente (Yahoo Finance)." />
< Tile label = "S&P 500" value = { sp500 } dec = { 0 } delta = { sp500ChangePct } deltaPct deltaDec = { 2 }
2026-06-27 14:12:05 +02:00
tooltip = "% vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo Finance." />
< Tile label = "BTC/USD" value = { btc } dec = { 0 } unit = " $"
delta = { btcDeltaPct ?? btcChange24h } deltaPct deltaDec = { 2 }
tooltip = "% vs clôture J-1 (investing.com, cache 1 min). Fallback : Binance/CoinGecko 24h." />
2026-06-08 14:33:23 +02:00
{ /* ── Crédit ────────────────────────────────────────────── */ }
< div className = "w-px bg-slate-800 self-stretch mx-0.5 hidden sm:block" />
< GroupLabel label = "Crédit" />
< Tile label = "HY Spread" value = { hySpread } dec = { 0 } unit = " bps"
accent = {( hySpread ?? 0 ) > 500 ? "red" : ( hySpread ?? 0 ) > 400 ? "amber" : undefined }
tooltip = "High Yield spread vs Treasuries US. >500 bps = risk-off fort." />
< Tile label = "IG Spread" value = { igSpread } dec = { 0 } unit = " bps"
tooltip = "Investment Grade spread vs Treasuries US." />
2026-06-27 14:12:05 +02:00
{ /* ── Taux ──────────────────────────────────────────────── */ }
2026-06-08 14:33:23 +02:00
< div className = "w-px bg-slate-800 self-stretch mx-0.5 hidden sm:block" />
2026-06-27 14:12:05 +02:00
< GroupLabel label = "Taux" />
2026-06-08 14:33:23 +02:00
< Tile
label = "Crb 2-10" value = { curveSlope } dec = { 0 } unit = " bps"
accent = {( curveSlope ?? 0 ) < - 50 ? "amber" : undefined }
tooltip = { `Spread US 10Y − US 2Y. Négatif = courbe inversée. \ nUS 10Y: ${ us10y != null ? us10y . toFixed ( 2 ) + "%" : "N/A" } | US 2Y: ${ us2y != null ? us2y . toFixed ( 2 ) + "%" : "N/A" } ` }
/>
{ /* ── Commodités ────────────────────────────────────────── */ }
< div className = "w-px bg-slate-800 self-stretch mx-0.5 hidden sm:block" />
< GroupLabel label = "Commodités" />
2026-06-27 14:12:05 +02:00
< Tile label = "Or $/oz" value = { gold } dec = { 0 }
delta = { goldDeltaPct } deltaPct deltaDec = { 2 }
tooltip = "% vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo GC=F." />
< Tile label = "Argent $/oz" value = { silver } dec = { 2 }
delta = { silverDeltaPct } deltaPct deltaDec = { 2 }
tooltip = "% vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo SI=F." />
< Tile label = "Brent $/b" value = { brent } dec = { 1 }
delta = { brentDeltaPct ?? brentDelta } deltaPct = { brentDeltaPct !== null } deltaDec = { 2 }
tooltip = { `% évolution vs clôture J-1 (abcbourse.com — Six Financial Information, temps réel). ${ brentDelta != null ? ` \ nDelta: ${ brentDelta > 0 ? "+" : "" }${ brentDelta . toFixed ( 2 ) } $` : "" } ` } />
< Tile label = "WTI $/b" value = { wti } dec = { 1 }
delta = { wtiDeltaPct ?? wtiDelta } deltaPct = { wtiDeltaPct !== null } deltaDec = { 2 }
tooltip = { `% évolution vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo Finance CL=F. ${ wtiDelta != null ? ` \ nDelta: ${ wtiDelta > 0 ? "+" : "" }${ wtiDelta . toFixed ( 2 ) } $` : "" } ` } />
2026-05-27 13:18:45 +02:00
2026-06-08 14:33:23 +02:00
</ div >
2026-05-27 13:18:45 +02:00
</ div >
);
}