@zerohive/hive-viewer 0.2.1 → 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
 
@@ -643,7 +700,11 @@ function ensureExt(name, ext) {
643
700
  // src/renderers/ImageRenderer.tsx
644
701
  var import_react4 = require("react");
645
702
  var import_jsx_runtime7 = require("react/jsx-runtime");
646
- function ImageRenderer({ arrayBuffer, fileType, fileName }) {
703
+ function ImageRenderer({
704
+ arrayBuffer,
705
+ fileType,
706
+ fileName
707
+ }) {
647
708
  const [zoom, setZoom] = (0, import_react4.useState)(1);
648
709
  const url = (0, import_react4.useMemo)(() => {
649
710
  if (!arrayBuffer) return void 0;
@@ -659,14 +720,42 @@ function ImageRenderer({ arrayBuffer, fileType, fileName }) {
659
720
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-mini-toolbar", children: [
660
721
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-title", children: fileName }),
661
722
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hv-spacer" }),
662
- /* @__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
+ ),
663
732
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hv-zoom", children: [
664
733
  Math.round(zoom * 100),
665
734
  "%"
666
735
  ] }),
667
- /* @__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
+ )
668
745
  ] }),
669
- /* @__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
+ ] })
670
759
  ] });
671
760
  }
672
761
 
@@ -683,37 +772,57 @@ function extractText(xml) {
683
772
  function PptxRenderer(props) {
684
773
  const [slides, setSlides] = (0, import_react5.useState)([]);
685
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);
686
777
  (0, import_react5.useEffect)(() => {
687
778
  let cancelled = false;
779
+ setError(null);
780
+ setLoading(true);
688
781
  (async () => {
689
782
  setSlides([]);
690
783
  setThumbs([]);
691
784
  if (!props.arrayBuffer) {
692
785
  props.onSlideCount(1);
693
786
  setSlides([{ index: 1, text: "No content" }]);
787
+ setError("No PPTX data provided.");
788
+ setLoading(false);
694
789
  return;
695
790
  }
696
- const zip = await import_jszip.default.loadAsync(props.arrayBuffer);
697
- const files = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort((a, b) => {
698
- const na = Number(a.match(/slide(\d+)\.xml/)?.[1] || 0);
699
- const nb = Number(b.match(/slide(\d+)\.xml/)?.[1] || 0);
700
- return na - nb;
701
- });
702
- const out = [];
703
- for (let i = 0; i < files.length; i++) {
704
- const xml = await zip.file(files[i]).async("string");
705
- 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);
706
824
  }
707
- if (cancelled) return;
708
- const count = Math.max(1, out.length);
709
- props.onSlideCount(count);
710
- setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
711
- setThumbs(Array.from({ length: count }, (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`));
712
- })().catch(() => {
713
- props.onSlideCount(1);
714
- setSlides([{ index: 1, text: "Unable to render this .pptx in-browser." }]);
715
- setThumbs([void 0]);
716
- });
825
+ })();
717
826
  return () => {
718
827
  cancelled = true;
719
828
  };
@@ -722,19 +831,43 @@ function PptxRenderer(props) {
722
831
  props.onThumbs(thumbs);
723
832
  }, [thumbs]);
724
833
  const pagesToShow = (0, import_react5.useMemo)(() => {
725
- 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
+ ];
726
839
  return [props.currentPage];
727
840
  }, [props.currentPage, props.layout, slides.length]);
728
- 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) => {
729
- const s = slides[p - 1];
730
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-slide", tabIndex: 0, onFocus: () => props.onCurrentPageChange(p), children: [
731
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "hv-slide-title", children: [
732
- "Slide ",
733
- p
734
- ] }),
735
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "hv-slide-text", children: s?.text || "" })
736
- ] }, p);
737
- }) }) });
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
+ ] });
738
871
  }
739
872
  function svgThumb(n) {
740
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>`;