@zenuml/core 4.3.0 → 4.3.1

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.
@@ -9723,6 +9723,7 @@ const PARTICIPANT_ICON_WIDTH = 40;
9723
9723
  const LIFELINE_WIDTH = 1;
9724
9724
  const PARTICIPANT_TOP_SPACE_FOR_GROUP = 20;
9725
9725
  const OCCURRENCE_EMPTY_HEIGHT = 24;
9726
+ const DIAGRAM_HEADER_HEIGHT = 24 + 4 * 2 + 1;
9726
9727
 
9727
9728
  var TextType = /* @__PURE__ */ ((TextType2) => {
9728
9729
  TextType2[TextType2["MessageContent"] = 0] = "MessageContent";
@@ -10120,6 +10121,7 @@ const ASYNC_SELF_MESSAGE_HEIGHT = 44;
10120
10121
  const CREATION_MESSAGE_HEIGHT = 40;
10121
10122
  const COMMENT_LINE_HEIGHT = tw(5);
10122
10123
  const FRAGMENT_BORDER_WIDTH = 1;
10124
+ const FRAGMENT_GUARD_HEIGHT = 28;
10123
10125
 
10124
10126
  function getLocalParticipantNames(ctx) {
10125
10127
  if (!ctx) return [];
@@ -10652,7 +10654,7 @@ class FragmentAltVM extends StatementVM {
10652
10654
  let cursor = top + 1 + this.metrics.fragmentHeaderHeight + commentHeight;
10653
10655
  const ifBlock = this.alt?.ifBlock?.();
10654
10656
  if (ifBlock) {
10655
- cursor += 20;
10657
+ cursor += FRAGMENT_GUARD_HEIGHT;
10656
10658
  cursor = this.layoutBlock(
10657
10659
  ifBlock.braceBlock()?.block(),
10658
10660
  leftParticipant,
@@ -10660,7 +10662,7 @@ class FragmentAltVM extends StatementVM {
10660
10662
  );
10661
10663
  }
10662
10664
  this.alt?.elseIfBlock?.()?.forEach((block) => {
10663
- cursor += 20;
10665
+ cursor += FRAGMENT_GUARD_HEIGHT;
10664
10666
  cursor += 8;
10665
10667
  cursor += 1;
10666
10668
  cursor = this.layoutBlock(
@@ -10671,7 +10673,7 @@ class FragmentAltVM extends StatementVM {
10671
10673
  });
10672
10674
  const elseBlock = this.alt?.elseBlock?.()?.braceBlock?.()?.block?.();
10673
10675
  if (elseBlock) {
10674
- cursor += 20;
10676
+ cursor += FRAGMENT_GUARD_HEIGHT;
10675
10677
  cursor += 8;
10676
10678
  cursor += 1;
10677
10679
  cursor = this.layoutBlock(elseBlock, leftParticipant, cursor);
@@ -10706,7 +10708,7 @@ class FragmentSingleBlockVM extends StatementVM {
10706
10708
  const commentHeight = this.measureComment(this.fragment);
10707
10709
  let cursor = top + FRAGMENT_BORDER_WIDTH + this.metrics.fragmentHeaderHeight + commentHeight;
10708
10710
  const hasCondition = Boolean(this.fragment?.parExpr?.()?.condition?.());
10709
- if (hasCondition) cursor += 20;
10711
+ if (hasCondition) cursor += FRAGMENT_GUARD_HEIGHT;
10710
10712
  const block = this.fragment?.braceBlock?.()?.block?.();
10711
10713
  if (block) {
10712
10714
  const fragmentOrigin = this.findLeftParticipant(this.fragment, origin) || origin;
@@ -10955,10 +10957,14 @@ const clearPersistentCache = () => {
10955
10957
  };
10956
10958
 
10957
10959
  const FONT_FAMILY = "Helvetica, Verdana, serif";
10958
- const FONT_SIZE_PARTICIPANT = "16px";
10960
+ const FONT_SIZE_PARTICIPANT = "14px";
10961
+ const FONT_SIZE_MESSAGE = "15px";
10959
10962
  const FONT_SIZE_FRAGMENT = "14px";
10960
- function getFontSpec() {
10961
- return `${FONT_SIZE_PARTICIPANT} ${FONT_FAMILY}`;
10963
+ function getFontSize(type) {
10964
+ return type === TextType.MessageContent ? FONT_SIZE_MESSAGE : FONT_SIZE_PARTICIPANT;
10965
+ }
10966
+ function getFontSpec(type) {
10967
+ return `${getFontSize(type)} ${FONT_FAMILY}`;
10962
10968
  }
10963
10969
  let canvasCtx = null;
10964
10970
  function setCanvasContext(ctx) {
@@ -10980,18 +10986,21 @@ function getCanvasContext() {
10980
10986
  }
10981
10987
  function WidthProviderOnCanvas(text, type) {
10982
10988
  const measured = text.trim();
10983
- const cacheKey = `WidthProviderOnCanvas_${getFontSpec()}_${measured}_${type}`;
10989
+ const cacheKey = `WidthProviderOnCanvas_${getFontSpec(type)}_${measured}_${type}`;
10984
10990
  const cacheValue = getCache(cacheKey);
10985
10991
  if (cacheValue != null) {
10986
10992
  return cacheValue;
10987
10993
  }
10988
10994
  const ctx = getCanvasContext();
10989
10995
  if (!ctx) {
10990
- const width2 = Math.ceil(measured.length * 16 * 0.6);
10996
+ const width2 = Math.ceil(
10997
+ measured.length * Number.parseFloat(getFontSize(type)) * 0.6
10998
+ );
10991
10999
  setCache(cacheKey, width2);
10992
11000
  return width2;
10993
11001
  }
10994
- ctx.font = getFontSpec();
11002
+ ctx.font = getFontSpec(type);
11003
+ ctx.fontVariantCaps = "normal";
10995
11004
  const width = Math.round(ctx.measureText(measured).width);
10996
11005
  setCache(cacheKey, width, true);
10997
11006
  return width;
@@ -11000,32 +11009,34 @@ const EMOJI_PATTERN = (
11000
11009
  // eslint-disable-next-line no-misleading-character-class
11001
11010
  /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u
11002
11011
  );
11003
- function measureWithSvg(text, fontSize) {
11012
+ function measureWithSvg(text, fontSize, caps = "normal") {
11004
11013
  if (typeof document === "undefined") return null;
11005
11014
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
11006
11015
  svg.setAttribute("style", "position:absolute;left:-9999px;top:-9999px");
11007
11016
  const textEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
11008
11017
  textEl.setAttribute("font-family", FONT_FAMILY);
11009
11018
  textEl.setAttribute("font-size", fontSize);
11019
+ textEl.style.fontVariantCaps = caps;
11020
+ if (typeof textEl.getBBox !== "function") return null;
11010
11021
  textEl.textContent = text;
11011
11022
  svg.appendChild(textEl);
11012
11023
  document.body.appendChild(svg);
11013
11024
  const width = textEl.getBBox().width;
11014
11025
  document.body.removeChild(svg);
11015
- return width;
11026
+ return Number.isFinite(width) && width > 0 ? width : null;
11016
11027
  }
11017
- function measureTextWithFont(text, fontSize) {
11028
+ function measureTextWithFont(text, fontSize, caps = "normal") {
11018
11029
  const measured = text.trim();
11019
11030
  if (!measured) return 0;
11020
11031
  const hasEmoji = EMOJI_PATTERN.test(measured);
11021
11032
  const font = `${fontSize} ${FONT_FAMILY}`;
11022
- const cacheKey = hasEmoji ? `measureTextWithFont_svg_${font}_${measured}` : `measureTextWithFont_${font}_${measured}`;
11033
+ const cacheKey = (hasEmoji ? `measureTextWithFont_svg_${font}_${measured}` : `measureTextWithFont_${font}_${measured}`) + `_${caps}`;
11023
11034
  const cacheValue = getCache(cacheKey);
11024
11035
  if (cacheValue != null) {
11025
11036
  return cacheValue;
11026
11037
  }
11027
- if (hasEmoji) {
11028
- const svgWidth = measureWithSvg(measured, fontSize);
11038
+ if (hasEmoji || caps !== "normal") {
11039
+ const svgWidth = measureWithSvg(measured, fontSize, caps);
11029
11040
  if (svgWidth != null) {
11030
11041
  setCache(cacheKey, svgWidth, true);
11031
11042
  return svgWidth;
@@ -11039,7 +11050,15 @@ function measureTextWithFont(text, fontSize) {
11039
11050
  return width2;
11040
11051
  }
11041
11052
  ctx.font = font;
11042
- const width = ctx.measureText(measured).width;
11053
+ ctx.fontVariantCaps = "normal";
11054
+ const normalWidth = ctx.measureText(measured).width;
11055
+ let width = normalWidth;
11056
+ if (caps !== "normal") {
11057
+ const normalProbe = ctx.measureText("iiii").width;
11058
+ ctx.fontVariantCaps = caps;
11059
+ width = Math.abs(ctx.measureText("iiii").width - normalProbe) > 0.01 ? ctx.measureText(measured).width : Math.max(normalWidth, ctx.measureText(measured.toUpperCase()).width);
11060
+ ctx.fontVariantCaps = "normal";
11061
+ }
11043
11062
  setCache(cacheKey, width, true);
11044
11063
  return width;
11045
11064
  }
@@ -11050,6 +11069,11 @@ function measureSvgParticipantLabelWidth(text) {
11050
11069
  return measureTextWithFont(text, FONT_SIZE_PARTICIPANT);
11051
11070
  }
11052
11071
 
11072
+ const fragmentGuardKeyword = (kind, sourceKeyword) => kind === "alt" || kind === "opt" ? "if" : kind === "loop" ? sourceKeyword ?? "loop" : void 0;
11073
+ const fragmentGuardWidth = (condition, keyword) => Math.ceil(
11074
+ 10 + measureTextWithFont(resolveEmojiInText(condition), "14px") + (keyword ? measureTextWithFont(keyword, "12px", "all-small-caps") + (condition ? 4 : 0) : 0)
11075
+ );
11076
+
11053
11077
  const walker = antlr4.tree.ParseTreeWalker.DEFAULT;
11054
11078
  class FrameBuilder extends sequenceParserListener {
11055
11079
  _orderedParticipants;
@@ -11164,6 +11188,23 @@ function FrameBorder(frame) {
11164
11188
  };
11165
11189
  }
11166
11190
 
11191
+ const fragmentHeaderLabel = (kind) => {
11192
+ const labels = {
11193
+ alt: "Alt",
11194
+ loop: "Loop",
11195
+ opt: "Opt",
11196
+ par: "Par",
11197
+ critical: "Critical",
11198
+ section: "Section",
11199
+ tcf: "Try",
11200
+ ref: "Ref"
11201
+ };
11202
+ return labels[kind] ?? kind.charAt(0).toUpperCase() + kind.slice(1);
11203
+ };
11204
+ const fragmentHeaderWidth = (label, number) => Math.ceil(
11205
+ 32 + measureTextWithFont(label, "14px", "all-small-caps") * 1.1 + (number ? measureTextWithFont(number, "12px") + 15 : 0)
11206
+ );
11207
+
11167
11208
  function TotalWidth(ctx, coordinates) {
11168
11209
  const allParticipants = coordinates.orderedParticipantNames();
11169
11210
  if (!ctx || allParticipants.length === 0) return 0;
@@ -11388,7 +11429,8 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11388
11429
  if (firstStatKey) {
11389
11430
  const innerCoord = verticalCoordinates.getStatementCoordinate(firstStatKey);
11390
11431
  if (innerCoord) {
11391
- sectionY = innerCoord.top - 20 - 8 - 8 - 1;
11432
+ const labelHeight = info.fragmentKind === "alt" ? FRAGMENT_GUARD_HEIGHT : 20;
11433
+ sectionY = innerCoord.top - labelHeight - 8 - 8 - 1;
11392
11434
  }
11393
11435
  }
11394
11436
  }
@@ -11397,11 +11439,14 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11397
11439
  const sectionHeight = i < info.fragmentSections.length - 1 ? 0 : coord.top + coord.height - sectionY;
11398
11440
  sections.push({
11399
11441
  label: section.label,
11442
+ guardKeyword: section.guardKeyword,
11400
11443
  y: sectionY,
11401
11444
  height: sectionHeight,
11402
11445
  labelWidth: section.label ? measureSvgFragmentLabelWidth(resolveEmojiInText(section.label)) : void 0,
11403
- innerLabel: /^\[\s*.*\s*\]$/.test(section.label) ? section.label.slice(1, -1).trim() : void 0,
11404
- innerLabelWidth: /^\[\s*.*\s*\]$/.test(section.label) ? measureSvgFragmentLabelWidth(
11446
+ innerLabel: section.condition ?? (/^\[\s*.*\s*\]$/.test(section.label) ? section.label.slice(1, -1).trim() : void 0),
11447
+ innerLabelWidth: section.condition !== void 0 ? measureSvgFragmentLabelWidth(
11448
+ resolveEmojiInText(section.condition)
11449
+ ) : /^\[\s*.*\s*\]$/.test(section.label) ? measureSvgFragmentLabelWidth(
11405
11450
  resolveEmojiInText(section.label.slice(1, -1).trim())
11406
11451
  ) : void 0,
11407
11452
  keyword: (() => {
@@ -11472,7 +11517,9 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11472
11517
  return {
11473
11518
  fragment: {
11474
11519
  kind: info.fragmentKind,
11520
+ headerLabel: info.fragmentHeaderLabel,
11475
11521
  label: info.fragmentLabel || "",
11522
+ guardKeyword: info.guardKeyword,
11476
11523
  labelWidth: info.fragmentLabel ? measureSvgFragmentLabelWidth(resolveEmojiInText(info.fragmentLabel)) : void 0,
11477
11524
  x: fragX,
11478
11525
  y: coord.top,
@@ -11688,6 +11735,8 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11688
11735
  hasBlock: false,
11689
11736
  fragmentKind: fragmentInfo.fragmentKind,
11690
11737
  fragmentLabel: fragmentInfo.label,
11738
+ fragmentHeaderLabel: fragmentInfo.headerLabel,
11739
+ guardKeyword: fragmentInfo.guardKeyword,
11691
11740
  fragmentSections: fragmentInfo.sections,
11692
11741
  comment,
11693
11742
  statNode: stat,
@@ -11714,7 +11763,9 @@ function extractFragmentInfo(stat) {
11714
11763
  const label = condition?.getFormattedText?.() || "";
11715
11764
  return {
11716
11765
  fragmentKind: kind,
11766
+ headerLabel: frag.atom?.()?.getFormattedText?.(),
11717
11767
  label,
11768
+ guardKeyword: kind === "loop" ? frag.WHILE?.()?.getText?.() : void 0,
11718
11769
  sections: [{ label, blockNode: frag.braceBlock?.()?.block?.() }]
11719
11770
  };
11720
11771
  }
@@ -11726,6 +11777,8 @@ function extractFragmentInfo(stat) {
11726
11777
  if (ifBlock) {
11727
11778
  sections.push({
11728
11779
  label: "Alt",
11780
+ guardKeyword: "if",
11781
+ condition: ifBlock.parExpr?.()?.condition?.()?.getFormattedText?.() || "",
11729
11782
  blockNode: ifBlock.braceBlock?.()?.block?.()
11730
11783
  });
11731
11784
  }
@@ -11733,14 +11786,17 @@ function extractFragmentInfo(stat) {
11733
11786
  const condition = elseIf.parExpr?.()?.condition?.();
11734
11787
  const label = condition?.getFormattedText?.() || "";
11735
11788
  sections.push({
11736
- label: `[ ${label} ]`,
11789
+ label: `else if ${label}`,
11790
+ guardKeyword: "else if",
11791
+ condition: label,
11737
11792
  blockNode: elseIf.braceBlock?.()?.block?.()
11738
11793
  });
11739
11794
  }
11740
11795
  const elseBlock = alt.elseBlock?.();
11741
11796
  if (elseBlock) {
11742
11797
  sections.push({
11743
- label: "[else]",
11798
+ label: "else",
11799
+ guardKeyword: "else",
11744
11800
  blockNode: elseBlock.braceBlock?.()?.block?.()
11745
11801
  });
11746
11802
  }
@@ -12381,13 +12437,36 @@ function buildGeometry(input) {
12381
12437
  participants,
12382
12438
  measureText
12383
12439
  );
12384
- const { messages, selfCalls, occurrences, creations, fragments, returns, dividers, comments } = buildResult;
12440
+ const {
12441
+ messages,
12442
+ selfCalls,
12443
+ occurrences,
12444
+ creations,
12445
+ fragments,
12446
+ returns,
12447
+ dividers,
12448
+ comments
12449
+ } = buildResult;
12450
+ if (input.enableNumbering === false) {
12451
+ const numberedItems = [
12452
+ ...messages,
12453
+ ...selfCalls,
12454
+ ...fragments,
12455
+ ...returns,
12456
+ ...creations.map((creation) => creation.message)
12457
+ ];
12458
+ for (const numbered of numberedItems) {
12459
+ numbered.number = void 0;
12460
+ }
12461
+ }
12385
12462
  let maxOccBottom = 0;
12386
12463
  let maxOtherY = 0;
12387
- for (const o of occurrences) maxOccBottom = Math.max(maxOccBottom, o.y + o.height);
12464
+ for (const o of occurrences)
12465
+ maxOccBottom = Math.max(maxOccBottom, o.y + o.height);
12388
12466
  for (const m of messages) maxOtherY = Math.max(maxOtherY, m.y);
12389
12467
  for (const s of selfCalls) maxOtherY = Math.max(maxOtherY, s.y + s.height);
12390
- for (const c of creations) maxOtherY = Math.max(maxOtherY, c.message.y + PARTICIPANT_VISUAL_HEIGHT);
12468
+ for (const c of creations)
12469
+ maxOtherY = Math.max(maxOtherY, c.message.y + PARTICIPANT_VISUAL_HEIGHT);
12391
12470
  for (const f of fragments) maxOtherY = Math.max(maxOtherY, f.y + f.height);
12392
12471
  for (const d of dividers) maxOtherY = Math.max(maxOtherY, d.y);
12393
12472
  const diagramHeight = Math.max(
@@ -12397,10 +12476,7 @@ function buildGeometry(input) {
12397
12476
  buildResult.maxReturnBottom
12398
12477
  );
12399
12478
  const lifelineBottom = diagramHeight + PARTICIPANT_VISUAL_HEIGHT - 28;
12400
- const lifelines = buildLifelines(
12401
- participants,
12402
- lifelineBottom
12403
- );
12479
+ const lifelines = buildLifelines(participants, lifelineBottom);
12404
12480
  let diagramWidth = TotalWidth(rootContext, coordinates) - frameBorder.left - frameBorder.right;
12405
12481
  if (measureText) {
12406
12482
  const LABEL_PAD = 10;
@@ -12452,6 +12528,42 @@ function buildGeometry(input) {
12452
12528
  f.width = targetRight - f.x;
12453
12529
  }
12454
12530
  }
12531
+ for (const inner of [...fragments].sort((a, b) => a.height - b.height)) {
12532
+ inner.width = Math.max(
12533
+ inner.width,
12534
+ fragmentHeaderWidth(
12535
+ inner.headerLabel ?? fragmentHeaderLabel(inner.kind),
12536
+ inner.number
12537
+ )
12538
+ );
12539
+ if (inner.label)
12540
+ inner.width = Math.max(
12541
+ inner.width,
12542
+ fragmentGuardWidth(
12543
+ inner.label,
12544
+ fragmentGuardKeyword(inner.kind, inner.guardKeyword)
12545
+ )
12546
+ );
12547
+ for (const section of inner.sections) {
12548
+ if (section.guardKeyword)
12549
+ inner.width = Math.max(
12550
+ inner.width,
12551
+ fragmentGuardWidth(section.innerLabel ?? "", section.guardKeyword)
12552
+ );
12553
+ }
12554
+ for (const outer of fragments) {
12555
+ if (outer.y < inner.y && outer.y + outer.height >= inner.y + inner.height && outer.x <= inner.x) {
12556
+ outer.width = Math.max(
12557
+ outer.width,
12558
+ inner.x + inner.width + FRAGMENT_PADDING_X - outer.x
12559
+ );
12560
+ }
12561
+ }
12562
+ diagramWidth = Math.max(
12563
+ diagramWidth,
12564
+ inner.x + inner.width - frameBorder.right
12565
+ );
12566
+ }
12455
12567
  const groups = buildGroups(participants, diagramHeight);
12456
12568
  return {
12457
12569
  width: diagramWidth,
@@ -12550,7 +12662,7 @@ function styleToAttr(style) {
12550
12662
  return Object.entries(style).map(([k, v]) => `${esc(k)}: ${esc(v)}`).join("; ");
12551
12663
  }
12552
12664
 
12553
- const STROKE_WIDTH$1 = 2;
12665
+ const STROKE_WIDTH$1 = 1;
12554
12666
  const HALF_STROKE$1 = STROKE_WIDTH$1 / 2;
12555
12667
  const ICON_SIZE = 24;
12556
12668
  const ICON_MARGIN_RIGHT = 4;
@@ -12558,12 +12670,12 @@ const ICON_PAINT_OFFSET_X = 4;
12558
12670
  const LABEL_PAD_LEFT = 8;
12559
12671
  const LABEL_HORIZONTAL_PADDING = 16;
12560
12672
  const STEREOTYPE_VERTICAL_OFFSET = 8;
12561
- const STEREOTYPE_FONT_SIZE = 16;
12673
+ const STEREOTYPE_FONT_SIZE = 14;
12562
12674
  const BOUNDARY_ICON_VERTICAL_TWEAK = 2.75;
12563
12675
  const PARTICIPANT_TEXT_FILL = "#222";
12564
12676
  function renderParticipant(p) {
12565
12677
  if (p.isStarter) return renderStarterParticipant(p);
12566
- const rx = 3;
12678
+ const rx = 4 - HALF_STROKE$1;
12567
12679
  const x = p.x - p.width / 2 + HALF_STROKE$1;
12568
12680
  const rectY = p.y + HALF_STROKE$1;
12569
12681
  const rectW = p.width - STROKE_WIDTH$1;
@@ -12632,7 +12744,7 @@ function renderStarterParticipant(p) {
12632
12744
  if (!icon) {
12633
12745
  return "";
12634
12746
  }
12635
- const rx = 3;
12747
+ const rx = 4 - HALF_STROKE$1;
12636
12748
  const boxX = p.x - p.width / 2 + HALF_STROKE$1;
12637
12749
  const rectY = p.y + HALF_STROKE$1;
12638
12750
  const rectW = p.width - STROKE_WIDTH$1;
@@ -12672,21 +12784,31 @@ function renderLifeline(l) {
12672
12784
  return `<line x1="${x}" y1="${l.topY}" x2="${x}" y2="${l.bottomY}" class="lifeline" stroke-dasharray="${l.dashed ? "5,5" : "none"}" stroke-dashoffset="${l.dashed ? "5" : "0"}" shape-rendering="crispEdges"/>`;
12673
12785
  }
12674
12786
 
12787
+ function messageNumberWidth(number) {
12788
+ return measureTextWithFont(number, "12px") + 8;
12789
+ }
12790
+ function renderMessageNumber(number, right, baseline) {
12791
+ if (!number) return "";
12792
+ return `<rect x="${right - messageNumberWidth(number)}" y="${baseline - 12}" width="${messageNumberWidth(number)}" height="16" rx="2" class="message-number-bg"/>
12793
+ <text x="${right - 4}" y="${baseline}" text-anchor="end" class="seq-number">${esc(number)}</text>`;
12794
+ }
12795
+
12675
12796
  function renderMessage(m) {
12676
12797
  const isLTR = m.fromX < m.toX;
12677
12798
  const fromX = isLTR ? m.fromX + 1 : m.fromX;
12678
12799
  const toX = isLTR ? m.toX : m.toX + 1;
12679
12800
  const direction = Math.sign(m.toX - m.fromX);
12680
12801
  const labelX = (m.fromX + m.toX) / 2 - direction * 3.5 + 0.5;
12681
- const labelY = m.y - 3.5;
12802
+ const labelY = m.y - 2.5;
12682
12803
  const dashAttr = m.arrowStyle === "dashed" ? ' stroke-dasharray="6,4"' : "";
12683
12804
  const styleAttr = m.style ? ` style="${styleToAttr(m.style)}"` : "";
12684
12805
  const numberX = Math.min(fromX, toX) - 4;
12685
- const numberSvg = m.number ? `<text x="${numberX}" y="${labelY}" text-anchor="end" class="seq-number">${esc(m.number)}</text>` : "";
12686
- const lineY = m.y - 0.5;
12806
+ const numberSvg = renderMessageNumber(m.number, numberX, m.y - 3.5);
12807
+ const headCenterY = m.y - 0.5;
12808
+ const lineY = headCenterY + 0.5;
12687
12809
  return `<g class="message">
12688
12810
  <line x1="${fromX}" y1="${lineY}" x2="${toX}" y2="${lineY}" class="message-line"${dashAttr}/>
12689
- ${renderArrowHead(toX, lineY, m.isReverse, m.arrowStyle)}
12811
+ ${renderArrowHead(toX, headCenterY, m.isReverse, m.arrowStyle)}
12690
12812
  <text x="${labelX}" y="${labelY}" text-anchor="middle" class="message-label"${styleAttr}>${esc(resolveEmojiInText(m.label))}</text>
12691
12813
  ${numberSvg}
12692
12814
  </g>`;
@@ -12697,7 +12819,7 @@ function renderSelfCall(s) {
12697
12819
  const labelX = x1 + 6;
12698
12820
  const labelY = s.y + (isAsync ? 15 : 12);
12699
12821
  const numberY = s.y + 12;
12700
- const numberSvg = s.number ? `<text x="${x1 - 3}" y="${numberY}" text-anchor="end" class="seq-number">${esc(s.number)}</text>` : "";
12822
+ const numberSvg = renderMessageNumber(s.number, x1 - 3, numberY);
12701
12823
  const svgX = x1 + 1;
12702
12824
  const svgY = s.y + (isAsync ? 20 : 14);
12703
12825
  const pathEnd = isAsync ? "L1,15" : "L14,15";
@@ -12706,11 +12828,11 @@ function renderSelfCall(s) {
12706
12828
  const arrowPath = isAsync ? "M1 1.25 L6.15 4.5 L1 7.75" : "M1 1.25 L6.15 4.5 L1 7.75 Z";
12707
12829
  return `<g class="message self-call">
12708
12830
  <svg x="${svgX}" y="${svgY}" width="30" height="24">
12709
- <path d="M0,2 L26,2 Q28,2 28,4 L28,13 Q28,15 26,15 ${pathEnd}" fill="none" stroke="#000" stroke-width="2"/>
12831
+ <path d="M0,2 L26,2 Q28,2 28,4 L28,13 Q28,15 26,15 ${pathEnd}" fill="none" stroke="#000" stroke-width="1"/>
12710
12832
  <g transform="translate(${arrowTx}, 10)">
12711
12833
  <svg height="10" width="7" viewBox="0 0 7 9">
12712
12834
  <g transform="scale(-1, 1) translate(-7, 0)">
12713
- <path d="${arrowPath}" stroke="#000" stroke-linecap="round" fill="${arrowFill}" stroke-width="2"/>
12835
+ <path d="${arrowPath}" stroke="#000" stroke-linecap="round" fill="${arrowFill}" stroke-width="1"/>
12714
12836
  </g>
12715
12837
  </svg>
12716
12838
  </g>
@@ -12728,96 +12850,122 @@ function renderArrowHead(tipX, tipY, pointsLeft, style) {
12728
12850
  const rtlTransform = pointsLeft ? ' transform="scale(-1, 1) translate(-7, 0)"' : "";
12729
12851
  return `<svg x="${svgX}" y="${svgY}" width="7" height="10" viewBox="0 0 7 9" overflow="visible" class="arrow-head${isFilled ? "" : " arrow-open"}">
12730
12852
  <g${rtlTransform}>
12731
- <path d="${pathD}" stroke="#000" stroke-linecap="round" stroke-width="2" fill="${fillAttr}"/>
12853
+ <path d="${pathD}" transform="translate(1 0.5)" stroke="#000" stroke-linecap="round" stroke-width="1" fill="${fillAttr}"/>
12732
12854
  </g>
12733
12855
  </svg>`;
12734
12856
  }
12735
12857
 
12736
12858
  function renderOccurrence(o) {
12737
- const strokeHalf = 1;
12859
+ const strokeHalf = 0.5;
12738
12860
  const rx = o.x + strokeHalf;
12739
- return `<rect x="${rx}" y="${o.y + strokeHalf}" width="${o.width - strokeHalf * 2}" height="${o.height - strokeHalf * 2}" rx="1" class="occurrence" data-participant="${esc(o.participantName)}"/>`;
12861
+ return `<rect x="${rx}" y="${o.y + strokeHalf}" width="${o.width - strokeHalf * 2}" height="${o.height - strokeHalf * 2}" rx="1.5" class="occurrence" data-participant="${esc(o.participantName)}"/>`;
12740
12862
  }
12741
12863
 
12742
12864
  const HEADER_HEIGHT = DEFAULT_LAYOUT_METRICS.fragmentHeaderHeight;
12743
- const BRACKET_WIDTH = 3.89;
12744
12865
  const TEXT_PAD_X = 4;
12866
+ const NUMBER_PAD_X = 4;
12745
12867
  const STROKE_WIDTH = 1;
12746
12868
  const HALF_STROKE = STROKE_WIDTH / 2;
12747
12869
  function renderFragment(f) {
12748
12870
  const parts = [];
12871
+ const kindLabel = f.headerLabel ?? fragmentHeaderLabel(f.kind);
12872
+ const width = Math.max(f.width, fragmentHeaderWidth(kindLabel, f.number));
12749
12873
  parts.push(
12750
- `<rect x="${f.x + HALF_STROKE}" y="${f.y + HALF_STROKE}" width="${f.width - STROKE_WIDTH}" height="${f.height - STROKE_WIDTH}" rx="4" class="fragment-border"/>`
12874
+ `<rect x="${f.x + HALF_STROKE}" y="${f.y + HALF_STROKE}" width="${width - STROKE_WIDTH}" height="${f.height - STROKE_WIDTH}" rx="${4 - HALF_STROKE}" class="fragment-border"/>`
12751
12875
  );
12752
12876
  const headerX = f.x + 1;
12753
12877
  const headerY = f.headerY;
12754
- const headerW = f.width - 2;
12878
+ const headerW = width - 2;
12755
12879
  parts.push(
12756
- `<rect x="${headerX}" y="${headerY}" width="${headerW}" height="${HEADER_HEIGHT}" class="fragment-header"/>`
12880
+ `<rect x="${headerX}" y="${headerY}" width="${headerW}" height="${HEADER_HEIGHT}" class="fragment-header"${f.kind === "opt" ? "" : ' style="clip-path: inset(0 round 4px 4px 0 0)"'}/>`
12757
12881
  );
12758
- const iconX = headerX + 4;
12882
+ const numberWidth = f.number ? Math.ceil(measureTextWithFont(f.number, "12px") * 64) / 64 : 0;
12883
+ const numberInset = f.number ? numberWidth + 14 : 0;
12884
+ const titleY = headerY + HEADER_HEIGHT / 2 - 0.5;
12885
+ if (f.number) {
12886
+ const numberBoxX = headerX + 4;
12887
+ const numberX = numberBoxX + NUMBER_PAD_X;
12888
+ parts.push(
12889
+ `<rect x="${numberBoxX}" y="${headerY + (HEADER_HEIGHT - 1 - 16) / 2}" width="${numberWidth + NUMBER_PAD_X * 2}" height="16" rx="2" class="fragment-number-bg"/>`,
12890
+ `<text x="${numberX}" y="${titleY}" dominant-baseline="central" class="seq-number">${esc(f.number)}</text>`
12891
+ );
12892
+ }
12893
+ const iconX = headerX + 4 + numberInset;
12759
12894
  const iconY = headerY;
12760
12895
  parts.push(getFragmentIcon(f.kind, iconX, iconY));
12761
- const kindLabel = getKindLabel(f.kind);
12762
- const labelX = headerX + 26;
12896
+ const labelX = headerX + 26 + numberInset;
12763
12897
  parts.push(
12764
12898
  `<text x="${labelX}" y="${headerY + HEADER_HEIGHT / 2 - 0.5}" dominant-baseline="central" class="fragment-label">${esc(kindLabel)}</text>`
12765
12899
  );
12766
- if (f.number) {
12900
+ if (f.label) {
12767
12901
  parts.push(
12768
- `<text x="${f.x - 3}" y="${headerY + HEADER_HEIGHT / 2 - 4.5}" text-anchor="end" dominant-baseline="central" class="seq-number">${esc(f.number)}</text>`
12902
+ renderGuardRow(
12903
+ headerX,
12904
+ headerY + HEADER_HEIGHT,
12905
+ f.label,
12906
+ fragmentGuardKeyword(f.kind, f.guardKeyword)
12907
+ )
12769
12908
  );
12770
12909
  }
12771
- if (f.label) {
12772
- const condY = headerY + HEADER_HEIGHT + 15;
12773
- parts.push(renderBracketedLabel(headerX, condY, f.label, f.labelWidth, "fragment-condition"));
12774
- }
12775
12910
  if (f.sections.length > 1) {
12776
12911
  for (let i = 1; i < f.sections.length; i++) {
12777
12912
  const section = f.sections[i];
12778
12913
  const lineY = section.y;
12779
12914
  const separatorY = lineY + HALF_STROKE;
12780
12915
  const sepX1 = f.x + 1 + (section.contentInsetLeft ?? 0);
12781
- const sepX2 = f.x + f.width - 1;
12916
+ const sepX2 = f.x + width - 1;
12782
12917
  parts.push(
12783
12918
  `<line x1="${sepX1}" y1="${separatorY}" x2="${sepX2}" y2="${separatorY}" class="fragment-separator"/>`
12784
12919
  );
12785
12920
  if (section.label) {
12921
+ if (f.kind === "alt") {
12922
+ parts.push(
12923
+ renderGuardRow(
12924
+ f.x + 1,
12925
+ lineY + 1,
12926
+ section.innerLabel ?? "",
12927
+ section.guardKeyword
12928
+ )
12929
+ );
12930
+ continue;
12931
+ }
12786
12932
  const labelY = lineY + 16;
12787
12933
  const isFinally = section.label.startsWith("finally");
12788
12934
  const isBracketed = !!section.innerLabel && section.label !== "[else]";
12789
12935
  if (isBracketed) {
12790
12936
  const labelX2 = f.x + 1;
12791
- parts.push(
12792
- renderBracketedLabel(labelX2, labelY, section.innerLabel, section.innerLabelWidth, "fragment-section-label")
12793
- );
12937
+ parts.push(renderGuardRow(labelX2, lineY + 1, section.innerLabel));
12794
12938
  continue;
12795
12939
  }
12796
12940
  const spaceIdx = section.label.indexOf(" ");
12797
12941
  if (spaceIdx > 0 && !isFinally) {
12798
12942
  const keyword = section.keyword || section.label.substring(0, spaceIdx);
12799
12943
  const condition = section.detail || section.label.substring(spaceIdx + 1);
12800
- const keywordWidth = section.keywordWidth ?? keyword.length * 7;
12944
+ const keywordWidth = measureTextWithFont(
12945
+ keyword,
12946
+ "14px",
12947
+ "all-small-caps"
12948
+ );
12801
12949
  const keywordX = f.x + 5;
12802
12950
  const conditionX = keywordX + keywordWidth + TEXT_PAD_X * 2;
12803
- const bgWidth = (section.keywordWidth ?? keyword.length * 7) + (section.detailWidth ?? condition.length * 7) + TEXT_PAD_X * 4;
12951
+ const bgWidth = keywordWidth + (section.detailWidth ?? condition.length * 7) + TEXT_PAD_X * 4;
12804
12952
  parts.push(
12805
- `<g opacity="0.65"><rect x="${keywordX - TEXT_PAD_X}" y="${lineY + 1}" width="${bgWidth}" height="20" fill="#fff"/><text x="${keywordX}" y="${labelY}" class="fragment-section-label" fill="#222">${esc(keyword)}</text><text x="${conditionX}" y="${labelY}" class="fragment-section-label" fill="#222">${esc(condition)}</text></g>`
12953
+ `<g opacity="0.65"><rect x="${keywordX - TEXT_PAD_X}" y="${lineY + 1}" width="${bgWidth}" height="20" fill="#fff"/><text x="${keywordX}" y="${labelY}" class="fragment-section-label fragment-section-keyword" fill="#222">${esc(keyword)}</text><text x="${conditionX}" y="${labelY}" class="fragment-section-label" fill="#222">${esc(condition)}</text></g>`
12806
12954
  );
12807
12955
  } else {
12808
12956
  const finallyX = f.x + 5;
12809
12957
  const finallyY = labelY;
12810
12958
  if (section.label === "[else]") {
12811
12959
  parts.push(
12812
- `<text x="${finallyX}" y="${finallyY}" class="fragment-section-label">${esc(section.label)}</text>`
12960
+ `<text x="${finallyX}" y="${finallyY}" class="fragment-section-label fragment-section-keyword">${esc(section.label)}</text>`
12813
12961
  );
12814
12962
  continue;
12815
12963
  }
12816
- const bgWidth = (section.labelWidth ?? section.label.length * 7) + TEXT_PAD_X * 2;
12964
+ const bgWidth = (isFinally ? measureTextWithFont(section.label, "14px", "all-small-caps") : section.labelWidth ?? section.label.length * 7) + TEXT_PAD_X * 2;
12817
12965
  const bgY = lineY + 1;
12818
12966
  const bgHeight = 20;
12819
12967
  parts.push(
12820
- `<g opacity="0.65"><rect x="${finallyX - TEXT_PAD_X}" y="${bgY}" width="${bgWidth}" height="${bgHeight}" fill="#fff"/><text x="${finallyX}" y="${finallyY}" class="fragment-section-label">${esc(section.label)}</text></g>`
12968
+ `<g opacity="0.65"><rect x="${finallyX - TEXT_PAD_X}" y="${bgY}" width="${bgWidth}" height="${bgHeight}" fill="#fff"/><text x="${finallyX}" y="${finallyY}" class="fragment-section-label fragment-section-keyword">${esc(section.label)}</text></g>`
12821
12969
  );
12822
12970
  }
12823
12971
  }
@@ -12827,33 +12975,11 @@ function renderFragment(f) {
12827
12975
  ${parts.join("\n ")}
12828
12976
  </g>`;
12829
12977
  }
12830
- function renderBracketedLabel(x, y, innerText, innerWidth, cls = "fragment-condition") {
12831
- const measuredInnerWidth = innerWidth ?? innerText.length * 7;
12832
- const innerX = x + BRACKET_WIDTH + TEXT_PAD_X;
12833
- const closeX = innerX + measuredInnerWidth + TEXT_PAD_X;
12834
- return `<g><text x="${x}" y="${y}" class="${cls}">[</text><text x="${innerX}" y="${y}" class="${cls}" opacity="0.65">${esc(resolveEmojiInText(innerText))}</text><text x="${closeX}" y="${y}" class="${cls}">]</text></g>`;
12835
- }
12836
- function getKindLabel(kind) {
12837
- switch (kind) {
12838
- case "alt":
12839
- return "Alt";
12840
- case "loop":
12841
- return "Loop";
12842
- case "opt":
12843
- return "Opt";
12844
- case "par":
12845
- return "Par";
12846
- case "critical":
12847
- return "Critical";
12848
- case "section":
12849
- return "Section";
12850
- case "tcf":
12851
- return "Try";
12852
- case "ref":
12853
- return "Ref";
12854
- default:
12855
- return kind.charAt(0).toUpperCase() + kind.slice(1);
12856
- }
12978
+ function renderGuardRow(x, rowY, innerText, keyword) {
12979
+ const baseline = rowY + 19;
12980
+ const textX = x + 4;
12981
+ const conditionX = textX + (keyword ? measureTextWithFont(keyword, "12px", "all-small-caps") + 4 : 0);
12982
+ return `<g class="guard-row">` + (keyword ? `<text x="${textX}" y="${baseline}" class="guard-keyword">${esc(keyword)}</text>` : "") + (innerText ? `<text x="${conditionX}" y="${baseline}" class="fragment-condition" opacity="0.65">${esc(resolveEmojiInText(innerText))}</text>` : "") + `</g>`;
12857
12983
  }
12858
12984
  function getFragmentIcon(kind, x, y) {
12859
12985
  const w = 20;
@@ -12937,7 +13063,7 @@ function renderOpenArrow(tipX, tipY, pointsLeft) {
12937
13063
  const x1 = tipX + dir * w;
12938
13064
  const y1 = tipY - halfH;
12939
13065
  const y2 = tipY + halfH;
12940
- return `<polyline points="${x1},${y1} ${tipX},${tipY} ${x1},${y2}" fill="none" stroke-linecap="round" class="arrow-head arrow-open"/>`;
13066
+ return `<polyline points="${x1},${y1} ${tipX},${tipY} ${x1},${y2}" transform="translate(${pointsLeft ? -1 : 1} 0)" fill="none" stroke-linecap="round" class="arrow-head arrow-open"/>`;
12941
13067
  }
12942
13068
 
12943
13069
  function renderReturn(r) {
@@ -12947,8 +13073,9 @@ function renderReturn(r) {
12947
13073
  const minX = Math.min(r.fromX, r.toX);
12948
13074
  const ARROW_PADDING_HALF = 3.5;
12949
13075
  const labelX = minX + Math.abs(r.toX - r.fromX) / 2 + (r.isReverse ? ARROW_PADDING_HALF : -ARROW_PADDING_HALF);
12950
- const lineY = Math.floor(r.y);
12951
- const labelY = lineY - 3;
13076
+ const snappedY = Math.floor(r.y);
13077
+ const lineY = snappedY + 0.5;
13078
+ const labelY = snappedY - 3;
12952
13079
  const arrowTipX = r.toX;
12953
13080
  const w = 5.15;
12954
13081
  const halfH = 3.25;
@@ -12957,10 +13084,10 @@ function renderReturn(r) {
12957
13084
  const ay1 = lineY - halfH;
12958
13085
  const ay2 = lineY + halfH;
12959
13086
  const numberX = Math.min(r.fromX, r.toX) - 4;
12960
- const numberSvg = r.number ? `<text x="${numberX}" y="${labelY}" text-anchor="end" class="seq-number">${esc(r.number)}</text>` : "";
13087
+ const numberSvg = renderMessageNumber(r.number, numberX, labelY);
12961
13088
  return `<g class="return">
12962
13089
  <line x1="${r.fromX}" y1="${lineY}" x2="${r.toX}" y2="${lineY}" class="return-line"/>
12963
- <polyline points="${ax1},${ay1} ${arrowTipX},${lineY} ${ax1},${ay2}" fill="none" stroke-linecap="round" class="return-arrow"/>
13090
+ <polyline points="${ax1},${ay1} ${arrowTipX},${lineY} ${ax1},${ay2}" transform="translate(${dir} 0)" fill="none" stroke-linecap="round" class="return-arrow"/>
12964
13091
  <text x="${labelX}" y="${labelY}" text-anchor="middle" class="return-label">${esc(resolveEmojiInText(r.label))}</text>
12965
13092
  ${numberSvg}
12966
13093
  </g>`;
@@ -13202,39 +13329,43 @@ function buildThemeStyles(p) {
13202
13329
  .frame-border-inner { fill: ${p.frameBg}; }
13203
13330
  .frame-header-bg { fill: ${p.frameBg}; }
13204
13331
  .frame-header-line { stroke: ${p.frameBorder}; stroke-width: 1; shape-rendering: crispEdges; }
13205
- .frame-title { font-family: ${p.fontFamily}; font-size: 16px; font-weight: 600; fill: ${p.titleText}; }
13206
- .participant-box { fill: ${p.participantBg}; stroke: ${p.participantBorder}; stroke-width: 2; }
13207
- .participant-label { font-family: ${p.fontFamily}; font-size: 16px; fill: ${p.participantText}; }
13332
+ .frame-title { font-family: ${p.fontFamily}; font-size: 14px; font-weight: 400; fill: ${p.titleText}; }
13333
+ .participant-box { fill: ${p.participantBg}; stroke: ${p.participantBorder}; stroke-width: 1; }
13334
+ .participant-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.participantText}; }
13208
13335
  .participant-icon { color: ${p.participantText}; }
13209
13336
  .participant-icon [fill="currentColor"]:not([stroke]) { stroke: ${p.participantBorder}; stroke-width: 1; }
13210
13337
  .lifeline { stroke: ${p.participantBorder}; stroke-width: 1; }
13211
- .message-line { stroke: ${p.messageArrow}; stroke-width: 2; shape-rendering: crispEdges; }
13212
- .message-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.messageText}; }
13213
- .arrow-head { fill: ${p.messageArrow}; stroke: ${p.messageArrow}; stroke-width: 2; }
13338
+ .message-line { stroke: ${p.messageArrow}; stroke-width: 1; shape-rendering: crispEdges; }
13339
+ .message-label { font-family: ${p.fontFamily}; font-size: 15px; fill: ${p.messageText}; }
13340
+ .arrow-head { fill: ${p.messageArrow}; stroke: ${p.messageArrow}; stroke-width: 1; }
13214
13341
  .arrow-open { fill: none; }
13215
- .occurrence { fill: ${p.occurrenceBg}; stroke: ${p.occurrenceBorder}; stroke-width: 2; shape-rendering: crispEdges; }
13216
- .fragment-border { fill: none; stroke: ${p.fragmentBorder}; stroke-width: 1; shape-rendering: crispEdges; }
13342
+ .occurrence { fill: ${p.occurrenceBg}; stroke: ${p.occurrenceBorder}; stroke-width: 1; shape-rendering: crispEdges; }
13343
+ .fragment-border { fill: none; stroke: ${p.fragmentBorder}; stroke-width: 1; }
13217
13344
  .fragment-header { fill: ${p.fragmentHeaderBg}; fill-opacity: ${p.fragmentHeaderOpacity}; stroke: none; shape-rendering: crispEdges; }
13218
- .fragment-label { font-family: ${p.fontFamily}; font-size: 14px; font-weight: 600; fill: ${p.fragmentText}; }
13345
+ .fragment-number-bg { fill: #6b7280; fill-opacity: 0.1; stroke: none; }
13346
+ .message-number-bg { fill: #6b7280; fill-opacity: 0.1; stroke: none; }
13347
+ .fragment-label { font-family: ${p.fontFamily}; font-size: 14px; font-weight: 400; font-variant-caps: all-small-caps; fill: ${p.fragmentText}; }
13219
13348
  .fragment-condition { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.fragmentText}; }
13220
13349
  .fragment-separator { stroke: ${p.fragmentSeparator}; stroke-width: 1; shape-rendering: crispEdges; }
13350
+ .fragment-section-keyword { font-variant-caps: all-small-caps; }
13221
13351
  .fragment-section-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.fragmentText}; }
13222
- .return-line { stroke: ${p.messageArrow}; stroke-width: 2; stroke-dasharray: 6,4; shape-rendering: crispEdges; }
13223
- .return-arrow { stroke: ${p.messageArrow}; stroke-width: 2; fill: none; }
13224
- .return-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.messageText}; }
13352
+ .return-line { stroke: ${p.messageArrow}; stroke-width: 1; stroke-dasharray: 6,4; shape-rendering: crispEdges; }
13353
+ .return-arrow { stroke: ${p.messageArrow}; stroke-width: 1; fill: none; }
13354
+ .return-label { font-family: ${p.fontFamily}; font-size: 15px; fill: ${p.messageText}; }
13225
13355
  .return-icon { fill: ${p.messageText}; }
13226
13356
  .divider-line { stroke: #aaaa33; stroke-width: 1; }
13227
13357
  .divider-bg { fill: #fff5ad; stroke: #aaaa33; stroke-width: 1; }
13228
13358
  .divider-label { font-family: ${p.fontFamily}; font-size: 14px; fill: #333; }
13229
13359
  .comment-text { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.commentText}; opacity: 0.5; }
13230
- .seq-number { font-family: ${p.fontFamily}; font-size: 12px; font-weight: 100; fill: #6b7280; }
13360
+ .guard-keyword { font-family: ${p.fontFamily}; font-size: 12px; font-weight: 400; font-variant-caps: all-small-caps; fill: #6b7280; }
13361
+ .seq-number { font-family: ${p.fontFamily}; font-size: 12px; font-weight: 400; fill: #6b7280; }
13231
13362
  .group-outline { fill: none; stroke: ${p.participantBorder}; }
13232
13363
  .group-title-bg { fill: ${p.frameBg}; stroke: none; }
13233
13364
  .group-title-text { font-family: ${p.fontFamily}; font-size: 13px; font-weight: 400; fill: ${p.participantText}; }
13234
13365
  `;
13235
13366
  }
13236
13367
 
13237
- const FRAME_HEADER_HEIGHT = 28;
13368
+ const FRAME_VIEWPORT_HEADER_ALLOWANCE = 28;
13238
13369
  const FRAME_BORDER_RADIUS = 4;
13239
13370
  function renderToSvg(code, options) {
13240
13371
  const rootContext = RootContext(code);
@@ -13258,14 +13389,41 @@ function renderToSvg(code, options) {
13258
13389
  coordinates,
13259
13390
  verticalCoordinates,
13260
13391
  title,
13261
- measureText: WidthProviderOnCanvas
13392
+ measureText: WidthProviderOnCanvas,
13393
+ enableNumbering: options?.enableNumbering
13262
13394
  });
13263
13395
  return { ...composeSvg(geometry, options), geometry };
13264
13396
  }
13265
13397
  function composeSvg(g, options) {
13266
13398
  const padding = 10;
13267
- const headerH = FRAME_HEADER_HEIGHT;
13268
- const contentLeftMargin = 1 + padding + g.frameBorderLeft;
13399
+ const headerH = FRAME_VIEWPORT_HEADER_ALLOWANCE;
13400
+ let contentLeftMargin = 1 + padding + g.frameBorderLeft;
13401
+ let numberLeft = 0;
13402
+ for (const message of [
13403
+ ...g.messages,
13404
+ ...g.creations.map((creation) => creation.message)
13405
+ ]) {
13406
+ if (!message.number) continue;
13407
+ numberLeft = Math.min(
13408
+ numberLeft,
13409
+ Math.min(message.fromX, message.toX) + 1 - 4 - messageNumberWidth(message.number)
13410
+ );
13411
+ }
13412
+ for (const call of g.selfCalls) {
13413
+ if (call.number)
13414
+ numberLeft = Math.min(
13415
+ numberLeft,
13416
+ call.x - 3 - messageNumberWidth(call.number)
13417
+ );
13418
+ }
13419
+ for (const returned of g.returns) {
13420
+ if (returned.number && !returned.isSelf)
13421
+ numberLeft = Math.min(
13422
+ numberLeft,
13423
+ Math.min(returned.fromX, returned.toX) - 4 - messageNumberWidth(returned.number)
13424
+ );
13425
+ }
13426
+ contentLeftMargin += Math.max(0, padding - contentLeftMargin - numberLeft);
13269
13427
  const viewWidth = g.width + contentLeftMargin + padding + g.frameBorderRight + 1;
13270
13428
  const viewHeight = g.height + padding * 2 + headerH - 1;
13271
13429
  const parts = [];
@@ -13315,8 +13473,7 @@ function composeSvg(g, options) {
13315
13473
  `<rect class="frame-border-outer" x="0" y="0" width="${viewWidth}" height="${viewHeight}" rx="${r}" fill="#666"/>`,
13316
13474
  `<rect class="frame-border-inner" x="1" y="1" width="${viewWidth - 2}" height="${viewHeight - 2}" rx="${Math.max(0, r - 1)}" fill="#fff"/>`
13317
13475
  ].join("\n");
13318
- const contentPaddingTop = 6;
13319
- const headerLineY = headerH + contentPaddingTop;
13476
+ const headerLineY = 1 + DIAGRAM_HEADER_HEIGHT;
13320
13477
  const headerLineDrawY = headerLineY - 0.5;
13321
13478
  const headerLineSvg = `<line class="frame-header-line" x1="1" y1="${headerLineDrawY}" x2="${viewWidth - 1}" y2="${headerLineDrawY}"/>`;
13322
13479
  const titleSvg = g.title ? `<text x="5" y="${headerLineDrawY / 2}" dominant-baseline="central" class="frame-title">${esc(g.title)}</text>` : "";