@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/dist/index.js CHANGED
@@ -8,18 +8,23 @@ import { pageEditPlugin } from "@embedpdf/react/page-edit";
8
8
  import { InteractionToken, InteractionToken as InteractionToken$1, feedbackPlugin, interactionPlugin, useTool, useToolCursor, vibrationFeedback } from "@embedpdf/react/interaction";
9
9
  import { SelectionClipboard, SelectionHandles, SelectionLayer, SelectionMenu, SelectionToken, selectionPlugin } from "@embedpdf/react/selection";
10
10
  import { AnnotationLayer, AnnotationToken, AnnotationToken as AnnotationToken$1, annotationPlugin, refKey, useAnnotation, useAnnotationDefaults, useAnnotationSelected, useAnnotationSelection, useCommentThreads, useComments, useCommentsHydration, useFilePickerProvider, useSelectionFlags, useSelectionProps } from "@embedpdf/react/annotation";
11
+ import { StampToken, StampToken as StampToken$1, indexedDbByteStore, persistStampLibraries, restoreStampLibraries, stampPlugin, useArmStampAsset, useStamp, useStampAssetPreviewUrl, useStampAssets, useStampLibraries } from "@embedpdf/react/stamp";
11
12
  import { RedactionToken, RedactionToken as RedactionToken$1, redactionPlugin, usePendingRedactions, useRedaction } from "@embedpdf/react/redaction";
12
13
  import { actionsPlugin, useActionsUiAdapter } from "@embedpdf/react/actions";
13
- import { FormToken, formPlugin, formWidgetRenderer } from "@embedpdf/react/form";
14
+ import { FormToken, FormToken as FormToken$1, formPlugin, formWidgetRenderer } from "@embedpdf/react/form";
15
+ import { INITIALS_MARK_NAME, SIGNATURES_LIBRARY_KIND, SIGNATURE_MARK_NAME, SignatureToken, createTestSigner, indexedDbKeyStore, memoryKeyStore, personalSigner, remoteSigner, signaturePlugin, useSignature, useSignatureEvent, useSignatureSnapshot, useSignatureTarget, useSignatureVerdicts, useSignerRows, webCryptoSigner } from "@embedpdf/react/signature";
14
16
  import { LinkLayer, linkPlugin } from "@embedpdf/react/link";
15
17
  import { SearchLayer, SearchToken, SearchToken as SearchToken$1, searchPlugin, useSearch, useSearchState, validateSearchQuery } from "@embedpdf/react/search";
16
- import { I18nToken, i18nPlugin, negotiateLocale, useT } from "@embedpdf/react/i18n";
18
+ import { I18nToken, i18nPlugin, negotiateLocale, useLocale, useT } from "@embedpdf/react/i18n";
17
19
  import { CommandsToken, CommandsToken as CommandsToken$1, commandsPlugin, resolvedCommandsEqual, useCommand, useCommandShortcuts, useCommands } from "@embedpdf/react/commands";
18
- import { ShellToken, ShellToken as ShellToken$1, shellPlugin, useMenus, useSurface } from "@embedpdf/react/shell";
20
+ import { ShellToken, ShellToken as ShellToken$1, shellPlugin, useMenus, useShell, useSurface } from "@embedpdf/react/shell";
19
21
  import { Toolbar, addItem, chromeHelpers, chromeHelpers as chromeHelpers$1, custom, defineChrome, group, item, removeItems, replaceItem, useStripView, validateChrome, validateChrome as validateChrome$1 } from "@embedpdf/react/toolbar";
20
22
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
21
23
  import { Scrollbar } from "@embedpdf/react/scrollbar";
24
+ import { LOCALES, loadDefaultLibrary } from "@embedpdf/default-stamps/library";
25
+ import { mountWebFont } from "@embedpdf/web";
22
26
  import { AnnotationMenu } from "@embedpdf/react/annotation-menu";
27
+ import { Anchored } from "@embedpdf/react/anchored";
23
28
  import { MetadataToken } from "@embedpdf/react/metadata";
24
29
  //#region src/config/stage.ts
25
30
  /**
@@ -34,24 +39,23 @@ const ThumbsStageToken = createCapabilityToken("stage-thumbs");
34
39
  //#endregion
35
40
  //#region src/config/demo-tools.plugin.ts
36
41
  /**
37
- * Example-only: register the insert / redact authoring tools as INERT
38
- * interaction tools. v3 doesn't yet ship signature or redaction plugins, but
39
- * this is a CHROME demo — its job is selection state and layout, not the
40
- * tool's effect. Registering them as real (behaviourless) tools lets every
41
- * tool button in every mode band go through the SAME path
42
- * (`InteractionToken.activateTool`), so `active` is uniform and honest: the
43
- * button really is the active tool; it just has no handler behind it.
42
+ * Example-only: register the still-unimplemented authoring tools as INERT
43
+ * interaction tools. v3 doesn't yet ship a signature plugin, but this is a
44
+ * CHROME demo — its job is selection state and layout, not the tool's effect.
45
+ * Registering them as real (behaviourless) tools lets every tool button in
46
+ * every mode band go through the SAME path (`InteractionToken.activateTool`),
47
+ * so `active` is uniform and honest: the button really is the active tool; it
48
+ * just has no handler behind it.
44
49
  *
45
- * The annotate + shapes tools are NOT here — those are real tools the
46
- * annotation plugin registers (highlight, ink, square, circle, …) — and
47
- * neither are the form palette tools (the form plugin's draw-to-place).
50
+ * Everything else in the Insert band is REAL now: stamps come from the stamp
51
+ * plugin's libraries (armed by the stamps sidebar), and image/attachment are
52
+ * the annotation plugin's click-then-pick tools. The annotate + shapes tools
53
+ * were never here either — nor the form palette (the form plugin's
54
+ * draw-to-place).
48
55
  */
49
56
  const INERT_TOOLS = [{
50
57
  id: "signature",
51
58
  cursor: "copy"
52
- }, {
53
- id: "image",
54
- cursor: "copy"
55
59
  }];
56
60
  const demoToolsPlugin = () => definePlugin({
57
61
  id: "demo-tools",
@@ -146,6 +150,7 @@ const en = {
146
150
  delete: "Delete",
147
151
  group: "Group",
148
152
  ungroup: "Ungroup",
153
+ stampFromSelection: "Make stamp",
149
154
  strip: "Annotation actions"
150
155
  },
151
156
  selection: {
@@ -171,7 +176,8 @@ const en = {
171
176
  checkbox: "Checkbox",
172
177
  radio: "Radio Button",
173
178
  select: "Dropdown",
174
- listbox: "List Box"
179
+ listbox: "List Box",
180
+ signature: "Signature Field"
175
181
  },
176
182
  redact: {
177
183
  mark: "Mark for Redaction",
@@ -268,9 +274,91 @@ const en = {
268
274
  redactConfirmCollateral: "{count} other annotations will also be removed.",
269
275
  redactApply: "Apply",
270
276
  redactApplying: "Applying…",
277
+ formatLabel: "Format",
271
278
  redactLabelTitle: "Redaction label",
272
279
  redactLabelPlaceholder: "e.g. REDACTED",
273
- redactLabelRepeat: "Repeat label across region"
280
+ redactLabelRepeat: "Repeat label across region",
281
+ stampsTitle: "Stamps",
282
+ stampsAll: "All stamps",
283
+ stampsLoading: "Loading stamps…",
284
+ stampsEmpty: "No stamps yet. Import a PDF to turn each of its pages into a stamp.",
285
+ stampsRemoveStamp: "Remove stamp",
286
+ stampsArmedHint: "Click a page to place the stamp.",
287
+ stampsImport: "Import PDF…",
288
+ stampsImporting: "Importing…",
289
+ stampsImportHint: "Import a PDF as a stamp library — each page becomes one stamp.",
290
+ stampsRemoveLibrary: "Remove library",
291
+ stampsExportLibrary: "Export library as PDF",
292
+ stampsCustomLibrary: "My stamps",
293
+ stampsCustomLabel: "Custom stamp",
294
+ stampsError: "The built-in stamps could not be loaded.",
295
+ stampsArmError: "That stamp could not be armed.",
296
+ stampsImportError: "That PDF could not be imported as a stamp library.",
297
+ quickStampsLabel: "Quick stamps",
298
+ signaturesTitle: "Signatures",
299
+ signaturesEmpty: "No signatures yet. Create one to sign with.",
300
+ signaturesNew: "New signature…",
301
+ signaturesArmedHint: "Click a signature field to sign it, or anywhere on a page to place the mark.",
302
+ signaturesTargetHint: "Pick a mark for the selected field.",
303
+ signaturesInDocument: "In this document",
304
+ signaturesNone: "This document has no signature fields.",
305
+ signaturesValidate: "Validate",
306
+ signaturesValidating: "Validating…",
307
+ signaturesSigned: "Signed",
308
+ signaturesUnsigned: "Unsigned",
309
+ signaturesRename: "Rename",
310
+ signaturesRemove: "Remove",
311
+ signaturesExport: "Export as PDF",
312
+ signaturesAddInitials: "Add initials",
313
+ signatureLabel: "Signature",
314
+ initialsLabel: "Initials",
315
+ makerTitle: "New signature",
316
+ makerName: "Name",
317
+ makerNamePlaceholder: "Your name",
318
+ makerDraw: "Draw",
319
+ makerType: "Type",
320
+ makerUpload: "Upload",
321
+ makerTypePlaceholder: "Type your name",
322
+ makerFont: "Font",
323
+ makerUploadHint: "Choose a PNG, JPEG, or one-page PDF",
324
+ makerClear: "Clear",
325
+ makerOptional: "(optional)",
326
+ makerSave: "Save",
327
+ makerSaving: "Saving…",
328
+ makerError: "The signature could not be saved.",
329
+ signTitle: "Sign this field",
330
+ signReason: "Reason",
331
+ signLocation: "Location",
332
+ signKind: "What may change afterwards",
333
+ signKindApproval: "Approval signature — form filling and more signatures stay valid",
334
+ signKindCertifyAnnotate: "Certify — allow comments, form filling and signatures",
335
+ signKindCertifyFill: "Certify — allow form filling and signatures only",
336
+ signKindCertifyLocked: "Certify — allow no changes",
337
+ signButton: "Sign",
338
+ signSigning: "Signing…",
339
+ signFill: "Place without signing",
340
+ signError: "The field could not be signed.",
341
+ inspectTitle: "Signature",
342
+ inspectSigner: "Signed by",
343
+ inspectReason: "Reason",
344
+ inspectLocation: "Location",
345
+ inspectTime: "Time",
346
+ inspectIntegrity: "Document",
347
+ inspectCryptography: "Signature",
348
+ inspectTrust: "Signer",
349
+ inspectChanges: "Changes since",
350
+ inspectDownload: "Download signed version",
351
+ verdictValid: "Valid",
352
+ verdictValidUntrusted: "Valid, signer not trusted",
353
+ verdictInvalid: "Invalid — modified after signing",
354
+ verdictWillInvalidate: "Valid, invalid once saved",
355
+ signaturesInvalidating: "This change will invalidate {field} when the document is saved. Undo it to keep the signature.",
356
+ verdictIndeterminate: "Could not be verified",
357
+ verdictUnchanged: "None",
358
+ verdictRestored: "Changed, then restored",
359
+ inspectLaterRevisions: "Revisions after signing",
360
+ verdictPermitted: "Permitted",
361
+ verdictForbidden: "Not permitted"
274
362
  }
275
363
  }
276
364
  };
@@ -287,7 +375,10 @@ const en = {
287
375
  */
288
376
  const ViewerConfigContext = createContext({
289
377
  chrome: defaultChrome,
290
- icons: {}
378
+ icons: {},
379
+ stamps: {},
380
+ signatures: {},
381
+ annotations: {}
291
382
  });
292
383
  const ViewerConfigProvider = ViewerConfigContext.Provider;
293
384
  function useChromeSchema() {
@@ -302,6 +393,15 @@ function useStripSchema(id) {
302
393
  function useCustomIcons() {
303
394
  return useContext(ViewerConfigContext).icons;
304
395
  }
396
+ function useStampsConfig() {
397
+ return useContext(ViewerConfigContext).stamps;
398
+ }
399
+ function useSignaturesConfig() {
400
+ return useContext(ViewerConfigContext).signatures;
401
+ }
402
+ function useAnnotationsConfig() {
403
+ return useContext(ViewerConfigContext).annotations;
404
+ }
305
405
  //#endregion
306
406
  //#region src/handle.ts
307
407
  function createViewerHandle(kernel) {
@@ -907,6 +1007,166 @@ function ThemeProvider({ children, preference = "system", target }) {
907
1007
  });
908
1008
  }
909
1009
  //#endregion
1010
+ //#region src/config/default-stamps.ts
1011
+ /**
1012
+ * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
1013
+ * standard rubber stamps as one Acrobat-compatible PDF per locale.
1014
+ *
1015
+ * Delivery: the library ships INSIDE whatever ships this viewer. The package
1016
+ * exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
1017
+ * so it travels through the module graph like any other code — every bundler
1018
+ * splits it into a chunk served from the app's own origin, the CDN snippet
1019
+ * carries it as a sibling chunk in its folder, and nothing is fetched from a
1020
+ * third party. `stamps.defaultLibrary` overrides that:
1021
+ * - `false` → no built-in library, no request (air-gapped);
1022
+ * - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
1023
+ *
1024
+ * Loading is LAZY (first open of the stamps panel, never at boot) and
1025
+ * locale-aware: the chrome locale leads, the browser languages break ties
1026
+ * for the locales the chrome does not translate itself, and a locale change
1027
+ * while a default library is loaded swaps it for the new one. The file names
1028
+ * itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
1029
+ */
1030
+ /** The built-in library's PieceInfo id — excluded from persistence (it is
1031
+ * loaded again on every first open, in the locale of that moment). */
1032
+ const DEFAULT_LIBRARY_ID = "embedpdf-standard";
1033
+ /** Which shipped locale to show for the chrome's locale + the browser's. */
1034
+ function resolveStampsLocale(chromeLocale) {
1035
+ return negotiateLocale(LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
1036
+ }
1037
+ const fetchBytes = async (url) => {
1038
+ const response = await fetch(url);
1039
+ if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
1040
+ return new Uint8Array(await response.arrayBuffer());
1041
+ };
1042
+ /** The library bytes for `locale`: your URL when you gave one, otherwise the
1043
+ * copy that shipped with the viewer. Never anything else. */
1044
+ const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : loadDefaultLibrary(locale);
1045
+ /** Per workspace: the locale the default library was last brought to, and
1046
+ * the in-flight work — the panel mounts and unmounts with the sidebar, so
1047
+ * this cannot live in component state. */
1048
+ const loads = /* @__PURE__ */ new WeakMap();
1049
+ /**
1050
+ * Bring the built-in library to `locale`. Idempotent per workspace and
1051
+ * locale; a locale change swaps the loaded default (remove, then import);
1052
+ * a default the user removed in this session stays removed — the locale is
1053
+ * recorded, nothing is re-added. An embedder that seeded a library under
1054
+ * the same id is left alone.
1055
+ */
1056
+ function ensureDefaultLibrary(stamp, locale, source) {
1057
+ if (source === false) return Promise.resolve();
1058
+ const current = loads.get(stamp);
1059
+ if (current?.locale === locale) return current.promise;
1060
+ const run = async () => {
1061
+ await current?.promise.catch(() => void 0);
1062
+ const loaded = stamp.library(DEFAULT_LIBRARY_ID);
1063
+ if (loaded?.locale === locale) return;
1064
+ if (current && !loaded) return;
1065
+ if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
1066
+ await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
1067
+ };
1068
+ const promise = run();
1069
+ loads.set(stamp, {
1070
+ locale,
1071
+ promise
1072
+ });
1073
+ return promise;
1074
+ }
1075
+ //#endregion
1076
+ //#region src/ui/stamp-store.tsx
1077
+ /**
1078
+ * The user's stamp libraries live in the browser (IndexedDB): restored ONCE
1079
+ * per workspace, written on every change, for as long as the viewer lives —
1080
+ * a signature made while the stamps sidebar is closed is persisted exactly
1081
+ * like a stamp imported while it is open. The built-in library is never
1082
+ * stored (it is fetched again next time, in the locale of that moment); the
1083
+ * panels that need it bring it in lazily through `ensureDefaultLibrary`.
1084
+ *
1085
+ * Mounted once in the Shell, workspace-scoped (the stamp plugin is).
1086
+ */
1087
+ const store = typeof indexedDB === "undefined" ? null : indexedDbByteStore("embedpdf-stamps", { storeName: "libraries" });
1088
+ const restored = /* @__PURE__ */ new WeakMap();
1089
+ /** Bring the stored libraries back — once per workspace, whoever asks first. */
1090
+ const restoreStampLibrariesOnce = (stamp) => {
1091
+ let pending = restored.get(stamp);
1092
+ if (!pending) {
1093
+ pending = store ? restoreStampLibraries(stamp, store) : Promise.resolve();
1094
+ restored.set(stamp, pending);
1095
+ }
1096
+ return pending;
1097
+ };
1098
+ function StampLibraryStore() {
1099
+ const stamp = useStamp();
1100
+ useEffect(() => {
1101
+ restoreStampLibrariesOnce(stamp);
1102
+ return store ? persistStampLibraries(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0;
1103
+ }, [stamp]);
1104
+ return null;
1105
+ }
1106
+ //#endregion
1107
+ //#region src/ui/quick-stamps.tsx
1108
+ /**
1109
+ * Quick marks in the toolbar: a row of stamp thumbnails that ARM on click —
1110
+ * the checkmark, the cross, whatever the embedder configured
1111
+ * (`stamps.toolbar`: asset ids, or one library's assets). The hover ghost and
1112
+ * the click placement are the annotation plugin's, exactly as from the
1113
+ * sidebar; only the picker moved. Renders nothing until the assets exist.
1114
+ * Lives in the workspace toolbar, so it reads the ACTIVE document's tool.
1115
+ */
1116
+ function QuickStamps() {
1117
+ const t = useT();
1118
+ const stamp = useStamp();
1119
+ const config = useStampsConfig();
1120
+ const { locale } = useLocale();
1121
+ const assets = useStampAssets();
1122
+ const documentId = useDocumentId();
1123
+ const { armAsset } = useArmStampAsset();
1124
+ const activeToolId = useOptionalSelector(InteractionToken$1, (c) => c.activeToolId(), null);
1125
+ const armedId = useOptionalSelector(StampToken$1, (c) => documentId ? c.armedAsset(documentId)?.id ?? null : null, null);
1126
+ const ids = Array.isArray(config.toolbar) ? config.toolbar : config.toolbar && "library" in config.toolbar ? stamp.library(config.toolbar.library)?.assetIds ?? [] : [];
1127
+ const wantsDefault = ids.some((id) => id.startsWith(`${DEFAULT_LIBRARY_ID}:`));
1128
+ useEffect(() => {
1129
+ if (!wantsDefault) return;
1130
+ restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), config.defaultLibrary)).catch((err) => console.error("[embedpdf] quick stamps failed:", err));
1131
+ }, [
1132
+ stamp,
1133
+ locale,
1134
+ wantsDefault
1135
+ ]);
1136
+ const shown = ids.map((id) => assets.find((a) => a.id === id)).filter((a) => !!a);
1137
+ if (shown.length === 0 || !documentId) return null;
1138
+ return /* @__PURE__ */ jsx("div", {
1139
+ className: "flex items-center gap-1",
1140
+ role: "group",
1141
+ "aria-label": t("demo.quickStampsLabel"),
1142
+ children: shown.map((asset) => /* @__PURE__ */ jsx(QuickStampButton, {
1143
+ asset,
1144
+ active: activeToolId === "stamp" && armedId === asset.id,
1145
+ onArm: () => void armAsset(asset.id).catch((e) => console.error("[embedpdf] arm failed", e))
1146
+ }, asset.id))
1147
+ });
1148
+ }
1149
+ function QuickStampButton({ asset, active, onArm }) {
1150
+ const url = useStampAssetPreviewUrl(asset.id);
1151
+ return /* @__PURE__ */ jsx("button", {
1152
+ type: "button",
1153
+ onClick: onArm,
1154
+ title: asset.label,
1155
+ "aria-label": asset.label,
1156
+ "aria-pressed": active || void 0,
1157
+ className: buttonClass(active, true),
1158
+ children: url ? /* @__PURE__ */ jsx("img", {
1159
+ src: url,
1160
+ alt: "",
1161
+ className: "h-5 w-5 object-contain",
1162
+ draggable: false
1163
+ }) : /* @__PURE__ */ jsx("span", {
1164
+ className: "text-xs",
1165
+ children: asset.label
1166
+ })
1167
+ });
1168
+ }
1169
+ //#endregion
910
1170
  //#region src/ui/menu.tsx
911
1171
  /**
912
1172
  * Menu rendering — a MenuSchema (a tree of command ids) rendered as rows.
@@ -1315,7 +1575,10 @@ function AppToolbar({ bar, className }) {
1315
1575
  variant,
1316
1576
  run
1317
1577
  }),
1318
- renderCustom: { "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ jsx(ZoomControls, {}) : /* @__PURE__ */ jsx(ZoomButton, {}) },
1578
+ renderCustom: {
1579
+ "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ jsx(ZoomControls, {}) : /* @__PURE__ */ jsx(ZoomButton, {}),
1580
+ "quick-stamps": () => /* @__PURE__ */ jsx(QuickStamps, {})
1581
+ },
1319
1582
  renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ jsx(CollapsedModes, { view }) : void 0,
1320
1583
  renderGroupTrigger: (view) => /* @__PURE__ */ jsx(GroupTrigger, { view }),
1321
1584
  renderSeparator: () => /* @__PURE__ */ jsx("span", { className: "bg-border h-6 w-px self-center" }),
@@ -1327,6 +1590,71 @@ function AppToolbar({ bar, className }) {
1327
1590
  });
1328
1591
  }
1329
1592
  //#endregion
1593
+ //#region src/ui/annotation-fonts.tsx
1594
+ /**
1595
+ * The configured annotation fonts (`annotations.fonts`), loaded once per
1596
+ * viewer: fetch the bytes → register them on the viewer's engine under the
1597
+ * key (what a free-text `fontFamily` carries and what the engine embeds) →
1598
+ * mount the same bytes as a `@font-face` named by that key (what the plugin's
1599
+ * `cssFontFamily` emits for a registered font, so the live editor renders the
1600
+ * face the appearance stream will bake). The style panel lists a font only
1601
+ * once BOTH have happened — an offered key always resolves on write.
1602
+ */
1603
+ const LoadedFontsContext = createContext([]);
1604
+ function AnnotationFontsProvider({ children }) {
1605
+ const kernel = useKernel();
1606
+ const { fonts } = useAnnotationsConfig();
1607
+ const [loaded, setLoaded] = useState([]);
1608
+ useEffect(() => {
1609
+ const engineFonts = kernel.engine.fonts;
1610
+ if (!fonts?.length || !engineFonts) return;
1611
+ let cancelled = false;
1612
+ const unmounts = [];
1613
+ (async () => {
1614
+ for (const font of fonts) try {
1615
+ const response = await fetch(font.url);
1616
+ if (!response.ok) throw new Error(`${font.url}: HTTP ${response.status}`);
1617
+ const bytes = new Uint8Array(await response.arrayBuffer());
1618
+ if (cancelled) return;
1619
+ await engineFonts.register({
1620
+ key: font.key,
1621
+ familyName: font.familyName,
1622
+ weight: font.weight,
1623
+ italic: font.italic,
1624
+ data: bytes
1625
+ });
1626
+ const unmount = await mountWebFont(font.key, bytes, {
1627
+ weight: font.weight,
1628
+ style: font.italic ? "italic" : "normal"
1629
+ });
1630
+ if (cancelled) {
1631
+ unmount();
1632
+ return;
1633
+ }
1634
+ unmounts.push(unmount);
1635
+ setLoaded((prev) => prev.some((f) => f.key === font.key) ? prev : [...prev, {
1636
+ key: font.key,
1637
+ label: font.label
1638
+ }]);
1639
+ } catch (error) {
1640
+ console.warn(`[embedpdf] annotation font "${font.key}" was not loaded:`, error);
1641
+ }
1642
+ })();
1643
+ return () => {
1644
+ cancelled = true;
1645
+ for (const unmount of unmounts) unmount();
1646
+ };
1647
+ }, [kernel, fonts]);
1648
+ return /* @__PURE__ */ jsx(LoadedFontsContext.Provider, {
1649
+ value: loaded,
1650
+ children
1651
+ });
1652
+ }
1653
+ /** The configured fonts that are registered AND mounted, in config order. */
1654
+ function useAnnotationFonts() {
1655
+ return useContext(LoadedFontsContext);
1656
+ }
1657
+ //#endregion
1330
1658
  //#region src/ui/link-editor.tsx
1331
1659
  /**
1332
1660
  * The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
@@ -2312,12 +2640,17 @@ function LineEndingSelect({ side, value, onChange }) {
2312
2640
  });
2313
2641
  }
2314
2642
  function FontFamilySelect({ value, onChange }) {
2643
+ const configured = useAnnotationFonts();
2644
+ const options = [...FONT_OPTIONS, ...configured.map((f) => ({
2645
+ v: f.key,
2646
+ label: f.label
2647
+ }))];
2315
2648
  return /* @__PURE__ */ jsx(DropdownShell, {
2316
2649
  trigger: /* @__PURE__ */ jsx("span", {
2317
2650
  className: "text-fg text-sm",
2318
- children: FONT_OPTIONS.find((o) => o.v === value)?.label ?? value
2651
+ children: options.find((o) => o.v === value)?.label ?? value
2319
2652
  }),
2320
- children: (close) => FONT_OPTIONS.map((o) => /* @__PURE__ */ jsx(OptionRow, {
2653
+ children: (close) => options.map((o) => /* @__PURE__ */ jsx(OptionRow, {
2321
2654
  selected: o.v === value,
2322
2655
  onClick: () => {
2323
2656
  onChange(o.v);
@@ -2329,17 +2662,33 @@ function FontFamilySelect({ value, onChange }) {
2329
2662
  }
2330
2663
  function FontSizeCombo({ value, onChange }) {
2331
2664
  const [open, setOpen] = useState(false);
2665
+ const rootRef = useOutsideClose(open, () => setOpen(false));
2666
+ const [draft, setDraft] = useState(null);
2667
+ const commit = () => {
2668
+ if (draft === null) return;
2669
+ const n = parseInt(draft, 10);
2670
+ setDraft(null);
2671
+ if (Number.isFinite(n) && n > 0 && n !== value) onChange(n);
2672
+ };
2332
2673
  return /* @__PURE__ */ jsxs("div", {
2333
- ref: useOutsideClose(open, () => setOpen(false)),
2674
+ ref: rootRef,
2334
2675
  className: "relative w-full",
2335
2676
  children: [
2336
2677
  /* @__PURE__ */ jsx("input", {
2337
2678
  type: "number",
2338
2679
  min: 1,
2339
- value,
2340
- onChange: (e) => {
2341
- const n = parseInt(e.target.value, 10);
2342
- if (Number.isFinite(n) && n > 0) onChange(n);
2680
+ value: draft ?? value,
2681
+ onChange: (e) => setDraft(e.target.value),
2682
+ onBlur: commit,
2683
+ onKeyDown: (e) => {
2684
+ if (e.key === "Enter") {
2685
+ e.preventDefault();
2686
+ commit();
2687
+ setOpen(false);
2688
+ } else if (e.key === "Escape") {
2689
+ setDraft(null);
2690
+ setOpen(false);
2691
+ }
2343
2692
  },
2344
2693
  onClick: () => setOpen(true),
2345
2694
  className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 pr-7 text-sm"
@@ -2360,6 +2709,7 @@ function FontSizeCombo({ value, onChange }) {
2360
2709
  children: FONT_SIZES.map((sz) => /* @__PURE__ */ jsx(OptionRow, {
2361
2710
  selected: sz === value,
2362
2711
  onClick: () => {
2712
+ setDraft(null);
2363
2713
  onChange(sz);
2364
2714
  setOpen(false);
2365
2715
  },
@@ -2380,15 +2730,45 @@ function BlendModeSelect({ value, onChange }) {
2380
2730
  }, mode.v))
2381
2731
  });
2382
2732
  }
2383
- function Toggle({ active, title, onClick, children }) {
2733
+ function Toggle({ active, title, onClick, keepFocus, children }) {
2384
2734
  return /* @__PURE__ */ jsx("button", {
2385
2735
  type: "button",
2386
2736
  title,
2387
2737
  onClick,
2738
+ onMouseDown: keepFocus ? (e) => e.preventDefault() : void 0,
2388
2739
  className: `flex h-9 w-9 items-center justify-center rounded border transition-colors ${active ? "border-accent bg-accent text-on-accent" : "border-border bg-surface text-fg hover:bg-hover"}`,
2389
2740
  children
2390
2741
  });
2391
2742
  }
2743
+ const isFormatSpec = (spec) => spec.key === "bold" || spec.key === "italic" || spec.key === "underline";
2744
+ /**
2745
+ * The format toggles a free-text kind declares, in one row. While the text
2746
+ * editor holds a range they read and write that range's runs (the plugin
2747
+ * routes `updateSelection`); otherwise the annotation's body. The buttons
2748
+ * keep focus in the editor so the range survives the click.
2749
+ */
2750
+ function FormatToggles({ specs, values, mixed, onChange }) {
2751
+ return /* @__PURE__ */ jsx(Field, {
2752
+ label: useT()("demo.formatLabel", { fallback: "Format" }),
2753
+ mixed: specs.some((s) => mixed.includes(s.key)),
2754
+ children: /* @__PURE__ */ jsx("div", {
2755
+ className: "flex gap-2",
2756
+ children: specs.map((spec) => {
2757
+ const active = values[spec.key] === true && !mixed.includes(spec.key);
2758
+ return /* @__PURE__ */ jsx(Toggle, {
2759
+ title: spec.label,
2760
+ active,
2761
+ keepFocus: true,
2762
+ onClick: () => onChange({ [spec.key]: !active }),
2763
+ children: /* @__PURE__ */ jsx(Icon, {
2764
+ name: spec.key,
2765
+ size: 18
2766
+ })
2767
+ }, spec.key);
2768
+ })
2769
+ })
2770
+ });
2771
+ }
2392
2772
  function PropControl({ spec, value, mixed, onChange }) {
2393
2773
  switch (spec.key) {
2394
2774
  case "color":
@@ -2523,6 +2903,9 @@ function PropControl({ spec, value, mixed, onChange }) {
2523
2903
  })
2524
2904
  });
2525
2905
  }
2906
+ case "bold":
2907
+ case "italic":
2908
+ case "underline": return null;
2526
2909
  case "blendMode": return /* @__PURE__ */ jsx(Field, {
2527
2910
  label: spec.label,
2528
2911
  mixed,
@@ -2570,7 +2953,12 @@ function AnnotationStylePanel() {
2570
2953
  className: "text-fg-muted mb-4 text-[11px] font-semibold uppercase tracking-wide",
2571
2954
  children: hasSel ? `${selected.length} selected` : `${activeToolId} defaults`
2572
2955
  }),
2573
- specs.map((spec) => /* @__PURE__ */ jsx(PropControl, {
2956
+ specs.map((spec) => spec.key === "bold" ? /* @__PURE__ */ jsx(FormatToggles, {
2957
+ specs: specs.filter((s) => isFormatSpec(s)),
2958
+ values,
2959
+ mixed: hasSel ? sel.mixed : [],
2960
+ onChange: write
2961
+ }, "format") : /* @__PURE__ */ jsx(PropControl, {
2574
2962
  spec,
2575
2963
  value: values[spec.key],
2576
2964
  mixed: hasSel && sel.mixed.includes(spec.key),
@@ -3441,6 +3829,515 @@ function SearchPanel() {
3441
3829
  });
3442
3830
  }
3443
3831
  //#endregion
3832
+ //#region src/ui/stamps-panel.tsx
3833
+ /**
3834
+ * The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
3835
+ * The stamp plugin owns the libraries, the asset bytes and the cached
3836
+ * previews; this panel is the PICKER over them:
3837
+ *
3838
+ * a library <select> ("All stamps" + one per library, shown once there
3839
+ * are two) → a two-column grid of thumbnails → click a stamp → `armAsset`
3840
+ * arms the annotation plugin's stamp tool → hovering a page ghosts the
3841
+ * exact placement → each click on a page places one.
3842
+ *
3843
+ * Deliberately NOT a file dialog: picking arbitrary image bytes is the Image
3844
+ * tool's job (a click-then-pick tool, `insert:add-image`). The stamp button
3845
+ * opens a library; a library is a set of reusable, named assets — the two
3846
+ * gestures are different, so they are different controls.
3847
+ *
3848
+ * The panel stays open while you place, so a stamp can be dropped on several
3849
+ * pages and swapped without a round trip through the toolbar. A command can
3850
+ * open it ON a library (`annotation:stamp-from-selection` → "My stamps")
3851
+ * through the surface's open props.
3852
+ */
3853
+ const ALL = "all";
3854
+ function StampsPanel() {
3855
+ const t = useT();
3856
+ const stamp = useStamp();
3857
+ const { defaultLibrary, sidebar } = useStampsConfig();
3858
+ const libraries = useStampLibraries({ kind: sidebar ?? ["stamps"] });
3859
+ const assets = useStampAssets();
3860
+ const { armAsset } = useArmStampAsset();
3861
+ const { activeToolId } = useTool();
3862
+ const { locale } = useLocale();
3863
+ const surface = useSurface("stamps");
3864
+ const fileRef = useRef(null);
3865
+ const [armedId, setArmedId] = useState(null);
3866
+ const [busy, setBusy] = useState(null);
3867
+ const [error, setError] = useState(null);
3868
+ const [picked, setPicked] = useState(ALL);
3869
+ const openedOn = surface.props?.libraryId;
3870
+ useEffect(() => {
3871
+ if (typeof openedOn === "string") setPicked(openedOn);
3872
+ }, [openedOn]);
3873
+ useEffect(() => {
3874
+ let live = true;
3875
+ setBusy("loading");
3876
+ restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
3877
+ console.error("[embedpdf] default stamps failed:", err);
3878
+ if (live) setError(t("demo.stampsError"));
3879
+ }).finally(() => live && setBusy(null));
3880
+ return () => {
3881
+ live = false;
3882
+ };
3883
+ }, [stamp, locale]);
3884
+ useEffect(() => {
3885
+ if (activeToolId !== "stamp") setArmedId(null);
3886
+ }, [activeToolId]);
3887
+ const arm = (asset) => {
3888
+ setError(null);
3889
+ setArmedId(asset.id);
3890
+ armAsset(asset.id).catch((err) => {
3891
+ console.error("[embedpdf] arm stamp failed:", err);
3892
+ setArmedId(null);
3893
+ setError(t("demo.stampsArmError"));
3894
+ });
3895
+ };
3896
+ const importPdf = (file) => {
3897
+ setError(null);
3898
+ setBusy("importing");
3899
+ stamp.importLibraryPdf(file, { name: file.name.replace(/\.pdf$/i, "") }).then((id) => setPicked(id)).catch((err) => {
3900
+ console.error("[embedpdf] stamp library import failed:", err);
3901
+ setError(t("demo.stampsImportError"));
3902
+ }).finally(() => setBusy(null));
3903
+ };
3904
+ /** The library as the PDF it is — title, registry, artwork — for Acrobat
3905
+ * or another viewer. */
3906
+ const exportPdf = (libraryId, name) => {
3907
+ const bytes = stamp.exportLibrary(libraryId);
3908
+ if (!bytes) return;
3909
+ const blob = new Blob([bytes], { type: "application/pdf" });
3910
+ const url = URL.createObjectURL(blob);
3911
+ const a = document.createElement("a");
3912
+ a.href = url;
3913
+ a.download = `${name || "stamps"}.pdf`;
3914
+ a.click();
3915
+ URL.revokeObjectURL(url);
3916
+ };
3917
+ const selectedId = libraries.length === 1 ? libraries[0].id : libraries.some((library) => library.id === picked) ? picked : ALL;
3918
+ const selected = selectedId === ALL ? null : libraries.find((l) => l.id === selectedId) ?? null;
3919
+ const shown = selectedId === ALL ? assets : assets.filter((a) => a.libraryId === selectedId);
3920
+ const isDefault = (libraryId) => libraryId === DEFAULT_LIBRARY_ID;
3921
+ return /* @__PURE__ */ jsxs("div", {
3922
+ className: "flex min-h-0 flex-1 flex-col",
3923
+ children: [
3924
+ libraries.length > 0 && /* @__PURE__ */ jsxs("div", {
3925
+ className: "border-border-subtle flex items-center gap-1 border-b p-2",
3926
+ children: [libraries.length > 1 ? /* @__PURE__ */ jsxs("select", {
3927
+ value: selectedId,
3928
+ onChange: (event) => setPicked(event.target.value),
3929
+ "aria-label": t("demo.stampsTitle"),
3930
+ className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-2 py-1.5 text-sm",
3931
+ children: [/* @__PURE__ */ jsx("option", {
3932
+ value: ALL,
3933
+ children: t("demo.stampsAll")
3934
+ }), libraries.map((library) => /* @__PURE__ */ jsx("option", {
3935
+ value: library.id,
3936
+ children: library.name
3937
+ }, library.id))]
3938
+ }) : /* @__PURE__ */ jsx("span", {
3939
+ className: "text-fg-muted min-w-0 flex-1 truncate px-1 text-xs font-semibold uppercase tracking-wide",
3940
+ children: libraries[0].name
3941
+ }), selected && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
3942
+ type: "button",
3943
+ onClick: () => exportPdf(selected.id, selected.name),
3944
+ title: t("demo.stampsExportLibrary"),
3945
+ className: "text-fg-muted hover:text-fg grid h-7 w-7 shrink-0 place-items-center rounded",
3946
+ children: /* @__PURE__ */ jsx(Icon, {
3947
+ name: "download",
3948
+ size: 14
3949
+ })
3950
+ }), !isDefault(selected.id) && /* @__PURE__ */ jsx("button", {
3951
+ type: "button",
3952
+ onClick: () => void stamp.removeLibrary(selected.id),
3953
+ title: t("demo.stampsRemoveLibrary"),
3954
+ className: "text-fg-muted hover:text-fg grid h-7 w-7 shrink-0 place-items-center rounded",
3955
+ children: /* @__PURE__ */ jsx(Icon, {
3956
+ name: "trash",
3957
+ size: 14
3958
+ })
3959
+ })] })]
3960
+ }),
3961
+ /* @__PURE__ */ jsx("div", {
3962
+ className: "min-h-0 flex-1 overflow-y-auto p-3",
3963
+ children: busy === "loading" && shown.length === 0 ? /* @__PURE__ */ jsxs("div", {
3964
+ className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
3965
+ children: [/* @__PURE__ */ jsx("div", { className: "border-border-subtle border-t-accent h-6 w-6 animate-spin rounded-full border-2" }), /* @__PURE__ */ jsx("p", {
3966
+ className: "text-fg-muted text-sm",
3967
+ children: t("demo.stampsLoading")
3968
+ })]
3969
+ }) : shown.length === 0 ? /* @__PURE__ */ jsxs("div", {
3970
+ className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
3971
+ children: [/* @__PURE__ */ jsx(Icon, {
3972
+ name: "rubberStamp",
3973
+ size: 32,
3974
+ className: "text-fg-muted"
3975
+ }), /* @__PURE__ */ jsx("p", {
3976
+ className: "text-fg-muted text-sm",
3977
+ children: t("demo.stampsEmpty")
3978
+ })]
3979
+ }) : /* @__PURE__ */ jsx("ul", {
3980
+ className: "grid grid-cols-2 gap-3",
3981
+ children: shown.map((asset) => /* @__PURE__ */ jsxs("li", {
3982
+ className: "group relative",
3983
+ children: [/* @__PURE__ */ jsx(StampAssetCell, {
3984
+ asset,
3985
+ armed: armedId === asset.id,
3986
+ onArm: () => arm(asset)
3987
+ }), !isDefault(asset.libraryId) && /* @__PURE__ */ jsx("button", {
3988
+ type: "button",
3989
+ onClick: () => void stamp.removeAsset(asset.id),
3990
+ title: t("demo.stampsRemoveStamp"),
3991
+ 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",
3992
+ children: /* @__PURE__ */ jsx(Icon, {
3993
+ name: "x",
3994
+ size: 12
3995
+ })
3996
+ })]
3997
+ }, asset.id))
3998
+ })
3999
+ }),
4000
+ (error || armedId) && /* @__PURE__ */ jsx("div", {
4001
+ className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
4002
+ children: error ?? t("demo.stampsArmedHint")
4003
+ }),
4004
+ /* @__PURE__ */ jsxs("div", {
4005
+ className: "border-border-subtle shrink-0 border-t p-3",
4006
+ children: [/* @__PURE__ */ jsxs("button", {
4007
+ type: "button",
4008
+ disabled: busy != null,
4009
+ onClick: () => fileRef.current?.click(),
4010
+ title: t("demo.stampsImportHint"),
4011
+ 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",
4012
+ children: [/* @__PURE__ */ jsx(Icon, {
4013
+ name: "rubberStampPlus",
4014
+ size: 16
4015
+ }), busy === "importing" ? t("demo.stampsImporting") : t("demo.stampsImport")]
4016
+ }), /* @__PURE__ */ jsx("input", {
4017
+ ref: fileRef,
4018
+ type: "file",
4019
+ accept: "application/pdf",
4020
+ className: "hidden",
4021
+ onChange: (e) => {
4022
+ const file = e.currentTarget.files?.[0];
4023
+ e.currentTarget.value = "";
4024
+ if (file) importPdf(file);
4025
+ }
4026
+ })]
4027
+ })
4028
+ ]
4029
+ });
4030
+ }
4031
+ /** One grid cell: the plugin's cached preview through the object-URL hook
4032
+ * (the DOM resource's lifetime belongs to the framework layer, not the
4033
+ * store). Image only, as v2 — the artwork IS the label; the label is the
4034
+ * tooltip for the curious. */
4035
+ function StampAssetCell({ asset, armed, onArm }) {
4036
+ const url = useStampAssetPreviewUrl(asset.id);
4037
+ return /* @__PURE__ */ jsx("button", {
4038
+ type: "button",
4039
+ onClick: onArm,
4040
+ title: asset.label,
4041
+ "aria-pressed": armed,
4042
+ 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"}`,
4043
+ children: url ? /* @__PURE__ */ jsx("img", {
4044
+ src: url,
4045
+ alt: asset.label,
4046
+ className: "max-h-full max-w-full object-contain"
4047
+ }) : /* @__PURE__ */ jsx("span", {
4048
+ className: "text-fg truncate text-sm",
4049
+ children: asset.label
4050
+ })
4051
+ });
4052
+ }
4053
+ //#endregion
4054
+ //#region src/ui/signatures-panel.tsx
4055
+ /**
4056
+ * The signatures sidebar (right panel) — Preview's signature sheet on v3
4057
+ * parts. One row per PERSON: a stamp library of kind `signatures` holding a
4058
+ * full signature and, optionally, initials. Nothing is stored beyond the
4059
+ * file; rename/export/delete are the library verbs.
4060
+ *
4061
+ * pick a mark → with a TARGET field ("sign here" was clicked, or a
4062
+ * signature widget selected) the mark goes into that field by the
4063
+ * plugin's mode; otherwise it ARMS — hover a page for the ghost, click a
4064
+ * signature field to sign it, click anywhere else to drop it as a stamp.
4065
+ *
4066
+ * Below the people: the signature fields of THIS document, signed or not,
4067
+ * with their verdicts — click an unsigned one to target it, a signed one to
4068
+ * inspect it.
4069
+ */
4070
+ const fieldLabel = (field) => field.kind === "fqn" ? field.name : `#${field.fieldObjectNumber}`;
4071
+ function SignaturesPanel() {
4072
+ const t = useT();
4073
+ const stamp = useStamp();
4074
+ const signature = useSignature();
4075
+ const config = useSignaturesConfig();
4076
+ const rows = useSignerRows();
4077
+ const { target, busy } = useSignatureTarget();
4078
+ const { armAsset } = useArmStampAsset();
4079
+ const { activeToolId } = useTool();
4080
+ const maker = useSurface("signature-maker");
4081
+ const [armedId, setArmedId] = useState(null);
4082
+ const [error, setError] = useState(null);
4083
+ const [invalidating, setInvalidating] = useState(null);
4084
+ useSignatureEvent((event) => {
4085
+ if (event.type === "invalidating") setInvalidating(signature.signatureOf(event.field)?.fieldName ?? fieldLabel(event.field));
4086
+ else if (event.type === "validated" && !event.verdicts.some((v) => v.summary === "invalid" && v.modifications.basis === "working-copy")) setInvalidating(null);
4087
+ });
4088
+ useEffect(() => {
4089
+ restoreStampLibrariesOnce(stamp);
4090
+ }, [stamp]);
4091
+ useEffect(() => {
4092
+ if (activeToolId !== "stamp") setArmedId(null);
4093
+ }, [activeToolId]);
4094
+ const pick = (asset) => {
4095
+ setError(null);
4096
+ if (target) {
4097
+ signature.placeMark({ assetId: asset.id }, { field: target }).catch((err) => {
4098
+ console.error("[embedpdf] placing the mark failed:", err);
4099
+ setError(t("demo.signError"));
4100
+ });
4101
+ return;
4102
+ }
4103
+ setArmedId(asset.id);
4104
+ armAsset(asset.id).catch((err) => {
4105
+ console.error("[embedpdf] arm mark failed:", err);
4106
+ setArmedId(null);
4107
+ setError(t("demo.stampsArmError"));
4108
+ });
4109
+ };
4110
+ const canAddPerson = (config.libraries ?? "many") === "many" || rows.length === 0;
4111
+ const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
4112
+ return /* @__PURE__ */ jsxs("div", {
4113
+ className: "flex min-h-0 flex-1 flex-col",
4114
+ children: [
4115
+ /* @__PURE__ */ jsxs("div", {
4116
+ className: "min-h-0 flex-1 overflow-y-auto p-3",
4117
+ children: [rows.length === 0 ? /* @__PURE__ */ jsxs("div", {
4118
+ className: "flex flex-col items-center gap-3 px-4 py-8 text-center",
4119
+ children: [/* @__PURE__ */ jsx(Icon, {
4120
+ name: "signature",
4121
+ size: 32,
4122
+ className: "text-fg-muted"
4123
+ }), /* @__PURE__ */ jsx("p", {
4124
+ className: "text-fg-muted text-sm",
4125
+ children: t("demo.signaturesEmpty")
4126
+ })]
4127
+ }) : /* @__PURE__ */ jsx("ul", {
4128
+ className: "flex flex-col gap-3",
4129
+ children: rows.map((row) => /* @__PURE__ */ jsx(SignerRowView, {
4130
+ row,
4131
+ armedId,
4132
+ wantsInitials,
4133
+ onPick: pick,
4134
+ onAddInitials: () => maker.open({
4135
+ exclusive: "modal",
4136
+ props: { libraryId: row.libraryId }
4137
+ })
4138
+ }, row.libraryId))
4139
+ }), /* @__PURE__ */ jsx(DocumentSignatures, {})]
4140
+ }),
4141
+ invalidating && /* @__PURE__ */ jsx("div", {
4142
+ className: "border-border-subtle shrink-0 border-t px-3 py-2 text-xs text-amber-700",
4143
+ children: t("demo.signaturesInvalidating", { params: { field: invalidating } })
4144
+ }),
4145
+ (error || target || armedId) && /* @__PURE__ */ jsx("div", {
4146
+ className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
4147
+ children: error ?? (target ? busy ? t("demo.signSigning") : t("demo.signaturesTargetHint") : t("demo.signaturesArmedHint"))
4148
+ }),
4149
+ canAddPerson ? /* @__PURE__ */ jsx("div", {
4150
+ className: "border-border-subtle shrink-0 border-t p-3",
4151
+ children: /* @__PURE__ */ jsxs("button", {
4152
+ type: "button",
4153
+ onClick: () => maker.open({ exclusive: "modal" }),
4154
+ className: "border-border text-fg hover:bg-hover flex w-full items-center justify-center gap-2 rounded-md border px-3 py-1.5 text-sm",
4155
+ children: [/* @__PURE__ */ jsx(Icon, {
4156
+ name: "plus",
4157
+ size: 16
4158
+ }), t("demo.signaturesNew")]
4159
+ })
4160
+ }) : null
4161
+ ]
4162
+ });
4163
+ }
4164
+ function SignerRowView({ row, armedId, wantsInitials, onPick, onAddInitials }) {
4165
+ const t = useT();
4166
+ const stamp = useStamp();
4167
+ const [renaming, setRenaming] = useState(null);
4168
+ const exportPdf = () => {
4169
+ const bytes = stamp.exportLibrary(row.libraryId);
4170
+ if (!bytes) return;
4171
+ const blob = new Blob([bytes], { type: "application/pdf" });
4172
+ const url = URL.createObjectURL(blob);
4173
+ const a = document.createElement("a");
4174
+ a.href = url;
4175
+ a.download = `${row.name || "signature"}.pdf`;
4176
+ a.click();
4177
+ URL.revokeObjectURL(url);
4178
+ };
4179
+ const commitRename = () => {
4180
+ const name = renaming?.trim();
4181
+ setRenaming(null);
4182
+ if (name && name !== row.name) stamp.updateLibrary(row.libraryId, { name });
4183
+ };
4184
+ const iconButton = "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded";
4185
+ return /* @__PURE__ */ jsxs("li", {
4186
+ className: "border-border-subtle rounded-md border p-2",
4187
+ children: [/* @__PURE__ */ jsxs("div", {
4188
+ className: "flex items-center gap-1",
4189
+ children: [
4190
+ renaming !== null ? /* @__PURE__ */ jsx("input", {
4191
+ autoFocus: true,
4192
+ value: renaming,
4193
+ onChange: (e) => setRenaming(e.target.value),
4194
+ onBlur: commitRename,
4195
+ onKeyDown: (e) => {
4196
+ if (e.key === "Enter") commitRename();
4197
+ if (e.key === "Escape") setRenaming(null);
4198
+ },
4199
+ className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-1.5 py-0.5 text-sm"
4200
+ }) : /* @__PURE__ */ jsx("span", {
4201
+ className: "text-fg min-w-0 flex-1 truncate text-sm font-medium",
4202
+ children: row.name
4203
+ }),
4204
+ /* @__PURE__ */ jsx("button", {
4205
+ type: "button",
4206
+ onClick: () => setRenaming(row.name),
4207
+ title: t("demo.signaturesRename"),
4208
+ className: iconButton,
4209
+ children: /* @__PURE__ */ jsx(Icon, {
4210
+ name: "pencilMarker",
4211
+ size: 13
4212
+ })
4213
+ }),
4214
+ /* @__PURE__ */ jsx("button", {
4215
+ type: "button",
4216
+ onClick: exportPdf,
4217
+ title: t("demo.signaturesExport"),
4218
+ className: iconButton,
4219
+ children: /* @__PURE__ */ jsx(Icon, {
4220
+ name: "download",
4221
+ size: 13
4222
+ })
4223
+ }),
4224
+ /* @__PURE__ */ jsx("button", {
4225
+ type: "button",
4226
+ onClick: () => void stamp.removeLibrary(row.libraryId),
4227
+ title: t("demo.signaturesRemove"),
4228
+ className: iconButton,
4229
+ children: /* @__PURE__ */ jsx(Icon, {
4230
+ name: "trash",
4231
+ size: 13
4232
+ })
4233
+ })
4234
+ ]
4235
+ }), /* @__PURE__ */ jsxs("div", {
4236
+ className: "mt-2 grid grid-cols-[2fr_1fr] gap-2",
4237
+ children: [row.signatures.map((asset) => /* @__PURE__ */ jsx(MarkThumb, {
4238
+ asset,
4239
+ label: t("demo.signatureLabel"),
4240
+ armed: armedId === asset.id,
4241
+ onPick: () => onPick(asset)
4242
+ }, asset.id)), wantsInitials ? row.initials ? /* @__PURE__ */ jsx(MarkThumb, {
4243
+ asset: row.initials,
4244
+ label: t("demo.initialsLabel"),
4245
+ armed: armedId === row.initials.id,
4246
+ onPick: () => onPick(row.initials)
4247
+ }) : /* @__PURE__ */ jsx("button", {
4248
+ type: "button",
4249
+ onClick: onAddInitials,
4250
+ title: t("demo.signaturesAddInitials"),
4251
+ className: "border-border-subtle text-fg-muted hover:bg-hover flex h-16 items-center justify-center rounded-md border border-dashed text-xs",
4252
+ children: /* @__PURE__ */ jsx(Icon, {
4253
+ name: "plus",
4254
+ size: 14
4255
+ })
4256
+ }) : null]
4257
+ })]
4258
+ });
4259
+ }
4260
+ function MarkThumb({ asset, label, armed, onPick }) {
4261
+ const url = useStampAssetPreviewUrl(asset.id);
4262
+ return /* @__PURE__ */ jsx("button", {
4263
+ type: "button",
4264
+ onClick: onPick,
4265
+ title: label,
4266
+ "aria-pressed": armed,
4267
+ className: `flex h-16 items-center justify-center rounded-md border p-1 ${armed ? "border-accent bg-accent-light ring-accent ring-2" : "border-border-subtle hover:border-border hover:bg-hover"}`,
4268
+ children: url ? /* @__PURE__ */ jsx("img", {
4269
+ src: url,
4270
+ alt: label,
4271
+ className: "max-h-full max-w-full object-contain"
4272
+ }) : /* @__PURE__ */ jsx("span", {
4273
+ className: "text-fg-muted text-xs",
4274
+ children: label
4275
+ })
4276
+ });
4277
+ }
4278
+ /** The signature fields of this document, with their state. */
4279
+ function DocumentSignatures() {
4280
+ const t = useT();
4281
+ const signature = useSignature();
4282
+ const snapshot = useSignatureSnapshot();
4283
+ const verdicts = useSignatureVerdicts();
4284
+ const { target } = useSignatureTarget();
4285
+ const [validating, setValidating] = useState(false);
4286
+ const fields = snapshot?.signatures ?? [];
4287
+ const verdictOf = (dto) => verdicts?.find((v) => v.signature.index === dto.index) ?? null;
4288
+ const verdictLabel = (v) => !v ? t("demo.signaturesSigned") : v.summary === "valid" ? t("demo.verdictValid") : v.summary === "valid-untrusted" ? t("demo.verdictValidUntrusted") : v.summary === "indeterminate" ? t("demo.verdictIndeterminate") : v.modifications.basis === "working-copy" ? t("demo.verdictWillInvalidate") : t("demo.verdictInvalid");
4289
+ const verdictTone = (v) => !v || v.summary === "indeterminate" ? "text-fg-muted" : v.summary === "invalid" ? v.modifications.basis === "working-copy" ? "text-amber-600" : "text-red-600" : v.summary === "valid-untrusted" ? "text-amber-600" : "text-green-600";
4290
+ const isTarget = (dto) => !!target && (target.kind === "fqn" ? target.name === dto.fieldName : target.fieldObjectNumber === (dto.field.kind === "objectNumber" ? dto.field.fieldObjectNumber : -1));
4291
+ const validate = () => {
4292
+ setValidating(true);
4293
+ signature.validate().finally(() => setValidating(false));
4294
+ };
4295
+ return /* @__PURE__ */ jsxs("div", {
4296
+ className: "mt-4",
4297
+ children: [/* @__PURE__ */ jsxs("div", {
4298
+ className: "flex items-center justify-between",
4299
+ children: [/* @__PURE__ */ jsx("span", {
4300
+ className: "text-fg-muted text-xs font-semibold uppercase tracking-wide",
4301
+ children: t("demo.signaturesInDocument")
4302
+ }), fields.some((f) => f.signed) ? /* @__PURE__ */ jsx("button", {
4303
+ type: "button",
4304
+ disabled: validating,
4305
+ onClick: validate,
4306
+ className: "text-accent text-xs hover:underline disabled:opacity-60",
4307
+ children: validating ? t("demo.signaturesValidating") : t("demo.signaturesValidate")
4308
+ }) : null]
4309
+ }), fields.length === 0 ? /* @__PURE__ */ jsx("p", {
4310
+ className: "text-fg-muted mt-1 text-xs",
4311
+ children: t("demo.signaturesNone")
4312
+ }) : /* @__PURE__ */ jsx("ul", {
4313
+ className: "mt-1 flex flex-col gap-1",
4314
+ children: fields.map((dto) => {
4315
+ const verdict = verdictOf(dto);
4316
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
4317
+ type: "button",
4318
+ onClick: () => dto.signed ? signature.inspect(dto.field) : signature.setTarget(dto.field),
4319
+ className: `hover:bg-hover flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm ${isTarget(dto) ? "bg-accent-light" : ""}`,
4320
+ children: [
4321
+ /* @__PURE__ */ jsx(Icon, {
4322
+ name: dto.signed ? "check" : "signature",
4323
+ size: 14,
4324
+ className: dto.signed ? "text-green-600" : "text-fg-muted"
4325
+ }),
4326
+ /* @__PURE__ */ jsx("span", {
4327
+ className: "text-fg min-w-0 flex-1 truncate",
4328
+ children: dto.fieldName
4329
+ }),
4330
+ /* @__PURE__ */ jsx("span", {
4331
+ className: `shrink-0 text-right text-xs ${dto.signed ? verdictTone(verdict) : "text-fg-muted"}`,
4332
+ children: dto.signed ? verdictLabel(verdict) : t("demo.signaturesUnsigned")
4333
+ })
4334
+ ]
4335
+ }) }, dto.index);
4336
+ })
4337
+ })]
4338
+ });
4339
+ }
4340
+ //#endregion
3444
4341
  //#region src/ui/panels.tsx
3445
4342
  /**
3446
4343
  * The static chrome around the toolbars: the left/right sidebars (shell
@@ -3534,10 +4431,12 @@ function RightSidebar() {
3534
4431
  const comment = useSurface("comment");
3535
4432
  const style = useSurface("annotation-style");
3536
4433
  const redaction = useSurface("redaction");
3537
- const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : null;
4434
+ const stamps = useSurface("stamps");
4435
+ const signatures = useSurface("signatures");
4436
+ const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : signatures.isOpen ? "signatures" : null;
3538
4437
  if (!active) return null;
3539
- const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : "demo.styleTitle";
3540
- const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : style.close;
4438
+ const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : active === "signatures" ? "demo.signaturesTitle" : "demo.styleTitle";
4439
+ const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : active === "signatures" ? signatures.close : style.close;
3541
4440
  return /* @__PURE__ */ jsxs("aside", {
3542
4441
  className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
3543
4442
  children: [/* @__PURE__ */ jsxs("div", {
@@ -3554,7 +4453,7 @@ function RightSidebar() {
3554
4453
  size: 16
3555
4454
  })
3556
4455
  })]
3557
- }), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : /* @__PURE__ */ jsx(CommentsPanel, {}, documentId ?? "none")]
4456
+ }), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ jsx(StampsPanel, {}) : active === "signatures" ? /* @__PURE__ */ jsx(SignaturesPanel, {}) : /* @__PURE__ */ jsx(CommentsPanel, {}, documentId ?? "none")]
3558
4457
  });
3559
4458
  }
3560
4459
  function PageControls() {
@@ -3645,6 +4544,684 @@ function RedactConfirmModal() {
3645
4544
  });
3646
4545
  }
3647
4546
  //#endregion
4547
+ //#region src/ui/sign-dialog.tsx
4548
+ /**
4549
+ * The sign dialog (`signature-sign` modal surface): a mark met a field and
4550
+ * the mode is `ask` — or a "sign here" flow wants the facts first. Shows
4551
+ * the mark as it will sit in the field, takes reason and location, offers
4552
+ * a certification when the config allows it, and either SEALS the field
4553
+ * (`signField`) or only draws the mark into it (`fillField`).
4554
+ */
4555
+ function SignDialog() {
4556
+ const t = useT();
4557
+ const surface = useSurface("signature-sign");
4558
+ const signature = useSignature();
4559
+ const [reason, setReason] = useState("");
4560
+ const [location, setLocation] = useState("");
4561
+ const [signKind, setSignKind] = useState("approval");
4562
+ const [busy, setBusy] = useState(null);
4563
+ const [error, setError] = useState(null);
4564
+ const field = surface.props?.field;
4565
+ const mark = surface.props?.mark;
4566
+ const preview = useStampAssetPreviewUrl(mark && "assetId" in mark ? mark.assetId : null);
4567
+ if (!surface.isOpen || !field || !mark) return null;
4568
+ const attribution = {
4569
+ ...reason.trim() ? { reason: reason.trim() } : {},
4570
+ ...location.trim() ? { location: location.trim() } : {}
4571
+ };
4572
+ const run = async (kind) => {
4573
+ setBusy(kind);
4574
+ setError(null);
4575
+ try {
4576
+ if (kind === "sign") await signature.signField({
4577
+ field,
4578
+ mark,
4579
+ attribution,
4580
+ ...signKind !== "approval" && signature.canCertify() ? { certify: { permission: Number(signKind.slice(-1)) } } : {}
4581
+ });
4582
+ else await signature.fillField(field, mark);
4583
+ surface.close();
4584
+ } catch (err) {
4585
+ setError(err instanceof Error ? err.message : String(err));
4586
+ } finally {
4587
+ setBusy(null);
4588
+ }
4589
+ };
4590
+ const input = "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm";
4591
+ return /* @__PURE__ */ jsx("div", {
4592
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
4593
+ children: /* @__PURE__ */ jsxs("div", {
4594
+ className: "border-border bg-surface w-96 rounded-lg border p-4 shadow-xl",
4595
+ children: [
4596
+ /* @__PURE__ */ jsx("h2", {
4597
+ className: "text-fg text-base font-semibold",
4598
+ children: t("demo.signTitle")
4599
+ }),
4600
+ /* @__PURE__ */ jsx("div", {
4601
+ className: "border-border-subtle mt-3 flex h-24 items-center justify-center rounded-md border p-2",
4602
+ children: preview ? /* @__PURE__ */ jsx("img", {
4603
+ src: preview,
4604
+ alt: "",
4605
+ className: "max-h-full max-w-full object-contain"
4606
+ }) : /* @__PURE__ */ jsx("span", {
4607
+ className: "text-fg-muted text-sm",
4608
+ children: t("demo.signatureLabel")
4609
+ })
4610
+ }),
4611
+ /* @__PURE__ */ jsxs("label", {
4612
+ className: "text-fg-muted mt-3 block text-xs",
4613
+ children: [t("demo.signReason"), /* @__PURE__ */ jsx("input", {
4614
+ value: reason,
4615
+ onChange: (e) => setReason(e.target.value),
4616
+ className: input
4617
+ })]
4618
+ }),
4619
+ /* @__PURE__ */ jsxs("label", {
4620
+ className: "text-fg-muted mt-2 block text-xs",
4621
+ children: [t("demo.signLocation"), /* @__PURE__ */ jsx("input", {
4622
+ value: location,
4623
+ onChange: (e) => setLocation(e.target.value),
4624
+ className: input
4625
+ })]
4626
+ }),
4627
+ signature.canCertify() ? /* @__PURE__ */ jsxs("label", {
4628
+ className: "text-fg-muted mt-3 block text-xs",
4629
+ children: [t("demo.signKind"), /* @__PURE__ */ jsxs("select", {
4630
+ value: signKind,
4631
+ onChange: (e) => setSignKind(e.target.value),
4632
+ className: input,
4633
+ children: [
4634
+ /* @__PURE__ */ jsx("option", {
4635
+ value: "approval",
4636
+ children: t("demo.signKindApproval")
4637
+ }),
4638
+ /* @__PURE__ */ jsx("option", {
4639
+ value: "certify-3",
4640
+ children: t("demo.signKindCertifyAnnotate")
4641
+ }),
4642
+ /* @__PURE__ */ jsx("option", {
4643
+ value: "certify-2",
4644
+ children: t("demo.signKindCertifyFill")
4645
+ }),
4646
+ /* @__PURE__ */ jsx("option", {
4647
+ value: "certify-1",
4648
+ children: t("demo.signKindCertifyLocked")
4649
+ })
4650
+ ]
4651
+ })]
4652
+ }) : null,
4653
+ error ? /* @__PURE__ */ jsx("p", {
4654
+ className: "mt-2 text-sm text-red-600",
4655
+ children: error
4656
+ }) : null,
4657
+ /* @__PURE__ */ jsxs("div", {
4658
+ className: "mt-4 flex justify-end gap-2",
4659
+ children: [
4660
+ /* @__PURE__ */ jsx("button", {
4661
+ type: "button",
4662
+ disabled: busy != null,
4663
+ onClick: surface.close,
4664
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
4665
+ children: t("demo.cancel")
4666
+ }),
4667
+ signature.canFill() ? /* @__PURE__ */ jsx("button", {
4668
+ type: "button",
4669
+ disabled: busy != null,
4670
+ onClick: () => void run("fill"),
4671
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-60",
4672
+ children: t("demo.signFill")
4673
+ }) : null,
4674
+ signature.canSign() ? /* @__PURE__ */ jsx("button", {
4675
+ type: "button",
4676
+ disabled: busy != null,
4677
+ onClick: () => void run("sign"),
4678
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
4679
+ children: busy === "sign" ? t("demo.signSigning") : t("demo.signButton")
4680
+ }) : null
4681
+ ]
4682
+ })
4683
+ ]
4684
+ })
4685
+ });
4686
+ }
4687
+ //#endregion
4688
+ //#region src/ui/signature-bridge.tsx
4689
+ /**
4690
+ * The signature plugin's UI intents → shell surfaces. The plugin owns the
4691
+ * act and says WHAT happened; the chrome decides WHAT OPENS:
4692
+ *
4693
+ * target (a "sign here" click) → the signatures panel, right side
4694
+ * ask (mode 'ask' met a field) → the sign dialog (modal)
4695
+ * inspect (a signed field clicked) → the validation popover
4696
+ *
4697
+ * Mounted once per document view; renders nothing.
4698
+ */
4699
+ function SignatureBridge() {
4700
+ const shell = useShell();
4701
+ useSignatureEvent((event) => {
4702
+ switch (event.type) {
4703
+ case "target":
4704
+ if (event.field) shell.open("signatures", { exclusive: "right" });
4705
+ return;
4706
+ case "ask":
4707
+ shell.open("signature-sign", {
4708
+ exclusive: "modal",
4709
+ props: {
4710
+ field: event.field,
4711
+ mark: event.mark
4712
+ }
4713
+ });
4714
+ return;
4715
+ case "inspect":
4716
+ shell.open("signature-inspector", { props: { field: event.field } });
4717
+ return;
4718
+ case "invalidating":
4719
+ shell.open("signatures", { exclusive: "right" });
4720
+ return;
4721
+ default: return;
4722
+ }
4723
+ });
4724
+ return null;
4725
+ }
4726
+ //#endregion
4727
+ //#region src/ui/signature-inspector.tsx
4728
+ /**
4729
+ * The validation popover (`signature-inspector` surface): what a signed
4730
+ * field's signature says and whether it holds — the four independent facts
4731
+ * (integrity, cryptography, trust, changes since) and the signer's claims,
4732
+ * anchored at the widget when its box is known, plus the signed revision as
4733
+ * a download (the exact bytes the signature covers). Mounted in the Stage
4734
+ * overlay so `<Anchored>` can project onto the page.
4735
+ */
4736
+ const verdictKey = (v) => !v ? "demo.verdictIndeterminate" : v.summary === "valid" ? "demo.verdictValid" : v.summary === "valid-untrusted" ? "demo.verdictValidUntrusted" : v.summary === "invalid" ? v.modifications.basis === "working-copy" ? "demo.verdictWillInvalidate" : "demo.verdictInvalid" : "demo.verdictIndeterminate";
4737
+ /** A PDF date string (`D:YYYYMMDDHHmmSSZ` or with a zone offset) as a locale date; the raw text when it is not one. */
4738
+ const pdfDate = (raw) => {
4739
+ const m = /^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}'?\d{2}'?)?/.exec(raw);
4740
+ if (!m) return raw;
4741
+ const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
4742
+ const offset = !zone || zone === "Z" ? "Z" : `${zone[0]}${zone.slice(1, 3)}:${zone.slice(3).replace(/'/g, "") || "00"}`;
4743
+ const date = /* @__PURE__ */ new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}${offset}`);
4744
+ return Number.isNaN(date.getTime()) ? raw : date.toLocaleString();
4745
+ };
4746
+ const tone = (summary) => summary === "valid" ? "text-green-600" : summary === "invalid" ? "text-red-600" : summary === "valid-untrusted" ? "text-amber-600" : "text-fg-muted";
4747
+ function SignatureInspector() {
4748
+ const t = useT();
4749
+ const surface = useSurface("signature-inspector");
4750
+ const signature = useSignature();
4751
+ const snapshot = useSignatureSnapshot();
4752
+ const verdicts = useSignatureVerdicts();
4753
+ const [validating, setValidating] = useState(false);
4754
+ const field = surface.props?.field;
4755
+ const dto = field ? signature.signatureOf(field) : null;
4756
+ const verdict = field ? signature.verdictOf(field) : null;
4757
+ const widget = dto?.widget ?? null;
4758
+ const box = useSelector(FormToken$1, (c) => widget && widget.annotObjectNumber > 0 ? c.fillItem(widget.annotObjectNumber)?.box ?? null : null);
4759
+ useEffect(() => {
4760
+ if (!surface.isOpen || !dto?.signed || verdicts) return;
4761
+ setValidating(true);
4762
+ signature.validate().finally(() => setValidating(false));
4763
+ }, [surface.isOpen, dto?.index]);
4764
+ if (!surface.isOpen || !field || !dto || !snapshot) return null;
4765
+ const revalidate = () => {
4766
+ setValidating(true);
4767
+ signature.validate().finally(() => setValidating(false));
4768
+ };
4769
+ const summary = verdict?.summary ?? null;
4770
+ const modifications = verdict?.modifications.verdict ?? null;
4771
+ const row = (label, value, className = "text-fg") => /* @__PURE__ */ jsxs("div", {
4772
+ className: "flex items-baseline justify-between gap-3 text-xs",
4773
+ children: [/* @__PURE__ */ jsx("span", {
4774
+ className: "text-fg-muted",
4775
+ children: label
4776
+ }), /* @__PURE__ */ jsx("span", {
4777
+ className: `text-right ${className}`,
4778
+ children: value
4779
+ })]
4780
+ });
4781
+ const card = /* @__PURE__ */ jsxs("div", {
4782
+ className: "border-border bg-elevated w-72 rounded-lg border p-3 text-sm shadow-xl",
4783
+ children: [
4784
+ /* @__PURE__ */ jsxs("div", {
4785
+ className: "flex items-center justify-between",
4786
+ children: [/* @__PURE__ */ jsx("span", {
4787
+ className: "text-fg font-semibold",
4788
+ children: t("demo.inspectTitle")
4789
+ }), /* @__PURE__ */ jsx("button", {
4790
+ type: "button",
4791
+ onClick: surface.close,
4792
+ className: "text-fg-muted hover:bg-hover grid h-6 w-6 place-items-center rounded",
4793
+ "aria-label": t("demo.cancel"),
4794
+ children: /* @__PURE__ */ jsx(Icon, {
4795
+ name: "x",
4796
+ size: 14
4797
+ })
4798
+ })]
4799
+ }),
4800
+ /* @__PURE__ */ jsx("p", {
4801
+ className: `mt-1 text-sm font-medium ${tone(summary)}`,
4802
+ children: validating ? t("demo.signaturesValidating") : t(verdictKey(verdict))
4803
+ }),
4804
+ /* @__PURE__ */ jsxs("div", {
4805
+ className: "mt-2 flex flex-col gap-1",
4806
+ children: [
4807
+ row(t("demo.inspectSigner"), dto.signer.name ?? "—"),
4808
+ dto.signer.reason ? row(t("demo.inspectReason"), dto.signer.reason) : null,
4809
+ dto.signer.location ? row(t("demo.inspectLocation"), dto.signer.location) : null,
4810
+ dto.signer.claimedTime ? row(t("demo.inspectTime"), pdfDate(dto.signer.claimedTime)) : null
4811
+ ]
4812
+ }),
4813
+ /* @__PURE__ */ jsxs("div", {
4814
+ className: "border-border-subtle mt-2 flex flex-col gap-1 border-t pt-2",
4815
+ children: [
4816
+ row(t("demo.inspectIntegrity"), verdict?.integrity ?? "—", tone(verdict?.integrity === "valid" ? "valid" : verdict?.integrity === "invalid" ? "invalid" : null)),
4817
+ row(t("demo.inspectCryptography"), verdict?.cryptography ?? "—"),
4818
+ row(t("demo.inspectTrust"), verdict?.trust ?? "—"),
4819
+ row(t("demo.inspectChanges"), modifications === "unchanged" ? verdict?.modifications.undone ? t("demo.verdictRestored") : t("demo.verdictUnchanged") : modifications === "permitted" ? t("demo.verdictPermitted") : modifications === "forbidden" ? t("demo.verdictForbidden") : "—", modifications === "forbidden" ? "text-red-600" : "text-fg"),
4820
+ verdict?.modifications.laterRevisions ? row(t("demo.inspectLaterRevisions"), String(verdict.modifications.laterRevisions)) : null
4821
+ ]
4822
+ }),
4823
+ /* @__PURE__ */ jsxs("div", {
4824
+ className: "mt-3 flex justify-end gap-2",
4825
+ children: [/* @__PURE__ */ jsx("button", {
4826
+ type: "button",
4827
+ disabled: validating,
4828
+ onClick: revalidate,
4829
+ className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
4830
+ children: t("demo.signaturesValidate")
4831
+ }), /* @__PURE__ */ jsx(DownloadRevision, { field })]
4832
+ })
4833
+ ]
4834
+ });
4835
+ if (widget && widget.pageObjectNumber > 0 && box) return /* @__PURE__ */ jsx(Anchored, {
4836
+ anchor: {
4837
+ pon: widget.pageObjectNumber,
4838
+ bounds: box
4839
+ },
4840
+ placement: "bottom",
4841
+ gap: 8,
4842
+ children: card
4843
+ });
4844
+ return /* @__PURE__ */ jsx("div", {
4845
+ className: "absolute right-4 top-4 z-40",
4846
+ children: card
4847
+ });
4848
+ }
4849
+ /** The signed revision — the exact bytes the signature covers — as a file. */
4850
+ function DownloadRevision({ field }) {
4851
+ const t = useT();
4852
+ const signature = useSignature();
4853
+ const dto = signature.signatureOf(field);
4854
+ const [busy, setBusy] = useState(false);
4855
+ if (!dto || dto.revisionIndex === null) return null;
4856
+ const revisionIndex = dto.revisionIndex;
4857
+ const run = async () => {
4858
+ setBusy(true);
4859
+ try {
4860
+ const bytes = await signature.revisionBytes(revisionIndex);
4861
+ const blob = new Blob([bytes], { type: "application/pdf" });
4862
+ const url = URL.createObjectURL(blob);
4863
+ const a = document.createElement("a");
4864
+ a.href = url;
4865
+ a.download = `${dto.fieldName}-signed.pdf`;
4866
+ a.click();
4867
+ URL.revokeObjectURL(url);
4868
+ } catch (err) {
4869
+ console.error("[embedpdf] signed revision download failed:", err);
4870
+ } finally {
4871
+ setBusy(false);
4872
+ }
4873
+ };
4874
+ return /* @__PURE__ */ jsx("button", {
4875
+ type: "button",
4876
+ disabled: busy,
4877
+ onClick: () => void run(),
4878
+ className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
4879
+ children: t("demo.inspectDownload")
4880
+ });
4881
+ }
4882
+ //#endregion
4883
+ //#region src/ui/signature-maker.tsx
4884
+ /**
4885
+ * The mark maker (`signature-maker` modal surface): a person's signature and
4886
+ * initials, drawn, typed, or uploaded, saved as ONE library of kind
4887
+ * `signatures` — the file IS the person (Preview's model). Chrome code over
4888
+ * the stamp plugin's generic calls: `createLibrary` + `addAsset({ mark })`;
4889
+ * rename/export/delete are the library verbs. Opened on an existing library
4890
+ * (`props.libraryId`) it only adds the missing initials.
4891
+ */
4892
+ const PAD = {
4893
+ width: 360,
4894
+ height: 120
4895
+ };
4896
+ const BUILTIN_FONTS = [{
4897
+ key: "helvetica-oblique",
4898
+ label: "Helvetica"
4899
+ }, {
4900
+ key: "times-italic",
4901
+ label: "Times"
4902
+ }];
4903
+ /** One pad: draw / type / upload, producing a `MarkSource` or null. */
4904
+ function MarkPad({ value, onChange, fonts, registerFont }) {
4905
+ const t = useT();
4906
+ const [tab, setTab] = useState("draw");
4907
+ const [strokes, setStrokes] = useState([]);
4908
+ const [text, setText] = useState("");
4909
+ const [font, setFont] = useState(fonts[0]?.key ?? "helvetica-oblique");
4910
+ const [fileName, setFileName] = useState(null);
4911
+ const canvas = useRef(null);
4912
+ const drawing = useRef(null);
4913
+ useEffect(() => {
4914
+ const el = canvas.current;
4915
+ if (!el) return;
4916
+ const dpr = window.devicePixelRatio || 1;
4917
+ el.width = PAD.width * dpr;
4918
+ el.height = PAD.height * dpr;
4919
+ const ctx = el.getContext("2d");
4920
+ if (!ctx) return;
4921
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
4922
+ ctx.clearRect(0, 0, PAD.width, PAD.height);
4923
+ ctx.lineCap = "round";
4924
+ ctx.lineJoin = "round";
4925
+ ctx.lineWidth = 2.5;
4926
+ ctx.strokeStyle = getComputedStyle(el).color || "#000";
4927
+ for (const stroke of strokes) {
4928
+ ctx.beginPath();
4929
+ stroke.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
4930
+ ctx.stroke();
4931
+ }
4932
+ }, [strokes, tab]);
4933
+ const point = (e) => {
4934
+ const r = e.currentTarget.getBoundingClientRect();
4935
+ return {
4936
+ x: e.clientX - r.left,
4937
+ y: e.clientY - r.top
4938
+ };
4939
+ };
4940
+ const commitStrokes = (next) => {
4941
+ setStrokes(next);
4942
+ onChange(next.length ? {
4943
+ kind: "ink",
4944
+ strokes: next,
4945
+ color: "#000000",
4946
+ strokeWidth: 2.5
4947
+ } : null);
4948
+ };
4949
+ const commitText = async (next, key) => {
4950
+ setText(next);
4951
+ setFont(key);
4952
+ if (!next.trim()) {
4953
+ onChange(null);
4954
+ return;
4955
+ }
4956
+ await registerFont(key);
4957
+ onChange({
4958
+ kind: "text",
4959
+ text: next.trim(),
4960
+ fontFamily: key,
4961
+ color: "#000000",
4962
+ fontSize: 36
4963
+ });
4964
+ };
4965
+ const pickFile = (file) => {
4966
+ setFileName(file.name);
4967
+ onChange(file.type === "application/pdf" ? {
4968
+ kind: "pdf",
4969
+ source: file,
4970
+ pageIndex: 0
4971
+ } : {
4972
+ kind: "image",
4973
+ source: file
4974
+ });
4975
+ };
4976
+ const tabClass = (id) => `rounded px-2 py-1 text-xs font-medium ${tab === id ? "bg-accent text-on-accent" : "text-fg-muted hover:bg-hover"}`;
4977
+ return /* @__PURE__ */ jsxs("div", {
4978
+ className: "flex flex-col gap-2",
4979
+ children: [/* @__PURE__ */ jsxs("div", {
4980
+ className: "flex items-center gap-1",
4981
+ children: [
4982
+ /* @__PURE__ */ jsx("button", {
4983
+ type: "button",
4984
+ className: tabClass("draw"),
4985
+ onClick: () => setTab("draw"),
4986
+ children: t("demo.makerDraw")
4987
+ }),
4988
+ /* @__PURE__ */ jsx("button", {
4989
+ type: "button",
4990
+ className: tabClass("type"),
4991
+ onClick: () => setTab("type"),
4992
+ children: t("demo.makerType")
4993
+ }),
4994
+ /* @__PURE__ */ jsx("button", {
4995
+ type: "button",
4996
+ className: tabClass("upload"),
4997
+ onClick: () => setTab("upload"),
4998
+ children: t("demo.makerUpload")
4999
+ }),
5000
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
5001
+ value ? /* @__PURE__ */ jsx("button", {
5002
+ type: "button",
5003
+ onClick: () => {
5004
+ commitStrokes([]);
5005
+ setText("");
5006
+ setFileName(null);
5007
+ onChange(null);
5008
+ },
5009
+ className: "text-fg-muted hover:text-fg text-xs",
5010
+ children: t("demo.makerClear")
5011
+ }) : null
5012
+ ]
5013
+ }), tab === "draw" ? /* @__PURE__ */ jsx("canvas", {
5014
+ ref: canvas,
5015
+ style: {
5016
+ width: PAD.width,
5017
+ height: PAD.height,
5018
+ touchAction: "none"
5019
+ },
5020
+ className: "border-border-subtle bg-surface text-fg rounded-md border",
5021
+ onPointerDown: (e) => {
5022
+ e.currentTarget.setPointerCapture(e.pointerId);
5023
+ drawing.current = [point(e)];
5024
+ setStrokes((s) => [...s, drawing.current]);
5025
+ },
5026
+ onPointerMove: (e) => {
5027
+ if (!drawing.current) return;
5028
+ drawing.current.push(point(e));
5029
+ setStrokes((s) => [...s.slice(0, -1), [...drawing.current]]);
5030
+ },
5031
+ onPointerUp: () => {
5032
+ if (!drawing.current) return;
5033
+ const done = drawing.current;
5034
+ drawing.current = null;
5035
+ setStrokes((s) => {
5036
+ const next = [...s.slice(0, -1), done];
5037
+ commitStrokes(next);
5038
+ return next;
5039
+ });
5040
+ }
5041
+ }) : tab === "type" ? /* @__PURE__ */ jsxs("div", {
5042
+ className: "flex flex-col gap-2",
5043
+ children: [/* @__PURE__ */ jsx("input", {
5044
+ value: text,
5045
+ placeholder: t("demo.makerTypePlaceholder"),
5046
+ onChange: (e) => void commitText(e.target.value, font),
5047
+ className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm"
5048
+ }), /* @__PURE__ */ jsxs("label", {
5049
+ className: "text-fg-muted flex items-center gap-2 text-xs",
5050
+ children: [t("demo.makerFont"), /* @__PURE__ */ jsx("select", {
5051
+ value: font,
5052
+ onChange: (e) => void commitText(text, e.target.value),
5053
+ className: "border-border bg-surface text-fg rounded border px-2 py-1 text-xs",
5054
+ children: fonts.map((f) => /* @__PURE__ */ jsx("option", {
5055
+ value: f.key,
5056
+ children: f.label
5057
+ }, f.key))
5058
+ })]
5059
+ })]
5060
+ }) : /* @__PURE__ */ jsxs("label", {
5061
+ className: "border-border-subtle text-fg-muted hover:bg-hover flex h-24 cursor-pointer items-center justify-center rounded-md border border-dashed text-sm",
5062
+ children: [fileName ?? t("demo.makerUploadHint"), /* @__PURE__ */ jsx("input", {
5063
+ type: "file",
5064
+ accept: "image/png,image/jpeg,application/pdf",
5065
+ className: "hidden",
5066
+ onChange: (e) => {
5067
+ const file = e.currentTarget.files?.[0];
5068
+ e.currentTarget.value = "";
5069
+ if (file) pickFile(file);
5070
+ }
5071
+ })]
5072
+ })]
5073
+ });
5074
+ }
5075
+ function SignatureMakerModal() {
5076
+ const t = useT();
5077
+ const surface = useSurface("signature-maker");
5078
+ const stamp = useStamp();
5079
+ const kernel = useKernel();
5080
+ const config = useSignaturesConfig();
5081
+ const [name, setName] = useState("");
5082
+ const [signatureMark, setSignatureMark] = useState(null);
5083
+ const [initialsMark, setInitialsMark] = useState(null);
5084
+ const [busy, setBusy] = useState(false);
5085
+ const [error, setError] = useState(null);
5086
+ const existingId = surface.props?.libraryId;
5087
+ const existing = existingId ? stamp.library(existingId) : null;
5088
+ const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
5089
+ const fonts = [...(config.fonts ?? []).map((f) => ({
5090
+ key: f.key,
5091
+ label: f.label
5092
+ })), ...BUILTIN_FONTS];
5093
+ const registered = useRef(/* @__PURE__ */ new Set());
5094
+ const registerFont = async (key) => {
5095
+ const spec = config.fonts?.find((f) => f.key === key);
5096
+ if (!spec || registered.current.has(key) || !kernel.engine.fonts) return;
5097
+ const response = await fetch(spec.url);
5098
+ if (!response.ok) throw new Error(`${spec.url}: HTTP ${response.status}`);
5099
+ await kernel.engine.fonts.register({
5100
+ key,
5101
+ data: new Uint8Array(await response.arrayBuffer())
5102
+ });
5103
+ registered.current.add(key);
5104
+ };
5105
+ if (!surface.isOpen) return null;
5106
+ const canSave = existing ? initialsMark != null : name.trim().length > 0 && signatureMark != null;
5107
+ const save = async () => {
5108
+ setBusy(true);
5109
+ setError(null);
5110
+ try {
5111
+ if (existing) await stamp.addAsset({
5112
+ libraryId: existing.id,
5113
+ name: INITIALS_MARK_NAME,
5114
+ label: t("demo.initialsLabel"),
5115
+ mark: initialsMark
5116
+ });
5117
+ else {
5118
+ const libraryId = await stamp.createLibrary(name.trim(), { kind: SIGNATURES_LIBRARY_KIND });
5119
+ await stamp.addAsset({
5120
+ libraryId,
5121
+ name: SIGNATURE_MARK_NAME,
5122
+ label: t("demo.signatureLabel"),
5123
+ mark: signatureMark
5124
+ });
5125
+ if (initialsMark) await stamp.addAsset({
5126
+ libraryId,
5127
+ name: INITIALS_MARK_NAME,
5128
+ label: t("demo.initialsLabel"),
5129
+ mark: initialsMark
5130
+ });
5131
+ }
5132
+ setName("");
5133
+ setSignatureMark(null);
5134
+ setInitialsMark(null);
5135
+ surface.close();
5136
+ } catch (err) {
5137
+ console.error("[embedpdf] saving the signature failed:", err);
5138
+ setError(t("demo.makerError"));
5139
+ } finally {
5140
+ setBusy(false);
5141
+ }
5142
+ };
5143
+ return /* @__PURE__ */ jsx("div", {
5144
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
5145
+ children: /* @__PURE__ */ jsxs("div", {
5146
+ className: "border-border bg-surface w-[26rem] rounded-lg border p-4 shadow-xl",
5147
+ children: [
5148
+ /* @__PURE__ */ jsxs("div", {
5149
+ className: "flex items-center justify-between",
5150
+ children: [/* @__PURE__ */ jsx("h2", {
5151
+ className: "text-fg text-base font-semibold",
5152
+ children: existing ? t("demo.signaturesAddInitials") : t("demo.makerTitle")
5153
+ }), /* @__PURE__ */ jsx("button", {
5154
+ type: "button",
5155
+ onClick: surface.close,
5156
+ className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
5157
+ "aria-label": t("demo.cancel"),
5158
+ children: /* @__PURE__ */ jsx(Icon, {
5159
+ name: "x",
5160
+ size: 16
5161
+ })
5162
+ })]
5163
+ }),
5164
+ existing ? null : /* @__PURE__ */ jsxs("label", {
5165
+ className: "text-fg-muted mt-3 block text-xs",
5166
+ children: [t("demo.makerName"), /* @__PURE__ */ jsx("input", {
5167
+ value: name,
5168
+ placeholder: t("demo.makerNamePlaceholder"),
5169
+ onChange: (e) => setName(e.target.value),
5170
+ className: "border-border bg-surface text-fg mt-1 w-full rounded border px-2 py-1.5 text-sm"
5171
+ })]
5172
+ }),
5173
+ existing ? null : /* @__PURE__ */ jsxs("div", {
5174
+ className: "mt-3",
5175
+ children: [/* @__PURE__ */ jsx("p", {
5176
+ className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
5177
+ children: t("demo.signatureLabel")
5178
+ }), /* @__PURE__ */ jsx(MarkPad, {
5179
+ value: signatureMark,
5180
+ onChange: setSignatureMark,
5181
+ fonts,
5182
+ registerFont
5183
+ })]
5184
+ }),
5185
+ wantsInitials ? /* @__PURE__ */ jsxs("div", {
5186
+ className: "mt-3",
5187
+ children: [/* @__PURE__ */ jsxs("p", {
5188
+ className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
5189
+ children: [t("demo.initialsLabel"), existing ? null : /* @__PURE__ */ jsx("span", {
5190
+ className: "text-fg-muted ml-1 font-normal normal-case tracking-normal",
5191
+ children: t("demo.makerOptional")
5192
+ })]
5193
+ }), /* @__PURE__ */ jsx(MarkPad, {
5194
+ value: initialsMark,
5195
+ onChange: setInitialsMark,
5196
+ fonts,
5197
+ registerFont
5198
+ })]
5199
+ }) : null,
5200
+ error ? /* @__PURE__ */ jsx("p", {
5201
+ className: "mt-2 text-sm text-red-600",
5202
+ children: error
5203
+ }) : null,
5204
+ /* @__PURE__ */ jsxs("div", {
5205
+ className: "mt-4 flex justify-end gap-2",
5206
+ children: [/* @__PURE__ */ jsx("button", {
5207
+ type: "button",
5208
+ disabled: busy,
5209
+ onClick: surface.close,
5210
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
5211
+ children: t("demo.cancel")
5212
+ }), /* @__PURE__ */ jsx("button", {
5213
+ type: "button",
5214
+ disabled: busy || !canSave,
5215
+ onClick: () => void save(),
5216
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
5217
+ children: busy ? t("demo.makerSaving") : t("demo.makerSave")
5218
+ })]
5219
+ })
5220
+ ]
5221
+ })
5222
+ });
5223
+ }
5224
+ //#endregion
3648
5225
  //#region src/ui/document-boot.tsx
3649
5226
  /**
3650
5227
  * Boot-state panes for the document area — what renders INSTEAD of the Stage
@@ -3886,9 +5463,10 @@ function Shell() {
3886
5463
  className: "w-full"
3887
5464
  })
3888
5465
  });
3889
- return /* @__PURE__ */ jsxs("div", {
5466
+ return /* @__PURE__ */ jsx(AnnotationFontsProvider, { children: /* @__PURE__ */ jsxs("div", {
3890
5467
  className: "bg-app text-fg flex h-full flex-col",
3891
5468
  children: [
5469
+ /* @__PURE__ */ jsx(StampLibraryStore, {}),
3892
5470
  (frame.header ?? true) && /* @__PURE__ */ jsx("slot", { name: "header" }),
3893
5471
  (frame.tabs ?? "always") !== "never" && /* @__PURE__ */ jsx("slot", {
3894
5472
  name: "tabs",
@@ -3908,6 +5486,7 @@ function Shell() {
3908
5486
  overlay: /* @__PURE__ */ jsxs(Fragment$1, { children: [
3909
5487
  /* @__PURE__ */ jsx(AnnotationStrip, {}),
3910
5488
  /* @__PURE__ */ jsx(SelectionStrip, {}),
5489
+ /* @__PURE__ */ jsx(SignatureInspector, {}),
3911
5490
  /* @__PURE__ */ jsx(SelectionHandles, {}),
3912
5491
  /* @__PURE__ */ jsx(Scrollbar, { axis: "y" }),
3913
5492
  /* @__PURE__ */ jsx(Scrollbar, { axis: "x" })
@@ -3926,12 +5505,15 @@ function Shell() {
3926
5505
  ]
3927
5506
  }),
3928
5507
  /* @__PURE__ */ jsx(RightSidebar, {}),
3929
- /* @__PURE__ */ jsx(RedactConfirmModal, {})
5508
+ /* @__PURE__ */ jsx(RedactConfirmModal, {}),
5509
+ /* @__PURE__ */ jsx(SignatureBridge, {}),
5510
+ /* @__PURE__ */ jsx(SignDialog, {}),
5511
+ /* @__PURE__ */ jsx(SignatureMakerModal, {})
3930
5512
  ] })
3931
5513
  }),
3932
5514
  toolbarEdge === "bottom" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ModeBand, { edge: "bottom" }), toolbarBand] })
3933
5515
  ]
3934
- });
5516
+ }) });
3935
5517
  }
3936
5518
  //#endregion
3937
5519
  //#region src/viewer.tsx
@@ -3954,7 +5536,7 @@ function Shell() {
3954
5536
  * changing these props on a mounted viewer does not rebuild the workspace.
3955
5537
  */
3956
5538
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
3957
- const BUILTIN_LOADERS = { es: () => import("./es-aQlZ3hcL.js").then((m) => m.es) };
5539
+ const BUILTIN_LOADERS = { es: () => import("./es-sN6uqBgD.js").then((m) => m.es) };
3958
5540
  const isDict = (v) => typeof v === "object" && v !== null;
3959
5541
  /**
3960
5542
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -3998,7 +5580,7 @@ function Booting() {
3998
5580
  })
3999
5581
  });
4000
5582
  }
4001
- function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, theme, themeTarget, onViewer }) {
5583
+ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, signatures, annotations, theme, themeTarget, onViewer }) {
4002
5584
  const [resolved] = useState(() => {
4003
5585
  const byId = new Map(defaultCommands.map((c) => [c.id, c]));
4004
5586
  for (const c of commands ?? []) byId.set(c.id, c);
@@ -4029,6 +5611,9 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4029
5611
  commands: resolvedCommands,
4030
5612
  chrome: resolvedChrome,
4031
5613
  icons: icons ?? {},
5614
+ stamps: stamps ?? {},
5615
+ signatures: signatures ?? {},
5616
+ annotations: annotations ?? {},
4032
5617
  i18n: {
4033
5618
  locales,
4034
5619
  loaders,
@@ -4077,9 +5662,29 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4077
5662
  start: "none",
4078
5663
  end: "open-arrow"
4079
5664
  } }
5665
+ }, {
5666
+ id: "image",
5667
+ extends: "stamp",
5668
+ source: {
5669
+ kind: "prompt",
5670
+ accept: "image/png,image/jpeg"
5671
+ }
4080
5672
  }] }),
4081
- actionsPlugin({ javascript: { enabled: true } }),
5673
+ stampPlugin(),
5674
+ actionsPlugin({ javascript: {
5675
+ enabled: true,
5676
+ identity: {
5677
+ name: "John Doe",
5678
+ corporation: "Acme Inc"
5679
+ }
5680
+ } }),
4082
5681
  formPlugin(),
5682
+ signaturePlugin({
5683
+ mode: resolved.signatures.mode,
5684
+ signer: resolved.signatures.signer,
5685
+ trust: resolved.signatures.trust,
5686
+ allowCertify: resolved.signatures.allowCertify
5687
+ }),
4083
5688
  linkPlugin(),
4084
5689
  redactionPlugin(),
4085
5690
  searchPlugin(),
@@ -4113,6 +5718,6 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4113
5718
  });
4114
5719
  }
4115
5720
  //#endregion
4116
- export { AnnotationToken, CommandsToken, DocumentsToken, FormToken, FullViewer, I18nToken, InteractionToken, MetadataToken, RedactionToken, SearchToken, SelectionToken, ShellToken, StageToken, addItem, chromeHelpers, createViewerHandle, custom, defaultChrome, defaultCommands, ICON_PATHS as defaultIcons, defineChrome, group, item, removeItems, replaceItem, themeConfigOf, validateChrome };
5721
+ export { AnnotationToken, CommandsToken, DocumentsToken, FormToken, FullViewer, I18nToken, InteractionToken, MetadataToken, RedactionToken, SearchToken, SelectionToken, ShellToken, SignatureToken, StageToken, StampToken, addItem, chromeHelpers, createTestSigner, createViewerHandle, custom, defaultChrome, defaultCommands, ICON_PATHS as defaultIcons, defineChrome, group, indexedDbKeyStore, item, memoryKeyStore, personalSigner, remoteSigner, removeItems, replaceItem, themeConfigOf, validateChrome, webCryptoSigner };
4117
5722
 
4118
5723
  //# sourceMappingURL=index.js.map