// 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 (
DOC.04 / SHEET 01 — VERT. EXPL. EXPLODED ASSEMBLY · INTERACTIVE REV 02 · 2026
{/* 04 GRID */} {e.stopPropagation();toggle("grid")}}> 04 · GRID {/* 03 FUSION */} {e.stopPropagation();toggle("fusion")}}> 03 · FUSION {/* 01 CORE */} {e.stopPropagation();toggle("core")}}> CORE 01 · STORAGE {/* 02 RAY */} {e.stopPropagation();toggle("ray")}}> 02 · RAY {/* 00 CONTROL */} {e.stopPropagation();toggle("control")}}> CONTROL EMS · L02 {/* callouts */} [02] [01] [03] [04] [00]
{m && (<>
{m.num} {m.name}
{m.role}
{m.desc}
{m.specs.map(([k,v])=>(
{k}{v}
))}
)}
{ISO_ORDER.map(k => { const mm = ISO_MODULES[k]; const cls = "iso-li" + (active === k ? " active" : ""); return ( ); })}
SCALE 1:NTS VERTICAL EXPLODED · 5 MODULES DRWN. WATTIQ ENG.
); };