// IsoStack — interactive exploded ISO with click-to-reveal speech bubbles const ISO_MODULES = { control: { num:"00", name:"Control", role:"EMS · brain", short:"Edge AI", desc:"Edge-AI controller die alle modules op locatie aanstuurt. Voorspelt belasting, schakelt bronnen en logt elke kWh.", specs:[["Hardware","Edge-AI"],["Connect","4G/5G/LAN"],["Update","OTA"],["Logging","24/7"]], anchor:{x:320, y:54, side:"below"} }, core: { num:"01", name:"Core", role:"Storage", short:"40–2.000 kWh", desc:"Het batterijhart van de stack. Buffert pieken, levert basislast en reduceert generator-uren met tot 70%.", specs:[["Capaciteit","40–2.000 kWh"],["Vermogen","tot 500 kVA"],["Behuizing","IP54"],["Cycli","6.000+"]], anchor:{x:320, y:290, side:"above"} }, ray: { num:"02", name:"Ray", role:"Solar", short:"Tot 30 kWp", desc:"Vouwbare zonnepanelen die in minuten uitvouwen. Volledig draagbaar — geen vaste installatie nodig.", specs:[["Vermogen","tot 30 kWp"],["Vorm","Foldable"],["Opzet","< 10 min"],["Connector","MC4"]], anchor:{x:320, y:180, side:"above"} }, fusion: { num:"03", name:"Fusion", role:"Hybrid backup", short:"60–200 kVA", desc:"Hybride generator die alleen bijspringt als Core en Ray het niet redden. HVO-ready, tot 92% minder CO₂.", specs:[["Vermogen","60–200 kVA"],["Brandstof","HVO/diesel"],["Emissie","Stage V"],["Modus","Smart standby"]], anchor:{x:175, y:425, side:"above"} }, grid: { num:"04", name:"Grid", role:"EV charging", short:"Tot 360 kW", desc:"DC-snelladers gevoed vanuit Core. Laadt voertuigen op locaties zonder netaansluiting.", specs:[["Vermogen","tot 360 kW"],["Connectoren","CCS/CHAdeMO"],["Voeding","via Core"],["Net","niet vereist"]], anchor:{x:445, y:425, side:"above"} } }; const ISO_ORDER = ["control","core","ray","fusion","grid"]; window.IsoStack = function IsoStack(){ const [active, setActive] = React.useState(null); const stageRef = React.useRef(null); const svgRef = React.useRef(null); const bubbleRef = React.useRef(null); const [bubbleStyle, setBubbleStyle] = React.useState({left:0, top:0, side:"above", visible:false}); const toggle = (k) => setActive(prev => prev === k ? null : k); const placeBubble = React.useCallback(() => { if (!active) return; const m = ISO_MODULES[active]; const stage = stageRef.current; const svg = svgRef.current; const bubble = bubbleRef.current; if (!stage || !svg || !bubble) return; const stageRect = stage.getBoundingClientRect(); const svgRect = svg.getBoundingClientRect(); const scale = svgRect.width / 600; const ax = (svgRect.left - stageRect.left) + m.anchor.x * scale; const ay = (svgRect.top - stageRect.top ) + m.anchor.y * scale; const bw = bubble.offsetWidth || 280; const bh = bubble.offsetHeight || 180; const tail = 14; let left = ax; const half = bw / 2; left = Math.max(half + 8, Math.min(stageRect.width - half - 8, left)); const top = m.anchor.side === "below" ? ay + tail : ay - bh - tail; setBubbleStyle({ left, top, side: m.anchor.side, visible: true }); }, [active]); React.useEffect(() => { if (!active){ setBubbleStyle(s => ({...s, visible:false})); return; } // measure on next frame so bubble has dimensions const r = requestAnimationFrame(placeBubble); return () => cancelAnimationFrame(r); }, [active, placeBubble]); React.useEffect(() => { const onResize = () => placeBubble(); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, [placeBubble]); React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape" && active){ setActive(null); return; } if (!active) return; const idx = ISO_ORDER.indexOf(active); if (e.key === "ArrowRight" || e.key === "ArrowDown") setActive(ISO_ORDER[(idx+1)%ISO_ORDER.length]); else if (e.key === "ArrowLeft" || e.key === "ArrowUp") setActive(ISO_ORDER[(idx-1+ISO_ORDER.length)%ISO_ORDER.length]); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [active]); const isoClass = (k) => "iso-group" + (active && active !== k ? " dim" : "") + (active === k ? " active" : ""); const m = active ? ISO_MODULES[active] : null; return (