dsh-figmark 1.1.3 → 1.2.0

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/lib/client.js CHANGED
@@ -4,6 +4,7 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_dom = require("react-dom");
7
8
  let react = require("react");
8
9
  let react_jsx_runtime = require("react/jsx-runtime");
9
10
  //#region src/client/locales.ts
@@ -32,11 +33,28 @@ window.__ModuleLoader__.load({
32
33
  summary: "总体目标",
33
34
  summaryPlaceholder: "可选:这张图整体要往哪个方向改(智能体会连同下面逐条说明一起读)",
34
35
  marks: "标注清单",
36
+ markPage: "{page} 页",
37
+ markKindLabel: "图形:{kind}",
38
+ revealMark: "跳到这一处",
35
39
  noMarks: "还没有标注",
36
40
  noMarksHint: "选一个工具,在图上点一下或拖一下就开始。",
37
41
  delete: "删除",
38
42
  close: "关闭",
39
43
  dock: "批注操作",
44
+ menuLabel: "图片操作",
45
+ menuSendToAgent: "发送给智能体",
46
+ menuSendToComposer: "发送到对话框",
47
+ menuOpen: "用默认应用打开",
48
+ menuReveal: "在文件管理器中显示",
49
+ menuCopyImage: "复制图片",
50
+ menuCopyRelative: "复制相对地址",
51
+ menuCopyAbsolute: "复制绝对地址",
52
+ menuOpened: "已请求用默认应用打开",
53
+ menuOpenFailed: "无法打开,请重试",
54
+ menuRevealed: "已请求在文件管理器中显示",
55
+ menuRevealFailed: "无法在文件管理器中显示,请重试",
56
+ menuCopied: "已复制路径",
57
+ menuCopyFailed: "复制失败,请重试",
40
58
  sendToAgent: "发送给智能体",
41
59
  sendToComposer: "发送到对话框",
42
60
  sending: "正在发送…",
@@ -55,6 +73,10 @@ window.__ModuleLoader__.load({
55
73
  pdfTitle: "PDF 批注",
56
74
  previousPage: "上一页",
57
75
  nextPage: "下一页",
76
+ pageJump: "跳转页码",
77
+ pageOf: "{page} / {pages}",
78
+ pagerLabel: "翻页",
79
+ sendNone: "还没有标注可发送",
58
80
  pdfRendering: "正在渲染这一页…",
59
81
  pdfOpenFailed: "打开 PDF 失败:",
60
82
  documentScope: "共 {pages} 页 · {marks} 处批注",
@@ -120,11 +142,28 @@ window.__ModuleLoader__.load({
120
142
  summary: "Overall goal",
121
143
  summaryPlaceholder: "Optional: what this figure should become overall (the agent reads it with the marks below)",
122
144
  marks: "Marks",
145
+ markPage: "p. {page}",
146
+ markKindLabel: "Shape: {kind}",
147
+ revealMark: "Go to this mark",
123
148
  noMarks: "No marks yet",
124
149
  noMarksHint: "Pick a tool, then click or drag on the figure to start.",
125
150
  delete: "Delete",
126
151
  close: "Close",
127
152
  dock: "Annotation actions",
153
+ menuLabel: "Figure actions",
154
+ menuSendToAgent: "Send to agent",
155
+ menuSendToComposer: "Send to composer",
156
+ menuOpen: "Open with default app",
157
+ menuReveal: "Reveal in File Manager",
158
+ menuCopyImage: "Copy image",
159
+ menuCopyRelative: "Copy relative path",
160
+ menuCopyAbsolute: "Copy absolute path",
161
+ menuOpened: "Asked the system to open it",
162
+ menuOpenFailed: "Could not open it; please try again",
163
+ menuRevealed: "Asked the file manager to show it",
164
+ menuRevealFailed: "Could not reveal it; please try again",
165
+ menuCopied: "Path copied",
166
+ menuCopyFailed: "Could not copy; please try again",
128
167
  sendToAgent: "Send to agent",
129
168
  sendToComposer: "Send to composer",
130
169
  sending: "Sending…",
@@ -143,6 +182,10 @@ window.__ModuleLoader__.load({
143
182
  pdfTitle: "PDF annotations",
144
183
  previousPage: "Previous page",
145
184
  nextPage: "Next page",
185
+ pageJump: "Go to page",
186
+ pageOf: "{page} / {pages}",
187
+ pagerLabel: "Paging",
188
+ sendNone: "Nothing to send yet",
146
189
  pdfRendering: "Rendering this page…",
147
190
  pdfOpenFailed: "Opening the PDF failed: ",
148
191
  documentScope: "{pages} pages · {marks} marks",
@@ -3222,6 +3265,182 @@ window.__ModuleLoader__.load({
3222
3265
  }));
3223
3266
  } catch {}
3224
3267
  }
3268
+ /**
3269
+ * Keep a menu wholly inside the window.
3270
+ *
3271
+ * A right-click near the bottom edge would otherwise open rows below it, where
3272
+ * they cannot be reached; the same on the right. Both axes are clamped
3273
+ * independently, so a click in one corner only moves the menu as far as it has
3274
+ * to move.
3275
+ * @param at - the click, in viewport coordinates.
3276
+ * @param viewport - visible width and height.
3277
+ * @param size - the menu's own width and height.
3278
+ * @returns the viewport position to place the menu's top-left corner at.
3279
+ */
3280
+ function clampMenu(at, viewport, size) {
3281
+ const rightmost = viewport.width - size.width - 8;
3282
+ const bottommost = viewport.height - size.height - 8;
3283
+ return {
3284
+ left: Math.max(8, Math.min(at.x, rightmost)),
3285
+ top: Math.max(8, Math.min(at.y, bottommost))
3286
+ };
3287
+ }
3288
+ /**
3289
+ * Read a path relative to the directory a Session runs in.
3290
+ *
3291
+ * The figure is usually somewhere under the workspace, which is what makes a
3292
+ * relative path worth copying at all — it survives the project being moved. A
3293
+ * figure outside the workspace, or a Session whose directory is unknown, has no
3294
+ * such path, and the menu leaves the row out rather than offering one that would
3295
+ * not resolve.
3296
+ * @param path - absolute path of the figure.
3297
+ * @param cwd - absolute workspace directory, or null when it is unknown.
3298
+ * @returns the relative path, or null when there is not one.
3299
+ */
3300
+ function relativeTo(path, cwd) {
3301
+ if (cwd === null) return null;
3302
+ const prefix = `${cwd}${cwd.endsWith("/") || cwd.endsWith("\\") ? "" : "/"}`;
3303
+ if (!path.startsWith(prefix)) return null;
3304
+ const rest = path.slice(prefix.length);
3305
+ return rest === "" || rest.startsWith("..") ? null : rest;
3306
+ }
3307
+ /**
3308
+ * The rows this figure's menu should show.
3309
+ *
3310
+ * Three groups, in the order a reader wants them: the two Host gestures (only
3311
+ * where the Host has a desktop to perform them), then the two copies, then — for
3312
+ * a figure outside the workspace — no relative row at all, because there is no
3313
+ * path from the workspace to it.
3314
+ * @param options - what the Host can do, and whether a relative path exists.
3315
+ * @param labels - the four rows' words.
3316
+ * @returns the rows, in the order they are drawn.
3317
+ */
3318
+ function figureMenuItems(options, labels) {
3319
+ const items = [];
3320
+ if (options.canSend) items.push({
3321
+ id: "sendToAgent",
3322
+ label: labels.sendToAgent
3323
+ }, {
3324
+ id: "sendToComposer",
3325
+ label: labels.sendToComposer
3326
+ });
3327
+ if (options.canOpen) items.push({
3328
+ id: "open",
3329
+ label: labels.open
3330
+ }, {
3331
+ id: "reveal",
3332
+ label: labels.reveal
3333
+ });
3334
+ if (options.hasImage) items.push({
3335
+ id: "copyImage",
3336
+ label: labels.copyImage
3337
+ });
3338
+ if (options.hasRelative) items.push({
3339
+ id: "copyRelative",
3340
+ label: labels.copyRelative
3341
+ });
3342
+ items.push({
3343
+ id: "copyAbsolute",
3344
+ label: labels.copyAbsolute
3345
+ });
3346
+ return items;
3347
+ }
3348
+ /**
3349
+ * Whether a pointer press landed inside the menu.
3350
+ *
3351
+ * A menu closes when the reader presses anywhere else, and "anywhere else" has to
3352
+ * be answered without knowing what was pressed: the event's composed path is the
3353
+ * one list that names every element between the target and the window, including
3354
+ * ones inside a shadow root.
3355
+ * @param event - the press.
3356
+ * @param panel - the menu element, or null before it has been attached.
3357
+ * @returns true when the press belongs to the menu itself.
3358
+ */
3359
+ function pressedInsideMenu(event, panel) {
3360
+ return panel !== null && event.composedPath().includes(panel);
3361
+ }
3362
+ /**
3363
+ * The text one copy row puts on the clipboard.
3364
+ * @param action - the row that was picked.
3365
+ * @param path - absolute path of the figure.
3366
+ * @param workspace - absolute workspace directory, or null when it is unknown.
3367
+ * @returns the text to copy, or null when the relative row has nothing to copy.
3368
+ */
3369
+ function copiedText(action, path, workspace) {
3370
+ return action === "copyRelative" ? relativeTo(path, workspace) : path;
3371
+ }
3372
+ //#endregion
3373
+ //#region src/client/host-actions.ts
3374
+ /**
3375
+ * Whether the Host can open paths at all.
3376
+ *
3377
+ * A remote Session has no desktop, and asking it to open a file would fail on
3378
+ * every row. Answering once at mount keeps the menu from offering gestures the
3379
+ * Session cannot perform; a failed probe reads as "no", which loses only the
3380
+ * rows, never a working one.
3381
+ * @param remote - the injected service, if the composition has one.
3382
+ * @returns whether the menu may offer open and reveal.
3383
+ */
3384
+ async function desktopAvailable(remote) {
3385
+ if (remote === void 0) return false;
3386
+ try {
3387
+ const result = await remote.session.canOpenWorkspacePath();
3388
+ return result.ok && result.value === true;
3389
+ } catch {
3390
+ return false;
3391
+ }
3392
+ }
3393
+ /**
3394
+ * Open one path in its default application.
3395
+ * @param remote - the injected service.
3396
+ * @param path - absolute path on the Host.
3397
+ * @returns whether the Host accepted it.
3398
+ */
3399
+ async function openOnDesktop(remote, path) {
3400
+ if (remote === void 0) return false;
3401
+ try {
3402
+ return (await remote.session.openWorkspacePath({ path })).ok;
3403
+ } catch {
3404
+ return false;
3405
+ }
3406
+ }
3407
+ /**
3408
+ * Reveal one path in the Host's file manager.
3409
+ * @param remote - the injected service.
3410
+ * @param path - absolute path on the Host.
3411
+ * @returns whether the Host accepted it.
3412
+ */
3413
+ async function revealOnDesktop(remote, path) {
3414
+ if (remote === void 0) return false;
3415
+ try {
3416
+ return (await remote.session.openWorkspacePath({
3417
+ path,
3418
+ action: "reveal"
3419
+ })).ok;
3420
+ } catch {
3421
+ return false;
3422
+ }
3423
+ }
3424
+ /**
3425
+ * Put a picture on the clipboard.
3426
+ *
3427
+ * The async Clipboard API only accepts a PNG, and only from a user gesture — both
3428
+ * of which a menu row is. A browser without it (or a document that refuses the
3429
+ * write) is reported as a refusal rather than thrown, because the caller's next
3430
+ * move is the same either way: tell the reader it did not work.
3431
+ * @param bytes - the PNG the menu composed.
3432
+ * @returns whether the clipboard took it.
3433
+ */
3434
+ async function copyImageToClipboard(bytes) {
3435
+ try {
3436
+ const clipboard = navigator.clipboard;
3437
+ if (clipboard === void 0 || typeof clipboard.write !== "function") return false;
3438
+ await clipboard.write([new ClipboardItem({ "image/png": bytes })]);
3439
+ return true;
3440
+ } catch {
3441
+ return false;
3442
+ }
3443
+ }
3225
3444
  //#endregion
3226
3445
  //#region src/shared/http.ts
3227
3446
  /**
@@ -3279,7 +3498,8 @@ window.__ModuleLoader__.load({
3279
3498
  path: body.figure.path,
3280
3499
  mediaType: body.figure.mediaType,
3281
3500
  sha256: body.figure.sha256,
3282
- annotation: body.annotation ?? null
3501
+ annotation: body.annotation ?? null,
3502
+ workspace: body.figure.workspace ?? null
3283
3503
  };
3284
3504
  }
3285
3505
  /**
@@ -3388,8 +3608,14 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3388
3608
  /* Every small picture in the panel is one stroke set, declared once: without this
3389
3609
  an svg with no fill of its own falls back to a solid black shape, which is what
3390
3610
  turned the dock and the cards into blobs of ink. */
3611
+ /* Every icon is drawn on one 24-unit grid, and every one of them is asked to
3612
+ *render* at the same thickness — 1.2px — so the row reads as one set. On a single
3613
+ grid that still means one stroke-width per drawn size, because the number is in
3614
+ the icon's own units: 1.2 x 24 / size. The sizes differ only because the buttons
3615
+ holding them do; the weight does not. */
3391
3616
  .da-ico{width:16px;height:16px;flex:0 0 auto;fill:none;stroke:currentColor;
3392
- stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
3617
+ stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
3618
+ .da-tool-icon svg{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
3393
3619
 
3394
3620
  /* --- the floating tool palette ------------------------------------------- */
3395
3621
  /* The tools float over the figure in a dark rounded pill so they can be parked
@@ -3423,7 +3649,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3423
3649
  .da-palette .da-tool:hover{background:rgba(255,255,255,.14)}
3424
3650
  .da-palette .da-tool[aria-pressed="true"]{background:#176df2;color:#fff}
3425
3651
  .da-palette .da-tool .da-tool-icon{height:auto}
3426
- .da-palette .da-tool .da-tool-icon svg{width:16px;height:16px;stroke-width:1.9}
3652
+ .da-palette .da-tool .da-tool-icon svg{width:17px;height:17px;stroke-width:1.7}
3427
3653
 
3428
3654
  /* --- the size rail ------------------------------------------------------- */
3429
3655
  /* A vertical rail along the left edge of the figure: how heavy the pen is, and how
@@ -3491,6 +3717,9 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3491
3717
  /* Parked: placed from the top by the reader's drag, so the bottom anchor that keeps
3492
3718
  it centred while untouched must not also apply — with both set it stretches. */
3493
3719
  .da-bar[data-parked="true"]{bottom:auto}
3720
+ /* A bar standing in for the page body's own: it is placed by the pane rather than
3721
+ dragged, because there is no page yet to park it over. */
3722
+ .da-bar[data-static="true"]{bottom:8px}
3494
3723
  .da-bar[data-dragging="true"]{opacity:.94;box-shadow:0 12px 32px rgba(10,14,24,.4),0 0 0 1px rgba(255,255,255,.14)}
3495
3724
  .da-bar[data-dragging="true"] *{cursor:move !important}
3496
3725
  /* The bar's buttons are white glyphs on the dark strip. The one filled control is
@@ -3499,8 +3728,6 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3499
3728
  .da-bar .da-dock-btn:hover{background:rgba(255,255,255,.14)}
3500
3729
  .da-bar .da-dock-btn[aria-pressed="true"]{background:#176df2;color:#fff}
3501
3730
  .da-bar .da-dock-sep{background:rgba(255,255,255,.22)}
3502
- .da-bar .da-dock-btn-primary{background:#176df2;color:#fff;border-color:#176df2}
3503
- .da-bar .da-dock-btn-primary:hover{background:#176df2;opacity:.92}
3504
3731
  .da-bar .da-dock-dot{background:#4c8dff}
3505
3732
  .da-bar .da-dock-count{background:#176df2;color:#fff}
3506
3733
  .da-bar .da-style{width:30px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#fff}
@@ -3519,7 +3746,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3519
3746
  .da-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}
3520
3747
  .da-btn:disabled{opacity:.4;cursor:default}
3521
3748
  .da-btn:disabled:hover{background:var(--dsw-alias-bg-base,transparent)}
3522
- .da-btn svg{width:14px;height:14px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
3749
+ .da-btn svg{width:15px;height:15px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
3523
3750
  /* A quiet button has no frame at all until the pointer is on it: it is an action,
3524
3751
  not a thing, and the row it sits in is already framed. */
3525
3752
  .da-btn-quiet{border-color:transparent;background:transparent;height:24px;padding:0 7px;font-size:11px;color:var(--dsw-alias-label-secondary,inherit)}
@@ -3529,13 +3756,13 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3529
3756
  picture is what a reader who has used the tool once recognizes without reading. */
3530
3757
  .da-tool{flex-direction:column;justify-content:center;gap:1px;width:40px;height:34px;padding:0;border-color:transparent;background:transparent;font-size:9px;border-radius:9px}
3531
3758
  .da-tool .da-tool-icon{display:flex;align-items:center;justify-content:center;height:14px}
3532
- .da-tool .da-tool-icon svg{width:13px;height:13px;stroke-width:1.8}
3759
+ .da-tool .da-tool-icon svg{width:14px;height:14px;stroke-width:2.05}
3533
3760
  .da-tool .da-tool-label{font-size:10px;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary,inherit)}
3534
3761
  /* The armed tool is the one state in this row the reader must never hunt for: it is
3535
3762
  the only tile with a fill, and the only label in the brand colour. */
3536
3763
  .da-tool[aria-pressed="true"]{border-color:transparent;background:var(--da-accent-soft,rgba(23,109,242,.1));color:var(--da-accent,currentColor)}
3537
3764
  .da-tool[aria-pressed="true"] .da-tool-label{color:var(--da-accent,currentColor);font-weight:600}
3538
- .da-tool[aria-pressed="true"] svg{stroke-width:2.1}
3765
+ .da-tool[aria-pressed="true"] svg{stroke-width:2.4}
3539
3766
  .da-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
3540
3767
  .da-btn-icon{width:30px;padding:0}
3541
3768
 
@@ -3598,13 +3825,73 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3598
3825
  .da-page{font-size:12px;min-width:44px;text-align:center;color:var(--dsw-alias-label-secondary,inherit)}
3599
3826
  /* The document's toolbar sits above the page body, so the pane is one column of
3600
3827
  definite height and the body takes what is left of it. */
3601
- .da-pdf{display:flex;flex-direction:column;height:100%;min-height:0}
3828
+ .da-pdf{position:relative;display:flex;flex-direction:column;height:100%;min-height:0}
3602
3829
  .da-pdf>.da-root{flex:1 1 auto;height:auto;min-height:0}
3830
+ /* A document's toolbar is a row of decisions above the page: paging on the left,
3831
+ what the document holds and the hand-off on the right. It is fixed height and
3832
+ never scrolls with the page, so the controls keep where the hand left them. */
3833
+ .da-toolbar{display:flex;align-items:center;gap:6px;flex:0 0 auto;
3834
+ padding:6px 10px;border-bottom:0.5px solid var(--dsw-alias-border-l3,currentColor)}
3835
+
3836
+ /* The PDF's own strip. It is the same object as the figure's bottom bar — a dark
3837
+ rounded card floating over the page — because a reader who has marked a figure
3838
+ and then opens a document should not have to learn a second set of controls in
3839
+ a second visual language. Everything the page is read with lives on it: where you
3840
+ are, how you move, how it flows, and the way out. */
3841
+ /* Paging is one decision, so its three controls sit closer together than the bar's
3842
+ own gap and read as a single reader rather than three buttons. It lives on the
3843
+ page body's bar, which the pane owns — not in a strip of its own. */
3844
+ .da-docbar-group{display:flex;align-items:center;gap:1px;flex:0 0 auto}
3845
+ /* The page number is the one control the reader reads rather than presses, so it
3846
+ gets a fixed slot: a number that shifts the buttons as it goes 9 to 100 is a
3847
+ toolbar that moves under the pointer. Not a tinted pill — the inverted strip is
3848
+ already the ground, and a second ground inside it reads as a button. */
3849
+ /* The page number reads as a caption on the dark card, not as a button: no ground of
3850
+ its own, no frame, just the digits. It is still pressable — pressing is how the
3851
+ reader jumps — but a chip inside a chip is two grounds for one control, and the
3852
+ white box read as a hole punched in the bar. */
3853
+ .da-bar button.da-page{display:inline-flex;align-items:center;justify-content:center;
3854
+ min-width:50px;height:28px;padding:0 4px;border:0;border-radius:8px;
3855
+ appearance:none;-webkit-appearance:none;
3856
+ background:transparent;box-shadow:none;color:#fff;cursor:pointer;
3857
+ font:inherit;font-size:12.5px;font-variant-numeric:tabular-nums}
3858
+ .da-bar button.da-page:hover{background:rgba(255,255,255,.14)}
3859
+ .da-bar button.da-page:focus-visible{outline:2px solid var(--da-accent,#176df2);outline-offset:1px}
3860
+
3861
+ .da-bar .da-dock-sep{background:rgba(255,255,255,.22)}
3862
+ .da-bar .da-ico{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.9;
3863
+ stroke-linecap:round;stroke-linejoin:round}
3864
+ /* What the document holds, said quietly: the strip's subject is the page, and the
3865
+ count is a caption on it rather than another control. */
3866
+ .da-bar .da-docbar-count{flex:0 0 auto;margin-left:6px;font-size:11.5px;
3867
+ color:rgba(255,255,255,.62);font-variant-numeric:tabular-nums;white-space:nowrap}
3868
+ /* The hand-off's note, only shown when there is nothing to hand over. */
3869
+ .da-bar .da-docbar-note{flex:0 0 auto;margin-right:2px;font-size:11.5px;color:rgba(255,255,255,.5)}
3870
+ /* The page number is the one control the reader reads rather than presses, so it
3871
+ gets a fixed slot: a number that shifts the buttons as it goes 9 → 10 → 100 is a
3872
+ toolbar that moves under the pointer. */
3873
+ .da-toolbar .da-page{min-width:52px;padding:0 2px;font-variant-numeric:tabular-nums}
3874
+ /* The hand-off sits under the page it describes, right-aligned and on its own
3875
+ ground, so it is not reachable by a stray press on the page controls. */
3876
+ .da-send{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex:0 0 auto;
3877
+ padding:6px 10px;border-top:0.5px solid var(--dsw-alias-border-l3,currentColor)}
3878
+ /* The page a reader types into, to jump. Sized to the widest page count a document
3879
+ can plausibly have, so the slot does not resize as the number grows. */
3880
+ .da-page-input{width:52px;height:28px;padding:0 6px;border:0.5px solid rgba(255,255,255,.35);
3881
+ border-radius:8px;background:rgba(255,255,255,.12);color:#fff;
3882
+ font:inherit;font-size:12.5px;line-height:1;text-align:center;font-variant-numeric:tabular-nums}
3883
+ .da-page-input:focus{outline:none;border-color:var(--da-accent,#176df2);background:rgba(255,255,255,.18)}
3884
+ /* The page number's own denominator: smaller and quieter than the number it follows. */
3885
+ .da-page-total{font-size:12px;color:var(--dsw-alias-label-tertiary,inherit);white-space:nowrap}
3603
3886
  /* The page slot before the first raster. It takes the column the body's stage
3604
3887
  takes, so the frame around it — the toolbar above all — is in place from the
3605
3888
  first paint and does not move when the page arrives. */
3606
3889
  .da-placeholder{flex:1 1 auto;min-height:0;background:var(--dsw-alias-bg-document-preview,transparent)}
3607
-
3890
+ /* The page-number box and its denominator, held together so the box cannot drift
3891
+ away from the count it is a fraction of. */
3892
+ .da-page-field{display:flex;align-items:center;gap:3px}
3893
+ /* The hand-off's own note, quiet enough to read as a state rather than a control. */
3894
+ .da-send-note{font-size:12px;color:var(--dsw-alias-label-tertiary,inherit);margin-right:2px}
3608
3895
  /* --- the two cards under the figure ------------------------------------- */
3609
3896
  /* --- the dock ------------------------------------------------------------ */
3610
3897
  /* The dock's buttons are quiet circles in the bar: they are the actions that are
@@ -3618,8 +3905,6 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3618
3905
  .da-dock-btn:disabled{opacity:.4;cursor:default}
3619
3906
  .da-dock-btn:disabled:hover{background:transparent}
3620
3907
  /* The one filled control in the panel is the way out of it. */
3621
- .da-dock-btn-primary{border-color:var(--da-accent,#176df2);background:var(--da-accent,#176df2);color:#fff;box-shadow:0 2px 8px rgba(23,109,242,.32)}
3622
- .da-dock-btn-primary:hover{opacity:.92;background:var(--da-accent,#176df2)}
3623
3908
  /* A dot, not a count: "there is a goal written" is all the bar needs to say about
3624
3909
  a field it is not a list of. */
3625
3910
  .da-dock-dot{position:absolute;top:3px;right:4px;width:5px;height:5px;border-radius:50%;
@@ -3706,14 +3991,23 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3706
3991
  sentence as the thing you press, and the sentence's own shape and anchor under it
3707
3992
  in smaller type. A borderless run of text would read as one paragraph of marks. */
3708
3993
  .da-marks{display:flex;flex-direction:column;gap:3px;min-height:0}
3709
- .da-mark{display:flex;align-items:center;gap:7px;padding:5px 6px;border-radius:8px;
3994
+ .da-mark{display:flex;align-items:center;gap:7px;padding:5px 4px;border-radius:7px;
3710
3995
  border:0.5px solid transparent;min-width:0}
3711
3996
  .da-mark:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
3712
3997
  .da-mark[data-selected="true"]{border-color:var(--da-accent,#176df2);
3713
- background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.08))}
3714
- .da-mark-index{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
3715
- width:20px;height:20px;border-radius:6px;color:#fff;font-size:11px;font-weight:700;
3716
- font-variant-numeric:tabular-nums}
3998
+ background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.06))}
3999
+ /* Two light strokes in the mark's colour, not a block of fill.
4000
+ A filled chip on every row is a wall of colour beside a figure that is mostly
4001
+ paper; the list should read as a list. The number keeps a fixed, right-aligned
4002
+ box so a column of rows lines up whether it holds 1 or 12, and the glyph beside
4003
+ it is drawn at the same 1.5px stroke the tools wear. */
4004
+ .da-mark-n{flex:0 0 auto;width:14px;text-align:right;font-size:11px;font-weight:600;
4005
+ font-variant-numeric:tabular-nums;line-height:1.35;
4006
+ color:var(--dsw-alias-label-secondary,currentColor)}
4007
+ .da-mark-glyph{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
4008
+ width:14px;height:14px}
4009
+ .da-mark-glyph svg{width:12px;height:12px;fill:none;stroke:currentColor;
4010
+ stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
3717
4011
  .da-mark-text{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;
3718
4012
  padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}
3719
4013
  .da-mark-title{display:block;font-size:12px;line-height:1.35;overflow:hidden;
@@ -3721,9 +4015,25 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3721
4015
  .da-mark-meta{display:block;font-size:10px;line-height:1.35;
3722
4016
  color:var(--dsw-alias-label-tertiary,currentColor);overflow:hidden;
3723
4017
  text-overflow:ellipsis;white-space:nowrap}
3724
- .da-mark-del{flex:0 0 auto;padding:2px 6px;border:0;border-radius:6px;background:transparent;
3725
- color:var(--dsw-alias-label-tertiary,currentColor);font:inherit;font-size:10px;cursor:pointer}
4018
+ /* The page number sits at the row's end rather than under its sentence: it is not
4019
+ part of what the mark says, it is where the mark is, and a list puts that where a
4020
+ table of contents puts it.
4021
+ Its size is the number's, not a step below it. Two sizes in one row — 11px on the
4022
+ left, 10px on the right — read as two different kinds of fact, and the page was
4023
+ the one that looked like it had been shrunk to fit. Weight is what demotes a fact
4024
+ here, not size: 500 against the number's 600, and the tertiary colour. */
4025
+ .da-mark-page{flex:0 0 auto;font-size:10px;font-weight:400;line-height:1.35;
4026
+ white-space:nowrap;color:var(--dsw-alias-label-tertiary,currentColor);
4027
+ font-variant-numeric:tabular-nums}
4028
+ /* A glyph rather than the word 删除: the row is a card, and a word in the corner of
4029
+ a card reads as another thing to press rather than as the way to get rid of it. */
4030
+ .da-mark-del{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
4031
+ width:20px;height:20px;padding:0;border:0;border-radius:6px;background:transparent;
4032
+ color:var(--dsw-alias-label-tertiary,currentColor);font:inherit;font-size:11px;line-height:1;
4033
+ cursor:pointer;opacity:0}
4034
+ .da-mark:hover .da-mark-del,.da-mark[data-selected="true"] .da-mark-del{opacity:1}
3726
4035
  .da-mark-del:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));color:inherit}
4036
+ .da-mark-del:focus-visible{opacity:1}
3727
4037
 
3728
4038
  /* The empty list is a picture and two lines: a card of grey text reads as broken,
3729
4039
  and "nothing yet" is a state, not an error. */
@@ -3731,6 +4041,16 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
3731
4041
  .da-empty-ico{width:46px;height:46px;fill:none;stroke:var(--dsw-alias-border-l4,currentColor);stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;opacity:.85}
3732
4042
  .da-empty-title{font-size:12px;font-weight:650;color:var(--dsw-alias-label-secondary,inherit)}
3733
4043
  .da-empty-hint{font-size:11px;line-height:1.6;color:var(--dsw-alias-label-tertiary,inherit);max-width:190px}
4044
+ /* The figure's context menu. Laid out at a fixed width because the placement
4045
+ maths needs one number; rows are the height that maths assumes. */
4046
+ .da-menu{position:fixed;z-index:40;display:flex;flex-direction:column;gap:1px;width:208px;
4047
+ padding:4px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);
4048
+ box-shadow:0 8px 24px rgba(15,17,21,.16),0 0 0 1px var(--dsw-alias-border-l2,rgba(0,0,0,.1))}
4049
+ .da-menu-item{display:flex;align-items:center;height:27px;padding:0 8px;border:0;border-radius:6px;
4050
+ background:transparent;color:var(--dsw-alias-label-primary,inherit);font:inherit;font-size:12.5px;
4051
+ text-align:left;cursor:pointer;white-space:nowrap}
4052
+ .da-menu-item:hover,.da-menu-item:focus-visible{background:var(--da-accent-soft,rgba(23,109,242,.14));outline:none}
4053
+
3734
4054
  .da-status{flex:0 0 auto;padding:6px 10px;font-size:12px;color:var(--dsw-alias-label-secondary,inherit)}
3735
4055
  .da-status[data-tone="error"]{color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary,currentColor))}
3736
4056
  .da-status[data-tone="ok"]{color:var(--dsw-alias-state-success-primary,currentColor)}
@@ -4127,30 +4447,41 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4127
4447
  /**
4128
4448
  * The stroke each marking tool draws, so the row says it in pictures as well as words.
4129
4449
  *
4450
+ * Every icon in the row is drawn on the same 24-unit grid as {@link PointerIcon}. That
4451
+ * is not cosmetic: `stroke-width` in an inline SVG is measured in the icon's own units,
4452
+ * so a 16-unit icon and a 24-unit icon given the same number draw at different
4453
+ * thicknesses. On one grid they cannot drift, and the six read as one set.
4454
+ *
4130
4455
  * `select` is not a marking tool and never comes here: it draws nothing, so it
4131
- * wears the pointer instead (see {@link PointerIcon}).
4456
+ * wears the pointer instead.
4132
4457
  */
4133
4458
  function ToolIcon(tool) {
4134
4459
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4135
- viewBox: "0 0 16 16",
4460
+ viewBox: "0 0 24 24",
4136
4461
  "aria-hidden": "true",
4137
4462
  focusable: "false",
4138
- children: tool === "arrow" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 3 13 L 13 3" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 7 3 h 6 v 6" })] }) : tool === "rect" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
4139
- x: "2.5",
4140
- y: "3.5",
4141
- width: "11",
4142
- height: "9",
4143
- rx: "1.5"
4463
+ children: tool === "arrow" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 4.5 19.5 L 19 5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 10 5 h 9 v 9" })] }) : tool === "rect" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
4464
+ x: "4",
4465
+ y: "6",
4466
+ width: "16",
4467
+ height: "12",
4468
+ rx: "2"
4144
4469
  }) : tool === "ellipse" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4145
- cx: "8",
4146
- cy: "8",
4147
- r: "5.5"
4148
- }) : tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 2.5 13 C 5 4 9 13 13.5 3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4470
+ cx: "12",
4471
+ cy: "12",
4472
+ r: "8.2"
4473
+ }) : tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 4 18 C 7 6 12 20 20 5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4149
4474
  fillRule: "evenodd",
4150
- d: "M 8 14.6 C 8 14.6 2.9 9.5 2.9 6.4 A 5.1 5.1 0 1 1 13.1 6.4 C 13.1 9.5 8 14.6 8 14.6 Z\n M 8 8.7 A 2.1 2.1 0 1 0 8 4.5 A 2.1 2.1 0 1 0 8 8.7 Z",
4475
+ d: "M 12 22.4 Q 4.9 15.1 4.1 10.16 A 7.9 7.9 0 1 1 19.9 10.16 Q 19.1 15.1 12 22.4 Z\n M 12 16.02 A 5.62 5.62 0 1 0 12 4.78 A 5.62 5.62 0 1 0 12 16.02 Z",
4151
4476
  fill: "currentColor",
4152
4477
  stroke: "none"
4153
- })
4478
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4479
+ cx: "12",
4480
+ cy: "10.4",
4481
+ r: "2",
4482
+ fill: "currentColor",
4483
+ stroke: "none"
4484
+ })] })
4154
4485
  });
4155
4486
  }
4156
4487
  /**
@@ -4164,22 +4495,22 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4164
4495
  function GoalIcon() {
4165
4496
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4166
4497
  className: "da-ico",
4167
- viewBox: "0 0 16 16",
4498
+ viewBox: "0 0 24 24",
4168
4499
  "aria-hidden": "true",
4169
4500
  focusable: "false",
4170
4501
  children: [
4171
4502
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4172
- cx: "7",
4173
- cy: "9",
4174
- r: "5.2"
4503
+ cx: "10.5",
4504
+ cy: "13.5",
4505
+ r: "7.8"
4175
4506
  }),
4176
4507
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4177
- cx: "7",
4178
- cy: "9",
4179
- r: "2.3"
4508
+ cx: "10.5",
4509
+ cy: "13.5",
4510
+ r: "3.45"
4180
4511
  }),
4181
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 13.8 2.2 L 8.2 7.8" }),
4182
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 9.6 2.6 L 13.8 2.2 L 13.4 6.4" })
4512
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 20.7 3.3 L 12.3 11.7" }),
4513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 14.4 3.9 L 20.7 3.3 L 20.1 9.6" })
4183
4514
  ]
4184
4515
  });
4185
4516
  }
@@ -4187,29 +4518,29 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4187
4518
  function ListIcon() {
4188
4519
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4189
4520
  className: "da-ico",
4190
- viewBox: "0 0 16 16",
4521
+ viewBox: "0 0 24 24",
4191
4522
  "aria-hidden": "true",
4192
4523
  focusable: "false",
4193
4524
  children: [
4194
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 6 4.5 h 7 M 6 8 h 7 M 6 11.5 h 7" }),
4525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 9 6.75 h 10.5 M 9 12 h 10.5 M 9 17.25 h 10.5" }),
4195
4526
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4196
- cx: "3.2",
4197
- cy: "4.5",
4198
- r: "1",
4527
+ cx: "4.8",
4528
+ cy: "6.75",
4529
+ r: "1.5",
4199
4530
  fill: "currentColor",
4200
4531
  stroke: "none"
4201
4532
  }),
4202
4533
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4203
- cx: "3.2",
4204
- cy: "8",
4205
- r: "1",
4534
+ cx: "4.8",
4535
+ cy: "12",
4536
+ r: "1.5",
4206
4537
  fill: "currentColor",
4207
4538
  stroke: "none"
4208
4539
  }),
4209
4540
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4210
- cx: "3.2",
4211
- cy: "11.5",
4212
- r: "1",
4541
+ cx: "4.8",
4542
+ cy: "17.25",
4543
+ r: "1.5",
4213
4544
  fill: "currentColor",
4214
4545
  stroke: "none"
4215
4546
  })
@@ -4235,40 +4566,40 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4235
4566
  function UndoIcon() {
4236
4567
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4237
4568
  className: "da-ico",
4238
- viewBox: "0 0 16 16",
4569
+ viewBox: "0 0 24 24",
4239
4570
  "aria-hidden": "true",
4240
4571
  focusable: "false",
4241
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 3.2 7.5 H 9.5 a 3.4 3.4 0 0 1 0 6.8 H 6" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 5.8 4.4 L 2.7 7.5 L 5.8 10.6" })]
4572
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 4.8 11.25 H 14.25 a 5.1 5.1 0 0 1 0 10.2 H 9" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 8.7 6.6 L 4.05 11.25 L 8.7 15.9" })]
4242
4573
  });
4243
4574
  }
4244
4575
  /** The same arrow the other way. */
4245
4576
  function RedoIcon() {
4246
4577
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4247
4578
  className: "da-ico",
4248
- viewBox: "0 0 16 16",
4579
+ viewBox: "0 0 24 24",
4249
4580
  "aria-hidden": "true",
4250
4581
  focusable: "false",
4251
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 12.8 7.5 H 6.5 a 3.4 3.4 0 0 0 0 6.8 H 10" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 10.2 4.4 L 13.3 7.5 L 10.2 10.6" })]
4582
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 19.2 11.25 H 9.75 a 5.1 5.1 0 0 0 0 10.2 H 15" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 15.3 6.6 L 19.95 11.25 L 15.3 15.9" })]
4252
4583
  });
4253
4584
  }
4254
4585
  /** The primary action's mark: a paper plane, the usual "this leaves here". */
4255
4586
  function SendIcon() {
4256
4587
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4257
4588
  className: "da-ico",
4258
- viewBox: "0 0 16 16",
4589
+ viewBox: "0 0 24 24",
4259
4590
  "aria-hidden": "true",
4260
4591
  focusable: "false",
4261
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 2 8 L 14 2.6 L 10.6 14 L 8 9.4 Z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 8 9.4 L 14 2.6" })]
4592
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 3 12 L 21 3.9 L 15.9 21 L 12 14.1 Z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 12 14.1 L 21 3.9" })]
4262
4593
  });
4263
4594
  }
4264
4595
  /** The secondary action's mark: a speech bubble, "this goes to the chat box". */
4265
4596
  function ChatIcon() {
4266
4597
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4267
4598
  className: "da-ico",
4268
- viewBox: "0 0 16 16",
4599
+ viewBox: "0 0 24 24",
4269
4600
  "aria-hidden": "true",
4270
4601
  focusable: "false",
4271
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 2.5 6.5 a 2 2 0 0 1 2 -2 h 7 a 2 2 0 0 1 2 2 v 3 a 2 2 0 0 1 -2 2 h -4 l -3 2.2 v -2.2 a 2 2 0 0 1 -2 -2 z" })
4602
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 3.75 9.75 a 3 3 0 0 1 3 -3 h 10.5 a 3 3 0 0 1 3 3 v 4.5 a 3 3 0 0 1 -3 3 h -6 l -4.5 3.3 v -3.3 a 3 3 0 0 1 -3 -3 z" })
4272
4603
  });
4273
4604
  }
4274
4605
  /**
@@ -4298,7 +4629,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4298
4629
  const parsedAddress = parseFileAddress(address);
4299
4630
  const mediaType = figureMediaType(parsedAddress?.path ?? "");
4300
4631
  const isVector = mediaType === "image/svg+xml";
4301
- const t = (0, react.useCallback)((key) => resolveTranslate(props, key), [props.t, props.localeId]);
4632
+ const t = (0, react.useCallback)((key, vars) => resolveTranslate(props, key, vars), [props.t, props.localeId]);
4302
4633
  const sessionId = props.sessionId ?? parsedAddress?.sessionId;
4303
4634
  /**
4304
4635
  * A private prefix for the rail's gradient and clip ids.
@@ -4370,6 +4701,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4370
4701
  const surface = props.page;
4371
4702
  /** Whether this body annotates one page of a document its owner persists. */
4372
4703
  const isPage = surface !== void 0;
4704
+ const chrome = props.chrome !== false;
4373
4705
  const figure = surface === void 0 ? fileFigure : {
4374
4706
  kind: "raster",
4375
4707
  objectUrl: surface.dataUrl,
@@ -4409,12 +4741,46 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4409
4741
  const [color, setColor] = (0, react.useState)(remembered.color ?? COLORS[0]);
4410
4742
  const [weight, setWeight] = (0, react.useState)(remembered.weight ?? 1);
4411
4743
  const [marks, setMarks] = (0, react.useState)(() => [...props.draft?.marks ?? []]);
4744
+ /**
4745
+ * What the list shows.
4746
+ *
4747
+ * One page of a paged document is handed the whole file's marks, because the list
4748
+ * is the map of what is about to be sent and what is sent is the file. A body
4749
+ * mounted on its own has only its own marks to show, which is the same thing said
4750
+ * with nothing extra.
4751
+ */
4752
+ const listedMarks = props.documentMarks ?? marks;
4412
4753
  const [past, setPast] = (0, react.useState)([]);
4413
4754
  const [future, setFuture] = (0, react.useState)([]);
4414
- const [selectedId, setSelectedId] = (0, react.useState)(null);
4755
+ const [ownSelectedId, setOwnSelectedId] = (0, react.useState)(null);
4756
+ /**
4757
+ * The selection, kept by the owner when there is one.
4758
+ *
4759
+ * A column mounts one body per page and moves the chrome between them as the
4760
+ * reader scrolls; a selection held here would be dropped at every page boundary,
4761
+ * and the comment pill would vanish under the sentence it belongs to.
4762
+ */
4763
+ const selectedId = props.selectedId === void 0 ? ownSelectedId : props.selectedId;
4764
+ const setSelectedId = (0, react.useCallback)((next) => {
4765
+ const value = typeof next === "function" ? next(selectedId) : next;
4766
+ if (props.onSelectedIdChange === void 0) setOwnSelectedId(value);
4767
+ else props.onSelectedIdChange(value);
4768
+ }, [props.onSelectedIdChange, selectedId]);
4415
4769
  /** The mark whose detail box should take the caret once it is on screen. */
4416
4770
  const [focusId, setFocusId] = (0, react.useState)(null);
4417
4771
  /**
4772
+ * Ask the owner to turn to a mark's page, when this body is one page of a document.
4773
+ *
4774
+ * The list is the document's, so a row can name a mark that is not on the page in
4775
+ * view. The owner keeps the page number, so it is the owner that has to turn —
4776
+ * and it must happen before the selection lands, or the pill would be measured
4777
+ * against a page the mark is not on.
4778
+ */
4779
+ const revealOnPage = (0, react.useCallback)((mark) => {
4780
+ if (props.onRevealMark === void 0 || mark.page === void 0) return;
4781
+ props.onRevealMark(mark);
4782
+ }, [props.onRevealMark]);
4783
+ /**
4418
4784
  * What the figure shows, as context for the marks below it.
4419
4785
  *
4420
4786
  * One per figure rather than one per mark: it is never drawn, and its only job
@@ -4443,7 +4809,14 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4443
4809
  * second one's own close button. Opening any popover closes whatever was open;
4444
4810
  * opening the one already showing closes it, so the button toggles.
4445
4811
  */
4446
- const [openPanel, setOpenPanel] = (0, react.useState)(null);
4812
+ const [ownPanel, setOwnPanel] = (0, react.useState)(null);
4813
+ /** The open card, kept by the owner when there is one, for the reason above. */
4814
+ const openPanel = props.openPanel === void 0 ? ownPanel : props.openPanel;
4815
+ const setOpenPanel = (0, react.useCallback)((next) => {
4816
+ const value = typeof next === "function" ? next(openPanel) : next;
4817
+ if (props.onOpenPanelChange === void 0) setOwnPanel(value);
4818
+ else props.onOpenPanelChange(value);
4819
+ }, [openPanel, props.onOpenPanelChange]);
4447
4820
  const styleOpen = openPanel === "style";
4448
4821
  const goalOpen = openPanel === "goal";
4449
4822
  const listOpen = openPanel === "list";
@@ -4481,7 +4854,19 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4481
4854
  * the reader picks a colour is the moment it has to be safe. Storage failures are
4482
4855
  * swallowed inside `rememberPreferences` — losing a colour is not worth an error.
4483
4856
  */
4484
- const [zoom, setZoom] = (0, react.useState)("fit");
4857
+ /**
4858
+ * The factor this figure is drawn at.
4859
+ *
4860
+ * An owner that magnifies the whole document hands the factor in and takes every
4861
+ * gesture back, so the size follows the reader from page to page instead of
4862
+ * restarting at fit on each sheet. A body mounted alone keeps its own.
4863
+ */
4864
+ const [ownZoom, setOwnZoom] = (0, react.useState)("fit");
4865
+ const zoom = props.zoom ?? ownZoom;
4866
+ const setZoom = (0, react.useCallback)((next) => {
4867
+ if (props.onZoom === void 0) setOwnZoom(next);
4868
+ else props.onZoom(next);
4869
+ }, [props.onZoom]);
4485
4870
  /**
4486
4871
  * Whether a drag is currently moving the view.
4487
4872
  *
@@ -4493,6 +4878,11 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4493
4878
  const [fitWidth, setFitWidth] = (0, react.useState)(0);
4494
4879
  const [busy, setBusy] = (0, react.useState)(null);
4495
4880
  const [status, setStatus] = (0, react.useState)(null);
4881
+ /** Where the figure's context menu was asked for, or null when it is closed. */
4882
+ const [menuAt, setMenuAt] = (0, react.useState)(null);
4883
+ /** Whether this Host can open and reveal paths; read once per mount. */
4884
+ const [canOpen, setCanOpen] = (0, react.useState)(false);
4885
+ const menuRef = (0, react.useRef)(null);
4496
4886
  /** The panel's own box, for placing the cards that float over it. */
4497
4887
  const rootRef = (0, react.useRef)(null);
4498
4888
  /**
@@ -4505,6 +4895,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4505
4895
  * off the edge when it narrows.
4506
4896
  */
4507
4897
  const [palette, setPalette] = (0, react.useState)(remembered.palette);
4898
+ const paletteRef = (0, react.useRef)(null);
4508
4899
  const [dragging, setDragging] = (0, react.useState)(false);
4509
4900
  const dragFrom = (0, react.useRef)(null);
4510
4901
  /**
@@ -4550,24 +4941,30 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4550
4941
  * @param from - which strip's press is in progress.
4551
4942
  * @param write - where its new spot goes.
4552
4943
  */
4553
- const followPointer = (0, react.useCallback)((event, from, write) => {
4944
+ const followPointer = (0, react.useCallback)((event, from, write, strip) => {
4554
4945
  const held = from.current;
4555
- const root = rootRef.current;
4946
+ const root = strip?.offsetParent ?? rootRef.current;
4556
4947
  /* v8 ignore next */
4557
- if (held === null || root === null) return;
4948
+ if (held === null || root === null || !(root instanceof Element)) return;
4558
4949
  const bounds = root.getBoundingClientRect();
4559
4950
  const x = held.origin.x + (event.clientX - held.box.left) / Math.max(1, bounds.width);
4560
4951
  const y = held.origin.y + (event.clientY - held.box.top) / Math.max(1, bounds.height);
4952
+ /* v8 ignore start -- both call sites hold a live ref for the whole drag. */
4953
+ const width = strip?.offsetWidth ?? 0;
4954
+ const height = strip?.offsetHeight ?? 0;
4955
+ /* v8 ignore stop */
4956
+ const roomX = Math.max(0, bounds.width - width) / Math.max(1, bounds.width);
4957
+ const roomY = Math.max(0, bounds.height - height) / Math.max(1, bounds.height);
4561
4958
  write({
4562
- x: Math.min(1, Math.max(0, x)),
4563
- y: Math.min(1, Math.max(0, y))
4959
+ x: Math.min(roomX, Math.max(0, x)),
4960
+ y: Math.min(roomY, Math.max(0, y))
4564
4961
  });
4565
4962
  }, []);
4566
4963
  const dragPalette = (0, react.useCallback)((event) => {
4567
- followPointer(event, dragFrom, setPalette);
4964
+ followPointer(event, dragFrom, setPalette, paletteRef.current);
4568
4965
  }, [followPointer]);
4569
4966
  const dragBar = (0, react.useCallback)((event) => {
4570
- followPointer(event, barFrom, setBarSpot);
4967
+ followPointer(event, barFrom, setBarSpot, barRef.current);
4571
4968
  }, [followPointer]);
4572
4969
  (0, react.useEffect)(() => {
4573
4970
  rememberPreferences({
@@ -4819,12 +5216,14 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
4819
5216
  const scale = (0, react.useMemo)(() => {
4820
5217
  if (size === null) return 1;
4821
5218
  if (zoom !== "fit") return zoom;
4822
- if (fitWidth <= 0) return 1;
4823
- return Math.min(1.5, Math.max(.05, fitWidth / size.width));
5219
+ const target = props.fitWidth ?? fitWidth;
5220
+ if (target <= 0) return 1;
5221
+ return Math.min(1.5, Math.max(.05, target / size.width));
4824
5222
  }, [
4825
5223
  zoom,
4826
5224
  fitWidth,
4827
- size
5225
+ size,
5226
+ props.fitWidth
4828
5227
  ]);
4829
5228
  /**
4830
5229
  * Where the records hang: above the bar, centred on it.
@@ -5013,13 +5412,20 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5013
5412
  const stage = stageRef.current;
5014
5413
  if (stage === null) return;
5015
5414
  /**
5016
- * A trackpad pinch arrives as a ctrl-modified wheel. The browser would zoom
5017
- * the whole app for it, and the plugin's own stage is the surface the user is
5018
- * pointing at, so the event is taken here. A plain two-finger scroll is left
5019
- * alone: that one pans the scrollport, which is what it means.
5415
+ * The wheel is the magnification, whatever modifier it carries.
5416
+ *
5417
+ * It used to be two gestures in one: a plain wheel panned the scrollport, and
5418
+ * only a ctrl-modified one — which is how a trackpad reports a pinch — zoomed.
5419
+ * That split is what made the gesture ambiguous: over a page that fits the pane
5420
+ * there is nothing to scroll, so a plain wheel looks like a request for another
5421
+ * page, while the reader who wants a closer look makes exactly the same motion.
5422
+ * One motion, one meaning: up magnifies, down shrinks, and a pinch arrives as the
5423
+ * same thing with a modifier on it.
5424
+ *
5425
+ * Panning is still there — a drag with the select tool moves the view — so
5426
+ * nothing is lost by taking the wheel's second job away from it.
5020
5427
  */
5021
5428
  const onWheel = (event) => {
5022
- if (!event.ctrlKey) return;
5023
5429
  event.preventDefault();
5024
5430
  zoomAt(liveScale.current * wheelZoomFactor(event.deltaY, event.deltaMode, stage.clientHeight), event.clientX, event.clientY);
5025
5431
  };
@@ -5037,9 +5443,17 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5037
5443
  commit([...marks, mark]);
5038
5444
  }, [commit, marks]);
5039
5445
  const removeMark = (0, react.useCallback)((id) => {
5446
+ if (props.onDocumentRemove !== void 0 && !marks.some((mark) => mark.id === id)) {
5447
+ props.onDocumentRemove(id);
5448
+ return;
5449
+ }
5040
5450
  commit(marks.filter((mark) => mark.id !== id));
5041
5451
  setSelectedId((current) => current === id ? null : current);
5042
- }, [commit, marks]);
5452
+ }, [
5453
+ commit,
5454
+ marks,
5455
+ props.onDocumentRemove
5456
+ ]);
5043
5457
  /**
5044
5458
  * Write one mark's detail.
5045
5459
  *
@@ -5464,6 +5878,178 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5464
5878
  size,
5465
5879
  t
5466
5880
  ]);
5881
+ /**
5882
+ * Probe the Host once, so the menu can offer the gestures it can perform.
5883
+ *
5884
+ * `canOpenWorkspacePath` answers for the whole page, and a probe that fails is
5885
+ * the same answer as "no desktop": rows that would fail every time are left out
5886
+ * rather than shown and refused.
5887
+ */
5888
+ (0, react.useEffect)(() => {
5889
+ let live = true;
5890
+ desktopAvailable(props.remote).then((available) => {
5891
+ if (live) setCanOpen(available);
5892
+ });
5893
+ return () => {
5894
+ live = false;
5895
+ };
5896
+ }, [props.remote]);
5897
+ /**
5898
+ * The rows this figure's menu offers, decided once per figure and Host.
5899
+ *
5900
+ * Empty until a figure has loaded: the rows all name its path, so there is
5901
+ * nothing to offer before there is one.
5902
+ */
5903
+ const menuItems = (0, react.useMemo)(() => {
5904
+ if (loaded === null) return [];
5905
+ return figureMenuItems({
5906
+ canOpen,
5907
+ canSend: !isPage && marks.length > 0 && busy === null,
5908
+ hasRelative: relativeTo(loaded.path, loaded.workspace) !== null,
5909
+ hasImage: layer !== null
5910
+ }, {
5911
+ sendToAgent: t("menuSendToAgent"),
5912
+ sendToComposer: t("menuSendToComposer"),
5913
+ open: t("menuOpen"),
5914
+ reveal: t("menuReveal"),
5915
+ copyImage: t("menuCopyImage"),
5916
+ copyRelative: t("menuCopyRelative"),
5917
+ copyAbsolute: t("menuCopyAbsolute")
5918
+ });
5919
+ }, [
5920
+ busy,
5921
+ canOpen,
5922
+ isPage,
5923
+ layer,
5924
+ loaded,
5925
+ marks.length,
5926
+ t
5927
+ ]);
5928
+ /**
5929
+ * Open the menu at the pointer, kept inside the window.
5930
+ * @param event - the right-click on the figure.
5931
+ */
5932
+ const openMenu = (0, react.useCallback)((event) => {
5933
+ /* v8 ignore next -- nothing to act on until a figure loaded. */
5934
+ if (loaded === null) return;
5935
+ event.preventDefault();
5936
+ const height = menuItems.length * 27;
5937
+ const placed = clampMenu({
5938
+ x: event.clientX,
5939
+ y: event.clientY
5940
+ }, {
5941
+ width: window.innerWidth,
5942
+ height: window.innerHeight
5943
+ }, {
5944
+ width: 208,
5945
+ height
5946
+ });
5947
+ setMenuAt({
5948
+ ...placed,
5949
+ path: loaded.path,
5950
+ workspace: loaded.workspace
5951
+ });
5952
+ }, [loaded, menuItems]);
5953
+ (0, react.useEffect)(() => {
5954
+ if (menuAt === null) return;
5955
+ const onDown = (event) => {
5956
+ if (pressedInsideMenu(event, menuRef.current)) return;
5957
+ setMenuAt(null);
5958
+ };
5959
+ const onKey = (event) => {
5960
+ if (event.key === "Escape") setMenuAt(null);
5961
+ };
5962
+ document.addEventListener("pointerdown", onDown, true);
5963
+ document.addEventListener("keydown", onKey);
5964
+ return () => {
5965
+ document.removeEventListener("pointerdown", onDown, true);
5966
+ document.removeEventListener("keydown", onKey);
5967
+ };
5968
+ }, [menuAt]);
5969
+ /**
5970
+ * Carry out one menu row.
5971
+ *
5972
+ * The two copies are the only rows that touch the clipboard, and they say so in
5973
+ * the status line: a copy with no feedback reads as a menu that did nothing.
5974
+ * @param id - the row the reader picked.
5975
+ * @param path - the figure's own path, as the Host named it.
5976
+ * @param workspace - the session's working directory, when there is one.
5977
+ */
5978
+ const runMenu = (0, react.useCallback)((id, path, workspace) => {
5979
+ setMenuAt(null);
5980
+ if (id === "sendToAgent") {
5981
+ run("agent");
5982
+ return;
5983
+ }
5984
+ if (id === "sendToComposer") {
5985
+ run("composer");
5986
+ return;
5987
+ }
5988
+ if (id === "open") {
5989
+ openOnDesktop(props.remote, path).then((ok) => {
5990
+ setStatus(ok ? {
5991
+ tone: "ok",
5992
+ text: t("menuOpened")
5993
+ } : {
5994
+ tone: "error",
5995
+ text: t("menuOpenFailed")
5996
+ });
5997
+ });
5998
+ return;
5999
+ }
6000
+ if (id === "reveal") {
6001
+ revealOnDesktop(props.remote, path).then((ok) => {
6002
+ setStatus(ok ? {
6003
+ tone: "ok",
6004
+ text: t("menuRevealed")
6005
+ } : {
6006
+ tone: "error",
6007
+ text: t("menuRevealFailed")
6008
+ });
6009
+ });
6010
+ return;
6011
+ }
6012
+ if (id === "copyImage") {
6013
+ /* v8 ignore next -- the row is only offered for a figure that has a layer. */
6014
+ if (layer === null) return;
6015
+ exportPng(layer).then(async (image) => {
6016
+ const copied = await copyImageToClipboard(image);
6017
+ setStatus(copied ? {
6018
+ tone: "ok",
6019
+ text: t("menuCopied")
6020
+ } : {
6021
+ tone: "error",
6022
+ text: t("menuCopyFailed")
6023
+ });
6024
+ }, () => {
6025
+ setStatus({
6026
+ tone: "error",
6027
+ text: t("menuCopyFailed")
6028
+ });
6029
+ });
6030
+ return;
6031
+ }
6032
+ const text = copiedText(id, path, workspace);
6033
+ /* v8 ignore next */
6034
+ if (text === null) return;
6035
+ navigator.clipboard.writeText(text).then(() => {
6036
+ setStatus({
6037
+ tone: "ok",
6038
+ text: t("menuCopied")
6039
+ });
6040
+ }, () => {
6041
+ setStatus({
6042
+ tone: "error",
6043
+ text: t("menuCopyFailed")
6044
+ });
6045
+ });
6046
+ }, [
6047
+ exportPng,
6048
+ layer,
6049
+ props.remote,
6050
+ run,
6051
+ t
6052
+ ]);
5467
6053
  /** Whether this body holds a rendered document rather than a figure file. */
5468
6054
  const isHtml = mediaType === "text/html";
5469
6055
  /** Name what one figure point landed on, for a surface that holds its own elements. */
@@ -5481,6 +6067,18 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5481
6067
  role: "alert",
5482
6068
  children: figureError
5483
6069
  });
6070
+ /**
6071
+ * Press a row of the list: show that mark and put the pill beside it.
6072
+ *
6073
+ * The selection goes first and the page turn second, because a turn remounts this
6074
+ * body for another page and the mark has to already be the one selected when the
6075
+ * new page's pill is measured — otherwise the row would turn the page and leave
6076
+ * the reader with nothing to read on it.
6077
+ */
6078
+ const reveal = (mark) => {
6079
+ setSelectedId(mark.id);
6080
+ revealOnPage(mark);
6081
+ };
5484
6082
  const selected = marks.find((mark) => mark.id === selectedId) ?? null;
5485
6083
  /**
5486
6084
  * What the colour control currently means.
@@ -5563,6 +6161,240 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5563
6161
  width: size?.width,
5564
6162
  height: size?.height
5565
6163
  };
6164
+ /**
6165
+ * The floating chrome: the weight rail, the tool palette and the bar.
6166
+ *
6167
+ * Assembled as a value rather than drawn in place because it may be rendered
6168
+ * somewhere else: a paged document hands this body a node of the pane, and a bar
6169
+ * drawn inside a page is a bar that scrolls with that page.
6170
+ */
6171
+ const chromeStrips = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6172
+ className: "da-palette",
6173
+ ref: paletteRef,
6174
+ "data-dragging": dragging ? "true" : void 0,
6175
+ style: paletteStyle,
6176
+ role: "toolbar",
6177
+ "aria-label": t("tools"),
6178
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6179
+ className: "da-palette-grip",
6180
+ title: t("moveTools"),
6181
+ onPointerDown: (event) => {
6182
+ /* v8 ignore next */
6183
+ if (rootRef.current === null) return;
6184
+ /* v8 ignore next -- jsdom has no pointer capture; a browser always does. */
6185
+ event.currentTarget.setPointerCapture?.(event.pointerId);
6186
+ dragFrom.current = {
6187
+ pointerId: event.pointerId,
6188
+ origin: palette ?? DEFAULT_PALETTE,
6189
+ box: {
6190
+ left: event.clientX,
6191
+ top: event.clientY
6192
+ }
6193
+ };
6194
+ setDragging(true);
6195
+ },
6196
+ onPointerMove: (event) => {
6197
+ dragPalette(event);
6198
+ },
6199
+ onPointerUp: (event) => {
6200
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
6201
+ dragFrom.current = null;
6202
+ setDragging(false);
6203
+ },
6204
+ onPointerCancel: () => {
6205
+ dragFrom.current = null;
6206
+ setDragging(false);
6207
+ },
6208
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: true })
6209
+ }), TOOLS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6210
+ type: "button",
6211
+ className: "da-btn da-tool",
6212
+ "aria-pressed": tool === entry.tool,
6213
+ "aria-label": t(entry.label),
6214
+ title: t(entry.label),
6215
+ onClick: () => {
6216
+ setTool(entry.tool);
6217
+ },
6218
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6219
+ className: "da-tool-icon",
6220
+ "aria-hidden": true,
6221
+ children: entry.tool === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointerIcon, {}) : ToolIcon(entry.tool)
6222
+ })
6223
+ }, entry.tool))]
6224
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6225
+ className: "da-bar",
6226
+ ref: barRef,
6227
+ "data-dragging": barDragging ? "true" : void 0,
6228
+ "data-parked": barSpot === void 0 ? void 0 : "true",
6229
+ style: barStyle,
6230
+ children: [
6231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6232
+ className: "da-palette-grip",
6233
+ title: t("moveBar"),
6234
+ onPointerDown: (event) => {
6235
+ /* v8 ignore next */
6236
+ if (rootRef.current === null) return;
6237
+ /* v8 ignore next -- jsdom has no pointer capture; a browser always does. */
6238
+ event.currentTarget.setPointerCapture?.(event.pointerId);
6239
+ barFrom.current = {
6240
+ pointerId: event.pointerId,
6241
+ origin: barSpot ?? DEFAULT_PALETTE,
6242
+ box: {
6243
+ left: event.clientX,
6244
+ top: event.clientY
6245
+ }
6246
+ };
6247
+ setBarDragging(true);
6248
+ },
6249
+ onPointerMove: (event) => {
6250
+ dragBar(event);
6251
+ },
6252
+ onPointerUp: (event) => {
6253
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
6254
+ barFrom.current = null;
6255
+ setBarDragging(false);
6256
+ },
6257
+ onPointerCancel: () => {
6258
+ barFrom.current = null;
6259
+ setBarDragging(false);
6260
+ },
6261
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: false })
6262
+ }),
6263
+ props.pagingSlot === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.pagingSlot, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6264
+ className: "da-sep",
6265
+ "aria-hidden": true
6266
+ })] }),
6267
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6268
+ className: "da-bar-tools",
6269
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6270
+ type: "button",
6271
+ className: "da-btn da-style",
6272
+ "aria-expanded": styleOpen,
6273
+ "aria-label": t("style"),
6274
+ title: t("style"),
6275
+ onClick: () => {
6276
+ setOpenPanel((open) => open === "style" ? null : "style");
6277
+ },
6278
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6279
+ className: "da-swatch-dot",
6280
+ style: { background: shownColor }
6281
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6282
+ className: "da-style-caret",
6283
+ "aria-hidden": true,
6284
+ children: styleOpen ? "▴" : "▾"
6285
+ })]
6286
+ })
6287
+ }),
6288
+ styleOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6289
+ className: "da-style-row",
6290
+ role: "group",
6291
+ "aria-label": t("style"),
6292
+ "data-side": foldOpensDown ? "below" : "above",
6293
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6294
+ className: "da-style-label",
6295
+ children: t("styleColor")
6296
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6297
+ className: "da-style-colors",
6298
+ children: COLORS.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6299
+ type: "button",
6300
+ className: "da-swatch",
6301
+ "aria-pressed": shownColor === value,
6302
+ style: { background: value },
6303
+ "aria-label": value,
6304
+ title: value,
6305
+ onClick: () => {
6306
+ chooseColor(value);
6307
+ }
6308
+ }, value))
6309
+ })]
6310
+ }) : null,
6311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6312
+ className: "da-sep",
6313
+ "aria-hidden": true
6314
+ }),
6315
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "da-spacer" }),
6316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6317
+ type: "button",
6318
+ className: "da-dock-btn",
6319
+ "aria-pressed": goalOpen,
6320
+ "aria-label": t("summary"),
6321
+ title: t("summary"),
6322
+ onClick: () => {
6323
+ setOpenPanel((open) => open === "goal" ? null : "goal");
6324
+ },
6325
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GoalIcon, {}), summary.trim() === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6326
+ className: "da-dock-dot",
6327
+ "aria-hidden": true
6328
+ })]
6329
+ }),
6330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6331
+ type: "button",
6332
+ className: "da-dock-btn",
6333
+ "aria-pressed": listOpen,
6334
+ "aria-label": t("marks"),
6335
+ title: t("marks"),
6336
+ onClick: () => {
6337
+ setOpenPanel((open) => open === "list" ? null : "list");
6338
+ },
6339
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListIcon, {}), listedMarks.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6340
+ className: "da-dock-count",
6341
+ children: listedMarks.length
6342
+ })]
6343
+ }),
6344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6345
+ className: "da-dock-sep",
6346
+ "aria-hidden": true
6347
+ }),
6348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6349
+ type: "button",
6350
+ className: "da-dock-btn",
6351
+ "aria-label": t("undo"),
6352
+ title: t("undo"),
6353
+ disabled: past.length === 0,
6354
+ onClick: undo,
6355
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UndoIcon, {})
6356
+ }),
6357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6358
+ type: "button",
6359
+ className: "da-dock-btn",
6360
+ "aria-label": t("redo"),
6361
+ title: t("redo"),
6362
+ disabled: future.length === 0,
6363
+ onClick: redo,
6364
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedoIcon, {})
6365
+ }),
6366
+ isPage && props.onSend === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6368
+ className: "da-dock-sep",
6369
+ "aria-hidden": true
6370
+ }),
6371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6372
+ type: "button",
6373
+ className: "da-dock-btn",
6374
+ disabled: busy !== null,
6375
+ "aria-label": t("sendToAgent"),
6376
+ title: t("sendToAgent"),
6377
+ onClick: () => {
6378
+ if (props.onSend === void 0) run("agent");
6379
+ else props.onSend("agent");
6380
+ },
6381
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendIcon, {})
6382
+ }),
6383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6384
+ type: "button",
6385
+ className: "da-dock-btn",
6386
+ disabled: busy !== null,
6387
+ "aria-label": t("sendToComposer"),
6388
+ title: t("sendToComposer"),
6389
+ onClick: () => {
6390
+ if (props.onSend === void 0) run("composer");
6391
+ else props.onSend("composer");
6392
+ },
6393
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatIcon, {})
6394
+ })
6395
+ ] })
6396
+ ]
6397
+ })] });
5566
6398
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5567
6399
  className: "da-root",
5568
6400
  ref: rootRef,
@@ -5585,6 +6417,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5585
6417
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5586
6418
  className: "da-stage",
5587
6419
  ref: attachStage,
6420
+ onContextMenu: openMenu,
5588
6421
  "data-hand": tool === "select" ? "true" : void 0,
5589
6422
  "data-panning": panning ? "true" : void 0,
5590
6423
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -5658,6 +6491,25 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5658
6491
  })
5659
6492
  })
5660
6493
  }),
6494
+ menuAt === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6495
+ className: "da-menu",
6496
+ ref: menuRef,
6497
+ role: "menu",
6498
+ "aria-label": t("menuLabel"),
6499
+ style: {
6500
+ left: menuAt.left,
6501
+ top: menuAt.top
6502
+ },
6503
+ children: menuItems.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6504
+ type: "button",
6505
+ className: "da-menu-item",
6506
+ role: "menuitem",
6507
+ onClick: () => {
6508
+ runMenu(item.id, menuAt.path, menuAt.workspace);
6509
+ },
6510
+ children: item.label
6511
+ }, item.id))
6512
+ }),
5661
6513
  goalOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5662
6514
  className: "da-float da-float-record da-float-goal",
5663
6515
  role: "group",
@@ -5715,14 +6567,14 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5715
6567
  t("marks"),
5716
6568
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5717
6569
  className: "da-card-count",
5718
- children: marks.length
6570
+ children: listedMarks.length
5719
6571
  })
5720
6572
  ]
5721
6573
  }),
5722
6574
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5723
6575
  type: "button",
5724
6576
  className: "da-btn da-btn-quiet",
5725
- disabled: marks.length === 0,
6577
+ disabled: listedMarks.length === 0,
5726
6578
  onClick: () => {
5727
6579
  discard();
5728
6580
  },
@@ -5741,7 +6593,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5741
6593
  ]
5742
6594
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5743
6595
  className: "da-marks",
5744
- children: marks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6596
+ children: listedMarks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5745
6597
  className: "da-empty",
5746
6598
  children: [
5747
6599
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BoxIcon, {}),
@@ -5754,41 +6606,56 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5754
6606
  children: t("noMarksHint")
5755
6607
  })
5756
6608
  ]
5757
- }) : marks.map((mark, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6609
+ }) : listedMarks.map((mark, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5758
6610
  className: "da-mark",
5759
6611
  "data-selected": mark.id === selectedId,
5760
6612
  onClick: () => {
5761
- setSelectedId(mark.id);
6613
+ reveal(mark);
5762
6614
  },
5763
6615
  children: [
5764
6616
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5765
- className: "da-mark-index",
5766
- style: { background: mark.color },
6617
+ className: "da-mark-n",
5767
6618
  children: index + 1
5768
6619
  }),
6620
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6621
+ className: "da-mark-glyph",
6622
+ style: { color: mark.color },
6623
+ title: t("markKindLabel", { kind: t(TOOL_COPY[mark.kind]) }),
6624
+ children: ToolIcon(mark.kind)
6625
+ }),
5769
6626
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5770
6627
  type: "button",
5771
6628
  className: "da-mark-text",
5772
6629
  onClick: (event) => {
5773
6630
  event.stopPropagation();
5774
- setSelectedId(mark.id);
6631
+ reveal(mark);
5775
6632
  },
5776
6633
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5777
6634
  className: "da-mark-title",
5778
6635
  children: mark.detail !== void 0 && mark.detail.trim() !== "" ? mark.detail : t("emptyDetail")
5779
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6636
+ }), anchorName(mark) === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5780
6637
  className: "da-mark-meta",
5781
- children: [t(TOOL_COPY[mark.kind]), anchorName(mark)].filter((part) => part !== "").join(" · ")
6638
+ children: anchorName(mark)
5782
6639
  })]
5783
6640
  }),
6641
+ mark.page === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6642
+ className: "da-mark-page",
6643
+ children: t("markPage", { page: mark.page })
6644
+ }),
5784
6645
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5785
6646
  type: "button",
5786
6647
  className: "da-mark-del",
6648
+ "aria-label": t("delete"),
6649
+ title: t("delete"),
5787
6650
  onClick: (event) => {
5788
6651
  event.stopPropagation();
5789
6652
  removeMark(mark.id);
5790
6653
  },
5791
- children: t("delete")
6654
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6655
+ className: "da-mark-del-x",
6656
+ "aria-hidden": "true",
6657
+ children: "✕"
6658
+ })
5792
6659
  })
5793
6660
  ]
5794
6661
  }, mark.id))
@@ -5837,7 +6704,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5837
6704
  })]
5838
6705
  })]
5839
6706
  }),
5840
- shownWeight === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6707
+ !chrome || shownWeight === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5841
6708
  className: "da-weight",
5842
6709
  title: t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
5843
6710
  children: [
@@ -5895,227 +6762,7 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
5895
6762
  })
5896
6763
  ]
5897
6764
  }),
5898
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5899
- className: "da-palette",
5900
- "data-dragging": dragging ? "true" : void 0,
5901
- style: paletteStyle,
5902
- role: "toolbar",
5903
- "aria-label": t("tools"),
5904
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5905
- className: "da-palette-grip",
5906
- title: t("moveTools"),
5907
- onPointerDown: (event) => {
5908
- /* v8 ignore next */
5909
- if (rootRef.current === null) return;
5910
- /* v8 ignore next -- jsdom has no pointer capture; a browser always does. */
5911
- event.currentTarget.setPointerCapture?.(event.pointerId);
5912
- dragFrom.current = {
5913
- pointerId: event.pointerId,
5914
- origin: palette ?? DEFAULT_PALETTE,
5915
- box: {
5916
- left: event.clientX,
5917
- top: event.clientY
5918
- }
5919
- };
5920
- setDragging(true);
5921
- },
5922
- onPointerMove: (event) => {
5923
- dragPalette(event);
5924
- },
5925
- onPointerUp: (event) => {
5926
- event.currentTarget.releasePointerCapture?.(event.pointerId);
5927
- dragFrom.current = null;
5928
- setDragging(false);
5929
- },
5930
- onPointerCancel: () => {
5931
- dragFrom.current = null;
5932
- setDragging(false);
5933
- },
5934
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: true })
5935
- }), TOOLS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5936
- type: "button",
5937
- className: "da-btn da-tool",
5938
- "aria-pressed": tool === entry.tool,
5939
- "aria-label": t(entry.label),
5940
- title: t(entry.label),
5941
- onClick: () => {
5942
- setTool(entry.tool);
5943
- },
5944
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5945
- className: "da-tool-icon",
5946
- "aria-hidden": true,
5947
- children: entry.tool === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointerIcon, {}) : ToolIcon(entry.tool)
5948
- })
5949
- }, entry.tool))]
5950
- }),
5951
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5952
- className: "da-bar",
5953
- ref: barRef,
5954
- "data-dragging": barDragging ? "true" : void 0,
5955
- "data-parked": barSpot === void 0 ? void 0 : "true",
5956
- style: barStyle,
5957
- children: [
5958
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5959
- className: "da-palette-grip",
5960
- title: t("moveBar"),
5961
- onPointerDown: (event) => {
5962
- /* v8 ignore next */
5963
- if (rootRef.current === null) return;
5964
- /* v8 ignore next -- jsdom has no pointer capture; a browser always does. */
5965
- event.currentTarget.setPointerCapture?.(event.pointerId);
5966
- barFrom.current = {
5967
- pointerId: event.pointerId,
5968
- origin: barSpot ?? DEFAULT_PALETTE,
5969
- box: {
5970
- left: event.clientX,
5971
- top: event.clientY
5972
- }
5973
- };
5974
- setBarDragging(true);
5975
- },
5976
- onPointerMove: (event) => {
5977
- dragBar(event);
5978
- },
5979
- onPointerUp: (event) => {
5980
- event.currentTarget.releasePointerCapture?.(event.pointerId);
5981
- barFrom.current = null;
5982
- setBarDragging(false);
5983
- },
5984
- onPointerCancel: () => {
5985
- barFrom.current = null;
5986
- setBarDragging(false);
5987
- },
5988
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: false })
5989
- }),
5990
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5991
- className: "da-bar-tools",
5992
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5993
- type: "button",
5994
- className: "da-btn da-style",
5995
- "aria-expanded": styleOpen,
5996
- "aria-label": t("style"),
5997
- title: t("style"),
5998
- onClick: () => {
5999
- setOpenPanel((open) => open === "style" ? null : "style");
6000
- },
6001
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6002
- className: "da-swatch-dot",
6003
- style: { background: shownColor }
6004
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6005
- className: "da-style-caret",
6006
- "aria-hidden": true,
6007
- children: styleOpen ? "▴" : "▾"
6008
- })]
6009
- })
6010
- }),
6011
- styleOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6012
- className: "da-style-row",
6013
- role: "group",
6014
- "aria-label": t("style"),
6015
- "data-side": foldOpensDown ? "below" : "above",
6016
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6017
- className: "da-style-label",
6018
- children: t("styleColor")
6019
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6020
- className: "da-style-colors",
6021
- children: COLORS.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6022
- type: "button",
6023
- className: "da-swatch",
6024
- "aria-pressed": shownColor === value,
6025
- style: { background: value },
6026
- "aria-label": value,
6027
- title: value,
6028
- onClick: () => {
6029
- chooseColor(value);
6030
- }
6031
- }, value))
6032
- })]
6033
- }) : null,
6034
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6035
- className: "da-sep",
6036
- "aria-hidden": true
6037
- }),
6038
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "da-spacer" }),
6039
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6040
- type: "button",
6041
- className: "da-dock-btn",
6042
- "aria-pressed": goalOpen,
6043
- "aria-label": t("summary"),
6044
- title: t("summary"),
6045
- onClick: () => {
6046
- setOpenPanel((open) => open === "goal" ? null : "goal");
6047
- },
6048
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GoalIcon, {}), summary.trim() === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6049
- className: "da-dock-dot",
6050
- "aria-hidden": true
6051
- })]
6052
- }),
6053
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6054
- type: "button",
6055
- className: "da-dock-btn",
6056
- "aria-pressed": listOpen,
6057
- "aria-label": t("marks"),
6058
- title: t("marks"),
6059
- onClick: () => {
6060
- setOpenPanel((open) => open === "list" ? null : "list");
6061
- },
6062
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListIcon, {}), marks.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6063
- className: "da-dock-count",
6064
- children: marks.length
6065
- })]
6066
- }),
6067
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6068
- className: "da-dock-sep",
6069
- "aria-hidden": true
6070
- }),
6071
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6072
- type: "button",
6073
- className: "da-dock-btn",
6074
- "aria-label": t("undo"),
6075
- title: t("undo"),
6076
- disabled: past.length === 0,
6077
- onClick: undo,
6078
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UndoIcon, {})
6079
- }),
6080
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6081
- type: "button",
6082
- className: "da-dock-btn",
6083
- "aria-label": t("redo"),
6084
- title: t("redo"),
6085
- disabled: future.length === 0,
6086
- onClick: redo,
6087
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedoIcon, {})
6088
- }),
6089
- isPage ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6090
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6091
- className: "da-dock-sep",
6092
- "aria-hidden": true
6093
- }),
6094
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6095
- type: "button",
6096
- className: "da-dock-btn da-dock-btn-primary",
6097
- disabled: busy !== null,
6098
- "aria-label": t("sendToAgent"),
6099
- title: t("sendToAgent"),
6100
- onClick: () => {
6101
- run("agent");
6102
- },
6103
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendIcon, {})
6104
- }),
6105
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6106
- type: "button",
6107
- className: "da-dock-btn",
6108
- disabled: busy !== null,
6109
- "aria-label": t("sendToComposer"),
6110
- title: t("sendToComposer"),
6111
- onClick: () => {
6112
- run("composer");
6113
- },
6114
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatIcon, {})
6115
- })
6116
- ] })
6117
- ]
6118
- })
6765
+ !chrome ? null : props.chromeHost === void 0 || props.chromeHost === null ? chromeStrips : (0, react_dom.createPortal)(chromeStrips, props.chromeHost)
6119
6766
  ]
6120
6767
  });
6121
6768
  }
@@ -6739,7 +7386,8 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
6739
7386
  sessions: ctx.get("sessions"),
6740
7387
  conversation: ctx.get("conversation"),
6741
7388
  sessionId: String(sessionId),
6742
- localeId: locale.getLocale().locale ?? "zh"
7389
+ localeId: locale.getLocale().locale ?? "zh",
7390
+ remote: ctx.get("remote")
6743
7391
  })
6744
7392
  }, AnnotatorBody)), "dsh-figmark: document body");
6745
7393
  ctx.effect(() => previews.register({
@@ -6805,7 +7453,8 @@ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,1
6805
7453
  sessions: ctx.get("sessions"),
6806
7454
  conversation: ctx.get("conversation"),
6807
7455
  sessionId: String(sessionId),
6808
- localeId: locale.getLocale().locale ?? "zh"
7456
+ localeId: locale.getLocale().locale ?? "zh",
7457
+ remote: ctx.get("remote")
6809
7458
  })
6810
7459
  }, AnnotatorBody)), "dsh-figmark: html document body");
6811
7460
  }