@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/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef3, useSta
3
3
 
4
4
  // src/utils/locale.ts
5
5
  var defaultLocale = {
6
- "loading": "Loading\u2026",
6
+ loading: "Loading\u2026",
7
7
  "error.title": "Error",
8
8
  "toolbar.layout.single": "Single page",
9
9
  "toolbar.layout.two": "Side-by-side",
@@ -26,7 +26,17 @@ var defaultLocale = {
26
26
  function guessFileType(name, explicit) {
27
27
  if (explicit) return explicit;
28
28
  const ext = (name?.split(".").pop() || "").toLowerCase();
29
- const allowed = ["pdf", "md", "docx", "xlsx", "pptx", "txt", "png", "jpg", "svg"];
29
+ const allowed = [
30
+ "pdf",
31
+ "md",
32
+ "docx",
33
+ "xlsx",
34
+ "pptx",
35
+ "txt",
36
+ "png",
37
+ "jpg",
38
+ "svg"
39
+ ];
30
40
  return allowed.includes(ext) ? ext : "txt";
31
41
  }
32
42
  function arrayBufferToBase64(buf) {
@@ -57,7 +67,8 @@ async function resolveSource(args) {
57
67
  const ab = await base64ToArrayBuffer(args.base64);
58
68
  return { fileType, fileName, arrayBuffer: ab };
59
69
  }
60
- if (!args.fileUrl) throw new Error("No file source provided. Use fileUrl, blob, or base64.");
70
+ if (!args.fileUrl)
71
+ throw new Error("No file source provided. Use fileUrl, blob, or base64.");
61
72
  const res = await fetch(args.fileUrl);
62
73
  if (!res.ok) throw new Error(`Failed to fetch file (${res.status})`);
63
74
  const total = Number(res.headers.get("content-length") || "") || void 0;
@@ -173,44 +184,69 @@ function SignaturePanel(props) {
173
184
 
174
185
  // src/renderers/PdfRenderer.tsx
175
186
  import { useEffect, useMemo, useRef, useState } from "react";
176
- import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
187
+ import {
188
+ GlobalWorkerOptions,
189
+ getDocument
190
+ } from "pdfjs-dist";
177
191
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
178
192
  function PdfRenderer(props) {
179
193
  const { url, arrayBuffer } = props;
180
194
  const [doc, setDoc] = useState(null);
181
195
  const [pageCount, setPageCount] = useState(0);
182
- const [rendered, setRendered] = useState(/* @__PURE__ */ new Map());
196
+ const [rendered, setRendered] = useState(
197
+ /* @__PURE__ */ new Map()
198
+ );
183
199
  const [thumbs, setThumbs] = useState([]);
184
200
  const [size, setSize] = useState({ w: 840, h: 1188 });
201
+ const [error, setError] = useState(null);
202
+ const [loading, setLoading] = useState(false);
185
203
  const containerRef = useRef(null);
186
204
  useEffect(() => {
187
205
  try {
188
- GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString();
206
+ GlobalWorkerOptions.workerSrc = new URL(
207
+ "pdfjs-dist/build/pdf.worker.min.mjs",
208
+ import.meta.url
209
+ ).toString();
189
210
  } catch {
190
211
  }
191
212
  }, []);
192
213
  useEffect(() => {
193
214
  let cancel = false;
215
+ setError(null);
216
+ setLoading(true);
194
217
  (async () => {
195
218
  setDoc(null);
196
219
  setRendered(/* @__PURE__ */ new Map());
197
220
  setThumbs([]);
198
- if (!url && !arrayBuffer) return;
199
- const task = getDocument(url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer });
200
- const pdf = await task.promise;
201
- if (cancel) return;
202
- setDoc(pdf);
203
- setPageCount(pdf.numPages);
204
- props.onPageCount(pdf.numPages);
205
- setThumbs(Array.from({ length: pdf.numPages }));
206
- const p1 = await pdf.getPage(1);
207
- const base = p1.getViewport({ scale: 1 });
208
- const w = Math.min(980, Math.max(640, base.width));
209
- const s = w / base.width;
210
- const vp = p1.getViewport({ scale: s });
211
- setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
212
- })().catch(() => {
213
- });
221
+ if (!url && !arrayBuffer) {
222
+ setError("No PDF source provided.");
223
+ setLoading(false);
224
+ return;
225
+ }
226
+ try {
227
+ const task = getDocument(
228
+ url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer }
229
+ );
230
+ const pdf = await task.promise;
231
+ if (cancel) return;
232
+ setDoc(pdf);
233
+ setPageCount(pdf.numPages);
234
+ props.onPageCount(pdf.numPages);
235
+ setThumbs(Array.from({ length: pdf.numPages }));
236
+ const p1 = await pdf.getPage(1);
237
+ const base = p1.getViewport({ scale: 1 });
238
+ const w = Math.min(980, Math.max(640, base.width));
239
+ const s = w / base.width;
240
+ const vp = p1.getViewport({ scale: s });
241
+ setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
242
+ } catch (e) {
243
+ setError(
244
+ "Failed to load PDF. " + (e instanceof Error ? e.message : "")
245
+ );
246
+ } finally {
247
+ setLoading(false);
248
+ }
249
+ })();
214
250
  return () => {
215
251
  cancel = true;
216
252
  };
@@ -232,37 +268,43 @@ function PdfRenderer(props) {
232
268
  (async () => {
233
269
  for (const p of pagesToShow) {
234
270
  if (rendered.has(p)) continue;
235
- const page = await doc.getPage(p);
236
- if (cancel) return;
237
- const base = page.getViewport({ scale: 1 });
238
- const vp = page.getViewport({ scale: size.w / base.width });
239
- const canvas = document.createElement("canvas");
240
- canvas.width = Math.round(vp.width);
241
- canvas.height = Math.round(vp.height);
242
- const ctx = canvas.getContext("2d", { alpha: false });
243
- if (!ctx) continue;
244
- await page.render({ canvasContext: ctx, viewport: vp }).promise;
245
- if (cancel) return;
246
- setRendered((prev) => {
247
- const next = new Map(prev);
248
- next.set(p, canvas);
249
- return next;
250
- });
251
271
  try {
252
- const tw = 140;
253
- const th = Math.round(tw * (canvas.height / canvas.width));
254
- const t = document.createElement("canvas");
255
- t.width = tw;
256
- t.height = th;
257
- const tctx = t.getContext("2d");
258
- if (tctx) {
259
- tctx.drawImage(canvas, 0, 0, tw, th);
260
- const url2 = t.toDataURL("image/jpeg", 0.75);
261
- setThumbs((prev) => {
262
- const next = prev.slice();
263
- next[p - 1] = url2;
264
- return next;
265
- });
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
+ if (!thumbs[p - 1]) {
289
+ const thumbCanvas = document.createElement("canvas");
290
+ const thumbScale = 120 / vp.width;
291
+ thumbCanvas.width = Math.round(vp.width * thumbScale);
292
+ thumbCanvas.height = Math.round(vp.height * thumbScale);
293
+ const thumbCtx = thumbCanvas.getContext("2d", { alpha: false });
294
+ if (thumbCtx) {
295
+ thumbCtx.drawImage(
296
+ canvas,
297
+ 0,
298
+ 0,
299
+ thumbCanvas.width,
300
+ thumbCanvas.height
301
+ );
302
+ setThumbs((prev) => {
303
+ const arr = prev.slice();
304
+ arr[p - 1] = thumbCanvas.toDataURL("image/png");
305
+ return arr;
306
+ });
307
+ }
266
308
  }
267
309
  } catch {
268
310
  }
@@ -271,13 +313,16 @@ function PdfRenderer(props) {
271
313
  return () => {
272
314
  cancel = true;
273
315
  };
274
- }, [doc, pagesToShow, size.w, rendered]);
316
+ }, [doc, pagesToShow, size.w, rendered, thumbs]);
275
317
  function onWheel(e) {
276
318
  if (!pageCount) return;
277
319
  if (Math.abs(e.deltaY) < 10) return;
278
320
  const dir = e.deltaY > 0 ? 1 : -1;
279
321
  const step = props.layout === "side-by-side" ? 2 : 1;
280
- const next = Math.max(1, Math.min(pageCount, props.currentPage + dir * step));
322
+ const next = Math.max(
323
+ 1,
324
+ Math.min(pageCount, props.currentPage + dir * step)
325
+ );
281
326
  props.onCurrentPageChange(next);
282
327
  }
283
328
  function clickPlace(e, page) {
@@ -290,22 +335,37 @@ function PdfRenderer(props) {
290
335
  }
291
336
  return /* @__PURE__ */ jsxs4("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
292
337
  !doc ? /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : null,
293
- doc ? /* @__PURE__ */ jsx4("div", { className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages", children: pagesToShow.map((p) => {
294
- const c = rendered.get(p);
295
- return /* @__PURE__ */ jsx4("div", { className: "hv-page", style: { width: size.w, height: size.h }, onClick: (e) => clickPlace(e, p), children: c ? /* @__PURE__ */ jsx4(
296
- "canvas",
297
- {
298
- className: "hv-canvas",
299
- width: c.width,
300
- height: c.height,
301
- ref: (node) => {
302
- if (!node) return;
303
- const ctx = node.getContext("2d");
304
- if (ctx) ctx.drawImage(c, 0, 0);
305
- }
306
- }
307
- ) : /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Rendering\u2026" }) }, p);
308
- }) }) : null
338
+ doc ? /* @__PURE__ */ jsx4(
339
+ "div",
340
+ {
341
+ className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
342
+ children: pagesToShow.map((p) => {
343
+ const c = rendered.get(p);
344
+ return /* @__PURE__ */ jsx4(
345
+ "div",
346
+ {
347
+ className: "hv-page",
348
+ style: { width: size.w, height: size.h },
349
+ onClick: (e) => clickPlace(e, p),
350
+ children: c ? /* @__PURE__ */ jsx4(
351
+ "canvas",
352
+ {
353
+ className: "hv-canvas",
354
+ width: c.width,
355
+ height: c.height,
356
+ ref: (node) => {
357
+ if (!node) return;
358
+ const ctx = node.getContext("2d");
359
+ if (ctx) ctx.drawImage(c, 0, 0);
360
+ }
361
+ }
362
+ ) : /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Rendering\u2026" })
363
+ },
364
+ p
365
+ );
366
+ })
367
+ }
368
+ ) : null
309
369
  ] });
310
370
  }
311
371
 
@@ -427,17 +487,29 @@ var RichTextEditor = forwardRef((props, ref) => {
427
487
  }
428
488
  if (props.fileType === "docx") {
429
489
  try {
430
- const htmlToDocx = (await import("html-to-docx")).default;
431
- const blob = await htmlToDocx(stitched);
432
- const ab = await blob.arrayBuffer();
433
- props.onSave(arrayBufferToBase64(ab), {
434
- fileName: replaceExt(props.fileName, "docx"),
435
- fileType: "docx",
436
- annotations: { signaturePlacements: props.signaturePlacements }
490
+ const response = await fetch("/api/export-docx", {
491
+ method: "POST",
492
+ headers: { "Content-Type": "application/json" },
493
+ body: JSON.stringify({
494
+ html: stitched,
495
+ fileName: replaceExt(props.fileName, "docx")
496
+ })
437
497
  });
498
+ if (!response.ok) throw new Error("Failed to generate DOCX");
499
+ const blob = await response.blob();
500
+ const url = window.URL.createObjectURL(blob);
501
+ const a = document.createElement("a");
502
+ a.href = url;
503
+ a.download = replaceExt(props.fileName, "docx");
504
+ document.body.appendChild(a);
505
+ a.click();
506
+ setTimeout(() => {
507
+ window.URL.revokeObjectURL(url);
508
+ a.remove();
509
+ }, 100);
438
510
  } catch (err) {
439
511
  alert(
440
- "DOCX export is not supported in this environment. Please use this feature in a Node.js/server context."
512
+ "DOCX export failed: " + (err instanceof Error ? err.message : String(err))
441
513
  );
442
514
  }
443
515
  return;
@@ -601,7 +673,11 @@ function ensureExt(name, ext) {
601
673
  // src/renderers/ImageRenderer.tsx
602
674
  import { useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
603
675
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
604
- function ImageRenderer({ arrayBuffer, fileType, fileName }) {
676
+ function ImageRenderer({
677
+ arrayBuffer,
678
+ fileType,
679
+ fileName
680
+ }) {
605
681
  const [zoom, setZoom] = useState4(1);
606
682
  const url = useMemo4(() => {
607
683
  if (!arrayBuffer) return void 0;
@@ -617,14 +693,42 @@ function ImageRenderer({ arrayBuffer, fileType, fileName }) {
617
693
  /* @__PURE__ */ jsxs7("div", { className: "hv-mini-toolbar", children: [
618
694
  /* @__PURE__ */ jsx7("div", { className: "hv-title", children: fileName }),
619
695
  /* @__PURE__ */ jsx7("div", { className: "hv-spacer" }),
620
- /* @__PURE__ */ jsx7("button", { type: "button", className: "hv-btn", onClick: () => setZoom((z) => Math.max(0.25, z - 0.25)), children: "-" }),
696
+ /* @__PURE__ */ jsx7(
697
+ "button",
698
+ {
699
+ type: "button",
700
+ className: "hv-btn",
701
+ onClick: () => setZoom((z) => Math.max(0.25, z - 0.25)),
702
+ children: "-"
703
+ }
704
+ ),
621
705
  /* @__PURE__ */ jsxs7("div", { className: "hv-zoom", children: [
622
706
  Math.round(zoom * 100),
623
707
  "%"
624
708
  ] }),
625
- /* @__PURE__ */ jsx7("button", { type: "button", className: "hv-btn", onClick: () => setZoom((z) => Math.min(4, z + 0.25)), children: "+" })
709
+ /* @__PURE__ */ jsx7(
710
+ "button",
711
+ {
712
+ type: "button",
713
+ className: "hv-btn",
714
+ onClick: () => setZoom((z) => Math.min(4, z + 0.25)),
715
+ children: "+"
716
+ }
717
+ )
626
718
  ] }),
627
- /* @__PURE__ */ jsx7("div", { className: "hv-center", children: url ? /* @__PURE__ */ jsx7("img", { src: url, alt: fileName, style: { transform: `scale(${zoom})` }, className: "hv-image" }) : /* @__PURE__ */ jsx7("div", { className: "hv-loading", children: "Loading\u2026" }) })
719
+ /* @__PURE__ */ jsxs7("div", { className: "hv-center", children: [
720
+ !arrayBuffer && /* @__PURE__ */ jsx7("div", { className: "hv-error", children: "No image data provided." }),
721
+ arrayBuffer && !url && /* @__PURE__ */ jsx7("div", { className: "hv-error", children: "Failed to load image." }),
722
+ url && /* @__PURE__ */ jsx7(
723
+ "img",
724
+ {
725
+ src: url,
726
+ alt: fileName,
727
+ style: { transform: `scale(${zoom})` },
728
+ className: "hv-image"
729
+ }
730
+ )
731
+ ] })
628
732
  ] });
629
733
  }
630
734
 
@@ -641,37 +745,57 @@ function extractText(xml) {
641
745
  function PptxRenderer(props) {
642
746
  const [slides, setSlides] = useState5([]);
643
747
  const [thumbs, setThumbs] = useState5([]);
748
+ const [error, setError] = useState5(null);
749
+ const [loading, setLoading] = useState5(false);
644
750
  useEffect5(() => {
645
751
  let cancelled = false;
752
+ setError(null);
753
+ setLoading(true);
646
754
  (async () => {
647
755
  setSlides([]);
648
756
  setThumbs([]);
649
757
  if (!props.arrayBuffer) {
650
758
  props.onSlideCount(1);
651
759
  setSlides([{ index: 1, text: "No content" }]);
760
+ setError("No PPTX data provided.");
761
+ setLoading(false);
652
762
  return;
653
763
  }
654
- const zip = await JSZip.loadAsync(props.arrayBuffer);
655
- const files = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort((a, b) => {
656
- const na = Number(a.match(/slide(\d+)\.xml/)?.[1] || 0);
657
- const nb = Number(b.match(/slide(\d+)\.xml/)?.[1] || 0);
658
- return na - nb;
659
- });
660
- const out = [];
661
- for (let i = 0; i < files.length; i++) {
662
- const xml = await zip.file(files[i]).async("string");
663
- out.push({ index: i + 1, text: extractText(xml) });
764
+ try {
765
+ const zip = await JSZip.loadAsync(props.arrayBuffer);
766
+ const files = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort((a, b) => {
767
+ const na = Number(a.match(/slide(\d+)\.xml/)?.[1] || 0);
768
+ const nb = Number(b.match(/slide(\d+)\.xml/)?.[1] || 0);
769
+ return na - nb;
770
+ });
771
+ const out = [];
772
+ for (let i = 0; i < files.length; i++) {
773
+ const xml = await zip.file(files[i]).async("string");
774
+ out.push({ index: i + 1, text: extractText(xml) });
775
+ }
776
+ if (cancelled) return;
777
+ const count = Math.max(1, out.length);
778
+ props.onSlideCount(count);
779
+ setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
780
+ setThumbs(
781
+ Array.from(
782
+ { length: count },
783
+ (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`
784
+ )
785
+ );
786
+ } catch (e) {
787
+ props.onSlideCount(1);
788
+ setSlides([
789
+ { index: 1, text: "Unable to render this .pptx in-browser." }
790
+ ]);
791
+ setThumbs([void 0]);
792
+ setError(
793
+ "Failed to load PPTX. " + (e instanceof Error ? e.message : "")
794
+ );
795
+ } finally {
796
+ setLoading(false);
664
797
  }
665
- if (cancelled) return;
666
- const count = Math.max(1, out.length);
667
- props.onSlideCount(count);
668
- setSlides(out.length ? out : [{ index: 1, text: "(empty)" }]);
669
- setThumbs(Array.from({ length: count }, (_, i) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`));
670
- })().catch(() => {
671
- props.onSlideCount(1);
672
- setSlides([{ index: 1, text: "Unable to render this .pptx in-browser." }]);
673
- setThumbs([void 0]);
674
- });
798
+ })();
675
799
  return () => {
676
800
  cancelled = true;
677
801
  };
@@ -680,19 +804,43 @@ function PptxRenderer(props) {
680
804
  props.onThumbs(thumbs);
681
805
  }, [thumbs]);
682
806
  const pagesToShow = useMemo5(() => {
683
- if (props.layout === "side-by-side") return [props.currentPage, Math.min(slides.length || props.currentPage + 1, props.currentPage + 1)];
807
+ if (props.layout === "side-by-side")
808
+ return [
809
+ props.currentPage,
810
+ Math.min(slides.length || props.currentPage + 1, props.currentPage + 1)
811
+ ];
684
812
  return [props.currentPage];
685
813
  }, [props.currentPage, props.layout, slides.length]);
686
- return /* @__PURE__ */ jsx8("div", { className: "hv-doc", children: /* @__PURE__ */ jsx8("div", { className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages", children: pagesToShow.map((p) => {
687
- const s = slides[p - 1];
688
- return /* @__PURE__ */ jsxs8("div", { className: "hv-slide", tabIndex: 0, onFocus: () => props.onCurrentPageChange(p), children: [
689
- /* @__PURE__ */ jsxs8("div", { className: "hv-slide-title", children: [
690
- "Slide ",
691
- p
692
- ] }),
693
- /* @__PURE__ */ jsx8("div", { className: "hv-slide-text", children: s?.text || "" })
694
- ] }, p);
695
- }) }) });
814
+ return /* @__PURE__ */ jsxs8("div", { className: "hv-doc", children: [
815
+ loading && /* @__PURE__ */ jsx8("div", { className: "hv-loading", children: "Loading PPTX\u2026" }),
816
+ error && /* @__PURE__ */ jsx8("div", { className: "hv-error", children: error }),
817
+ !loading && !error && (!slides || slides.length === 0) && /* @__PURE__ */ jsx8("div", { className: "hv-error", children: "No slides to display." }),
818
+ !error && slides && slides.length > 0 && /* @__PURE__ */ jsx8(
819
+ "div",
820
+ {
821
+ className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
822
+ children: pagesToShow.map((p) => {
823
+ const s = slides[p - 1];
824
+ return /* @__PURE__ */ jsxs8(
825
+ "div",
826
+ {
827
+ className: "hv-slide",
828
+ tabIndex: 0,
829
+ onFocus: () => props.onCurrentPageChange(p),
830
+ children: [
831
+ /* @__PURE__ */ jsxs8("div", { className: "hv-slide-title", children: [
832
+ "Slide ",
833
+ p
834
+ ] }),
835
+ /* @__PURE__ */ jsx8("div", { className: "hv-slide-text", children: s?.text || "" })
836
+ ]
837
+ },
838
+ p
839
+ );
840
+ })
841
+ }
842
+ )
843
+ ] });
696
844
  }
697
845
  function svgThumb(n) {
698
846
  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>`;