@embedpdf/viewer-chrome 3.0.0-next.11 → 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 +6 -0
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.js +6 -0
- package/dist/chrome.js.map +1 -1
- package/dist/commands.cjs +62 -2
- package/dist/commands.cjs.map +1 -1
- package/dist/commands.js +62 -2
- package/dist/commands.js.map +1 -1
- package/dist/{es-B7b8KjR3.cjs → es-DYwzfxyt.cjs} +87 -3
- package/dist/es-DYwzfxyt.cjs.map +1 -0
- package/dist/{es-aQlZ3hcL.js → es-sN6uqBgD.js} +87 -3
- package/dist/es-sN6uqBgD.js.map +1 -0
- package/dist/index.cjs +1689 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -18
- package/dist/index.d.ts +91 -18
- package/dist/index.js +1645 -40
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/dist/es-B7b8KjR3.cjs.map +0 -1
- package/dist/es-aQlZ3hcL.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -32,9 +32,11 @@ let _embedpdf_react_page_edit = require("@embedpdf/react/page-edit");
|
|
|
32
32
|
let _embedpdf_react_interaction = require("@embedpdf/react/interaction");
|
|
33
33
|
let _embedpdf_react_selection = require("@embedpdf/react/selection");
|
|
34
34
|
let _embedpdf_react_annotation = require("@embedpdf/react/annotation");
|
|
35
|
+
let _embedpdf_react_stamp = require("@embedpdf/react/stamp");
|
|
35
36
|
let _embedpdf_react_redaction = require("@embedpdf/react/redaction");
|
|
36
37
|
let _embedpdf_react_actions = require("@embedpdf/react/actions");
|
|
37
38
|
let _embedpdf_react_form = require("@embedpdf/react/form");
|
|
39
|
+
let _embedpdf_react_signature = require("@embedpdf/react/signature");
|
|
38
40
|
let _embedpdf_react_link = require("@embedpdf/react/link");
|
|
39
41
|
let _embedpdf_react_search = require("@embedpdf/react/search");
|
|
40
42
|
let _embedpdf_react_i18n = require("@embedpdf/react/i18n");
|
|
@@ -43,7 +45,10 @@ let _embedpdf_react_shell = require("@embedpdf/react/shell");
|
|
|
43
45
|
let _embedpdf_react_toolbar = require("@embedpdf/react/toolbar");
|
|
44
46
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
45
47
|
let _embedpdf_react_scrollbar = require("@embedpdf/react/scrollbar");
|
|
48
|
+
let _embedpdf_default_stamps_library = require("@embedpdf/default-stamps/library");
|
|
49
|
+
let _embedpdf_web = require("@embedpdf/web");
|
|
46
50
|
let _embedpdf_react_annotation_menu = require("@embedpdf/react/annotation-menu");
|
|
51
|
+
let _embedpdf_react_anchored = require("@embedpdf/react/anchored");
|
|
47
52
|
let _embedpdf_react_metadata = require("@embedpdf/react/metadata");
|
|
48
53
|
//#region src/config/stage.ts
|
|
49
54
|
/**
|
|
@@ -58,24 +63,23 @@ const ThumbsStageToken = (0, _embedpdf_react_runtime.createCapabilityToken)("sta
|
|
|
58
63
|
//#endregion
|
|
59
64
|
//#region src/config/demo-tools.plugin.ts
|
|
60
65
|
/**
|
|
61
|
-
* Example-only: register the
|
|
62
|
-
* interaction tools. v3 doesn't yet ship signature
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* Example-only: register the still-unimplemented authoring tools as INERT
|
|
67
|
+
* interaction tools. v3 doesn't yet ship a signature plugin, but this is a
|
|
68
|
+
* CHROME demo — its job is selection state and layout, not the tool's effect.
|
|
69
|
+
* Registering them as real (behaviourless) tools lets every tool button in
|
|
70
|
+
* every mode band go through the SAME path (`InteractionToken.activateTool`),
|
|
71
|
+
* so `active` is uniform and honest: the button really is the active tool; it
|
|
72
|
+
* just has no handler behind it.
|
|
68
73
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
74
|
+
* Everything else in the Insert band is REAL now: stamps come from the stamp
|
|
75
|
+
* plugin's libraries (armed by the stamps sidebar), and image/attachment are
|
|
76
|
+
* the annotation plugin's click-then-pick tools. The annotate + shapes tools
|
|
77
|
+
* were never here either — nor the form palette (the form plugin's
|
|
78
|
+
* draw-to-place).
|
|
72
79
|
*/
|
|
73
80
|
const INERT_TOOLS = [{
|
|
74
81
|
id: "signature",
|
|
75
82
|
cursor: "copy"
|
|
76
|
-
}, {
|
|
77
|
-
id: "image",
|
|
78
|
-
cursor: "copy"
|
|
79
83
|
}];
|
|
80
84
|
const demoToolsPlugin = () => (0, _embedpdf_react_runtime.definePlugin)({
|
|
81
85
|
id: "demo-tools",
|
|
@@ -170,6 +174,7 @@ const en = {
|
|
|
170
174
|
delete: "Delete",
|
|
171
175
|
group: "Group",
|
|
172
176
|
ungroup: "Ungroup",
|
|
177
|
+
stampFromSelection: "Make stamp",
|
|
173
178
|
strip: "Annotation actions"
|
|
174
179
|
},
|
|
175
180
|
selection: {
|
|
@@ -195,7 +200,8 @@ const en = {
|
|
|
195
200
|
checkbox: "Checkbox",
|
|
196
201
|
radio: "Radio Button",
|
|
197
202
|
select: "Dropdown",
|
|
198
|
-
listbox: "List Box"
|
|
203
|
+
listbox: "List Box",
|
|
204
|
+
signature: "Signature Field"
|
|
199
205
|
},
|
|
200
206
|
redact: {
|
|
201
207
|
mark: "Mark for Redaction",
|
|
@@ -292,9 +298,91 @@ const en = {
|
|
|
292
298
|
redactConfirmCollateral: "{count} other annotations will also be removed.",
|
|
293
299
|
redactApply: "Apply",
|
|
294
300
|
redactApplying: "Applying…",
|
|
301
|
+
formatLabel: "Format",
|
|
295
302
|
redactLabelTitle: "Redaction label",
|
|
296
303
|
redactLabelPlaceholder: "e.g. REDACTED",
|
|
297
|
-
redactLabelRepeat: "Repeat label across region"
|
|
304
|
+
redactLabelRepeat: "Repeat label across region",
|
|
305
|
+
stampsTitle: "Stamps",
|
|
306
|
+
stampsAll: "All stamps",
|
|
307
|
+
stampsLoading: "Loading stamps…",
|
|
308
|
+
stampsEmpty: "No stamps yet. Import a PDF to turn each of its pages into a stamp.",
|
|
309
|
+
stampsRemoveStamp: "Remove stamp",
|
|
310
|
+
stampsArmedHint: "Click a page to place the stamp.",
|
|
311
|
+
stampsImport: "Import PDF…",
|
|
312
|
+
stampsImporting: "Importing…",
|
|
313
|
+
stampsImportHint: "Import a PDF as a stamp library — each page becomes one stamp.",
|
|
314
|
+
stampsRemoveLibrary: "Remove library",
|
|
315
|
+
stampsExportLibrary: "Export library as PDF",
|
|
316
|
+
stampsCustomLibrary: "My stamps",
|
|
317
|
+
stampsCustomLabel: "Custom stamp",
|
|
318
|
+
stampsError: "The built-in stamps could not be loaded.",
|
|
319
|
+
stampsArmError: "That stamp could not be armed.",
|
|
320
|
+
stampsImportError: "That PDF could not be imported as a stamp library.",
|
|
321
|
+
quickStampsLabel: "Quick stamps",
|
|
322
|
+
signaturesTitle: "Signatures",
|
|
323
|
+
signaturesEmpty: "No signatures yet. Create one to sign with.",
|
|
324
|
+
signaturesNew: "New signature…",
|
|
325
|
+
signaturesArmedHint: "Click a signature field to sign it, or anywhere on a page to place the mark.",
|
|
326
|
+
signaturesTargetHint: "Pick a mark for the selected field.",
|
|
327
|
+
signaturesInDocument: "In this document",
|
|
328
|
+
signaturesNone: "This document has no signature fields.",
|
|
329
|
+
signaturesValidate: "Validate",
|
|
330
|
+
signaturesValidating: "Validating…",
|
|
331
|
+
signaturesSigned: "Signed",
|
|
332
|
+
signaturesUnsigned: "Unsigned",
|
|
333
|
+
signaturesRename: "Rename",
|
|
334
|
+
signaturesRemove: "Remove",
|
|
335
|
+
signaturesExport: "Export as PDF",
|
|
336
|
+
signaturesAddInitials: "Add initials",
|
|
337
|
+
signatureLabel: "Signature",
|
|
338
|
+
initialsLabel: "Initials",
|
|
339
|
+
makerTitle: "New signature",
|
|
340
|
+
makerName: "Name",
|
|
341
|
+
makerNamePlaceholder: "Your name",
|
|
342
|
+
makerDraw: "Draw",
|
|
343
|
+
makerType: "Type",
|
|
344
|
+
makerUpload: "Upload",
|
|
345
|
+
makerTypePlaceholder: "Type your name",
|
|
346
|
+
makerFont: "Font",
|
|
347
|
+
makerUploadHint: "Choose a PNG, JPEG, or one-page PDF",
|
|
348
|
+
makerClear: "Clear",
|
|
349
|
+
makerOptional: "(optional)",
|
|
350
|
+
makerSave: "Save",
|
|
351
|
+
makerSaving: "Saving…",
|
|
352
|
+
makerError: "The signature could not be saved.",
|
|
353
|
+
signTitle: "Sign this field",
|
|
354
|
+
signReason: "Reason",
|
|
355
|
+
signLocation: "Location",
|
|
356
|
+
signKind: "What may change afterwards",
|
|
357
|
+
signKindApproval: "Approval signature — form filling and more signatures stay valid",
|
|
358
|
+
signKindCertifyAnnotate: "Certify — allow comments, form filling and signatures",
|
|
359
|
+
signKindCertifyFill: "Certify — allow form filling and signatures only",
|
|
360
|
+
signKindCertifyLocked: "Certify — allow no changes",
|
|
361
|
+
signButton: "Sign",
|
|
362
|
+
signSigning: "Signing…",
|
|
363
|
+
signFill: "Place without signing",
|
|
364
|
+
signError: "The field could not be signed.",
|
|
365
|
+
inspectTitle: "Signature",
|
|
366
|
+
inspectSigner: "Signed by",
|
|
367
|
+
inspectReason: "Reason",
|
|
368
|
+
inspectLocation: "Location",
|
|
369
|
+
inspectTime: "Time",
|
|
370
|
+
inspectIntegrity: "Document",
|
|
371
|
+
inspectCryptography: "Signature",
|
|
372
|
+
inspectTrust: "Signer",
|
|
373
|
+
inspectChanges: "Changes since",
|
|
374
|
+
inspectDownload: "Download signed version",
|
|
375
|
+
verdictValid: "Valid",
|
|
376
|
+
verdictValidUntrusted: "Valid, signer not trusted",
|
|
377
|
+
verdictInvalid: "Invalid — modified after signing",
|
|
378
|
+
verdictWillInvalidate: "Valid, invalid once saved",
|
|
379
|
+
signaturesInvalidating: "This change will invalidate {field} when the document is saved. Undo it to keep the signature.",
|
|
380
|
+
verdictIndeterminate: "Could not be verified",
|
|
381
|
+
verdictUnchanged: "None",
|
|
382
|
+
verdictRestored: "Changed, then restored",
|
|
383
|
+
inspectLaterRevisions: "Revisions after signing",
|
|
384
|
+
verdictPermitted: "Permitted",
|
|
385
|
+
verdictForbidden: "Not permitted"
|
|
298
386
|
}
|
|
299
387
|
}
|
|
300
388
|
};
|
|
@@ -311,7 +399,10 @@ const en = {
|
|
|
311
399
|
*/
|
|
312
400
|
const ViewerConfigContext = (0, react.createContext)({
|
|
313
401
|
chrome: require_chrome.defaultChrome,
|
|
314
|
-
icons: {}
|
|
402
|
+
icons: {},
|
|
403
|
+
stamps: {},
|
|
404
|
+
signatures: {},
|
|
405
|
+
annotations: {}
|
|
315
406
|
});
|
|
316
407
|
const ViewerConfigProvider = ViewerConfigContext.Provider;
|
|
317
408
|
function useChromeSchema() {
|
|
@@ -326,6 +417,15 @@ function useStripSchema(id) {
|
|
|
326
417
|
function useCustomIcons() {
|
|
327
418
|
return (0, react.useContext)(ViewerConfigContext).icons;
|
|
328
419
|
}
|
|
420
|
+
function useStampsConfig() {
|
|
421
|
+
return (0, react.useContext)(ViewerConfigContext).stamps;
|
|
422
|
+
}
|
|
423
|
+
function useSignaturesConfig() {
|
|
424
|
+
return (0, react.useContext)(ViewerConfigContext).signatures;
|
|
425
|
+
}
|
|
426
|
+
function useAnnotationsConfig() {
|
|
427
|
+
return (0, react.useContext)(ViewerConfigContext).annotations;
|
|
428
|
+
}
|
|
329
429
|
//#endregion
|
|
330
430
|
//#region src/handle.ts
|
|
331
431
|
function createViewerHandle(kernel) {
|
|
@@ -947,6 +1047,166 @@ function ThemeProvider({ children, preference = "system", target }) {
|
|
|
947
1047
|
});
|
|
948
1048
|
}
|
|
949
1049
|
//#endregion
|
|
1050
|
+
//#region src/config/default-stamps.ts
|
|
1051
|
+
/**
|
|
1052
|
+
* The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
|
|
1053
|
+
* standard rubber stamps as one Acrobat-compatible PDF per locale.
|
|
1054
|
+
*
|
|
1055
|
+
* Delivery: the library ships INSIDE whatever ships this viewer. The package
|
|
1056
|
+
* exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
|
|
1057
|
+
* so it travels through the module graph like any other code — every bundler
|
|
1058
|
+
* splits it into a chunk served from the app's own origin, the CDN snippet
|
|
1059
|
+
* carries it as a sibling chunk in its folder, and nothing is fetched from a
|
|
1060
|
+
* third party. `stamps.defaultLibrary` overrides that:
|
|
1061
|
+
* - `false` → no built-in library, no request (air-gapped);
|
|
1062
|
+
* - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
|
|
1063
|
+
*
|
|
1064
|
+
* Loading is LAZY (first open of the stamps panel, never at boot) and
|
|
1065
|
+
* locale-aware: the chrome locale leads, the browser languages break ties
|
|
1066
|
+
* for the locales the chrome does not translate itself, and a locale change
|
|
1067
|
+
* while a default library is loaded swaps it for the new one. The file names
|
|
1068
|
+
* itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
|
|
1069
|
+
*/
|
|
1070
|
+
/** The built-in library's PieceInfo id — excluded from persistence (it is
|
|
1071
|
+
* loaded again on every first open, in the locale of that moment). */
|
|
1072
|
+
const DEFAULT_LIBRARY_ID = "embedpdf-standard";
|
|
1073
|
+
/** Which shipped locale to show for the chrome's locale + the browser's. */
|
|
1074
|
+
function resolveStampsLocale(chromeLocale) {
|
|
1075
|
+
return (0, _embedpdf_react_i18n.negotiateLocale)(_embedpdf_default_stamps_library.LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
|
|
1076
|
+
}
|
|
1077
|
+
const fetchBytes = async (url) => {
|
|
1078
|
+
const response = await fetch(url);
|
|
1079
|
+
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
1080
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1081
|
+
};
|
|
1082
|
+
/** The library bytes for `locale`: your URL when you gave one, otherwise the
|
|
1083
|
+
* copy that shipped with the viewer. Never anything else. */
|
|
1084
|
+
const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : (0, _embedpdf_default_stamps_library.loadDefaultLibrary)(locale);
|
|
1085
|
+
/** Per workspace: the locale the default library was last brought to, and
|
|
1086
|
+
* the in-flight work — the panel mounts and unmounts with the sidebar, so
|
|
1087
|
+
* this cannot live in component state. */
|
|
1088
|
+
const loads = /* @__PURE__ */ new WeakMap();
|
|
1089
|
+
/**
|
|
1090
|
+
* Bring the built-in library to `locale`. Idempotent per workspace and
|
|
1091
|
+
* locale; a locale change swaps the loaded default (remove, then import);
|
|
1092
|
+
* a default the user removed in this session stays removed — the locale is
|
|
1093
|
+
* recorded, nothing is re-added. An embedder that seeded a library under
|
|
1094
|
+
* the same id is left alone.
|
|
1095
|
+
*/
|
|
1096
|
+
function ensureDefaultLibrary(stamp, locale, source) {
|
|
1097
|
+
if (source === false) return Promise.resolve();
|
|
1098
|
+
const current = loads.get(stamp);
|
|
1099
|
+
if (current?.locale === locale) return current.promise;
|
|
1100
|
+
const run = async () => {
|
|
1101
|
+
await current?.promise.catch(() => void 0);
|
|
1102
|
+
const loaded = stamp.library(DEFAULT_LIBRARY_ID);
|
|
1103
|
+
if (loaded?.locale === locale) return;
|
|
1104
|
+
if (current && !loaded) return;
|
|
1105
|
+
if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
|
|
1106
|
+
await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
|
|
1107
|
+
};
|
|
1108
|
+
const promise = run();
|
|
1109
|
+
loads.set(stamp, {
|
|
1110
|
+
locale,
|
|
1111
|
+
promise
|
|
1112
|
+
});
|
|
1113
|
+
return promise;
|
|
1114
|
+
}
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region src/ui/stamp-store.tsx
|
|
1117
|
+
/**
|
|
1118
|
+
* The user's stamp libraries live in the browser (IndexedDB): restored ONCE
|
|
1119
|
+
* per workspace, written on every change, for as long as the viewer lives —
|
|
1120
|
+
* a signature made while the stamps sidebar is closed is persisted exactly
|
|
1121
|
+
* like a stamp imported while it is open. The built-in library is never
|
|
1122
|
+
* stored (it is fetched again next time, in the locale of that moment); the
|
|
1123
|
+
* panels that need it bring it in lazily through `ensureDefaultLibrary`.
|
|
1124
|
+
*
|
|
1125
|
+
* Mounted once in the Shell, workspace-scoped (the stamp plugin is).
|
|
1126
|
+
*/
|
|
1127
|
+
const store = typeof indexedDB === "undefined" ? null : (0, _embedpdf_react_stamp.indexedDbByteStore)("embedpdf-stamps", { storeName: "libraries" });
|
|
1128
|
+
const restored = /* @__PURE__ */ new WeakMap();
|
|
1129
|
+
/** Bring the stored libraries back — once per workspace, whoever asks first. */
|
|
1130
|
+
const restoreStampLibrariesOnce = (stamp) => {
|
|
1131
|
+
let pending = restored.get(stamp);
|
|
1132
|
+
if (!pending) {
|
|
1133
|
+
pending = store ? (0, _embedpdf_react_stamp.restoreStampLibraries)(stamp, store) : Promise.resolve();
|
|
1134
|
+
restored.set(stamp, pending);
|
|
1135
|
+
}
|
|
1136
|
+
return pending;
|
|
1137
|
+
};
|
|
1138
|
+
function StampLibraryStore() {
|
|
1139
|
+
const stamp = (0, _embedpdf_react_stamp.useStamp)();
|
|
1140
|
+
(0, react.useEffect)(() => {
|
|
1141
|
+
restoreStampLibrariesOnce(stamp);
|
|
1142
|
+
return store ? (0, _embedpdf_react_stamp.persistStampLibraries)(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0;
|
|
1143
|
+
}, [stamp]);
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/ui/quick-stamps.tsx
|
|
1148
|
+
/**
|
|
1149
|
+
* Quick marks in the toolbar: a row of stamp thumbnails that ARM on click —
|
|
1150
|
+
* the checkmark, the cross, whatever the embedder configured
|
|
1151
|
+
* (`stamps.toolbar`: asset ids, or one library's assets). The hover ghost and
|
|
1152
|
+
* the click placement are the annotation plugin's, exactly as from the
|
|
1153
|
+
* sidebar; only the picker moved. Renders nothing until the assets exist.
|
|
1154
|
+
* Lives in the workspace toolbar, so it reads the ACTIVE document's tool.
|
|
1155
|
+
*/
|
|
1156
|
+
function QuickStamps() {
|
|
1157
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
1158
|
+
const stamp = (0, _embedpdf_react_stamp.useStamp)();
|
|
1159
|
+
const config = useStampsConfig();
|
|
1160
|
+
const { locale } = (0, _embedpdf_react_i18n.useLocale)();
|
|
1161
|
+
const assets = (0, _embedpdf_react_stamp.useStampAssets)();
|
|
1162
|
+
const documentId = (0, _embedpdf_react_runtime.useDocumentId)();
|
|
1163
|
+
const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
|
|
1164
|
+
const activeToolId = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_interaction.InteractionToken, (c) => c.activeToolId(), null);
|
|
1165
|
+
const armedId = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_stamp.StampToken, (c) => documentId ? c.armedAsset(documentId)?.id ?? null : null, null);
|
|
1166
|
+
const ids = Array.isArray(config.toolbar) ? config.toolbar : config.toolbar && "library" in config.toolbar ? stamp.library(config.toolbar.library)?.assetIds ?? [] : [];
|
|
1167
|
+
const wantsDefault = ids.some((id) => id.startsWith(`${DEFAULT_LIBRARY_ID}:`));
|
|
1168
|
+
(0, react.useEffect)(() => {
|
|
1169
|
+
if (!wantsDefault) return;
|
|
1170
|
+
restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), config.defaultLibrary)).catch((err) => console.error("[embedpdf] quick stamps failed:", err));
|
|
1171
|
+
}, [
|
|
1172
|
+
stamp,
|
|
1173
|
+
locale,
|
|
1174
|
+
wantsDefault
|
|
1175
|
+
]);
|
|
1176
|
+
const shown = ids.map((id) => assets.find((a) => a.id === id)).filter((a) => !!a);
|
|
1177
|
+
if (shown.length === 0 || !documentId) return null;
|
|
1178
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1179
|
+
className: "flex items-center gap-1",
|
|
1180
|
+
role: "group",
|
|
1181
|
+
"aria-label": t("demo.quickStampsLabel"),
|
|
1182
|
+
children: shown.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStampButton, {
|
|
1183
|
+
asset,
|
|
1184
|
+
active: activeToolId === "stamp" && armedId === asset.id,
|
|
1185
|
+
onArm: () => void armAsset(asset.id).catch((e) => console.error("[embedpdf] arm failed", e))
|
|
1186
|
+
}, asset.id))
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
function QuickStampButton({ asset, active, onArm }) {
|
|
1190
|
+
const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
|
|
1191
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1192
|
+
type: "button",
|
|
1193
|
+
onClick: onArm,
|
|
1194
|
+
title: asset.label,
|
|
1195
|
+
"aria-label": asset.label,
|
|
1196
|
+
"aria-pressed": active || void 0,
|
|
1197
|
+
className: buttonClass(active, true),
|
|
1198
|
+
children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1199
|
+
src: url,
|
|
1200
|
+
alt: "",
|
|
1201
|
+
className: "h-5 w-5 object-contain",
|
|
1202
|
+
draggable: false
|
|
1203
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1204
|
+
className: "text-xs",
|
|
1205
|
+
children: asset.label
|
|
1206
|
+
})
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
//#endregion
|
|
950
1210
|
//#region src/ui/menu.tsx
|
|
951
1211
|
/**
|
|
952
1212
|
* Menu rendering — a MenuSchema (a tree of command ids) rendered as rows.
|
|
@@ -1355,7 +1615,10 @@ function AppToolbar({ bar, className }) {
|
|
|
1355
1615
|
variant,
|
|
1356
1616
|
run
|
|
1357
1617
|
}),
|
|
1358
|
-
renderCustom: {
|
|
1618
|
+
renderCustom: {
|
|
1619
|
+
"zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomControls, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomButton, {}),
|
|
1620
|
+
"quick-stamps": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStamps, {})
|
|
1621
|
+
},
|
|
1359
1622
|
renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedModes, { view }) : void 0,
|
|
1360
1623
|
renderGroupTrigger: (view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupTrigger, { view }),
|
|
1361
1624
|
renderSeparator: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "bg-border h-6 w-px self-center" }),
|
|
@@ -1367,6 +1630,71 @@ function AppToolbar({ bar, className }) {
|
|
|
1367
1630
|
});
|
|
1368
1631
|
}
|
|
1369
1632
|
//#endregion
|
|
1633
|
+
//#region src/ui/annotation-fonts.tsx
|
|
1634
|
+
/**
|
|
1635
|
+
* The configured annotation fonts (`annotations.fonts`), loaded once per
|
|
1636
|
+
* viewer: fetch the bytes → register them on the viewer's engine under the
|
|
1637
|
+
* key (what a free-text `fontFamily` carries and what the engine embeds) →
|
|
1638
|
+
* mount the same bytes as a `@font-face` named by that key (what the plugin's
|
|
1639
|
+
* `cssFontFamily` emits for a registered font, so the live editor renders the
|
|
1640
|
+
* face the appearance stream will bake). The style panel lists a font only
|
|
1641
|
+
* once BOTH have happened — an offered key always resolves on write.
|
|
1642
|
+
*/
|
|
1643
|
+
const LoadedFontsContext = (0, react.createContext)([]);
|
|
1644
|
+
function AnnotationFontsProvider({ children }) {
|
|
1645
|
+
const kernel = (0, _embedpdf_react_runtime.useKernel)();
|
|
1646
|
+
const { fonts } = useAnnotationsConfig();
|
|
1647
|
+
const [loaded, setLoaded] = (0, react.useState)([]);
|
|
1648
|
+
(0, react.useEffect)(() => {
|
|
1649
|
+
const engineFonts = kernel.engine.fonts;
|
|
1650
|
+
if (!fonts?.length || !engineFonts) return;
|
|
1651
|
+
let cancelled = false;
|
|
1652
|
+
const unmounts = [];
|
|
1653
|
+
(async () => {
|
|
1654
|
+
for (const font of fonts) try {
|
|
1655
|
+
const response = await fetch(font.url);
|
|
1656
|
+
if (!response.ok) throw new Error(`${font.url}: HTTP ${response.status}`);
|
|
1657
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1658
|
+
if (cancelled) return;
|
|
1659
|
+
await engineFonts.register({
|
|
1660
|
+
key: font.key,
|
|
1661
|
+
familyName: font.familyName,
|
|
1662
|
+
weight: font.weight,
|
|
1663
|
+
italic: font.italic,
|
|
1664
|
+
data: bytes
|
|
1665
|
+
});
|
|
1666
|
+
const unmount = await (0, _embedpdf_web.mountWebFont)(font.key, bytes, {
|
|
1667
|
+
weight: font.weight,
|
|
1668
|
+
style: font.italic ? "italic" : "normal"
|
|
1669
|
+
});
|
|
1670
|
+
if (cancelled) {
|
|
1671
|
+
unmount();
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
unmounts.push(unmount);
|
|
1675
|
+
setLoaded((prev) => prev.some((f) => f.key === font.key) ? prev : [...prev, {
|
|
1676
|
+
key: font.key,
|
|
1677
|
+
label: font.label
|
|
1678
|
+
}]);
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
console.warn(`[embedpdf] annotation font "${font.key}" was not loaded:`, error);
|
|
1681
|
+
}
|
|
1682
|
+
})();
|
|
1683
|
+
return () => {
|
|
1684
|
+
cancelled = true;
|
|
1685
|
+
for (const unmount of unmounts) unmount();
|
|
1686
|
+
};
|
|
1687
|
+
}, [kernel, fonts]);
|
|
1688
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadedFontsContext.Provider, {
|
|
1689
|
+
value: loaded,
|
|
1690
|
+
children
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
/** The configured fonts that are registered AND mounted, in config order. */
|
|
1694
|
+
function useAnnotationFonts() {
|
|
1695
|
+
return (0, react.useContext)(LoadedFontsContext);
|
|
1696
|
+
}
|
|
1697
|
+
//#endregion
|
|
1370
1698
|
//#region src/ui/link-editor.tsx
|
|
1371
1699
|
/**
|
|
1372
1700
|
* The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
|
|
@@ -2352,12 +2680,17 @@ function LineEndingSelect({ side, value, onChange }) {
|
|
|
2352
2680
|
});
|
|
2353
2681
|
}
|
|
2354
2682
|
function FontFamilySelect({ value, onChange }) {
|
|
2683
|
+
const configured = useAnnotationFonts();
|
|
2684
|
+
const options = [...FONT_OPTIONS, ...configured.map((f) => ({
|
|
2685
|
+
v: f.key,
|
|
2686
|
+
label: f.label
|
|
2687
|
+
}))];
|
|
2355
2688
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownShell, {
|
|
2356
2689
|
trigger: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2357
2690
|
className: "text-fg text-sm",
|
|
2358
|
-
children:
|
|
2691
|
+
children: options.find((o) => o.v === value)?.label ?? value
|
|
2359
2692
|
}),
|
|
2360
|
-
children: (close) =>
|
|
2693
|
+
children: (close) => options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
|
|
2361
2694
|
selected: o.v === value,
|
|
2362
2695
|
onClick: () => {
|
|
2363
2696
|
onChange(o.v);
|
|
@@ -2369,17 +2702,33 @@ function FontFamilySelect({ value, onChange }) {
|
|
|
2369
2702
|
}
|
|
2370
2703
|
function FontSizeCombo({ value, onChange }) {
|
|
2371
2704
|
const [open, setOpen] = (0, react.useState)(false);
|
|
2705
|
+
const rootRef = useOutsideClose(open, () => setOpen(false));
|
|
2706
|
+
const [draft, setDraft] = (0, react.useState)(null);
|
|
2707
|
+
const commit = () => {
|
|
2708
|
+
if (draft === null) return;
|
|
2709
|
+
const n = parseInt(draft, 10);
|
|
2710
|
+
setDraft(null);
|
|
2711
|
+
if (Number.isFinite(n) && n > 0 && n !== value) onChange(n);
|
|
2712
|
+
};
|
|
2372
2713
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2373
|
-
ref:
|
|
2714
|
+
ref: rootRef,
|
|
2374
2715
|
className: "relative w-full",
|
|
2375
2716
|
children: [
|
|
2376
2717
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2377
2718
|
type: "number",
|
|
2378
2719
|
min: 1,
|
|
2379
|
-
value,
|
|
2380
|
-
onChange: (e) =>
|
|
2381
|
-
|
|
2382
|
-
|
|
2720
|
+
value: draft ?? value,
|
|
2721
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2722
|
+
onBlur: commit,
|
|
2723
|
+
onKeyDown: (e) => {
|
|
2724
|
+
if (e.key === "Enter") {
|
|
2725
|
+
e.preventDefault();
|
|
2726
|
+
commit();
|
|
2727
|
+
setOpen(false);
|
|
2728
|
+
} else if (e.key === "Escape") {
|
|
2729
|
+
setDraft(null);
|
|
2730
|
+
setOpen(false);
|
|
2731
|
+
}
|
|
2383
2732
|
},
|
|
2384
2733
|
onClick: () => setOpen(true),
|
|
2385
2734
|
className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 pr-7 text-sm"
|
|
@@ -2400,6 +2749,7 @@ function FontSizeCombo({ value, onChange }) {
|
|
|
2400
2749
|
children: FONT_SIZES.map((sz) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
|
|
2401
2750
|
selected: sz === value,
|
|
2402
2751
|
onClick: () => {
|
|
2752
|
+
setDraft(null);
|
|
2403
2753
|
onChange(sz);
|
|
2404
2754
|
setOpen(false);
|
|
2405
2755
|
},
|
|
@@ -2420,15 +2770,45 @@ function BlendModeSelect({ value, onChange }) {
|
|
|
2420
2770
|
}, mode.v))
|
|
2421
2771
|
});
|
|
2422
2772
|
}
|
|
2423
|
-
function Toggle({ active, title, onClick, children }) {
|
|
2773
|
+
function Toggle({ active, title, onClick, keepFocus, children }) {
|
|
2424
2774
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2425
2775
|
type: "button",
|
|
2426
2776
|
title,
|
|
2427
2777
|
onClick,
|
|
2778
|
+
onMouseDown: keepFocus ? (e) => e.preventDefault() : void 0,
|
|
2428
2779
|
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"}`,
|
|
2429
2780
|
children
|
|
2430
2781
|
});
|
|
2431
2782
|
}
|
|
2783
|
+
const isFormatSpec = (spec) => spec.key === "bold" || spec.key === "italic" || spec.key === "underline";
|
|
2784
|
+
/**
|
|
2785
|
+
* The format toggles a free-text kind declares, in one row. While the text
|
|
2786
|
+
* editor holds a range they read and write that range's runs (the plugin
|
|
2787
|
+
* routes `updateSelection`); otherwise the annotation's body. The buttons
|
|
2788
|
+
* keep focus in the editor so the range survives the click.
|
|
2789
|
+
*/
|
|
2790
|
+
function FormatToggles({ specs, values, mixed, onChange }) {
|
|
2791
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2792
|
+
label: (0, _embedpdf_react_i18n.useT)()("demo.formatLabel", { fallback: "Format" }),
|
|
2793
|
+
mixed: specs.some((s) => mixed.includes(s.key)),
|
|
2794
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2795
|
+
className: "flex gap-2",
|
|
2796
|
+
children: specs.map((spec) => {
|
|
2797
|
+
const active = values[spec.key] === true && !mixed.includes(spec.key);
|
|
2798
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
|
|
2799
|
+
title: spec.label,
|
|
2800
|
+
active,
|
|
2801
|
+
keepFocus: true,
|
|
2802
|
+
onClick: () => onChange({ [spec.key]: !active }),
|
|
2803
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2804
|
+
name: spec.key,
|
|
2805
|
+
size: 18
|
|
2806
|
+
})
|
|
2807
|
+
}, spec.key);
|
|
2808
|
+
})
|
|
2809
|
+
})
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2432
2812
|
function PropControl({ spec, value, mixed, onChange }) {
|
|
2433
2813
|
switch (spec.key) {
|
|
2434
2814
|
case "color":
|
|
@@ -2563,6 +2943,9 @@ function PropControl({ spec, value, mixed, onChange }) {
|
|
|
2563
2943
|
})
|
|
2564
2944
|
});
|
|
2565
2945
|
}
|
|
2946
|
+
case "bold":
|
|
2947
|
+
case "italic":
|
|
2948
|
+
case "underline": return null;
|
|
2566
2949
|
case "blendMode": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2567
2950
|
label: spec.label,
|
|
2568
2951
|
mixed,
|
|
@@ -2610,7 +2993,12 @@ function AnnotationStylePanel() {
|
|
|
2610
2993
|
className: "text-fg-muted mb-4 text-[11px] font-semibold uppercase tracking-wide",
|
|
2611
2994
|
children: hasSel ? `${selected.length} selected` : `${activeToolId} defaults`
|
|
2612
2995
|
}),
|
|
2613
|
-
specs.map((spec) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2996
|
+
specs.map((spec) => spec.key === "bold" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormatToggles, {
|
|
2997
|
+
specs: specs.filter((s) => isFormatSpec(s)),
|
|
2998
|
+
values,
|
|
2999
|
+
mixed: hasSel ? sel.mixed : [],
|
|
3000
|
+
onChange: write
|
|
3001
|
+
}, "format") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PropControl, {
|
|
2614
3002
|
spec,
|
|
2615
3003
|
value: values[spec.key],
|
|
2616
3004
|
mixed: hasSel && sel.mixed.includes(spec.key),
|
|
@@ -3481,6 +3869,515 @@ function SearchPanel() {
|
|
|
3481
3869
|
});
|
|
3482
3870
|
}
|
|
3483
3871
|
//#endregion
|
|
3872
|
+
//#region src/ui/stamps-panel.tsx
|
|
3873
|
+
/**
|
|
3874
|
+
* The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
|
|
3875
|
+
* The stamp plugin owns the libraries, the asset bytes and the cached
|
|
3876
|
+
* previews; this panel is the PICKER over them:
|
|
3877
|
+
*
|
|
3878
|
+
* a library <select> ("All stamps" + one per library, shown once there
|
|
3879
|
+
* are two) → a two-column grid of thumbnails → click a stamp → `armAsset`
|
|
3880
|
+
* arms the annotation plugin's stamp tool → hovering a page ghosts the
|
|
3881
|
+
* exact placement → each click on a page places one.
|
|
3882
|
+
*
|
|
3883
|
+
* Deliberately NOT a file dialog: picking arbitrary image bytes is the Image
|
|
3884
|
+
* tool's job (a click-then-pick tool, `insert:add-image`). The stamp button
|
|
3885
|
+
* opens a library; a library is a set of reusable, named assets — the two
|
|
3886
|
+
* gestures are different, so they are different controls.
|
|
3887
|
+
*
|
|
3888
|
+
* The panel stays open while you place, so a stamp can be dropped on several
|
|
3889
|
+
* pages and swapped without a round trip through the toolbar. A command can
|
|
3890
|
+
* open it ON a library (`annotation:stamp-from-selection` → "My stamps")
|
|
3891
|
+
* through the surface's open props.
|
|
3892
|
+
*/
|
|
3893
|
+
const ALL = "all";
|
|
3894
|
+
function StampsPanel() {
|
|
3895
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
3896
|
+
const stamp = (0, _embedpdf_react_stamp.useStamp)();
|
|
3897
|
+
const { defaultLibrary, sidebar } = useStampsConfig();
|
|
3898
|
+
const libraries = (0, _embedpdf_react_stamp.useStampLibraries)({ kind: sidebar ?? ["stamps"] });
|
|
3899
|
+
const assets = (0, _embedpdf_react_stamp.useStampAssets)();
|
|
3900
|
+
const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
|
|
3901
|
+
const { activeToolId } = (0, _embedpdf_react_interaction.useTool)();
|
|
3902
|
+
const { locale } = (0, _embedpdf_react_i18n.useLocale)();
|
|
3903
|
+
const surface = (0, _embedpdf_react_shell.useSurface)("stamps");
|
|
3904
|
+
const fileRef = (0, react.useRef)(null);
|
|
3905
|
+
const [armedId, setArmedId] = (0, react.useState)(null);
|
|
3906
|
+
const [busy, setBusy] = (0, react.useState)(null);
|
|
3907
|
+
const [error, setError] = (0, react.useState)(null);
|
|
3908
|
+
const [picked, setPicked] = (0, react.useState)(ALL);
|
|
3909
|
+
const openedOn = surface.props?.libraryId;
|
|
3910
|
+
(0, react.useEffect)(() => {
|
|
3911
|
+
if (typeof openedOn === "string") setPicked(openedOn);
|
|
3912
|
+
}, [openedOn]);
|
|
3913
|
+
(0, react.useEffect)(() => {
|
|
3914
|
+
let live = true;
|
|
3915
|
+
setBusy("loading");
|
|
3916
|
+
restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
|
|
3917
|
+
console.error("[embedpdf] default stamps failed:", err);
|
|
3918
|
+
if (live) setError(t("demo.stampsError"));
|
|
3919
|
+
}).finally(() => live && setBusy(null));
|
|
3920
|
+
return () => {
|
|
3921
|
+
live = false;
|
|
3922
|
+
};
|
|
3923
|
+
}, [stamp, locale]);
|
|
3924
|
+
(0, react.useEffect)(() => {
|
|
3925
|
+
if (activeToolId !== "stamp") setArmedId(null);
|
|
3926
|
+
}, [activeToolId]);
|
|
3927
|
+
const arm = (asset) => {
|
|
3928
|
+
setError(null);
|
|
3929
|
+
setArmedId(asset.id);
|
|
3930
|
+
armAsset(asset.id).catch((err) => {
|
|
3931
|
+
console.error("[embedpdf] arm stamp failed:", err);
|
|
3932
|
+
setArmedId(null);
|
|
3933
|
+
setError(t("demo.stampsArmError"));
|
|
3934
|
+
});
|
|
3935
|
+
};
|
|
3936
|
+
const importPdf = (file) => {
|
|
3937
|
+
setError(null);
|
|
3938
|
+
setBusy("importing");
|
|
3939
|
+
stamp.importLibraryPdf(file, { name: file.name.replace(/\.pdf$/i, "") }).then((id) => setPicked(id)).catch((err) => {
|
|
3940
|
+
console.error("[embedpdf] stamp library import failed:", err);
|
|
3941
|
+
setError(t("demo.stampsImportError"));
|
|
3942
|
+
}).finally(() => setBusy(null));
|
|
3943
|
+
};
|
|
3944
|
+
/** The library as the PDF it is — title, registry, artwork — for Acrobat
|
|
3945
|
+
* or another viewer. */
|
|
3946
|
+
const exportPdf = (libraryId, name) => {
|
|
3947
|
+
const bytes = stamp.exportLibrary(libraryId);
|
|
3948
|
+
if (!bytes) return;
|
|
3949
|
+
const blob = new Blob([bytes], { type: "application/pdf" });
|
|
3950
|
+
const url = URL.createObjectURL(blob);
|
|
3951
|
+
const a = document.createElement("a");
|
|
3952
|
+
a.href = url;
|
|
3953
|
+
a.download = `${name || "stamps"}.pdf`;
|
|
3954
|
+
a.click();
|
|
3955
|
+
URL.revokeObjectURL(url);
|
|
3956
|
+
};
|
|
3957
|
+
const selectedId = libraries.length === 1 ? libraries[0].id : libraries.some((library) => library.id === picked) ? picked : ALL;
|
|
3958
|
+
const selected = selectedId === ALL ? null : libraries.find((l) => l.id === selectedId) ?? null;
|
|
3959
|
+
const shown = selectedId === ALL ? assets : assets.filter((a) => a.libraryId === selectedId);
|
|
3960
|
+
const isDefault = (libraryId) => libraryId === DEFAULT_LIBRARY_ID;
|
|
3961
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3962
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
3963
|
+
children: [
|
|
3964
|
+
libraries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3965
|
+
className: "border-border-subtle flex items-center gap-1 border-b p-2",
|
|
3966
|
+
children: [libraries.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3967
|
+
value: selectedId,
|
|
3968
|
+
onChange: (event) => setPicked(event.target.value),
|
|
3969
|
+
"aria-label": t("demo.stampsTitle"),
|
|
3970
|
+
className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-2 py-1.5 text-sm",
|
|
3971
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3972
|
+
value: ALL,
|
|
3973
|
+
children: t("demo.stampsAll")
|
|
3974
|
+
}), libraries.map((library) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3975
|
+
value: library.id,
|
|
3976
|
+
children: library.name
|
|
3977
|
+
}, library.id))]
|
|
3978
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3979
|
+
className: "text-fg-muted min-w-0 flex-1 truncate px-1 text-xs font-semibold uppercase tracking-wide",
|
|
3980
|
+
children: libraries[0].name
|
|
3981
|
+
}), selected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3982
|
+
type: "button",
|
|
3983
|
+
onClick: () => exportPdf(selected.id, selected.name),
|
|
3984
|
+
title: t("demo.stampsExportLibrary"),
|
|
3985
|
+
className: "text-fg-muted hover:text-fg grid h-7 w-7 shrink-0 place-items-center rounded",
|
|
3986
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
3987
|
+
name: "download",
|
|
3988
|
+
size: 14
|
|
3989
|
+
})
|
|
3990
|
+
}), !isDefault(selected.id) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3991
|
+
type: "button",
|
|
3992
|
+
onClick: () => void stamp.removeLibrary(selected.id),
|
|
3993
|
+
title: t("demo.stampsRemoveLibrary"),
|
|
3994
|
+
className: "text-fg-muted hover:text-fg grid h-7 w-7 shrink-0 place-items-center rounded",
|
|
3995
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
3996
|
+
name: "trash",
|
|
3997
|
+
size: 14
|
|
3998
|
+
})
|
|
3999
|
+
})] })]
|
|
4000
|
+
}),
|
|
4001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4002
|
+
className: "min-h-0 flex-1 overflow-y-auto p-3",
|
|
4003
|
+
children: busy === "loading" && shown.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4004
|
+
className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
|
|
4005
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-border-subtle border-t-accent h-6 w-6 animate-spin rounded-full border-2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4006
|
+
className: "text-fg-muted text-sm",
|
|
4007
|
+
children: t("demo.stampsLoading")
|
|
4008
|
+
})]
|
|
4009
|
+
}) : shown.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4010
|
+
className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
|
|
4011
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4012
|
+
name: "rubberStamp",
|
|
4013
|
+
size: 32,
|
|
4014
|
+
className: "text-fg-muted"
|
|
4015
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4016
|
+
className: "text-fg-muted text-sm",
|
|
4017
|
+
children: t("demo.stampsEmpty")
|
|
4018
|
+
})]
|
|
4019
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4020
|
+
className: "grid grid-cols-2 gap-3",
|
|
4021
|
+
children: shown.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4022
|
+
className: "group relative",
|
|
4023
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampAssetCell, {
|
|
4024
|
+
asset,
|
|
4025
|
+
armed: armedId === asset.id,
|
|
4026
|
+
onArm: () => arm(asset)
|
|
4027
|
+
}), !isDefault(asset.libraryId) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4028
|
+
type: "button",
|
|
4029
|
+
onClick: () => void stamp.removeAsset(asset.id),
|
|
4030
|
+
title: t("demo.stampsRemoveStamp"),
|
|
4031
|
+
className: "bg-surface border-border text-fg-muted hover:text-fg absolute right-1 top-1 grid h-5 w-5 place-items-center rounded-full border opacity-0 shadow-sm focus:opacity-100 group-hover:opacity-100",
|
|
4032
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4033
|
+
name: "x",
|
|
4034
|
+
size: 12
|
|
4035
|
+
})
|
|
4036
|
+
})]
|
|
4037
|
+
}, asset.id))
|
|
4038
|
+
})
|
|
4039
|
+
}),
|
|
4040
|
+
(error || armedId) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4041
|
+
className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
|
|
4042
|
+
children: error ?? t("demo.stampsArmedHint")
|
|
4043
|
+
}),
|
|
4044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4045
|
+
className: "border-border-subtle shrink-0 border-t p-3",
|
|
4046
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4047
|
+
type: "button",
|
|
4048
|
+
disabled: busy != null,
|
|
4049
|
+
onClick: () => fileRef.current?.click(),
|
|
4050
|
+
title: t("demo.stampsImportHint"),
|
|
4051
|
+
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 disabled:opacity-40",
|
|
4052
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4053
|
+
name: "rubberStampPlus",
|
|
4054
|
+
size: 16
|
|
4055
|
+
}), busy === "importing" ? t("demo.stampsImporting") : t("demo.stampsImport")]
|
|
4056
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4057
|
+
ref: fileRef,
|
|
4058
|
+
type: "file",
|
|
4059
|
+
accept: "application/pdf",
|
|
4060
|
+
className: "hidden",
|
|
4061
|
+
onChange: (e) => {
|
|
4062
|
+
const file = e.currentTarget.files?.[0];
|
|
4063
|
+
e.currentTarget.value = "";
|
|
4064
|
+
if (file) importPdf(file);
|
|
4065
|
+
}
|
|
4066
|
+
})]
|
|
4067
|
+
})
|
|
4068
|
+
]
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
/** One grid cell: the plugin's cached preview through the object-URL hook
|
|
4072
|
+
* (the DOM resource's lifetime belongs to the framework layer, not the
|
|
4073
|
+
* store). Image only, as v2 — the artwork IS the label; the label is the
|
|
4074
|
+
* tooltip for the curious. */
|
|
4075
|
+
function StampAssetCell({ asset, armed, onArm }) {
|
|
4076
|
+
const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
|
|
4077
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4078
|
+
type: "button",
|
|
4079
|
+
onClick: onArm,
|
|
4080
|
+
title: asset.label,
|
|
4081
|
+
"aria-pressed": armed,
|
|
4082
|
+
className: `flex aspect-square w-full items-center justify-center rounded-md border p-2 ${armed ? "border-accent bg-accent-light ring-accent ring-2" : "border-border-subtle hover:border-border hover:bg-hover"}`,
|
|
4083
|
+
children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
4084
|
+
src: url,
|
|
4085
|
+
alt: asset.label,
|
|
4086
|
+
className: "max-h-full max-w-full object-contain"
|
|
4087
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4088
|
+
className: "text-fg truncate text-sm",
|
|
4089
|
+
children: asset.label
|
|
4090
|
+
})
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4093
|
+
//#endregion
|
|
4094
|
+
//#region src/ui/signatures-panel.tsx
|
|
4095
|
+
/**
|
|
4096
|
+
* The signatures sidebar (right panel) — Preview's signature sheet on v3
|
|
4097
|
+
* parts. One row per PERSON: a stamp library of kind `signatures` holding a
|
|
4098
|
+
* full signature and, optionally, initials. Nothing is stored beyond the
|
|
4099
|
+
* file; rename/export/delete are the library verbs.
|
|
4100
|
+
*
|
|
4101
|
+
* pick a mark → with a TARGET field ("sign here" was clicked, or a
|
|
4102
|
+
* signature widget selected) the mark goes into that field by the
|
|
4103
|
+
* plugin's mode; otherwise it ARMS — hover a page for the ghost, click a
|
|
4104
|
+
* signature field to sign it, click anywhere else to drop it as a stamp.
|
|
4105
|
+
*
|
|
4106
|
+
* Below the people: the signature fields of THIS document, signed or not,
|
|
4107
|
+
* with their verdicts — click an unsigned one to target it, a signed one to
|
|
4108
|
+
* inspect it.
|
|
4109
|
+
*/
|
|
4110
|
+
const fieldLabel = (field) => field.kind === "fqn" ? field.name : `#${field.fieldObjectNumber}`;
|
|
4111
|
+
function SignaturesPanel() {
|
|
4112
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
4113
|
+
const stamp = (0, _embedpdf_react_stamp.useStamp)();
|
|
4114
|
+
const signature = (0, _embedpdf_react_signature.useSignature)();
|
|
4115
|
+
const config = useSignaturesConfig();
|
|
4116
|
+
const rows = (0, _embedpdf_react_signature.useSignerRows)();
|
|
4117
|
+
const { target, busy } = (0, _embedpdf_react_signature.useSignatureTarget)();
|
|
4118
|
+
const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
|
|
4119
|
+
const { activeToolId } = (0, _embedpdf_react_interaction.useTool)();
|
|
4120
|
+
const maker = (0, _embedpdf_react_shell.useSurface)("signature-maker");
|
|
4121
|
+
const [armedId, setArmedId] = (0, react.useState)(null);
|
|
4122
|
+
const [error, setError] = (0, react.useState)(null);
|
|
4123
|
+
const [invalidating, setInvalidating] = (0, react.useState)(null);
|
|
4124
|
+
(0, _embedpdf_react_signature.useSignatureEvent)((event) => {
|
|
4125
|
+
if (event.type === "invalidating") setInvalidating(signature.signatureOf(event.field)?.fieldName ?? fieldLabel(event.field));
|
|
4126
|
+
else if (event.type === "validated" && !event.verdicts.some((v) => v.summary === "invalid" && v.modifications.basis === "working-copy")) setInvalidating(null);
|
|
4127
|
+
});
|
|
4128
|
+
(0, react.useEffect)(() => {
|
|
4129
|
+
restoreStampLibrariesOnce(stamp);
|
|
4130
|
+
}, [stamp]);
|
|
4131
|
+
(0, react.useEffect)(() => {
|
|
4132
|
+
if (activeToolId !== "stamp") setArmedId(null);
|
|
4133
|
+
}, [activeToolId]);
|
|
4134
|
+
const pick = (asset) => {
|
|
4135
|
+
setError(null);
|
|
4136
|
+
if (target) {
|
|
4137
|
+
signature.placeMark({ assetId: asset.id }, { field: target }).catch((err) => {
|
|
4138
|
+
console.error("[embedpdf] placing the mark failed:", err);
|
|
4139
|
+
setError(t("demo.signError"));
|
|
4140
|
+
});
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
setArmedId(asset.id);
|
|
4144
|
+
armAsset(asset.id).catch((err) => {
|
|
4145
|
+
console.error("[embedpdf] arm mark failed:", err);
|
|
4146
|
+
setArmedId(null);
|
|
4147
|
+
setError(t("demo.stampsArmError"));
|
|
4148
|
+
});
|
|
4149
|
+
};
|
|
4150
|
+
const canAddPerson = (config.libraries ?? "many") === "many" || rows.length === 0;
|
|
4151
|
+
const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
|
|
4152
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4153
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
4154
|
+
children: [
|
|
4155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4156
|
+
className: "min-h-0 flex-1 overflow-y-auto p-3",
|
|
4157
|
+
children: [rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4158
|
+
className: "flex flex-col items-center gap-3 px-4 py-8 text-center",
|
|
4159
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4160
|
+
name: "signature",
|
|
4161
|
+
size: 32,
|
|
4162
|
+
className: "text-fg-muted"
|
|
4163
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4164
|
+
className: "text-fg-muted text-sm",
|
|
4165
|
+
children: t("demo.signaturesEmpty")
|
|
4166
|
+
})]
|
|
4167
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4168
|
+
className: "flex flex-col gap-3",
|
|
4169
|
+
children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignerRowView, {
|
|
4170
|
+
row,
|
|
4171
|
+
armedId,
|
|
4172
|
+
wantsInitials,
|
|
4173
|
+
onPick: pick,
|
|
4174
|
+
onAddInitials: () => maker.open({
|
|
4175
|
+
exclusive: "modal",
|
|
4176
|
+
props: { libraryId: row.libraryId }
|
|
4177
|
+
})
|
|
4178
|
+
}, row.libraryId))
|
|
4179
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DocumentSignatures, {})]
|
|
4180
|
+
}),
|
|
4181
|
+
invalidating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4182
|
+
className: "border-border-subtle shrink-0 border-t px-3 py-2 text-xs text-amber-700",
|
|
4183
|
+
children: t("demo.signaturesInvalidating", { params: { field: invalidating } })
|
|
4184
|
+
}),
|
|
4185
|
+
(error || target || armedId) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4186
|
+
className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
|
|
4187
|
+
children: error ?? (target ? busy ? t("demo.signSigning") : t("demo.signaturesTargetHint") : t("demo.signaturesArmedHint"))
|
|
4188
|
+
}),
|
|
4189
|
+
canAddPerson ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4190
|
+
className: "border-border-subtle shrink-0 border-t p-3",
|
|
4191
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4192
|
+
type: "button",
|
|
4193
|
+
onClick: () => maker.open({ exclusive: "modal" }),
|
|
4194
|
+
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",
|
|
4195
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4196
|
+
name: "plus",
|
|
4197
|
+
size: 16
|
|
4198
|
+
}), t("demo.signaturesNew")]
|
|
4199
|
+
})
|
|
4200
|
+
}) : null
|
|
4201
|
+
]
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
function SignerRowView({ row, armedId, wantsInitials, onPick, onAddInitials }) {
|
|
4205
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
4206
|
+
const stamp = (0, _embedpdf_react_stamp.useStamp)();
|
|
4207
|
+
const [renaming, setRenaming] = (0, react.useState)(null);
|
|
4208
|
+
const exportPdf = () => {
|
|
4209
|
+
const bytes = stamp.exportLibrary(row.libraryId);
|
|
4210
|
+
if (!bytes) return;
|
|
4211
|
+
const blob = new Blob([bytes], { type: "application/pdf" });
|
|
4212
|
+
const url = URL.createObjectURL(blob);
|
|
4213
|
+
const a = document.createElement("a");
|
|
4214
|
+
a.href = url;
|
|
4215
|
+
a.download = `${row.name || "signature"}.pdf`;
|
|
4216
|
+
a.click();
|
|
4217
|
+
URL.revokeObjectURL(url);
|
|
4218
|
+
};
|
|
4219
|
+
const commitRename = () => {
|
|
4220
|
+
const name = renaming?.trim();
|
|
4221
|
+
setRenaming(null);
|
|
4222
|
+
if (name && name !== row.name) stamp.updateLibrary(row.libraryId, { name });
|
|
4223
|
+
};
|
|
4224
|
+
const iconButton = "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded";
|
|
4225
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4226
|
+
className: "border-border-subtle rounded-md border p-2",
|
|
4227
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4228
|
+
className: "flex items-center gap-1",
|
|
4229
|
+
children: [
|
|
4230
|
+
renaming !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4231
|
+
autoFocus: true,
|
|
4232
|
+
value: renaming,
|
|
4233
|
+
onChange: (e) => setRenaming(e.target.value),
|
|
4234
|
+
onBlur: commitRename,
|
|
4235
|
+
onKeyDown: (e) => {
|
|
4236
|
+
if (e.key === "Enter") commitRename();
|
|
4237
|
+
if (e.key === "Escape") setRenaming(null);
|
|
4238
|
+
},
|
|
4239
|
+
className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-1.5 py-0.5 text-sm"
|
|
4240
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4241
|
+
className: "text-fg min-w-0 flex-1 truncate text-sm font-medium",
|
|
4242
|
+
children: row.name
|
|
4243
|
+
}),
|
|
4244
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4245
|
+
type: "button",
|
|
4246
|
+
onClick: () => setRenaming(row.name),
|
|
4247
|
+
title: t("demo.signaturesRename"),
|
|
4248
|
+
className: iconButton,
|
|
4249
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4250
|
+
name: "pencilMarker",
|
|
4251
|
+
size: 13
|
|
4252
|
+
})
|
|
4253
|
+
}),
|
|
4254
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4255
|
+
type: "button",
|
|
4256
|
+
onClick: exportPdf,
|
|
4257
|
+
title: t("demo.signaturesExport"),
|
|
4258
|
+
className: iconButton,
|
|
4259
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4260
|
+
name: "download",
|
|
4261
|
+
size: 13
|
|
4262
|
+
})
|
|
4263
|
+
}),
|
|
4264
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4265
|
+
type: "button",
|
|
4266
|
+
onClick: () => void stamp.removeLibrary(row.libraryId),
|
|
4267
|
+
title: t("demo.signaturesRemove"),
|
|
4268
|
+
className: iconButton,
|
|
4269
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4270
|
+
name: "trash",
|
|
4271
|
+
size: 13
|
|
4272
|
+
})
|
|
4273
|
+
})
|
|
4274
|
+
]
|
|
4275
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4276
|
+
className: "mt-2 grid grid-cols-[2fr_1fr] gap-2",
|
|
4277
|
+
children: [row.signatures.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkThumb, {
|
|
4278
|
+
asset,
|
|
4279
|
+
label: t("demo.signatureLabel"),
|
|
4280
|
+
armed: armedId === asset.id,
|
|
4281
|
+
onPick: () => onPick(asset)
|
|
4282
|
+
}, asset.id)), wantsInitials ? row.initials ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkThumb, {
|
|
4283
|
+
asset: row.initials,
|
|
4284
|
+
label: t("demo.initialsLabel"),
|
|
4285
|
+
armed: armedId === row.initials.id,
|
|
4286
|
+
onPick: () => onPick(row.initials)
|
|
4287
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4288
|
+
type: "button",
|
|
4289
|
+
onClick: onAddInitials,
|
|
4290
|
+
title: t("demo.signaturesAddInitials"),
|
|
4291
|
+
className: "border-border-subtle text-fg-muted hover:bg-hover flex h-16 items-center justify-center rounded-md border border-dashed text-xs",
|
|
4292
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4293
|
+
name: "plus",
|
|
4294
|
+
size: 14
|
|
4295
|
+
})
|
|
4296
|
+
}) : null]
|
|
4297
|
+
})]
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
function MarkThumb({ asset, label, armed, onPick }) {
|
|
4301
|
+
const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
|
|
4302
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4303
|
+
type: "button",
|
|
4304
|
+
onClick: onPick,
|
|
4305
|
+
title: label,
|
|
4306
|
+
"aria-pressed": armed,
|
|
4307
|
+
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"}`,
|
|
4308
|
+
children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
4309
|
+
src: url,
|
|
4310
|
+
alt: label,
|
|
4311
|
+
className: "max-h-full max-w-full object-contain"
|
|
4312
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4313
|
+
className: "text-fg-muted text-xs",
|
|
4314
|
+
children: label
|
|
4315
|
+
})
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
/** The signature fields of this document, with their state. */
|
|
4319
|
+
function DocumentSignatures() {
|
|
4320
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
4321
|
+
const signature = (0, _embedpdf_react_signature.useSignature)();
|
|
4322
|
+
const snapshot = (0, _embedpdf_react_signature.useSignatureSnapshot)();
|
|
4323
|
+
const verdicts = (0, _embedpdf_react_signature.useSignatureVerdicts)();
|
|
4324
|
+
const { target } = (0, _embedpdf_react_signature.useSignatureTarget)();
|
|
4325
|
+
const [validating, setValidating] = (0, react.useState)(false);
|
|
4326
|
+
const fields = snapshot?.signatures ?? [];
|
|
4327
|
+
const verdictOf = (dto) => verdicts?.find((v) => v.signature.index === dto.index) ?? null;
|
|
4328
|
+
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");
|
|
4329
|
+
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";
|
|
4330
|
+
const isTarget = (dto) => !!target && (target.kind === "fqn" ? target.name === dto.fieldName : target.fieldObjectNumber === (dto.field.kind === "objectNumber" ? dto.field.fieldObjectNumber : -1));
|
|
4331
|
+
const validate = () => {
|
|
4332
|
+
setValidating(true);
|
|
4333
|
+
signature.validate().finally(() => setValidating(false));
|
|
4334
|
+
};
|
|
4335
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4336
|
+
className: "mt-4",
|
|
4337
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4338
|
+
className: "flex items-center justify-between",
|
|
4339
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4340
|
+
className: "text-fg-muted text-xs font-semibold uppercase tracking-wide",
|
|
4341
|
+
children: t("demo.signaturesInDocument")
|
|
4342
|
+
}), fields.some((f) => f.signed) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4343
|
+
type: "button",
|
|
4344
|
+
disabled: validating,
|
|
4345
|
+
onClick: validate,
|
|
4346
|
+
className: "text-accent text-xs hover:underline disabled:opacity-60",
|
|
4347
|
+
children: validating ? t("demo.signaturesValidating") : t("demo.signaturesValidate")
|
|
4348
|
+
}) : null]
|
|
4349
|
+
}), fields.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4350
|
+
className: "text-fg-muted mt-1 text-xs",
|
|
4351
|
+
children: t("demo.signaturesNone")
|
|
4352
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4353
|
+
className: "mt-1 flex flex-col gap-1",
|
|
4354
|
+
children: fields.map((dto) => {
|
|
4355
|
+
const verdict = verdictOf(dto);
|
|
4356
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4357
|
+
type: "button",
|
|
4358
|
+
onClick: () => dto.signed ? signature.inspect(dto.field) : signature.setTarget(dto.field),
|
|
4359
|
+
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" : ""}`,
|
|
4360
|
+
children: [
|
|
4361
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4362
|
+
name: dto.signed ? "check" : "signature",
|
|
4363
|
+
size: 14,
|
|
4364
|
+
className: dto.signed ? "text-green-600" : "text-fg-muted"
|
|
4365
|
+
}),
|
|
4366
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4367
|
+
className: "text-fg min-w-0 flex-1 truncate",
|
|
4368
|
+
children: dto.fieldName
|
|
4369
|
+
}),
|
|
4370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4371
|
+
className: `shrink-0 text-right text-xs ${dto.signed ? verdictTone(verdict) : "text-fg-muted"}`,
|
|
4372
|
+
children: dto.signed ? verdictLabel(verdict) : t("demo.signaturesUnsigned")
|
|
4373
|
+
})
|
|
4374
|
+
]
|
|
4375
|
+
}) }, dto.index);
|
|
4376
|
+
})
|
|
4377
|
+
})]
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
//#endregion
|
|
3484
4381
|
//#region src/ui/panels.tsx
|
|
3485
4382
|
/**
|
|
3486
4383
|
* The static chrome around the toolbars: the left/right sidebars (shell
|
|
@@ -3574,10 +4471,12 @@ function RightSidebar() {
|
|
|
3574
4471
|
const comment = (0, _embedpdf_react_shell.useSurface)("comment");
|
|
3575
4472
|
const style = (0, _embedpdf_react_shell.useSurface)("annotation-style");
|
|
3576
4473
|
const redaction = (0, _embedpdf_react_shell.useSurface)("redaction");
|
|
3577
|
-
const
|
|
4474
|
+
const stamps = (0, _embedpdf_react_shell.useSurface)("stamps");
|
|
4475
|
+
const signatures = (0, _embedpdf_react_shell.useSurface)("signatures");
|
|
4476
|
+
const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : signatures.isOpen ? "signatures" : null;
|
|
3578
4477
|
if (!active) return null;
|
|
3579
|
-
const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : "demo.styleTitle";
|
|
3580
|
-
const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : style.close;
|
|
4478
|
+
const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : active === "signatures" ? "demo.signaturesTitle" : "demo.styleTitle";
|
|
4479
|
+
const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : active === "signatures" ? signatures.close : style.close;
|
|
3581
4480
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
3582
4481
|
className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
|
|
3583
4482
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3594,7 +4493,7 @@ function RightSidebar() {
|
|
|
3594
4493
|
size: 16
|
|
3595
4494
|
})
|
|
3596
4495
|
})]
|
|
3597
|
-
}), active === "style" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactionPanel, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsPanel, {}, documentId ?? "none")]
|
|
4496
|
+
}), active === "style" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampsPanel, {}) : active === "signatures" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignaturesPanel, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsPanel, {}, documentId ?? "none")]
|
|
3598
4497
|
});
|
|
3599
4498
|
}
|
|
3600
4499
|
function PageControls() {
|
|
@@ -3685,6 +4584,684 @@ function RedactConfirmModal() {
|
|
|
3685
4584
|
});
|
|
3686
4585
|
}
|
|
3687
4586
|
//#endregion
|
|
4587
|
+
//#region src/ui/sign-dialog.tsx
|
|
4588
|
+
/**
|
|
4589
|
+
* The sign dialog (`signature-sign` modal surface): a mark met a field and
|
|
4590
|
+
* the mode is `ask` — or a "sign here" flow wants the facts first. Shows
|
|
4591
|
+
* the mark as it will sit in the field, takes reason and location, offers
|
|
4592
|
+
* a certification when the config allows it, and either SEALS the field
|
|
4593
|
+
* (`signField`) or only draws the mark into it (`fillField`).
|
|
4594
|
+
*/
|
|
4595
|
+
function SignDialog() {
|
|
4596
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
4597
|
+
const surface = (0, _embedpdf_react_shell.useSurface)("signature-sign");
|
|
4598
|
+
const signature = (0, _embedpdf_react_signature.useSignature)();
|
|
4599
|
+
const [reason, setReason] = (0, react.useState)("");
|
|
4600
|
+
const [location, setLocation] = (0, react.useState)("");
|
|
4601
|
+
const [signKind, setSignKind] = (0, react.useState)("approval");
|
|
4602
|
+
const [busy, setBusy] = (0, react.useState)(null);
|
|
4603
|
+
const [error, setError] = (0, react.useState)(null);
|
|
4604
|
+
const field = surface.props?.field;
|
|
4605
|
+
const mark = surface.props?.mark;
|
|
4606
|
+
const preview = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(mark && "assetId" in mark ? mark.assetId : null);
|
|
4607
|
+
if (!surface.isOpen || !field || !mark) return null;
|
|
4608
|
+
const attribution = {
|
|
4609
|
+
...reason.trim() ? { reason: reason.trim() } : {},
|
|
4610
|
+
...location.trim() ? { location: location.trim() } : {}
|
|
4611
|
+
};
|
|
4612
|
+
const run = async (kind) => {
|
|
4613
|
+
setBusy(kind);
|
|
4614
|
+
setError(null);
|
|
4615
|
+
try {
|
|
4616
|
+
if (kind === "sign") await signature.signField({
|
|
4617
|
+
field,
|
|
4618
|
+
mark,
|
|
4619
|
+
attribution,
|
|
4620
|
+
...signKind !== "approval" && signature.canCertify() ? { certify: { permission: Number(signKind.slice(-1)) } } : {}
|
|
4621
|
+
});
|
|
4622
|
+
else await signature.fillField(field, mark);
|
|
4623
|
+
surface.close();
|
|
4624
|
+
} catch (err) {
|
|
4625
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
4626
|
+
} finally {
|
|
4627
|
+
setBusy(null);
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
const input = "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm";
|
|
4631
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4632
|
+
className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
|
|
4633
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4634
|
+
className: "border-border bg-surface w-96 rounded-lg border p-4 shadow-xl",
|
|
4635
|
+
children: [
|
|
4636
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
4637
|
+
className: "text-fg text-base font-semibold",
|
|
4638
|
+
children: t("demo.signTitle")
|
|
4639
|
+
}),
|
|
4640
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4641
|
+
className: "border-border-subtle mt-3 flex h-24 items-center justify-center rounded-md border p-2",
|
|
4642
|
+
children: preview ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
4643
|
+
src: preview,
|
|
4644
|
+
alt: "",
|
|
4645
|
+
className: "max-h-full max-w-full object-contain"
|
|
4646
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4647
|
+
className: "text-fg-muted text-sm",
|
|
4648
|
+
children: t("demo.signatureLabel")
|
|
4649
|
+
})
|
|
4650
|
+
}),
|
|
4651
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4652
|
+
className: "text-fg-muted mt-3 block text-xs",
|
|
4653
|
+
children: [t("demo.signReason"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4654
|
+
value: reason,
|
|
4655
|
+
onChange: (e) => setReason(e.target.value),
|
|
4656
|
+
className: input
|
|
4657
|
+
})]
|
|
4658
|
+
}),
|
|
4659
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4660
|
+
className: "text-fg-muted mt-2 block text-xs",
|
|
4661
|
+
children: [t("demo.signLocation"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4662
|
+
value: location,
|
|
4663
|
+
onChange: (e) => setLocation(e.target.value),
|
|
4664
|
+
className: input
|
|
4665
|
+
})]
|
|
4666
|
+
}),
|
|
4667
|
+
signature.canCertify() ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4668
|
+
className: "text-fg-muted mt-3 block text-xs",
|
|
4669
|
+
children: [t("demo.signKind"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
4670
|
+
value: signKind,
|
|
4671
|
+
onChange: (e) => setSignKind(e.target.value),
|
|
4672
|
+
className: input,
|
|
4673
|
+
children: [
|
|
4674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4675
|
+
value: "approval",
|
|
4676
|
+
children: t("demo.signKindApproval")
|
|
4677
|
+
}),
|
|
4678
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4679
|
+
value: "certify-3",
|
|
4680
|
+
children: t("demo.signKindCertifyAnnotate")
|
|
4681
|
+
}),
|
|
4682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4683
|
+
value: "certify-2",
|
|
4684
|
+
children: t("demo.signKindCertifyFill")
|
|
4685
|
+
}),
|
|
4686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4687
|
+
value: "certify-1",
|
|
4688
|
+
children: t("demo.signKindCertifyLocked")
|
|
4689
|
+
})
|
|
4690
|
+
]
|
|
4691
|
+
})]
|
|
4692
|
+
}) : null,
|
|
4693
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4694
|
+
className: "mt-2 text-sm text-red-600",
|
|
4695
|
+
children: error
|
|
4696
|
+
}) : null,
|
|
4697
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4698
|
+
className: "mt-4 flex justify-end gap-2",
|
|
4699
|
+
children: [
|
|
4700
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4701
|
+
type: "button",
|
|
4702
|
+
disabled: busy != null,
|
|
4703
|
+
onClick: surface.close,
|
|
4704
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
|
|
4705
|
+
children: t("demo.cancel")
|
|
4706
|
+
}),
|
|
4707
|
+
signature.canFill() ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4708
|
+
type: "button",
|
|
4709
|
+
disabled: busy != null,
|
|
4710
|
+
onClick: () => void run("fill"),
|
|
4711
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-60",
|
|
4712
|
+
children: t("demo.signFill")
|
|
4713
|
+
}) : null,
|
|
4714
|
+
signature.canSign() ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4715
|
+
type: "button",
|
|
4716
|
+
disabled: busy != null,
|
|
4717
|
+
onClick: () => void run("sign"),
|
|
4718
|
+
className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
|
|
4719
|
+
children: busy === "sign" ? t("demo.signSigning") : t("demo.signButton")
|
|
4720
|
+
}) : null
|
|
4721
|
+
]
|
|
4722
|
+
})
|
|
4723
|
+
]
|
|
4724
|
+
})
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
//#endregion
|
|
4728
|
+
//#region src/ui/signature-bridge.tsx
|
|
4729
|
+
/**
|
|
4730
|
+
* The signature plugin's UI intents → shell surfaces. The plugin owns the
|
|
4731
|
+
* act and says WHAT happened; the chrome decides WHAT OPENS:
|
|
4732
|
+
*
|
|
4733
|
+
* target (a "sign here" click) → the signatures panel, right side
|
|
4734
|
+
* ask (mode 'ask' met a field) → the sign dialog (modal)
|
|
4735
|
+
* inspect (a signed field clicked) → the validation popover
|
|
4736
|
+
*
|
|
4737
|
+
* Mounted once per document view; renders nothing.
|
|
4738
|
+
*/
|
|
4739
|
+
function SignatureBridge() {
|
|
4740
|
+
const shell = (0, _embedpdf_react_shell.useShell)();
|
|
4741
|
+
(0, _embedpdf_react_signature.useSignatureEvent)((event) => {
|
|
4742
|
+
switch (event.type) {
|
|
4743
|
+
case "target":
|
|
4744
|
+
if (event.field) shell.open("signatures", { exclusive: "right" });
|
|
4745
|
+
return;
|
|
4746
|
+
case "ask":
|
|
4747
|
+
shell.open("signature-sign", {
|
|
4748
|
+
exclusive: "modal",
|
|
4749
|
+
props: {
|
|
4750
|
+
field: event.field,
|
|
4751
|
+
mark: event.mark
|
|
4752
|
+
}
|
|
4753
|
+
});
|
|
4754
|
+
return;
|
|
4755
|
+
case "inspect":
|
|
4756
|
+
shell.open("signature-inspector", { props: { field: event.field } });
|
|
4757
|
+
return;
|
|
4758
|
+
case "invalidating":
|
|
4759
|
+
shell.open("signatures", { exclusive: "right" });
|
|
4760
|
+
return;
|
|
4761
|
+
default: return;
|
|
4762
|
+
}
|
|
4763
|
+
});
|
|
4764
|
+
return null;
|
|
4765
|
+
}
|
|
4766
|
+
//#endregion
|
|
4767
|
+
//#region src/ui/signature-inspector.tsx
|
|
4768
|
+
/**
|
|
4769
|
+
* The validation popover (`signature-inspector` surface): what a signed
|
|
4770
|
+
* field's signature says and whether it holds — the four independent facts
|
|
4771
|
+
* (integrity, cryptography, trust, changes since) and the signer's claims,
|
|
4772
|
+
* anchored at the widget when its box is known, plus the signed revision as
|
|
4773
|
+
* a download (the exact bytes the signature covers). Mounted in the Stage
|
|
4774
|
+
* overlay so `<Anchored>` can project onto the page.
|
|
4775
|
+
*/
|
|
4776
|
+
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";
|
|
4777
|
+
/** A PDF date string (`D:YYYYMMDDHHmmSSZ` or with a zone offset) as a locale date; the raw text when it is not one. */
|
|
4778
|
+
const pdfDate = (raw) => {
|
|
4779
|
+
const m = /^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}'?\d{2}'?)?/.exec(raw);
|
|
4780
|
+
if (!m) return raw;
|
|
4781
|
+
const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
|
|
4782
|
+
const offset = !zone || zone === "Z" ? "Z" : `${zone[0]}${zone.slice(1, 3)}:${zone.slice(3).replace(/'/g, "") || "00"}`;
|
|
4783
|
+
const date = /* @__PURE__ */ new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}${offset}`);
|
|
4784
|
+
return Number.isNaN(date.getTime()) ? raw : date.toLocaleString();
|
|
4785
|
+
};
|
|
4786
|
+
const tone = (summary) => summary === "valid" ? "text-green-600" : summary === "invalid" ? "text-red-600" : summary === "valid-untrusted" ? "text-amber-600" : "text-fg-muted";
|
|
4787
|
+
function SignatureInspector() {
|
|
4788
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
4789
|
+
const surface = (0, _embedpdf_react_shell.useSurface)("signature-inspector");
|
|
4790
|
+
const signature = (0, _embedpdf_react_signature.useSignature)();
|
|
4791
|
+
const snapshot = (0, _embedpdf_react_signature.useSignatureSnapshot)();
|
|
4792
|
+
const verdicts = (0, _embedpdf_react_signature.useSignatureVerdicts)();
|
|
4793
|
+
const [validating, setValidating] = (0, react.useState)(false);
|
|
4794
|
+
const field = surface.props?.field;
|
|
4795
|
+
const dto = field ? signature.signatureOf(field) : null;
|
|
4796
|
+
const verdict = field ? signature.verdictOf(field) : null;
|
|
4797
|
+
const widget = dto?.widget ?? null;
|
|
4798
|
+
const box = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_form.FormToken, (c) => widget && widget.annotObjectNumber > 0 ? c.fillItem(widget.annotObjectNumber)?.box ?? null : null);
|
|
4799
|
+
(0, react.useEffect)(() => {
|
|
4800
|
+
if (!surface.isOpen || !dto?.signed || verdicts) return;
|
|
4801
|
+
setValidating(true);
|
|
4802
|
+
signature.validate().finally(() => setValidating(false));
|
|
4803
|
+
}, [surface.isOpen, dto?.index]);
|
|
4804
|
+
if (!surface.isOpen || !field || !dto || !snapshot) return null;
|
|
4805
|
+
const revalidate = () => {
|
|
4806
|
+
setValidating(true);
|
|
4807
|
+
signature.validate().finally(() => setValidating(false));
|
|
4808
|
+
};
|
|
4809
|
+
const summary = verdict?.summary ?? null;
|
|
4810
|
+
const modifications = verdict?.modifications.verdict ?? null;
|
|
4811
|
+
const row = (label, value, className = "text-fg") => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4812
|
+
className: "flex items-baseline justify-between gap-3 text-xs",
|
|
4813
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4814
|
+
className: "text-fg-muted",
|
|
4815
|
+
children: label
|
|
4816
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4817
|
+
className: `text-right ${className}`,
|
|
4818
|
+
children: value
|
|
4819
|
+
})]
|
|
4820
|
+
});
|
|
4821
|
+
const card = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4822
|
+
className: "border-border bg-elevated w-72 rounded-lg border p-3 text-sm shadow-xl",
|
|
4823
|
+
children: [
|
|
4824
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4825
|
+
className: "flex items-center justify-between",
|
|
4826
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4827
|
+
className: "text-fg font-semibold",
|
|
4828
|
+
children: t("demo.inspectTitle")
|
|
4829
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4830
|
+
type: "button",
|
|
4831
|
+
onClick: surface.close,
|
|
4832
|
+
className: "text-fg-muted hover:bg-hover grid h-6 w-6 place-items-center rounded",
|
|
4833
|
+
"aria-label": t("demo.cancel"),
|
|
4834
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
4835
|
+
name: "x",
|
|
4836
|
+
size: 14
|
|
4837
|
+
})
|
|
4838
|
+
})]
|
|
4839
|
+
}),
|
|
4840
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4841
|
+
className: `mt-1 text-sm font-medium ${tone(summary)}`,
|
|
4842
|
+
children: validating ? t("demo.signaturesValidating") : t(verdictKey(verdict))
|
|
4843
|
+
}),
|
|
4844
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4845
|
+
className: "mt-2 flex flex-col gap-1",
|
|
4846
|
+
children: [
|
|
4847
|
+
row(t("demo.inspectSigner"), dto.signer.name ?? "—"),
|
|
4848
|
+
dto.signer.reason ? row(t("demo.inspectReason"), dto.signer.reason) : null,
|
|
4849
|
+
dto.signer.location ? row(t("demo.inspectLocation"), dto.signer.location) : null,
|
|
4850
|
+
dto.signer.claimedTime ? row(t("demo.inspectTime"), pdfDate(dto.signer.claimedTime)) : null
|
|
4851
|
+
]
|
|
4852
|
+
}),
|
|
4853
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4854
|
+
className: "border-border-subtle mt-2 flex flex-col gap-1 border-t pt-2",
|
|
4855
|
+
children: [
|
|
4856
|
+
row(t("demo.inspectIntegrity"), verdict?.integrity ?? "—", tone(verdict?.integrity === "valid" ? "valid" : verdict?.integrity === "invalid" ? "invalid" : null)),
|
|
4857
|
+
row(t("demo.inspectCryptography"), verdict?.cryptography ?? "—"),
|
|
4858
|
+
row(t("demo.inspectTrust"), verdict?.trust ?? "—"),
|
|
4859
|
+
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"),
|
|
4860
|
+
verdict?.modifications.laterRevisions ? row(t("demo.inspectLaterRevisions"), String(verdict.modifications.laterRevisions)) : null
|
|
4861
|
+
]
|
|
4862
|
+
}),
|
|
4863
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4864
|
+
className: "mt-3 flex justify-end gap-2",
|
|
4865
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4866
|
+
type: "button",
|
|
4867
|
+
disabled: validating,
|
|
4868
|
+
onClick: revalidate,
|
|
4869
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
|
|
4870
|
+
children: t("demo.signaturesValidate")
|
|
4871
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadRevision, { field })]
|
|
4872
|
+
})
|
|
4873
|
+
]
|
|
4874
|
+
});
|
|
4875
|
+
if (widget && widget.pageObjectNumber > 0 && box) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_anchored.Anchored, {
|
|
4876
|
+
anchor: {
|
|
4877
|
+
pon: widget.pageObjectNumber,
|
|
4878
|
+
bounds: box
|
|
4879
|
+
},
|
|
4880
|
+
placement: "bottom",
|
|
4881
|
+
gap: 8,
|
|
4882
|
+
children: card
|
|
4883
|
+
});
|
|
4884
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4885
|
+
className: "absolute right-4 top-4 z-40",
|
|
4886
|
+
children: card
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
/** The signed revision — the exact bytes the signature covers — as a file. */
|
|
4890
|
+
function DownloadRevision({ field }) {
|
|
4891
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
4892
|
+
const signature = (0, _embedpdf_react_signature.useSignature)();
|
|
4893
|
+
const dto = signature.signatureOf(field);
|
|
4894
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
4895
|
+
if (!dto || dto.revisionIndex === null) return null;
|
|
4896
|
+
const revisionIndex = dto.revisionIndex;
|
|
4897
|
+
const run = async () => {
|
|
4898
|
+
setBusy(true);
|
|
4899
|
+
try {
|
|
4900
|
+
const bytes = await signature.revisionBytes(revisionIndex);
|
|
4901
|
+
const blob = new Blob([bytes], { type: "application/pdf" });
|
|
4902
|
+
const url = URL.createObjectURL(blob);
|
|
4903
|
+
const a = document.createElement("a");
|
|
4904
|
+
a.href = url;
|
|
4905
|
+
a.download = `${dto.fieldName}-signed.pdf`;
|
|
4906
|
+
a.click();
|
|
4907
|
+
URL.revokeObjectURL(url);
|
|
4908
|
+
} catch (err) {
|
|
4909
|
+
console.error("[embedpdf] signed revision download failed:", err);
|
|
4910
|
+
} finally {
|
|
4911
|
+
setBusy(false);
|
|
4912
|
+
}
|
|
4913
|
+
};
|
|
4914
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4915
|
+
type: "button",
|
|
4916
|
+
disabled: busy,
|
|
4917
|
+
onClick: () => void run(),
|
|
4918
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
|
|
4919
|
+
children: t("demo.inspectDownload")
|
|
4920
|
+
});
|
|
4921
|
+
}
|
|
4922
|
+
//#endregion
|
|
4923
|
+
//#region src/ui/signature-maker.tsx
|
|
4924
|
+
/**
|
|
4925
|
+
* The mark maker (`signature-maker` modal surface): a person's signature and
|
|
4926
|
+
* initials, drawn, typed, or uploaded, saved as ONE library of kind
|
|
4927
|
+
* `signatures` — the file IS the person (Preview's model). Chrome code over
|
|
4928
|
+
* the stamp plugin's generic calls: `createLibrary` + `addAsset({ mark })`;
|
|
4929
|
+
* rename/export/delete are the library verbs. Opened on an existing library
|
|
4930
|
+
* (`props.libraryId`) it only adds the missing initials.
|
|
4931
|
+
*/
|
|
4932
|
+
const PAD = {
|
|
4933
|
+
width: 360,
|
|
4934
|
+
height: 120
|
|
4935
|
+
};
|
|
4936
|
+
const BUILTIN_FONTS = [{
|
|
4937
|
+
key: "helvetica-oblique",
|
|
4938
|
+
label: "Helvetica"
|
|
4939
|
+
}, {
|
|
4940
|
+
key: "times-italic",
|
|
4941
|
+
label: "Times"
|
|
4942
|
+
}];
|
|
4943
|
+
/** One pad: draw / type / upload, producing a `MarkSource` or null. */
|
|
4944
|
+
function MarkPad({ value, onChange, fonts, registerFont }) {
|
|
4945
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
4946
|
+
const [tab, setTab] = (0, react.useState)("draw");
|
|
4947
|
+
const [strokes, setStrokes] = (0, react.useState)([]);
|
|
4948
|
+
const [text, setText] = (0, react.useState)("");
|
|
4949
|
+
const [font, setFont] = (0, react.useState)(fonts[0]?.key ?? "helvetica-oblique");
|
|
4950
|
+
const [fileName, setFileName] = (0, react.useState)(null);
|
|
4951
|
+
const canvas = (0, react.useRef)(null);
|
|
4952
|
+
const drawing = (0, react.useRef)(null);
|
|
4953
|
+
(0, react.useEffect)(() => {
|
|
4954
|
+
const el = canvas.current;
|
|
4955
|
+
if (!el) return;
|
|
4956
|
+
const dpr = window.devicePixelRatio || 1;
|
|
4957
|
+
el.width = PAD.width * dpr;
|
|
4958
|
+
el.height = PAD.height * dpr;
|
|
4959
|
+
const ctx = el.getContext("2d");
|
|
4960
|
+
if (!ctx) return;
|
|
4961
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
4962
|
+
ctx.clearRect(0, 0, PAD.width, PAD.height);
|
|
4963
|
+
ctx.lineCap = "round";
|
|
4964
|
+
ctx.lineJoin = "round";
|
|
4965
|
+
ctx.lineWidth = 2.5;
|
|
4966
|
+
ctx.strokeStyle = getComputedStyle(el).color || "#000";
|
|
4967
|
+
for (const stroke of strokes) {
|
|
4968
|
+
ctx.beginPath();
|
|
4969
|
+
stroke.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
|
|
4970
|
+
ctx.stroke();
|
|
4971
|
+
}
|
|
4972
|
+
}, [strokes, tab]);
|
|
4973
|
+
const point = (e) => {
|
|
4974
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
4975
|
+
return {
|
|
4976
|
+
x: e.clientX - r.left,
|
|
4977
|
+
y: e.clientY - r.top
|
|
4978
|
+
};
|
|
4979
|
+
};
|
|
4980
|
+
const commitStrokes = (next) => {
|
|
4981
|
+
setStrokes(next);
|
|
4982
|
+
onChange(next.length ? {
|
|
4983
|
+
kind: "ink",
|
|
4984
|
+
strokes: next,
|
|
4985
|
+
color: "#000000",
|
|
4986
|
+
strokeWidth: 2.5
|
|
4987
|
+
} : null);
|
|
4988
|
+
};
|
|
4989
|
+
const commitText = async (next, key) => {
|
|
4990
|
+
setText(next);
|
|
4991
|
+
setFont(key);
|
|
4992
|
+
if (!next.trim()) {
|
|
4993
|
+
onChange(null);
|
|
4994
|
+
return;
|
|
4995
|
+
}
|
|
4996
|
+
await registerFont(key);
|
|
4997
|
+
onChange({
|
|
4998
|
+
kind: "text",
|
|
4999
|
+
text: next.trim(),
|
|
5000
|
+
fontFamily: key,
|
|
5001
|
+
color: "#000000",
|
|
5002
|
+
fontSize: 36
|
|
5003
|
+
});
|
|
5004
|
+
};
|
|
5005
|
+
const pickFile = (file) => {
|
|
5006
|
+
setFileName(file.name);
|
|
5007
|
+
onChange(file.type === "application/pdf" ? {
|
|
5008
|
+
kind: "pdf",
|
|
5009
|
+
source: file,
|
|
5010
|
+
pageIndex: 0
|
|
5011
|
+
} : {
|
|
5012
|
+
kind: "image",
|
|
5013
|
+
source: file
|
|
5014
|
+
});
|
|
5015
|
+
};
|
|
5016
|
+
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"}`;
|
|
5017
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5018
|
+
className: "flex flex-col gap-2",
|
|
5019
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5020
|
+
className: "flex items-center gap-1",
|
|
5021
|
+
children: [
|
|
5022
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5023
|
+
type: "button",
|
|
5024
|
+
className: tabClass("draw"),
|
|
5025
|
+
onClick: () => setTab("draw"),
|
|
5026
|
+
children: t("demo.makerDraw")
|
|
5027
|
+
}),
|
|
5028
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5029
|
+
type: "button",
|
|
5030
|
+
className: tabClass("type"),
|
|
5031
|
+
onClick: () => setTab("type"),
|
|
5032
|
+
children: t("demo.makerType")
|
|
5033
|
+
}),
|
|
5034
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5035
|
+
type: "button",
|
|
5036
|
+
className: tabClass("upload"),
|
|
5037
|
+
onClick: () => setTab("upload"),
|
|
5038
|
+
children: t("demo.makerUpload")
|
|
5039
|
+
}),
|
|
5040
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "flex-1" }),
|
|
5041
|
+
value ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5042
|
+
type: "button",
|
|
5043
|
+
onClick: () => {
|
|
5044
|
+
commitStrokes([]);
|
|
5045
|
+
setText("");
|
|
5046
|
+
setFileName(null);
|
|
5047
|
+
onChange(null);
|
|
5048
|
+
},
|
|
5049
|
+
className: "text-fg-muted hover:text-fg text-xs",
|
|
5050
|
+
children: t("demo.makerClear")
|
|
5051
|
+
}) : null
|
|
5052
|
+
]
|
|
5053
|
+
}), tab === "draw" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
5054
|
+
ref: canvas,
|
|
5055
|
+
style: {
|
|
5056
|
+
width: PAD.width,
|
|
5057
|
+
height: PAD.height,
|
|
5058
|
+
touchAction: "none"
|
|
5059
|
+
},
|
|
5060
|
+
className: "border-border-subtle bg-surface text-fg rounded-md border",
|
|
5061
|
+
onPointerDown: (e) => {
|
|
5062
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
5063
|
+
drawing.current = [point(e)];
|
|
5064
|
+
setStrokes((s) => [...s, drawing.current]);
|
|
5065
|
+
},
|
|
5066
|
+
onPointerMove: (e) => {
|
|
5067
|
+
if (!drawing.current) return;
|
|
5068
|
+
drawing.current.push(point(e));
|
|
5069
|
+
setStrokes((s) => [...s.slice(0, -1), [...drawing.current]]);
|
|
5070
|
+
},
|
|
5071
|
+
onPointerUp: () => {
|
|
5072
|
+
if (!drawing.current) return;
|
|
5073
|
+
const done = drawing.current;
|
|
5074
|
+
drawing.current = null;
|
|
5075
|
+
setStrokes((s) => {
|
|
5076
|
+
const next = [...s.slice(0, -1), done];
|
|
5077
|
+
commitStrokes(next);
|
|
5078
|
+
return next;
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
5081
|
+
}) : tab === "type" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5082
|
+
className: "flex flex-col gap-2",
|
|
5083
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5084
|
+
value: text,
|
|
5085
|
+
placeholder: t("demo.makerTypePlaceholder"),
|
|
5086
|
+
onChange: (e) => void commitText(e.target.value, font),
|
|
5087
|
+
className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm"
|
|
5088
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5089
|
+
className: "text-fg-muted flex items-center gap-2 text-xs",
|
|
5090
|
+
children: [t("demo.makerFont"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
5091
|
+
value: font,
|
|
5092
|
+
onChange: (e) => void commitText(text, e.target.value),
|
|
5093
|
+
className: "border-border bg-surface text-fg rounded border px-2 py-1 text-xs",
|
|
5094
|
+
children: fonts.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
5095
|
+
value: f.key,
|
|
5096
|
+
children: f.label
|
|
5097
|
+
}, f.key))
|
|
5098
|
+
})]
|
|
5099
|
+
})]
|
|
5100
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5101
|
+
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",
|
|
5102
|
+
children: [fileName ?? t("demo.makerUploadHint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5103
|
+
type: "file",
|
|
5104
|
+
accept: "image/png,image/jpeg,application/pdf",
|
|
5105
|
+
className: "hidden",
|
|
5106
|
+
onChange: (e) => {
|
|
5107
|
+
const file = e.currentTarget.files?.[0];
|
|
5108
|
+
e.currentTarget.value = "";
|
|
5109
|
+
if (file) pickFile(file);
|
|
5110
|
+
}
|
|
5111
|
+
})]
|
|
5112
|
+
})]
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
function SignatureMakerModal() {
|
|
5116
|
+
const t = (0, _embedpdf_react_i18n.useT)();
|
|
5117
|
+
const surface = (0, _embedpdf_react_shell.useSurface)("signature-maker");
|
|
5118
|
+
const stamp = (0, _embedpdf_react_stamp.useStamp)();
|
|
5119
|
+
const kernel = (0, _embedpdf_react_runtime.useKernel)();
|
|
5120
|
+
const config = useSignaturesConfig();
|
|
5121
|
+
const [name, setName] = (0, react.useState)("");
|
|
5122
|
+
const [signatureMark, setSignatureMark] = (0, react.useState)(null);
|
|
5123
|
+
const [initialsMark, setInitialsMark] = (0, react.useState)(null);
|
|
5124
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
5125
|
+
const [error, setError] = (0, react.useState)(null);
|
|
5126
|
+
const existingId = surface.props?.libraryId;
|
|
5127
|
+
const existing = existingId ? stamp.library(existingId) : null;
|
|
5128
|
+
const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
|
|
5129
|
+
const fonts = [...(config.fonts ?? []).map((f) => ({
|
|
5130
|
+
key: f.key,
|
|
5131
|
+
label: f.label
|
|
5132
|
+
})), ...BUILTIN_FONTS];
|
|
5133
|
+
const registered = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
5134
|
+
const registerFont = async (key) => {
|
|
5135
|
+
const spec = config.fonts?.find((f) => f.key === key);
|
|
5136
|
+
if (!spec || registered.current.has(key) || !kernel.engine.fonts) return;
|
|
5137
|
+
const response = await fetch(spec.url);
|
|
5138
|
+
if (!response.ok) throw new Error(`${spec.url}: HTTP ${response.status}`);
|
|
5139
|
+
await kernel.engine.fonts.register({
|
|
5140
|
+
key,
|
|
5141
|
+
data: new Uint8Array(await response.arrayBuffer())
|
|
5142
|
+
});
|
|
5143
|
+
registered.current.add(key);
|
|
5144
|
+
};
|
|
5145
|
+
if (!surface.isOpen) return null;
|
|
5146
|
+
const canSave = existing ? initialsMark != null : name.trim().length > 0 && signatureMark != null;
|
|
5147
|
+
const save = async () => {
|
|
5148
|
+
setBusy(true);
|
|
5149
|
+
setError(null);
|
|
5150
|
+
try {
|
|
5151
|
+
if (existing) await stamp.addAsset({
|
|
5152
|
+
libraryId: existing.id,
|
|
5153
|
+
name: _embedpdf_react_signature.INITIALS_MARK_NAME,
|
|
5154
|
+
label: t("demo.initialsLabel"),
|
|
5155
|
+
mark: initialsMark
|
|
5156
|
+
});
|
|
5157
|
+
else {
|
|
5158
|
+
const libraryId = await stamp.createLibrary(name.trim(), { kind: _embedpdf_react_signature.SIGNATURES_LIBRARY_KIND });
|
|
5159
|
+
await stamp.addAsset({
|
|
5160
|
+
libraryId,
|
|
5161
|
+
name: _embedpdf_react_signature.SIGNATURE_MARK_NAME,
|
|
5162
|
+
label: t("demo.signatureLabel"),
|
|
5163
|
+
mark: signatureMark
|
|
5164
|
+
});
|
|
5165
|
+
if (initialsMark) await stamp.addAsset({
|
|
5166
|
+
libraryId,
|
|
5167
|
+
name: _embedpdf_react_signature.INITIALS_MARK_NAME,
|
|
5168
|
+
label: t("demo.initialsLabel"),
|
|
5169
|
+
mark: initialsMark
|
|
5170
|
+
});
|
|
5171
|
+
}
|
|
5172
|
+
setName("");
|
|
5173
|
+
setSignatureMark(null);
|
|
5174
|
+
setInitialsMark(null);
|
|
5175
|
+
surface.close();
|
|
5176
|
+
} catch (err) {
|
|
5177
|
+
console.error("[embedpdf] saving the signature failed:", err);
|
|
5178
|
+
setError(t("demo.makerError"));
|
|
5179
|
+
} finally {
|
|
5180
|
+
setBusy(false);
|
|
5181
|
+
}
|
|
5182
|
+
};
|
|
5183
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5184
|
+
className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
|
|
5185
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5186
|
+
className: "border-border bg-surface w-[26rem] rounded-lg border p-4 shadow-xl",
|
|
5187
|
+
children: [
|
|
5188
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5189
|
+
className: "flex items-center justify-between",
|
|
5190
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
5191
|
+
className: "text-fg text-base font-semibold",
|
|
5192
|
+
children: existing ? t("demo.signaturesAddInitials") : t("demo.makerTitle")
|
|
5193
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5194
|
+
type: "button",
|
|
5195
|
+
onClick: surface.close,
|
|
5196
|
+
className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
|
|
5197
|
+
"aria-label": t("demo.cancel"),
|
|
5198
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
5199
|
+
name: "x",
|
|
5200
|
+
size: 16
|
|
5201
|
+
})
|
|
5202
|
+
})]
|
|
5203
|
+
}),
|
|
5204
|
+
existing ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5205
|
+
className: "text-fg-muted mt-3 block text-xs",
|
|
5206
|
+
children: [t("demo.makerName"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5207
|
+
value: name,
|
|
5208
|
+
placeholder: t("demo.makerNamePlaceholder"),
|
|
5209
|
+
onChange: (e) => setName(e.target.value),
|
|
5210
|
+
className: "border-border bg-surface text-fg mt-1 w-full rounded border px-2 py-1.5 text-sm"
|
|
5211
|
+
})]
|
|
5212
|
+
}),
|
|
5213
|
+
existing ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5214
|
+
className: "mt-3",
|
|
5215
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5216
|
+
className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
|
|
5217
|
+
children: t("demo.signatureLabel")
|
|
5218
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkPad, {
|
|
5219
|
+
value: signatureMark,
|
|
5220
|
+
onChange: setSignatureMark,
|
|
5221
|
+
fonts,
|
|
5222
|
+
registerFont
|
|
5223
|
+
})]
|
|
5224
|
+
}),
|
|
5225
|
+
wantsInitials ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5226
|
+
className: "mt-3",
|
|
5227
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
5228
|
+
className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
|
|
5229
|
+
children: [t("demo.initialsLabel"), existing ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5230
|
+
className: "text-fg-muted ml-1 font-normal normal-case tracking-normal",
|
|
5231
|
+
children: t("demo.makerOptional")
|
|
5232
|
+
})]
|
|
5233
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkPad, {
|
|
5234
|
+
value: initialsMark,
|
|
5235
|
+
onChange: setInitialsMark,
|
|
5236
|
+
fonts,
|
|
5237
|
+
registerFont
|
|
5238
|
+
})]
|
|
5239
|
+
}) : null,
|
|
5240
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5241
|
+
className: "mt-2 text-sm text-red-600",
|
|
5242
|
+
children: error
|
|
5243
|
+
}) : null,
|
|
5244
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5245
|
+
className: "mt-4 flex justify-end gap-2",
|
|
5246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5247
|
+
type: "button",
|
|
5248
|
+
disabled: busy,
|
|
5249
|
+
onClick: surface.close,
|
|
5250
|
+
className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
|
|
5251
|
+
children: t("demo.cancel")
|
|
5252
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5253
|
+
type: "button",
|
|
5254
|
+
disabled: busy || !canSave,
|
|
5255
|
+
onClick: () => void save(),
|
|
5256
|
+
className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
|
|
5257
|
+
children: busy ? t("demo.makerSaving") : t("demo.makerSave")
|
|
5258
|
+
})]
|
|
5259
|
+
})
|
|
5260
|
+
]
|
|
5261
|
+
})
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
//#endregion
|
|
3688
5265
|
//#region src/ui/document-boot.tsx
|
|
3689
5266
|
/**
|
|
3690
5267
|
* Boot-state panes for the document area — what renders INSTEAD of the Stage
|
|
@@ -3926,9 +5503,10 @@ function Shell() {
|
|
|
3926
5503
|
className: "w-full"
|
|
3927
5504
|
})
|
|
3928
5505
|
});
|
|
3929
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5506
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationFontsProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3930
5507
|
className: "bg-app text-fg flex h-full flex-col",
|
|
3931
5508
|
children: [
|
|
5509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampLibraryStore, {}),
|
|
3932
5510
|
(frame.header ?? true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", { name: "header" }),
|
|
3933
5511
|
(frame.tabs ?? "always") !== "never" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", {
|
|
3934
5512
|
name: "tabs",
|
|
@@ -3948,6 +5526,7 @@ function Shell() {
|
|
|
3948
5526
|
overlay: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3949
5527
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStrip, {}),
|
|
3950
5528
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionStrip, {}),
|
|
5529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureInspector, {}),
|
|
3951
5530
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_selection.SelectionHandles, {}),
|
|
3952
5531
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "y" }),
|
|
3953
5532
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "x" })
|
|
@@ -3966,12 +5545,15 @@ function Shell() {
|
|
|
3966
5545
|
]
|
|
3967
5546
|
}),
|
|
3968
5547
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RightSidebar, {}),
|
|
3969
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactConfirmModal, {})
|
|
5548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactConfirmModal, {}),
|
|
5549
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureBridge, {}),
|
|
5550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignDialog, {}),
|
|
5551
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureMakerModal, {})
|
|
3970
5552
|
] })
|
|
3971
5553
|
}),
|
|
3972
5554
|
toolbarEdge === "bottom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeBand, { edge: "bottom" }), toolbarBand] })
|
|
3973
5555
|
]
|
|
3974
|
-
});
|
|
5556
|
+
}) });
|
|
3975
5557
|
}
|
|
3976
5558
|
//#endregion
|
|
3977
5559
|
//#region src/viewer.tsx
|
|
@@ -3994,7 +5576,7 @@ function Shell() {
|
|
|
3994
5576
|
* changing these props on a mounted viewer does not rebuild the workspace.
|
|
3995
5577
|
*/
|
|
3996
5578
|
/** Lazy built-in packs — a strings override for one of these wraps its loader. */
|
|
3997
|
-
const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-
|
|
5579
|
+
const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-DYwzfxyt.cjs")).then((m) => m.es) };
|
|
3998
5580
|
const isDict = (v) => typeof v === "object" && v !== null;
|
|
3999
5581
|
/**
|
|
4000
5582
|
* Deep-merge `over` into `base` (immutably), expanding dotted keys —
|
|
@@ -4038,7 +5620,7 @@ function Booting() {
|
|
|
4038
5620
|
})
|
|
4039
5621
|
});
|
|
4040
5622
|
}
|
|
4041
|
-
function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, theme, themeTarget, onViewer }) {
|
|
5623
|
+
function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, signatures, annotations, theme, themeTarget, onViewer }) {
|
|
4042
5624
|
const [resolved] = (0, react.useState)(() => {
|
|
4043
5625
|
const byId = new Map(require_commands.defaultCommands.map((c) => [c.id, c]));
|
|
4044
5626
|
for (const c of commands ?? []) byId.set(c.id, c);
|
|
@@ -4069,6 +5651,9 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
|
|
|
4069
5651
|
commands: resolvedCommands,
|
|
4070
5652
|
chrome: resolvedChrome,
|
|
4071
5653
|
icons: icons ?? {},
|
|
5654
|
+
stamps: stamps ?? {},
|
|
5655
|
+
signatures: signatures ?? {},
|
|
5656
|
+
annotations: annotations ?? {},
|
|
4072
5657
|
i18n: {
|
|
4073
5658
|
locales,
|
|
4074
5659
|
loaders,
|
|
@@ -4117,9 +5702,29 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
|
|
|
4117
5702
|
start: "none",
|
|
4118
5703
|
end: "open-arrow"
|
|
4119
5704
|
} }
|
|
5705
|
+
}, {
|
|
5706
|
+
id: "image",
|
|
5707
|
+
extends: "stamp",
|
|
5708
|
+
source: {
|
|
5709
|
+
kind: "prompt",
|
|
5710
|
+
accept: "image/png,image/jpeg"
|
|
5711
|
+
}
|
|
4120
5712
|
}] }),
|
|
4121
|
-
(0,
|
|
5713
|
+
(0, _embedpdf_react_stamp.stampPlugin)(),
|
|
5714
|
+
(0, _embedpdf_react_actions.actionsPlugin)({ javascript: {
|
|
5715
|
+
enabled: true,
|
|
5716
|
+
identity: {
|
|
5717
|
+
name: "John Doe",
|
|
5718
|
+
corporation: "Acme Inc"
|
|
5719
|
+
}
|
|
5720
|
+
} }),
|
|
4122
5721
|
(0, _embedpdf_react_form.formPlugin)(),
|
|
5722
|
+
(0, _embedpdf_react_signature.signaturePlugin)({
|
|
5723
|
+
mode: resolved.signatures.mode,
|
|
5724
|
+
signer: resolved.signatures.signer,
|
|
5725
|
+
trust: resolved.signatures.trust,
|
|
5726
|
+
allowCertify: resolved.signatures.allowCertify
|
|
5727
|
+
}),
|
|
4123
5728
|
(0, _embedpdf_react_link.linkPlugin)(),
|
|
4124
5729
|
(0, _embedpdf_react_redaction.redactionPlugin)(),
|
|
4125
5730
|
(0, _embedpdf_react_search.searchPlugin)(),
|
|
@@ -4220,12 +5825,24 @@ Object.defineProperty(exports, "ShellToken", {
|
|
|
4220
5825
|
return _embedpdf_react_shell.ShellToken;
|
|
4221
5826
|
}
|
|
4222
5827
|
});
|
|
5828
|
+
Object.defineProperty(exports, "SignatureToken", {
|
|
5829
|
+
enumerable: true,
|
|
5830
|
+
get: function() {
|
|
5831
|
+
return _embedpdf_react_signature.SignatureToken;
|
|
5832
|
+
}
|
|
5833
|
+
});
|
|
4223
5834
|
Object.defineProperty(exports, "StageToken", {
|
|
4224
5835
|
enumerable: true,
|
|
4225
5836
|
get: function() {
|
|
4226
5837
|
return _embedpdf_react_stage.StageToken;
|
|
4227
5838
|
}
|
|
4228
5839
|
});
|
|
5840
|
+
Object.defineProperty(exports, "StampToken", {
|
|
5841
|
+
enumerable: true,
|
|
5842
|
+
get: function() {
|
|
5843
|
+
return _embedpdf_react_stamp.StampToken;
|
|
5844
|
+
}
|
|
5845
|
+
});
|
|
4229
5846
|
Object.defineProperty(exports, "addItem", {
|
|
4230
5847
|
enumerable: true,
|
|
4231
5848
|
get: function() {
|
|
@@ -4238,6 +5855,12 @@ Object.defineProperty(exports, "chromeHelpers", {
|
|
|
4238
5855
|
return _embedpdf_react_toolbar.chromeHelpers;
|
|
4239
5856
|
}
|
|
4240
5857
|
});
|
|
5858
|
+
Object.defineProperty(exports, "createTestSigner", {
|
|
5859
|
+
enumerable: true,
|
|
5860
|
+
get: function() {
|
|
5861
|
+
return _embedpdf_react_signature.createTestSigner;
|
|
5862
|
+
}
|
|
5863
|
+
});
|
|
4241
5864
|
exports.createViewerHandle = createViewerHandle;
|
|
4242
5865
|
Object.defineProperty(exports, "custom", {
|
|
4243
5866
|
enumerable: true,
|
|
@@ -4260,12 +5883,36 @@ Object.defineProperty(exports, "group", {
|
|
|
4260
5883
|
return _embedpdf_react_toolbar.group;
|
|
4261
5884
|
}
|
|
4262
5885
|
});
|
|
5886
|
+
Object.defineProperty(exports, "indexedDbKeyStore", {
|
|
5887
|
+
enumerable: true,
|
|
5888
|
+
get: function() {
|
|
5889
|
+
return _embedpdf_react_signature.indexedDbKeyStore;
|
|
5890
|
+
}
|
|
5891
|
+
});
|
|
4263
5892
|
Object.defineProperty(exports, "item", {
|
|
4264
5893
|
enumerable: true,
|
|
4265
5894
|
get: function() {
|
|
4266
5895
|
return _embedpdf_react_toolbar.item;
|
|
4267
5896
|
}
|
|
4268
5897
|
});
|
|
5898
|
+
Object.defineProperty(exports, "memoryKeyStore", {
|
|
5899
|
+
enumerable: true,
|
|
5900
|
+
get: function() {
|
|
5901
|
+
return _embedpdf_react_signature.memoryKeyStore;
|
|
5902
|
+
}
|
|
5903
|
+
});
|
|
5904
|
+
Object.defineProperty(exports, "personalSigner", {
|
|
5905
|
+
enumerable: true,
|
|
5906
|
+
get: function() {
|
|
5907
|
+
return _embedpdf_react_signature.personalSigner;
|
|
5908
|
+
}
|
|
5909
|
+
});
|
|
5910
|
+
Object.defineProperty(exports, "remoteSigner", {
|
|
5911
|
+
enumerable: true,
|
|
5912
|
+
get: function() {
|
|
5913
|
+
return _embedpdf_react_signature.remoteSigner;
|
|
5914
|
+
}
|
|
5915
|
+
});
|
|
4269
5916
|
Object.defineProperty(exports, "removeItems", {
|
|
4270
5917
|
enumerable: true,
|
|
4271
5918
|
get: function() {
|
|
@@ -4285,5 +5932,11 @@ Object.defineProperty(exports, "validateChrome", {
|
|
|
4285
5932
|
return _embedpdf_react_toolbar.validateChrome;
|
|
4286
5933
|
}
|
|
4287
5934
|
});
|
|
5935
|
+
Object.defineProperty(exports, "webCryptoSigner", {
|
|
5936
|
+
enumerable: true,
|
|
5937
|
+
get: function() {
|
|
5938
|
+
return _embedpdf_react_signature.webCryptoSigner;
|
|
5939
|
+
}
|
|
5940
|
+
});
|
|
4288
5941
|
|
|
4289
5942
|
//# sourceMappingURL=index.cjs.map
|