dsh-figmark 1.1.1 → 1.1.3

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
@@ -268,7 +268,7 @@ window.__ModuleLoader__.load({
268
268
  * @returns the value, pulled to the nearest end of the range.
269
269
  */
270
270
  function clampWeight(value) {
271
- return Math.min(4, Math.max(WEIGHT_MIN, value));
271
+ return Math.min(8, Math.max(WEIGHT_MIN, value));
272
272
  }
273
273
  /**
274
274
  * Whether a value is a stroke weight this plugin would write.
@@ -277,7 +277,7 @@ window.__ModuleLoader__.load({
277
277
  * @returns true when it is one.
278
278
  */
279
279
  function isWeight(value) {
280
- return typeof value === "number" && Number.isFinite(value) && value >= .5 && value <= 4;
280
+ return typeof value === "number" && Number.isFinite(value) && value >= .5 && value <= 8;
281
281
  }
282
282
  /**
283
283
  * Suffix naming one page's files inside a paged document's name.
@@ -326,6 +326,86 @@ window.__ModuleLoader__.load({
326
326
  function round(value) {
327
327
  return Math.round(value);
328
328
  }
329
+ /**
330
+ * Write a freehand stroke's extent as the box it was drawn in.
331
+ *
332
+ * The stroke itself is dozens of coordinates, most of which say nothing an agent
333
+ * can act on; the box says where on the figure to look, which is what the reading
334
+ * of the mark needs. `strokeExtents` names the area, and the phrase above it names
335
+ * the mark, so the pair still reads as one line.
336
+ * @param points - the stroke's sampled points.
337
+ * @returns the box as ` (x,y)-(x,y)`, or nothing for an empty stroke.
338
+ */
339
+ function strokeExtents(points) {
340
+ let left = Number.POSITIVE_INFINITY;
341
+ let top = Number.POSITIVE_INFINITY;
342
+ let right = Number.NEGATIVE_INFINITY;
343
+ let bottom = Number.NEGATIVE_INFINITY;
344
+ for (const [x, y] of points) {
345
+ left = Math.min(left, x);
346
+ top = Math.min(top, y);
347
+ right = Math.max(right, x);
348
+ bottom = Math.max(bottom, y);
349
+ }
350
+ if (!Number.isFinite(left)) return "";
351
+ return ` (${round(left)},${round(top)})-(${round(right)},${round(bottom)})`;
352
+ }
353
+ /**
354
+ * Write two dragged corners as the rectangle they make.
355
+ *
356
+ * A box dragged up and to the left arrives as (bottom-right, top-left), and the
357
+ * agent reading `(187,138)-(42,88)` has to take a min and a max before it knows
358
+ * which area is meant. Both corners are therefore written left-to-right and
359
+ * top-to-bottom, so the same box always reads the same way however it was drawn.
360
+ * @param first - where the drag started.
361
+ * @param second - where it ended.
362
+ * @returns the corner pair as `(x,y)-(x,y)`, or nothing when the mark is unplaced.
363
+ */
364
+ function extents(first, second) {
365
+ if (first === void 0 || second === void 0) return "";
366
+ 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]))})`;
367
+ }
368
+ /**
369
+ * Say how far a mark reaches outside the figure, when it does.
370
+ *
371
+ * A negative coordinate is an accurate description — the origin is the figure's
372
+ * top-left corner, so -57 is 57 pixels above it — but a reader has to notice the
373
+ * sign and then reason about what it implies. Naming the overhang outright costs
374
+ * about a dozen characters and removes that step: the agent is told the mark is
375
+ * off the picture and by how much, rather than being left to infer it. A mark that
376
+ * sits inside the figure says nothing at all, which is the common case.
377
+ * @param mark - the mark to measure.
378
+ * @param figure - the figure's size, when the document knows it.
379
+ * @param locale - language of the surrounding message.
380
+ * @returns the sentence fragment to append, or nothing when the mark is inside.
381
+ */
382
+ function overhang(mark, figure, locale) {
383
+ const { width, height } = figure;
384
+ if (width === void 0 || height === void 0) return "";
385
+ let left = Number.POSITIVE_INFINITY;
386
+ let top = Number.POSITIVE_INFINITY;
387
+ let right = Number.NEGATIVE_INFINITY;
388
+ let bottom = Number.NEGATIVE_INFINITY;
389
+ for (const [x, y] of mark.points) {
390
+ left = Math.min(left, x);
391
+ top = Math.min(top, y);
392
+ right = Math.max(right, x);
393
+ bottom = Math.max(bottom, y);
394
+ }
395
+ const escape = (amount, zhWord, enWord) => {
396
+ const pixels = round(amount);
397
+ if (pixels <= 0) return "";
398
+ return locale === "zh" ? `${zhWord} ${pixels}` : `${enWord} ${pixels}`;
399
+ };
400
+ const escapes = [
401
+ escape(-left, "左", "left"),
402
+ escape(-top, "上", "up"),
403
+ escape(right - width, "右", "right"),
404
+ escape(bottom - height, "下", "down")
405
+ ].filter((part) => part !== "");
406
+ if (escapes.length === 0) return "";
407
+ return locale === "zh" ? `(超出图面:${escapes.join("、")} 像素)` : ` (off the figure: ${escapes.join(", ")} px)`;
408
+ }
329
409
  /** Describe one mark's shape in words, for the model-facing summary. */
330
410
  function describeShape(mark, locale) {
331
411
  const [first, second] = mark.points;
@@ -333,9 +413,12 @@ window.__ModuleLoader__.load({
333
413
  const turned = mark.rotation === void 0 || mark.rotation === 0 ? "" : zh ? `(旋转 ${Math.round(mark.rotation)}°)` : ` (rotated ${Math.round(mark.rotation)}°)`;
334
414
  switch (mark.kind) {
335
415
  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}`;
416
+ case "rect": return (zh ? "框选" : "box") + extents(first, second) + turned;
417
+ case "ellipse": return (zh ? "圈选" : "ellipse") + extents(first, second) + turned;
418
+ case "pen": {
419
+ const box = strokeExtents(mark.points);
420
+ return `${zh ? "手绘线" : "freehand stroke"}${box}${turned}`;
421
+ }
339
422
  case "point": return first !== void 0 ? zh ? `单点位于 (${round(first[0])},${round(first[1])})` : `at (${round(first[0])},${round(first[1])})` : zh ? "单点" : "a point";
340
423
  }
341
424
  }
@@ -350,7 +433,7 @@ window.__ModuleLoader__.load({
350
433
  if (anchor.text !== void 0 && anchor.text !== "") parts.push(zh ? `元素文字「${anchor.text}」` : `element text "${anchor.text}"`);
351
434
  if (anchor.selector !== void 0 && anchor.selector !== "") parts.push(zh ? `选择器 ${anchor.selector}` : `selector ${anchor.selector}`);
352
435
  if (parts.length === 0) parts.push(`<${anchor.tag}>`);
353
- return `(${parts.join(",")})`;
436
+ return zh ? `(${parts.join(",")})` : ` (${parts.join(", ")})`;
354
437
  }
355
438
  /**
356
439
  * Render one mark as a numbered line the model reads.
@@ -362,9 +445,11 @@ window.__ModuleLoader__.load({
362
445
  * @param mark - the mark to describe.
363
446
  * @param index - zero-based position in the document, rendered as a circled number.
364
447
  * @param locale - language of the surrounding message.
448
+ * @param figure - the figure these marks belong to, when its size is known; a mark
449
+ * that reaches past it then says so rather than leaving the sign to be noticed.
365
450
  * @returns one line of the annotation summary.
366
451
  */
367
- function describeMark(mark, index, locale) {
452
+ function describeMark(mark, index, locale, figure) {
368
453
  const zh = locale === "zh";
369
454
  const marker = [
370
455
  "①",
@@ -380,8 +465,9 @@ window.__ModuleLoader__.load({
380
465
  ][index] ?? `${index + 1}.`;
381
466
  const page = mark.page === void 0 ? "" : zh ? `第 ${mark.page} 页 ` : `page ${mark.page} `;
382
467
  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}`);
468
+ const escape = figure === void 0 ? "" : overhang(mark, figure, locale);
469
+ const parts = [`${marker} ${page}${describeShape(mark, locale)}${describeAnchor(mark, locale)}${escape}`];
470
+ if (detail !== "") parts.push(zh ? `批注:${detail}` : `note: ${detail}`);
385
471
  if (detail === "") parts.push(zh ? "(未写说明)" : "(no detail)");
386
472
  return parts.join(zh ? "," : ", ");
387
473
  }
@@ -392,7 +478,7 @@ window.__ModuleLoader__.load({
392
478
  * @returns one line per mark.
393
479
  */
394
480
  function describeMarks(document, locale) {
395
- return document.marks.map((mark, index) => describeMark(mark, index, locale));
481
+ return document.marks.map((mark, index) => describeMark(mark, index, locale, document.figure));
396
482
  }
397
483
  //#endregion
398
484
  //#region src/shared/figure-kind.ts
@@ -867,7 +953,7 @@ window.__ModuleLoader__.load({
867
953
  * @param rotation - the mark's rotation in degrees.
868
954
  * @returns the handles.
869
955
  */
870
- function handlesFor(box, rotation) {
956
+ function handlesFor(box, rotation, size = 7) {
871
957
  const centre = boxCentre(box);
872
958
  const radians = rotation * Math.PI / 180;
873
959
  const at = (x, y) => rotateAround([x, y], centre, radians);
@@ -884,11 +970,23 @@ window.__ModuleLoader__.load({
884
970
  });
885
971
  placed.push({
886
972
  id: "rotate",
887
- point: at(centre[0], box.y - 22)
973
+ point: at(centre[0], box.y - armFor(size))
888
974
  });
889
975
  return placed;
890
976
  }
891
977
  /**
978
+ * How far the turn handle stands off the top edge, for a handle of a given size.
979
+ *
980
+ * Proportional to the handle: a bigger grip on a short arm would have the circle
981
+ * overlapping the corner squares, and a smaller one on a long arm would look
982
+ * detached from the frame it turns.
983
+ * @param size - the handle's side, in figure pixels.
984
+ * @returns the distance from the top edge, in figure pixels.
985
+ */
986
+ function armFor(size) {
987
+ return size * (22 / 7);
988
+ }
989
+ /**
892
990
  * The line that ties the rotation handle to the box it turns.
893
991
  *
894
992
  * It reads as attached to the mark only because it is anchored on the top edge
@@ -897,11 +995,11 @@ window.__ModuleLoader__.load({
897
995
  * @param rotation - the mark's rotation in degrees.
898
996
  * @returns the two ends of the tether, from the turn handle to the top edge.
899
997
  */
900
- function tetherFor(box, rotation) {
998
+ function tetherFor(box, rotation, size = 7) {
901
999
  const centre = boxCentre(box);
902
1000
  const radians = rotation * Math.PI / 180;
903
1001
  return {
904
- from: rotateAround([centre[0], box.y - 22], centre, radians),
1002
+ from: rotateAround([centre[0], box.y - armFor(size)], centre, radians),
905
1003
  to: rotateAround([centre[0], box.y], centre, radians)
906
1004
  };
907
1005
  }
@@ -913,10 +1011,10 @@ window.__ModuleLoader__.load({
913
1011
  * @param reach - how far from a handle's centre still counts, in figure pixels.
914
1012
  * @returns the handle grabbed, or undefined for a press on the mark itself.
915
1013
  */
916
- function handleAt(box, rotation, at, reach = 7) {
1014
+ function handleAt(box, rotation, at, reach = 7, size = 7) {
917
1015
  let best;
918
1016
  let bestDistance = reach;
919
- for (const handle of handlesFor(box, rotation)) {
1017
+ for (const handle of handlesFor(box, rotation, size)) {
920
1018
  const distance = Math.hypot(handle.point[0] - at[0], handle.point[1] - at[1]);
921
1019
  if (distance <= bestDistance) {
922
1020
  best = handle.id;
@@ -993,7 +1091,6 @@ window.__ModuleLoader__.load({
993
1091
  const degrees = normalizeDegrees((Math.atan2(at[1] - centre[1], at[0] - centre[0]) + Math.PI / 2) * 180 / Math.PI);
994
1092
  return snap ? normalizeDegrees(Math.round(degrees / 15) * 15) : degrees;
995
1093
  }
996
- /** The cursor for one handle: what dragging it will do. */
997
1094
  const CURSORS = {
998
1095
  nw: "nwse-resize",
999
1096
  n: "ns-resize",
@@ -1003,7 +1100,7 @@ window.__ModuleLoader__.load({
1003
1100
  s: "ns-resize",
1004
1101
  sw: "nesw-resize",
1005
1102
  w: "ew-resize",
1006
- rotate: "grab"
1103
+ 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
1104
  };
1008
1105
  /**
1009
1106
  * The mouse cursor for one handle.
@@ -1083,6 +1180,20 @@ window.__ModuleLoader__.load({
1083
1180
  * every colour the palette offers.
1084
1181
  */
1085
1182
  const PIN_NUMBER_COLOR = "#ffffff";
1183
+ /**
1184
+ * Font size of the number inside a pin, in figure pixels.
1185
+ *
1186
+ * Scaled by the mark's weight as well as the figure: the rail that makes a pin bigger
1187
+ * makes its head bigger, and a head that grows around a fixed-size numeral reads as a
1188
+ * circle with a speck in it. The number is the pin's whole payload, so it keeps the
1189
+ * same share of the head at every size.
1190
+ * @param mark - the point mark whose pin is being drawn.
1191
+ * @param unit - the figure's size multiple.
1192
+ * @returns the font size to draw the number at.
1193
+ */
1194
+ function pinFontSizeOf(mark, unit = 1) {
1195
+ return 11 * (mark.weight ?? 1) * unit;
1196
+ }
1086
1197
  /** Normalize two opposite corners into a top-left origin and positive extents. */
1087
1198
  function bounds(first, second) {
1088
1199
  return {
@@ -1344,27 +1455,15 @@ window.__ModuleLoader__.load({
1344
1455
  const cy = box.y + box.height / 2;
1345
1456
  return `rotate(${n(radians * 180 / Math.PI)} ${n(cx)} ${n(cy)})`;
1346
1457
  }
1347
- //#endregion
1348
- //#region src/client/scale.ts
1349
1458
  /**
1350
- * How big the marks should be for the figure they are drawn on.
1351
- *
1352
- * A stroke is not a physical thing: 2.5 pixels is a bold line on a 300px thumbnail
1353
- * and a hairline on a 4000px scan. Marks are comparisons the reader makes against
1354
- * the line work underneath them, so they have to be sized in the same currency as
1355
- * the figure — a fraction of it — rather than in the figure's pixels.
1459
+ * Smallest multiple of the base sizes, for figures far below the reference.
1356
1460
  *
1357
- * The reference edge is the size a science figure is usually exported at. An
1358
- * ordinary figure then gets the unit it always had, and only the outliers (a
1359
- * thumbnail, a poster) are scaled. The range is clamped rather than proportional
1360
- * because a 20000px figure would otherwise get strokes 40px wide, which reads as a
1361
- * highlighter rather than a line.
1362
- * @module dsh-figmark/client/scale
1363
- */
1364
- /** The figure edge, in pixels, that gets the unit it always had. */
1365
- const REFERENCE_EDGE = 1200;
1366
- /** Smallest multiple of the base sizes, for figures far below the reference. */
1367
- const MIN_UNIT = .75;
1461
+ * Low enough that a matplotlib SVG — which measures itself in points, ~283 wide for
1462
+ * the same figure a PNG exports at 1179px — is still scaled down rather than propped
1463
+ * up to the old floor. The floor is a guard against division, not a design choice:
1464
+ * a figure below ~240px is one nothing sensible is drawn on.
1465
+ */
1466
+ const MIN_UNIT = .2;
1368
1467
  /**
1369
1468
  * The size everything drawn on a figure is multiplied by.
1370
1469
  *
@@ -1378,7 +1477,7 @@ window.__ModuleLoader__.load({
1378
1477
  function figureUnit(width, height) {
1379
1478
  const edge = Math.max(width, height);
1380
1479
  if (!Number.isFinite(edge) || edge <= 0) return 1;
1381
- return Math.min(3, Math.max(MIN_UNIT, edge / REFERENCE_EDGE));
1480
+ return Math.min(10, Math.max(MIN_UNIT, edge / 800));
1382
1481
  }
1383
1482
  //#endregion
1384
1483
  //#region src/client/Canvas.tsx
@@ -1409,7 +1508,8 @@ window.__ModuleLoader__.load({
1409
1508
  /** Minimum distance between recorded freehand samples, in figure pixels. */
1410
1509
  const PEN_SAMPLE_STEP = 3;
1411
1510
  /** Reach of a handle's hit area, in figure pixels: generous enough to grab. */
1412
- const HANDLE_REACH = 7 * 2.2;
1511
+ /** How far from a handle's centre a press still counts, as a multiple of its side. */
1512
+ const HANDLE_REACH = 2.2;
1413
1513
  /** Monotonic id source for marks created in this session. */
1414
1514
  let markSeq = 0;
1415
1515
  /**
@@ -1566,10 +1666,20 @@ window.__ModuleLoader__.load({
1566
1666
  * @param event - the pointer event.
1567
1667
  * @returns true when a handle took the press.
1568
1668
  */
1669
+ /**
1670
+ * The control frame's handle size in figure pixels.
1671
+ *
1672
+ * The frame is drawn in the same svg as the marks, which is in figure pixels, and
1673
+ * the whole paper is then scaled by CSS. A handle has to be a fixed number of screen
1674
+ * pixels to be hittable, so its figure size is that target divided by the scale.
1675
+ */
1676
+ const grip = 7 / Math.max(.05, props.scale ?? 1);
1677
+ /** A hairline that stays a hairline on screen, whatever the paper is scaled to. */
1678
+ const hairline = 1 / Math.max(.05, props.scale ?? 1);
1569
1679
  const beginHandle = (mark, event) => {
1570
1680
  if (props.selectedId !== mark.id) return false;
1571
1681
  if (mark.kind === "point") return false;
1572
- const grabbed = handleAt(controlBox(mark), mark.rotation ?? 0, pressedAt.current.value, HANDLE_REACH);
1682
+ const grabbed = handleAt(controlBox(mark), mark.rotation ?? 0, pressedAt.current.value, HANDLE_REACH * grip, grip);
1573
1683
  if (grabbed === void 0) return false;
1574
1684
  beginGesture(mark, event, grabbed);
1575
1685
  return true;
@@ -1800,26 +1910,26 @@ window.__ModuleLoader__.load({
1800
1910
  /** The resize and rotation handles of the selected mark. */
1801
1911
  const renderHandles = (mark) => {
1802
1912
  const frame = controlBox(mark);
1803
- const half = 7 / 2;
1804
- const handles = handlesFor(frame, mark.rotation ?? 0);
1805
- const tether = tetherFor(frame, mark.rotation ?? 0);
1913
+ const half = grip / 2;
1914
+ const handles = handlesFor(frame, mark.rotation ?? 0, grip);
1915
+ const tether = tetherFor(frame, mark.rotation ?? 0, grip);
1806
1916
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
1807
1917
  x1: tether.from[0],
1808
1918
  y1: tether.from[1],
1809
1919
  x2: tether.to[0],
1810
1920
  y2: tether.to[1],
1811
1921
  stroke: "#4c8dff",
1812
- strokeWidth: 1
1922
+ strokeWidth: hairline
1813
1923
  }), handles.map((handle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1814
1924
  "data-da-handle": handle.id,
1815
1925
  x: handle.point[0] - half,
1816
1926
  y: handle.point[1] - half,
1817
- width: 7,
1818
- height: 7,
1819
- rx: handle.id === "rotate" ? 7 / 2 : 1,
1927
+ width: grip,
1928
+ height: grip,
1929
+ rx: handle.id === "rotate" ? grip / 2 : grip / 7,
1820
1930
  fill: "#ffffff",
1821
1931
  stroke: "#4c8dff",
1822
- strokeWidth: 1.5,
1932
+ strokeWidth: 1.5 * hairline,
1823
1933
  style: {
1824
1934
  pointerEvents: "all",
1825
1935
  cursor: cursorFor(handle.id)
@@ -1867,7 +1977,7 @@ window.__ModuleLoader__.load({
1867
1977
  x: centre[0],
1868
1978
  y: centre[1],
1869
1979
  fontFamily: PIN_FONT_FAMILY,
1870
- fontSize: 11 * unit,
1980
+ fontSize: pinFontSizeOf(mark, unit),
1871
1981
  fontWeight: 700,
1872
1982
  fill: PIN_NUMBER_COLOR,
1873
1983
  textAnchor: "middle",
@@ -1888,16 +1998,17 @@ window.__ModuleLoader__.load({
1888
1998
  const box = pointMarkBox(shown, unit);
1889
1999
  /* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
1890
2000
  if (box === void 0) return null;
2001
+ const inset = 3 * hairline;
1891
2002
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1892
- x: box.x - 3 * unit,
1893
- y: box.y - 3 * unit,
1894
- width: box.width + 6 * unit,
1895
- height: box.height + 6 * unit,
1896
- rx: 3 * unit,
2003
+ x: box.x - inset,
2004
+ y: box.y - inset,
2005
+ width: box.width + 2 * inset,
2006
+ height: box.height + 2 * inset,
2007
+ rx: inset,
1897
2008
  fill: "none",
1898
2009
  stroke: "#4c8dff",
1899
- strokeWidth: unit,
1900
- strokeDasharray: `${3 * unit} ${2 * unit}`,
2010
+ strokeWidth: hairline,
2011
+ strokeDasharray: `${3 * hairline} ${2 * hairline}`,
1901
2012
  style: { pointerEvents: "none" }
1902
2013
  });
1903
2014
  };
@@ -1984,10 +2095,6 @@ window.__ModuleLoader__.load({
1984
2095
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1985
2096
  className: "da-overlay",
1986
2097
  "data-da-overlay": "",
1987
- style: {
1988
- left: originX,
1989
- top: originY
1990
- },
1991
2098
  width,
1992
2099
  height,
1993
2100
  viewBox: `0 0 ${width} ${height}`,
@@ -2267,14 +2374,13 @@ window.__ModuleLoader__.load({
2267
2374
  const pages = annotatedPages(document);
2268
2375
  const marked = pages.length === 0 ? "" : zh ? `其中 ${pages.length} 页有标注,` : `${pages.length} pages marked, `;
2269
2376
  const kind = rendered ? zh ? "【HTML 标注】" : "[HTML annotations]" : zh ? "【附图标注】" : "[figure annotations]";
2270
- const hash = zh ? `${rendered ? "文件" : "图"} sha256:${figure.sha256.slice(0, 12)}` : `sha256:${figure.sha256.slice(0, 12)}`;
2271
- const header = zh ? `${kind}${figure.path}(${scope === "" ? "" : `${scope},`}${size}${marked}标注 ${marks.length} 处,${hash})` : `${kind} ${figure.path} (${scope === "" ? "" : `${scope}, `}${size}${marked}${marks.length} marks, ${hash})`;
2377
+ const header = zh ? `${kind}${figure.path}(${scope === "" ? "" : `${scope},`}${size}${marked}标注 ${marks.length} 处)` : `${kind} ${figure.path} (${scope === "" ? "" : `${scope}, `}${size}${marked}${marks.length} marks)`;
2272
2378
  const paged = figure.pageCount !== void 0;
2273
2379
  const pictured = paths.reviewPath !== null;
2274
2380
  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."];
2275
2381
  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."] : [];
2276
2382
  const summary = document.summary !== void 0 && document.summary.trim() !== "" ? [zh ? `这张图是:${document.summary.trim()}` : `the figure shows: ${document.summary.trim()}`, ""] : [];
2277
- 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.";
2383
+ 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.";
2278
2384
  return [
2279
2385
  header,
2280
2386
  ...attachment,
@@ -2283,7 +2389,7 @@ window.__ModuleLoader__.load({
2283
2389
  ...summary,
2284
2390
  ...describeMarks(document, locale),
2285
2391
  "",
2286
- closing
2392
+ closer
2287
2393
  ].join("\n");
2288
2394
  }
2289
2395
  /**
@@ -2631,7 +2737,7 @@ window.__ModuleLoader__.load({
2631
2737
  const centre = mark.kind === "point" ? pinNumberCentre(mark, unit) : void 0;
2632
2738
  if (pin !== void 0 && centre !== void 0) {
2633
2739
  parts.push(`<path d="${pin}" fill="${mark.color}" stroke="#ffffff" stroke-width="${2 * unit}" stroke-linejoin="round"/>`);
2634
- if (number !== void 0) parts.push(`<text x="${centre[0]}" y="${centre[1]}" font-family="${PIN_FONT_FAMILY}" font-size="${11 * unit}" font-weight="700" fill="${PIN_NUMBER_COLOR}" text-anchor="middle" dominant-baseline="central">${number}</text>`);
2740
+ if (number !== void 0) parts.push(`<text x="${centre[0]}" y="${centre[1]}" font-family="${PIN_FONT_FAMILY}" font-size="${pinFontSizeOf(mark, unit)}" font-weight="700" fill="${PIN_NUMBER_COLOR}" text-anchor="middle" dominant-baseline="central">${number}</text>`);
2635
2741
  }
2636
2742
  return parts.join("");
2637
2743
  }
@@ -2670,7 +2776,25 @@ window.__ModuleLoader__.load({
2670
2776
  const unit = figureUnit(layer.width, layer.height);
2671
2777
  const paper = plotBounds(layer.width, layer.height, marks, unit);
2672
2778
  const shift = `translate(${-paper.x} ${-paper.y})`;
2673
- 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>";
2779
+ const edge = paper.x !== 0 || paper.y !== 0 || paper.width !== layer.width || paper.height !== layer.height ? figureEdge(layer.width, layer.height, unit) : "";
2780
+ 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>";
2781
+ }
2782
+ /** Round a stroke measurement to a tenth, as the path data does. */
2783
+ function hair9(value) {
2784
+ return (Math.round(value * 10) / 10).toString();
2785
+ }
2786
+ /**
2787
+ * The rule that marks where the figure ends and the grown paper begins.
2788
+ *
2789
+ * Drawn only when the paper grew, and drawn grey and dashed: it is a note about the
2790
+ * picture, not part of it, so it must not read as a mark the reader made.
2791
+ * @param width - the figure's width, in figure pixels.
2792
+ * @param height - the figure's height, in figure pixels.
2793
+ * @param unit - the figure's size multiple, so the rule is sized like the marks.
2794
+ * @returns an SVG rect, or an empty string when there is nothing to outline.
2795
+ */
2796
+ function figureEdge(width, height, unit) {
2797
+ 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)}"/>`;
2674
2798
  }
2675
2799
  /**
2676
2800
  * The size of the picture {@link composeReviewSvg} builds.
@@ -3239,10 +3363,27 @@ window.__ModuleLoader__.load({
3239
3363
  const STYLE_MARKER = "data-dsh-figmark";
3240
3364
  /** The stylesheet text. */
3241
3365
  const STYLES = `
3366
+ /* An accent of the plugin's own.
3367
+ The shell's --dsw-alias-brand-primary is not a brand colour in this theme — it
3368
+ resolves to the *ink* (near-black on light, near-white on dark), because the shell
3369
+ uses it for filled buttons in monochrome. Reading it as "the blue" turned this
3370
+ panel's sliders, filled buttons and selection rings into ink: a black knob on a
3371
+ white rail, and a wedge that goes solid black at the heaviest setting. The alias
3372
+ stays as the fallback so a shell that really does define a brand colour still wins,
3373
+ but the accent is named here so every rule reads one variable instead of quietly
3374
+ inheriting whatever "primary" means that week.
3375
+ The dark block re-states it, because a blue tuned for white paper is too dark on a
3376
+ near-black panel. */
3242
3377
  /* The panel follows the shell's palette rather than carrying one of its own: every
3243
3378
  colour below is an alias the theme owner resolves, with the inherited value as the
3244
3379
  fallback so a shell that defines nothing still renders something legible. */
3245
- .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)}
3380
+ .da-root{--da-accent:#176df2;--da-accent-soft:rgba(23,109,242,.14);
3381
+ 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)}
3382
+ /* The shell marks its dark palette on <body> rather than through prefers-color-scheme:
3383
+ a reader can pick either palette whatever the OS is set to, so following the shell's
3384
+ own switch is what keeps the accent readable in both. A blue tuned for white paper
3385
+ is too dark on a near-black panel. */
3386
+ body[data-ds-dark-theme] .da-root{--da-accent:#4c8dff;--da-accent-soft:rgba(76,141,255,.22)}
3246
3387
 
3247
3388
  /* Every small picture in the panel is one stroke set, declared once: without this
3248
3389
  an svg with no fill of its own falls back to a solid black shape, which is what
@@ -3260,12 +3401,20 @@ window.__ModuleLoader__.load({
3260
3401
  box-shadow:0 8px 26px rgba(10,14,24,.3),0 0 0 1px rgba(255,255,255,.08)}
3261
3402
  /* While it is being dragged the pointer is the grip, whatever it passes over. */
3262
3403
  .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)}
3263
- .da-palette[data-dragging="true"] *{cursor:grabbing !important}
3264
- /* The handle: a short row of dots over the tiles, the one part that only drags. */
3265
- .da-palette-grip{display:flex;align-items:center;justify-content:center;width:32px;height:14px;
3266
- border-radius:7px;cursor:grab;color:rgba(255,255,255,.55);touch-action:none;flex:0 0 auto}
3267
- .da-palette-grip:hover{color:rgba(255,255,255,.9);background:rgba(255,255,255,.1)}
3268
- .da-palette-grip-dots{font-size:9px;line-height:1;letter-spacing:1px}
3404
+ .da-palette[data-dragging="true"] *{cursor:move !important}
3405
+ /* The handle: a block of six dots, the one part of a strip that only drags. The block
3406
+ is turned to suit the strip — wider than tall on the bar, taller than wide on the
3407
+ palette — because a grip whose proportions fight its strip looks borrowed from the
3408
+ other one. */
3409
+ .da-palette-grip{display:flex;align-items:center;justify-content:center;width:26px;height:17px;
3410
+ border-radius:5px;cursor:move;color:rgba(255,255,255,.55);touch-action:none;flex:0 0 auto}
3411
+ .da-palette-grip:hover{color:rgba(255,255,255,.92);background:rgba(255,255,255,.1)}
3412
+ /* The dots' box follows the block: three across by two down needs width, two across
3413
+ by three down needs height. Sized per block rather than once, so the svg is not
3414
+ letterboxed inside its own handle. */
3415
+ .da-grip-dots{width:12px;height:8px;fill:currentColor;display:block}
3416
+ .da-bar .da-palette-grip{width:16px;height:18px}
3417
+ .da-bar .da-grip-dots{width:8px;height:12px}
3269
3418
  /* A tile is a round button holding one white glyph. No word under it: six glyphs in
3270
3419
  a column are read at a glance, and the word is still there as the tooltip and as
3271
3420
  the accessible name. */
@@ -3287,38 +3436,42 @@ window.__ModuleLoader__.load({
3287
3436
  drawing. The drawn parts never take the pointer, or grabbing the knob would grab
3288
3437
  the knob instead of the slider. */
3289
3438
  .da-weight{position:absolute;z-index:7;left:6px;top:50%;transform:translateY(-50%);
3439
+ --da-track-inset:10px;--da-knob-size:11px;
3290
3440
  display:flex;align-items:center;justify-content:center;width:7px;height:200px;
3291
3441
  border-radius:999px;background:var(--dsw-alias-border-l2,rgba(127,127,127,.22));
3292
3442
  cursor:ns-resize;transition:width .16s ease,background .16s ease,box-shadow .16s ease}
3293
3443
  .da-weight:hover,.da-weight:focus-within{width:30px;background:var(--dsw-alias-bg-layer-2,#fff);
3294
3444
  box-shadow:0 5px 18px rgba(20,40,80,.16),0 0 0 0.5px var(--dsw-alias-border-l2,currentColor)}
3295
- /* The wedge: a hairline at the bottom, the heaviest width at the top. Drawn as a
3296
- clipped gradient box so it costs one element and no SVG. */
3297
- .da-weight::before{content:"";position:absolute;left:50%;top:14px;bottom:14px;width:11px;
3298
- transform:translateX(-50%);opacity:0;
3299
- background:linear-gradient(to top,var(--dsw-alias-border-l3,rgba(127,127,127,.34)),
3300
- var(--dsw-alias-border-l4,rgba(127,127,127,.5)));
3301
- clip-path:polygon(calc(50% - 0.5px) 100%,calc(50% + 0.5px) 100%,
3302
- calc(50% + 5.5px) 0,calc(50% - 5.5px) 0);
3445
+ /* The track is an svg in the body: a hairline at the bottom that opens into a rounded
3446
+ shoulder at the top. It is not a clip-path polygon because a polygon has no rounded
3447
+ ends — the corners were what made the wedge read as a cut-out blade rather than a
3448
+ part of the panel. preserveAspectRatio="none" stretches one 18x100 shape over the
3449
+ rail's height, so the shoulders stay smooth at any length.
3450
+ The height is written out rather than expressed as top+bottom: an svg is a replaced
3451
+ element: with a viewBox it takes its intrinsic height for its width and ignores the
3452
+ bottom offset entirely. The track was therefore a fixed 100px tall while the knob
3453
+ travelled the whole 200px rail, which is why the two could never line up. */
3454
+ .da-weight-svg{position:absolute;left:50%;top:var(--da-track-inset);height:calc(100% - 2 * var(--da-track-inset));
3455
+ width:18px;transform:translateX(-50%);opacity:0;pointer-events:none;
3303
3456
  transition:opacity .16s ease}
3304
- .da-weight:hover::before,.da-weight:focus-within::before{opacity:1}
3305
- /* The part already chosen, drawn from the bottom up in the brand colour. */
3306
- .da-weight-fill{position:absolute;left:50%;bottom:14px;width:11px;transform:translateX(-50%);
3307
- opacity:0;pointer-events:none;transition:opacity .16s ease;
3308
- background:var(--dsw-alias-brand-primary,#176df2);
3309
- clip-path:polygon(calc(50% - 0.5px) 100%,calc(50% + 0.5px) 100%,
3310
- calc(50% + 5.5px) 0,calc(50% - 5.5px) 0)}
3311
- .da-weight:hover .da-weight-fill,.da-weight:focus-within .da-weight-fill{opacity:1}
3457
+ .da-weight:hover .da-weight-svg,.da-weight:focus-within .da-weight-svg{opacity:1}
3312
3458
  /* The knob sits on the value: the only part that shows at rest, so the rail reads as
3313
3459
  a slider waiting to be touched rather than a bare rule. */
3314
- .da-weight-knob{position:absolute;left:50%;width:11px;height:11px;border-radius:50%;
3315
- transform:translateX(-50%);pointer-events:none;
3460
+ /* The blue: a bar of the same width as the groove, its height the one number the
3461
+ knob also uses, so its top edge is the knob's centre at every weight. */
3462
+ .da-weight-fill{position:absolute;left:50%;bottom:var(--da-track-inset);width:5px;
3463
+ transform:translateX(-50%);border-radius:999px;background:var(--da-accent,#176df2);
3464
+ opacity:0;pointer-events:none;transition:opacity .16s ease}
3465
+ .da-weight:hover .da-weight-fill,.da-weight:focus-within .da-weight-fill{opacity:1}
3466
+ .da-weight-knob{position:absolute;left:50%;width:var(--da-knob-size,11px);height:var(--da-knob-size,11px);
3467
+ border-radius:50%;transform:translateX(-50%);pointer-events:none;
3316
3468
  background:var(--dsw-alias-bg-layer-2,#fff);
3317
- box-shadow:0 1px 4px rgba(20,40,80,.32),0 0 0 1px var(--dsw-alias-border-l3,currentColor);
3469
+ box-shadow:0 1px 4px rgba(20,40,80,.28),0 0 0 1px var(--dsw-alias-border-l3,currentColor);
3318
3470
  transition:width .16s ease,height .16s ease,background .16s ease}
3471
+ .da-weight:hover,.da-weight:focus-within{--da-knob-size:15px}
3319
3472
  .da-weight:hover .da-weight-knob,.da-weight:focus-within .da-weight-knob{
3320
- width:15px;height:15px;background:var(--dsw-alias-brand-primary,#176df2);
3321
- box-shadow:0 2px 7px rgba(20,40,80,.34)}
3473
+ background:var(--da-accent,#176df2);
3474
+ box-shadow:0 2px 7px rgba(20,40,80,.3)}
3322
3475
  /* The browser's own range, rotated upright and made transparent. It spans the whole
3323
3476
  rail so a press anywhere on it lands on the slider — including on the knob, which
3324
3477
  is why the knob is pointer-transparent. */
@@ -3339,7 +3492,7 @@ window.__ModuleLoader__.load({
3339
3492
  it centred while untouched must not also apply — with both set it stretches. */
3340
3493
  .da-bar[data-parked="true"]{bottom:auto}
3341
3494
  .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)}
3342
- .da-bar[data-dragging="true"] *{cursor:grabbing !important}
3495
+ .da-bar[data-dragging="true"] *{cursor:move !important}
3343
3496
  /* The bar's buttons are white glyphs on the dark strip. The one filled control is
3344
3497
  still the way out. */
3345
3498
  .da-bar .da-dock-btn{color:#fff}
@@ -3380,8 +3533,8 @@ window.__ModuleLoader__.load({
3380
3533
  .da-tool .da-tool-label{font-size:10px;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary,inherit)}
3381
3534
  /* The armed tool is the one state in this row the reader must never hunt for: it is
3382
3535
  the only tile with a fill, and the only label in the brand colour. */
3383
- .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)}
3384
- .da-tool[aria-pressed="true"] .da-tool-label{color:var(--dsw-alias-brand-primary,currentColor);font-weight:600}
3536
+ .da-tool[aria-pressed="true"]{border-color:transparent;background:var(--da-accent-soft,rgba(23,109,242,.1));color:var(--da-accent,currentColor)}
3537
+ .da-tool[aria-pressed="true"] .da-tool-label{color:var(--da-accent,currentColor);font-weight:600}
3385
3538
  .da-tool[aria-pressed="true"] svg{stroke-width:2.1}
3386
3539
  .da-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
3387
3540
  .da-btn-icon{width:30px;padding:0}
@@ -3392,6 +3545,10 @@ window.__ModuleLoader__.load({
3392
3545
  .da-stylewrap{position:relative;flex:0 0 auto}
3393
3546
  /* The style fold floats above the bar rather than taking a row of it: opened over
3394
3547
  the figure, it costs the picture nothing and lands where the eye already is. */
3548
+ /* Opens upwards by default, because the bar lives at the bottom. Parked at the top
3549
+ there is nothing above it to open into, so the fold turns over and hangs below its
3550
+ own button instead — decided by the same measurement the records use. */
3551
+ .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)}
3395
3552
  .da-style-row{position:absolute;left:8px;bottom:calc(100% + 6px);z-index:9;
3396
3553
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;max-width:calc(100% - 16px);
3397
3554
  padding:8px 10px;border:0.5px solid var(--dsw-alias-border-l2,currentColor);border-radius:11px;
@@ -3406,17 +3563,24 @@ window.__ModuleLoader__.load({
3406
3563
  /* Anywhere else is "done here": the panel closes on the click that lands outside it
3407
3564
  rather than trapping the reader in a popup with one way out. */
3408
3565
  .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}
3409
- .da-swatch[aria-pressed="true"]{box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,currentColor)}
3566
+ .da-swatch[aria-pressed="true"]{box-shadow:0 0 0 2px var(--da-accent,currentColor)}
3410
3567
  /* --- the figure on its table -------------------------------------------- */
3411
3568
  /* A figure is a sheet on a table, not a picture floating in a void: the white paper
3412
3569
  is what gives the panel a middle, and the gap around it is the margin the eye
3413
3570
  needs to see where the drawing stops. */
3414
3571
  .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)}
3415
- .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))}
3416
- /* The paper: the figure plus the margin a mark may have needed. It is the white sheet
3417
- the drawing sits on, so a mark that reaches past the figure lands on paper rather
3418
- than on the panel behind it. */
3419
- .da-paper{background:#fff}
3572
+ .da-surface{position:relative;flex:0 0 auto;margin:0;transform-origin:top left}
3573
+ /* The paper: the figure plus the margin a mark may have needed. It is deliberately
3574
+ left transparent — the white sheet belongs to the figure, so a margin that only
3575
+ exists because a mark reached past the edge reads as board round a card rather than
3576
+ as more of the figure. The exported picture still gets its white fill: that one is
3577
+ looked at by someone who has no board behind it. */
3578
+ .da-paper{background:transparent}
3579
+ /* The figure is the sheet: white, rounded, lifted off the panel. This is the element
3580
+ that carries the paper look, which is why the margin can be see-through without the
3581
+ drawing losing its edge. */
3582
+ .da-figure-frame{background:var(--dsw-alias-bg-base,#fff);border-radius:4px;overflow:hidden;
3583
+ box-shadow:var(--dsw-elevation-prominent,0 1px 3px rgba(0,0,0,.14),0 8px 24px rgba(0,0,0,.08))}
3420
3584
  .da-figure{display:block;user-select:none;-webkit-user-drag:none}
3421
3585
  /* The table is a thing you can push. The hand only shows under the tools that
3422
3586
  leave a drag free to move the view: under a drawing tool the same drag draws. */
@@ -3449,22 +3613,22 @@ window.__ModuleLoader__.load({
3449
3613
  width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;
3450
3614
  color:inherit;cursor:pointer;flex:0 0 auto}
3451
3615
  .da-dock-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
3452
- .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)}
3616
+ .da-dock-btn[aria-pressed="true"]{background:var(--da-accent-soft,rgba(23,109,242,.12));color:var(--da-accent,#176df2)}
3453
3617
  .da-dock-btn svg{width:15px;height:15px}
3454
3618
  .da-dock-btn:disabled{opacity:.4;cursor:default}
3455
3619
  .da-dock-btn:disabled:hover{background:transparent}
3456
3620
  /* The one filled control in the panel is the way out of it. */
3457
- .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)}
3458
- .da-dock-btn-primary:hover{opacity:.92;background:var(--dsw-alias-brand-primary,#176df2)}
3621
+ .da-dock-btn-primary{border-color:var(--da-accent,#176df2);background:var(--da-accent,#176df2);color:#fff;box-shadow:0 2px 8px rgba(23,109,242,.32)}
3622
+ .da-dock-btn-primary:hover{opacity:.92;background:var(--da-accent,#176df2)}
3459
3623
  /* A dot, not a count: "there is a goal written" is all the bar needs to say about
3460
3624
  a field it is not a list of. */
3461
3625
  .da-dock-dot{position:absolute;top:3px;right:4px;width:5px;height:5px;border-radius:50%;
3462
- background:var(--dsw-alias-brand-primary,#176df2)}
3626
+ background:var(--da-accent,#176df2)}
3463
3627
  .da-dock-sep{width:1px;height:16px;margin:0 3px;background:var(--dsw-alias-border-l3,currentColor);opacity:.6;flex:0 0 auto}
3464
3628
  /* The count rides on the list's own button: it is the one thing about the list a
3465
3629
  reader wants without opening it. */
3466
3630
  .da-dock-count{position:absolute;top:-1px;right:-2px;min-width:14px;height:14px;padding:0 3px;
3467
- border-radius:7px;background:var(--dsw-alias-brand-primary,#176df2);color:#fff;
3631
+ border-radius:7px;background:var(--da-accent,#176df2);color:#fff;
3468
3632
  font-size:9px;font-weight:700;line-height:14px;text-align:center;font-variant-numeric:tabular-nums}
3469
3633
 
3470
3634
  /* --- the floating records ------------------------------------------------ */
@@ -3476,7 +3640,7 @@ window.__ModuleLoader__.load({
3476
3640
  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)}
3477
3641
  .da-card-title{display:flex;align-items:center;gap:5px;min-width:0;font-size:12px;font-weight:650;
3478
3642
  color:var(--dsw-alias-label-primary,inherit)}
3479
- .da-card-title svg{width:14px;height:14px;flex:0 0 auto;color:var(--dsw-alias-brand-primary,#176df2)}
3643
+ .da-card-title svg{width:14px;height:14px;flex:0 0 auto;color:var(--da-accent,#176df2)}
3480
3644
 
3481
3645
  .da-float-head{display:flex;align-items:center;gap:7px}
3482
3646
  .da-float-head .da-card-title{flex:1 1 auto;min-width:0}
@@ -3491,7 +3655,7 @@ window.__ModuleLoader__.load({
3491
3655
  come from the mark it belongs to. It is a pill rather than a card — no head, no
3492
3656
  border between the field and the panel — so writing about a mark costs one line
3493
3657
  over the figure instead of a box that covers it. */
3494
- .da-float-detail{width:300px;gap:0;padding:4px;border-radius:22px;
3658
+ .da-float-detail{width:300px;max-width:calc(100% - 16px);gap:0;padding:4px;border-radius:22px;
3495
3659
  flex-direction:row;align-items:flex-end;
3496
3660
  box-shadow:0 8px 26px rgba(20,40,80,.2),0 0 0 1px rgba(20,40,80,.04)}
3497
3661
  .da-float .da-comment{flex:1 1 auto;min-width:0;box-sizing:border-box;border:0;background:transparent;
@@ -3515,10 +3679,10 @@ window.__ModuleLoader__.load({
3515
3679
  color:var(--dsw-alias-label-primary,currentColor)}
3516
3680
  .da-comment-send-btn:disabled{opacity:.45;cursor:default}
3517
3681
  .da-comment-send-btn svg{width:15px;height:15px}
3518
- .da-comment-send-primary{background:var(--dsw-alias-brand-primary,#176df2);color:#fff}
3519
- .da-comment-send-primary:hover{background:var(--dsw-alias-brand-primary,#176df2);color:#fff;filter:brightness(1.08)}
3682
+ .da-comment-send-primary{background:var(--da-accent,#176df2);color:#fff}
3683
+ .da-comment-send-primary:hover{background:var(--da-accent,#176df2);color:#fff;filter:brightness(1.08)}
3520
3684
  .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}
3521
- .da-float textarea:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#176df2)}
3685
+ .da-float textarea:focus{outline:none;border-color:var(--da-accent,#176df2)}
3522
3686
  .da-btn-count{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;
3523
3687
  padding:0 4px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));
3524
3688
  font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}
@@ -3531,8 +3695,8 @@ window.__ModuleLoader__.load({
3531
3695
  /* What the selected mark is, in one word: the detail field is disabled until there
3532
3696
  is something to describe, and this says what it would describe. */
3533
3697
  .da-chip{display:inline-flex;align-items:center;height:16px;padding:0 6px;border-radius:5px;
3534
- background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.12));
3535
- color:var(--dsw-alias-brand-primary,#176df2);font-size:10px;font-weight:600}
3698
+ background:var(--da-accent-soft,rgba(23,109,242,.12));
3699
+ color:var(--da-accent,#176df2);font-size:10px;font-weight:600}
3536
3700
  /* The "?" beside a card's name: the long form of what that field is for. */
3537
3701
  .da-hintdot{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;
3538
3702
  border-radius:50%;border:0.5px solid var(--dsw-alias-border-l3,currentColor);
@@ -3545,7 +3709,7 @@ window.__ModuleLoader__.load({
3545
3709
  .da-mark{display:flex;align-items:center;gap:7px;padding:5px 6px;border-radius:8px;
3546
3710
  border:0.5px solid transparent;min-width:0}
3547
3711
  .da-mark:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
3548
- .da-mark[data-selected="true"]{border-color:var(--dsw-alias-brand-primary,#176df2);
3712
+ .da-mark[data-selected="true"]{border-color:var(--da-accent,#176df2);
3549
3713
  background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.08))}
3550
3714
  .da-mark-index{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
3551
3715
  width:20px;height:20px;border-radius:6px;color:#fff;font-size:11px;font-weight:700;
@@ -3814,6 +3978,39 @@ window.__ModuleLoader__.load({
3814
3978
  "#2f9e44"
3815
3979
  ];
3816
3980
  /** Tools in toolbar order. */
3981
+ /**
3982
+ * The rail's track, drawn on an 18x100 grid and stretched to whatever height it gets.
3983
+ *
3984
+ * The control's subject is thickness, so the track says which way is heavier: a little
3985
+ * narrower at the bottom, about one part in six. "A little" is the whole note — a
3986
+ * water-drop taper reads as decoration rather than as a gauge, and the reader said so
3987
+ * twice. One cubic per side and no shoulders, so each flank is a single continuous
3988
+ * curve from end to end with nothing for the eye to catch on; the two ends close with
3989
+ * faint arcs so the track looks drawn rather than cut off.
3990
+ *
3991
+ * Stretching it vertically (preserveAspectRatio="none") is what makes the caps need
3992
+ * care: the svg is 18x100 over a box about 180 tall, so one y unit is 1.8 of an x unit.
3993
+ * An `r` that reads as a circle in the viewBox would be a tall ellipse on screen — the
3994
+ * cap's `ry` is therefore set to `rx / 1.8`, which is what puts a round end on it.
3995
+ */
3996
+ const WEIGHT_TRACK = [
3997
+ "M 6.9 98.9",
3998
+ "A 2.1 1.15 0 0 0 11.1 98.9",
3999
+ "C 11.4 66 11.9 33 12.2 1.1",
4000
+ "A 3.2 1.15 0 0 0 5.8 1.1",
4001
+ "C 6.1 33 6.6 66 6.9 98.9",
4002
+ "Z"
4003
+ ].join(" ");
4004
+ /**
4005
+ * How far the blue reaches, as a `height` in the rail.
4006
+ *
4007
+ * The same expression as the knob's `bottom`, because the blue has to end exactly where
4008
+ * the knob's centre is: written once as a function so a change to the travel cannot
4009
+ * leave the fill behind. `inset` keeps both clear of the rail's rounded caps; the extra
4010
+ * half knob at each end keeps the knob's whole circle between the two ends of the bar
4011
+ * rather than hanging half of itself past the top when the pen is at its heaviest.
4012
+ */
4013
+ const travelHeight = (fraction) => `calc(var(--da-track-inset) + var(--da-knob-size) / 2 + (100% - 2 * var(--da-track-inset) - var(--da-knob-size)) * ${fraction})`;
3817
4014
  const TOOLS = [
3818
4015
  {
3819
4016
  tool: "select",
@@ -3870,21 +4067,61 @@ window.__ModuleLoader__.load({
3870
4067
  * with the mouse rather than a mark you leave behind. Drawn rather than written so
3871
4068
  * the toolbar stays one row of controls at any locale or font size.
3872
4069
  */
4070
+ /**
4071
+ * The handle's dots: six of them, three by two, turned as the reader asked.
4072
+ *
4073
+ * The palette stands up and takes a grip three columns wide by two rows tall; the bar
4074
+ * lies down and takes one two columns wide by three rows tall. That is the pair the
4075
+ * reader called for, and it is the opposite of what the strips' own proportions would
4076
+ * suggest — noted here so it is not "corrected" back by the next pair of eyes.
4077
+ *
4078
+ * Drawn instead of typed — the braille character this replaces was one glyph at one
4079
+ * size, so the dots could not be sized or spaced, and at 9px they came out a smudge.
4080
+ */
4081
+ function GripDots({ wide }) {
4082
+ const xs = wide ? [
4083
+ 2,
4084
+ 6,
4085
+ 10
4086
+ ] : [2, 6];
4087
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4088
+ className: "da-grip-dots",
4089
+ viewBox: wide ? "0 0 12 8" : "0 0 8 12",
4090
+ "aria-hidden": "true",
4091
+ focusable: "false",
4092
+ children: (wide ? [2, 6] : [
4093
+ 2,
4094
+ 6,
4095
+ 10
4096
+ ]).map((y) => xs.map((x) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4097
+ cx: x,
4098
+ cy: y,
4099
+ r: 1.4
4100
+ }, `${x}-${y}`)))
4101
+ });
4102
+ }
4103
+ /**
4104
+ * The select tool: a cursor, outlined rather than filled.
4105
+ *
4106
+ * Filled, it read as a black blob against the dark chip — the silhouette was there but
4107
+ * the direction it points was not, which is the only thing a cursor has to say. As an
4108
+ * outline it belongs to the same stroke set as the other five glyphs.
4109
+ *
4110
+ * The shape is Tabler's `cursor-text`-sibling `pointer` glyph, taken whole at its own
4111
+ * 24 grid: its body is one closed curve with round joins rather than the seven hard
4112
+ * corners a hand-traced outline has, and at 16px that softness is the whole difference
4113
+ * between a cursor and a shard. Scaled by hand it lost exactly that.
4114
+ */
3873
4115
  function PointerIcon() {
3874
4116
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3875
- width: "13",
3876
- height: "15",
3877
- viewBox: "0 0 13 15",
4117
+ className: "da-ico",
4118
+ viewBox: "0 0 24 24",
3878
4119
  "aria-hidden": "true",
3879
4120
  focusable: "false",
3880
- style: { display: "block" },
3881
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
3882
- 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",
3883
- fill: "currentColor",
3884
- stroke: "currentColor",
3885
- strokeWidth: "0.9",
3886
- strokeLinejoin: "round"
3887
- })
4121
+ strokeWidth: "2",
4122
+ strokeLinecap: "round",
4123
+ strokeLinejoin: "round",
4124
+ 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" })
3888
4125
  });
3889
4126
  }
3890
4127
  /**
@@ -4063,12 +4300,31 @@ window.__ModuleLoader__.load({
4063
4300
  const isVector = mediaType === "image/svg+xml";
4064
4301
  const t = (0, react.useCallback)((key) => resolveTranslate(props, key), [props.t, props.localeId]);
4065
4302
  const sessionId = props.sessionId ?? parsedAddress?.sessionId;
4303
+ /**
4304
+ * A private prefix for the rail's gradient and clip ids.
4305
+ *
4306
+ * Two bodies can be mounted at once (a PDF lays out one per page), and an id is
4307
+ * document-wide: sharing one would make every rail's fill follow the first rail's
4308
+ * selection. Declared here, with the other hooks, because the ids are read far below
4309
+ * the early returns for a figure that failed to load.
4310
+ */
4311
+ const trackId = `da-weight-${(0, react.useId)().replaceAll(":", "")}`;
4066
4312
  const stageRef = (0, react.useRef)(null);
4067
4313
  /** The box a double clicked mark's detail is written in. */
4068
4314
  const detailRef = (0, react.useRef)(null);
4315
+ /**
4316
+ * The comment pill around that box.
4317
+ *
4318
+ * Measured rather than the textarea inside it: the field gives up width to the two
4319
+ * hand-off buttons as soon as there is something to send, so a placement computed
4320
+ * from the field's box would put the buttons past the edge of the panel — which is
4321
+ * exactly what a comment pill against the right edge used to do.
4322
+ */
4323
+ const pillRef = (0, react.useRef)(null);
4069
4324
  const barRef = (0, react.useRef)(null);
4070
4325
  const figureHostRef = (0, react.useRef)(null);
4071
4326
  const surfaceRef = (0, react.useRef)(null);
4327
+ const paperRef = (0, react.useRef)(null);
4072
4328
  const figureRef = (0, react.useRef)(null);
4073
4329
  /**
4074
4330
  * Hand the stage to the owner as its scrollport, and keep it here too.
@@ -4610,6 +4866,17 @@ window.__ModuleLoader__.load({
4610
4866
  htmlHeight
4611
4867
  ]);
4612
4868
  /**
4869
+ * Whether an upward fold would come out of the top of the panel.
4870
+ *
4871
+ * The style fold is anchored to its own button, so it climbs — which is right while
4872
+ * the bar sits at the bottom and wrong the moment the reader parks the bar along the
4873
+ * top, where climbing puts the swatches out of sight above the panel. The record
4874
+ * placement already answers this question for the records; the fold reads the same
4875
+ * answer rather than measuring a second time, so the two can never disagree about
4876
+ * where there is room.
4877
+ */
4878
+ const foldOpensDown = floatsAt !== null && floatsAt.bottom === void 0;
4879
+ /**
4613
4880
  * Put a record away when the reader clicks anywhere else.
4614
4881
  *
4615
4882
  * A record is a fold, not a mode: it has done its job the moment the reader looks
@@ -4653,7 +4920,7 @@ window.__ModuleLoader__.load({
4653
4920
  }
4654
4921
  const panel = root.getBoundingClientRect();
4655
4922
  const measured = host.getBoundingClientRect();
4656
- const pillSize = measuredSize(detailRef.current, {
4923
+ const pillSize = measuredSize(pillRef.current, {
4657
4924
  width: 300,
4658
4925
  height: 48
4659
4926
  });
@@ -4707,9 +4974,9 @@ window.__ModuleLoader__.load({
4707
4974
  const zoomAt = (0, react.useCallback)((next, clientX, clientY) => {
4708
4975
  const stage = stageRef.current;
4709
4976
  const surface = surfaceRef.current;
4710
- const figureHost = figureRef.current;
4977
+ const sheet = paperRef.current;
4711
4978
  /* v8 ignore next -- a wheel event only reaches a handler on a mounted stage, and size is what allows one. */
4712
- if (stage === null || surface === null || figureHost === null || size === null) return;
4979
+ if (stage === null || surface === null || sheet === null || paper === null) return;
4713
4980
  const before = liveScale.current;
4714
4981
  const reached = clampZoom(next);
4715
4982
  if (reached === before) return;
@@ -4723,19 +4990,22 @@ window.__ModuleLoader__.load({
4723
4990
  }, {
4724
4991
  clientX,
4725
4992
  clientY
4726
- }, before, reached, size);
4993
+ }, before, reached, paper);
4727
4994
  liveScale.current = reached;
4728
- surface.style.width = `${size.width * reached}px`;
4729
- surface.style.height = `${size.height * reached}px`;
4730
- figureHost.style.transform = `scale(${reached})`;
4995
+ surface.style.width = `${paper.width * reached}px`;
4996
+ surface.style.height = `${paper.height * reached}px`;
4997
+ sheet.style.transform = `scale(${reached})`;
4731
4998
  stage.scrollLeft = target.scrollLeft;
4732
4999
  stage.scrollTop = target.scrollTop;
4733
5000
  clearTimeout(settleTimer.current);
4734
5001
  settleTimer.current = setTimeout(() => {
4735
5002
  settleTimer.current = void 0;
5003
+ const settled = paperRef.current;
5004
+ /* v8 ignore next -- the sheet is the element the pinch was measured on. */
5005
+ if (settled !== null) settled.style.transform = "";
4736
5006
  setZoom(reached);
4737
5007
  }, 120);
4738
- }, [size]);
5008
+ }, [paper]);
4739
5009
  (0, react.useEffect)(() => () => {
4740
5010
  clearTimeout(settleTimer.current);
4741
5011
  }, []);
@@ -5231,6 +5501,13 @@ window.__ModuleLoader__.load({
5231
5501
  */
5232
5502
  const shownWeight = selected === null ? WEIGHT_TOOLS.has(tool) ? weight : null : selected.weight ?? 1;
5233
5503
  /**
5504
+ * How far up the rail that value sits, from 0 at the lightest to 1 at the heaviest.
5505
+ *
5506
+ * One number for the fill, the knob and the input: computed three times the three
5507
+ * could round differently and the knob would sit a pixel off the fill it belongs to.
5508
+ */
5509
+ const weightFraction = shownWeight === null ? 0 : (shownWeight - WEIGHT_MIN) / (8 - WEIGHT_MIN);
5510
+ /**
5234
5511
  * Change one attribute of the selected mark, or of the next stroke when nothing
5235
5512
  * is selected.
5236
5513
  *
@@ -5314,13 +5591,15 @@ window.__ModuleLoader__.load({
5314
5591
  className: "da-surface",
5315
5592
  ref: surfaceRef,
5316
5593
  style: surfaceStyle,
5317
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5594
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5318
5595
  className: "da-paper",
5596
+ ref: paperRef,
5319
5597
  style: paperStyle,
5320
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5599
+ className: "da-figure-frame",
5321
5600
  ref: figureRef,
5322
5601
  style: figureStyle,
5323
- children: [figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5602
+ children: figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5324
5603
  ref: figureHostRef,
5325
5604
  dangerouslySetInnerHTML: { __html: figure.markup }
5326
5605
  }) : figure?.kind === "raster" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
@@ -5345,36 +5624,37 @@ window.__ModuleLoader__.load({
5345
5624
  width: figure.width,
5346
5625
  height: htmlHeight
5347
5626
  }
5348
- }) : null, paper === null || size === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
5349
- width: paper.width,
5350
- height: paper.height,
5351
- origin: [-paper.x, -paper.y],
5352
- marks,
5353
- tool,
5354
- color,
5355
- weight,
5356
- containerRef: figureHostRef,
5357
- anchoring: isVector,
5358
- anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
5359
- selectedId,
5360
- onSelect: setSelectedId,
5361
- onAdd: addMark,
5362
- onMove: moveMark,
5363
- onUpdate: updateMark,
5364
- onRemove: removeMark,
5365
- onPan: panBy,
5366
- onPanStart: () => {
5367
- setPanning(true);
5368
- },
5369
- onPanEnd: () => {
5370
- setPanning(false);
5371
- },
5372
- onEdit: editMark,
5373
- onDrawn: finishDrawing,
5374
- onLiveEdit: setLiveMark,
5375
- numbering: props.numbering
5376
- })]
5377
- })
5627
+ }) : null
5628
+ }), paper === null || size === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
5629
+ width: paper.width,
5630
+ height: paper.height,
5631
+ origin: [-paper.x, -paper.y],
5632
+ marks,
5633
+ tool,
5634
+ color,
5635
+ weight,
5636
+ scale,
5637
+ containerRef: figureHostRef,
5638
+ anchoring: isVector,
5639
+ anchorAtFigurePoint: figure?.kind === "html" ? anchorAtFigurePoint : void 0,
5640
+ selectedId,
5641
+ onSelect: setSelectedId,
5642
+ onAdd: addMark,
5643
+ onMove: moveMark,
5644
+ onUpdate: updateMark,
5645
+ onRemove: removeMark,
5646
+ onPan: panBy,
5647
+ onPanStart: () => {
5648
+ setPanning(true);
5649
+ },
5650
+ onPanEnd: () => {
5651
+ setPanning(false);
5652
+ },
5653
+ onEdit: editMark,
5654
+ onDrawn: finishDrawing,
5655
+ onLiveEdit: setLiveMark,
5656
+ numbering: props.numbering
5657
+ })]
5378
5658
  })
5379
5659
  })
5380
5660
  }),
@@ -5518,6 +5798,7 @@ window.__ModuleLoader__.load({
5518
5798
  className: "da-float da-float-detail",
5519
5799
  role: "group",
5520
5800
  "aria-label": t("detail"),
5801
+ ref: pillRef,
5521
5802
  style: {
5522
5803
  left: detailAt.left,
5523
5804
  top: detailAt.top
@@ -5565,22 +5846,52 @@ window.__ModuleLoader__.load({
5565
5846
  className: "da-weight-input",
5566
5847
  "aria-label": t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
5567
5848
  min: WEIGHT_MIN,
5568
- max: 4,
5849
+ max: 8,
5569
5850
  step: .1,
5570
5851
  value: shownWeight,
5571
5852
  onChange: (event) => {
5572
5853
  chooseWeight(Number(event.target.value));
5573
5854
  }
5574
5855
  }),
5856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
5857
+ className: "da-weight-svg",
5858
+ viewBox: "0 0 18 100",
5859
+ preserveAspectRatio: "none",
5860
+ "aria-hidden": "true",
5861
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
5862
+ id: `${trackId}-shade`,
5863
+ x1: "0",
5864
+ y1: "1",
5865
+ x2: "0",
5866
+ y2: "0",
5867
+ children: [
5868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
5869
+ offset: "0",
5870
+ stopColor: "rgba(20,40,80,.06)"
5871
+ }),
5872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
5873
+ offset: ".55",
5874
+ stopColor: "rgba(20,40,80,.13)"
5875
+ }),
5876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
5877
+ offset: "1",
5878
+ stopColor: "rgba(20,40,80,.24)"
5879
+ })
5880
+ ]
5881
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
5882
+ d: WEIGHT_TRACK,
5883
+ fill: `url(#${trackId}-shade)`
5884
+ })]
5885
+ }),
5575
5886
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5576
5887
  className: "da-weight-fill",
5577
5888
  "aria-hidden": true,
5578
- style: { height: `${(shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN) * 100}%` }
5889
+ style: { height: travelHeight(weightFraction) }
5579
5890
  }),
5580
5891
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5581
5892
  className: "da-weight-knob",
5582
5893
  "aria-hidden": true,
5583
- style: { bottom: `calc(${(shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN) * 100}% - 5.5px)` }
5894
+ style: { bottom: `calc(${travelHeight(weightFraction)} - var(--da-knob-size) / 2)` }
5584
5895
  })
5585
5896
  ]
5586
5897
  }),
@@ -5620,11 +5931,7 @@ window.__ModuleLoader__.load({
5620
5931
  dragFrom.current = null;
5621
5932
  setDragging(false);
5622
5933
  },
5623
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5624
- className: "da-palette-grip-dots",
5625
- "aria-hidden": true,
5626
- children: "⠿"
5627
- })
5934
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: true })
5628
5935
  }), TOOLS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5629
5936
  type: "button",
5630
5937
  className: "da-btn da-tool",
@@ -5678,11 +5985,7 @@ window.__ModuleLoader__.load({
5678
5985
  barFrom.current = null;
5679
5986
  setBarDragging(false);
5680
5987
  },
5681
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5682
- className: "da-palette-grip-dots",
5683
- "aria-hidden": true,
5684
- children: "⠿"
5685
- })
5988
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: false })
5686
5989
  }),
5687
5990
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5688
5991
  className: "da-bar-tools",
@@ -5709,6 +6012,7 @@ window.__ModuleLoader__.load({
5709
6012
  className: "da-style-row",
5710
6013
  role: "group",
5711
6014
  "aria-label": t("style"),
6015
+ "data-side": foldOpensDown ? "below" : "above",
5712
6016
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5713
6017
  className: "da-style-label",
5714
6018
  children: t("styleColor")
package/lib/index.js CHANGED
@@ -21,9 +21,9 @@ const GUIDANCE = [
21
21
  "",
22
22
  "用户可能在文档预览里直接标注(箭头/框选/椭圆/手绘/单点),发送给你的消息以【附图标注】开头,正文就是**逐条标注清单**,不需要再去读任何附加文件(标注文件是投递用的临时件,发出即删)。收到时按下列纪律处理:",
23
23
  "",
24
- "1. 逐条读消息正文的清单:每条形如 `① 箭头 (10,10) → (40,30),要改:标号指错了`,括号里是图面像素坐标(原点在左上角),`要改:` 之后是用户写的**详细说明**——那是这条标注唯一的内容,按它判断意图。有的条目还带 `锚定:` 后缀,说明该处落在一个具体元素上(SVG 给 `title`/`id`/元素文字,HTML 给 `selector`/元素文字),定位时优先用它。",
24
+ "1. 逐条读消息正文的清单:每条形如 `① 箭头 (10,10) → (40,30),批注:标号指错了`。括号里是图面像素坐标,原点在左上角;框选与圈选给的是**左上角-右下角**,手绘线给的是它画在哪个框里。`批注:` 之后是用户写的**详细说明**——那是这条标注唯一的内容,按它判断意图。有的条目在形状后面紧跟一个圆括号,写着该处落在哪个元素上(SVG 给元素标题/`id`/元素文字,HTML 给 `selector`/元素文字),定位时优先用它。",
25
25
  "2. **PDF 是整份文档一份标注**:每条标注都带 `第 N 页`,坐标是那一页自己的(原点在该页左上角)。看每条标注前先确认它的页码。",
26
- "3. 有形状的标注(箭头/框选/椭圆/手绘)会附一张标注图(`<图名>.annotated.png`,PDF 则是一份**带批注的 PDF**,其批注是原生 PDF 注释对象:`/Ink` 手绘、`/Square` 框选、`/Circle` 圈选、`/Line` 箭头),可直接打开核对图面位置。**若消息说「本条标注只有位置点,没有标注图」,那就是全部内容:只有坐标加一句说明,直接按原图定位处理,不要去找不存在的附件。**",
26
+ "3. 若标注超出原图边界,标注图会把越界的那圈纸也画出来,并用**灰色虚线**框住原图真正的边缘:虚线**里面**是原图,**外面**是你画的标注伸出去的部分——那块不属于图,不要把它当成新增的画布内容。有形状的标注(箭头/框选/椭圆/手绘)会附一张标注图(`<图名>.annotated.png`,PDF 则是一份**带批注的 PDF**,其批注是原生 PDF 注释对象:`/Ink` 手绘、`/Square` 框选、`/Circle` 圈选、`/Line` 箭头),可直接打开核对图面位置。**若消息说「本条标注只有位置点,没有标注图」,那就是全部内容:只有坐标加一句说明,直接按原图定位处理,不要去找不存在的附件。**",
27
27
  "4. 逐条处理标注:每条标注都要落到具体的图面改动,并在答复里逐条回应(编号与标注一致)。无法实现的标注要说明原因,不要静默跳过。",
28
28
  "5. 修改附图以**标注所指的生成源**为准(绘制脚本、SVG 源或 `generate_patent_figure` 的入参),不要直接涂改导出的位图;PDF 同理,改的是生成该 PDF 的源头,不是那份带批注的副本。未标注的部分保持不动。",
29
29
  "6. 改完图后重新渲染并核对:图面尺寸/坐标若变化,旧标注的坐标会失准,此时以 `anchor` 与标注文字为准复核一遍,再回报结果。",
@@ -270,7 +270,7 @@ const WEIGHT_MIN = .5;
270
270
  * @returns the value, pulled to the nearest end of the range.
271
271
  */
272
272
  function clampWeight(value) {
273
- return Math.min(4, Math.max(WEIGHT_MIN, value));
273
+ return Math.min(8, Math.max(WEIGHT_MIN, value));
274
274
  }
275
275
  //#endregion
276
276
  //#region src/host/store.ts
package/market/index.json CHANGED
@@ -93,8 +93,8 @@
93
93
  },
94
94
  "versions": [
95
95
  {
96
- "version": "1.1.1",
97
- "publishTimestamp": 1792729486,
96
+ "version": "1.1.3",
97
+ "publishTimestamp": 1792762042,
98
98
  "source": {
99
99
  "type": "npm",
100
100
  "registry": "https://registry.npmjs.org"
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-figmark",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "private": false,
5
- "description": "DSH 插件:在右侧栏预览里对科研配图、SVG、PDF、HTML 直接圈画批注,标注写进侧车文件并可一键交给智能体改图源",
5
+ "description": "DSH \u63d2\u4ef6\uff1a\u5728\u53f3\u4fa7\u680f\u9884\u89c8\u91cc\u5bf9\u79d1\u7814\u914d\u56fe\u3001SVG\u3001PDF\u3001HTML \u76f4\u63a5\u5708\u753b\u6279\u6ce8\uff0c\u6807\u6ce8\u5199\u8fdb\u4fa7\u8f66\u6587\u4ef6\u5e76\u53ef\u4e00\u952e\u4ea4\u7ed9\u667a\u80fd\u4f53\u6539\u56fe\u6e90",
6
6
  "license": "MIT",
7
7
  "icon": "./icon.svg",
8
8
  "keywords": [