import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { importFigureHdf5, FigureView, type FigureState } from "numbl/graphics"; import { buildTree } from "./tree"; import { ObjectTree } from "./ObjectTree"; import { InfoPanel } from "./InfoPanel"; import { useMediaQuery } from "./useMediaQuery"; const clamp = (x: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, x)); /** A draggable vertical divider; reports incremental drag deltas in px. */ function Divider({ onResize }: { onResize: (dx: number) => void }) { const onPointerDown = (e: React.PointerEvent) => { e.preventDefault(); let lastX = e.clientX; const move = (ev: PointerEvent) => { onResize(ev.clientX - lastX); lastX = ev.clientX; }; const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; window.addEventListener("pointermove", move); window.addEventListener("pointerup", up); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }; return
; } export function App() { const [figure, setFigure] = useState(null); const [fileName, setFileName] = useState("figure"); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [selectedId, setSelectedId] = useState(null); const [dragOver, setDragOver] = useState(false); const isMobile = useMediaQuery("(max-width: 820px)"); const [leftOpen, setLeftOpen] = useState(true); const [rightOpen, setRightOpen] = useState(true); const [leftW, setLeftW] = useState(260); const [rightW, setRightW] = useState(320); const inputRef = useRef(null); // Panels are inline columns on desktop, slide-in drawers on mobile. useEffect(() => { setLeftOpen(!isMobile); setRightOpen(!isMobile); }, [isMobile]); const tree = useMemo( () => (figure ? buildTree(figure, fileName) : null), [figure, fileName] ); const selectedNode = tree && selectedId ? (tree.byId.get(selectedId) ?? null) : null; const loadFile = useCallback(async (file: File) => { setLoading(true); setError(null); try { const bytes = new Uint8Array(await file.arrayBuffer()); const fig = await importFigureHdf5(bytes); setFigure(fig); setFileName(file.name); setSelectedId("figure"); } catch (e) { setError(e instanceof Error ? e.message : String(e)); setFigure(null); } finally { setLoading(false); } }, []); const pickFile = () => inputRef.current?.click(); const onSelect = (id: string) => { setSelectedId(id); if (isMobile) setLeftOpen(false); }; const fileInput = ( { const f = e.target.files?.[0]; if (f) loadFile(f); e.target.value = ""; }} /> ); const dropHandlers = { onDragOver: (e: React.DragEvent) => { e.preventDefault(); setDragOver(true); }, onDragLeave: () => setDragOver(false), onDrop: (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files[0]; if (f) loadFile(f); }, }; return (
{fileInput}
{figure && ( )} numbl figure viewer {figure && {fileName}} {figure && ( )}
{error &&
Failed to open file: {error}
} {!figure ? (

Open a numbl figure

Drag a .h5 figure file here, or click to choose one.

Exported from numbl via “Download data (.h5)”.

) : (
{/* Left: object tree */} {leftOpen && ( )} {leftOpen && !isMobile && ( setLeftW(w => clamp(w + dx, 180, 520))} /> )} {/* Center: figure */}
{dragOver &&
Drop to open
}
{/* Right: details */} {rightOpen && !isMobile && ( setRightW(w => clamp(w - dx, 220, 620))} /> )} {rightOpen && ( )} {/* Mobile drawer backdrop */} {isMobile && (leftOpen || rightOpen) && (
{ setLeftOpen(false); setRightOpen(false); }} /> )}
)}
); }