@zerohive/hive-viewer 0.2.2 → 0.2.3

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