dsh-figmark 1.1.2 → 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.
@@ -1358,27 +1455,15 @@ window.__ModuleLoader__.load({
1358
1455
  const cy = box.y + box.height / 2;
1359
1456
  return `rotate(${n(radians * 180 / Math.PI)} ${n(cx)} ${n(cy)})`;
1360
1457
  }
1361
- //#endregion
1362
- //#region src/client/scale.ts
1363
1458
  /**
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.
1459
+ * Smallest multiple of the base sizes, for figures far below the reference.
1370
1460
  *
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;
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;
1382
1467
  /**
1383
1468
  * The size everything drawn on a figure is multiplied by.
1384
1469
  *
@@ -1392,7 +1477,7 @@ window.__ModuleLoader__.load({
1392
1477
  function figureUnit(width, height) {
1393
1478
  const edge = Math.max(width, height);
1394
1479
  if (!Number.isFinite(edge) || edge <= 0) return 1;
1395
- return Math.min(3, Math.max(MIN_UNIT, edge / REFERENCE_EDGE));
1480
+ return Math.min(10, Math.max(MIN_UNIT, edge / 800));
1396
1481
  }
1397
1482
  //#endregion
1398
1483
  //#region src/client/Canvas.tsx
@@ -1423,7 +1508,8 @@ window.__ModuleLoader__.load({
1423
1508
  /** Minimum distance between recorded freehand samples, in figure pixels. */
1424
1509
  const PEN_SAMPLE_STEP = 3;
1425
1510
  /** Reach of a handle's hit area, in figure pixels: generous enough to grab. */
1426
- 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;
1427
1513
  /** Monotonic id source for marks created in this session. */
1428
1514
  let markSeq = 0;
1429
1515
  /**
@@ -1580,10 +1666,20 @@ window.__ModuleLoader__.load({
1580
1666
  * @param event - the pointer event.
1581
1667
  * @returns true when a handle took the press.
1582
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);
1583
1679
  const beginHandle = (mark, event) => {
1584
1680
  if (props.selectedId !== mark.id) return false;
1585
1681
  if (mark.kind === "point") return false;
1586
- 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);
1587
1683
  if (grabbed === void 0) return false;
1588
1684
  beginGesture(mark, event, grabbed);
1589
1685
  return true;
@@ -1814,26 +1910,26 @@ window.__ModuleLoader__.load({
1814
1910
  /** The resize and rotation handles of the selected mark. */
1815
1911
  const renderHandles = (mark) => {
1816
1912
  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);
1913
+ const half = grip / 2;
1914
+ const handles = handlesFor(frame, mark.rotation ?? 0, grip);
1915
+ const tether = tetherFor(frame, mark.rotation ?? 0, grip);
1820
1916
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
1821
1917
  x1: tether.from[0],
1822
1918
  y1: tether.from[1],
1823
1919
  x2: tether.to[0],
1824
1920
  y2: tether.to[1],
1825
1921
  stroke: "#4c8dff",
1826
- strokeWidth: 1
1922
+ strokeWidth: hairline
1827
1923
  }), handles.map((handle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1828
1924
  "data-da-handle": handle.id,
1829
1925
  x: handle.point[0] - half,
1830
1926
  y: handle.point[1] - half,
1831
- width: 7,
1832
- height: 7,
1833
- rx: handle.id === "rotate" ? 7 / 2 : 1,
1927
+ width: grip,
1928
+ height: grip,
1929
+ rx: handle.id === "rotate" ? grip / 2 : grip / 7,
1834
1930
  fill: "#ffffff",
1835
1931
  stroke: "#4c8dff",
1836
- strokeWidth: 1.5,
1932
+ strokeWidth: 1.5 * hairline,
1837
1933
  style: {
1838
1934
  pointerEvents: "all",
1839
1935
  cursor: cursorFor(handle.id)
@@ -1902,16 +1998,17 @@ window.__ModuleLoader__.load({
1902
1998
  const box = pointMarkBox(shown, unit);
1903
1999
  /* v8 ignore next -- a point mark carries exactly the one coordinate it was placed at. */
1904
2000
  if (box === void 0) return null;
2001
+ const inset = 3 * hairline;
1905
2002
  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,
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,
1911
2008
  fill: "none",
1912
2009
  stroke: "#4c8dff",
1913
- strokeWidth: unit,
1914
- strokeDasharray: `${3 * unit} ${2 * unit}`,
2010
+ strokeWidth: hairline,
2011
+ strokeDasharray: `${3 * hairline} ${2 * hairline}`,
1915
2012
  style: { pointerEvents: "none" }
1916
2013
  });
1917
2014
  };
@@ -1998,10 +2095,6 @@ window.__ModuleLoader__.load({
1998
2095
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1999
2096
  className: "da-overlay",
2000
2097
  "data-da-overlay": "",
2001
- style: {
2002
- left: originX,
2003
- top: originY
2004
- },
2005
2098
  width,
2006
2099
  height,
2007
2100
  viewBox: `0 0 ${width} ${height}`,
@@ -2281,14 +2374,13 @@ window.__ModuleLoader__.load({
2281
2374
  const pages = annotatedPages(document);
2282
2375
  const marked = pages.length === 0 ? "" : zh ? `其中 ${pages.length} 页有标注,` : `${pages.length} pages marked, `;
2283
2376
  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})`;
2377
+ const header = zh ? `${kind}${figure.path}(${scope === "" ? "" : `${scope},`}${size}${marked}标注 ${marks.length} 处)` : `${kind} ${figure.path} (${scope === "" ? "" : `${scope}, `}${size}${marked}${marks.length} marks)`;
2286
2378
  const paged = figure.pageCount !== void 0;
2287
2379
  const pictured = paths.reviewPath !== null;
2288
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."];
2289
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."] : [];
2290
2382
  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.";
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.";
2292
2384
  return [
2293
2385
  header,
2294
2386
  ...attachment,
@@ -2297,7 +2389,7 @@ window.__ModuleLoader__.load({
2297
2389
  ...summary,
2298
2390
  ...describeMarks(document, locale),
2299
2391
  "",
2300
- closing
2392
+ closer
2301
2393
  ].join("\n");
2302
2394
  }
2303
2395
  /**
@@ -2684,7 +2776,25 @@ window.__ModuleLoader__.load({
2684
2776
  const unit = figureUnit(layer.width, layer.height);
2685
2777
  const paper = plotBounds(layer.width, layer.height, marks, unit);
2686
2778
  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>";
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)}"/>`;
2688
2798
  }
2689
2799
  /**
2690
2800
  * The size of the picture {@link composeReviewSvg} builds.
@@ -3253,10 +3363,27 @@ window.__ModuleLoader__.load({
3253
3363
  const STYLE_MARKER = "data-dsh-figmark";
3254
3364
  /** The stylesheet text. */
3255
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. */
3256
3377
  /* The panel follows the shell's palette rather than carrying one of its own: every
3257
3378
  colour below is an alias the theme owner resolves, with the inherited value as the
3258
3379
  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)}
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)}
3260
3387
 
3261
3388
  /* Every small picture in the panel is one stroke set, declared once: without this
3262
3389
  an svg with no fill of its own falls back to a solid black shape, which is what
@@ -3274,12 +3401,20 @@ window.__ModuleLoader__.load({
3274
3401
  box-shadow:0 8px 26px rgba(10,14,24,.3),0 0 0 1px rgba(255,255,255,.08)}
3275
3402
  /* While it is being dragged the pointer is the grip, whatever it passes over. */
3276
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)}
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}
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}
3283
3418
  /* A tile is a round button holding one white glyph. No word under it: six glyphs in
3284
3419
  a column are read at a glance, and the word is still there as the tooltip and as
3285
3420
  the accessible name. */
@@ -3301,6 +3436,7 @@ window.__ModuleLoader__.load({
3301
3436
  drawing. The drawn parts never take the pointer, or grabbing the knob would grab
3302
3437
  the knob instead of the slider. */
3303
3438
  .da-weight{position:absolute;z-index:7;left:6px;top:50%;transform:translateY(-50%);
3439
+ --da-track-inset:10px;--da-knob-size:11px;
3304
3440
  display:flex;align-items:center;justify-content:center;width:7px;height:200px;
3305
3441
  border-radius:999px;background:var(--dsw-alias-border-l2,rgba(127,127,127,.22));
3306
3442
  cursor:ns-resize;transition:width .16s ease,background .16s ease,box-shadow .16s ease}
@@ -3309,13 +3445,24 @@ window.__ModuleLoader__.load({
3309
3445
  /* The track is an svg in the body: a hairline at the bottom that opens into a rounded
3310
3446
  shoulder at the top. It is not a clip-path polygon because a polygon has no rounded
3311
3447
  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}
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;
3456
+ transition:opacity .16s ease}
3316
3457
  .da-weight:hover .da-weight-svg,.da-weight:focus-within .da-weight-svg{opacity:1}
3317
3458
  /* The knob sits on the value: the only part that shows at rest, so the rail reads as
3318
3459
  a slider waiting to be touched rather than a bare rule. */
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}
3319
3466
  .da-weight-knob{position:absolute;left:50%;width:var(--da-knob-size,11px);height:var(--da-knob-size,11px);
3320
3467
  border-radius:50%;transform:translateX(-50%);pointer-events:none;
3321
3468
  background:var(--dsw-alias-bg-layer-2,#fff);
@@ -3323,7 +3470,7 @@ window.__ModuleLoader__.load({
3323
3470
  transition:width .16s ease,height .16s ease,background .16s ease}
3324
3471
  .da-weight:hover,.da-weight:focus-within{--da-knob-size:15px}
3325
3472
  .da-weight:hover .da-weight-knob,.da-weight:focus-within .da-weight-knob{
3326
- background:var(--dsw-alias-brand-primary,#176df2);
3473
+ background:var(--da-accent,#176df2);
3327
3474
  box-shadow:0 2px 7px rgba(20,40,80,.3)}
3328
3475
  /* The browser's own range, rotated upright and made transparent. It spans the whole
3329
3476
  rail so a press anywhere on it lands on the slider — including on the knob, which
@@ -3345,7 +3492,7 @@ window.__ModuleLoader__.load({
3345
3492
  it centred while untouched must not also apply — with both set it stretches. */
3346
3493
  .da-bar[data-parked="true"]{bottom:auto}
3347
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)}
3348
- .da-bar[data-dragging="true"] *{cursor:grabbing !important}
3495
+ .da-bar[data-dragging="true"] *{cursor:move !important}
3349
3496
  /* The bar's buttons are white glyphs on the dark strip. The one filled control is
3350
3497
  still the way out. */
3351
3498
  .da-bar .da-dock-btn{color:#fff}
@@ -3386,8 +3533,8 @@ window.__ModuleLoader__.load({
3386
3533
  .da-tool .da-tool-label{font-size:10px;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary,inherit)}
3387
3534
  /* The armed tool is the one state in this row the reader must never hunt for: it is
3388
3535
  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}
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}
3391
3538
  .da-tool[aria-pressed="true"] svg{stroke-width:2.1}
3392
3539
  .da-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
3393
3540
  .da-btn-icon{width:30px;padding:0}
@@ -3398,6 +3545,10 @@ window.__ModuleLoader__.load({
3398
3545
  .da-stylewrap{position:relative;flex:0 0 auto}
3399
3546
  /* The style fold floats above the bar rather than taking a row of it: opened over
3400
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)}
3401
3552
  .da-style-row{position:absolute;left:8px;bottom:calc(100% + 6px);z-index:9;
3402
3553
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;max-width:calc(100% - 16px);
3403
3554
  padding:8px 10px;border:0.5px solid var(--dsw-alias-border-l2,currentColor);border-radius:11px;
@@ -3412,17 +3563,24 @@ window.__ModuleLoader__.load({
3412
3563
  /* Anywhere else is "done here": the panel closes on the click that lands outside it
3413
3564
  rather than trapping the reader in a popup with one way out. */
3414
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}
3415
- .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)}
3416
3567
  /* --- the figure on its table -------------------------------------------- */
3417
3568
  /* A figure is a sheet on a table, not a picture floating in a void: the white paper
3418
3569
  is what gives the panel a middle, and the gap around it is the margin the eye
3419
3570
  needs to see where the drawing stops. */
3420
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)}
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}
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))}
3426
3584
  .da-figure{display:block;user-select:none;-webkit-user-drag:none}
3427
3585
  /* The table is a thing you can push. The hand only shows under the tools that
3428
3586
  leave a drag free to move the view: under a drawing tool the same drag draws. */
@@ -3455,22 +3613,22 @@ window.__ModuleLoader__.load({
3455
3613
  width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;
3456
3614
  color:inherit;cursor:pointer;flex:0 0 auto}
3457
3615
  .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)}
3616
+ .da-dock-btn[aria-pressed="true"]{background:var(--da-accent-soft,rgba(23,109,242,.12));color:var(--da-accent,#176df2)}
3459
3617
  .da-dock-btn svg{width:15px;height:15px}
3460
3618
  .da-dock-btn:disabled{opacity:.4;cursor:default}
3461
3619
  .da-dock-btn:disabled:hover{background:transparent}
3462
3620
  /* 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)}
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)}
3465
3623
  /* A dot, not a count: "there is a goal written" is all the bar needs to say about
3466
3624
  a field it is not a list of. */
3467
3625
  .da-dock-dot{position:absolute;top:3px;right:4px;width:5px;height:5px;border-radius:50%;
3468
- background:var(--dsw-alias-brand-primary,#176df2)}
3626
+ background:var(--da-accent,#176df2)}
3469
3627
  .da-dock-sep{width:1px;height:16px;margin:0 3px;background:var(--dsw-alias-border-l3,currentColor);opacity:.6;flex:0 0 auto}
3470
3628
  /* The count rides on the list's own button: it is the one thing about the list a
3471
3629
  reader wants without opening it. */
3472
3630
  .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;
3631
+ border-radius:7px;background:var(--da-accent,#176df2);color:#fff;
3474
3632
  font-size:9px;font-weight:700;line-height:14px;text-align:center;font-variant-numeric:tabular-nums}
3475
3633
 
3476
3634
  /* --- the floating records ------------------------------------------------ */
@@ -3482,7 +3640,7 @@ window.__ModuleLoader__.load({
3482
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)}
3483
3641
  .da-card-title{display:flex;align-items:center;gap:5px;min-width:0;font-size:12px;font-weight:650;
3484
3642
  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)}
3643
+ .da-card-title svg{width:14px;height:14px;flex:0 0 auto;color:var(--da-accent,#176df2)}
3486
3644
 
3487
3645
  .da-float-head{display:flex;align-items:center;gap:7px}
3488
3646
  .da-float-head .da-card-title{flex:1 1 auto;min-width:0}
@@ -3521,10 +3679,10 @@ window.__ModuleLoader__.load({
3521
3679
  color:var(--dsw-alias-label-primary,currentColor)}
3522
3680
  .da-comment-send-btn:disabled{opacity:.45;cursor:default}
3523
3681
  .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)}
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)}
3526
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}
3527
- .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)}
3528
3686
  .da-btn-count{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;
3529
3687
  padding:0 4px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));
3530
3688
  font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}
@@ -3537,8 +3695,8 @@ window.__ModuleLoader__.load({
3537
3695
  /* What the selected mark is, in one word: the detail field is disabled until there
3538
3696
  is something to describe, and this says what it would describe. */
3539
3697
  .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}
3698
+ background:var(--da-accent-soft,rgba(23,109,242,.12));
3699
+ color:var(--da-accent,#176df2);font-size:10px;font-weight:600}
3542
3700
  /* The "?" beside a card's name: the long form of what that field is for. */
3543
3701
  .da-hintdot{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;
3544
3702
  border-radius:50%;border:0.5px solid var(--dsw-alias-border-l3,currentColor);
@@ -3551,7 +3709,7 @@ window.__ModuleLoader__.load({
3551
3709
  .da-mark{display:flex;align-items:center;gap:7px;padding:5px 6px;border-radius:8px;
3552
3710
  border:0.5px solid transparent;min-width:0}
3553
3711
  .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);
3712
+ .da-mark[data-selected="true"]{border-color:var(--da-accent,#176df2);
3555
3713
  background:var(--dsw-alias-interactive-bg-hover,rgba(23,109,242,.08))}
3556
3714
  .da-mark-index{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
3557
3715
  width:20px;height:20px;border-radius:6px;color:#fff;font-size:11px;font-weight:700;
@@ -3823,24 +3981,36 @@ window.__ModuleLoader__.load({
3823
3981
  /**
3824
3982
  * The rail's track, drawn on an 18x100 grid and stretched to whatever height it gets.
3825
3983
  *
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.
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.
3833
3995
  */
3834
3996
  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",
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",
3842
4002
  "Z"
3843
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})`;
3844
4014
  const TOOLS = [
3845
4015
  {
3846
4016
  tool: "select",
@@ -3897,21 +4067,61 @@ window.__ModuleLoader__.load({
3897
4067
  * with the mouse rather than a mark you leave behind. Drawn rather than written so
3898
4068
  * the toolbar stays one row of controls at any locale or font size.
3899
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
+ */
3900
4115
  function PointerIcon() {
3901
4116
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3902
- width: "13",
3903
- height: "15",
3904
- viewBox: "0 0 13 15",
4117
+ className: "da-ico",
4118
+ viewBox: "0 0 24 24",
3905
4119
  "aria-hidden": "true",
3906
4120
  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
- })
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" })
3915
4125
  });
3916
4126
  }
3917
4127
  /**
@@ -4114,6 +4324,7 @@ window.__ModuleLoader__.load({
4114
4324
  const barRef = (0, react.useRef)(null);
4115
4325
  const figureHostRef = (0, react.useRef)(null);
4116
4326
  const surfaceRef = (0, react.useRef)(null);
4327
+ const paperRef = (0, react.useRef)(null);
4117
4328
  const figureRef = (0, react.useRef)(null);
4118
4329
  /**
4119
4330
  * Hand the stage to the owner as its scrollport, and keep it here too.
@@ -4655,6 +4866,17 @@ window.__ModuleLoader__.load({
4655
4866
  htmlHeight
4656
4867
  ]);
4657
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
+ /**
4658
4880
  * Put a record away when the reader clicks anywhere else.
4659
4881
  *
4660
4882
  * A record is a fold, not a mode: it has done its job the moment the reader looks
@@ -4752,9 +4974,9 @@ window.__ModuleLoader__.load({
4752
4974
  const zoomAt = (0, react.useCallback)((next, clientX, clientY) => {
4753
4975
  const stage = stageRef.current;
4754
4976
  const surface = surfaceRef.current;
4755
- const figureHost = figureRef.current;
4977
+ const sheet = paperRef.current;
4756
4978
  /* 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;
4979
+ if (stage === null || surface === null || sheet === null || paper === null) return;
4758
4980
  const before = liveScale.current;
4759
4981
  const reached = clampZoom(next);
4760
4982
  if (reached === before) return;
@@ -4768,19 +4990,22 @@ window.__ModuleLoader__.load({
4768
4990
  }, {
4769
4991
  clientX,
4770
4992
  clientY
4771
- }, before, reached, size);
4993
+ }, before, reached, paper);
4772
4994
  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})`;
4995
+ surface.style.width = `${paper.width * reached}px`;
4996
+ surface.style.height = `${paper.height * reached}px`;
4997
+ sheet.style.transform = `scale(${reached})`;
4776
4998
  stage.scrollLeft = target.scrollLeft;
4777
4999
  stage.scrollTop = target.scrollTop;
4778
5000
  clearTimeout(settleTimer.current);
4779
5001
  settleTimer.current = setTimeout(() => {
4780
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 = "";
4781
5006
  setZoom(reached);
4782
5007
  }, 120);
4783
- }, [size]);
5008
+ }, [paper]);
4784
5009
  (0, react.useEffect)(() => () => {
4785
5010
  clearTimeout(settleTimer.current);
4786
5011
  }, []);
@@ -5281,7 +5506,7 @@ window.__ModuleLoader__.load({
5281
5506
  * One number for the fill, the knob and the input: computed three times the three
5282
5507
  * could round differently and the knob would sit a pixel off the fill it belongs to.
5283
5508
  */
5284
- const weightFraction = shownWeight === null ? 0 : (shownWeight - WEIGHT_MIN) / (4 - WEIGHT_MIN);
5509
+ const weightFraction = shownWeight === null ? 0 : (shownWeight - WEIGHT_MIN) / (8 - WEIGHT_MIN);
5285
5510
  /**
5286
5511
  * Change one attribute of the selected mark, or of the next stroke when nothing
5287
5512
  * is selected.
@@ -5366,13 +5591,15 @@ window.__ModuleLoader__.load({
5366
5591
  className: "da-surface",
5367
5592
  ref: surfaceRef,
5368
5593
  style: surfaceStyle,
5369
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5594
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5370
5595
  className: "da-paper",
5596
+ ref: paperRef,
5371
5597
  style: paperStyle,
5372
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5599
+ className: "da-figure-frame",
5373
5600
  ref: figureRef,
5374
5601
  style: figureStyle,
5375
- children: [figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5602
+ children: figure?.kind === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5376
5603
  ref: figureHostRef,
5377
5604
  dangerouslySetInnerHTML: { __html: figure.markup }
5378
5605
  }) : figure?.kind === "raster" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
@@ -5397,36 +5624,37 @@ window.__ModuleLoader__.load({
5397
5624
  width: figure.width,
5398
5625
  height: htmlHeight
5399
5626
  }
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
- })
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
+ })]
5430
5658
  })
5431
5659
  })
5432
5660
  }),
@@ -5618,7 +5846,7 @@ window.__ModuleLoader__.load({
5618
5846
  className: "da-weight-input",
5619
5847
  "aria-label": t(selected?.kind === "point" || tool === "point" ? "pinSize" : "weight"),
5620
5848
  min: WEIGHT_MIN,
5621
- max: 4,
5849
+ max: 8,
5622
5850
  step: .1,
5623
5851
  value: shownWeight,
5624
5852
  onChange: (event) => {
@@ -5630,44 +5858,40 @@ window.__ModuleLoader__.load({
5630
5858
  viewBox: "0 0 18 100",
5631
5859
  preserveAspectRatio: "none",
5632
5860
  "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", {
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", {
5641
5869
  offset: "0",
5642
- stopColor: "rgba(20,40,80,.05)"
5643
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
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", {
5644
5877
  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
5878
+ stopColor: "rgba(20,40,80,.24)"
5654
5879
  })
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
- ]
5880
+ ]
5881
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
5882
+ d: WEIGHT_TRACK,
5883
+ fill: `url(#${trackId}-shade)`
5884
+ })]
5885
+ }),
5886
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5887
+ className: "da-weight-fill",
5888
+ "aria-hidden": true,
5889
+ style: { height: travelHeight(weightFraction) }
5666
5890
  }),
5667
5891
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5668
5892
  className: "da-weight-knob",
5669
5893
  "aria-hidden": true,
5670
- style: { bottom: `calc(12px + ${weightFraction} * (100% - 24px - var(--da-knob-size, 11px)))` }
5894
+ style: { bottom: `calc(${travelHeight(weightFraction)} - var(--da-knob-size) / 2)` }
5671
5895
  })
5672
5896
  ]
5673
5897
  }),
@@ -5707,11 +5931,7 @@ window.__ModuleLoader__.load({
5707
5931
  dragFrom.current = null;
5708
5932
  setDragging(false);
5709
5933
  },
5710
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5711
- className: "da-palette-grip-dots",
5712
- "aria-hidden": true,
5713
- children: "⠿"
5714
- })
5934
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: true })
5715
5935
  }), TOOLS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5716
5936
  type: "button",
5717
5937
  className: "da-btn da-tool",
@@ -5765,11 +5985,7 @@ window.__ModuleLoader__.load({
5765
5985
  barFrom.current = null;
5766
5986
  setBarDragging(false);
5767
5987
  },
5768
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5769
- className: "da-palette-grip-dots",
5770
- "aria-hidden": true,
5771
- children: "⠿"
5772
- })
5988
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, { wide: false })
5773
5989
  }),
5774
5990
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5775
5991
  className: "da-bar-tools",
@@ -5796,6 +6012,7 @@ window.__ModuleLoader__.load({
5796
6012
  className: "da-style-row",
5797
6013
  role: "group",
5798
6014
  "aria-label": t("style"),
6015
+ "data-side": foldOpensDown ? "below" : "above",
5799
6016
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5800
6017
  className: "da-style-label",
5801
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.2",
97
- "publishTimestamp": 1792731605,
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.2",
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": [