@zerohive/hive-viewer 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,36 @@
1
+ # ModalViewer Usage Example
2
+
3
+ You can use the ModalViewer component to display any content (such as DocumentViewer) in a modal dialog. Here is a simple example:
4
+
5
+ ```tsx
6
+ import React, { useState } from 'react';
7
+ import { ModalViewer } from './src/components/ModalViewer';
8
+ import { DocumentViewer } from './src/components/DocumentViewer';
9
+ import './src/components/ModalViewer.css';
10
+
11
+ export default function App() {
12
+ const [open, setOpen] = useState(false);
13
+ return (
14
+ <>
15
+ <button onClick={() => setOpen(true)}>Open Document Modal</button>
16
+ <ModalViewer open={open} onClose={() => setOpen(false)}>
17
+ <DocumentViewer url="/path/to/document.pdf" />
18
+ </ModalViewer>
19
+ </>
20
+ );
21
+ }
22
+ ```
23
+
24
+ **Props:**
25
+
26
+ - `open` (boolean): Whether the modal is visible.
27
+ - `onClose` (function): Called when the modal requests to close (overlay click, ESC, close button).
28
+ - `children` (ReactNode): Content to display inside the modal.
29
+ - `ariaLabel` (optional string): Accessibility label for the modal dialog.
30
+
31
+ **Styling:**
32
+
33
+ Import `ModalViewer.css` for default modal styles, or customize as needed.
1
34
  # @zerohive/hive-viewer
2
35
 
3
36
  A self-hostable, browser-first document viewer/editor for React and Next.js.
@@ -11,13 +44,15 @@ npm i @zerohive/hive-viewer
11
44
  Import styles once in your app:
12
45
 
13
46
  ```ts
14
- import '@zerohive/hive-viewer/styles.css';
47
+ import "@zerohive/hive-viewer/styles.css";
15
48
  ```
16
49
 
17
50
  ## Usage
18
51
 
52
+ ### Basic Usage
53
+
19
54
  ```tsx
20
- import { DocumentViewer } from '@zerohive/hive-viewer';
55
+ import { DocumentViewer } from "@zerohive/hive-viewer";
21
56
 
22
57
  export default function Page() {
23
58
  return (
@@ -28,17 +63,99 @@ export default function Page() {
28
63
  fileType="pdf"
29
64
  allowSigning
30
65
  onSignRequest={async () => ({
31
- signatureImageUrl: 'https://.../sig.png',
32
- signedBy: 'Jane Doe',
66
+ signatureImageUrl: "https://.../sig.png",
67
+ signedBy: "Jane Doe",
33
68
  dateSigned: new Date().toISOString(),
34
- comment: 'Approved'
69
+ comment: "Approved",
35
70
  })}
36
- onSave={(b64, meta) => { /* persist */ }}
71
+ onSave={(b64, meta) => {
72
+ /* persist */
73
+ }}
37
74
  />
38
75
  );
39
76
  }
40
77
  ```
41
78
 
79
+ ### Using in a Modal (Recommended)
80
+
81
+ Most consumers use the viewer in a modal dialog. Here is a recommended pattern:
82
+
83
+ ```tsx
84
+ import React, { useState } from "react";
85
+ import { DocumentViewer } from "@zerohive/hive-viewer";
86
+
87
+ function ModalDocViewer({ open, onClose, fileUrl, fileName, fileType }) {
88
+ if (!open) return null;
89
+ return (
90
+ <div
91
+ style={{
92
+ position: "fixed",
93
+ inset: 0,
94
+ background: "rgba(0,0,0,0.45)",
95
+ zIndex: 1000,
96
+ display: "flex",
97
+ alignItems: "center",
98
+ justifyContent: "center",
99
+ }}
100
+ >
101
+ <div
102
+ style={{
103
+ background: "#fff",
104
+ borderRadius: 16,
105
+ maxWidth: "90vw",
106
+ maxHeight: "90vh",
107
+ overflow: "auto",
108
+ position: "relative",
109
+ padding: 0,
110
+ boxShadow: "0 8px 32px rgba(0,0,0,0.25)",
111
+ }}
112
+ >
113
+ <button
114
+ onClick={onClose}
115
+ aria-label="Close"
116
+ style={{
117
+ position: "absolute",
118
+ top: 12,
119
+ right: 16,
120
+ background: "none",
121
+ border: "none",
122
+ fontSize: "2rem",
123
+ color: "#888",
124
+ cursor: "pointer",
125
+ zIndex: 1,
126
+ }}
127
+ >
128
+ ×
129
+ </button>
130
+ <DocumentViewer
131
+ mode="view"
132
+ fileUrl={fileUrl}
133
+ fileName={fileName}
134
+ fileType={fileType}
135
+ />
136
+ </div>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ // Usage example:
142
+ export default function Example() {
143
+ const [open, setOpen] = useState(false);
144
+ return (
145
+ <>
146
+ <button onClick={() => setOpen(true)}>Open Document</button>
147
+ <ModalDocViewer
148
+ open={open}
149
+ onClose={() => setOpen(false)}
150
+ fileUrl="https://example.com/my.pdf"
151
+ fileName="my.pdf"
152
+ fileType="pdf"
153
+ />
154
+ </>
155
+ );
156
+ }
157
+ ```
158
+
42
159
  ## Signing Workflow (decoupled)
43
160
 
44
161
  - If `allowSigning={true}`, the toolbar shows **Sign Document**.
package/dist/index.cjs CHANGED
@@ -39,7 +39,7 @@ var import_react6 = require("react");
39
39
 
40
40
  // src/utils/locale.ts
41
41
  var defaultLocale = {
42
- "loading": "Loading\u2026",
42
+ loading: "Loading\u2026",
43
43
  "error.title": "Error",
44
44
  "toolbar.layout.single": "Single page",
45
45
  "toolbar.layout.two": "Side-by-side",
@@ -62,7 +62,17 @@ var defaultLocale = {
62
62
  function guessFileType(name, explicit) {
63
63
  if (explicit) return explicit;
64
64
  const ext = (name?.split(".").pop() || "").toLowerCase();
65
- const allowed = ["pdf", "md", "docx", "xlsx", "pptx", "txt", "png", "jpg", "svg"];
65
+ const allowed = [
66
+ "pdf",
67
+ "md",
68
+ "docx",
69
+ "xlsx",
70
+ "pptx",
71
+ "txt",
72
+ "png",
73
+ "jpg",
74
+ "svg"
75
+ ];
66
76
  return allowed.includes(ext) ? ext : "txt";
67
77
  }
68
78
  function arrayBufferToBase64(buf) {
@@ -93,7 +103,8 @@ async function resolveSource(args) {
93
103
  const ab = await base64ToArrayBuffer(args.base64);
94
104
  return { fileType, fileName, arrayBuffer: ab };
95
105
  }
96
- if (!args.fileUrl) throw new Error("No file source provided. Use fileUrl, blob, or base64.");
106
+ if (!args.fileUrl)
107
+ throw new Error("No file source provided. Use fileUrl, blob, or base64.");
97
108
  const res = await fetch(args.fileUrl);
98
109
  if (!res.ok) throw new Error(`Failed to fetch file (${res.status})`);
99
110
  const total = Number(res.headers.get("content-length") || "") || void 0;
@@ -216,38 +227,60 @@ function PdfRenderer(props) {
216
227
  const { url, arrayBuffer } = props;
217
228
  const [doc, setDoc] = (0, import_react.useState)(null);
218
229
  const [pageCount, setPageCount] = (0, import_react.useState)(0);
219
- const [rendered, setRendered] = (0, import_react.useState)(/* @__PURE__ */ new Map());
230
+ const [rendered, setRendered] = (0, import_react.useState)(
231
+ /* @__PURE__ */ new Map()
232
+ );
220
233
  const [thumbs, setThumbs] = (0, import_react.useState)([]);
221
234
  const [size, setSize] = (0, import_react.useState)({ w: 840, h: 1188 });
235
+ const [error, setError] = (0, import_react.useState)(null);
236
+ const [loading, setLoading] = (0, import_react.useState)(false);
222
237
  const containerRef = (0, import_react.useRef)(null);
223
238
  (0, import_react.useEffect)(() => {
224
239
  try {
225
- import_pdfjs_dist.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import_meta.url).toString();
240
+ import_pdfjs_dist.GlobalWorkerOptions.workerSrc = new URL(
241
+ "pdfjs-dist/build/pdf.worker.min.mjs",
242
+ import_meta.url
243
+ ).toString();
226
244
  } catch {
227
245
  }
228
246
  }, []);
229
247
  (0, import_react.useEffect)(() => {
230
248
  let cancel = false;
249
+ setError(null);
250
+ setLoading(true);
231
251
  (async () => {
232
252
  setDoc(null);
233
253
  setRendered(/* @__PURE__ */ new Map());
234
254
  setThumbs([]);
235
- if (!url && !arrayBuffer) return;
236
- const task = (0, import_pdfjs_dist.getDocument)(url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer });
237
- const pdf = await task.promise;
238
- if (cancel) return;
239
- setDoc(pdf);
240
- setPageCount(pdf.numPages);
241
- props.onPageCount(pdf.numPages);
242
- setThumbs(Array.from({ length: pdf.numPages }));
243
- const p1 = await pdf.getPage(1);
244
- const base = p1.getViewport({ scale: 1 });
245
- const w = Math.min(980, Math.max(640, base.width));
246
- const s = w / base.width;
247
- const vp = p1.getViewport({ scale: s });
248
- setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
249
- })().catch(() => {
250
- });
255
+ if (!url && !arrayBuffer) {
256
+ setError("No PDF source provided.");
257
+ setLoading(false);
258
+ return;
259
+ }
260
+ try {
261
+ const task = (0, import_pdfjs_dist.getDocument)(
262
+ url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer }
263
+ );
264
+ const pdf = await task.promise;
265
+ if (cancel) return;
266
+ setDoc(pdf);
267
+ setPageCount(pdf.numPages);
268
+ props.onPageCount(pdf.numPages);
269
+ setThumbs(Array.from({ length: pdf.numPages }));
270
+ const p1 = await pdf.getPage(1);
271
+ const base = p1.getViewport({ scale: 1 });
272
+ const w = Math.min(980, Math.max(640, base.width));
273
+ const s = w / base.width;
274
+ const vp = p1.getViewport({ scale: s });
275
+ setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
276
+ } catch (e) {
277
+ setError(
278
+ "Failed to load PDF. " + (e instanceof Error ? e.message : "")
279
+ );
280
+ } finally {
281
+ setLoading(false);
282
+ }
283
+ })();
251
284
  return () => {
252
285
  cancel = true;
253
286
  };
@@ -269,37 +302,43 @@ function PdfRenderer(props) {
269
302
  (async () => {
270
303
  for (const p of pagesToShow) {
271
304
  if (rendered.has(p)) continue;
272
- const page = await doc.getPage(p);
273
- if (cancel) return;
274
- const base = page.getViewport({ scale: 1 });
275
- const vp = page.getViewport({ scale: size.w / base.width });
276
- const canvas = document.createElement("canvas");
277
- canvas.width = Math.round(vp.width);
278
- canvas.height = Math.round(vp.height);
279
- const ctx = canvas.getContext("2d", { alpha: false });
280
- if (!ctx) continue;
281
- await page.render({ canvasContext: ctx, viewport: vp }).promise;
282
- if (cancel) return;
283
- setRendered((prev) => {
284
- const next = new Map(prev);
285
- next.set(p, canvas);
286
- return next;
287
- });
288
305
  try {
289
- const tw = 140;
290
- const th = Math.round(tw * (canvas.height / canvas.width));
291
- const t = document.createElement("canvas");
292
- t.width = tw;
293
- t.height = th;
294
- const tctx = t.getContext("2d");
295
- if (tctx) {
296
- tctx.drawImage(canvas, 0, 0, tw, th);
297
- const url2 = t.toDataURL("image/jpeg", 0.75);
298
- setThumbs((prev) => {
299
- const next = prev.slice();
300
- next[p - 1] = url2;
301
- return next;
302
- });
306
+ const page = await doc.getPage(p);
307
+ if (cancel) return;
308
+ const base = page.getViewport({ scale: 1 });
309
+ const vp = page.getViewport({ scale: size.w / base.width });
310
+ const canvas = document.createElement("canvas");
311
+ canvas.width = Math.round(vp.width);
312
+ canvas.height = Math.round(vp.height);
313
+ const ctx = canvas.getContext("2d", { alpha: false });
314
+ if (!ctx) continue;
315
+ await page.render({ canvasContext: ctx, viewport: vp }).promise;
316
+ if (cancel) return;
317
+ setRendered((prev) => {
318
+ const next = new Map(prev);
319
+ next.set(p, canvas);
320
+ return next;
321
+ });
322
+ if (!thumbs[p - 1]) {
323
+ const thumbCanvas = document.createElement("canvas");
324
+ const thumbScale = 120 / vp.width;
325
+ thumbCanvas.width = Math.round(vp.width * thumbScale);
326
+ thumbCanvas.height = Math.round(vp.height * thumbScale);
327
+ const thumbCtx = thumbCanvas.getContext("2d", { alpha: false });
328
+ if (thumbCtx) {
329
+ thumbCtx.drawImage(
330
+ canvas,
331
+ 0,
332
+ 0,
333
+ thumbCanvas.width,
334
+ thumbCanvas.height
335
+ );
336
+ setThumbs((prev) => {
337
+ const arr = prev.slice();
338
+ arr[p - 1] = thumbCanvas.toDataURL("image/png");
339
+ return arr;
340
+ });
341
+ }
303
342
  }
304
343
  } catch {
305
344
  }
@@ -308,13 +347,16 @@ function PdfRenderer(props) {
308
347
  return () => {
309
348
  cancel = true;
310
349
  };
311
- }, [doc, pagesToShow, size.w, rendered]);
350
+ }, [doc, pagesToShow, size.w, rendered, thumbs]);
312
351
  function onWheel(e) {
313
352
  if (!pageCount) return;
314
353
  if (Math.abs(e.deltaY) < 10) return;
315
354
  const dir = e.deltaY > 0 ? 1 : -1;
316
355
  const step = props.layout === "side-by-side" ? 2 : 1;
317
- const next = Math.max(1, Math.min(pageCount, props.currentPage + dir * step));
356
+ const next = Math.max(
357
+ 1,
358
+ Math.min(pageCount, props.currentPage + dir * step)
359
+ );
318
360
  props.onCurrentPageChange(next);
319
361
  }
320
362
  function clickPlace(e, page) {
@@ -327,22 +369,37 @@ function PdfRenderer(props) {
327
369
  }
328
370
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
329
371
  !doc ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : null,
330
- doc ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages", children: pagesToShow.map((p) => {
331
- const c = rendered.get(p);
332
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hv-page", style: { width: size.w, height: size.h }, onClick: (e) => clickPlace(e, p), children: c ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
333
- "canvas",
334
- {
335
- className: "hv-canvas",
336
- width: c.width,
337
- height: c.height,
338
- ref: (node) => {
339
- if (!node) return;
340
- const ctx = node.getContext("2d");
341
- if (ctx) ctx.drawImage(c, 0, 0);
342
- }
343
- }
344
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hv-loading", children: "Rendering\u2026" }) }, p);
345
- }) }) : null
372
+ doc ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
373
+ "div",
374
+ {
375
+ className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
376
+ children: pagesToShow.map((p) => {
377
+ const c = rendered.get(p);
378
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
379
+ "div",
380
+ {
381
+ className: "hv-page",
382
+ style: { width: size.w, height: size.h },
383
+ onClick: (e) => clickPlace(e, p),
384
+ children: c ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
385
+ "canvas",
386
+ {
387
+ className: "hv-canvas",
388
+ width: c.width,
389
+ height: c.height,
390
+ ref: (node) => {
391
+ if (!node) return;
392
+ const ctx = node.getContext("2d");
393
+ if (ctx) ctx.drawImage(c, 0, 0);
394
+ }
395
+ }
396
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hv-loading", children: "Rendering\u2026" })
397
+ },
398
+ p
399
+ );
400
+ })
401
+ }
402
+ ) : null
346
403
  ] });
347
404
  }
348
405
 
@@ -457,17 +514,29 @@ var RichTextEditor = (0, import_react2.forwardRef)((props, ref) => {
457
514
  }
458
515
  if (props.fileType === "docx") {
459
516
  try {
460
- const htmlToDocx = (await import("html-to-docx")).default;
461
- const blob = await htmlToDocx(stitched);
462
- const ab = await blob.arrayBuffer();
463
- props.onSave(arrayBufferToBase64(ab), {
464
- fileName: replaceExt(props.fileName, "docx"),
465
- fileType: "docx",
466
- annotations: { signaturePlacements: props.signaturePlacements }
517
+ const response = await fetch("/api/export-docx", {
518
+ method: "POST",
519
+ headers: { "Content-Type": "application/json" },
520
+ body: JSON.stringify({
521
+ html: stitched,
522
+ fileName: replaceExt(props.fileName, "docx")
523
+ })
467
524
  });
525
+ if (!response.ok) throw new Error("Failed to generate DOCX");
526
+ const blob = await response.blob();
527
+ const url = window.URL.createObjectURL(blob);
528
+ const a = document.createElement("a");
529
+ a.href = url;
530
+ a.download = replaceExt(props.fileName, "docx");
531
+ document.body.appendChild(a);
532
+ a.click();
533
+ setTimeout(() => {
534
+ window.URL.revokeObjectURL(url);
535
+ a.remove();
536
+ }, 100);
468
537
  } catch (err) {
469
538
  alert(
470
- "DOCX export is not supported in this environment. Please use this feature in a Node.js/server context."
539
+ "DOCX export failed: " + (err instanceof Error ? err.message : String(err))
471
540
  );
472
541
  }
473
542
  return;
@@ -631,7 +700,11 @@ function ensureExt(name, ext) {
631
700
  // src/renderers/ImageRenderer.tsx
632
701
  var import_react4 = require("react");
633
702
  var import_jsx_runtime7 = require("react/jsx-runtime");
634
- function ImageRenderer({ arrayBuffer, fileType, fileName }) {
703
+ function ImageRenderer({
704
+ arrayBuffer,
705
+ fileType,
706
+ fileName
707
+ }) {
635
708
  const [zoom, setZoom] = (0, import_react4.useState)(1);
636
709
  const url = (0, import_react4.useMemo)(() => {
637
710
  if (!arrayBuffer) return void 0;
@@ -647,14 +720,42 @@ function ImageRenderer({ arrayBuffer, fileType, fileName }) {
647
720
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-mini-toolbar", children: [
648
721
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-title", children: fileName }),
649
722
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-spacer" }),
650
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "hv-btn", onClick: () => setZoom((z) => Math.max(0.25, z - 0.25)), children: "-" }),
723
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
724
+ "button",
725
+ {
726
+ type: "button",
727
+ className: "hv-btn",
728
+ onClick: () => setZoom((z) => Math.max(0.25, z - 0.25)),
729
+ children: "-"
730
+ }
731
+ ),
651
732
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-zoom", children: [
652
733
  Math.round(zoom * 100),
653
734
  "%"
654
735
  ] }),
655
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "hv-btn", onClick: () => setZoom((z) => Math.min(4, z + 0.25)), children: "+" })
736
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
737
+ "button",
738
+ {
739
+ type: "button",
740
+ className: "hv-btn",
741
+ onClick: () => setZoom((z) => Math.min(4, z + 0.25)),
742
+ children: "+"
743
+ }
744
+ )
656
745
  ] }),
657
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-center", children: url ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("img", { src: url, alt: fileName, style: { transform: `scale(${zoom})` }, className: "hv-image" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-loading", children: "Loading\u2026" }) })
746
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-center", children: [
747
+ !arrayBuffer && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-error", children: "No image data provided." }),
748
+ arrayBuffer && !url && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-error", children: "Failed to load image." }),
749
+ url && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
750
+ "img",
751
+ {
752
+ src: url,
753
+ alt: fileName,
754
+ style: { transform: `scale(${zoom})` },
755
+ className: "hv-image"
756
+ }
757
+ )
758
+ ] })
658
759
  ] });
659
760
  }
660
761
 
@@ -671,37 +772,57 @@ function extractText(xml) {
671
772
  function PptxRenderer(props) {
672
773
  const [slides, setSlides] = (0, import_react5.useState)([]);
673
774
  const [thumbs, setThumbs] = (0, import_react5.useState)([]);
775
+ const [error, setError] = (0, import_react5.useState)(null);
776
+ const [loading, setLoading] = (0, import_react5.useState)(false);
674
777
  (0, import_react5.useEffect)(() => {
675
778
  let cancelled = false;
779
+ setError(null);
780
+ setLoading(true);
676
781
  (async () => {
677
782
  setSlides([]);
678
783
  setThumbs([]);
679
784
  if (!props.arrayBuffer) {
680
785
  props.onSlideCount(1);
681
786
  setSlides([{ index: 1, text: "No content" }]);
787
+ setError("No PPTX data provided.");
788
+ setLoading(false);
682
789
  return;
683
790
  }
684
- const zip = await import_jszip.default.loadAsync(props.arrayBuffer);
685
- const files = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort((a, b) => {
686
- const na = Number(a.match(/slide(\d+)\.xml/)?.[1] || 0);
687
- const nb = Number(b.match(/slide(\d+)\.xml/)?.[1] || 0);
688
- return na - nb;
689
- });
690
- const out = [];
691
- for (let i = 0; i < files.length; i++) {
692
- const xml = await zip.file(files[i]).async("string");
693
- out.push({ index: i + 1, text: extractText(xml) });
791
+ try {
792
+ const zip = await import_jszip.default.loadAsync(props.arrayBuffer);
793
+ const files = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort((a, b) => {
794
+ const na = Number(a.match(/slide(\d+)\.xml/)?.[1] || 0);
795
+ const nb = Number(b.match(/slide(\d+)\.xml/)?.[1] || 0);
796
+ return na - nb;
797
+ });
798
+ const out = [];
799
+ for (let i = 0; i < files.length; i++) {
800
+ const xml = await zip.file(files[i]).async("string");
801
+ out.push({ index: i + 1, text: extractText(xml) });
802
+ }
803
+ if (cancelled) return;
804
+ const count = Math.max(1, out.length);
805
+ props.onSlideCount(count);
806
+ setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
807
+ setThumbs(
808
+ Array.from(
809
+ { length: count },
810
+ (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`
811
+ )
812
+ );
813
+ } catch (e) {
814
+ props.onSlideCount(1);
815
+ setSlides([
816
+ { index: 1, text: "Unable to render this .pptx in-browser." }
817
+ ]);
818
+ setThumbs([void 0]);
819
+ setError(
820
+ "Failed to load PPTX. " + (e instanceof Error ? e.message : "")
821
+ );
822
+ } finally {
823
+ setLoading(false);
694
824
  }
695
- if (cancelled) return;
696
- const count = Math.max(1, out.length);
697
- props.onSlideCount(count);
698
- setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
699
- setThumbs(Array.from({ length: count }, (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`));
700
- })().catch(() => {
701
- props.onSlideCount(1);
702
- setSlides([{ index: 1, text: "Unable to render this .pptx in-browser." }]);
703
- setThumbs([void 0]);
704
- });
825
+ })();
705
826
  return () => {
706
827
  cancelled = true;
707
828
  };
@@ -710,19 +831,43 @@ function PptxRenderer(props) {
710
831
  props.onThumbs(thumbs);
711
832
  }, [thumbs]);
712
833
  const pagesToShow = (0, import_react5.useMemo)(() => {
713
- if (props.layout === "side-by-side") return [props.currentPage, Math.min(slides.length || props.currentPage + 1, props.currentPage + 1)];
834
+ if (props.layout === "side-by-side")
835
+ return [
836
+ props.currentPage,
837
+ Math.min(slides.length || props.currentPage + 1, props.currentPage + 1)
838
+ ];
714
839
  return [props.currentPage];
715
840
  }, [props.currentPage, props.layout, slides.length]);
716
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-doc", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages", children: pagesToShow.map((p) => {
717
- const s = slides[p - 1];
718
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-slide", tabIndex: 0, onFocus: () => props.onCurrentPageChange(p), children: [
719
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-slide-title", children: [
720
- "Slide ",
721
- p
722
- ] }),
723
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-slide-text", children: s?.text || "" })
724
- ] }, p);
725
- }) }) });
841
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-doc", children: [
842
+ loading && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-loading", children: "Loading PPTX\u2026" }),
843
+ error && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-error", children: error }),
844
+ !loading && !error && (!slides || slides.length === 0) && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-error", children: "No slides to display." }),
845
+ !error && slides && slides.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
846
+ "div",
847
+ {
848
+ className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
849
+ children: pagesToShow.map((p) => {
850
+ const s = slides[p - 1];
851
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
852
+ "div",
853
+ {
854
+ className: "hv-slide",
855
+ tabIndex: 0,
856
+ onFocus: () => props.onCurrentPageChange(p),
857
+ children: [
858
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-slide-title", children: [
859
+ "Slide ",
860
+ p
861
+ ] }),
862
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-slide-text", children: s?.text || "" })
863
+ ]
864
+ },
865
+ p
866
+ );
867
+ })
868
+ }
869
+ )
870
+ ] });
726
871
  }
727
872
  function svgThumb(n) {
728
873
  return `<svg xmlns="http://www.w3.org/2000/svg" width="180" height="100"><rect width="100%" height="100%" rx="12" fill="#111827"/><text x="50%" y="54%" font-size="18" fill="#e5e7eb" text-anchor="middle">${n}</text></svg>`;