@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.js CHANGED
@@ -3,14 +3,15 @@ import { TOOL_ICONS, defaultCommands } from "./commands.js";
3
3
  import { Fragment, createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { DocumentGate, DocumentsToken, Viewer, createCapabilityToken, definePlugin, useCapability, useDocumentId, useDocumentStatus, useDocuments, useKernel, useOptionalCapability, useOptionalSelector, useSelector } from "@embedpdf/react/runtime";
5
5
  import { Stage, StageToken, StageToken as StageToken$1, stagePlugin, usePages } from "@embedpdf/react/stage";
6
- import { RenderLayer, TileLayer, renderPlugin } from "@embedpdf/react/render";
6
+ import { RenderLayer, renderPlugin } from "@embedpdf/react/render";
7
7
  import { pageEditPlugin } from "@embedpdf/react/page-edit";
8
- import { InteractionToken, InteractionToken as InteractionToken$1, interactionPlugin, useTool, useToolCursor } from "@embedpdf/react/interaction";
9
- import { SelectionLayer, SelectionToken, selectionPlugin } from "@embedpdf/react/selection";
10
- import { AnnotationLayer, AnnotationToken, AnnotationToken as AnnotationToken$1, annotationPlugin, useAnnotation, useAnnotationDefaults, useAnnotationSelected, useFilePickerProvider, useSelectionFlags, useSelectionProps } from "@embedpdf/react/annotation";
8
+ import { InteractionToken, InteractionToken as InteractionToken$1, feedbackPlugin, interactionPlugin, useTool, useToolCursor, vibrationFeedback } from "@embedpdf/react/interaction";
9
+ import { SelectionClipboard, SelectionHandles, SelectionLayer, SelectionMenu, SelectionToken, selectionPlugin } from "@embedpdf/react/selection";
10
+ import { AnnotationLayer, AnnotationToken, AnnotationToken as AnnotationToken$1, annotationPlugin, refKey, useAnnotation, useAnnotationDefaults, useAnnotationSelected, useAnnotationSelection, useCommentThreads, useComments, useCommentsHydration, useFilePickerProvider, useSelectionFlags, useSelectionProps } from "@embedpdf/react/annotation";
11
11
  import { RedactionToken, RedactionToken as RedactionToken$1, redactionPlugin, usePendingRedactions, useRedaction } from "@embedpdf/react/redaction";
12
- import { FormToken, formPlugin, formWidgetRenderer, useFormScriptingProvider } from "@embedpdf/react/form";
13
- import { LinkLayer, LinkToken, linkPlugin } from "@embedpdf/react/link";
12
+ import { actionsPlugin, useActionsUiAdapter } from "@embedpdf/react/actions";
13
+ import { FormToken, formPlugin, formWidgetRenderer } from "@embedpdf/react/form";
14
+ import { LinkLayer, linkPlugin } from "@embedpdf/react/link";
14
15
  import { SearchLayer, SearchToken, SearchToken as SearchToken$1, searchPlugin, useSearch, useSearchState, validateSearchQuery } from "@embedpdf/react/search";
15
16
  import { I18nToken, i18nPlugin, negotiateLocale, useT } from "@embedpdf/react/i18n";
16
17
  import { CommandsToken, CommandsToken as CommandsToken$1, commandsPlugin, resolvedCommandsEqual, useCommand, useCommandShortcuts, useCommands } from "@embedpdf/react/commands";
@@ -147,6 +148,10 @@ const en = {
147
148
  ungroup: "Ungroup",
148
149
  strip: "Annotation actions"
149
150
  },
151
+ selection: {
152
+ copy: "Copy",
153
+ strip: "Text selection actions"
154
+ },
150
155
  shapes: {
151
156
  rectangle: "Rectangle",
152
157
  circle: "Circle",
@@ -193,6 +198,24 @@ const en = {
193
198
  unknown: "The document could not be opened.",
194
199
  close: "Close document"
195
200
  },
201
+ annotation: {
202
+ comment: "Comment",
203
+ highlight: "Highlight",
204
+ underline: "Underline",
205
+ squiggly: "Squiggly",
206
+ strikeout: "Strikethrough",
207
+ square: "Square",
208
+ circle: "Circle",
209
+ line: "Line",
210
+ polygon: "Polygon",
211
+ polyline: "Polyline",
212
+ ink: "Ink",
213
+ freeText: "Text",
214
+ stamp: "Stamp",
215
+ caret: "Caret",
216
+ redact: "Redact",
217
+ fileAttachment: "Attachment"
218
+ },
196
219
  demo: {
197
220
  starting: "Starting viewer…",
198
221
  opening: "Opening document…",
@@ -209,6 +232,27 @@ const en = {
209
232
  searchNoResults: "No results",
210
233
  searchPage: "Page {page}",
211
234
  commentsTitle: "Comments",
235
+ commentsForbidden: "You don’t have access to comments on this document.",
236
+ commentsLoading: "Loading comments…",
237
+ commentsError: "Comments failed to load.",
238
+ commentsRetry: "Retry",
239
+ commentsEmpty: "No comments yet. Use the note tool to start a thread.",
240
+ commentsAnonymous: "Unknown",
241
+ commentsPage: "Page {page}",
242
+ commentCount: "{count} comment",
243
+ commentCountPlural: "{count} comments",
244
+ commentsReply: "Reply",
245
+ commentsReplyPlaceholder: "Reply…",
246
+ commentsAddPlaceholder: "Add a comment…",
247
+ commentsDelete: "Delete comment",
248
+ commentsDeleteThread: "Delete thread",
249
+ commentsEdit: "Edit comment",
250
+ commentsStatus: "Status",
251
+ commentsStatusNone: "No status",
252
+ commentsStatusAccepted: "Accepted",
253
+ commentsStatusRejected: "Rejected",
254
+ commentsStatusCancelled: "Cancelled",
255
+ commentsStatusCompleted: "Completed",
212
256
  styleTitle: "Style",
213
257
  empty: "Nothing here yet in this demo panel.",
214
258
  page: "Page {current} of {total}",
@@ -1283,19 +1327,129 @@ function AppToolbar({ bar, className }) {
1283
1327
  });
1284
1328
  }
1285
1329
  //#endregion
1286
- //#region src/ui/annotation-strip.tsx
1330
+ //#region src/ui/link-editor.tsx
1287
1331
  /**
1288
- * The annotation selection strip — v2's floating "selection menu" rebuilt as
1289
- * chrome. Three parties, none of them this file's business to compute:
1332
+ * The link editor POPOVER — v2's "add a link" popup, reborn. It rides the
1333
+ * SAME selection anchor as the action strip (`<AnnotationMenu>` solves
1334
+ * WHERE), opening from the strip's link icon (`annotation:link` toggles the
1335
+ * `link-editor` shell surface) and rendering IN PLACE of the strip while
1336
+ * open — one anchored card at a time.
1290
1337
  *
1291
- * config/chrome.ts `strips.annotation` declares WHAT may appear
1292
- * each command's visible/enabled decides WHICH items show
1293
- * <AnnotationMenu> solves WHERE (camera transform,
1294
- * rotate-knob dodging, pointer isolation)
1295
- *
1296
- * useStripView is the live intersection: hidden commands drop out, empty
1297
- * groups vanish, null when nothing applies. This file only draws pixels.
1298
- * Mounted in the Stage `overlay` slot.
1338
+ * Reads the selection's current target from selection props (the `linkOf`
1339
+ * lens — parents derive from their committed child annotations) and writes
1340
+ * through the ONE `updateSelection({ link })` path; the plugin's reconciler
1341
+ * materializes/retargets the attached children. Links are a VERB on the
1342
+ * selection, not a style — which is why this is a popover, not a sidebar
1343
+ * section.
1344
+ */
1345
+ function ModeToggle({ active, title, onClick, children }) {
1346
+ return /* @__PURE__ */ jsx("button", {
1347
+ type: "button",
1348
+ title,
1349
+ onClick,
1350
+ 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"}`,
1351
+ children
1352
+ });
1353
+ }
1354
+ function LinkEditorCard({ onClose }) {
1355
+ const t = useT();
1356
+ const kernel = useKernel();
1357
+ const anno = useAnnotation();
1358
+ const value = useSelectionProps().values.link ?? null;
1359
+ const [mode, setMode] = useState(value?.kind === "goto" ? "page" : "uri");
1360
+ const [uri, setUri] = useState(value?.kind === "uri" ? value.uri : "");
1361
+ const [pageNo, setPageNo] = useState("1");
1362
+ const apply = () => {
1363
+ if (mode === "uri") {
1364
+ const trimmed = uri.trim();
1365
+ if (!trimmed) return;
1366
+ anno.updateSelection({ link: {
1367
+ kind: "uri",
1368
+ uri: trimmed
1369
+ } });
1370
+ onClose();
1371
+ return;
1372
+ }
1373
+ const activeId = kernel.documents.activeId();
1374
+ const layout = (activeId ? kernel.getState().core.documents[activeId] : null)?.pages[Math.max(0, Number(pageNo) - 1)];
1375
+ if (!layout) return;
1376
+ anno.updateSelection({ link: {
1377
+ kind: "goto",
1378
+ destination: {
1379
+ kind: "fit",
1380
+ pageObjectNumber: layout.pageObjectNumber
1381
+ }
1382
+ } });
1383
+ onClose();
1384
+ };
1385
+ 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";
1386
+ return /* @__PURE__ */ jsxs("div", {
1387
+ className: "border-border bg-surface w-64 rounded-lg border p-3 shadow-lg",
1388
+ children: [
1389
+ /* @__PURE__ */ jsxs("div", {
1390
+ className: "mb-2 flex items-center justify-between",
1391
+ children: [/* @__PURE__ */ jsx("span", {
1392
+ className: "text-fg text-sm font-semibold",
1393
+ children: t("commands.annotate.link")
1394
+ }), /* @__PURE__ */ jsx("button", {
1395
+ type: "button",
1396
+ onClick: onClose,
1397
+ className: "text-fg-muted hover:bg-hover grid h-6 w-6 place-items-center rounded",
1398
+ children: /* @__PURE__ */ jsx(Icon, {
1399
+ name: "x",
1400
+ size: 14
1401
+ })
1402
+ })]
1403
+ }),
1404
+ /* @__PURE__ */ jsxs("div", {
1405
+ className: "mb-2 flex gap-1.5",
1406
+ children: [/* @__PURE__ */ jsx(ModeToggle, {
1407
+ title: "Link to a URL",
1408
+ active: mode === "uri",
1409
+ onClick: () => setMode("uri"),
1410
+ children: "URL"
1411
+ }), /* @__PURE__ */ jsx(ModeToggle, {
1412
+ title: "Link to a page",
1413
+ active: mode === "page",
1414
+ onClick: () => setMode("page"),
1415
+ children: "Page"
1416
+ })]
1417
+ }),
1418
+ mode === "uri" ? /* @__PURE__ */ jsx("input", {
1419
+ type: "url",
1420
+ className: inputCls,
1421
+ placeholder: "https://…",
1422
+ autoFocus: true,
1423
+ value: uri,
1424
+ onChange: (e) => setUri(e.target.value),
1425
+ onKeyDown: (e) => e.key === "Enter" && apply()
1426
+ }) : /* @__PURE__ */ jsx("input", {
1427
+ type: "number",
1428
+ min: 1,
1429
+ className: inputCls,
1430
+ placeholder: "Page number",
1431
+ autoFocus: true,
1432
+ value: pageNo,
1433
+ onChange: (e) => setPageNo(e.target.value),
1434
+ onKeyDown: (e) => e.key === "Enter" && apply()
1435
+ }),
1436
+ /* @__PURE__ */ jsx("button", {
1437
+ type: "button",
1438
+ className: "bg-accent text-on-accent mt-2.5 w-full rounded px-2 py-1.5 text-sm font-medium",
1439
+ onClick: apply,
1440
+ children: value == null ? "Add link" : "Update link"
1441
+ })
1442
+ ]
1443
+ });
1444
+ }
1445
+ //#endregion
1446
+ //#region src/ui/strip-bar.tsx
1447
+ /**
1448
+ * The floating strip's PIXELS, shared by every contextual strip (annotation
1449
+ * selection, text selection, …): a rounded toolbar of command buttons with
1450
+ * derived group separators. The WHERE is each strip's menu component
1451
+ * (`<AnnotationMenu>`, `<SelectionMenu>`); the WHICH is the commands'
1452
+ * `visible`/`enabled` derivations via `useStripView`. This file only draws.
1299
1453
  */
1300
1454
  function StripButton({ cmd, run }) {
1301
1455
  return /* @__PURE__ */ jsx("button", {
@@ -1313,24 +1467,76 @@ function StripButton({ cmd, run }) {
1313
1467
  })
1314
1468
  });
1315
1469
  }
1470
+ function StripBar({ view, label }) {
1471
+ return /* @__PURE__ */ jsx("div", {
1472
+ role: "toolbar",
1473
+ "aria-label": label,
1474
+ className: "border-border-subtle bg-elevated flex items-center gap-1 rounded-lg border p-1 shadow-xl",
1475
+ children: view.groups.map((g, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && /* @__PURE__ */ jsx("span", {
1476
+ "aria-hidden": true,
1477
+ className: "bg-border h-5 w-px"
1478
+ }), g.commands.map((cmd) => /* @__PURE__ */ jsx(StripButton, {
1479
+ cmd,
1480
+ run: () => view.execute(cmd.id)
1481
+ }, cmd.id))] }, g.id))
1482
+ });
1483
+ }
1484
+ //#endregion
1485
+ //#region src/ui/annotation-strip.tsx
1486
+ /**
1487
+ * The ANNOTATION-selection floating strip:
1488
+ *
1489
+ * config/chrome.ts `strips.annotation` declares WHAT may appear
1490
+ * each command's visible/enabled decides WHICH items show
1491
+ * <AnnotationMenu> solves WHERE (camera transform,
1492
+ * rotate-knob dodging, pointer isolation)
1493
+ *
1494
+ * useStripView is the live intersection: hidden commands drop out, empty
1495
+ * groups vanish, null when nothing applies. Mounted in the Stage `overlay`
1496
+ * slot. The pixels are the shared <StripBar>.
1497
+ */
1316
1498
  function AnnotationStrip() {
1317
1499
  const t = useT();
1318
1500
  const view = useStripView(useStripSchema("annotation"));
1501
+ const editor = useSurface("link-editor");
1502
+ const selection = useAnnotationSelection();
1503
+ useEffect(() => {
1504
+ if (!selection.length && editor.isOpen) editor.close();
1505
+ }, [selection.length, editor.isOpen]);
1319
1506
  if (!view) return null;
1320
1507
  return /* @__PURE__ */ jsx(AnnotationMenu, {
1321
1508
  placement: "bottom",
1322
1509
  gap: 15,
1323
- children: () => /* @__PURE__ */ jsx("div", {
1324
- role: "toolbar",
1325
- "aria-label": t("commands.annotate.strip"),
1326
- className: "border-border-subtle bg-elevated flex items-center gap-1 rounded-lg border p-1 shadow-xl",
1327
- children: view.groups.map((g, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && /* @__PURE__ */ jsx("span", {
1328
- "aria-hidden": true,
1329
- className: "bg-border h-5 w-px"
1330
- }), g.commands.map((cmd) => /* @__PURE__ */ jsx(StripButton, {
1331
- cmd,
1332
- run: () => view.execute(cmd.id)
1333
- }, cmd.id))] }, g.id))
1510
+ children: editor.isOpen ? /* @__PURE__ */ jsx(LinkEditorCard, { onClose: editor.close }) : /* @__PURE__ */ jsx(StripBar, {
1511
+ view,
1512
+ label: t("commands.annotate.strip")
1513
+ })
1514
+ });
1515
+ }
1516
+ //#endregion
1517
+ //#region src/ui/selection-strip.tsx
1518
+ /**
1519
+ * The TEXT-SELECTION floating strip:
1520
+ *
1521
+ * config/chrome.ts `strips.selection` declares WHAT may appear
1522
+ * each command's visible/enabled decides WHICH items show
1523
+ * <SelectionMenu> solves WHERE (settles on pointer-up,
1524
+ * camera transform, pointer isolation)
1525
+ *
1526
+ * `useStripView` is the live intersection: with copy denied (no
1527
+ * doc.text.copy) every command drops out and this renders nothing — no
1528
+ * empty bubble. Mounted in the Stage `overlay` slot.
1529
+ */
1530
+ function SelectionStrip() {
1531
+ const t = useT();
1532
+ const view = useStripView(useStripSchema("selection"));
1533
+ if (!view) return null;
1534
+ return /* @__PURE__ */ jsx(SelectionMenu, {
1535
+ placement: "bottom",
1536
+ gap: 12,
1537
+ children: /* @__PURE__ */ jsx(StripBar, {
1538
+ view,
1539
+ label: t("commands.selection.strip")
1334
1540
  })
1335
1541
  });
1336
1542
  }
@@ -2183,99 +2389,6 @@ function Toggle({ active, title, onClick, children }) {
2183
2389
  children
2184
2390
  });
2185
2391
  }
2186
- /**
2187
- * The link-target editor — v2's "Link / Go to link / Remove link" menu items,
2188
- * rebuilt on the schema: any kind whose table declares the `link` spec gets
2189
- * this control, and every write is the ONE `updateSelection({ link })` path
2190
- * (the plugin materializes/retargets/deletes the attached children).
2191
- */
2192
- function LinkTargetControl({ label, value, mixed, onChange }) {
2193
- const kernel = useKernel();
2194
- const link = useOptionalCapability(LinkToken);
2195
- const [mode, setMode] = useState(value?.kind === "goto" ? "page" : "uri");
2196
- const [uri, setUri] = useState(value?.kind === "uri" ? value.uri : "");
2197
- const [pageNo, setPageNo] = useState("1");
2198
- useEffect(() => {
2199
- setMode(value?.kind === "goto" ? "page" : "uri");
2200
- if (value?.kind === "uri") setUri(value.uri);
2201
- }, [value]);
2202
- const apply = () => {
2203
- if (mode === "uri") {
2204
- const trimmed = uri.trim();
2205
- if (trimmed) onChange({ link: {
2206
- kind: "uri",
2207
- uri: trimmed
2208
- } });
2209
- return;
2210
- }
2211
- const activeId = kernel.documents.activeId();
2212
- const layout = (activeId ? kernel.getState().core.documents[activeId] : null)?.pages[Math.max(0, Number(pageNo) - 1)];
2213
- if (layout) onChange({ link: {
2214
- kind: "goto",
2215
- destination: {
2216
- kind: "fit",
2217
- pageObjectNumber: layout.pageObjectNumber
2218
- }
2219
- } });
2220
- };
2221
- const inputCls = "border-border bg-surface text-fg w-full rounded border px-3 py-1.5 text-sm";
2222
- return /* @__PURE__ */ jsxs(Field, {
2223
- label,
2224
- mixed,
2225
- children: [
2226
- value != null && /* @__PURE__ */ jsxs("div", {
2227
- className: "mb-2 flex items-center gap-2",
2228
- children: [/* @__PURE__ */ jsx("button", {
2229
- type: "button",
2230
- className: "border-border bg-surface text-fg flex-1 rounded border px-2 py-1.5 text-sm",
2231
- onClick: () => link?.activate(value),
2232
- children: "Go to link"
2233
- }), /* @__PURE__ */ jsx("button", {
2234
- type: "button",
2235
- className: "border-border bg-surface text-fg flex-1 rounded border px-2 py-1.5 text-sm",
2236
- onClick: () => onChange({ link: null }),
2237
- children: "Remove link"
2238
- })]
2239
- }),
2240
- /* @__PURE__ */ jsxs("div", {
2241
- className: "mb-2 flex gap-2",
2242
- children: [/* @__PURE__ */ jsx(Toggle, {
2243
- title: "Link to a URL",
2244
- active: mode === "uri",
2245
- onClick: () => setMode("uri"),
2246
- children: "URL"
2247
- }), /* @__PURE__ */ jsx(Toggle, {
2248
- title: "Link to a page",
2249
- active: mode === "page",
2250
- onClick: () => setMode("page"),
2251
- children: "Page"
2252
- })]
2253
- }),
2254
- mode === "uri" ? /* @__PURE__ */ jsx("input", {
2255
- type: "url",
2256
- className: inputCls,
2257
- placeholder: "https://…",
2258
- value: uri,
2259
- onChange: (e) => setUri(e.target.value),
2260
- onKeyDown: (e) => e.key === "Enter" && apply()
2261
- }) : /* @__PURE__ */ jsx("input", {
2262
- type: "number",
2263
- min: 1,
2264
- className: inputCls,
2265
- placeholder: "Page number",
2266
- value: pageNo,
2267
- onChange: (e) => setPageNo(e.target.value),
2268
- onKeyDown: (e) => e.key === "Enter" && apply()
2269
- }),
2270
- /* @__PURE__ */ jsx("button", {
2271
- type: "button",
2272
- className: "border-border bg-surface text-fg mt-2 w-full rounded border px-2 py-1.5 text-sm font-medium",
2273
- onClick: apply,
2274
- children: value == null ? "Add link" : "Update link"
2275
- })
2276
- ]
2277
- });
2278
- }
2279
2392
  function PropControl({ spec, value, mixed, onChange }) {
2280
2393
  switch (spec.key) {
2281
2394
  case "color":
@@ -2418,12 +2531,7 @@ function PropControl({ spec, value, mixed, onChange }) {
2418
2531
  onChange: (blendMode) => onChange({ blendMode })
2419
2532
  })
2420
2533
  });
2421
- case "link": return /* @__PURE__ */ jsx(LinkTargetControl, {
2422
- label: spec.label,
2423
- value: value ?? null,
2424
- mixed,
2425
- onChange
2426
- });
2534
+ case "link": return null;
2427
2535
  default: return null;
2428
2536
  }
2429
2537
  }
@@ -2525,6 +2633,493 @@ function refKeyOfRedact(ref) {
2525
2633
  return JSON.stringify(ref);
2526
2634
  }
2527
2635
  //#endregion
2636
+ //#region src/ui/comment-config.ts
2637
+ /** `/C`-style Color → CSS. */
2638
+ const cssColor = (c) => c ? `rgb(${c.r}, ${c.g}, ${c.b})` : void 0;
2639
+ const TYPE_CONFIG = {
2640
+ text: {
2641
+ icon: "message",
2642
+ labelKey: "annotation.comment",
2643
+ label: "Comment"
2644
+ },
2645
+ highlight: {
2646
+ icon: "highlight",
2647
+ labelKey: "annotation.highlight",
2648
+ label: "Highlight"
2649
+ },
2650
+ underline: {
2651
+ icon: "underline",
2652
+ labelKey: "annotation.underline",
2653
+ label: "Underline"
2654
+ },
2655
+ squiggly: {
2656
+ icon: "squiggly",
2657
+ labelKey: "annotation.squiggly",
2658
+ label: "Squiggly"
2659
+ },
2660
+ strikeout: {
2661
+ icon: "strikethrough",
2662
+ labelKey: "annotation.strikeout",
2663
+ label: "Strikethrough"
2664
+ },
2665
+ square: {
2666
+ icon: "square",
2667
+ labelKey: "annotation.square",
2668
+ label: "Square"
2669
+ },
2670
+ circle: {
2671
+ icon: "circle",
2672
+ labelKey: "annotation.circle",
2673
+ label: "Circle"
2674
+ },
2675
+ line: {
2676
+ icon: "line",
2677
+ labelKey: "annotation.line",
2678
+ label: "Line"
2679
+ },
2680
+ polygon: {
2681
+ icon: "polygon",
2682
+ labelKey: "annotation.polygon",
2683
+ label: "Polygon"
2684
+ },
2685
+ polyline: {
2686
+ icon: "zigzag",
2687
+ labelKey: "annotation.polyline",
2688
+ label: "Polyline"
2689
+ },
2690
+ ink: {
2691
+ icon: "pencilMarker",
2692
+ labelKey: "annotation.ink",
2693
+ label: "Ink"
2694
+ },
2695
+ "free-text": {
2696
+ icon: "freeText",
2697
+ labelKey: "annotation.freeText",
2698
+ label: "Text"
2699
+ },
2700
+ stamp: {
2701
+ icon: "rubberStamp",
2702
+ labelKey: "annotation.stamp",
2703
+ label: "Stamp"
2704
+ },
2705
+ caret: {
2706
+ icon: "insertText",
2707
+ labelKey: "annotation.caret",
2708
+ label: "Caret"
2709
+ },
2710
+ redact: {
2711
+ icon: "redact",
2712
+ labelKey: "annotation.redact",
2713
+ label: "Redact"
2714
+ },
2715
+ "file-attachment": {
2716
+ icon: "paperclip",
2717
+ labelKey: "annotation.fileAttachment",
2718
+ label: "Attachment"
2719
+ }
2720
+ };
2721
+ const FALLBACK = {
2722
+ icon: "message",
2723
+ labelKey: "annotation.comment",
2724
+ label: "Comment"
2725
+ };
2726
+ const commentTypeConfig = (a) => TYPE_CONFIG[a.subtype] ?? FALLBACK;
2727
+ /**
2728
+ * The glyph's tint slots, read off the annotation itself — this is what makes
2729
+ * a card recognizable as "that yellow highlight on page 3". Free-text prefers
2730
+ * its `fontColor` override, since that is the color a reader actually sees.
2731
+ */
2732
+ const commentIconAccent = (a) => {
2733
+ const anyA = a;
2734
+ const primary = cssColor(anyA.fontColor ?? anyA.color);
2735
+ const secondary = cssColor(anyA.interiorColor);
2736
+ return {
2737
+ ...primary ? { primary } : {},
2738
+ ...secondary ? { secondary } : {}
2739
+ };
2740
+ };
2741
+ //#endregion
2742
+ //#region src/ui/comments-panel.tsx
2743
+ /**
2744
+ * The comments sidebar (right panel): a live view over the conversation
2745
+ * plane, in the shape v2's reviewers knew — page-grouped sections, and a
2746
+ * card that identifies its annotation at a glance by the type's glyph tinted
2747
+ * with that annotation's own colors. What v2 lacked, and this adds, is the
2748
+ * ISO 32000 §12.5.6.3 review status per thread.
2749
+ *
2750
+ * There is no per-page loading dance: the plugin hydrates the WHOLE document
2751
+ * at open (`listRawAll`), so the list is complete the moment `hydration()`
2752
+ * says so, whether or not a page was ever scrolled to.
2753
+ *
2754
+ * Selection is two-way, as in v2: clicking a card selects the annotation and
2755
+ * flies the camera to it (`stage.reveal`, the same verb search hits use);
2756
+ * selecting in the document scrolls that card into view here.
2757
+ *
2758
+ * Every action routes through the `comments` verbs — plain annotation writes
2759
+ * on the one optimistic pipeline — and every control is gated by
2760
+ * `permissionsFor`, the engine's own collab-resolver mirrors: what shows here
2761
+ * is what the engine will allow.
2762
+ */
2763
+ const REVIEW_STATES = [
2764
+ "none",
2765
+ "accepted",
2766
+ "rejected",
2767
+ "cancelled",
2768
+ "completed"
2769
+ ];
2770
+ const dateLabel = (iso) => {
2771
+ if (!iso) return "";
2772
+ const d = new Date(iso);
2773
+ if (Number.isNaN(d.getTime())) return "";
2774
+ return d.toLocaleDateString(void 0, {
2775
+ month: "short",
2776
+ day: "numeric"
2777
+ });
2778
+ };
2779
+ function CommentsPanel() {
2780
+ const t = useT();
2781
+ const hydration = useCommentsHydration();
2782
+ const comments = useComments();
2783
+ const threads = useCommentThreads();
2784
+ const anno = useCapability(AnnotationToken$1);
2785
+ const stage = useCapability(StageToken$1);
2786
+ const selection = useAnnotationSelection();
2787
+ const scrollRef = useRef(null);
2788
+ const cardRefs = useRef({});
2789
+ /** Which thread the document's selection points at — ANY member counts, so
2790
+ * selecting a reply's parent shape highlights the same card. */
2791
+ const selectedThreadKey = useMemo(() => {
2792
+ for (const id of selection) {
2793
+ const ref = anno.getSelected().find((a) => refKey(a.ref) === id || a.nm === id)?.ref ?? null;
2794
+ const thread = ref ? comments.thread(ref) : null;
2795
+ if (thread) return refKey(thread.root.ref);
2796
+ }
2797
+ return null;
2798
+ }, [
2799
+ selection,
2800
+ anno,
2801
+ comments
2802
+ ]);
2803
+ useEffect(() => {
2804
+ if (!selectedThreadKey || !scrollRef.current) return;
2805
+ const el = cardRefs.current[selectedThreadKey];
2806
+ if (!el) return;
2807
+ const container = scrollRef.current;
2808
+ const top = el.offsetTop - container.clientHeight / 2 + el.clientHeight / 2 - container.offsetTop;
2809
+ container.scrollTo({
2810
+ top,
2811
+ behavior: "smooth"
2812
+ });
2813
+ }, [selectedThreadKey]);
2814
+ /** Click a card: select the annotation AND fly the camera to it. */
2815
+ const goTo = (view) => {
2816
+ anno.select(view.root.ref);
2817
+ if (view.pageIndex < 0) return;
2818
+ stage?.reveal(view.pageIndex, {
2819
+ ...view.contentRect ? { rect: view.contentRect } : {},
2820
+ anchor: {
2821
+ x: "center",
2822
+ y: .35
2823
+ },
2824
+ behavior: "smooth"
2825
+ });
2826
+ };
2827
+ const byPage = useMemo(() => {
2828
+ const groups = /* @__PURE__ */ new Map();
2829
+ for (const v of threads) {
2830
+ const arr = groups.get(v.pageIndex);
2831
+ if (arr) arr.push(v);
2832
+ else groups.set(v.pageIndex, [v]);
2833
+ }
2834
+ return [...groups.entries()].sort((a, b) => a[0] - b[0]);
2835
+ }, [threads]);
2836
+ if (hydration.status === "loading") return /* @__PURE__ */ jsx(Empty, {
2837
+ icon: "comment",
2838
+ text: t("demo.commentsLoading")
2839
+ });
2840
+ if (hydration.status === "forbidden") return /* @__PURE__ */ jsx(Empty, {
2841
+ icon: "lock",
2842
+ text: t("demo.commentsForbidden")
2843
+ });
2844
+ if (hydration.status === "error") return /* @__PURE__ */ jsx(Empty, {
2845
+ icon: "alertTriangle",
2846
+ text: t("demo.commentsError"),
2847
+ children: /* @__PURE__ */ jsx("button", {
2848
+ type: "button",
2849
+ onClick: () => void comments.rehydrate(),
2850
+ className: "bg-accent text-on-accent rounded-md px-3 py-1.5 text-sm font-medium",
2851
+ children: t("demo.commentsRetry")
2852
+ })
2853
+ });
2854
+ if (threads.length === 0) return /* @__PURE__ */ jsx(Empty, {
2855
+ icon: "comment",
2856
+ text: t("demo.commentsEmpty")
2857
+ });
2858
+ return /* @__PURE__ */ jsx("div", {
2859
+ ref: scrollRef,
2860
+ className: "min-h-0 flex-1 overflow-y-auto",
2861
+ children: /* @__PURE__ */ jsx("div", {
2862
+ className: "flex flex-col gap-5 p-3",
2863
+ children: byPage.map(([pageIndex, group]) => /* @__PURE__ */ jsxs("div", {
2864
+ className: "flex flex-col gap-2",
2865
+ children: [/* @__PURE__ */ jsx("div", {
2866
+ className: "bg-surface sticky top-0 z-10",
2867
+ children: /* @__PURE__ */ jsxs("div", {
2868
+ className: "border-border-subtle border-b pb-1.5",
2869
+ children: [/* @__PURE__ */ jsx("h3", {
2870
+ className: "text-fg text-sm font-semibold",
2871
+ children: t("demo.commentsPage", { params: { page: group[0].pageLabel } })
2872
+ }), /* @__PURE__ */ jsx("p", {
2873
+ className: "text-fg-muted text-xs",
2874
+ children: t(group.length === 1 ? "demo.commentCount" : "demo.commentCountPlural", { params: { count: String(group.length) } })
2875
+ })]
2876
+ })
2877
+ }), /* @__PURE__ */ jsx("ul", {
2878
+ className: "flex flex-col gap-2",
2879
+ children: group.map((view) => {
2880
+ const key = refKey(view.root.ref);
2881
+ return /* @__PURE__ */ jsx(ThreadCard, {
2882
+ view,
2883
+ selected: selectedThreadKey === key,
2884
+ onSelect: () => goTo(view),
2885
+ cardRef: (el) => {
2886
+ cardRefs.current[key] = el;
2887
+ }
2888
+ }, key);
2889
+ })
2890
+ })]
2891
+ }, pageIndex))
2892
+ })
2893
+ });
2894
+ }
2895
+ function Empty({ icon, text, children }) {
2896
+ return /* @__PURE__ */ jsxs("div", {
2897
+ className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
2898
+ children: [
2899
+ /* @__PURE__ */ jsx(Icon, {
2900
+ name: icon,
2901
+ size: 32,
2902
+ className: "text-fg-muted"
2903
+ }),
2904
+ /* @__PURE__ */ jsx("p", {
2905
+ className: "text-fg-muted text-sm",
2906
+ children: text
2907
+ }),
2908
+ children
2909
+ ]
2910
+ });
2911
+ }
2912
+ function ThreadCard({ view, selected, onSelect, cardRef }) {
2913
+ const t = useT();
2914
+ const comments = useComments();
2915
+ const [reply, setReply] = useState("");
2916
+ const perms = comments.permissionsFor(view.root.ref);
2917
+ const config = commentTypeConfig(view.root);
2918
+ const status = view.review.mine?.state ?? "none";
2919
+ const latest = view.review.lastChange;
2920
+ const sendReply = () => {
2921
+ const text = reply.trim();
2922
+ if (!text) return;
2923
+ setReply("");
2924
+ comments.reply(view.root.ref, text);
2925
+ };
2926
+ return /* @__PURE__ */ jsx("li", {
2927
+ ref: cardRef,
2928
+ onClick: onSelect,
2929
+ 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"}`,
2930
+ children: /* @__PURE__ */ jsxs("div", {
2931
+ className: "flex flex-col gap-2 p-3",
2932
+ children: [
2933
+ /* @__PURE__ */ jsxs("div", {
2934
+ className: "flex items-start gap-2.5",
2935
+ children: [
2936
+ /* @__PURE__ */ jsx("div", {
2937
+ className: "bg-hover mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-full",
2938
+ title: t(config.labelKey, { fallback: config.label }),
2939
+ children: /* @__PURE__ */ jsx(Icon, {
2940
+ name: config.icon,
2941
+ size: 18,
2942
+ accent: commentIconAccent(view.root)
2943
+ })
2944
+ }),
2945
+ /* @__PURE__ */ jsxs("div", {
2946
+ className: "min-w-0 flex-1",
2947
+ children: [/* @__PURE__ */ jsxs("div", {
2948
+ className: "flex items-baseline gap-2",
2949
+ children: [/* @__PURE__ */ jsx("span", {
2950
+ className: "text-fg truncate text-sm font-medium",
2951
+ children: view.root.author ?? t("demo.commentsAnonymous")
2952
+ }), /* @__PURE__ */ jsx("span", {
2953
+ className: "text-fg-muted ml-auto shrink-0 text-xs",
2954
+ children: dateLabel(view.root.modified ?? view.root.created)
2955
+ })]
2956
+ }), latest && /* @__PURE__ */ jsxs("span", {
2957
+ className: "bg-accent-light text-accent mt-1 inline-block rounded px-1.5 py-0.5 text-[11px] font-medium",
2958
+ children: [statusLabel(t, latest.state), latest.by ? ` — ${latest.by}` : ""]
2959
+ })]
2960
+ }),
2961
+ perms.canDeleteThread && /* @__PURE__ */ jsx("button", {
2962
+ type: "button",
2963
+ onClick: (e) => {
2964
+ e.stopPropagation();
2965
+ comments.removeThread(view.root.ref);
2966
+ },
2967
+ className: "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded",
2968
+ title: t("demo.commentsDeleteThread"),
2969
+ children: /* @__PURE__ */ jsx(Icon, {
2970
+ name: "trash",
2971
+ size: 14
2972
+ })
2973
+ })
2974
+ ]
2975
+ }),
2976
+ /* @__PURE__ */ jsx(CommentBody, {
2977
+ annotationRef: view.root.ref,
2978
+ text: view.root.contents ?? "",
2979
+ deletable: false
2980
+ }),
2981
+ view.replies.length > 0 && /* @__PURE__ */ jsx("div", {
2982
+ className: "border-border-subtle flex flex-col gap-2 border-t pt-2",
2983
+ children: view.replies.map((r) => /* @__PURE__ */ jsx(Reply, { dto: r }, refKey(r.ref)))
2984
+ }),
2985
+ /* @__PURE__ */ jsxs("div", {
2986
+ className: "border-border-subtle flex flex-col gap-1.5 border-t pt-2",
2987
+ children: [perms.canSetStatus && /* @__PURE__ */ jsxs("label", {
2988
+ className: "text-fg-muted flex items-center gap-2 text-xs",
2989
+ onClick: (e) => e.stopPropagation(),
2990
+ children: [t("demo.commentsStatus"), /* @__PURE__ */ jsx("select", {
2991
+ value: status,
2992
+ onChange: (e) => void comments.setStatus(view.root.ref, e.target.value),
2993
+ className: "border-border bg-surface text-fg flex-1 rounded-md border px-1.5 py-1 text-xs outline-none",
2994
+ children: REVIEW_STATES.map((s) => /* @__PURE__ */ jsx("option", {
2995
+ value: s,
2996
+ children: statusLabel(t, s)
2997
+ }, s))
2998
+ })]
2999
+ }), perms.canReply && /* @__PURE__ */ jsxs("div", {
3000
+ className: "flex items-center gap-1.5",
3001
+ onClick: (e) => e.stopPropagation(),
3002
+ children: [/* @__PURE__ */ jsx("input", {
3003
+ type: "text",
3004
+ value: reply,
3005
+ placeholder: view.root.contents || view.replies.length ? t("demo.commentsReplyPlaceholder") : t("demo.commentsAddPlaceholder"),
3006
+ onChange: (e) => setReply(e.target.value),
3007
+ onKeyDown: (e) => e.key === "Enter" && sendReply(),
3008
+ 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"
3009
+ }), /* @__PURE__ */ jsx("button", {
3010
+ type: "button",
3011
+ onClick: sendReply,
3012
+ disabled: !reply.trim(),
3013
+ className: "text-accent disabled:text-fg-muted grid h-7 w-7 shrink-0 place-items-center rounded-md",
3014
+ title: t("demo.commentsReply"),
3015
+ children: /* @__PURE__ */ jsx(Icon, {
3016
+ name: "arrowForwardUp",
3017
+ size: 16
3018
+ })
3019
+ })]
3020
+ })]
3021
+ })
3022
+ ]
3023
+ })
3024
+ });
3025
+ }
3026
+ function Reply({ dto }) {
3027
+ const t = useT();
3028
+ return /* @__PURE__ */ jsxs("div", {
3029
+ className: "border-border-subtle ml-1 border-l-2 pl-2",
3030
+ children: [/* @__PURE__ */ jsxs("div", {
3031
+ className: "flex items-baseline gap-2",
3032
+ children: [/* @__PURE__ */ jsx("span", {
3033
+ className: "text-fg-secondary text-xs font-medium",
3034
+ children: dto.author ?? t("demo.commentsAnonymous")
3035
+ }), /* @__PURE__ */ jsx("span", {
3036
+ className: "text-fg-muted text-[11px]",
3037
+ children: dateLabel(dto.modified ?? dto.created)
3038
+ })]
3039
+ }), /* @__PURE__ */ jsx(CommentBody, {
3040
+ annotationRef: dto.ref,
3041
+ text: dto.contents ?? "",
3042
+ deletable: true
3043
+ })]
3044
+ });
3045
+ }
3046
+ /** One comment's text with inline edit (own, unlocked) and delete (replies). */
3047
+ function CommentBody({ annotationRef, text, deletable }) {
3048
+ const t = useT();
3049
+ const comments = useComments();
3050
+ const perms = comments.permissionsFor(annotationRef);
3051
+ const [draft, setDraft] = useState(null);
3052
+ if (draft !== null) {
3053
+ const save = () => {
3054
+ const next = draft.trim();
3055
+ setDraft(null);
3056
+ if (next && next !== text) comments.edit(annotationRef, next);
3057
+ };
3058
+ return /* @__PURE__ */ jsx("textarea", {
3059
+ value: draft,
3060
+ autoFocus: true,
3061
+ rows: 2,
3062
+ onClick: (e) => e.stopPropagation(),
3063
+ onChange: (e) => setDraft(e.target.value),
3064
+ onBlur: save,
3065
+ onKeyDown: (e) => {
3066
+ if (e.key === "Enter" && !e.shiftKey) {
3067
+ e.preventDefault();
3068
+ save();
3069
+ }
3070
+ if (e.key === "Escape") setDraft(null);
3071
+ },
3072
+ className: "border-accent bg-surface text-fg w-full resize-none rounded-md border px-2 py-1 text-sm outline-none"
3073
+ });
3074
+ }
3075
+ if (!text && !perms.canEditText && !deletable) return null;
3076
+ return /* @__PURE__ */ jsxs("div", {
3077
+ className: "group/body flex items-start gap-1",
3078
+ children: [
3079
+ /* @__PURE__ */ jsx("p", {
3080
+ className: "text-fg min-w-0 flex-1 whitespace-pre-wrap text-sm",
3081
+ children: text
3082
+ }),
3083
+ perms.canEditText && /* @__PURE__ */ jsx("button", {
3084
+ type: "button",
3085
+ onClick: (e) => {
3086
+ e.stopPropagation();
3087
+ setDraft(text);
3088
+ },
3089
+ 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",
3090
+ title: t("demo.commentsEdit"),
3091
+ children: /* @__PURE__ */ jsx(Icon, {
3092
+ name: "pencilMarker",
3093
+ size: 12
3094
+ })
3095
+ }),
3096
+ deletable && perms.canDelete && /* @__PURE__ */ jsx("button", {
3097
+ type: "button",
3098
+ onClick: (e) => {
3099
+ e.stopPropagation();
3100
+ comments.remove(annotationRef);
3101
+ },
3102
+ 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",
3103
+ title: t("demo.commentsDelete"),
3104
+ children: /* @__PURE__ */ jsx(Icon, {
3105
+ name: "trash",
3106
+ size: 12
3107
+ })
3108
+ })
3109
+ ]
3110
+ });
3111
+ }
3112
+ function statusLabel(t, state) {
3113
+ switch (state) {
3114
+ case "none": return t("demo.commentsStatusNone");
3115
+ case "accepted": return t("demo.commentsStatusAccepted");
3116
+ case "rejected": return t("demo.commentsStatusRejected");
3117
+ case "cancelled": return t("demo.commentsStatusCancelled");
3118
+ case "completed": return t("demo.commentsStatusCompleted");
3119
+ default: return state;
3120
+ }
3121
+ }
3122
+ //#endregion
2528
3123
  //#region src/ui/redaction-panel.tsx
2529
3124
  /**
2530
3125
  * The pending-redactions review panel (right sidebar). The list is a LIVE view
@@ -2959,13 +3554,7 @@ function RightSidebar() {
2959
3554
  size: 16
2960
3555
  })
2961
3556
  })]
2962
- }), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : /* @__PURE__ */ jsx("div", {
2963
- className: "p-3",
2964
- children: /* @__PURE__ */ jsx("p", {
2965
- className: "text-fg-muted text-sm",
2966
- children: t("demo.empty")
2967
- })
2968
- })]
3557
+ }), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : /* @__PURE__ */ jsx(CommentsPanel, {}, documentId ?? "none")]
2969
3558
  });
2970
3559
  }
2971
3560
  function PageControls() {
@@ -3285,7 +3874,7 @@ function DocumentArea({ children }) {
3285
3874
  function Shell() {
3286
3875
  useCommandShortcuts();
3287
3876
  useFilePickerProvider();
3288
- useFormScriptingProvider();
3877
+ useActionsUiAdapter();
3289
3878
  const schema = useChromeSchema();
3290
3879
  const frame = schema.frame ?? {};
3291
3880
  const toolbarEdge = frame.toolbar ?? "top";
@@ -3314,10 +3903,12 @@ function Shell() {
3314
3903
  className: "relative min-w-0 flex-1",
3315
3904
  children: [
3316
3905
  /* @__PURE__ */ jsx(ArmedToolCursor, {}),
3906
+ /* @__PURE__ */ jsx(SelectionClipboard, {}),
3317
3907
  /* @__PURE__ */ jsx(Stage, {
3318
- interaction: true,
3319
3908
  overlay: /* @__PURE__ */ jsxs(Fragment$1, { children: [
3320
3909
  /* @__PURE__ */ jsx(AnnotationStrip, {}),
3910
+ /* @__PURE__ */ jsx(SelectionStrip, {}),
3911
+ /* @__PURE__ */ jsx(SelectionHandles, {}),
3321
3912
  /* @__PURE__ */ jsx(Scrollbar, { axis: "y" }),
3322
3913
  /* @__PURE__ */ jsx(Scrollbar, { axis: "x" })
3323
3914
  ] }),
@@ -3325,7 +3916,6 @@ function Shell() {
3325
3916
  style: { background: "var(--ep-canvas)" },
3326
3917
  children: () => /* @__PURE__ */ jsxs(Fragment$1, { children: [
3327
3918
  /* @__PURE__ */ jsx(RenderLayer, { annotations: false }),
3328
- /* @__PURE__ */ jsx(TileLayer, { annotations: false }),
3329
3919
  /* @__PURE__ */ jsx(SelectionLayer, {}),
3330
3920
  /* @__PURE__ */ jsx(SearchLayer, {}),
3331
3921
  /* @__PURE__ */ jsx(LinkLayer, {}),
@@ -3364,7 +3954,7 @@ function Shell() {
3364
3954
  * changing these props on a mounted viewer does not rebuild the workspace.
3365
3955
  */
3366
3956
  /** Lazy built-in packs — a strings override for one of these wraps its loader. */
3367
- const BUILTIN_LOADERS = { es: () => import("./es-D0PzYE9Z.js").then((m) => m.es) };
3957
+ const BUILTIN_LOADERS = { es: () => import("./es-aQlZ3hcL.js").then((m) => m.es) };
3368
3958
  const isDict = (v) => typeof v === "object" && v !== null;
3369
3959
  /**
3370
3960
  * Deep-merge `over` into `base` (immutably), expanding dotted keys —
@@ -3453,10 +4043,7 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
3453
4043
  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}"`);
3454
4044
  }, [resolved]);
3455
4045
  const [plugins] = useState(() => [
3456
- stagePlugin({
3457
- layout: "vertical",
3458
- interaction: true
3459
- }),
4046
+ stagePlugin({ layout: "vertical" }),
3460
4047
  stagePlugin({
3461
4048
  id: "stage-thumbs",
3462
4049
  token: ThumbsStageToken,
@@ -3480,7 +4067,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
3480
4067
  }),
3481
4068
  renderPlugin(),
3482
4069
  pageEditPlugin(),
3483
- interactionPlugin({ defaultTool: "pointer" }),
4070
+ interactionPlugin(),
4071
+ feedbackPlugin({ provider: vibrationFeedback }),
3484
4072
  selectionPlugin(),
3485
4073
  annotationPlugin({ tools: [{
3486
4074
  id: "arrow",
@@ -3490,7 +4078,8 @@ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strin
3490
4078
  end: "open-arrow"
3491
4079
  } }
3492
4080
  }] }),
3493
- formPlugin({ scripting: { enabled: true } }),
4081
+ actionsPlugin({ javascript: { enabled: true } }),
4082
+ formPlugin(),
3494
4083
  linkPlugin(),
3495
4084
  redactionPlugin(),
3496
4085
  searchPlugin(),