@zerohive/hive-viewer 0.2.2 → 0.2.4

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
@@ -1,386 +1,18 @@
1
1
  // src/components/DocumentViewer.tsx
2
2
  import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef3, useState as useState6 } from "react";
3
3
 
4
- // src/utils/locale.ts
5
- var defaultLocale = {
6
- loading: "Loading\u2026",
7
- "error.title": "Error",
8
- "toolbar.layout.single": "Single page",
9
- "toolbar.layout.two": "Side-by-side",
10
- "toolbar.thumbs": "Thumbnails",
11
- "toolbar.signatures": "Signatures",
12
- "toolbar.sign": "Sign Document",
13
- "toolbar.save": "Save",
14
- "toolbar.exportPdf": "Export as PDF",
15
- "thumbnails.title": "Thumbnails",
16
- "thumbnails.page": "Page",
17
- "signatures.title": "Signatures",
18
- "signatures.empty": "No signatures",
19
- "signatures.placeHint": "Click on the document to place the signature.",
20
- "a11y.viewer": "Document viewer",
21
- "a11y.ribbon": "Ribbon",
22
- "a11y.editor": "Document editor"
23
- };
24
-
25
- // src/utils/fileSource.ts
26
- function guessFileType(name, explicit) {
27
- if (explicit) return explicit;
28
- const ext = (name?.split(".").pop() || "").toLowerCase();
29
- const allowed = [
30
- "pdf",
31
- "md",
32
- "docx",
33
- "xlsx",
34
- "pptx",
35
- "txt",
36
- "png",
37
- "jpg",
38
- "svg"
39
- ];
40
- return allowed.includes(ext) ? ext : "txt";
41
- }
42
- function arrayBufferToBase64(buf) {
43
- const bytes = new Uint8Array(buf);
44
- let binary = "";
45
- const chunk = 32768;
46
- for (let i = 0; i < bytes.length; i += chunk) {
47
- binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
48
- }
49
- return btoa(binary);
50
- }
51
- async function base64ToArrayBuffer(b64) {
52
- const bin = atob(b64);
53
- const len = bin.length;
54
- const bytes = new Uint8Array(len);
55
- for (let i = 0; i < len; i++) bytes[i] = bin.charCodeAt(i);
56
- return bytes.buffer;
57
- }
58
- async function resolveSource(args) {
59
- const fileType = guessFileType(args.fileName, args.fileType);
60
- const fileName = args.fileName ?? `document.${fileType}`;
61
- if (args.blob) {
62
- const ab = await args.blob.arrayBuffer();
63
- const url = URL.createObjectURL(args.blob);
64
- return { fileType, fileName, arrayBuffer: ab, url };
65
- }
66
- if (args.base64) {
67
- const ab = await base64ToArrayBuffer(args.base64);
68
- return { fileType, fileName, arrayBuffer: ab };
69
- }
70
- if (!args.fileUrl)
71
- throw new Error("No file source provided. Use fileUrl, blob, or base64.");
72
- const res = await fetch(args.fileUrl);
73
- if (!res.ok) throw new Error(`Failed to fetch file (${res.status})`);
74
- const total = Number(res.headers.get("content-length") || "") || void 0;
75
- if (!res.body) {
76
- const ab = await res.arrayBuffer();
77
- args.onProgress?.(ab.byteLength, total);
78
- return { fileType, fileName, arrayBuffer: ab, url: args.fileUrl };
79
- }
80
- const reader = res.body.getReader();
81
- const chunks = [];
82
- let loaded = 0;
83
- while (true) {
84
- const { done, value } = await reader.read();
85
- if (done) break;
86
- if (value) {
87
- chunks.push(value);
88
- loaded += value.length;
89
- args.onProgress?.(loaded, total);
90
- }
91
- }
92
- const out = new Uint8Array(loaded);
93
- let offset = 0;
94
- for (const c of chunks) {
95
- out.set(c, offset);
96
- offset += c.length;
97
- }
98
- return { fileType, fileName, arrayBuffer: out.buffer, url: args.fileUrl };
99
- }
100
-
101
- // src/components/Toolbar.tsx
102
- import { jsx, jsxs } from "react/jsx-runtime";
103
- function Toolbar(props) {
104
- const t = (k, fallback) => props.locale[k] ?? fallback;
105
- return /* @__PURE__ */ jsxs("div", { className: "hv-toolbar", role: "toolbar", "aria-label": t("a11y.toolbar", "Document toolbar"), children: [
106
- /* @__PURE__ */ jsxs("div", { className: "hv-toolbar__left", children: [
107
- /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onToggleThumbnails, "aria-pressed": props.showThumbnails, children: t("toolbar.thumbs", "Thumbnails") }),
108
- props.mode !== "create" && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onToggleSignatures, "aria-pressed": props.showSignatures, children: t("toolbar.signatures", "Signatures") }),
109
- /* @__PURE__ */ jsx("span", { className: "hv-sep" }),
110
- /* @__PURE__ */ jsx("button", { type: "button", className: props.layout === "single" ? "hv-btn hv-btn--active" : "hv-btn", onClick: () => props.onChangeLayout("single"), children: t("toolbar.layout.single", "Single") }),
111
- /* @__PURE__ */ jsx("button", { type: "button", className: props.layout === "side-by-side" ? "hv-btn hv-btn--active" : "hv-btn", onClick: () => props.onChangeLayout("side-by-side"), children: t("toolbar.layout.two", "Two") })
112
- ] }),
113
- /* @__PURE__ */ jsxs("div", { className: "hv-toolbar__right", children: [
114
- props.showHeaderFooterToggle && /* @__PURE__ */ jsxs("label", { className: "hv-toggle", children: [
115
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: props.headerFooterEnabled, onChange: props.onToggleHeaderFooter }),
116
- /* @__PURE__ */ jsx("span", { children: t("toolbar.letterhead", "Letterhead") })
117
- ] }),
118
- props.allowSigning && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn hv-btn--primary", onClick: props.onSign, disabled: props.signingDisabled, children: t("toolbar.sign", "Sign Document") }),
119
- props.canExportPdf && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn", onClick: props.onExportPdf, children: t("toolbar.exportPdf", "Export as PDF") }),
120
- props.canSave && /* @__PURE__ */ jsx("button", { type: "button", className: "hv-btn hv-btn--primary", onClick: props.onSave, children: t("toolbar.save", "Save") })
121
- ] })
122
- ] });
123
- }
124
-
125
- // src/components/ThumbnailsSidebar.tsx
126
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
127
- function ThumbnailsSidebar(props) {
128
- const t = props.locale["thumbnails.title"] ?? "Thumbnails";
129
- return /* @__PURE__ */ jsxs2("aside", { className: props.collapsed ? "hv-thumbs hv-thumbs--collapsed" : "hv-thumbs", "aria-label": t, children: [
130
- /* @__PURE__ */ jsxs2("div", { className: "hv-thumbs__header", children: [
131
- /* @__PURE__ */ jsx2(
132
- "button",
133
- {
134
- type: "button",
135
- className: "hv-icon",
136
- onClick: props.onToggle,
137
- "aria-label": props.collapsed ? props.locale["thumbnails.open"] ?? "Open thumbnails" : props.locale["thumbnails.close"] ?? "Close thumbnails",
138
- children: props.collapsed ? "\u25B8" : "\u25BE"
139
- }
140
- ),
141
- !props.collapsed ? /* @__PURE__ */ jsx2("div", { className: "hv-thumbs__title", children: t }) : null
142
- ] }),
143
- !props.collapsed ? /* @__PURE__ */ jsx2("div", { className: "hv-thumbs__list", role: "list", children: props.thumbnails.map((th, idx) => {
144
- const p = idx + 1;
145
- const active = p === props.currentPage;
146
- return /* @__PURE__ */ jsxs2(
147
- "button",
148
- {
149
- type: "button",
150
- role: "listitem",
151
- className: active ? "hv-thumb hv-thumb--active" : "hv-thumb",
152
- onClick: () => props.onSelectPage(p),
153
- "aria-current": active ? "page" : void 0,
154
- children: [
155
- /* @__PURE__ */ jsx2("div", { className: "hv-thumb__img", "aria-hidden": true, children: th.dataUrl ? /* @__PURE__ */ jsx2("img", { src: th.dataUrl, alt: "" }) : /* @__PURE__ */ jsx2("div", { className: "hv-thumb__placeholder" }) }),
156
- /* @__PURE__ */ jsx2("div", { className: "hv-thumb__label", children: th.label })
157
- ]
158
- },
159
- th.id
160
- );
161
- }) }) : null
162
- ] });
163
- }
164
-
165
- // src/components/SignaturePanel.tsx
166
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
167
- function SignaturePanel(props) {
168
- const title = props.locale["signatures.title"] ?? "Signatures";
169
- return /* @__PURE__ */ jsxs3("aside", { className: props.collapsed ? "hv-side hv-side--collapsed" : "hv-side", "aria-label": title, children: [
170
- /* @__PURE__ */ jsxs3("div", { className: "hv-sidebar-header", children: [
171
- /* @__PURE__ */ jsx3("button", { type: "button", className: "hv-icon", onClick: props.onToggle, "aria-label": props.locale["toolbar.signatures"] ?? "Signatures", children: "\u270D" }),
172
- /* @__PURE__ */ jsx3("div", { className: "hv-sidebar-title", children: title })
173
- ] }),
174
- /* @__PURE__ */ jsx3("div", { className: "hv-sidebar-body", children: props.signatures.map((s, idx) => /* @__PURE__ */ jsxs3("div", { className: "hv-signature-card", children: [
175
- /* @__PURE__ */ jsx3("img", { src: s.signatureImageUrl, alt: `Signature by ${s.signedBy}`, className: "hv-signature-img" }),
176
- /* @__PURE__ */ jsxs3("div", { className: "hv-signature-meta", children: [
177
- /* @__PURE__ */ jsx3("div", { className: "hv-signature-name", children: s.signedBy }),
178
- /* @__PURE__ */ jsx3("div", { className: "hv-signature-date", children: new Date(s.dateSigned).toLocaleString() }),
179
- s.comment ? /* @__PURE__ */ jsx3("div", { className: "hv-signature-comment", children: s.comment }) : null
180
- ] })
181
- ] }, `${s.signedBy}-${s.dateSigned}-${idx}`)) })
182
- ] });
183
- }
184
-
185
- // src/renderers/PdfRenderer.tsx
186
- import { useEffect, useMemo, useRef, useState } from "react";
187
- import {
188
- GlobalWorkerOptions,
189
- getDocument
190
- } from "pdfjs-dist";
191
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
192
- function PdfRenderer(props) {
193
- const { url, arrayBuffer } = props;
194
- const [doc, setDoc] = useState(null);
195
- const [pageCount, setPageCount] = useState(0);
196
- const [rendered, setRendered] = useState(
197
- /* @__PURE__ */ new Map()
198
- );
199
- const [thumbs, setThumbs] = useState([]);
200
- const [size, setSize] = useState({ w: 840, h: 1188 });
201
- const [error, setError] = useState(null);
202
- const [loading, setLoading] = useState(false);
203
- const containerRef = useRef(null);
204
- useEffect(() => {
205
- try {
206
- GlobalWorkerOptions.workerSrc = new URL(
207
- "pdfjs-dist/build/pdf.worker.min.mjs",
208
- import.meta.url
209
- ).toString();
210
- } catch {
211
- }
212
- }, []);
213
- useEffect(() => {
214
- let cancel = false;
215
- setError(null);
216
- setLoading(true);
217
- (async () => {
218
- setDoc(null);
219
- setRendered(/* @__PURE__ */ new Map());
220
- setThumbs([]);
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
- })();
250
- return () => {
251
- cancel = true;
252
- };
253
- }, [url, arrayBuffer]);
254
- useEffect(() => {
255
- props.onThumbs(thumbs);
256
- }, [thumbs]);
257
- const pagesToShow = useMemo(() => {
258
- if (props.layout === "side-by-side") {
259
- const left = props.currentPage;
260
- const right = Math.min(pageCount || left + 1, left + 1);
261
- return [left, right];
262
- }
263
- return [props.currentPage];
264
- }, [props.currentPage, props.layout, pageCount]);
265
- useEffect(() => {
266
- if (!doc) return;
267
- let cancel = false;
268
- (async () => {
269
- for (const p of pagesToShow) {
270
- if (rendered.has(p)) continue;
271
- try {
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
- }
308
- }
309
- } catch {
310
- }
311
- }
312
- })();
313
- return () => {
314
- cancel = true;
315
- };
316
- }, [doc, pagesToShow, size.w, rendered, thumbs]);
317
- function onWheel(e) {
318
- if (!pageCount) return;
319
- if (Math.abs(e.deltaY) < 10) return;
320
- const dir = e.deltaY > 0 ? 1 : -1;
321
- const step = props.layout === "side-by-side" ? 2 : 1;
322
- const next = Math.max(
323
- 1,
324
- Math.min(pageCount, props.currentPage + dir * step)
325
- );
326
- props.onCurrentPageChange(next);
327
- }
328
- function clickPlace(e, page) {
329
- const stamp = props.signatureStamp;
330
- if (!stamp?.armed) return;
331
- const rect = e.currentTarget.getBoundingClientRect();
332
- const x = (e.clientX - rect.left) / rect.width;
333
- const y = (e.clientY - rect.top) / rect.height;
334
- stamp.onPlaced({ page, x, y, w: 0.22, h: 0.08 });
335
- }
336
- return /* @__PURE__ */ jsxs4("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
337
- !doc ? /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : 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
369
- ] });
370
- }
371
-
372
4
  // src/editors/RichTextEditor.tsx
5
+ import html2canvas from "html2canvas";
6
+ import mammoth from "mammoth";
7
+ import MarkdownIt from "markdown-it";
373
8
  import {
374
9
  forwardRef,
375
- useEffect as useEffect2,
10
+ useEffect,
376
11
  useImperativeHandle,
377
- useMemo as useMemo2,
378
- useRef as useRef2,
379
- useState as useState2
12
+ useMemo,
13
+ useRef,
14
+ useState
380
15
  } from "react";
381
- import mammoth from "mammoth";
382
- import MarkdownIt from "markdown-it";
383
- import html2canvas from "html2canvas";
384
16
 
385
17
  // src/utils/sanitize.ts
386
18
  import DOMPurify from "dompurify";
@@ -392,44 +24,53 @@ function sanitizeHtml(html) {
392
24
  }
393
25
 
394
26
  // src/editors/RichTextEditor.tsx
395
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
27
+ import { jsx, jsxs } from "react/jsx-runtime";
396
28
  var PAGE_H = 1122;
397
29
  var RichTextEditor = forwardRef((props, ref) => {
398
30
  const readOnly = props.mode === "view";
399
- const md = useMemo2(
31
+ const md = useMemo(
400
32
  () => new MarkdownIt({ html: false, linkify: true, breaks: true }),
401
33
  []
402
34
  );
403
- const scrollerRef = useRef2(null);
404
- const editorRef = useRef2(null);
405
- const captureRef = useRef2(null);
406
- const [html, setHtml] = useState2("<p><br/></p>");
407
- useEffect2(() => {
35
+ const scrollerRef = useRef(null);
36
+ const editorRef = useRef(null);
37
+ const captureRef = useRef(null);
38
+ const [html, setHtml] = useState("<p><br/></p>");
39
+ useEffect(() => {
408
40
  let cancelled = false;
409
41
  (async () => {
410
42
  if (props.mode === "create") {
411
43
  setHtml("<p><br/></p>");
412
44
  return;
413
45
  }
414
- if (!props.arrayBuffer) return;
46
+ if (!props.arrayBuffer) {
47
+ return;
48
+ }
415
49
  if (props.fileType === "docx") {
416
50
  const res = await mammoth.convertToHtml({
417
51
  arrayBuffer: props.arrayBuffer
418
52
  });
419
- if (!cancelled) setHtml(sanitizeHtml(res.value || "<p><br/></p>"));
53
+ if (!cancelled) {
54
+ setHtml(sanitizeHtml(res.value || "<p><br/></p>"));
55
+ }
420
56
  } else {
421
57
  const text = new TextDecoder().decode(props.arrayBuffer);
422
- if (props.fileType === "md") setHtml(sanitizeHtml(md.render(text)));
423
- else setHtml(`<pre>${escapeHtml(text)}</pre>`);
58
+ if (props.fileType === "md") {
59
+ setHtml(sanitizeHtml(md.render(text)));
60
+ } else {
61
+ setHtml(`<pre>${escapeHtml(text)}</pre>`);
62
+ }
424
63
  }
425
64
  })();
426
65
  return () => {
427
66
  cancelled = true;
428
67
  };
429
68
  }, [props.arrayBuffer, props.fileType, props.mode, md]);
430
- useEffect2(() => {
69
+ useEffect(() => {
431
70
  const el = scrollerRef.current;
432
- if (!el) return;
71
+ if (!el) {
72
+ return;
73
+ }
433
74
  const recompute = () => props.onPageCount(Math.max(1, Math.ceil(el.scrollHeight / PAGE_H)));
434
75
  recompute();
435
76
  const ro = new ResizeObserver(recompute);
@@ -437,13 +78,19 @@ var RichTextEditor = forwardRef((props, ref) => {
437
78
  return () => ro.disconnect();
438
79
  }, [html, props.headerFooterEnabled]);
439
80
  function exec(cmd) {
440
- if (readOnly) return;
81
+ if (readOnly) {
82
+ return;
83
+ }
441
84
  document.execCommand(cmd);
442
85
  }
443
86
  function onClick(e) {
444
- if (!props.armedSignatureUrl) return;
87
+ if (!props.armedSignatureUrl) {
88
+ return;
89
+ }
445
90
  const scroller = scrollerRef.current;
446
- if (!scroller) return;
91
+ if (!scroller) {
92
+ return;
93
+ }
447
94
  const rect = e.currentTarget.getBoundingClientRect();
448
95
  const absY = scroller.scrollTop + (e.clientY - rect.top);
449
96
  const page = Math.max(1, Math.floor(absY / PAGE_H) + 1);
@@ -455,7 +102,9 @@ var RichTextEditor = forwardRef((props, ref) => {
455
102
  async function requestThumbnail(index) {
456
103
  const scroller = scrollerRef.current;
457
104
  const capture = captureRef.current;
458
- if (!scroller || !capture) return void 0;
105
+ if (!scroller || !capture) {
106
+ return void 0;
107
+ }
459
108
  const old = scroller.scrollTop;
460
109
  scroller.scrollTop = index * PAGE_H;
461
110
  await new Promise((r) => requestAnimationFrame(() => r(null)));
@@ -495,7 +144,9 @@ var RichTextEditor = forwardRef((props, ref) => {
495
144
  fileName: replaceExt(props.fileName, "docx")
496
145
  })
497
146
  });
498
- if (!response.ok) throw new Error("Failed to generate DOCX");
147
+ if (!response.ok) {
148
+ throw new Error("Failed to generate DOCX");
149
+ }
499
150
  const blob = await response.blob();
500
151
  const url = window.URL.createObjectURL(blob);
501
152
  const a = document.createElement("a");
@@ -523,9 +174,9 @@ var RichTextEditor = forwardRef((props, ref) => {
523
174
  });
524
175
  }
525
176
  useImperativeHandle(ref, () => ({ save, requestThumbnail }));
526
- return /* @__PURE__ */ jsxs5("div", { className: "hv-doc", children: [
527
- /* @__PURE__ */ jsxs5("div", { className: "hv-ribbon", role: "toolbar", children: [
528
- /* @__PURE__ */ jsx5(
177
+ return /* @__PURE__ */ jsxs("div", { className: "hv-doc", children: [
178
+ /* @__PURE__ */ jsxs("div", { className: "hv-ribbon", role: "toolbar", children: [
179
+ /* @__PURE__ */ jsx(
529
180
  "button",
530
181
  {
531
182
  className: "hv-btn",
@@ -534,7 +185,7 @@ var RichTextEditor = forwardRef((props, ref) => {
534
185
  children: "B"
535
186
  }
536
187
  ),
537
- /* @__PURE__ */ jsx5(
188
+ /* @__PURE__ */ jsx(
538
189
  "button",
539
190
  {
540
191
  className: "hv-btn",
@@ -543,7 +194,7 @@ var RichTextEditor = forwardRef((props, ref) => {
543
194
  children: "I"
544
195
  }
545
196
  ),
546
- /* @__PURE__ */ jsx5(
197
+ /* @__PURE__ */ jsx(
547
198
  "button",
548
199
  {
549
200
  className: "hv-btn",
@@ -552,11 +203,11 @@ var RichTextEditor = forwardRef((props, ref) => {
552
203
  children: "U"
553
204
  }
554
205
  ),
555
- props.armedSignatureUrl ? /* @__PURE__ */ jsx5("div", { className: "hv-hint", children: "Click to place signature" }) : null
206
+ props.armedSignatureUrl ? /* @__PURE__ */ jsx("div", { className: "hv-hint", children: "Click to place signature" }) : null
556
207
  ] }),
557
- /* @__PURE__ */ jsx5("div", { className: "hv-scroll", ref: scrollerRef, onClick, children: /* @__PURE__ */ jsxs5("div", { className: "hv-pageStage", ref: captureRef, children: [
558
- props.headerFooterEnabled && props.headerComponent ? /* @__PURE__ */ jsx5("div", { className: "hv-letterhead", children: props.headerComponent }) : null,
559
- /* @__PURE__ */ jsx5(
208
+ /* @__PURE__ */ jsx("div", { className: "hv-scroll", ref: scrollerRef, onClick, children: /* @__PURE__ */ jsxs("div", { className: "hv-pageStage", ref: captureRef, children: [
209
+ props.headerFooterEnabled && props.headerComponent ? /* @__PURE__ */ jsx("div", { className: "hv-letterhead", children: props.headerComponent }) : null,
210
+ /* @__PURE__ */ jsx(
560
211
  "div",
561
212
  {
562
213
  ref: editorRef,
@@ -567,9 +218,9 @@ var RichTextEditor = forwardRef((props, ref) => {
567
218
  dangerouslySetInnerHTML: { __html: html }
568
219
  }
569
220
  ),
570
- props.headerFooterEnabled && props.footerComponent ? /* @__PURE__ */ jsx5("div", { className: "hv-letterhead hv-letterhead--footer", children: props.footerComponent }) : null,
571
- props.mode === "create" && props.signatures.length ? /* @__PURE__ */ jsx5("div", { className: "hv-signatures-inline", children: props.signatures.map((s, i) => /* @__PURE__ */ jsxs5("div", { className: "hv-sign-inline", children: [
572
- /* @__PURE__ */ jsx5(
221
+ props.headerFooterEnabled && props.footerComponent ? /* @__PURE__ */ jsx("div", { className: "hv-letterhead hv-letterhead--footer", children: props.footerComponent }) : null,
222
+ props.mode === "create" && props.signatures.length ? /* @__PURE__ */ jsx("div", { className: "hv-signatures-inline", children: props.signatures.map((s, i) => /* @__PURE__ */ jsxs("div", { className: "hv-sign-inline", children: [
223
+ /* @__PURE__ */ jsx(
573
224
  "img",
574
225
  {
575
226
  src: s.signatureImageUrl,
@@ -577,9 +228,9 @@ var RichTextEditor = forwardRef((props, ref) => {
577
228
  className: "hv-sign-img"
578
229
  }
579
230
  ),
580
- /* @__PURE__ */ jsxs5("div", { children: [
581
- /* @__PURE__ */ jsx5("div", { className: "hv-sign-name", children: s.signedBy }),
582
- /* @__PURE__ */ jsx5("div", { className: "hv-sign-date", children: new Date(s.dateSigned).toLocaleString() })
231
+ /* @__PURE__ */ jsxs("div", { children: [
232
+ /* @__PURE__ */ jsx("div", { className: "hv-sign-name", children: s.signedBy }),
233
+ /* @__PURE__ */ jsx("div", { className: "hv-sign-date", children: new Date(s.dateSigned).toLocaleString() })
583
234
  ] })
584
235
  ] }, i)) }) : null
585
236
  ] }) })
@@ -594,14 +245,103 @@ function escapeHtml(s) {
594
245
  }
595
246
 
596
247
  // src/editors/SpreadsheetEditor.tsx
597
- import { forwardRef as forwardRef2, useEffect as useEffect3, useImperativeHandle as useImperativeHandle2, useMemo as useMemo3, useState as useState3 } from "react";
248
+ import { forwardRef as forwardRef2, useEffect as useEffect2, useImperativeHandle as useImperativeHandle2, useMemo as useMemo2, useState as useState2 } from "react";
598
249
  import * as XLSX from "xlsx";
599
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
250
+
251
+ // src/utils/fileSource.ts
252
+ function guessFileType(name, explicit) {
253
+ if (explicit) {
254
+ return explicit;
255
+ }
256
+ const ext = (name?.split(".").pop() || "").toLowerCase();
257
+ const allowed = [
258
+ "pdf",
259
+ "md",
260
+ "docx",
261
+ "xlsx",
262
+ "pptx",
263
+ "txt",
264
+ "png",
265
+ "jpg",
266
+ "svg"
267
+ ];
268
+ return allowed.includes(ext) ? ext : "txt";
269
+ }
270
+ function arrayBufferToBase64(buf) {
271
+ const bytes = new Uint8Array(buf);
272
+ let binary = "";
273
+ const chunk = 32768;
274
+ for (let i = 0; i < bytes.length; i += chunk) {
275
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
276
+ }
277
+ return btoa(binary);
278
+ }
279
+ async function base64ToArrayBuffer(b64) {
280
+ const bin = atob(b64);
281
+ const len = bin.length;
282
+ const bytes = new Uint8Array(len);
283
+ for (let i = 0; i < len; i++) {
284
+ bytes[i] = bin.charCodeAt(i);
285
+ }
286
+ return bytes.buffer;
287
+ }
288
+ async function resolveSource(args) {
289
+ const fileType = guessFileType(args.fileName, args.fileType);
290
+ const fileName = args.fileName ?? `document.${fileType}`;
291
+ if (args.blob) {
292
+ const ab = await args.blob.arrayBuffer();
293
+ const url = URL.createObjectURL(args.blob);
294
+ return { fileType, fileName, arrayBuffer: ab, url };
295
+ }
296
+ if (args.base64) {
297
+ const ab = await base64ToArrayBuffer(args.base64);
298
+ return { fileType, fileName, arrayBuffer: ab };
299
+ }
300
+ if (!args.fileUrl) {
301
+ throw new Error("No file source provided. Use fileUrl, blob, or base64.");
302
+ }
303
+ const res = await fetch(args.fileUrl);
304
+ if (!res.ok) {
305
+ throw new Error(`Failed to fetch file (${res.status})`);
306
+ }
307
+ const total = Number(res.headers.get("content-length") || "") || void 0;
308
+ if (!res.body) {
309
+ const ab = await res.arrayBuffer();
310
+ args.onProgress?.(ab.byteLength, total);
311
+ return { fileType, fileName, arrayBuffer: ab, url: args.fileUrl };
312
+ }
313
+ const reader = res.body.getReader();
314
+ const chunks = [];
315
+ let loaded = 0;
316
+ while (true) {
317
+ const { done, value } = await reader.read();
318
+ if (done) {
319
+ break;
320
+ }
321
+ if (value) {
322
+ chunks.push(value);
323
+ loaded += value.length;
324
+ args.onProgress?.(loaded, total);
325
+ }
326
+ }
327
+ const out = new Uint8Array(loaded);
328
+ let offset = 0;
329
+ for (const c of chunks) {
330
+ out.set(c, offset);
331
+ offset += c.length;
332
+ }
333
+ return { fileType, fileName, arrayBuffer: out.buffer, url: args.fileUrl };
334
+ }
335
+
336
+ // src/editors/SpreadsheetEditor.tsx
337
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
600
338
  var SpreadsheetEditor = forwardRef2(function SpreadsheetEditor2(props, ref) {
601
339
  const readonly = props.mode === "view";
602
- const [grid, setGrid] = useState3(() => Array.from({ length: 30 }, () => Array.from({ length: 12 }, () => "")));
603
- useEffect3(() => {
604
- if (!props.arrayBuffer) return;
340
+ const [grid, setGrid] = useState2(() => Array.from({ length: 30 }, () => Array.from({ length: 12 }, () => "")));
341
+ useEffect2(() => {
342
+ if (!props.arrayBuffer) {
343
+ return;
344
+ }
605
345
  try {
606
346
  const wb = XLSX.read(props.arrayBuffer, { type: "array" });
607
347
  const name = wb.SheetNames[0];
@@ -629,20 +369,20 @@ var SpreadsheetEditor = forwardRef2(function SpreadsheetEditor2(props, ref) {
629
369
  save,
630
370
  requestThumbnails: async () => void 0
631
371
  }));
632
- const cols = useMemo3(() => Array.from({ length: grid[0]?.length ?? 0 }, (_, i) => String.fromCharCode(65 + i % 26)), [grid]);
633
- return /* @__PURE__ */ jsxs6("div", { className: "hv-sheet", children: [
634
- /* @__PURE__ */ jsxs6("div", { className: "hv-sheetbar", children: [
635
- /* @__PURE__ */ jsx6("div", { className: "hv-sheetbar-title", children: props.fileName }),
636
- !readonly ? /* @__PURE__ */ jsx6("button", { className: "hv-btn", type: "button", onClick: () => void save(false), children: props.locale["toolbar.save"] ?? "Save" }) : null
372
+ const cols = useMemo2(() => Array.from({ length: grid[0]?.length ?? 0 }, (_, i) => String.fromCharCode(65 + i % 26)), [grid]);
373
+ return /* @__PURE__ */ jsxs2("div", { className: "hv-sheet", children: [
374
+ /* @__PURE__ */ jsxs2("div", { className: "hv-sheetbar", children: [
375
+ /* @__PURE__ */ jsx2("div", { className: "hv-sheetbar-title", children: props.fileName }),
376
+ !readonly ? /* @__PURE__ */ jsx2("button", { className: "hv-btn", type: "button", onClick: () => void save(false), children: props.locale["toolbar.save"] ?? "Save" }) : null
637
377
  ] }),
638
- /* @__PURE__ */ jsxs6("div", { className: "hv-sheetgrid", role: "table", "aria-label": "Spreadsheet", children: [
639
- /* @__PURE__ */ jsxs6("div", { className: "hv-sheetrow hv-sheetrow--header", role: "row", children: [
640
- /* @__PURE__ */ jsx6("div", { className: "hv-sheetcell hv-sheetcell--corner", role: "columnheader" }),
641
- cols.map((c, i) => /* @__PURE__ */ jsx6("div", { className: "hv-sheetcell hv-sheetcell--header", role: "columnheader", children: c }, i))
378
+ /* @__PURE__ */ jsxs2("div", { className: "hv-sheetgrid", role: "table", "aria-label": "Spreadsheet", children: [
379
+ /* @__PURE__ */ jsxs2("div", { className: "hv-sheetrow hv-sheetrow--header", role: "row", children: [
380
+ /* @__PURE__ */ jsx2("div", { className: "hv-sheetcell hv-sheetcell--corner", role: "columnheader" }),
381
+ cols.map((c, i) => /* @__PURE__ */ jsx2("div", { className: "hv-sheetcell hv-sheetcell--header", role: "columnheader", children: c }, i))
642
382
  ] }),
643
- grid.map((row, r) => /* @__PURE__ */ jsxs6("div", { className: "hv-sheetrow", role: "row", children: [
644
- /* @__PURE__ */ jsx6("div", { className: "hv-sheetcell hv-sheetcell--header", role: "rowheader", children: r + 1 }),
645
- row.map((val, c) => /* @__PURE__ */ jsx6(
383
+ grid.map((row, r) => /* @__PURE__ */ jsxs2("div", { className: "hv-sheetrow", role: "row", children: [
384
+ /* @__PURE__ */ jsx2("div", { className: "hv-sheetcell hv-sheetcell--header", role: "rowheader", children: r + 1 }),
385
+ row.map((val, c) => /* @__PURE__ */ jsx2(
646
386
  "div",
647
387
  {
648
388
  className: "hv-sheetcell",
@@ -671,29 +411,33 @@ function ensureExt(name, ext) {
671
411
  }
672
412
 
673
413
  // src/renderers/ImageRenderer.tsx
674
- import { useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
675
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
414
+ import { useEffect as useEffect3, useMemo as useMemo3, useState as useState3 } from "react";
415
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
676
416
  function ImageRenderer({
677
417
  arrayBuffer,
678
418
  fileType,
679
419
  fileName
680
420
  }) {
681
- const [zoom, setZoom] = useState4(1);
682
- const url = useMemo4(() => {
683
- if (!arrayBuffer) return void 0;
421
+ const [zoom, setZoom] = useState3(1);
422
+ const url = useMemo3(() => {
423
+ if (!arrayBuffer) {
424
+ return void 0;
425
+ }
684
426
  const mime = fileType === "svg" ? "image/svg+xml" : fileType === "png" ? "image/png" : "image/jpeg";
685
427
  return URL.createObjectURL(new Blob([arrayBuffer], { type: mime }));
686
428
  }, [arrayBuffer, fileType]);
687
- useEffect4(() => {
429
+ useEffect3(() => {
688
430
  return () => {
689
- if (url) URL.revokeObjectURL(url);
431
+ if (url) {
432
+ URL.revokeObjectURL(url);
433
+ }
690
434
  };
691
435
  }, [url]);
692
- return /* @__PURE__ */ jsxs7("div", { className: "hv-doc", children: [
693
- /* @__PURE__ */ jsxs7("div", { className: "hv-mini-toolbar", children: [
694
- /* @__PURE__ */ jsx7("div", { className: "hv-title", children: fileName }),
695
- /* @__PURE__ */ jsx7("div", { className: "hv-spacer" }),
696
- /* @__PURE__ */ jsx7(
436
+ return /* @__PURE__ */ jsxs3("div", { className: "hv-doc", children: [
437
+ /* @__PURE__ */ jsxs3("div", { className: "hv-mini-toolbar", children: [
438
+ /* @__PURE__ */ jsx3("div", { className: "hv-title", children: fileName }),
439
+ /* @__PURE__ */ jsx3("div", { className: "hv-spacer" }),
440
+ /* @__PURE__ */ jsx3(
697
441
  "button",
698
442
  {
699
443
  type: "button",
@@ -702,11 +446,11 @@ function ImageRenderer({
702
446
  children: "-"
703
447
  }
704
448
  ),
705
- /* @__PURE__ */ jsxs7("div", { className: "hv-zoom", children: [
449
+ /* @__PURE__ */ jsxs3("div", { className: "hv-zoom", children: [
706
450
  Math.round(zoom * 100),
707
451
  "%"
708
452
  ] }),
709
- /* @__PURE__ */ jsx7(
453
+ /* @__PURE__ */ jsx3(
710
454
  "button",
711
455
  {
712
456
  type: "button",
@@ -716,10 +460,10 @@ function ImageRenderer({
716
460
  }
717
461
  )
718
462
  ] }),
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(
463
+ /* @__PURE__ */ jsxs3("div", { className: "hv-center", children: [
464
+ !arrayBuffer && /* @__PURE__ */ jsx3("div", { className: "hv-error", children: "No image data provided." }),
465
+ arrayBuffer && !url && /* @__PURE__ */ jsx3("div", { className: "hv-error", children: "Failed to load image." }),
466
+ url && /* @__PURE__ */ jsx3(
723
467
  "img",
724
468
  {
725
469
  src: url,
@@ -732,10 +476,216 @@ function ImageRenderer({
732
476
  ] });
733
477
  }
734
478
 
479
+ // src/renderers/PdfRenderer.tsx
480
+ import {
481
+ getDocument,
482
+ GlobalWorkerOptions
483
+ } from "pdfjs-dist";
484
+ import { useEffect as useEffect4, useMemo as useMemo4, useRef as useRef2, useState as useState4 } from "react";
485
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
486
+ function PdfRenderer(props) {
487
+ const { url, arrayBuffer } = props;
488
+ const [doc, setDoc] = useState4(null);
489
+ const [pageCount, setPageCount] = useState4(0);
490
+ const [rendered, setRendered] = useState4(
491
+ /* @__PURE__ */ new Map()
492
+ );
493
+ const [thumbs, setThumbs] = useState4([]);
494
+ const [size, setSize] = useState4({ w: 840, h: 1188 });
495
+ const [error, setError] = useState4(null);
496
+ const [loading, setLoading] = useState4(false);
497
+ const containerRef = useRef2(null);
498
+ useEffect4(() => {
499
+ try {
500
+ GlobalWorkerOptions.workerSrc = new URL(
501
+ "pdfjs-dist/build/pdf.worker.min.mjs",
502
+ import.meta.url
503
+ ).toString();
504
+ } catch {
505
+ }
506
+ }, []);
507
+ useEffect4(() => {
508
+ let cancel = false;
509
+ setError(null);
510
+ setLoading(true);
511
+ (async () => {
512
+ setDoc(null);
513
+ setRendered(/* @__PURE__ */ new Map());
514
+ setThumbs([]);
515
+ if (!url && !arrayBuffer) {
516
+ setError("No PDF source provided.");
517
+ setLoading(false);
518
+ return;
519
+ }
520
+ try {
521
+ const task = getDocument(
522
+ url ? { url, rangeChunkSize: 512 * 1024 } : { data: arrayBuffer }
523
+ );
524
+ const pdf = await task.promise;
525
+ if (cancel) {
526
+ return;
527
+ }
528
+ setDoc(pdf);
529
+ setPageCount(pdf.numPages);
530
+ props.onPageCount(pdf.numPages);
531
+ const p1 = await pdf.getPage(1);
532
+ const base = p1.getViewport({ scale: 1 });
533
+ const w = Math.min(980, Math.max(640, base.width));
534
+ const s = w / base.width;
535
+ const vp = p1.getViewport({ scale: s });
536
+ setSize({ w: Math.round(vp.width), h: Math.round(vp.height) });
537
+ const thumbWidth = 56;
538
+ const thumbsArr = [];
539
+ for (let i = 1; i <= pdf.numPages; i++) {
540
+ const page = await pdf.getPage(i);
541
+ const pageBase = page.getViewport({ scale: 1 });
542
+ const thumbScale = thumbWidth / pageBase.width;
543
+ const thumbVp = page.getViewport({ scale: thumbScale });
544
+ const thumbCanvas = document.createElement("canvas");
545
+ thumbCanvas.width = Math.round(thumbVp.width);
546
+ thumbCanvas.height = Math.round(thumbVp.height);
547
+ const thumbCtx = thumbCanvas.getContext("2d", { alpha: false });
548
+ if (thumbCtx) {
549
+ await page.render({ canvasContext: thumbCtx, viewport: thumbVp }).promise;
550
+ thumbsArr.push(thumbCanvas.toDataURL("image/png"));
551
+ } else {
552
+ thumbsArr.push(void 0);
553
+ }
554
+ }
555
+ setThumbs(thumbsArr);
556
+ } catch (e) {
557
+ setError(
558
+ "Failed to load PDF. " + (e instanceof Error ? e.message : "")
559
+ );
560
+ } finally {
561
+ setLoading(false);
562
+ }
563
+ })();
564
+ return () => {
565
+ cancel = true;
566
+ };
567
+ }, [url, arrayBuffer]);
568
+ useEffect4(() => {
569
+ props.onThumbs(thumbs);
570
+ }, [thumbs]);
571
+ const pagesToShow = useMemo4(() => {
572
+ if (props.layout === "side-by-side" && pageCount > 1) {
573
+ const left = Math.max(1, Math.min(props.currentPage, pageCount));
574
+ const right = Math.max(1, Math.min(left + 1, pageCount));
575
+ return left === right ? [left] : [left, right];
576
+ }
577
+ return [Math.max(1, Math.min(props.currentPage, pageCount))];
578
+ }, [props.currentPage, props.layout, pageCount]);
579
+ useEffect4(() => {
580
+ if (!doc) {
581
+ return;
582
+ }
583
+ let cancel = false;
584
+ (async () => {
585
+ for (const p of pagesToShow) {
586
+ if (rendered.has(p)) {
587
+ continue;
588
+ }
589
+ try {
590
+ const page = await doc.getPage(p);
591
+ if (cancel) {
592
+ return;
593
+ }
594
+ const base = page.getViewport({ scale: 1 });
595
+ const vp = page.getViewport({ scale: size.w / base.width });
596
+ const canvas = document.createElement("canvas");
597
+ canvas.width = Math.round(vp.width);
598
+ canvas.height = Math.round(vp.height);
599
+ const ctx = canvas.getContext("2d", { alpha: false });
600
+ if (!ctx) {
601
+ continue;
602
+ }
603
+ await page.render({ canvasContext: ctx, viewport: vp }).promise;
604
+ if (cancel) {
605
+ return;
606
+ }
607
+ setRendered((prev) => {
608
+ const next = new Map(prev);
609
+ next.set(p, canvas);
610
+ return next;
611
+ });
612
+ } catch {
613
+ }
614
+ }
615
+ })();
616
+ return () => {
617
+ cancel = true;
618
+ };
619
+ }, [doc, pagesToShow, size.w, rendered]);
620
+ function onWheel(e) {
621
+ if (!pageCount) {
622
+ return;
623
+ }
624
+ if (Math.abs(e.deltaY) < 10) {
625
+ return;
626
+ }
627
+ const dir = e.deltaY > 0 ? 1 : -1;
628
+ const step = props.layout === "side-by-side" ? 2 : 1;
629
+ const next = Math.max(
630
+ 1,
631
+ Math.min(pageCount, props.currentPage + dir * step)
632
+ );
633
+ props.onCurrentPageChange(next);
634
+ }
635
+ function clickPlace(e, page) {
636
+ const stamp = props.signatureStamp;
637
+ if (!stamp?.armed) {
638
+ return;
639
+ }
640
+ const rect = e.currentTarget.getBoundingClientRect();
641
+ const x = (e.clientX - rect.left) / rect.width;
642
+ const y = (e.clientY - rect.top) / rect.height;
643
+ stamp.onPlaced({ page, x, y, w: 0.22, h: 0.08 });
644
+ }
645
+ return /* @__PURE__ */ jsxs4("div", { className: "hv-doc", ref: containerRef, onWheel, children: [
646
+ !doc ? /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Loading PDF\u2026" }) : null,
647
+ doc ? /* @__PURE__ */ jsx4(
648
+ "div",
649
+ {
650
+ className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
651
+ children: pagesToShow.map((p) => {
652
+ const c = rendered.get(p);
653
+ return /* @__PURE__ */ jsx4(
654
+ "div",
655
+ {
656
+ className: "hv-page",
657
+ style: { width: size.w, height: size.h },
658
+ onClick: (e) => clickPlace(e, p),
659
+ children: c ? /* @__PURE__ */ jsx4(
660
+ "canvas",
661
+ {
662
+ className: "hv-canvas",
663
+ width: c.width,
664
+ height: c.height,
665
+ ref: (node) => {
666
+ if (!node) {
667
+ return;
668
+ }
669
+ const ctx = node.getContext("2d");
670
+ if (ctx) {
671
+ ctx.drawImage(c, 0, 0);
672
+ }
673
+ }
674
+ }
675
+ ) : /* @__PURE__ */ jsx4("div", { className: "hv-loading", children: "Rendering\u2026" })
676
+ },
677
+ p
678
+ );
679
+ })
680
+ }
681
+ ) : null
682
+ ] });
683
+ }
684
+
735
685
  // src/renderers/PptxRenderer.tsx
736
686
  import { useEffect as useEffect5, useMemo as useMemo5, useState as useState5 } from "react";
737
687
  import JSZip from "jszip";
738
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
688
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
739
689
  function decodeXml(s) {
740
690
  return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
741
691
  }
@@ -748,43 +698,39 @@ function PptxRenderer(props) {
748
698
  const [error, setError] = useState5(null);
749
699
  const [loading, setLoading] = useState5(false);
750
700
  useEffect5(() => {
751
- let cancelled = false;
701
+ let cancel = false;
752
702
  setError(null);
753
703
  setLoading(true);
754
704
  (async () => {
755
705
  setSlides([]);
756
706
  setThumbs([]);
757
707
  if (!props.arrayBuffer) {
758
- props.onSlideCount(1);
759
- setSlides([{ index: 1, text: "No content" }]);
760
- setError("No PPTX data provided.");
708
+ setError("No PPTX source provided.");
761
709
  setLoading(false);
762
710
  return;
763
711
  }
764
712
  try {
765
713
  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) });
714
+ const slidePaths = Object.keys(zip.files).filter((p) => /^ppt\/slides\/slide\d+\.xml$/.test(p)).sort();
715
+ const slidesOut = [];
716
+ for (let i = 0; i < slidePaths.length; i++) {
717
+ const xml = await zip.files[slidePaths[i]].async("string");
718
+ slidesOut.push({ index: i + 1, text: extractText(xml) });
775
719
  }
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
- )
720
+ if (cancel) return;
721
+ setSlides(
722
+ slidesOut.length ? slidesOut : [{ index: 1, text: "(empty)" }]
785
723
  );
724
+ props.onSlideCount(slidesOut.length || 1);
725
+ const thumbWidth = 56;
726
+ const thumbsArr = [];
727
+ for (let i = 0; i < (slidesOut.length || 1); i++) {
728
+ thumbsArr.push(
729
+ `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgThumb(i + 1))}`
730
+ );
731
+ }
732
+ setThumbs(thumbsArr);
786
733
  } catch (e) {
787
- props.onSlideCount(1);
788
734
  setSlides([
789
735
  { index: 1, text: "Unable to render this .pptx in-browser." }
790
736
  ]);
@@ -797,42 +743,43 @@ function PptxRenderer(props) {
797
743
  }
798
744
  })();
799
745
  return () => {
800
- cancelled = true;
746
+ cancel = true;
801
747
  };
802
748
  }, [props.arrayBuffer]);
803
749
  useEffect5(() => {
804
750
  props.onThumbs(thumbs);
805
751
  }, [thumbs]);
806
752
  const pagesToShow = useMemo5(() => {
807
- if (props.layout === "side-by-side")
808
- return [
809
- props.currentPage,
810
- Math.min(slides.length || props.currentPage + 1, props.currentPage + 1)
811
- ];
812
- return [props.currentPage];
753
+ const total = slides.length;
754
+ if (props.layout === "side-by-side" && total > 1) {
755
+ const left = Math.max(1, Math.min(props.currentPage, total));
756
+ const right = Math.max(1, Math.min(left + 1, total));
757
+ return left === right ? [left] : [left, right];
758
+ }
759
+ return [Math.max(1, Math.min(props.currentPage, total))];
813
760
  }, [props.currentPage, props.layout, slides.length]);
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(
761
+ return /* @__PURE__ */ jsxs5("div", { className: "hv-doc", children: [
762
+ loading && /* @__PURE__ */ jsx5("div", { className: "hv-loading", children: "Loading PPTX\u2026" }),
763
+ error && /* @__PURE__ */ jsx5("div", { className: "hv-error", children: error }),
764
+ !loading && !error && (!slides || slides.length === 0) && /* @__PURE__ */ jsx5("div", { className: "hv-error", children: "No slides to display." }),
765
+ !error && slides && slides.length > 0 && /* @__PURE__ */ jsx5(
819
766
  "div",
820
767
  {
821
768
  className: props.layout === "side-by-side" ? "hv-pages hv-pages--two" : "hv-pages",
822
769
  children: pagesToShow.map((p) => {
823
770
  const s = slides[p - 1];
824
- return /* @__PURE__ */ jsxs8(
771
+ return /* @__PURE__ */ jsxs5(
825
772
  "div",
826
773
  {
827
774
  className: "hv-slide",
828
775
  tabIndex: 0,
829
776
  onFocus: () => props.onCurrentPageChange(p),
830
777
  children: [
831
- /* @__PURE__ */ jsxs8("div", { className: "hv-slide-title", children: [
778
+ /* @__PURE__ */ jsxs5("div", { className: "hv-slide-title", children: [
832
779
  "Slide ",
833
780
  p
834
781
  ] }),
835
- /* @__PURE__ */ jsx8("div", { className: "hv-slide-text", children: s?.text || "" })
782
+ /* @__PURE__ */ jsx5("div", { className: "hv-slide-text", children: s?.text || "" })
836
783
  ]
837
784
  },
838
785
  p
@@ -846,6 +793,233 @@ function svgThumb(n) {
846
793
  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>`;
847
794
  }
848
795
 
796
+ // src/utils/locale.ts
797
+ var defaultLocale = {
798
+ "loading": "Loading\u2026",
799
+ "error.title": "Error",
800
+ "toolbar.layout.single": "Single page",
801
+ "toolbar.layout.two": "Side-by-side",
802
+ "toolbar.thumbs": "Thumbnails",
803
+ "toolbar.signatures": "Signatures",
804
+ "toolbar.sign": "Sign Document",
805
+ "toolbar.save": "Save",
806
+ "toolbar.exportPdf": "Export as PDF",
807
+ "thumbnails.title": "Thumbnails",
808
+ "thumbnails.page": "Page",
809
+ "signatures.title": "Signatures",
810
+ "signatures.empty": "No signatures",
811
+ "signatures.placeHint": "Click on the document to place the signature.",
812
+ "a11y.viewer": "Document viewer",
813
+ "a11y.ribbon": "Ribbon",
814
+ "a11y.editor": "Document editor"
815
+ };
816
+
817
+ // src/components/SignaturePanel.tsx
818
+ import React6 from "react";
819
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
820
+ function SignaturePanel(props) {
821
+ const title = props.locale["signatures.title"] ?? "Signatures";
822
+ const deduped = React6.useMemo(() => {
823
+ const seen = /* @__PURE__ */ new Set();
824
+ return props.signatures.filter((s) => {
825
+ const key = `${s.signedBy}|${s.dateSigned}|${s.signatureImageUrl}`;
826
+ if (seen.has(key)) return false;
827
+ seen.add(key);
828
+ return true;
829
+ });
830
+ }, [props.signatures]);
831
+ return /* @__PURE__ */ jsxs6(
832
+ "aside",
833
+ {
834
+ className: props.collapsed ? "hv-side hv-side--collapsed" : "hv-side",
835
+ "aria-label": title,
836
+ children: [
837
+ /* @__PURE__ */ jsxs6("div", { className: "hv-sidebar-header", children: [
838
+ /* @__PURE__ */ jsx6(
839
+ "button",
840
+ {
841
+ type: "button",
842
+ className: "hv-icon",
843
+ onClick: props.onToggle,
844
+ "aria-label": props.locale["toolbar.signatures"] ?? "Signatures",
845
+ children: /* @__PURE__ */ jsx6("span", { "aria-hidden": true, children: "\u270D" })
846
+ }
847
+ ),
848
+ /* @__PURE__ */ jsx6("div", { className: "hv-sidebar-title", children: title })
849
+ ] }),
850
+ /* @__PURE__ */ jsxs6("div", { className: "hv-sidebar-body", children: [
851
+ deduped.length === 0 && /* @__PURE__ */ jsx6("div", { className: "hv-signature-empty", "aria-live": "polite", children: props.locale["signatures.empty"] ?? "No signatures yet." }),
852
+ deduped.map((s, idx) => /* @__PURE__ */ jsxs6(
853
+ "div",
854
+ {
855
+ className: "hv-signature-card",
856
+ tabIndex: 0,
857
+ "aria-label": `Signature by ${s.signedBy}`,
858
+ children: [
859
+ /* @__PURE__ */ jsx6(
860
+ "img",
861
+ {
862
+ src: s.signatureImageUrl,
863
+ alt: props.locale["signatures.imgAlt"] ? props.locale["signatures.imgAlt"].replace(
864
+ "{name}",
865
+ s.signedBy
866
+ ) : `Signature by ${s.signedBy}`,
867
+ className: "hv-signature-img"
868
+ }
869
+ ),
870
+ /* @__PURE__ */ jsxs6("div", { className: "hv-signature-meta", children: [
871
+ /* @__PURE__ */ jsx6("div", { className: "hv-signature-name", children: s.signedBy }),
872
+ /* @__PURE__ */ jsx6("div", { className: "hv-signature-date", children: new Date(s.dateSigned).toLocaleString() }),
873
+ s.comment ? /* @__PURE__ */ jsx6("div", { className: "hv-signature-comment", children: s.comment }) : null
874
+ ] })
875
+ ]
876
+ },
877
+ `${s.signedBy}-${s.dateSigned}-${s.signatureImageUrl}`
878
+ ))
879
+ ] })
880
+ ]
881
+ }
882
+ );
883
+ }
884
+
885
+ // src/components/ThumbnailsSidebar.tsx
886
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
887
+ function ThumbnailsSidebar(props) {
888
+ const t = props.locale["thumbnails.title"] ?? "Thumbnails";
889
+ return /* @__PURE__ */ jsxs7(
890
+ "aside",
891
+ {
892
+ className: props.collapsed ? "hv-thumbs hv-thumbs--collapsed" : "hv-thumbs",
893
+ "aria-label": t,
894
+ children: [
895
+ /* @__PURE__ */ jsxs7("div", { className: "hv-thumbs-header", children: [
896
+ /* @__PURE__ */ jsx7(
897
+ "button",
898
+ {
899
+ type: "button",
900
+ className: "hv-thumbs-toggle",
901
+ onClick: props.onToggle,
902
+ "aria-label": props.collapsed ? props.locale["thumbnails.open"] ?? "Open thumbnails" : props.locale["thumbnails.close"] ?? "Close thumbnails",
903
+ children: /* @__PURE__ */ jsx7("span", { className: "hv-thumbs-toggle-icon", children: props.collapsed ? "\u25B8" : "\u25BE" })
904
+ }
905
+ ),
906
+ !props.collapsed && /* @__PURE__ */ jsx7("div", { className: "hv-thumbs-title", children: t })
907
+ ] }),
908
+ !props.collapsed && /* @__PURE__ */ jsx7("div", { className: "hv-thumbs-list", role: "list", children: props.thumbnails.map((th, idx) => {
909
+ const p = idx + 1;
910
+ const active = p === props.currentPage;
911
+ return /* @__PURE__ */ jsxs7(
912
+ "button",
913
+ {
914
+ type: "button",
915
+ role: "listitem",
916
+ className: active ? "hv-thumb hv-thumb--active" : "hv-thumb",
917
+ onClick: () => props.onSelectPage(p),
918
+ "aria-current": active ? "page" : void 0,
919
+ tabIndex: 0,
920
+ children: [
921
+ /* @__PURE__ */ jsx7("div", { className: "hv-thumb-img", "aria-hidden": true, children: th.dataUrl ? /* @__PURE__ */ jsx7("img", { src: th.dataUrl, alt: "" }) : /* @__PURE__ */ jsx7("div", { className: "hv-thumb-placeholder" }) }),
922
+ /* @__PURE__ */ jsx7("div", { className: "hv-thumb-label", children: th.label })
923
+ ]
924
+ },
925
+ th.id
926
+ );
927
+ }) })
928
+ ]
929
+ }
930
+ );
931
+ }
932
+
933
+ // src/components/Toolbar.tsx
934
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
935
+ function Toolbar(props) {
936
+ const t = (k, fallback) => props.locale[k] ?? fallback;
937
+ return /* @__PURE__ */ jsxs8(
938
+ "div",
939
+ {
940
+ className: "hv-toolbar",
941
+ role: "toolbar",
942
+ "aria-label": t("a11y.toolbar", "Document toolbar"),
943
+ children: [
944
+ /* @__PURE__ */ jsxs8("div", { className: "hv-toolbar__left gap-2", children: [
945
+ /* @__PURE__ */ jsx8(
946
+ "button",
947
+ {
948
+ type: "button",
949
+ className: "hv-btn",
950
+ onClick: props.onToggleThumbnails,
951
+ "aria-pressed": props.showThumbnails,
952
+ children: t("toolbar.thumbs", "Thumbnails")
953
+ }
954
+ ),
955
+ props.mode !== "create" && /* @__PURE__ */ jsx8(
956
+ "button",
957
+ {
958
+ type: "button",
959
+ className: "hv-btn",
960
+ onClick: props.onToggleSignatures,
961
+ "aria-pressed": props.showSignatures,
962
+ children: t("toolbar.signatures", "Signatures")
963
+ }
964
+ ),
965
+ /* @__PURE__ */ jsx8("span", { className: "hv-sep" }),
966
+ /* @__PURE__ */ jsx8(
967
+ "button",
968
+ {
969
+ type: "button",
970
+ className: props.layout === "single" ? "hv-btn hv-btn--active" : "hv-btn",
971
+ onClick: () => props.onChangeLayout("single"),
972
+ children: t("toolbar.layout.single", "Single")
973
+ }
974
+ ),
975
+ /* @__PURE__ */ jsx8(
976
+ "button",
977
+ {
978
+ type: "button",
979
+ className: props.layout === "side-by-side" ? "hv-btn hv-btn--active" : "hv-btn",
980
+ onClick: () => props.onChangeLayout("side-by-side"),
981
+ children: t("toolbar.layout.two", "Two")
982
+ }
983
+ )
984
+ ] }),
985
+ /* @__PURE__ */ jsxs8("div", { className: "hv-toolbar__right", children: [
986
+ props.showHeaderFooterToggle && /* @__PURE__ */ jsxs8("label", { className: "hv-toggle", children: [
987
+ /* @__PURE__ */ jsx8(
988
+ "input",
989
+ {
990
+ type: "checkbox",
991
+ checked: props.headerFooterEnabled,
992
+ onChange: props.onToggleHeaderFooter
993
+ }
994
+ ),
995
+ /* @__PURE__ */ jsx8("span", { children: t("toolbar.letterhead", "Letterhead") })
996
+ ] }),
997
+ props.allowSigning && /* @__PURE__ */ jsx8(
998
+ "button",
999
+ {
1000
+ type: "button",
1001
+ className: "hv-btn hv-btn--primary",
1002
+ onClick: props.onSign,
1003
+ disabled: props.signingDisabled,
1004
+ children: t("toolbar.sign", "Sign Document")
1005
+ }
1006
+ ),
1007
+ props.canExportPdf && /* @__PURE__ */ jsx8("button", { type: "button", className: "hv-btn", onClick: props.onExportPdf, children: t("toolbar.exportPdf", "Export as PDF") }),
1008
+ props.canSave && /* @__PURE__ */ jsx8(
1009
+ "button",
1010
+ {
1011
+ type: "button",
1012
+ className: "hv-btn hv-btn--primary",
1013
+ onClick: props.onSave,
1014
+ children: t("toolbar.save", "Save")
1015
+ }
1016
+ )
1017
+ ] })
1018
+ ]
1019
+ }
1020
+ );
1021
+ }
1022
+
849
1023
  // src/components/DocumentViewer.tsx
850
1024
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
851
1025
  function DocumentViewer(props) {
@@ -891,10 +1065,14 @@ function DocumentViewer(props) {
891
1065
  fileName: props.fileName,
892
1066
  fileType: props.fileType
893
1067
  });
894
- if (cancelled) return;
1068
+ if (cancelled) {
1069
+ return;
1070
+ }
895
1071
  setResolved({ fileType: res.fileType, fileName: res.fileName, url: res.url, arrayBuffer: res.arrayBuffer });
896
1072
  } catch (e) {
897
- if (cancelled) return;
1073
+ if (cancelled) {
1074
+ return;
1075
+ }
898
1076
  setError(e instanceof Error ? e.message : String(e));
899
1077
  }
900
1078
  })();
@@ -911,7 +1089,9 @@ function DocumentViewer(props) {
911
1089
  }));
912
1090
  }, [pageCount, thumbs, locale]);
913
1091
  async function handleSignRequest() {
914
- if (!allowSigning || signingBusy || !props.onSignRequest) return;
1092
+ if (!allowSigning || signingBusy || !props.onSignRequest) {
1093
+ return;
1094
+ }
915
1095
  setSigningBusy(true);
916
1096
  try {
917
1097
  const sig = await props.onSignRequest();
@@ -922,7 +1102,9 @@ function DocumentViewer(props) {
922
1102
  }
923
1103
  }
924
1104
  function placeSignature(p) {
925
- if (!armedSignatureUrl) return;
1105
+ if (!armedSignatureUrl) {
1106
+ return;
1107
+ }
926
1108
  setSigPlacements((prev) => [...prev, { ...p, signatureImageUrl: armedSignatureUrl }]);
927
1109
  setArmedSignatureUrl(null);
928
1110
  }
@@ -931,7 +1113,9 @@ function DocumentViewer(props) {
931
1113
  await editorRef.current.save(!!exportPdf);
932
1114
  return;
933
1115
  }
934
- if (!resolved?.arrayBuffer) return;
1116
+ if (!resolved?.arrayBuffer) {
1117
+ return;
1118
+ }
935
1119
  const b64 = arrayBufferToBase642(resolved.arrayBuffer);
936
1120
  props.onSave?.(b64, { fileName: resolved.fileName, fileType: resolved.fileType, annotations: { sigPlacements } });
937
1121
  }
@@ -966,7 +1150,7 @@ function DocumentViewer(props) {
966
1150
  /* @__PURE__ */ jsx9("div", { className: "hv-error-title", children: locale["error.title"] ?? "Error" }),
967
1151
  /* @__PURE__ */ jsx9("div", { className: "hv-error-body", children: error })
968
1152
  ] }) : null,
969
- !resolved && !error ? /* @__PURE__ */ jsx9("div", { className: "hv-loading", "aria-busy": "true", children: locale["loading"] ?? "Loading\u2026" }) : null,
1153
+ !resolved && !error ? /* @__PURE__ */ jsx9("div", { className: "hv-loading", "aria-busy": "true", children: locale.loading ?? "Loading\u2026" }) : null,
970
1154
  resolved ? /* @__PURE__ */ jsxs9("div", { className: "hv-shell", children: [
971
1155
  mode !== "create" ? /* @__PURE__ */ jsx9(
972
1156
  ThumbnailsSidebar,