@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.cjs CHANGED
@@ -33,9 +33,11 @@ let _embedpdf_react_interaction = require("@embedpdf/react/interaction");
33
33
  let _embedpdf_react_selection = require("@embedpdf/react/selection");
34
34
  let _embedpdf_react_annotation = require("@embedpdf/react/annotation");
35
35
  let _embedpdf_react_stamp = require("@embedpdf/react/stamp");
36
+ let _embedpdf_react_measurement = require("@embedpdf/react/measurement");
36
37
  let _embedpdf_react_redaction = require("@embedpdf/react/redaction");
37
38
  let _embedpdf_react_actions = require("@embedpdf/react/actions");
38
39
  let _embedpdf_react_form = require("@embedpdf/react/form");
40
+ let _embedpdf_react_signature = require("@embedpdf/react/signature");
39
41
  let _embedpdf_react_link = require("@embedpdf/react/link");
40
42
  let _embedpdf_react_search = require("@embedpdf/react/search");
41
43
  let _embedpdf_react_i18n = require("@embedpdf/react/i18n");
@@ -44,8 +46,10 @@ let _embedpdf_react_shell = require("@embedpdf/react/shell");
44
46
  let _embedpdf_react_toolbar = require("@embedpdf/react/toolbar");
45
47
  let react_jsx_runtime = require("react/jsx-runtime");
46
48
  let _embedpdf_react_scrollbar = require("@embedpdf/react/scrollbar");
47
- let _embedpdf_react_annotation_menu = require("@embedpdf/react/annotation-menu");
48
49
  let _embedpdf_default_stamps_library = require("@embedpdf/default-stamps/library");
50
+ let _embedpdf_web = require("@embedpdf/web");
51
+ let _embedpdf_react_annotation_menu = require("@embedpdf/react/annotation-menu");
52
+ let _embedpdf_react_anchored = require("@embedpdf/react/anchored");
49
53
  let _embedpdf_react_metadata = require("@embedpdf/react/metadata");
50
54
  //#region src/config/stage.ts
51
55
  /**
@@ -102,6 +106,32 @@ const en = {
102
106
  code: "en",
103
107
  name: "English",
104
108
  translations: {
109
+ measurement: {
110
+ title: "Measurements",
111
+ distance: "Distance",
112
+ perimeter: "Perimeter",
113
+ area: "Area",
114
+ areaUnit: "Area unit",
115
+ resetLabel: "Reset label position",
116
+ calibrate: "Calibrate scale",
117
+ scale: "Scale",
118
+ custom: "Custom",
119
+ unavailable: "Unavailable",
120
+ 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.",
121
+ sessionOnly: "This engine keeps page calibration for this session only.",
122
+ allPages: "Apply to all pages",
123
+ recalculate: "Recalculate existing measurements",
124
+ preset: "Scale preset",
125
+ choosePreset: "Choose a preset",
126
+ unit: "Unit",
127
+ precision: "Precision",
128
+ selection: "Selected measurements",
129
+ report: "{updated} updated · {skipped} skipped · {failed} failed",
130
+ knownLength: "Enter the real length represented by the line you just drew.",
131
+ length: "Known length",
132
+ apply: "Apply scale",
133
+ applying: "Applying…"
134
+ },
105
135
  commands: {
106
136
  zoom: {
107
137
  in: "Zoom In",
@@ -145,6 +175,7 @@ const en = {
145
175
  counterclockwise: "Rotate Counter-Clockwise"
146
176
  },
147
177
  mode: {
178
+ measure: "Measure",
148
179
  group: "Mode",
149
180
  view: "View",
150
181
  annotate: "Annotate",
@@ -154,6 +185,7 @@ const en = {
154
185
  redact: "Redact"
155
186
  },
156
187
  annotate: {
188
+ cancelCreation: "Cancel drawing",
157
189
  highlight: "Highlight",
158
190
  strikeout: "Strikeout",
159
191
  underline: "Underline",
@@ -197,7 +229,8 @@ const en = {
197
229
  checkbox: "Checkbox",
198
230
  radio: "Radio Button",
199
231
  select: "Dropdown",
200
- listbox: "List Box"
232
+ listbox: "List Box",
233
+ signature: "Signature Field"
201
234
  },
202
235
  redact: {
203
236
  mark: "Mark for Redaction",
@@ -294,6 +327,7 @@ const en = {
294
327
  redactConfirmCollateral: "{count} other annotations will also be removed.",
295
328
  redactApply: "Apply",
296
329
  redactApplying: "Applying…",
330
+ formatLabel: "Format",
297
331
  redactLabelTitle: "Redaction label",
298
332
  redactLabelPlaceholder: "e.g. REDACTED",
299
333
  redactLabelRepeat: "Repeat label across region",
@@ -312,7 +346,72 @@ const en = {
312
346
  stampsCustomLabel: "Custom stamp",
313
347
  stampsError: "The built-in stamps could not be loaded.",
314
348
  stampsArmError: "That stamp could not be armed.",
315
- stampsImportError: "That PDF could not be imported as a stamp library."
349
+ stampsImportError: "That PDF could not be imported as a stamp library.",
350
+ quickStampsLabel: "Quick stamps",
351
+ signaturesTitle: "Signatures",
352
+ signaturesEmpty: "No signatures yet. Create one to sign with.",
353
+ signaturesNew: "New signature…",
354
+ signaturesArmedHint: "Click a signature field to sign it, or anywhere on a page to place the mark.",
355
+ signaturesTargetHint: "Pick a mark for the selected field.",
356
+ signaturesInDocument: "In this document",
357
+ signaturesNone: "This document has no signature fields.",
358
+ signaturesValidate: "Validate",
359
+ signaturesValidating: "Validating…",
360
+ signaturesSigned: "Signed",
361
+ signaturesUnsigned: "Unsigned",
362
+ signaturesRename: "Rename",
363
+ signaturesRemove: "Remove",
364
+ signaturesExport: "Export as PDF",
365
+ signaturesAddInitials: "Add initials",
366
+ signatureLabel: "Signature",
367
+ initialsLabel: "Initials",
368
+ makerTitle: "New signature",
369
+ makerName: "Name",
370
+ makerNamePlaceholder: "Your name",
371
+ makerDraw: "Draw",
372
+ makerType: "Type",
373
+ makerUpload: "Upload",
374
+ makerTypePlaceholder: "Type your name",
375
+ makerFont: "Font",
376
+ makerUploadHint: "Choose a PNG, JPEG, or one-page PDF",
377
+ makerClear: "Clear",
378
+ makerOptional: "(optional)",
379
+ makerSave: "Save",
380
+ makerSaving: "Saving…",
381
+ makerError: "The signature could not be saved.",
382
+ signTitle: "Sign this field",
383
+ signReason: "Reason",
384
+ signLocation: "Location",
385
+ signKind: "What may change afterwards",
386
+ signKindApproval: "Approval signature — form filling and more signatures stay valid",
387
+ signKindCertifyAnnotate: "Certify — allow comments, form filling and signatures",
388
+ signKindCertifyFill: "Certify — allow form filling and signatures only",
389
+ signKindCertifyLocked: "Certify — allow no changes",
390
+ signButton: "Sign",
391
+ signSigning: "Signing…",
392
+ signFill: "Place without signing",
393
+ signError: "The field could not be signed.",
394
+ inspectTitle: "Signature",
395
+ inspectSigner: "Signed by",
396
+ inspectReason: "Reason",
397
+ inspectLocation: "Location",
398
+ inspectTime: "Time",
399
+ inspectIntegrity: "Document",
400
+ inspectCryptography: "Signature",
401
+ inspectTrust: "Signer",
402
+ inspectChanges: "Changes since",
403
+ inspectDownload: "Download signed version",
404
+ verdictValid: "Valid",
405
+ verdictValidUntrusted: "Valid, signer not trusted",
406
+ verdictInvalid: "Invalid — modified after signing",
407
+ verdictWillInvalidate: "Valid, invalid once saved",
408
+ signaturesInvalidating: "This change will invalidate {field} when the document is saved. Undo it to keep the signature.",
409
+ verdictIndeterminate: "Could not be verified",
410
+ verdictUnchanged: "None",
411
+ verdictRestored: "Changed, then restored",
412
+ inspectLaterRevisions: "Revisions after signing",
413
+ verdictPermitted: "Permitted",
414
+ verdictForbidden: "Not permitted"
316
415
  }
317
416
  }
318
417
  };
@@ -330,7 +429,9 @@ const en = {
330
429
  const ViewerConfigContext = (0, react.createContext)({
331
430
  chrome: require_chrome.defaultChrome,
332
431
  icons: {},
333
- stamps: {}
432
+ stamps: {},
433
+ signatures: {},
434
+ annotations: {}
334
435
  });
335
436
  const ViewerConfigProvider = ViewerConfigContext.Provider;
336
437
  function useChromeSchema() {
@@ -348,6 +449,12 @@ function useCustomIcons() {
348
449
  function useStampsConfig() {
349
450
  return (0, react.useContext)(ViewerConfigContext).stamps;
350
451
  }
452
+ function useSignaturesConfig() {
453
+ return (0, react.useContext)(ViewerConfigContext).signatures;
454
+ }
455
+ function useAnnotationsConfig() {
456
+ return (0, react.useContext)(ViewerConfigContext).annotations;
457
+ }
351
458
  //#endregion
352
459
  //#region src/handle.ts
353
460
  function createViewerHandle(kernel) {
@@ -394,7 +501,40 @@ function createViewerHandle(kernel) {
394
501
  *
395
502
  * GENERATED by scratchpad/extract-icons.mjs — 93 icons (slot tags by hand).
396
503
  */
504
+ const measurementOutline = [{
505
+ d: "M5 4H19M20 5V19M19 20H5M4 19V5",
506
+ stroke: "primary"
507
+ }, {
508
+ 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",
509
+ stroke: "primary"
510
+ }];
397
511
  const ICON_PATHS = {
512
+ ruler: ["M3 8h18v8H3z", "M7 8v4M11 8v2M15 8v4M19 8v2"],
513
+ distance: [{
514
+ d: "M5 12H19",
515
+ stroke: "primary",
516
+ strokeDasharray: "2 2"
517
+ }, {
518
+ 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",
519
+ stroke: "primary",
520
+ fill: "primary",
521
+ fillOpacity: .25
522
+ }],
523
+ perimeter: measurementOutline,
524
+ area: [
525
+ {
526
+ d: "M5 4H19V5H20V19H19V20H5V19H4V5H5Z",
527
+ fill: "primary",
528
+ fillOpacity: .08
529
+ },
530
+ {
531
+ 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",
532
+ stroke: "primary",
533
+ strokeWidth: .6,
534
+ strokeOpacity: .5
535
+ },
536
+ ...measurementOutline
537
+ ],
398
538
  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"],
399
539
  alignBottom: [
400
540
  "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",
@@ -444,6 +584,11 @@ const ICON_PATHS = {
444
584
  "M19 16h-12a2 2 0 0 0 -2 2",
445
585
  "M9 8h6"
446
586
  ],
587
+ calibrate: [
588
+ "M12 12m-7.5 0a7.5 7.5 0 1 0 15 0a7.5 7.5 0 1 0 -15 0",
589
+ "M12 2.75V7M12 17V21.25M2.75 12H7M17 12H21.25",
590
+ "M12 12m-2.5 0a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0"
591
+ ],
447
592
  callout: [
448
593
  {
449
594
  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",
@@ -866,6 +1011,11 @@ const ICON_PATHS = {
866
1011
  ],
867
1012
  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"],
868
1013
  unlock: ["M7 11V7a5 5 0 0 1 9.9-1"],
1014
+ updateScale: [
1015
+ "M3.5 13.5L18.5 7.5L21.25 14.5L6.25 20.5L3.5 13.5Z",
1016
+ "M8 12.8L9 15.2M11.5 11.4L12.2 13.2M15 10L16 12.4",
1017
+ "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"
1018
+ ],
869
1019
  vertical: [
870
1020
  "M8 7l4 -4l4 4",
871
1021
  "M8 17l4 4l4 -4",
@@ -935,7 +1085,11 @@ function Icon({ name, size = 20, strokeWidth = 2, className, title, accent }) {
935
1085
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
936
1086
  d: p.d,
937
1087
  fill: fill ?? "none",
938
- stroke
1088
+ stroke,
1089
+ strokeWidth: p.strokeWidth,
1090
+ strokeDasharray: p.strokeDasharray,
1091
+ strokeOpacity: p.strokeOpacity,
1092
+ fillOpacity: p.fillOpacity
939
1093
  }, i);
940
1094
  })
941
1095
  });
@@ -969,6 +1123,495 @@ function ThemeProvider({ children, preference = "system", target }) {
969
1123
  });
970
1124
  }
971
1125
  //#endregion
1126
+ //#region src/ui/measurement.tsx
1127
+ const control = "border-border bg-surface text-fg w-full rounded-md border px-2 py-1.5 text-sm disabled:opacity-50";
1128
+ const button = "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-50";
1129
+ const useCurrentPon = () => (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_stage.StageToken, (c) => c.pages()[c.currentPage()]?.pon ?? -1);
1130
+ function MeasurementScaleButton() {
1131
+ const t = (0, _embedpdf_react_i18n.useT)();
1132
+ const scale = (0, _embedpdf_react_measurement.usePageScale)(useCurrentPon());
1133
+ const surface = (0, _embedpdf_react_shell.useSurface)("measurement");
1134
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1135
+ className: `${button} inline-flex items-center gap-1.5 whitespace-nowrap`,
1136
+ onClick: () => surface.open({ exclusive: "right" }),
1137
+ title: t("measurement.title"),
1138
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1139
+ name: "updateScale",
1140
+ size: 20,
1141
+ className: "shrink-0"
1142
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1143
+ t("measurement.scale"),
1144
+ ":",
1145
+ " ",
1146
+ scale.measure?.subtype === "RL" ? scale.measure.ratio ?? t("measurement.custom") : t("measurement.unavailable")
1147
+ ] })]
1148
+ });
1149
+ }
1150
+ function MeasurementSection() {
1151
+ const t = (0, _embedpdf_react_i18n.useT)();
1152
+ const pon = useCurrentPon();
1153
+ const measurement = (0, _embedpdf_react_measurement.useMeasurement)();
1154
+ const scale = (0, _embedpdf_react_measurement.usePageScale)(pon);
1155
+ const anno = (0, _embedpdf_react_runtime.useCapability)(_embedpdf_react_annotation.AnnotationToken);
1156
+ const selected = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_annotation.AnnotationToken, (c) => c.selection());
1157
+ const resettable = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_annotation.AnnotationToken, (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]));
1158
+ const readouts = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_measurement.MeasurementToken, (c) => anno.getSelected().map((d) => c.readout(d.ref)).filter((r) => !("unavailable" in r)), (a, b) => JSON.stringify(a) === JSON.stringify(b));
1159
+ const reports = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_measurement.MeasurementToken, (c) => c.lastReports());
1160
+ const [allPages, setAllPages] = (0, react.useState)(false);
1161
+ const [recalculate, setRecalculate] = (0, react.useState)(true);
1162
+ const [error, setError] = (0, react.useState)(null);
1163
+ const disabled = measurement.busy || !measurement.canCalibratePage || !scale.ready;
1164
+ const rectilinear = scale.measure?.subtype === "RL" ? scale.measure : null;
1165
+ const options = {
1166
+ allPages,
1167
+ recalculate
1168
+ };
1169
+ const run = async (work) => {
1170
+ setError(null);
1171
+ try {
1172
+ await work();
1173
+ } catch (e) {
1174
+ setError(e instanceof Error ? e.message : String(e));
1175
+ }
1176
+ };
1177
+ const precision = rectilinear?.distance[rectilinear.distance.length - 1]?.precision ?? 100;
1178
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1179
+ className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 text-sm",
1180
+ children: [
1181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1182
+ className: "text-fg-muted",
1183
+ children: t("measurement.instructions")
1184
+ }),
1185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1186
+ className: "text-fg-muted",
1187
+ children: [t("measurement.scale"), ": "]
1188
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rectilinear?.ratio ?? t("measurement.unavailable") })] }),
1189
+ scale.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1190
+ role: "alert",
1191
+ className: "text-red-600",
1192
+ children: scale.error.message
1193
+ }),
1194
+ !scale.persistent && scale.ready && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1195
+ className: "text-fg-muted",
1196
+ children: t("measurement.sessionOnly")
1197
+ }),
1198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1199
+ className: "flex items-center gap-2",
1200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1201
+ type: "checkbox",
1202
+ checked: allPages,
1203
+ disabled: measurement.busy,
1204
+ onChange: (e) => setAllPages(e.target.checked)
1205
+ }), t("measurement.allPages")]
1206
+ }),
1207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1208
+ className: "flex items-center gap-2",
1209
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1210
+ type: "checkbox",
1211
+ checked: recalculate,
1212
+ disabled: measurement.busy,
1213
+ onChange: (e) => setRecalculate(e.target.checked)
1214
+ }), t("measurement.recalculate")]
1215
+ }),
1216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.preset"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1217
+ "aria-label": t("measurement.preset"),
1218
+ className: control,
1219
+ value: "",
1220
+ disabled,
1221
+ onChange: (e) => void run(() => measurement.setPreset(pon, e.target.value, options)),
1222
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1223
+ value: "",
1224
+ disabled: true,
1225
+ children: t("measurement.choosePreset")
1226
+ }), measurement.presets().map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1227
+ value: p.id,
1228
+ children: p.label
1229
+ }, p.id))]
1230
+ })] }),
1231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.unit"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1232
+ "aria-label": t("measurement.unit"),
1233
+ className: control,
1234
+ value: rectilinear?.distance[0]?.unit.trim() ?? "",
1235
+ disabled: disabled || !rectilinear,
1236
+ onChange: (e) => void run(() => measurement.setUnit(pon, e.target.value, void 0, options)),
1237
+ children: [!measurement.units().includes(rectilinear?.distance[0]?.unit.trim()) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1238
+ value: rectilinear?.distance[0]?.unit.trim() ?? "",
1239
+ children: t("measurement.custom")
1240
+ }), measurement.units().map((u) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1241
+ value: u,
1242
+ children: u
1243
+ }, u))]
1244
+ })] }),
1245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.areaUnit"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1246
+ "aria-label": t("measurement.areaUnit"),
1247
+ className: control,
1248
+ value: rectilinear?.area[0]?.unit.trim().replace("²", "2") ?? "",
1249
+ disabled: disabled || !rectilinear,
1250
+ onChange: (e) => void run(() => measurement.setAreaUnit(pon, e.target.value, options)),
1251
+ children: [!measurement.areaUnits().includes(rectilinear?.area[0]?.unit.trim().replace("²", "2")) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1252
+ value: rectilinear?.area[0]?.unit.trim().replace("²", "2") ?? "",
1253
+ children: t("measurement.custom")
1254
+ }), measurement.areaUnits().map((unit) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1255
+ value: unit,
1256
+ children: unit.replace("2", "²")
1257
+ }, unit))]
1258
+ })] }),
1259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.precision"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1260
+ "aria-label": t("measurement.precision"),
1261
+ className: control,
1262
+ value: precision,
1263
+ disabled: disabled || !rectilinear,
1264
+ onChange: (e) => void run(() => measurement.setPrecision(pon, Number(e.target.value), options)),
1265
+ children: [![
1266
+ 1,
1267
+ 10,
1268
+ 100,
1269
+ 1e3,
1270
+ 1e4
1271
+ ].includes(precision) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1272
+ value: precision,
1273
+ children: t("measurement.custom")
1274
+ }), [
1275
+ 1,
1276
+ 10,
1277
+ 100,
1278
+ 1e3,
1279
+ 1e4
1280
+ ].map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1281
+ value: p,
1282
+ children: 1 / p
1283
+ }, p))]
1284
+ })] }),
1285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1286
+ className: button,
1287
+ disabled,
1288
+ onClick: () => measurement.startCalibration(),
1289
+ children: t("measurement.calibrate")
1290
+ }),
1291
+ selected.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1292
+ className: "border-border-subtle border-t pt-3",
1293
+ children: [
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1295
+ className: "mb-1 font-medium",
1296
+ children: t("measurement.selection")
1297
+ }),
1298
+ readouts.length ? readouts.map((readout, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1299
+ className: "mb-2",
1300
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
1301
+ t(`measurement.${readout.kind}`),
1302
+ ": ",
1303
+ readout.label
1304
+ ] }), readout.perimeter && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
1305
+ t("measurement.perimeter"),
1306
+ ": ",
1307
+ readout.perimeter
1308
+ ] })]
1309
+ }, i)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1310
+ className: "text-fg-muted",
1311
+ children: t("measurement.unavailable")
1312
+ }),
1313
+ resettable.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1314
+ className: button,
1315
+ disabled: measurement.busy,
1316
+ onClick: () => void run(async () => {
1317
+ for (const annotation of resettable) if (annotation.subtype === "polygon" || annotation.subtype === "polyline") await anno.update(annotation.ref, {
1318
+ subtype: annotation.subtype,
1319
+ caption: { center: null }
1320
+ });
1321
+ }),
1322
+ children: t("measurement.resetLabel")
1323
+ })
1324
+ ]
1325
+ }),
1326
+ reports.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1327
+ role: "status",
1328
+ children: t("measurement.report", { params: {
1329
+ updated: reports.reduce((n, r) => n + r.updated.length, 0),
1330
+ skipped: reports.reduce((n, r) => n + r.skipped.length, 0),
1331
+ failed: reports.reduce((n, r) => n + r.failed.length + (r.scaleError || r.error ? 1 : 0), 0)
1332
+ } })
1333
+ }),
1334
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1335
+ role: "alert",
1336
+ className: "text-red-600",
1337
+ children: error
1338
+ })
1339
+ ]
1340
+ });
1341
+ }
1342
+ function CalibrationDialog() {
1343
+ const t = (0, _embedpdf_react_i18n.useT)();
1344
+ const measurement = (0, _embedpdf_react_measurement.useMeasurement)();
1345
+ const request = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_measurement.MeasurementToken, (c) => c.calibrationRequest());
1346
+ const input = (0, react.useRef)(null);
1347
+ const [value, setValue] = (0, react.useState)("");
1348
+ const [unit, setUnit] = (0, react.useState)("m");
1349
+ const [allPages, setAllPages] = (0, react.useState)(false);
1350
+ const [recalculate, setRecalculate] = (0, react.useState)(true);
1351
+ const [error, setError] = (0, react.useState)(null);
1352
+ (0, react.useEffect)(() => {
1353
+ setValue("");
1354
+ setError(null);
1355
+ input.current?.focus();
1356
+ }, [request]);
1357
+ if (!request) return null;
1358
+ const submit = async () => {
1359
+ setError(null);
1360
+ try {
1361
+ await measurement.calibrate(request.pon, request.from, request.to, {
1362
+ value: Number(value),
1363
+ unit
1364
+ }, {
1365
+ allPages,
1366
+ recalculate
1367
+ });
1368
+ measurement.dismissCalibration();
1369
+ } catch (e) {
1370
+ setError(e instanceof Error ? e.message : String(e));
1371
+ }
1372
+ };
1373
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1374
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
1375
+ onKeyDown: (e) => {
1376
+ if (e.key === "Escape" && !measurement.busy) measurement.dismissCalibration();
1377
+ },
1378
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1379
+ role: "dialog",
1380
+ "aria-modal": "true",
1381
+ "aria-labelledby": "measurement-calibration-title",
1382
+ className: "border-border bg-surface flex w-96 flex-col gap-4 rounded-lg border p-5 shadow-xl",
1383
+ onSubmit: (e) => {
1384
+ e.preventDefault();
1385
+ submit();
1386
+ },
1387
+ children: [
1388
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1389
+ id: "measurement-calibration-title",
1390
+ className: "font-semibold",
1391
+ children: t("measurement.calibrate")
1392
+ }),
1393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1394
+ className: "text-fg-muted text-sm",
1395
+ children: t("measurement.knownLength")
1396
+ }),
1397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.length"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1398
+ ref: input,
1399
+ "aria-label": t("measurement.length"),
1400
+ className: control,
1401
+ type: "number",
1402
+ min: "0",
1403
+ step: "any",
1404
+ value,
1405
+ onChange: (e) => setValue(e.target.value)
1406
+ })] }),
1407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("measurement.unit"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1408
+ className: control,
1409
+ "aria-label": t("measurement.unit"),
1410
+ value: unit,
1411
+ onChange: (e) => setUnit(e.target.value),
1412
+ children: measurement.units().map((u) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { children: u }, u))
1413
+ })] }),
1414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1415
+ className: "flex items-center gap-2 text-sm",
1416
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1417
+ type: "checkbox",
1418
+ checked: allPages,
1419
+ onChange: (e) => setAllPages(e.target.checked)
1420
+ }), t("measurement.allPages")]
1421
+ }),
1422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1423
+ className: "flex items-center gap-2 text-sm",
1424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1425
+ type: "checkbox",
1426
+ checked: recalculate,
1427
+ onChange: (e) => setRecalculate(e.target.checked)
1428
+ }), t("measurement.recalculate")]
1429
+ }),
1430
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1431
+ role: "alert",
1432
+ className: "text-sm text-red-600",
1433
+ children: error
1434
+ }),
1435
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1436
+ className: "flex justify-end gap-2",
1437
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1438
+ type: "button",
1439
+ className: button,
1440
+ disabled: measurement.busy,
1441
+ onClick: () => measurement.dismissCalibration(),
1442
+ children: t("demo.cancel")
1443
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1444
+ className: button,
1445
+ type: "submit",
1446
+ disabled: measurement.busy || !measurement.canCalibratePage || !(Number(value) > 0) || !Number.isFinite(Number(value)),
1447
+ children: measurement.busy ? t("measurement.applying") : t("measurement.apply")
1448
+ })]
1449
+ })
1450
+ ]
1451
+ })
1452
+ });
1453
+ }
1454
+ //#endregion
1455
+ //#region src/config/default-stamps.ts
1456
+ /**
1457
+ * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
1458
+ * standard rubber stamps as one Acrobat-compatible PDF per locale.
1459
+ *
1460
+ * Delivery: the library ships INSIDE whatever ships this viewer. The package
1461
+ * exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
1462
+ * so it travels through the module graph like any other code — every bundler
1463
+ * splits it into a chunk served from the app's own origin, the CDN snippet
1464
+ * carries it as a sibling chunk in its folder, and nothing is fetched from a
1465
+ * third party. `stamps.defaultLibrary` overrides that:
1466
+ * - `false` → no built-in library, no request (air-gapped);
1467
+ * - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
1468
+ *
1469
+ * Loading is LAZY (first open of the stamps panel, never at boot) and
1470
+ * locale-aware: the chrome locale leads, the browser languages break ties
1471
+ * for the locales the chrome does not translate itself, and a locale change
1472
+ * while a default library is loaded swaps it for the new one. The file names
1473
+ * itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
1474
+ */
1475
+ /** The built-in library's PieceInfo id — excluded from persistence (it is
1476
+ * loaded again on every first open, in the locale of that moment). */
1477
+ const DEFAULT_LIBRARY_ID = "embedpdf-standard";
1478
+ /** Which shipped locale to show for the chrome's locale + the browser's. */
1479
+ function resolveStampsLocale(chromeLocale) {
1480
+ return (0, _embedpdf_react_i18n.negotiateLocale)(_embedpdf_default_stamps_library.LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
1481
+ }
1482
+ const fetchBytes = async (url) => {
1483
+ const response = await fetch(url);
1484
+ if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
1485
+ return new Uint8Array(await response.arrayBuffer());
1486
+ };
1487
+ /** The library bytes for `locale`: your URL when you gave one, otherwise the
1488
+ * copy that shipped with the viewer. Never anything else. */
1489
+ const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : (0, _embedpdf_default_stamps_library.loadDefaultLibrary)(locale);
1490
+ /** Per workspace: the locale the default library was last brought to, and
1491
+ * the in-flight work — the panel mounts and unmounts with the sidebar, so
1492
+ * this cannot live in component state. */
1493
+ const loads = /* @__PURE__ */ new WeakMap();
1494
+ /**
1495
+ * Bring the built-in library to `locale`. Idempotent per workspace and
1496
+ * locale; a locale change swaps the loaded default (remove, then import);
1497
+ * a default the user removed in this session stays removed — the locale is
1498
+ * recorded, nothing is re-added. An embedder that seeded a library under
1499
+ * the same id is left alone.
1500
+ */
1501
+ function ensureDefaultLibrary(stamp, locale, source) {
1502
+ if (source === false) return Promise.resolve();
1503
+ const current = loads.get(stamp);
1504
+ if (current?.locale === locale) return current.promise;
1505
+ const run = async () => {
1506
+ await current?.promise.catch(() => void 0);
1507
+ const loaded = stamp.library(DEFAULT_LIBRARY_ID);
1508
+ if (loaded?.locale === locale) return;
1509
+ if (current && !loaded) return;
1510
+ if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
1511
+ await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
1512
+ };
1513
+ const promise = run();
1514
+ loads.set(stamp, {
1515
+ locale,
1516
+ promise
1517
+ });
1518
+ return promise;
1519
+ }
1520
+ //#endregion
1521
+ //#region src/ui/stamp-store.tsx
1522
+ /**
1523
+ * The user's stamp libraries live in the browser (IndexedDB): restored ONCE
1524
+ * per workspace, written on every change, for as long as the viewer lives —
1525
+ * a signature made while the stamps sidebar is closed is persisted exactly
1526
+ * like a stamp imported while it is open. The built-in library is never
1527
+ * stored (it is fetched again next time, in the locale of that moment); the
1528
+ * panels that need it bring it in lazily through `ensureDefaultLibrary`.
1529
+ *
1530
+ * Mounted once in the Shell, workspace-scoped (the stamp plugin is).
1531
+ */
1532
+ const store = typeof indexedDB === "undefined" ? null : (0, _embedpdf_react_stamp.indexedDbByteStore)("embedpdf-stamps", { storeName: "libraries" });
1533
+ const restored = /* @__PURE__ */ new WeakMap();
1534
+ /** Bring the stored libraries back — once per workspace, whoever asks first. */
1535
+ const restoreStampLibrariesOnce = (stamp) => {
1536
+ let pending = restored.get(stamp);
1537
+ if (!pending) {
1538
+ pending = store ? (0, _embedpdf_react_stamp.restoreStampLibraries)(stamp, store) : Promise.resolve();
1539
+ restored.set(stamp, pending);
1540
+ }
1541
+ return pending;
1542
+ };
1543
+ function StampLibraryStore() {
1544
+ const stamp = (0, _embedpdf_react_stamp.useStamp)();
1545
+ (0, react.useEffect)(() => {
1546
+ restoreStampLibrariesOnce(stamp);
1547
+ return store ? (0, _embedpdf_react_stamp.persistStampLibraries)(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0;
1548
+ }, [stamp]);
1549
+ return null;
1550
+ }
1551
+ //#endregion
1552
+ //#region src/ui/quick-stamps.tsx
1553
+ /**
1554
+ * Quick marks in the toolbar: a row of stamp thumbnails that ARM on click —
1555
+ * the checkmark, the cross, whatever the embedder configured
1556
+ * (`stamps.toolbar`: asset ids, or one library's assets). The hover ghost and
1557
+ * the click placement are the annotation plugin's, exactly as from the
1558
+ * sidebar; only the picker moved. Renders nothing until the assets exist.
1559
+ * Lives in the workspace toolbar, so it reads the ACTIVE document's tool.
1560
+ */
1561
+ function QuickStamps() {
1562
+ const t = (0, _embedpdf_react_i18n.useT)();
1563
+ const stamp = (0, _embedpdf_react_stamp.useStamp)();
1564
+ const config = useStampsConfig();
1565
+ const { locale } = (0, _embedpdf_react_i18n.useLocale)();
1566
+ const assets = (0, _embedpdf_react_stamp.useStampAssets)();
1567
+ const documentId = (0, _embedpdf_react_runtime.useDocumentId)();
1568
+ const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
1569
+ const activeToolId = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_interaction.InteractionToken, (c) => c.activeToolId(), null);
1570
+ const armedId = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_stamp.StampToken, (c) => documentId ? c.armedAsset(documentId)?.id ?? null : null, null);
1571
+ const ids = Array.isArray(config.toolbar) ? config.toolbar : config.toolbar && "library" in config.toolbar ? stamp.library(config.toolbar.library)?.assetIds ?? [] : [];
1572
+ const wantsDefault = ids.some((id) => id.startsWith(`${DEFAULT_LIBRARY_ID}:`));
1573
+ (0, react.useEffect)(() => {
1574
+ if (!wantsDefault) return;
1575
+ restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), config.defaultLibrary)).catch((err) => console.error("[embedpdf] quick stamps failed:", err));
1576
+ }, [
1577
+ stamp,
1578
+ locale,
1579
+ wantsDefault
1580
+ ]);
1581
+ const shown = ids.map((id) => assets.find((a) => a.id === id)).filter((a) => !!a);
1582
+ if (shown.length === 0 || !documentId) return null;
1583
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1584
+ className: "flex items-center gap-1",
1585
+ role: "group",
1586
+ "aria-label": t("demo.quickStampsLabel"),
1587
+ children: shown.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStampButton, {
1588
+ asset,
1589
+ active: activeToolId === "stamp" && armedId === asset.id,
1590
+ onArm: () => void armAsset(asset.id).catch((e) => console.error("[embedpdf] arm failed", e))
1591
+ }, asset.id))
1592
+ });
1593
+ }
1594
+ function QuickStampButton({ asset, active, onArm }) {
1595
+ const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
1596
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1597
+ type: "button",
1598
+ onClick: onArm,
1599
+ title: asset.label,
1600
+ "aria-label": asset.label,
1601
+ "aria-pressed": active || void 0,
1602
+ className: buttonClass(active, true),
1603
+ children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1604
+ src: url,
1605
+ alt: "",
1606
+ className: "h-5 w-5 object-contain",
1607
+ draggable: false
1608
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1609
+ className: "text-xs",
1610
+ children: asset.label
1611
+ })
1612
+ });
1613
+ }
1614
+ //#endregion
972
1615
  //#region src/ui/menu.tsx
973
1616
  /**
974
1617
  * Menu rendering — a MenuSchema (a tree of command ids) rendered as rows.
@@ -1377,7 +2020,11 @@ function AppToolbar({ bar, className }) {
1377
2020
  variant,
1378
2021
  run
1379
2022
  }),
1380
- renderCustom: { "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomControls, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomButton, {}) },
2023
+ renderCustom: {
2024
+ "measurement-scale": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MeasurementScaleButton, {}),
2025
+ "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomControls, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomButton, {}),
2026
+ "quick-stamps": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStamps, {})
2027
+ },
1381
2028
  renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedModes, { view }) : void 0,
1382
2029
  renderGroupTrigger: (view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupTrigger, { view }),
1383
2030
  renderSeparator: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "bg-border h-6 w-px self-center" }),
@@ -1389,6 +2036,71 @@ function AppToolbar({ bar, className }) {
1389
2036
  });
1390
2037
  }
1391
2038
  //#endregion
2039
+ //#region src/ui/annotation-fonts.tsx
2040
+ /**
2041
+ * The configured annotation fonts (`annotations.fonts`), loaded once per
2042
+ * viewer: fetch the bytes → register them on the viewer's engine under the
2043
+ * key (what a free-text `fontFamily` carries and what the engine embeds) →
2044
+ * mount the same bytes as a `@font-face` named by that key (what the plugin's
2045
+ * `cssFontFamily` emits for a registered font, so the live editor renders the
2046
+ * face the appearance stream will bake). The style panel lists a font only
2047
+ * once BOTH have happened — an offered key always resolves on write.
2048
+ */
2049
+ const LoadedFontsContext = (0, react.createContext)([]);
2050
+ function AnnotationFontsProvider({ children }) {
2051
+ const kernel = (0, _embedpdf_react_runtime.useKernel)();
2052
+ const { fonts } = useAnnotationsConfig();
2053
+ const [loaded, setLoaded] = (0, react.useState)([]);
2054
+ (0, react.useEffect)(() => {
2055
+ const engineFonts = kernel.engine.fonts;
2056
+ if (!fonts?.length || !engineFonts) return;
2057
+ let cancelled = false;
2058
+ const unmounts = [];
2059
+ (async () => {
2060
+ for (const font of fonts) try {
2061
+ const response = await fetch(font.url);
2062
+ if (!response.ok) throw new Error(`${font.url}: HTTP ${response.status}`);
2063
+ const bytes = new Uint8Array(await response.arrayBuffer());
2064
+ if (cancelled) return;
2065
+ await engineFonts.register({
2066
+ key: font.key,
2067
+ familyName: font.familyName,
2068
+ weight: font.weight,
2069
+ italic: font.italic,
2070
+ data: bytes
2071
+ });
2072
+ const unmount = await (0, _embedpdf_web.mountWebFont)(font.key, bytes, {
2073
+ weight: font.weight,
2074
+ style: font.italic ? "italic" : "normal"
2075
+ });
2076
+ if (cancelled) {
2077
+ unmount();
2078
+ return;
2079
+ }
2080
+ unmounts.push(unmount);
2081
+ setLoaded((prev) => prev.some((f) => f.key === font.key) ? prev : [...prev, {
2082
+ key: font.key,
2083
+ label: font.label
2084
+ }]);
2085
+ } catch (error) {
2086
+ console.warn(`[embedpdf] annotation font "${font.key}" was not loaded:`, error);
2087
+ }
2088
+ })();
2089
+ return () => {
2090
+ cancelled = true;
2091
+ for (const unmount of unmounts) unmount();
2092
+ };
2093
+ }, [kernel, fonts]);
2094
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadedFontsContext.Provider, {
2095
+ value: loaded,
2096
+ children
2097
+ });
2098
+ }
2099
+ /** The configured fonts that are registered AND mounted, in config order. */
2100
+ function useAnnotationFonts() {
2101
+ return (0, react.useContext)(LoadedFontsContext);
2102
+ }
2103
+ //#endregion
1392
2104
  //#region src/ui/link-editor.tsx
1393
2105
  /**
1394
2106
  * The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
@@ -1756,7 +2468,13 @@ function iconGlyph(name, accent) {
1756
2468
  const fill = spec.fill === true ? INK : spec.fill ? accent?.[spec.fill] : void 0;
1757
2469
  const stroke = spec.stroke ? accent?.[spec.stroke] ?? INK : spec.fill ? "none" : INK;
1758
2470
  halo.push(`<path d="${spec.d}" fill="none" stroke="${HALO}" stroke-width="3.5"/>`);
1759
- draw.push(`<path d="${spec.d}" fill="${fill ?? "none"}" stroke="${stroke}"/>`);
2471
+ const decoration = [
2472
+ ["stroke-width", spec.strokeWidth],
2473
+ ["stroke-dasharray", spec.strokeDasharray],
2474
+ ["stroke-opacity", spec.strokeOpacity],
2475
+ ["fill-opacity", spec.fillOpacity]
2476
+ ].filter(([, value]) => value !== void 0).map(([key, value]) => `${key}="${value}"`).join(" ");
2477
+ draw.push(`<path d="${spec.d}" fill="${fill ?? "none"}" stroke="${stroke}" ${decoration}/>`);
1760
2478
  }
1761
2479
  return halo.join("") + draw.join("");
1762
2480
  }
@@ -2374,12 +3092,17 @@ function LineEndingSelect({ side, value, onChange }) {
2374
3092
  });
2375
3093
  }
2376
3094
  function FontFamilySelect({ value, onChange }) {
3095
+ const configured = useAnnotationFonts();
3096
+ const options = [...FONT_OPTIONS, ...configured.map((f) => ({
3097
+ v: f.key,
3098
+ label: f.label
3099
+ }))];
2377
3100
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownShell, {
2378
3101
  trigger: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2379
3102
  className: "text-fg text-sm",
2380
- children: FONT_OPTIONS.find((o) => o.v === value)?.label ?? value
3103
+ children: options.find((o) => o.v === value)?.label ?? value
2381
3104
  }),
2382
- children: (close) => FONT_OPTIONS.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
3105
+ children: (close) => options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
2383
3106
  selected: o.v === value,
2384
3107
  onClick: () => {
2385
3108
  onChange(o.v);
@@ -2391,17 +3114,33 @@ function FontFamilySelect({ value, onChange }) {
2391
3114
  }
2392
3115
  function FontSizeCombo({ value, onChange }) {
2393
3116
  const [open, setOpen] = (0, react.useState)(false);
3117
+ const rootRef = useOutsideClose(open, () => setOpen(false));
3118
+ const [draft, setDraft] = (0, react.useState)(null);
3119
+ const commit = () => {
3120
+ if (draft === null) return;
3121
+ const n = parseInt(draft, 10);
3122
+ setDraft(null);
3123
+ if (Number.isFinite(n) && n > 0 && n !== value) onChange(n);
3124
+ };
2394
3125
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2395
- ref: useOutsideClose(open, () => setOpen(false)),
3126
+ ref: rootRef,
2396
3127
  className: "relative w-full",
2397
3128
  children: [
2398
3129
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2399
3130
  type: "number",
2400
3131
  min: 1,
2401
- value,
2402
- onChange: (e) => {
2403
- const n = parseInt(e.target.value, 10);
2404
- if (Number.isFinite(n) && n > 0) onChange(n);
3132
+ value: draft ?? value,
3133
+ onChange: (e) => setDraft(e.target.value),
3134
+ onBlur: commit,
3135
+ onKeyDown: (e) => {
3136
+ if (e.key === "Enter") {
3137
+ e.preventDefault();
3138
+ commit();
3139
+ setOpen(false);
3140
+ } else if (e.key === "Escape") {
3141
+ setDraft(null);
3142
+ setOpen(false);
3143
+ }
2405
3144
  },
2406
3145
  onClick: () => setOpen(true),
2407
3146
  className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 pr-7 text-sm"
@@ -2422,6 +3161,7 @@ function FontSizeCombo({ value, onChange }) {
2422
3161
  children: FONT_SIZES.map((sz) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
2423
3162
  selected: sz === value,
2424
3163
  onClick: () => {
3164
+ setDraft(null);
2425
3165
  onChange(sz);
2426
3166
  setOpen(false);
2427
3167
  },
@@ -2442,15 +3182,45 @@ function BlendModeSelect({ value, onChange }) {
2442
3182
  }, mode.v))
2443
3183
  });
2444
3184
  }
2445
- function Toggle({ active, title, onClick, children }) {
3185
+ function Toggle({ active, title, onClick, keepFocus, children }) {
2446
3186
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2447
3187
  type: "button",
2448
3188
  title,
2449
3189
  onClick,
3190
+ onMouseDown: keepFocus ? (e) => e.preventDefault() : void 0,
2450
3191
  className: `flex h-9 w-9 items-center justify-center rounded border transition-colors ${active ? "border-accent bg-accent text-on-accent" : "border-border bg-surface text-fg hover:bg-hover"}`,
2451
3192
  children
2452
3193
  });
2453
3194
  }
3195
+ const isFormatSpec = (spec) => spec.key === "bold" || spec.key === "italic" || spec.key === "underline";
3196
+ /**
3197
+ * The format toggles a free-text kind declares, in one row. While the text
3198
+ * editor holds a range they read and write that range's runs (the plugin
3199
+ * routes `updateSelection`); otherwise the annotation's body. The buttons
3200
+ * keep focus in the editor so the range survives the click.
3201
+ */
3202
+ function FormatToggles({ specs, values, mixed, onChange }) {
3203
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3204
+ label: (0, _embedpdf_react_i18n.useT)()("demo.formatLabel", { fallback: "Format" }),
3205
+ mixed: specs.some((s) => mixed.includes(s.key)),
3206
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3207
+ className: "flex gap-2",
3208
+ children: specs.map((spec) => {
3209
+ const active = values[spec.key] === true && !mixed.includes(spec.key);
3210
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
3211
+ title: spec.label,
3212
+ active,
3213
+ keepFocus: true,
3214
+ onClick: () => onChange({ [spec.key]: !active }),
3215
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3216
+ name: spec.key,
3217
+ size: 18
3218
+ })
3219
+ }, spec.key);
3220
+ })
3221
+ })
3222
+ });
3223
+ }
2454
3224
  function PropControl({ spec, value, mixed, onChange }) {
2455
3225
  switch (spec.key) {
2456
3226
  case "color":
@@ -2585,6 +3355,9 @@ function PropControl({ spec, value, mixed, onChange }) {
2585
3355
  })
2586
3356
  });
2587
3357
  }
3358
+ case "bold":
3359
+ case "italic":
3360
+ case "underline": return null;
2588
3361
  case "blendMode": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2589
3362
  label: spec.label,
2590
3363
  mixed,
@@ -2632,7 +3405,12 @@ function AnnotationStylePanel() {
2632
3405
  className: "text-fg-muted mb-4 text-[11px] font-semibold uppercase tracking-wide",
2633
3406
  children: hasSel ? `${selected.length} selected` : `${activeToolId} defaults`
2634
3407
  }),
2635
- specs.map((spec) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PropControl, {
3408
+ specs.map((spec) => spec.key === "bold" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormatToggles, {
3409
+ specs: specs.filter((s) => isFormatSpec(s)),
3410
+ values,
3411
+ mixed: hasSel ? sel.mixed : [],
3412
+ onChange: write
3413
+ }, "format") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PropControl, {
2636
3414
  spec,
2637
3415
  value: values[spec.key],
2638
3416
  mixed: hasSel && sel.mixed.includes(spec.key),
@@ -3503,77 +4281,11 @@ function SearchPanel() {
3503
4281
  });
3504
4282
  }
3505
4283
  //#endregion
3506
- //#region src/config/default-stamps.ts
4284
+ //#region src/ui/stamps-panel.tsx
3507
4285
  /**
3508
- * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
3509
- * standard rubber stamps as one Acrobat-compatible PDF per locale.
3510
- *
3511
- * Delivery: the library ships INSIDE whatever ships this viewer. The package
3512
- * exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
3513
- * so it travels through the module graph like any other code — every bundler
3514
- * splits it into a chunk served from the app's own origin, the CDN snippet
3515
- * carries it as a sibling chunk in its folder, and nothing is fetched from a
3516
- * third party. `stamps.defaultLibrary` overrides that:
3517
- * - `false` → no built-in library, no request (air-gapped);
3518
- * - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
3519
- *
3520
- * Loading is LAZY (first open of the stamps panel, never at boot) and
3521
- * locale-aware: the chrome locale leads, the browser languages break ties
3522
- * for the locales the chrome does not translate itself, and a locale change
3523
- * while a default library is loaded swaps it for the new one. The file names
3524
- * itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
3525
- */
3526
- /** The built-in library's PieceInfo id — excluded from persistence (it is
3527
- * loaded again on every first open, in the locale of that moment). */
3528
- const DEFAULT_LIBRARY_ID = "embedpdf-standard";
3529
- /** Which shipped locale to show for the chrome's locale + the browser's. */
3530
- function resolveStampsLocale(chromeLocale) {
3531
- return (0, _embedpdf_react_i18n.negotiateLocale)(_embedpdf_default_stamps_library.LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
3532
- }
3533
- const fetchBytes = async (url) => {
3534
- const response = await fetch(url);
3535
- if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
3536
- return new Uint8Array(await response.arrayBuffer());
3537
- };
3538
- /** The library bytes for `locale`: your URL when you gave one, otherwise the
3539
- * copy that shipped with the viewer. Never anything else. */
3540
- const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : (0, _embedpdf_default_stamps_library.loadDefaultLibrary)(locale);
3541
- /** Per workspace: the locale the default library was last brought to, and
3542
- * the in-flight work — the panel mounts and unmounts with the sidebar, so
3543
- * this cannot live in component state. */
3544
- const loads = /* @__PURE__ */ new WeakMap();
3545
- /**
3546
- * Bring the built-in library to `locale`. Idempotent per workspace and
3547
- * locale; a locale change swaps the loaded default (remove, then import);
3548
- * a default the user removed in this session stays removed — the locale is
3549
- * recorded, nothing is re-added. An embedder that seeded a library under
3550
- * the same id is left alone.
3551
- */
3552
- function ensureDefaultLibrary(stamp, locale, source) {
3553
- if (source === false) return Promise.resolve();
3554
- const current = loads.get(stamp);
3555
- if (current?.locale === locale) return current.promise;
3556
- const run = async () => {
3557
- await current?.promise.catch(() => void 0);
3558
- const loaded = stamp.library(DEFAULT_LIBRARY_ID);
3559
- if (loaded?.locale === locale) return;
3560
- if (current && !loaded) return;
3561
- if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
3562
- await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
3563
- };
3564
- const promise = run();
3565
- loads.set(stamp, {
3566
- locale,
3567
- promise
3568
- });
3569
- return promise;
3570
- }
3571
- //#endregion
3572
- //#region src/ui/stamps-panel.tsx
3573
- /**
3574
- * The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
3575
- * The stamp plugin owns the libraries, the asset bytes and the cached
3576
- * previews; this panel is the PICKER over them:
4286
+ * The stamps sidebar (right panel) — v2's rubber-stamp sidebar on v3 parts.
4287
+ * The stamp plugin owns the libraries, the asset bytes and the cached
4288
+ * previews; this panel is the PICKER over them:
3577
4289
  *
3578
4290
  * a library <select> ("All stamps" + one per library, shown once there
3579
4291
  * are two) → a two-column grid of thumbnails → click a stamp → `armAsset`
@@ -3591,30 +4303,15 @@ function ensureDefaultLibrary(stamp, locale, source) {
3591
4303
  * through the surface's open props.
3592
4304
  */
3593
4305
  const ALL = "all";
3594
- /** The user's libraries live in the browser (IndexedDB): restored on first
3595
- * open, written on every change. The built-in set is never stored — it is
3596
- * fetched again next time, in the locale of that moment. */
3597
- const store = typeof indexedDB === "undefined" ? null : (0, _embedpdf_react_stamp.indexedDbByteStore)("embedpdf-stamps", { storeName: "libraries" });
3598
- /** Restore runs ONCE per workspace — the panel mounts and unmounts with the
3599
- * sidebar, so the guard cannot live in component state. */
3600
- const restored = /* @__PURE__ */ new WeakMap();
3601
- const restoreOnce = (stamp) => {
3602
- let pending = restored.get(stamp);
3603
- if (!pending) {
3604
- pending = store ? (0, _embedpdf_react_stamp.restoreStampLibraries)(stamp, store) : Promise.resolve();
3605
- restored.set(stamp, pending);
3606
- }
3607
- return pending;
3608
- };
3609
4306
  function StampsPanel() {
3610
4307
  const t = (0, _embedpdf_react_i18n.useT)();
3611
4308
  const stamp = (0, _embedpdf_react_stamp.useStamp)();
3612
- const libraries = (0, _embedpdf_react_stamp.useStampLibraries)();
4309
+ const { defaultLibrary, sidebar } = useStampsConfig();
4310
+ const libraries = (0, _embedpdf_react_stamp.useStampLibraries)({ kind: sidebar ?? ["stamps"] });
3613
4311
  const assets = (0, _embedpdf_react_stamp.useStampAssets)();
3614
4312
  const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
3615
4313
  const { activeToolId } = (0, _embedpdf_react_interaction.useTool)();
3616
4314
  const { locale } = (0, _embedpdf_react_i18n.useLocale)();
3617
- const { defaultLibrary } = useStampsConfig();
3618
4315
  const surface = (0, _embedpdf_react_shell.useSurface)("stamps");
3619
4316
  const fileRef = (0, react.useRef)(null);
3620
4317
  const [armedId, setArmedId] = (0, react.useState)(null);
@@ -3628,7 +4325,7 @@ function StampsPanel() {
3628
4325
  (0, react.useEffect)(() => {
3629
4326
  let live = true;
3630
4327
  setBusy("loading");
3631
- restoreOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
4328
+ restoreStampLibrariesOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
3632
4329
  console.error("[embedpdf] default stamps failed:", err);
3633
4330
  if (live) setError(t("demo.stampsError"));
3634
4331
  }).finally(() => live && setBusy(null));
@@ -3636,7 +4333,6 @@ function StampsPanel() {
3636
4333
  live = false;
3637
4334
  };
3638
4335
  }, [stamp, locale]);
3639
- (0, react.useEffect)(() => store ? (0, _embedpdf_react_stamp.persistStampLibraries)(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0, [stamp]);
3640
4336
  (0, react.useEffect)(() => {
3641
4337
  if (activeToolId !== "stamp") setArmedId(null);
3642
4338
  }, [activeToolId]);
@@ -3807,6 +4503,293 @@ function StampAssetCell({ asset, armed, onArm }) {
3807
4503
  });
3808
4504
  }
3809
4505
  //#endregion
4506
+ //#region src/ui/signatures-panel.tsx
4507
+ /**
4508
+ * The signatures sidebar (right panel) — Preview's signature sheet on v3
4509
+ * parts. One row per PERSON: a stamp library of kind `signatures` holding a
4510
+ * full signature and, optionally, initials. Nothing is stored beyond the
4511
+ * file; rename/export/delete are the library verbs.
4512
+ *
4513
+ * pick a mark → with a TARGET field ("sign here" was clicked, or a
4514
+ * signature widget selected) the mark goes into that field by the
4515
+ * plugin's mode; otherwise it ARMS — hover a page for the ghost, click a
4516
+ * signature field to sign it, click anywhere else to drop it as a stamp.
4517
+ *
4518
+ * Below the people: the signature fields of THIS document, signed or not,
4519
+ * with their verdicts — click an unsigned one to target it, a signed one to
4520
+ * inspect it.
4521
+ */
4522
+ const fieldLabel = (field) => field.kind === "fqn" ? field.name : `#${field.fieldObjectNumber}`;
4523
+ function SignaturesPanel() {
4524
+ const t = (0, _embedpdf_react_i18n.useT)();
4525
+ const stamp = (0, _embedpdf_react_stamp.useStamp)();
4526
+ const signature = (0, _embedpdf_react_signature.useSignature)();
4527
+ const config = useSignaturesConfig();
4528
+ const rows = (0, _embedpdf_react_signature.useSignerRows)();
4529
+ const { target, busy } = (0, _embedpdf_react_signature.useSignatureTarget)();
4530
+ const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
4531
+ const { activeToolId } = (0, _embedpdf_react_interaction.useTool)();
4532
+ const maker = (0, _embedpdf_react_shell.useSurface)("signature-maker");
4533
+ const [armedId, setArmedId] = (0, react.useState)(null);
4534
+ const [error, setError] = (0, react.useState)(null);
4535
+ const [invalidating, setInvalidating] = (0, react.useState)(null);
4536
+ (0, _embedpdf_react_signature.useSignatureEvent)((event) => {
4537
+ if (event.type === "invalidating") setInvalidating(signature.signatureOf(event.field)?.fieldName ?? fieldLabel(event.field));
4538
+ else if (event.type === "validated" && !event.verdicts.some((v) => v.summary === "invalid" && v.modifications.basis === "working-copy")) setInvalidating(null);
4539
+ });
4540
+ (0, react.useEffect)(() => {
4541
+ restoreStampLibrariesOnce(stamp);
4542
+ }, [stamp]);
4543
+ (0, react.useEffect)(() => {
4544
+ if (activeToolId !== "stamp") setArmedId(null);
4545
+ }, [activeToolId]);
4546
+ const pick = (asset) => {
4547
+ setError(null);
4548
+ if (target) {
4549
+ signature.placeMark({ assetId: asset.id }, { field: target }).catch((err) => {
4550
+ console.error("[embedpdf] placing the mark failed:", err);
4551
+ setError(t("demo.signError"));
4552
+ });
4553
+ return;
4554
+ }
4555
+ setArmedId(asset.id);
4556
+ armAsset(asset.id).catch((err) => {
4557
+ console.error("[embedpdf] arm mark failed:", err);
4558
+ setArmedId(null);
4559
+ setError(t("demo.stampsArmError"));
4560
+ });
4561
+ };
4562
+ const canAddPerson = (config.libraries ?? "many") === "many" || rows.length === 0;
4563
+ const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
4564
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4565
+ className: "flex min-h-0 flex-1 flex-col",
4566
+ children: [
4567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4568
+ className: "min-h-0 flex-1 overflow-y-auto p-3",
4569
+ children: [rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4570
+ className: "flex flex-col items-center gap-3 px-4 py-8 text-center",
4571
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4572
+ name: "signature",
4573
+ size: 32,
4574
+ className: "text-fg-muted"
4575
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4576
+ className: "text-fg-muted text-sm",
4577
+ children: t("demo.signaturesEmpty")
4578
+ })]
4579
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4580
+ className: "flex flex-col gap-3",
4581
+ children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignerRowView, {
4582
+ row,
4583
+ armedId,
4584
+ wantsInitials,
4585
+ onPick: pick,
4586
+ onAddInitials: () => maker.open({
4587
+ exclusive: "modal",
4588
+ props: { libraryId: row.libraryId }
4589
+ })
4590
+ }, row.libraryId))
4591
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DocumentSignatures, {})]
4592
+ }),
4593
+ invalidating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4594
+ className: "border-border-subtle shrink-0 border-t px-3 py-2 text-xs text-amber-700",
4595
+ children: t("demo.signaturesInvalidating", { params: { field: invalidating } })
4596
+ }),
4597
+ (error || target || armedId) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4598
+ className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
4599
+ children: error ?? (target ? busy ? t("demo.signSigning") : t("demo.signaturesTargetHint") : t("demo.signaturesArmedHint"))
4600
+ }),
4601
+ canAddPerson ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4602
+ className: "border-border-subtle shrink-0 border-t p-3",
4603
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4604
+ type: "button",
4605
+ onClick: () => maker.open({ exclusive: "modal" }),
4606
+ 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",
4607
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4608
+ name: "plus",
4609
+ size: 16
4610
+ }), t("demo.signaturesNew")]
4611
+ })
4612
+ }) : null
4613
+ ]
4614
+ });
4615
+ }
4616
+ function SignerRowView({ row, armedId, wantsInitials, onPick, onAddInitials }) {
4617
+ const t = (0, _embedpdf_react_i18n.useT)();
4618
+ const stamp = (0, _embedpdf_react_stamp.useStamp)();
4619
+ const [renaming, setRenaming] = (0, react.useState)(null);
4620
+ const exportPdf = () => {
4621
+ const bytes = stamp.exportLibrary(row.libraryId);
4622
+ if (!bytes) return;
4623
+ const blob = new Blob([bytes], { type: "application/pdf" });
4624
+ const url = URL.createObjectURL(blob);
4625
+ const a = document.createElement("a");
4626
+ a.href = url;
4627
+ a.download = `${row.name || "signature"}.pdf`;
4628
+ a.click();
4629
+ URL.revokeObjectURL(url);
4630
+ };
4631
+ const commitRename = () => {
4632
+ const name = renaming?.trim();
4633
+ setRenaming(null);
4634
+ if (name && name !== row.name) stamp.updateLibrary(row.libraryId, { name });
4635
+ };
4636
+ const iconButton = "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded";
4637
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
4638
+ className: "border-border-subtle rounded-md border p-2",
4639
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4640
+ className: "flex items-center gap-1",
4641
+ children: [
4642
+ renaming !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4643
+ autoFocus: true,
4644
+ value: renaming,
4645
+ onChange: (e) => setRenaming(e.target.value),
4646
+ onBlur: commitRename,
4647
+ onKeyDown: (e) => {
4648
+ if (e.key === "Enter") commitRename();
4649
+ if (e.key === "Escape") setRenaming(null);
4650
+ },
4651
+ className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-1.5 py-0.5 text-sm"
4652
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4653
+ className: "text-fg min-w-0 flex-1 truncate text-sm font-medium",
4654
+ children: row.name
4655
+ }),
4656
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4657
+ type: "button",
4658
+ onClick: () => setRenaming(row.name),
4659
+ title: t("demo.signaturesRename"),
4660
+ className: iconButton,
4661
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4662
+ name: "pencilMarker",
4663
+ size: 13
4664
+ })
4665
+ }),
4666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4667
+ type: "button",
4668
+ onClick: exportPdf,
4669
+ title: t("demo.signaturesExport"),
4670
+ className: iconButton,
4671
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4672
+ name: "download",
4673
+ size: 13
4674
+ })
4675
+ }),
4676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4677
+ type: "button",
4678
+ onClick: () => void stamp.removeLibrary(row.libraryId),
4679
+ title: t("demo.signaturesRemove"),
4680
+ className: iconButton,
4681
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4682
+ name: "trash",
4683
+ size: 13
4684
+ })
4685
+ })
4686
+ ]
4687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4688
+ className: "mt-2 grid grid-cols-[2fr_1fr] gap-2",
4689
+ children: [row.signatures.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkThumb, {
4690
+ asset,
4691
+ label: t("demo.signatureLabel"),
4692
+ armed: armedId === asset.id,
4693
+ onPick: () => onPick(asset)
4694
+ }, asset.id)), wantsInitials ? row.initials ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkThumb, {
4695
+ asset: row.initials,
4696
+ label: t("demo.initialsLabel"),
4697
+ armed: armedId === row.initials.id,
4698
+ onPick: () => onPick(row.initials)
4699
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4700
+ type: "button",
4701
+ onClick: onAddInitials,
4702
+ title: t("demo.signaturesAddInitials"),
4703
+ className: "border-border-subtle text-fg-muted hover:bg-hover flex h-16 items-center justify-center rounded-md border border-dashed text-xs",
4704
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4705
+ name: "plus",
4706
+ size: 14
4707
+ })
4708
+ }) : null]
4709
+ })]
4710
+ });
4711
+ }
4712
+ function MarkThumb({ asset, label, armed, onPick }) {
4713
+ const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
4714
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4715
+ type: "button",
4716
+ onClick: onPick,
4717
+ title: label,
4718
+ "aria-pressed": armed,
4719
+ 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"}`,
4720
+ children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
4721
+ src: url,
4722
+ alt: label,
4723
+ className: "max-h-full max-w-full object-contain"
4724
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4725
+ className: "text-fg-muted text-xs",
4726
+ children: label
4727
+ })
4728
+ });
4729
+ }
4730
+ /** The signature fields of this document, with their state. */
4731
+ function DocumentSignatures() {
4732
+ const t = (0, _embedpdf_react_i18n.useT)();
4733
+ const signature = (0, _embedpdf_react_signature.useSignature)();
4734
+ const snapshot = (0, _embedpdf_react_signature.useSignatureSnapshot)();
4735
+ const verdicts = (0, _embedpdf_react_signature.useSignatureVerdicts)();
4736
+ const { target } = (0, _embedpdf_react_signature.useSignatureTarget)();
4737
+ const [validating, setValidating] = (0, react.useState)(false);
4738
+ const fields = snapshot?.signatures ?? [];
4739
+ const verdictOf = (dto) => verdicts?.find((v) => v.signature.index === dto.index) ?? null;
4740
+ 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");
4741
+ 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";
4742
+ const isTarget = (dto) => !!target && (target.kind === "fqn" ? target.name === dto.fieldName : target.fieldObjectNumber === (dto.field.kind === "objectNumber" ? dto.field.fieldObjectNumber : -1));
4743
+ const validate = () => {
4744
+ setValidating(true);
4745
+ signature.validate().finally(() => setValidating(false));
4746
+ };
4747
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4748
+ className: "mt-4",
4749
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4750
+ className: "flex items-center justify-between",
4751
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4752
+ className: "text-fg-muted text-xs font-semibold uppercase tracking-wide",
4753
+ children: t("demo.signaturesInDocument")
4754
+ }), fields.some((f) => f.signed) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4755
+ type: "button",
4756
+ disabled: validating,
4757
+ onClick: validate,
4758
+ className: "text-accent text-xs hover:underline disabled:opacity-60",
4759
+ children: validating ? t("demo.signaturesValidating") : t("demo.signaturesValidate")
4760
+ }) : null]
4761
+ }), fields.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4762
+ className: "text-fg-muted mt-1 text-xs",
4763
+ children: t("demo.signaturesNone")
4764
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4765
+ className: "mt-1 flex flex-col gap-1",
4766
+ children: fields.map((dto) => {
4767
+ const verdict = verdictOf(dto);
4768
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4769
+ type: "button",
4770
+ onClick: () => dto.signed ? signature.inspect(dto.field) : signature.setTarget(dto.field),
4771
+ 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" : ""}`,
4772
+ children: [
4773
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4774
+ name: dto.signed ? "check" : "signature",
4775
+ size: 14,
4776
+ className: dto.signed ? "text-green-600" : "text-fg-muted"
4777
+ }),
4778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4779
+ className: "text-fg min-w-0 flex-1 truncate",
4780
+ children: dto.fieldName
4781
+ }),
4782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4783
+ className: `shrink-0 text-right text-xs ${dto.signed ? verdictTone(verdict) : "text-fg-muted"}`,
4784
+ children: dto.signed ? verdictLabel(verdict) : t("demo.signaturesUnsigned")
4785
+ })
4786
+ ]
4787
+ }) }, dto.index);
4788
+ })
4789
+ })]
4790
+ });
4791
+ }
4792
+ //#endregion
3810
4793
  //#region src/ui/panels.tsx
3811
4794
  /**
3812
4795
  * The static chrome around the toolbars: the left/right sidebars (shell
@@ -3900,11 +4883,13 @@ function RightSidebar() {
3900
4883
  const comment = (0, _embedpdf_react_shell.useSurface)("comment");
3901
4884
  const style = (0, _embedpdf_react_shell.useSurface)("annotation-style");
3902
4885
  const redaction = (0, _embedpdf_react_shell.useSurface)("redaction");
4886
+ const measurement = (0, _embedpdf_react_shell.useSurface)("measurement");
3903
4887
  const stamps = (0, _embedpdf_react_shell.useSurface)("stamps");
3904
- const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : null;
4888
+ const signatures = (0, _embedpdf_react_shell.useSurface)("signatures");
4889
+ const active = measurement.isOpen ? "measurement" : search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : stamps.isOpen ? "stamps" : signatures.isOpen ? "signatures" : null;
3905
4890
  if (!active) return null;
3906
- const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : active === "stamps" ? "demo.stampsTitle" : "demo.styleTitle";
3907
- const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : active === "stamps" ? stamps.close : style.close;
4891
+ 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";
4892
+ 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;
3908
4893
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3909
4894
  className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
3910
4895
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3921,7 +4906,7 @@ function RightSidebar() {
3921
4906
  size: 16
3922
4907
  })
3923
4908
  })]
3924
- }), active === "style" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampsPanel, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsPanel, {}, documentId ?? "none")]
4909
+ }), active === "measurement" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MeasurementSection, {}, documentId ?? "none") : active === "style" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactionPanel, {}) : active === "stamps" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampsPanel, {}) : active === "signatures" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignaturesPanel, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsPanel, {}, documentId ?? "none")]
3925
4910
  });
3926
4911
  }
3927
4912
  function PageControls() {
@@ -4012,6 +4997,684 @@ function RedactConfirmModal() {
4012
4997
  });
4013
4998
  }
4014
4999
  //#endregion
5000
+ //#region src/ui/sign-dialog.tsx
5001
+ /**
5002
+ * The sign dialog (`signature-sign` modal surface): a mark met a field and
5003
+ * the mode is `ask` — or a "sign here" flow wants the facts first. Shows
5004
+ * the mark as it will sit in the field, takes reason and location, offers
5005
+ * a certification when the config allows it, and either SEALS the field
5006
+ * (`signField`) or only draws the mark into it (`fillField`).
5007
+ */
5008
+ function SignDialog() {
5009
+ const t = (0, _embedpdf_react_i18n.useT)();
5010
+ const surface = (0, _embedpdf_react_shell.useSurface)("signature-sign");
5011
+ const signature = (0, _embedpdf_react_signature.useSignature)();
5012
+ const [reason, setReason] = (0, react.useState)("");
5013
+ const [location, setLocation] = (0, react.useState)("");
5014
+ const [signKind, setSignKind] = (0, react.useState)("approval");
5015
+ const [busy, setBusy] = (0, react.useState)(null);
5016
+ const [error, setError] = (0, react.useState)(null);
5017
+ const field = surface.props?.field;
5018
+ const mark = surface.props?.mark;
5019
+ const preview = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(mark && "assetId" in mark ? mark.assetId : null);
5020
+ if (!surface.isOpen || !field || !mark) return null;
5021
+ const attribution = {
5022
+ ...reason.trim() ? { reason: reason.trim() } : {},
5023
+ ...location.trim() ? { location: location.trim() } : {}
5024
+ };
5025
+ const run = async (kind) => {
5026
+ setBusy(kind);
5027
+ setError(null);
5028
+ try {
5029
+ if (kind === "sign") await signature.signField({
5030
+ field,
5031
+ mark,
5032
+ attribution,
5033
+ ...signKind !== "approval" && signature.canCertify() ? { certify: { permission: Number(signKind.slice(-1)) } } : {}
5034
+ });
5035
+ else await signature.fillField(field, mark);
5036
+ surface.close();
5037
+ } catch (err) {
5038
+ setError(err instanceof Error ? err.message : String(err));
5039
+ } finally {
5040
+ setBusy(null);
5041
+ }
5042
+ };
5043
+ const input = "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm";
5044
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5045
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
5046
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5047
+ className: "border-border bg-surface w-96 rounded-lg border p-4 shadow-xl",
5048
+ children: [
5049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5050
+ className: "text-fg text-base font-semibold",
5051
+ children: t("demo.signTitle")
5052
+ }),
5053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5054
+ className: "border-border-subtle mt-3 flex h-24 items-center justify-center rounded-md border p-2",
5055
+ children: preview ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5056
+ src: preview,
5057
+ alt: "",
5058
+ className: "max-h-full max-w-full object-contain"
5059
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5060
+ className: "text-fg-muted text-sm",
5061
+ children: t("demo.signatureLabel")
5062
+ })
5063
+ }),
5064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5065
+ className: "text-fg-muted mt-3 block text-xs",
5066
+ children: [t("demo.signReason"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5067
+ value: reason,
5068
+ onChange: (e) => setReason(e.target.value),
5069
+ className: input
5070
+ })]
5071
+ }),
5072
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5073
+ className: "text-fg-muted mt-2 block text-xs",
5074
+ children: [t("demo.signLocation"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5075
+ value: location,
5076
+ onChange: (e) => setLocation(e.target.value),
5077
+ className: input
5078
+ })]
5079
+ }),
5080
+ signature.canCertify() ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5081
+ className: "text-fg-muted mt-3 block text-xs",
5082
+ children: [t("demo.signKind"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
5083
+ value: signKind,
5084
+ onChange: (e) => setSignKind(e.target.value),
5085
+ className: input,
5086
+ children: [
5087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5088
+ value: "approval",
5089
+ children: t("demo.signKindApproval")
5090
+ }),
5091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5092
+ value: "certify-3",
5093
+ children: t("demo.signKindCertifyAnnotate")
5094
+ }),
5095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5096
+ value: "certify-2",
5097
+ children: t("demo.signKindCertifyFill")
5098
+ }),
5099
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5100
+ value: "certify-1",
5101
+ children: t("demo.signKindCertifyLocked")
5102
+ })
5103
+ ]
5104
+ })]
5105
+ }) : null,
5106
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5107
+ className: "mt-2 text-sm text-red-600",
5108
+ children: error
5109
+ }) : null,
5110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5111
+ className: "mt-4 flex justify-end gap-2",
5112
+ children: [
5113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5114
+ type: "button",
5115
+ disabled: busy != null,
5116
+ onClick: surface.close,
5117
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
5118
+ children: t("demo.cancel")
5119
+ }),
5120
+ signature.canFill() ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5121
+ type: "button",
5122
+ disabled: busy != null,
5123
+ onClick: () => void run("fill"),
5124
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-60",
5125
+ children: t("demo.signFill")
5126
+ }) : null,
5127
+ signature.canSign() ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5128
+ type: "button",
5129
+ disabled: busy != null,
5130
+ onClick: () => void run("sign"),
5131
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
5132
+ children: busy === "sign" ? t("demo.signSigning") : t("demo.signButton")
5133
+ }) : null
5134
+ ]
5135
+ })
5136
+ ]
5137
+ })
5138
+ });
5139
+ }
5140
+ //#endregion
5141
+ //#region src/ui/signature-bridge.tsx
5142
+ /**
5143
+ * The signature plugin's UI intents → shell surfaces. The plugin owns the
5144
+ * act and says WHAT happened; the chrome decides WHAT OPENS:
5145
+ *
5146
+ * target (a "sign here" click) → the signatures panel, right side
5147
+ * ask (mode 'ask' met a field) → the sign dialog (modal)
5148
+ * inspect (a signed field clicked) → the validation popover
5149
+ *
5150
+ * Mounted once per document view; renders nothing.
5151
+ */
5152
+ function SignatureBridge() {
5153
+ const shell = (0, _embedpdf_react_shell.useShell)();
5154
+ (0, _embedpdf_react_signature.useSignatureEvent)((event) => {
5155
+ switch (event.type) {
5156
+ case "target":
5157
+ if (event.field) shell.open("signatures", { exclusive: "right" });
5158
+ return;
5159
+ case "ask":
5160
+ shell.open("signature-sign", {
5161
+ exclusive: "modal",
5162
+ props: {
5163
+ field: event.field,
5164
+ mark: event.mark
5165
+ }
5166
+ });
5167
+ return;
5168
+ case "inspect":
5169
+ shell.open("signature-inspector", { props: { field: event.field } });
5170
+ return;
5171
+ case "invalidating":
5172
+ shell.open("signatures", { exclusive: "right" });
5173
+ return;
5174
+ default: return;
5175
+ }
5176
+ });
5177
+ return null;
5178
+ }
5179
+ //#endregion
5180
+ //#region src/ui/signature-inspector.tsx
5181
+ /**
5182
+ * The validation popover (`signature-inspector` surface): what a signed
5183
+ * field's signature says and whether it holds — the four independent facts
5184
+ * (integrity, cryptography, trust, changes since) and the signer's claims,
5185
+ * anchored at the widget when its box is known, plus the signed revision as
5186
+ * a download (the exact bytes the signature covers). Mounted in the Stage
5187
+ * overlay so `<Anchored>` can project onto the page.
5188
+ */
5189
+ 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";
5190
+ /** A PDF date string (`D:YYYYMMDDHHmmSSZ` or with a zone offset) as a locale date; the raw text when it is not one. */
5191
+ const pdfDate = (raw) => {
5192
+ const m = /^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}'?\d{2}'?)?/.exec(raw);
5193
+ if (!m) return raw;
5194
+ const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
5195
+ const offset = !zone || zone === "Z" ? "Z" : `${zone[0]}${zone.slice(1, 3)}:${zone.slice(3).replace(/'/g, "") || "00"}`;
5196
+ const date = /* @__PURE__ */ new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}${offset}`);
5197
+ return Number.isNaN(date.getTime()) ? raw : date.toLocaleString();
5198
+ };
5199
+ const tone = (summary) => summary === "valid" ? "text-green-600" : summary === "invalid" ? "text-red-600" : summary === "valid-untrusted" ? "text-amber-600" : "text-fg-muted";
5200
+ function SignatureInspector() {
5201
+ const t = (0, _embedpdf_react_i18n.useT)();
5202
+ const surface = (0, _embedpdf_react_shell.useSurface)("signature-inspector");
5203
+ const signature = (0, _embedpdf_react_signature.useSignature)();
5204
+ const snapshot = (0, _embedpdf_react_signature.useSignatureSnapshot)();
5205
+ const verdicts = (0, _embedpdf_react_signature.useSignatureVerdicts)();
5206
+ const [validating, setValidating] = (0, react.useState)(false);
5207
+ const field = surface.props?.field;
5208
+ const dto = field ? signature.signatureOf(field) : null;
5209
+ const verdict = field ? signature.verdictOf(field) : null;
5210
+ const widget = dto?.widget ?? null;
5211
+ const box = (0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_form.FormToken, (c) => widget && widget.annotObjectNumber > 0 ? c.fillItem(widget.annotObjectNumber)?.box ?? null : null);
5212
+ (0, react.useEffect)(() => {
5213
+ if (!surface.isOpen || !dto?.signed || verdicts) return;
5214
+ setValidating(true);
5215
+ signature.validate().finally(() => setValidating(false));
5216
+ }, [surface.isOpen, dto?.index]);
5217
+ if (!surface.isOpen || !field || !dto || !snapshot) return null;
5218
+ const revalidate = () => {
5219
+ setValidating(true);
5220
+ signature.validate().finally(() => setValidating(false));
5221
+ };
5222
+ const summary = verdict?.summary ?? null;
5223
+ const modifications = verdict?.modifications.verdict ?? null;
5224
+ const row = (label, value, className = "text-fg") => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5225
+ className: "flex items-baseline justify-between gap-3 text-xs",
5226
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5227
+ className: "text-fg-muted",
5228
+ children: label
5229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5230
+ className: `text-right ${className}`,
5231
+ children: value
5232
+ })]
5233
+ });
5234
+ const card = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5235
+ className: "border-border bg-elevated w-72 rounded-lg border p-3 text-sm shadow-xl",
5236
+ children: [
5237
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5238
+ className: "flex items-center justify-between",
5239
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5240
+ className: "text-fg font-semibold",
5241
+ children: t("demo.inspectTitle")
5242
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5243
+ type: "button",
5244
+ onClick: surface.close,
5245
+ className: "text-fg-muted hover:bg-hover grid h-6 w-6 place-items-center rounded",
5246
+ "aria-label": t("demo.cancel"),
5247
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
5248
+ name: "x",
5249
+ size: 14
5250
+ })
5251
+ })]
5252
+ }),
5253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5254
+ className: `mt-1 text-sm font-medium ${tone(summary)}`,
5255
+ children: validating ? t("demo.signaturesValidating") : t(verdictKey(verdict))
5256
+ }),
5257
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5258
+ className: "mt-2 flex flex-col gap-1",
5259
+ children: [
5260
+ row(t("demo.inspectSigner"), dto.signer.name ?? "—"),
5261
+ dto.signer.reason ? row(t("demo.inspectReason"), dto.signer.reason) : null,
5262
+ dto.signer.location ? row(t("demo.inspectLocation"), dto.signer.location) : null,
5263
+ dto.signer.claimedTime ? row(t("demo.inspectTime"), pdfDate(dto.signer.claimedTime)) : null
5264
+ ]
5265
+ }),
5266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5267
+ className: "border-border-subtle mt-2 flex flex-col gap-1 border-t pt-2",
5268
+ children: [
5269
+ row(t("demo.inspectIntegrity"), verdict?.integrity ?? "—", tone(verdict?.integrity === "valid" ? "valid" : verdict?.integrity === "invalid" ? "invalid" : null)),
5270
+ row(t("demo.inspectCryptography"), verdict?.cryptography ?? "—"),
5271
+ row(t("demo.inspectTrust"), verdict?.trust ?? "—"),
5272
+ 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"),
5273
+ verdict?.modifications.laterRevisions ? row(t("demo.inspectLaterRevisions"), String(verdict.modifications.laterRevisions)) : null
5274
+ ]
5275
+ }),
5276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5277
+ className: "mt-3 flex justify-end gap-2",
5278
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5279
+ type: "button",
5280
+ disabled: validating,
5281
+ onClick: revalidate,
5282
+ className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
5283
+ children: t("demo.signaturesValidate")
5284
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadRevision, { field })]
5285
+ })
5286
+ ]
5287
+ });
5288
+ if (widget && widget.pageObjectNumber > 0 && box) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_anchored.Anchored, {
5289
+ anchor: {
5290
+ pon: widget.pageObjectNumber,
5291
+ bounds: box
5292
+ },
5293
+ placement: "bottom",
5294
+ gap: 8,
5295
+ children: card
5296
+ });
5297
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5298
+ className: "absolute right-4 top-4 z-40",
5299
+ children: card
5300
+ });
5301
+ }
5302
+ /** The signed revision — the exact bytes the signature covers — as a file. */
5303
+ function DownloadRevision({ field }) {
5304
+ const t = (0, _embedpdf_react_i18n.useT)();
5305
+ const signature = (0, _embedpdf_react_signature.useSignature)();
5306
+ const dto = signature.signatureOf(field);
5307
+ const [busy, setBusy] = (0, react.useState)(false);
5308
+ if (!dto || dto.revisionIndex === null) return null;
5309
+ const revisionIndex = dto.revisionIndex;
5310
+ const run = async () => {
5311
+ setBusy(true);
5312
+ try {
5313
+ const bytes = await signature.revisionBytes(revisionIndex);
5314
+ const blob = new Blob([bytes], { type: "application/pdf" });
5315
+ const url = URL.createObjectURL(blob);
5316
+ const a = document.createElement("a");
5317
+ a.href = url;
5318
+ a.download = `${dto.fieldName}-signed.pdf`;
5319
+ a.click();
5320
+ URL.revokeObjectURL(url);
5321
+ } catch (err) {
5322
+ console.error("[embedpdf] signed revision download failed:", err);
5323
+ } finally {
5324
+ setBusy(false);
5325
+ }
5326
+ };
5327
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5328
+ type: "button",
5329
+ disabled: busy,
5330
+ onClick: () => void run(),
5331
+ className: "border-border text-fg hover:bg-hover rounded-md border px-2 py-1 text-xs disabled:opacity-60",
5332
+ children: t("demo.inspectDownload")
5333
+ });
5334
+ }
5335
+ //#endregion
5336
+ //#region src/ui/signature-maker.tsx
5337
+ /**
5338
+ * The mark maker (`signature-maker` modal surface): a person's signature and
5339
+ * initials, drawn, typed, or uploaded, saved as ONE library of kind
5340
+ * `signatures` — the file IS the person (Preview's model). Chrome code over
5341
+ * the stamp plugin's generic calls: `createLibrary` + `addAsset({ mark })`;
5342
+ * rename/export/delete are the library verbs. Opened on an existing library
5343
+ * (`props.libraryId`) it only adds the missing initials.
5344
+ */
5345
+ const PAD = {
5346
+ width: 360,
5347
+ height: 120
5348
+ };
5349
+ const BUILTIN_FONTS = [{
5350
+ key: "helvetica-oblique",
5351
+ label: "Helvetica"
5352
+ }, {
5353
+ key: "times-italic",
5354
+ label: "Times"
5355
+ }];
5356
+ /** One pad: draw / type / upload, producing a `MarkSource` or null. */
5357
+ function MarkPad({ value, onChange, fonts, registerFont }) {
5358
+ const t = (0, _embedpdf_react_i18n.useT)();
5359
+ const [tab, setTab] = (0, react.useState)("draw");
5360
+ const [strokes, setStrokes] = (0, react.useState)([]);
5361
+ const [text, setText] = (0, react.useState)("");
5362
+ const [font, setFont] = (0, react.useState)(fonts[0]?.key ?? "helvetica-oblique");
5363
+ const [fileName, setFileName] = (0, react.useState)(null);
5364
+ const canvas = (0, react.useRef)(null);
5365
+ const drawing = (0, react.useRef)(null);
5366
+ (0, react.useEffect)(() => {
5367
+ const el = canvas.current;
5368
+ if (!el) return;
5369
+ const dpr = window.devicePixelRatio || 1;
5370
+ el.width = PAD.width * dpr;
5371
+ el.height = PAD.height * dpr;
5372
+ const ctx = el.getContext("2d");
5373
+ if (!ctx) return;
5374
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
5375
+ ctx.clearRect(0, 0, PAD.width, PAD.height);
5376
+ ctx.lineCap = "round";
5377
+ ctx.lineJoin = "round";
5378
+ ctx.lineWidth = 2.5;
5379
+ ctx.strokeStyle = getComputedStyle(el).color || "#000";
5380
+ for (const stroke of strokes) {
5381
+ ctx.beginPath();
5382
+ stroke.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
5383
+ ctx.stroke();
5384
+ }
5385
+ }, [strokes, tab]);
5386
+ const point = (e) => {
5387
+ const r = e.currentTarget.getBoundingClientRect();
5388
+ return {
5389
+ x: e.clientX - r.left,
5390
+ y: e.clientY - r.top
5391
+ };
5392
+ };
5393
+ const commitStrokes = (next) => {
5394
+ setStrokes(next);
5395
+ onChange(next.length ? {
5396
+ kind: "ink",
5397
+ strokes: next,
5398
+ color: "#000000",
5399
+ strokeWidth: 2.5
5400
+ } : null);
5401
+ };
5402
+ const commitText = async (next, key) => {
5403
+ setText(next);
5404
+ setFont(key);
5405
+ if (!next.trim()) {
5406
+ onChange(null);
5407
+ return;
5408
+ }
5409
+ await registerFont(key);
5410
+ onChange({
5411
+ kind: "text",
5412
+ text: next.trim(),
5413
+ fontFamily: key,
5414
+ color: "#000000",
5415
+ fontSize: 36
5416
+ });
5417
+ };
5418
+ const pickFile = (file) => {
5419
+ setFileName(file.name);
5420
+ onChange(file.type === "application/pdf" ? {
5421
+ kind: "pdf",
5422
+ source: file,
5423
+ pageIndex: 0
5424
+ } : {
5425
+ kind: "image",
5426
+ source: file
5427
+ });
5428
+ };
5429
+ 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"}`;
5430
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5431
+ className: "flex flex-col gap-2",
5432
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5433
+ className: "flex items-center gap-1",
5434
+ children: [
5435
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5436
+ type: "button",
5437
+ className: tabClass("draw"),
5438
+ onClick: () => setTab("draw"),
5439
+ children: t("demo.makerDraw")
5440
+ }),
5441
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5442
+ type: "button",
5443
+ className: tabClass("type"),
5444
+ onClick: () => setTab("type"),
5445
+ children: t("demo.makerType")
5446
+ }),
5447
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5448
+ type: "button",
5449
+ className: tabClass("upload"),
5450
+ onClick: () => setTab("upload"),
5451
+ children: t("demo.makerUpload")
5452
+ }),
5453
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "flex-1" }),
5454
+ value ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5455
+ type: "button",
5456
+ onClick: () => {
5457
+ commitStrokes([]);
5458
+ setText("");
5459
+ setFileName(null);
5460
+ onChange(null);
5461
+ },
5462
+ className: "text-fg-muted hover:text-fg text-xs",
5463
+ children: t("demo.makerClear")
5464
+ }) : null
5465
+ ]
5466
+ }), tab === "draw" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
5467
+ ref: canvas,
5468
+ style: {
5469
+ width: PAD.width,
5470
+ height: PAD.height,
5471
+ touchAction: "none"
5472
+ },
5473
+ className: "border-border-subtle bg-surface text-fg rounded-md border",
5474
+ onPointerDown: (e) => {
5475
+ e.currentTarget.setPointerCapture(e.pointerId);
5476
+ drawing.current = [point(e)];
5477
+ setStrokes((s) => [...s, drawing.current]);
5478
+ },
5479
+ onPointerMove: (e) => {
5480
+ if (!drawing.current) return;
5481
+ drawing.current.push(point(e));
5482
+ setStrokes((s) => [...s.slice(0, -1), [...drawing.current]]);
5483
+ },
5484
+ onPointerUp: () => {
5485
+ if (!drawing.current) return;
5486
+ const done = drawing.current;
5487
+ drawing.current = null;
5488
+ setStrokes((s) => {
5489
+ const next = [...s.slice(0, -1), done];
5490
+ commitStrokes(next);
5491
+ return next;
5492
+ });
5493
+ }
5494
+ }) : tab === "type" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5495
+ className: "flex flex-col gap-2",
5496
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5497
+ value: text,
5498
+ placeholder: t("demo.makerTypePlaceholder"),
5499
+ onChange: (e) => void commitText(e.target.value, font),
5500
+ className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 text-sm"
5501
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5502
+ className: "text-fg-muted flex items-center gap-2 text-xs",
5503
+ children: [t("demo.makerFont"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
5504
+ value: font,
5505
+ onChange: (e) => void commitText(text, e.target.value),
5506
+ className: "border-border bg-surface text-fg rounded border px-2 py-1 text-xs",
5507
+ children: fonts.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5508
+ value: f.key,
5509
+ children: f.label
5510
+ }, f.key))
5511
+ })]
5512
+ })]
5513
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5514
+ 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",
5515
+ children: [fileName ?? t("demo.makerUploadHint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5516
+ type: "file",
5517
+ accept: "image/png,image/jpeg,application/pdf",
5518
+ className: "hidden",
5519
+ onChange: (e) => {
5520
+ const file = e.currentTarget.files?.[0];
5521
+ e.currentTarget.value = "";
5522
+ if (file) pickFile(file);
5523
+ }
5524
+ })]
5525
+ })]
5526
+ });
5527
+ }
5528
+ function SignatureMakerModal() {
5529
+ const t = (0, _embedpdf_react_i18n.useT)();
5530
+ const surface = (0, _embedpdf_react_shell.useSurface)("signature-maker");
5531
+ const stamp = (0, _embedpdf_react_stamp.useStamp)();
5532
+ const kernel = (0, _embedpdf_react_runtime.useKernel)();
5533
+ const config = useSignaturesConfig();
5534
+ const [name, setName] = (0, react.useState)("");
5535
+ const [signatureMark, setSignatureMark] = (0, react.useState)(null);
5536
+ const [initialsMark, setInitialsMark] = (0, react.useState)(null);
5537
+ const [busy, setBusy] = (0, react.useState)(false);
5538
+ const [error, setError] = (0, react.useState)(null);
5539
+ const existingId = surface.props?.libraryId;
5540
+ const existing = existingId ? stamp.library(existingId) : null;
5541
+ const wantsInitials = (config.kinds ?? ["signature", "initials"]).includes("initials");
5542
+ const fonts = [...(config.fonts ?? []).map((f) => ({
5543
+ key: f.key,
5544
+ label: f.label
5545
+ })), ...BUILTIN_FONTS];
5546
+ const registered = (0, react.useRef)(/* @__PURE__ */ new Set());
5547
+ const registerFont = async (key) => {
5548
+ const spec = config.fonts?.find((f) => f.key === key);
5549
+ if (!spec || registered.current.has(key) || !kernel.engine.fonts) return;
5550
+ const response = await fetch(spec.url);
5551
+ if (!response.ok) throw new Error(`${spec.url}: HTTP ${response.status}`);
5552
+ await kernel.engine.fonts.register({
5553
+ key,
5554
+ data: new Uint8Array(await response.arrayBuffer())
5555
+ });
5556
+ registered.current.add(key);
5557
+ };
5558
+ if (!surface.isOpen) return null;
5559
+ const canSave = existing ? initialsMark != null : name.trim().length > 0 && signatureMark != null;
5560
+ const save = async () => {
5561
+ setBusy(true);
5562
+ setError(null);
5563
+ try {
5564
+ if (existing) await stamp.addAsset({
5565
+ libraryId: existing.id,
5566
+ name: _embedpdf_react_signature.INITIALS_MARK_NAME,
5567
+ label: t("demo.initialsLabel"),
5568
+ mark: initialsMark
5569
+ });
5570
+ else {
5571
+ const libraryId = await stamp.createLibrary(name.trim(), { kind: _embedpdf_react_signature.SIGNATURES_LIBRARY_KIND });
5572
+ await stamp.addAsset({
5573
+ libraryId,
5574
+ name: _embedpdf_react_signature.SIGNATURE_MARK_NAME,
5575
+ label: t("demo.signatureLabel"),
5576
+ mark: signatureMark
5577
+ });
5578
+ if (initialsMark) await stamp.addAsset({
5579
+ libraryId,
5580
+ name: _embedpdf_react_signature.INITIALS_MARK_NAME,
5581
+ label: t("demo.initialsLabel"),
5582
+ mark: initialsMark
5583
+ });
5584
+ }
5585
+ setName("");
5586
+ setSignatureMark(null);
5587
+ setInitialsMark(null);
5588
+ surface.close();
5589
+ } catch (err) {
5590
+ console.error("[embedpdf] saving the signature failed:", err);
5591
+ setError(t("demo.makerError"));
5592
+ } finally {
5593
+ setBusy(false);
5594
+ }
5595
+ };
5596
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5597
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
5598
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5599
+ className: "border-border bg-surface w-[26rem] rounded-lg border p-4 shadow-xl",
5600
+ children: [
5601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5602
+ className: "flex items-center justify-between",
5603
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5604
+ className: "text-fg text-base font-semibold",
5605
+ children: existing ? t("demo.signaturesAddInitials") : t("demo.makerTitle")
5606
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5607
+ type: "button",
5608
+ onClick: surface.close,
5609
+ className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
5610
+ "aria-label": t("demo.cancel"),
5611
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
5612
+ name: "x",
5613
+ size: 16
5614
+ })
5615
+ })]
5616
+ }),
5617
+ existing ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5618
+ className: "text-fg-muted mt-3 block text-xs",
5619
+ children: [t("demo.makerName"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5620
+ value: name,
5621
+ placeholder: t("demo.makerNamePlaceholder"),
5622
+ onChange: (e) => setName(e.target.value),
5623
+ className: "border-border bg-surface text-fg mt-1 w-full rounded border px-2 py-1.5 text-sm"
5624
+ })]
5625
+ }),
5626
+ existing ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5627
+ className: "mt-3",
5628
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5629
+ className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
5630
+ children: t("demo.signatureLabel")
5631
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkPad, {
5632
+ value: signatureMark,
5633
+ onChange: setSignatureMark,
5634
+ fonts,
5635
+ registerFont
5636
+ })]
5637
+ }),
5638
+ wantsInitials ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5639
+ className: "mt-3",
5640
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
5641
+ className: "text-fg mb-1 text-xs font-semibold uppercase tracking-wide",
5642
+ children: [t("demo.initialsLabel"), existing ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5643
+ className: "text-fg-muted ml-1 font-normal normal-case tracking-normal",
5644
+ children: t("demo.makerOptional")
5645
+ })]
5646
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkPad, {
5647
+ value: initialsMark,
5648
+ onChange: setInitialsMark,
5649
+ fonts,
5650
+ registerFont
5651
+ })]
5652
+ }) : null,
5653
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5654
+ className: "mt-2 text-sm text-red-600",
5655
+ children: error
5656
+ }) : null,
5657
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5658
+ className: "mt-4 flex justify-end gap-2",
5659
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5660
+ type: "button",
5661
+ disabled: busy,
5662
+ onClick: surface.close,
5663
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
5664
+ children: t("demo.cancel")
5665
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5666
+ type: "button",
5667
+ disabled: busy || !canSave,
5668
+ onClick: () => void save(),
5669
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm disabled:opacity-60",
5670
+ children: busy ? t("demo.makerSaving") : t("demo.makerSave")
5671
+ })]
5672
+ })
5673
+ ]
5674
+ })
5675
+ });
5676
+ }
5677
+ //#endregion
4015
5678
  //#region src/ui/document-boot.tsx
4016
5679
  /**
4017
5680
  * Boot-state panes for the document area — what renders INSTEAD of the Stage
@@ -4253,9 +5916,10 @@ function Shell() {
4253
5916
  className: "w-full"
4254
5917
  })
4255
5918
  });
4256
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5919
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationFontsProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4257
5920
  className: "bg-app text-fg flex h-full flex-col",
4258
5921
  children: [
5922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampLibraryStore, {}),
4259
5923
  (frame.header ?? true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", { name: "header" }),
4260
5924
  (frame.tabs ?? "always") !== "never" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", {
4261
5925
  name: "tabs",
@@ -4275,6 +5939,7 @@ function Shell() {
4275
5939
  overlay: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4276
5940
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStrip, {}),
4277
5941
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionStrip, {}),
5942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureInspector, {}),
4278
5943
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_selection.SelectionHandles, {}),
4279
5944
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "y" }),
4280
5945
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "x" })
@@ -4293,12 +5958,16 @@ function Shell() {
4293
5958
  ]
4294
5959
  }),
4295
5960
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RightSidebar, {}),
4296
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactConfirmModal, {})
5961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactConfirmModal, {}),
5962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CalibrationDialog, {}),
5963
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureBridge, {}),
5964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignDialog, {}),
5965
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignatureMakerModal, {})
4297
5966
  ] })
4298
5967
  }),
4299
5968
  toolbarEdge === "bottom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeBand, { edge: "bottom" }), toolbarBand] })
4300
5969
  ]
4301
- });
5970
+ }) });
4302
5971
  }
4303
5972
  //#endregion
4304
5973
  //#region src/viewer.tsx
@@ -4321,7 +5990,7 @@ function Shell() {
4321
5990
  * changing these props on a mounted viewer does not rebuild the workspace.
4322
5991
  */
4323
5992
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
4324
- const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-BRYSk4rY.cjs")).then((m) => m.es) };
5993
+ const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-BXLlBo2b.cjs")).then((m) => m.es) };
4325
5994
  const isDict = (v) => typeof v === "object" && v !== null;
4326
5995
  /**
4327
5996
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -4365,7 +6034,7 @@ function Booting() {
4365
6034
  })
4366
6035
  });
4367
6036
  }
4368
- function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, theme, themeTarget, onViewer }) {
6037
+ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, signatures, annotations, theme, themeTarget, onViewer }) {
4369
6038
  const [resolved] = (0, react.useState)(() => {
4370
6039
  const byId = new Map(require_commands.defaultCommands.map((c) => [c.id, c]));
4371
6040
  for (const c of commands ?? []) byId.set(c.id, c);
@@ -4397,6 +6066,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4397
6066
  chrome: resolvedChrome,
4398
6067
  icons: icons ?? {},
4399
6068
  stamps: stamps ?? {},
6069
+ signatures: signatures ?? {},
6070
+ annotations: annotations ?? {},
4400
6071
  i18n: {
4401
6072
  locales,
4402
6073
  loaders,
@@ -4462,8 +6133,15 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4462
6133
  }
4463
6134
  } }),
4464
6135
  (0, _embedpdf_react_form.formPlugin)(),
6136
+ (0, _embedpdf_react_signature.signaturePlugin)({
6137
+ mode: resolved.signatures.mode,
6138
+ signer: resolved.signatures.signer,
6139
+ trust: resolved.signatures.trust,
6140
+ allowCertify: resolved.signatures.allowCertify
6141
+ }),
4465
6142
  (0, _embedpdf_react_link.linkPlugin)(),
4466
6143
  (0, _embedpdf_react_redaction.redactionPlugin)(),
6144
+ (0, _embedpdf_react_measurement.measurementPlugin)(),
4467
6145
  (0, _embedpdf_react_search.searchPlugin)(),
4468
6146
  demoToolsPlugin(),
4469
6147
  (0, _embedpdf_react_i18n.i18nPlugin)({
@@ -4532,6 +6210,12 @@ Object.defineProperty(exports, "InteractionToken", {
4532
6210
  return _embedpdf_react_interaction.InteractionToken;
4533
6211
  }
4534
6212
  });
6213
+ Object.defineProperty(exports, "MeasurementToken", {
6214
+ enumerable: true,
6215
+ get: function() {
6216
+ return _embedpdf_react_measurement.MeasurementToken;
6217
+ }
6218
+ });
4535
6219
  Object.defineProperty(exports, "MetadataToken", {
4536
6220
  enumerable: true,
4537
6221
  get: function() {
@@ -4562,6 +6246,12 @@ Object.defineProperty(exports, "ShellToken", {
4562
6246
  return _embedpdf_react_shell.ShellToken;
4563
6247
  }
4564
6248
  });
6249
+ Object.defineProperty(exports, "SignatureToken", {
6250
+ enumerable: true,
6251
+ get: function() {
6252
+ return _embedpdf_react_signature.SignatureToken;
6253
+ }
6254
+ });
4565
6255
  Object.defineProperty(exports, "StageToken", {
4566
6256
  enumerable: true,
4567
6257
  get: function() {
@@ -4586,6 +6276,12 @@ Object.defineProperty(exports, "chromeHelpers", {
4586
6276
  return _embedpdf_react_toolbar.chromeHelpers;
4587
6277
  }
4588
6278
  });
6279
+ Object.defineProperty(exports, "createTestSigner", {
6280
+ enumerable: true,
6281
+ get: function() {
6282
+ return _embedpdf_react_signature.createTestSigner;
6283
+ }
6284
+ });
4589
6285
  exports.createViewerHandle = createViewerHandle;
4590
6286
  Object.defineProperty(exports, "custom", {
4591
6287
  enumerable: true,
@@ -4608,12 +6304,36 @@ Object.defineProperty(exports, "group", {
4608
6304
  return _embedpdf_react_toolbar.group;
4609
6305
  }
4610
6306
  });
6307
+ Object.defineProperty(exports, "indexedDbKeyStore", {
6308
+ enumerable: true,
6309
+ get: function() {
6310
+ return _embedpdf_react_signature.indexedDbKeyStore;
6311
+ }
6312
+ });
4611
6313
  Object.defineProperty(exports, "item", {
4612
6314
  enumerable: true,
4613
6315
  get: function() {
4614
6316
  return _embedpdf_react_toolbar.item;
4615
6317
  }
4616
6318
  });
6319
+ Object.defineProperty(exports, "memoryKeyStore", {
6320
+ enumerable: true,
6321
+ get: function() {
6322
+ return _embedpdf_react_signature.memoryKeyStore;
6323
+ }
6324
+ });
6325
+ Object.defineProperty(exports, "personalSigner", {
6326
+ enumerable: true,
6327
+ get: function() {
6328
+ return _embedpdf_react_signature.personalSigner;
6329
+ }
6330
+ });
6331
+ Object.defineProperty(exports, "remoteSigner", {
6332
+ enumerable: true,
6333
+ get: function() {
6334
+ return _embedpdf_react_signature.remoteSigner;
6335
+ }
6336
+ });
4617
6337
  Object.defineProperty(exports, "removeItems", {
4618
6338
  enumerable: true,
4619
6339
  get: function() {
@@ -4633,5 +6353,11 @@ Object.defineProperty(exports, "validateChrome", {
4633
6353
  return _embedpdf_react_toolbar.validateChrome;
4634
6354
  }
4635
6355
  });
6356
+ Object.defineProperty(exports, "webCryptoSigner", {
6357
+ enumerable: true,
6358
+ get: function() {
6359
+ return _embedpdf_react_signature.webCryptoSigner;
6360
+ }
6361
+ });
4636
6362
 
4637
6363
  //# sourceMappingURL=index.cjs.map