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