1import { useCallback, useRef, useState } from "react";
2import { importFigureHdf5, FigureView, type FigureState } from "numbl/graphics";
4export function App() {
5 const [figure, setFigure] = useState<FigureState | null>(null);
6 const [fileName, setFileName] = useState<string | null>(null);
7 const [error, setError] = useState<string | null>(null);
8 const [loading, setLoading] = useState(false);
9 const [dragOver, setDragOver] = useState(false);
10 const inputRef = useRef<HTMLInputElement>(null);
12 const loadBytes = useCallback(async (bytes: Uint8Array, name: string) => {
13 setLoading(true);
14 setError(null);
15 try {
16 const fig = await importFigureHdf5(bytes);
17 setFigure(fig);
18 setFileName(name);
19 } catch (e) {
20 setError(e instanceof Error ? e.message : String(e));
21 setFigure(null);
22 setFileName(null);
23 } finally {
24 setLoading(false);
25 }
26 }, []);
28 const loadFile = useCallback(
29 async (file: File) => loadBytes(new Uint8Array(await file.arrayBuffer()), file.name),
30 [loadBytes]
31 );
33 const loadSample = useCallback(async () => {
34 setLoading(true);
35 setError(null);
36 try {
37 const resp = await fetch(`${import.meta.env.BASE_URL}sample.h5`);
38 await loadBytes(new Uint8Array(await resp.arrayBuffer()), "sample.h5");
39 } catch (e) {
40 setError(e instanceof Error ? e.message : String(e));
41 setLoading(false);
42 }
43 }, [loadBytes]);
45 return (
46 <div className="app">
47 <header>
48 <h1>numbl figure viewer</h1>
49 <p className="sub">
50 Open a <code>.h5</code> figure file exported from{" "}
51 <a href="https://numbl.org">numbl</a> and view it here. Rendering uses
52 numbl’s own figure components.
53 </p>
54 </header>
56 <div
57 className={"dropzone" + (dragOver ? " over" : "")}
58 onClick={() => inputRef.current?.click()}
59 onDragOver={e => {
60 e.preventDefault();
61 setDragOver(true);
62 }}
63 onDragLeave={() => setDragOver(false)}
64 onDrop={e => {
65 e.preventDefault();
66 setDragOver(false);
67 const f = e.dataTransfer.files[0];
68 if (f) loadFile(f);
69 }}
70 >
71 <input
72 ref={inputRef}
73 type="file"
74 accept=".h5,.hdf5"
75 style={{ display: "none" }}
76 onChange={e => {
77 const f = e.target.files?.[0];
78 if (f) loadFile(f);
79 }}
80 />
81 {loading
82 ? "Loading…"
83 : fileName
84 ? `Loaded: ${fileName} — click or drop to open another`
85 : "Click to choose, or drag a .h5 figure file here"}
86 </div>
88 <div className="actions">
89 <button onClick={loadSample} disabled={loading}>
90 Try a sample figure
91 </button>
92 </div>
94 {error && <div className="error">Failed to open file: {error}</div>}
96 {figure && (
97 <div className="figure-wrap">
98 <div className="figure-inner">
99 <FigureView figure={figure} />
100 </div>
101 </div>
102 )}
103 </div>
104 );
105}