dsh-figmark 1.1.2 → 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",
@@ -268,7 +311,7 @@ window.__ModuleLoader__.load({
268
311
  * @returns the value, pulled to the nearest end of the range.
269
312
  */
270
313
  function clampWeight(value) {
271
- return Math.min(4, Math.max(WEIGHT_MIN, value));
314
+ return Math.min(8, Math.max(WEIGHT_MIN, value));
272
315
  }
273
316
  /**
274
317
  * Whether a value is a stroke weight this plugin would write.
@@ -277,7 +320,7 @@ window.__ModuleLoader__.load({
277
320
  * @returns true when it is one.
278
321
  */
279
322
  function isWeight(value) {
280
- return typeof value === "number" && Number.isFinite(value) && value >= .5 && value <= 4;
323
+ return typeof value === "number" && Number.isFinite(value) && value >= .5 && value <= 8;
281
324
  }
282
325
  /**
283
326
  * Suffix naming one page's files inside a paged document's name.
@@ -326,6 +369,86 @@ window.__ModuleLoader__.load({
326
369
  function round(value) {
327
370
  return Math.round(value);
328
371
  }
372
+ /**
373
+ * Write a freehand stroke's extent as the box it was drawn in.
374
+ *
375
+ * The stroke itself is dozens of coordinates, most of which say nothing an agent
376
+ * can act on; the box says where on the figure to look, which is what the reading
377
+ * of the mark needs. `strokeExtents` names the area, and the phrase above it names
378
+ * the mark, so the pair still reads as one line.
379
+ * @param points - the stroke's sampled points.
380
+ * @returns the box as ` (x,y)-(x,y)`, or nothing for an empty stroke.
381
+ */
382
+ function strokeExtents(points) {
383
+ let left = Number.POSITIVE_INFINITY;
384
+ let top = Number.POSITIVE_INFINITY;
385
+ let right = Number.NEGATIVE_INFINITY;
386
+ let bottom = Number.NEGATIVE_INFINITY;
387
+ for (const [x, y] of points) {
388
+ left = Math.min(left, x);
389
+ top = Math.min(top, y);
390
+ right = Math.max(right, x);
391
+ bottom = Math.max(bottom, y);
392
+ }
393
+ if (!Number.isFinite(left)) return "";
394
+ return ` (${round(left)},${round(top)})-(${round(right)},${round(bottom)})`;
395
+ }
396
+ /**
397
+ * Write two dragged corners as the rectangle they make.
398
+ *
399
+ * A box dragged up and to the left arrives as (bottom-right, top-left), and the
400
+ * agent reading `(187,138)-(42,88)` has to take a min and a max before it knows
401
+ * which area is meant. Both corners are therefore written left-to-right and
402
+ * top-to-bottom, so the same box always reads the same way however it was drawn.
403
+ * @param first - where the drag started.
404
+ * @param second - where it ended.
405
+ * @returns the corner pair as `(x,y)-(x,y)`, or nothing when the mark is unplaced.
406
+ */
407
+ function extents(first, second) {
408
+ if (first === void 0 || second === void 0) return "";
409
+ return ` (${round(Math.min(first[0], second[0]))},${round(Math.min(first[1], second[1]))})-(${round(Math.max(first[0], second[0]))},${round(Math.max(first[1], second[1]))})`;
410
+ }
411
+ /**
412
+ * Say how far a mark reaches outside the figure, when it does.
413
+ *
414
+ * A negative coordinate is an accurate description — the origin is the figure's
415
+ * top-left corner, so -57 is 57 pixels above it — but a reader has to notice the
416
+ * sign and then reason about what it implies. Naming the overhang outright costs
417
+ * about a dozen characters and removes that step: the agent is told the mark is
418
+ * off the picture and by how much, rather than being left to infer it. A mark that
419
+ * sits inside the figure says nothing at all, which is the common case.
420
+ * @param mark - the mark to measure.
421
+ * @param figure - the figure's size, when the document knows it.
422
+ * @param locale - language of the surrounding message.
423
+ * @returns the sentence fragment to append, or nothing when the mark is inside.
424
+ */
425
+ function overhang(mark, figure, locale) {
426
+ const { width, height } = figure;
427
+ if (width === void 0 || height === void 0) return "";
428
+ let left = Number.POSITIVE_INFINITY;
429
+ let top = Number.POSITIVE_INFINITY;
430
+ let right = Number.NEGATIVE_INFINITY;
431
+ let bottom = Number.NEGATIVE_INFINITY;
432
+ for (const [x, y] of mark.points) {
433
+ left = Math.min(left, x);
434
+ top = Math.min(top, y);
435
+ right = Math.max(right, x);
436
+ bottom = Math.max(bottom, y);
437
+ }
438
+ const escape = (amount, zhWord, enWord) => {
439
+ const pixels = round(amount);
440
+ if (pixels <= 0) return "";
441
+ return locale === "zh" ? `${zhWord} ${pixels}` : `${enWord} ${pixels}`;
442
+ };
443
+ const escapes = [
444
+ escape(-left, "左", "left"),
445
+ escape(-top, "上", "up"),
446
+ escape(right - width, "右", "right"),
447
+ escape(bottom - height, "下", "down")
448
+ ].filter((part) => part !== "");
449
+ if (escapes.length === 0) return "";
450
+ return locale === "zh" ? `(超出图面:${escapes.join("、")} 像素)` : ` (off the figure: ${escapes.join(", ")} px)`;
451
+ }
329
452
  /** Describe one mark's shape in words, for the model-facing summary. */
330
453
  function describeShape(mark, locale) {
331
454
  const [first, second] = mark.points;
@@ -333,9 +456,12 @@ window.__ModuleLoader__.load({
333
456
  const turned = mark.rotation === void 0 || mark.rotation === 0 ? "" : zh ? `(旋转 ${Math.round(mark.rotation)}°)` : ` (rotated ${Math.round(mark.rotation)}°)`;
334
457
  switch (mark.kind) {
335
458
  case "arrow": return first !== void 0 && second !== void 0 ? zh ? `箭头 (${round(first[0])},${round(first[1])}) → (${round(second[0])},${round(second[1])})${turned}` : `arrow (${round(first[0])},${round(first[1])}) → (${round(second[0])},${round(second[1])})${turned}` : zh ? "箭头" : "arrow";
336
- case "rect": return first !== void 0 && second !== void 0 ? zh ? `框选 (${round(first[0])},${round(first[1])})-(${round(second[0])},${round(second[1])})${turned}` : `box (${round(first[0])},${round(first[1])})-(${round(second[0])},${round(second[1])})${turned}` : zh ? "框选" : "box";
337
- case "ellipse": return first !== void 0 && second !== void 0 ? zh ? `圈选 (${round(first[0])},${round(first[1])})-(${round(second[0])},${round(second[1])})${turned}` : `ellipse (${round(first[0])},${round(first[1])})-(${round(second[0])},${round(second[1])})${turned}` : zh ? "圈选" : "ellipse";
338
- case "pen": return zh ? `手绘线(${mark.points.length} 点)${turned}` : `freehand stroke (${mark.points.length} points)${turned}`;
459
+ case "rect": return (zh ? "框选" : "box") + extents(first, second) + turned;
460
+ case "ellipse": return (zh ? "圈选" : "ellipse") + extents(first, second) + turned;
461
+ case "pen": {
462
+ const box = strokeExtents(mark.points);
463
+ return `${zh ? "手绘线" : "freehand stroke"}${box}${turned}`;
464
+ }
339
465
  case "point": return first !== void 0 ? zh ? `单点位于 (${round(first[0])},${round(first[1])})` : `at (${round(first[0])},${round(first[1])})` : zh ? "单点" : "a point";
340
466
  }
341
467
  }
@@ -350,7 +476,7 @@ window.__ModuleLoader__.load({
350
476
  if (anchor.text !== void 0 && anchor.text !== "") parts.push(zh ? `元素文字「${anchor.text}」` : `element text "${anchor.text}"`);
351
477
  if (anchor.selector !== void 0 && anchor.selector !== "") parts.push(zh ? `选择器 ${anchor.selector}` : `selector ${anchor.selector}`);
352
478
  if (parts.length === 0) parts.push(`<${anchor.tag}>`);
353
- return `(${parts.join(",")})`;
479
+ return zh ? `(${parts.join(",")})` : ` (${parts.join(", ")})`;
354
480
  }
355
481
  /**
356
482
  * Render one mark as a numbered line the model reads.
@@ -362,9 +488,11 @@ window.__ModuleLoader__.load({
362
488
  * @param mark - the mark to describe.
363
489
  * @param index - zero-based position in the document, rendered as a circled number.
364
490
  * @param locale - language of the surrounding message.
491
+ * @param figure - the figure these marks belong to, when its size is known; a mark
492
+ * that reaches past it then says so rather than leaving the sign to be noticed.
365
493
  * @returns one line of the annotation summary.
366
494
  */
367
- function describeMark(mark, index, locale) {
495
+ function describeMark(mark, index, locale, figure) {
368
496
  const zh = locale === "zh";
369
497
  const marker = [
370
498
  "①",
@@ -380,8 +508,9 @@ window.__ModuleLoader__.load({
380
508
  ][index] ?? `${index + 1}.`;
381
509
  const page = mark.page === void 0 ? "" : zh ? `第 ${mark.page} 页 ` : `page ${mark.page} `;
382
510
  const detail = mark.detail !== void 0 && mark.detail.trim() !== "" ? mark.detail.trim() : "";
383
- const parts = [`${marker} ${page}${describeShape(mark, locale)}${describeAnchor(mark, locale)}`];
384
- if (detail !== "") parts.push(zh ? `要改:${detail}` : `change: ${detail}`);
511
+ const escape = figure === void 0 ? "" : overhang(mark, figure, locale);
512
+ const parts = [`${marker} ${page}${describeShape(mark, locale)}${describeAnchor(mark, locale)}${escape}`];
513
+ if (detail !== "") parts.push(zh ? `批注:${detail}` : `note: ${detail}`);
385
514
  if (detail === "") parts.push(zh ? "(未写说明)" : "(no detail)");
386
515
  return parts.join(zh ? "," : ", ");
387
516
  }
@@ -392,7 +521,7 @@ window.__ModuleLoader__.load({
392
521
  * @returns one line per mark.
393
522
  */
394
523
  function describeMarks(document, locale) {
395
- return document.marks.map((mark, index) => describeMark(mark, index, locale));
524
+ return document.marks.map((mark, index) => describeMark(mark, index, locale, document.figure));
396
525
  }
397
526
  //#endregion
398
527
  //#region src/shared/figure-kind.ts
@@ -867,7 +996,7 @@ window.__ModuleLoader__.load({
867
996
  * @param rotation - the mark's rotation in degrees.
868
997
  * @returns the handles.
869
998
  */
870
- function handlesFor(box, rotation) {
999
+ function handlesFor(box, rotation, size = 7) {
871
1000
  const centre = boxCentre(box);
872
1001
  const radians = rotation * Math.PI / 180;
873
1002
  const at = (x, y) => rotateAround([x, y], centre, radians);
@@ -884,11 +1013,23 @@ window.__ModuleLoader__.load({
884
1013
  });
885
1014
  placed.push({
886
1015
  id: "rotate",
887
- point: at(centre[0], box.y - 22)
1016
+ point: at(centre[0], box.y - armFor(size))
888
1017
  });
889
1018
  return placed;
890
1019
  }
891
1020
  /**
1021
+ * How far the turn handle stands off the top edge, for a handle of a given size.
1022
+ *
1023
+ * Proportional to the handle: a bigger grip on a short arm would have the circle
1024
+ * overlapping the corner squares, and a smaller one on a long arm would look
1025
+ * detached from the frame it turns.
1026
+ * @param size - the handle's side, in figure pixels.
1027
+ * @returns the distance from the top edge, in figure pixels.
1028
+ */
1029
+ function armFor(size) {
1030
+ return size * (22 / 7);
1031
+ }
1032
+ /**
892
1033
  * The line that ties the rotation handle to the box it turns.
893
1034
  *
894
1035
  * It reads as attached to the mark only because it is anchored on the top edge
@@ -897,11 +1038,11 @@ window.__ModuleLoader__.load({
897
1038
  * @param rotation - the mark's rotation in degrees.
898
1039
  * @returns the two ends of the tether, from the turn handle to the top edge.
899
1040
  */
900
- function tetherFor(box, rotation) {
1041
+ function tetherFor(box, rotation, size = 7) {
901
1042
  const centre = boxCentre(box);
902
1043
  const radians = rotation * Math.PI / 180;
903
1044
  return {
904
- from: rotateAround([centre[0], box.y - 22], centre, radians),
1045
+ from: rotateAround([centre[0], box.y - armFor(size)], centre, radians),
905
1046
  to: rotateAround([centre[0], box.y], centre, radians)
906
1047
  };
907
1048
  }
@@ -913,10 +1054,10 @@ window.__ModuleLoader__.load({
913
1054
  * @param reach - how far from a handle's centre still counts, in figure pixels.
914
1055
  * @returns the handle grabbed, or undefined for a press on the mark itself.
915
1056
  */
916
- function handleAt(box, rotation, at, reach = 7) {
1057
+ function handleAt(box, rotation, at, reach = 7, size = 7) {
917
1058
  let best;
918
1059
  let bestDistance = reach;
919
- for (const handle of handlesFor(box, rotation)) {
1060
+ for (const handle of handlesFor(box, rotation, size)) {
920
1061
  const distance = Math.hypot(handle.point[0] - at[0], handle.point[1] - at[1]);
921
1062
  if (distance <= bestDistance) {
922
1063
  best = handle.id;
@@ -993,7 +1134,6 @@ window.__ModuleLoader__.load({
993
1134
  const degrees = normalizeDegrees((Math.atan2(at[1] - centre[1], at[0] - centre[0]) + Math.PI / 2) * 180 / Math.PI);
994
1135
  return snap ? normalizeDegrees(Math.round(degrees / 15) * 15) : degrees;
995
1136
  }
996
- /** The cursor for one handle: what dragging it will do. */
997
1137
  const CURSORS = {
998
1138
  nw: "nwse-resize",
999
1139
  n: "ns-resize",
@@ -1003,7 +1143,7 @@ window.__ModuleLoader__.load({
1003
1143
  s: "ns-resize",
1004
1144
  sw: "nesw-resize",
1005
1145
  w: "ew-resize",
1006
- rotate: "grab"
1146
+ rotate: `url("data:image/svg+xml,${encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><g fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M17.5 7.5A7 7 0 1 1 8.6 6.1\" stroke=\"#ffffff\" stroke-width=\"4.4\"/><path d=\"M17.5 7.5A7 7 0 1 1 8.6 6.1\" stroke=\"#2d2d2d\" stroke-width=\"2\"/><path d=\"M15.2 3.1L18.6 8.2L12.9 8.9\" stroke=\"#ffffff\" stroke-width=\"4.4\"/><path d=\"M15.2 3.1L18.6 8.2L12.9 8.9\" stroke=\"#2d2d2d\" stroke-width=\"2\"/></g></svg>")}") 12 12, crosshair`
1007
1147
  };
1008
1148
  /**
1009
1149
  * The mouse cursor for one handle.
@@ -1358,27 +1498,15 @@ window.__ModuleLoader__.load({
1358
1498
  const cy = box.y + box.height / 2;
1359
1499
  return `rotate(${n(radians * 180 / Math.PI)} ${n(cx)} ${n(cy)})`;
1360
1500
  }
1361
- //#endregion
1362
- //#region src/client/scale.ts
1363
1501
  /**
1364
- * How big the marks should be for the figure they are drawn on.
1365
- *
1366
- * A stroke is not a physical thing: 2.5 pixels is a bold line on a 300px thumbnail
1367
- * and a hairline on a 4000px scan. Marks are comparisons the reader makes against
1368
- * the line work underneath them, so they have to be sized in the same currency as
1369
- * the figure — a fraction of it — rather than in the figure's pixels.
1502
+ * Smallest multiple of the base sizes, for figures far below the reference.
1370
1503
  *
1371
- * The reference edge is the size a science figure is usually exported at. An
1372
- * ordinary figure then gets the unit it always had, and only the outliers (a
1373
- * thumbnail, a poster) are scaled. The range is clamped rather than proportional
1374
- * because a 20000px figure would otherwise get strokes 40px wide, which reads as a
1375
- * highlighter rather than a line.
1376
- * @module dsh-figmark/client/scale
1377
- */
1378
- /** The figure edge, in pixels, that gets the unit it always had. */
1379
- const REFERENCE_EDGE = 1200;
1380
- /** Smallest multiple of the base sizes, for figures far below the reference. */
1381
- const MIN_UNIT = .75;
1504
+ * Low enough that a matplotlib SVG — which measures itself in points, ~283 wide for
1505
+ * the same figure a PNG exports at 1179px — is still scaled down rather than propped
1506
+ * up to the old floor. The floor is a guard against division, not a design choice:
1507
+ * a figure below ~240px is one nothing sensible is drawn on.
1508
+ */
1509
+ const MIN_UNIT = .2;
1382
1510
  /**
1383
1511
  * The size everything drawn on a figure is multiplied by.
1384
1512
  *
@@ -1392,7 +1520,7 @@ window.__ModuleLoader__.load({
1392
1520
  function figureUnit(width, height) {
1393
1521
  const edge = Math.max(width, height);
1394
1522
  if (!Number.isFinite(edge) || edge <= 0) return 1;
1395
- return Math.min(3, Math.max(MIN_UNIT, edge / REFERENCE_EDGE));
1523
+ return Math.min(10, Math.max(MIN_UNIT, edge / 800));
1396
1524
  }
1397
1525
  //#endregion
1398
1526
  //#region src/client/Canvas.tsx
@@ -1423,7 +1551,8 @@ window.__ModuleLoader__.load({
1423
1551
  /** Minimum distance between recorded freehand samples, in figure pixels. */
1424
1552
  const PEN_SAMPLE_STEP = 3;
1425
1553
  /** Reach of a handle's hit area, in figure pixels: generous enough to grab. */
1426
- const HANDLE_REACH = 7 * 2.2;
1554
+ /** How far from a handle's centre a press still counts, as a multiple of its side. */
1555
+ const HANDLE_REACH = 2.2;
1427
1556
  /** Monotonic id source for marks created in this session. */
1428
1557
  let markSeq = 0;
1429
1558
  /**
@@ -1580,10 +1709,20 @@ window.__ModuleLoader__.load({
1580
1709
  * @param event - the pointer event.
1581
1710
  * @returns true when a handle took the press.
1582
1711
  */
1712
+ /**
1713
+ * The control frame's handle size in figure pixels.
1714
+ *
1715
+ * The frame is drawn in the same svg as the marks, which is in figure pixels, and
1716
+ * the whole paper is then scaled by CSS. A handle has to be a fixed number of screen
1717
+ * pixels to be hittable, so its figure size is that target divided by the scale.
1718
+ */
1719
+ const grip = 7 / Math.max(.05, props.scale ?? 1);
1720
+ /** A hairline that stays a hairline on screen, whatever the paper is scaled to. */
1721
+ const hairline = 1 / Math.max(.05, props.scale ?? 1);
1583
1722
  const beginHandle = (mark, event) => {
1584
1723
  if (props.selectedId !== mark.id) return false;
1585
1724
  if (mark.kind === "point") return false;
1586
- const grabbed = handleAt(controlBox(mark), mark.rotation ?? 0, pressedAt.current.value, HANDLE_REACH);
1725
+ const grabbed = handleAt(controlBox(mark), mark.rotation ?? 0, pressedAt.current.value, HANDLE_REACH * grip, grip);
1587
1726
  if (grabbed === void 0) return false;
1588
1727
  beginGesture(mark, event, grabbed);
1589
1728
  return true;
@@ -1814,26 +1953,26 @@ window.__ModuleLoader__.load({
1814
1953
  /** The resize and rotation handles of the selected mark. */
1815
1954
  const renderHandles = (mark) => {
1816
1955
  const frame = controlBox(mark);
1817
- const half = 7 / 2;
1818
- const handles = handlesFor(frame, mark.rotation ?? 0);
1819
- const tether = tetherFor(frame, mark.rotation ?? 0);
1956
+ const half = grip / 2;
1957
+ const handles = handlesFor(frame, mark.rotation ?? 0, grip);
1958
+ const tether = tetherFor(frame, mark.rotation ?? 0, grip);
1820
1959
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
1821
1960
  x1: tether.from[0],
1822
1961
  y1: tether.from[1],
1823
1962
  x2: tether.to[0],
1824
1963
  y2: tether.to[1],
1825
1964
  stroke: "#4c8dff",
1826
- strokeWidth: 1
1965
+ strokeWidth: hairline
1827
1966
  }), handles.map((handle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1828
1967
  "data-da-handle": handle.id,
1829
1968
  x: handle.point[0] - half,
1830
1969
  y: handle.point[1] - half,
1831
- width: 7,
1832
- height: 7,
1833
- rx: handle.id === "rotate" ? 7 / 2 : 1,
1970
+ width: grip,
1971
+ height: grip,
1972
+ rx: handle.id === "rotate" ? grip / 2 : grip / 7,
1834
1973
  fill: "#ffffff",
1835
1974
  stroke: "#4c8dff",
1836
- strokeWidth: 1.5,
1975
+ strokeWidth: 1.5 * hairline,
1837
1976
  style: {
1838
1977
  pointerEvents: "all",
1839
1978
  cursor: cursorFor(handle.id)
@@ -1902,16 +2041,17 @@ window.__ModuleLoader__.load({
1902
2041
  const box = pointMarkBox(shown, unit);
1903
2042
  /* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
1904
2043
  if (box === void 0) return null;
2044
+ const inset = 3 * hairline;
1905
2045
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1906
- x: box.x - 3 * unit,
1907
- y: box.y - 3 * unit,
1908
- width: box.width + 6 * unit,
1909
- height: box.height + 6 * unit,
1910
- rx: 3 * unit,
2046
+ x: box.x - inset,
2047
+ y: box.y - inset,
2048
+ width: box.width + 2 * inset,
2049
+ height: box.height + 2 * inset,
2050
+ rx: inset,
1911
2051
  fill: "none",
1912
2052
  stroke: "#4c8dff",
1913
- strokeWidth: unit,
1914
- strokeDasharray: `${3 * unit} ${2 * unit}`,
2053
+ strokeWidth: hairline,
2054
+ strokeDasharray: `${3 * hairline} ${2 * hairline}`,
1915
2055
  style: { pointerEvents: "none" }
1916
2056
  });
1917
2057
  };
@@ -1998,10 +2138,6 @@ window.__ModuleLoader__.load({
1998
2138
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1999
2139
  className: "da-overlay",
2000
2140
  "data-da-overlay": "",
2001
- style: {
2002
- left: originX,
2003
- top: originY
2004
- },
2005
2141
  width,
2006
2142
  height,
2007
2143
  viewBox: `0 0 ${width} ${height}`,
@@ -2281,14 +2417,13 @@ window.__ModuleLoader__.load({
2281
2417
  const pages = annotatedPages(document);
2282
2418
  const marked = pages.length === 0 ? "" : zh ? `其中 ${pages.length} 页有标注,` : `${pages.length} pages marked, `;
2283
2419
  const kind = rendered ? zh ? "【HTML 标注】" : "[HTML annotations]" : zh ? "【附图标注】" : "[figure annotations]";
2284
- const hash = zh ? `${rendered ? "文件" : "图"} sha256:${figure.sha256.slice(0, 12)}` : `sha256:${figure.sha256.slice(0, 12)}`;
2285
- const header = zh ? `${kind}${figure.path}(${scope === "" ? "" : `${scope},`}${size}${marked}标注 ${marks.length} 处,${hash})` : `${kind} ${figure.path} (${scope === "" ? "" : `${scope}, `}${size}${marked}${marks.length} marks, ${hash})`;
2420
+ const header = zh ? `${kind}${figure.path}(${scope === "" ? "" : `${scope},`}${size}${marked}标注 ${marks.length} 处)` : `${kind} ${figure.path} (${scope === "" ? "" : `${scope}, `}${size}${marked}${marks.length} marks)`;
2286
2421
  const paged = figure.pageCount !== void 0;
2287
2422
  const pictured = paths.reviewPath !== null;
2288
2423
  const attachment = !pictured ? [] : [paged ? zh ? "带批注 PDF 已作为本条消息的附件发送,可直接在阅读器里核对。" : "The annotated PDF is attached to this message; you can check it in any reader." : zh ? "标注图已作为本条消息的附件发送。" : "The annotated image is attached to this message."];
2289
2424
  const noPicture = !pictured && !rendered && marks.length > 0 && !needsReviewImage(document) ? [zh ? "本条标注只有位置点,没有标注图:按上面逐条的坐标与说明处理原图即可。" : "These marks are points only, so no annotated image was made: work from the coordinates and notes above against the original figure."] : [];
2290
2425
  const summary = document.summary !== void 0 && document.summary.trim() !== "" ? [zh ? `这张图是:${document.summary.trim()}` : `the figure shows: ${document.summary.trim()}`, ""] : [];
2291
- const closing = rendered ? zh ? "请按上述标注逐条修改这份 HTML 的生成源(不要改动渲染结果本身)。定位元素以 `anchor.selector` 为准,`points` 只是辅助:渲染宽度变化会让坐标漂移。未标注的部分保持不动,改完后逐条回应。" : "Address each mark above in the HTML source that generates this document (not in the rendered result). Locate elements by `anchor.selector`; `points` only assists, because coordinates drift when the render width changes. Leave unmarked parts untouched and answer mark by mark." : paged ? zh ? "请按上述标注逐条处理该 PDF(带批注 PDF 的每一页已带原生批注对象,可直接在阅读器里核对);未标注的部分保持不动,改完后逐条回应。" : "Address each mark above in the PDF (every page of the annotated PDF carries native annotation objects you can check in a reader); leave unmarked parts untouched and answer mark by mark." : zh ? "请按上述标注逐条修复该附图(改生成源,不要涂改导出的位图);未标注的部分保持不动,改完后逐条回应。" : "Fix the figure for each mark above (change the generating source, not the exported bitmap); leave unmarked parts untouched and answer mark by mark.";
2426
+ const closer = rendered ? zh ? "逐条改 HTML 生成源。" : "Edit the HTML source, mark by mark." : paged ? zh ? "逐条处理该 PDF。" : "Work through the PDF, mark by mark." : zh ? "逐条改生成源。" : "Edit the generating source, mark by mark.";
2292
2427
  return [
2293
2428
  header,
2294
2429
  ...attachment,
@@ -2297,7 +2432,7 @@ window.__ModuleLoader__.load({
2297
2432
  ...summary,
2298
2433
  ...describeMarks(document, locale),
2299
2434
  "",
2300
- closing
2435
+ closer
2301
2436
  ].join("\n");
2302
2437
  }
2303
2438
  /**
@@ -2684,7 +2819,25 @@ window.__ModuleLoader__.load({
2684
2819
  const unit = figureUnit(layer.width, layer.height);
2685
2820
  const paper = plotBounds(layer.width, layer.height, marks, unit);
2686
2821
  const shift = `translate(${-paper.x} ${-paper.y})`;
2687
- return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${paper.width}" height="${paper.height}" viewBox="0 0 ${paper.width} ${paper.height}"><rect x="0" y="0" width="${paper.width}" height="${paper.height}" fill="#ffffff"/><g transform="${shift}">` + figureSvg(layer) + marks.map((mark) => markSvg(mark, numbers.get(mark.id), unit)).join("") + "</g></svg>";
2822
+ const edge = paper.x !== 0 || paper.y !== 0 || paper.width !== layer.width || paper.height !== layer.height ? figureEdge(layer.width, layer.height, unit) : "";
2823
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${paper.width}" height="${paper.height}" viewBox="0 0 ${paper.width} ${paper.height}"><rect x="0" y="0" width="${paper.width}" height="${paper.height}" fill="#ffffff"/><g transform="${shift}">` + figureSvg(layer) + edge + marks.map((mark) => markSvg(mark, numbers.get(mark.id), unit)).join("") + "</g></svg>";
2824
+ }
2825
+ /** Round a stroke measurement to a tenth, as the path data does. */
2826
+ function hair9(value) {
2827
+ return (Math.round(value * 10) / 10).toString();
2828
+ }
2829
+ /**
2830
+ * The rule that marks where the figure ends and the grown paper begins.
2831
+ *
2832
+ * Drawn only when the paper grew, and drawn grey and dashed: it is a note about the
2833
+ * picture, not part of it, so it must not read as a mark the reader made.
2834
+ * @param width - the figure's width, in figure pixels.
2835
+ * @param height - the figure's height, in figure pixels.
2836
+ * @param unit - the figure's size multiple, so the rule is sized like the marks.
2837
+ * @returns an SVG rect, or an empty string when there is nothing to outline.
2838
+ */
2839
+ function figureEdge(width, height, unit) {
2840
+ return `<rect x="0" y="0" width="${width}" height="${height}" fill="none" stroke="#9aa3b2" stroke-width="${hair9(Math.max(1, 1.5 * unit))}" stroke-dasharray="${hair9(6 * unit)} ${hair9(4 * unit)}"/>`;
2688
2841
  }
2689
2842
  /**
2690
2843
  * The size of the picture {@link composeReviewSvg} builds.
@@ -3112,6 +3265,182 @@ window.__ModuleLoader__.load({
3112
3265
  }));
3113
3266
  } catch {}
3114
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
+ }
3115
3444
  //#endregion
3116
3445
  //#region src/shared/http.ts
3117
3446
  /**
@@ -3169,7 +3498,8 @@ window.__ModuleLoader__.load({
3169
3498
  path: body.figure.path,
3170
3499
  mediaType: body.figure.mediaType,
3171
3500
  sha256: body.figure.sha256,
3172
- annotation: body.annotation ?? null
3501
+ annotation: body.annotation ?? null,
3502
+ workspace: body.figure.workspace ?? null
3173
3503
  };
3174
3504
  }
3175
3505
  /**
@@ -3253,16 +3583,39 @@ window.__ModuleLoader__.load({
3253
3583
  const STYLE_MARKER = "data-dsh-figmark";
3254
3584
  /** The stylesheet text. */
3255
3585
  const STYLES = `
3586
+ /* An accent of the plugin's own.
3587
+ The shell's --dsw-alias-brand-primary is not a brand colour in this theme — it
3588
+ resolves to the *ink* (near-black on light, near-white on dark), because the shell
3589
+ uses it for filled buttons in monochrome. Reading it as "the blue" turned this
3590
+ panel's sliders, filled buttons and selection rings into ink: a black knob on a
3591
+ white rail, and a wedge that goes solid black at the heaviest setting. The alias
3592
+ stays as the fallback so a shell that really does define a brand colour still wins,
3593
+ but the accent is named here so every rule reads one variable instead of quietly
3594
+ inheriting whatever "primary" means that week.
3595
+ The dark block re-states it, because a blue tuned for white paper is too dark on a
3596
+ near-black panel. */
3256
3597
  /* The panel follows the shell's palette rather than carrying one of its own: every
3257
3598
  colour below is an alias the theme owner resolves, with the inherited value as the
3258
3599
  fallback so a shell that defines nothing still renders something legible. */
3259
- .da-root{position:relative;display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;color:var(--dsw-alias-label-primary,inherit);background:var(--dsw-alias-bg-document-preview,transparent)}
3600
+ .da-root{--da-accent:#176df2;--da-accent-soft:rgba(23,109,242,.14);
3601
+ position:relative;display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;color:var(--dsw-alias-label-primary,inherit);background:var(--dsw-alias-bg-document-preview,transparent)}
3602
+ /* The shell marks its dark palette on <body> rather than through prefers-color-scheme:
3603
+ a reader can pick either palette whatever the OS is set to, so following the shell's
3604
+ own switch is what keeps the accent readable in both. A blue tuned for white paper
3605
+ is too dark on a near-black panel. */
3606
+ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,141,255,.22)}
3260
3607
 
3261
3608
  /* Every small picture in the panel is one stroke set, declared once: without this
3262
3609
  an svg with no fill of its own falls back to a solid black shape, which is what
3263
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. */
3264
3616
  .da-ico{width:16px;height:16px;flex:0 0 auto;fill:none;stroke:currentColor;
3265
- 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}
3266
3619
 
3267
3620
  /* --- the floating tool palette ------------------------------------------- */
3268
3621
  /* The tools float over the figure in a dark rounded pill so they can be parked
@@ -3274,12 +3627,20 @@ window.__ModuleLoader__.load({
3274
3627
  box-shadow:0 8px 26px rgba(10,14,24,.3),0 0 0 1px rgba(255,255,255,.08)}
3275
3628
  /* While it is being dragged the pointer is the grip, whatever it passes over. */
3276
3629
  .da-palette[data-dragging="true"]{opacity:.94;box-shadow:0 12px 32px rgba(10,14,24,.4),0 0 0 1px rgba(255,255,255,.14)}
3277
- .da-palette[data-dragging="true"] *{cursor:grabbing !important}
3278
- /* The handle: a short row of dots over the tiles, the one part that only drags. */
3279
- .da-palette-grip{display:flex;align-items:center;justify-content:center;width:32px;height:14px;
3280
- border-radius:7px;cursor:grab;color:rgba(255,255,255,.55);touch-action:none;flex:0 0 auto}
3281
- .da-palette-grip:hover{color:rgba(255,255,255,.9);background:rgba(255,255,255,.1)}
3282
- .da-palette-grip-dots{font-size:9px;line-height:1;letter-spacing:1px}
3630
+ .da-palette[data-dragging="true"] *{cursor:move !important}
3631
+ /* The handle: a block of six dots, the one part of a strip that only drags. The block
3632
+ is turned to suit the strip — wider than tall on the bar, taller than wide on the
3633
+ palette — because a grip whose proportions fight its strip looks borrowed from the
3634
+ other one. */
3635
+ .da-palette-grip{display:flex;align-items:center;justify-content:center;width:26px;height:17px;
3636
+ border-radius:5px;cursor:move;color:rgba(255,255,255,.55);touch-action:none;flex:0 0 auto}
3637
+ .da-palette-grip:hover{color:rgba(255,255,255,.92);background:rgba(255,255,255,.1)}
3638
+ /* The dots' box follows the block: three across by two down needs width, two across
3639
+ by three down needs height. Sized per block rather than once, so the svg is not
3640
+ letterboxed inside its own handle. */
3641
+ .da-grip-dots{width:12px;height:8px;fill:currentColor;display:block}
3642
+ .da-bar .da-palette-grip{width:16px;height:18px}
3643
+ .da-bar .da-grip-dots{width:8px;height:12px}
3283
3644
  /* A tile is a round button holding one white glyph. No word under it: six glyphs in
3284
3645
  a column are read at a glance, and the word is still there as the tooltip and as
3285
3646
  the accessible name. */
@@ -3288,7 +3649,7 @@ window.__ModuleLoader__.load({
3288
3649
  .da-palette .da-tool:hover{background:rgba(255,255,255,.14)}
3289
3650
  .da-palette .da-tool[aria-pressed="true"]{background:#176df2;color:#fff}
3290
3651
  .da-palette .da-tool .da-tool-icon{height:auto}
3291
- .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}
3292
3653
 
3293
3654
  /* --- the size rail ------------------------------------------------------- */
3294
3655
  /* A vertical rail along the left edge of the figure: how heavy the pen is, and how
@@ -3301,6 +3662,7 @@ window.__ModuleLoader__.load({
3301
3662
  drawing. The drawn parts never take the pointer, or grabbing the knob would grab
3302
3663
  the knob instead of the slider. */
3303
3664
  .da-weight{position:absolute;z-index:7;left:6px;top:50%;transform:translateY(-50%);
3665
+ --da-track-inset:10px;--da-knob-size:11px;
3304
3666
  display:flex;align-items:center;justify-content:center;width:7px;height:200px;
3305
3667
  border-radius:999px;background:var(--dsw-alias-border-l2,rgba(127,127,127,.22));
3306
3668
  cursor:ns-resize;transition:width .16s ease,background .16s ease,box-shadow .16s ease}
@@ -3309,13 +3671,24 @@ window.__ModuleLoader__.load({
3309
3671
  /* The track is an svg in the body: a hairline at the bottom that opens into a rounded
3310
3672
  shoulder at the top. It is not a clip-path polygon because a polygon has no rounded
3311
3673
  ends — the corners were what made the wedge read as a cut-out blade rather than a
3312
- part of the panel. preserveAspectRatio="none" stretches one 12x100 shape over the
3313
- rail's height, so the shoulders stay smooth at any length. */
3314
- .da-weight-svg{position:absolute;left:50%;top:12px;bottom:12px;width:18px;
3315
- transform:translateX(-50%);opacity:0;pointer-events:none;transition:opacity .16s ease}
3674
+ part of the panel. preserveAspectRatio="none" stretches one 18x100 shape over the
3675
+ rail's height, so the shoulders stay smooth at any length.
3676
+ The height is written out rather than expressed as top+bottom: an svg is a replaced
3677
+ element: with a viewBox it takes its intrinsic height for its width and ignores the
3678
+ bottom offset entirely. The track was therefore a fixed 100px tall while the knob
3679
+ travelled the whole 200px rail, which is why the two could never line up. */
3680
+ .da-weight-svg{position:absolute;left:50%;top:var(--da-track-inset);height:calc(100% - 2 * var(--da-track-inset));
3681
+ width:18px;transform:translateX(-50%);opacity:0;pointer-events:none;
3682
+ transition:opacity .16s ease}
3316
3683
  .da-weight:hover .da-weight-svg,.da-weight:focus-within .da-weight-svg{opacity:1}
3317
3684
  /* The knob sits on the value: the only part that shows at rest, so the rail reads as
3318
3685
  a slider waiting to be touched rather than a bare rule. */
3686
+ /* The blue: a bar of the same width as the groove, its height the one number the
3687
+ knob also uses, so its top edge is the knob's centre at every weight. */
3688
+ .da-weight-fill{position:absolute;left:50%;bottom:var(--da-track-inset);width:5px;
3689
+ transform:translateX(-50%);border-radius:999px;background:var(--da-accent,#176df2);
3690
+ opacity:0;pointer-events:none;transition:opacity .16s ease}
3691
+ .da-weight:hover .da-weight-fill,.da-weight:focus-within .da-weight-fill{opacity:1}
3319
3692
  .da-weight-knob{position:absolute;left:50%;width:var(--da-knob-size,11px);height:var(--da-knob-size,11px);
3320
3693
  border-radius:50%;transform:translateX(-50%);pointer-events:none;
3321
3694
  background:var(--dsw-alias-bg-layer-2,#fff);
@@ -3323,7 +3696,7 @@ window.__ModuleLoader__.load({
3323
3696
  transition:width .16s ease,height .16s ease,background .16s ease}
3324
3697
  .da-weight:hover,.da-weight:focus-within{--da-knob-size:15px}
3325
3698
  .da-weight:hover .da-weight-knob,.da-weight:focus-within .da-weight-knob{
3326
- background:var(--dsw-alias-brand-primary,#176df2);
3699
+ background:var(--da-accent,#176df2);
3327
3700
  box-shadow:0 2px 7px rgba(20,40,80,.3)}
3328
3701
  /* The browser's own range, rotated upright and made transparent. It spans the whole
3329
3702
  rail so a press anywhere on it lands on the slider — including on the knob, which
@@ -3344,16 +3717,17 @@ window.__ModuleLoader__.load({
3344
3717
  /* Parked: placed from the top by the reader's drag, so the bottom anchor that keeps
3345
3718
  it centred while untouched must not also apply — with both set it stretches. */
3346
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}
3347
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)}
3348
- .da-bar[data-dragging="true"] *{cursor:grabbing !important}
3724
+ .da-bar[data-dragging="true"] *{cursor:move !important}
3349
3725
  /* The bar's buttons are white glyphs on the dark strip. The one filled control is
3350
3726
  still the way out. */
3351
3727
  .da-bar .da-dock-btn{color:#fff}
3352
3728
  .da-bar .da-dock-btn:hover{background:rgba(255,255,255,.14)}
3353
3729
  .da-bar .da-dock-btn[aria-pressed="true"]{background:#176df2;color:#fff}
3354
3730
  .da-bar .da-dock-sep{background:rgba(255,255,255,.22)}
3355
- .da-bar .da-dock-btn-primary{background:#176df2;color:#fff;border-color:#176df2}
3356
- .da-bar .da-dock-btn-primary:hover{background:#176df2;opacity:.92}
3357
3731
  .da-bar .da-dock-dot{background:#4c8dff}
3358
3732
  .da-bar .da-dock-count{background:#176df2;color:#fff}
3359
3733
  .da-bar .da-style{width:30px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#fff}
@@ -3372,7 +3746,7 @@ window.__ModuleLoader__.load({
3372
3746
  .da-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}
3373
3747
  .da-btn:disabled{opacity:.4;cursor:default}
3374
3748
  .da-btn:disabled:hover{background:var(--dsw-alias-bg-base,transparent)}
3375
- .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}
3376
3750
  /* A quiet button has no frame at all until the pointer is on it: it is an action,
3377
3751
  not a thing, and the row it sits in is already framed. */
3378
3752
  .da-btn-quiet{border-color:transparent;background:transparent;height:24px;padding:0 7px;font-size:11px;color:var(--dsw-alias-label-secondary,inherit)}
@@ -3382,13 +3756,13 @@ window.__ModuleLoader__.load({
3382
3756
  picture is what a reader who has used the tool once recognizes without reading. */
3383
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}
3384
3758
  .da-tool .da-tool-icon{display:flex;align-items:center;justify-content:center;height:14px}
3385
- .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}
3386
3760
  .da-tool .da-tool-label{font-size:10px;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary,inherit)}
3387
3761
  /* The armed tool is the one state in this row the reader must never hunt for: it is
3388
3762
  the only tile with a fill, and the only label in the brand colour. */
3389
- .da-tool[aria-pressed="true"]{border-color:transparent;background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.1));color:var(--dsw-alias-brand-primary,currentColor)}
3390
- .da-tool[aria-pressed="true"] .da-tool-label{color:var(--dsw-alias-brand-primary,currentColor);font-weight:600}
3391
- .da-tool[aria-pressed="true"] svg{stroke-width:2.1}
3763
+ .da-tool[aria-pressed="true"]{border-color:transparent;background:var(--da-accent-soft,rgba(23,109,242,.1));color:var(--da-accent,currentColor)}
3764
+ .da-tool[aria-pressed="true"] .da-tool-label{color:var(--da-accent,currentColor);font-weight:600}
3765
+ .da-tool[aria-pressed="true"] svg{stroke-width:2.4}
3392
3766
  .da-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
3393
3767
  .da-btn-icon{width:30px;padding:0}
3394
3768
 
@@ -3398,6 +3772,10 @@ window.__ModuleLoader__.load({
3398
3772
  .da-stylewrap{position:relative;flex:0 0 auto}
3399
3773
  /* The style fold floats above the bar rather than taking a row of it: opened over
3400
3774
  the figure, it costs the picture nothing and lands where the eye already is. */
3775
+ /* Opens upwards by default, because the bar lives at the bottom. Parked at the top
3776
+ there is nothing above it to open into, so the fold turns over and hangs below its
3777
+ own button instead — decided by the same measurement the records use. */
3778
+ .da-style-row[data-side="below"]{bottom:auto;top:calc(100% + 6px);box-shadow:0 -6px 22px rgba(20,40,80,.12),0 6px 22px rgba(20,40,80,.16)}
3401
3779
  .da-style-row{position:absolute;left:8px;bottom:calc(100% + 6px);z-index:9;
3402
3780
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;max-width:calc(100% - 16px);
3403
3781
  padding:8px 10px;border:0.5px solid var(--dsw-alias-border-l2,currentColor);border-radius:11px;
@@ -3412,17 +3790,24 @@ window.__ModuleLoader__.load({
3412
3790
  /* Anywhere else is "done here": the panel closes on the click that lands outside it
3413
3791
  rather than trapping the reader in a popup with one way out. */
3414
3792
  .da-swatch{width:20px;height:20px;border-radius:50%;corner-shape:round;border:2px solid var(--dsw-alias-bg-base,transparent);box-shadow:0 0 0 1px var(--dsw-alias-border-l4,rgba(127,127,127,.5));cursor:pointer;padding:0}
3415
- .da-swatch[aria-pressed="true"]{box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,currentColor)}
3793
+ .da-swatch[aria-pressed="true"]{box-shadow:0 0 0 2px var(--da-accent,currentColor)}
3416
3794
  /* --- the figure on its table -------------------------------------------- */
3417
3795
  /* A figure is a sheet on a table, not a picture floating in a void: the white paper
3418
3796
  is what gives the panel a middle, and the gap around it is the margin the eye
3419
3797
  needs to see where the drawing stops. */
3420
3798
  .da-stage{position:relative;display:flex;align-items:safe center;justify-content:safe center;flex:1 1 auto;overflow:auto;min-height:0;padding:14px;background:var(--dsw-alias-bg-document-preview,transparent)}
3421
- .da-surface{position:relative;flex:0 0 auto;margin:0;transform-origin:top left;background:#fff;border-radius:4px;box-shadow:var(--dsw-elevation-prominent,0 1px 3px rgba(0,0,0,.14),0 8px 24px rgba(0,0,0,.08))}
3422
- /* The paper: the figure plus the margin a mark may have needed. It is the white sheet
3423
- the drawing sits on, so a mark that reaches past the figure lands on paper rather
3424
- than on the panel behind it. */
3425
- .da-paper{background:#fff}
3799
+ .da-surface{position:relative;flex:0 0 auto;margin:0;transform-origin:top left}
3800
+ /* The paper: the figure plus the margin a mark may have needed. It is deliberately
3801
+ left transparent — the white sheet belongs to the figure, so a margin that only
3802
+ exists because a mark reached past the edge reads as board round a card rather than
3803
+ as more of the figure. The exported picture still gets its white fill: that one is
3804
+ looked at by someone who has no board behind it. */
3805
+ .da-paper{background:transparent}
3806
+ /* The figure is the sheet: white, rounded, lifted off the panel. This is the element
3807
+ that carries the paper look, which is why the margin can be see-through without the
3808
+ drawing losing its edge. */
3809
+ .da-figure-frame{background:var(--dsw-alias-bg-base,#fff);border-radius:4px;overflow:hidden;
3810
+ box-shadow:var(--dsw-elevation-prominent,0 1px 3px rgba(0,0,0,.14),0 8px 24px rgba(0,0,0,.08))}
3426
3811
  .da-figure{display:block;user-select:none;-webkit-user-drag:none}
3427
3812
  /* The table is a thing you can push. The hand only shows under the tools that
3428
3813
  leave a drag free to move the view: under a drawing tool the same drag draws. */
@@ -3440,13 +3825,73 @@ window.__ModuleLoader__.load({
3440
3825
  .da-page{font-size:12px;min-width:44px;text-align:center;color:var(--dsw-alias-label-secondary,inherit)}
3441
3826
  /* The document's toolbar sits above the page body, so the pane is one column of
3442
3827
  definite height and the body takes what is left of it. */
3443
- .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}
3444
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}
3445
3886
  /* The page slot before the first raster. It takes the column the body's stage
3446
3887
  takes, so the frame around it — the toolbar above all — is in place from the
3447
3888
  first paint and does not move when the page arrives. */
3448
3889
  .da-placeholder{flex:1 1 auto;min-height:0;background:var(--dsw-alias-bg-document-preview,transparent)}
3449
-
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}
3450
3895
  /* --- the two cards under the figure ------------------------------------- */
3451
3896
  /* --- the dock ------------------------------------------------------------ */
3452
3897
  /* The dock's buttons are quiet circles in the bar: they are the actions that are
@@ -3455,22 +3900,20 @@ window.__ModuleLoader__.load({
3455
3900
  width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;
3456
3901
  color:inherit;cursor:pointer;flex:0 0 auto}
3457
3902
  .da-dock-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
3458
- .da-dock-btn[aria-pressed="true"]{background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.12));color:var(--dsw-alias-brand-primary,#176df2)}
3903
+ .da-dock-btn[aria-pressed="true"]{background:var(--da-accent-soft,rgba(23,109,242,.12));color:var(--da-accent,#176df2)}
3459
3904
  .da-dock-btn svg{width:15px;height:15px}
3460
3905
  .da-dock-btn:disabled{opacity:.4;cursor:default}
3461
3906
  .da-dock-btn:disabled:hover{background:transparent}
3462
3907
  /* The one filled control in the panel is the way out of it. */
3463
- .da-dock-btn-primary{border-color:var(--dsw-alias-brand-primary,#176df2);background:var(--dsw-alias-brand-primary,#176df2);color:#fff;box-shadow:0 2px 8px rgba(23,109,242,.32)}
3464
- .da-dock-btn-primary:hover{opacity:.92;background:var(--dsw-alias-brand-primary,#176df2)}
3465
3908
  /* A dot, not a count: "there is a goal written" is all the bar needs to say about
3466
3909
  a field it is not a list of. */
3467
3910
  .da-dock-dot{position:absolute;top:3px;right:4px;width:5px;height:5px;border-radius:50%;
3468
- background:var(--dsw-alias-brand-primary,#176df2)}
3911
+ background:var(--da-accent,#176df2)}
3469
3912
  .da-dock-sep{width:1px;height:16px;margin:0 3px;background:var(--dsw-alias-border-l3,currentColor);opacity:.6;flex:0 0 auto}
3470
3913
  /* The count rides on the list's own button: it is the one thing about the list a
3471
3914
  reader wants without opening it. */
3472
3915
  .da-dock-count{position:absolute;top:-1px;right:-2px;min-width:14px;height:14px;padding:0 3px;
3473
- border-radius:7px;background:var(--dsw-alias-brand-primary,#176df2);color:#fff;
3916
+ border-radius:7px;background:var(--da-accent,#176df2);color:#fff;
3474
3917
  font-size:9px;font-weight:700;line-height:14px;text-align:center;font-variant-numeric:tabular-nums}
3475
3918
 
3476
3919
  /* --- the floating records ------------------------------------------------ */
@@ -3482,7 +3925,7 @@ window.__ModuleLoader__.load({
3482
3925
  background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 6px 22px rgba(20,40,80,.14),0 0 0 1px rgba(20,40,80,.03)}
3483
3926
  .da-card-title{display:flex;align-items:center;gap:5px;min-width:0;font-size:12px;font-weight:650;
3484
3927
  color:var(--dsw-alias-label-primary,inherit)}
3485
- .da-card-title svg{width:14px;height:14px;flex:0 0 auto;color:var(--dsw-alias-brand-primary,#176df2)}
3928
+ .da-card-title svg{width:14px;height:14px;flex:0 0 auto;color:var(--da-accent,#176df2)}
3486
3929
 
3487
3930
  .da-float-head{display:flex;align-items:center;gap:7px}
3488
3931
  .da-float-head .da-card-title{flex:1 1 auto;min-width:0}
@@ -3521,10 +3964,10 @@ window.__ModuleLoader__.load({
3521
3964
  color:var(--dsw-alias-label-primary,currentColor)}
3522
3965
  .da-comment-send-btn:disabled{opacity:.45;cursor:default}
3523
3966
  .da-comment-send-btn svg{width:15px;height:15px}
3524
- .da-comment-send-primary{background:var(--dsw-alias-brand-primary,#176df2);color:#fff}
3525
- .da-comment-send-primary:hover{background:var(--dsw-alias-brand-primary,#176df2);color:#fff;filter:brightness(1.08)}
3967
+ .da-comment-send-primary{background:var(--da-accent,#176df2);color:#fff}
3968
+ .da-comment-send-primary:hover{background:var(--da-accent,#176df2);color:#fff;filter:brightness(1.08)}
3526
3969
  .da-float textarea{width:100%;box-sizing:border-box;min-height:74px;max-height:200px;resize:vertical;border:0.5px solid var(--dsw-alias-border-l3,currentColor);border-radius:9px;background:var(--dsw-alias-bg-layer-2,transparent);color:inherit;font:inherit;font-size:12px;padding:8px 10px;line-height:1.65}
3527
- .da-float textarea:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#176df2)}
3970
+ .da-float textarea:focus{outline:none;border-color:var(--da-accent,#176df2)}
3528
3971
  .da-btn-count{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;
3529
3972
  padding:0 4px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));
3530
3973
  font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}
@@ -3537,8 +3980,8 @@ window.__ModuleLoader__.load({
3537
3980
  /* What the selected mark is, in one word: the detail field is disabled until there
3538
3981
  is something to describe, and this says what it would describe. */
3539
3982
  .da-chip{display:inline-flex;align-items:center;height:16px;padding:0 6px;border-radius:5px;
3540
- background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.12));
3541
- color:var(--dsw-alias-brand-primary,#176df2);font-size:10px;font-weight:600}
3983
+ background:var(--da-accent-soft,rgba(23,109,242,.12));
3984
+ color:var(--da-accent,#176df2);font-size:10px;font-weight:600}
3542
3985
  /* The "?" beside a card's name: the long form of what that field is for. */
3543
3986
  .da-hintdot{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;
3544
3987
  border-radius:50%;border:0.5px solid var(--dsw-alias-border-l3,currentColor);
@@ -3548,14 +3991,23 @@ window.__ModuleLoader__.load({
3548
3991
  sentence as the thing you press, and the sentence's own shape and anchor under it
3549
3992
  in smaller type. A borderless run of text would read as one paragraph of marks. */
3550
3993
  .da-marks{display:flex;flex-direction:column;gap:3px;min-height:0}
3551
- .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;
3552
3995
  border:0.5px solid transparent;min-width:0}
3553
3996
  .da-mark:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
3554
- .da-mark[data-selected="true"]{border-color:var(--dsw-alias-brand-primary,#176df2);
3555
- background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.08))}
3556
- .da-mark-index{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
3557
- width:20px;height:20px;border-radius:6px;color:#fff;font-size:11px;font-weight:700;
3558
- font-variant-numeric:tabular-nums}
3997
+ .da-mark[data-selected="true"]{border-color:var(--da-accent,#176df2);
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}
3559
4011
  .da-mark-text{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;
3560
4012
  padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}
3561
4013
  .da-mark-title{display:block;font-size:12px;line-height:1.35;overflow:hidden;
@@ -3563,9 +4015,25 @@ window.__ModuleLoader__.load({
3563
4015
  .da-mark-meta{display:block;font-size:10px;line-height:1.35;
3564
4016
  color:var(--dsw-alias-label-tertiary,currentColor);overflow:hidden;
3565
4017
  text-overflow:ellipsis;white-space:nowrap}
3566
- .da-mark-del{flex:0 0 auto;padding:2px 6px;border:0;border-radius:6px;background:transparent;
3567
- 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}
3568
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}
3569
4037
 
3570
4038
  /* The empty list is a picture and two lines: a card of grey text reads as broken,
3571
4039
  and "nothing yet" is a state, not an error. */
@@ -3573,6 +4041,16 @@ window.__ModuleLoader__.load({
3573
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}
3574
4042
  .da-empty-title{font-size:12px;font-weight:650;color:var(--dsw-alias-label-secondary,inherit)}
3575
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
+
3576
4054
  .da-status{flex:0 0 auto;padding:6px 10px;font-size:12px;color:var(--dsw-alias-label-secondary,inherit)}
3577
4055
  .da-status[data-tone="error"]{color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary,currentColor))}
3578
4056
  .da-status[data-tone="ok"]{color:var(--dsw-alias-state-success-primary,currentColor)}
@@ -3823,24 +4301,36 @@ window.__ModuleLoader__.load({
3823
4301
  /**
3824
4302
  * The rail's track, drawn on an 18x100 grid and stretched to whatever height it gets.
3825
4303
  *
3826
- * A hairline at the bottom opening into a round shoulder at the top: the control's
3827
- * subject is thickness, so the track itself says which way is heavier. Every edge is
3828
- * curved — straight flanks make a paper dart, and a square cap makes a cut-off blade —
3829
- * so the profile is eased and the top is closed with an arc a little wider than the
3830
- * shoulders. Stretching it vertically (preserveAspectRatio="none") costs nothing
3831
- * visually: the only vertical features are straight lines, and the horizontal ones are
3832
- * what the gradient is for.
4304
+ * The control's subject is thickness, so the track says which way is heavier: a little
4305
+ * narrower at the bottom, about one part in six. "A little" is the whole note — a
4306
+ * water-drop taper reads as decoration rather than as a gauge, and the reader said so
4307
+ * twice. One cubic per side and no shoulders, so each flank is a single continuous
4308
+ * curve from end to end with nothing for the eye to catch on; the two ends close with
4309
+ * faint arcs so the track looks drawn rather than cut off.
4310
+ *
4311
+ * Stretching it vertically (preserveAspectRatio="none") is what makes the caps need
4312
+ * care: the svg is 18x100 over a box about 180 tall, so one y unit is 1.8 of an x unit.
4313
+ * An `r` that reads as a circle in the viewBox would be a tall ellipse on screen — the
4314
+ * cap's `ry` is therefore set to `rx / 1.8`, which is what puts a round end on it.
3833
4315
  */
3834
4316
  const WEIGHT_TRACK = [
3835
- "M 8.2 100",
3836
- "C 8.2 78 7.9 58 6.4 38",
3837
- "C 5.4 25 3.2 16 1.9 11",
3838
- "A 7.1 7.1 0 0 1 16.1 11",
3839
- "C 14.8 16 12.6 25 11.6 38",
3840
- "C 10.1 58 9.8 78 9.8 100",
3841
- "A 0.8 0.8 0 0 1 8.2 100",
4317
+ "M 6.9 98.9",
4318
+ "A 2.1 1.15 0 0 0 11.1 98.9",
4319
+ "C 11.4 66 11.9 33 12.2 1.1",
4320
+ "A 3.2 1.15 0 0 0 5.8 1.1",
4321
+ "C 6.1 33 6.6 66 6.9 98.9",
3842
4322
  "Z"
3843
4323
  ].join(" ");
4324
+ /**
4325
+ * How far the blue reaches, as a `height` in the rail.
4326
+ *
4327
+ * The same expression as the knob's `bottom`, because the blue has to end exactly where
4328
+ * the knob's centre is: written once as a function so a change to the travel cannot
4329
+ * leave the fill behind. `inset` keeps both clear of the rail's rounded caps; the extra
4330
+ * half knob at each end keeps the knob's whole circle between the two ends of the bar
4331
+ * rather than hanging half of itself past the top when the pen is at its heaviest.
4332
+ */
4333
+ const travelHeight = (fraction) => `calc(var(--da-track-inset) + var(--da-knob-size) / 2 + (100% - 2 * var(--da-track-inset) - var(--da-knob-size)) * ${fraction})`;
3844
4334
  const TOOLS = [
3845
4335
  {
3846
4336
  tool: "select",
@@ -3897,50 +4387,101 @@ window.__ModuleLoader__.load({
3897
4387
  * with the mouse rather than a mark you leave behind. Drawn rather than written so
3898
4388
  * the toolbar stays one row of controls at any locale or font size.
3899
4389
  */
4390
+ /**
4391
+ * The handle's dots: six of them, three by two, turned as the reader asked.
4392
+ *
4393
+ * The palette stands up and takes a grip three columns wide by two rows tall; the bar
4394
+ * lies down and takes one two columns wide by three rows tall. That is the pair the
4395
+ * reader called for, and it is the opposite of what the strips' own proportions would
4396
+ * suggest — noted here so it is not "corrected" back by the next pair of eyes.
4397
+ *
4398
+ * Drawn instead of typed — the braille character this replaces was one glyph at one
4399
+ * size, so the dots could not be sized or spaced, and at 9px they came out a smudge.
4400
+ */
4401
+ function GripDots({ wide }) {
4402
+ const xs = wide ? [
4403
+ 2,
4404
+ 6,
4405
+ 10
4406
+ ] : [2, 6];
4407
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4408
+ className: "da-grip-dots",
4409
+ viewBox: wide ? "0 0 12 8" : "0 0 8 12",
4410
+ "aria-hidden": "true",
4411
+ focusable: "false",
4412
+ children: (wide ? [2, 6] : [
4413
+ 2,
4414
+ 6,
4415
+ 10
4416
+ ]).map((y) => xs.map((x) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4417
+ cx: x,
4418
+ cy: y,
4419
+ r: 1.4
4420
+ }, `${x}-${y}`)))
4421
+ });
4422
+ }
4423
+ /**
4424
+ * The select tool: a cursor, outlined rather than filled.
4425
+ *
4426
+ * Filled, it read as a black blob against the dark chip — the silhouette was there but
4427
+ * the direction it points was not, which is the only thing a cursor has to say. As an
4428
+ * outline it belongs to the same stroke set as the other five glyphs.
4429
+ *
4430
+ * The shape is Tabler's `cursor-text`-sibling `pointer` glyph, taken whole at its own
4431
+ * 24 grid: its body is one closed curve with round joins rather than the seven hard
4432
+ * corners a hand-traced outline has, and at 16px that softness is the whole difference
4433
+ * between a cursor and a shard. Scaled by hand it lost exactly that.
4434
+ */
3900
4435
  function PointerIcon() {
3901
4436
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3902
- width: "13",
3903
- height: "15",
3904
- viewBox: "0 0 13 15",
4437
+ className: "da-ico",
4438
+ viewBox: "0 0 24 24",
3905
4439
  "aria-hidden": "true",
3906
4440
  focusable: "false",
3907
- style: { display: "block" },
3908
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
3909
- d: "M 1.5 1 L 1.5 12.2 L 4.1 9.9 L 5.9 13.6 L 8 12.6 L 6.2 9 L 9.7 8.7 Z",
3910
- fill: "currentColor",
3911
- stroke: "currentColor",
3912
- strokeWidth: "0.9",
3913
- strokeLinejoin: "round"
3914
- })
4441
+ strokeWidth: "2",
4442
+ strokeLinecap: "round",
4443
+ strokeLinejoin: "round",
4444
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14.185 13.14l5.644 -2.202c1.625 -.634 1.538 -2.962 -.13 -3.473l-14.319 -4.382c-1.41 -.431 -2.73 .888 -2.298 2.298l4.382 14.318c.51 1.668 2.84 1.755 3.473 .13l2.202 -5.644a1.84 1.84 0 0 1 1.045 -1.045" })
3915
4445
  });
3916
4446
  }
3917
4447
  /**
3918
4448
  * The stroke each marking tool draws, so the row says it in pictures as well as words.
3919
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
+ *
3920
4455
  * `select` is not a marking tool and never comes here: it draws nothing, so it
3921
- * wears the pointer instead (see {@link PointerIcon}).
4456
+ * wears the pointer instead.
3922
4457
  */
3923
4458
  function ToolIcon(tool) {
3924
4459
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3925
- viewBox: "0 0 16 16",
4460
+ viewBox: "0 0 24 24",
3926
4461
  "aria-hidden": "true",
3927
4462
  focusable: "false",
3928
- 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", {
3929
- x: "2.5",
3930
- y: "3.5",
3931
- width: "11",
3932
- height: "9",
3933
- 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"
3934
4469
  }) : tool === "ellipse" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3935
- cx: "8",
3936
- cy: "8",
3937
- r: "5.5"
3938
- }) : 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", {
3939
4474
  fillRule: "evenodd",
3940
- 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",
3941
4476
  fill: "currentColor",
3942
4477
  stroke: "none"
3943
- })
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
+ })] })
3944
4485
  });
3945
4486
  }
3946
4487
  /**
@@ -3954,22 +4495,22 @@ window.__ModuleLoader__.load({
3954
4495
  function GoalIcon() {
3955
4496
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3956
4497
  className: "da-ico",
3957
- viewBox: "0 0 16 16",
4498
+ viewBox: "0 0 24 24",
3958
4499
  "aria-hidden": "true",
3959
4500
  focusable: "false",
3960
4501
  children: [
3961
4502
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3962
- cx: "7",
3963
- cy: "9",
3964
- r: "5.2"
4503
+ cx: "10.5",
4504
+ cy: "13.5",
4505
+ r: "7.8"
3965
4506
  }),
3966
4507
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3967
- cx: "7",
3968
- cy: "9",
3969
- r: "2.3"
4508
+ cx: "10.5",
4509
+ cy: "13.5",
4510
+ r: "3.45"
3970
4511
  }),
3971
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M 13.8 2.2 L 8.2 7.8" }),
3972
- /* @__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" })
3973
4514
  ]
3974
4515
  });
3975
4516
  }
@@ -3977,29 +4518,29 @@ window.__ModuleLoader__.load({
3977
4518
  function ListIcon() {
3978
4519
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3979
4520
  className: "da-ico",
3980
- viewBox: "0 0 16 16",
4521
+ viewBox: "0 0 24 24",
3981
4522
  "aria-hidden": "true",
3982
4523
  focusable: "false",
3983
4524
  children: [
3984
- /* @__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" }),
3985
4526
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3986
- cx: "3.2",
3987
- cy: "4.5",
3988
- r: "1",
4527
+ cx: "4.8",
4528
+ cy: "6.75",
4529
+ r: "1.5",
3989
4530
  fill: "currentColor",
3990
4531
  stroke: "none"
3991
4532
  }),
3992
4533
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3993
- cx: "3.2",
3994
- cy: "8",
3995
- r: "1",
4534
+ cx: "4.8",
4535
+ cy: "12",
4536
+ r: "1.5",
3996
4537
  fill: "currentColor",
3997
4538
  stroke: "none"
3998
4539
  }),
3999
4540
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4000
- cx: "3.2",
4001
- cy: "11.5",
4002
- r: "1",
4541
+ cx: "4.8",
4542
+ cy: "17.25",
4543
+ r: "1.5",
4003
4544
  fill: "currentColor",
4004
4545
  stroke: "none"
4005
4546
  })
@@ -4025,40 +4566,40 @@ window.__ModuleLoader__.load({
4025
4566
  function UndoIcon() {
4026
4567
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4027
4568
  className: "da-ico",
4028
- viewBox: "0 0 16 16",
4569
+ viewBox: "0 0 24 24",
4029
4570
  "aria-hidden": "true",
4030
4571
  focusable: "false",
4031
- 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" })]
4032
4573
  });
4033
4574
  }
4034
4575
  /** The same arrow the other way. */
4035
4576
  function RedoIcon() {
4036
4577
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4037
4578
  className: "da-ico",
4038
- viewBox: "0 0 16 16",
4579
+ viewBox: "0 0 24 24",
4039
4580
  "aria-hidden": "true",
4040
4581
  focusable: "false",
4041
- 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" })]
4042
4583
  });
4043
4584
  }
4044
4585
  /** The primary action's mark: a paper plane, the usual "this leaves here". */
4045
4586
  function SendIcon() {
4046
4587
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4047
4588
  className: "da-ico",
4048
- viewBox: "0 0 16 16",
4589
+ viewBox: "0 0 24 24",
4049
4590
  "aria-hidden": "true",
4050
4591
  focusable: "false",
4051
- 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" })]
4052
4593
  });
4053
4594
  }
4054
4595
  /** The secondary action's mark: a speech bubble, "this goes to the chat box". */
4055
4596
  function ChatIcon() {
4056
4597
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4057
4598
  className: "da-ico",
4058
- viewBox: "0 0 16 16",
4599
+ viewBox: "0 0 24 24",
4059
4600
  "aria-hidden": "true",
4060
4601
  focusable: "false",
4061
- 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" })
4062
4603
  });
4063
4604
  }
4064
4605
  /**
@@ -4088,7 +4629,7 @@ window.__ModuleLoader__.load({
4088
4629
  const parsedAddress = parseFileAddress(address);
4089
4630
  const mediaType = figureMediaType(parsedAddress?.path ?? "");
4090
4631
  const isVector = mediaType === "image/svg+xml";
4091
- 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]);
4092
4633
  const sessionId = props.sessionId ?? parsedAddress?.sessionId;
4093
4634
  /**
4094
4635
  * A private prefix for the rail's gradient and clip ids.
@@ -4114,6 +4655,7 @@ window.__ModuleLoader__.load({
4114
4655
  const barRef = (0, react.useRef)(null);
4115
4656
  const figureHostRef = (0, react.useRef)(null);
4116
4657
  const surfaceRef = (0, react.useRef)(null);
4658
+ const paperRef = (0, react.useRef)(null);
4117
4659
  const figureRef = (0, react.useRef)(null);
4118
4660
  /**
4119
4661
  * Hand the stage to the owner as its scrollport, and keep it here too.
@@ -4159,6 +4701,7 @@ window.__ModuleLoader__.load({
4159
4701
  const surface = props.page;
4160
4702
  /** Whether this body annotates one page of a document its owner persists. */
4161
4703
  const isPage = surface !== void 0;
4704
+ const chrome = props.chrome !== false;
4162
4705
  const figure = surface === void 0 ? fileFigure : {
4163
4706
  kind: "raster",
4164
4707
  objectUrl: surface.dataUrl,
@@ -4198,12 +4741,46 @@ window.__ModuleLoader__.load({
4198
4741
  const [color, setColor] = (0, react.useState)(remembered.color ?? COLORS[0]);
4199
4742
  const [weight, setWeight] = (0, react.useState)(remembered.weight ?? 1);
4200
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;
4201
4753
  const [past, setPast] = (0, react.useState)([]);
4202
4754
  const [future, setFuture] = (0, react.useState)([]);
4203
- 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]);
4204
4769
  /** The mark whose detail box should take the caret once it is on screen. */
4205
4770
  const [focusId, setFocusId] = (0, react.useState)(null);
4206
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
+ /**
4207
4784
  * What the figure shows, as context for the marks below it.
4208
4785
  *
4209
4786
  * One per figure rather than one per mark: it is never drawn, and its only job
@@ -4232,7 +4809,14 @@ window.__ModuleLoader__.load({
4232
4809
  * second one's own close button. Opening any popover closes whatever was open;
4233
4810
  * opening the one already showing closes it, so the button toggles.
4234
4811
  */
4235
- 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]);
4236
4820
  const styleOpen = openPanel === "style";
4237
4821
  const goalOpen = openPanel === "goal";
4238
4822
  const listOpen = openPanel === "list";
@@ -4270,7 +4854,19 @@ window.__ModuleLoader__.load({
4270
4854
  * the reader picks a colour is the moment it has to be safe. Storage failures are
4271
4855
  * swallowed inside `rememberPreferences` — losing a colour is not worth an error.
4272
4856
  */
4273
- 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]);
4274
4870
  /**
4275
4871
  * Whether a drag is currently moving the view.
4276
4872
  *
@@ -4282,6 +4878,11 @@ window.__ModuleLoader__.load({
4282
4878
  const [fitWidth, setFitWidth] = (0, react.useState)(0);
4283
4879
  const [busy, setBusy] = (0, react.useState)(null);
4284
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);
4285
4886
  /** The panel's own box, for placing the cards that float over it. */
4286
4887
  const rootRef = (0, react.useRef)(null);
4287
4888
  /**
@@ -4294,6 +4895,7 @@ window.__ModuleLoader__.load({
4294
4895
  * off the edge when it narrows.
4295
4896
  */
4296
4897
  const [palette, setPalette] = (0, react.useState)(remembered.palette);
4898
+ const paletteRef = (0, react.useRef)(null);
4297
4899
  const [dragging, setDragging] = (0, react.useState)(false);
4298
4900
  const dragFrom = (0, react.useRef)(null);
4299
4901
  /**
@@ -4339,24 +4941,30 @@ window.__ModuleLoader__.load({
4339
4941
  * @param from - which strip's press is in progress.
4340
4942
  * @param write - where its new spot goes.
4341
4943
  */
4342
- const followPointer = (0, react.useCallback)((event, from, write) => {
4944
+ const followPointer = (0, react.useCallback)((event, from, write, strip) => {
4343
4945
  const held = from.current;
4344
- const root = rootRef.current;
4946
+ const root = strip?.offsetParent ?? rootRef.current;
4345
4947
  /* v8 ignore next */
4346
- if (held === null || root === null) return;
4948
+ if (held === null || root === null || !(root instanceof Element)) return;
4347
4949
  const bounds = root.getBoundingClientRect();
4348
4950
  const x = held.origin.x + (event.clientX - held.box.left) / Math.max(1, bounds.width);
4349
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);
4350
4958
  write({
4351
- x: Math.min(1, Math.max(0, x)),
4352
- 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))
4353
4961
  });
4354
4962
  }, []);
4355
4963
  const dragPalette = (0, react.useCallback)((event) => {
4356
- followPointer(event, dragFrom, setPalette);
4964
+ followPointer(event, dragFrom, setPalette, paletteRef.current);
4357
4965
  }, [followPointer]);
4358
4966
  const dragBar = (0, react.useCallback)((event) => {
4359
- followPointer(event, barFrom, setBarSpot);
4967
+ followPointer(event, barFrom, setBarSpot, barRef.current);
4360
4968
  }, [followPointer]);
4361
4969
  (0, react.useEffect)(() => {
4362
4970
  rememberPreferences({
@@ -4608,12 +5216,14 @@ window.__ModuleLoader__.load({
4608
5216
  const scale = (0, react.useMemo)(() => {
4609
5217
  if (size === null) return 1;
4610
5218
  if (zoom !== "fit") return zoom;
4611
- if (fitWidth <= 0) return 1;
4612
- 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));
4613
5222
  }, [
4614
5223
  zoom,
4615
5224
  fitWidth,
4616
- size
5225
+ size,
5226
+ props.fitWidth
4617
5227
  ]);
4618
5228
  /**
4619
5229
  * Where the records hang: above the bar, centred on it.
@@ -4655,6 +5265,17 @@ window.__ModuleLoader__.load({
4655
5265
  htmlHeight
4656
5266
  ]);
4657
5267
  /**
5268
+ * Whether an upward fold would come out of the top of the panel.
5269
+ *
5270
+ * The style fold is anchored to its own button, so it climbs — which is right while
5271
+ * the bar sits at the bottom and wrong the moment the reader parks the bar along the
5272
+ * top, where climbing puts the swatches out of sight above the panel. The record
5273
+ * placement already answers this question for the records; the fold reads the same
5274
+ * answer rather than measuring a second time, so the two can never disagree about
5275
+ * where there is room.
5276
+ */
5277
+ const foldOpensDown = floatsAt !== null && floatsAt.bottom === void 0;
5278
+ /**
4658
5279
  * Put a record away when the reader clicks anywhere else.
4659
5280
  *
4660
5281
  * A record is a fold, not a mode: it has done its job the moment the reader looks
@@ -4752,9 +5373,9 @@ window.__ModuleLoader__.load({
4752
5373
  const zoomAt = (0, react.useCallback)((next, clientX, clientY) => {
4753
5374
  const stage = stageRef.current;
4754
5375
  const surface = surfaceRef.current;
4755
- const figureHost = figureRef.current;
5376
+ const sheet = paperRef.current;
4756
5377
  /* v8 ignore next -- a wheel event only reaches a handler on a mounted stage, and size is what allows one. */
4757
- if (stage === null || surface === null || figureHost === null || size === null) return;
5378
+ if (stage === null || surface === null || sheet === null || paper === null) return;
4758
5379
  const before = liveScale.current;
4759
5380
  const reached = clampZoom(next);
4760
5381
  if (reached === before) return;
@@ -4768,19 +5389,22 @@ window.__ModuleLoader__.load({
4768
5389
  }, {
4769
5390
  clientX,
4770
5391
  clientY
4771
- }, before, reached, size);
5392
+ }, before, reached, paper);
4772
5393
  liveScale.current = reached;
4773
- surface.style.width = `${size.width * reached}px`;
4774
- surface.style.height = `${size.height * reached}px`;
4775
- figureHost.style.transform = `scale(${reached})`;
5394
+ surface.style.width = `${paper.width * reached}px`;
5395
+ surface.style.height = `${paper.height * reached}px`;
5396
+ sheet.style.transform = `scale(${reached})`;
4776
5397
  stage.scrollLeft = target.scrollLeft;
4777
5398
  stage.scrollTop = target.scrollTop;
4778
5399
  clearTimeout(settleTimer.current);
4779
5400
  settleTimer.current = setTimeout(() => {
4780
5401
  settleTimer.current = void 0;
5402
+ const settled = paperRef.current;
5403
+ /* v8 ignore next -- the sheet is the element the pinch was measured on. */
5404
+ if (settled !== null) settled.style.transform = "";
4781
5405
  setZoom(reached);
4782
5406
  }, 120);
4783
- }, [size]);
5407
+ }, [paper]);
4784
5408
  (0, react.useEffect)(() => () => {
4785
5409
  clearTimeout(settleTimer.current);
4786
5410
  }, []);
@@ -4788,13 +5412,20 @@ window.__ModuleLoader__.load({
4788
5412
  const stage = stageRef.current;
4789
5413
  if (stage === null) return;
4790
5414
  /**
4791
- * A trackpad pinch arrives as a ctrl-modified wheel. The browser would zoom
4792
- * the whole app for it, and the plugin's own stage is the surface the user is
4793
- * pointing at, so the event is taken here. A plain two-finger scroll is left
4794
- * 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.
4795
5427
  */
4796
5428
  const onWheel = (event) => {
4797
- if (!event.ctrlKey) return;
4798
5429
  event.preventDefault();
4799
5430
  zoomAt(liveScale.current * wheelZoomFactor(event.deltaY, event.deltaMode, stage.clientHeight), event.clientX, event.clientY);
4800
5431
  };
@@ -4812,9 +5443,17 @@ window.__ModuleLoader__.load({
4812
5443
  commit([...marks, mark]);
4813
5444
  }, [commit, marks]);
4814
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
+ }
4815
5450
  commit(marks.filter((mark) => mark.id !== id));
4816
5451
  setSelectedId((current) => current === id ? null : current);
4817
- }, [commit, marks]);
5452
+ }, [
5453
+ commit,
5454
+ marks,
5455
+ props.onDocumentRemove
5456
+ ]);
4818
5457
  /**
4819
5458
  * Write one mark's detail.
4820
5459
  *
@@ -5239,6 +5878,178 @@ window.__ModuleLoader__.load({
5239
5878
  size,
5240
5879
  t
5241
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
+ ]);
5242
6053
  /** Whether this body holds a rendered document rather than a figure file. */
5243
6054
  const isHtml = mediaType === "text/html";
5244
6055
  /** Name what one figure point landed on, for a surface that holds its own elements. */
@@ -5256,6 +6067,18 @@ window.__ModuleLoader__.load({
5256
6067
  role: "alert",
5257
6068
  children: figureError
5258
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
+ };
5259
6082
  const selected = marks.find((mark) => mark.id === selectedId) ?? null;
5260
6083
  /**
5261
6084
  * What the colour control currently means.
@@ -5281,7 +6104,7 @@ window.__ModuleLoader__.load({
5281
6104
  * One number for the fill, the knob and the input: computed three times the three
5282
6105
  * could round differently and the knob would sit a pixel off the fill it belongs to.
5283
6106
  */
5284
- const weightFraction = shownWeight === null ? 0 : (shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN);
6107
+ const weightFraction = shownWeight === null ? 0 : (shownWeight - WEIGHT_MIN) / (8 - WEIGHT_MIN);
5285
6108
  /**
5286
6109
  * Change one attribute of the selected mark, or of the next stroke when nothing
5287
6110
  * is selected.
@@ -5338,6 +6161,240 @@ window.__ModuleLoader__.load({
5338
6161
  width: size?.width,
5339
6162
  height: size?.height
5340
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
+ })] });
5341
6398
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5342
6399
  className: "da-root",
5343
6400
  ref: rootRef,
@@ -5360,19 +6417,22 @@ window.__ModuleLoader__.load({
5360
6417
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5361
6418
  className: "da-stage",
5362
6419
  ref: attachStage,
6420
+ onContextMenu: openMenu,
5363
6421
  "data-hand": tool === "select" ? "true" : void 0,
5364
6422
  "data-panning": panning ? "true" : void 0,
5365
6423
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5366
6424
  className: "da-surface",
5367
6425
  ref: surfaceRef,
5368
6426
  style: surfaceStyle,
5369
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6427
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5370
6428
  className: "da-paper",
6429
+ ref: paperRef,
5371
6430
  style: paperStyle,
5372
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6431
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6432
+ className: "da-figure-frame",
5373
6433
  ref: figureRef,
5374
6434
  style: figureStyle,
5375
- children: [figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6435
+ children: figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5376
6436
  ref: figureHostRef,
5377
6437
  dangerouslySetInnerHTML: { __html: figure.markup }
5378
6438
  }) : figure?.kind === "raster" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
@@ -5397,39 +6457,59 @@ window.__ModuleLoader__.load({
5397
6457
  width: figure.width,
5398
6458
  height: htmlHeight
5399
6459
  }
5400
- }) : null, paper === null || size === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
5401
- width: paper.width,
5402
- height: paper.height,
5403
- origin: [-paper.x, -paper.y],
5404
- marks,
5405
- tool,
5406
- color,
5407
- weight,
5408
- containerRef: figureHostRef,
5409
- anchoring: isVector,
5410
- anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
5411
- selectedId,
5412
- onSelect: setSelectedId,
5413
- onAdd: addMark,
5414
- onMove: moveMark,
5415
- onUpdate: updateMark,
5416
- onRemove: removeMark,
5417
- onPan: panBy,
5418
- onPanStart: () => {
5419
- setPanning(true);
5420
- },
5421
- onPanEnd: () => {
5422
- setPanning(false);
5423
- },
5424
- onEdit: editMark,
5425
- onDrawn: finishDrawing,
5426
- onLiveEdit: setLiveMark,
5427
- numbering: props.numbering
5428
- })]
5429
- })
6460
+ }) : null
6461
+ }), paper === null || size === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
6462
+ width: paper.width,
6463
+ height: paper.height,
6464
+ origin: [-paper.x, -paper.y],
6465
+ marks,
6466
+ tool,
6467
+ color,
6468
+ weight,
6469
+ scale,
6470
+ containerRef: figureHostRef,
6471
+ anchoring: isVector,
6472
+ anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
6473
+ selectedId,
6474
+ onSelect: setSelectedId,
6475
+ onAdd: addMark,
6476
+ onMove: moveMark,
6477
+ onUpdate: updateMark,
6478
+ onRemove: removeMark,
6479
+ onPan: panBy,
6480
+ onPanStart: () => {
6481
+ setPanning(true);
6482
+ },
6483
+ onPanEnd: () => {
6484
+ setPanning(false);
6485
+ },
6486
+ onEdit: editMark,
6487
+ onDrawn: finishDrawing,
6488
+ onLiveEdit: setLiveMark,
6489
+ numbering: props.numbering
6490
+ })]
5430
6491
  })
5431
6492
  })
5432
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
+ }),
5433
6513
  goalOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5434
6514
  className: "da-float da-float-record da-float-goal",
5435
6515
  role: "group",
@@ -5487,14 +6567,14 @@ window.__ModuleLoader__.load({
5487
6567
  t("marks"),
5488
6568
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5489
6569
  className: "da-card-count",
5490
- children: marks.length
6570
+ children: listedMarks.length
5491
6571
  })
5492
6572
  ]
5493
6573
  }),
5494
6574
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5495
6575
  type: "button",
5496
6576
  className: "da-btn da-btn-quiet",
5497
- disabled: marks.length === 0,
6577
+ disabled: listedMarks.length === 0,
5498
6578
  onClick: () => {
5499
6579
  discard();
5500
6580
  },
@@ -5513,7 +6593,7 @@ window.__ModuleLoader__.load({
5513
6593
  ]
5514
6594
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5515
6595
  className: "da-marks",
5516
- children: marks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6596
+ children: listedMarks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5517
6597
  className: "da-empty",
5518
6598
  children: [
5519
6599
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BoxIcon, {}),
@@ -5526,41 +6606,56 @@ window.__ModuleLoader__.load({
5526
6606
  children: t("noMarksHint")
5527
6607
  })
5528
6608
  ]
5529
- }) : marks.map((mark, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6609
+ }) : listedMarks.map((mark, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5530
6610
  className: "da-mark",
5531
6611
  "data-selected": mark.id === selectedId,
5532
6612
  onClick: () => {
5533
- setSelectedId(mark.id);
6613
+ reveal(mark);
5534
6614
  },
5535
6615
  children: [
5536
6616
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5537
- className: "da-mark-index",
5538
- style: { background: mark.color },
6617
+ className: "da-mark-n",
5539
6618
  children: index + 1
5540
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
+ }),
5541
6626
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5542
6627
  type: "button",
5543
6628
  className: "da-mark-text",
5544
6629
  onClick: (event) => {
5545
6630
  event.stopPropagation();
5546
- setSelectedId(mark.id);
6631
+ reveal(mark);
5547
6632
  },
5548
6633
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5549
6634
  className: "da-mark-title",
5550
6635
  children: mark.detail !== void 0 && mark.detail.trim() !== "" ? mark.detail : t("emptyDetail")
5551
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6636
+ }), anchorName(mark) === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5552
6637
  className: "da-mark-meta",
5553
- children: [t(TOOL_COPY[mark.kind]), anchorName(mark)].filter((part) => part !== "").join(" · ")
6638
+ children: anchorName(mark)
5554
6639
  })]
5555
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
+ }),
5556
6645
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5557
6646
  type: "button",
5558
6647
  className: "da-mark-del",
6648
+ "aria-label": t("delete"),
6649
+ title: t("delete"),
5559
6650
  onClick: (event) => {
5560
6651
  event.stopPropagation();
5561
6652
  removeMark(mark.id);
5562
6653
  },
5563
- 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
+ })
5564
6659
  })
5565
6660
  ]
5566
6661
  }, mark.id))
@@ -5609,7 +6704,7 @@ window.__ModuleLoader__.load({
5609
6704
  })]
5610
6705
  })]
5611
6706
  }),
5612
- shownWeight === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6707
+ !chrome || shownWeight === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5613
6708
  className: "da-weight",
5614
6709
  title: t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
5615
6710
  children: [
@@ -5618,7 +6713,7 @@ window.__ModuleLoader__.load({
5618
6713
  className: "da-weight-input",
5619
6714
  "aria-label": t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
5620
6715
  min: WEIGHT_MIN,
5621
- max: 4,
6716
+ max: 8,
5622
6717
  step: .1,
5623
6718
  value: shownWeight,
5624
6719
  onChange: (event) => {
@@ -5630,275 +6725,44 @@ window.__ModuleLoader__.load({
5630
6725
  viewBox: "0 0 18 100",
5631
6726
  preserveAspectRatio: "none",
5632
6727
  "aria-hidden": "true",
5633
- children: [
5634
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("defs", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
5635
- id: `${trackId}-shade`,
5636
- x1: "0",
5637
- y1: "1",
5638
- x2: "0",
5639
- y2: "0",
5640
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
6728
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
6729
+ id: `${trackId}-shade`,
6730
+ x1: "0",
6731
+ y1: "1",
6732
+ x2: "0",
6733
+ y2: "0",
6734
+ children: [
6735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
5641
6736
  offset: "0",
5642
- stopColor: "rgba(20,40,80,.05)"
5643
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
6737
+ stopColor: "rgba(20,40,80,.06)"
6738
+ }),
6739
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
6740
+ offset: ".55",
6741
+ stopColor: "rgba(20,40,80,.13)"
6742
+ }),
6743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
5644
6744
  offset: "1",
5645
- stopColor: "rgba(20,40,80,.2)"
5646
- })]
5647
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("clipPath", {
5648
- id: `${trackId}-cut`,
5649
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
5650
- x: "0",
5651
- y: 100 - weightFraction * 100,
5652
- width: "18",
5653
- height: weightFraction * 100
6745
+ stopColor: "rgba(20,40,80,.24)"
5654
6746
  })
5655
- })] }),
5656
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
5657
- d: WEIGHT_TRACK,
5658
- fill: `url(#${trackId}-shade)`
5659
- }),
5660
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
5661
- d: WEIGHT_TRACK,
5662
- fill: "var(--dsw-alias-brand-primary,#176df2)",
5663
- clipPath: `url(#${trackId}-cut)`
5664
- })
5665
- ]
6747
+ ]
6748
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
6749
+ d: WEIGHT_TRACK,
6750
+ fill: `url(#${trackId}-shade)`
6751
+ })]
6752
+ }),
6753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6754
+ className: "da-weight-fill",
6755
+ "aria-hidden": true,
6756
+ style: { height: travelHeight(weightFraction) }
5666
6757
  }),
5667
6758
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5668
6759
  className: "da-weight-knob",
5669
6760
  "aria-hidden": true,
5670
- style: { bottom: `calc(12px + ${weightFraction} * (100% - 24px - var(--da-knob-size, 11px)))` }
6761
+ style: { bottom: `calc(${travelHeight(weightFraction)} - var(--da-knob-size) / 2)` }
5671
6762
  })
5672
6763
  ]
5673
6764
  }),
5674
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5675
- className: "da-palette",
5676
- "data-dragging": dragging ? "true" : void 0,
5677
- style: paletteStyle,
5678
- role: "toolbar",
5679
- "aria-label": t("tools"),
5680
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5681
- className: "da-palette-grip",
5682
- title: t("moveTools"),
5683
- onPointerDown: (event) => {
5684
- /* v8 ignore next */
5685
- if (rootRef.current === null) return;
5686
- /* v8 ignore next -- jsdom has no pointer capture; a browser always does. */
5687
- event.currentTarget.setPointerCapture?.(event.pointerId);
5688
- dragFrom.current = {
5689
- pointerId: event.pointerId,
5690
- origin: palette ?? DEFAULT_PALETTE,
5691
- box: {
5692
- left: event.clientX,
5693
- top: event.clientY
5694
- }
5695
- };
5696
- setDragging(true);
5697
- },
5698
- onPointerMove: (event) => {
5699
- dragPalette(event);
5700
- },
5701
- onPointerUp: (event) => {
5702
- event.currentTarget.releasePointerCapture?.(event.pointerId);
5703
- dragFrom.current = null;
5704
- setDragging(false);
5705
- },
5706
- onPointerCancel: () => {
5707
- dragFrom.current = null;
5708
- setDragging(false);
5709
- },
5710
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5711
- className: "da-palette-grip-dots",
5712
- "aria-hidden": true,
5713
- children: "⠿"
5714
- })
5715
- }), TOOLS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5716
- type: "button",
5717
- className: "da-btn da-tool",
5718
- "aria-pressed": tool === entry.tool,
5719
- "aria-label": t(entry.label),
5720
- title: t(entry.label),
5721
- onClick: () => {
5722
- setTool(entry.tool);
5723
- },
5724
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5725
- className: "da-tool-icon",
5726
- "aria-hidden": true,
5727
- children: entry.tool === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointerIcon, {}) : ToolIcon(entry.tool)
5728
- })
5729
- }, entry.tool))]
5730
- }),
5731
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5732
- className: "da-bar",
5733
- ref: barRef,
5734
- "data-dragging": barDragging ? "true" : void 0,
5735
- "data-parked": barSpot === void 0 ? void 0 : "true",
5736
- style: barStyle,
5737
- children: [
5738
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5739
- className: "da-palette-grip",
5740
- title: t("moveBar"),
5741
- onPointerDown: (event) => {
5742
- /* v8 ignore next */
5743
- if (rootRef.current === null) return;
5744
- /* v8 ignore next -- jsdom has no pointer capture; a browser always does. */
5745
- event.currentTarget.setPointerCapture?.(event.pointerId);
5746
- barFrom.current = {
5747
- pointerId: event.pointerId,
5748
- origin: barSpot ?? DEFAULT_PALETTE,
5749
- box: {
5750
- left: event.clientX,
5751
- top: event.clientY
5752
- }
5753
- };
5754
- setBarDragging(true);
5755
- },
5756
- onPointerMove: (event) => {
5757
- dragBar(event);
5758
- },
5759
- onPointerUp: (event) => {
5760
- event.currentTarget.releasePointerCapture?.(event.pointerId);
5761
- barFrom.current = null;
5762
- setBarDragging(false);
5763
- },
5764
- onPointerCancel: () => {
5765
- barFrom.current = null;
5766
- setBarDragging(false);
5767
- },
5768
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5769
- className: "da-palette-grip-dots",
5770
- "aria-hidden": true,
5771
- children: "⠿"
5772
- })
5773
- }),
5774
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5775
- className: "da-bar-tools",
5776
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5777
- type: "button",
5778
- className: "da-btn da-style",
5779
- "aria-expanded": styleOpen,
5780
- "aria-label": t("style"),
5781
- title: t("style"),
5782
- onClick: () => {
5783
- setOpenPanel((open) => open === "style" ? null : "style");
5784
- },
5785
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5786
- className: "da-swatch-dot",
5787
- style: { background: shownColor }
5788
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5789
- className: "da-style-caret",
5790
- "aria-hidden": true,
5791
- children: styleOpen ? "▴" : "▾"
5792
- })]
5793
- })
5794
- }),
5795
- styleOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5796
- className: "da-style-row",
5797
- role: "group",
5798
- "aria-label": t("style"),
5799
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5800
- className: "da-style-label",
5801
- children: t("styleColor")
5802
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5803
- className: "da-style-colors",
5804
- children: COLORS.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5805
- type: "button",
5806
- className: "da-swatch",
5807
- "aria-pressed": shownColor === value,
5808
- style: { background: value },
5809
- "aria-label": value,
5810
- title: value,
5811
- onClick: () => {
5812
- chooseColor(value);
5813
- }
5814
- }, value))
5815
- })]
5816
- }) : null,
5817
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5818
- className: "da-sep",
5819
- "aria-hidden": true
5820
- }),
5821
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "da-spacer" }),
5822
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5823
- type: "button",
5824
- className: "da-dock-btn",
5825
- "aria-pressed": goalOpen,
5826
- "aria-label": t("summary"),
5827
- title: t("summary"),
5828
- onClick: () => {
5829
- setOpenPanel((open) => open === "goal" ? null : "goal");
5830
- },
5831
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GoalIcon, {}), summary.trim() === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5832
- className: "da-dock-dot",
5833
- "aria-hidden": true
5834
- })]
5835
- }),
5836
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5837
- type: "button",
5838
- className: "da-dock-btn",
5839
- "aria-pressed": listOpen,
5840
- "aria-label": t("marks"),
5841
- title: t("marks"),
5842
- onClick: () => {
5843
- setOpenPanel((open) => open === "list" ? null : "list");
5844
- },
5845
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListIcon, {}), marks.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5846
- className: "da-dock-count",
5847
- children: marks.length
5848
- })]
5849
- }),
5850
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5851
- className: "da-dock-sep",
5852
- "aria-hidden": true
5853
- }),
5854
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5855
- type: "button",
5856
- className: "da-dock-btn",
5857
- "aria-label": t("undo"),
5858
- title: t("undo"),
5859
- disabled: past.length === 0,
5860
- onClick: undo,
5861
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UndoIcon, {})
5862
- }),
5863
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5864
- type: "button",
5865
- className: "da-dock-btn",
5866
- "aria-label": t("redo"),
5867
- title: t("redo"),
5868
- disabled: future.length === 0,
5869
- onClick: redo,
5870
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedoIcon, {})
5871
- }),
5872
- isPage ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5873
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5874
- className: "da-dock-sep",
5875
- "aria-hidden": true
5876
- }),
5877
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5878
- type: "button",
5879
- className: "da-dock-btn da-dock-btn-primary",
5880
- disabled: busy !== null,
5881
- "aria-label": t("sendToAgent"),
5882
- title: t("sendToAgent"),
5883
- onClick: () => {
5884
- run("agent");
5885
- },
5886
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendIcon, {})
5887
- }),
5888
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5889
- type: "button",
5890
- className: "da-dock-btn",
5891
- disabled: busy !== null,
5892
- "aria-label": t("sendToComposer"),
5893
- title: t("sendToComposer"),
5894
- onClick: () => {
5895
- run("composer");
5896
- },
5897
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatIcon, {})
5898
- })
5899
- ] })
5900
- ]
5901
- })
6765
+ !chrome ? null : props.chromeHost === void 0 || props.chromeHost === null ? chromeStrips : (0, react_dom.createPortal)(chromeStrips, props.chromeHost)
5902
6766
  ]
5903
6767
  });
5904
6768
  }
@@ -6522,7 +7386,8 @@ window.__ModuleLoader__.load({
6522
7386
  sessions: ctx.get("sessions"),
6523
7387
  conversation: ctx.get("conversation"),
6524
7388
  sessionId: String(sessionId),
6525
- localeId: locale.getLocale().locale ?? "zh"
7389
+ localeId: locale.getLocale().locale ?? "zh",
7390
+ remote: ctx.get("remote")
6526
7391
  })
6527
7392
  }, AnnotatorBody)), "dsh-figmark: document body");
6528
7393
  ctx.effect(() => previews.register({
@@ -6588,7 +7453,8 @@ window.__ModuleLoader__.load({
6588
7453
  sessions: ctx.get("sessions"),
6589
7454
  conversation: ctx.get("conversation"),
6590
7455
  sessionId: String(sessionId),
6591
- localeId: locale.getLocale().locale ?? "zh"
7456
+ localeId: locale.getLocale().locale ?? "zh",
7457
+ remote: ctx.get("remote")
6592
7458
  })
6593
7459
  }, AnnotatorBody)), "dsh-figmark: html document body");
6594
7460
  }