sketchmark 1.3.4 → 1.3.6

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.
@@ -601,6 +601,12 @@ var AIDiagram = (function (exports) {
601
601
  id,
602
602
  shape,
603
603
  label: props.label || "",
604
+ ...(props["label-dx"] !== undefined
605
+ ? { labelDx: parseFloat(props["label-dx"]) }
606
+ : {}),
607
+ ...(props["label-dy"] !== undefined
608
+ ? { labelDy: parseFloat(props["label-dy"]) }
609
+ : {}),
604
610
  ...(props.width ? { width: parseFloat(props.width) } : {}),
605
611
  ...(props.height ? { height: parseFloat(props.height) } : {}),
606
612
  ...(props.x ? { x: parseFloat(props.x) } : {}),
@@ -641,6 +647,12 @@ var AIDiagram = (function (exports) {
641
647
  id,
642
648
  shape: "note",
643
649
  label: (props.label ?? "").replace(/\\n/g, "\n"),
650
+ ...(props["label-dx"] !== undefined
651
+ ? { labelDx: parseFloat(props["label-dx"]) }
652
+ : {}),
653
+ ...(props["label-dy"] !== undefined
654
+ ? { labelDy: parseFloat(props["label-dy"]) }
655
+ : {}),
644
656
  theme: props.theme,
645
657
  ...(meta ? { meta } : {}),
646
658
  style: propsToStyle(props),
@@ -689,6 +701,8 @@ var AIDiagram = (function (exports) {
689
701
  kind: "group",
690
702
  id,
691
703
  label: props.label ?? "",
704
+ labelDx: props["label-dx"] !== undefined ? parseFloat(props["label-dx"]) : undefined,
705
+ labelDy: props["label-dy"] !== undefined ? parseFloat(props["label-dy"]) : undefined,
692
706
  children: [],
693
707
  layout: props.layout,
694
708
  columns: props.columns !== undefined ? parseInt(props.columns, 10) : undefined,
@@ -733,6 +747,8 @@ var AIDiagram = (function (exports) {
733
747
  to: toTok.value,
734
748
  connector: connector,
735
749
  label: props.label,
750
+ labelDx: props["label-dx"] !== undefined ? parseFloat(props["label-dx"]) : undefined,
751
+ labelDy: props["label-dy"] !== undefined ? parseFloat(props["label-dy"]) : undefined,
736
752
  fromAnchor: props["anchor-from"],
737
753
  toAnchor: props["anchor-to"],
738
754
  dashed,
@@ -1234,6 +1250,7 @@ var AIDiagram = (function (exports) {
1234
1250
  const NODE = {
1235
1251
  minW: 90, // minimum auto-sized node width (px)
1236
1252
  maxW: 300, // maximum auto-sized node width (px)
1253
+ mediaLabelH: 20, // reserved bottom strip for icon/image/line labels (px)
1237
1254
  basePad: 26, // base padding added to label width (px)
1238
1255
  };
1239
1256
  // ── Shape-specific sizing ──────────────────────────────────
@@ -3557,6 +3574,8 @@ var AIDiagram = (function (exports) {
3557
3574
  id: n.id,
3558
3575
  shape: n.shape,
3559
3576
  label: n.label,
3577
+ labelDx: n.labelDx,
3578
+ labelDy: n.labelDy,
3560
3579
  style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
3561
3580
  groupId: nodeParentById.get(n.id),
3562
3581
  width: n.width,
@@ -3582,6 +3601,8 @@ var AIDiagram = (function (exports) {
3582
3601
  return {
3583
3602
  id: g.id,
3584
3603
  label: g.label,
3604
+ labelDx: g.labelDx,
3605
+ labelDy: g.labelDy,
3585
3606
  parentId: groupParentById.get(g.id),
3586
3607
  children: g.children,
3587
3608
  layout: (g.layout ?? "column"),
@@ -3659,6 +3680,8 @@ var AIDiagram = (function (exports) {
3659
3680
  to: e.to,
3660
3681
  connector: e.connector,
3661
3682
  label: e.label,
3683
+ labelDx: e.labelDx,
3684
+ labelDy: e.labelDy,
3662
3685
  fromAnchor: e.fromAnchor,
3663
3686
  toAnchor: e.toAnchor,
3664
3687
  dashed: e.dashed ?? false,
@@ -4000,10 +4023,25 @@ var AIDiagram = (function (exports) {
4000
4023
  },
4001
4024
  };
4002
4025
 
4026
+ const MEDIA_LABEL_SHAPES = new Set(["icon", "image", "line"]);
4027
+ function usesBottomLabelStrip(shape) {
4028
+ return MEDIA_LABEL_SHAPES.has(shape);
4029
+ }
4030
+ function getBottomLabelStripHeight(node) {
4031
+ return usesBottomLabelStrip(node.shape) && node.label ? NODE.mediaLabelH : 0;
4032
+ }
4033
+ function getBottomLabelContentHeight(node) {
4034
+ return node.h - getBottomLabelStripHeight(node);
4035
+ }
4036
+ function getBottomLabelCenterY(node) {
4037
+ const stripH = getBottomLabelStripHeight(node);
4038
+ return stripH > 0 ? node.y + node.h - stripH / 2 : node.y + node.h / 2;
4039
+ }
4040
+
4003
4041
  const iconShape = {
4004
4042
  size(n, labelW) {
4005
4043
  const iconBase = 48;
4006
- const labelH = n.label ? 20 : 0;
4044
+ const labelH = getBottomLabelStripHeight(n);
4007
4045
  n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
4008
4046
  n.h = n.h || (iconBase + labelH);
4009
4047
  },
@@ -4016,8 +4054,7 @@ var AIDiagram = (function (exports) {
4016
4054
  const iconColor = s.color
4017
4055
  ? encodeURIComponent(String(s.color))
4018
4056
  : encodeURIComponent(String(palette.nodeStroke));
4019
- const labelSpace = n.label ? 20 : 0;
4020
- const iconAreaH = n.h - labelSpace;
4057
+ const iconAreaH = getBottomLabelContentHeight(n);
4021
4058
  const iconSize = Math.min(n.w, iconAreaH) - 4;
4022
4059
  const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
4023
4060
  const img = document.createElementNS(SVG_NS, "image");
@@ -4061,8 +4098,7 @@ var AIDiagram = (function (exports) {
4061
4098
  const iconColor = s.color
4062
4099
  ? encodeURIComponent(String(s.color))
4063
4100
  : encodeURIComponent(String(palette.nodeStroke));
4064
- const iconLabelSpace = n.label ? 20 : 0;
4065
- const iconAreaH = n.h - iconLabelSpace;
4101
+ const iconAreaH = getBottomLabelContentHeight(n);
4066
4102
  const iconSize = Math.min(n.w, iconAreaH) - 4;
4067
4103
  const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
4068
4104
  const img = new Image();
@@ -4105,21 +4141,21 @@ var AIDiagram = (function (exports) {
4105
4141
  const w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
4106
4142
  n.w = w;
4107
4143
  if (!n.h) {
4144
+ const labelH = getBottomLabelStripHeight(n);
4108
4145
  if (labelW > w) {
4109
4146
  const fontSize = Number(n.style?.fontSize ?? 14);
4110
4147
  const lines = Math.ceil(labelW / (w - 16));
4111
- n.h = Math.max(52, lines * fontSize * 1.5 + 20);
4148
+ n.h = Math.max(52, lines * fontSize * 1.5 + labelH);
4112
4149
  }
4113
4150
  else {
4114
- n.h = 52;
4151
+ n.h = 52 + labelH;
4115
4152
  }
4116
4153
  }
4117
4154
  },
4118
4155
  renderSVG(rc, n, _palette, opts) {
4119
4156
  const s = n.style ?? {};
4120
4157
  if (n.imageUrl) {
4121
- const imgLabelSpace = n.label ? 20 : 0;
4122
- const imgAreaH = n.h - imgLabelSpace;
4158
+ const imgAreaH = getBottomLabelContentHeight(n);
4123
4159
  const img = document.createElementNS(SVG_NS, "image");
4124
4160
  img.setAttribute("href", n.imageUrl);
4125
4161
  img.setAttribute("x", String(n.x + 1));
@@ -4151,8 +4187,7 @@ var AIDiagram = (function (exports) {
4151
4187
  renderCanvas(rc, ctx, n, _palette, opts) {
4152
4188
  const s = n.style ?? {};
4153
4189
  if (n.imageUrl) {
4154
- const imgLblSpace = n.label ? 20 : 0;
4155
- const imgAreaH = n.h - imgLblSpace;
4190
+ const imgAreaH = getBottomLabelContentHeight(n);
4156
4191
  const img = new Image();
4157
4192
  img.crossOrigin = "anonymous";
4158
4193
  img.onload = () => {
@@ -4329,17 +4364,17 @@ var AIDiagram = (function (exports) {
4329
4364
 
4330
4365
  const lineShape = {
4331
4366
  size(n, labelW) {
4332
- const labelH = n.label ? 20 : 0;
4367
+ const labelH = getBottomLabelStripHeight(n);
4333
4368
  n.w = n.width ?? Math.max(MIN_W, labelW + 20);
4334
4369
  n.h = n.height ?? (6 + labelH);
4335
4370
  },
4336
4371
  renderSVG(rc, n, _palette, opts) {
4337
- const labelH = n.label ? 20 : 0;
4372
+ const labelH = getBottomLabelStripHeight(n);
4338
4373
  const lineY = n.y + (n.h - labelH) / 2;
4339
4374
  return [rc.line(n.x, lineY, n.x + n.w, lineY, opts)];
4340
4375
  },
4341
4376
  renderCanvas(rc, _ctx, n, _palette, opts) {
4342
- const labelH = n.label ? 20 : 0;
4377
+ const labelH = getBottomLabelStripHeight(n);
4343
4378
  const lineY = n.y + (n.h - labelH) / 2;
4344
4379
  rc.line(n.x, lineY, n.x + n.w, lineY, opts);
4345
4380
  },
@@ -5585,18 +5620,18 @@ var AIDiagram = (function (exports) {
5585
5620
  stroke: bgStroke, strokeWidth: Number(s.strokeWidth ?? 1.2),
5586
5621
  ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
5587
5622
  }));
5623
+ const { px, py, pw, ph, titleH, cx, cy } = chartLayout(c);
5588
5624
  // Title
5589
5625
  if (c.label) {
5590
- cg.appendChild(mkT(c.label, c.x + c.w / 2, c.y + 14, cFontSize, cFontWeight, lc, 'middle', cFont));
5626
+ cg.appendChild(mkT(c.label, c.x + c.w / 2, c.y + titleH / 2 + 2, cFontSize, cFontWeight, lc, 'middle', cFont));
5591
5627
  }
5592
- const { px, py, pw, ph, cx, cy } = chartLayout(c);
5593
5628
  // ── Pie / Donut ──────────────────────────────────────────
5594
5629
  if (c.chartType === 'pie' || c.chartType === 'donut') {
5595
5630
  const { segments, total } = parsePie(c.data);
5596
- const r = Math.min(c.w * 0.38, (c.h - (c.label ? 24 : 8)) * 0.44);
5631
+ const r = Math.min(c.w * 0.38, (c.h - titleH) * 0.44);
5597
5632
  const ir = c.chartType === 'donut' ? r * 0.48 : 0;
5598
5633
  const legendX = c.x + 8;
5599
- const legendY = c.y + (c.label ? 28 : 12);
5634
+ const legendY = c.y + titleH + 4;
5600
5635
  let angle = -Math.PI / 2;
5601
5636
  for (const seg of segments) {
5602
5637
  const sweep = (seg.value / total) * Math.PI * 2;
@@ -5634,7 +5669,7 @@ var AIDiagram = (function (exports) {
5634
5669
  strokeWidth: 1.2,
5635
5670
  }));
5636
5671
  });
5637
- legend(cg, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.label ? 28 : 12), lc, cFont);
5672
+ legend(cg, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + titleH + 4, lc, cFont);
5638
5673
  return cg;
5639
5674
  }
5640
5675
  // ── Bar / Line / Area ─────────────────────────────────────
@@ -8380,9 +8415,10 @@ var AIDiagram = (function (exports) {
8380
8415
  }));
8381
8416
  // ── Group label typography ──────────────────────────
8382
8417
  const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
8383
- const gTextX = computeTextX(gTypo, g.x, g.w);
8418
+ const gTextX = computeTextX(gTypo, g.x, g.w) + (g.labelDx ?? 0);
8419
+ const gTextY = g.y + gTypo.padding + (g.labelDy ?? 0);
8384
8420
  if (g.label) {
8385
- gg.appendChild(mkText(g.label, gTextX, g.y + gTypo.padding, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAnchor, gTypo.font, gTypo.letterSpacing));
8421
+ gg.appendChild(mkText(g.label, gTextX, gTextY, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAnchor, gTypo.font, gTypo.letterSpacing));
8386
8422
  }
8387
8423
  GL.appendChild(gg);
8388
8424
  }
@@ -8434,8 +8470,8 @@ var AIDiagram = (function (exports) {
8434
8470
  eg.appendChild(startHead);
8435
8471
  }
8436
8472
  if (e.label) {
8437
- const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
8438
- const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
8473
+ const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset + (e.labelDx ?? 0);
8474
+ const my = (y1 + y2) / 2 + nx * EDGE.labelOffset + (e.labelDy ?? 0);
8439
8475
  const tw = Math.max(e.label.length * 7 + 12, 36);
8440
8476
  const bg = se("rect");
8441
8477
  bg.setAttribute("x", String(mx - tw / 2));
@@ -8503,7 +8539,7 @@ var AIDiagram = (function (exports) {
8503
8539
  // ── Node / text typography ─────────────────────────
8504
8540
  const isText = n.shape === "text";
8505
8541
  const isNote = n.shape === "note";
8506
- const isMediaShape = n.shape === "icon" || n.shape === "image" || n.shape === "line";
8542
+ const usesBottomStrip = usesBottomLabelStrip(n.shape);
8507
8543
  const typo = resolveTypography(n.style, {
8508
8544
  fontSize: isText ? 13 : isNote ? 12 : 14,
8509
8545
  fontWeight: isText || isNote ? 400 : 500,
@@ -8521,20 +8557,22 @@ var AIDiagram = (function (exports) {
8521
8557
  : n.x + typo.padding)
8522
8558
  : computeTextX(typo, n.x, n.w);
8523
8559
  const fontStr = buildFontStr(typo.fontSize, typo.fontWeight, typo.font);
8524
- const shouldWrap = !isMediaShape && !n.label.includes('\n');
8560
+ const shouldWrap = !usesBottomStrip && !n.label.includes('\n');
8525
8561
  const innerW = shapeInnerTextWidth(n.shape, n.w, typo.padding);
8526
8562
  const lines = shouldWrap
8527
8563
  ? wrapText(n.label, innerW, typo.fontSize, fontStr)
8528
8564
  : n.label.split('\n');
8529
- const textCY = isMediaShape
8530
- ? n.y + n.h - 10
8565
+ const textCY = usesBottomStrip
8566
+ ? getBottomLabelCenterY(n)
8531
8567
  : isNote
8532
8568
  ? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
8533
8569
  : computeTextCY(typo, n.y, n.h, lines.length);
8570
+ const labelX = textX + (n.labelDx ?? 0);
8571
+ const labelY = textCY + (n.labelDy ?? 0);
8534
8572
  if (n.label) {
8535
8573
  ng.appendChild(lines.length > 1
8536
- ? mkMultilineText(lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.lineHeight, typo.font, typo.letterSpacing)
8537
- : mkText(n.label, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.font, typo.letterSpacing));
8574
+ ? mkMultilineText(lines, labelX, labelY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.lineHeight, typo.font, typo.letterSpacing)
8575
+ : mkText(n.label, labelX, labelY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.font, typo.letterSpacing));
8538
8576
  }
8539
8577
  if (options.interactive) {
8540
8578
  ng.style.cursor = "pointer";
@@ -8848,6 +8886,7 @@ var AIDiagram = (function (exports) {
8848
8886
  strokeWidth: Number(s.strokeWidth ?? 1.2),
8849
8887
  ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
8850
8888
  });
8889
+ const { px, py, pw, ph, titleH, cx, cy } = chartLayout(c);
8851
8890
  // Title
8852
8891
  if (c.label) {
8853
8892
  ctx.save();
@@ -8855,17 +8894,16 @@ var AIDiagram = (function (exports) {
8855
8894
  ctx.fillStyle = lc;
8856
8895
  ctx.textAlign = 'center';
8857
8896
  ctx.textBaseline = 'middle';
8858
- ctx.fillText(c.label, c.x + c.w / 2, c.y + 14);
8897
+ ctx.fillText(c.label, c.x + c.w / 2, c.y + titleH / 2 + 2);
8859
8898
  ctx.restore();
8860
8899
  }
8861
- const { px, py, pw, ph, cx, cy } = chartLayout(c);
8862
8900
  // ── Pie / Donut ──────────────────────────────────────────
8863
8901
  if (c.chartType === 'pie' || c.chartType === 'donut') {
8864
8902
  const { segments, total } = parsePie(c.data);
8865
- const r = Math.min(c.w * 0.38, (c.h - (c.label ? 24 : 8)) * 0.44);
8903
+ const r = Math.min(c.w * 0.38, (c.h - titleH) * 0.44);
8866
8904
  const ir = c.chartType === 'donut' ? r * 0.48 : 0;
8867
8905
  const legendX = c.x + 8;
8868
- const legendY = c.y + (c.label ? 28 : 12);
8906
+ const legendY = c.y + titleH + 4;
8869
8907
  let angle = -Math.PI / 2;
8870
8908
  segments.forEach((seg, i) => {
8871
8909
  const sweep = (seg.value / total) * Math.PI * 2;
@@ -8893,7 +8931,7 @@ var AIDiagram = (function (exports) {
8893
8931
  strokeWidth: 1.2,
8894
8932
  });
8895
8933
  });
8896
- drawLegend(ctx, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.label ? 28 : 12), lc, cFont);
8934
+ drawLegend(ctx, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + titleH + 4, lc, cFont);
8897
8935
  ctx.globalAlpha = 1;
8898
8936
  return;
8899
8937
  }
@@ -9128,8 +9166,9 @@ var AIDiagram = (function (exports) {
9128
9166
  });
9129
9167
  if (g.label) {
9130
9168
  const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
9131
- const gTextX = computeTextX(gTypo, g.x, g.w);
9132
- drawText(ctx, g.label, gTextX, g.y + gTypo.padding + 2, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAlign, gTypo.font, gTypo.letterSpacing);
9169
+ const gTextX = computeTextX(gTypo, g.x, g.w) + (g.labelDx ?? 0);
9170
+ const gTextY = g.y + gTypo.padding + 2 + (g.labelDy ?? 0);
9171
+ drawText(ctx, g.label, gTextX, gTextY, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAlign, gTypo.font, gTypo.letterSpacing);
9133
9172
  }
9134
9173
  if (gs.opacity != null)
9135
9174
  ctx.globalAlpha = 1;
@@ -9167,8 +9206,8 @@ var AIDiagram = (function (exports) {
9167
9206
  if (arrowAt === 'start' || arrowAt === 'both')
9168
9207
  drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + 'back'));
9169
9208
  if (e.label) {
9170
- const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
9171
- const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
9209
+ const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset + (e.labelDx ?? 0);
9210
+ const my = (y1 + y2) / 2 + nx * EDGE.labelOffset + (e.labelDy ?? 0);
9172
9211
  // ── Edge label: font, font-size, letter-spacing ──
9173
9212
  // always center-anchored (single line)
9174
9213
  const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
@@ -9208,7 +9247,7 @@ var AIDiagram = (function (exports) {
9208
9247
  // ── Node / text typography ─────────────────────────
9209
9248
  const isText = n.shape === 'text';
9210
9249
  const isNote = n.shape === 'note';
9211
- const isMediaShape = n.shape === 'icon' || n.shape === 'image' || n.shape === 'line';
9250
+ const usesBottomStrip = usesBottomLabelStrip(n.shape);
9212
9251
  const typo = resolveTypography(n.style, {
9213
9252
  fontSize: isText ? 13 : isNote ? 12 : 14,
9214
9253
  fontWeight: isText || isNote ? 400 : 500,
@@ -9226,23 +9265,25 @@ var AIDiagram = (function (exports) {
9226
9265
  : n.x + typo.padding)
9227
9266
  : computeTextX(typo, n.x, n.w);
9228
9267
  const fontStr = buildFontStr(typo.fontSize, typo.fontWeight, typo.font);
9229
- const shouldWrap = !isMediaShape && !n.label.includes('\n');
9268
+ const shouldWrap = !usesBottomStrip && !n.label.includes('\n');
9230
9269
  const innerW = shapeInnerTextWidth(n.shape, n.w, typo.padding);
9231
9270
  const rawLines = n.label.split('\n');
9232
9271
  const lines = shouldWrap && rawLines.length === 1
9233
9272
  ? wrapText(n.label, innerW, typo.fontSize, fontStr)
9234
9273
  : rawLines;
9235
- const textCY = isMediaShape
9236
- ? n.y + n.h - 10
9274
+ const textCY = usesBottomStrip
9275
+ ? getBottomLabelCenterY(n)
9237
9276
  : isNote
9238
9277
  ? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
9239
9278
  : computeTextCY(typo, n.y, n.h, lines.length);
9279
+ const labelX = textX + (n.labelDx ?? 0);
9280
+ const labelY = textCY + (n.labelDy ?? 0);
9240
9281
  if (n.label) {
9241
9282
  if (lines.length > 1) {
9242
- drawMultilineText(ctx, lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.lineHeight, typo.font, typo.letterSpacing);
9283
+ drawMultilineText(ctx, lines, labelX, labelY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.lineHeight, typo.font, typo.letterSpacing);
9243
9284
  }
9244
9285
  else {
9245
- drawText(ctx, lines[0] ?? '', textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.font, typo.letterSpacing);
9286
+ drawText(ctx, lines[0] ?? '', labelX, labelY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.font, typo.letterSpacing);
9246
9287
  }
9247
9288
  }
9248
9289
  if (hasTx)
@@ -9580,7 +9621,7 @@ var AIDiagram = (function (exports) {
9580
9621
  [x + 1, y + h - 1],
9581
9622
  ]);
9582
9623
  case "line": {
9583
- const labelH = el.querySelector("text") ? 20 : 0;
9624
+ const labelH = el.querySelector("text") ? NODE.mediaLabelH : 0;
9584
9625
  const lineY = y + (h - labelH) / 2;
9585
9626
  return `M ${x} ${lineY} L ${x + w} ${lineY}`;
9586
9627
  }
@@ -9951,8 +9992,12 @@ var AIDiagram = (function (exports) {
9951
9992
  this._rc = _rc;
9952
9993
  this._config = _config;
9953
9994
  this._step = -1;
9995
+ this._isPlaying = false;
9996
+ this._playRunId = 0;
9954
9997
  this._pendingStepTimers = new Set();
9955
9998
  this._pendingNarrationTimers = new Set();
9999
+ this._playbackDelayTimerId = null;
10000
+ this._resolvePlaybackDelay = null;
9956
10001
  this._transforms = new Map();
9957
10002
  this._listeners = [];
9958
10003
  // ── Narration caption ──
@@ -9968,6 +10013,7 @@ var AIDiagram = (function (exports) {
9968
10013
  // ── TTS ──
9969
10014
  this._tts = false;
9970
10015
  this._speechDone = null;
10016
+ this._resolveSpeechDone = null;
9971
10017
  this.drawTargetEdges = getDrawTargetEdgeIds(steps);
9972
10018
  this.drawTargetNodes = getDrawTargetNodeIds(steps);
9973
10019
  // Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
@@ -10191,6 +10237,9 @@ var AIDiagram = (function (exports) {
10191
10237
  get atEnd() {
10192
10238
  return this._step === this.steps.length - 1;
10193
10239
  }
10240
+ get isPlaying() {
10241
+ return this._isPlaying;
10242
+ }
10194
10243
  on(listener) {
10195
10244
  this._listeners.push(listener);
10196
10245
  return () => {
@@ -10208,12 +10257,14 @@ var AIDiagram = (function (exports) {
10208
10257
  l(e);
10209
10258
  }
10210
10259
  reset() {
10260
+ this.stop();
10211
10261
  this._step = -1;
10212
10262
  this._clearAll();
10213
10263
  this.emit("animation-reset");
10214
10264
  }
10215
10265
  /** Remove caption and annotation layer from the DOM */
10216
10266
  destroy() {
10267
+ this.stop();
10217
10268
  this._clearAll();
10218
10269
  this._captionEl?.remove();
10219
10270
  this._captionEl = null;
@@ -10224,16 +10275,11 @@ var AIDiagram = (function (exports) {
10224
10275
  this._pointerEl = null;
10225
10276
  }
10226
10277
  next() {
10227
- if (!this.canNext)
10228
- return false;
10229
- this._step++;
10230
- this._applyStep(this._step, false);
10231
- this.emit("step-change");
10232
- if (!this.canNext)
10233
- this.emit("animation-end");
10234
- return true;
10278
+ this.stop();
10279
+ return this._advanceNext();
10235
10280
  }
10236
10281
  prev() {
10282
+ this.stop();
10237
10283
  if (!this.canPrev)
10238
10284
  return false;
10239
10285
  this._step--;
@@ -10244,18 +10290,33 @@ var AIDiagram = (function (exports) {
10244
10290
  return true;
10245
10291
  }
10246
10292
  async play(msPerStep = 900) {
10293
+ if (this._isPlaying || !this.canNext)
10294
+ return;
10295
+ const runId = ++this._playRunId;
10296
+ this._isPlaying = true;
10247
10297
  this.emit("animation-start");
10248
- while (this.canNext) {
10249
- const nextStep = this.steps[this._step + 1];
10250
- this.next();
10251
- // Wait for timer AND speech to finish (whichever is longer)
10252
- await Promise.all([
10253
- new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep))),
10254
- this._speechDone ?? Promise.resolve(),
10255
- ]);
10298
+ try {
10299
+ while (this.canNext && this._playRunId === runId) {
10300
+ const nextStep = this.steps[this._step + 1];
10301
+ if (!this._advanceNext())
10302
+ break;
10303
+ if (this._playRunId !== runId)
10304
+ break;
10305
+ await Promise.all([
10306
+ this._waitForPlaybackDelay(this._playbackWaitMs(nextStep, msPerStep)),
10307
+ this._speechDone ?? Promise.resolve(),
10308
+ ]);
10309
+ }
10310
+ }
10311
+ finally {
10312
+ if (this._playRunId === runId) {
10313
+ this._isPlaying = false;
10314
+ this._cancelPlaybackDelay();
10315
+ }
10256
10316
  }
10257
10317
  }
10258
10318
  goTo(index) {
10319
+ this.stop();
10259
10320
  index = Math.max(-1, Math.min(this.steps.length - 1, index));
10260
10321
  if (index === this._step)
10261
10322
  return;
@@ -10269,6 +10330,30 @@ var AIDiagram = (function (exports) {
10269
10330
  }
10270
10331
  this.emit("step-change");
10271
10332
  }
10333
+ stop() {
10334
+ if (!this._isPlaying && !this._resolvePlaybackDelay) {
10335
+ this._clearPendingStepTimers();
10336
+ this._cancelNarrationTyping();
10337
+ this._cancelSpeech();
10338
+ return;
10339
+ }
10340
+ this._isPlaying = false;
10341
+ this._playRunId += 1;
10342
+ this._cancelPlaybackDelay();
10343
+ this._clearPendingStepTimers();
10344
+ this._cancelNarrationTyping();
10345
+ this._cancelSpeech();
10346
+ }
10347
+ _advanceNext() {
10348
+ if (!this.canNext)
10349
+ return false;
10350
+ this._step++;
10351
+ this._applyStep(this._step, false);
10352
+ this.emit("step-change");
10353
+ if (!this.canNext)
10354
+ this.emit("animation-end");
10355
+ return true;
10356
+ }
10272
10357
  _clearTimerBucket(bucket) {
10273
10358
  bucket.forEach((id) => window.clearTimeout(id));
10274
10359
  bucket.clear();
@@ -10294,6 +10379,34 @@ var AIDiagram = (function (exports) {
10294
10379
  _scheduleStep(fn, delayMs) {
10295
10380
  this._scheduleTimer(fn, delayMs, this._pendingStepTimers);
10296
10381
  }
10382
+ _waitForPlaybackDelay(delayMs) {
10383
+ this._cancelPlaybackDelay();
10384
+ return new Promise((resolve) => {
10385
+ let settled = false;
10386
+ const finish = () => {
10387
+ if (settled)
10388
+ return;
10389
+ settled = true;
10390
+ if (this._playbackDelayTimerId !== null) {
10391
+ window.clearTimeout(this._playbackDelayTimerId);
10392
+ this._playbackDelayTimerId = null;
10393
+ }
10394
+ if (this._resolvePlaybackDelay === finish) {
10395
+ this._resolvePlaybackDelay = null;
10396
+ }
10397
+ resolve();
10398
+ };
10399
+ this._resolvePlaybackDelay = finish;
10400
+ if (delayMs <= 0) {
10401
+ finish();
10402
+ return;
10403
+ }
10404
+ this._playbackDelayTimerId = window.setTimeout(finish, delayMs);
10405
+ });
10406
+ }
10407
+ _cancelPlaybackDelay() {
10408
+ this._resolvePlaybackDelay?.();
10409
+ }
10297
10410
  _stepWaitMs(step, fallbackMs) {
10298
10411
  const delay = Math.max(0, step.delay ?? 0);
10299
10412
  const duration = Math.max(0, step.duration ?? 0);
@@ -10335,6 +10448,7 @@ var AIDiagram = (function (exports) {
10335
10448
  return this._stepWaitMs(step, fallbackMs);
10336
10449
  }
10337
10450
  _clearAll() {
10451
+ this._cancelPlaybackDelay();
10338
10452
  this._clearPendingStepTimers();
10339
10453
  this._cancelNarrationTyping();
10340
10454
  this._cancelSpeech();
@@ -10946,16 +11060,30 @@ var AIDiagram = (function (exports) {
10946
11060
  utter.rate = 0.95;
10947
11061
  utter.pitch = 1;
10948
11062
  utter.lang = "en-US";
10949
- // Track when speech actually finishes
11063
+ // Track when speech actually finishes so play() can block until the utterance ends.
10950
11064
  this._speechDone = new Promise((resolve) => {
10951
- utter.onend = () => resolve();
10952
- utter.onerror = () => resolve();
11065
+ let settled = false;
11066
+ const finish = () => {
11067
+ if (settled)
11068
+ return;
11069
+ settled = true;
11070
+ if (this._resolveSpeechDone === finish) {
11071
+ this._resolveSpeechDone = null;
11072
+ this._speechDone = null;
11073
+ }
11074
+ resolve();
11075
+ };
11076
+ this._resolveSpeechDone = finish;
11077
+ utter.onend = finish;
11078
+ utter.onerror = finish;
10953
11079
  });
10954
11080
  speechSynthesis.speak(utter);
10955
11081
  }
10956
11082
  _cancelSpeech() {
10957
11083
  if (typeof speechSynthesis !== "undefined")
10958
11084
  speechSynthesis.cancel();
11085
+ this._resolveSpeechDone?.();
11086
+ this._resolveSpeechDone = null;
10959
11087
  this._speechDone = null;
10960
11088
  }
10961
11089
  /** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
@@ -11764,7 +11892,13 @@ var AIDiagram = (function (exports) {
11764
11892
  this.resetButton.addEventListener("click", () => this.resetAnimation());
11765
11893
  this.prevButton.addEventListener("click", () => this.prevStep());
11766
11894
  this.nextButton.addEventListener("click", () => this.nextStep());
11767
- this.playButton.addEventListener("click", () => void this.play());
11895
+ this.playButton.addEventListener("click", () => {
11896
+ if (this.playInFlight) {
11897
+ this.stopPlayback();
11898
+ return;
11899
+ }
11900
+ void this.play();
11901
+ });
11768
11902
  this.captionButton.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
11769
11903
  this.ttsButton.addEventListener("click", () => this.setTtsEnabled(!this.getTtsEnabled()));
11770
11904
  this.viewport.addEventListener("pointerdown", this.onPointerDown);
@@ -11828,6 +11962,7 @@ var AIDiagram = (function (exports) {
11828
11962
  this.dsl = normalizeNewlines(nextDsl);
11829
11963
  this.clearError();
11830
11964
  this.mirroredEditor?.clearError();
11965
+ this.playInFlight = false;
11831
11966
  this.animUnsub?.();
11832
11967
  this.animUnsub = null;
11833
11968
  this.instance?.anim?.destroy();
@@ -11898,9 +12033,16 @@ var AIDiagram = (function (exports) {
11898
12033
  this.syncAnimationUi();
11899
12034
  }
11900
12035
  }
12036
+ stopPlayback() {
12037
+ this.playInFlight = false;
12038
+ if (this.renderer === "svg")
12039
+ this.instance?.anim.stop();
12040
+ this.syncAnimationUi();
12041
+ }
11901
12042
  nextStep() {
11902
12043
  if (!this.instance || this.renderer !== "svg")
11903
12044
  return;
12045
+ this.playInFlight = false;
11904
12046
  this.instance.anim.next();
11905
12047
  this.syncAnimationUi();
11906
12048
  this.focusCurrentStep();
@@ -11908,6 +12050,7 @@ var AIDiagram = (function (exports) {
11908
12050
  prevStep() {
11909
12051
  if (!this.instance || this.renderer !== "svg")
11910
12052
  return;
12053
+ this.playInFlight = false;
11911
12054
  this.instance.anim.prev();
11912
12055
  this.syncAnimationUi();
11913
12056
  this.focusCurrentStep();
@@ -11915,6 +12058,7 @@ var AIDiagram = (function (exports) {
11915
12058
  resetAnimation() {
11916
12059
  if (!this.instance || this.renderer !== "svg")
11917
12060
  return;
12061
+ this.playInFlight = false;
11918
12062
  this.instance.anim.reset();
11919
12063
  this.syncAnimationUi();
11920
12064
  }
@@ -11943,6 +12087,7 @@ var AIDiagram = (function (exports) {
11943
12087
  this.render();
11944
12088
  }
11945
12089
  destroy() {
12090
+ this.playInFlight = false;
11946
12091
  this.editorCleanup?.();
11947
12092
  this.animUnsub?.();
11948
12093
  this.instance?.anim?.destroy();
@@ -12017,6 +12162,9 @@ var AIDiagram = (function (exports) {
12017
12162
  this.prevButton.disabled = true;
12018
12163
  this.nextButton.disabled = true;
12019
12164
  this.resetButton.disabled = true;
12165
+ this.playButton.textContent = "Play";
12166
+ this.playButton.classList.remove("is-active");
12167
+ this.playButton.setAttribute("aria-pressed", "false");
12020
12168
  this.playButton.disabled = true;
12021
12169
  this.syncToggleUi();
12022
12170
  return;
@@ -12026,7 +12174,10 @@ var AIDiagram = (function (exports) {
12026
12174
  this.prevButton.disabled = !anim.canPrev;
12027
12175
  this.nextButton.disabled = !anim.canNext;
12028
12176
  this.resetButton.disabled = false;
12029
- this.playButton.disabled = this.playInFlight || !anim.canNext;
12177
+ this.playButton.textContent = this.playInFlight ? "Stop" : "Play";
12178
+ this.playButton.classList.toggle("is-active", this.playInFlight);
12179
+ this.playButton.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
12180
+ this.playButton.disabled = this.playInFlight ? false : !anim.canNext;
12030
12181
  this.syncToggleUi();
12031
12182
  }
12032
12183
  getStepTarget(stepItem) {
@@ -12932,6 +13083,10 @@ var AIDiagram = (function (exports) {
12932
13083
  this.btnPrev.addEventListener("click", () => this.prevStep());
12933
13084
  this.btnNext.addEventListener("click", () => this.nextStep());
12934
13085
  this.btnPlay.addEventListener("click", () => {
13086
+ if (this.playInFlight) {
13087
+ this.stopPlayback();
13088
+ return;
13089
+ }
12935
13090
  void this.play();
12936
13091
  });
12937
13092
  this.btnCaption.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
@@ -12989,6 +13144,7 @@ var AIDiagram = (function (exports) {
12989
13144
  }
12990
13145
  this.clearError();
12991
13146
  this.stopMotion();
13147
+ this.playInFlight = false;
12992
13148
  this.animUnsub?.();
12993
13149
  this.animUnsub = null;
12994
13150
  this.instance?.anim?.destroy();
@@ -13061,9 +13217,15 @@ var AIDiagram = (function (exports) {
13061
13217
  this.syncControls();
13062
13218
  }
13063
13219
  }
13220
+ stopPlayback() {
13221
+ this.playInFlight = false;
13222
+ this.instance?.anim.stop();
13223
+ this.syncControls();
13224
+ }
13064
13225
  nextStep() {
13065
13226
  if (!this.instance)
13066
13227
  return;
13228
+ this.playInFlight = false;
13067
13229
  this.instance.anim.next();
13068
13230
  this.syncControls();
13069
13231
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13073,6 +13235,7 @@ var AIDiagram = (function (exports) {
13073
13235
  prevStep() {
13074
13236
  if (!this.instance)
13075
13237
  return;
13238
+ this.playInFlight = false;
13076
13239
  this.instance.anim.prev();
13077
13240
  this.syncControls();
13078
13241
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13082,6 +13245,7 @@ var AIDiagram = (function (exports) {
13082
13245
  resetAnimation() {
13083
13246
  if (!this.instance)
13084
13247
  return;
13248
+ this.playInFlight = false;
13085
13249
  this.instance.anim.reset();
13086
13250
  this.syncControls();
13087
13251
  }
@@ -13108,6 +13272,7 @@ var AIDiagram = (function (exports) {
13108
13272
  }
13109
13273
  destroy() {
13110
13274
  this.stopMotion();
13275
+ this.playInFlight = false;
13111
13276
  this.animUnsub?.();
13112
13277
  this.instance?.anim?.destroy();
13113
13278
  this.instance = null;
@@ -13140,6 +13305,9 @@ var AIDiagram = (function (exports) {
13140
13305
  this.btnRestart.disabled = true;
13141
13306
  this.btnPrev.disabled = true;
13142
13307
  this.btnNext.disabled = true;
13308
+ this.btnPlay.textContent = "Play";
13309
+ this.btnPlay.classList.remove("is-active");
13310
+ this.btnPlay.setAttribute("aria-pressed", "false");
13143
13311
  this.btnPlay.disabled = true;
13144
13312
  return;
13145
13313
  }
@@ -13148,7 +13316,10 @@ var AIDiagram = (function (exports) {
13148
13316
  this.btnRestart.disabled = false;
13149
13317
  this.btnPrev.disabled = !anim.canPrev;
13150
13318
  this.btnNext.disabled = !anim.canNext;
13151
- this.btnPlay.disabled = this.playInFlight || !anim.canNext;
13319
+ this.btnPlay.textContent = this.playInFlight ? "Stop" : "Play";
13320
+ this.btnPlay.classList.toggle("is-active", this.playInFlight);
13321
+ this.btnPlay.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
13322
+ this.btnPlay.disabled = this.playInFlight ? false : !anim.canNext;
13152
13323
  }
13153
13324
  syncViewControls() {
13154
13325
  const hasView = !!this.instance?.svg;