@embedpdf/viewer-chrome 3.0.0-next.12 → 3.0.0-next.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ let _embedpdf_react_stamp = require("@embedpdf/react/stamp");
36
36
  let _embedpdf_react_redaction = require("@embedpdf/react/redaction");
37
37
  let _embedpdf_react_actions = require("@embedpdf/react/actions");
38
38
  let _embedpdf_react_form = require("@embedpdf/react/form");
39
+ let _embedpdf_react_signature = require("@embedpdf/react/signature");
39
40
  let _embedpdf_react_link = require("@embedpdf/react/link");
40
41
  let _embedpdf_react_search = require("@embedpdf/react/search");
41
42
  let _embedpdf_react_i18n = require("@embedpdf/react/i18n");
@@ -44,8 +45,10 @@ let _embedpdf_react_shell = require("@embedpdf/react/shell");
44
45
  let _embedpdf_react_toolbar = require("@embedpdf/react/toolbar");
45
46
  let react_jsx_runtime = require("react/jsx-runtime");
46
47
  let _embedpdf_react_scrollbar = require("@embedpdf/react/scrollbar");
47
- let _embedpdf_react_annotation_menu = require("@embedpdf/react/annotation-menu");
48
48
  let _embedpdf_default_stamps_library = require("@embedpdf/default-stamps/library");
49
+ let _embedpdf_web = require("@embedpdf/web");
50
+ let _embedpdf_react_annotation_menu = require("@embedpdf/react/annotation-menu");
51
+ let _embedpdf_react_anchored = require("@embedpdf/react/anchored");
49
52
  let _embedpdf_react_metadata = require("@embedpdf/react/metadata");
50
53
  //#region src/config/stage.ts
51
54
  /**
@@ -197,7 +200,8 @@ const en = {
197
200
  checkbox: "Checkbox",
198
201
  radio: "Radio Button",
199
202
  select: "Dropdown",
200
- listbox: "List Box"
203
+ listbox: "List Box",
204
+ signature: "Signature Field"
201
205
  },
202
206
  redact: {
203
207
  mark: "Mark for Redaction",
@@ -294,6 +298,7 @@ const en = {
294
298
  redactConfirmCollateral: "{count} other annotations will also be removed.",
295
299
  redactApply: "Apply",
296
300
  redactApplying: "Applying…",
301
+ formatLabel: "Format",
297
302
  redactLabelTitle: "Redaction label",
298
303
  redactLabelPlaceholder: "e.g. REDACTED",
299
304
  redactLabelRepeat: "Repeat label across region",
@@ -312,7 +317,72 @@ const en = {
312
317
  stampsCustomLabel: "Custom stamp",
313
318
  stampsError: "The built-in stamps could not be loaded.",
314
319
  stampsArmError: "That stamp could not be armed.",
315
- stampsImportError: "That PDF could not be imported as a stamp library."
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"
316
386
  }
317
387
  }
318
388
  };
@@ -330,7 +400,9 @@ const en = {
330
400
  const ViewerConfigContext = (0, react.createContext)({
331
401
  chrome: require_chrome.defaultChrome,
332
402
  icons: {},
333
- stamps: {}
403
+ stamps: {},
404
+ signatures: {},
405
+ annotations: {}
334
406
  });
335
407
  const ViewerConfigProvider = ViewerConfigContext.Provider;
336
408
  function useChromeSchema() {
@@ -348,6 +420,12 @@ function useCustomIcons() {
348
420
  function useStampsConfig() {
349
421
  return (0, react.useContext)(ViewerConfigContext).stamps;
350
422
  }
423
+ function useSignaturesConfig() {
424
+ return (0, react.useContext)(ViewerConfigContext).signatures;
425
+ }
426
+ function useAnnotationsConfig() {
427
+ return (0, react.useContext)(ViewerConfigContext).annotations;
428
+ }
351
429
  //#endregion
352
430
  //#region src/handle.ts
353
431
  function createViewerHandle(kernel) {
@@ -969,6 +1047,166 @@ function ThemeProvider({ children, preference = "system", target }) {
969
1047
  });
970
1048
  }
971
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
972
1210
  //#region src/ui/menu.tsx
973
1211
  /**
974
1212
  * Menu rendering — a MenuSchema (a tree of command ids) rendered as rows.
@@ -1377,7 +1615,10 @@ function AppToolbar({ bar, className }) {
1377
1615
  variant,
1378
1616
  run
1379
1617
  }),
1380
- renderCustom: { "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomControls, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomButton, {}) },
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
+ },
1381
1622
  renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedModes, { view }) : void 0,
1382
1623
  renderGroupTrigger: (view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupTrigger, { view }),
1383
1624
  renderSeparator: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "bg-border h-6 w-px self-center" }),
@@ -1389,6 +1630,71 @@ function AppToolbar({ bar, className }) {
1389
1630
  });
1390
1631
  }
1391
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
1392
1698
  //#region src/ui/link-editor.tsx
1393
1699
  /**
1394
1700
  * The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
@@ -2374,12 +2680,17 @@ function LineEndingSelect({ side, value, onChange }) {
2374
2680
  });
2375
2681
  }
2376
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
+ }))];
2377
2688
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownShell, {
2378
2689
  trigger: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2379
2690
  className: "text-fg text-sm",
2380
- children: FONT_OPTIONS.find((o) => o.v === value)?.label ?? value
2691
+ children: options.find((o) => o.v === value)?.label ?? value
2381
2692
  }),
2382
- children: (close) => FONT_OPTIONS.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
2693
+ children: (close) => options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
2383
2694
  selected: o.v === value,
2384
2695
  onClick: () => {
2385
2696
  onChange(o.v);
@@ -2391,17 +2702,33 @@ function FontFamilySelect({ value, onChange }) {
2391
2702
  }
2392
2703
  function FontSizeCombo({ value, onChange }) {
2393
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
+ };
2394
2713
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2395
- ref: useOutsideClose(open, () => setOpen(false)),
2714
+ ref: rootRef,
2396
2715
  className: "relative w-full",
2397
2716
  children: [
2398
2717
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2399
2718
  type: "number",
2400
2719
  min: 1,
2401
- value,
2402
- onChange: (e) => {
2403
- const n = parseInt(e.target.value, 10);
2404
- if (Number.isFinite(n) && n > 0) onChange(n);
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
+ }
2405
2732
  },
2406
2733
  onClick: () => setOpen(true),
2407
2734
  className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 pr-7 text-sm"
@@ -2422,6 +2749,7 @@ function FontSizeCombo({ value, onChange }) {
2422
2749
  children: FONT_SIZES.map((sz) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
2423
2750
  selected: sz === value,
2424
2751
  onClick: () => {
2752
+ setDraft(null);
2425
2753
  onChange(sz);
2426
2754
  setOpen(false);
2427
2755
  },
@@ -2442,15 +2770,45 @@ function BlendModeSelect({ value, onChange }) {
2442
2770
  }, mode.v))
2443
2771
  });
2444
2772
  }
2445
- function Toggle({ active, title, onClick, children }) {
2773
+ function Toggle({ active, title, onClick, keepFocus, children }) {
2446
2774
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2447
2775
  type: "button",
2448
2776
  title,
2449
2777
  onClick,
2778
+ onMouseDown: keepFocus ? (e) => e.preventDefault() : void 0,
2450
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"}`,
2451
2780
  children
2452
2781
  });
2453
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
+ }
2454
2812
  function PropControl({ spec, value, mixed, onChange }) {
2455
2813
  switch (spec.key) {
2456
2814
  case "color":
@@ -2585,6 +2943,9 @@ function PropControl({ spec, value, mixed, onChange }) {
2585
2943
  })
2586
2944
  });
2587
2945
  }
2946
+ case "bold":
2947
+ case "italic":
2948
+ case "underline": return null;
2588
2949
  case "blendMode": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2589
2950
  label: spec.label,
2590
2951
  mixed,
@@ -2632,7 +2993,12 @@ function AnnotationStylePanel() {
2632
2993
  className: "text-fg-muted mb-4 text-[11px] font-semibold uppercase tracking-wide",
2633
2994
  children: hasSel ? `${selected.length} selected` : `${activeToolId} defaults`
2634
2995
  }),
2635
- specs.map((spec) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PropControl, {
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, {
2636
3002
  spec,
2637
3003
  value: values[spec.key],
2638
3004
  mixed: hasSel && sel.mixed.includes(spec.key),
@@ -3503,72 +3869,6 @@ function SearchPanel() {
3503
3869
  });
3504
3870
  }
3505
3871
  //#endregion
3506
- //#region src/config/default-stamps.ts
3507
- /**
3508
- * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
3509
- * standard rubber stamps as one Acrobat-compatible PDF per locale.
3510
- *
3511
- * Delivery: the library ships INSIDE whatever ships this viewer. The package
3512
- * exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
3513
- * so it travels through the module graph like any other code — every bundler
3514
- * splits it into a chunk served from the app's own origin, the CDN snippet
3515
- * carries it as a sibling chunk in its folder, and nothing is fetched from a
3516
- * third party. `stamps.defaultLibrary` overrides that:
3517
- * - `false` → no built-in library, no request (air-gapped);
3518
- * - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
3519
- *
3520
- * Loading is LAZY (first open of the stamps panel, never at boot) and
3521
- * locale-aware: the chrome locale leads, the browser languages break ties
3522
- * for the locales the chrome does not translate itself, and a locale change
3523
- * while a default library is loaded swaps it for the new one. The file names
3524
- * itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
3525
- */
3526
- /** The built-in library's PieceInfo id — excluded from persistence (it is
3527
- * loaded again on every first open, in the locale of that moment). */
3528
- const DEFAULT_LIBRARY_ID = "embedpdf-standard";
3529
- /** Which shipped locale to show for the chrome's locale + the browser's. */
3530
- function resolveStampsLocale(chromeLocale) {
3531
- return (0, _embedpdf_react_i18n.negotiateLocale)(_embedpdf_default_stamps_library.LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
3532
- }
3533
- const fetchBytes = async (url) => {
3534
- const response = await fetch(url);
3535
- if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
3536
- return new Uint8Array(await response.arrayBuffer());
3537
- };
3538
- /** The library bytes for `locale`: your URL when you gave one, otherwise the
3539
- * copy that shipped with the viewer. Never anything else. */
3540
- const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : (0, _embedpdf_default_stamps_library.loadDefaultLibrary)(locale);
3541
- /** Per workspace: the locale the default library was last brought to, and
3542
- * the in-flight work — the panel mounts and unmounts with the sidebar, so
3543
- * this cannot live in component state. */
3544
- const loads = /* @__PURE__ */ new WeakMap();
3545
- /**
3546
- * Bring the built-in library to `locale`. Idempotent per workspace and
3547
- * locale; a locale change swaps the loaded default (remove, then import);
3548
- * a default the user removed in this session stays removed — the locale is
3549
- * recorded, nothing is re-added. An embedder that seeded a library under
3550
- * the same id is left alone.
3551
- */
3552
- function ensureDefaultLibrary(stamp, locale, source) {
3553
- if (source === false) return Promise.resolve();
3554
- const current = loads.get(stamp);
3555
- if (current?.locale === locale) return current.promise;
3556
- const run = async () => {
3557
- await current?.promise.catch(() => void 0);
3558
- const loaded = stamp.library(DEFAULT_LIBRARY_ID);
3559
- if (loaded?.locale === locale) return;
3560
- if (current && !loaded) return;
3561
- if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
3562
- await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
3563
- };
3564
- const promise = run();
3565
- loads.set(stamp, {
3566
- locale,
3567
- promise
3568
- });
3569
- return promise;
3570
- }
3571
- //#endregion
3572
3872
  //#region src/ui/stamps-panel.tsx
3573
3873
  /**
3574
3874
  * The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
@@ -3591,30 +3891,15 @@ function ensureDefaultLibrary(stamp, locale, source) {
3591
3891
  * through the surface's open props.
3592
3892
  */
3593
3893
  const ALL = "all";
3594
- /** The user's libraries live in the browser (IndexedDB): restored on first
3595
- * open, written on every change. The built-in set is never stored — it is
3596
- * fetched again next time, in the locale of that moment. */
3597
- const store = typeof indexedDB === "undefined" ? null : (0, _embedpdf_react_stamp.indexedDbByteStore)("embedpdf-stamps", { storeName: "libraries" });
3598
- /** Restore runs ONCE per workspace — the panel mounts and unmounts with the
3599
- * sidebar, so the guard cannot live in component state. */
3600
- const restored = /* @__PURE__ */ new WeakMap();
3601
- const restoreOnce = (stamp) => {
3602
- let pending = restored.get(stamp);
3603
- if (!pending) {
3604
- pending = store ? (0, _embedpdf_react_stamp.restoreStampLibraries)(stamp, store) : Promise.resolve();
3605
- restored.set(stamp, pending);
3606
- }
3607
- return pending;
3608
- };
3609
3894
  function StampsPanel() {
3610
3895
  const t = (0, _embedpdf_react_i18n.useT)();
3611
3896
  const stamp = (0, _embedpdf_react_stamp.useStamp)();
3612
- const libraries = (0, _embedpdf_react_stamp.useStampLibraries)();
3897
+ const { defaultLibrary, sidebar } = useStampsConfig();
3898
+ const libraries = (0, _embedpdf_react_stamp.useStampLibraries)({ kind: sidebar ?? ["stamps"] });
3613
3899
  const assets = (0, _embedpdf_react_stamp.useStampAssets)();
3614
3900
  const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
3615
3901
  const { activeToolId } = (0, _embedpdf_react_interaction.useTool)();
3616
3902
  const { locale } = (0, _embedpdf_react_i18n.useLocale)();
3617
- const { defaultLibrary } = useStampsConfig();
3618
3903
  const surface = (0, _embedpdf_react_shell.useSurface)("stamps");
3619
3904
  const fileRef = (0, react.useRef)(null);
3620
3905
  const [armedId, setArmedId] = (0, react.useState)(null);
@@ -3628,7 +3913,7 @@ function StampsPanel() {
3628
3913
  (0, react.useEffect)(() => {
3629
3914
  let live = true;
3630
3915
  setBusy("loading");
3631
- restoreOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
3916
+ restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
3632
3917
  console.error("[embedpdf] default stamps failed:", err);
3633
3918
  if (live) setError(t("demo.stampsError"));
3634
3919
  }).finally(() => live && setBusy(null));
@@ -3636,7 +3921,6 @@ function StampsPanel() {
3636
3921
  live = false;
3637
3922
  };
3638
3923
  }, [stamp, locale]);
3639
- (0, react.useEffect)(() => store ? (0, _embedpdf_react_stamp.persistStampLibraries)(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0, [stamp]);
3640
3924
  (0, react.useEffect)(() => {
3641
3925
  if (activeToolId !== "stamp") setArmedId(null);
3642
3926
  }, [activeToolId]);
@@ -3807,38 +4091,325 @@ function StampAssetCell({ asset, armed, onArm }) {
3807
4091
  });
3808
4092
  }
3809
4093
  //#endregion
3810
- //#region src/ui/panels.tsx
4094
+ //#region src/ui/signatures-panel.tsx
3811
4095
  /**
3812
- * The static chrome around the toolbars: the left/right sidebars (shell
3813
- * surfaces) and the bottom page-controls overlay. Deliberately minimal —
3814
- * chrome-parity scope. Panels read their open state from plugin-shell; the
3815
- * app owns their DOM.
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.
3816
4100
  *
3817
- * There is no built-in header: branding, a locale picker and a theme switch
3818
- * are the EMBEDDER's chrome, not the viewer's. The frame keeps a `header`
3819
- * socket for a slotted one (see Shell).
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.
3820
4109
  */
3821
- function LeftSidebar() {
4110
+ const fieldLabel = (field) => field.kind === "fqn" ? field.name : `#${field.fieldObjectNumber}`;
4111
+ function SignaturesPanel() {
3822
4112
  const t = (0, _embedpdf_react_i18n.useT)();
3823
- const sidebar = (0, _embedpdf_react_shell.useSurface)("sidebar");
3824
- if (!sidebar.isOpen) return null;
3825
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3826
- className: "border-border-subtle bg-surface flex w-60 shrink-0 flex-col border-r",
3827
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3828
- className: "border-border-subtle flex items-center gap-1 border-b p-2",
3829
- children: [
3830
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3831
- className: "bg-accent-light text-accent rounded-md px-2 py-1 text-xs font-medium",
3832
- children: t("demo.thumbnails")
3833
- }),
3834
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3835
- className: "text-fg-muted rounded-md px-2 py-1 text-xs font-medium",
3836
- children: t("demo.outline")
3837
- }),
3838
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "flex-1" }),
3839
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3840
- type: "button",
3841
- onClick: sidebar.close,
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
4381
+ //#region src/ui/panels.tsx
4382
+ /**
4383
+ * The static chrome around the toolbars: the left/right sidebars (shell
4384
+ * surfaces) and the bottom page-controls overlay. Deliberately minimal —
4385
+ * chrome-parity scope. Panels read their open state from plugin-shell; the
4386
+ * app owns their DOM.
4387
+ *
4388
+ * There is no built-in header: branding, a locale picker and a theme switch
4389
+ * are the EMBEDDER's chrome, not the viewer's. The frame keeps a `header`
4390
+ * socket for a slotted one (see Shell).
4391
+ */
4392
+ function LeftSidebar() {
4393
+ const t = (0, _embedpdf_react_i18n.useT)();
4394
+ const sidebar = (0, _embedpdf_react_shell.useSurface)("sidebar");
4395
+ if (!sidebar.isOpen) return null;
4396
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
4397
+ className: "border-border-subtle bg-surface flex w-60 shrink-0 flex-col border-r",
4398
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4399
+ className: "border-border-subtle flex items-center gap-1 border-b p-2",
4400
+ children: [
4401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4402
+ className: "bg-accent-light text-accent rounded-md px-2 py-1 text-xs font-medium",
4403
+ children: t("demo.thumbnails")
4404
+ }),
4405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4406
+ className: "text-fg-muted rounded-md px-2 py-1 text-xs font-medium",
4407
+ children: t("demo.outline")
4408
+ }),
4409
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "flex-1" }),
4410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4411
+ type: "button",
4412
+ onClick: sidebar.close,
3842
4413
  className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
3843
4414
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3844
4415
  name: "x",
@@ -3901,10 +4472,11 @@ function RightSidebar() {
3901
4472
  const style = (0, _embedpdf_react_shell.useSurface)("annotation-style");
3902
4473
  const redaction = (0, _embedpdf_react_shell.useSurface)("redaction");
3903
4474
  const stamps = (0, _embedpdf_react_shell.useSurface)("stamps");
3904
- const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : null;
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;
3905
4477
  if (!active) return null;
3906
- const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : "demo.styleTitle";
3907
- const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.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;
3908
4480
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3909
4481
  className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
3910
4482
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3921,7 +4493,7 @@ function RightSidebar() {
3921
4493
  size: 16
3922
4494
  })
3923
4495
  })]
3924
- }), 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, {}) : /* @__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")]
3925
4497
  });
3926
4498
  }
3927
4499
  function PageControls() {
@@ -4012,6 +4584,684 @@ function RedactConfirmModal() {
4012
4584
  });
4013
4585
  }
4014
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
4015
5265
  //#region src/ui/document-boot.tsx
4016
5266
  /**
4017
5267
  * Boot-state panes for the document area — what renders INSTEAD of the Stage
@@ -4253,9 +5503,10 @@ function Shell() {
4253
5503
  className: "w-full"
4254
5504
  })
4255
5505
  });
4256
- 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", {
4257
5507
  className: "bg-app text-fg flex h-full flex-col",
4258
5508
  children: [
5509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampLibraryStore, {}),
4259
5510
  (frame.header ?? true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", { name: "header" }),
4260
5511
  (frame.tabs ?? "always") !== "never" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", {
4261
5512
  name: "tabs",
@@ -4275,6 +5526,7 @@ function Shell() {
4275
5526
  overlay: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4276
5527
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStrip, {}),
4277
5528
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionStrip, {}),
5529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureInspector, {}),
4278
5530
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_selection.SelectionHandles, {}),
4279
5531
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "y" }),
4280
5532
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "x" })
@@ -4293,12 +5545,15 @@ function Shell() {
4293
5545
  ]
4294
5546
  }),
4295
5547
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RightSidebar, {}),
4296
- /* @__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, {})
4297
5552
  ] })
4298
5553
  }),
4299
5554
  toolbarEdge === "bottom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeBand, { edge: "bottom" }), toolbarBand] })
4300
5555
  ]
4301
- });
5556
+ }) });
4302
5557
  }
4303
5558
  //#endregion
4304
5559
  //#region src/viewer.tsx
@@ -4321,7 +5576,7 @@ function Shell() {
4321
5576
  * changing these props on a mounted viewer does not rebuild the workspace.
4322
5577
  */
4323
5578
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
4324
- const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-BRYSk4rY.cjs")).then((m) => m.es) };
5579
+ const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-DYwzfxyt.cjs")).then((m) => m.es) };
4325
5580
  const isDict = (v) => typeof v === "object" && v !== null;
4326
5581
  /**
4327
5582
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -4365,7 +5620,7 @@ function Booting() {
4365
5620
  })
4366
5621
  });
4367
5622
  }
4368
- function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, theme, themeTarget, onViewer }) {
5623
+ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, signatures, annotations, theme, themeTarget, onViewer }) {
4369
5624
  const [resolved] = (0, react.useState)(() => {
4370
5625
  const byId = new Map(require_commands.defaultCommands.map((c) => [c.id, c]));
4371
5626
  for (const c of commands ?? []) byId.set(c.id, c);
@@ -4397,6 +5652,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4397
5652
  chrome: resolvedChrome,
4398
5653
  icons: icons ?? {},
4399
5654
  stamps: stamps ?? {},
5655
+ signatures: signatures ?? {},
5656
+ annotations: annotations ?? {},
4400
5657
  i18n: {
4401
5658
  locales,
4402
5659
  loaders,
@@ -4462,6 +5719,12 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4462
5719
  }
4463
5720
  } }),
4464
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
+ }),
4465
5728
  (0, _embedpdf_react_link.linkPlugin)(),
4466
5729
  (0, _embedpdf_react_redaction.redactionPlugin)(),
4467
5730
  (0, _embedpdf_react_search.searchPlugin)(),
@@ -4562,6 +5825,12 @@ Object.defineProperty(exports, "ShellToken", {
4562
5825
  return _embedpdf_react_shell.ShellToken;
4563
5826
  }
4564
5827
  });
5828
+ Object.defineProperty(exports, "SignatureToken", {
5829
+ enumerable: true,
5830
+ get: function() {
5831
+ return _embedpdf_react_signature.SignatureToken;
5832
+ }
5833
+ });
4565
5834
  Object.defineProperty(exports, "StageToken", {
4566
5835
  enumerable: true,
4567
5836
  get: function() {
@@ -4586,6 +5855,12 @@ Object.defineProperty(exports, "chromeHelpers", {
4586
5855
  return _embedpdf_react_toolbar.chromeHelpers;
4587
5856
  }
4588
5857
  });
5858
+ Object.defineProperty(exports, "createTestSigner", {
5859
+ enumerable: true,
5860
+ get: function() {
5861
+ return _embedpdf_react_signature.createTestSigner;
5862
+ }
5863
+ });
4589
5864
  exports.createViewerHandle = createViewerHandle;
4590
5865
  Object.defineProperty(exports, "custom", {
4591
5866
  enumerable: true,
@@ -4608,12 +5883,36 @@ Object.defineProperty(exports, "group", {
4608
5883
  return _embedpdf_react_toolbar.group;
4609
5884
  }
4610
5885
  });
5886
+ Object.defineProperty(exports, "indexedDbKeyStore", {
5887
+ enumerable: true,
5888
+ get: function() {
5889
+ return _embedpdf_react_signature.indexedDbKeyStore;
5890
+ }
5891
+ });
4611
5892
  Object.defineProperty(exports, "item", {
4612
5893
  enumerable: true,
4613
5894
  get: function() {
4614
5895
  return _embedpdf_react_toolbar.item;
4615
5896
  }
4616
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
+ });
4617
5916
  Object.defineProperty(exports, "removeItems", {
4618
5917
  enumerable: true,
4619
5918
  get: function() {
@@ -4633,5 +5932,11 @@ Object.defineProperty(exports, "validateChrome", {
4633
5932
  return _embedpdf_react_toolbar.validateChrome;
4634
5933
  }
4635
5934
  });
5935
+ Object.defineProperty(exports, "webCryptoSigner", {
5936
+ enumerable: true,
5937
+ get: function() {
5938
+ return _embedpdf_react_signature.webCryptoSigner;
5939
+ }
5940
+ });
4636
5941
 
4637
5942
  //# sourceMappingURL=index.cjs.map