@embedpdf/viewer-chrome 3.0.0-next.12 → 3.0.0-next.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/chrome.cjs +5 -0
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.js +5 -0
- package/dist/chrome.js.map +1 -1
- package/dist/commands.cjs +14 -1
- package/dist/commands.cjs.map +1 -1
- package/dist/commands.js +14 -1
- package/dist/commands.js.map +1 -1
- package/dist/{es-BRYSk4rY.cjs → es-DYwzfxyt.cjs} +70 -3
- package/dist/es-DYwzfxyt.cjs.map +1 -0
- package/dist/{es-DT5ZoB99.js → es-sN6uqBgD.js} +70 -3
- package/dist/es-sN6uqBgD.js.map +1 -0
- package/dist/index.cjs +1441 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +1403 -140
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/es-BRYSk4rY.cjs.map +0 -1
- package/dist/es-DT5ZoB99.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -8,20 +8,23 @@ import { pageEditPlugin } from "@embedpdf/react/page-edit";
|
|
|
8
8
|
import { InteractionToken, InteractionToken as InteractionToken$1, feedbackPlugin, interactionPlugin, useTool, useToolCursor, vibrationFeedback } from "@embedpdf/react/interaction";
|
|
9
9
|
import { SelectionClipboard, SelectionHandles, SelectionLayer, SelectionMenu, SelectionToken, selectionPlugin } from "@embedpdf/react/selection";
|
|
10
10
|
import { AnnotationLayer, AnnotationToken, AnnotationToken as AnnotationToken$1, annotationPlugin, refKey, useAnnotation, useAnnotationDefaults, useAnnotationSelected, useAnnotationSelection, useCommentThreads, useComments, useCommentsHydration, useFilePickerProvider, useSelectionFlags, useSelectionProps } from "@embedpdf/react/annotation";
|
|
11
|
-
import { StampToken, indexedDbByteStore, persistStampLibraries, restoreStampLibraries, stampPlugin, useArmStampAsset, useStamp, useStampAssetPreviewUrl, useStampAssets, useStampLibraries } from "@embedpdf/react/stamp";
|
|
11
|
+
import { StampToken, StampToken as StampToken$1, indexedDbByteStore, persistStampLibraries, restoreStampLibraries, stampPlugin, useArmStampAsset, useStamp, useStampAssetPreviewUrl, useStampAssets, useStampLibraries } from "@embedpdf/react/stamp";
|
|
12
12
|
import { RedactionToken, RedactionToken as RedactionToken$1, redactionPlugin, usePendingRedactions, useRedaction } from "@embedpdf/react/redaction";
|
|
13
13
|
import { actionsPlugin, useActionsUiAdapter } from "@embedpdf/react/actions";
|
|
14
|
-
import { FormToken, formPlugin, formWidgetRenderer } from "@embedpdf/react/form";
|
|
14
|
+
import { FormToken, FormToken as FormToken$1, formPlugin, formWidgetRenderer } from "@embedpdf/react/form";
|
|
15
|
+
import { INITIALS_MARK_NAME, SIGNATURES_LIBRARY_KIND, SIGNATURE_MARK_NAME, SignatureToken, createTestSigner, indexedDbKeyStore, memoryKeyStore, personalSigner, remoteSigner, signaturePlugin, useSignature, useSignatureEvent, useSignatureSnapshot, useSignatureTarget, useSignatureVerdicts, useSignerRows, webCryptoSigner } from "@embedpdf/react/signature";
|
|
15
16
|
import { LinkLayer, linkPlugin } from "@embedpdf/react/link";
|
|
16
17
|
import { SearchLayer, SearchToken, SearchToken as SearchToken$1, searchPlugin, useSearch, useSearchState, validateSearchQuery } from "@embedpdf/react/search";
|
|
17
18
|
import { I18nToken, i18nPlugin, negotiateLocale, useLocale, useT } from "@embedpdf/react/i18n";
|
|
18
19
|
import { CommandsToken, CommandsToken as CommandsToken$1, commandsPlugin, resolvedCommandsEqual, useCommand, useCommandShortcuts, useCommands } from "@embedpdf/react/commands";
|
|
19
|
-
import { ShellToken, ShellToken as ShellToken$1, shellPlugin, useMenus, useSurface } from "@embedpdf/react/shell";
|
|
20
|
+
import { ShellToken, ShellToken as ShellToken$1, shellPlugin, useMenus, useShell, useSurface } from "@embedpdf/react/shell";
|
|
20
21
|
import { Toolbar, addItem, chromeHelpers, chromeHelpers as chromeHelpers$1, custom, defineChrome, group, item, removeItems, replaceItem, useStripView, validateChrome, validateChrome as validateChrome$1 } from "@embedpdf/react/toolbar";
|
|
21
22
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
22
23
|
import { Scrollbar } from "@embedpdf/react/scrollbar";
|
|
23
|
-
import { AnnotationMenu } from "@embedpdf/react/annotation-menu";
|
|
24
24
|
import { LOCALES, loadDefaultLibrary } from "@embedpdf/default-stamps/library";
|
|
25
|
+
import { mountWebFont } from "@embedpdf/web";
|
|
26
|
+
import { AnnotationMenu } from "@embedpdf/react/annotation-menu";
|
|
27
|
+
import { Anchored } from "@embedpdf/react/anchored";
|
|
25
28
|
import { MetadataToken } from "@embedpdf/react/metadata";
|
|
26
29
|
//#region src/config/stage.ts
|
|
27
30
|
/**
|
|
@@ -173,7 +176,8 @@ const en = {
|
|
|
173
176
|
checkbox: "Checkbox",
|
|
174
177
|
radio: "Radio Button",
|
|
175
178
|
select: "Dropdown",
|
|
176
|
-
listbox: "List Box"
|
|
179
|
+
listbox: "List Box",
|
|
180
|
+
signature: "Signature Field"
|
|
177
181
|
},
|
|
178
182
|
redact: {
|
|
179
183
|
mark: "Mark for Redaction",
|
|
@@ -270,6 +274,7 @@ const en = {
|
|
|
270
274
|
redactConfirmCollateral: "{count} other annotations will also be removed.",
|
|
271
275
|
redactApply: "Apply",
|
|
272
276
|
redactApplying: "Applying…",
|
|
277
|
+
formatLabel: "Format",
|
|
273
278
|
redactLabelTitle: "Redaction label",
|
|
274
279
|
redactLabelPlaceholder: "e.g. REDACTED",
|
|
275
280
|
redactLabelRepeat: "Repeat label across region",
|
|
@@ -288,7 +293,72 @@ const en = {
|
|
|
288
293
|
stampsCustomLabel: "Custom stamp",
|
|
289
294
|
stampsError: "The built-in stamps could not be loaded.",
|
|
290
295
|
stampsArmError: "That stamp could not be armed.",
|
|
291
|
-
stampsImportError: "That PDF could not be imported as a stamp library."
|
|
296
|
+
stampsImportError: "That PDF could not be imported as a stamp library.",
|
|
297
|
+
quickStampsLabel: "Quick stamps",
|
|
298
|
+
signaturesTitle: "Signatures",
|
|
299
|
+
signaturesEmpty: "No signatures yet. Create one to sign with.",
|
|
300
|
+
signaturesNew: "New signature…",
|
|
301
|
+
signaturesArmedHint: "Click a signature field to sign it, or anywhere on a page to place the mark.",
|
|
302
|
+
signaturesTargetHint: "Pick a mark for the selected field.",
|
|
303
|
+
signaturesInDocument: "In this document",
|
|
304
|
+
signaturesNone: "This document has no signature fields.",
|
|
305
|
+
signaturesValidate: "Validate",
|
|
306
|
+
signaturesValidating: "Validating…",
|
|
307
|
+
signaturesSigned: "Signed",
|
|
308
|
+
signaturesUnsigned: "Unsigned",
|
|
309
|
+
signaturesRename: "Rename",
|
|
310
|
+
signaturesRemove: "Remove",
|
|
311
|
+
signaturesExport: "Export as PDF",
|
|
312
|
+
signaturesAddInitials: "Add initials",
|
|
313
|
+
signatureLabel: "Signature",
|
|
314
|
+
initialsLabel: "Initials",
|
|
315
|
+
makerTitle: "New signature",
|
|
316
|
+
makerName: "Name",
|
|
317
|
+
makerNamePlaceholder: "Your name",
|
|
318
|
+
makerDraw: "Draw",
|
|
319
|
+
makerType: "Type",
|
|
320
|
+
makerUpload: "Upload",
|
|
321
|
+
makerTypePlaceholder: "Type your name",
|
|
322
|
+
makerFont: "Font",
|
|
323
|
+
makerUploadHint: "Choose a PNG, JPEG, or one-page PDF",
|
|
324
|
+
makerClear: "Clear",
|
|
325
|
+
makerOptional: "(optional)",
|
|
326
|
+
makerSave: "Save",
|
|
327
|
+
makerSaving: "Saving…",
|
|
328
|
+
makerError: "The signature could not be saved.",
|
|
329
|
+
signTitle: "Sign this field",
|
|
330
|
+
signReason: "Reason",
|
|
331
|
+
signLocation: "Location",
|
|
332
|
+
signKind: "What may change afterwards",
|
|
333
|
+
signKindApproval: "Approval signature — form filling and more signatures stay valid",
|
|
334
|
+
signKindCertifyAnnotate: "Certify — allow comments, form filling and signatures",
|
|
335
|
+
signKindCertifyFill: "Certify — allow form filling and signatures only",
|
|
336
|
+
signKindCertifyLocked: "Certify — allow no changes",
|
|
337
|
+
signButton: "Sign",
|
|
338
|
+
signSigning: "Signing…",
|
|
339
|
+
signFill: "Place without signing",
|
|
340
|
+
signError: "The field could not be signed.",
|
|
341
|
+
inspectTitle: "Signature",
|
|
342
|
+
inspectSigner: "Signed by",
|
|
343
|
+
inspectReason: "Reason",
|
|
344
|
+
inspectLocation: "Location",
|
|
345
|
+
inspectTime: "Time",
|
|
346
|
+
inspectIntegrity: "Document",
|
|
347
|
+
inspectCryptography: "Signature",
|
|
348
|
+
inspectTrust: "Signer",
|
|
349
|
+
inspectChanges: "Changes since",
|
|
350
|
+
inspectDownload: "Download signed version",
|
|
351
|
+
verdictValid: "Valid",
|
|
352
|
+
verdictValidUntrusted: "Valid, signer not trusted",
|
|
353
|
+
verdictInvalid: "Invalid — modified after signing",
|
|
354
|
+
verdictWillInvalidate: "Valid, invalid once saved",
|
|
355
|
+
signaturesInvalidating: "This change will invalidate {field} when the document is saved. Undo it to keep the signature.",
|
|
356
|
+
verdictIndeterminate: "Could not be verified",
|
|
357
|
+
verdictUnchanged: "None",
|
|
358
|
+
verdictRestored: "Changed, then restored",
|
|
359
|
+
inspectLaterRevisions: "Revisions after signing",
|
|
360
|
+
verdictPermitted: "Permitted",
|
|
361
|
+
verdictForbidden: "Not permitted"
|
|
292
362
|
}
|
|
293
363
|
}
|
|
294
364
|
};
|
|
@@ -306,7 +376,9 @@ const en = {
|
|
|
306
376
|
const ViewerConfigContext = createContext({
|
|
307
377
|
chrome: defaultChrome,
|
|
308
378
|
icons: {},
|
|
309
|
-
stamps: {}
|
|
379
|
+
stamps: {},
|
|
380
|
+
signatures: {},
|
|
381
|
+
annotations: {}
|
|
310
382
|
});
|
|
311
383
|
const ViewerConfigProvider = ViewerConfigContext.Provider;
|
|
312
384
|
function useChromeSchema() {
|
|
@@ -324,6 +396,12 @@ function useCustomIcons() {
|
|
|
324
396
|
function useStampsConfig() {
|
|
325
397
|
return useContext(ViewerConfigContext).stamps;
|
|
326
398
|
}
|
|
399
|
+
function useSignaturesConfig() {
|
|
400
|
+
return useContext(ViewerConfigContext).signatures;
|
|
401
|
+
}
|
|
402
|
+
function useAnnotationsConfig() {
|
|
403
|
+
return useContext(ViewerConfigContext).annotations;
|
|
404
|
+
}
|
|
327
405
|
//#endregion
|
|
328
406
|
//#region src/handle.ts
|
|
329
407
|
function createViewerHandle(kernel) {
|
|
@@ -929,6 +1007,166 @@ function ThemeProvider({ children, preference = "system", target }) {
|
|
|
929
1007
|
});
|
|
930
1008
|
}
|
|
931
1009
|
//#endregion
|
|
1010
|
+
//#region src/config/default-stamps.ts
|
|
1011
|
+
/**
|
|
1012
|
+
* The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
|
|
1013
|
+
* standard rubber stamps as one Acrobat-compatible PDF per locale.
|
|
1014
|
+
*
|
|
1015
|
+
* Delivery: the library ships INSIDE whatever ships this viewer. The package
|
|
1016
|
+
* exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
|
|
1017
|
+
* so it travels through the module graph like any other code — every bundler
|
|
1018
|
+
* splits it into a chunk served from the app's own origin, the CDN snippet
|
|
1019
|
+
* carries it as a sibling chunk in its folder, and nothing is fetched from a
|
|
1020
|
+
* third party. `stamps.defaultLibrary` overrides that:
|
|
1021
|
+
* - `false` → no built-in library, no request (air-gapped);
|
|
1022
|
+
* - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
|
|
1023
|
+
*
|
|
1024
|
+
* Loading is LAZY (first open of the stamps panel, never at boot) and
|
|
1025
|
+
* locale-aware: the chrome locale leads, the browser languages break ties
|
|
1026
|
+
* for the locales the chrome does not translate itself, and a locale change
|
|
1027
|
+
* while a default library is loaded swaps it for the new one. The file names
|
|
1028
|
+
* itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
|
|
1029
|
+
*/
|
|
1030
|
+
/** The built-in library's PieceInfo id — excluded from persistence (it is
|
|
1031
|
+
* loaded again on every first open, in the locale of that moment). */
|
|
1032
|
+
const DEFAULT_LIBRARY_ID = "embedpdf-standard";
|
|
1033
|
+
/** Which shipped locale to show for the chrome's locale + the browser's. */
|
|
1034
|
+
function resolveStampsLocale(chromeLocale) {
|
|
1035
|
+
return negotiateLocale(LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
|
|
1036
|
+
}
|
|
1037
|
+
const fetchBytes = async (url) => {
|
|
1038
|
+
const response = await fetch(url);
|
|
1039
|
+
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
1040
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1041
|
+
};
|
|
1042
|
+
/** The library bytes for `locale`: your URL when you gave one, otherwise the
|
|
1043
|
+
* copy that shipped with the viewer. Never anything else. */
|
|
1044
|
+
const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : loadDefaultLibrary(locale);
|
|
1045
|
+
/** Per workspace: the locale the default library was last brought to, and
|
|
1046
|
+
* the in-flight work — the panel mounts and unmounts with the sidebar, so
|
|
1047
|
+
* this cannot live in component state. */
|
|
1048
|
+
const loads = /* @__PURE__ */ new WeakMap();
|
|
1049
|
+
/**
|
|
1050
|
+
* Bring the built-in library to `locale`. Idempotent per workspace and
|
|
1051
|
+
* locale; a locale change swaps the loaded default (remove, then import);
|
|
1052
|
+
* a default the user removed in this session stays removed — the locale is
|
|
1053
|
+
* recorded, nothing is re-added. An embedder that seeded a library under
|
|
1054
|
+
* the same id is left alone.
|
|
1055
|
+
*/
|
|
1056
|
+
function ensureDefaultLibrary(stamp, locale, source) {
|
|
1057
|
+
if (source === false) return Promise.resolve();
|
|
1058
|
+
const current = loads.get(stamp);
|
|
1059
|
+
if (current?.locale === locale) return current.promise;
|
|
1060
|
+
const run = async () => {
|
|
1061
|
+
await current?.promise.catch(() => void 0);
|
|
1062
|
+
const loaded = stamp.library(DEFAULT_LIBRARY_ID);
|
|
1063
|
+
if (loaded?.locale === locale) return;
|
|
1064
|
+
if (current && !loaded) return;
|
|
1065
|
+
if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
|
|
1066
|
+
await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
|
|
1067
|
+
};
|
|
1068
|
+
const promise = run();
|
|
1069
|
+
loads.set(stamp, {
|
|
1070
|
+
locale,
|
|
1071
|
+
promise
|
|
1072
|
+
});
|
|
1073
|
+
return promise;
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/ui/stamp-store.tsx
|
|
1077
|
+
/**
|
|
1078
|
+
* The user's stamp libraries live in the browser (IndexedDB): restored ONCE
|
|
1079
|
+
* per workspace, written on every change, for as long as the viewer lives —
|
|
1080
|
+
* a signature made while the stamps sidebar is closed is persisted exactly
|
|
1081
|
+
* like a stamp imported while it is open. The built-in library is never
|
|
1082
|
+
* stored (it is fetched again next time, in the locale of that moment); the
|
|
1083
|
+
* panels that need it bring it in lazily through `ensureDefaultLibrary`.
|
|
1084
|
+
*
|
|
1085
|
+
* Mounted once in the Shell, workspace-scoped (the stamp plugin is).
|
|
1086
|
+
*/
|
|
1087
|
+
const store = typeof indexedDB === "undefined" ? null : indexedDbByteStore("embedpdf-stamps", { storeName: "libraries" });
|
|
1088
|
+
const restored = /* @__PURE__ */ new WeakMap();
|
|
1089
|
+
/** Bring the stored libraries back — once per workspace, whoever asks first. */
|
|
1090
|
+
const restoreStampLibrariesOnce = (stamp) => {
|
|
1091
|
+
let pending = restored.get(stamp);
|
|
1092
|
+
if (!pending) {
|
|
1093
|
+
pending = store ? restoreStampLibraries(stamp, store) : Promise.resolve();
|
|
1094
|
+
restored.set(stamp, pending);
|
|
1095
|
+
}
|
|
1096
|
+
return pending;
|
|
1097
|
+
};
|
|
1098
|
+
function StampLibraryStore() {
|
|
1099
|
+
const stamp = useStamp();
|
|
1100
|
+
useEffect(() => {
|
|
1101
|
+
restoreStampLibrariesOnce(stamp);
|
|
1102
|
+
return store ? persistStampLibraries(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0;
|
|
1103
|
+
}, [stamp]);
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/ui/quick-stamps.tsx
|
|
1108
|
+
/**
|
|
1109
|
+
* Quick marks in the toolbar: a row of stamp thumbnails that ARM on click —
|
|
1110
|
+
* the checkmark, the cross, whatever the embedder configured
|
|
1111
|
+
* (`stamps.toolbar`: asset ids, or one library's assets). The hover ghost and
|
|
1112
|
+
* the click placement are the annotation plugin's, exactly as from the
|
|
1113
|
+
* sidebar; only the picker moved. Renders nothing until the assets exist.
|
|
1114
|
+
* Lives in the workspace toolbar, so it reads the ACTIVE document's tool.
|
|
1115
|
+
*/
|
|
1116
|
+
function QuickStamps() {
|
|
1117
|
+
const t = useT();
|
|
1118
|
+
const stamp = useStamp();
|
|
1119
|
+
const config = useStampsConfig();
|
|
1120
|
+
const { locale } = useLocale();
|
|
1121
|
+
const assets = useStampAssets();
|
|
1122
|
+
const documentId = useDocumentId();
|
|
1123
|
+
const { armAsset } = useArmStampAsset();
|
|
1124
|
+
const activeToolId = useOptionalSelector(InteractionToken$1, (c) => c.activeToolId(), null);
|
|
1125
|
+
const armedId = useOptionalSelector(StampToken$1, (c) => documentId ? c.armedAsset(documentId)?.id ?? null : null, null);
|
|
1126
|
+
const ids = Array.isArray(config.toolbar) ? config.toolbar : config.toolbar && "library" in config.toolbar ? stamp.library(config.toolbar.library)?.assetIds ?? [] : [];
|
|
1127
|
+
const wantsDefault = ids.some((id) => id.startsWith(`${DEFAULT_LIBRARY_ID}:`));
|
|
1128
|
+
useEffect(() => {
|
|
1129
|
+
if (!wantsDefault) return;
|
|
1130
|
+
restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), config.defaultLibrary)).catch((err) => console.error("[embedpdf] quick stamps failed:", err));
|
|
1131
|
+
}, [
|
|
1132
|
+
stamp,
|
|
1133
|
+
locale,
|
|
1134
|
+
wantsDefault
|
|
1135
|
+
]);
|
|
1136
|
+
const shown = ids.map((id) => assets.find((a) => a.id === id)).filter((a) => !!a);
|
|
1137
|
+
if (shown.length === 0 || !documentId) return null;
|
|
1138
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1139
|
+
className: "flex items-center gap-1",
|
|
1140
|
+
role: "group",
|
|
1141
|
+
"aria-label": t("demo.quickStampsLabel"),
|
|
1142
|
+
children: shown.map((asset) => /* @__PURE__ */ jsx(QuickStampButton, {
|
|
1143
|
+
asset,
|
|
1144
|
+
active: activeToolId === "stamp" && armedId === asset.id,
|
|
1145
|
+
onArm: () => void armAsset(asset.id).catch((e) => console.error("[embedpdf] arm failed", e))
|
|
1146
|
+
}, asset.id))
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
function QuickStampButton({ asset, active, onArm }) {
|
|
1150
|
+
const url = useStampAssetPreviewUrl(asset.id);
|
|
1151
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1152
|
+
type: "button",
|
|
1153
|
+
onClick: onArm,
|
|
1154
|
+
title: asset.label,
|
|
1155
|
+
"aria-label": asset.label,
|
|
1156
|
+
"aria-pressed": active || void 0,
|
|
1157
|
+
className: buttonClass(active, true),
|
|
1158
|
+
children: url ? /* @__PURE__ */ jsx("img", {
|
|
1159
|
+
src: url,
|
|
1160
|
+
alt: "",
|
|
1161
|
+
className: "h-5 w-5 object-contain",
|
|
1162
|
+
draggable: false
|
|
1163
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
1164
|
+
className: "text-xs",
|
|
1165
|
+
children: asset.label
|
|
1166
|
+
})
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
//#endregion
|
|
932
1170
|
//#region src/ui/menu.tsx
|
|
933
1171
|
/**
|
|
934
1172
|
* Menu rendering — a MenuSchema (a tree of command ids) rendered as rows.
|
|
@@ -1337,7 +1575,10 @@ function AppToolbar({ bar, className }) {
|
|
|
1337
1575
|
variant,
|
|
1338
1576
|
run
|
|
1339
1577
|
}),
|
|
1340
|
-
renderCustom: {
|
|
1578
|
+
renderCustom: {
|
|
1579
|
+
"zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ jsx(ZoomControls, {}) : /* @__PURE__ */ jsx(ZoomButton, {}),
|
|
1580
|
+
"quick-stamps": () => /* @__PURE__ */ jsx(QuickStamps, {})
|
|
1581
|
+
},
|
|
1341
1582
|
renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ jsx(CollapsedModes, { view }) : void 0,
|
|
1342
1583
|
renderGroupTrigger: (view) => /* @__PURE__ */ jsx(GroupTrigger, { view }),
|
|
1343
1584
|
renderSeparator: () => /* @__PURE__ */ jsx("span", { className: "bg-border h-6 w-px self-center" }),
|
|
@@ -1349,6 +1590,71 @@ function AppToolbar({ bar, className }) {
|
|
|
1349
1590
|
});
|
|
1350
1591
|
}
|
|
1351
1592
|
//#endregion
|
|
1593
|
+
//#region src/ui/annotation-fonts.tsx
|
|
1594
|
+
/**
|
|
1595
|
+
* The configured annotation fonts (`annotations.fonts`), loaded once per
|
|
1596
|
+
* viewer: fetch the bytes → register them on the viewer's engine under the
|
|
1597
|
+
* key (what a free-text `fontFamily` carries and what the engine embeds) →
|
|
1598
|
+
* mount the same bytes as a `@font-face` named by that key (what the plugin's
|
|
1599
|
+
* `cssFontFamily` emits for a registered font, so the live editor renders the
|
|
1600
|
+
* face the appearance stream will bake). The style panel lists a font only
|
|
1601
|
+
* once BOTH have happened — an offered key always resolves on write.
|
|
1602
|
+
*/
|
|
1603
|
+
const LoadedFontsContext = createContext([]);
|
|
1604
|
+
function AnnotationFontsProvider({ children }) {
|
|
1605
|
+
const kernel = useKernel();
|
|
1606
|
+
const { fonts } = useAnnotationsConfig();
|
|
1607
|
+
const [loaded, setLoaded] = useState([]);
|
|
1608
|
+
useEffect(() => {
|
|
1609
|
+
const engineFonts = kernel.engine.fonts;
|
|
1610
|
+
if (!fonts?.length || !engineFonts) return;
|
|
1611
|
+
let cancelled = false;
|
|
1612
|
+
const unmounts = [];
|
|
1613
|
+
(async () => {
|
|
1614
|
+
for (const font of fonts) try {
|
|
1615
|
+
const response = await fetch(font.url);
|
|
1616
|
+
if (!response.ok) throw new Error(`${font.url}: HTTP ${response.status}`);
|
|
1617
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1618
|
+
if (cancelled) return;
|
|
1619
|
+
await engineFonts.register({
|
|
1620
|
+
key: font.key,
|
|
1621
|
+
familyName: font.familyName,
|
|
1622
|
+
weight: font.weight,
|
|
1623
|
+
italic: font.italic,
|
|
1624
|
+
data: bytes
|
|
1625
|
+
});
|
|
1626
|
+
const unmount = await mountWebFont(font.key, bytes, {
|
|
1627
|
+
weight: font.weight,
|
|
1628
|
+
style: font.italic ? "italic" : "normal"
|
|
1629
|
+
});
|
|
1630
|
+
if (cancelled) {
|
|
1631
|
+
unmount();
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
unmounts.push(unmount);
|
|
1635
|
+
setLoaded((prev) => prev.some((f) => f.key === font.key) ? prev : [...prev, {
|
|
1636
|
+
key: font.key,
|
|
1637
|
+
label: font.label
|
|
1638
|
+
}]);
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
console.warn(`[embedpdf] annotation font "${font.key}" was not loaded:`, error);
|
|
1641
|
+
}
|
|
1642
|
+
})();
|
|
1643
|
+
return () => {
|
|
1644
|
+
cancelled = true;
|
|
1645
|
+
for (const unmount of unmounts) unmount();
|
|
1646
|
+
};
|
|
1647
|
+
}, [kernel, fonts]);
|
|
1648
|
+
return /* @__PURE__ */ jsx(LoadedFontsContext.Provider, {
|
|
1649
|
+
value: loaded,
|
|
1650
|
+
children
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
/** The configured fonts that are registered AND mounted, in config order. */
|
|
1654
|
+
function useAnnotationFonts() {
|
|
1655
|
+
return useContext(LoadedFontsContext);
|
|
1656
|
+
}
|
|
1657
|
+
//#endregion
|
|
1352
1658
|
//#region src/ui/link-editor.tsx
|
|
1353
1659
|
/**
|
|
1354
1660
|
* The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
|
|
@@ -2334,12 +2640,17 @@ function LineEndingSelect({ side, value, onChange }) {
|
|
|
2334
2640
|
});
|
|
2335
2641
|
}
|
|
2336
2642
|
function FontFamilySelect({ value, onChange }) {
|
|
2643
|
+
const configured = useAnnotationFonts();
|
|
2644
|
+
const options = [...FONT_OPTIONS, ...configured.map((f) => ({
|
|
2645
|
+
v: f.key,
|
|
2646
|
+
label: f.label
|
|
2647
|
+
}))];
|
|
2337
2648
|
return /* @__PURE__ */ jsx(DropdownShell, {
|
|
2338
2649
|
trigger: /* @__PURE__ */ jsx("span", {
|
|
2339
2650
|
className: "text-fg text-sm",
|
|
2340
|
-
children:
|
|
2651
|
+
children: options.find((o) => o.v === value)?.label ?? value
|
|
2341
2652
|
}),
|
|
2342
|
-
children: (close) =>
|
|
2653
|
+
children: (close) => options.map((o) => /* @__PURE__ */ jsx(OptionRow, {
|
|
2343
2654
|
selected: o.v === value,
|
|
2344
2655
|
onClick: () => {
|
|
2345
2656
|
onChange(o.v);
|
|
@@ -2351,17 +2662,33 @@ function FontFamilySelect({ value, onChange }) {
|
|
|
2351
2662
|
}
|
|
2352
2663
|
function FontSizeCombo({ value, onChange }) {
|
|
2353
2664
|
const [open, setOpen] = useState(false);
|
|
2665
|
+
const rootRef = useOutsideClose(open, () => setOpen(false));
|
|
2666
|
+
const [draft, setDraft] = useState(null);
|
|
2667
|
+
const commit = () => {
|
|
2668
|
+
if (draft === null) return;
|
|
2669
|
+
const n = parseInt(draft, 10);
|
|
2670
|
+
setDraft(null);
|
|
2671
|
+
if (Number.isFinite(n) && n > 0 && n !== value) onChange(n);
|
|
2672
|
+
};
|
|
2354
2673
|
return /* @__PURE__ */ jsxs("div", {
|
|
2355
|
-
ref:
|
|
2674
|
+
ref: rootRef,
|
|
2356
2675
|
className: "relative w-full",
|
|
2357
2676
|
children: [
|
|
2358
2677
|
/* @__PURE__ */ jsx("input", {
|
|
2359
2678
|
type: "number",
|
|
2360
2679
|
min: 1,
|
|
2361
|
-
value,
|
|
2362
|
-
onChange: (e) =>
|
|
2363
|
-
|
|
2364
|
-
|
|
2680
|
+
value: draft ?? value,
|
|
2681
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2682
|
+
onBlur: commit,
|
|
2683
|
+
onKeyDown: (e) => {
|
|
2684
|
+
if (e.key === "Enter") {
|
|
2685
|
+
e.preventDefault();
|
|
2686
|
+
commit();
|
|
2687
|
+
setOpen(false);
|
|
2688
|
+
} else if (e.key === "Escape") {
|
|
2689
|
+
setDraft(null);
|
|
2690
|
+
setOpen(false);
|
|
2691
|
+
}
|
|
2365
2692
|
},
|
|
2366
2693
|
onClick: () => setOpen(true),
|
|
2367
2694
|
className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 pr-7 text-sm"
|
|
@@ -2382,6 +2709,7 @@ function FontSizeCombo({ value, onChange }) {
|
|
|
2382
2709
|
children: FONT_SIZES.map((sz) => /* @__PURE__ */ jsx(OptionRow, {
|
|
2383
2710
|
selected: sz === value,
|
|
2384
2711
|
onClick: () => {
|
|
2712
|
+
setDraft(null);
|
|
2385
2713
|
onChange(sz);
|
|
2386
2714
|
setOpen(false);
|
|
2387
2715
|
},
|
|
@@ -2402,15 +2730,45 @@ function BlendModeSelect({ value, onChange }) {
|
|
|
2402
2730
|
}, mode.v))
|
|
2403
2731
|
});
|
|
2404
2732
|
}
|
|
2405
|
-
function Toggle({ active, title, onClick, children }) {
|
|
2733
|
+
function Toggle({ active, title, onClick, keepFocus, children }) {
|
|
2406
2734
|
return /* @__PURE__ */ jsx("button", {
|
|
2407
2735
|
type: "button",
|
|
2408
2736
|
title,
|
|
2409
2737
|
onClick,
|
|
2738
|
+
onMouseDown: keepFocus ? (e) => e.preventDefault() : void 0,
|
|
2410
2739
|
className: `flex h-9 w-9 items-center justify-center rounded border transition-colors ${active ? "border-accent bg-accent text-on-accent" : "border-border bg-surface text-fg hover:bg-hover"}`,
|
|
2411
2740
|
children
|
|
2412
2741
|
});
|
|
2413
2742
|
}
|
|
2743
|
+
const isFormatSpec = (spec) => spec.key === "bold" || spec.key === "italic" || spec.key === "underline";
|
|
2744
|
+
/**
|
|
2745
|
+
* The format toggles a free-text kind declares, in one row. While the text
|
|
2746
|
+
* editor holds a range they read and write that range's runs (the plugin
|
|
2747
|
+
* routes `updateSelection`); otherwise the annotation's body. The buttons
|
|
2748
|
+
* keep focus in the editor so the range survives the click.
|
|
2749
|
+
*/
|
|
2750
|
+
function FormatToggles({ specs, values, mixed, onChange }) {
|
|
2751
|
+
return /* @__PURE__ */ jsx(Field, {
|
|
2752
|
+
label: useT()("demo.formatLabel", { fallback: "Format" }),
|
|
2753
|
+
mixed: specs.some((s) => mixed.includes(s.key)),
|
|
2754
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2755
|
+
className: "flex gap-2",
|
|
2756
|
+
children: specs.map((spec) => {
|
|
2757
|
+
const active = values[spec.key] === true && !mixed.includes(spec.key);
|
|
2758
|
+
return /* @__PURE__ */ jsx(Toggle, {
|
|
2759
|
+
title: spec.label,
|
|
2760
|
+
active,
|
|
2761
|
+
keepFocus: true,
|
|
2762
|
+
onClick: () => onChange({ [spec.key]: !active }),
|
|
2763
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
2764
|
+
name: spec.key,
|
|
2765
|
+
size: 18
|
|
2766
|
+
})
|
|
2767
|
+
}, spec.key);
|
|
2768
|
+
})
|
|
2769
|
+
})
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2414
2772
|
function PropControl({ spec, value, mixed, onChange }) {
|
|
2415
2773
|
switch (spec.key) {
|
|
2416
2774
|
case "color":
|
|
@@ -2545,6 +2903,9 @@ function PropControl({ spec, value, mixed, onChange }) {
|
|
|
2545
2903
|
})
|
|
2546
2904
|
});
|
|
2547
2905
|
}
|
|
2906
|
+
case "bold":
|
|
2907
|
+
case "italic":
|
|
2908
|
+
case "underline": return null;
|
|
2548
2909
|
case "blendMode": return /* @__PURE__ */ jsx(Field, {
|
|
2549
2910
|
label: spec.label,
|
|
2550
2911
|
mixed,
|
|
@@ -2592,7 +2953,12 @@ function AnnotationStylePanel() {
|
|
|
2592
2953
|
className: "text-fg-muted mb-4 text-[11px] font-semibold uppercase tracking-wide",
|
|
2593
2954
|
children: hasSel ? `${selected.length} selected` : `${activeToolId} defaults`
|
|
2594
2955
|
}),
|
|
2595
|
-
specs.map((spec) => /* @__PURE__ */ jsx(
|
|
2956
|
+
specs.map((spec) => spec.key === "bold" ? /* @__PURE__ */ jsx(FormatToggles, {
|
|
2957
|
+
specs: specs.filter((s) => isFormatSpec(s)),
|
|
2958
|
+
values,
|
|
2959
|
+
mixed: hasSel ? sel.mixed : [],
|
|
2960
|
+
onChange: write
|
|
2961
|
+
}, "format") : /* @__PURE__ */ jsx(PropControl, {
|
|
2596
2962
|
spec,
|
|
2597
2963
|
value: values[spec.key],
|
|
2598
2964
|
mixed: hasSel && sel.mixed.includes(spec.key),
|
|
@@ -3463,72 +3829,6 @@ function SearchPanel() {
|
|
|
3463
3829
|
});
|
|
3464
3830
|
}
|
|
3465
3831
|
//#endregion
|
|
3466
|
-
//#region src/config/default-stamps.ts
|
|
3467
|
-
/**
|
|
3468
|
-
* The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
|
|
3469
|
-
* standard rubber stamps as one Acrobat-compatible PDF per locale.
|
|
3470
|
-
*
|
|
3471
|
-
* Delivery: the library ships INSIDE whatever ships this viewer. The package
|
|
3472
|
-
* exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
|
|
3473
|
-
* so it travels through the module graph like any other code — every bundler
|
|
3474
|
-
* splits it into a chunk served from the app's own origin, the CDN snippet
|
|
3475
|
-
* carries it as a sibling chunk in its folder, and nothing is fetched from a
|
|
3476
|
-
* third party. `stamps.defaultLibrary` overrides that:
|
|
3477
|
-
* - `false` → no built-in library, no request (air-gapped);
|
|
3478
|
-
* - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
|
|
3479
|
-
*
|
|
3480
|
-
* Loading is LAZY (first open of the stamps panel, never at boot) and
|
|
3481
|
-
* locale-aware: the chrome locale leads, the browser languages break ties
|
|
3482
|
-
* for the locales the chrome does not translate itself, and a locale change
|
|
3483
|
-
* while a default library is loaded swaps it for the new one. The file names
|
|
3484
|
-
* itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
|
|
3485
|
-
*/
|
|
3486
|
-
/** The built-in library's PieceInfo id — excluded from persistence (it is
|
|
3487
|
-
* loaded again on every first open, in the locale of that moment). */
|
|
3488
|
-
const DEFAULT_LIBRARY_ID = "embedpdf-standard";
|
|
3489
|
-
/** Which shipped locale to show for the chrome's locale + the browser's. */
|
|
3490
|
-
function resolveStampsLocale(chromeLocale) {
|
|
3491
|
-
return negotiateLocale(LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
|
|
3492
|
-
}
|
|
3493
|
-
const fetchBytes = async (url) => {
|
|
3494
|
-
const response = await fetch(url);
|
|
3495
|
-
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
3496
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
3497
|
-
};
|
|
3498
|
-
/** The library bytes for `locale`: your URL when you gave one, otherwise the
|
|
3499
|
-
* copy that shipped with the viewer. Never anything else. */
|
|
3500
|
-
const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : loadDefaultLibrary(locale);
|
|
3501
|
-
/** Per workspace: the locale the default library was last brought to, and
|
|
3502
|
-
* the in-flight work — the panel mounts and unmounts with the sidebar, so
|
|
3503
|
-
* this cannot live in component state. */
|
|
3504
|
-
const loads = /* @__PURE__ */ new WeakMap();
|
|
3505
|
-
/**
|
|
3506
|
-
* Bring the built-in library to `locale`. Idempotent per workspace and
|
|
3507
|
-
* locale; a locale change swaps the loaded default (remove, then import);
|
|
3508
|
-
* a default the user removed in this session stays removed — the locale is
|
|
3509
|
-
* recorded, nothing is re-added. An embedder that seeded a library under
|
|
3510
|
-
* the same id is left alone.
|
|
3511
|
-
*/
|
|
3512
|
-
function ensureDefaultLibrary(stamp, locale, source) {
|
|
3513
|
-
if (source === false) return Promise.resolve();
|
|
3514
|
-
const current = loads.get(stamp);
|
|
3515
|
-
if (current?.locale === locale) return current.promise;
|
|
3516
|
-
const run = async () => {
|
|
3517
|
-
await current?.promise.catch(() => void 0);
|
|
3518
|
-
const loaded = stamp.library(DEFAULT_LIBRARY_ID);
|
|
3519
|
-
if (loaded?.locale === locale) return;
|
|
3520
|
-
if (current && !loaded) return;
|
|
3521
|
-
if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
|
|
3522
|
-
await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
|
|
3523
|
-
};
|
|
3524
|
-
const promise = run();
|
|
3525
|
-
loads.set(stamp, {
|
|
3526
|
-
locale,
|
|
3527
|
-
promise
|
|
3528
|
-
});
|
|
3529
|
-
return promise;
|
|
3530
|
-
}
|
|
3531
|
-
//#endregion
|
|
3532
3832
|
//#region src/ui/stamps-panel.tsx
|
|
3533
3833
|
/**
|
|
3534
3834
|
* The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
|
|
@@ -3551,30 +3851,15 @@ function ensureDefaultLibrary(stamp, locale, source) {
|
|
|
3551
3851
|
* through the surface's open props.
|
|
3552
3852
|
*/
|
|
3553
3853
|
const ALL = "all";
|
|
3554
|
-
/** The user's libraries live in the browser (IndexedDB): restored on first
|
|
3555
|
-
* open, written on every change. The built-in set is never stored — it is
|
|
3556
|
-
* fetched again next time, in the locale of that moment. */
|
|
3557
|
-
const store = typeof indexedDB === "undefined" ? null : indexedDbByteStore("embedpdf-stamps", { storeName: "libraries" });
|
|
3558
|
-
/** Restore runs ONCE per workspace — the panel mounts and unmounts with the
|
|
3559
|
-
* sidebar, so the guard cannot live in component state. */
|
|
3560
|
-
const restored = /* @__PURE__ */ new WeakMap();
|
|
3561
|
-
const restoreOnce = (stamp) => {
|
|
3562
|
-
let pending = restored.get(stamp);
|
|
3563
|
-
if (!pending) {
|
|
3564
|
-
pending = store ? restoreStampLibraries(stamp, store) : Promise.resolve();
|
|
3565
|
-
restored.set(stamp, pending);
|
|
3566
|
-
}
|
|
3567
|
-
return pending;
|
|
3568
|
-
};
|
|
3569
3854
|
function StampsPanel() {
|
|
3570
3855
|
const t = useT();
|
|
3571
3856
|
const stamp = useStamp();
|
|
3572
|
-
const
|
|
3857
|
+
const { defaultLibrary, sidebar } = useStampsConfig();
|
|
3858
|
+
const libraries = useStampLibraries({ kind: sidebar ?? ["stamps"] });
|
|
3573
3859
|
const assets = useStampAssets();
|
|
3574
3860
|
const { armAsset } = useArmStampAsset();
|
|
3575
3861
|
const { activeToolId } = useTool();
|
|
3576
3862
|
const { locale } = useLocale();
|
|
3577
|
-
const { defaultLibrary } = useStampsConfig();
|
|
3578
3863
|
const surface = useSurface("stamps");
|
|
3579
3864
|
const fileRef = useRef(null);
|
|
3580
3865
|
const [armedId, setArmedId] = useState(null);
|
|
@@ -3588,7 +3873,7 @@ function StampsPanel() {
|
|
|
3588
3873
|
useEffect(() => {
|
|
3589
3874
|
let live = true;
|
|
3590
3875
|
setBusy("loading");
|
|
3591
|
-
|
|
3876
|
+
restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
|
|
3592
3877
|
console.error("[embedpdf] default stamps failed:", err);
|
|
3593
3878
|
if (live) setError(t("demo.stampsError"));
|
|
3594
3879
|
}).finally(() => live && setBusy(null));
|
|
@@ -3596,7 +3881,6 @@ function StampsPanel() {
|
|
|
3596
3881
|
live = false;
|
|
3597
3882
|
};
|
|
3598
3883
|
}, [stamp, locale]);
|
|
3599
|
-
useEffect(() => store ? persistStampLibraries(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0, [stamp]);
|
|
3600
3884
|
useEffect(() => {
|
|
3601
3885
|
if (activeToolId !== "stamp") setArmedId(null);
|
|
3602
3886
|
}, [activeToolId]);
|
|
@@ -3767,38 +4051,325 @@ function StampAssetCell({ asset, armed, onArm }) {
|
|
|
3767
4051
|
});
|
|
3768
4052
|
}
|
|
3769
4053
|
//#endregion
|
|
3770
|
-
//#region src/ui/
|
|
4054
|
+
//#region src/ui/signatures-panel.tsx
|
|
3771
4055
|
/**
|
|
3772
|
-
* The
|
|
3773
|
-
*
|
|
3774
|
-
*
|
|
3775
|
-
*
|
|
4056
|
+
* The signatures sidebar (right panel) — Preview's signature sheet on v3
|
|
4057
|
+
* parts. One row per PERSON: a stamp library of kind `signatures` holding a
|
|
4058
|
+
* full signature and, optionally, initials. Nothing is stored beyond the
|
|
4059
|
+
* file; rename/export/delete are the library verbs.
|
|
3776
4060
|
*
|
|
3777
|
-
*
|
|
3778
|
-
*
|
|
3779
|
-
*
|
|
4061
|
+
* pick a mark → with a TARGET field ("sign here" was clicked, or a
|
|
4062
|
+
* signature widget selected) the mark goes into that field by the
|
|
4063
|
+
* plugin's mode; otherwise it ARMS — hover a page for the ghost, click a
|
|
4064
|
+
* signature field to sign it, click anywhere else to drop it as a stamp.
|
|
4065
|
+
*
|
|
4066
|
+
* Below the people: the signature fields of THIS document, signed or not,
|
|
4067
|
+
* with their verdicts — click an unsigned one to target it, a signed one to
|
|
4068
|
+
* inspect it.
|
|
3780
4069
|
*/
|
|
3781
|
-
|
|
4070
|
+
const fieldLabel = (field) => field.kind === "fqn" ? field.name : `#${field.fieldObjectNumber}`;
|
|
4071
|
+
function SignaturesPanel() {
|
|
3782
4072
|
const t = useT();
|
|
3783
|
-
const
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
4073
|
+
const stamp = useStamp();
|
|
4074
|
+
const signature = useSignature();
|
|
4075
|
+
const config = useSignaturesConfig();
|
|
4076
|
+
const rows = useSignerRows();
|
|
4077
|
+
const { target, busy } = useSignatureTarget();
|
|
4078
|
+
const { armAsset } = useArmStampAsset();
|
|
4079
|
+
const { activeToolId } = useTool();
|
|
4080
|
+
const maker = useSurface("signature-maker");
|
|
4081
|
+
const [armedId, setArmedId] = useState(null);
|
|
4082
|
+
const [error, setError] = useState(null);
|
|
4083
|
+
const [invalidating, setInvalidating] = useState(null);
|
|
4084
|
+
useSignatureEvent((event) => {
|
|
4085
|
+
if (event.type === "invalidating") setInvalidating(signature.signatureOf(event.field)?.fieldName ?? fieldLabel(event.field));
|
|
4086
|
+
else if (event.type === "validated" && !event.verdicts.some((v) => v.summary === "invalid" && v.modifications.basis === "working-copy")) setInvalidating(null);
|
|
4087
|
+
});
|
|
4088
|
+
useEffect(() => {
|
|
4089
|
+
restoreStampLibrariesOnce(stamp);
|
|
4090
|
+
}, [stamp]);
|
|
4091
|
+
useEffect(() => {
|
|
4092
|
+
if (activeToolId !== "stamp") setArmedId(null);
|
|
4093
|
+
}, [activeToolId]);
|
|
4094
|
+
const pick = (asset) => {
|
|
4095
|
+
setError(null);
|
|
4096
|
+
if (target) {
|
|
4097
|
+
signature.placeMark({ assetId: asset.id }, { field: target }).catch((err) => {
|
|
4098
|
+
console.error("[embedpdf] placing the mark failed:", err);
|
|
4099
|
+
setError(t("demo.signError"));
|
|
4100
|
+
});
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
setArmedId(asset.id);
|
|
4104
|
+
armAsset(asset.id).catch((err) => {
|
|
4105
|
+
console.error("[embedpdf] arm mark failed:", err);
|
|
4106
|
+
setArmedId(null);
|
|
4107
|
+
setError(t("demo.stampsArmError"));
|
|
4108
|
+
});
|
|
4109
|
+
};
|
|
4110
|
+
const canAddPerson = (config.libraries ?? "many") === "many" || rows.length === 0;
|
|
4111
|
+
const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
|
|
4112
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4113
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
4114
|
+
children: [
|
|
4115
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4116
|
+
className: "min-h-0 flex-1 overflow-y-auto p-3",
|
|
4117
|
+
children: [rows.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
4118
|
+
className: "flex flex-col items-center gap-3 px-4 py-8 text-center",
|
|
4119
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
4120
|
+
name: "signature",
|
|
4121
|
+
size: 32,
|
|
4122
|
+
className: "text-fg-muted"
|
|
4123
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
4124
|
+
className: "text-fg-muted text-sm",
|
|
4125
|
+
children: t("demo.signaturesEmpty")
|
|
4126
|
+
})]
|
|
4127
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
4128
|
+
className: "flex flex-col gap-3",
|
|
4129
|
+
children: rows.map((row) => /* @__PURE__ */ jsx(SignerRowView, {
|
|
4130
|
+
row,
|
|
4131
|
+
armedId,
|
|
4132
|
+
wantsInitials,
|
|
4133
|
+
onPick: pick,
|
|
4134
|
+
onAddInitials: () => maker.open({
|
|
4135
|
+
exclusive: "modal",
|
|
4136
|
+
props: { libraryId: row.libraryId }
|
|
4137
|
+
})
|
|
4138
|
+
}, row.libraryId))
|
|
4139
|
+
}), /* @__PURE__ */ jsx(DocumentSignatures, {})]
|
|
4140
|
+
}),
|
|
4141
|
+
invalidating && /* @__PURE__ */ jsx("div", {
|
|
4142
|
+
className: "border-border-subtle shrink-0 border-t px-3 py-2 text-xs text-amber-700",
|
|
4143
|
+
children: t("demo.signaturesInvalidating", { params: { field: invalidating } })
|
|
4144
|
+
}),
|
|
4145
|
+
(error || target || armedId) && /* @__PURE__ */ jsx("div", {
|
|
4146
|
+
className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
|
|
4147
|
+
children: error ?? (target ? busy ? t("demo.signSigning") : t("demo.signaturesTargetHint") : t("demo.signaturesArmedHint"))
|
|
4148
|
+
}),
|
|
4149
|
+
canAddPerson ? /* @__PURE__ */ jsx("div", {
|
|
4150
|
+
className: "border-border-subtle shrink-0 border-t p-3",
|
|
4151
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
4152
|
+
type: "button",
|
|
4153
|
+
onClick: () => maker.open({ exclusive: "modal" }),
|
|
4154
|
+
className: "border-border text-fg hover:bg-hover flex w-full items-center justify-center gap-2 rounded-md border px-3 py-1.5 text-sm",
|
|
4155
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
4156
|
+
name: "plus",
|
|
4157
|
+
size: 16
|
|
4158
|
+
}), t("demo.signaturesNew")]
|
|
4159
|
+
})
|
|
4160
|
+
}) : null
|
|
4161
|
+
]
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
function SignerRowView({ row, armedId, wantsInitials, onPick, onAddInitials }) {
|
|
4165
|
+
const t = useT();
|
|
4166
|
+
const stamp = useStamp();
|
|
4167
|
+
const [renaming, setRenaming] = useState(null);
|
|
4168
|
+
const exportPdf = () => {
|
|
4169
|
+
const bytes = stamp.exportLibrary(row.libraryId);
|
|
4170
|
+
if (!bytes) return;
|
|
4171
|
+
const blob = new Blob([bytes], { type: "application/pdf" });
|
|
4172
|
+
const url = URL.createObjectURL(blob);
|
|
4173
|
+
const a = document.createElement("a");
|
|
4174
|
+
a.href = url;
|
|
4175
|
+
a.download = `${row.name || "signature"}.pdf`;
|
|
4176
|
+
a.click();
|
|
4177
|
+
URL.revokeObjectURL(url);
|
|
4178
|
+
};
|
|
4179
|
+
const commitRename = () => {
|
|
4180
|
+
const name = renaming?.trim();
|
|
4181
|
+
setRenaming(null);
|
|
4182
|
+
if (name && name !== row.name) stamp.updateLibrary(row.libraryId, { name });
|
|
4183
|
+
};
|
|
4184
|
+
const iconButton = "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded";
|
|
4185
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
4186
|
+
className: "border-border-subtle rounded-md border p-2",
|
|
4187
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
4188
|
+
className: "flex items-center gap-1",
|
|
4189
|
+
children: [
|
|
4190
|
+
renaming !== null ? /* @__PURE__ */ jsx("input", {
|
|
4191
|
+
autoFocus: true,
|
|
4192
|
+
value: renaming,
|
|
4193
|
+
onChange: (e) => setRenaming(e.target.value),
|
|
4194
|
+
onBlur: commitRename,
|
|
4195
|
+
onKeyDown: (e) => {
|
|
4196
|
+
if (e.key === "Enter") commitRename();
|
|
4197
|
+
if (e.key === "Escape") setRenaming(null);
|
|
4198
|
+
},
|
|
4199
|
+
className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-1.5 py-0.5 text-sm"
|
|
4200
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
4201
|
+
className: "text-fg min-w-0 flex-1 truncate text-sm font-medium",
|
|
4202
|
+
children: row.name
|
|
4203
|
+
}),
|
|
4204
|
+
/* @__PURE__ */ jsx("button", {
|
|
4205
|
+
type: "button",
|
|
4206
|
+
onClick: () => setRenaming(row.name),
|
|
4207
|
+
title: t("demo.signaturesRename"),
|
|
4208
|
+
className: iconButton,
|
|
4209
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
4210
|
+
name: "pencilMarker",
|
|
4211
|
+
size: 13
|
|
4212
|
+
})
|
|
4213
|
+
}),
|
|
4214
|
+
/* @__PURE__ */ jsx("button", {
|
|
4215
|
+
type: "button",
|
|
4216
|
+
onClick: exportPdf,
|
|
4217
|
+
title: t("demo.signaturesExport"),
|
|
4218
|
+
className: iconButton,
|
|
4219
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
4220
|
+
name: "download",
|
|
4221
|
+
size: 13
|
|
4222
|
+
})
|
|
4223
|
+
}),
|
|
4224
|
+
/* @__PURE__ */ jsx("button", {
|
|
4225
|
+
type: "button",
|
|
4226
|
+
onClick: () => void stamp.removeLibrary(row.libraryId),
|
|
4227
|
+
title: t("demo.signaturesRemove"),
|
|
4228
|
+
className: iconButton,
|
|
4229
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
4230
|
+
name: "trash",
|
|
4231
|
+
size: 13
|
|
4232
|
+
})
|
|
4233
|
+
})
|
|
4234
|
+
]
|
|
4235
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
4236
|
+
className: "mt-2 grid grid-cols-[2fr_1fr] gap-2",
|
|
4237
|
+
children: [row.signatures.map((asset) => /* @__PURE__ */ jsx(MarkThumb, {
|
|
4238
|
+
asset,
|
|
4239
|
+
label: t("demo.signatureLabel"),
|
|
4240
|
+
armed: armedId === asset.id,
|
|
4241
|
+
onPick: () => onPick(asset)
|
|
4242
|
+
}, asset.id)), wantsInitials ? row.initials ? /* @__PURE__ */ jsx(MarkThumb, {
|
|
4243
|
+
asset: row.initials,
|
|
4244
|
+
label: t("demo.initialsLabel"),
|
|
4245
|
+
armed: armedId === row.initials.id,
|
|
4246
|
+
onPick: () => onPick(row.initials)
|
|
4247
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
4248
|
+
type: "button",
|
|
4249
|
+
onClick: onAddInitials,
|
|
4250
|
+
title: t("demo.signaturesAddInitials"),
|
|
4251
|
+
className: "border-border-subtle text-fg-muted hover:bg-hover flex h-16 items-center justify-center rounded-md border border-dashed text-xs",
|
|
4252
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
4253
|
+
name: "plus",
|
|
4254
|
+
size: 14
|
|
4255
|
+
})
|
|
4256
|
+
}) : null]
|
|
4257
|
+
})]
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
function MarkThumb({ asset, label, armed, onPick }) {
|
|
4261
|
+
const url = useStampAssetPreviewUrl(asset.id);
|
|
4262
|
+
return /* @__PURE__ */ jsx("button", {
|
|
4263
|
+
type: "button",
|
|
4264
|
+
onClick: onPick,
|
|
4265
|
+
title: label,
|
|
4266
|
+
"aria-pressed": armed,
|
|
4267
|
+
className: `flex h-16 items-center justify-center rounded-md border p-1 ${armed ? "border-accent bg-accent-light ring-accent ring-2" : "border-border-subtle hover:border-border hover:bg-hover"}`,
|
|
4268
|
+
children: url ? /* @__PURE__ */ jsx("img", {
|
|
4269
|
+
src: url,
|
|
4270
|
+
alt: label,
|
|
4271
|
+
className: "max-h-full max-w-full object-contain"
|
|
4272
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
4273
|
+
className: "text-fg-muted text-xs",
|
|
4274
|
+
children: label
|
|
4275
|
+
})
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
/** The signature fields of this document, with their state. */
|
|
4279
|
+
function DocumentSignatures() {
|
|
4280
|
+
const t = useT();
|
|
4281
|
+
const signature = useSignature();
|
|
4282
|
+
const snapshot = useSignatureSnapshot();
|
|
4283
|
+
const verdicts = useSignatureVerdicts();
|
|
4284
|
+
const { target } = useSignatureTarget();
|
|
4285
|
+
const [validating, setValidating] = useState(false);
|
|
4286
|
+
const fields = snapshot?.signatures ?? [];
|
|
4287
|
+
const verdictOf = (dto) => verdicts?.find((v) => v.signature.index === dto.index) ?? null;
|
|
4288
|
+
const verdictLabel = (v) => !v ? t("demo.signaturesSigned") : v.summary === "valid" ? t("demo.verdictValid") : v.summary === "valid-untrusted" ? t("demo.verdictValidUntrusted") : v.summary === "indeterminate" ? t("demo.verdictIndeterminate") : v.modifications.basis === "working-copy" ? t("demo.verdictWillInvalidate") : t("demo.verdictInvalid");
|
|
4289
|
+
const verdictTone = (v) => !v || v.summary === "indeterminate" ? "text-fg-muted" : v.summary === "invalid" ? v.modifications.basis === "working-copy" ? "text-amber-600" : "text-red-600" : v.summary === "valid-untrusted" ? "text-amber-600" : "text-green-600";
|
|
4290
|
+
const isTarget = (dto) => !!target && (target.kind === "fqn" ? target.name === dto.fieldName : target.fieldObjectNumber === (dto.field.kind === "objectNumber" ? dto.field.fieldObjectNumber : -1));
|
|
4291
|
+
const validate = () => {
|
|
4292
|
+
setValidating(true);
|
|
4293
|
+
signature.validate().finally(() => setValidating(false));
|
|
4294
|
+
};
|
|
4295
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4296
|
+
className: "mt-4",
|
|
4297
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
4298
|
+
className: "flex items-center justify-between",
|
|
4299
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4300
|
+
className: "text-fg-muted text-xs font-semibold uppercase tracking-wide",
|
|
4301
|
+
children: t("demo.signaturesInDocument")
|
|
4302
|
+
}), fields.some((f) => f.signed) ? /* @__PURE__ */ jsx("button", {
|
|
4303
|
+
type: "button",
|
|
4304
|
+
disabled: validating,
|
|
4305
|
+
onClick: validate,
|
|
4306
|
+
className: "text-accent text-xs hover:underline disabled:opacity-60",
|
|
4307
|
+
children: validating ? t("demo.signaturesValidating") : t("demo.signaturesValidate")
|
|
4308
|
+
}) : null]
|
|
4309
|
+
}), fields.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
4310
|
+
className: "text-fg-muted mt-1 text-xs",
|
|
4311
|
+
children: t("demo.signaturesNone")
|
|
4312
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
4313
|
+
className: "mt-1 flex flex-col gap-1",
|
|
4314
|
+
children: fields.map((dto) => {
|
|
4315
|
+
const verdict = verdictOf(dto);
|
|
4316
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
|
|
4317
|
+
type: "button",
|
|
4318
|
+
onClick: () => dto.signed ? signature.inspect(dto.field) : signature.setTarget(dto.field),
|
|
4319
|
+
className: `hover:bg-hover flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm ${isTarget(dto) ? "bg-accent-light" : ""}`,
|
|
4320
|
+
children: [
|
|
4321
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
4322
|
+
name: dto.signed ? "check" : "signature",
|
|
4323
|
+
size: 14,
|
|
4324
|
+
className: dto.signed ? "text-green-600" : "text-fg-muted"
|
|
4325
|
+
}),
|
|
4326
|
+
/* @__PURE__ */ jsx("span", {
|
|
4327
|
+
className: "text-fg min-w-0 flex-1 truncate",
|
|
4328
|
+
children: dto.fieldName
|
|
4329
|
+
}),
|
|
4330
|
+
/* @__PURE__ */ jsx("span", {
|
|
4331
|
+
className: `shrink-0 text-right text-xs ${dto.signed ? verdictTone(verdict) : "text-fg-muted"}`,
|
|
4332
|
+
children: dto.signed ? verdictLabel(verdict) : t("demo.signaturesUnsigned")
|
|
4333
|
+
})
|
|
4334
|
+
]
|
|
4335
|
+
}) }, dto.index);
|
|
4336
|
+
})
|
|
4337
|
+
})]
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
//#endregion
|
|
4341
|
+
//#region src/ui/panels.tsx
|
|
4342
|
+
/**
|
|
4343
|
+
* The static chrome around the toolbars: the left/right sidebars (shell
|
|
4344
|
+
* surfaces) and the bottom page-controls overlay. Deliberately minimal —
|
|
4345
|
+
* chrome-parity scope. Panels read their open state from plugin-shell; the
|
|
4346
|
+
* app owns their DOM.
|
|
4347
|
+
*
|
|
4348
|
+
* There is no built-in header: branding, a locale picker and a theme switch
|
|
4349
|
+
* are the EMBEDDER's chrome, not the viewer's. The frame keeps a `header`
|
|
4350
|
+
* socket for a slotted one (see Shell).
|
|
4351
|
+
*/
|
|
4352
|
+
function LeftSidebar() {
|
|
4353
|
+
const t = useT();
|
|
4354
|
+
const sidebar = useSurface("sidebar");
|
|
4355
|
+
if (!sidebar.isOpen) return null;
|
|
4356
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
4357
|
+
className: "border-border-subtle bg-surface flex w-60 shrink-0 flex-col border-r",
|
|
4358
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
4359
|
+
className: "border-border-subtle flex items-center gap-1 border-b p-2",
|
|
4360
|
+
children: [
|
|
4361
|
+
/* @__PURE__ */ jsx("span", {
|
|
4362
|
+
className: "bg-accent-light text-accent rounded-md px-2 py-1 text-xs font-medium",
|
|
4363
|
+
children: t("demo.thumbnails")
|
|
4364
|
+
}),
|
|
4365
|
+
/* @__PURE__ */ jsx("span", {
|
|
4366
|
+
className: "text-fg-muted rounded-md px-2 py-1 text-xs font-medium",
|
|
4367
|
+
children: t("demo.outline")
|
|
4368
|
+
}),
|
|
4369
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
4370
|
+
/* @__PURE__ */ jsx("button", {
|
|
4371
|
+
type: "button",
|
|
4372
|
+
onClick: sidebar.close,
|
|
3802
4373
|
className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
|
|
3803
4374
|
children: /* @__PURE__ */ jsx(Icon, {
|
|
3804
4375
|
name: "x",
|
|
@@ -3861,10 +4432,11 @@ function RightSidebar() {
|
|
|
3861
4432
|
const style = useSurface("annotation-style");
|
|
3862
4433
|
const redaction = useSurface("redaction");
|
|
3863
4434
|
const stamps = useSurface("stamps");
|
|
3864
|
-
const
|
|
4435
|
+
const signatures = useSurface("signatures");
|
|
4436
|
+
const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : signatures.isOpen ? "signatures" : null;
|
|
3865
4437
|
if (!active) return null;
|
|
3866
|
-
const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : "demo.styleTitle";
|
|
3867
|
-
const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : style.close;
|
|
4438
|
+
const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : active === "signatures" ? "demo.signaturesTitle" : "demo.styleTitle";
|
|
4439
|
+
const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : active === "signatures" ? signatures.close : style.close;
|
|
3868
4440
|
return /* @__PURE__ */ jsxs("aside", {
|
|
3869
4441
|
className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
|
|
3870
4442
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -3881,7 +4453,7 @@ function RightSidebar() {
|
|
|
3881
4453
|
size: 16
|
|
3882
4454
|
})
|
|
3883
4455
|
})]
|
|
3884
|
-
}), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ jsx(StampsPanel, {}) : /* @__PURE__ */ jsx(CommentsPanel, {}, documentId ?? "none")]
|
|
4456
|
+
}), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ jsx(StampsPanel, {}) : active === "signatures" ? /* @__PURE__ */ jsx(SignaturesPanel, {}) : /* @__PURE__ */ jsx(CommentsPanel, {}, documentId ?? "none")]
|
|
3885
4457
|
});
|
|
3886
4458
|
}
|
|
3887
4459
|
function PageControls() {
|
|
@@ -3972,6 +4544,684 @@ function RedactConfirmModal() {
|
|
|
3972
4544
|
});
|
|
3973
4545
|
}
|
|
3974
4546
|
//#endregion
|
|
4547
|
+
//#region src/ui/sign-dialog.tsx
|
|
4548
|
+
/**
|
|
4549
|
+
* The sign dialog (`signature-sign` modal surface): a mark met a field and
|
|
4550
|
+
* the mode is `ask` — or a "sign here" flow wants the facts first. Shows
|
|
4551
|
+
* the mark as it will sit in the field, takes reason and location, offers
|
|
4552
|
+
* a certification when the config allows it, and either SEALS the field
|
|
4553
|
+
* (`signField`) or only draws the mark into it (`fillField`).
|
|
4554
|
+
*/
|
|
4555
|
+
function SignDialog() {
|
|
4556
|
+
const t = useT();
|
|
4557
|
+
const surface = useSurface("signature-sign");
|
|
4558
|
+
const signature = useSignature();
|
|
4559
|
+
const [reason, setReason] = useState("");
|
|
4560
|
+
const [location, setLocation] = useState("");
|
|
4561
|
+
const [signKind, setSignKind] = useState("approval");
|
|
4562
|
+
const [busy, setBusy] = useState(null);
|
|
4563
|
+
const [error, setError] = useState(null);
|
|
4564
|
+
const field = surface.props?.field;
|
|
4565
|
+
const mark = surface.props?.mark;
|
|
4566
|
+
const preview = useStampAssetPreviewUrl(mark && "assetId" in mark ? mark.assetId : null);
|
|
4567
|
+
if (!surface.isOpen || !field || !mark) return null;
|
|
4568
|
+
const attribution = {
|
|
4569
|
+
...reason.trim() ? { reason: reason.trim() } : {},
|
|
4570
|
+
...location.trim() ? { location: location.trim() } : {}
|
|
4571
|
+
};
|
|
4572
|
+
const run = async (kind) => {
|
|
4573
|
+
setBusy(kind);
|
|
4574
|
+
setError(null);
|
|
4575
|
+
try {
|
|
4576
|
+
if (kind === "sign") await signature.signField({
|
|
4577
|
+
field,
|
|
4578
|
+
mark,
|
|
4579
|
+
attribution,
|
|
4580
|
+
...signKind !== "approval" && signature.canCertify() ? { certify: { permission: Number(signKind.slice(-1)) } } : {}
|
|
4581
|
+
});
|
|
4582
|
+
else await signature.fillField(field, mark);
|
|
4583
|
+
surface.close();
|
|
4584
|
+
} catch (err) {
|
|
4585
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
4586
|
+
} finally {
|
|
4587
|
+
setBusy(null);
|
|
4588
|
+
}
|
|
4589
|
+
};
|
|
4590
|
+
const input = "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm";
|
|
4591
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4592
|
+
className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
|
|
4593
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
4594
|
+
className: "border-border bg-surface w-96 rounded-lg border p-4 shadow-xl",
|
|
4595
|
+
children: [
|
|
4596
|
+
/* @__PURE__ */ jsx("h2", {
|
|
4597
|
+
className: "text-fg text-base font-semibold",
|
|
4598
|
+
children: t("demo.signTitle")
|
|
4599
|
+
}),
|
|
4600
|
+
/* @__PURE__ */ jsx("div", {
|
|
4601
|
+
className: "border-border-subtle mt-3 flex h-24 items-center justify-center rounded-md border p-2",
|
|
4602
|
+
children: preview ? /* @__PURE__ */ jsx("img", {
|
|
4603
|
+
src: preview,
|
|
4604
|
+
alt: "",
|
|
4605
|
+
className: "max-h-full max-w-full object-contain"
|
|
4606
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
4607
|
+
className: "text-fg-muted text-sm",
|
|
4608
|
+
children: t("demo.signatureLabel")
|
|
4609
|
+
})
|
|
4610
|
+
}),
|
|
4611
|
+
/* @__PURE__ */ jsxs("label", {
|
|
4612
|
+
className: "text-fg-muted mt-3 block text-xs",
|
|
4613
|
+
children: [t("demo.signReason"), /* @__PURE__ */ jsx("input", {
|
|
4614
|
+
value: reason,
|
|
4615
|
+
onChange: (e) => setReason(e.target.value),
|
|
4616
|
+
className: input
|
|
4617
|
+
})]
|
|
4618
|
+
}),
|
|
4619
|
+
/* @__PURE__ */ jsxs("label", {
|
|
4620
|
+
className: "text-fg-muted mt-2 block text-xs",
|
|
4621
|
+
children: [t("demo.signLocation"), /* @__PURE__ */ jsx("input", {
|
|
4622
|
+
value: location,
|
|
4623
|
+
onChange: (e) => setLocation(e.target.value),
|
|
4624
|
+
className: input
|
|
4625
|
+
})]
|
|
4626
|
+
}),
|
|
4627
|
+
signature.canCertify() ? /* @__PURE__ */ jsxs("label", {
|
|
4628
|
+
className: "text-fg-muted mt-3 block text-xs",
|
|
4629
|
+
children: [t("demo.signKind"), /* @__PURE__ */ jsxs("select", {
|
|
4630
|
+
value: signKind,
|
|
4631
|
+
onChange: (e) => setSignKind(e.target.value),
|
|
4632
|
+
className: input,
|
|
4633
|
+
children: [
|
|
4634
|
+
/* @__PURE__ */ jsx("option", {
|
|
4635
|
+
value: "approval",
|
|
4636
|
+
children: t("demo.signKindApproval")
|
|
4637
|
+
}),
|
|
4638
|
+
/* @__PURE__ */ jsx("option", {
|
|
4639
|
+
value: "certify-3",
|
|
4640
|
+
children: t("demo.signKindCertifyAnnotate")
|
|
4641
|
+
}),
|
|
4642
|
+
/* @__PURE__ */ jsx("option", {
|
|
4643
|
+
value: "certify-2",
|
|
4644
|
+
children: t("demo.signKindCertifyFill")
|
|
4645
|
+
}),
|
|
4646
|
+
/* @__PURE__ */ jsx("option", {
|
|
4647
|
+
value: "certify-1",
|
|
4648
|
+
children: t("demo.signKindCertifyLocked")
|
|
4649
|
+
})
|
|
4650
|
+
]
|
|
4651
|
+
})]
|
|
4652
|
+
}) : null,
|
|
4653
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
4654
|
+
className: "mt-2 text-sm text-red-600",
|
|
4655
|
+
children: error
|
|
4656
|
+
}) : null,
|
|
4657
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4658
|
+
className: "mt-4 flex justify-end gap-2",
|
|
4659
|
+
children: [
|
|
4660
|
+
/* @__PURE__ */ jsx("button", {
|
|
4661
|
+
type: "button",
|
|
4662
|
+
disabled: busy != null,
|
|
4663
|
+
onClick: surface.close,
|
|
4664
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
|
|
4665
|
+
children: t("demo.cancel")
|
|
4666
|
+
}),
|
|
4667
|
+
signature.canFill() ? /* @__PURE__ */ jsx("button", {
|
|
4668
|
+
type: "button",
|
|
4669
|
+
disabled: busy != null,
|
|
4670
|
+
onClick: () => void run("fill"),
|
|
4671
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-60",
|
|
4672
|
+
children: t("demo.signFill")
|
|
4673
|
+
}) : null,
|
|
4674
|
+
signature.canSign() ? /* @__PURE__ */ jsx("button", {
|
|
4675
|
+
type: "button",
|
|
4676
|
+
disabled: busy != null,
|
|
4677
|
+
onClick: () => void run("sign"),
|
|
4678
|
+
className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
|
|
4679
|
+
children: busy === "sign" ? t("demo.signSigning") : t("demo.signButton")
|
|
4680
|
+
}) : null
|
|
4681
|
+
]
|
|
4682
|
+
})
|
|
4683
|
+
]
|
|
4684
|
+
})
|
|
4685
|
+
});
|
|
4686
|
+
}
|
|
4687
|
+
//#endregion
|
|
4688
|
+
//#region src/ui/signature-bridge.tsx
|
|
4689
|
+
/**
|
|
4690
|
+
* The signature plugin's UI intents → shell surfaces. The plugin owns the
|
|
4691
|
+
* act and says WHAT happened; the chrome decides WHAT OPENS:
|
|
4692
|
+
*
|
|
4693
|
+
* target (a "sign here" click) → the signatures panel, right side
|
|
4694
|
+
* ask (mode 'ask' met a field) → the sign dialog (modal)
|
|
4695
|
+
* inspect (a signed field clicked) → the validation popover
|
|
4696
|
+
*
|
|
4697
|
+
* Mounted once per document view; renders nothing.
|
|
4698
|
+
*/
|
|
4699
|
+
function SignatureBridge() {
|
|
4700
|
+
const shell = useShell();
|
|
4701
|
+
useSignatureEvent((event) => {
|
|
4702
|
+
switch (event.type) {
|
|
4703
|
+
case "target":
|
|
4704
|
+
if (event.field) shell.open("signatures", { exclusive: "right" });
|
|
4705
|
+
return;
|
|
4706
|
+
case "ask":
|
|
4707
|
+
shell.open("signature-sign", {
|
|
4708
|
+
exclusive: "modal",
|
|
4709
|
+
props: {
|
|
4710
|
+
field: event.field,
|
|
4711
|
+
mark: event.mark
|
|
4712
|
+
}
|
|
4713
|
+
});
|
|
4714
|
+
return;
|
|
4715
|
+
case "inspect":
|
|
4716
|
+
shell.open("signature-inspector", { props: { field: event.field } });
|
|
4717
|
+
return;
|
|
4718
|
+
case "invalidating":
|
|
4719
|
+
shell.open("signatures", { exclusive: "right" });
|
|
4720
|
+
return;
|
|
4721
|
+
default: return;
|
|
4722
|
+
}
|
|
4723
|
+
});
|
|
4724
|
+
return null;
|
|
4725
|
+
}
|
|
4726
|
+
//#endregion
|
|
4727
|
+
//#region src/ui/signature-inspector.tsx
|
|
4728
|
+
/**
|
|
4729
|
+
* The validation popover (`signature-inspector` surface): what a signed
|
|
4730
|
+
* field's signature says and whether it holds — the four independent facts
|
|
4731
|
+
* (integrity, cryptography, trust, changes since) and the signer's claims,
|
|
4732
|
+
* anchored at the widget when its box is known, plus the signed revision as
|
|
4733
|
+
* a download (the exact bytes the signature covers). Mounted in the Stage
|
|
4734
|
+
* overlay so `<Anchored>` can project onto the page.
|
|
4735
|
+
*/
|
|
4736
|
+
const verdictKey = (v) => !v ? "demo.verdictIndeterminate" : v.summary === "valid" ? "demo.verdictValid" : v.summary === "valid-untrusted" ? "demo.verdictValidUntrusted" : v.summary === "invalid" ? v.modifications.basis === "working-copy" ? "demo.verdictWillInvalidate" : "demo.verdictInvalid" : "demo.verdictIndeterminate";
|
|
4737
|
+
/** A PDF date string (`D:YYYYMMDDHHmmSSZ` or with a zone offset) as a locale date; the raw text when it is not one. */
|
|
4738
|
+
const pdfDate = (raw) => {
|
|
4739
|
+
const m = /^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}'?\d{2}'?)?/.exec(raw);
|
|
4740
|
+
if (!m) return raw;
|
|
4741
|
+
const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
|
|
4742
|
+
const offset = !zone || zone === "Z" ? "Z" : `${zone[0]}${zone.slice(1, 3)}:${zone.slice(3).replace(/'/g, "") || "00"}`;
|
|
4743
|
+
const date = /* @__PURE__ */ new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}${offset}`);
|
|
4744
|
+
return Number.isNaN(date.getTime()) ? raw : date.toLocaleString();
|
|
4745
|
+
};
|
|
4746
|
+
const tone = (summary) => summary === "valid" ? "text-green-600" : summary === "invalid" ? "text-red-600" : summary === "valid-untrusted" ? "text-amber-600" : "text-fg-muted";
|
|
4747
|
+
function SignatureInspector() {
|
|
4748
|
+
const t = useT();
|
|
4749
|
+
const surface = useSurface("signature-inspector");
|
|
4750
|
+
const signature = useSignature();
|
|
4751
|
+
const snapshot = useSignatureSnapshot();
|
|
4752
|
+
const verdicts = useSignatureVerdicts();
|
|
4753
|
+
const [validating, setValidating] = useState(false);
|
|
4754
|
+
const field = surface.props?.field;
|
|
4755
|
+
const dto = field ? signature.signatureOf(field) : null;
|
|
4756
|
+
const verdict = field ? signature.verdictOf(field) : null;
|
|
4757
|
+
const widget = dto?.widget ?? null;
|
|
4758
|
+
const box = useSelector(FormToken$1, (c) => widget && widget.annotObjectNumber > 0 ? c.fillItem(widget.annotObjectNumber)?.box ?? null : null);
|
|
4759
|
+
useEffect(() => {
|
|
4760
|
+
if (!surface.isOpen || !dto?.signed || verdicts) return;
|
|
4761
|
+
setValidating(true);
|
|
4762
|
+
signature.validate().finally(() => setValidating(false));
|
|
4763
|
+
}, [surface.isOpen, dto?.index]);
|
|
4764
|
+
if (!surface.isOpen || !field || !dto || !snapshot) return null;
|
|
4765
|
+
const revalidate = () => {
|
|
4766
|
+
setValidating(true);
|
|
4767
|
+
signature.validate().finally(() => setValidating(false));
|
|
4768
|
+
};
|
|
4769
|
+
const summary = verdict?.summary ?? null;
|
|
4770
|
+
const modifications = verdict?.modifications.verdict ?? null;
|
|
4771
|
+
const row = (label, value, className = "text-fg") => /* @__PURE__ */ jsxs("div", {
|
|
4772
|
+
className: "flex items-baseline justify-between gap-3 text-xs",
|
|
4773
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4774
|
+
className: "text-fg-muted",
|
|
4775
|
+
children: label
|
|
4776
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
4777
|
+
className: `text-right ${className}`,
|
|
4778
|
+
children: value
|
|
4779
|
+
})]
|
|
4780
|
+
});
|
|
4781
|
+
const card = /* @__PURE__ */ jsxs("div", {
|
|
4782
|
+
className: "border-border bg-elevated w-72 rounded-lg border p-3 text-sm shadow-xl",
|
|
4783
|
+
children: [
|
|
4784
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4785
|
+
className: "flex items-center justify-between",
|
|
4786
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4787
|
+
className: "text-fg font-semibold",
|
|
4788
|
+
children: t("demo.inspectTitle")
|
|
4789
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
4790
|
+
type: "button",
|
|
4791
|
+
onClick: surface.close,
|
|
4792
|
+
className: "text-fg-muted hover:bg-hover grid h-6 w-6 place-items-center rounded",
|
|
4793
|
+
"aria-label": t("demo.cancel"),
|
|
4794
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
4795
|
+
name: "x",
|
|
4796
|
+
size: 14
|
|
4797
|
+
})
|
|
4798
|
+
})]
|
|
4799
|
+
}),
|
|
4800
|
+
/* @__PURE__ */ jsx("p", {
|
|
4801
|
+
className: `mt-1 text-sm font-medium ${tone(summary)}`,
|
|
4802
|
+
children: validating ? t("demo.signaturesValidating") : t(verdictKey(verdict))
|
|
4803
|
+
}),
|
|
4804
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4805
|
+
className: "mt-2 flex flex-col gap-1",
|
|
4806
|
+
children: [
|
|
4807
|
+
row(t("demo.inspectSigner"), dto.signer.name ?? "—"),
|
|
4808
|
+
dto.signer.reason ? row(t("demo.inspectReason"), dto.signer.reason) : null,
|
|
4809
|
+
dto.signer.location ? row(t("demo.inspectLocation"), dto.signer.location) : null,
|
|
4810
|
+
dto.signer.claimedTime ? row(t("demo.inspectTime"), pdfDate(dto.signer.claimedTime)) : null
|
|
4811
|
+
]
|
|
4812
|
+
}),
|
|
4813
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4814
|
+
className: "border-border-subtle mt-2 flex flex-col gap-1 border-t pt-2",
|
|
4815
|
+
children: [
|
|
4816
|
+
row(t("demo.inspectIntegrity"), verdict?.integrity ?? "—", tone(verdict?.integrity === "valid" ? "valid" : verdict?.integrity === "invalid" ? "invalid" : null)),
|
|
4817
|
+
row(t("demo.inspectCryptography"), verdict?.cryptography ?? "—"),
|
|
4818
|
+
row(t("demo.inspectTrust"), verdict?.trust ?? "—"),
|
|
4819
|
+
row(t("demo.inspectChanges"), modifications === "unchanged" ? verdict?.modifications.undone ? t("demo.verdictRestored") : t("demo.verdictUnchanged") : modifications === "permitted" ? t("demo.verdictPermitted") : modifications === "forbidden" ? t("demo.verdictForbidden") : "—", modifications === "forbidden" ? "text-red-600" : "text-fg"),
|
|
4820
|
+
verdict?.modifications.laterRevisions ? row(t("demo.inspectLaterRevisions"), String(verdict.modifications.laterRevisions)) : null
|
|
4821
|
+
]
|
|
4822
|
+
}),
|
|
4823
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4824
|
+
className: "mt-3 flex justify-end gap-2",
|
|
4825
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
4826
|
+
type: "button",
|
|
4827
|
+
disabled: validating,
|
|
4828
|
+
onClick: revalidate,
|
|
4829
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
|
|
4830
|
+
children: t("demo.signaturesValidate")
|
|
4831
|
+
}), /* @__PURE__ */ jsx(DownloadRevision, { field })]
|
|
4832
|
+
})
|
|
4833
|
+
]
|
|
4834
|
+
});
|
|
4835
|
+
if (widget && widget.pageObjectNumber > 0 && box) return /* @__PURE__ */ jsx(Anchored, {
|
|
4836
|
+
anchor: {
|
|
4837
|
+
pon: widget.pageObjectNumber,
|
|
4838
|
+
bounds: box
|
|
4839
|
+
},
|
|
4840
|
+
placement: "bottom",
|
|
4841
|
+
gap: 8,
|
|
4842
|
+
children: card
|
|
4843
|
+
});
|
|
4844
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4845
|
+
className: "absolute right-4 top-4 z-40",
|
|
4846
|
+
children: card
|
|
4847
|
+
});
|
|
4848
|
+
}
|
|
4849
|
+
/** The signed revision — the exact bytes the signature covers — as a file. */
|
|
4850
|
+
function DownloadRevision({ field }) {
|
|
4851
|
+
const t = useT();
|
|
4852
|
+
const signature = useSignature();
|
|
4853
|
+
const dto = signature.signatureOf(field);
|
|
4854
|
+
const [busy, setBusy] = useState(false);
|
|
4855
|
+
if (!dto || dto.revisionIndex === null) return null;
|
|
4856
|
+
const revisionIndex = dto.revisionIndex;
|
|
4857
|
+
const run = async () => {
|
|
4858
|
+
setBusy(true);
|
|
4859
|
+
try {
|
|
4860
|
+
const bytes = await signature.revisionBytes(revisionIndex);
|
|
4861
|
+
const blob = new Blob([bytes], { type: "application/pdf" });
|
|
4862
|
+
const url = URL.createObjectURL(blob);
|
|
4863
|
+
const a = document.createElement("a");
|
|
4864
|
+
a.href = url;
|
|
4865
|
+
a.download = `${dto.fieldName}-signed.pdf`;
|
|
4866
|
+
a.click();
|
|
4867
|
+
URL.revokeObjectURL(url);
|
|
4868
|
+
} catch (err) {
|
|
4869
|
+
console.error("[embedpdf] signed revision download failed:", err);
|
|
4870
|
+
} finally {
|
|
4871
|
+
setBusy(false);
|
|
4872
|
+
}
|
|
4873
|
+
};
|
|
4874
|
+
return /* @__PURE__ */ jsx("button", {
|
|
4875
|
+
type: "button",
|
|
4876
|
+
disabled: busy,
|
|
4877
|
+
onClick: () => void run(),
|
|
4878
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
|
|
4879
|
+
children: t("demo.inspectDownload")
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
//#endregion
|
|
4883
|
+
//#region src/ui/signature-maker.tsx
|
|
4884
|
+
/**
|
|
4885
|
+
* The mark maker (`signature-maker` modal surface): a person's signature and
|
|
4886
|
+
* initials, drawn, typed, or uploaded, saved as ONE library of kind
|
|
4887
|
+
* `signatures` — the file IS the person (Preview's model). Chrome code over
|
|
4888
|
+
* the stamp plugin's generic calls: `createLibrary` + `addAsset({ mark })`;
|
|
4889
|
+
* rename/export/delete are the library verbs. Opened on an existing library
|
|
4890
|
+
* (`props.libraryId`) it only adds the missing initials.
|
|
4891
|
+
*/
|
|
4892
|
+
const PAD = {
|
|
4893
|
+
width: 360,
|
|
4894
|
+
height: 120
|
|
4895
|
+
};
|
|
4896
|
+
const BUILTIN_FONTS = [{
|
|
4897
|
+
key: "helvetica-oblique",
|
|
4898
|
+
label: "Helvetica"
|
|
4899
|
+
}, {
|
|
4900
|
+
key: "times-italic",
|
|
4901
|
+
label: "Times"
|
|
4902
|
+
}];
|
|
4903
|
+
/** One pad: draw / type / upload, producing a `MarkSource` or null. */
|
|
4904
|
+
function MarkPad({ value, onChange, fonts, registerFont }) {
|
|
4905
|
+
const t = useT();
|
|
4906
|
+
const [tab, setTab] = useState("draw");
|
|
4907
|
+
const [strokes, setStrokes] = useState([]);
|
|
4908
|
+
const [text, setText] = useState("");
|
|
4909
|
+
const [font, setFont] = useState(fonts[0]?.key ?? "helvetica-oblique");
|
|
4910
|
+
const [fileName, setFileName] = useState(null);
|
|
4911
|
+
const canvas = useRef(null);
|
|
4912
|
+
const drawing = useRef(null);
|
|
4913
|
+
useEffect(() => {
|
|
4914
|
+
const el = canvas.current;
|
|
4915
|
+
if (!el) return;
|
|
4916
|
+
const dpr = window.devicePixelRatio || 1;
|
|
4917
|
+
el.width = PAD.width * dpr;
|
|
4918
|
+
el.height = PAD.height * dpr;
|
|
4919
|
+
const ctx = el.getContext("2d");
|
|
4920
|
+
if (!ctx) return;
|
|
4921
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
4922
|
+
ctx.clearRect(0, 0, PAD.width, PAD.height);
|
|
4923
|
+
ctx.lineCap = "round";
|
|
4924
|
+
ctx.lineJoin = "round";
|
|
4925
|
+
ctx.lineWidth = 2.5;
|
|
4926
|
+
ctx.strokeStyle = getComputedStyle(el).color || "#000";
|
|
4927
|
+
for (const stroke of strokes) {
|
|
4928
|
+
ctx.beginPath();
|
|
4929
|
+
stroke.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
|
|
4930
|
+
ctx.stroke();
|
|
4931
|
+
}
|
|
4932
|
+
}, [strokes, tab]);
|
|
4933
|
+
const point = (e) => {
|
|
4934
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
4935
|
+
return {
|
|
4936
|
+
x: e.clientX - r.left,
|
|
4937
|
+
y: e.clientY - r.top
|
|
4938
|
+
};
|
|
4939
|
+
};
|
|
4940
|
+
const commitStrokes = (next) => {
|
|
4941
|
+
setStrokes(next);
|
|
4942
|
+
onChange(next.length ? {
|
|
4943
|
+
kind: "ink",
|
|
4944
|
+
strokes: next,
|
|
4945
|
+
color: "#000000",
|
|
4946
|
+
strokeWidth: 2.5
|
|
4947
|
+
} : null);
|
|
4948
|
+
};
|
|
4949
|
+
const commitText = async (next, key) => {
|
|
4950
|
+
setText(next);
|
|
4951
|
+
setFont(key);
|
|
4952
|
+
if (!next.trim()) {
|
|
4953
|
+
onChange(null);
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4956
|
+
await registerFont(key);
|
|
4957
|
+
onChange({
|
|
4958
|
+
kind: "text",
|
|
4959
|
+
text: next.trim(),
|
|
4960
|
+
fontFamily: key,
|
|
4961
|
+
color: "#000000",
|
|
4962
|
+
fontSize: 36
|
|
4963
|
+
});
|
|
4964
|
+
};
|
|
4965
|
+
const pickFile = (file) => {
|
|
4966
|
+
setFileName(file.name);
|
|
4967
|
+
onChange(file.type === "application/pdf" ? {
|
|
4968
|
+
kind: "pdf",
|
|
4969
|
+
source: file,
|
|
4970
|
+
pageIndex: 0
|
|
4971
|
+
} : {
|
|
4972
|
+
kind: "image",
|
|
4973
|
+
source: file
|
|
4974
|
+
});
|
|
4975
|
+
};
|
|
4976
|
+
const tabClass = (id) => `rounded px-2 py-1 text-xs font-medium ${tab === id ? "bg-accent text-on-accent" : "text-fg-muted hover:bg-hover"}`;
|
|
4977
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4978
|
+
className: "flex flex-col gap-2",
|
|
4979
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
4980
|
+
className: "flex items-center gap-1",
|
|
4981
|
+
children: [
|
|
4982
|
+
/* @__PURE__ */ jsx("button", {
|
|
4983
|
+
type: "button",
|
|
4984
|
+
className: tabClass("draw"),
|
|
4985
|
+
onClick: () => setTab("draw"),
|
|
4986
|
+
children: t("demo.makerDraw")
|
|
4987
|
+
}),
|
|
4988
|
+
/* @__PURE__ */ jsx("button", {
|
|
4989
|
+
type: "button",
|
|
4990
|
+
className: tabClass("type"),
|
|
4991
|
+
onClick: () => setTab("type"),
|
|
4992
|
+
children: t("demo.makerType")
|
|
4993
|
+
}),
|
|
4994
|
+
/* @__PURE__ */ jsx("button", {
|
|
4995
|
+
type: "button",
|
|
4996
|
+
className: tabClass("upload"),
|
|
4997
|
+
onClick: () => setTab("upload"),
|
|
4998
|
+
children: t("demo.makerUpload")
|
|
4999
|
+
}),
|
|
5000
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
5001
|
+
value ? /* @__PURE__ */ jsx("button", {
|
|
5002
|
+
type: "button",
|
|
5003
|
+
onClick: () => {
|
|
5004
|
+
commitStrokes([]);
|
|
5005
|
+
setText("");
|
|
5006
|
+
setFileName(null);
|
|
5007
|
+
onChange(null);
|
|
5008
|
+
},
|
|
5009
|
+
className: "text-fg-muted hover:text-fg text-xs",
|
|
5010
|
+
children: t("demo.makerClear")
|
|
5011
|
+
}) : null
|
|
5012
|
+
]
|
|
5013
|
+
}), tab === "draw" ? /* @__PURE__ */ jsx("canvas", {
|
|
5014
|
+
ref: canvas,
|
|
5015
|
+
style: {
|
|
5016
|
+
width: PAD.width,
|
|
5017
|
+
height: PAD.height,
|
|
5018
|
+
touchAction: "none"
|
|
5019
|
+
},
|
|
5020
|
+
className: "border-border-subtle bg-surface text-fg rounded-md border",
|
|
5021
|
+
onPointerDown: (e) => {
|
|
5022
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
5023
|
+
drawing.current = [point(e)];
|
|
5024
|
+
setStrokes((s) => [...s, drawing.current]);
|
|
5025
|
+
},
|
|
5026
|
+
onPointerMove: (e) => {
|
|
5027
|
+
if (!drawing.current) return;
|
|
5028
|
+
drawing.current.push(point(e));
|
|
5029
|
+
setStrokes((s) => [...s.slice(0, -1), [...drawing.current]]);
|
|
5030
|
+
},
|
|
5031
|
+
onPointerUp: () => {
|
|
5032
|
+
if (!drawing.current) return;
|
|
5033
|
+
const done = drawing.current;
|
|
5034
|
+
drawing.current = null;
|
|
5035
|
+
setStrokes((s) => {
|
|
5036
|
+
const next = [...s.slice(0, -1), done];
|
|
5037
|
+
commitStrokes(next);
|
|
5038
|
+
return next;
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
5041
|
+
}) : tab === "type" ? /* @__PURE__ */ jsxs("div", {
|
|
5042
|
+
className: "flex flex-col gap-2",
|
|
5043
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
5044
|
+
value: text,
|
|
5045
|
+
placeholder: t("demo.makerTypePlaceholder"),
|
|
5046
|
+
onChange: (e) => void commitText(e.target.value, font),
|
|
5047
|
+
className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm"
|
|
5048
|
+
}), /* @__PURE__ */ jsxs("label", {
|
|
5049
|
+
className: "text-fg-muted flex items-center gap-2 text-xs",
|
|
5050
|
+
children: [t("demo.makerFont"), /* @__PURE__ */ jsx("select", {
|
|
5051
|
+
value: font,
|
|
5052
|
+
onChange: (e) => void commitText(text, e.target.value),
|
|
5053
|
+
className: "border-border bg-surface text-fg rounded border px-2 py-1 text-xs",
|
|
5054
|
+
children: fonts.map((f) => /* @__PURE__ */ jsx("option", {
|
|
5055
|
+
value: f.key,
|
|
5056
|
+
children: f.label
|
|
5057
|
+
}, f.key))
|
|
5058
|
+
})]
|
|
5059
|
+
})]
|
|
5060
|
+
}) : /* @__PURE__ */ jsxs("label", {
|
|
5061
|
+
className: "border-border-subtle text-fg-muted hover:bg-hover flex h-24 cursor-pointer items-center justify-center rounded-md border border-dashed text-sm",
|
|
5062
|
+
children: [fileName ?? t("demo.makerUploadHint"), /* @__PURE__ */ jsx("input", {
|
|
5063
|
+
type: "file",
|
|
5064
|
+
accept: "image/png,image/jpeg,application/pdf",
|
|
5065
|
+
className: "hidden",
|
|
5066
|
+
onChange: (e) => {
|
|
5067
|
+
const file = e.currentTarget.files?.[0];
|
|
5068
|
+
e.currentTarget.value = "";
|
|
5069
|
+
if (file) pickFile(file);
|
|
5070
|
+
}
|
|
5071
|
+
})]
|
|
5072
|
+
})]
|
|
5073
|
+
});
|
|
5074
|
+
}
|
|
5075
|
+
function SignatureMakerModal() {
|
|
5076
|
+
const t = useT();
|
|
5077
|
+
const surface = useSurface("signature-maker");
|
|
5078
|
+
const stamp = useStamp();
|
|
5079
|
+
const kernel = useKernel();
|
|
5080
|
+
const config = useSignaturesConfig();
|
|
5081
|
+
const [name, setName] = useState("");
|
|
5082
|
+
const [signatureMark, setSignatureMark] = useState(null);
|
|
5083
|
+
const [initialsMark, setInitialsMark] = useState(null);
|
|
5084
|
+
const [busy, setBusy] = useState(false);
|
|
5085
|
+
const [error, setError] = useState(null);
|
|
5086
|
+
const existingId = surface.props?.libraryId;
|
|
5087
|
+
const existing = existingId ? stamp.library(existingId) : null;
|
|
5088
|
+
const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
|
|
5089
|
+
const fonts = [...(config.fonts ?? []).map((f) => ({
|
|
5090
|
+
key: f.key,
|
|
5091
|
+
label: f.label
|
|
5092
|
+
})), ...BUILTIN_FONTS];
|
|
5093
|
+
const registered = useRef(/* @__PURE__ */ new Set());
|
|
5094
|
+
const registerFont = async (key) => {
|
|
5095
|
+
const spec = config.fonts?.find((f) => f.key === key);
|
|
5096
|
+
if (!spec || registered.current.has(key) || !kernel.engine.fonts) return;
|
|
5097
|
+
const response = await fetch(spec.url);
|
|
5098
|
+
if (!response.ok) throw new Error(`${spec.url}: HTTP ${response.status}`);
|
|
5099
|
+
await kernel.engine.fonts.register({
|
|
5100
|
+
key,
|
|
5101
|
+
data: new Uint8Array(await response.arrayBuffer())
|
|
5102
|
+
});
|
|
5103
|
+
registered.current.add(key);
|
|
5104
|
+
};
|
|
5105
|
+
if (!surface.isOpen) return null;
|
|
5106
|
+
const canSave = existing ? initialsMark != null : name.trim().length > 0 && signatureMark != null;
|
|
5107
|
+
const save = async () => {
|
|
5108
|
+
setBusy(true);
|
|
5109
|
+
setError(null);
|
|
5110
|
+
try {
|
|
5111
|
+
if (existing) await stamp.addAsset({
|
|
5112
|
+
libraryId: existing.id,
|
|
5113
|
+
name: INITIALS_MARK_NAME,
|
|
5114
|
+
label: t("demo.initialsLabel"),
|
|
5115
|
+
mark: initialsMark
|
|
5116
|
+
});
|
|
5117
|
+
else {
|
|
5118
|
+
const libraryId = await stamp.createLibrary(name.trim(), { kind: SIGNATURES_LIBRARY_KIND });
|
|
5119
|
+
await stamp.addAsset({
|
|
5120
|
+
libraryId,
|
|
5121
|
+
name: SIGNATURE_MARK_NAME,
|
|
5122
|
+
label: t("demo.signatureLabel"),
|
|
5123
|
+
mark: signatureMark
|
|
5124
|
+
});
|
|
5125
|
+
if (initialsMark) await stamp.addAsset({
|
|
5126
|
+
libraryId,
|
|
5127
|
+
name: INITIALS_MARK_NAME,
|
|
5128
|
+
label: t("demo.initialsLabel"),
|
|
5129
|
+
mark: initialsMark
|
|
5130
|
+
});
|
|
5131
|
+
}
|
|
5132
|
+
setName("");
|
|
5133
|
+
setSignatureMark(null);
|
|
5134
|
+
setInitialsMark(null);
|
|
5135
|
+
surface.close();
|
|
5136
|
+
} catch (err) {
|
|
5137
|
+
console.error("[embedpdf] saving the signature failed:", err);
|
|
5138
|
+
setError(t("demo.makerError"));
|
|
5139
|
+
} finally {
|
|
5140
|
+
setBusy(false);
|
|
5141
|
+
}
|
|
5142
|
+
};
|
|
5143
|
+
return /* @__PURE__ */ jsx("div", {
|
|
5144
|
+
className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
|
|
5145
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
5146
|
+
className: "border-border bg-surface w-[26rem] rounded-lg border p-4 shadow-xl",
|
|
5147
|
+
children: [
|
|
5148
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5149
|
+
className: "flex items-center justify-between",
|
|
5150
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
5151
|
+
className: "text-fg text-base font-semibold",
|
|
5152
|
+
children: existing ? t("demo.signaturesAddInitials") : t("demo.makerTitle")
|
|
5153
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
5154
|
+
type: "button",
|
|
5155
|
+
onClick: surface.close,
|
|
5156
|
+
className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
|
|
5157
|
+
"aria-label": t("demo.cancel"),
|
|
5158
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
5159
|
+
name: "x",
|
|
5160
|
+
size: 16
|
|
5161
|
+
})
|
|
5162
|
+
})]
|
|
5163
|
+
}),
|
|
5164
|
+
existing ? null : /* @__PURE__ */ jsxs("label", {
|
|
5165
|
+
className: "text-fg-muted mt-3 block text-xs",
|
|
5166
|
+
children: [t("demo.makerName"), /* @__PURE__ */ jsx("input", {
|
|
5167
|
+
value: name,
|
|
5168
|
+
placeholder: t("demo.makerNamePlaceholder"),
|
|
5169
|
+
onChange: (e) => setName(e.target.value),
|
|
5170
|
+
className: "border-border bg-surface text-fg mt-1 w-full rounded border px-2 py-1.5 text-sm"
|
|
5171
|
+
})]
|
|
5172
|
+
}),
|
|
5173
|
+
existing ? null : /* @__PURE__ */ jsxs("div", {
|
|
5174
|
+
className: "mt-3",
|
|
5175
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
5176
|
+
className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
|
|
5177
|
+
children: t("demo.signatureLabel")
|
|
5178
|
+
}), /* @__PURE__ */ jsx(MarkPad, {
|
|
5179
|
+
value: signatureMark,
|
|
5180
|
+
onChange: setSignatureMark,
|
|
5181
|
+
fonts,
|
|
5182
|
+
registerFont
|
|
5183
|
+
})]
|
|
5184
|
+
}),
|
|
5185
|
+
wantsInitials ? /* @__PURE__ */ jsxs("div", {
|
|
5186
|
+
className: "mt-3",
|
|
5187
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
5188
|
+
className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
|
|
5189
|
+
children: [t("demo.initialsLabel"), existing ? null : /* @__PURE__ */ jsx("span", {
|
|
5190
|
+
className: "text-fg-muted ml-1 font-normal normal-case tracking-normal",
|
|
5191
|
+
children: t("demo.makerOptional")
|
|
5192
|
+
})]
|
|
5193
|
+
}), /* @__PURE__ */ jsx(MarkPad, {
|
|
5194
|
+
value: initialsMark,
|
|
5195
|
+
onChange: setInitialsMark,
|
|
5196
|
+
fonts,
|
|
5197
|
+
registerFont
|
|
5198
|
+
})]
|
|
5199
|
+
}) : null,
|
|
5200
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
5201
|
+
className: "mt-2 text-sm text-red-600",
|
|
5202
|
+
children: error
|
|
5203
|
+
}) : null,
|
|
5204
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5205
|
+
className: "mt-4 flex justify-end gap-2",
|
|
5206
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
5207
|
+
type: "button",
|
|
5208
|
+
disabled: busy,
|
|
5209
|
+
onClick: surface.close,
|
|
5210
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
|
|
5211
|
+
children: t("demo.cancel")
|
|
5212
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
5213
|
+
type: "button",
|
|
5214
|
+
disabled: busy || !canSave,
|
|
5215
|
+
onClick: () => void save(),
|
|
5216
|
+
className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
|
|
5217
|
+
children: busy ? t("demo.makerSaving") : t("demo.makerSave")
|
|
5218
|
+
})]
|
|
5219
|
+
})
|
|
5220
|
+
]
|
|
5221
|
+
})
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
//#endregion
|
|
3975
5225
|
//#region src/ui/document-boot.tsx
|
|
3976
5226
|
/**
|
|
3977
5227
|
* Boot-state panes for the document area — what renders INSTEAD of the Stage
|
|
@@ -4213,9 +5463,10 @@ function Shell() {
|
|
|
4213
5463
|
className: "w-full"
|
|
4214
5464
|
})
|
|
4215
5465
|
});
|
|
4216
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
5466
|
+
return /* @__PURE__ */ jsx(AnnotationFontsProvider, { children: /* @__PURE__ */ jsxs("div", {
|
|
4217
5467
|
className: "bg-app text-fg flex h-full flex-col",
|
|
4218
5468
|
children: [
|
|
5469
|
+
/* @__PURE__ */ jsx(StampLibraryStore, {}),
|
|
4219
5470
|
(frame.header ?? true) && /* @__PURE__ */ jsx("slot", { name: "header" }),
|
|
4220
5471
|
(frame.tabs ?? "always") !== "never" && /* @__PURE__ */ jsx("slot", {
|
|
4221
5472
|
name: "tabs",
|
|
@@ -4235,6 +5486,7 @@ function Shell() {
|
|
|
4235
5486
|
overlay: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4236
5487
|
/* @__PURE__ */ jsx(AnnotationStrip, {}),
|
|
4237
5488
|
/* @__PURE__ */ jsx(SelectionStrip, {}),
|
|
5489
|
+
/* @__PURE__ */ jsx(SignatureInspector, {}),
|
|
4238
5490
|
/* @__PURE__ */ jsx(SelectionHandles, {}),
|
|
4239
5491
|
/* @__PURE__ */ jsx(Scrollbar, { axis: "y" }),
|
|
4240
5492
|
/* @__PURE__ */ jsx(Scrollbar, { axis: "x" })
|
|
@@ -4253,12 +5505,15 @@ function Shell() {
|
|
|
4253
5505
|
]
|
|
4254
5506
|
}),
|
|
4255
5507
|
/* @__PURE__ */ jsx(RightSidebar, {}),
|
|
4256
|
-
/* @__PURE__ */ jsx(RedactConfirmModal, {})
|
|
5508
|
+
/* @__PURE__ */ jsx(RedactConfirmModal, {}),
|
|
5509
|
+
/* @__PURE__ */ jsx(SignatureBridge, {}),
|
|
5510
|
+
/* @__PURE__ */ jsx(SignDialog, {}),
|
|
5511
|
+
/* @__PURE__ */ jsx(SignatureMakerModal, {})
|
|
4257
5512
|
] })
|
|
4258
5513
|
}),
|
|
4259
5514
|
toolbarEdge === "bottom" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ModeBand, { edge: "bottom" }), toolbarBand] })
|
|
4260
5515
|
]
|
|
4261
|
-
});
|
|
5516
|
+
}) });
|
|
4262
5517
|
}
|
|
4263
5518
|
//#endregion
|
|
4264
5519
|
//#region src/viewer.tsx
|
|
@@ -4281,7 +5536,7 @@ function Shell() {
|
|
|
4281
5536
|
* changing these props on a mounted viewer does not rebuild the workspace.
|
|
4282
5537
|
*/
|
|
4283
5538
|
/** Lazy built-in packs — a strings override for one of these wraps its loader. */
|
|
4284
|
-
const BUILTIN_LOADERS = { es: () => import("./es-
|
|
5539
|
+
const BUILTIN_LOADERS = { es: () => import("./es-sN6uqBgD.js").then((m) => m.es) };
|
|
4285
5540
|
const isDict = (v) => typeof v === "object" && v !== null;
|
|
4286
5541
|
/**
|
|
4287
5542
|
* Deep-merge `over` into `base` (immutably), expanding dotted keys —
|
|
@@ -4325,7 +5580,7 @@ function Booting() {
|
|
|
4325
5580
|
})
|
|
4326
5581
|
});
|
|
4327
5582
|
}
|
|
4328
|
-
function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, theme, themeTarget, onViewer }) {
|
|
5583
|
+
function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, signatures, annotations, theme, themeTarget, onViewer }) {
|
|
4329
5584
|
const [resolved] = useState(() => {
|
|
4330
5585
|
const byId = new Map(defaultCommands.map((c) => [c.id, c]));
|
|
4331
5586
|
for (const c of commands ?? []) byId.set(c.id, c);
|
|
@@ -4357,6 +5612,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
|
|
|
4357
5612
|
chrome: resolvedChrome,
|
|
4358
5613
|
icons: icons ?? {},
|
|
4359
5614
|
stamps: stamps ?? {},
|
|
5615
|
+
signatures: signatures ?? {},
|
|
5616
|
+
annotations: annotations ?? {},
|
|
4360
5617
|
i18n: {
|
|
4361
5618
|
locales,
|
|
4362
5619
|
loaders,
|
|
@@ -4422,6 +5679,12 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
|
|
|
4422
5679
|
}
|
|
4423
5680
|
} }),
|
|
4424
5681
|
formPlugin(),
|
|
5682
|
+
signaturePlugin({
|
|
5683
|
+
mode: resolved.signatures.mode,
|
|
5684
|
+
signer: resolved.signatures.signer,
|
|
5685
|
+
trust: resolved.signatures.trust,
|
|
5686
|
+
allowCertify: resolved.signatures.allowCertify
|
|
5687
|
+
}),
|
|
4425
5688
|
linkPlugin(),
|
|
4426
5689
|
redactionPlugin(),
|
|
4427
5690
|
searchPlugin(),
|
|
@@ -4455,6 +5718,6 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
|
|
|
4455
5718
|
});
|
|
4456
5719
|
}
|
|
4457
5720
|
//#endregion
|
|
4458
|
-
export { AnnotationToken, CommandsToken, DocumentsToken, FormToken, FullViewer, I18nToken, InteractionToken, MetadataToken, RedactionToken, SearchToken, SelectionToken, ShellToken, StageToken, StampToken, addItem, chromeHelpers, createViewerHandle, custom, defaultChrome, defaultCommands, ICON_PATHS as defaultIcons, defineChrome, group, item, removeItems, replaceItem, themeConfigOf, validateChrome };
|
|
5721
|
+
export { AnnotationToken, CommandsToken, DocumentsToken, FormToken, FullViewer, I18nToken, InteractionToken, MetadataToken, RedactionToken, SearchToken, SelectionToken, ShellToken, SignatureToken, StageToken, StampToken, addItem, chromeHelpers, createTestSigner, createViewerHandle, custom, defaultChrome, defaultCommands, ICON_PATHS as defaultIcons, defineChrome, group, indexedDbKeyStore, item, memoryKeyStore, personalSigner, remoteSigner, removeItems, replaceItem, themeConfigOf, validateChrome, webCryptoSigner };
|
|
4459
5722
|
|
|
4460
5723
|
//# sourceMappingURL=index.js.map
|