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);
}}
/>
)}
)}
);
}