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

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,20 +8,24 @@ import { pageEditPlugin } from "@embedpdf/react/page-edit";
8
8
  import { InteractionToken, InteractionToken as InteractionToken$1, feedbackPlugin, interactionPlugin, useTool, useToolCursor, vibrationFeedback } from "@embedpdf/react/interaction";
9
9
  import { SelectionClipboard, SelectionHandles, SelectionLayer, SelectionMenu, SelectionToken, selectionPlugin } from "@embedpdf/react/selection";
10
10
  import { AnnotationLayer, AnnotationToken, AnnotationToken as AnnotationToken$1, annotationPlugin, refKey, useAnnotation, useAnnotationDefaults, useAnnotationSelected, useAnnotationSelection, useCommentThreads, useComments, useCommentsHydration, useFilePickerProvider, useSelectionFlags, useSelectionProps } from "@embedpdf/react/annotation";
11
- import { StampToken, indexedDbByteStore, persistStampLibraries, restoreStampLibraries, stampPlugin, useArmStampAsset, useStamp, useStampAssetPreviewUrl, useStampAssets, useStampLibraries } from "@embedpdf/react/stamp";
11
+ import { StampToken, StampToken as StampToken$1, indexedDbByteStore, persistStampLibraries, restoreStampLibraries, stampPlugin, useArmStampAsset, useStamp, useStampAssetPreviewUrl, useStampAssets, useStampLibraries } from "@embedpdf/react/stamp";
12
+ import { MeasurementToken, MeasurementToken as MeasurementToken$1, measurementPlugin, useMeasurement, usePageScale } from "@embedpdf/react/measurement";
12
13
  import { RedactionToken, RedactionToken as RedactionToken$1, redactionPlugin, usePendingRedactions, useRedaction } from "@embedpdf/react/redaction";
13
14
  import { actionsPlugin, useActionsUiAdapter } from "@embedpdf/react/actions";
14
- import { FormToken, formPlugin, formWidgetRenderer } from "@embedpdf/react/form";
15
+ import { FormToken, FormToken as FormToken$1, formPlugin, formWidgetRenderer } from "@embedpdf/react/form";
16
+ import { INITIALS_MARK_NAME, SIGNATURES_LIBRARY_KIND, SIGNATURE_MARK_NAME, SignatureToken, createTestSigner, indexedDbKeyStore, memoryKeyStore, personalSigner, remoteSigner, signaturePlugin, useSignature, useSignatureEvent, useSignatureSnapshot, useSignatureTarget, useSignatureVerdicts, useSignerRows, webCryptoSigner } from "@embedpdf/react/signature";
15
17
  import { LinkLayer, linkPlugin } from "@embedpdf/react/link";
16
18
  import { SearchLayer, SearchToken, SearchToken as SearchToken$1, searchPlugin, useSearch, useSearchState, validateSearchQuery } from "@embedpdf/react/search";
17
19
  import { I18nToken, i18nPlugin, negotiateLocale, useLocale, useT } from "@embedpdf/react/i18n";
18
20
  import { CommandsToken, CommandsToken as CommandsToken$1, commandsPlugin, resolvedCommandsEqual, useCommand, useCommandShortcuts, useCommands } from "@embedpdf/react/commands";
19
- import { ShellToken, ShellToken as ShellToken$1, shellPlugin, useMenus, useSurface } from "@embedpdf/react/shell";
21
+ import { ShellToken, ShellToken as ShellToken$1, shellPlugin, useMenus, useShell, useSurface } from "@embedpdf/react/shell";
20
22
  import { Toolbar, addItem, chromeHelpers, chromeHelpers as chromeHelpers$1, custom, defineChrome, group, item, removeItems, replaceItem, useStripView, validateChrome, validateChrome as validateChrome$1 } from "@embedpdf/react/toolbar";
21
23
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
22
24
  import { Scrollbar } from "@embedpdf/react/scrollbar";
23
- import { AnnotationMenu } from "@embedpdf/react/annotation-menu";
24
25
  import { LOCALES, loadDefaultLibrary } from "@embedpdf/default-stamps/library";
26
+ import { mountWebFont } from "@embedpdf/web";
27
+ import { AnnotationMenu } from "@embedpdf/react/annotation-menu";
28
+ import { Anchored } from "@embedpdf/react/anchored";
25
29
  import { MetadataToken } from "@embedpdf/react/metadata";
26
30
  //#region src/config/stage.ts
27
31
  /**
@@ -78,6 +82,32 @@ const en = {
78
82
  code: "en",
79
83
  name: "English",
80
84
  translations: {
85
+ measurement: {
86
+ title: "Measurements",
87
+ distance: "Distance",
88
+ perimeter: "Perimeter",
89
+ area: "Area",
90
+ areaUnit: "Area unit",
91
+ resetLabel: "Reset label position",
92
+ calibrate: "Calibrate scale",
93
+ scale: "Scale",
94
+ custom: "Custom",
95
+ unavailable: "Unavailable",
96
+ instructions: "Drag to measure a distance, then click to place its dimension line. Click vertices for a perimeter or area and double-click to finish. Area boundaries must not cross; an unavailable preview cannot be finished. Press Escape to cancel. Drag the label itself to move it.",
97
+ sessionOnly: "This engine keeps page calibration for this session only.",
98
+ allPages: "Apply to all pages",
99
+ recalculate: "Recalculate existing measurements",
100
+ preset: "Scale preset",
101
+ choosePreset: "Choose a preset",
102
+ unit: "Unit",
103
+ precision: "Precision",
104
+ selection: "Selected measurements",
105
+ report: "{updated} updated · {skipped} skipped · {failed} failed",
106
+ knownLength: "Enter the real length represented by the line you just drew.",
107
+ length: "Known length",
108
+ apply: "Apply scale",
109
+ applying: "Applying…"
110
+ },
81
111
  commands: {
82
112
  zoom: {
83
113
  in: "Zoom In",
@@ -121,6 +151,7 @@ const en = {
121
151
  counterclockwise: "Rotate Counter-Clockwise"
122
152
  },
123
153
  mode: {
154
+ measure: "Measure",
124
155
  group: "Mode",
125
156
  view: "View",
126
157
  annotate: "Annotate",
@@ -130,6 +161,7 @@ const en = {
130
161
  redact: "Redact"
131
162
  },
132
163
  annotate: {
164
+ cancelCreation: "Cancel drawing",
133
165
  highlight: "Highlight",
134
166
  strikeout: "Strikeout",
135
167
  underline: "Underline",
@@ -173,7 +205,8 @@ const en = {
173
205
  checkbox: "Checkbox",
174
206
  radio: "Radio Button",
175
207
  select: "Dropdown",
176
- listbox: "List Box"
208
+ listbox: "List Box",
209
+ signature: "Signature Field"
177
210
  },
178
211
  redact: {
179
212
  mark: "Mark for Redaction",
@@ -270,6 +303,7 @@ const en = {
270
303
  redactConfirmCollateral: "{count} other annotations will also be removed.",
271
304
  redactApply: "Apply",
272
305
  redactApplying: "Applying…",
306
+ formatLabel: "Format",
273
307
  redactLabelTitle: "Redaction label",
274
308
  redactLabelPlaceholder: "e.g. REDACTED",
275
309
  redactLabelRepeat: "Repeat label across region",
@@ -288,7 +322,72 @@ const en = {
288
322
  stampsCustomLabel: "Custom stamp",
289
323
  stampsError: "The built-in stamps could not be loaded.",
290
324
  stampsArmError: "That stamp could not be armed.",
291
- stampsImportError: "That PDF could not be imported as a stamp library."
325
+ stampsImportError: "That PDF could not be imported as a stamp library.",
326
+ quickStampsLabel: "Quick stamps",
327
+ signaturesTitle: "Signatures",
328
+ signaturesEmpty: "No signatures yet. Create one to sign with.",
329
+ signaturesNew: "New signature…",
330
+ signaturesArmedHint: "Click a signature field to sign it, or anywhere on a page to place the mark.",
331
+ signaturesTargetHint: "Pick a mark for the selected field.",
332
+ signaturesInDocument: "In this document",
333
+ signaturesNone: "This document has no signature fields.",
334
+ signaturesValidate: "Validate",
335
+ signaturesValidating: "Validating…",
336
+ signaturesSigned: "Signed",
337
+ signaturesUnsigned: "Unsigned",
338
+ signaturesRename: "Rename",
339
+ signaturesRemove: "Remove",
340
+ signaturesExport: "Export as PDF",
341
+ signaturesAddInitials: "Add initials",
342
+ signatureLabel: "Signature",
343
+ initialsLabel: "Initials",
344
+ makerTitle: "New signature",
345
+ makerName: "Name",
346
+ makerNamePlaceholder: "Your name",
347
+ makerDraw: "Draw",
348
+ makerType: "Type",
349
+ makerUpload: "Upload",
350
+ makerTypePlaceholder: "Type your name",
351
+ makerFont: "Font",
352
+ makerUploadHint: "Choose a PNG, JPEG, or one-page PDF",
353
+ makerClear: "Clear",
354
+ makerOptional: "(optional)",
355
+ makerSave: "Save",
356
+ makerSaving: "Saving…",
357
+ makerError: "The signature could not be saved.",
358
+ signTitle: "Sign this field",
359
+ signReason: "Reason",
360
+ signLocation: "Location",
361
+ signKind: "What may change afterwards",
362
+ signKindApproval: "Approval signature — form filling and more signatures stay valid",
363
+ signKindCertifyAnnotate: "Certify — allow comments, form filling and signatures",
364
+ signKindCertifyFill: "Certify — allow form filling and signatures only",
365
+ signKindCertifyLocked: "Certify — allow no changes",
366
+ signButton: "Sign",
367
+ signSigning: "Signing…",
368
+ signFill: "Place without signing",
369
+ signError: "The field could not be signed.",
370
+ inspectTitle: "Signature",
371
+ inspectSigner: "Signed by",
372
+ inspectReason: "Reason",
373
+ inspectLocation: "Location",
374
+ inspectTime: "Time",
375
+ inspectIntegrity: "Document",
376
+ inspectCryptography: "Signature",
377
+ inspectTrust: "Signer",
378
+ inspectChanges: "Changes since",
379
+ inspectDownload: "Download signed version",
380
+ verdictValid: "Valid",
381
+ verdictValidUntrusted: "Valid, signer not trusted",
382
+ verdictInvalid: "Invalid — modified after signing",
383
+ verdictWillInvalidate: "Valid, invalid once saved",
384
+ signaturesInvalidating: "This change will invalidate {field} when the document is saved. Undo it to keep the signature.",
385
+ verdictIndeterminate: "Could not be verified",
386
+ verdictUnchanged: "None",
387
+ verdictRestored: "Changed, then restored",
388
+ inspectLaterRevisions: "Revisions after signing",
389
+ verdictPermitted: "Permitted",
390
+ verdictForbidden: "Not permitted"
292
391
  }
293
392
  }
294
393
  };
@@ -306,7 +405,9 @@ const en = {
306
405
  const ViewerConfigContext = createContext({
307
406
  chrome: defaultChrome,
308
407
  icons: {},
309
- stamps: {}
408
+ stamps: {},
409
+ signatures: {},
410
+ annotations: {}
310
411
  });
311
412
  const ViewerConfigProvider = ViewerConfigContext.Provider;
312
413
  function useChromeSchema() {
@@ -324,6 +425,12 @@ function useCustomIcons() {
324
425
  function useStampsConfig() {
325
426
  return useContext(ViewerConfigContext).stamps;
326
427
  }
428
+ function useSignaturesConfig() {
429
+ return useContext(ViewerConfigContext).signatures;
430
+ }
431
+ function useAnnotationsConfig() {
432
+ return useContext(ViewerConfigContext).annotations;
433
+ }
327
434
  //#endregion
328
435
  //#region src/handle.ts
329
436
  function createViewerHandle(kernel) {
@@ -354,7 +461,40 @@ function createViewerHandle(kernel) {
354
461
  }
355
462
  //#endregion
356
463
  //#region src/ui/icons.tsx
464
+ const measurementOutline = [{
465
+ d: "M5 4H19M20 5V19M19 20H5M4 19V5",
466
+ stroke: "primary"
467
+ }, {
468
+ d: "M3.1 3.1h1.8v1.8H3.1zM19.1 3.1h1.8v1.8h-1.8zM3.1 19.1h1.8v1.8H3.1zM19.1 19.1h1.8v1.8h-1.8z",
469
+ stroke: "primary"
470
+ }];
357
471
  const ICON_PATHS = {
472
+ ruler: ["M3 8h18v8H3z", "M7 8v4M11 8v2M15 8v4M19 8v2"],
473
+ distance: [{
474
+ d: "M5 12H19",
475
+ stroke: "primary",
476
+ strokeDasharray: "2 2"
477
+ }, {
478
+ d: "M3.5 10.5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 1 0 0-3M20.5 10.5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 1 0 0-3",
479
+ stroke: "primary",
480
+ fill: "primary",
481
+ fillOpacity: .25
482
+ }],
483
+ perimeter: measurementOutline,
484
+ area: [
485
+ {
486
+ d: "M5 4H19V5H20V19H19V20H5V19H4V5H5Z",
487
+ fill: "primary",
488
+ fillOpacity: .08
489
+ },
490
+ {
491
+ d: "M4.5 7.5L7.5 4.5M4.5 10.5L10.5 4.5M4.5 13.5L13.5 4.5M4.5 16.5L16.5 4.5M4.5 19.5L19.5 4.5M7.5 19.5L19.5 7.5M10.5 19.5L19.5 10.5M13.5 19.5L19.5 13.5M16.5 19.5L19.5 16.5",
492
+ stroke: "primary",
493
+ strokeWidth: .6,
494
+ strokeOpacity: .5
495
+ },
496
+ ...measurementOutline
497
+ ],
358
498
  alertTriangle: ["M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"],
359
499
  alignBottom: [
360
500
  "M3 19v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z",
@@ -404,6 +544,11 @@ const ICON_PATHS = {
404
544
  "M19 16h-12a2 2 0 0 0 -2 2",
405
545
  "M9 8h6"
406
546
  ],
547
+ calibrate: [
548
+ "M12 12m-7.5 0a7.5 7.5 0 1 0 15 0a7.5 7.5 0 1 0 -15 0",
549
+ "M12 2.75V7M12 17V21.25M2.75 12H7M17 12H21.25",
550
+ "M12 12m-2.5 0a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0"
551
+ ],
407
552
  callout: [
408
553
  {
409
554
  d: "M20 6v4a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1",
@@ -826,6 +971,11 @@ const ICON_PATHS = {
826
971
  ],
827
972
  ungroup: ["M4 5a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z", "M13 14a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z"],
828
973
  unlock: ["M7 11V7a5 5 0 0 1 9.9-1"],
974
+ updateScale: [
975
+ "M3.5 13.5L18.5 7.5L21.25 14.5L6.25 20.5L3.5 13.5Z",
976
+ "M8 12.8L9 15.2M11.5 11.4L12.2 13.2M15 10L16 12.4",
977
+ "M4 9.39849C4.10585 8.34804 4.487 7.34411 5.10497 6.48809C5.72294 5.63207 6.55584 4.95427 7.51957 4.52312C8.4833 4.09198 9.54373 3.92277 10.5938 4.03257C11.6438 4.14237 12.6463 4.5273 13.5 5.14849M11.5 6.00003L13.5 5.14849L12.7807 2.96999"
978
+ ],
829
979
  vertical: [
830
980
  "M8 7l4 -4l4 4",
831
981
  "M8 17l4 4l4 -4",
@@ -895,7 +1045,11 @@ function Icon({ name, size = 20, strokeWidth = 2, className, title, accent }) {
895
1045
  return /* @__PURE__ */ jsx("path", {
896
1046
  d: p.d,
897
1047
  fill: fill ?? "none",
898
- stroke
1048
+ stroke,
1049
+ strokeWidth: p.strokeWidth,
1050
+ strokeDasharray: p.strokeDasharray,
1051
+ strokeOpacity: p.strokeOpacity,
1052
+ fillOpacity: p.fillOpacity
899
1053
  }, i);
900
1054
  })
901
1055
  });
@@ -929,6 +1083,495 @@ function ThemeProvider({ children, preference = "system", target }) {
929
1083
  });
930
1084
  }
931
1085
  //#endregion
1086
+ //#region src/ui/measurement.tsx
1087
+ const control = "border-border bg-surface text-fg w-full rounded-md border px-2 py-1.5 text-sm disabled:opacity-50";
1088
+ const button = "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-50";
1089
+ const useCurrentPon = () => useSelector(StageToken$1, (c) => c.pages()[c.currentPage()]?.pon ?? -1);
1090
+ function MeasurementScaleButton() {
1091
+ const t = useT();
1092
+ const scale = usePageScale(useCurrentPon());
1093
+ const surface = useSurface("measurement");
1094
+ return /* @__PURE__ */ jsxs("button", {
1095
+ className: `${button} inline-flex items-center gap-1.5 whitespace-nowrap`,
1096
+ onClick: () => surface.open({ exclusive: "right" }),
1097
+ title: t("measurement.title"),
1098
+ children: [/* @__PURE__ */ jsx(Icon, {
1099
+ name: "updateScale",
1100
+ size: 20,
1101
+ className: "shrink-0"
1102
+ }), /* @__PURE__ */ jsxs("span", { children: [
1103
+ t("measurement.scale"),
1104
+ ":",
1105
+ " ",
1106
+ scale.measure?.subtype === "RL" ? scale.measure.ratio ?? t("measurement.custom") : t("measurement.unavailable")
1107
+ ] })]
1108
+ });
1109
+ }
1110
+ function MeasurementSection() {
1111
+ const t = useT();
1112
+ const pon = useCurrentPon();
1113
+ const measurement = useMeasurement();
1114
+ const scale = usePageScale(pon);
1115
+ const anno = useCapability(AnnotationToken$1);
1116
+ const selected = useSelector(AnnotationToken$1, (c) => c.selection());
1117
+ const resettable = useSelector(AnnotationToken$1, (c) => c.getSelected().filter((annotation) => (annotation.subtype === "polygon" || annotation.subtype === "polyline") && annotation.caption?.center && c.canEdit(annotation.ref) && !annotation.flags.lockedContents), (a, b) => a.length === b.length && a.every((annotation, i) => annotation === b[i]));
1118
+ const readouts = useSelector(MeasurementToken$1, (c) => anno.getSelected().map((d) => c.readout(d.ref)).filter((r) => !("unavailable" in r)), (a, b) => JSON.stringify(a) === JSON.stringify(b));
1119
+ const reports = useSelector(MeasurementToken$1, (c) => c.lastReports());
1120
+ const [allPages, setAllPages] = useState(false);
1121
+ const [recalculate, setRecalculate] = useState(true);
1122
+ const [error, setError] = useState(null);
1123
+ const disabled = measurement.busy || !measurement.canCalibratePage || !scale.ready;
1124
+ const rectilinear = scale.measure?.subtype === "RL" ? scale.measure : null;
1125
+ const options = {
1126
+ allPages,
1127
+ recalculate
1128
+ };
1129
+ const run = async (work) => {
1130
+ setError(null);
1131
+ try {
1132
+ await work();
1133
+ } catch (e) {
1134
+ setError(e instanceof Error ? e.message : String(e));
1135
+ }
1136
+ };
1137
+ const precision = rectilinear?.distance[rectilinear.distance.length - 1]?.precision ?? 100;
1138
+ return /* @__PURE__ */ jsxs("div", {
1139
+ className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 text-sm",
1140
+ children: [
1141
+ /* @__PURE__ */ jsx("p", {
1142
+ className: "text-fg-muted",
1143
+ children: t("measurement.instructions")
1144
+ }),
1145
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("span", {
1146
+ className: "text-fg-muted",
1147
+ children: [t("measurement.scale"), ": "]
1148
+ }), /* @__PURE__ */ jsx("strong", { children: rectilinear?.ratio ?? t("measurement.unavailable") })] }),
1149
+ scale.error && /* @__PURE__ */ jsx("p", {
1150
+ role: "alert",
1151
+ className: "text-red-600",
1152
+ children: scale.error.message
1153
+ }),
1154
+ !scale.persistent && scale.ready && /* @__PURE__ */ jsx("p", {
1155
+ className: "text-fg-muted",
1156
+ children: t("measurement.sessionOnly")
1157
+ }),
1158
+ /* @__PURE__ */ jsxs("label", {
1159
+ className: "flex items-center gap-2",
1160
+ children: [/* @__PURE__ */ jsx("input", {
1161
+ type: "checkbox",
1162
+ checked: allPages,
1163
+ disabled: measurement.busy,
1164
+ onChange: (e) => setAllPages(e.target.checked)
1165
+ }), t("measurement.allPages")]
1166
+ }),
1167
+ /* @__PURE__ */ jsxs("label", {
1168
+ className: "flex items-center gap-2",
1169
+ children: [/* @__PURE__ */ jsx("input", {
1170
+ type: "checkbox",
1171
+ checked: recalculate,
1172
+ disabled: measurement.busy,
1173
+ onChange: (e) => setRecalculate(e.target.checked)
1174
+ }), t("measurement.recalculate")]
1175
+ }),
1176
+ /* @__PURE__ */ jsxs("label", { children: [t("measurement.preset"), /* @__PURE__ */ jsxs("select", {
1177
+ "aria-label": t("measurement.preset"),
1178
+ className: control,
1179
+ value: "",
1180
+ disabled,
1181
+ onChange: (e) => void run(() => measurement.setPreset(pon, e.target.value, options)),
1182
+ children: [/* @__PURE__ */ jsx("option", {
1183
+ value: "",
1184
+ disabled: true,
1185
+ children: t("measurement.choosePreset")
1186
+ }), measurement.presets().map((p) => /* @__PURE__ */ jsx("option", {
1187
+ value: p.id,
1188
+ children: p.label
1189
+ }, p.id))]
1190
+ })] }),
1191
+ /* @__PURE__ */ jsxs("label", { children: [t("measurement.unit"), /* @__PURE__ */ jsxs("select", {
1192
+ "aria-label": t("measurement.unit"),
1193
+ className: control,
1194
+ value: rectilinear?.distance[0]?.unit.trim() ?? "",
1195
+ disabled: disabled || !rectilinear,
1196
+ onChange: (e) => void run(() => measurement.setUnit(pon, e.target.value, void 0, options)),
1197
+ children: [!measurement.units().includes(rectilinear?.distance[0]?.unit.trim()) && /* @__PURE__ */ jsx("option", {
1198
+ value: rectilinear?.distance[0]?.unit.trim() ?? "",
1199
+ children: t("measurement.custom")
1200
+ }), measurement.units().map((u) => /* @__PURE__ */ jsx("option", {
1201
+ value: u,
1202
+ children: u
1203
+ }, u))]
1204
+ })] }),
1205
+ /* @__PURE__ */ jsxs("label", { children: [t("measurement.areaUnit"), /* @__PURE__ */ jsxs("select", {
1206
+ "aria-label": t("measurement.areaUnit"),
1207
+ className: control,
1208
+ value: rectilinear?.area[0]?.unit.trim().replace("²", "2") ?? "",
1209
+ disabled: disabled || !rectilinear,
1210
+ onChange: (e) => void run(() => measurement.setAreaUnit(pon, e.target.value, options)),
1211
+ children: [!measurement.areaUnits().includes(rectilinear?.area[0]?.unit.trim().replace("²", "2")) && /* @__PURE__ */ jsx("option", {
1212
+ value: rectilinear?.area[0]?.unit.trim().replace("²", "2") ?? "",
1213
+ children: t("measurement.custom")
1214
+ }), measurement.areaUnits().map((unit) => /* @__PURE__ */ jsx("option", {
1215
+ value: unit,
1216
+ children: unit.replace("2", "²")
1217
+ }, unit))]
1218
+ })] }),
1219
+ /* @__PURE__ */ jsxs("label", { children: [t("measurement.precision"), /* @__PURE__ */ jsxs("select", {
1220
+ "aria-label": t("measurement.precision"),
1221
+ className: control,
1222
+ value: precision,
1223
+ disabled: disabled || !rectilinear,
1224
+ onChange: (e) => void run(() => measurement.setPrecision(pon, Number(e.target.value), options)),
1225
+ children: [![
1226
+ 1,
1227
+ 10,
1228
+ 100,
1229
+ 1e3,
1230
+ 1e4
1231
+ ].includes(precision) && /* @__PURE__ */ jsx("option", {
1232
+ value: precision,
1233
+ children: t("measurement.custom")
1234
+ }), [
1235
+ 1,
1236
+ 10,
1237
+ 100,
1238
+ 1e3,
1239
+ 1e4
1240
+ ].map((p) => /* @__PURE__ */ jsx("option", {
1241
+ value: p,
1242
+ children: 1 / p
1243
+ }, p))]
1244
+ })] }),
1245
+ /* @__PURE__ */ jsx("button", {
1246
+ className: button,
1247
+ disabled,
1248
+ onClick: () => measurement.startCalibration(),
1249
+ children: t("measurement.calibrate")
1250
+ }),
1251
+ selected.length > 0 && /* @__PURE__ */ jsxs("section", {
1252
+ className: "border-border-subtle border-t pt-3",
1253
+ children: [
1254
+ /* @__PURE__ */ jsx("h3", {
1255
+ className: "mb-1 font-medium",
1256
+ children: t("measurement.selection")
1257
+ }),
1258
+ readouts.length ? readouts.map((readout, i) => /* @__PURE__ */ jsxs("div", {
1259
+ className: "mb-2",
1260
+ children: [/* @__PURE__ */ jsxs("p", { children: [
1261
+ t(`measurement.${readout.kind}`),
1262
+ ": ",
1263
+ readout.label
1264
+ ] }), readout.perimeter && /* @__PURE__ */ jsxs("p", { children: [
1265
+ t("measurement.perimeter"),
1266
+ ": ",
1267
+ readout.perimeter
1268
+ ] })]
1269
+ }, i)) : /* @__PURE__ */ jsx("p", {
1270
+ className: "text-fg-muted",
1271
+ children: t("measurement.unavailable")
1272
+ }),
1273
+ resettable.length > 0 && /* @__PURE__ */ jsx("button", {
1274
+ className: button,
1275
+ disabled: measurement.busy,
1276
+ onClick: () => void run(async () => {
1277
+ for (const annotation of resettable) if (annotation.subtype === "polygon" || annotation.subtype === "polyline") await anno.update(annotation.ref, {
1278
+ subtype: annotation.subtype,
1279
+ caption: { center: null }
1280
+ });
1281
+ }),
1282
+ children: t("measurement.resetLabel")
1283
+ })
1284
+ ]
1285
+ }),
1286
+ reports.length > 0 && /* @__PURE__ */ jsx("p", {
1287
+ role: "status",
1288
+ children: t("measurement.report", { params: {
1289
+ updated: reports.reduce((n, r) => n + r.updated.length, 0),
1290
+ skipped: reports.reduce((n, r) => n + r.skipped.length, 0),
1291
+ failed: reports.reduce((n, r) => n + r.failed.length + (r.scaleError || r.error ? 1 : 0), 0)
1292
+ } })
1293
+ }),
1294
+ error && /* @__PURE__ */ jsx("p", {
1295
+ role: "alert",
1296
+ className: "text-red-600",
1297
+ children: error
1298
+ })
1299
+ ]
1300
+ });
1301
+ }
1302
+ function CalibrationDialog() {
1303
+ const t = useT();
1304
+ const measurement = useMeasurement();
1305
+ const request = useSelector(MeasurementToken$1, (c) => c.calibrationRequest());
1306
+ const input = useRef(null);
1307
+ const [value, setValue] = useState("");
1308
+ const [unit, setUnit] = useState("m");
1309
+ const [allPages, setAllPages] = useState(false);
1310
+ const [recalculate, setRecalculate] = useState(true);
1311
+ const [error, setError] = useState(null);
1312
+ useEffect(() => {
1313
+ setValue("");
1314
+ setError(null);
1315
+ input.current?.focus();
1316
+ }, [request]);
1317
+ if (!request) return null;
1318
+ const submit = async () => {
1319
+ setError(null);
1320
+ try {
1321
+ await measurement.calibrate(request.pon, request.from, request.to, {
1322
+ value: Number(value),
1323
+ unit
1324
+ }, {
1325
+ allPages,
1326
+ recalculate
1327
+ });
1328
+ measurement.dismissCalibration();
1329
+ } catch (e) {
1330
+ setError(e instanceof Error ? e.message : String(e));
1331
+ }
1332
+ };
1333
+ return /* @__PURE__ */ jsx("div", {
1334
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
1335
+ onKeyDown: (e) => {
1336
+ if (e.key === "Escape" && !measurement.busy) measurement.dismissCalibration();
1337
+ },
1338
+ children: /* @__PURE__ */ jsxs("form", {
1339
+ role: "dialog",
1340
+ "aria-modal": "true",
1341
+ "aria-labelledby": "measurement-calibration-title",
1342
+ className: "border-border bg-surface flex w-96 flex-col gap-4 rounded-lg border p-5 shadow-xl",
1343
+ onSubmit: (e) => {
1344
+ e.preventDefault();
1345
+ submit();
1346
+ },
1347
+ children: [
1348
+ /* @__PURE__ */ jsx("h2", {
1349
+ id: "measurement-calibration-title",
1350
+ className: "font-semibold",
1351
+ children: t("measurement.calibrate")
1352
+ }),
1353
+ /* @__PURE__ */ jsx("p", {
1354
+ className: "text-fg-muted text-sm",
1355
+ children: t("measurement.knownLength")
1356
+ }),
1357
+ /* @__PURE__ */ jsxs("label", { children: [t("measurement.length"), /* @__PURE__ */ jsx("input", {
1358
+ ref: input,
1359
+ "aria-label": t("measurement.length"),
1360
+ className: control,
1361
+ type: "number",
1362
+ min: "0",
1363
+ step: "any",
1364
+ value,
1365
+ onChange: (e) => setValue(e.target.value)
1366
+ })] }),
1367
+ /* @__PURE__ */ jsxs("label", { children: [t("measurement.unit"), /* @__PURE__ */ jsx("select", {
1368
+ className: control,
1369
+ "aria-label": t("measurement.unit"),
1370
+ value: unit,
1371
+ onChange: (e) => setUnit(e.target.value),
1372
+ children: measurement.units().map((u) => /* @__PURE__ */ jsx("option", { children: u }, u))
1373
+ })] }),
1374
+ /* @__PURE__ */ jsxs("label", {
1375
+ className: "flex items-center gap-2 text-sm",
1376
+ children: [/* @__PURE__ */ jsx("input", {
1377
+ type: "checkbox",
1378
+ checked: allPages,
1379
+ onChange: (e) => setAllPages(e.target.checked)
1380
+ }), t("measurement.allPages")]
1381
+ }),
1382
+ /* @__PURE__ */ jsxs("label", {
1383
+ className: "flex items-center gap-2 text-sm",
1384
+ children: [/* @__PURE__ */ jsx("input", {
1385
+ type: "checkbox",
1386
+ checked: recalculate,
1387
+ onChange: (e) => setRecalculate(e.target.checked)
1388
+ }), t("measurement.recalculate")]
1389
+ }),
1390
+ error && /* @__PURE__ */ jsx("p", {
1391
+ role: "alert",
1392
+ className: "text-sm text-red-600",
1393
+ children: error
1394
+ }),
1395
+ /* @__PURE__ */ jsxs("div", {
1396
+ className: "flex justify-end gap-2",
1397
+ children: [/* @__PURE__ */ jsx("button", {
1398
+ type: "button",
1399
+ className: button,
1400
+ disabled: measurement.busy,
1401
+ onClick: () => measurement.dismissCalibration(),
1402
+ children: t("demo.cancel")
1403
+ }), /* @__PURE__ */ jsx("button", {
1404
+ className: button,
1405
+ type: "submit",
1406
+ disabled: measurement.busy || !measurement.canCalibratePage || !(Number(value) > 0) || !Number.isFinite(Number(value)),
1407
+ children: measurement.busy ? t("measurement.applying") : t("measurement.apply")
1408
+ })]
1409
+ })
1410
+ ]
1411
+ })
1412
+ });
1413
+ }
1414
+ //#endregion
1415
+ //#region src/config/default-stamps.ts
1416
+ /**
1417
+ * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
1418
+ * standard rubber stamps as one Acrobat-compatible PDF per locale.
1419
+ *
1420
+ * Delivery: the library ships INSIDE whatever ships this viewer. The package
1421
+ * exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
1422
+ * so it travels through the module graph like any other code — every bundler
1423
+ * splits it into a chunk served from the app's own origin, the CDN snippet
1424
+ * carries it as a sibling chunk in its folder, and nothing is fetched from a
1425
+ * third party. `stamps.defaultLibrary` overrides that:
1426
+ * - `false` → no built-in library, no request (air-gapped);
1427
+ * - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
1428
+ *
1429
+ * Loading is LAZY (first open of the stamps panel, never at boot) and
1430
+ * locale-aware: the chrome locale leads, the browser languages break ties
1431
+ * for the locales the chrome does not translate itself, and a locale change
1432
+ * while a default library is loaded swaps it for the new one. The file names
1433
+ * itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
1434
+ */
1435
+ /** The built-in library's PieceInfo id — excluded from persistence (it is
1436
+ * loaded again on every first open, in the locale of that moment). */
1437
+ const DEFAULT_LIBRARY_ID = "embedpdf-standard";
1438
+ /** Which shipped locale to show for the chrome's locale + the browser's. */
1439
+ function resolveStampsLocale(chromeLocale) {
1440
+ return negotiateLocale(LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
1441
+ }
1442
+ const fetchBytes = async (url) => {
1443
+ const response = await fetch(url);
1444
+ if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
1445
+ return new Uint8Array(await response.arrayBuffer());
1446
+ };
1447
+ /** The library bytes for `locale`: your URL when you gave one, otherwise the
1448
+ * copy that shipped with the viewer. Never anything else. */
1449
+ const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : loadDefaultLibrary(locale);
1450
+ /** Per workspace: the locale the default library was last brought to, and
1451
+ * the in-flight work — the panel mounts and unmounts with the sidebar, so
1452
+ * this cannot live in component state. */
1453
+ const loads = /* @__PURE__ */ new WeakMap();
1454
+ /**
1455
+ * Bring the built-in library to `locale`. Idempotent per workspace and
1456
+ * locale; a locale change swaps the loaded default (remove, then import);
1457
+ * a default the user removed in this session stays removed — the locale is
1458
+ * recorded, nothing is re-added. An embedder that seeded a library under
1459
+ * the same id is left alone.
1460
+ */
1461
+ function ensureDefaultLibrary(stamp, locale, source) {
1462
+ if (source === false) return Promise.resolve();
1463
+ const current = loads.get(stamp);
1464
+ if (current?.locale === locale) return current.promise;
1465
+ const run = async () => {
1466
+ await current?.promise.catch(() => void 0);
1467
+ const loaded = stamp.library(DEFAULT_LIBRARY_ID);
1468
+ if (loaded?.locale === locale) return;
1469
+ if (current && !loaded) return;
1470
+ if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
1471
+ await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
1472
+ };
1473
+ const promise = run();
1474
+ loads.set(stamp, {
1475
+ locale,
1476
+ promise
1477
+ });
1478
+ return promise;
1479
+ }
1480
+ //#endregion
1481
+ //#region src/ui/stamp-store.tsx
1482
+ /**
1483
+ * The user's stamp libraries live in the browser (IndexedDB): restored ONCE
1484
+ * per workspace, written on every change, for as long as the viewer lives —
1485
+ * a signature made while the stamps sidebar is closed is persisted exactly
1486
+ * like a stamp imported while it is open. The built-in library is never
1487
+ * stored (it is fetched again next time, in the locale of that moment); the
1488
+ * panels that need it bring it in lazily through `ensureDefaultLibrary`.
1489
+ *
1490
+ * Mounted once in the Shell, workspace-scoped (the stamp plugin is).
1491
+ */
1492
+ const store = typeof indexedDB === "undefined" ? null : indexedDbByteStore("embedpdf-stamps", { storeName: "libraries" });
1493
+ const restored = /* @__PURE__ */ new WeakMap();
1494
+ /** Bring the stored libraries back — once per workspace, whoever asks first. */
1495
+ const restoreStampLibrariesOnce = (stamp) => {
1496
+ let pending = restored.get(stamp);
1497
+ if (!pending) {
1498
+ pending = store ? restoreStampLibraries(stamp, store) : Promise.resolve();
1499
+ restored.set(stamp, pending);
1500
+ }
1501
+ return pending;
1502
+ };
1503
+ function StampLibraryStore() {
1504
+ const stamp = useStamp();
1505
+ useEffect(() => {
1506
+ restoreStampLibrariesOnce(stamp);
1507
+ return store ? persistStampLibraries(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0;
1508
+ }, [stamp]);
1509
+ return null;
1510
+ }
1511
+ //#endregion
1512
+ //#region src/ui/quick-stamps.tsx
1513
+ /**
1514
+ * Quick marks in the toolbar: a row of stamp thumbnails that ARM on click —
1515
+ * the checkmark, the cross, whatever the embedder configured
1516
+ * (`stamps.toolbar`: asset ids, or one library's assets). The hover ghost and
1517
+ * the click placement are the annotation plugin's, exactly as from the
1518
+ * sidebar; only the picker moved. Renders nothing until the assets exist.
1519
+ * Lives in the workspace toolbar, so it reads the ACTIVE document's tool.
1520
+ */
1521
+ function QuickStamps() {
1522
+ const t = useT();
1523
+ const stamp = useStamp();
1524
+ const config = useStampsConfig();
1525
+ const { locale } = useLocale();
1526
+ const assets = useStampAssets();
1527
+ const documentId = useDocumentId();
1528
+ const { armAsset } = useArmStampAsset();
1529
+ const activeToolId = useOptionalSelector(InteractionToken$1, (c) => c.activeToolId(), null);
1530
+ const armedId = useOptionalSelector(StampToken$1, (c) => documentId ? c.armedAsset(documentId)?.id ?? null : null, null);
1531
+ const ids = Array.isArray(config.toolbar) ? config.toolbar : config.toolbar && "library" in config.toolbar ? stamp.library(config.toolbar.library)?.assetIds ?? [] : [];
1532
+ const wantsDefault = ids.some((id) => id.startsWith(`${DEFAULT_LIBRARY_ID}:`));
1533
+ useEffect(() => {
1534
+ if (!wantsDefault) return;
1535
+ restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), config.defaultLibrary)).catch((err) => console.error("[embedpdf] quick stamps failed:", err));
1536
+ }, [
1537
+ stamp,
1538
+ locale,
1539
+ wantsDefault
1540
+ ]);
1541
+ const shown = ids.map((id) => assets.find((a) => a.id === id)).filter((a) => !!a);
1542
+ if (shown.length === 0 || !documentId) return null;
1543
+ return /* @__PURE__ */ jsx("div", {
1544
+ className: "flex items-center gap-1",
1545
+ role: "group",
1546
+ "aria-label": t("demo.quickStampsLabel"),
1547
+ children: shown.map((asset) => /* @__PURE__ */ jsx(QuickStampButton, {
1548
+ asset,
1549
+ active: activeToolId === "stamp" && armedId === asset.id,
1550
+ onArm: () => void armAsset(asset.id).catch((e) => console.error("[embedpdf] arm failed", e))
1551
+ }, asset.id))
1552
+ });
1553
+ }
1554
+ function QuickStampButton({ asset, active, onArm }) {
1555
+ const url = useStampAssetPreviewUrl(asset.id);
1556
+ return /* @__PURE__ */ jsx("button", {
1557
+ type: "button",
1558
+ onClick: onArm,
1559
+ title: asset.label,
1560
+ "aria-label": asset.label,
1561
+ "aria-pressed": active || void 0,
1562
+ className: buttonClass(active, true),
1563
+ children: url ? /* @__PURE__ */ jsx("img", {
1564
+ src: url,
1565
+ alt: "",
1566
+ className: "h-5 w-5 object-contain",
1567
+ draggable: false
1568
+ }) : /* @__PURE__ */ jsx("span", {
1569
+ className: "text-xs",
1570
+ children: asset.label
1571
+ })
1572
+ });
1573
+ }
1574
+ //#endregion
932
1575
  //#region src/ui/menu.tsx
933
1576
  /**
934
1577
  * Menu rendering — a MenuSchema (a tree of command ids) rendered as rows.
@@ -1337,7 +1980,11 @@ function AppToolbar({ bar, className }) {
1337
1980
  variant,
1338
1981
  run
1339
1982
  }),
1340
- renderCustom: { "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ jsx(ZoomControls, {}) : /* @__PURE__ */ jsx(ZoomButton, {}) },
1983
+ renderCustom: {
1984
+ "measurement-scale": () => /* @__PURE__ */ jsx(MeasurementScaleButton, {}),
1985
+ "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ jsx(ZoomControls, {}) : /* @__PURE__ */ jsx(ZoomButton, {}),
1986
+ "quick-stamps": () => /* @__PURE__ */ jsx(QuickStamps, {})
1987
+ },
1341
1988
  renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ jsx(CollapsedModes, { view }) : void 0,
1342
1989
  renderGroupTrigger: (view) => /* @__PURE__ */ jsx(GroupTrigger, { view }),
1343
1990
  renderSeparator: () => /* @__PURE__ */ jsx("span", { className: "bg-border h-6 w-px self-center" }),
@@ -1349,6 +1996,71 @@ function AppToolbar({ bar, className }) {
1349
1996
  });
1350
1997
  }
1351
1998
  //#endregion
1999
+ //#region src/ui/annotation-fonts.tsx
2000
+ /**
2001
+ * The configured annotation fonts (`annotations.fonts`), loaded once per
2002
+ * viewer: fetch the bytes → register them on the viewer's engine under the
2003
+ * key (what a free-text `fontFamily` carries and what the engine embeds) →
2004
+ * mount the same bytes as a `@font-face` named by that key (what the plugin's
2005
+ * `cssFontFamily` emits for a registered font, so the live editor renders the
2006
+ * face the appearance stream will bake). The style panel lists a font only
2007
+ * once BOTH have happened — an offered key always resolves on write.
2008
+ */
2009
+ const LoadedFontsContext = createContext([]);
2010
+ function AnnotationFontsProvider({ children }) {
2011
+ const kernel = useKernel();
2012
+ const { fonts } = useAnnotationsConfig();
2013
+ const [loaded, setLoaded] = useState([]);
2014
+ useEffect(() => {
2015
+ const engineFonts = kernel.engine.fonts;
2016
+ if (!fonts?.length || !engineFonts) return;
2017
+ let cancelled = false;
2018
+ const unmounts = [];
2019
+ (async () => {
2020
+ for (const font of fonts) try {
2021
+ const response = await fetch(font.url);
2022
+ if (!response.ok) throw new Error(`${font.url}: HTTP ${response.status}`);
2023
+ const bytes = new Uint8Array(await response.arrayBuffer());
2024
+ if (cancelled) return;
2025
+ await engineFonts.register({
2026
+ key: font.key,
2027
+ familyName: font.familyName,
2028
+ weight: font.weight,
2029
+ italic: font.italic,
2030
+ data: bytes
2031
+ });
2032
+ const unmount = await mountWebFont(font.key, bytes, {
2033
+ weight: font.weight,
2034
+ style: font.italic ? "italic" : "normal"
2035
+ });
2036
+ if (cancelled) {
2037
+ unmount();
2038
+ return;
2039
+ }
2040
+ unmounts.push(unmount);
2041
+ setLoaded((prev) => prev.some((f) => f.key === font.key) ? prev : [...prev, {
2042
+ key: font.key,
2043
+ label: font.label
2044
+ }]);
2045
+ } catch (error) {
2046
+ console.warn(`[embedpdf] annotation font "${font.key}" was not loaded:`, error);
2047
+ }
2048
+ })();
2049
+ return () => {
2050
+ cancelled = true;
2051
+ for (const unmount of unmounts) unmount();
2052
+ };
2053
+ }, [kernel, fonts]);
2054
+ return /* @__PURE__ */ jsx(LoadedFontsContext.Provider, {
2055
+ value: loaded,
2056
+ children
2057
+ });
2058
+ }
2059
+ /** The configured fonts that are registered AND mounted, in config order. */
2060
+ function useAnnotationFonts() {
2061
+ return useContext(LoadedFontsContext);
2062
+ }
2063
+ //#endregion
1352
2064
  //#region src/ui/link-editor.tsx
1353
2065
  /**
1354
2066
  * The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
@@ -1716,7 +2428,13 @@ function iconGlyph(name, accent) {
1716
2428
  const fill = spec.fill === true ? INK : spec.fill ? accent?.[spec.fill] : void 0;
1717
2429
  const stroke = spec.stroke ? accent?.[spec.stroke] ?? INK : spec.fill ? "none" : INK;
1718
2430
  halo.push(`<path d="${spec.d}" fill="none" stroke="${HALO}" stroke-width="3.5"/>`);
1719
- draw.push(`<path d="${spec.d}" fill="${fill ?? "none"}" stroke="${stroke}"/>`);
2431
+ const decoration = [
2432
+ ["stroke-width", spec.strokeWidth],
2433
+ ["stroke-dasharray", spec.strokeDasharray],
2434
+ ["stroke-opacity", spec.strokeOpacity],
2435
+ ["fill-opacity", spec.fillOpacity]
2436
+ ].filter(([, value]) => value !== void 0).map(([key, value]) => `${key}="${value}"`).join(" ");
2437
+ draw.push(`<path d="${spec.d}" fill="${fill ?? "none"}" stroke="${stroke}" ${decoration}/>`);
1720
2438
  }
1721
2439
  return halo.join("") + draw.join("");
1722
2440
  }
@@ -2334,12 +3052,17 @@ function LineEndingSelect({ side, value, onChange }) {
2334
3052
  });
2335
3053
  }
2336
3054
  function FontFamilySelect({ value, onChange }) {
3055
+ const configured = useAnnotationFonts();
3056
+ const options = [...FONT_OPTIONS, ...configured.map((f) => ({
3057
+ v: f.key,
3058
+ label: f.label
3059
+ }))];
2337
3060
  return /* @__PURE__ */ jsx(DropdownShell, {
2338
3061
  trigger: /* @__PURE__ */ jsx("span", {
2339
3062
  className: "text-fg text-sm",
2340
- children: FONT_OPTIONS.find((o) => o.v === value)?.label ?? value
3063
+ children: options.find((o) => o.v === value)?.label ?? value
2341
3064
  }),
2342
- children: (close) => FONT_OPTIONS.map((o) => /* @__PURE__ */ jsx(OptionRow, {
3065
+ children: (close) => options.map((o) => /* @__PURE__ */ jsx(OptionRow, {
2343
3066
  selected: o.v === value,
2344
3067
  onClick: () => {
2345
3068
  onChange(o.v);
@@ -2351,17 +3074,33 @@ function FontFamilySelect({ value, onChange }) {
2351
3074
  }
2352
3075
  function FontSizeCombo({ value, onChange }) {
2353
3076
  const [open, setOpen] = useState(false);
3077
+ const rootRef = useOutsideClose(open, () => setOpen(false));
3078
+ const [draft, setDraft] = useState(null);
3079
+ const commit = () => {
3080
+ if (draft === null) return;
3081
+ const n = parseInt(draft, 10);
3082
+ setDraft(null);
3083
+ if (Number.isFinite(n) && n > 0 && n !== value) onChange(n);
3084
+ };
2354
3085
  return /* @__PURE__ */ jsxs("div", {
2355
- ref: useOutsideClose(open, () => setOpen(false)),
3086
+ ref: rootRef,
2356
3087
  className: "relative w-full",
2357
3088
  children: [
2358
3089
  /* @__PURE__ */ jsx("input", {
2359
3090
  type: "number",
2360
3091
  min: 1,
2361
- value,
2362
- onChange: (e) => {
2363
- const n = parseInt(e.target.value, 10);
2364
- if (Number.isFinite(n) && n > 0) onChange(n);
3092
+ value: draft ?? value,
3093
+ onChange: (e) => setDraft(e.target.value),
3094
+ onBlur: commit,
3095
+ onKeyDown: (e) => {
3096
+ if (e.key === "Enter") {
3097
+ e.preventDefault();
3098
+ commit();
3099
+ setOpen(false);
3100
+ } else if (e.key === "Escape") {
3101
+ setDraft(null);
3102
+ setOpen(false);
3103
+ }
2365
3104
  },
2366
3105
  onClick: () => setOpen(true),
2367
3106
  className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 pr-7 text-sm"
@@ -2382,6 +3121,7 @@ function FontSizeCombo({ value, onChange }) {
2382
3121
  children: FONT_SIZES.map((sz) => /* @__PURE__ */ jsx(OptionRow, {
2383
3122
  selected: sz === value,
2384
3123
  onClick: () => {
3124
+ setDraft(null);
2385
3125
  onChange(sz);
2386
3126
  setOpen(false);
2387
3127
  },
@@ -2402,15 +3142,45 @@ function BlendModeSelect({ value, onChange }) {
2402
3142
  }, mode.v))
2403
3143
  });
2404
3144
  }
2405
- function Toggle({ active, title, onClick, children }) {
3145
+ function Toggle({ active, title, onClick, keepFocus, children }) {
2406
3146
  return /* @__PURE__ */ jsx("button", {
2407
3147
  type: "button",
2408
3148
  title,
2409
3149
  onClick,
3150
+ onMouseDown: keepFocus ? (e) => e.preventDefault() : void 0,
2410
3151
  className: `flex h-9 w-9 items-center justify-center rounded border transition-colors ${active ? "border-accent bg-accent text-on-accent" : "border-border bg-surface text-fg hover:bg-hover"}`,
2411
3152
  children
2412
3153
  });
2413
3154
  }
3155
+ const isFormatSpec = (spec) => spec.key === "bold" || spec.key === "italic" || spec.key === "underline";
3156
+ /**
3157
+ * The format toggles a free-text kind declares, in one row. While the text
3158
+ * editor holds a range they read and write that range's runs (the plugin
3159
+ * routes `updateSelection`); otherwise the annotation's body. The buttons
3160
+ * keep focus in the editor so the range survives the click.
3161
+ */
3162
+ function FormatToggles({ specs, values, mixed, onChange }) {
3163
+ return /* @__PURE__ */ jsx(Field, {
3164
+ label: useT()("demo.formatLabel", { fallback: "Format" }),
3165
+ mixed: specs.some((s) => mixed.includes(s.key)),
3166
+ children: /* @__PURE__ */ jsx("div", {
3167
+ className: "flex gap-2",
3168
+ children: specs.map((spec) => {
3169
+ const active = values[spec.key] === true && !mixed.includes(spec.key);
3170
+ return /* @__PURE__ */ jsx(Toggle, {
3171
+ title: spec.label,
3172
+ active,
3173
+ keepFocus: true,
3174
+ onClick: () => onChange({ [spec.key]: !active }),
3175
+ children: /* @__PURE__ */ jsx(Icon, {
3176
+ name: spec.key,
3177
+ size: 18
3178
+ })
3179
+ }, spec.key);
3180
+ })
3181
+ })
3182
+ });
3183
+ }
2414
3184
  function PropControl({ spec, value, mixed, onChange }) {
2415
3185
  switch (spec.key) {
2416
3186
  case "color":
@@ -2545,6 +3315,9 @@ function PropControl({ spec, value, mixed, onChange }) {
2545
3315
  })
2546
3316
  });
2547
3317
  }
3318
+ case "bold":
3319
+ case "italic":
3320
+ case "underline": return null;
2548
3321
  case "blendMode": return /* @__PURE__ */ jsx(Field, {
2549
3322
  label: spec.label,
2550
3323
  mixed,
@@ -2592,7 +3365,12 @@ function AnnotationStylePanel() {
2592
3365
  className: "text-fg-muted mb-4 text-[11px] font-semibold uppercase tracking-wide",
2593
3366
  children: hasSel ? `${selected.length} selected` : `${activeToolId} defaults`
2594
3367
  }),
2595
- specs.map((spec) => /* @__PURE__ */ jsx(PropControl, {
3368
+ specs.map((spec) => spec.key === "bold" ? /* @__PURE__ */ jsx(FormatToggles, {
3369
+ specs: specs.filter((s) => isFormatSpec(s)),
3370
+ values,
3371
+ mixed: hasSel ? sel.mixed : [],
3372
+ onChange: write
3373
+ }, "format") : /* @__PURE__ */ jsx(PropControl, {
2596
3374
  spec,
2597
3375
  value: values[spec.key],
2598
3376
  mixed: hasSel && sel.mixed.includes(spec.key),
@@ -3463,77 +4241,11 @@ function SearchPanel() {
3463
4241
  });
3464
4242
  }
3465
4243
  //#endregion
3466
- //#region src/config/default-stamps.ts
4244
+ //#region src/ui/stamps-panel.tsx
3467
4245
  /**
3468
- * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
3469
- * standard rubber stamps as one Acrobat-compatible PDF per locale.
3470
- *
3471
- * Delivery: the library ships INSIDE whatever ships this viewer. The package
3472
- * exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
3473
- * so it travels through the module graph like any other code — every bundler
3474
- * splits it into a chunk served from the app's own origin, the CDN snippet
3475
- * carries it as a sibling chunk in its folder, and nothing is fetched from a
3476
- * third party. `stamps.defaultLibrary` overrides that:
3477
- * - `false` → no built-in library, no request (air-gapped);
3478
- * - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
3479
- *
3480
- * Loading is LAZY (first open of the stamps panel, never at boot) and
3481
- * locale-aware: the chrome locale leads, the browser languages break ties
3482
- * for the locales the chrome does not translate itself, and a locale change
3483
- * while a default library is loaded swaps it for the new one. The file names
3484
- * itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
3485
- */
3486
- /** The built-in library's PieceInfo id — excluded from persistence (it is
3487
- * loaded again on every first open, in the locale of that moment). */
3488
- const DEFAULT_LIBRARY_ID = "embedpdf-standard";
3489
- /** Which shipped locale to show for the chrome's locale + the browser's. */
3490
- function resolveStampsLocale(chromeLocale) {
3491
- return negotiateLocale(LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
3492
- }
3493
- const fetchBytes = async (url) => {
3494
- const response = await fetch(url);
3495
- if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
3496
- return new Uint8Array(await response.arrayBuffer());
3497
- };
3498
- /** The library bytes for `locale`: your URL when you gave one, otherwise the
3499
- * copy that shipped with the viewer. Never anything else. */
3500
- const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : loadDefaultLibrary(locale);
3501
- /** Per workspace: the locale the default library was last brought to, and
3502
- * the in-flight work — the panel mounts and unmounts with the sidebar, so
3503
- * this cannot live in component state. */
3504
- const loads = /* @__PURE__ */ new WeakMap();
3505
- /**
3506
- * Bring the built-in library to `locale`. Idempotent per workspace and
3507
- * locale; a locale change swaps the loaded default (remove, then import);
3508
- * a default the user removed in this session stays removed — the locale is
3509
- * recorded, nothing is re-added. An embedder that seeded a library under
3510
- * the same id is left alone.
3511
- */
3512
- function ensureDefaultLibrary(stamp, locale, source) {
3513
- if (source === false) return Promise.resolve();
3514
- const current = loads.get(stamp);
3515
- if (current?.locale === locale) return current.promise;
3516
- const run = async () => {
3517
- await current?.promise.catch(() => void 0);
3518
- const loaded = stamp.library(DEFAULT_LIBRARY_ID);
3519
- if (loaded?.locale === locale) return;
3520
- if (current && !loaded) return;
3521
- if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
3522
- await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
3523
- };
3524
- const promise = run();
3525
- loads.set(stamp, {
3526
- locale,
3527
- promise
3528
- });
3529
- return promise;
3530
- }
3531
- //#endregion
3532
- //#region src/ui/stamps-panel.tsx
3533
- /**
3534
- * The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
3535
- * The stamp plugin owns the libraries, the asset bytes and the cached
3536
- * previews; this panel is the PICKER over them:
4246
+ * The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
4247
+ * The stamp plugin owns the libraries, the asset bytes and the cached
4248
+ * previews; this panel is the PICKER over them:
3537
4249
  *
3538
4250
  * a library <select> ("All stamps" + one per library, shown once there
3539
4251
  * are two) → a two-column grid of thumbnails → click a stamp → `armAsset`
@@ -3551,30 +4263,15 @@ function ensureDefaultLibrary(stamp, locale, source) {
3551
4263
  * through the surface's open props.
3552
4264
  */
3553
4265
  const ALL = "all";
3554
- /** The user's libraries live in the browser (IndexedDB): restored on first
3555
- * open, written on every change. The built-in set is never stored — it is
3556
- * fetched again next time, in the locale of that moment. */
3557
- const store = typeof indexedDB === "undefined" ? null : indexedDbByteStore("embedpdf-stamps", { storeName: "libraries" });
3558
- /** Restore runs ONCE per workspace — the panel mounts and unmounts with the
3559
- * sidebar, so the guard cannot live in component state. */
3560
- const restored = /* @__PURE__ */ new WeakMap();
3561
- const restoreOnce = (stamp) => {
3562
- let pending = restored.get(stamp);
3563
- if (!pending) {
3564
- pending = store ? restoreStampLibraries(stamp, store) : Promise.resolve();
3565
- restored.set(stamp, pending);
3566
- }
3567
- return pending;
3568
- };
3569
4266
  function StampsPanel() {
3570
4267
  const t = useT();
3571
4268
  const stamp = useStamp();
3572
- const libraries = useStampLibraries();
4269
+ const { defaultLibrary, sidebar } = useStampsConfig();
4270
+ const libraries = useStampLibraries({ kind: sidebar ?? ["stamps"] });
3573
4271
  const assets = useStampAssets();
3574
4272
  const { armAsset } = useArmStampAsset();
3575
4273
  const { activeToolId } = useTool();
3576
4274
  const { locale } = useLocale();
3577
- const { defaultLibrary } = useStampsConfig();
3578
4275
  const surface = useSurface("stamps");
3579
4276
  const fileRef = useRef(null);
3580
4277
  const [armedId, setArmedId] = useState(null);
@@ -3588,7 +4285,7 @@ function StampsPanel() {
3588
4285
  useEffect(() => {
3589
4286
  let live = true;
3590
4287
  setBusy("loading");
3591
- restoreOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
4288
+ restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
3592
4289
  console.error("[embedpdf] default stamps failed:", err);
3593
4290
  if (live) setError(t("demo.stampsError"));
3594
4291
  }).finally(() => live && setBusy(null));
@@ -3596,7 +4293,6 @@ function StampsPanel() {
3596
4293
  live = false;
3597
4294
  };
3598
4295
  }, [stamp, locale]);
3599
- useEffect(() => store ? persistStampLibraries(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0, [stamp]);
3600
4296
  useEffect(() => {
3601
4297
  if (activeToolId !== "stamp") setArmedId(null);
3602
4298
  }, [activeToolId]);
@@ -3767,6 +4463,293 @@ function StampAssetCell({ asset, armed, onArm }) {
3767
4463
  });
3768
4464
  }
3769
4465
  //#endregion
4466
+ //#region src/ui/signatures-panel.tsx
4467
+ /**
4468
+ * The signatures sidebar (right panel) — Preview's signature sheet on v3
4469
+ * parts. One row per PERSON: a stamp library of kind `signatures` holding a
4470
+ * full signature and, optionally, initials. Nothing is stored beyond the
4471
+ * file; rename/export/delete are the library verbs.
4472
+ *
4473
+ * pick a mark → with a TARGET field ("sign here" was clicked, or a
4474
+ * signature widget selected) the mark goes into that field by the
4475
+ * plugin's mode; otherwise it ARMS — hover a page for the ghost, click a
4476
+ * signature field to sign it, click anywhere else to drop it as a stamp.
4477
+ *
4478
+ * Below the people: the signature fields of THIS document, signed or not,
4479
+ * with their verdicts — click an unsigned one to target it, a signed one to
4480
+ * inspect it.
4481
+ */
4482
+ const fieldLabel = (field) => field.kind === "fqn" ? field.name : `#${field.fieldObjectNumber}`;
4483
+ function SignaturesPanel() {
4484
+ const t = useT();
4485
+ const stamp = useStamp();
4486
+ const signature = useSignature();
4487
+ const config = useSignaturesConfig();
4488
+ const rows = useSignerRows();
4489
+ const { target, busy } = useSignatureTarget();
4490
+ const { armAsset } = useArmStampAsset();
4491
+ const { activeToolId } = useTool();
4492
+ const maker = useSurface("signature-maker");
4493
+ const [armedId, setArmedId] = useState(null);
4494
+ const [error, setError] = useState(null);
4495
+ const [invalidating, setInvalidating] = useState(null);
4496
+ useSignatureEvent((event) => {
4497
+ if (event.type === "invalidating") setInvalidating(signature.signatureOf(event.field)?.fieldName ?? fieldLabel(event.field));
4498
+ else if (event.type === "validated" && !event.verdicts.some((v) => v.summary === "invalid" && v.modifications.basis === "working-copy")) setInvalidating(null);
4499
+ });
4500
+ useEffect(() => {
4501
+ restoreStampLibrariesOnce(stamp);
4502
+ }, [stamp]);
4503
+ useEffect(() => {
4504
+ if (activeToolId !== "stamp") setArmedId(null);
4505
+ }, [activeToolId]);
4506
+ const pick = (asset) => {
4507
+ setError(null);
4508
+ if (target) {
4509
+ signature.placeMark({ assetId: asset.id }, { field: target }).catch((err) => {
4510
+ console.error("[embedpdf] placing the mark failed:", err);
4511
+ setError(t("demo.signError"));
4512
+ });
4513
+ return;
4514
+ }
4515
+ setArmedId(asset.id);
4516
+ armAsset(asset.id).catch((err) => {
4517
+ console.error("[embedpdf] arm mark failed:", err);
4518
+ setArmedId(null);
4519
+ setError(t("demo.stampsArmError"));
4520
+ });
4521
+ };
4522
+ const canAddPerson = (config.libraries ?? "many") === "many" || rows.length === 0;
4523
+ const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
4524
+ return /* @__PURE__ */ jsxs("div", {
4525
+ className: "flex min-h-0 flex-1 flex-col",
4526
+ children: [
4527
+ /* @__PURE__ */ jsxs("div", {
4528
+ className: "min-h-0 flex-1 overflow-y-auto p-3",
4529
+ children: [rows.length === 0 ? /* @__PURE__ */ jsxs("div", {
4530
+ className: "flex flex-col items-center gap-3 px-4 py-8 text-center",
4531
+ children: [/* @__PURE__ */ jsx(Icon, {
4532
+ name: "signature",
4533
+ size: 32,
4534
+ className: "text-fg-muted"
4535
+ }), /* @__PURE__ */ jsx("p", {
4536
+ className: "text-fg-muted text-sm",
4537
+ children: t("demo.signaturesEmpty")
4538
+ })]
4539
+ }) : /* @__PURE__ */ jsx("ul", {
4540
+ className: "flex flex-col gap-3",
4541
+ children: rows.map((row) => /* @__PURE__ */ jsx(SignerRowView, {
4542
+ row,
4543
+ armedId,
4544
+ wantsInitials,
4545
+ onPick: pick,
4546
+ onAddInitials: () => maker.open({
4547
+ exclusive: "modal",
4548
+ props: { libraryId: row.libraryId }
4549
+ })
4550
+ }, row.libraryId))
4551
+ }), /* @__PURE__ */ jsx(DocumentSignatures, {})]
4552
+ }),
4553
+ invalidating && /* @__PURE__ */ jsx("div", {
4554
+ className: "border-border-subtle shrink-0 border-t px-3 py-2 text-xs text-amber-700",
4555
+ children: t("demo.signaturesInvalidating", { params: { field: invalidating } })
4556
+ }),
4557
+ (error || target || armedId) && /* @__PURE__ */ jsx("div", {
4558
+ className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
4559
+ children: error ?? (target ? busy ? t("demo.signSigning") : t("demo.signaturesTargetHint") : t("demo.signaturesArmedHint"))
4560
+ }),
4561
+ canAddPerson ? /* @__PURE__ */ jsx("div", {
4562
+ className: "border-border-subtle shrink-0 border-t p-3",
4563
+ children: /* @__PURE__ */ jsxs("button", {
4564
+ type: "button",
4565
+ onClick: () => maker.open({ exclusive: "modal" }),
4566
+ 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",
4567
+ children: [/* @__PURE__ */ jsx(Icon, {
4568
+ name: "plus",
4569
+ size: 16
4570
+ }), t("demo.signaturesNew")]
4571
+ })
4572
+ }) : null
4573
+ ]
4574
+ });
4575
+ }
4576
+ function SignerRowView({ row, armedId, wantsInitials, onPick, onAddInitials }) {
4577
+ const t = useT();
4578
+ const stamp = useStamp();
4579
+ const [renaming, setRenaming] = useState(null);
4580
+ const exportPdf = () => {
4581
+ const bytes = stamp.exportLibrary(row.libraryId);
4582
+ if (!bytes) return;
4583
+ const blob = new Blob([bytes], { type: "application/pdf" });
4584
+ const url = URL.createObjectURL(blob);
4585
+ const a = document.createElement("a");
4586
+ a.href = url;
4587
+ a.download = `${row.name || "signature"}.pdf`;
4588
+ a.click();
4589
+ URL.revokeObjectURL(url);
4590
+ };
4591
+ const commitRename = () => {
4592
+ const name = renaming?.trim();
4593
+ setRenaming(null);
4594
+ if (name && name !== row.name) stamp.updateLibrary(row.libraryId, { name });
4595
+ };
4596
+ const iconButton = "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded";
4597
+ return /* @__PURE__ */ jsxs("li", {
4598
+ className: "border-border-subtle rounded-md border p-2",
4599
+ children: [/* @__PURE__ */ jsxs("div", {
4600
+ className: "flex items-center gap-1",
4601
+ children: [
4602
+ renaming !== null ? /* @__PURE__ */ jsx("input", {
4603
+ autoFocus: true,
4604
+ value: renaming,
4605
+ onChange: (e) => setRenaming(e.target.value),
4606
+ onBlur: commitRename,
4607
+ onKeyDown: (e) => {
4608
+ if (e.key === "Enter") commitRename();
4609
+ if (e.key === "Escape") setRenaming(null);
4610
+ },
4611
+ className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-1.5 py-0.5 text-sm"
4612
+ }) : /* @__PURE__ */ jsx("span", {
4613
+ className: "text-fg min-w-0 flex-1 truncate text-sm font-medium",
4614
+ children: row.name
4615
+ }),
4616
+ /* @__PURE__ */ jsx("button", {
4617
+ type: "button",
4618
+ onClick: () => setRenaming(row.name),
4619
+ title: t("demo.signaturesRename"),
4620
+ className: iconButton,
4621
+ children: /* @__PURE__ */ jsx(Icon, {
4622
+ name: "pencilMarker",
4623
+ size: 13
4624
+ })
4625
+ }),
4626
+ /* @__PURE__ */ jsx("button", {
4627
+ type: "button",
4628
+ onClick: exportPdf,
4629
+ title: t("demo.signaturesExport"),
4630
+ className: iconButton,
4631
+ children: /* @__PURE__ */ jsx(Icon, {
4632
+ name: "download",
4633
+ size: 13
4634
+ })
4635
+ }),
4636
+ /* @__PURE__ */ jsx("button", {
4637
+ type: "button",
4638
+ onClick: () => void stamp.removeLibrary(row.libraryId),
4639
+ title: t("demo.signaturesRemove"),
4640
+ className: iconButton,
4641
+ children: /* @__PURE__ */ jsx(Icon, {
4642
+ name: "trash",
4643
+ size: 13
4644
+ })
4645
+ })
4646
+ ]
4647
+ }), /* @__PURE__ */ jsxs("div", {
4648
+ className: "mt-2 grid grid-cols-[2fr_1fr] gap-2",
4649
+ children: [row.signatures.map((asset) => /* @__PURE__ */ jsx(MarkThumb, {
4650
+ asset,
4651
+ label: t("demo.signatureLabel"),
4652
+ armed: armedId === asset.id,
4653
+ onPick: () => onPick(asset)
4654
+ }, asset.id)), wantsInitials ? row.initials ? /* @__PURE__ */ jsx(MarkThumb, {
4655
+ asset: row.initials,
4656
+ label: t("demo.initialsLabel"),
4657
+ armed: armedId === row.initials.id,
4658
+ onPick: () => onPick(row.initials)
4659
+ }) : /* @__PURE__ */ jsx("button", {
4660
+ type: "button",
4661
+ onClick: onAddInitials,
4662
+ title: t("demo.signaturesAddInitials"),
4663
+ className: "border-border-subtle text-fg-muted hover:bg-hover flex h-16 items-center justify-center rounded-md border border-dashed text-xs",
4664
+ children: /* @__PURE__ */ jsx(Icon, {
4665
+ name: "plus",
4666
+ size: 14
4667
+ })
4668
+ }) : null]
4669
+ })]
4670
+ });
4671
+ }
4672
+ function MarkThumb({ asset, label, armed, onPick }) {
4673
+ const url = useStampAssetPreviewUrl(asset.id);
4674
+ return /* @__PURE__ */ jsx("button", {
4675
+ type: "button",
4676
+ onClick: onPick,
4677
+ title: label,
4678
+ "aria-pressed": armed,
4679
+ 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"}`,
4680
+ children: url ? /* @__PURE__ */ jsx("img", {
4681
+ src: url,
4682
+ alt: label,
4683
+ className: "max-h-full max-w-full object-contain"
4684
+ }) : /* @__PURE__ */ jsx("span", {
4685
+ className: "text-fg-muted text-xs",
4686
+ children: label
4687
+ })
4688
+ });
4689
+ }
4690
+ /** The signature fields of this document, with their state. */
4691
+ function DocumentSignatures() {
4692
+ const t = useT();
4693
+ const signature = useSignature();
4694
+ const snapshot = useSignatureSnapshot();
4695
+ const verdicts = useSignatureVerdicts();
4696
+ const { target } = useSignatureTarget();
4697
+ const [validating, setValidating] = useState(false);
4698
+ const fields = snapshot?.signatures ?? [];
4699
+ const verdictOf = (dto) => verdicts?.find((v) => v.signature.index === dto.index) ?? null;
4700
+ 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");
4701
+ 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";
4702
+ const isTarget = (dto) => !!target && (target.kind === "fqn" ? target.name === dto.fieldName : target.fieldObjectNumber === (dto.field.kind === "objectNumber" ? dto.field.fieldObjectNumber : -1));
4703
+ const validate = () => {
4704
+ setValidating(true);
4705
+ signature.validate().finally(() => setValidating(false));
4706
+ };
4707
+ return /* @__PURE__ */ jsxs("div", {
4708
+ className: "mt-4",
4709
+ children: [/* @__PURE__ */ jsxs("div", {
4710
+ className: "flex items-center justify-between",
4711
+ children: [/* @__PURE__ */ jsx("span", {
4712
+ className: "text-fg-muted text-xs font-semibold uppercase tracking-wide",
4713
+ children: t("demo.signaturesInDocument")
4714
+ }), fields.some((f) => f.signed) ? /* @__PURE__ */ jsx("button", {
4715
+ type: "button",
4716
+ disabled: validating,
4717
+ onClick: validate,
4718
+ className: "text-accent text-xs hover:underline disabled:opacity-60",
4719
+ children: validating ? t("demo.signaturesValidating") : t("demo.signaturesValidate")
4720
+ }) : null]
4721
+ }), fields.length === 0 ? /* @__PURE__ */ jsx("p", {
4722
+ className: "text-fg-muted mt-1 text-xs",
4723
+ children: t("demo.signaturesNone")
4724
+ }) : /* @__PURE__ */ jsx("ul", {
4725
+ className: "mt-1 flex flex-col gap-1",
4726
+ children: fields.map((dto) => {
4727
+ const verdict = verdictOf(dto);
4728
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
4729
+ type: "button",
4730
+ onClick: () => dto.signed ? signature.inspect(dto.field) : signature.setTarget(dto.field),
4731
+ 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" : ""}`,
4732
+ children: [
4733
+ /* @__PURE__ */ jsx(Icon, {
4734
+ name: dto.signed ? "check" : "signature",
4735
+ size: 14,
4736
+ className: dto.signed ? "text-green-600" : "text-fg-muted"
4737
+ }),
4738
+ /* @__PURE__ */ jsx("span", {
4739
+ className: "text-fg min-w-0 flex-1 truncate",
4740
+ children: dto.fieldName
4741
+ }),
4742
+ /* @__PURE__ */ jsx("span", {
4743
+ className: `shrink-0 text-right text-xs ${dto.signed ? verdictTone(verdict) : "text-fg-muted"}`,
4744
+ children: dto.signed ? verdictLabel(verdict) : t("demo.signaturesUnsigned")
4745
+ })
4746
+ ]
4747
+ }) }, dto.index);
4748
+ })
4749
+ })]
4750
+ });
4751
+ }
4752
+ //#endregion
3770
4753
  //#region src/ui/panels.tsx
3771
4754
  /**
3772
4755
  * The static chrome around the toolbars: the left/right sidebars (shell
@@ -3860,11 +4843,13 @@ function RightSidebar() {
3860
4843
  const comment = useSurface("comment");
3861
4844
  const style = useSurface("annotation-style");
3862
4845
  const redaction = useSurface("redaction");
4846
+ const measurement = useSurface("measurement");
3863
4847
  const stamps = useSurface("stamps");
3864
- const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : null;
4848
+ const signatures = useSurface("signatures");
4849
+ const active = measurement.isOpen ? "measurement" : search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : signatures.isOpen ? "signatures" : null;
3865
4850
  if (!active) return null;
3866
- const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : "demo.styleTitle";
3867
- const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : style.close;
4851
+ const titleKey = active === "measurement" ? "measurement.title" : active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : active === "signatures" ? "demo.signaturesTitle" : "demo.styleTitle";
4852
+ const close = active === "measurement" ? measurement.close : active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : active === "signatures" ? signatures.close : style.close;
3868
4853
  return /* @__PURE__ */ jsxs("aside", {
3869
4854
  className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
3870
4855
  children: [/* @__PURE__ */ jsxs("div", {
@@ -3881,7 +4866,7 @@ function RightSidebar() {
3881
4866
  size: 16
3882
4867
  })
3883
4868
  })]
3884
- }), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ jsx(StampsPanel, {}) : /* @__PURE__ */ jsx(CommentsPanel, {}, documentId ?? "none")]
4869
+ }), active === "measurement" ? /* @__PURE__ */ jsx(MeasurementSection, {}, documentId ?? "none") : active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ jsx(StampsPanel, {}) : active === "signatures" ? /* @__PURE__ */ jsx(SignaturesPanel, {}) : /* @__PURE__ */ jsx(CommentsPanel, {}, documentId ?? "none")]
3885
4870
  });
3886
4871
  }
3887
4872
  function PageControls() {
@@ -3972,6 +4957,684 @@ function RedactConfirmModal() {
3972
4957
  });
3973
4958
  }
3974
4959
  //#endregion
4960
+ //#region src/ui/sign-dialog.tsx
4961
+ /**
4962
+ * The sign dialog (`signature-sign` modal surface): a mark met a field and
4963
+ * the mode is `ask` — or a "sign here" flow wants the facts first. Shows
4964
+ * the mark as it will sit in the field, takes reason and location, offers
4965
+ * a certification when the config allows it, and either SEALS the field
4966
+ * (`signField`) or only draws the mark into it (`fillField`).
4967
+ */
4968
+ function SignDialog() {
4969
+ const t = useT();
4970
+ const surface = useSurface("signature-sign");
4971
+ const signature = useSignature();
4972
+ const [reason, setReason] = useState("");
4973
+ const [location, setLocation] = useState("");
4974
+ const [signKind, setSignKind] = useState("approval");
4975
+ const [busy, setBusy] = useState(null);
4976
+ const [error, setError] = useState(null);
4977
+ const field = surface.props?.field;
4978
+ const mark = surface.props?.mark;
4979
+ const preview = useStampAssetPreviewUrl(mark && "assetId" in mark ? mark.assetId : null);
4980
+ if (!surface.isOpen || !field || !mark) return null;
4981
+ const attribution = {
4982
+ ...reason.trim() ? { reason: reason.trim() } : {},
4983
+ ...location.trim() ? { location: location.trim() } : {}
4984
+ };
4985
+ const run = async (kind) => {
4986
+ setBusy(kind);
4987
+ setError(null);
4988
+ try {
4989
+ if (kind === "sign") await signature.signField({
4990
+ field,
4991
+ mark,
4992
+ attribution,
4993
+ ...signKind !== "approval" && signature.canCertify() ? { certify: { permission: Number(signKind.slice(-1)) } } : {}
4994
+ });
4995
+ else await signature.fillField(field, mark);
4996
+ surface.close();
4997
+ } catch (err) {
4998
+ setError(err instanceof Error ? err.message : String(err));
4999
+ } finally {
5000
+ setBusy(null);
5001
+ }
5002
+ };
5003
+ const input = "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm";
5004
+ return /* @__PURE__ */ jsx("div", {
5005
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
5006
+ children: /* @__PURE__ */ jsxs("div", {
5007
+ className: "border-border bg-surface w-96 rounded-lg border p-4 shadow-xl",
5008
+ children: [
5009
+ /* @__PURE__ */ jsx("h2", {
5010
+ className: "text-fg text-base font-semibold",
5011
+ children: t("demo.signTitle")
5012
+ }),
5013
+ /* @__PURE__ */ jsx("div", {
5014
+ className: "border-border-subtle mt-3 flex h-24 items-center justify-center rounded-md border p-2",
5015
+ children: preview ? /* @__PURE__ */ jsx("img", {
5016
+ src: preview,
5017
+ alt: "",
5018
+ className: "max-h-full max-w-full object-contain"
5019
+ }) : /* @__PURE__ */ jsx("span", {
5020
+ className: "text-fg-muted text-sm",
5021
+ children: t("demo.signatureLabel")
5022
+ })
5023
+ }),
5024
+ /* @__PURE__ */ jsxs("label", {
5025
+ className: "text-fg-muted mt-3 block text-xs",
5026
+ children: [t("demo.signReason"), /* @__PURE__ */ jsx("input", {
5027
+ value: reason,
5028
+ onChange: (e) => setReason(e.target.value),
5029
+ className: input
5030
+ })]
5031
+ }),
5032
+ /* @__PURE__ */ jsxs("label", {
5033
+ className: "text-fg-muted mt-2 block text-xs",
5034
+ children: [t("demo.signLocation"), /* @__PURE__ */ jsx("input", {
5035
+ value: location,
5036
+ onChange: (e) => setLocation(e.target.value),
5037
+ className: input
5038
+ })]
5039
+ }),
5040
+ signature.canCertify() ? /* @__PURE__ */ jsxs("label", {
5041
+ className: "text-fg-muted mt-3 block text-xs",
5042
+ children: [t("demo.signKind"), /* @__PURE__ */ jsxs("select", {
5043
+ value: signKind,
5044
+ onChange: (e) => setSignKind(e.target.value),
5045
+ className: input,
5046
+ children: [
5047
+ /* @__PURE__ */ jsx("option", {
5048
+ value: "approval",
5049
+ children: t("demo.signKindApproval")
5050
+ }),
5051
+ /* @__PURE__ */ jsx("option", {
5052
+ value: "certify-3",
5053
+ children: t("demo.signKindCertifyAnnotate")
5054
+ }),
5055
+ /* @__PURE__ */ jsx("option", {
5056
+ value: "certify-2",
5057
+ children: t("demo.signKindCertifyFill")
5058
+ }),
5059
+ /* @__PURE__ */ jsx("option", {
5060
+ value: "certify-1",
5061
+ children: t("demo.signKindCertifyLocked")
5062
+ })
5063
+ ]
5064
+ })]
5065
+ }) : null,
5066
+ error ? /* @__PURE__ */ jsx("p", {
5067
+ className: "mt-2 text-sm text-red-600",
5068
+ children: error
5069
+ }) : null,
5070
+ /* @__PURE__ */ jsxs("div", {
5071
+ className: "mt-4 flex justify-end gap-2",
5072
+ children: [
5073
+ /* @__PURE__ */ jsx("button", {
5074
+ type: "button",
5075
+ disabled: busy != null,
5076
+ onClick: surface.close,
5077
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
5078
+ children: t("demo.cancel")
5079
+ }),
5080
+ signature.canFill() ? /* @__PURE__ */ jsx("button", {
5081
+ type: "button",
5082
+ disabled: busy != null,
5083
+ onClick: () => void run("fill"),
5084
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-60",
5085
+ children: t("demo.signFill")
5086
+ }) : null,
5087
+ signature.canSign() ? /* @__PURE__ */ jsx("button", {
5088
+ type: "button",
5089
+ disabled: busy != null,
5090
+ onClick: () => void run("sign"),
5091
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
5092
+ children: busy === "sign" ? t("demo.signSigning") : t("demo.signButton")
5093
+ }) : null
5094
+ ]
5095
+ })
5096
+ ]
5097
+ })
5098
+ });
5099
+ }
5100
+ //#endregion
5101
+ //#region src/ui/signature-bridge.tsx
5102
+ /**
5103
+ * The signature plugin's UI intents → shell surfaces. The plugin owns the
5104
+ * act and says WHAT happened; the chrome decides WHAT OPENS:
5105
+ *
5106
+ * target (a "sign here" click) → the signatures panel, right side
5107
+ * ask (mode 'ask' met a field) → the sign dialog (modal)
5108
+ * inspect (a signed field clicked) → the validation popover
5109
+ *
5110
+ * Mounted once per document view; renders nothing.
5111
+ */
5112
+ function SignatureBridge() {
5113
+ const shell = useShell();
5114
+ useSignatureEvent((event) => {
5115
+ switch (event.type) {
5116
+ case "target":
5117
+ if (event.field) shell.open("signatures", { exclusive: "right" });
5118
+ return;
5119
+ case "ask":
5120
+ shell.open("signature-sign", {
5121
+ exclusive: "modal",
5122
+ props: {
5123
+ field: event.field,
5124
+ mark: event.mark
5125
+ }
5126
+ });
5127
+ return;
5128
+ case "inspect":
5129
+ shell.open("signature-inspector", { props: { field: event.field } });
5130
+ return;
5131
+ case "invalidating":
5132
+ shell.open("signatures", { exclusive: "right" });
5133
+ return;
5134
+ default: return;
5135
+ }
5136
+ });
5137
+ return null;
5138
+ }
5139
+ //#endregion
5140
+ //#region src/ui/signature-inspector.tsx
5141
+ /**
5142
+ * The validation popover (`signature-inspector` surface): what a signed
5143
+ * field's signature says and whether it holds — the four independent facts
5144
+ * (integrity, cryptography, trust, changes since) and the signer's claims,
5145
+ * anchored at the widget when its box is known, plus the signed revision as
5146
+ * a download (the exact bytes the signature covers). Mounted in the Stage
5147
+ * overlay so `<Anchored>` can project onto the page.
5148
+ */
5149
+ 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";
5150
+ /** A PDF date string (`D:YYYYMMDDHHmmSSZ` or with a zone offset) as a locale date; the raw text when it is not one. */
5151
+ const pdfDate = (raw) => {
5152
+ const m = /^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}'?\d{2}'?)?/.exec(raw);
5153
+ if (!m) return raw;
5154
+ const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
5155
+ const offset = !zone || zone === "Z" ? "Z" : `${zone[0]}${zone.slice(1, 3)}:${zone.slice(3).replace(/'/g, "") || "00"}`;
5156
+ const date = /* @__PURE__ */ new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}${offset}`);
5157
+ return Number.isNaN(date.getTime()) ? raw : date.toLocaleString();
5158
+ };
5159
+ const tone = (summary) => summary === "valid" ? "text-green-600" : summary === "invalid" ? "text-red-600" : summary === "valid-untrusted" ? "text-amber-600" : "text-fg-muted";
5160
+ function SignatureInspector() {
5161
+ const t = useT();
5162
+ const surface = useSurface("signature-inspector");
5163
+ const signature = useSignature();
5164
+ const snapshot = useSignatureSnapshot();
5165
+ const verdicts = useSignatureVerdicts();
5166
+ const [validating, setValidating] = useState(false);
5167
+ const field = surface.props?.field;
5168
+ const dto = field ? signature.signatureOf(field) : null;
5169
+ const verdict = field ? signature.verdictOf(field) : null;
5170
+ const widget = dto?.widget ?? null;
5171
+ const box = useSelector(FormToken$1, (c) => widget && widget.annotObjectNumber > 0 ? c.fillItem(widget.annotObjectNumber)?.box ?? null : null);
5172
+ useEffect(() => {
5173
+ if (!surface.isOpen || !dto?.signed || verdicts) return;
5174
+ setValidating(true);
5175
+ signature.validate().finally(() => setValidating(false));
5176
+ }, [surface.isOpen, dto?.index]);
5177
+ if (!surface.isOpen || !field || !dto || !snapshot) return null;
5178
+ const revalidate = () => {
5179
+ setValidating(true);
5180
+ signature.validate().finally(() => setValidating(false));
5181
+ };
5182
+ const summary = verdict?.summary ?? null;
5183
+ const modifications = verdict?.modifications.verdict ?? null;
5184
+ const row = (label, value, className = "text-fg") => /* @__PURE__ */ jsxs("div", {
5185
+ className: "flex items-baseline justify-between gap-3 text-xs",
5186
+ children: [/* @__PURE__ */ jsx("span", {
5187
+ className: "text-fg-muted",
5188
+ children: label
5189
+ }), /* @__PURE__ */ jsx("span", {
5190
+ className: `text-right ${className}`,
5191
+ children: value
5192
+ })]
5193
+ });
5194
+ const card = /* @__PURE__ */ jsxs("div", {
5195
+ className: "border-border bg-elevated w-72 rounded-lg border p-3 text-sm shadow-xl",
5196
+ children: [
5197
+ /* @__PURE__ */ jsxs("div", {
5198
+ className: "flex items-center justify-between",
5199
+ children: [/* @__PURE__ */ jsx("span", {
5200
+ className: "text-fg font-semibold",
5201
+ children: t("demo.inspectTitle")
5202
+ }), /* @__PURE__ */ jsx("button", {
5203
+ type: "button",
5204
+ onClick: surface.close,
5205
+ className: "text-fg-muted hover:bg-hover grid h-6 w-6 place-items-center rounded",
5206
+ "aria-label": t("demo.cancel"),
5207
+ children: /* @__PURE__ */ jsx(Icon, {
5208
+ name: "x",
5209
+ size: 14
5210
+ })
5211
+ })]
5212
+ }),
5213
+ /* @__PURE__ */ jsx("p", {
5214
+ className: `mt-1 text-sm font-medium ${tone(summary)}`,
5215
+ children: validating ? t("demo.signaturesValidating") : t(verdictKey(verdict))
5216
+ }),
5217
+ /* @__PURE__ */ jsxs("div", {
5218
+ className: "mt-2 flex flex-col gap-1",
5219
+ children: [
5220
+ row(t("demo.inspectSigner"), dto.signer.name ?? "—"),
5221
+ dto.signer.reason ? row(t("demo.inspectReason"), dto.signer.reason) : null,
5222
+ dto.signer.location ? row(t("demo.inspectLocation"), dto.signer.location) : null,
5223
+ dto.signer.claimedTime ? row(t("demo.inspectTime"), pdfDate(dto.signer.claimedTime)) : null
5224
+ ]
5225
+ }),
5226
+ /* @__PURE__ */ jsxs("div", {
5227
+ className: "border-border-subtle mt-2 flex flex-col gap-1 border-t pt-2",
5228
+ children: [
5229
+ row(t("demo.inspectIntegrity"), verdict?.integrity ?? "—", tone(verdict?.integrity === "valid" ? "valid" : verdict?.integrity === "invalid" ? "invalid" : null)),
5230
+ row(t("demo.inspectCryptography"), verdict?.cryptography ?? "—"),
5231
+ row(t("demo.inspectTrust"), verdict?.trust ?? "—"),
5232
+ 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"),
5233
+ verdict?.modifications.laterRevisions ? row(t("demo.inspectLaterRevisions"), String(verdict.modifications.laterRevisions)) : null
5234
+ ]
5235
+ }),
5236
+ /* @__PURE__ */ jsxs("div", {
5237
+ className: "mt-3 flex justify-end gap-2",
5238
+ children: [/* @__PURE__ */ jsx("button", {
5239
+ type: "button",
5240
+ disabled: validating,
5241
+ onClick: revalidate,
5242
+ className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
5243
+ children: t("demo.signaturesValidate")
5244
+ }), /* @__PURE__ */ jsx(DownloadRevision, { field })]
5245
+ })
5246
+ ]
5247
+ });
5248
+ if (widget && widget.pageObjectNumber > 0 && box) return /* @__PURE__ */ jsx(Anchored, {
5249
+ anchor: {
5250
+ pon: widget.pageObjectNumber,
5251
+ bounds: box
5252
+ },
5253
+ placement: "bottom",
5254
+ gap: 8,
5255
+ children: card
5256
+ });
5257
+ return /* @__PURE__ */ jsx("div", {
5258
+ className: "absolute right-4 top-4 z-40",
5259
+ children: card
5260
+ });
5261
+ }
5262
+ /** The signed revision — the exact bytes the signature covers — as a file. */
5263
+ function DownloadRevision({ field }) {
5264
+ const t = useT();
5265
+ const signature = useSignature();
5266
+ const dto = signature.signatureOf(field);
5267
+ const [busy, setBusy] = useState(false);
5268
+ if (!dto || dto.revisionIndex === null) return null;
5269
+ const revisionIndex = dto.revisionIndex;
5270
+ const run = async () => {
5271
+ setBusy(true);
5272
+ try {
5273
+ const bytes = await signature.revisionBytes(revisionIndex);
5274
+ const blob = new Blob([bytes], { type: "application/pdf" });
5275
+ const url = URL.createObjectURL(blob);
5276
+ const a = document.createElement("a");
5277
+ a.href = url;
5278
+ a.download = `${dto.fieldName}-signed.pdf`;
5279
+ a.click();
5280
+ URL.revokeObjectURL(url);
5281
+ } catch (err) {
5282
+ console.error("[embedpdf] signed revision download failed:", err);
5283
+ } finally {
5284
+ setBusy(false);
5285
+ }
5286
+ };
5287
+ return /* @__PURE__ */ jsx("button", {
5288
+ type: "button",
5289
+ disabled: busy,
5290
+ onClick: () => void run(),
5291
+ className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
5292
+ children: t("demo.inspectDownload")
5293
+ });
5294
+ }
5295
+ //#endregion
5296
+ //#region src/ui/signature-maker.tsx
5297
+ /**
5298
+ * The mark maker (`signature-maker` modal surface): a person's signature and
5299
+ * initials, drawn, typed, or uploaded, saved as ONE library of kind
5300
+ * `signatures` — the file IS the person (Preview's model). Chrome code over
5301
+ * the stamp plugin's generic calls: `createLibrary` + `addAsset({ mark })`;
5302
+ * rename/export/delete are the library verbs. Opened on an existing library
5303
+ * (`props.libraryId`) it only adds the missing initials.
5304
+ */
5305
+ const PAD = {
5306
+ width: 360,
5307
+ height: 120
5308
+ };
5309
+ const BUILTIN_FONTS = [{
5310
+ key: "helvetica-oblique",
5311
+ label: "Helvetica"
5312
+ }, {
5313
+ key: "times-italic",
5314
+ label: "Times"
5315
+ }];
5316
+ /** One pad: draw / type / upload, producing a `MarkSource` or null. */
5317
+ function MarkPad({ value, onChange, fonts, registerFont }) {
5318
+ const t = useT();
5319
+ const [tab, setTab] = useState("draw");
5320
+ const [strokes, setStrokes] = useState([]);
5321
+ const [text, setText] = useState("");
5322
+ const [font, setFont] = useState(fonts[0]?.key ?? "helvetica-oblique");
5323
+ const [fileName, setFileName] = useState(null);
5324
+ const canvas = useRef(null);
5325
+ const drawing = useRef(null);
5326
+ useEffect(() => {
5327
+ const el = canvas.current;
5328
+ if (!el) return;
5329
+ const dpr = window.devicePixelRatio || 1;
5330
+ el.width = PAD.width * dpr;
5331
+ el.height = PAD.height * dpr;
5332
+ const ctx = el.getContext("2d");
5333
+ if (!ctx) return;
5334
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
5335
+ ctx.clearRect(0, 0, PAD.width, PAD.height);
5336
+ ctx.lineCap = "round";
5337
+ ctx.lineJoin = "round";
5338
+ ctx.lineWidth = 2.5;
5339
+ ctx.strokeStyle = getComputedStyle(el).color || "#000";
5340
+ for (const stroke of strokes) {
5341
+ ctx.beginPath();
5342
+ stroke.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
5343
+ ctx.stroke();
5344
+ }
5345
+ }, [strokes, tab]);
5346
+ const point = (e) => {
5347
+ const r = e.currentTarget.getBoundingClientRect();
5348
+ return {
5349
+ x: e.clientX - r.left,
5350
+ y: e.clientY - r.top
5351
+ };
5352
+ };
5353
+ const commitStrokes = (next) => {
5354
+ setStrokes(next);
5355
+ onChange(next.length ? {
5356
+ kind: "ink",
5357
+ strokes: next,
5358
+ color: "#000000",
5359
+ strokeWidth: 2.5
5360
+ } : null);
5361
+ };
5362
+ const commitText = async (next, key) => {
5363
+ setText(next);
5364
+ setFont(key);
5365
+ if (!next.trim()) {
5366
+ onChange(null);
5367
+ return;
5368
+ }
5369
+ await registerFont(key);
5370
+ onChange({
5371
+ kind: "text",
5372
+ text: next.trim(),
5373
+ fontFamily: key,
5374
+ color: "#000000",
5375
+ fontSize: 36
5376
+ });
5377
+ };
5378
+ const pickFile = (file) => {
5379
+ setFileName(file.name);
5380
+ onChange(file.type === "application/pdf" ? {
5381
+ kind: "pdf",
5382
+ source: file,
5383
+ pageIndex: 0
5384
+ } : {
5385
+ kind: "image",
5386
+ source: file
5387
+ });
5388
+ };
5389
+ 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"}`;
5390
+ return /* @__PURE__ */ jsxs("div", {
5391
+ className: "flex flex-col gap-2",
5392
+ children: [/* @__PURE__ */ jsxs("div", {
5393
+ className: "flex items-center gap-1",
5394
+ children: [
5395
+ /* @__PURE__ */ jsx("button", {
5396
+ type: "button",
5397
+ className: tabClass("draw"),
5398
+ onClick: () => setTab("draw"),
5399
+ children: t("demo.makerDraw")
5400
+ }),
5401
+ /* @__PURE__ */ jsx("button", {
5402
+ type: "button",
5403
+ className: tabClass("type"),
5404
+ onClick: () => setTab("type"),
5405
+ children: t("demo.makerType")
5406
+ }),
5407
+ /* @__PURE__ */ jsx("button", {
5408
+ type: "button",
5409
+ className: tabClass("upload"),
5410
+ onClick: () => setTab("upload"),
5411
+ children: t("demo.makerUpload")
5412
+ }),
5413
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
5414
+ value ? /* @__PURE__ */ jsx("button", {
5415
+ type: "button",
5416
+ onClick: () => {
5417
+ commitStrokes([]);
5418
+ setText("");
5419
+ setFileName(null);
5420
+ onChange(null);
5421
+ },
5422
+ className: "text-fg-muted hover:text-fg text-xs",
5423
+ children: t("demo.makerClear")
5424
+ }) : null
5425
+ ]
5426
+ }), tab === "draw" ? /* @__PURE__ */ jsx("canvas", {
5427
+ ref: canvas,
5428
+ style: {
5429
+ width: PAD.width,
5430
+ height: PAD.height,
5431
+ touchAction: "none"
5432
+ },
5433
+ className: "border-border-subtle bg-surface text-fg rounded-md border",
5434
+ onPointerDown: (e) => {
5435
+ e.currentTarget.setPointerCapture(e.pointerId);
5436
+ drawing.current = [point(e)];
5437
+ setStrokes((s) => [...s, drawing.current]);
5438
+ },
5439
+ onPointerMove: (e) => {
5440
+ if (!drawing.current) return;
5441
+ drawing.current.push(point(e));
5442
+ setStrokes((s) => [...s.slice(0, -1), [...drawing.current]]);
5443
+ },
5444
+ onPointerUp: () => {
5445
+ if (!drawing.current) return;
5446
+ const done = drawing.current;
5447
+ drawing.current = null;
5448
+ setStrokes((s) => {
5449
+ const next = [...s.slice(0, -1), done];
5450
+ commitStrokes(next);
5451
+ return next;
5452
+ });
5453
+ }
5454
+ }) : tab === "type" ? /* @__PURE__ */ jsxs("div", {
5455
+ className: "flex flex-col gap-2",
5456
+ children: [/* @__PURE__ */ jsx("input", {
5457
+ value: text,
5458
+ placeholder: t("demo.makerTypePlaceholder"),
5459
+ onChange: (e) => void commitText(e.target.value, font),
5460
+ className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm"
5461
+ }), /* @__PURE__ */ jsxs("label", {
5462
+ className: "text-fg-muted flex items-center gap-2 text-xs",
5463
+ children: [t("demo.makerFont"), /* @__PURE__ */ jsx("select", {
5464
+ value: font,
5465
+ onChange: (e) => void commitText(text, e.target.value),
5466
+ className: "border-border bg-surface text-fg rounded border px-2 py-1 text-xs",
5467
+ children: fonts.map((f) => /* @__PURE__ */ jsx("option", {
5468
+ value: f.key,
5469
+ children: f.label
5470
+ }, f.key))
5471
+ })]
5472
+ })]
5473
+ }) : /* @__PURE__ */ jsxs("label", {
5474
+ 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",
5475
+ children: [fileName ?? t("demo.makerUploadHint"), /* @__PURE__ */ jsx("input", {
5476
+ type: "file",
5477
+ accept: "image/png,image/jpeg,application/pdf",
5478
+ className: "hidden",
5479
+ onChange: (e) => {
5480
+ const file = e.currentTarget.files?.[0];
5481
+ e.currentTarget.value = "";
5482
+ if (file) pickFile(file);
5483
+ }
5484
+ })]
5485
+ })]
5486
+ });
5487
+ }
5488
+ function SignatureMakerModal() {
5489
+ const t = useT();
5490
+ const surface = useSurface("signature-maker");
5491
+ const stamp = useStamp();
5492
+ const kernel = useKernel();
5493
+ const config = useSignaturesConfig();
5494
+ const [name, setName] = useState("");
5495
+ const [signatureMark, setSignatureMark] = useState(null);
5496
+ const [initialsMark, setInitialsMark] = useState(null);
5497
+ const [busy, setBusy] = useState(false);
5498
+ const [error, setError] = useState(null);
5499
+ const existingId = surface.props?.libraryId;
5500
+ const existing = existingId ? stamp.library(existingId) : null;
5501
+ const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
5502
+ const fonts = [...(config.fonts ?? []).map((f) => ({
5503
+ key: f.key,
5504
+ label: f.label
5505
+ })), ...BUILTIN_FONTS];
5506
+ const registered = useRef(/* @__PURE__ */ new Set());
5507
+ const registerFont = async (key) => {
5508
+ const spec = config.fonts?.find((f) => f.key === key);
5509
+ if (!spec || registered.current.has(key) || !kernel.engine.fonts) return;
5510
+ const response = await fetch(spec.url);
5511
+ if (!response.ok) throw new Error(`${spec.url}: HTTP ${response.status}`);
5512
+ await kernel.engine.fonts.register({
5513
+ key,
5514
+ data: new Uint8Array(await response.arrayBuffer())
5515
+ });
5516
+ registered.current.add(key);
5517
+ };
5518
+ if (!surface.isOpen) return null;
5519
+ const canSave = existing ? initialsMark != null : name.trim().length > 0 && signatureMark != null;
5520
+ const save = async () => {
5521
+ setBusy(true);
5522
+ setError(null);
5523
+ try {
5524
+ if (existing) await stamp.addAsset({
5525
+ libraryId: existing.id,
5526
+ name: INITIALS_MARK_NAME,
5527
+ label: t("demo.initialsLabel"),
5528
+ mark: initialsMark
5529
+ });
5530
+ else {
5531
+ const libraryId = await stamp.createLibrary(name.trim(), { kind: SIGNATURES_LIBRARY_KIND });
5532
+ await stamp.addAsset({
5533
+ libraryId,
5534
+ name: SIGNATURE_MARK_NAME,
5535
+ label: t("demo.signatureLabel"),
5536
+ mark: signatureMark
5537
+ });
5538
+ if (initialsMark) await stamp.addAsset({
5539
+ libraryId,
5540
+ name: INITIALS_MARK_NAME,
5541
+ label: t("demo.initialsLabel"),
5542
+ mark: initialsMark
5543
+ });
5544
+ }
5545
+ setName("");
5546
+ setSignatureMark(null);
5547
+ setInitialsMark(null);
5548
+ surface.close();
5549
+ } catch (err) {
5550
+ console.error("[embedpdf] saving the signature failed:", err);
5551
+ setError(t("demo.makerError"));
5552
+ } finally {
5553
+ setBusy(false);
5554
+ }
5555
+ };
5556
+ return /* @__PURE__ */ jsx("div", {
5557
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
5558
+ children: /* @__PURE__ */ jsxs("div", {
5559
+ className: "border-border bg-surface w-[26rem] rounded-lg border p-4 shadow-xl",
5560
+ children: [
5561
+ /* @__PURE__ */ jsxs("div", {
5562
+ className: "flex items-center justify-between",
5563
+ children: [/* @__PURE__ */ jsx("h2", {
5564
+ className: "text-fg text-base font-semibold",
5565
+ children: existing ? t("demo.signaturesAddInitials") : t("demo.makerTitle")
5566
+ }), /* @__PURE__ */ jsx("button", {
5567
+ type: "button",
5568
+ onClick: surface.close,
5569
+ className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
5570
+ "aria-label": t("demo.cancel"),
5571
+ children: /* @__PURE__ */ jsx(Icon, {
5572
+ name: "x",
5573
+ size: 16
5574
+ })
5575
+ })]
5576
+ }),
5577
+ existing ? null : /* @__PURE__ */ jsxs("label", {
5578
+ className: "text-fg-muted mt-3 block text-xs",
5579
+ children: [t("demo.makerName"), /* @__PURE__ */ jsx("input", {
5580
+ value: name,
5581
+ placeholder: t("demo.makerNamePlaceholder"),
5582
+ onChange: (e) => setName(e.target.value),
5583
+ className: "border-border bg-surface text-fg mt-1 w-full rounded border px-2 py-1.5 text-sm"
5584
+ })]
5585
+ }),
5586
+ existing ? null : /* @__PURE__ */ jsxs("div", {
5587
+ className: "mt-3",
5588
+ children: [/* @__PURE__ */ jsx("p", {
5589
+ className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
5590
+ children: t("demo.signatureLabel")
5591
+ }), /* @__PURE__ */ jsx(MarkPad, {
5592
+ value: signatureMark,
5593
+ onChange: setSignatureMark,
5594
+ fonts,
5595
+ registerFont
5596
+ })]
5597
+ }),
5598
+ wantsInitials ? /* @__PURE__ */ jsxs("div", {
5599
+ className: "mt-3",
5600
+ children: [/* @__PURE__ */ jsxs("p", {
5601
+ className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
5602
+ children: [t("demo.initialsLabel"), existing ? null : /* @__PURE__ */ jsx("span", {
5603
+ className: "text-fg-muted ml-1 font-normal normal-case tracking-normal",
5604
+ children: t("demo.makerOptional")
5605
+ })]
5606
+ }), /* @__PURE__ */ jsx(MarkPad, {
5607
+ value: initialsMark,
5608
+ onChange: setInitialsMark,
5609
+ fonts,
5610
+ registerFont
5611
+ })]
5612
+ }) : null,
5613
+ error ? /* @__PURE__ */ jsx("p", {
5614
+ className: "mt-2 text-sm text-red-600",
5615
+ children: error
5616
+ }) : null,
5617
+ /* @__PURE__ */ jsxs("div", {
5618
+ className: "mt-4 flex justify-end gap-2",
5619
+ children: [/* @__PURE__ */ jsx("button", {
5620
+ type: "button",
5621
+ disabled: busy,
5622
+ onClick: surface.close,
5623
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
5624
+ children: t("demo.cancel")
5625
+ }), /* @__PURE__ */ jsx("button", {
5626
+ type: "button",
5627
+ disabled: busy || !canSave,
5628
+ onClick: () => void save(),
5629
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
5630
+ children: busy ? t("demo.makerSaving") : t("demo.makerSave")
5631
+ })]
5632
+ })
5633
+ ]
5634
+ })
5635
+ });
5636
+ }
5637
+ //#endregion
3975
5638
  //#region src/ui/document-boot.tsx
3976
5639
  /**
3977
5640
  * Boot-state panes for the document area — what renders INSTEAD of the Stage
@@ -4213,9 +5876,10 @@ function Shell() {
4213
5876
  className: "w-full"
4214
5877
  })
4215
5878
  });
4216
- return /* @__PURE__ */ jsxs("div", {
5879
+ return /* @__PURE__ */ jsx(AnnotationFontsProvider, { children: /* @__PURE__ */ jsxs("div", {
4217
5880
  className: "bg-app text-fg flex h-full flex-col",
4218
5881
  children: [
5882
+ /* @__PURE__ */ jsx(StampLibraryStore, {}),
4219
5883
  (frame.header ?? true) && /* @__PURE__ */ jsx("slot", { name: "header" }),
4220
5884
  (frame.tabs ?? "always") !== "never" && /* @__PURE__ */ jsx("slot", {
4221
5885
  name: "tabs",
@@ -4235,6 +5899,7 @@ function Shell() {
4235
5899
  overlay: /* @__PURE__ */ jsxs(Fragment$1, { children: [
4236
5900
  /* @__PURE__ */ jsx(AnnotationStrip, {}),
4237
5901
  /* @__PURE__ */ jsx(SelectionStrip, {}),
5902
+ /* @__PURE__ */ jsx(SignatureInspector, {}),
4238
5903
  /* @__PURE__ */ jsx(SelectionHandles, {}),
4239
5904
  /* @__PURE__ */ jsx(Scrollbar, { axis: "y" }),
4240
5905
  /* @__PURE__ */ jsx(Scrollbar, { axis: "x" })
@@ -4253,12 +5918,16 @@ function Shell() {
4253
5918
  ]
4254
5919
  }),
4255
5920
  /* @__PURE__ */ jsx(RightSidebar, {}),
4256
- /* @__PURE__ */ jsx(RedactConfirmModal, {})
5921
+ /* @__PURE__ */ jsx(RedactConfirmModal, {}),
5922
+ /* @__PURE__ */ jsx(CalibrationDialog, {}),
5923
+ /* @__PURE__ */ jsx(SignatureBridge, {}),
5924
+ /* @__PURE__ */ jsx(SignDialog, {}),
5925
+ /* @__PURE__ */ jsx(SignatureMakerModal, {})
4257
5926
  ] })
4258
5927
  }),
4259
5928
  toolbarEdge === "bottom" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ModeBand, { edge: "bottom" }), toolbarBand] })
4260
5929
  ]
4261
- });
5930
+ }) });
4262
5931
  }
4263
5932
  //#endregion
4264
5933
  //#region src/viewer.tsx
@@ -4281,7 +5950,7 @@ function Shell() {
4281
5950
  * changing these props on a mounted viewer does not rebuild the workspace.
4282
5951
  */
4283
5952
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
4284
- const BUILTIN_LOADERS = { es: () => import("./es-DT5ZoB99.js").then((m) => m.es) };
5953
+ const BUILTIN_LOADERS = { es: () => import("./es-CLgTsFDS.js").then((m) => m.es) };
4285
5954
  const isDict = (v) => typeof v === "object" && v !== null;
4286
5955
  /**
4287
5956
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -4325,7 +5994,7 @@ function Booting() {
4325
5994
  })
4326
5995
  });
4327
5996
  }
4328
- function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, theme, themeTarget, onViewer }) {
5997
+ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, signatures, annotations, theme, themeTarget, onViewer }) {
4329
5998
  const [resolved] = useState(() => {
4330
5999
  const byId = new Map(defaultCommands.map((c) => [c.id, c]));
4331
6000
  for (const c of commands ?? []) byId.set(c.id, c);
@@ -4357,6 +6026,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4357
6026
  chrome: resolvedChrome,
4358
6027
  icons: icons ?? {},
4359
6028
  stamps: stamps ?? {},
6029
+ signatures: signatures ?? {},
6030
+ annotations: annotations ?? {},
4360
6031
  i18n: {
4361
6032
  locales,
4362
6033
  loaders,
@@ -4422,8 +6093,15 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4422
6093
  }
4423
6094
  } }),
4424
6095
  formPlugin(),
6096
+ signaturePlugin({
6097
+ mode: resolved.signatures.mode,
6098
+ signer: resolved.signatures.signer,
6099
+ trust: resolved.signatures.trust,
6100
+ allowCertify: resolved.signatures.allowCertify
6101
+ }),
4425
6102
  linkPlugin(),
4426
6103
  redactionPlugin(),
6104
+ measurementPlugin(),
4427
6105
  searchPlugin(),
4428
6106
  demoToolsPlugin(),
4429
6107
  i18nPlugin({
@@ -4455,6 +6133,6 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4455
6133
  });
4456
6134
  }
4457
6135
  //#endregion
4458
- export { AnnotationToken, CommandsToken, DocumentsToken, FormToken, FullViewer, I18nToken, InteractionToken, MetadataToken, RedactionToken, SearchToken, SelectionToken, ShellToken, StageToken, StampToken, addItem, chromeHelpers, createViewerHandle, custom, defaultChrome, defaultCommands, ICON_PATHS as defaultIcons, defineChrome, group, item, removeItems, replaceItem, themeConfigOf, validateChrome };
6136
+ export { AnnotationToken, CommandsToken, DocumentsToken, FormToken, FullViewer, I18nToken, InteractionToken, MeasurementToken, MetadataToken, RedactionToken, SearchToken, SelectionToken, ShellToken, SignatureToken, StageToken, StampToken, addItem, chromeHelpers, createTestSigner, createViewerHandle, custom, defaultChrome, defaultCommands, ICON_PATHS as defaultIcons, defineChrome, group, indexedDbKeyStore, item, memoryKeyStore, personalSigner, remoteSigner, removeItems, replaceItem, themeConfigOf, validateChrome, webCryptoSigner };
4459
6137
 
4460
6138
  //# sourceMappingURL=index.js.map