@diagrammo/dgmo 0.6.0 → 0.6.2

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/dist/index.cjs CHANGED
@@ -1467,6 +1467,29 @@ var init_tag_groups = __esm({
1467
1467
  }
1468
1468
  });
1469
1469
 
1470
+ // src/utils/legend-constants.ts
1471
+ var LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_SIZE, LEGEND_PILL_FONT_W, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_FONT_W, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP, LEGEND_EYE_SIZE, LEGEND_EYE_GAP, EYE_OPEN_PATH, EYE_CLOSED_PATH;
1472
+ var init_legend_constants = __esm({
1473
+ "src/utils/legend-constants.ts"() {
1474
+ "use strict";
1475
+ LEGEND_HEIGHT = 28;
1476
+ LEGEND_PILL_PAD = 16;
1477
+ LEGEND_PILL_FONT_SIZE = 11;
1478
+ LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
1479
+ LEGEND_CAPSULE_PAD = 4;
1480
+ LEGEND_DOT_R = 4;
1481
+ LEGEND_ENTRY_FONT_SIZE = 10;
1482
+ LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
1483
+ LEGEND_ENTRY_DOT_GAP = 4;
1484
+ LEGEND_ENTRY_TRAIL = 8;
1485
+ LEGEND_GROUP_GAP = 12;
1486
+ LEGEND_EYE_SIZE = 14;
1487
+ LEGEND_EYE_GAP = 6;
1488
+ EYE_OPEN_PATH = "M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z";
1489
+ EYE_CLOSED_PATH = "M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1";
1490
+ }
1491
+ });
1492
+
1470
1493
  // src/sequence/participant-inference.ts
1471
1494
  function inferParticipantType(name) {
1472
1495
  for (const rule of PARTICIPANT_RULES) {
@@ -4777,10 +4800,12 @@ function makeGridAxis(type, textColor, axisLineColor, splitLineColor, gridOpacit
4777
4800
  if (type === "category" && data && data.length > 0) {
4778
4801
  const maxLabelLen = Math.max(...data.map((l) => l.length));
4779
4802
  const count = data.length;
4780
- if (count > 10 || maxLabelLen > 20) catFontSize = 10;
4781
- else if (count > 5 || maxLabelLen > 14) catFontSize = 11;
4803
+ const step = intervalOverride != null && intervalOverride > 0 ? intervalOverride + 1 : 1;
4804
+ const visibleCount = Math.ceil(count / step);
4805
+ if (visibleCount > 10 || maxLabelLen > 20) catFontSize = 10;
4806
+ else if (visibleCount > 5 || maxLabelLen > 14) catFontSize = 11;
4782
4807
  else if (maxLabelLen > 8) catFontSize = 12;
4783
- if (chartWidthHint && count > 0) {
4808
+ if ((intervalOverride == null || intervalOverride === 0) && chartWidthHint && count > 0) {
4784
4809
  const availPerLabel = Math.floor(chartWidthHint * 0.85 / count);
4785
4810
  catLabelExtras = {
4786
4811
  width: availPerLabel,
@@ -4798,6 +4823,9 @@ function makeGridAxis(type, textColor, axisLineColor, splitLineColor, gridOpacit
4798
4823
  fontFamily: FONT_FAMILY,
4799
4824
  ...type === "category" && {
4800
4825
  interval: intervalOverride ?? 0,
4826
+ // Prevent ECharts auto-rotation: it measures raw slot width (chartWidth/N),
4827
+ // which is too narrow when an interval skips most labels, and rotates to 90°.
4828
+ rotate: 0,
4801
4829
  formatter: (value) => value.replace(/([a-z])([A-Z])/g, "$1\n$2"),
4802
4830
  ...catLabelExtras
4803
4831
  }
@@ -4873,21 +4901,13 @@ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOp
4873
4901
  ]
4874
4902
  };
4875
4903
  }
4876
- function buildIntervalCallback(labels, eras) {
4904
+ function buildIntervalStep(labels) {
4877
4905
  const count = labels.length;
4878
- if (count <= 8) return () => true;
4879
- const snapSteps = [1, 2, 4, 5, 10, 20, 25, 50];
4880
- const raw = Math.ceil(count / 8);
4906
+ if (count <= 6) return 0;
4907
+ const snapSteps = [1, 2, 5, 10, 25, 50, 100];
4908
+ const raw = Math.ceil(count / 5);
4881
4909
  const N = [...snapSteps].reverse().find((s) => s <= raw) ?? 1;
4882
- const pinned = /* @__PURE__ */ new Set();
4883
- for (let i = 0; i < count; i += N) pinned.add(i);
4884
- for (const era of eras) {
4885
- const si = labels.indexOf(era.start);
4886
- const ei = labels.indexOf(era.end);
4887
- if (si >= 0) pinned.add(si);
4888
- if (ei >= 0) pinned.add(ei);
4889
- }
4890
- return (index) => pinned.has(index);
4910
+ return N - 1;
4891
4911
  }
4892
4912
  function buildMarkArea(eras, labels, textColor, defaultColor) {
4893
4913
  if (eras.length === 0) return void 0;
@@ -4922,7 +4942,7 @@ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineCol
4922
4942
  const labels = parsed.data.map((d) => d.label);
4923
4943
  const values = parsed.data.map((d) => d.value);
4924
4944
  const eras = parsed.eras ?? [];
4925
- const interval = buildIntervalCallback(labels, eras);
4945
+ const interval = buildIntervalStep(labels);
4926
4946
  const markArea = buildMarkArea(eras, labels, textColor, palette.colors.blue);
4927
4947
  return {
4928
4948
  ...CHART_BASE,
@@ -4954,7 +4974,7 @@ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLi
4954
4974
  const seriesNames = parsed.seriesNames ?? [];
4955
4975
  const labels = parsed.data.map((d) => d.label);
4956
4976
  const eras = parsed.eras ?? [];
4957
- const interval = buildIntervalCallback(labels, eras);
4977
+ const interval = buildIntervalStep(labels);
4958
4978
  const markArea = buildMarkArea(eras, labels, textColor, palette.colors.blue);
4959
4979
  const series = seriesNames.map((name, idx) => {
4960
4980
  const color = parsed.seriesNameColors?.[idx] ?? colors[idx % colors.length];
@@ -4998,7 +5018,7 @@ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineCol
4998
5018
  const labels = parsed.data.map((d) => d.label);
4999
5019
  const values = parsed.data.map((d) => d.value);
5000
5020
  const eras = parsed.eras ?? [];
5001
- const interval = buildIntervalCallback(labels, eras);
5021
+ const interval = buildIntervalStep(labels);
5002
5022
  const markArea = buildMarkArea(eras, labels, textColor, palette.colors.blue);
5003
5023
  return {
5004
5024
  ...CHART_BASE,
@@ -7012,7 +7032,6 @@ function parseInfra(content) {
7012
7032
  edges: [],
7013
7033
  groups: [],
7014
7034
  tagGroups: [],
7015
- scenarios: [],
7016
7035
  options: {},
7017
7036
  diagnostics: [],
7018
7037
  error: null
@@ -7115,16 +7134,7 @@ function parseInfra(content) {
7115
7134
  continue;
7116
7135
  }
7117
7136
  if (/^scenario\s*:/i.test(trimmed)) {
7118
- finishCurrentNode();
7119
- finishCurrentTagGroup();
7120
- currentGroup = null;
7121
- const scenarioName = trimmed.replace(/^scenario\s*:\s*/i, "").trim();
7122
- const scenario = {
7123
- name: scenarioName,
7124
- overrides: {},
7125
- lineNumber
7126
- };
7127
- let scenarioNodeId = null;
7137
+ console.warn("[dgmo warn] scenario syntax is deprecated and will be ignored");
7128
7138
  let si = i + 1;
7129
7139
  while (si < lines.length) {
7130
7140
  const sLine = lines[si];
@@ -7135,23 +7145,9 @@ function parseInfra(content) {
7135
7145
  }
7136
7146
  const sIndent = sLine.length - sLine.trimStart().length;
7137
7147
  if (sIndent === 0) break;
7138
- if (sIndent <= 2) {
7139
- scenarioNodeId = nodeId2(sTrimmed.replace(/\|.*$/, "").trim());
7140
- if (!scenario.overrides[scenarioNodeId]) {
7141
- scenario.overrides[scenarioNodeId] = {};
7142
- }
7143
- } else if (scenarioNodeId) {
7144
- const pm = sTrimmed.match(PROPERTY_RE);
7145
- if (pm) {
7146
- const key = pm[1].toLowerCase();
7147
- const val = parsePropertyValue(pm[2].trim());
7148
- scenario.overrides[scenarioNodeId][key] = val;
7149
- }
7150
- }
7151
7148
  si++;
7152
7149
  }
7153
7150
  i = si - 1;
7154
- result.scenarios.push(scenario);
7155
7151
  continue;
7156
7152
  }
7157
7153
  const tagMatch = trimmed.match(TAG_GROUP_RE);
@@ -7683,14 +7679,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
7683
7679
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
7684
7680
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
7685
7681
  if (visibleEntries.length === 0) continue;
7686
- const pillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
7682
+ const pillWidth = group.name.length * LEGEND_PILL_FONT_W2 + LEGEND_PILL_PAD2;
7687
7683
  const minPillWidth = pillWidth;
7688
7684
  let entriesWidth = 0;
7689
7685
  for (const entry of visibleEntries) {
7690
- entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
7686
+ entriesWidth += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + entry.value.length * LEGEND_ENTRY_FONT_W2 + LEGEND_ENTRY_TRAIL2;
7691
7687
  }
7692
- const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
7693
- const capsuleWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
7688
+ const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
7689
+ const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
7694
7690
  groups.push({
7695
7691
  name: group.name,
7696
7692
  alias: group.alias,
@@ -7701,9 +7697,9 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
7701
7697
  x: 0,
7702
7698
  y: 0,
7703
7699
  width: capsuleWidth,
7704
- height: LEGEND_HEIGHT,
7700
+ height: LEGEND_HEIGHT2,
7705
7701
  minifiedWidth: minPillWidth,
7706
- minifiedHeight: LEGEND_HEIGHT
7702
+ minifiedHeight: LEGEND_HEIGHT2
7707
7703
  });
7708
7704
  }
7709
7705
  return groups;
@@ -7733,7 +7729,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
7733
7729
  for (const g of legendGroups2) {
7734
7730
  g.x = MARGIN;
7735
7731
  g.y = cy;
7736
- cy += LEGEND_HEIGHT + LEGEND_GROUP_GAP;
7732
+ cy += LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
7737
7733
  if (g.width > maxWidth2) maxWidth2 = g.width;
7738
7734
  }
7739
7735
  return {
@@ -7742,7 +7738,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
7742
7738
  containers: [],
7743
7739
  legend: legendGroups2,
7744
7740
  width: maxWidth2 + MARGIN * 2,
7745
- height: cy - LEGEND_GROUP_GAP + MARGIN
7741
+ height: cy - LEGEND_GROUP_GAP2 + MARGIN
7746
7742
  };
7747
7743
  }
7748
7744
  injectDefaultMetadata(parsed.roots, parsed.tagGroups);
@@ -8272,7 +8268,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8272
8268
  const effectiveH = (g) => activeTagGroup != null || allExpanded ? g.height : g.minifiedHeight;
8273
8269
  if (visibleGroups.length > 0) {
8274
8270
  if (legendPosition === "bottom") {
8275
- const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
8271
+ const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP2;
8276
8272
  const neededWidth = totalGroupsWidth + MARGIN * 2;
8277
8273
  if (neededWidth > totalWidth) {
8278
8274
  finalWidth = neededWidth;
@@ -8290,22 +8286,22 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8290
8286
  for (const g of visibleGroups) {
8291
8287
  g.x = cx;
8292
8288
  g.y = legendY;
8293
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
8289
+ cx += effectiveW(g) + LEGEND_GROUP_GAP2;
8294
8290
  }
8295
- finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
8291
+ finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT2;
8296
8292
  } else {
8297
- const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
8293
+ const legendShift = LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
8298
8294
  for (const n of layoutNodes) n.y += legendShift;
8299
8295
  for (const c of containers) c.y += legendShift;
8300
8296
  for (const e of layoutEdges) {
8301
8297
  for (const p of e.points) p.y += legendShift;
8302
8298
  }
8303
- const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
8299
+ const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP2;
8304
8300
  let cx = MARGIN;
8305
8301
  for (const g of visibleGroups) {
8306
8302
  g.x = cx;
8307
8303
  g.y = MARGIN;
8308
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
8304
+ cx += effectiveW(g) + LEGEND_GROUP_GAP2;
8309
8305
  }
8310
8306
  finalHeight += legendShift;
8311
8307
  const neededWidth = totalGroupsWidth + MARGIN * 2;
@@ -8323,7 +8319,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8323
8319
  height: finalHeight
8324
8320
  };
8325
8321
  }
8326
- var import_d3_hierarchy, CHAR_WIDTH, META_LINE_HEIGHT, HEADER_HEIGHT, SEPARATOR_GAP, CARD_H_PAD, CARD_V_PAD, MIN_CARD_WIDTH, H_GAP, V_GAP, MARGIN, CONTAINER_PAD_X, CONTAINER_PAD_BOTTOM, CONTAINER_LABEL_HEIGHT, CONTAINER_META_LINE_HEIGHT, STACK_V_GAP, LEGEND_GAP, LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_W, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_W, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP, LEGEND_EYE_SIZE, LEGEND_EYE_GAP;
8322
+ var import_d3_hierarchy, CHAR_WIDTH, META_LINE_HEIGHT, HEADER_HEIGHT, SEPARATOR_GAP, CARD_H_PAD, CARD_V_PAD, MIN_CARD_WIDTH, H_GAP, V_GAP, MARGIN, CONTAINER_PAD_X, CONTAINER_PAD_BOTTOM, CONTAINER_LABEL_HEIGHT, CONTAINER_META_LINE_HEIGHT, STACK_V_GAP, LEGEND_GAP, LEGEND_HEIGHT2, LEGEND_PILL_PAD2, LEGEND_PILL_FONT_W2, LEGEND_CAPSULE_PAD2, LEGEND_DOT_R2, LEGEND_ENTRY_FONT_W2, LEGEND_ENTRY_DOT_GAP2, LEGEND_ENTRY_TRAIL2, LEGEND_GROUP_GAP2, LEGEND_EYE_SIZE2, LEGEND_EYE_GAP2;
8327
8323
  var init_layout = __esm({
8328
8324
  "src/org/layout.ts"() {
8329
8325
  "use strict";
@@ -8345,17 +8341,17 @@ var init_layout = __esm({
8345
8341
  CONTAINER_META_LINE_HEIGHT = 16;
8346
8342
  STACK_V_GAP = 20;
8347
8343
  LEGEND_GAP = 30;
8348
- LEGEND_HEIGHT = 28;
8349
- LEGEND_PILL_PAD = 16;
8350
- LEGEND_PILL_FONT_W = 11 * 0.6;
8351
- LEGEND_CAPSULE_PAD = 4;
8352
- LEGEND_DOT_R = 4;
8353
- LEGEND_ENTRY_FONT_W = 10 * 0.6;
8354
- LEGEND_ENTRY_DOT_GAP = 4;
8355
- LEGEND_ENTRY_TRAIL = 8;
8356
- LEGEND_GROUP_GAP = 12;
8357
- LEGEND_EYE_SIZE = 14;
8358
- LEGEND_EYE_GAP = 6;
8344
+ LEGEND_HEIGHT2 = 28;
8345
+ LEGEND_PILL_PAD2 = 16;
8346
+ LEGEND_PILL_FONT_W2 = 11 * 0.6;
8347
+ LEGEND_CAPSULE_PAD2 = 4;
8348
+ LEGEND_DOT_R2 = 4;
8349
+ LEGEND_ENTRY_FONT_W2 = 10 * 0.6;
8350
+ LEGEND_ENTRY_DOT_GAP2 = 4;
8351
+ LEGEND_ENTRY_TRAIL2 = 8;
8352
+ LEGEND_GROUP_GAP2 = 12;
8353
+ LEGEND_EYE_SIZE2 = 14;
8354
+ LEGEND_EYE_GAP2 = 6;
8359
8355
  }
8360
8356
  });
8361
8357
 
@@ -8452,11 +8448,11 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8452
8448
  if (width <= 0 || height <= 0) return;
8453
8449
  const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
8454
8450
  const legendOnly = layout.nodes.length === 0;
8455
- const legendPosition = parsed.options?.["legend-position"] ?? "top";
8451
+ const legendPosition = parsed.options?.["legend-position"] ?? "bottom";
8456
8452
  const hasLegend = layout.legend.length > 0;
8457
- const layoutLegendShift = LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
8453
+ const layoutLegendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
8458
8454
  const fixedLegend = !exportDims && hasLegend && !legendOnly;
8459
- const legendReserve = fixedLegend ? LEGEND_HEIGHT2 + LEGEND_FIXED_GAP : 0;
8455
+ const legendReserve = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
8460
8456
  const fixedTitle = !exportDims && !!parsed.title;
8461
8457
  const titleReserve = fixedTitle ? TITLE_HEIGHT : 0;
8462
8458
  const diagramW = layout.width;
@@ -8611,56 +8607,60 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8611
8607
  if (fixedLegend && visibleGroups.length > 0) {
8612
8608
  fixedPositions = /* @__PURE__ */ new Map();
8613
8609
  const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
8614
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP2;
8610
+ const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
8615
8611
  let cx = (width - totalW) / 2;
8616
8612
  for (const g of visibleGroups) {
8617
8613
  fixedPositions.set(g.name, cx);
8618
- cx += effectiveW(g) + LEGEND_GROUP_GAP2;
8614
+ cx += effectiveW(g) + LEGEND_GROUP_GAP;
8619
8615
  }
8620
8616
  }
8621
- const legendParent = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr(
8617
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr(
8622
8618
  "transform",
8623
- legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT2})` : `translate(0, ${DIAGRAM_PADDING + titleReserve})`
8619
+ legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})` : `translate(0, ${DIAGRAM_PADDING + titleReserve})`
8624
8620
  ) : contentG;
8621
+ const legendParent = legendParentBase;
8622
+ if (fixedLegend && activeTagGroup) {
8623
+ legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
8624
+ }
8625
8625
  for (const group of visibleGroups) {
8626
8626
  const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
8627
8627
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
8628
8628
  const pillLabel = group.name;
8629
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W2 + LEGEND_PILL_PAD2;
8629
+ const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
8630
8630
  const gX = fixedPositions?.get(group.name) ?? group.x;
8631
8631
  const gY = fixedPositions ? 0 : group.y;
8632
8632
  const gEl = legendParent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "org-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", legendOnly ? "default" : "pointer");
8633
8633
  if (isActive) {
8634
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT2).attr("rx", LEGEND_HEIGHT2 / 2).attr("fill", groupBg);
8634
+ gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
8635
8635
  }
8636
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD2 : 0;
8637
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD2 : 0;
8638
- const pillH = LEGEND_HEIGHT2 - (isActive ? LEGEND_CAPSULE_PAD2 * 2 : 0);
8636
+ const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
8637
+ const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
8638
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
8639
8639
  gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
8640
8640
  if (isActive) {
8641
8641
  gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
8642
8642
  }
8643
- gEl.append("text").attr("x", pillXOff + pillWidth / 2).attr("y", LEGEND_HEIGHT2 / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
8643
+ gEl.append("text").attr("x", pillXOff + pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
8644
8644
  if (isActive && fixedLegend) {
8645
8645
  const groupKey = group.name.toLowerCase();
8646
8646
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
8647
- const eyeX = pillXOff + pillWidth + LEGEND_EYE_GAP2;
8648
- const eyeY = (LEGEND_HEIGHT2 - LEGEND_EYE_SIZE2) / 2;
8647
+ const eyeX = pillXOff + pillWidth + LEGEND_EYE_GAP;
8648
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
8649
8649
  const hitPad = 6;
8650
8650
  const eyeG = gEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
8651
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE2 + hitPad * 2).attr("height", LEGEND_EYE_SIZE2 + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
8651
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
8652
8652
  eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
8653
8653
  }
8654
8654
  if (isActive) {
8655
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
8655
+ const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
8656
8656
  let entryX = pillXOff + pillWidth + 4 + eyeShift;
8657
8657
  for (const entry of group.entries) {
8658
8658
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
8659
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R2).attr("cy", LEGEND_HEIGHT2 / 2).attr("r", LEGEND_DOT_R2).attr("fill", entry.color);
8660
- const textX = entryX + LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2;
8659
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
8660
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
8661
8661
  const entryLabel = entry.value;
8662
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT2 / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entryLabel);
8663
- entryX = textX + entryLabel.length * LEGEND_ENTRY_FONT_W2 + LEGEND_ENTRY_TRAIL2;
8662
+ entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entryLabel);
8663
+ entryX = textX + entryLabel.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
8664
8664
  }
8665
8665
  }
8666
8666
  }
@@ -8699,7 +8699,7 @@ function renderOrgForExport(content, theme, palette) {
8699
8699
  document.body.removeChild(container);
8700
8700
  }
8701
8701
  }
8702
- var d3Selection, DIAGRAM_PADDING, MAX_SCALE, TITLE_HEIGHT, TITLE_FONT_SIZE, LABEL_FONT_SIZE, META_FONT_SIZE, META_LINE_HEIGHT2, HEADER_HEIGHT2, SEPARATOR_GAP2, EDGE_STROKE_WIDTH, NODE_STROKE_WIDTH, CARD_RADIUS, CONTAINER_RADIUS, CONTAINER_LABEL_FONT_SIZE, CONTAINER_META_FONT_SIZE, CONTAINER_META_LINE_HEIGHT2, CONTAINER_HEADER_HEIGHT, COLLAPSE_BAR_HEIGHT, COLLAPSE_BAR_INSET, LEGEND_HEIGHT2, LEGEND_PILL_PAD2, LEGEND_PILL_FONT_SIZE, LEGEND_PILL_FONT_W2, LEGEND_CAPSULE_PAD2, LEGEND_DOT_R2, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_FONT_W2, LEGEND_ENTRY_DOT_GAP2, LEGEND_ENTRY_TRAIL2, LEGEND_GROUP_GAP2, LEGEND_EYE_SIZE2, LEGEND_EYE_GAP2, LEGEND_FIXED_GAP, EYE_OPEN_PATH, EYE_CLOSED_PATH;
8702
+ var d3Selection, DIAGRAM_PADDING, MAX_SCALE, TITLE_HEIGHT, TITLE_FONT_SIZE, LABEL_FONT_SIZE, META_FONT_SIZE, META_LINE_HEIGHT2, HEADER_HEIGHT2, SEPARATOR_GAP2, EDGE_STROKE_WIDTH, NODE_STROKE_WIDTH, CARD_RADIUS, CONTAINER_RADIUS, CONTAINER_LABEL_FONT_SIZE, CONTAINER_META_FONT_SIZE, CONTAINER_META_LINE_HEIGHT2, CONTAINER_HEADER_HEIGHT, COLLAPSE_BAR_HEIGHT, COLLAPSE_BAR_INSET, LEGEND_FIXED_GAP;
8703
8703
  var init_renderer = __esm({
8704
8704
  "src/org/renderer.ts"() {
8705
8705
  "use strict";
@@ -8708,6 +8708,7 @@ var init_renderer = __esm({
8708
8708
  init_color_utils();
8709
8709
  init_parser4();
8710
8710
  init_layout();
8711
+ init_legend_constants();
8711
8712
  DIAGRAM_PADDING = 20;
8712
8713
  MAX_SCALE = 3;
8713
8714
  TITLE_HEIGHT = 30;
@@ -8727,22 +8728,7 @@ var init_renderer = __esm({
8727
8728
  CONTAINER_HEADER_HEIGHT = 28;
8728
8729
  COLLAPSE_BAR_HEIGHT = 6;
8729
8730
  COLLAPSE_BAR_INSET = 0;
8730
- LEGEND_HEIGHT2 = 28;
8731
- LEGEND_PILL_PAD2 = 16;
8732
- LEGEND_PILL_FONT_SIZE = 11;
8733
- LEGEND_PILL_FONT_W2 = LEGEND_PILL_FONT_SIZE * 0.6;
8734
- LEGEND_CAPSULE_PAD2 = 4;
8735
- LEGEND_DOT_R2 = 4;
8736
- LEGEND_ENTRY_FONT_SIZE = 10;
8737
- LEGEND_ENTRY_FONT_W2 = LEGEND_ENTRY_FONT_SIZE * 0.6;
8738
- LEGEND_ENTRY_DOT_GAP2 = 4;
8739
- LEGEND_ENTRY_TRAIL2 = 8;
8740
- LEGEND_GROUP_GAP2 = 12;
8741
- LEGEND_EYE_SIZE2 = 14;
8742
- LEGEND_EYE_GAP2 = 6;
8743
8731
  LEGEND_FIXED_GAP = 8;
8744
- EYE_OPEN_PATH = "M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z";
8745
- EYE_CLOSED_PATH = "M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1";
8746
8732
  }
8747
8733
  });
8748
8734
 
@@ -9166,23 +9152,17 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
9166
9152
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
9167
9153
  if (visibleGroups.length > 0) {
9168
9154
  const legendShift = LEGEND_HEIGHT3 + LEGEND_GROUP_GAP3;
9169
- for (const n of layoutNodes) n.y += legendShift;
9170
- for (const c of layoutContainers) c.y += legendShift;
9171
- for (const e of layoutEdges) {
9172
- for (const p of e.points) p.y += legendShift;
9173
- }
9174
9155
  const totalGroupsWidth = visibleGroups.reduce((s, g2) => s + effectiveW(g2), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP3;
9175
- let cx = MARGIN2;
9156
+ const neededWidth = totalGroupsWidth + MARGIN2 * 2;
9157
+ if (neededWidth > totalWidth) totalWidth = neededWidth;
9158
+ let cx = (totalWidth - totalGroupsWidth) / 2;
9159
+ const legendY = totalHeight + LEGEND_GROUP_GAP3;
9176
9160
  for (const g2 of visibleGroups) {
9177
9161
  g2.x = cx;
9178
- g2.y = MARGIN2;
9162
+ g2.y = legendY;
9179
9163
  cx += effectiveW(g2) + LEGEND_GROUP_GAP3;
9180
9164
  }
9181
9165
  totalHeight += legendShift;
9182
- const neededWidth = totalGroupsWidth + MARGIN2 * 2;
9183
- if (neededWidth > totalWidth) {
9184
- totalWidth = neededWidth;
9185
- }
9186
9166
  }
9187
9167
  return {
9188
9168
  nodes: layoutNodes,
@@ -9376,25 +9356,26 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
9376
9356
  const height = exportDims?.height ?? container.clientHeight;
9377
9357
  if (width <= 0 || height <= 0) return;
9378
9358
  const hasLegend = layout.legend.length > 0;
9379
- const layoutLegendShift = LEGEND_HEIGHT4 + LEGEND_GROUP_GAP4;
9359
+ const layoutLegendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
9380
9360
  const fixedLegend = !exportDims && hasLegend;
9381
9361
  const fixedTitle = fixedLegend && !!parsed.title;
9382
9362
  const fixedTitleH = fixedTitle ? TITLE_HEIGHT2 : 0;
9383
- const legendReserveH = fixedLegend ? LEGEND_HEIGHT4 + LEGEND_FIXED_GAP2 : 0;
9384
- const fixedReserve = fixedTitleH + legendReserveH;
9363
+ const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP2 : 0;
9364
+ const fixedReserveTop = fixedTitleH;
9365
+ const fixedReserveBottom = legendReserveH;
9385
9366
  const titleOffset = !fixedTitle && parsed.title ? TITLE_HEIGHT2 : 0;
9386
9367
  const diagramW = layout.width;
9387
9368
  let diagramH = layout.height + titleOffset;
9388
9369
  if (fixedLegend) {
9389
9370
  diagramH -= layoutLegendShift;
9390
9371
  }
9391
- const availH = height - DIAGRAM_PADDING2 * 2 - fixedReserve;
9372
+ const availH = height - DIAGRAM_PADDING2 * 2 - fixedReserveTop - fixedReserveBottom;
9392
9373
  const scaleX = (width - DIAGRAM_PADDING2 * 2) / diagramW;
9393
9374
  const scaleY = availH / diagramH;
9394
9375
  const scale = Math.min(MAX_SCALE2, scaleX, scaleY);
9395
9376
  const scaledW = diagramW * scale;
9396
9377
  const offsetX = (width - scaledW) / 2;
9397
- const offsetY = DIAGRAM_PADDING2 + fixedReserve;
9378
+ const offsetY = DIAGRAM_PADDING2 + fixedReserveTop;
9398
9379
  const svg = d3Selection2.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
9399
9380
  const defs = svg.append("defs");
9400
9381
  defs.append("marker").attr("id", "sm-arrow").attr("viewBox", `0 0 ${ARROWHEAD_W} ${ARROWHEAD_H}`).attr("refX", ARROWHEAD_W).attr("refY", ARROWHEAD_H / 2).attr("markerWidth", ARROWHEAD_W).attr("markerHeight", ARROWHEAD_H).attr("orient", "auto").append("polygon").attr("points", `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`).attr("fill", palette.textMuted);
@@ -9535,7 +9516,10 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
9535
9516
  titleEl.text(parsed.title);
9536
9517
  }
9537
9518
  if (fixedLegend) {
9538
- const legendParent = svg.append("g").attr("class", "sitemap-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING2 + fixedTitleH})`);
9519
+ const legendParent = svg.append("g").attr("class", "sitemap-legend-fixed").attr("transform", `translate(0, ${height - DIAGRAM_PADDING2 - LEGEND_HEIGHT})`);
9520
+ if (activeTagGroup) {
9521
+ legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
9522
+ }
9539
9523
  renderLegend(legendParent, layout.legend, palette, isDark, activeTagGroup, width, hiddenAttributes);
9540
9524
  }
9541
9525
  }
@@ -9547,49 +9531,49 @@ function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fix
9547
9531
  if (fixedWidth != null && visibleGroups.length > 0) {
9548
9532
  fixedPositions = /* @__PURE__ */ new Map();
9549
9533
  const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
9550
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP4;
9534
+ const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
9551
9535
  let cx = (fixedWidth - totalW) / 2;
9552
9536
  for (const g of visibleGroups) {
9553
9537
  fixedPositions.set(g.name, cx);
9554
- cx += effectiveW(g) + LEGEND_GROUP_GAP4;
9538
+ cx += effectiveW(g) + LEGEND_GROUP_GAP;
9555
9539
  }
9556
9540
  }
9557
9541
  for (const group of visibleGroups) {
9558
9542
  const isActive = activeTagGroup != null;
9559
- const pillW = group.name.length * LEGEND_PILL_FONT_W4 + LEGEND_PILL_PAD4;
9543
+ const pillW = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
9560
9544
  const gX = fixedPositions?.get(group.name) ?? group.x;
9561
9545
  const gY = fixedPositions ? 0 : group.y;
9562
9546
  const legendG = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "sitemap-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
9563
9547
  if (isActive) {
9564
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT4).attr("rx", LEGEND_HEIGHT4 / 2).attr("fill", groupBg);
9548
+ legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
9565
9549
  }
9566
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD4 : 0;
9567
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD4 : 0;
9568
- const pillH = LEGEND_HEIGHT4 - (isActive ? LEGEND_CAPSULE_PAD4 * 2 : 0);
9550
+ const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
9551
+ const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
9552
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
9569
9553
  legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
9570
9554
  if (isActive) {
9571
9555
  legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
9572
9556
  }
9573
- legendG.append("text").attr("x", pillXOff + pillW / 2).attr("y", LEGEND_HEIGHT4 / 2 + LEGEND_PILL_FONT_SIZE2 / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE2).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
9557
+ legendG.append("text").attr("x", pillXOff + pillW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
9574
9558
  if (isActive && fixedWidth != null) {
9575
9559
  const groupKey = group.name.toLowerCase();
9576
9560
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
9577
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP4;
9578
- const eyeY = (LEGEND_HEIGHT4 - LEGEND_EYE_SIZE4) / 2;
9561
+ const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
9562
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
9579
9563
  const hitPad = 6;
9580
9564
  const eyeG = legendG.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
9581
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE4 + hitPad * 2).attr("height", LEGEND_EYE_SIZE4 + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
9582
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH2 : EYE_OPEN_PATH2).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
9565
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
9566
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
9583
9567
  }
9584
9568
  if (isActive) {
9585
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE4 + LEGEND_EYE_GAP4 : 0;
9569
+ const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
9586
9570
  let entryX = pillXOff + pillW + 4 + eyeShift;
9587
9571
  for (const entry of group.entries) {
9588
9572
  const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
9589
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R4).attr("cy", LEGEND_HEIGHT4 / 2).attr("r", LEGEND_DOT_R4).attr("fill", entry.color);
9590
- const textX = entryX + LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4;
9591
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT4 / 2 + LEGEND_ENTRY_FONT_SIZE2 / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE2).attr("fill", palette.textMuted).text(entry.value);
9592
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W4 + LEGEND_ENTRY_TRAIL4;
9573
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
9574
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
9575
+ entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
9576
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
9593
9577
  }
9594
9578
  }
9595
9579
  }
@@ -9635,7 +9619,7 @@ async function renderSitemapForExport(content, theme, palette) {
9635
9619
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
9636
9620
  return injectBranding2(svgHtml, brandColor);
9637
9621
  }
9638
- var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, TITLE_FONT_SIZE2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_HEIGHT4, LEGEND_FIXED_GAP2, LEGEND_PILL_PAD4, LEGEND_PILL_FONT_SIZE2, LEGEND_PILL_FONT_W4, LEGEND_CAPSULE_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_FONT_SIZE2, LEGEND_ENTRY_FONT_W4, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_GROUP_GAP4, LEGEND_EYE_SIZE4, LEGEND_EYE_GAP4, lineGenerator, EYE_OPEN_PATH2, EYE_CLOSED_PATH2;
9622
+ var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, TITLE_FONT_SIZE2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator;
9639
9623
  var init_renderer2 = __esm({
9640
9624
  "src/sitemap/renderer.ts"() {
9641
9625
  "use strict";
@@ -9643,6 +9627,7 @@ var init_renderer2 = __esm({
9643
9627
  d3Shape = __toESM(require("d3-shape"), 1);
9644
9628
  init_fonts();
9645
9629
  init_color_utils();
9630
+ init_legend_constants();
9646
9631
  DIAGRAM_PADDING2 = 20;
9647
9632
  MAX_SCALE2 = 3;
9648
9633
  TITLE_HEIGHT2 = 30;
@@ -9664,23 +9649,8 @@ var init_renderer2 = __esm({
9664
9649
  ARROWHEAD_H = 7;
9665
9650
  EDGE_LABEL_FONT_SIZE = 11;
9666
9651
  COLLAPSE_BAR_HEIGHT2 = 6;
9667
- LEGEND_HEIGHT4 = 28;
9668
9652
  LEGEND_FIXED_GAP2 = 8;
9669
- LEGEND_PILL_PAD4 = 16;
9670
- LEGEND_PILL_FONT_SIZE2 = 11;
9671
- LEGEND_PILL_FONT_W4 = LEGEND_PILL_FONT_SIZE2 * 0.6;
9672
- LEGEND_CAPSULE_PAD4 = 4;
9673
- LEGEND_DOT_R4 = 4;
9674
- LEGEND_ENTRY_FONT_SIZE2 = 10;
9675
- LEGEND_ENTRY_FONT_W4 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
9676
- LEGEND_ENTRY_DOT_GAP4 = 4;
9677
- LEGEND_ENTRY_TRAIL4 = 8;
9678
- LEGEND_GROUP_GAP4 = 12;
9679
- LEGEND_EYE_SIZE4 = 14;
9680
- LEGEND_EYE_GAP4 = 6;
9681
9653
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
9682
- EYE_OPEN_PATH2 = "M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z";
9683
- EYE_CLOSED_PATH2 = "M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1";
9684
9654
  }
9685
9655
  });
9686
9656
 
@@ -9882,8 +9852,7 @@ function resolveCardTagColor(card, tagGroups, activeTagGroup) {
9882
9852
  return entry?.color;
9883
9853
  }
9884
9854
  function computeLayout(parsed, _palette) {
9885
- const hasHeader = !!parsed.title || parsed.tagGroups.length > 0;
9886
- const headerHeight = hasHeader ? Math.max(TITLE_HEIGHT3, LEGEND_HEIGHT5) + 8 : 0;
9855
+ const headerHeight = parsed.title ? TITLE_HEIGHT3 + 8 : 0;
9887
9856
  const startY = DIAGRAM_PADDING3 + headerHeight;
9888
9857
  const charWidth = CARD_TITLE_FONT_SIZE * 0.6;
9889
9858
  const columnLayouts = [];
@@ -9940,7 +9909,8 @@ function computeLayout(parsed, _palette) {
9940
9909
  currentX += cl.width + COLUMN_GAP;
9941
9910
  }
9942
9911
  const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING3;
9943
- const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3;
9912
+ const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
9913
+ const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3 + legendSpace;
9944
9914
  return { columns: columnLayouts, totalWidth, totalHeight };
9945
9915
  }
9946
9916
  function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exportDims, activeTagGroup) {
@@ -9953,42 +9923,45 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
9953
9923
  svg.append("text").attr("class", "chart-title").attr("data-line-number", parsed.titleLineNumber ?? 0).attr("x", DIAGRAM_PADDING3).attr("y", DIAGRAM_PADDING3 + TITLE_FONT_SIZE3).attr("font-size", TITLE_FONT_SIZE3).attr("font-weight", "bold").attr("fill", palette.text).text(parsed.title);
9954
9924
  }
9955
9925
  if (parsed.tagGroups.length > 0) {
9956
- const legendY = DIAGRAM_PADDING3;
9957
- const titleTextWidth = parsed.title ? parsed.title.length * TITLE_FONT_SIZE3 * 0.6 + 16 : 0;
9958
- let legendX = DIAGRAM_PADDING3 + titleTextWidth;
9926
+ const legendY = height - LEGEND_HEIGHT;
9927
+ let legendX = DIAGRAM_PADDING3;
9959
9928
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
9960
- const capsulePad = 4;
9929
+ const capsulePad = LEGEND_CAPSULE_PAD;
9930
+ const legendContainer = svg.append("g").attr("class", "kanban-legend");
9931
+ if (activeTagGroup) {
9932
+ legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
9933
+ }
9961
9934
  for (const group of parsed.tagGroups) {
9962
9935
  const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
9963
9936
  if (activeTagGroup != null && !isActive) continue;
9964
- const pillTextWidth = group.name.length * LEGEND_FONT_SIZE * 0.6;
9937
+ const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
9965
9938
  const pillWidth = pillTextWidth + 16;
9966
9939
  let capsuleContentWidth = pillWidth;
9967
9940
  if (isActive) {
9968
9941
  capsuleContentWidth += 4;
9969
9942
  for (const entry of group.entries) {
9970
- capsuleContentWidth += LEGEND_DOT_R5 * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE3 * 0.6 + 8;
9943
+ capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
9971
9944
  }
9972
9945
  }
9973
9946
  const capsuleWidth = capsuleContentWidth + capsulePad * 2;
9974
9947
  if (isActive) {
9975
- svg.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT5).attr("rx", LEGEND_HEIGHT5 / 2).attr("fill", groupBg);
9948
+ legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
9976
9949
  }
9977
9950
  const pillX = legendX + (isActive ? capsulePad : 0);
9978
9951
  const pillBg = isActive ? palette.bg : groupBg;
9979
- svg.append("rect").attr("x", pillX).attr("y", legendY + (isActive ? capsulePad : 0)).attr("width", pillWidth).attr("height", LEGEND_HEIGHT5 - (isActive ? capsulePad * 2 : 0)).attr("rx", (LEGEND_HEIGHT5 - (isActive ? capsulePad * 2 : 0)) / 2).attr("fill", pillBg).attr("class", "kanban-legend-group").attr("data-legend-group", group.name.toLowerCase());
9952
+ legendContainer.append("rect").attr("x", pillX).attr("y", legendY + (isActive ? capsulePad : 0)).attr("width", pillWidth).attr("height", LEGEND_HEIGHT - (isActive ? capsulePad * 2 : 0)).attr("rx", (LEGEND_HEIGHT - (isActive ? capsulePad * 2 : 0)) / 2).attr("fill", pillBg).attr("class", "kanban-legend-group").attr("data-legend-group", group.name.toLowerCase());
9980
9953
  if (isActive) {
9981
- svg.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth).attr("height", LEGEND_HEIGHT5 - capsulePad * 2).attr("rx", (LEGEND_HEIGHT5 - capsulePad * 2) / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
9954
+ legendContainer.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth).attr("height", LEGEND_HEIGHT - capsulePad * 2).attr("rx", (LEGEND_HEIGHT - capsulePad * 2) / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
9982
9955
  }
9983
- svg.append("text").attr("x", pillX + pillWidth / 2).attr("y", legendY + LEGEND_HEIGHT5 / 2 + LEGEND_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
9956
+ legendContainer.append("text").attr("x", pillX + pillWidth / 2).attr("y", legendY + LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
9984
9957
  if (isActive) {
9985
9958
  let entryX = pillX + pillWidth + 4;
9986
9959
  for (const entry of group.entries) {
9987
- const entryG = svg.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
9988
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R5).attr("cy", legendY + LEGEND_HEIGHT5 / 2).attr("r", LEGEND_DOT_R5).attr("fill", entry.color);
9989
- const entryTextX = entryX + LEGEND_DOT_R5 * 2 + 4;
9990
- entryG.append("text").attr("x", entryTextX).attr("y", legendY + LEGEND_HEIGHT5 / 2 + LEGEND_ENTRY_FONT_SIZE3 / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE3).attr("fill", palette.textMuted).text(entry.value);
9991
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE3 * 0.6 + 8;
9960
+ const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
9961
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
9962
+ const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
9963
+ entryG.append("text").attr("x", entryTextX).attr("y", legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
9964
+ entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
9992
9965
  }
9993
9966
  legendX += capsuleWidth + 12;
9994
9967
  } else {
@@ -10075,7 +10048,7 @@ function renderKanbanForExport(content, theme, palette) {
10075
10048
  const svgEl = container.querySelector("svg");
10076
10049
  return svgEl?.outerHTML ?? "";
10077
10050
  }
10078
- var d3Selection3, DIAGRAM_PADDING3, COLUMN_GAP, COLUMN_HEADER_HEIGHT, COLUMN_PADDING, COLUMN_MIN_WIDTH, CARD_HEADER_HEIGHT, CARD_META_LINE_HEIGHT, CARD_SEPARATOR_GAP, CARD_GAP, CARD_RADIUS3, CARD_PADDING_X, CARD_PADDING_Y, CARD_STROKE_WIDTH, TITLE_HEIGHT3, TITLE_FONT_SIZE3, COLUMN_HEADER_FONT_SIZE, CARD_TITLE_FONT_SIZE, CARD_META_FONT_SIZE, WIP_FONT_SIZE, COLUMN_RADIUS, COLUMN_HEADER_RADIUS, LEGEND_HEIGHT5, LEGEND_FONT_SIZE, LEGEND_DOT_R5, LEGEND_ENTRY_FONT_SIZE3;
10051
+ var d3Selection3, DIAGRAM_PADDING3, COLUMN_GAP, COLUMN_HEADER_HEIGHT, COLUMN_PADDING, COLUMN_MIN_WIDTH, CARD_HEADER_HEIGHT, CARD_META_LINE_HEIGHT, CARD_SEPARATOR_GAP, CARD_GAP, CARD_RADIUS3, CARD_PADDING_X, CARD_PADDING_Y, CARD_STROKE_WIDTH, TITLE_HEIGHT3, TITLE_FONT_SIZE3, COLUMN_HEADER_FONT_SIZE, CARD_TITLE_FONT_SIZE, CARD_META_FONT_SIZE, WIP_FONT_SIZE, COLUMN_RADIUS, COLUMN_HEADER_RADIUS;
10079
10052
  var init_renderer3 = __esm({
10080
10053
  "src/kanban/renderer.ts"() {
10081
10054
  "use strict";
@@ -10085,6 +10058,7 @@ var init_renderer3 = __esm({
10085
10058
  init_inline_markdown();
10086
10059
  init_parser5();
10087
10060
  init_mutations();
10061
+ init_legend_constants();
10088
10062
  DIAGRAM_PADDING3 = 20;
10089
10063
  COLUMN_GAP = 16;
10090
10064
  COLUMN_HEADER_HEIGHT = 36;
@@ -10106,10 +10080,6 @@ var init_renderer3 = __esm({
10106
10080
  WIP_FONT_SIZE = 10;
10107
10081
  COLUMN_RADIUS = 8;
10108
10082
  COLUMN_HEADER_RADIUS = 8;
10109
- LEGEND_HEIGHT5 = 28;
10110
- LEGEND_FONT_SIZE = 11;
10111
- LEGEND_DOT_R5 = 4;
10112
- LEGEND_ENTRY_FONT_SIZE3 = 10;
10113
10083
  }
10114
10084
  });
10115
10085
 
@@ -10527,110 +10497,211 @@ function computeNodeDimensions2(table) {
10527
10497
  const height = headerHeight + columnsHeight + (columnsHeight === 0 ? 4 : 0);
10528
10498
  return { width, height, headerHeight, columnsHeight };
10529
10499
  }
10500
+ function findConnectedComponents(tableIds, relationships) {
10501
+ const adj = /* @__PURE__ */ new Map();
10502
+ for (const id of tableIds) adj.set(id, /* @__PURE__ */ new Set());
10503
+ for (const rel of relationships) {
10504
+ adj.get(rel.source)?.add(rel.target);
10505
+ adj.get(rel.target)?.add(rel.source);
10506
+ }
10507
+ const visited = /* @__PURE__ */ new Set();
10508
+ const components = [];
10509
+ for (const id of tableIds) {
10510
+ if (visited.has(id)) continue;
10511
+ const comp = [];
10512
+ const queue = [id];
10513
+ while (queue.length > 0) {
10514
+ const cur = queue.shift();
10515
+ if (visited.has(cur)) continue;
10516
+ visited.add(cur);
10517
+ comp.push(cur);
10518
+ for (const nb of adj.get(cur) ?? []) {
10519
+ if (!visited.has(nb)) queue.push(nb);
10520
+ }
10521
+ }
10522
+ components.push(comp);
10523
+ }
10524
+ return components;
10525
+ }
10526
+ function layoutComponent(tables, rels, dimMap) {
10527
+ const nodePositions = /* @__PURE__ */ new Map();
10528
+ const edgePoints = /* @__PURE__ */ new Map();
10529
+ if (tables.length === 1) {
10530
+ const dims = dimMap.get(tables[0].id);
10531
+ nodePositions.set(tables[0].id, { x: dims.width / 2, y: dims.height / 2, ...dims });
10532
+ return { nodePositions, edgePoints, width: dims.width, height: dims.height };
10533
+ }
10534
+ const g = new import_dagre3.default.graphlib.Graph({ multigraph: true });
10535
+ g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80, edgesep: 20 });
10536
+ g.setDefaultEdgeLabel(() => ({}));
10537
+ for (const table of tables) {
10538
+ const dims = dimMap.get(table.id);
10539
+ g.setNode(table.id, { width: dims.width, height: dims.height });
10540
+ }
10541
+ for (const rel of rels) {
10542
+ g.setEdge(rel.source, rel.target, { label: rel.label ?? "" }, String(rel.lineNumber));
10543
+ }
10544
+ import_dagre3.default.layout(g);
10545
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
10546
+ for (const table of tables) {
10547
+ const pos = g.node(table.id);
10548
+ const dims = dimMap.get(table.id);
10549
+ minX = Math.min(minX, pos.x - dims.width / 2);
10550
+ minY = Math.min(minY, pos.y - dims.height / 2);
10551
+ maxX = Math.max(maxX, pos.x + dims.width / 2);
10552
+ maxY = Math.max(maxY, pos.y + dims.height / 2);
10553
+ }
10554
+ for (const rel of rels) {
10555
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10556
+ for (const pt of ed?.points ?? []) {
10557
+ minX = Math.min(minX, pt.x);
10558
+ minY = Math.min(minY, pt.y);
10559
+ maxX = Math.max(maxX, pt.x);
10560
+ maxY = Math.max(maxY, pt.y);
10561
+ }
10562
+ if (rel.label && (ed?.points ?? []).length > 0) {
10563
+ const pts = ed.points;
10564
+ const mid = pts[Math.floor(pts.length / 2)];
10565
+ const hw = (rel.label.length * 7 + 8) / 2;
10566
+ minX = Math.min(minX, mid.x - hw);
10567
+ maxX = Math.max(maxX, mid.x + hw);
10568
+ }
10569
+ }
10570
+ for (const table of tables) {
10571
+ const pos = g.node(table.id);
10572
+ const dims = dimMap.get(table.id);
10573
+ nodePositions.set(table.id, {
10574
+ x: pos.x - minX,
10575
+ y: pos.y - minY,
10576
+ ...dims
10577
+ });
10578
+ }
10579
+ for (const rel of rels) {
10580
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10581
+ edgePoints.set(
10582
+ rel.lineNumber,
10583
+ (ed?.points ?? []).map((pt) => ({ x: pt.x - minX, y: pt.y - minY }))
10584
+ );
10585
+ }
10586
+ return {
10587
+ nodePositions,
10588
+ edgePoints,
10589
+ width: Math.max(0, maxX - minX),
10590
+ height: Math.max(0, maxY - minY)
10591
+ };
10592
+ }
10593
+ function packComponents(items) {
10594
+ if (items.length === 0) return [];
10595
+ const sorted = [...items].sort((a, b) => {
10596
+ const aConnected = a.compIds.length > 1 ? 1 : 0;
10597
+ const bConnected = b.compIds.length > 1 ? 1 : 0;
10598
+ if (aConnected !== bConnected) return bConnected - aConnected;
10599
+ return b.compLayout.height - a.compLayout.height;
10600
+ });
10601
+ const totalArea = items.reduce(
10602
+ (s, c) => s + (c.compLayout.width || MIN_WIDTH2) * (c.compLayout.height || HEADER_BASE2),
10603
+ 0
10604
+ );
10605
+ const targetW = Math.max(
10606
+ Math.sqrt(totalArea) * 1.5,
10607
+ sorted[0].compLayout.width
10608
+ // at least as wide as the widest component
10609
+ );
10610
+ const placements = [];
10611
+ let curX = 0;
10612
+ let curY = 0;
10613
+ let rowH = 0;
10614
+ for (const item of sorted) {
10615
+ const w = item.compLayout.width || MIN_WIDTH2;
10616
+ const h = item.compLayout.height || HEADER_BASE2;
10617
+ if (curX > 0 && curX + w > targetW) {
10618
+ curY += rowH + COMP_GAP;
10619
+ curX = 0;
10620
+ rowH = 0;
10621
+ }
10622
+ placements.push({ compIds: item.compIds, compLayout: item.compLayout, offsetX: curX, offsetY: curY });
10623
+ curX += w + COMP_GAP;
10624
+ rowH = Math.max(rowH, h);
10625
+ }
10626
+ return placements;
10627
+ }
10530
10628
  function layoutERDiagram(parsed) {
10531
10629
  if (parsed.tables.length === 0) {
10532
10630
  return { nodes: [], edges: [], width: 0, height: 0 };
10533
10631
  }
10534
- const g = new import_dagre3.default.graphlib.Graph();
10535
- g.setGraph({
10536
- rankdir: "TB",
10537
- nodesep: 60,
10538
- ranksep: 80,
10539
- edgesep: 20
10540
- });
10541
- g.setDefaultEdgeLabel(() => ({}));
10542
10632
  const dimMap = /* @__PURE__ */ new Map();
10543
10633
  for (const table of parsed.tables) {
10544
- const dims = computeNodeDimensions2(table);
10545
- dimMap.set(table.id, dims);
10546
- g.setNode(table.id, {
10547
- label: table.name,
10548
- width: dims.width,
10549
- height: dims.height
10550
- });
10634
+ dimMap.set(table.id, computeNodeDimensions2(table));
10551
10635
  }
10552
- for (const rel of parsed.relationships) {
10553
- g.setEdge(rel.source, rel.target, { label: rel.label ?? "" });
10636
+ const compIdSets = findConnectedComponents(
10637
+ parsed.tables.map((t) => t.id),
10638
+ parsed.relationships
10639
+ );
10640
+ const tableById = new Map(parsed.tables.map((t) => [t.id, t]));
10641
+ const componentItems = compIdSets.map((ids) => {
10642
+ const tables = ids.map((id) => tableById.get(id));
10643
+ const rels = parsed.relationships.filter((r) => ids.includes(r.source));
10644
+ return { compIds: ids, compLayout: layoutComponent(tables, rels, dimMap) };
10645
+ });
10646
+ const packed = packComponents(componentItems);
10647
+ const placementByTableId = /* @__PURE__ */ new Map();
10648
+ for (const p of packed) {
10649
+ for (const id of p.compIds) placementByTableId.set(id, p);
10650
+ }
10651
+ const placementByRelLine = /* @__PURE__ */ new Map();
10652
+ for (const p of packed) {
10653
+ for (const lineNum of p.compLayout.edgePoints.keys()) {
10654
+ placementByRelLine.set(lineNum, p);
10655
+ }
10554
10656
  }
10555
- import_dagre3.default.layout(g);
10556
10657
  const layoutNodes = parsed.tables.map((table) => {
10557
- const pos = g.node(table.id);
10558
- const dims = dimMap.get(table.id);
10658
+ const p = placementByTableId.get(table.id);
10659
+ const pos = p.compLayout.nodePositions.get(table.id);
10559
10660
  return {
10560
10661
  ...table,
10561
- x: pos.x,
10562
- y: pos.y,
10563
- width: dims.width,
10564
- height: dims.height,
10565
- headerHeight: dims.headerHeight,
10566
- columnsHeight: dims.columnsHeight
10662
+ x: pos.x + p.offsetX + HALF_MARGIN,
10663
+ y: pos.y + p.offsetY + HALF_MARGIN,
10664
+ width: pos.width,
10665
+ height: pos.height,
10666
+ headerHeight: pos.headerHeight,
10667
+ columnsHeight: pos.columnsHeight
10567
10668
  };
10568
10669
  });
10569
10670
  const layoutEdges = parsed.relationships.map((rel) => {
10570
- const edgeData = g.edge(rel.source, rel.target);
10671
+ const p = placementByRelLine.get(rel.lineNumber);
10672
+ const pts = p?.compLayout.edgePoints.get(rel.lineNumber) ?? [];
10571
10673
  return {
10572
10674
  source: rel.source,
10573
10675
  target: rel.target,
10574
10676
  cardinality: rel.cardinality,
10575
- points: edgeData?.points ?? [],
10677
+ points: pts.map((pt) => ({
10678
+ x: pt.x + (p?.offsetX ?? 0) + HALF_MARGIN,
10679
+ y: pt.y + (p?.offsetY ?? 0) + HALF_MARGIN
10680
+ })),
10576
10681
  label: rel.label,
10577
10682
  lineNumber: rel.lineNumber
10578
10683
  };
10579
10684
  });
10580
- let minX = Infinity;
10581
- let minY = Infinity;
10582
10685
  let maxX = 0;
10583
10686
  let maxY = 0;
10584
10687
  for (const node of layoutNodes) {
10585
- const left = node.x - node.width / 2;
10586
- const right = node.x + node.width / 2;
10587
- const top = node.y - node.height / 2;
10588
- const bottom = node.y + node.height / 2;
10589
- if (left < minX) minX = left;
10590
- if (right > maxX) maxX = right;
10591
- if (top < minY) minY = top;
10592
- if (bottom > maxY) maxY = bottom;
10688
+ maxX = Math.max(maxX, node.x + node.width / 2);
10689
+ maxY = Math.max(maxY, node.y + node.height / 2);
10593
10690
  }
10594
10691
  for (const edge of layoutEdges) {
10595
10692
  for (const pt of edge.points) {
10596
- if (pt.x < minX) minX = pt.x;
10597
- if (pt.x > maxX) maxX = pt.x;
10598
- if (pt.y < minY) minY = pt.y;
10599
- if (pt.y > maxY) maxY = pt.y;
10600
- }
10601
- if (edge.label && edge.points.length > 0) {
10602
- const midPt = edge.points[Math.floor(edge.points.length / 2)];
10603
- const labelHalfW = (edge.label.length * 7 + 8) / 2;
10604
- if (midPt.x + labelHalfW > maxX) maxX = midPt.x + labelHalfW;
10605
- if (midPt.x - labelHalfW < minX) minX = midPt.x - labelHalfW;
10693
+ maxX = Math.max(maxX, pt.x);
10694
+ maxY = Math.max(maxY, pt.y);
10606
10695
  }
10607
10696
  }
10608
- const EDGE_MARGIN2 = 60;
10609
- const HALF_MARGIN = EDGE_MARGIN2 / 2;
10610
- const shiftX = -minX + HALF_MARGIN;
10611
- const shiftY = -minY + HALF_MARGIN;
10612
- for (const node of layoutNodes) {
10613
- node.x += shiftX;
10614
- node.y += shiftY;
10615
- }
10616
- for (const edge of layoutEdges) {
10617
- for (const pt of edge.points) {
10618
- pt.x += shiftX;
10619
- pt.y += shiftY;
10620
- }
10621
- }
10622
- maxX += shiftX;
10623
- maxY += shiftY;
10624
- const totalWidth = maxX + HALF_MARGIN;
10625
- const totalHeight = maxY + HALF_MARGIN;
10626
10697
  return {
10627
10698
  nodes: layoutNodes,
10628
10699
  edges: layoutEdges,
10629
- width: totalWidth,
10630
- height: totalHeight
10700
+ width: maxX + HALF_MARGIN,
10701
+ height: maxY + HALF_MARGIN
10631
10702
  };
10632
10703
  }
10633
- var import_dagre3, MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2;
10704
+ var import_dagre3, MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2, HALF_MARGIN, COMP_GAP;
10634
10705
  var init_layout4 = __esm({
10635
10706
  "src/er/layout.ts"() {
10636
10707
  "use strict";
@@ -10642,6 +10713,135 @@ var init_layout4 = __esm({
10642
10713
  MEMBER_LINE_HEIGHT3 = 18;
10643
10714
  COMPARTMENT_PADDING_Y3 = 8;
10644
10715
  SEPARATOR_HEIGHT2 = 1;
10716
+ HALF_MARGIN = 30;
10717
+ COMP_GAP = 60;
10718
+ }
10719
+ });
10720
+
10721
+ // src/er/classify.ts
10722
+ function classifyEREntities(tables, relationships) {
10723
+ const result = /* @__PURE__ */ new Map();
10724
+ if (tables.length === 0) return result;
10725
+ const indegreeMap = {};
10726
+ for (const t of tables) indegreeMap[t.id] = 0;
10727
+ for (const rel of relationships) {
10728
+ if (rel.source === rel.target) continue;
10729
+ if (rel.cardinality.from === "1" && rel.cardinality.to !== "1") {
10730
+ indegreeMap[rel.source] = (indegreeMap[rel.source] ?? 0) + 1;
10731
+ }
10732
+ if (rel.cardinality.to === "1" && rel.cardinality.from !== "1") {
10733
+ indegreeMap[rel.target] = (indegreeMap[rel.target] ?? 0) + 1;
10734
+ }
10735
+ }
10736
+ const tableStarNeighbors = /* @__PURE__ */ new Map();
10737
+ for (const rel of relationships) {
10738
+ if (rel.source === rel.target) continue;
10739
+ if (rel.cardinality.from === "*") {
10740
+ if (!tableStarNeighbors.has(rel.source)) tableStarNeighbors.set(rel.source, /* @__PURE__ */ new Set());
10741
+ tableStarNeighbors.get(rel.source).add(rel.target);
10742
+ }
10743
+ if (rel.cardinality.to === "*") {
10744
+ if (!tableStarNeighbors.has(rel.target)) tableStarNeighbors.set(rel.target, /* @__PURE__ */ new Set());
10745
+ tableStarNeighbors.get(rel.target).add(rel.source);
10746
+ }
10747
+ }
10748
+ const mmParticipants = /* @__PURE__ */ new Set();
10749
+ for (const [id, neighbors] of tableStarNeighbors) {
10750
+ if (neighbors.size >= 2) mmParticipants.add(id);
10751
+ }
10752
+ const indegreeValues = Object.values(indegreeMap);
10753
+ const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
10754
+ const variance = indegreeValues.reduce((a, b) => a + (b - mean) ** 2, 0) / indegreeValues.length;
10755
+ const stddev = Math.sqrt(variance);
10756
+ const sorted = [...indegreeValues].sort((a, b) => a - b);
10757
+ const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)];
10758
+ for (const table of tables) {
10759
+ const id = table.id;
10760
+ const cols = table.columns;
10761
+ const fkCols = cols.filter((c) => c.constraints.includes("fk"));
10762
+ const pkFkCols = cols.filter(
10763
+ (c) => c.constraints.includes("pk") && c.constraints.includes("fk")
10764
+ );
10765
+ const fkCount = fkCols.length;
10766
+ const fkRatio = cols.length === 0 ? 0 : fkCount / cols.length;
10767
+ const indegree = indegreeMap[id] ?? 0;
10768
+ const nameLower = table.name.toLowerCase();
10769
+ const externalRels = relationships.filter(
10770
+ (r) => (r.source === id || r.target === id) && r.source !== r.target
10771
+ );
10772
+ const hasSelfRef = relationships.some((r) => r.source === id && r.target === id);
10773
+ const externalTargets = /* @__PURE__ */ new Set();
10774
+ for (const rel of externalRels) {
10775
+ externalTargets.add(rel.source === id ? rel.target : rel.source);
10776
+ }
10777
+ if (hasSelfRef && externalRels.length === 0) {
10778
+ result.set(id, "self-referential");
10779
+ continue;
10780
+ }
10781
+ const isInheritancePattern = pkFkCols.length >= 2 && externalTargets.size === 1;
10782
+ const junctionByRatio = fkRatio >= 0.6 && !isInheritancePattern;
10783
+ const junctionByCompositePk = pkFkCols.length >= 2 && externalTargets.size >= 2;
10784
+ const junctionByMm = mmParticipants.has(id);
10785
+ if (junctionByRatio || junctionByCompositePk || junctionByMm) {
10786
+ result.set(id, "junction");
10787
+ continue;
10788
+ }
10789
+ if (fkRatio >= 0.4 && fkRatio < 0.6 && pkFkCols.length < 2 && !mmParticipants.has(id)) {
10790
+ result.set(id, "ambiguous");
10791
+ continue;
10792
+ }
10793
+ const nameMatchesLookup = LOOKUP_NAME_SUFFIXES.some((s) => nameLower.endsWith(s));
10794
+ if (nameMatchesLookup && cols.length <= 6 && fkCount <= 1 && indegree > median) {
10795
+ result.set(id, "lookup");
10796
+ continue;
10797
+ }
10798
+ if (tables.length >= 6 && indegree > 0 && indegree > mean + 1.5 * stddev && indegree >= 2 * mean) {
10799
+ result.set(id, "hub");
10800
+ continue;
10801
+ }
10802
+ if (fkCount > 0) {
10803
+ result.set(id, "dependent");
10804
+ continue;
10805
+ }
10806
+ result.set(id, "core");
10807
+ }
10808
+ return result;
10809
+ }
10810
+ var ROLE_COLORS, ROLE_LABELS, ROLE_ORDER, LOOKUP_NAME_SUFFIXES;
10811
+ var init_classify = __esm({
10812
+ "src/er/classify.ts"() {
10813
+ "use strict";
10814
+ ROLE_COLORS = {
10815
+ core: "green",
10816
+ dependent: "blue",
10817
+ junction: "red",
10818
+ ambiguous: "purple",
10819
+ lookup: "yellow",
10820
+ hub: "orange",
10821
+ "self-referential": "teal",
10822
+ unclassified: "gray"
10823
+ };
10824
+ ROLE_LABELS = {
10825
+ core: "Core entity",
10826
+ dependent: "Dependent",
10827
+ junction: "Junction / M:M",
10828
+ ambiguous: "Bridge",
10829
+ lookup: "Lookup / Reference",
10830
+ hub: "Hub",
10831
+ "self-referential": "Self-referential",
10832
+ unclassified: "Unclassified"
10833
+ };
10834
+ ROLE_ORDER = [
10835
+ "core",
10836
+ "dependent",
10837
+ "junction",
10838
+ "ambiguous",
10839
+ "lookup",
10840
+ "hub",
10841
+ "self-referential",
10842
+ "unclassified"
10843
+ ];
10844
+ LOOKUP_NAME_SUFFIXES = ["_type", "_status", "_code", "_category"];
10645
10845
  }
10646
10846
  });
10647
10847
 
@@ -10711,25 +10911,41 @@ function drawCardinality(g, point, prevPoint, cardinality, color, useLabels) {
10711
10911
  g.append("line").attr("x1", bx + px * spread).attr("y1", by + py * spread).attr("x2", bx - px * spread).attr("y2", by - py * spread).attr("stroke", color).attr("stroke-width", sw);
10712
10912
  }
10713
10913
  }
10714
- function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
10914
+ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup, semanticColorsActive) {
10715
10915
  d3Selection5.select(container).selectAll(":not([data-d3-tooltip])").remove();
10716
- const width = exportDims?.width ?? container.clientWidth;
10717
- const height = exportDims?.height ?? container.clientHeight;
10718
- if (width <= 0 || height <= 0) return;
10916
+ const useSemanticColors = parsed.tagGroups.length === 0 && layout.nodes.every((n) => !n.color);
10917
+ const legendReserveH = useSemanticColors ? LEGEND_HEIGHT + DIAGRAM_PADDING5 : 0;
10719
10918
  const titleHeight = parsed.title ? 40 : 0;
10720
10919
  const diagramW = layout.width;
10721
10920
  const diagramH = layout.height;
10722
- const availH = height - titleHeight;
10723
- const scaleX = (width - DIAGRAM_PADDING5 * 2) / diagramW;
10724
- const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10725
- const scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10726
- const scaledW = diagramW * scale;
10727
- const scaledH = diagramH * scale;
10728
- const offsetX = (width - scaledW) / 2;
10729
- const offsetY = titleHeight + DIAGRAM_PADDING5;
10730
- const svg = d3Selection5.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
10921
+ const naturalW = diagramW + DIAGRAM_PADDING5 * 2;
10922
+ const naturalH = diagramH + titleHeight + legendReserveH + DIAGRAM_PADDING5 * 2;
10923
+ let viewW;
10924
+ let viewH;
10925
+ let scale;
10926
+ let offsetX;
10927
+ let offsetY;
10928
+ if (exportDims) {
10929
+ viewW = exportDims.width ?? naturalW;
10930
+ viewH = exportDims.height ?? naturalH;
10931
+ const availH = viewH - titleHeight - legendReserveH;
10932
+ const scaleX = (viewW - DIAGRAM_PADDING5 * 2) / diagramW;
10933
+ const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10934
+ scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10935
+ const scaledW = diagramW * scale;
10936
+ offsetX = (viewW - scaledW) / 2;
10937
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10938
+ } else {
10939
+ viewW = naturalW;
10940
+ viewH = naturalH;
10941
+ scale = 1;
10942
+ offsetX = DIAGRAM_PADDING5;
10943
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10944
+ }
10945
+ if (viewW <= 0 || viewH <= 0) return;
10946
+ const svg = d3Selection5.select(container).append("svg").attr("width", exportDims ? viewW : "100%").attr("height", exportDims ? viewH : "100%").attr("viewBox", `0 0 ${viewW} ${viewH}`).attr("preserveAspectRatio", "xMidYMid meet").style("font-family", FONT_FAMILY);
10731
10947
  if (parsed.title) {
10732
- const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", 30).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", "20px").attr("font-weight", "700").style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
10948
+ const titleEl = svg.append("text").attr("class", "chart-title").attr("x", viewW / 2).attr("y", 30).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", "20px").attr("font-weight", "700").style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
10733
10949
  if (parsed.titleLineNumber) {
10734
10950
  titleEl.attr("data-line-number", parsed.titleLineNumber);
10735
10951
  if (onClickItem) {
@@ -10743,6 +10959,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10743
10959
  }
10744
10960
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
10745
10961
  const seriesColors2 = getSeriesColors(palette);
10962
+ const semanticRoles = useSemanticColors ? classifyEREntities(parsed.tables, parsed.relationships) : null;
10963
+ const semanticActive = semanticRoles !== null && (semanticColorsActive ?? true);
10746
10964
  const useLabels = parsed.options.notation === "labels";
10747
10965
  for (const edge of layout.edges) {
10748
10966
  if (edge.points.length < 2) continue;
@@ -10782,7 +11000,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10782
11000
  for (let ni = 0; ni < layout.nodes.length; ni++) {
10783
11001
  const node = layout.nodes[ni];
10784
11002
  const tagColor = resolveTagColor(node.metadata, parsed.tagGroups, activeTagGroup ?? null);
10785
- const nodeColor2 = node.color ?? tagColor ?? seriesColors2[ni % seriesColors2.length];
11003
+ const semanticColor = semanticActive ? palette.colors[ROLE_COLORS[semanticRoles.get(node.id) ?? "unclassified"]] : semanticRoles ? palette.primary : void 0;
11004
+ const nodeColor2 = node.color ?? tagColor ?? semanticColor ?? seriesColors2[ni % seriesColors2.length];
10786
11005
  const nodeG = contentG.append("g").attr("transform", `translate(${node.x}, ${node.y})`).attr("class", "er-table").attr("data-line-number", String(node.lineNumber)).attr("data-node-id", node.id);
10787
11006
  if (activeTagGroup) {
10788
11007
  const tagKey = activeTagGroup.toLowerCase();
@@ -10791,6 +11010,10 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10791
11010
  nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
10792
11011
  }
10793
11012
  }
11013
+ if (semanticRoles) {
11014
+ const role = semanticRoles.get(node.id);
11015
+ if (role) nodeG.attr("data-er-role", role);
11016
+ }
10794
11017
  if (onClickItem) {
10795
11018
  nodeG.style("cursor", "pointer").on("click", () => {
10796
11019
  onClickItem(node.lineNumber);
@@ -10823,32 +11046,87 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10823
11046
  }
10824
11047
  }
10825
11048
  if (parsed.tagGroups.length > 0) {
10826
- const LEGEND_Y_PAD = 16;
10827
- const LEGEND_PILL_H = 22;
10828
- const LEGEND_PILL_RX = 11;
10829
- const LEGEND_PILL_PAD9 = 10;
11049
+ const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
11050
+ const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
10830
11051
  const LEGEND_GAP2 = 8;
10831
- const LEGEND_FONT_SIZE2 = 11;
10832
- const LEGEND_GROUP_GAP7 = 16;
10833
11052
  const legendG = svg.append("g").attr("class", "er-tag-legend");
11053
+ if (activeTagGroup) {
11054
+ legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
11055
+ }
10834
11056
  let legendX = DIAGRAM_PADDING5;
10835
- let legendY = height - DIAGRAM_PADDING5;
11057
+ let legendY = viewH - DIAGRAM_PADDING5;
10836
11058
  for (const group of parsed.tagGroups) {
10837
11059
  const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
10838
- const labelText = groupG.append("text").attr("x", legendX).attr("y", legendY + LEGEND_PILL_H / 2).attr("dominant-baseline", "central").attr("fill", palette.textMuted).attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(`${group.name}:`);
11060
+ const labelText = groupG.append("text").attr("x", legendX).attr("y", legendY + LEGEND_PILL_H / 2).attr("dominant-baseline", "central").attr("fill", palette.textMuted).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(`${group.name}:`);
10839
11061
  const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
10840
11062
  legendX += labelWidth;
10841
11063
  for (const entry of group.entries) {
10842
11064
  const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
10843
- const tmpText = legendG.append("text").attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(entry.value);
11065
+ const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
10844
11066
  const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
10845
11067
  tmpText.remove();
10846
- const pillW = textW + LEGEND_PILL_PAD9 * 2;
11068
+ const pillW = textW + LEGEND_PILL_PAD * 2;
10847
11069
  pillG.append("rect").attr("x", legendX).attr("y", legendY).attr("width", pillW).attr("height", LEGEND_PILL_H).attr("rx", LEGEND_PILL_RX).attr("ry", LEGEND_PILL_RX).attr("fill", mix(entry.color, isDark ? palette.surface : palette.bg, 25)).attr("stroke", entry.color).attr("stroke-width", 1);
10848
- pillG.append("text").attr("x", legendX + pillW / 2).attr("y", legendY + LEGEND_PILL_H / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(entry.value);
11070
+ pillG.append("text").attr("x", legendX + pillW / 2).attr("y", legendY + LEGEND_PILL_H / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
10849
11071
  legendX += pillW + LEGEND_GAP2;
10850
11072
  }
10851
- legendX += LEGEND_GROUP_GAP7;
11073
+ legendX += LEGEND_GROUP_GAP;
11074
+ }
11075
+ }
11076
+ if (semanticRoles) {
11077
+ const presentRoles = ROLE_ORDER.filter((role) => {
11078
+ for (const r of semanticRoles.values()) {
11079
+ if (r === role) return true;
11080
+ }
11081
+ return false;
11082
+ });
11083
+ if (presentRoles.length > 0) {
11084
+ const measureLabelW = (text, fontSize) => {
11085
+ const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
11086
+ const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
11087
+ dummy.remove();
11088
+ return measured > 0 ? measured : text.length * fontSize * 0.6;
11089
+ };
11090
+ const labelWidths = /* @__PURE__ */ new Map();
11091
+ for (const role of presentRoles) {
11092
+ labelWidths.set(role, measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE));
11093
+ }
11094
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
11095
+ const groupName = "Role";
11096
+ const pillWidth = groupName.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
11097
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
11098
+ let totalWidth;
11099
+ let entriesWidth = 0;
11100
+ if (semanticActive) {
11101
+ for (const role of presentRoles) {
11102
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11103
+ }
11104
+ totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + LEGEND_ENTRY_TRAIL + entriesWidth;
11105
+ } else {
11106
+ totalWidth = pillWidth;
11107
+ }
11108
+ const legendX = (viewW - totalWidth) / 2;
11109
+ const legendY = viewH - DIAGRAM_PADDING5 - LEGEND_HEIGHT;
11110
+ const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
11111
+ if (semanticActive) {
11112
+ semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11113
+ semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
11114
+ semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
11115
+ semanticLegendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
11116
+ let entryX = LEGEND_CAPSULE_PAD + pillWidth + LEGEND_ENTRY_TRAIL;
11117
+ for (const role of presentRoles) {
11118
+ const label = ROLE_LABELS[role];
11119
+ const roleColor = palette.colors[ROLE_COLORS[role]];
11120
+ const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
11121
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
11122
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
11123
+ entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(label);
11124
+ entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11125
+ }
11126
+ } else {
11127
+ semanticLegendG.append("rect").attr("width", pillWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11128
+ semanticLegendG.append("text").attr("x", pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
11129
+ }
10852
11130
  }
10853
11131
  }
10854
11132
  }
@@ -10897,8 +11175,10 @@ var init_renderer5 = __esm({
10897
11175
  init_color_utils();
10898
11176
  init_palettes();
10899
11177
  init_tag_groups();
11178
+ init_legend_constants();
10900
11179
  init_parser3();
10901
11180
  init_layout4();
11181
+ init_classify();
10902
11182
  DIAGRAM_PADDING5 = 20;
10903
11183
  MAX_SCALE4 = 3;
10904
11184
  TABLE_FONT_SIZE = 13;
@@ -11026,9 +11306,10 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11026
11306
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
11027
11307
  const dagrePoints = dagreEdge?.points ?? [];
11028
11308
  const hasIntermediateRank = allNodeX.some((x) => x > src.x + 20 && x < tgt.x - 20);
11029
- const step = Math.min((enterX - exitX) * 0.15, 20);
11309
+ const step = Math.max(0, Math.min((enterX - exitX) * 0.15, 20));
11030
11310
  const isBackEdge = tgt.x < src.x - 5;
11031
- const isYDisplaced = !isBackEdge && Math.abs(tgt.y - src.y) > NODESEP;
11311
+ const isTopExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y < src.y - NODESEP;
11312
+ const isBottomExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y > src.y + NODESEP;
11032
11313
  let points;
11033
11314
  if (isBackEdge) {
11034
11315
  const routeAbove = Math.min(src.y, tgt.y) > avgNodeY;
@@ -11038,28 +11319,43 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11038
11319
  const spreadDir = avgNodeX < rawMidX ? 1 : -1;
11039
11320
  const unclamped = Math.abs(src.x - tgt.x) < NODE_WIDTH ? rawMidX + spreadDir * BACK_EDGE_MIN_SPREAD : rawMidX;
11040
11321
  const midX = Math.min(src.x, Math.max(tgt.x, unclamped));
11322
+ const srcDepart = Math.max(midX + 1, src.x - TOP_EXIT_STEP);
11323
+ const tgtApproach = Math.min(midX - 1, tgt.x + TOP_EXIT_STEP);
11041
11324
  if (routeAbove) {
11042
11325
  const arcY = Math.min(src.y - srcHalfH, tgt.y - tgtHalfH) - BACK_EDGE_MARGIN;
11043
11326
  points = [
11044
11327
  { x: src.x, y: src.y - srcHalfH },
11328
+ { x: srcDepart, y: src.y - srcHalfH - TOP_EXIT_STEP },
11045
11329
  { x: midX, y: arcY },
11330
+ { x: tgtApproach, y: tgt.y - tgtHalfH - TOP_EXIT_STEP },
11046
11331
  { x: tgt.x, y: tgt.y - tgtHalfH }
11047
11332
  ];
11048
11333
  } else {
11049
11334
  const arcY = Math.max(src.y + srcHalfH, tgt.y + tgtHalfH) + BACK_EDGE_MARGIN;
11050
11335
  points = [
11051
11336
  { x: src.x, y: src.y + srcHalfH },
11337
+ { x: srcDepart, y: src.y + srcHalfH + TOP_EXIT_STEP },
11052
11338
  { x: midX, y: arcY },
11339
+ { x: tgtApproach, y: tgt.y + tgtHalfH + TOP_EXIT_STEP },
11053
11340
  { x: tgt.x, y: tgt.y + tgtHalfH }
11054
11341
  ];
11055
11342
  }
11056
- } else if (isYDisplaced) {
11057
- const exitY = tgt.y > src.y + NODESEP ? src.y + src.height / 2 : src.y - src.height / 2;
11058
- const midX = Math.max(src.x + 1, (src.x + enterX) / 2);
11059
- const midY = (exitY + tgt.y) / 2;
11343
+ } else if (isTopExit) {
11344
+ const exitY = src.y - src.height / 2;
11345
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11060
11346
  points = [
11061
11347
  { x: src.x, y: exitY },
11062
- { x: midX, y: midY },
11348
+ { x: p1x, y: exitY - TOP_EXIT_STEP },
11349
+ { x: enterX - step, y: tgt.y + yOffset },
11350
+ { x: enterX, y: tgt.y }
11351
+ ];
11352
+ } else if (isBottomExit) {
11353
+ const exitY = src.y + src.height / 2;
11354
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11355
+ points = [
11356
+ { x: src.x, y: exitY },
11357
+ { x: p1x, y: exitY + TOP_EXIT_STEP },
11358
+ { x: enterX - step, y: tgt.y + yOffset },
11063
11359
  { x: enterX, y: tgt.y }
11064
11360
  ];
11065
11361
  } else if (tgt.x > src.x && !hasIntermediateRank) {
@@ -11157,7 +11453,7 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11157
11453
  totalHeight += 40;
11158
11454
  return { nodes: layoutNodes, edges: layoutEdges, groups: layoutGroups, width: totalWidth, height: totalHeight };
11159
11455
  }
11160
- var import_dagre4, STATUS_PRIORITY, PHI, NODE_HEIGHT, NODE_WIDTH, GROUP_PADDING, NODESEP, RANKSEP, PARALLEL_SPACING, PARALLEL_EDGE_MARGIN, MAX_PARALLEL_EDGES, BACK_EDGE_MARGIN, BACK_EDGE_MIN_SPREAD, CHAR_WIDTH_RATIO, NODE_FONT_SIZE, NODE_TEXT_PADDING;
11456
+ var import_dagre4, STATUS_PRIORITY, PHI, NODE_HEIGHT, NODE_WIDTH, GROUP_PADDING, NODESEP, RANKSEP, PARALLEL_SPACING, PARALLEL_EDGE_MARGIN, MAX_PARALLEL_EDGES, BACK_EDGE_MARGIN, BACK_EDGE_MIN_SPREAD, TOP_EXIT_STEP, CHAR_WIDTH_RATIO, NODE_FONT_SIZE, NODE_TEXT_PADDING;
11161
11457
  var init_layout5 = __esm({
11162
11458
  "src/initiative-status/layout.ts"() {
11163
11459
  "use strict";
@@ -11174,6 +11470,7 @@ var init_layout5 = __esm({
11174
11470
  MAX_PARALLEL_EDGES = 5;
11175
11471
  BACK_EDGE_MARGIN = 40;
11176
11472
  BACK_EDGE_MIN_SPREAD = Math.round(NODE_WIDTH * 0.75);
11473
+ TOP_EXIT_STEP = 10;
11177
11474
  CHAR_WIDTH_RATIO = 0.6;
11178
11475
  NODE_FONT_SIZE = 13;
11179
11476
  NODE_TEXT_PADDING = 12;
@@ -11672,7 +11969,7 @@ __export(layout_exports6, {
11672
11969
  layoutC4Deployment: () => layoutC4Deployment,
11673
11970
  rollUpContextRelationships: () => rollUpContextRelationships
11674
11971
  });
11675
- function computeEdgePenalty(edgeList, nodePositions, degrees) {
11972
+ function computeEdgePenalty(edgeList, nodePositions, degrees, nodeGeometry) {
11676
11973
  let penalty = 0;
11677
11974
  for (const edge of edgeList) {
11678
11975
  const sx = nodePositions.get(edge.source);
@@ -11682,6 +11979,32 @@ function computeEdgePenalty(edgeList, nodePositions, degrees) {
11682
11979
  const weight = Math.min(degrees.get(edge.source) ?? 1, degrees.get(edge.target) ?? 1);
11683
11980
  penalty += dist * weight;
11684
11981
  }
11982
+ if (nodeGeometry) {
11983
+ for (const edge of edgeList) {
11984
+ const geomA = nodeGeometry.get(edge.source);
11985
+ const geomB = nodeGeometry.get(edge.target);
11986
+ if (!geomA || !geomB) continue;
11987
+ const ax = nodePositions.get(edge.source) ?? 0;
11988
+ const bx = nodePositions.get(edge.target) ?? 0;
11989
+ const ay = geomA.y;
11990
+ const by = geomB.y;
11991
+ if (ay === by) continue;
11992
+ const edgeMinX = Math.min(ax, bx);
11993
+ const edgeMaxX = Math.max(ax, bx);
11994
+ const edgeMinY = Math.min(ay, by);
11995
+ const edgeMaxY = Math.max(ay, by);
11996
+ for (const [name, geomC] of nodeGeometry) {
11997
+ if (name === edge.source || name === edge.target) continue;
11998
+ const cx = nodePositions.get(name) ?? 0;
11999
+ const cy = geomC.y;
12000
+ const hw = geomC.width / 2;
12001
+ const hh = geomC.height / 2;
12002
+ if (cx + hw > edgeMinX && cx - hw < edgeMaxX && cy + hh > edgeMinY && cy - hh < edgeMaxY) {
12003
+ penalty += EDGE_NODE_COLLISION_WEIGHT;
12004
+ }
12005
+ }
12006
+ }
12007
+ }
11685
12008
  return penalty;
11686
12009
  }
11687
12010
  function reduceCrossings(g, edgeList, nodeGroupMap) {
@@ -11691,6 +12014,11 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11691
12014
  degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
11692
12015
  degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
11693
12016
  }
12017
+ const nodeGeometry = /* @__PURE__ */ new Map();
12018
+ for (const name of g.nodes()) {
12019
+ const pos = g.node(name);
12020
+ if (pos) nodeGeometry.set(name, { y: pos.y, width: pos.width, height: pos.height });
12021
+ }
11694
12022
  const rankMap = /* @__PURE__ */ new Map();
11695
12023
  for (const name of g.nodes()) {
11696
12024
  const pos = g.node(name);
@@ -11733,7 +12061,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11733
12061
  const pos = g.node(name);
11734
12062
  if (pos) basePositions.set(name, pos.x);
11735
12063
  }
11736
- const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees);
12064
+ const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees, nodeGeometry);
11737
12065
  let bestPerm = [...partition];
11738
12066
  let bestPenalty = currentPenalty;
11739
12067
  if (partition.length <= 8) {
@@ -11743,7 +12071,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11743
12071
  for (let i = 0; i < perm.length; i++) {
11744
12072
  testPositions.set(perm[i], xSlots[i]);
11745
12073
  }
11746
- const penalty = computeEdgePenalty(edgeList, testPositions, degrees);
12074
+ const penalty = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11747
12075
  if (penalty < bestPenalty) {
11748
12076
  bestPenalty = penalty;
11749
12077
  bestPerm = [...perm];
@@ -11761,13 +12089,13 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11761
12089
  for (let k = 0; k < workingOrder.length; k++) {
11762
12090
  testPositions.set(workingOrder[k], xSlots[k]);
11763
12091
  }
11764
- const before = computeEdgePenalty(edgeList, testPositions, degrees);
12092
+ const before = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11765
12093
  [workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1], workingOrder[i]];
11766
12094
  const testPositions2 = new Map(basePositions);
11767
12095
  for (let k = 0; k < workingOrder.length; k++) {
11768
12096
  testPositions2.set(workingOrder[k], xSlots[k]);
11769
12097
  }
11770
- const after = computeEdgePenalty(edgeList, testPositions2, degrees);
12098
+ const after = computeEdgePenalty(edgeList, testPositions2, degrees, nodeGeometry);
11771
12099
  if (after < before) {
11772
12100
  improved = true;
11773
12101
  if (after < bestPenalty) {
@@ -11995,31 +12323,27 @@ function computeC4NodeDimensions(el, options) {
11995
12323
  height += CARD_V_PAD3;
11996
12324
  return { width, height };
11997
12325
  }
11998
- function computeLegendGroups3(tagGroups, usedValuesByGroup) {
12326
+ function computeLegendGroups3(tagGroups) {
11999
12327
  const result = [];
12000
12328
  for (const group of tagGroups) {
12001
12329
  const entries = [];
12002
12330
  for (const entry of group.entries) {
12003
- if (usedValuesByGroup) {
12004
- const used = usedValuesByGroup.get(group.name.toLowerCase());
12005
- if (!used?.has(entry.value.toLowerCase())) continue;
12006
- }
12007
12331
  entries.push({ value: entry.value, color: entry.color });
12008
12332
  }
12009
12333
  if (entries.length === 0) continue;
12010
- const nameW = group.name.length * LEGEND_PILL_FONT_W5 + LEGEND_PILL_PAD5 * 2;
12011
- let capsuleW = LEGEND_CAPSULE_PAD5;
12334
+ const nameW = group.name.length * LEGEND_PILL_FONT_W4 + LEGEND_PILL_PAD4 * 2;
12335
+ let capsuleW = LEGEND_CAPSULE_PAD4;
12012
12336
  for (const e of entries) {
12013
- capsuleW += LEGEND_DOT_R6 * 2 + LEGEND_ENTRY_DOT_GAP5 + e.value.length * LEGEND_ENTRY_FONT_W5 + LEGEND_ENTRY_TRAIL5;
12337
+ capsuleW += LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4 + e.value.length * LEGEND_ENTRY_FONT_W5 + LEGEND_ENTRY_TRAIL4;
12014
12338
  }
12015
- capsuleW += LEGEND_CAPSULE_PAD5;
12339
+ capsuleW += LEGEND_CAPSULE_PAD4;
12016
12340
  result.push({
12017
12341
  name: group.name,
12018
12342
  entries,
12019
12343
  x: 0,
12020
12344
  y: 0,
12021
12345
  width: nameW + capsuleW,
12022
- height: LEGEND_HEIGHT6
12346
+ height: LEGEND_HEIGHT4
12023
12347
  });
12024
12348
  }
12025
12349
  return result;
@@ -12139,18 +12463,7 @@ function layoutC4Context(parsed, activeTagGroup) {
12139
12463
  }
12140
12464
  let totalWidth = nodes.length > 0 ? maxX - minX + MARGIN3 * 2 : 0;
12141
12465
  let totalHeight = nodes.length > 0 ? maxY - minY + MARGIN3 * 2 : 0;
12142
- const usedValuesByGroup = /* @__PURE__ */ new Map();
12143
- for (const el of contextElements) {
12144
- for (const group of parsed.tagGroups) {
12145
- const key = group.name.toLowerCase();
12146
- const val = el.metadata[key];
12147
- if (val) {
12148
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
12149
- usedValuesByGroup.get(key).add(val.toLowerCase());
12150
- }
12151
- }
12152
- }
12153
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
12466
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
12154
12467
  if (legendGroups.length > 0) {
12155
12468
  const legendY = totalHeight + MARGIN3;
12156
12469
  let legendX = MARGIN3;
@@ -12160,7 +12473,7 @@ function layoutC4Context(parsed, activeTagGroup) {
12160
12473
  legendX += lg.width + 12;
12161
12474
  }
12162
12475
  const legendRight = legendX;
12163
- const legendBottom = legendY + LEGEND_HEIGHT6;
12476
+ const legendBottom = legendY + LEGEND_HEIGHT4;
12164
12477
  if (legendRight > totalWidth) totalWidth = legendRight;
12165
12478
  if (legendBottom > totalHeight) totalHeight = legendBottom;
12166
12479
  }
@@ -12466,18 +12779,7 @@ function layoutC4Containers(parsed, systemName, activeTagGroup) {
12466
12779
  }
12467
12780
  let totalWidth = maxX - minX + MARGIN3 * 2;
12468
12781
  let totalHeight = maxY - minY + MARGIN3 * 2;
12469
- const usedValuesByGroup = /* @__PURE__ */ new Map();
12470
- for (const el of [...containers, ...externals]) {
12471
- for (const group of parsed.tagGroups) {
12472
- const key = group.name.toLowerCase();
12473
- const val = el.metadata[key];
12474
- if (val) {
12475
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
12476
- usedValuesByGroup.get(key).add(val.toLowerCase());
12477
- }
12478
- }
12479
- }
12480
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
12782
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
12481
12783
  if (legendGroups.length > 0) {
12482
12784
  const legendY = totalHeight + MARGIN3;
12483
12785
  let legendX = MARGIN3;
@@ -12487,7 +12789,7 @@ function layoutC4Containers(parsed, systemName, activeTagGroup) {
12487
12789
  legendX += lg.width + 12;
12488
12790
  }
12489
12791
  const legendRight = legendX;
12490
- const legendBottom = legendY + LEGEND_HEIGHT6;
12792
+ const legendBottom = legendY + LEGEND_HEIGHT4;
12491
12793
  if (legendRight > totalWidth) totalWidth = legendRight;
12492
12794
  if (legendBottom > totalHeight) totalHeight = legendBottom;
12493
12795
  }
@@ -12842,21 +13144,7 @@ function layoutC4Components(parsed, systemName, containerName, activeTagGroup) {
12842
13144
  }
12843
13145
  let totalWidth = maxX - minX + MARGIN3 * 2;
12844
13146
  let totalHeight = maxY - minY + MARGIN3 * 2;
12845
- const usedValuesByGroup = /* @__PURE__ */ new Map();
12846
- for (const el of [...components, ...externals]) {
12847
- for (const group of parsed.tagGroups) {
12848
- const key = group.name.toLowerCase();
12849
- let val = el.metadata[key];
12850
- if (!val && components.includes(el)) {
12851
- val = targetContainer.metadata[key] ?? system.metadata[key];
12852
- }
12853
- if (val) {
12854
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
12855
- usedValuesByGroup.get(key).add(val.toLowerCase());
12856
- }
12857
- }
12858
- }
12859
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
13147
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
12860
13148
  if (legendGroups.length > 0) {
12861
13149
  const legendY = totalHeight + MARGIN3;
12862
13150
  let legendX = MARGIN3;
@@ -12866,7 +13154,7 @@ function layoutC4Components(parsed, systemName, containerName, activeTagGroup) {
12866
13154
  legendX += lg.width + 12;
12867
13155
  }
12868
13156
  const legendRight = legendX;
12869
- const legendBottom = legendY + LEGEND_HEIGHT6;
13157
+ const legendBottom = legendY + LEGEND_HEIGHT4;
12870
13158
  if (legendRight > totalWidth) totalWidth = legendRight;
12871
13159
  if (legendBottom > totalHeight) totalHeight = legendBottom;
12872
13160
  }
@@ -13111,18 +13399,7 @@ function layoutC4Deployment(parsed, activeTagGroup) {
13111
13399
  }
13112
13400
  let totalWidth = maxX - minX + MARGIN3 * 2;
13113
13401
  let totalHeight = maxY - minY + MARGIN3 * 2;
13114
- const usedValuesByGroup = /* @__PURE__ */ new Map();
13115
- for (const r of refEntries) {
13116
- for (const group of parsed.tagGroups) {
13117
- const key = group.name.toLowerCase();
13118
- const val = r.element.metadata[key];
13119
- if (val) {
13120
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
13121
- usedValuesByGroup.get(key).add(val.toLowerCase());
13122
- }
13123
- }
13124
- }
13125
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
13402
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
13126
13403
  if (legendGroups.length > 0) {
13127
13404
  const legendY = totalHeight + MARGIN3;
13128
13405
  let legendX = MARGIN3;
@@ -13132,13 +13409,13 @@ function layoutC4Deployment(parsed, activeTagGroup) {
13132
13409
  legendX += lg.width + 12;
13133
13410
  }
13134
13411
  const legendRight = legendX;
13135
- const legendBottom = legendY + LEGEND_HEIGHT6;
13412
+ const legendBottom = legendY + LEGEND_HEIGHT4;
13136
13413
  if (legendRight > totalWidth) totalWidth = legendRight;
13137
13414
  if (legendBottom > totalHeight) totalHeight = legendBottom;
13138
13415
  }
13139
13416
  return { nodes, edges, legend: legendGroups, groupBoundaries, width: totalWidth, height: totalHeight };
13140
13417
  }
13141
- var import_dagre5, CHAR_WIDTH5, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TYPE_LABEL_HEIGHT, DIVIDER_GAP, NAME_HEIGHT, DESC_LINE_HEIGHT, DESC_CHAR_WIDTH, CARD_V_PAD3, CARD_H_PAD3, META_LINE_HEIGHT5, META_CHAR_WIDTH, MARGIN3, BOUNDARY_PAD, GROUP_BOUNDARY_PAD, LEGEND_HEIGHT6, LEGEND_PILL_FONT_SIZE3, LEGEND_PILL_FONT_W5, LEGEND_PILL_PAD5, LEGEND_DOT_R6, LEGEND_ENTRY_FONT_SIZE4, LEGEND_ENTRY_FONT_W5, LEGEND_ENTRY_DOT_GAP5, LEGEND_ENTRY_TRAIL5, LEGEND_CAPSULE_PAD5, META_EXCLUDE_KEYS;
13418
+ var import_dagre5, CHAR_WIDTH5, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TYPE_LABEL_HEIGHT, DIVIDER_GAP, NAME_HEIGHT, DESC_LINE_HEIGHT, DESC_CHAR_WIDTH, CARD_V_PAD3, CARD_H_PAD3, META_LINE_HEIGHT5, META_CHAR_WIDTH, MARGIN3, BOUNDARY_PAD, GROUP_BOUNDARY_PAD, LEGEND_HEIGHT4, LEGEND_PILL_FONT_SIZE2, LEGEND_PILL_FONT_W4, LEGEND_PILL_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_FONT_SIZE2, LEGEND_ENTRY_FONT_W5, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_CAPSULE_PAD4, EDGE_NODE_COLLISION_WEIGHT, META_EXCLUDE_KEYS;
13142
13419
  var init_layout6 = __esm({
13143
13420
  "src/c4/layout.ts"() {
13144
13421
  "use strict";
@@ -13158,16 +13435,17 @@ var init_layout6 = __esm({
13158
13435
  MARGIN3 = 40;
13159
13436
  BOUNDARY_PAD = 40;
13160
13437
  GROUP_BOUNDARY_PAD = 24;
13161
- LEGEND_HEIGHT6 = 28;
13162
- LEGEND_PILL_FONT_SIZE3 = 11;
13163
- LEGEND_PILL_FONT_W5 = LEGEND_PILL_FONT_SIZE3 * 0.6;
13164
- LEGEND_PILL_PAD5 = 16;
13165
- LEGEND_DOT_R6 = 4;
13166
- LEGEND_ENTRY_FONT_SIZE4 = 10;
13167
- LEGEND_ENTRY_FONT_W5 = LEGEND_ENTRY_FONT_SIZE4 * 0.6;
13168
- LEGEND_ENTRY_DOT_GAP5 = 4;
13169
- LEGEND_ENTRY_TRAIL5 = 8;
13170
- LEGEND_CAPSULE_PAD5 = 4;
13438
+ LEGEND_HEIGHT4 = 28;
13439
+ LEGEND_PILL_FONT_SIZE2 = 11;
13440
+ LEGEND_PILL_FONT_W4 = LEGEND_PILL_FONT_SIZE2 * 0.6;
13441
+ LEGEND_PILL_PAD4 = 16;
13442
+ LEGEND_DOT_R4 = 4;
13443
+ LEGEND_ENTRY_FONT_SIZE2 = 10;
13444
+ LEGEND_ENTRY_FONT_W5 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
13445
+ LEGEND_ENTRY_DOT_GAP4 = 4;
13446
+ LEGEND_ENTRY_TRAIL4 = 8;
13447
+ LEGEND_CAPSULE_PAD4 = 4;
13448
+ EDGE_NODE_COLLISION_WEIGHT = 5e3;
13171
13449
  META_EXCLUDE_KEYS = /* @__PURE__ */ new Set(["description", "tech", "technology", "is a"]);
13172
13450
  }
13173
13451
  });
@@ -13243,8 +13521,14 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
13243
13521
  if (width <= 0 || height <= 0) return;
13244
13522
  const titleHeight = parsed.title ? TITLE_HEIGHT4 + 10 : 0;
13245
13523
  const diagramW = layout.width;
13246
- const diagramH = layout.height;
13247
- const availH = height - titleHeight;
13524
+ const hasLegend = layout.legend.length > 0;
13525
+ const C4_LAYOUT_MARGIN = 40;
13526
+ const LEGEND_FIXED_GAP4 = 8;
13527
+ const fixedLegend = !exportDims && hasLegend;
13528
+ const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
13529
+ const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP4 : 0;
13530
+ const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
13531
+ const availH = height - titleHeight - legendReserveH;
13248
13532
  const scaleX = (width - DIAGRAM_PADDING7 * 2) / diagramW;
13249
13533
  const scaleY = (availH - DIAGRAM_PADDING7 * 2) / diagramH;
13250
13534
  const scale = Math.min(MAX_SCALE6, scaleX, scaleY);
@@ -13316,6 +13600,20 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
13316
13600
  }
13317
13601
  for (const node of layout.nodes) {
13318
13602
  const nodeG = contentG.append("g").attr("transform", `translate(${node.x}, ${node.y})`).attr("class", "c4-card").attr("data-line-number", String(node.lineNumber)).attr("data-node-id", node.id);
13603
+ if (activeTagGroup) {
13604
+ const tagKey = activeTagGroup.toLowerCase();
13605
+ const tagValue = node.metadata[tagKey];
13606
+ if (tagValue) {
13607
+ nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
13608
+ } else {
13609
+ const tagGroup = parsed.tagGroups.find(
13610
+ (g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
13611
+ );
13612
+ if (tagGroup?.defaultValue) {
13613
+ nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
13614
+ }
13615
+ }
13616
+ }
13319
13617
  if (node.importPath) {
13320
13618
  nodeG.attr("data-import-path", node.importPath);
13321
13619
  }
@@ -13368,36 +13666,12 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
13368
13666
  nodeG.append("rect").attr("x", -w / 2).attr("y", h / 2 - DRILL_BAR_HEIGHT).attr("width", w).attr("height", DRILL_BAR_HEIGHT).attr("fill", stroke2).attr("clip-path", `url(#${clipId})`).attr("class", "c4-drill-bar");
13369
13667
  }
13370
13668
  }
13371
- if (!exportDims) {
13372
- for (const group of layout.legend) {
13373
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
13374
- if (activeTagGroup != null && !isActive) continue;
13375
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13376
- const pillLabel = group.name;
13377
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W6 + LEGEND_PILL_PAD6;
13378
- const gEl = contentG.append("g").attr("transform", `translate(${group.x}, ${group.y})`).attr("class", "c4-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
13379
- if (isActive) {
13380
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT7).attr("rx", LEGEND_HEIGHT7 / 2).attr("fill", groupBg);
13381
- }
13382
- const pillX = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13383
- const pillY = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13384
- const pillH = LEGEND_HEIGHT7 - (isActive ? LEGEND_CAPSULE_PAD6 * 2 : 0);
13385
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
13386
- if (isActive) {
13387
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13388
- }
13389
- gEl.append("text").attr("x", pillX + pillWidth / 2).attr("y", LEGEND_HEIGHT7 / 2 + LEGEND_PILL_FONT_SIZE4 / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE4).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
13390
- if (isActive) {
13391
- let entryX = pillX + pillWidth + 4;
13392
- for (const entry of group.entries) {
13393
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13394
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R7).attr("cy", LEGEND_HEIGHT7 / 2).attr("r", LEGEND_DOT_R7).attr("fill", entry.color);
13395
- const textX = entryX + LEGEND_DOT_R7 * 2 + LEGEND_ENTRY_DOT_GAP6;
13396
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT7 / 2 + LEGEND_ENTRY_FONT_SIZE5 / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE5).attr("fill", palette.textMuted).text(entry.value);
13397
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W6 + LEGEND_ENTRY_TRAIL6;
13398
- }
13399
- }
13669
+ if (hasLegend) {
13670
+ const legendParent = fixedLegend ? svg.append("g").attr("class", "c4-legend-fixed").attr("transform", `translate(0, ${height - DIAGRAM_PADDING7 - LEGEND_HEIGHT})`) : contentG.append("g").attr("class", "c4-legend");
13671
+ if (activeTagGroup) {
13672
+ legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
13400
13673
  }
13674
+ renderLegend2(legendParent, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
13401
13675
  }
13402
13676
  }
13403
13677
  function renderC4ContextForExport(content, theme, palette) {
@@ -13685,33 +13959,47 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
13685
13959
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
13686
13960
  }
13687
13961
  }
13688
- function renderLegend2(contentG, layout, palette, isDark, activeTagGroup) {
13689
- for (const group of layout.legend) {
13962
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
13963
+ const visibleGroups = activeTagGroup != null ? layout.legend.filter((g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()) : layout.legend;
13964
+ const pillWidthOf = (g) => g.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
13965
+ const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
13966
+ let fixedPositions = null;
13967
+ if (fixedWidth != null && visibleGroups.length > 0) {
13968
+ fixedPositions = /* @__PURE__ */ new Map();
13969
+ const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13970
+ let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
13971
+ for (const g of visibleGroups) {
13972
+ fixedPositions.set(g.name, cx);
13973
+ cx += effectiveW(g) + LEGEND_GROUP_GAP;
13974
+ }
13975
+ }
13976
+ for (const group of visibleGroups) {
13690
13977
  const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
13691
- if (activeTagGroup != null && !isActive) continue;
13692
13978
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13693
13979
  const pillLabel = group.name;
13694
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W6 + LEGEND_PILL_PAD6;
13695
- const gEl = contentG.append("g").attr("transform", `translate(${group.x}, ${group.y})`).attr("class", "c4-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
13980
+ const pillWidth = pillWidthOf(group);
13981
+ const gX = fixedPositions?.get(group.name) ?? group.x;
13982
+ const gY = fixedPositions != null ? 0 : group.y;
13983
+ const gEl = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "c4-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
13696
13984
  if (isActive) {
13697
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT7).attr("rx", LEGEND_HEIGHT7 / 2).attr("fill", groupBg);
13985
+ gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13698
13986
  }
13699
- const pillX = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13700
- const pillY = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13701
- const pillH = LEGEND_HEIGHT7 - (isActive ? LEGEND_CAPSULE_PAD6 * 2 : 0);
13987
+ const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
13988
+ const pillY = isActive ? LEGEND_CAPSULE_PAD : 0;
13989
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
13702
13990
  gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
13703
13991
  if (isActive) {
13704
13992
  gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13705
13993
  }
13706
- gEl.append("text").attr("x", pillX + pillWidth / 2).attr("y", LEGEND_HEIGHT7 / 2 + LEGEND_PILL_FONT_SIZE4 / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE4).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
13994
+ gEl.append("text").attr("x", pillX + pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
13707
13995
  if (isActive) {
13708
13996
  let entryX = pillX + pillWidth + 4;
13709
13997
  for (const entry of group.entries) {
13710
13998
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13711
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R7).attr("cy", LEGEND_HEIGHT7 / 2).attr("r", LEGEND_DOT_R7).attr("fill", entry.color);
13712
- const textX = entryX + LEGEND_DOT_R7 * 2 + LEGEND_ENTRY_DOT_GAP6;
13713
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT7 / 2 + LEGEND_ENTRY_FONT_SIZE5 / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE5).attr("fill", palette.textMuted).text(entry.value);
13714
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W6 + LEGEND_ENTRY_TRAIL6;
13999
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
14000
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
14001
+ entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
14002
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
13715
14003
  }
13716
14004
  }
13717
14005
  }
@@ -13723,8 +14011,14 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
13723
14011
  if (width <= 0 || height <= 0) return;
13724
14012
  const titleHeight = parsed.title ? TITLE_HEIGHT4 + 10 : 0;
13725
14013
  const diagramW = layout.width;
13726
- const diagramH = layout.height;
13727
- const availH = height - titleHeight;
14014
+ const hasLegend = layout.legend.length > 0;
14015
+ const C4_LAYOUT_MARGIN = 40;
14016
+ const LEGEND_FIXED_GAP4 = 8;
14017
+ const fixedLegend = !exportDims && hasLegend;
14018
+ const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
14019
+ const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP4 : 0;
14020
+ const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
14021
+ const availH = height - titleHeight - legendReserveH;
13728
14022
  const scaleX = (width - DIAGRAM_PADDING7 * 2) / diagramW;
13729
14023
  const scaleY = (availH - DIAGRAM_PADDING7 * 2) / diagramH;
13730
14024
  const scale = Math.min(MAX_SCALE6, scaleX, scaleY);
@@ -13795,6 +14089,20 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
13795
14089
  renderEdges(contentG, layout.edges, palette, onClickItem, boundaryLabelObstacles);
13796
14090
  for (const node of layout.nodes) {
13797
14091
  const nodeG = contentG.append("g").attr("transform", `translate(${node.x}, ${node.y})`).attr("class", "c4-card").attr("data-line-number", String(node.lineNumber)).attr("data-node-id", node.id);
14092
+ if (activeTagGroup) {
14093
+ const tagKey = activeTagGroup.toLowerCase();
14094
+ const tagValue = node.metadata[tagKey];
14095
+ if (tagValue) {
14096
+ nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
14097
+ } else {
14098
+ const tagGroup = parsed.tagGroups.find(
14099
+ (g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
14100
+ );
14101
+ if (tagGroup?.defaultValue) {
14102
+ nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
14103
+ }
14104
+ }
14105
+ }
13798
14106
  if (node.shape) {
13799
14107
  nodeG.attr("data-shape", node.shape);
13800
14108
  }
@@ -13880,8 +14188,12 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
13880
14188
  nodeG.append("rect").attr("x", -w / 2).attr("y", h / 2 - DRILL_BAR_HEIGHT).attr("width", w).attr("height", DRILL_BAR_HEIGHT).attr("fill", stroke2).attr("clip-path", `url(#${clipId})`).attr("class", "c4-drill-bar");
13881
14189
  }
13882
14190
  }
13883
- if (!exportDims) {
13884
- renderLegend2(contentG, layout, palette, isDark, activeTagGroup);
14191
+ if (hasLegend) {
14192
+ const legendParent = fixedLegend ? svg.append("g").attr("class", "c4-legend-fixed").attr("transform", `translate(0, ${height - DIAGRAM_PADDING7 - LEGEND_HEIGHT})`) : contentG.append("g").attr("class", "c4-legend");
14193
+ if (activeTagGroup) {
14194
+ legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
14195
+ }
14196
+ renderLegend2(legendParent, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
13885
14197
  }
13886
14198
  }
13887
14199
  function renderC4ContainersForExport(content, systemName, theme, palette) {
@@ -13983,7 +14295,7 @@ function renderC4DeploymentForExport(content, theme, palette) {
13983
14295
  document.body.removeChild(el);
13984
14296
  }
13985
14297
  }
13986
- var d3Selection7, d3Shape5, DIAGRAM_PADDING7, MAX_SCALE6, TITLE_HEIGHT4, TITLE_FONT_SIZE4, TYPE_FONT_SIZE, NAME_FONT_SIZE, DESC_FONT_SIZE, DESC_LINE_HEIGHT2, DESC_CHAR_WIDTH2, EDGE_LABEL_FONT_SIZE5, TECH_FONT_SIZE, EDGE_STROKE_WIDTH6, NODE_STROKE_WIDTH6, CARD_RADIUS4, CARD_H_PAD4, CARD_V_PAD4, TYPE_LABEL_HEIGHT2, DIVIDER_GAP2, NAME_HEIGHT2, META_FONT_SIZE3, META_CHAR_WIDTH2, META_LINE_HEIGHT6, BOUNDARY_LABEL_FONT_SIZE, BOUNDARY_STROKE_WIDTH, BOUNDARY_RADIUS, DRILL_BAR_HEIGHT, CYLINDER_RY, PERSON_HEAD_R, PERSON_ARM_SPAN, PERSON_LEG_SPAN, PERSON_ICON_W, PERSON_SW, LEGEND_HEIGHT7, LEGEND_PILL_FONT_SIZE4, LEGEND_PILL_FONT_W6, LEGEND_PILL_PAD6, LEGEND_DOT_R7, LEGEND_ENTRY_FONT_SIZE5, LEGEND_ENTRY_FONT_W6, LEGEND_ENTRY_DOT_GAP6, LEGEND_ENTRY_TRAIL6, LEGEND_CAPSULE_PAD6, lineGenerator5;
14298
+ var d3Selection7, d3Shape5, DIAGRAM_PADDING7, MAX_SCALE6, TITLE_HEIGHT4, TITLE_FONT_SIZE4, TYPE_FONT_SIZE, NAME_FONT_SIZE, DESC_FONT_SIZE, DESC_LINE_HEIGHT2, DESC_CHAR_WIDTH2, EDGE_LABEL_FONT_SIZE5, TECH_FONT_SIZE, EDGE_STROKE_WIDTH6, NODE_STROKE_WIDTH6, CARD_RADIUS4, CARD_H_PAD4, CARD_V_PAD4, TYPE_LABEL_HEIGHT2, DIVIDER_GAP2, NAME_HEIGHT2, META_FONT_SIZE3, META_CHAR_WIDTH2, META_LINE_HEIGHT6, BOUNDARY_LABEL_FONT_SIZE, BOUNDARY_STROKE_WIDTH, BOUNDARY_RADIUS, DRILL_BAR_HEIGHT, CYLINDER_RY, PERSON_HEAD_R, PERSON_ARM_SPAN, PERSON_LEG_SPAN, PERSON_ICON_W, PERSON_SW, lineGenerator5;
13987
14299
  var init_renderer7 = __esm({
13988
14300
  "src/c4/renderer.ts"() {
13989
14301
  "use strict";
@@ -13994,6 +14306,7 @@ var init_renderer7 = __esm({
13994
14306
  init_inline_markdown();
13995
14307
  init_parser6();
13996
14308
  init_layout6();
14309
+ init_legend_constants();
13997
14310
  DIAGRAM_PADDING7 = 20;
13998
14311
  MAX_SCALE6 = 3;
13999
14312
  TITLE_HEIGHT4 = 30;
@@ -14026,16 +14339,6 @@ var init_renderer7 = __esm({
14026
14339
  PERSON_LEG_SPAN = 7;
14027
14340
  PERSON_ICON_W = PERSON_ARM_SPAN * 2;
14028
14341
  PERSON_SW = 1.5;
14029
- LEGEND_HEIGHT7 = 28;
14030
- LEGEND_PILL_FONT_SIZE4 = 11;
14031
- LEGEND_PILL_FONT_W6 = LEGEND_PILL_FONT_SIZE4 * 0.6;
14032
- LEGEND_PILL_PAD6 = 16;
14033
- LEGEND_DOT_R7 = 4;
14034
- LEGEND_ENTRY_FONT_SIZE5 = 10;
14035
- LEGEND_ENTRY_FONT_W6 = LEGEND_ENTRY_FONT_SIZE5 * 0.6;
14036
- LEGEND_ENTRY_DOT_GAP6 = 4;
14037
- LEGEND_ENTRY_TRAIL6 = 8;
14038
- LEGEND_CAPSULE_PAD6 = 4;
14039
14342
  lineGenerator5 = d3Shape5.line().x((d) => d.x).y((d) => d.y).curve(d3Shape5.curveBasis);
14040
14343
  }
14041
14344
  });
@@ -14891,23 +15194,6 @@ function computeInfra(parsed, params = {}) {
14891
15194
  const defaultLatencyMs = parseFloat(parsed.options["default-latency-ms"] ?? "") || 0;
14892
15195
  const defaultUptime = parseFloat(parsed.options["default-uptime"] ?? "") || 100;
14893
15196
  let effectiveNodes = parsed.nodes;
14894
- if (params.scenario) {
14895
- const overrides = params.scenario.overrides;
14896
- effectiveNodes = parsed.nodes.map((node) => {
14897
- const nodeOverrides = overrides[node.id];
14898
- if (!nodeOverrides) return node;
14899
- const props = node.properties.map((p) => {
14900
- const ov = nodeOverrides[p.key];
14901
- return ov != null ? { ...p, value: ov } : p;
14902
- });
14903
- for (const [key, val] of Object.entries(nodeOverrides)) {
14904
- if (!props.some((p) => p.key === key)) {
14905
- props.push({ key, value: val, lineNumber: node.lineNumber });
14906
- }
14907
- }
14908
- return { ...node, properties: props };
14909
- });
14910
- }
14911
15197
  if (params.propertyOverrides) {
14912
15198
  const propOv = params.propertyOverrides;
14913
15199
  effectiveNodes = effectiveNodes.map((node) => {
@@ -15344,6 +15630,7 @@ var init_compute = __esm({
15344
15630
  // src/infra/layout.ts
15345
15631
  var layout_exports8 = {};
15346
15632
  __export(layout_exports8, {
15633
+ fixEdgeWaypoints: () => fixEdgeWaypoints,
15347
15634
  layoutInfra: () => layoutInfra,
15348
15635
  separateGroups: () => separateGroups
15349
15636
  });
@@ -15527,6 +15814,8 @@ function formatUptime(fraction) {
15527
15814
  return `${pct.toFixed(1)}%`;
15528
15815
  }
15529
15816
  function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15817
+ const groupDeltas = /* @__PURE__ */ new Map();
15818
+ let converged = false;
15530
15819
  for (let iter = 0; iter < maxIterations; iter++) {
15531
15820
  let anyOverlap = false;
15532
15821
  for (let i = 0; i < groups.length; i++) {
@@ -15544,6 +15833,9 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15544
15833
  const groupToShift = aCenter <= bCenter ? gb : ga;
15545
15834
  if (isLR) groupToShift.y += shift;
15546
15835
  else groupToShift.x += shift;
15836
+ const prev = groupDeltas.get(groupToShift.id) ?? { dx: 0, dy: 0 };
15837
+ if (isLR) groupDeltas.set(groupToShift.id, { dx: prev.dx, dy: prev.dy + shift });
15838
+ else groupDeltas.set(groupToShift.id, { dx: prev.dx + shift, dy: prev.dy });
15547
15839
  for (const node of nodes) {
15548
15840
  if (node.groupId === groupToShift.id) {
15549
15841
  if (isLR) node.y += shift;
@@ -15552,19 +15844,48 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15552
15844
  }
15553
15845
  }
15554
15846
  }
15555
- if (!anyOverlap) break;
15847
+ if (!anyOverlap) {
15848
+ converged = true;
15849
+ break;
15850
+ }
15851
+ }
15852
+ if (!converged && maxIterations > 0) {
15853
+ console.warn(`separateGroups: hit maxIterations (${maxIterations}) without fully resolving all group overlaps`);
15854
+ }
15855
+ return groupDeltas;
15856
+ }
15857
+ function fixEdgeWaypoints(edges, nodes, groupDeltas) {
15858
+ if (groupDeltas.size === 0) return;
15859
+ const nodeToGroup = /* @__PURE__ */ new Map();
15860
+ for (const node of nodes) nodeToGroup.set(node.id, node.groupId);
15861
+ for (const edge of edges) {
15862
+ const srcGroup = nodeToGroup.get(edge.sourceId) ?? null;
15863
+ const tgtGroup = nodeToGroup.get(edge.targetId) ?? null;
15864
+ const srcDelta = srcGroup ? groupDeltas.get(srcGroup) : void 0;
15865
+ const tgtDelta = tgtGroup ? groupDeltas.get(tgtGroup) : void 0;
15866
+ if (!srcDelta && !tgtDelta) continue;
15867
+ if (srcDelta && tgtDelta && srcGroup !== tgtGroup) {
15868
+ edge.points = [];
15869
+ continue;
15870
+ }
15871
+ const delta = srcDelta ?? tgtDelta;
15872
+ for (const pt of edge.points) {
15873
+ pt.x += delta.dx;
15874
+ pt.y += delta.dy;
15875
+ }
15556
15876
  }
15557
15877
  }
15558
15878
  function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15559
15879
  if (computed.nodes.length === 0) {
15560
- return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15880
+ return { nodes: [], edges: [], groups: [], options: {}, direction: computed.direction, width: 0, height: 0 };
15561
15881
  }
15882
+ const isLR = computed.direction !== "TB";
15562
15883
  const g = new import_dagre7.default.graphlib.Graph();
15563
15884
  g.setGraph({
15564
15885
  rankdir: computed.direction === "TB" ? "TB" : "LR",
15565
- nodesep: 50,
15566
- ranksep: 100,
15567
- edgesep: 20
15886
+ nodesep: isLR ? 70 : 60,
15887
+ ranksep: isLR ? 150 : 120,
15888
+ edgesep: 30
15568
15889
  });
15569
15890
  g.setDefaultEdgeLabel(() => ({}));
15570
15891
  const groupedNodeIds = /* @__PURE__ */ new Set();
@@ -15572,7 +15893,6 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15572
15893
  if (node.groupId) groupedNodeIds.add(node.id);
15573
15894
  }
15574
15895
  const GROUP_INFLATE = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15575
- const isLR = computed.direction !== "TB";
15576
15896
  const widthMap = /* @__PURE__ */ new Map();
15577
15897
  const heightMap = /* @__PURE__ */ new Map();
15578
15898
  for (const node of computed.nodes) {
@@ -15707,7 +16027,8 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15707
16027
  lineNumber: group.lineNumber
15708
16028
  };
15709
16029
  });
15710
- separateGroups(layoutGroups, layoutNodes, isLR);
16030
+ const groupDeltas = separateGroups(layoutGroups, layoutNodes, isLR);
16031
+ fixEdgeWaypoints(layoutEdges, layoutNodes, groupDeltas);
15711
16032
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
15712
16033
  for (const node of layoutNodes) {
15713
16034
  const left = node.x - node.width / 2;
@@ -15765,6 +16086,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15765
16086
  edges: layoutEdges,
15766
16087
  groups: layoutGroups,
15767
16088
  options: computed.options,
16089
+ direction: computed.direction,
15768
16090
  width: totalWidth,
15769
16091
  height: totalHeight
15770
16092
  };
@@ -15807,23 +16129,23 @@ var init_layout8 = __esm({
15807
16129
  ]);
15808
16130
  DISPLAY_NAMES = {
15809
16131
  "cache-hit": "cache hit",
15810
- "firewall-block": "fw block",
16132
+ "firewall-block": "firewall block",
15811
16133
  "ratelimit-rps": "rate limit RPS",
15812
16134
  "latency-ms": "latency",
15813
16135
  "uptime": "uptime",
15814
16136
  "instances": "instances",
15815
16137
  "max-rps": "max RPS",
15816
- "cb-error-threshold": "CB error",
15817
- "cb-latency-threshold-ms": "CB latency",
16138
+ "cb-error-threshold": "CB error threshold",
16139
+ "cb-latency-threshold-ms": "CB latency threshold",
15818
16140
  "concurrency": "concurrency",
15819
16141
  "duration-ms": "duration",
15820
16142
  "cold-start-ms": "cold start",
15821
16143
  "buffer": "buffer",
15822
- "drain-rate": "drain",
16144
+ "drain-rate": "drain rate",
15823
16145
  "retention-hours": "retention",
15824
16146
  "partitions": "partitions"
15825
16147
  };
15826
- GROUP_GAP = 24;
16148
+ GROUP_GAP = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15827
16149
  }
15828
16150
  });
15829
16151
 
@@ -15896,6 +16218,236 @@ function resolveNodeSlo(node, diagramOptions) {
15896
16218
  if (availThreshold == null && latencyP90 == null) return null;
15897
16219
  return { availThreshold, latencyP90, warningMargin };
15898
16220
  }
16221
+ function buildPathD(pts, direction) {
16222
+ const gen = d3Shape7.line().x((d) => d.x).y((d) => d.y);
16223
+ if (pts.length <= 2) {
16224
+ gen.curve(direction === "TB" ? d3Shape7.curveBumpY : d3Shape7.curveBumpX);
16225
+ } else {
16226
+ gen.curve(d3Shape7.curveCatmullRom.alpha(0.5));
16227
+ }
16228
+ return gen(pts) ?? "";
16229
+ }
16230
+ function computePortPts(edges, nodeMap, direction) {
16231
+ const srcPts = /* @__PURE__ */ new Map();
16232
+ const tgtPts = /* @__PURE__ */ new Map();
16233
+ const PAD = 0.1;
16234
+ const activeEdges = edges.filter((e) => e.points.length > 0);
16235
+ const bySource = /* @__PURE__ */ new Map();
16236
+ for (const e of activeEdges) {
16237
+ if (!bySource.has(e.sourceId)) bySource.set(e.sourceId, []);
16238
+ bySource.get(e.sourceId).push(e);
16239
+ }
16240
+ for (const [sourceId, es] of bySource) {
16241
+ if (es.length < 2) continue;
16242
+ const source = nodeMap.get(sourceId);
16243
+ if (!source) continue;
16244
+ const sorted = es.map((e) => ({ e, t: nodeMap.get(e.targetId) })).filter((x) => x.t != null).sort((a, b) => direction === "LR" ? a.t.y - b.t.y : a.t.x - b.t.x);
16245
+ const n = sorted.length;
16246
+ for (let i = 0; i < n; i++) {
16247
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16248
+ const { e, t } = sorted[i];
16249
+ const isBackward = direction === "LR" ? t.x < source.x : t.y < source.y;
16250
+ if (direction === "LR") {
16251
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16252
+ x: isBackward ? source.x - source.width / 2 : source.x + source.width / 2,
16253
+ y: source.y - source.height / 2 + frac * source.height
16254
+ });
16255
+ } else {
16256
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16257
+ x: source.x - source.width / 2 + frac * source.width,
16258
+ y: isBackward ? source.y - source.height / 2 : source.y + source.height / 2
16259
+ });
16260
+ }
16261
+ }
16262
+ }
16263
+ const byTarget = /* @__PURE__ */ new Map();
16264
+ for (const e of activeEdges) {
16265
+ if (!byTarget.has(e.targetId)) byTarget.set(e.targetId, []);
16266
+ byTarget.get(e.targetId).push(e);
16267
+ }
16268
+ for (const [targetId, es] of byTarget) {
16269
+ if (es.length < 2) continue;
16270
+ const target = nodeMap.get(targetId);
16271
+ if (!target) continue;
16272
+ const sorted = es.map((e) => ({ e, s: nodeMap.get(e.sourceId) })).filter((x) => x.s != null).sort((a, b) => direction === "LR" ? a.s.y - b.s.y : a.s.x - b.s.x);
16273
+ const n = sorted.length;
16274
+ for (let i = 0; i < n; i++) {
16275
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16276
+ const { e, s } = sorted[i];
16277
+ const isBackward = direction === "LR" ? target.x < s.x : target.y < s.y;
16278
+ if (direction === "LR") {
16279
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16280
+ x: isBackward ? target.x + target.width / 2 : target.x - target.width / 2,
16281
+ y: target.y - target.height / 2 + frac * target.height
16282
+ });
16283
+ } else {
16284
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16285
+ x: target.x - target.width / 2 + frac * target.width,
16286
+ y: isBackward ? target.y + target.height / 2 : target.y - target.height / 2
16287
+ });
16288
+ }
16289
+ }
16290
+ }
16291
+ return { srcPts, tgtPts };
16292
+ }
16293
+ function findRoutingLane(blocking, targetY, margin) {
16294
+ const MERGE_SLOP = 4;
16295
+ const sorted = [...blocking].sort((a, b) => a.y + a.height / 2 - (b.y + b.height / 2));
16296
+ const merged = [];
16297
+ for (const r of sorted) {
16298
+ const lo = r.y - MERGE_SLOP;
16299
+ const hi = r.y + r.height + MERGE_SLOP;
16300
+ if (merged.length && lo <= merged[merged.length - 1][1]) {
16301
+ merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], hi);
16302
+ } else {
16303
+ merged.push([lo, hi]);
16304
+ }
16305
+ }
16306
+ if (merged.length === 0) return targetY;
16307
+ const MIN_GAP = 10;
16308
+ const candidates = [
16309
+ merged[0][0] - margin,
16310
+ // above all blocking rects
16311
+ merged[merged.length - 1][1] + margin
16312
+ // below all blocking rects
16313
+ ];
16314
+ for (let i = 0; i < merged.length - 1; i++) {
16315
+ const gapLo = merged[i][1];
16316
+ const gapHi = merged[i + 1][0];
16317
+ if (gapHi - gapLo >= MIN_GAP) {
16318
+ candidates.push((gapLo + gapHi) / 2);
16319
+ }
16320
+ }
16321
+ return candidates.reduce(
16322
+ (best, c) => Math.abs(c - targetY) < Math.abs(best - targetY) ? c : best,
16323
+ candidates[0]
16324
+ );
16325
+ }
16326
+ function segmentIntersectsRect(p1, p2, rect) {
16327
+ const { x: rx, y: ry, width: rw, height: rh } = rect;
16328
+ const rr = rx + rw;
16329
+ const rb = ry + rh;
16330
+ const inRect = (p) => p.x >= rx && p.x <= rr && p.y >= ry && p.y <= rb;
16331
+ if (inRect(p1) || inRect(p2)) return true;
16332
+ if (Math.max(p1.x, p2.x) < rx || Math.min(p1.x, p2.x) > rr) return false;
16333
+ if (Math.max(p1.y, p2.y) < ry || Math.min(p1.y, p2.y) > rb) return false;
16334
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
16335
+ const crosses = (a, b) => {
16336
+ const d1 = cross(a, b, p1);
16337
+ const d2 = cross(a, b, p2);
16338
+ const d3 = cross(p1, p2, a);
16339
+ const d4 = cross(p1, p2, b);
16340
+ return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
16341
+ };
16342
+ const tl = { x: rx, y: ry };
16343
+ const tr = { x: rr, y: ry };
16344
+ const br = { x: rr, y: rb };
16345
+ const bl = { x: rx, y: rb };
16346
+ return crosses(tl, tr) || crosses(tr, br) || crosses(br, bl) || crosses(bl, tl);
16347
+ }
16348
+ function curveIntersectsRect(sc, tc, rect, direction) {
16349
+ if (direction === "LR") {
16350
+ const midX = (sc.x + tc.x) / 2;
16351
+ const m1 = { x: midX, y: sc.y };
16352
+ const m2 = { x: midX, y: tc.y };
16353
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16354
+ } else {
16355
+ const midY = (sc.y + tc.y) / 2;
16356
+ const m1 = { x: sc.x, y: midY };
16357
+ const m2 = { x: tc.x, y: midY };
16358
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16359
+ }
16360
+ }
16361
+ function edgeWaypoints(source, target, groups, nodes, direction, margin = 30, srcExitPt, tgtEnterPt) {
16362
+ const sc = { x: source.x, y: source.y };
16363
+ const tc = { x: target.x, y: target.y };
16364
+ const isBackward = direction === "LR" ? tc.x < sc.x : tc.y < sc.y;
16365
+ if (isBackward) {
16366
+ if (direction === "LR") {
16367
+ const xBandObs = [];
16368
+ for (const g of groups) {
16369
+ if (g.x + g.width < tc.x - margin || g.x > sc.x + margin) continue;
16370
+ xBandObs.push({ x: g.x, y: g.y, width: g.width, height: g.height });
16371
+ }
16372
+ for (const n of nodes) {
16373
+ if (n.id === source.id || n.id === target.id) continue;
16374
+ const nLeft = n.x - n.width / 2;
16375
+ const nRight = n.x + n.width / 2;
16376
+ if (nRight < tc.x - margin || nLeft > sc.x + margin) continue;
16377
+ xBandObs.push({ x: nLeft, y: n.y - n.height / 2, width: n.width, height: n.height });
16378
+ }
16379
+ const midY = (sc.y + tc.y) / 2;
16380
+ const routeY2 = xBandObs.length > 0 ? findRoutingLane(xBandObs, midY, margin) : midY;
16381
+ const exitBorder = srcExitPt ?? nodeBorderPoint(source, { x: sc.x, y: routeY2 });
16382
+ const exitPt2 = { x: exitBorder.x, y: routeY2 };
16383
+ const enterPt2 = { x: tc.x, y: routeY2 };
16384
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, enterPt2);
16385
+ return srcExitPt ? [srcExitPt, exitPt2, enterPt2, tp2] : [exitBorder, exitPt2, enterPt2, tp2];
16386
+ } else {
16387
+ const yBandObs = [];
16388
+ for (const g of groups) {
16389
+ if (g.y + g.height < tc.y - margin || g.y > sc.y + margin) continue;
16390
+ yBandObs.push({ x: g.x, y: g.y, width: g.width, height: g.height });
16391
+ }
16392
+ for (const n of nodes) {
16393
+ if (n.id === source.id || n.id === target.id) continue;
16394
+ const nTop = n.y - n.height / 2;
16395
+ const nBot = n.y + n.height / 2;
16396
+ if (nBot < tc.y - margin || nTop > sc.y + margin) continue;
16397
+ yBandObs.push({ x: n.x - n.width / 2, y: nTop, width: n.width, height: n.height });
16398
+ }
16399
+ const rotated = yBandObs.map((r) => ({ x: r.y, y: r.x, width: r.height, height: r.width }));
16400
+ const midX = (sc.x + tc.x) / 2;
16401
+ const routeX = rotated.length > 0 ? findRoutingLane(rotated, midX, margin) : midX;
16402
+ const exitPt2 = srcExitPt ?? { x: routeX, y: sc.y };
16403
+ const enterPt2 = { x: routeX, y: tc.y };
16404
+ return [
16405
+ srcExitPt ?? nodeBorderPoint(source, exitPt2),
16406
+ exitPt2,
16407
+ enterPt2,
16408
+ tgtEnterPt ?? nodeBorderPoint(target, enterPt2)
16409
+ ];
16410
+ }
16411
+ }
16412
+ const blocking = [];
16413
+ const blockingGroupIds = /* @__PURE__ */ new Set();
16414
+ const pathSrc = srcExitPt ?? sc;
16415
+ const pathTgt = tgtEnterPt ?? tc;
16416
+ for (const g of groups) {
16417
+ if (g.id === source.groupId || g.id === target.groupId) continue;
16418
+ const gRect = { x: g.x, y: g.y, width: g.width, height: g.height };
16419
+ if (curveIntersectsRect(pathSrc, pathTgt, gRect, direction)) {
16420
+ blocking.push(gRect);
16421
+ blockingGroupIds.add(g.id);
16422
+ }
16423
+ }
16424
+ for (const n of nodes) {
16425
+ if (n.id === source.id || n.id === target.id) continue;
16426
+ if (n.groupId && (n.groupId === source.groupId || n.groupId === target.groupId)) continue;
16427
+ if (n.groupId && blockingGroupIds.has(n.groupId)) continue;
16428
+ const nodeRect = { x: n.x - n.width / 2, y: n.y - n.height / 2, width: n.width, height: n.height };
16429
+ if (curveIntersectsRect(pathSrc, pathTgt, nodeRect, direction)) {
16430
+ blocking.push(nodeRect);
16431
+ }
16432
+ }
16433
+ if (blocking.length === 0) {
16434
+ const sp = srcExitPt ?? nodeBorderPoint(source, tc);
16435
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, sp);
16436
+ return [sp, tp2];
16437
+ }
16438
+ const obsLeft = Math.min(...blocking.map((o) => o.x));
16439
+ const obsRight = Math.max(...blocking.map((o) => o.x + o.width));
16440
+ const routeY = findRoutingLane(blocking, tc.y, margin);
16441
+ const exitX = direction === "LR" ? Math.max(sc.x, obsLeft - margin) : obsLeft - margin;
16442
+ const enterX = direction === "LR" ? Math.min(tc.x, obsRight + margin) : obsRight + margin;
16443
+ const exitPt = { x: exitX, y: routeY };
16444
+ const enterPt = { x: enterX, y: routeY };
16445
+ const tp = tgtEnterPt ?? nodeBorderPoint(target, enterPt);
16446
+ if (srcExitPt) {
16447
+ return [srcExitPt, exitPt, enterPt, tp];
16448
+ }
16449
+ return [nodeBorderPoint(source, exitPt), exitPt, enterPt, tp];
16450
+ }
15899
16451
  function nodeBorderPoint(node, target) {
15900
16452
  const hw = node.width / 2;
15901
16453
  const hh = node.height / 2;
@@ -16179,33 +16731,29 @@ function renderGroups(svg, groups, palette, isDark) {
16179
16731
  }
16180
16732
  }
16181
16733
  }
16182
- function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16734
+ function renderEdgePaths(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16183
16735
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16184
16736
  const maxRps = Math.max(...edges.map((e) => e.computedRps), 1);
16737
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16185
16738
  for (const edge of edges) {
16186
16739
  if (edge.points.length === 0) continue;
16187
16740
  const targetNode = nodeMap.get(edge.targetId);
16188
16741
  const sourceNode = nodeMap.get(edge.sourceId);
16189
16742
  const color = edgeColor(edge, palette);
16190
16743
  const strokeW = edgeWidth();
16191
- let pts = edge.points;
16192
- if (sourceNode && targetNode && pts.length >= 2) {
16193
- const first = pts[0];
16194
- const distFirstToSource = (first.x - sourceNode.x) ** 2 + (first.y - sourceNode.y) ** 2;
16195
- const distFirstToTarget = (first.x - targetNode.x) ** 2 + (first.y - targetNode.y) ** 2;
16196
- if (distFirstToTarget < distFirstToSource) {
16197
- pts = [...pts].reverse();
16198
- }
16199
- }
16200
- if (sourceNode && pts.length > 0) {
16201
- const bp = nodeBorderPoint(sourceNode, pts[0]);
16202
- pts = [bp, ...pts];
16203
- }
16204
- if (targetNode && pts.length > 0) {
16205
- const bp = nodeBorderPoint(targetNode, pts[pts.length - 1]);
16206
- pts = [...pts, bp];
16207
- }
16208
- const pathD = lineGenerator7(pts) ?? "";
16744
+ if (!sourceNode || !targetNode) continue;
16745
+ const key = `${edge.sourceId}:${edge.targetId}`;
16746
+ const pts = edgeWaypoints(
16747
+ sourceNode,
16748
+ targetNode,
16749
+ groups,
16750
+ nodes,
16751
+ direction,
16752
+ 30,
16753
+ srcPts.get(key),
16754
+ tgtPts.get(key)
16755
+ );
16756
+ const pathD = buildPathD(pts, direction);
16209
16757
  const edgeG = svg.append("g").attr("class", "infra-edge").attr("data-line-number", edge.lineNumber);
16210
16758
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", color).attr("stroke-width", strokeW);
16211
16759
  if (animate && edge.computedRps > 0) {
@@ -16220,19 +16768,34 @@ function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16220
16768
  }
16221
16769
  }
16222
16770
  }
16223
- function renderEdgeLabels(svg, edges, palette, isDark, animate) {
16771
+ function renderEdgeLabels(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16772
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16773
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16224
16774
  for (const edge of edges) {
16225
16775
  if (edge.points.length === 0) continue;
16226
16776
  if (!edge.label) continue;
16227
- const midIdx = Math.floor(edge.points.length / 2);
16228
- const midPt = edge.points[midIdx];
16777
+ const sourceNode = nodeMap.get(edge.sourceId);
16778
+ const targetNode = nodeMap.get(edge.targetId);
16779
+ if (!sourceNode || !targetNode) continue;
16780
+ const key = `${edge.sourceId}:${edge.targetId}`;
16781
+ const wps = edgeWaypoints(
16782
+ sourceNode,
16783
+ targetNode,
16784
+ groups,
16785
+ nodes,
16786
+ direction,
16787
+ 30,
16788
+ srcPts.get(key),
16789
+ tgtPts.get(key)
16790
+ );
16791
+ const midPt = wps[Math.floor(wps.length / 2)];
16229
16792
  const labelText = edge.label;
16230
16793
  const g = svg.append("g").attr("class", animate ? "infra-edge-label" : "");
16231
16794
  const textWidth = labelText.length * 6.5 + 8;
16232
16795
  g.append("rect").attr("x", midPt.x - textWidth / 2).attr("y", midPt.y - 8).attr("width", textWidth).attr("height", 16).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.9);
16233
16796
  g.append("text").attr("x", midPt.x).attr("y", midPt.y + 4).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", EDGE_LABEL_FONT_SIZE7).attr("fill", palette.textMuted).text(labelText);
16234
16797
  if (animate) {
16235
- const pathD = lineGenerator7(edge.points) ?? "";
16798
+ const pathD = buildPathD(wps, direction);
16236
16799
  g.insert("path", ":first-child").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", 20);
16237
16800
  }
16238
16801
  }
@@ -16504,16 +17067,16 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
16504
17067
  color: r.color,
16505
17068
  key: r.name.toLowerCase().replace(/\s+/g, "-")
16506
17069
  }));
16507
- const pillWidth = "Capabilities".length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
17070
+ const pillWidth = "Capabilities".length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
16508
17071
  let entriesWidth = 0;
16509
17072
  for (const e of entries) {
16510
- entriesWidth += LEGEND_DOT_R8 * 2 + LEGEND_ENTRY_DOT_GAP7 + e.value.length * LEGEND_ENTRY_FONT_W7 + LEGEND_ENTRY_TRAIL7;
17073
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
16511
17074
  }
16512
17075
  groups.push({
16513
17076
  name: "Capabilities",
16514
17077
  type: "role",
16515
17078
  entries,
16516
- width: LEGEND_CAPSULE_PAD7 * 2 + pillWidth + 4 + entriesWidth,
17079
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth,
16517
17080
  minifiedWidth: pillWidth
16518
17081
  });
16519
17082
  }
@@ -16529,123 +17092,72 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
16529
17092
  }
16530
17093
  }
16531
17094
  if (entries.length === 0) continue;
16532
- const pillWidth = tg.name.length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
17095
+ const pillWidth = tg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
16533
17096
  let entriesWidth = 0;
16534
17097
  for (const e of entries) {
16535
- entriesWidth += LEGEND_DOT_R8 * 2 + LEGEND_ENTRY_DOT_GAP7 + e.value.length * LEGEND_ENTRY_FONT_W7 + LEGEND_ENTRY_TRAIL7;
17098
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
16536
17099
  }
16537
17100
  groups.push({
16538
17101
  name: tg.name,
16539
17102
  type: "tag",
16540
17103
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
16541
17104
  entries,
16542
- width: LEGEND_CAPSULE_PAD7 * 2 + pillWidth + 4 + entriesWidth,
17105
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth,
16543
17106
  minifiedWidth: pillWidth
16544
17107
  });
16545
17108
  }
16546
17109
  return groups;
16547
17110
  }
16548
- function computePlaybackWidth(playback) {
16549
- if (!playback) return 0;
16550
- const pillWidth = "Playback".length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
16551
- if (!playback.expanded) return pillWidth;
16552
- let entriesW = 8;
16553
- entriesW += LEGEND_PILL_FONT_SIZE5 * 0.8 + 6;
16554
- for (const s of playback.speedOptions) {
16555
- entriesW += `${s}x`.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
16556
- }
16557
- return LEGEND_CAPSULE_PAD7 * 2 + pillWidth + entriesW;
16558
- }
16559
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
16560
- if (legendGroups.length === 0 && !playback) return;
17111
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup) {
17112
+ if (legendGroups.length === 0) return;
16561
17113
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
17114
+ if (activeGroup) {
17115
+ legendG.attr("data-legend-active", activeGroup.toLowerCase());
17116
+ }
16562
17117
  const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
16563
- const playbackW = computePlaybackWidth(playback);
16564
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP5 : 0;
16565
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP5 + trailingGaps + playbackW;
17118
+ const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP;
16566
17119
  let cursorX = (totalWidth - totalLegendW) / 2;
16567
17120
  for (const group of legendGroups) {
16568
17121
  const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
16569
17122
  const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
16570
17123
  const pillLabel = group.name;
16571
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
16572
- const gEl = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group").attr("data-legend-group", group.name.toLowerCase()).attr("data-legend-type", group.type).style("cursor", "pointer");
17124
+ const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
17125
+ const gEl = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
16573
17126
  if (isActive) {
16574
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT8).attr("rx", LEGEND_HEIGHT8 / 2).attr("fill", groupBg);
17127
+ gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
16575
17128
  }
16576
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD7 : 0;
16577
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD7 : 0;
16578
- const pillH = LEGEND_HEIGHT8 - (isActive ? LEGEND_CAPSULE_PAD7 * 2 : 0);
17129
+ const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
17130
+ const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
17131
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
16579
17132
  gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
16580
17133
  if (isActive) {
16581
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", isDark ? mix(palette.textMuted, palette.bg, 50) : mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
17134
+ gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
16582
17135
  }
16583
- gEl.append("text").attr("x", pillXOff + pillWidth / 2).attr("y", LEGEND_HEIGHT8 / 2 + LEGEND_PILL_FONT_SIZE5 / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE5).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
17136
+ gEl.append("text").attr("x", pillXOff + pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
16584
17137
  if (isActive) {
16585
17138
  let entryX = pillXOff + pillWidth + 4;
16586
17139
  for (const entry of group.entries) {
16587
- const entryG = gEl.append("g").attr("class", "infra-legend-entry").attr("data-legend-entry", entry.key).attr("data-legend-type", group.type).attr("data-legend-color", entry.color).style("cursor", "pointer");
16588
- if (group.type === "tag" && group.tagKey) {
16589
- entryG.attr("data-legend-tag-group", group.tagKey);
16590
- }
16591
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R8).attr("cy", LEGEND_HEIGHT8 / 2).attr("r", LEGEND_DOT_R8).attr("fill", entry.color);
16592
- const textX = entryX + LEGEND_DOT_R8 * 2 + LEGEND_ENTRY_DOT_GAP7;
16593
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT8 / 2 + LEGEND_ENTRY_FONT_SIZE6 / 2 - 1).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE6).attr("fill", palette.textMuted).text(entry.value);
16594
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W7 + LEGEND_ENTRY_TRAIL7;
17140
+ const entryG = gEl.append("g").attr("class", "infra-legend-entry").attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr("data-legend-tag-group", group.type === "tag" ? group.tagKey ?? "" : null).style("cursor", "pointer");
17141
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17142
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17143
+ entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
17144
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
16595
17145
  }
16596
17146
  }
16597
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP5;
17147
+ cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
16598
17148
  }
16599
- if (playback) {
16600
- const isExpanded = playback.expanded;
16601
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
16602
- const pillLabel = "Playback";
16603
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
16604
- const fullW = computePlaybackWidth(playback);
16605
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
16606
- if (isExpanded) {
16607
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT8).attr("rx", LEGEND_HEIGHT8 / 2).attr("fill", groupBg);
16608
- }
16609
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD7 : 0;
16610
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD7 : 0;
16611
- const pillH = LEGEND_HEIGHT8 - (isExpanded ? LEGEND_CAPSULE_PAD7 * 2 : 0);
16612
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isExpanded ? palette.bg : groupBg);
16613
- if (isExpanded) {
16614
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
16615
- }
16616
- pbG.append("text").attr("x", pillXOff + pillWidth / 2).attr("y", LEGEND_HEIGHT8 / 2 + LEGEND_PILL_FONT_SIZE5 / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE5).attr("font-weight", "500").attr("fill", isExpanded ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
16617
- if (isExpanded) {
16618
- let entryX = pillXOff + pillWidth + 8;
16619
- const entryY = LEGEND_HEIGHT8 / 2 + LEGEND_ENTRY_FONT_SIZE6 / 2 - 1;
16620
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
16621
- pbG.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE5).attr("fill", palette.textMuted).attr("data-playback-action", "toggle-pause").style("cursor", "pointer").text(ppLabel);
16622
- entryX += LEGEND_PILL_FONT_SIZE5 * 0.8 + 6;
16623
- for (const s of playback.speedOptions) {
16624
- const label = `${s}x`;
16625
- const isActive = playback.speed === s;
16626
- const slotW = label.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2;
16627
- const badgeH = LEGEND_ENTRY_FONT_SIZE6 + SPEED_BADGE_V_PAD * 2;
16628
- const badgeY = (LEGEND_HEIGHT8 - badgeH) / 2;
16629
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
16630
- speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isActive ? palette.primary : "transparent");
16631
- speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE6).attr("font-weight", isActive ? "600" : "400").attr("fill", isActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
16632
- entryX += slotW + SPEED_BADGE_GAP;
16633
- }
16634
- }
16635
- cursorX += fullW + LEGEND_GROUP_GAP5;
16636
- }
16637
- }
16638
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
17149
+ }
17150
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, _playback, expandedNodeIds, exportMode, collapsedNodes) {
16639
17151
  d3Selection9.select(container).selectAll(":not([data-d3-tooltip])").remove();
16640
17152
  const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette, layout.edges);
16641
- const hasLegend = legendGroups.length > 0 || !!playback;
17153
+ const hasLegend = legendGroups.length > 0;
16642
17154
  const fixedLegend = !exportMode && hasLegend;
16643
- const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT8 : 0;
17155
+ const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT : 0;
16644
17156
  const titleOffset = title ? 40 : 0;
16645
17157
  const totalWidth = layout.width;
16646
17158
  const totalHeight = layout.height + titleOffset + legendOffset;
16647
17159
  const shouldAnimate = animate !== false;
16648
- const rootSvg = d3Selection9.select(container).append("svg").attr("xmlns", "http://www.w3.org/2000/svg").attr("width", "100%").attr("height", fixedLegend ? `calc(100% - ${LEGEND_HEIGHT8 + LEGEND_FIXED_GAP3}px)` : "100%").attr("viewBox", `0 0 ${totalWidth} ${totalHeight}`).attr("preserveAspectRatio", "xMidYMid meet");
17160
+ const rootSvg = d3Selection9.select(container).append("svg").attr("xmlns", "http://www.w3.org/2000/svg").attr("width", "100%").attr("height", fixedLegend ? `calc(100% - ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}px)` : "100%").attr("viewBox", `0 0 ${totalWidth} ${totalHeight}`).attr("preserveAspectRatio", "xMidYMid meet");
16649
17161
  if (shouldAnimate) {
16650
17162
  rootSvg.append("style").text(`
16651
17163
  @keyframes infra-pulse-warning {
@@ -16693,7 +17205,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16693
17205
  rootSvg.append("text").attr("class", "chart-title").attr("x", totalWidth / 2).attr("y", 28).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", 18).attr("font-weight", "700").attr("fill", palette.text).attr("data-line-number", titleLineNumber != null ? titleLineNumber : "").text(title);
16694
17206
  }
16695
17207
  renderGroups(svg, layout.groups, palette, isDark);
16696
- renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
17208
+ renderEdgePaths(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16697
17209
  const fanoutSourceIds = collectFanoutSourceIds(layout.edges);
16698
17210
  const scaledGroupIds = new Set(
16699
17211
  layout.groups.filter((g) => {
@@ -16705,14 +17217,14 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16705
17217
  if (shouldAnimate) {
16706
17218
  renderRejectParticles(svg, layout.nodes);
16707
17219
  }
16708
- renderEdgeLabels(svg, layout.edges, palette, isDark, shouldAnimate);
17220
+ renderEdgeLabels(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16709
17221
  if (hasLegend) {
16710
17222
  if (fixedLegend) {
16711
17223
  const containerWidth = container.clientWidth || totalWidth;
16712
- const legendSvg = d3Selection9.select(container).append("svg").attr("class", "infra-legend-fixed").attr("width", "100%").attr("height", LEGEND_HEIGHT8 + LEGEND_FIXED_GAP3).attr("viewBox", `0 0 ${containerWidth} ${LEGEND_HEIGHT8 + LEGEND_FIXED_GAP3}`).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block");
16713
- renderLegend3(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP3 / 2, palette, isDark, activeGroup ?? null, playback ?? void 0);
17224
+ const legendSvg = d3Selection9.select(container).append("svg").attr("class", "infra-legend-fixed").attr("width", "100%").attr("height", LEGEND_HEIGHT + LEGEND_FIXED_GAP3).attr("viewBox", `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}`).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block");
17225
+ renderLegend3(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP3 / 2, palette, isDark, activeGroup ?? null);
16714
17226
  } else {
16715
- renderLegend3(rootSvg, legendGroups, totalWidth, titleOffset + layout.height + 4, palette, isDark, activeGroup ?? null, playback ?? void 0);
17227
+ renderLegend3(rootSvg, legendGroups, totalWidth, titleOffset + layout.height + 4, palette, isDark, activeGroup ?? null);
16716
17228
  }
16717
17229
  }
16718
17230
  }
@@ -16723,7 +17235,7 @@ function parseAndLayoutInfra(content) {
16723
17235
  const layout = layoutInfra(computed);
16724
17236
  return { parsed, computed, layout };
16725
17237
  }
16726
- var d3Selection9, d3Shape7, NODE_FONT_SIZE4, META_FONT_SIZE4, META_LINE_HEIGHT8, EDGE_LABEL_FONT_SIZE7, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, EDGE_STROKE_WIDTH8, NODE_STROKE_WIDTH8, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, COLLAPSE_BAR_HEIGHT5, COLLAPSE_BAR_INSET2, LEGEND_HEIGHT8, LEGEND_PILL_PAD7, LEGEND_PILL_FONT_SIZE5, LEGEND_PILL_FONT_W7, LEGEND_CAPSULE_PAD7, LEGEND_DOT_R8, LEGEND_ENTRY_FONT_SIZE6, LEGEND_ENTRY_FONT_W7, LEGEND_ENTRY_DOT_GAP7, LEGEND_ENTRY_TRAIL7, LEGEND_GROUP_GAP5, LEGEND_FIXED_GAP3, SPEED_BADGE_H_PAD, SPEED_BADGE_V_PAD, SPEED_BADGE_GAP, COLOR_HEALTHY, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_PARTICLE_R, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, lineGenerator7, PROP_DISPLAY, DESC_MAX_CHARS, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
17238
+ var d3Selection9, d3Shape7, NODE_FONT_SIZE4, META_FONT_SIZE4, META_LINE_HEIGHT8, EDGE_LABEL_FONT_SIZE7, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, EDGE_STROKE_WIDTH8, NODE_STROKE_WIDTH8, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, COLLAPSE_BAR_HEIGHT5, COLLAPSE_BAR_INSET2, LEGEND_FIXED_GAP3, COLOR_HEALTHY, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_PARTICLE_R, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, PROP_DISPLAY, DESC_MAX_CHARS, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
16727
17239
  var init_renderer8 = __esm({
16728
17240
  "src/infra/renderer.ts"() {
16729
17241
  "use strict";
@@ -16736,6 +17248,7 @@ var init_renderer8 = __esm({
16736
17248
  init_parser9();
16737
17249
  init_compute();
16738
17250
  init_layout8();
17251
+ init_legend_constants();
16739
17252
  NODE_FONT_SIZE4 = 13;
16740
17253
  META_FONT_SIZE4 = 10;
16741
17254
  META_LINE_HEIGHT8 = 14;
@@ -16751,21 +17264,7 @@ var init_renderer8 = __esm({
16751
17264
  NODE_PAD_BOTTOM2 = 10;
16752
17265
  COLLAPSE_BAR_HEIGHT5 = 6;
16753
17266
  COLLAPSE_BAR_INSET2 = 0;
16754
- LEGEND_HEIGHT8 = 28;
16755
- LEGEND_PILL_PAD7 = 16;
16756
- LEGEND_PILL_FONT_SIZE5 = 11;
16757
- LEGEND_PILL_FONT_W7 = LEGEND_PILL_FONT_SIZE5 * 0.6;
16758
- LEGEND_CAPSULE_PAD7 = 4;
16759
- LEGEND_DOT_R8 = 4;
16760
- LEGEND_ENTRY_FONT_SIZE6 = 10;
16761
- LEGEND_ENTRY_FONT_W7 = LEGEND_ENTRY_FONT_SIZE6 * 0.6;
16762
- LEGEND_ENTRY_DOT_GAP7 = 4;
16763
- LEGEND_ENTRY_TRAIL7 = 8;
16764
- LEGEND_GROUP_GAP5 = 12;
16765
17267
  LEGEND_FIXED_GAP3 = 16;
16766
- SPEED_BADGE_H_PAD = 5;
16767
- SPEED_BADGE_V_PAD = 3;
16768
- SPEED_BADGE_GAP = 6;
16769
17268
  COLOR_HEALTHY = "#22c55e";
16770
17269
  COLOR_WARNING = "#eab308";
16771
17270
  COLOR_OVERLOADED = "#ef4444";
@@ -16782,7 +17281,6 @@ var init_renderer8 = __esm({
16782
17281
  REJECT_DURATION_MAX = 3;
16783
17282
  REJECT_COUNT_MIN = 1;
16784
17283
  REJECT_COUNT_MAX = 3;
16785
- lineGenerator7 = d3Shape7.line().x((d) => d.x).y((d) => d.y).curve(d3Shape7.curveBasis);
16786
17284
  PROP_DISPLAY = {
16787
17285
  "cache-hit": "cache hit",
16788
17286
  "firewall-block": "firewall block",
@@ -16965,7 +17463,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
16965
17463
  }
16966
17464
  }
16967
17465
  } else if (edge.points.length >= 2) {
16968
- const pathD = lineGenerator8(edge.points);
17466
+ const pathD = lineGenerator7(edge.points);
16969
17467
  if (pathD) {
16970
17468
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor2).attr("stroke-width", EDGE_STROKE_WIDTH9).attr("marker-end", `url(#${markerId})`).attr("class", "st-edge");
16971
17469
  }
@@ -17029,7 +17527,7 @@ function renderStateForExport(content, theme, palette) {
17029
17527
  document.body.removeChild(container);
17030
17528
  }
17031
17529
  }
17032
- var d3Selection10, d3Shape8, DIAGRAM_PADDING9, MAX_SCALE8, NODE_FONT_SIZE5, EDGE_LABEL_FONT_SIZE8, GROUP_LABEL_FONT_SIZE3, EDGE_STROKE_WIDTH9, NODE_STROKE_WIDTH9, ARROWHEAD_W4, ARROWHEAD_H4, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING2, lineGenerator8;
17530
+ var d3Selection10, d3Shape8, DIAGRAM_PADDING9, MAX_SCALE8, NODE_FONT_SIZE5, EDGE_LABEL_FONT_SIZE8, GROUP_LABEL_FONT_SIZE3, EDGE_STROKE_WIDTH9, NODE_STROKE_WIDTH9, ARROWHEAD_W4, ARROWHEAD_H4, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING2, lineGenerator7;
17033
17531
  var init_state_renderer = __esm({
17034
17532
  "src/graph/state-renderer.ts"() {
17035
17533
  "use strict";
@@ -17051,7 +17549,7 @@ var init_state_renderer = __esm({
17051
17549
  PSEUDOSTATE_RADIUS = 10;
17052
17550
  STATE_CORNER_RADIUS = 10;
17053
17551
  GROUP_EXTRA_PADDING2 = 12;
17054
- lineGenerator8 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
17552
+ lineGenerator7 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
17055
17553
  }
17056
17554
  });
17057
17555
 
@@ -17785,9 +18283,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17785
18283
  const GROUP_PADDING_BOTTOM = 8;
17786
18284
  const GROUP_LABEL_SIZE = 11;
17787
18285
  const titleOffset = title ? TITLE_HEIGHT5 : 0;
17788
- const legendOffset = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT9 + LEGEND_BOTTOM_GAP : 0;
17789
18286
  const groupOffset = groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
17790
- const participantStartY = TOP_MARGIN + titleOffset + legendOffset + PARTICIPANT_Y_OFFSET + groupOffset;
18287
+ const participantStartY = TOP_MARGIN + titleOffset + PARTICIPANT_Y_OFFSET + groupOffset;
17791
18288
  const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
17792
18289
  const hasActors = participants.some((p) => p.type === "actor");
17793
18290
  const messageStartOffset = MESSAGE_START_OFFSET + (hasActors ? 20 : 0);
@@ -17869,7 +18366,9 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17869
18366
  participants.length * PARTICIPANT_GAP,
17870
18367
  PARTICIPANT_BOX_WIDTH + 40
17871
18368
  );
17872
- const totalHeight = participantStartY + PARTICIPANT_BOX_HEIGHT + Math.max(lifelineLength, 40) + 40;
18369
+ const contentHeight = participantStartY + PARTICIPANT_BOX_HEIGHT + Math.max(lifelineLength, 40) + 40;
18370
+ const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
18371
+ const totalHeight = contentHeight + legendSpace;
17873
18372
  const containerWidth = options?.exportWidth ?? container.getBoundingClientRect().width;
17874
18373
  const svgWidth = Math.max(totalWidth, containerWidth);
17875
18374
  const diagramWidth = participants.length * PARTICIPANT_GAP;
@@ -17927,13 +18426,13 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17927
18426
  }
17928
18427
  }
17929
18428
  if (parsed.tagGroups.length > 0) {
17930
- const legendY = TOP_MARGIN + titleOffset;
18429
+ const legendY = contentHeight;
17931
18430
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17932
18431
  const legendItems = [];
17933
18432
  for (const tg of parsed.tagGroups) {
17934
18433
  if (tg.entries.length === 0) continue;
17935
18434
  const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
17936
- const pillWidth = tg.name.length * LEGEND_PILL_FONT_W8 + LEGEND_PILL_PAD8;
18435
+ const pillWidth = tg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
17937
18436
  const entries = tg.entries.map((e) => ({
17938
18437
  value: e.value,
17939
18438
  color: resolveColor(e.color)
@@ -17942,38 +18441,42 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17942
18441
  if (isActive) {
17943
18442
  let entriesWidth = 0;
17944
18443
  for (const entry of entries) {
17945
- entriesWidth += LEGEND_DOT_R9 * 2 + LEGEND_ENTRY_DOT_GAP8 + entry.value.length * LEGEND_ENTRY_FONT_W8 + LEGEND_ENTRY_TRAIL8;
18444
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
17946
18445
  }
17947
- totalWidth2 = LEGEND_CAPSULE_PAD8 * 2 + pillWidth + 4 + entriesWidth;
18446
+ totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
17948
18447
  }
17949
18448
  legendItems.push({ group: tg, isActive, pillWidth, totalWidth: totalWidth2, entries });
17950
18449
  }
17951
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP6;
18450
+ const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
17952
18451
  let legendX = (svgWidth - totalLegendWidth) / 2;
18452
+ const legendContainer = svg.append("g").attr("class", "sequence-legend");
18453
+ if (activeTagGroup) {
18454
+ legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
18455
+ }
17953
18456
  for (const item of legendItems) {
17954
- const gEl = svg.append("g").attr("transform", `translate(${legendX}, ${legendY})`).attr("class", "sequence-legend-group").attr("data-legend-group", item.group.name.toLowerCase()).style("cursor", "pointer");
18457
+ const gEl = legendContainer.append("g").attr("transform", `translate(${legendX}, ${legendY})`).attr("class", "sequence-legend-group").attr("data-legend-group", item.group.name.toLowerCase()).style("cursor", "pointer");
17955
18458
  if (item.isActive) {
17956
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT9).attr("rx", LEGEND_HEIGHT9 / 2).attr("fill", groupBg);
18459
+ gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17957
18460
  }
17958
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD8 : 0;
17959
- const pillYOff = item.isActive ? LEGEND_CAPSULE_PAD8 : 0;
17960
- const pillH = LEGEND_HEIGHT9 - (item.isActive ? LEGEND_CAPSULE_PAD8 * 2 : 0);
18461
+ const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
18462
+ const pillYOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
18463
+ const pillH = LEGEND_HEIGHT - (item.isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
17961
18464
  gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", item.isActive ? palette.bg : groupBg);
17962
18465
  if (item.isActive) {
17963
18466
  gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
17964
18467
  }
17965
- gEl.append("text").attr("x", pillXOff + item.pillWidth / 2).attr("y", LEGEND_HEIGHT9 / 2 + LEGEND_PILL_FONT_SIZE6 / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE6).attr("font-weight", "500").attr("fill", item.isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(item.group.name);
18468
+ gEl.append("text").attr("x", pillXOff + item.pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", item.isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(item.group.name);
17966
18469
  if (item.isActive) {
17967
18470
  let entryX = pillXOff + item.pillWidth + 4;
17968
18471
  for (const entry of item.entries) {
17969
18472
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17970
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R9).attr("cy", LEGEND_HEIGHT9 / 2).attr("r", LEGEND_DOT_R9).attr("fill", entry.color);
17971
- const textX = entryX + LEGEND_DOT_R9 * 2 + LEGEND_ENTRY_DOT_GAP8;
17972
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT9 / 2 + LEGEND_ENTRY_FONT_SIZE7 / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE7).attr("fill", palette.textMuted).text(entry.value);
17973
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W8 + LEGEND_ENTRY_TRAIL8;
18473
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
18474
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
18475
+ entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
18476
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
17974
18477
  }
17975
18478
  }
17976
- legendX += item.totalWidth + LEGEND_GROUP_GAP6;
18479
+ legendX += item.totalWidth + LEGEND_GROUP_GAP;
17977
18480
  }
17978
18481
  }
17979
18482
  for (const group of groups) {
@@ -18494,7 +18997,7 @@ function renderParticipant(svg, participant, cx, cy, palette, isDark, color, tag
18494
18997
  });
18495
18998
  }
18496
18999
  }
18497
- var d3Selection11, PARTICIPANT_GAP, PARTICIPANT_BOX_WIDTH, PARTICIPANT_BOX_HEIGHT, TOP_MARGIN, TITLE_HEIGHT5, PARTICIPANT_Y_OFFSET, SERVICE_BORDER_RADIUS, MESSAGE_START_OFFSET, LIFELINE_TAIL, ARROWHEAD_SIZE, NOTE_MAX_W, NOTE_FOLD, NOTE_PAD_H, NOTE_PAD_V, NOTE_FONT_SIZE, NOTE_LINE_H, NOTE_GAP, NOTE_CHAR_W, NOTE_CHARS_PER_LINE, COLLAPSED_NOTE_H, COLLAPSED_NOTE_W, LEGEND_HEIGHT9, LEGEND_PILL_PAD8, LEGEND_PILL_FONT_SIZE6, LEGEND_PILL_FONT_W8, LEGEND_CAPSULE_PAD8, LEGEND_DOT_R9, LEGEND_ENTRY_FONT_SIZE7, LEGEND_ENTRY_FONT_W8, LEGEND_ENTRY_DOT_GAP8, LEGEND_ENTRY_TRAIL8, LEGEND_GROUP_GAP6, LEGEND_BOTTOM_GAP, LABEL_CHAR_WIDTH, LABEL_MAX_CHARS, fill, stroke, SW, W, H;
19000
+ var d3Selection11, PARTICIPANT_GAP, PARTICIPANT_BOX_WIDTH, PARTICIPANT_BOX_HEIGHT, TOP_MARGIN, TITLE_HEIGHT5, PARTICIPANT_Y_OFFSET, SERVICE_BORDER_RADIUS, MESSAGE_START_OFFSET, LIFELINE_TAIL, ARROWHEAD_SIZE, NOTE_MAX_W, NOTE_FOLD, NOTE_PAD_H, NOTE_PAD_V, NOTE_FONT_SIZE, NOTE_LINE_H, NOTE_GAP, NOTE_CHAR_W, NOTE_CHARS_PER_LINE, COLLAPSED_NOTE_H, COLLAPSED_NOTE_W, LABEL_CHAR_WIDTH, LABEL_MAX_CHARS, fill, stroke, SW, W, H;
18498
19001
  var init_renderer9 = __esm({
18499
19002
  "src/sequence/renderer.ts"() {
18500
19003
  "use strict";
@@ -18505,6 +19008,7 @@ var init_renderer9 = __esm({
18505
19008
  init_colors();
18506
19009
  init_parser();
18507
19010
  init_tag_resolution();
19011
+ init_legend_constants();
18508
19012
  PARTICIPANT_GAP = 160;
18509
19013
  PARTICIPANT_BOX_WIDTH = 120;
18510
19014
  PARTICIPANT_BOX_HEIGHT = 50;
@@ -18526,18 +19030,6 @@ var init_renderer9 = __esm({
18526
19030
  NOTE_CHARS_PER_LINE = Math.floor((NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
18527
19031
  COLLAPSED_NOTE_H = 20;
18528
19032
  COLLAPSED_NOTE_W = 40;
18529
- LEGEND_HEIGHT9 = 28;
18530
- LEGEND_PILL_PAD8 = 16;
18531
- LEGEND_PILL_FONT_SIZE6 = 11;
18532
- LEGEND_PILL_FONT_W8 = LEGEND_PILL_FONT_SIZE6 * 0.6;
18533
- LEGEND_CAPSULE_PAD8 = 4;
18534
- LEGEND_DOT_R9 = 4;
18535
- LEGEND_ENTRY_FONT_SIZE7 = 10;
18536
- LEGEND_ENTRY_FONT_W8 = LEGEND_ENTRY_FONT_SIZE7 * 0.6;
18537
- LEGEND_ENTRY_DOT_GAP8 = 4;
18538
- LEGEND_ENTRY_TRAIL8 = 8;
18539
- LEGEND_GROUP_GAP6 = 12;
18540
- LEGEND_BOTTOM_GAP = 8;
18541
19033
  LABEL_CHAR_WIDTH = 7.5;
18542
19034
  LABEL_MAX_CHARS = Math.floor((PARTICIPANT_BOX_WIDTH - 10) / LABEL_CHAR_WIDTH);
18543
19035
  fill = (palette, isDark, color) => color ? mix(color, isDark ? palette.surface : palette.bg, isDark ? 30 : 40) : isDark ? mix(palette.overlay, palette.surface, 50) : mix(palette.bg, palette.surface, 50);
@@ -20145,9 +20637,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20145
20637
  const scaleMargin = timelineScale ? 40 : 0;
20146
20638
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
20147
20639
  const margin = {
20148
- top: 104 + markerMargin + tagLegendReserve,
20640
+ top: 104 + markerMargin,
20149
20641
  right: 40 + scaleMargin,
20150
- bottom: 40,
20642
+ bottom: 40 + tagLegendReserve,
20151
20643
  left: 60 + scaleMargin
20152
20644
  };
20153
20645
  const innerWidth = width - margin.left - margin.right;
@@ -20254,9 +20746,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20254
20746
  const scaleMargin = timelineScale ? 40 : 0;
20255
20747
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
20256
20748
  const margin = {
20257
- top: 104 + markerMargin + tagLegendReserve,
20749
+ top: 104 + markerMargin,
20258
20750
  right: 200,
20259
- bottom: 40,
20751
+ bottom: 40 + tagLegendReserve,
20260
20752
  left: 60 + scaleMargin
20261
20753
  };
20262
20754
  const innerWidth = width - margin.left - margin.right;
@@ -20393,9 +20885,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20393
20885
  const dynamicLeftMargin = Math.max(120, maxGroupNameLen * 7 + 30);
20394
20886
  const baseTopMargin = title ? 50 : 20;
20395
20887
  const margin = {
20396
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
20888
+ top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin,
20397
20889
  right: 40,
20398
- bottom: 40 + scaleMargin,
20890
+ bottom: 40 + scaleMargin + tagLegendReserve,
20399
20891
  left: dynamicLeftMargin
20400
20892
  };
20401
20893
  const innerWidth = width - margin.left - margin.right;
@@ -20539,9 +21031,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20539
21031
  const scaleMargin = timelineScale ? 24 : 0;
20540
21032
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
20541
21033
  const margin = {
20542
- top: 104 + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
21034
+ top: 104 + (timelineScale ? 40 : 0) + markerMargin,
20543
21035
  right: 40,
20544
- bottom: 40 + scaleMargin,
21036
+ bottom: 40 + scaleMargin + tagLegendReserve,
20545
21037
  left: 60
20546
21038
  };
20547
21039
  const innerWidth = width - margin.left - margin.right;
@@ -20672,17 +21164,17 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20672
21164
  });
20673
21165
  }
20674
21166
  if (parsed.timelineTagGroups.length > 0) {
20675
- const LG_HEIGHT = 28;
20676
- const LG_PILL_PAD = 16;
20677
- const LG_PILL_FONT_SIZE = 11;
20678
- const LG_PILL_FONT_W = LG_PILL_FONT_SIZE * 0.6;
20679
- const LG_CAPSULE_PAD = 4;
20680
- const LG_DOT_R = 4;
20681
- const LG_ENTRY_FONT_SIZE = 10;
20682
- const LG_ENTRY_FONT_W = LG_ENTRY_FONT_SIZE * 0.6;
20683
- const LG_ENTRY_DOT_GAP = 4;
20684
- const LG_ENTRY_TRAIL = 8;
20685
- const LG_GROUP_GAP = 12;
21167
+ const LG_HEIGHT = LEGEND_HEIGHT;
21168
+ const LG_PILL_PAD = LEGEND_PILL_PAD;
21169
+ const LG_PILL_FONT_SIZE = LEGEND_PILL_FONT_SIZE;
21170
+ const LG_PILL_FONT_W = LEGEND_PILL_FONT_W;
21171
+ const LG_CAPSULE_PAD = LEGEND_CAPSULE_PAD;
21172
+ const LG_DOT_R = LEGEND_DOT_R;
21173
+ const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
21174
+ const LG_ENTRY_FONT_W = LEGEND_ENTRY_FONT_W;
21175
+ const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
21176
+ const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
21177
+ const LG_GROUP_GAP = LEGEND_GROUP_GAP;
20686
21178
  const LG_ICON_W = 20;
20687
21179
  const mainSvg = d3Selection12.select(container).select("svg");
20688
21180
  const mainG = mainSvg.select("g");
@@ -20715,6 +21207,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20715
21207
  );
20716
21208
  }, drawLegend2 = function() {
20717
21209
  mainSvg.selectAll(".tl-tag-legend-group").remove();
21210
+ mainSvg.selectAll(".tl-tag-legend-container").remove();
20718
21211
  const effectiveColorKey = (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
20719
21212
  const visibleGroups = viewMode ? legendGroups.filter(
20720
21213
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
@@ -20725,13 +21218,17 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20725
21218
  return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
20726
21219
  }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
20727
21220
  let cx = (width - totalW) / 2;
21221
+ const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
21222
+ if (currentActiveGroup) {
21223
+ legendContainer.attr("data-legend-active", currentActiveGroup.toLowerCase());
21224
+ }
20728
21225
  for (const lg of visibleGroups) {
20729
21226
  const groupKey = lg.group.name.toLowerCase();
20730
21227
  const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
20731
21228
  const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
20732
21229
  const pillLabel = lg.group.name;
20733
21230
  const pillWidth = pillLabel.length * LG_PILL_FONT_W + LG_PILL_PAD;
20734
- const gEl = mainSvg.append("g").attr("transform", `translate(${cx}, ${legendY})`).attr("class", "tl-tag-legend-group tl-tag-legend-entry").attr("data-legend-group", groupKey).attr("data-tag-group", groupKey).attr("data-legend-entry", "__group__");
21231
+ const gEl = legendContainer.append("g").attr("transform", `translate(${cx}, ${legendY})`).attr("class", "tl-tag-legend-group tl-tag-legend-entry").attr("data-legend-group", groupKey).attr("data-tag-group", groupKey).attr("data-legend-entry", "__group__");
20735
21232
  if (!viewMode) {
20736
21233
  gEl.style("cursor", "pointer").on("click", () => {
20737
21234
  currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
@@ -20821,7 +21318,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20821
21318
  });
20822
21319
  };
20823
21320
  var drawSwimlaneIcon = drawSwimlaneIcon2, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
20824
- const legendY = title ? 50 : 10;
21321
+ const legendY = height - LG_HEIGHT - 4;
20825
21322
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20826
21323
  const legendGroups = parsed.timelineTagGroups.map((g) => {
20827
21324
  const pillW = g.name.length * LG_PILL_FONT_W + LG_PILL_PAD;
@@ -21592,7 +22089,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21592
22089
  const orgParsed = parseOrg2(content, effectivePalette2);
21593
22090
  if (orgParsed.error) return "";
21594
22091
  const collapsedNodes = orgExportState?.collapsedNodes;
21595
- const activeTagGroup = orgExportState?.activeTagGroup ?? null;
22092
+ const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
21596
22093
  const hiddenAttributes = orgExportState?.hiddenAttributes;
21597
22094
  const { parsed: effectiveParsed, hiddenCounts } = collapsedNodes && collapsedNodes.size > 0 ? collapseOrgTree2(orgParsed, collapsedNodes) : { parsed: orgParsed, hiddenCounts: /* @__PURE__ */ new Map() };
21598
22095
  const orgLayout = layoutOrg2(
@@ -21621,7 +22118,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21621
22118
  const sitemapParsed = parseSitemap2(content, effectivePalette2);
21622
22119
  if (sitemapParsed.error || sitemapParsed.roots.length === 0) return "";
21623
22120
  const collapsedNodes = orgExportState?.collapsedNodes;
21624
- const activeTagGroup = orgExportState?.activeTagGroup ?? null;
22121
+ const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
21625
22122
  const hiddenAttributes = orgExportState?.hiddenAttributes;
21626
22123
  const { parsed: effectiveParsed, hiddenCounts } = collapsedNodes && collapsedNodes.size > 0 ? collapseSitemapTree2(sitemapParsed, collapsedNodes) : { parsed: sitemapParsed, hiddenCounts: /* @__PURE__ */ new Map() };
21627
22124
  const sitemapLayout = layoutSitemap2(
@@ -21649,7 +22146,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21649
22146
  container2.style.position = "absolute";
21650
22147
  container2.style.left = "-9999px";
21651
22148
  document.body.appendChild(container2);
21652
- renderKanban2(container2, kanbanParsed, effectivePalette2, theme === "dark");
22149
+ renderKanban2(container2, kanbanParsed, effectivePalette2, theme === "dark", void 0, void 0, options?.tagGroup);
21653
22150
  return finalizeSvgExport(container2, theme, effectivePalette2, options);
21654
22151
  }
21655
22152
  if (detectedType === "class") {
@@ -21681,7 +22178,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21681
22178
  const exportWidth = erLayout.width + PADDING * 2;
21682
22179
  const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
21683
22180
  const container2 = createExportContainer(exportWidth, exportHeight);
21684
- renderERDiagram2(container2, erParsed, erLayout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight });
22181
+ renderERDiagram2(container2, erParsed, erLayout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight }, options?.tagGroup);
21685
22182
  return finalizeSvgExport(container2, theme, effectivePalette2, options);
21686
22183
  }
21687
22184
  if (detectedType === "initiative-status") {
@@ -21718,7 +22215,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21718
22215
  const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
21719
22216
  const container2 = createExportContainer(exportWidth, exportHeight);
21720
22217
  const renderFn = c4Level === "deployment" || c4Level === "components" && c4System && c4Container || c4Level === "containers" && c4System ? renderC4Containers2 : renderC4Context2;
21721
- renderFn(container2, c4Parsed, c4Layout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight });
22218
+ renderFn(container2, c4Parsed, c4Layout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight }, options?.tagGroup);
21722
22219
  return finalizeSvgExport(container2, theme, effectivePalette2, options);
21723
22220
  }
21724
22221
  if (detectedType === "flowchart") {
@@ -21741,16 +22238,16 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21741
22238
  const effectivePalette2 = await resolveExportPalette(theme, palette);
21742
22239
  const infraParsed = parseInfra2(content);
21743
22240
  if (infraParsed.error || infraParsed.nodes.length === 0) return "";
21744
- const selectedScenario = options?.scenario ? infraParsed.scenarios.find((s) => s.name.toLowerCase() === options.scenario.toLowerCase()) ?? null : null;
21745
- const infraComputed = computeInfra2(infraParsed, selectedScenario ? { scenario: selectedScenario } : {});
22241
+ const infraComputed = computeInfra2(infraParsed);
21746
22242
  const infraLayout = layoutInfra2(infraComputed);
22243
+ const activeTagGroup = options?.tagGroup ?? null;
21747
22244
  const titleOffset = infraParsed.title ? 40 : 0;
21748
22245
  const legendGroups = computeInfraLegendGroups2(infraLayout.nodes, infraParsed.tagGroups, effectivePalette2);
21749
22246
  const legendOffset = legendGroups.length > 0 ? 28 : 0;
21750
22247
  const exportWidth = infraLayout.width;
21751
22248
  const exportHeight = infraLayout.height + titleOffset + legendOffset;
21752
22249
  const container2 = createExportContainer(exportWidth, exportHeight);
21753
- renderInfra2(container2, infraLayout, effectivePalette2, theme === "dark", infraParsed.title, infraParsed.titleLineNumber, infraParsed.tagGroups, null, false, null, null, true);
22250
+ renderInfra2(container2, infraLayout, effectivePalette2, theme === "dark", infraParsed.title, infraParsed.titleLineNumber, infraParsed.tagGroups, activeTagGroup, false, null, null, true);
21754
22251
  const infraSvg = container2.querySelector("svg");
21755
22252
  if (infraSvg) {
21756
22253
  infraSvg.setAttribute("width", String(exportWidth));
@@ -21794,7 +22291,8 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21794
22291
  const seqParsed = parseSequenceDgmo2(content);
21795
22292
  if (seqParsed.error || seqParsed.participants.length === 0) return "";
21796
22293
  renderSequenceDiagram2(container, seqParsed, effectivePalette, isDark, void 0, {
21797
- exportWidth: EXPORT_WIDTH
22294
+ exportWidth: EXPORT_WIDTH,
22295
+ activeTagGroup: options?.tagGroup
21798
22296
  });
21799
22297
  } else if (parsed.type === "wordcloud") {
21800
22298
  await renderWordCloudAsync(container, parsed, effectivePalette, isDark, dims);
@@ -21808,7 +22306,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21808
22306
  isDark,
21809
22307
  void 0,
21810
22308
  dims,
21811
- orgExportState?.activeTagGroup,
22309
+ orgExportState?.activeTagGroup ?? options?.tagGroup,
21812
22310
  orgExportState?.swimlaneTagGroup
21813
22311
  );
21814
22312
  } else if (parsed.type === "venn") {
@@ -21837,6 +22335,7 @@ var init_d3 = __esm({
21837
22335
  init_diagnostics();
21838
22336
  init_parsing();
21839
22337
  init_tag_groups();
22338
+ init_legend_constants();
21840
22339
  DEFAULT_CLOUD_OPTIONS = {
21841
22340
  rotate: "none",
21842
22341
  max: 0,
@@ -22154,7 +22653,7 @@ async function render(content, options) {
22154
22653
  c4Level: options?.c4Level,
22155
22654
  c4System: options?.c4System,
22156
22655
  c4Container: options?.c4Container,
22157
- scenario: options?.scenario
22656
+ tagGroup: options?.tagGroup
22158
22657
  });
22159
22658
  }
22160
22659