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

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,6 +33,7 @@ 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_redaction = require("@embedpdf/react/redaction");
36
+ let _embedpdf_react_actions = require("@embedpdf/react/actions");
36
37
  let _embedpdf_react_form = require("@embedpdf/react/form");
37
38
  let _embedpdf_react_link = require("@embedpdf/react/link");
38
39
  let _embedpdf_react_search = require("@embedpdf/react/search");
@@ -171,6 +172,10 @@ const en = {
171
172
  ungroup: "Ungroup",
172
173
  strip: "Annotation actions"
173
174
  },
175
+ selection: {
176
+ copy: "Copy",
177
+ strip: "Text selection actions"
178
+ },
174
179
  shapes: {
175
180
  rectangle: "Rectangle",
176
181
  circle: "Circle",
@@ -217,6 +222,24 @@ const en = {
217
222
  unknown: "The document could not be opened.",
218
223
  close: "Close document"
219
224
  },
225
+ annotation: {
226
+ comment: "Comment",
227
+ highlight: "Highlight",
228
+ underline: "Underline",
229
+ squiggly: "Squiggly",
230
+ strikeout: "Strikethrough",
231
+ square: "Square",
232
+ circle: "Circle",
233
+ line: "Line",
234
+ polygon: "Polygon",
235
+ polyline: "Polyline",
236
+ ink: "Ink",
237
+ freeText: "Text",
238
+ stamp: "Stamp",
239
+ caret: "Caret",
240
+ redact: "Redact",
241
+ fileAttachment: "Attachment"
242
+ },
220
243
  demo: {
221
244
  starting: "Starting viewer…",
222
245
  opening: "Opening document…",
@@ -233,6 +256,27 @@ const en = {
233
256
  searchNoResults: "No results",
234
257
  searchPage: "Page {page}",
235
258
  commentsTitle: "Comments",
259
+ commentsForbidden: "You don’t have access to comments on this document.",
260
+ commentsLoading: "Loading comments…",
261
+ commentsError: "Comments failed to load.",
262
+ commentsRetry: "Retry",
263
+ commentsEmpty: "No comments yet. Use the note tool to start a thread.",
264
+ commentsAnonymous: "Unknown",
265
+ commentsPage: "Page {page}",
266
+ commentCount: "{count} comment",
267
+ commentCountPlural: "{count} comments",
268
+ commentsReply: "Reply",
269
+ commentsReplyPlaceholder: "Reply…",
270
+ commentsAddPlaceholder: "Add a comment…",
271
+ commentsDelete: "Delete comment",
272
+ commentsDeleteThread: "Delete thread",
273
+ commentsEdit: "Edit comment",
274
+ commentsStatus: "Status",
275
+ commentsStatusNone: "No status",
276
+ commentsStatusAccepted: "Accepted",
277
+ commentsStatusRejected: "Rejected",
278
+ commentsStatusCancelled: "Cancelled",
279
+ commentsStatusCompleted: "Completed",
236
280
  styleTitle: "Style",
237
281
  empty: "Nothing here yet in this demo panel.",
238
282
  page: "Page {current} of {total}",
@@ -1323,19 +1367,129 @@ function AppToolbar({ bar, className }) {
1323
1367
  });
1324
1368
  }
1325
1369
  //#endregion
1326
- //#region src/ui/annotation-strip.tsx
1370
+ //#region src/ui/link-editor.tsx
1327
1371
  /**
1328
- * The annotation selection strip — v2's floating "selection menu" rebuilt as
1329
- * chrome. Three parties, none of them this file's business to compute:
1372
+ * The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
1373
+ * SAME selection anchor as the action strip (`<AnnotationMenu>` solves
1374
+ * WHERE), opening from the strip's link icon (`annotation:link` toggles the
1375
+ * `link-editor` shell surface) and rendering IN PLACE of the strip while
1376
+ * open — one anchored card at a time.
1330
1377
  *
1331
- * config/chrome.ts `strips.annotation` declares WHAT may appear
1332
- * each command's visible/enabled decides WHICH items show
1333
- * <AnnotationMenu> solves WHERE (camera transform,
1334
- * rotate-knob dodging, pointer isolation)
1335
- *
1336
- * useStripView is the live intersection: hidden commands drop out, empty
1337
- * groups vanish, null when nothing applies. This file only draws pixels.
1338
- * Mounted in the Stage `overlay` slot.
1378
+ * Reads the selection's current target from selection props (the `linkOf`
1379
+ * lens — parents derive from their committed child annotations) and writes
1380
+ * through the ONE `updateSelection({ link })` path; the plugin's reconciler
1381
+ * materializes/retargets the attached children. Links are a VERB on the
1382
+ * selection, not a style — which is why this is a popover, not a sidebar
1383
+ * section.
1384
+ */
1385
+ function ModeToggle({ active, title, onClick, children }) {
1386
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1387
+ type: "button",
1388
+ title,
1389
+ onClick,
1390
+ className: `rounded border px-2.5 py-1 text-xs font-medium transition-colors ${active ? "border-accent bg-accent text-on-accent" : "border-border bg-surface text-fg hover:bg-hover"}`,
1391
+ children
1392
+ });
1393
+ }
1394
+ function LinkEditorCard({ onClose }) {
1395
+ const t = (0, _embedpdf_react_i18n.useT)();
1396
+ const kernel = (0, _embedpdf_react_runtime.useKernel)();
1397
+ const anno = (0, _embedpdf_react_annotation.useAnnotation)();
1398
+ const value = (0, _embedpdf_react_annotation.useSelectionProps)().values.link ?? null;
1399
+ const [mode, setMode] = (0, react.useState)(value?.kind === "goto" ? "page" : "uri");
1400
+ const [uri, setUri] = (0, react.useState)(value?.kind === "uri" ? value.uri : "");
1401
+ const [pageNo, setPageNo] = (0, react.useState)("1");
1402
+ const apply = () => {
1403
+ if (mode === "uri") {
1404
+ const trimmed = uri.trim();
1405
+ if (!trimmed) return;
1406
+ anno.updateSelection({ link: {
1407
+ kind: "uri",
1408
+ uri: trimmed
1409
+ } });
1410
+ onClose();
1411
+ return;
1412
+ }
1413
+ const activeId = kernel.documents.activeId();
1414
+ const layout = (activeId ? kernel.getState().core.documents[activeId] : null)?.pages[Math.max(0, Number(pageNo) - 1)];
1415
+ if (!layout) return;
1416
+ anno.updateSelection({ link: {
1417
+ kind: "goto",
1418
+ destination: {
1419
+ kind: "fit",
1420
+ pageObjectNumber: layout.pageObjectNumber
1421
+ }
1422
+ } });
1423
+ onClose();
1424
+ };
1425
+ const inputCls = "border-border bg-surface text-fg focus:border-accent w-full rounded border px-2.5 py-1.5 text-sm outline-none";
1426
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1427
+ className: "border-border bg-surface w-64 rounded-lg border p-3 shadow-lg",
1428
+ children: [
1429
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1430
+ className: "mb-2 flex items-center justify-between",
1431
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1432
+ className: "text-fg text-sm font-semibold",
1433
+ children: t("commands.annotate.link")
1434
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1435
+ type: "button",
1436
+ onClick: onClose,
1437
+ className: "text-fg-muted hover:bg-hover grid h-6 w-6 place-items-center rounded",
1438
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1439
+ name: "x",
1440
+ size: 14
1441
+ })
1442
+ })]
1443
+ }),
1444
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1445
+ className: "mb-2 flex gap-1.5",
1446
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeToggle, {
1447
+ title: "Link to a URL",
1448
+ active: mode === "uri",
1449
+ onClick: () => setMode("uri"),
1450
+ children: "URL"
1451
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeToggle, {
1452
+ title: "Link to a page",
1453
+ active: mode === "page",
1454
+ onClick: () => setMode("page"),
1455
+ children: "Page"
1456
+ })]
1457
+ }),
1458
+ mode === "uri" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1459
+ type: "url",
1460
+ className: inputCls,
1461
+ placeholder: "https://…",
1462
+ autoFocus: true,
1463
+ value: uri,
1464
+ onChange: (e) => setUri(e.target.value),
1465
+ onKeyDown: (e) => e.key === "Enter" && apply()
1466
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1467
+ type: "number",
1468
+ min: 1,
1469
+ className: inputCls,
1470
+ placeholder: "Page number",
1471
+ autoFocus: true,
1472
+ value: pageNo,
1473
+ onChange: (e) => setPageNo(e.target.value),
1474
+ onKeyDown: (e) => e.key === "Enter" && apply()
1475
+ }),
1476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1477
+ type: "button",
1478
+ className: "bg-accent text-on-accent mt-2.5 w-full rounded px-2 py-1.5 text-sm font-medium",
1479
+ onClick: apply,
1480
+ children: value == null ? "Add link" : "Update link"
1481
+ })
1482
+ ]
1483
+ });
1484
+ }
1485
+ //#endregion
1486
+ //#region src/ui/strip-bar.tsx
1487
+ /**
1488
+ * The floating strip's PIXELS, shared by every contextual strip (annotation
1489
+ * selection, text selection, …): a rounded toolbar of command buttons with
1490
+ * derived group separators. The WHERE is each strip's menu component
1491
+ * (`<AnnotationMenu>`, `<SelectionMenu>`); the WHICH is the commands'
1492
+ * `visible`/`enabled` derivations via `useStripView`. This file only draws.
1339
1493
  */
1340
1494
  function StripButton({ cmd, run }) {
1341
1495
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1353,24 +1507,76 @@ function StripButton({ cmd, run }) {
1353
1507
  })
1354
1508
  });
1355
1509
  }
1510
+ function StripBar({ view, label }) {
1511
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1512
+ role: "toolbar",
1513
+ "aria-label": label,
1514
+ className: "border-border-subtle bg-elevated flex items-center gap-1 rounded-lg border p-1 shadow-xl",
1515
+ children: view.groups.map((g, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [i > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1516
+ "aria-hidden": true,
1517
+ className: "bg-border h-5 w-px"
1518
+ }), g.commands.map((cmd) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StripButton, {
1519
+ cmd,
1520
+ run: () => view.execute(cmd.id)
1521
+ }, cmd.id))] }, g.id))
1522
+ });
1523
+ }
1524
+ //#endregion
1525
+ //#region src/ui/annotation-strip.tsx
1526
+ /**
1527
+ * The ANNOTATION-selection floating strip:
1528
+ *
1529
+ * config/chrome.ts `strips.annotation` declares WHAT may appear
1530
+ * each command's visible/enabled decides WHICH items show
1531
+ * <AnnotationMenu> solves WHERE (camera transform,
1532
+ * rotate-knob dodging, pointer isolation)
1533
+ *
1534
+ * useStripView is the live intersection: hidden commands drop out, empty
1535
+ * groups vanish, null when nothing applies. Mounted in the Stage `overlay`
1536
+ * slot. The pixels are the shared <StripBar>.
1537
+ */
1356
1538
  function AnnotationStrip() {
1357
1539
  const t = (0, _embedpdf_react_i18n.useT)();
1358
1540
  const view = (0, _embedpdf_react_toolbar.useStripView)(useStripSchema("annotation"));
1541
+ const editor = (0, _embedpdf_react_shell.useSurface)("link-editor");
1542
+ const selection = (0, _embedpdf_react_annotation.useAnnotationSelection)();
1543
+ (0, react.useEffect)(() => {
1544
+ if (!selection.length && editor.isOpen) editor.close();
1545
+ }, [selection.length, editor.isOpen]);
1359
1546
  if (!view) return null;
1360
1547
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_annotation_menu.AnnotationMenu, {
1361
1548
  placement: "bottom",
1362
1549
  gap: 15,
1363
- children: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1364
- role: "toolbar",
1365
- "aria-label": t("commands.annotate.strip"),
1366
- className: "border-border-subtle bg-elevated flex items-center gap-1 rounded-lg border p-1 shadow-xl",
1367
- children: view.groups.map((g, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [i > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1368
- "aria-hidden": true,
1369
- className: "bg-border h-5 w-px"
1370
- }), g.commands.map((cmd) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StripButton, {
1371
- cmd,
1372
- run: () => view.execute(cmd.id)
1373
- }, cmd.id))] }, g.id))
1550
+ children: editor.isOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkEditorCard, { onClose: editor.close }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StripBar, {
1551
+ view,
1552
+ label: t("commands.annotate.strip")
1553
+ })
1554
+ });
1555
+ }
1556
+ //#endregion
1557
+ //#region src/ui/selection-strip.tsx
1558
+ /**
1559
+ * The TEXT-SELECTION floating strip:
1560
+ *
1561
+ * config/chrome.ts `strips.selection` declares WHAT may appear
1562
+ * each command's visible/enabled decides WHICH items show
1563
+ * <SelectionMenu> solves WHERE (settles on pointer-up,
1564
+ * camera transform, pointer isolation)
1565
+ *
1566
+ * `useStripView` is the live intersection: with copy denied (no
1567
+ * doc.text.copy) every command drops out and this renders nothing — no
1568
+ * empty bubble. Mounted in the Stage `overlay` slot.
1569
+ */
1570
+ function SelectionStrip() {
1571
+ const t = (0, _embedpdf_react_i18n.useT)();
1572
+ const view = (0, _embedpdf_react_toolbar.useStripView)(useStripSchema("selection"));
1573
+ if (!view) return null;
1574
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_selection.SelectionMenu, {
1575
+ placement: "bottom",
1576
+ gap: 12,
1577
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StripBar, {
1578
+ view,
1579
+ label: t("commands.selection.strip")
1374
1580
  })
1375
1581
  });
1376
1582
  }
@@ -2223,99 +2429,6 @@ function Toggle({ active, title, onClick, children }) {
2223
2429
  children
2224
2430
  });
2225
2431
  }
2226
- /**
2227
- * The link-target editor — v2's "Link / Go to link / Remove link" menu items,
2228
- * rebuilt on the schema: any kind whose table declares the `link` spec gets
2229
- * this control, and every write is the ONE `updateSelection({ link })` path
2230
- * (the plugin materializes/retargets/deletes the attached children).
2231
- */
2232
- function LinkTargetControl({ label, value, mixed, onChange }) {
2233
- const kernel = (0, _embedpdf_react_runtime.useKernel)();
2234
- const link = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_link.LinkToken);
2235
- const [mode, setMode] = (0, react.useState)(value?.kind === "goto" ? "page" : "uri");
2236
- const [uri, setUri] = (0, react.useState)(value?.kind === "uri" ? value.uri : "");
2237
- const [pageNo, setPageNo] = (0, react.useState)("1");
2238
- (0, react.useEffect)(() => {
2239
- setMode(value?.kind === "goto" ? "page" : "uri");
2240
- if (value?.kind === "uri") setUri(value.uri);
2241
- }, [value]);
2242
- const apply = () => {
2243
- if (mode === "uri") {
2244
- const trimmed = uri.trim();
2245
- if (trimmed) onChange({ link: {
2246
- kind: "uri",
2247
- uri: trimmed
2248
- } });
2249
- return;
2250
- }
2251
- const activeId = kernel.documents.activeId();
2252
- const layout = (activeId ? kernel.getState().core.documents[activeId] : null)?.pages[Math.max(0, Number(pageNo) - 1)];
2253
- if (layout) onChange({ link: {
2254
- kind: "goto",
2255
- destination: {
2256
- kind: "fit",
2257
- pageObjectNumber: layout.pageObjectNumber
2258
- }
2259
- } });
2260
- };
2261
- const inputCls = "border-border bg-surface text-fg w-full rounded border px-3 py-1.5 text-sm";
2262
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
2263
- label,
2264
- mixed,
2265
- children: [
2266
- value != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2267
- className: "mb-2 flex items-center gap-2",
2268
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2269
- type: "button",
2270
- className: "border-border bg-surface text-fg flex-1 rounded border px-2 py-1.5 text-sm",
2271
- onClick: () => link?.activate(value),
2272
- children: "Go to link"
2273
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2274
- type: "button",
2275
- className: "border-border bg-surface text-fg flex-1 rounded border px-2 py-1.5 text-sm",
2276
- onClick: () => onChange({ link: null }),
2277
- children: "Remove link"
2278
- })]
2279
- }),
2280
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2281
- className: "mb-2 flex gap-2",
2282
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
2283
- title: "Link to a URL",
2284
- active: mode === "uri",
2285
- onClick: () => setMode("uri"),
2286
- children: "URL"
2287
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
2288
- title: "Link to a page",
2289
- active: mode === "page",
2290
- onClick: () => setMode("page"),
2291
- children: "Page"
2292
- })]
2293
- }),
2294
- mode === "uri" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2295
- type: "url",
2296
- className: inputCls,
2297
- placeholder: "https://…",
2298
- value: uri,
2299
- onChange: (e) => setUri(e.target.value),
2300
- onKeyDown: (e) => e.key === "Enter" && apply()
2301
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2302
- type: "number",
2303
- min: 1,
2304
- className: inputCls,
2305
- placeholder: "Page number",
2306
- value: pageNo,
2307
- onChange: (e) => setPageNo(e.target.value),
2308
- onKeyDown: (e) => e.key === "Enter" && apply()
2309
- }),
2310
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2311
- type: "button",
2312
- className: "border-border bg-surface text-fg mt-2 w-full rounded border px-2 py-1.5 text-sm font-medium",
2313
- onClick: apply,
2314
- children: value == null ? "Add link" : "Update link"
2315
- })
2316
- ]
2317
- });
2318
- }
2319
2432
  function PropControl({ spec, value, mixed, onChange }) {
2320
2433
  switch (spec.key) {
2321
2434
  case "color":
@@ -2458,12 +2571,7 @@ function PropControl({ spec, value, mixed, onChange }) {
2458
2571
  onChange: (blendMode) => onChange({ blendMode })
2459
2572
  })
2460
2573
  });
2461
- case "link": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkTargetControl, {
2462
- label: spec.label,
2463
- value: value ?? null,
2464
- mixed,
2465
- onChange
2466
- });
2574
+ case "link": return null;
2467
2575
  default: return null;
2468
2576
  }
2469
2577
  }
@@ -2565,6 +2673,493 @@ function refKeyOfRedact(ref) {
2565
2673
  return JSON.stringify(ref);
2566
2674
  }
2567
2675
  //#endregion
2676
+ //#region src/ui/comment-config.ts
2677
+ /** `/C`-style Color → CSS. */
2678
+ const cssColor = (c) => c ? `rgb(${c.r}, ${c.g}, ${c.b})` : void 0;
2679
+ const TYPE_CONFIG = {
2680
+ text: {
2681
+ icon: "message",
2682
+ labelKey: "annotation.comment",
2683
+ label: "Comment"
2684
+ },
2685
+ highlight: {
2686
+ icon: "highlight",
2687
+ labelKey: "annotation.highlight",
2688
+ label: "Highlight"
2689
+ },
2690
+ underline: {
2691
+ icon: "underline",
2692
+ labelKey: "annotation.underline",
2693
+ label: "Underline"
2694
+ },
2695
+ squiggly: {
2696
+ icon: "squiggly",
2697
+ labelKey: "annotation.squiggly",
2698
+ label: "Squiggly"
2699
+ },
2700
+ strikeout: {
2701
+ icon: "strikethrough",
2702
+ labelKey: "annotation.strikeout",
2703
+ label: "Strikethrough"
2704
+ },
2705
+ square: {
2706
+ icon: "square",
2707
+ labelKey: "annotation.square",
2708
+ label: "Square"
2709
+ },
2710
+ circle: {
2711
+ icon: "circle",
2712
+ labelKey: "annotation.circle",
2713
+ label: "Circle"
2714
+ },
2715
+ line: {
2716
+ icon: "line",
2717
+ labelKey: "annotation.line",
2718
+ label: "Line"
2719
+ },
2720
+ polygon: {
2721
+ icon: "polygon",
2722
+ labelKey: "annotation.polygon",
2723
+ label: "Polygon"
2724
+ },
2725
+ polyline: {
2726
+ icon: "zigzag",
2727
+ labelKey: "annotation.polyline",
2728
+ label: "Polyline"
2729
+ },
2730
+ ink: {
2731
+ icon: "pencilMarker",
2732
+ labelKey: "annotation.ink",
2733
+ label: "Ink"
2734
+ },
2735
+ "free-text": {
2736
+ icon: "freeText",
2737
+ labelKey: "annotation.freeText",
2738
+ label: "Text"
2739
+ },
2740
+ stamp: {
2741
+ icon: "rubberStamp",
2742
+ labelKey: "annotation.stamp",
2743
+ label: "Stamp"
2744
+ },
2745
+ caret: {
2746
+ icon: "insertText",
2747
+ labelKey: "annotation.caret",
2748
+ label: "Caret"
2749
+ },
2750
+ redact: {
2751
+ icon: "redact",
2752
+ labelKey: "annotation.redact",
2753
+ label: "Redact"
2754
+ },
2755
+ "file-attachment": {
2756
+ icon: "paperclip",
2757
+ labelKey: "annotation.fileAttachment",
2758
+ label: "Attachment"
2759
+ }
2760
+ };
2761
+ const FALLBACK = {
2762
+ icon: "message",
2763
+ labelKey: "annotation.comment",
2764
+ label: "Comment"
2765
+ };
2766
+ const commentTypeConfig = (a) => TYPE_CONFIG[a.subtype] ?? FALLBACK;
2767
+ /**
2768
+ * The glyph's tint slots, read off the annotation itself — this is what makes
2769
+ * a card recognizable as "that yellow highlight on page 3". Free-text prefers
2770
+ * its `fontColor` override, since that is the color a reader actually sees.
2771
+ */
2772
+ const commentIconAccent = (a) => {
2773
+ const anyA = a;
2774
+ const primary = cssColor(anyA.fontColor ?? anyA.color);
2775
+ const secondary = cssColor(anyA.interiorColor);
2776
+ return {
2777
+ ...primary ? { primary } : {},
2778
+ ...secondary ? { secondary } : {}
2779
+ };
2780
+ };
2781
+ //#endregion
2782
+ //#region src/ui/comments-panel.tsx
2783
+ /**
2784
+ * The comments sidebar (right panel): a live view over the conversation
2785
+ * plane, in the shape v2's reviewers knew — page-grouped sections, and a
2786
+ * card that identifies its annotation at a glance by the type's glyph tinted
2787
+ * with that annotation's own colors. What v2 lacked, and this adds, is the
2788
+ * ISO 32000 §12.5.6.3 review status per thread.
2789
+ *
2790
+ * There is no per-page loading dance: the plugin hydrates the WHOLE document
2791
+ * at open (`listRawAll`), so the list is complete the moment `hydration()`
2792
+ * says so, whether or not a page was ever scrolled to.
2793
+ *
2794
+ * Selection is two-way, as in v2: clicking a card selects the annotation and
2795
+ * flies the camera to it (`stage.reveal`, the same verb search hits use);
2796
+ * selecting in the document scrolls that card into view here.
2797
+ *
2798
+ * Every action routes through the `comments` verbs — plain annotation writes
2799
+ * on the one optimistic pipeline — and every control is gated by
2800
+ * `permissionsFor`, the engine's own collab-resolver mirrors: what shows here
2801
+ * is what the engine will allow.
2802
+ */
2803
+ const REVIEW_STATES = [
2804
+ "none",
2805
+ "accepted",
2806
+ "rejected",
2807
+ "cancelled",
2808
+ "completed"
2809
+ ];
2810
+ const dateLabel = (iso) => {
2811
+ if (!iso) return "";
2812
+ const d = new Date(iso);
2813
+ if (Number.isNaN(d.getTime())) return "";
2814
+ return d.toLocaleDateString(void 0, {
2815
+ month: "short",
2816
+ day: "numeric"
2817
+ });
2818
+ };
2819
+ function CommentsPanel() {
2820
+ const t = (0, _embedpdf_react_i18n.useT)();
2821
+ const hydration = (0, _embedpdf_react_annotation.useCommentsHydration)();
2822
+ const comments = (0, _embedpdf_react_annotation.useComments)();
2823
+ const threads = (0, _embedpdf_react_annotation.useCommentThreads)();
2824
+ const anno = (0, _embedpdf_react_runtime.useCapability)(_embedpdf_react_annotation.AnnotationToken);
2825
+ const stage = (0, _embedpdf_react_runtime.useCapability)(_embedpdf_react_stage.StageToken);
2826
+ const selection = (0, _embedpdf_react_annotation.useAnnotationSelection)();
2827
+ const scrollRef = (0, react.useRef)(null);
2828
+ const cardRefs = (0, react.useRef)({});
2829
+ /** Which thread the document's selection points at — ANY member counts, so
2830
+ * selecting a reply's parent shape highlights the same card. */
2831
+ const selectedThreadKey = (0, react.useMemo)(() => {
2832
+ for (const id of selection) {
2833
+ const ref = anno.getSelected().find((a) => (0, _embedpdf_react_annotation.refKey)(a.ref) === id || a.nm === id)?.ref ?? null;
2834
+ const thread = ref ? comments.thread(ref) : null;
2835
+ if (thread) return (0, _embedpdf_react_annotation.refKey)(thread.root.ref);
2836
+ }
2837
+ return null;
2838
+ }, [
2839
+ selection,
2840
+ anno,
2841
+ comments
2842
+ ]);
2843
+ (0, react.useEffect)(() => {
2844
+ if (!selectedThreadKey || !scrollRef.current) return;
2845
+ const el = cardRefs.current[selectedThreadKey];
2846
+ if (!el) return;
2847
+ const container = scrollRef.current;
2848
+ const top = el.offsetTop - container.clientHeight / 2 + el.clientHeight / 2 - container.offsetTop;
2849
+ container.scrollTo({
2850
+ top,
2851
+ behavior: "smooth"
2852
+ });
2853
+ }, [selectedThreadKey]);
2854
+ /** Click a card: select the annotation AND fly the camera to it. */
2855
+ const goTo = (view) => {
2856
+ anno.select(view.root.ref);
2857
+ if (view.pageIndex < 0) return;
2858
+ stage?.reveal(view.pageIndex, {
2859
+ ...view.contentRect ? { rect: view.contentRect } : {},
2860
+ anchor: {
2861
+ x: "center",
2862
+ y: .35
2863
+ },
2864
+ behavior: "smooth"
2865
+ });
2866
+ };
2867
+ const byPage = (0, react.useMemo)(() => {
2868
+ const groups = /* @__PURE__ */ new Map();
2869
+ for (const v of threads) {
2870
+ const arr = groups.get(v.pageIndex);
2871
+ if (arr) arr.push(v);
2872
+ else groups.set(v.pageIndex, [v]);
2873
+ }
2874
+ return [...groups.entries()].sort((a, b) => a[0] - b[0]);
2875
+ }, [threads]);
2876
+ if (hydration.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Empty, {
2877
+ icon: "comment",
2878
+ text: t("demo.commentsLoading")
2879
+ });
2880
+ if (hydration.status === "forbidden") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Empty, {
2881
+ icon: "lock",
2882
+ text: t("demo.commentsForbidden")
2883
+ });
2884
+ if (hydration.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Empty, {
2885
+ icon: "alertTriangle",
2886
+ text: t("demo.commentsError"),
2887
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2888
+ type: "button",
2889
+ onClick: () => void comments.rehydrate(),
2890
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm font-medium",
2891
+ children: t("demo.commentsRetry")
2892
+ })
2893
+ });
2894
+ if (threads.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Empty, {
2895
+ icon: "comment",
2896
+ text: t("demo.commentsEmpty")
2897
+ });
2898
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2899
+ ref: scrollRef,
2900
+ className: "min-h-0 flex-1 overflow-y-auto",
2901
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2902
+ className: "flex flex-col gap-5 p-3",
2903
+ children: byPage.map(([pageIndex, group]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2904
+ className: "flex flex-col gap-2",
2905
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2906
+ className: "bg-surface sticky top-0 z-10",
2907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2908
+ className: "border-border-subtle border-b pb-1.5",
2909
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2910
+ className: "text-fg text-sm font-semibold",
2911
+ children: t("demo.commentsPage", { params: { page: group[0].pageLabel } })
2912
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2913
+ className: "text-fg-muted text-xs",
2914
+ children: t(group.length === 1 ? "demo.commentCount" : "demo.commentCountPlural", { params: { count: String(group.length) } })
2915
+ })]
2916
+ })
2917
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2918
+ className: "flex flex-col gap-2",
2919
+ children: group.map((view) => {
2920
+ const key = (0, _embedpdf_react_annotation.refKey)(view.root.ref);
2921
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreadCard, {
2922
+ view,
2923
+ selected: selectedThreadKey === key,
2924
+ onSelect: () => goTo(view),
2925
+ cardRef: (el) => {
2926
+ cardRefs.current[key] = el;
2927
+ }
2928
+ }, key);
2929
+ })
2930
+ })]
2931
+ }, pageIndex))
2932
+ })
2933
+ });
2934
+ }
2935
+ function Empty({ icon, text, children }) {
2936
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2937
+ className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
2938
+ children: [
2939
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2940
+ name: icon,
2941
+ size: 32,
2942
+ className: "text-fg-muted"
2943
+ }),
2944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2945
+ className: "text-fg-muted text-sm",
2946
+ children: text
2947
+ }),
2948
+ children
2949
+ ]
2950
+ });
2951
+ }
2952
+ function ThreadCard({ view, selected, onSelect, cardRef }) {
2953
+ const t = (0, _embedpdf_react_i18n.useT)();
2954
+ const comments = (0, _embedpdf_react_annotation.useComments)();
2955
+ const [reply, setReply] = (0, react.useState)("");
2956
+ const perms = comments.permissionsFor(view.root.ref);
2957
+ const config = commentTypeConfig(view.root);
2958
+ const status = view.review.mine?.state ?? "none";
2959
+ const latest = view.review.lastChange;
2960
+ const sendReply = () => {
2961
+ const text = reply.trim();
2962
+ if (!text) return;
2963
+ setReply("");
2964
+ comments.reply(view.root.ref, text);
2965
+ };
2966
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
2967
+ ref: cardRef,
2968
+ onClick: onSelect,
2969
+ className: `bg-surface cursor-pointer rounded-lg border shadow-sm transition-all hover:shadow-md ${selected ? "border-accent ring-accent/40 ring-2" : "border-border-subtle"}`,
2970
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2971
+ className: "flex flex-col gap-2 p-3",
2972
+ children: [
2973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2974
+ className: "flex items-start gap-2.5",
2975
+ children: [
2976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2977
+ className: "bg-hover mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-full",
2978
+ title: t(config.labelKey, { fallback: config.label }),
2979
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2980
+ name: config.icon,
2981
+ size: 18,
2982
+ accent: commentIconAccent(view.root)
2983
+ })
2984
+ }),
2985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2986
+ className: "min-w-0 flex-1",
2987
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2988
+ className: "flex items-baseline gap-2",
2989
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2990
+ className: "text-fg truncate text-sm font-medium",
2991
+ children: view.root.author ?? t("demo.commentsAnonymous")
2992
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2993
+ className: "text-fg-muted ml-auto shrink-0 text-xs",
2994
+ children: dateLabel(view.root.modified ?? view.root.created)
2995
+ })]
2996
+ }), latest && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2997
+ className: "bg-accent-light text-accent mt-1 inline-block rounded px-1.5 py-0.5 text-[11px] font-medium",
2998
+ children: [statusLabel(t, latest.state), latest.by ? ` — ${latest.by}` : ""]
2999
+ })]
3000
+ }),
3001
+ perms.canDeleteThread && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3002
+ type: "button",
3003
+ onClick: (e) => {
3004
+ e.stopPropagation();
3005
+ comments.removeThread(view.root.ref);
3006
+ },
3007
+ className: "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded",
3008
+ title: t("demo.commentsDeleteThread"),
3009
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3010
+ name: "trash",
3011
+ size: 14
3012
+ })
3013
+ })
3014
+ ]
3015
+ }),
3016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentBody, {
3017
+ annotationRef: view.root.ref,
3018
+ text: view.root.contents ?? "",
3019
+ deletable: false
3020
+ }),
3021
+ view.replies.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3022
+ className: "border-border-subtle flex flex-col gap-2 border-t pt-2",
3023
+ children: view.replies.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Reply, { dto: r }, (0, _embedpdf_react_annotation.refKey)(r.ref)))
3024
+ }),
3025
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3026
+ className: "border-border-subtle flex flex-col gap-1.5 border-t pt-2",
3027
+ children: [perms.canSetStatus && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3028
+ className: "text-fg-muted flex items-center gap-2 text-xs",
3029
+ onClick: (e) => e.stopPropagation(),
3030
+ children: [t("demo.commentsStatus"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3031
+ value: status,
3032
+ onChange: (e) => void comments.setStatus(view.root.ref, e.target.value),
3033
+ className: "border-border bg-surface text-fg flex-1 rounded-md border px-1.5 py-1 text-xs outline-none",
3034
+ children: REVIEW_STATES.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3035
+ value: s,
3036
+ children: statusLabel(t, s)
3037
+ }, s))
3038
+ })]
3039
+ }), perms.canReply && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3040
+ className: "flex items-center gap-1.5",
3041
+ onClick: (e) => e.stopPropagation(),
3042
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3043
+ type: "text",
3044
+ value: reply,
3045
+ placeholder: view.root.contents || view.replies.length ? t("demo.commentsReplyPlaceholder") : t("demo.commentsAddPlaceholder"),
3046
+ onChange: (e) => setReply(e.target.value),
3047
+ onKeyDown: (e) => e.key === "Enter" && sendReply(),
3048
+ className: "border-border bg-surface text-fg focus:border-accent w-full min-w-0 flex-1 rounded-md border px-2 py-1 text-sm outline-none"
3049
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3050
+ type: "button",
3051
+ onClick: sendReply,
3052
+ disabled: !reply.trim(),
3053
+ className: "text-accent disabled:text-fg-muted grid h-7 w-7 shrink-0 place-items-center rounded-md",
3054
+ title: t("demo.commentsReply"),
3055
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3056
+ name: "arrowForwardUp",
3057
+ size: 16
3058
+ })
3059
+ })]
3060
+ })]
3061
+ })
3062
+ ]
3063
+ })
3064
+ });
3065
+ }
3066
+ function Reply({ dto }) {
3067
+ const t = (0, _embedpdf_react_i18n.useT)();
3068
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3069
+ className: "border-border-subtle ml-1 border-l-2 pl-2",
3070
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3071
+ className: "flex items-baseline gap-2",
3072
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3073
+ className: "text-fg-secondary text-xs font-medium",
3074
+ children: dto.author ?? t("demo.commentsAnonymous")
3075
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3076
+ className: "text-fg-muted text-[11px]",
3077
+ children: dateLabel(dto.modified ?? dto.created)
3078
+ })]
3079
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentBody, {
3080
+ annotationRef: dto.ref,
3081
+ text: dto.contents ?? "",
3082
+ deletable: true
3083
+ })]
3084
+ });
3085
+ }
3086
+ /** One comment's text with inline edit (own, unlocked) and delete (replies). */
3087
+ function CommentBody({ annotationRef, text, deletable }) {
3088
+ const t = (0, _embedpdf_react_i18n.useT)();
3089
+ const comments = (0, _embedpdf_react_annotation.useComments)();
3090
+ const perms = comments.permissionsFor(annotationRef);
3091
+ const [draft, setDraft] = (0, react.useState)(null);
3092
+ if (draft !== null) {
3093
+ const save = () => {
3094
+ const next = draft.trim();
3095
+ setDraft(null);
3096
+ if (next && next !== text) comments.edit(annotationRef, next);
3097
+ };
3098
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3099
+ value: draft,
3100
+ autoFocus: true,
3101
+ rows: 2,
3102
+ onClick: (e) => e.stopPropagation(),
3103
+ onChange: (e) => setDraft(e.target.value),
3104
+ onBlur: save,
3105
+ onKeyDown: (e) => {
3106
+ if (e.key === "Enter" && !e.shiftKey) {
3107
+ e.preventDefault();
3108
+ save();
3109
+ }
3110
+ if (e.key === "Escape") setDraft(null);
3111
+ },
3112
+ className: "border-accent bg-surface text-fg w-full resize-none rounded-md border px-2 py-1 text-sm outline-none"
3113
+ });
3114
+ }
3115
+ if (!text && !perms.canEditText && !deletable) return null;
3116
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3117
+ className: "group/body flex items-start gap-1",
3118
+ children: [
3119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3120
+ className: "text-fg min-w-0 flex-1 whitespace-pre-wrap text-sm",
3121
+ children: text
3122
+ }),
3123
+ perms.canEditText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3124
+ type: "button",
3125
+ onClick: (e) => {
3126
+ e.stopPropagation();
3127
+ setDraft(text);
3128
+ },
3129
+ className: "text-fg-muted hover:text-fg grid h-5 w-5 shrink-0 place-items-center rounded opacity-0 group-hover/body:opacity-100",
3130
+ title: t("demo.commentsEdit"),
3131
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3132
+ name: "pencilMarker",
3133
+ size: 12
3134
+ })
3135
+ }),
3136
+ deletable && perms.canDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3137
+ type: "button",
3138
+ onClick: (e) => {
3139
+ e.stopPropagation();
3140
+ comments.remove(annotationRef);
3141
+ },
3142
+ className: "text-fg-muted hover:text-fg grid h-5 w-5 shrink-0 place-items-center rounded opacity-0 group-hover/body:opacity-100",
3143
+ title: t("demo.commentsDelete"),
3144
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3145
+ name: "trash",
3146
+ size: 12
3147
+ })
3148
+ })
3149
+ ]
3150
+ });
3151
+ }
3152
+ function statusLabel(t, state) {
3153
+ switch (state) {
3154
+ case "none": return t("demo.commentsStatusNone");
3155
+ case "accepted": return t("demo.commentsStatusAccepted");
3156
+ case "rejected": return t("demo.commentsStatusRejected");
3157
+ case "cancelled": return t("demo.commentsStatusCancelled");
3158
+ case "completed": return t("demo.commentsStatusCompleted");
3159
+ default: return state;
3160
+ }
3161
+ }
3162
+ //#endregion
2568
3163
  //#region src/ui/redaction-panel.tsx
2569
3164
  /**
2570
3165
  * The pending-redactions review panel (right sidebar). The list is a LIVE view
@@ -2999,13 +3594,7 @@ function RightSidebar() {
2999
3594
  size: 16
3000
3595
  })
3001
3596
  })]
3002
- }), 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)("div", {
3003
- className: "p-3",
3004
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3005
- className: "text-fg-muted text-sm",
3006
- children: t("demo.empty")
3007
- })
3008
- })]
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")]
3009
3598
  });
3010
3599
  }
3011
3600
  function PageControls() {
@@ -3325,7 +3914,7 @@ function DocumentArea({ children }) {
3325
3914
  function Shell() {
3326
3915
  (0, _embedpdf_react_commands.useCommandShortcuts)();
3327
3916
  (0, _embedpdf_react_annotation.useFilePickerProvider)();
3328
- (0, _embedpdf_react_form.useFormScriptingProvider)();
3917
+ (0, _embedpdf_react_actions.useActionsUiAdapter)();
3329
3918
  const schema = useChromeSchema();
3330
3919
  const frame = schema.frame ?? {};
3331
3920
  const toolbarEdge = frame.toolbar ?? "top";
@@ -3354,10 +3943,12 @@ function Shell() {
3354
3943
  className: "relative min-w-0 flex-1",
3355
3944
  children: [
3356
3945
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArmedToolCursor, {}),
3946
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_selection.SelectionClipboard, {}),
3357
3947
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_stage.Stage, {
3358
- interaction: true,
3359
3948
  overlay: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3360
3949
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationStrip, {}),
3950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionStrip, {}),
3951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_selection.SelectionHandles, {}),
3361
3952
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "y" }),
3362
3953
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_scrollbar.Scrollbar, { axis: "x" })
3363
3954
  ] }),
@@ -3365,7 +3956,6 @@ function Shell() {
3365
3956
  style: { background: "var(--ep-canvas)" },
3366
3957
  children: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3367
3958
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_render.RenderLayer, { annotations: false }),
3368
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_render.TileLayer, { annotations: false }),
3369
3959
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_selection.SelectionLayer, {}),
3370
3960
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_search.SearchLayer, {}),
3371
3961
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_link.LinkLayer, {}),
@@ -3404,7 +3994,7 @@ function Shell() {
3404
3994
  * changing these props on a mounted viewer does not rebuild the workspace.
3405
3995
  */
3406
3996
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
3407
- const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-C5084JO6.cjs")).then((m) => m.es) };
3997
+ const BUILTIN_LOADERS = { es: () => Promise.resolve().then(() => require("./es-B7b8KjR3.cjs")).then((m) => m.es) };
3408
3998
  const isDict = (v) => typeof v === "object" && v !== null;
3409
3999
  /**
3410
4000
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -3493,10 +4083,7 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
3493
4083
  for (const c of resolved.commands) if (c.icon && !(c.icon in ICON_PATHS) && !(c.icon in resolved.icons)) console.warn(`[embedpdf] command "${c.id}": unknown icon "${c.icon}"`);
3494
4084
  }, [resolved]);
3495
4085
  const [plugins] = (0, react.useState)(() => [
3496
- (0, _embedpdf_react_stage.stagePlugin)({
3497
- layout: "vertical",
3498
- interaction: true
3499
- }),
4086
+ (0, _embedpdf_react_stage.stagePlugin)({ layout: "vertical" }),
3500
4087
  (0, _embedpdf_react_stage.stagePlugin)({
3501
4088
  id: "stage-thumbs",
3502
4089
  token: ThumbsStageToken,
@@ -3520,7 +4107,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
3520
4107
  }),
3521
4108
  (0, _embedpdf_react_render.renderPlugin)(),
3522
4109
  (0, _embedpdf_react_page_edit.pageEditPlugin)(),
3523
- (0, _embedpdf_react_interaction.interactionPlugin)({ defaultTool: "pointer" }),
4110
+ (0, _embedpdf_react_interaction.interactionPlugin)(),
4111
+ (0, _embedpdf_react_interaction.feedbackPlugin)({ provider: _embedpdf_react_interaction.vibrationFeedback }),
3524
4112
  (0, _embedpdf_react_selection.selectionPlugin)(),
3525
4113
  (0, _embedpdf_react_annotation.annotationPlugin)({ tools: [{
3526
4114
  id: "arrow",
@@ -3530,7 +4118,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
3530
4118
  end: "open-arrow"
3531
4119
  } }
3532
4120
  }] }),
3533
- (0, _embedpdf_react_form.formPlugin)({ scripting: { enabled: true } }),
4121
+ (0, _embedpdf_react_actions.actionsPlugin)({ javascript: { enabled: true } }),
4122
+ (0, _embedpdf_react_form.formPlugin)(),
3534
4123
  (0, _embedpdf_react_link.linkPlugin)(),
3535
4124
  (0, _embedpdf_react_redaction.redactionPlugin)(),
3536
4125
  (0, _embedpdf_react_search.searchPlugin)(),