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

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
@@ -32,6 +32,7 @@ let _embedpdf_react_page_edit = require("@embedpdf/react/page-edit");
32
32
  let _embedpdf_react_interaction = require("@embedpdf/react/interaction");
33
33
  let _embedpdf_react_selection = require("@embedpdf/react/selection");
34
34
  let _embedpdf_react_annotation = require("@embedpdf/react/annotation");
35
+ let _embedpdf_react_stamp = require("@embedpdf/react/stamp");
35
36
  let _embedpdf_react_redaction = require("@embedpdf/react/redaction");
36
37
  let _embedpdf_react_actions = require("@embedpdf/react/actions");
37
38
  let _embedpdf_react_form = require("@embedpdf/react/form");
@@ -44,6 +45,7 @@ let _embedpdf_react_toolbar = require("@embedpdf/react/toolbar");
44
45
  let react_jsx_runtime = require("react/jsx-runtime");
45
46
  let _embedpdf_react_scrollbar = require("@embedpdf/react/scrollbar");
46
47
  let _embedpdf_react_annotation_menu = require("@embedpdf/react/annotation-menu");
48
+ let _embedpdf_default_stamps_library = require("@embedpdf/default-stamps/library");
47
49
  let _embedpdf_react_metadata = require("@embedpdf/react/metadata");
48
50
  //#region src/config/stage.ts
49
51
  /**
@@ -58,24 +60,23 @@ const ThumbsStageToken = (0, _embedpdf_react_runtime.createCapabilityToken)("sta
58
60
  //#endregion
59
61
  //#region src/config/demo-tools.plugin.ts
60
62
  /**
61
- * Example-only: register the insert / redact authoring tools as INERT
62
- * interaction tools. v3 doesn't yet ship signature or redaction plugins, but
63
- * this is a CHROME demo — its job is selection state and layout, not the
64
- * tool's effect. Registering them as real (behaviourless) tools lets every
65
- * tool button in every mode band go through the SAME path
66
- * (`InteractionToken.activateTool`), so `active` is uniform and honest: the
67
- * button really is the active tool; it just has no handler behind it.
63
+ * Example-only: register the still-unimplemented authoring tools as INERT
64
+ * interaction tools. v3 doesn't yet ship a signature plugin, but this is a
65
+ * CHROME demo — its job is selection state and layout, not the tool's effect.
66
+ * Registering them as real (behaviourless) tools lets every tool button in
67
+ * every mode band go through the SAME path (`InteractionToken.activateTool`),
68
+ * so `active` is uniform and honest: the button really is the active tool; it
69
+ * just has no handler behind it.
68
70
  *
69
- * The annotate + shapes tools are NOT here — those are real tools the
70
- * annotation plugin registers (highlight, ink, square, circle, …) — and
71
- * neither are the form palette tools (the form plugin's draw-to-place).
71
+ * Everything else in the Insert band is REAL now: stamps come from the stamp
72
+ * plugin's libraries (armed by the stamps sidebar), and image/attachment are
73
+ * the annotation plugin's click-then-pick tools. The annotate + shapes tools
74
+ * were never here either — nor the form palette (the form plugin's
75
+ * draw-to-place).
72
76
  */
73
77
  const INERT_TOOLS = [{
74
78
  id: "signature",
75
79
  cursor: "copy"
76
- }, {
77
- id: "image",
78
- cursor: "copy"
79
80
  }];
80
81
  const demoToolsPlugin = () => (0, _embedpdf_react_runtime.definePlugin)({
81
82
  id: "demo-tools",
@@ -170,6 +171,7 @@ const en = {
170
171
  delete: "Delete",
171
172
  group: "Group",
172
173
  ungroup: "Ungroup",
174
+ stampFromSelection: "Make stamp",
173
175
  strip: "Annotation actions"
174
176
  },
175
177
  selection: {
@@ -294,7 +296,23 @@ const en = {
294
296
  redactApplying: "Applying…",
295
297
  redactLabelTitle: "Redaction label",
296
298
  redactLabelPlaceholder: "e.g. REDACTED",
297
- redactLabelRepeat: "Repeat label across region"
299
+ redactLabelRepeat: "Repeat label across region",
300
+ stampsTitle: "Stamps",
301
+ stampsAll: "All stamps",
302
+ stampsLoading: "Loading stamps…",
303
+ stampsEmpty: "No stamps yet. Import a PDF to turn each of its pages into a stamp.",
304
+ stampsRemoveStamp: "Remove stamp",
305
+ stampsArmedHint: "Click a page to place the stamp.",
306
+ stampsImport: "Import PDF…",
307
+ stampsImporting: "Importing…",
308
+ stampsImportHint: "Import a PDF as a stamp library — each page becomes one stamp.",
309
+ stampsRemoveLibrary: "Remove library",
310
+ stampsExportLibrary: "Export library as PDF",
311
+ stampsCustomLibrary: "My stamps",
312
+ stampsCustomLabel: "Custom stamp",
313
+ stampsError: "The built-in stamps could not be loaded.",
314
+ stampsArmError: "That stamp could not be armed.",
315
+ stampsImportError: "That PDF could not be imported as a stamp library."
298
316
  }
299
317
  }
300
318
  };
@@ -311,7 +329,8 @@ const en = {
311
329
  */
312
330
  const ViewerConfigContext = (0, react.createContext)({
313
331
  chrome: require_chrome.defaultChrome,
314
- icons: {}
332
+ icons: {},
333
+ stamps: {}
315
334
  });
316
335
  const ViewerConfigProvider = ViewerConfigContext.Provider;
317
336
  function useChromeSchema() {
@@ -326,6 +345,9 @@ function useStripSchema(id) {
326
345
  function useCustomIcons() {
327
346
  return (0, react.useContext)(ViewerConfigContext).icons;
328
347
  }
348
+ function useStampsConfig() {
349
+ return (0, react.useContext)(ViewerConfigContext).stamps;
350
+ }
329
351
  //#endregion
330
352
  //#region src/handle.ts
331
353
  function createViewerHandle(kernel) {
@@ -3481,6 +3503,310 @@ function SearchPanel() {
3481
3503
  });
3482
3504
  }
3483
3505
  //#endregion
3506
+ //#region src/config/default-stamps.ts
3507
+ /**
3508
+ * The viewer's BUILT-IN stamp library: `@embedpdf/default-stamps`, the
3509
+ * standard rubber stamps as one Acrobat-compatible PDF per locale.
3510
+ *
3511
+ * Delivery: the library ships INSIDE whatever ships this viewer. The package
3512
+ * exposes each locale as a lazy ES module (`@embedpdf/default-stamps/library`),
3513
+ * so it travels through the module graph like any other code — every bundler
3514
+ * splits it into a chunk served from the app's own origin, the CDN snippet
3515
+ * carries it as a sibling chunk in its folder, and nothing is fetched from a
3516
+ * third party. `stamps.defaultLibrary` overrides that:
3517
+ * - `false` → no built-in library, no request (air-gapped);
3518
+ * - a URL template → exactly that (`{locale}` slot), for self-hosted copies.
3519
+ *
3520
+ * Loading is LAZY (first open of the stamps panel, never at boot) and
3521
+ * locale-aware: the chrome locale leads, the browser languages break ties
3522
+ * for the locales the chrome does not translate itself, and a locale change
3523
+ * while a default library is loaded swaps it for the new one. The file names
3524
+ * itself (`/Title`, `Id: embedpdf-standard`), so no overrides are passed.
3525
+ */
3526
+ /** The built-in library's PieceInfo id — excluded from persistence (it is
3527
+ * loaded again on every first open, in the locale of that moment). */
3528
+ const DEFAULT_LIBRARY_ID = "embedpdf-standard";
3529
+ /** Which shipped locale to show for the chrome's locale + the browser's. */
3530
+ function resolveStampsLocale(chromeLocale) {
3531
+ return (0, _embedpdf_react_i18n.negotiateLocale)(_embedpdf_default_stamps_library.LOCALES, [chromeLocale, ...typeof navigator !== "undefined" ? navigator.languages ?? [] : []]) ?? "en";
3532
+ }
3533
+ const fetchBytes = async (url) => {
3534
+ const response = await fetch(url);
3535
+ if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
3536
+ return new Uint8Array(await response.arrayBuffer());
3537
+ };
3538
+ /** The library bytes for `locale`: your URL when you gave one, otherwise the
3539
+ * copy that shipped with the viewer. Never anything else. */
3540
+ const defaultLibraryBytes = (locale, source) => source ? fetchBytes(source.replace("{locale}", locale)) : (0, _embedpdf_default_stamps_library.loadDefaultLibrary)(locale);
3541
+ /** Per workspace: the locale the default library was last brought to, and
3542
+ * the in-flight work — the panel mounts and unmounts with the sidebar, so
3543
+ * this cannot live in component state. */
3544
+ const loads = /* @__PURE__ */ new WeakMap();
3545
+ /**
3546
+ * Bring the built-in library to `locale`. Idempotent per workspace and
3547
+ * locale; a locale change swaps the loaded default (remove, then import);
3548
+ * a default the user removed in this session stays removed — the locale is
3549
+ * recorded, nothing is re-added. An embedder that seeded a library under
3550
+ * the same id is left alone.
3551
+ */
3552
+ function ensureDefaultLibrary(stamp, locale, source) {
3553
+ if (source === false) return Promise.resolve();
3554
+ const current = loads.get(stamp);
3555
+ if (current?.locale === locale) return current.promise;
3556
+ const run = async () => {
3557
+ await current?.promise.catch(() => void 0);
3558
+ const loaded = stamp.library(DEFAULT_LIBRARY_ID);
3559
+ if (loaded?.locale === locale) return;
3560
+ if (current && !loaded) return;
3561
+ if (loaded) await stamp.removeLibrary(DEFAULT_LIBRARY_ID);
3562
+ await stamp.importLibraryPdf(await defaultLibraryBytes(locale, source));
3563
+ };
3564
+ const promise = run();
3565
+ loads.set(stamp, {
3566
+ locale,
3567
+ promise
3568
+ });
3569
+ return promise;
3570
+ }
3571
+ //#endregion
3572
+ //#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:
3577
+ *
3578
+ * a library <select> ("All stamps" + one per library, shown once there
3579
+ * are two) → a two-column grid of thumbnails → click a stamp → `armAsset`
3580
+ * arms the annotation plugin's stamp tool → hovering a page ghosts the
3581
+ * exact placement → each click on a page places one.
3582
+ *
3583
+ * Deliberately NOT a file dialog: picking arbitrary image bytes is the Image
3584
+ * tool's job (a click-then-pick tool, `insert:add-image`). The stamp button
3585
+ * opens a library; a library is a set of reusable, named assets — the two
3586
+ * gestures are different, so they are different controls.
3587
+ *
3588
+ * The panel stays open while you place, so a stamp can be dropped on several
3589
+ * pages and swapped without a round trip through the toolbar. A command can
3590
+ * open it ON a library (`annotation:stamp-from-selection` → "My stamps")
3591
+ * through the surface's open props.
3592
+ */
3593
+ 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
+ function StampsPanel() {
3610
+ const t = (0, _embedpdf_react_i18n.useT)();
3611
+ const stamp = (0, _embedpdf_react_stamp.useStamp)();
3612
+ const libraries = (0, _embedpdf_react_stamp.useStampLibraries)();
3613
+ const assets = (0, _embedpdf_react_stamp.useStampAssets)();
3614
+ const { armAsset } = (0, _embedpdf_react_stamp.useArmStampAsset)();
3615
+ const { activeToolId } = (0, _embedpdf_react_interaction.useTool)();
3616
+ const { locale } = (0, _embedpdf_react_i18n.useLocale)();
3617
+ const { defaultLibrary } = useStampsConfig();
3618
+ const surface = (0, _embedpdf_react_shell.useSurface)("stamps");
3619
+ const fileRef = (0, react.useRef)(null);
3620
+ const [armedId, setArmedId] = (0, react.useState)(null);
3621
+ const [busy, setBusy] = (0, react.useState)(null);
3622
+ const [error, setError] = (0, react.useState)(null);
3623
+ const [picked, setPicked] = (0, react.useState)(ALL);
3624
+ const openedOn = surface.props?.libraryId;
3625
+ (0, react.useEffect)(() => {
3626
+ if (typeof openedOn === "string") setPicked(openedOn);
3627
+ }, [openedOn]);
3628
+ (0, react.useEffect)(() => {
3629
+ let live = true;
3630
+ setBusy("loading");
3631
+ restoreOnce(stamp).then(() => ensureDefaultLibrary(stamp, resolveStampsLocale(locale), defaultLibrary)).catch((err) => {
3632
+ console.error("[embedpdf] default stamps failed:", err);
3633
+ if (live) setError(t("demo.stampsError"));
3634
+ }).finally(() => live && setBusy(null));
3635
+ return () => {
3636
+ live = false;
3637
+ };
3638
+ }, [stamp, locale]);
3639
+ (0, react.useEffect)(() => store ? (0, _embedpdf_react_stamp.persistStampLibraries)(stamp, store, { except: [DEFAULT_LIBRARY_ID] }) : void 0, [stamp]);
3640
+ (0, react.useEffect)(() => {
3641
+ if (activeToolId !== "stamp") setArmedId(null);
3642
+ }, [activeToolId]);
3643
+ const arm = (asset) => {
3644
+ setError(null);
3645
+ setArmedId(asset.id);
3646
+ armAsset(asset.id).catch((err) => {
3647
+ console.error("[embedpdf] arm stamp failed:", err);
3648
+ setArmedId(null);
3649
+ setError(t("demo.stampsArmError"));
3650
+ });
3651
+ };
3652
+ const importPdf = (file) => {
3653
+ setError(null);
3654
+ setBusy("importing");
3655
+ stamp.importLibraryPdf(file, { name: file.name.replace(/\.pdf$/i, "") }).then((id) => setPicked(id)).catch((err) => {
3656
+ console.error("[embedpdf] stamp library import failed:", err);
3657
+ setError(t("demo.stampsImportError"));
3658
+ }).finally(() => setBusy(null));
3659
+ };
3660
+ /** The library as the PDF it is — title, registry, artwork — for Acrobat
3661
+ * or another viewer. */
3662
+ const exportPdf = (libraryId, name) => {
3663
+ const bytes = stamp.exportLibrary(libraryId);
3664
+ if (!bytes) return;
3665
+ const blob = new Blob([bytes], { type: "application/pdf" });
3666
+ const url = URL.createObjectURL(blob);
3667
+ const a = document.createElement("a");
3668
+ a.href = url;
3669
+ a.download = `${name || "stamps"}.pdf`;
3670
+ a.click();
3671
+ URL.revokeObjectURL(url);
3672
+ };
3673
+ const selectedId = libraries.length === 1 ? libraries[0].id : libraries.some((library) => library.id === picked) ? picked : ALL;
3674
+ const selected = selectedId === ALL ? null : libraries.find((l) => l.id === selectedId) ?? null;
3675
+ const shown = selectedId === ALL ? assets : assets.filter((a) => a.libraryId === selectedId);
3676
+ const isDefault = (libraryId) => libraryId === DEFAULT_LIBRARY_ID;
3677
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3678
+ className: "flex min-h-0 flex-1 flex-col",
3679
+ children: [
3680
+ libraries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3681
+ className: "border-border-subtle flex items-center gap-1 border-b p-2",
3682
+ children: [libraries.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3683
+ value: selectedId,
3684
+ onChange: (event) => setPicked(event.target.value),
3685
+ "aria-label": t("demo.stampsTitle"),
3686
+ className: "border-border bg-surface text-fg min-w-0 flex-1 rounded border px-2 py-1.5 text-sm",
3687
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3688
+ value: ALL,
3689
+ children: t("demo.stampsAll")
3690
+ }), libraries.map((library) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3691
+ value: library.id,
3692
+ children: library.name
3693
+ }, library.id))]
3694
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3695
+ className: "text-fg-muted min-w-0 flex-1 truncate px-1 text-xs font-semibold uppercase tracking-wide",
3696
+ children: libraries[0].name
3697
+ }), selected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3698
+ type: "button",
3699
+ onClick: () => exportPdf(selected.id, selected.name),
3700
+ title: t("demo.stampsExportLibrary"),
3701
+ className: "text-fg-muted hover:text-fg grid h-7 w-7 shrink-0 place-items-center rounded",
3702
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3703
+ name: "download",
3704
+ size: 14
3705
+ })
3706
+ }), !isDefault(selected.id) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3707
+ type: "button",
3708
+ onClick: () => void stamp.removeLibrary(selected.id),
3709
+ title: t("demo.stampsRemoveLibrary"),
3710
+ className: "text-fg-muted hover:text-fg grid h-7 w-7 shrink-0 place-items-center rounded",
3711
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3712
+ name: "trash",
3713
+ size: 14
3714
+ })
3715
+ })] })]
3716
+ }),
3717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3718
+ className: "min-h-0 flex-1 overflow-y-auto p-3",
3719
+ children: busy === "loading" && shown.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3720
+ className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
3721
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-border-subtle border-t-accent h-6 w-6 animate-spin rounded-full border-2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3722
+ className: "text-fg-muted text-sm",
3723
+ children: t("demo.stampsLoading")
3724
+ })]
3725
+ }) : shown.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3726
+ className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
3727
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3728
+ name: "rubberStamp",
3729
+ size: 32,
3730
+ className: "text-fg-muted"
3731
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3732
+ className: "text-fg-muted text-sm",
3733
+ children: t("demo.stampsEmpty")
3734
+ })]
3735
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3736
+ className: "grid grid-cols-2 gap-3",
3737
+ children: shown.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
3738
+ className: "group relative",
3739
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StampAssetCell, {
3740
+ asset,
3741
+ armed: armedId === asset.id,
3742
+ onArm: () => arm(asset)
3743
+ }), !isDefault(asset.libraryId) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3744
+ type: "button",
3745
+ onClick: () => void stamp.removeAsset(asset.id),
3746
+ title: t("demo.stampsRemoveStamp"),
3747
+ className: "bg-surface border-border text-fg-muted hover:text-fg absolute right-1 top-1 grid h-5 w-5 place-items-center rounded-full border opacity-0 shadow-sm focus:opacity-100 group-hover:opacity-100",
3748
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3749
+ name: "x",
3750
+ size: 12
3751
+ })
3752
+ })]
3753
+ }, asset.id))
3754
+ })
3755
+ }),
3756
+ (error || armedId) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3757
+ className: `border-border-subtle shrink-0 border-t px-3 py-2 text-xs ${error ? "text-red-600" : "text-fg-muted"}`,
3758
+ children: error ?? t("demo.stampsArmedHint")
3759
+ }),
3760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3761
+ className: "border-border-subtle shrink-0 border-t p-3",
3762
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3763
+ type: "button",
3764
+ disabled: busy != null,
3765
+ onClick: () => fileRef.current?.click(),
3766
+ title: t("demo.stampsImportHint"),
3767
+ className: "border-border text-fg hover:bg-hover flex w-full items-center justify-center gap-2 rounded-md border px-3 py-1.5 text-sm disabled:opacity-40",
3768
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3769
+ name: "rubberStampPlus",
3770
+ size: 16
3771
+ }), busy === "importing" ? t("demo.stampsImporting") : t("demo.stampsImport")]
3772
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3773
+ ref: fileRef,
3774
+ type: "file",
3775
+ accept: "application/pdf",
3776
+ className: "hidden",
3777
+ onChange: (e) => {
3778
+ const file = e.currentTarget.files?.[0];
3779
+ e.currentTarget.value = "";
3780
+ if (file) importPdf(file);
3781
+ }
3782
+ })]
3783
+ })
3784
+ ]
3785
+ });
3786
+ }
3787
+ /** One grid cell: the plugin's cached preview through the object-URL hook
3788
+ * (the DOM resource's lifetime belongs to the framework layer, not the
3789
+ * store). Image only, as v2 — the artwork IS the label; the label is the
3790
+ * tooltip for the curious. */
3791
+ function StampAssetCell({ asset, armed, onArm }) {
3792
+ const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
3793
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3794
+ type: "button",
3795
+ onClick: onArm,
3796
+ title: asset.label,
3797
+ "aria-pressed": armed,
3798
+ className: `flex aspect-square w-full items-center justify-center rounded-md border p-2 ${armed ? "border-accent bg-accent-light ring-accent ring-2" : "border-border-subtle hover:border-border hover:bg-hover"}`,
3799
+ children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3800
+ src: url,
3801
+ alt: asset.label,
3802
+ className: "max-h-full max-w-full object-contain"
3803
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3804
+ className: "text-fg truncate text-sm",
3805
+ children: asset.label
3806
+ })
3807
+ });
3808
+ }
3809
+ //#endregion
3484
3810
  //#region src/ui/panels.tsx
3485
3811
  /**
3486
3812
  * The static chrome around the toolbars: the left/right sidebars (shell
@@ -3574,10 +3900,11 @@ function RightSidebar() {
3574
3900
  const comment = (0, _embedpdf_react_shell.useSurface)("comment");
3575
3901
  const style = (0, _embedpdf_react_shell.useSurface)("annotation-style");
3576
3902
  const redaction = (0, _embedpdf_react_shell.useSurface)("redaction");
3577
- const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : null;
3903
+ 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;
3578
3905
  if (!active) return null;
3579
- const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : "demo.styleTitle";
3580
- const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : style.close;
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;
3581
3908
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3582
3909
  className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
3583
3910
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3594,7 +3921,7 @@ function RightSidebar() {
3594
3921
  size: 16
3595
3922
  })
3596
3923
  })]
3597
- }), active === "style" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedactionPanel, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsPanel, {}, documentId ?? "none")]
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")]
3598
3925
  });
3599
3926
  }
3600
3927
  function PageControls() {
@@ -3994,7 +4321,7 @@ function Shell() {
3994
4321
  * changing these props on a mounted viewer does not rebuild the workspace.
3995
4322
  */
3996
4323
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
3997
- const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-B7b8KjR3.cjs")).then((m) => m.es) };
4324
+ const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-BRYSk4rY.cjs")).then((m) => m.es) };
3998
4325
  const isDict = (v) => typeof v === "object" && v !== null;
3999
4326
  /**
4000
4327
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -4038,7 +4365,7 @@ function Booting() {
4038
4365
  })
4039
4366
  });
4040
4367
  }
4041
- function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, theme, themeTarget, onViewer }) {
4368
+ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, stamps, theme, themeTarget, onViewer }) {
4042
4369
  const [resolved] = (0, react.useState)(() => {
4043
4370
  const byId = new Map(require_commands.defaultCommands.map((c) => [c.id, c]));
4044
4371
  for (const c of commands ?? []) byId.set(c.id, c);
@@ -4069,6 +4396,7 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4069
4396
  commands: resolvedCommands,
4070
4397
  chrome: resolvedChrome,
4071
4398
  icons: icons ?? {},
4399
+ stamps: stamps ?? {},
4072
4400
  i18n: {
4073
4401
  locales,
4074
4402
  loaders,
@@ -4117,8 +4445,22 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
4117
4445
  start: "none",
4118
4446
  end: "open-arrow"
4119
4447
  } }
4448
+ }, {
4449
+ id: "image",
4450
+ extends: "stamp",
4451
+ source: {
4452
+ kind: "prompt",
4453
+ accept: "image/png,image/jpeg"
4454
+ }
4120
4455
  }] }),
4121
- (0, _embedpdf_react_actions.actionsPlugin)({ javascript: { enabled: true } }),
4456
+ (0, _embedpdf_react_stamp.stampPlugin)(),
4457
+ (0, _embedpdf_react_actions.actionsPlugin)({ javascript: {
4458
+ enabled: true,
4459
+ identity: {
4460
+ name: "John Doe",
4461
+ corporation: "Acme Inc"
4462
+ }
4463
+ } }),
4122
4464
  (0, _embedpdf_react_form.formPlugin)(),
4123
4465
  (0, _embedpdf_react_link.linkPlugin)(),
4124
4466
  (0, _embedpdf_react_redaction.redactionPlugin)(),
@@ -4226,6 +4568,12 @@ Object.defineProperty(exports, "StageToken", {
4226
4568
  return _embedpdf_react_stage.StageToken;
4227
4569
  }
4228
4570
  });
4571
+ Object.defineProperty(exports, "StampToken", {
4572
+ enumerable: true,
4573
+ get: function() {
4574
+ return _embedpdf_react_stamp.StampToken;
4575
+ }
4576
+ });
4229
4577
  Object.defineProperty(exports, "addItem", {
4230
4578
  enumerable: true,
4231
4579
  get: function() {