@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.js CHANGED
@@ -1445,6 +1445,29 @@ var init_tag_groups = __esm({
1445
1445
  }
1446
1446
  });
1447
1447
 
1448
+ // src/utils/legend-constants.ts
1449
+ 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;
1450
+ var init_legend_constants = __esm({
1451
+ "src/utils/legend-constants.ts"() {
1452
+ "use strict";
1453
+ LEGEND_HEIGHT = 28;
1454
+ LEGEND_PILL_PAD = 16;
1455
+ LEGEND_PILL_FONT_SIZE = 11;
1456
+ LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
1457
+ LEGEND_CAPSULE_PAD = 4;
1458
+ LEGEND_DOT_R = 4;
1459
+ LEGEND_ENTRY_FONT_SIZE = 10;
1460
+ LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
1461
+ LEGEND_ENTRY_DOT_GAP = 4;
1462
+ LEGEND_ENTRY_TRAIL = 8;
1463
+ LEGEND_GROUP_GAP = 12;
1464
+ LEGEND_EYE_SIZE = 14;
1465
+ LEGEND_EYE_GAP = 6;
1466
+ 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";
1467
+ 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";
1468
+ }
1469
+ });
1470
+
1448
1471
  // src/sequence/participant-inference.ts
1449
1472
  function inferParticipantType(name) {
1450
1473
  for (const rule of PARTICIPANT_RULES) {
@@ -4756,10 +4779,12 @@ function makeGridAxis(type, textColor, axisLineColor, splitLineColor, gridOpacit
4756
4779
  if (type === "category" && data && data.length > 0) {
4757
4780
  const maxLabelLen = Math.max(...data.map((l) => l.length));
4758
4781
  const count = data.length;
4759
- if (count > 10 || maxLabelLen > 20) catFontSize = 10;
4760
- else if (count > 5 || maxLabelLen > 14) catFontSize = 11;
4782
+ const step = intervalOverride != null && intervalOverride > 0 ? intervalOverride + 1 : 1;
4783
+ const visibleCount = Math.ceil(count / step);
4784
+ if (visibleCount > 10 || maxLabelLen > 20) catFontSize = 10;
4785
+ else if (visibleCount > 5 || maxLabelLen > 14) catFontSize = 11;
4761
4786
  else if (maxLabelLen > 8) catFontSize = 12;
4762
- if (chartWidthHint && count > 0) {
4787
+ if ((intervalOverride == null || intervalOverride === 0) && chartWidthHint && count > 0) {
4763
4788
  const availPerLabel = Math.floor(chartWidthHint * 0.85 / count);
4764
4789
  catLabelExtras = {
4765
4790
  width: availPerLabel,
@@ -4777,6 +4802,9 @@ function makeGridAxis(type, textColor, axisLineColor, splitLineColor, gridOpacit
4777
4802
  fontFamily: FONT_FAMILY,
4778
4803
  ...type === "category" && {
4779
4804
  interval: intervalOverride ?? 0,
4805
+ // Prevent ECharts auto-rotation: it measures raw slot width (chartWidth/N),
4806
+ // which is too narrow when an interval skips most labels, and rotates to 90°.
4807
+ rotate: 0,
4780
4808
  formatter: (value) => value.replace(/([a-z])([A-Z])/g, "$1\n$2"),
4781
4809
  ...catLabelExtras
4782
4810
  }
@@ -4852,21 +4880,13 @@ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOp
4852
4880
  ]
4853
4881
  };
4854
4882
  }
4855
- function buildIntervalCallback(labels, eras) {
4883
+ function buildIntervalStep(labels) {
4856
4884
  const count = labels.length;
4857
- if (count <= 8) return () => true;
4858
- const snapSteps = [1, 2, 4, 5, 10, 20, 25, 50];
4859
- const raw = Math.ceil(count / 8);
4885
+ if (count <= 6) return 0;
4886
+ const snapSteps = [1, 2, 5, 10, 25, 50, 100];
4887
+ const raw = Math.ceil(count / 5);
4860
4888
  const N = [...snapSteps].reverse().find((s) => s <= raw) ?? 1;
4861
- const pinned = /* @__PURE__ */ new Set();
4862
- for (let i = 0; i < count; i += N) pinned.add(i);
4863
- for (const era of eras) {
4864
- const si = labels.indexOf(era.start);
4865
- const ei = labels.indexOf(era.end);
4866
- if (si >= 0) pinned.add(si);
4867
- if (ei >= 0) pinned.add(ei);
4868
- }
4869
- return (index) => pinned.has(index);
4889
+ return N - 1;
4870
4890
  }
4871
4891
  function buildMarkArea(eras, labels, textColor, defaultColor) {
4872
4892
  if (eras.length === 0) return void 0;
@@ -4901,7 +4921,7 @@ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineCol
4901
4921
  const labels = parsed.data.map((d) => d.label);
4902
4922
  const values = parsed.data.map((d) => d.value);
4903
4923
  const eras = parsed.eras ?? [];
4904
- const interval = buildIntervalCallback(labels, eras);
4924
+ const interval = buildIntervalStep(labels);
4905
4925
  const markArea = buildMarkArea(eras, labels, textColor, palette.colors.blue);
4906
4926
  return {
4907
4927
  ...CHART_BASE,
@@ -4933,7 +4953,7 @@ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLi
4933
4953
  const seriesNames = parsed.seriesNames ?? [];
4934
4954
  const labels = parsed.data.map((d) => d.label);
4935
4955
  const eras = parsed.eras ?? [];
4936
- const interval = buildIntervalCallback(labels, eras);
4956
+ const interval = buildIntervalStep(labels);
4937
4957
  const markArea = buildMarkArea(eras, labels, textColor, palette.colors.blue);
4938
4958
  const series = seriesNames.map((name, idx) => {
4939
4959
  const color = parsed.seriesNameColors?.[idx] ?? colors[idx % colors.length];
@@ -4977,7 +4997,7 @@ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineCol
4977
4997
  const labels = parsed.data.map((d) => d.label);
4978
4998
  const values = parsed.data.map((d) => d.value);
4979
4999
  const eras = parsed.eras ?? [];
4980
- const interval = buildIntervalCallback(labels, eras);
5000
+ const interval = buildIntervalStep(labels);
4981
5001
  const markArea = buildMarkArea(eras, labels, textColor, palette.colors.blue);
4982
5002
  return {
4983
5003
  ...CHART_BASE,
@@ -6990,7 +7010,6 @@ function parseInfra(content) {
6990
7010
  edges: [],
6991
7011
  groups: [],
6992
7012
  tagGroups: [],
6993
- scenarios: [],
6994
7013
  options: {},
6995
7014
  diagnostics: [],
6996
7015
  error: null
@@ -7093,16 +7112,7 @@ function parseInfra(content) {
7093
7112
  continue;
7094
7113
  }
7095
7114
  if (/^scenario\s*:/i.test(trimmed)) {
7096
- finishCurrentNode();
7097
- finishCurrentTagGroup();
7098
- currentGroup = null;
7099
- const scenarioName = trimmed.replace(/^scenario\s*:\s*/i, "").trim();
7100
- const scenario = {
7101
- name: scenarioName,
7102
- overrides: {},
7103
- lineNumber
7104
- };
7105
- let scenarioNodeId = null;
7115
+ console.warn("[dgmo warn] scenario syntax is deprecated and will be ignored");
7106
7116
  let si = i + 1;
7107
7117
  while (si < lines.length) {
7108
7118
  const sLine = lines[si];
@@ -7113,23 +7123,9 @@ function parseInfra(content) {
7113
7123
  }
7114
7124
  const sIndent = sLine.length - sLine.trimStart().length;
7115
7125
  if (sIndent === 0) break;
7116
- if (sIndent <= 2) {
7117
- scenarioNodeId = nodeId2(sTrimmed.replace(/\|.*$/, "").trim());
7118
- if (!scenario.overrides[scenarioNodeId]) {
7119
- scenario.overrides[scenarioNodeId] = {};
7120
- }
7121
- } else if (scenarioNodeId) {
7122
- const pm = sTrimmed.match(PROPERTY_RE);
7123
- if (pm) {
7124
- const key = pm[1].toLowerCase();
7125
- const val = parsePropertyValue(pm[2].trim());
7126
- scenario.overrides[scenarioNodeId][key] = val;
7127
- }
7128
- }
7129
7126
  si++;
7130
7127
  }
7131
7128
  i = si - 1;
7132
- result.scenarios.push(scenario);
7133
7129
  continue;
7134
7130
  }
7135
7131
  const tagMatch = trimmed.match(TAG_GROUP_RE);
@@ -7662,14 +7658,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
7662
7658
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
7663
7659
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
7664
7660
  if (visibleEntries.length === 0) continue;
7665
- const pillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
7661
+ const pillWidth = group.name.length * LEGEND_PILL_FONT_W2 + LEGEND_PILL_PAD2;
7666
7662
  const minPillWidth = pillWidth;
7667
7663
  let entriesWidth = 0;
7668
7664
  for (const entry of visibleEntries) {
7669
- entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
7665
+ entriesWidth += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + entry.value.length * LEGEND_ENTRY_FONT_W2 + LEGEND_ENTRY_TRAIL2;
7670
7666
  }
7671
- const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
7672
- const capsuleWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
7667
+ const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
7668
+ const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
7673
7669
  groups.push({
7674
7670
  name: group.name,
7675
7671
  alias: group.alias,
@@ -7680,9 +7676,9 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
7680
7676
  x: 0,
7681
7677
  y: 0,
7682
7678
  width: capsuleWidth,
7683
- height: LEGEND_HEIGHT,
7679
+ height: LEGEND_HEIGHT2,
7684
7680
  minifiedWidth: minPillWidth,
7685
- minifiedHeight: LEGEND_HEIGHT
7681
+ minifiedHeight: LEGEND_HEIGHT2
7686
7682
  });
7687
7683
  }
7688
7684
  return groups;
@@ -7712,7 +7708,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
7712
7708
  for (const g of legendGroups2) {
7713
7709
  g.x = MARGIN;
7714
7710
  g.y = cy;
7715
- cy += LEGEND_HEIGHT + LEGEND_GROUP_GAP;
7711
+ cy += LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
7716
7712
  if (g.width > maxWidth2) maxWidth2 = g.width;
7717
7713
  }
7718
7714
  return {
@@ -7721,7 +7717,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
7721
7717
  containers: [],
7722
7718
  legend: legendGroups2,
7723
7719
  width: maxWidth2 + MARGIN * 2,
7724
- height: cy - LEGEND_GROUP_GAP + MARGIN
7720
+ height: cy - LEGEND_GROUP_GAP2 + MARGIN
7725
7721
  };
7726
7722
  }
7727
7723
  injectDefaultMetadata(parsed.roots, parsed.tagGroups);
@@ -8251,7 +8247,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8251
8247
  const effectiveH = (g) => activeTagGroup != null || allExpanded ? g.height : g.minifiedHeight;
8252
8248
  if (visibleGroups.length > 0) {
8253
8249
  if (legendPosition === "bottom") {
8254
- const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
8250
+ const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP2;
8255
8251
  const neededWidth = totalGroupsWidth + MARGIN * 2;
8256
8252
  if (neededWidth > totalWidth) {
8257
8253
  finalWidth = neededWidth;
@@ -8269,22 +8265,22 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8269
8265
  for (const g of visibleGroups) {
8270
8266
  g.x = cx;
8271
8267
  g.y = legendY;
8272
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
8268
+ cx += effectiveW(g) + LEGEND_GROUP_GAP2;
8273
8269
  }
8274
- finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
8270
+ finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT2;
8275
8271
  } else {
8276
- const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
8272
+ const legendShift = LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
8277
8273
  for (const n of layoutNodes) n.y += legendShift;
8278
8274
  for (const c of containers) c.y += legendShift;
8279
8275
  for (const e of layoutEdges) {
8280
8276
  for (const p of e.points) p.y += legendShift;
8281
8277
  }
8282
- const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
8278
+ const totalGroupsWidth = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP2;
8283
8279
  let cx = MARGIN;
8284
8280
  for (const g of visibleGroups) {
8285
8281
  g.x = cx;
8286
8282
  g.y = MARGIN;
8287
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
8283
+ cx += effectiveW(g) + LEGEND_GROUP_GAP2;
8288
8284
  }
8289
8285
  finalHeight += legendShift;
8290
8286
  const neededWidth = totalGroupsWidth + MARGIN * 2;
@@ -8302,7 +8298,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8302
8298
  height: finalHeight
8303
8299
  };
8304
8300
  }
8305
- var 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;
8301
+ var 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;
8306
8302
  var init_layout = __esm({
8307
8303
  "src/org/layout.ts"() {
8308
8304
  "use strict";
@@ -8323,17 +8319,17 @@ var init_layout = __esm({
8323
8319
  CONTAINER_META_LINE_HEIGHT = 16;
8324
8320
  STACK_V_GAP = 20;
8325
8321
  LEGEND_GAP = 30;
8326
- LEGEND_HEIGHT = 28;
8327
- LEGEND_PILL_PAD = 16;
8328
- LEGEND_PILL_FONT_W = 11 * 0.6;
8329
- LEGEND_CAPSULE_PAD = 4;
8330
- LEGEND_DOT_R = 4;
8331
- LEGEND_ENTRY_FONT_W = 10 * 0.6;
8332
- LEGEND_ENTRY_DOT_GAP = 4;
8333
- LEGEND_ENTRY_TRAIL = 8;
8334
- LEGEND_GROUP_GAP = 12;
8335
- LEGEND_EYE_SIZE = 14;
8336
- LEGEND_EYE_GAP = 6;
8322
+ LEGEND_HEIGHT2 = 28;
8323
+ LEGEND_PILL_PAD2 = 16;
8324
+ LEGEND_PILL_FONT_W2 = 11 * 0.6;
8325
+ LEGEND_CAPSULE_PAD2 = 4;
8326
+ LEGEND_DOT_R2 = 4;
8327
+ LEGEND_ENTRY_FONT_W2 = 10 * 0.6;
8328
+ LEGEND_ENTRY_DOT_GAP2 = 4;
8329
+ LEGEND_ENTRY_TRAIL2 = 8;
8330
+ LEGEND_GROUP_GAP2 = 12;
8331
+ LEGEND_EYE_SIZE2 = 14;
8332
+ LEGEND_EYE_GAP2 = 6;
8337
8333
  }
8338
8334
  });
8339
8335
 
@@ -8431,11 +8427,11 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8431
8427
  if (width <= 0 || height <= 0) return;
8432
8428
  const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
8433
8429
  const legendOnly = layout.nodes.length === 0;
8434
- const legendPosition = parsed.options?.["legend-position"] ?? "top";
8430
+ const legendPosition = parsed.options?.["legend-position"] ?? "bottom";
8435
8431
  const hasLegend = layout.legend.length > 0;
8436
- const layoutLegendShift = LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
8432
+ const layoutLegendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
8437
8433
  const fixedLegend = !exportDims && hasLegend && !legendOnly;
8438
- const legendReserve = fixedLegend ? LEGEND_HEIGHT2 + LEGEND_FIXED_GAP : 0;
8434
+ const legendReserve = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
8439
8435
  const fixedTitle = !exportDims && !!parsed.title;
8440
8436
  const titleReserve = fixedTitle ? TITLE_HEIGHT : 0;
8441
8437
  const diagramW = layout.width;
@@ -8590,56 +8586,60 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8590
8586
  if (fixedLegend && visibleGroups.length > 0) {
8591
8587
  fixedPositions = /* @__PURE__ */ new Map();
8592
8588
  const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
8593
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP2;
8589
+ const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
8594
8590
  let cx = (width - totalW) / 2;
8595
8591
  for (const g of visibleGroups) {
8596
8592
  fixedPositions.set(g.name, cx);
8597
- cx += effectiveW(g) + LEGEND_GROUP_GAP2;
8593
+ cx += effectiveW(g) + LEGEND_GROUP_GAP;
8598
8594
  }
8599
8595
  }
8600
- const legendParent = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr(
8596
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr(
8601
8597
  "transform",
8602
- legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT2})` : `translate(0, ${DIAGRAM_PADDING + titleReserve})`
8598
+ legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})` : `translate(0, ${DIAGRAM_PADDING + titleReserve})`
8603
8599
  ) : contentG;
8600
+ const legendParent = legendParentBase;
8601
+ if (fixedLegend && activeTagGroup) {
8602
+ legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
8603
+ }
8604
8604
  for (const group of visibleGroups) {
8605
8605
  const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
8606
8606
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
8607
8607
  const pillLabel = group.name;
8608
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W2 + LEGEND_PILL_PAD2;
8608
+ const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
8609
8609
  const gX = fixedPositions?.get(group.name) ?? group.x;
8610
8610
  const gY = fixedPositions ? 0 : group.y;
8611
8611
  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");
8612
8612
  if (isActive) {
8613
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT2).attr("rx", LEGEND_HEIGHT2 / 2).attr("fill", groupBg);
8613
+ gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
8614
8614
  }
8615
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD2 : 0;
8616
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD2 : 0;
8617
- const pillH = LEGEND_HEIGHT2 - (isActive ? LEGEND_CAPSULE_PAD2 * 2 : 0);
8615
+ const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
8616
+ const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
8617
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
8618
8618
  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);
8619
8619
  if (isActive) {
8620
8620
  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);
8621
8621
  }
8622
- 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);
8622
+ 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);
8623
8623
  if (isActive && fixedLegend) {
8624
8624
  const groupKey = group.name.toLowerCase();
8625
8625
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
8626
- const eyeX = pillXOff + pillWidth + LEGEND_EYE_GAP2;
8627
- const eyeY = (LEGEND_HEIGHT2 - LEGEND_EYE_SIZE2) / 2;
8626
+ const eyeX = pillXOff + pillWidth + LEGEND_EYE_GAP;
8627
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
8628
8628
  const hitPad = 6;
8629
8629
  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);
8630
- 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");
8630
+ 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");
8631
8631
  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");
8632
8632
  }
8633
8633
  if (isActive) {
8634
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
8634
+ const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
8635
8635
  let entryX = pillXOff + pillWidth + 4 + eyeShift;
8636
8636
  for (const entry of group.entries) {
8637
8637
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
8638
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R2).attr("cy", LEGEND_HEIGHT2 / 2).attr("r", LEGEND_DOT_R2).attr("fill", entry.color);
8639
- const textX = entryX + LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2;
8638
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
8639
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
8640
8640
  const entryLabel = entry.value;
8641
- 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);
8642
- entryX = textX + entryLabel.length * LEGEND_ENTRY_FONT_W2 + LEGEND_ENTRY_TRAIL2;
8641
+ 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);
8642
+ entryX = textX + entryLabel.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
8643
8643
  }
8644
8644
  }
8645
8645
  }
@@ -8678,7 +8678,7 @@ function renderOrgForExport(content, theme, palette) {
8678
8678
  document.body.removeChild(container);
8679
8679
  }
8680
8680
  }
8681
- var 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;
8681
+ var 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;
8682
8682
  var init_renderer = __esm({
8683
8683
  "src/org/renderer.ts"() {
8684
8684
  "use strict";
@@ -8686,6 +8686,7 @@ var init_renderer = __esm({
8686
8686
  init_color_utils();
8687
8687
  init_parser4();
8688
8688
  init_layout();
8689
+ init_legend_constants();
8689
8690
  DIAGRAM_PADDING = 20;
8690
8691
  MAX_SCALE = 3;
8691
8692
  TITLE_HEIGHT = 30;
@@ -8705,22 +8706,7 @@ var init_renderer = __esm({
8705
8706
  CONTAINER_HEADER_HEIGHT = 28;
8706
8707
  COLLAPSE_BAR_HEIGHT = 6;
8707
8708
  COLLAPSE_BAR_INSET = 0;
8708
- LEGEND_HEIGHT2 = 28;
8709
- LEGEND_PILL_PAD2 = 16;
8710
- LEGEND_PILL_FONT_SIZE = 11;
8711
- LEGEND_PILL_FONT_W2 = LEGEND_PILL_FONT_SIZE * 0.6;
8712
- LEGEND_CAPSULE_PAD2 = 4;
8713
- LEGEND_DOT_R2 = 4;
8714
- LEGEND_ENTRY_FONT_SIZE = 10;
8715
- LEGEND_ENTRY_FONT_W2 = LEGEND_ENTRY_FONT_SIZE * 0.6;
8716
- LEGEND_ENTRY_DOT_GAP2 = 4;
8717
- LEGEND_ENTRY_TRAIL2 = 8;
8718
- LEGEND_GROUP_GAP2 = 12;
8719
- LEGEND_EYE_SIZE2 = 14;
8720
- LEGEND_EYE_GAP2 = 6;
8721
8709
  LEGEND_FIXED_GAP = 8;
8722
- 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";
8723
- 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";
8724
8710
  }
8725
8711
  });
8726
8712
 
@@ -9145,23 +9131,17 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
9145
9131
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
9146
9132
  if (visibleGroups.length > 0) {
9147
9133
  const legendShift = LEGEND_HEIGHT3 + LEGEND_GROUP_GAP3;
9148
- for (const n of layoutNodes) n.y += legendShift;
9149
- for (const c of layoutContainers) c.y += legendShift;
9150
- for (const e of layoutEdges) {
9151
- for (const p of e.points) p.y += legendShift;
9152
- }
9153
9134
  const totalGroupsWidth = visibleGroups.reduce((s, g2) => s + effectiveW(g2), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP3;
9154
- let cx = MARGIN2;
9135
+ const neededWidth = totalGroupsWidth + MARGIN2 * 2;
9136
+ if (neededWidth > totalWidth) totalWidth = neededWidth;
9137
+ let cx = (totalWidth - totalGroupsWidth) / 2;
9138
+ const legendY = totalHeight + LEGEND_GROUP_GAP3;
9155
9139
  for (const g2 of visibleGroups) {
9156
9140
  g2.x = cx;
9157
- g2.y = MARGIN2;
9141
+ g2.y = legendY;
9158
9142
  cx += effectiveW(g2) + LEGEND_GROUP_GAP3;
9159
9143
  }
9160
9144
  totalHeight += legendShift;
9161
- const neededWidth = totalGroupsWidth + MARGIN2 * 2;
9162
- if (neededWidth > totalWidth) {
9163
- totalWidth = neededWidth;
9164
- }
9165
9145
  }
9166
9146
  return {
9167
9147
  nodes: layoutNodes,
@@ -9356,25 +9336,26 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
9356
9336
  const height = exportDims?.height ?? container.clientHeight;
9357
9337
  if (width <= 0 || height <= 0) return;
9358
9338
  const hasLegend = layout.legend.length > 0;
9359
- const layoutLegendShift = LEGEND_HEIGHT4 + LEGEND_GROUP_GAP4;
9339
+ const layoutLegendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
9360
9340
  const fixedLegend = !exportDims && hasLegend;
9361
9341
  const fixedTitle = fixedLegend && !!parsed.title;
9362
9342
  const fixedTitleH = fixedTitle ? TITLE_HEIGHT2 : 0;
9363
- const legendReserveH = fixedLegend ? LEGEND_HEIGHT4 + LEGEND_FIXED_GAP2 : 0;
9364
- const fixedReserve = fixedTitleH + legendReserveH;
9343
+ const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP2 : 0;
9344
+ const fixedReserveTop = fixedTitleH;
9345
+ const fixedReserveBottom = legendReserveH;
9365
9346
  const titleOffset = !fixedTitle && parsed.title ? TITLE_HEIGHT2 : 0;
9366
9347
  const diagramW = layout.width;
9367
9348
  let diagramH = layout.height + titleOffset;
9368
9349
  if (fixedLegend) {
9369
9350
  diagramH -= layoutLegendShift;
9370
9351
  }
9371
- const availH = height - DIAGRAM_PADDING2 * 2 - fixedReserve;
9352
+ const availH = height - DIAGRAM_PADDING2 * 2 - fixedReserveTop - fixedReserveBottom;
9372
9353
  const scaleX = (width - DIAGRAM_PADDING2 * 2) / diagramW;
9373
9354
  const scaleY = availH / diagramH;
9374
9355
  const scale = Math.min(MAX_SCALE2, scaleX, scaleY);
9375
9356
  const scaledW = diagramW * scale;
9376
9357
  const offsetX = (width - scaledW) / 2;
9377
- const offsetY = DIAGRAM_PADDING2 + fixedReserve;
9358
+ const offsetY = DIAGRAM_PADDING2 + fixedReserveTop;
9378
9359
  const svg = d3Selection2.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
9379
9360
  const defs = svg.append("defs");
9380
9361
  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);
@@ -9515,7 +9496,10 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
9515
9496
  titleEl.text(parsed.title);
9516
9497
  }
9517
9498
  if (fixedLegend) {
9518
- const legendParent = svg.append("g").attr("class", "sitemap-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING2 + fixedTitleH})`);
9499
+ const legendParent = svg.append("g").attr("class", "sitemap-legend-fixed").attr("transform", `translate(0, ${height - DIAGRAM_PADDING2 - LEGEND_HEIGHT})`);
9500
+ if (activeTagGroup) {
9501
+ legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
9502
+ }
9519
9503
  renderLegend(legendParent, layout.legend, palette, isDark, activeTagGroup, width, hiddenAttributes);
9520
9504
  }
9521
9505
  }
@@ -9527,49 +9511,49 @@ function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fix
9527
9511
  if (fixedWidth != null && visibleGroups.length > 0) {
9528
9512
  fixedPositions = /* @__PURE__ */ new Map();
9529
9513
  const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
9530
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP4;
9514
+ const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
9531
9515
  let cx = (fixedWidth - totalW) / 2;
9532
9516
  for (const g of visibleGroups) {
9533
9517
  fixedPositions.set(g.name, cx);
9534
- cx += effectiveW(g) + LEGEND_GROUP_GAP4;
9518
+ cx += effectiveW(g) + LEGEND_GROUP_GAP;
9535
9519
  }
9536
9520
  }
9537
9521
  for (const group of visibleGroups) {
9538
9522
  const isActive = activeTagGroup != null;
9539
- const pillW = group.name.length * LEGEND_PILL_FONT_W4 + LEGEND_PILL_PAD4;
9523
+ const pillW = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
9540
9524
  const gX = fixedPositions?.get(group.name) ?? group.x;
9541
9525
  const gY = fixedPositions ? 0 : group.y;
9542
9526
  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");
9543
9527
  if (isActive) {
9544
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT4).attr("rx", LEGEND_HEIGHT4 / 2).attr("fill", groupBg);
9528
+ legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
9545
9529
  }
9546
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD4 : 0;
9547
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD4 : 0;
9548
- const pillH = LEGEND_HEIGHT4 - (isActive ? LEGEND_CAPSULE_PAD4 * 2 : 0);
9530
+ const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
9531
+ const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
9532
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
9549
9533
  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);
9550
9534
  if (isActive) {
9551
9535
  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);
9552
9536
  }
9553
- 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);
9537
+ 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);
9554
9538
  if (isActive && fixedWidth != null) {
9555
9539
  const groupKey = group.name.toLowerCase();
9556
9540
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
9557
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP4;
9558
- const eyeY = (LEGEND_HEIGHT4 - LEGEND_EYE_SIZE4) / 2;
9541
+ const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
9542
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
9559
9543
  const hitPad = 6;
9560
9544
  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);
9561
- 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");
9562
- 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");
9545
+ 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");
9546
+ 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");
9563
9547
  }
9564
9548
  if (isActive) {
9565
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE4 + LEGEND_EYE_GAP4 : 0;
9549
+ const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
9566
9550
  let entryX = pillXOff + pillW + 4 + eyeShift;
9567
9551
  for (const entry of group.entries) {
9568
9552
  const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
9569
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R4).attr("cy", LEGEND_HEIGHT4 / 2).attr("r", LEGEND_DOT_R4).attr("fill", entry.color);
9570
- const textX = entryX + LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4;
9571
- 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);
9572
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W4 + LEGEND_ENTRY_TRAIL4;
9553
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
9554
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
9555
+ 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);
9556
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
9573
9557
  }
9574
9558
  }
9575
9559
  }
@@ -9615,12 +9599,13 @@ async function renderSitemapForExport(content, theme, palette) {
9615
9599
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
9616
9600
  return injectBranding2(svgHtml, brandColor);
9617
9601
  }
9618
- var 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;
9602
+ var 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;
9619
9603
  var init_renderer2 = __esm({
9620
9604
  "src/sitemap/renderer.ts"() {
9621
9605
  "use strict";
9622
9606
  init_fonts();
9623
9607
  init_color_utils();
9608
+ init_legend_constants();
9624
9609
  DIAGRAM_PADDING2 = 20;
9625
9610
  MAX_SCALE2 = 3;
9626
9611
  TITLE_HEIGHT2 = 30;
@@ -9642,23 +9627,8 @@ var init_renderer2 = __esm({
9642
9627
  ARROWHEAD_H = 7;
9643
9628
  EDGE_LABEL_FONT_SIZE = 11;
9644
9629
  COLLAPSE_BAR_HEIGHT2 = 6;
9645
- LEGEND_HEIGHT4 = 28;
9646
9630
  LEGEND_FIXED_GAP2 = 8;
9647
- LEGEND_PILL_PAD4 = 16;
9648
- LEGEND_PILL_FONT_SIZE2 = 11;
9649
- LEGEND_PILL_FONT_W4 = LEGEND_PILL_FONT_SIZE2 * 0.6;
9650
- LEGEND_CAPSULE_PAD4 = 4;
9651
- LEGEND_DOT_R4 = 4;
9652
- LEGEND_ENTRY_FONT_SIZE2 = 10;
9653
- LEGEND_ENTRY_FONT_W4 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
9654
- LEGEND_ENTRY_DOT_GAP4 = 4;
9655
- LEGEND_ENTRY_TRAIL4 = 8;
9656
- LEGEND_GROUP_GAP4 = 12;
9657
- LEGEND_EYE_SIZE4 = 14;
9658
- LEGEND_EYE_GAP4 = 6;
9659
9631
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
9660
- 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";
9661
- 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";
9662
9632
  }
9663
9633
  });
9664
9634
 
@@ -9861,8 +9831,7 @@ function resolveCardTagColor(card, tagGroups, activeTagGroup) {
9861
9831
  return entry?.color;
9862
9832
  }
9863
9833
  function computeLayout(parsed, _palette) {
9864
- const hasHeader = !!parsed.title || parsed.tagGroups.length > 0;
9865
- const headerHeight = hasHeader ? Math.max(TITLE_HEIGHT3, LEGEND_HEIGHT5) + 8 : 0;
9834
+ const headerHeight = parsed.title ? TITLE_HEIGHT3 + 8 : 0;
9866
9835
  const startY = DIAGRAM_PADDING3 + headerHeight;
9867
9836
  const charWidth = CARD_TITLE_FONT_SIZE * 0.6;
9868
9837
  const columnLayouts = [];
@@ -9919,7 +9888,8 @@ function computeLayout(parsed, _palette) {
9919
9888
  currentX += cl.width + COLUMN_GAP;
9920
9889
  }
9921
9890
  const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING3;
9922
- const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3;
9891
+ const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
9892
+ const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3 + legendSpace;
9923
9893
  return { columns: columnLayouts, totalWidth, totalHeight };
9924
9894
  }
9925
9895
  function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exportDims, activeTagGroup) {
@@ -9932,42 +9902,45 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
9932
9902
  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);
9933
9903
  }
9934
9904
  if (parsed.tagGroups.length > 0) {
9935
- const legendY = DIAGRAM_PADDING3;
9936
- const titleTextWidth = parsed.title ? parsed.title.length * TITLE_FONT_SIZE3 * 0.6 + 16 : 0;
9937
- let legendX = DIAGRAM_PADDING3 + titleTextWidth;
9905
+ const legendY = height - LEGEND_HEIGHT;
9906
+ let legendX = DIAGRAM_PADDING3;
9938
9907
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
9939
- const capsulePad = 4;
9908
+ const capsulePad = LEGEND_CAPSULE_PAD;
9909
+ const legendContainer = svg.append("g").attr("class", "kanban-legend");
9910
+ if (activeTagGroup) {
9911
+ legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
9912
+ }
9940
9913
  for (const group of parsed.tagGroups) {
9941
9914
  const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
9942
9915
  if (activeTagGroup != null && !isActive) continue;
9943
- const pillTextWidth = group.name.length * LEGEND_FONT_SIZE * 0.6;
9916
+ const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
9944
9917
  const pillWidth = pillTextWidth + 16;
9945
9918
  let capsuleContentWidth = pillWidth;
9946
9919
  if (isActive) {
9947
9920
  capsuleContentWidth += 4;
9948
9921
  for (const entry of group.entries) {
9949
- capsuleContentWidth += LEGEND_DOT_R5 * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE3 * 0.6 + 8;
9922
+ capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
9950
9923
  }
9951
9924
  }
9952
9925
  const capsuleWidth = capsuleContentWidth + capsulePad * 2;
9953
9926
  if (isActive) {
9954
- svg.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT5).attr("rx", LEGEND_HEIGHT5 / 2).attr("fill", groupBg);
9927
+ legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
9955
9928
  }
9956
9929
  const pillX = legendX + (isActive ? capsulePad : 0);
9957
9930
  const pillBg = isActive ? palette.bg : groupBg;
9958
- 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());
9931
+ 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());
9959
9932
  if (isActive) {
9960
- 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);
9933
+ 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);
9961
9934
  }
9962
- 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);
9935
+ 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);
9963
9936
  if (isActive) {
9964
9937
  let entryX = pillX + pillWidth + 4;
9965
9938
  for (const entry of group.entries) {
9966
- const entryG = svg.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
9967
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R5).attr("cy", legendY + LEGEND_HEIGHT5 / 2).attr("r", LEGEND_DOT_R5).attr("fill", entry.color);
9968
- const entryTextX = entryX + LEGEND_DOT_R5 * 2 + 4;
9969
- 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);
9970
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE3 * 0.6 + 8;
9939
+ const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
9940
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
9941
+ const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
9942
+ 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);
9943
+ entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
9971
9944
  }
9972
9945
  legendX += capsuleWidth + 12;
9973
9946
  } else {
@@ -10054,7 +10027,7 @@ function renderKanbanForExport(content, theme, palette) {
10054
10027
  const svgEl = container.querySelector("svg");
10055
10028
  return svgEl?.outerHTML ?? "";
10056
10029
  }
10057
- var 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;
10030
+ var 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;
10058
10031
  var init_renderer3 = __esm({
10059
10032
  "src/kanban/renderer.ts"() {
10060
10033
  "use strict";
@@ -10063,6 +10036,7 @@ var init_renderer3 = __esm({
10063
10036
  init_inline_markdown();
10064
10037
  init_parser5();
10065
10038
  init_mutations();
10039
+ init_legend_constants();
10066
10040
  DIAGRAM_PADDING3 = 20;
10067
10041
  COLUMN_GAP = 16;
10068
10042
  COLUMN_HEADER_HEIGHT = 36;
@@ -10084,10 +10058,6 @@ var init_renderer3 = __esm({
10084
10058
  WIP_FONT_SIZE = 10;
10085
10059
  COLUMN_RADIUS = 8;
10086
10060
  COLUMN_HEADER_RADIUS = 8;
10087
- LEGEND_HEIGHT5 = 28;
10088
- LEGEND_FONT_SIZE = 11;
10089
- LEGEND_DOT_R5 = 4;
10090
- LEGEND_ENTRY_FONT_SIZE3 = 10;
10091
10061
  }
10092
10062
  });
10093
10063
 
@@ -10506,110 +10476,211 @@ function computeNodeDimensions2(table) {
10506
10476
  const height = headerHeight + columnsHeight + (columnsHeight === 0 ? 4 : 0);
10507
10477
  return { width, height, headerHeight, columnsHeight };
10508
10478
  }
10479
+ function findConnectedComponents(tableIds, relationships) {
10480
+ const adj = /* @__PURE__ */ new Map();
10481
+ for (const id of tableIds) adj.set(id, /* @__PURE__ */ new Set());
10482
+ for (const rel of relationships) {
10483
+ adj.get(rel.source)?.add(rel.target);
10484
+ adj.get(rel.target)?.add(rel.source);
10485
+ }
10486
+ const visited = /* @__PURE__ */ new Set();
10487
+ const components = [];
10488
+ for (const id of tableIds) {
10489
+ if (visited.has(id)) continue;
10490
+ const comp = [];
10491
+ const queue = [id];
10492
+ while (queue.length > 0) {
10493
+ const cur = queue.shift();
10494
+ if (visited.has(cur)) continue;
10495
+ visited.add(cur);
10496
+ comp.push(cur);
10497
+ for (const nb of adj.get(cur) ?? []) {
10498
+ if (!visited.has(nb)) queue.push(nb);
10499
+ }
10500
+ }
10501
+ components.push(comp);
10502
+ }
10503
+ return components;
10504
+ }
10505
+ function layoutComponent(tables, rels, dimMap) {
10506
+ const nodePositions = /* @__PURE__ */ new Map();
10507
+ const edgePoints = /* @__PURE__ */ new Map();
10508
+ if (tables.length === 1) {
10509
+ const dims = dimMap.get(tables[0].id);
10510
+ nodePositions.set(tables[0].id, { x: dims.width / 2, y: dims.height / 2, ...dims });
10511
+ return { nodePositions, edgePoints, width: dims.width, height: dims.height };
10512
+ }
10513
+ const g = new dagre3.graphlib.Graph({ multigraph: true });
10514
+ g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80, edgesep: 20 });
10515
+ g.setDefaultEdgeLabel(() => ({}));
10516
+ for (const table of tables) {
10517
+ const dims = dimMap.get(table.id);
10518
+ g.setNode(table.id, { width: dims.width, height: dims.height });
10519
+ }
10520
+ for (const rel of rels) {
10521
+ g.setEdge(rel.source, rel.target, { label: rel.label ?? "" }, String(rel.lineNumber));
10522
+ }
10523
+ dagre3.layout(g);
10524
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
10525
+ for (const table of tables) {
10526
+ const pos = g.node(table.id);
10527
+ const dims = dimMap.get(table.id);
10528
+ minX = Math.min(minX, pos.x - dims.width / 2);
10529
+ minY = Math.min(minY, pos.y - dims.height / 2);
10530
+ maxX = Math.max(maxX, pos.x + dims.width / 2);
10531
+ maxY = Math.max(maxY, pos.y + dims.height / 2);
10532
+ }
10533
+ for (const rel of rels) {
10534
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10535
+ for (const pt of ed?.points ?? []) {
10536
+ minX = Math.min(minX, pt.x);
10537
+ minY = Math.min(minY, pt.y);
10538
+ maxX = Math.max(maxX, pt.x);
10539
+ maxY = Math.max(maxY, pt.y);
10540
+ }
10541
+ if (rel.label && (ed?.points ?? []).length > 0) {
10542
+ const pts = ed.points;
10543
+ const mid = pts[Math.floor(pts.length / 2)];
10544
+ const hw = (rel.label.length * 7 + 8) / 2;
10545
+ minX = Math.min(minX, mid.x - hw);
10546
+ maxX = Math.max(maxX, mid.x + hw);
10547
+ }
10548
+ }
10549
+ for (const table of tables) {
10550
+ const pos = g.node(table.id);
10551
+ const dims = dimMap.get(table.id);
10552
+ nodePositions.set(table.id, {
10553
+ x: pos.x - minX,
10554
+ y: pos.y - minY,
10555
+ ...dims
10556
+ });
10557
+ }
10558
+ for (const rel of rels) {
10559
+ const ed = g.edge(rel.source, rel.target, String(rel.lineNumber));
10560
+ edgePoints.set(
10561
+ rel.lineNumber,
10562
+ (ed?.points ?? []).map((pt) => ({ x: pt.x - minX, y: pt.y - minY }))
10563
+ );
10564
+ }
10565
+ return {
10566
+ nodePositions,
10567
+ edgePoints,
10568
+ width: Math.max(0, maxX - minX),
10569
+ height: Math.max(0, maxY - minY)
10570
+ };
10571
+ }
10572
+ function packComponents(items) {
10573
+ if (items.length === 0) return [];
10574
+ const sorted = [...items].sort((a, b) => {
10575
+ const aConnected = a.compIds.length > 1 ? 1 : 0;
10576
+ const bConnected = b.compIds.length > 1 ? 1 : 0;
10577
+ if (aConnected !== bConnected) return bConnected - aConnected;
10578
+ return b.compLayout.height - a.compLayout.height;
10579
+ });
10580
+ const totalArea = items.reduce(
10581
+ (s, c) => s + (c.compLayout.width || MIN_WIDTH2) * (c.compLayout.height || HEADER_BASE2),
10582
+ 0
10583
+ );
10584
+ const targetW = Math.max(
10585
+ Math.sqrt(totalArea) * 1.5,
10586
+ sorted[0].compLayout.width
10587
+ // at least as wide as the widest component
10588
+ );
10589
+ const placements = [];
10590
+ let curX = 0;
10591
+ let curY = 0;
10592
+ let rowH = 0;
10593
+ for (const item of sorted) {
10594
+ const w = item.compLayout.width || MIN_WIDTH2;
10595
+ const h = item.compLayout.height || HEADER_BASE2;
10596
+ if (curX > 0 && curX + w > targetW) {
10597
+ curY += rowH + COMP_GAP;
10598
+ curX = 0;
10599
+ rowH = 0;
10600
+ }
10601
+ placements.push({ compIds: item.compIds, compLayout: item.compLayout, offsetX: curX, offsetY: curY });
10602
+ curX += w + COMP_GAP;
10603
+ rowH = Math.max(rowH, h);
10604
+ }
10605
+ return placements;
10606
+ }
10509
10607
  function layoutERDiagram(parsed) {
10510
10608
  if (parsed.tables.length === 0) {
10511
10609
  return { nodes: [], edges: [], width: 0, height: 0 };
10512
10610
  }
10513
- const g = new dagre3.graphlib.Graph();
10514
- g.setGraph({
10515
- rankdir: "TB",
10516
- nodesep: 60,
10517
- ranksep: 80,
10518
- edgesep: 20
10519
- });
10520
- g.setDefaultEdgeLabel(() => ({}));
10521
10611
  const dimMap = /* @__PURE__ */ new Map();
10522
10612
  for (const table of parsed.tables) {
10523
- const dims = computeNodeDimensions2(table);
10524
- dimMap.set(table.id, dims);
10525
- g.setNode(table.id, {
10526
- label: table.name,
10527
- width: dims.width,
10528
- height: dims.height
10529
- });
10613
+ dimMap.set(table.id, computeNodeDimensions2(table));
10530
10614
  }
10531
- for (const rel of parsed.relationships) {
10532
- g.setEdge(rel.source, rel.target, { label: rel.label ?? "" });
10615
+ const compIdSets = findConnectedComponents(
10616
+ parsed.tables.map((t) => t.id),
10617
+ parsed.relationships
10618
+ );
10619
+ const tableById = new Map(parsed.tables.map((t) => [t.id, t]));
10620
+ const componentItems = compIdSets.map((ids) => {
10621
+ const tables = ids.map((id) => tableById.get(id));
10622
+ const rels = parsed.relationships.filter((r) => ids.includes(r.source));
10623
+ return { compIds: ids, compLayout: layoutComponent(tables, rels, dimMap) };
10624
+ });
10625
+ const packed = packComponents(componentItems);
10626
+ const placementByTableId = /* @__PURE__ */ new Map();
10627
+ for (const p of packed) {
10628
+ for (const id of p.compIds) placementByTableId.set(id, p);
10629
+ }
10630
+ const placementByRelLine = /* @__PURE__ */ new Map();
10631
+ for (const p of packed) {
10632
+ for (const lineNum of p.compLayout.edgePoints.keys()) {
10633
+ placementByRelLine.set(lineNum, p);
10634
+ }
10533
10635
  }
10534
- dagre3.layout(g);
10535
10636
  const layoutNodes = parsed.tables.map((table) => {
10536
- const pos = g.node(table.id);
10537
- const dims = dimMap.get(table.id);
10637
+ const p = placementByTableId.get(table.id);
10638
+ const pos = p.compLayout.nodePositions.get(table.id);
10538
10639
  return {
10539
10640
  ...table,
10540
- x: pos.x,
10541
- y: pos.y,
10542
- width: dims.width,
10543
- height: dims.height,
10544
- headerHeight: dims.headerHeight,
10545
- columnsHeight: dims.columnsHeight
10641
+ x: pos.x + p.offsetX + HALF_MARGIN,
10642
+ y: pos.y + p.offsetY + HALF_MARGIN,
10643
+ width: pos.width,
10644
+ height: pos.height,
10645
+ headerHeight: pos.headerHeight,
10646
+ columnsHeight: pos.columnsHeight
10546
10647
  };
10547
10648
  });
10548
10649
  const layoutEdges = parsed.relationships.map((rel) => {
10549
- const edgeData = g.edge(rel.source, rel.target);
10650
+ const p = placementByRelLine.get(rel.lineNumber);
10651
+ const pts = p?.compLayout.edgePoints.get(rel.lineNumber) ?? [];
10550
10652
  return {
10551
10653
  source: rel.source,
10552
10654
  target: rel.target,
10553
10655
  cardinality: rel.cardinality,
10554
- points: edgeData?.points ?? [],
10656
+ points: pts.map((pt) => ({
10657
+ x: pt.x + (p?.offsetX ?? 0) + HALF_MARGIN,
10658
+ y: pt.y + (p?.offsetY ?? 0) + HALF_MARGIN
10659
+ })),
10555
10660
  label: rel.label,
10556
10661
  lineNumber: rel.lineNumber
10557
10662
  };
10558
10663
  });
10559
- let minX = Infinity;
10560
- let minY = Infinity;
10561
10664
  let maxX = 0;
10562
10665
  let maxY = 0;
10563
10666
  for (const node of layoutNodes) {
10564
- const left = node.x - node.width / 2;
10565
- const right = node.x + node.width / 2;
10566
- const top = node.y - node.height / 2;
10567
- const bottom = node.y + node.height / 2;
10568
- if (left < minX) minX = left;
10569
- if (right > maxX) maxX = right;
10570
- if (top < minY) minY = top;
10571
- if (bottom > maxY) maxY = bottom;
10667
+ maxX = Math.max(maxX, node.x + node.width / 2);
10668
+ maxY = Math.max(maxY, node.y + node.height / 2);
10572
10669
  }
10573
10670
  for (const edge of layoutEdges) {
10574
10671
  for (const pt of edge.points) {
10575
- if (pt.x < minX) minX = pt.x;
10576
- if (pt.x > maxX) maxX = pt.x;
10577
- if (pt.y < minY) minY = pt.y;
10578
- if (pt.y > maxY) maxY = pt.y;
10579
- }
10580
- if (edge.label && edge.points.length > 0) {
10581
- const midPt = edge.points[Math.floor(edge.points.length / 2)];
10582
- const labelHalfW = (edge.label.length * 7 + 8) / 2;
10583
- if (midPt.x + labelHalfW > maxX) maxX = midPt.x + labelHalfW;
10584
- if (midPt.x - labelHalfW < minX) minX = midPt.x - labelHalfW;
10672
+ maxX = Math.max(maxX, pt.x);
10673
+ maxY = Math.max(maxY, pt.y);
10585
10674
  }
10586
10675
  }
10587
- const EDGE_MARGIN2 = 60;
10588
- const HALF_MARGIN = EDGE_MARGIN2 / 2;
10589
- const shiftX = -minX + HALF_MARGIN;
10590
- const shiftY = -minY + HALF_MARGIN;
10591
- for (const node of layoutNodes) {
10592
- node.x += shiftX;
10593
- node.y += shiftY;
10594
- }
10595
- for (const edge of layoutEdges) {
10596
- for (const pt of edge.points) {
10597
- pt.x += shiftX;
10598
- pt.y += shiftY;
10599
- }
10600
- }
10601
- maxX += shiftX;
10602
- maxY += shiftY;
10603
- const totalWidth = maxX + HALF_MARGIN;
10604
- const totalHeight = maxY + HALF_MARGIN;
10605
10676
  return {
10606
10677
  nodes: layoutNodes,
10607
10678
  edges: layoutEdges,
10608
- width: totalWidth,
10609
- height: totalHeight
10679
+ width: maxX + HALF_MARGIN,
10680
+ height: maxY + HALF_MARGIN
10610
10681
  };
10611
10682
  }
10612
- var MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2;
10683
+ var MIN_WIDTH2, CHAR_WIDTH4, PADDING_X2, HEADER_BASE2, MEMBER_LINE_HEIGHT3, COMPARTMENT_PADDING_Y3, SEPARATOR_HEIGHT2, HALF_MARGIN, COMP_GAP;
10613
10684
  var init_layout4 = __esm({
10614
10685
  "src/er/layout.ts"() {
10615
10686
  "use strict";
@@ -10620,6 +10691,135 @@ var init_layout4 = __esm({
10620
10691
  MEMBER_LINE_HEIGHT3 = 18;
10621
10692
  COMPARTMENT_PADDING_Y3 = 8;
10622
10693
  SEPARATOR_HEIGHT2 = 1;
10694
+ HALF_MARGIN = 30;
10695
+ COMP_GAP = 60;
10696
+ }
10697
+ });
10698
+
10699
+ // src/er/classify.ts
10700
+ function classifyEREntities(tables, relationships) {
10701
+ const result = /* @__PURE__ */ new Map();
10702
+ if (tables.length === 0) return result;
10703
+ const indegreeMap = {};
10704
+ for (const t of tables) indegreeMap[t.id] = 0;
10705
+ for (const rel of relationships) {
10706
+ if (rel.source === rel.target) continue;
10707
+ if (rel.cardinality.from === "1" && rel.cardinality.to !== "1") {
10708
+ indegreeMap[rel.source] = (indegreeMap[rel.source] ?? 0) + 1;
10709
+ }
10710
+ if (rel.cardinality.to === "1" && rel.cardinality.from !== "1") {
10711
+ indegreeMap[rel.target] = (indegreeMap[rel.target] ?? 0) + 1;
10712
+ }
10713
+ }
10714
+ const tableStarNeighbors = /* @__PURE__ */ new Map();
10715
+ for (const rel of relationships) {
10716
+ if (rel.source === rel.target) continue;
10717
+ if (rel.cardinality.from === "*") {
10718
+ if (!tableStarNeighbors.has(rel.source)) tableStarNeighbors.set(rel.source, /* @__PURE__ */ new Set());
10719
+ tableStarNeighbors.get(rel.source).add(rel.target);
10720
+ }
10721
+ if (rel.cardinality.to === "*") {
10722
+ if (!tableStarNeighbors.has(rel.target)) tableStarNeighbors.set(rel.target, /* @__PURE__ */ new Set());
10723
+ tableStarNeighbors.get(rel.target).add(rel.source);
10724
+ }
10725
+ }
10726
+ const mmParticipants = /* @__PURE__ */ new Set();
10727
+ for (const [id, neighbors] of tableStarNeighbors) {
10728
+ if (neighbors.size >= 2) mmParticipants.add(id);
10729
+ }
10730
+ const indegreeValues = Object.values(indegreeMap);
10731
+ const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
10732
+ const variance = indegreeValues.reduce((a, b) => a + (b - mean) ** 2, 0) / indegreeValues.length;
10733
+ const stddev = Math.sqrt(variance);
10734
+ const sorted = [...indegreeValues].sort((a, b) => a - b);
10735
+ const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)];
10736
+ for (const table of tables) {
10737
+ const id = table.id;
10738
+ const cols = table.columns;
10739
+ const fkCols = cols.filter((c) => c.constraints.includes("fk"));
10740
+ const pkFkCols = cols.filter(
10741
+ (c) => c.constraints.includes("pk") && c.constraints.includes("fk")
10742
+ );
10743
+ const fkCount = fkCols.length;
10744
+ const fkRatio = cols.length === 0 ? 0 : fkCount / cols.length;
10745
+ const indegree = indegreeMap[id] ?? 0;
10746
+ const nameLower = table.name.toLowerCase();
10747
+ const externalRels = relationships.filter(
10748
+ (r) => (r.source === id || r.target === id) && r.source !== r.target
10749
+ );
10750
+ const hasSelfRef = relationships.some((r) => r.source === id && r.target === id);
10751
+ const externalTargets = /* @__PURE__ */ new Set();
10752
+ for (const rel of externalRels) {
10753
+ externalTargets.add(rel.source === id ? rel.target : rel.source);
10754
+ }
10755
+ if (hasSelfRef && externalRels.length === 0) {
10756
+ result.set(id, "self-referential");
10757
+ continue;
10758
+ }
10759
+ const isInheritancePattern = pkFkCols.length >= 2 && externalTargets.size === 1;
10760
+ const junctionByRatio = fkRatio >= 0.6 && !isInheritancePattern;
10761
+ const junctionByCompositePk = pkFkCols.length >= 2 && externalTargets.size >= 2;
10762
+ const junctionByMm = mmParticipants.has(id);
10763
+ if (junctionByRatio || junctionByCompositePk || junctionByMm) {
10764
+ result.set(id, "junction");
10765
+ continue;
10766
+ }
10767
+ if (fkRatio >= 0.4 && fkRatio < 0.6 && pkFkCols.length < 2 && !mmParticipants.has(id)) {
10768
+ result.set(id, "ambiguous");
10769
+ continue;
10770
+ }
10771
+ const nameMatchesLookup = LOOKUP_NAME_SUFFIXES.some((s) => nameLower.endsWith(s));
10772
+ if (nameMatchesLookup && cols.length <= 6 && fkCount <= 1 && indegree > median) {
10773
+ result.set(id, "lookup");
10774
+ continue;
10775
+ }
10776
+ if (tables.length >= 6 && indegree > 0 && indegree > mean + 1.5 * stddev && indegree >= 2 * mean) {
10777
+ result.set(id, "hub");
10778
+ continue;
10779
+ }
10780
+ if (fkCount > 0) {
10781
+ result.set(id, "dependent");
10782
+ continue;
10783
+ }
10784
+ result.set(id, "core");
10785
+ }
10786
+ return result;
10787
+ }
10788
+ var ROLE_COLORS, ROLE_LABELS, ROLE_ORDER, LOOKUP_NAME_SUFFIXES;
10789
+ var init_classify = __esm({
10790
+ "src/er/classify.ts"() {
10791
+ "use strict";
10792
+ ROLE_COLORS = {
10793
+ core: "green",
10794
+ dependent: "blue",
10795
+ junction: "red",
10796
+ ambiguous: "purple",
10797
+ lookup: "yellow",
10798
+ hub: "orange",
10799
+ "self-referential": "teal",
10800
+ unclassified: "gray"
10801
+ };
10802
+ ROLE_LABELS = {
10803
+ core: "Core entity",
10804
+ dependent: "Dependent",
10805
+ junction: "Junction / M:M",
10806
+ ambiguous: "Bridge",
10807
+ lookup: "Lookup / Reference",
10808
+ hub: "Hub",
10809
+ "self-referential": "Self-referential",
10810
+ unclassified: "Unclassified"
10811
+ };
10812
+ ROLE_ORDER = [
10813
+ "core",
10814
+ "dependent",
10815
+ "junction",
10816
+ "ambiguous",
10817
+ "lookup",
10818
+ "hub",
10819
+ "self-referential",
10820
+ "unclassified"
10821
+ ];
10822
+ LOOKUP_NAME_SUFFIXES = ["_type", "_status", "_code", "_category"];
10623
10823
  }
10624
10824
  });
10625
10825
 
@@ -10691,25 +10891,41 @@ function drawCardinality(g, point, prevPoint, cardinality, color, useLabels) {
10691
10891
  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);
10692
10892
  }
10693
10893
  }
10694
- function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
10894
+ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup, semanticColorsActive) {
10695
10895
  d3Selection5.select(container).selectAll(":not([data-d3-tooltip])").remove();
10696
- const width = exportDims?.width ?? container.clientWidth;
10697
- const height = exportDims?.height ?? container.clientHeight;
10698
- if (width <= 0 || height <= 0) return;
10896
+ const useSemanticColors = parsed.tagGroups.length === 0 && layout.nodes.every((n) => !n.color);
10897
+ const legendReserveH = useSemanticColors ? LEGEND_HEIGHT + DIAGRAM_PADDING5 : 0;
10699
10898
  const titleHeight = parsed.title ? 40 : 0;
10700
10899
  const diagramW = layout.width;
10701
10900
  const diagramH = layout.height;
10702
- const availH = height - titleHeight;
10703
- const scaleX = (width - DIAGRAM_PADDING5 * 2) / diagramW;
10704
- const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10705
- const scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10706
- const scaledW = diagramW * scale;
10707
- const scaledH = diagramH * scale;
10708
- const offsetX = (width - scaledW) / 2;
10709
- const offsetY = titleHeight + DIAGRAM_PADDING5;
10710
- const svg = d3Selection5.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
10901
+ const naturalW = diagramW + DIAGRAM_PADDING5 * 2;
10902
+ const naturalH = diagramH + titleHeight + legendReserveH + DIAGRAM_PADDING5 * 2;
10903
+ let viewW;
10904
+ let viewH;
10905
+ let scale;
10906
+ let offsetX;
10907
+ let offsetY;
10908
+ if (exportDims) {
10909
+ viewW = exportDims.width ?? naturalW;
10910
+ viewH = exportDims.height ?? naturalH;
10911
+ const availH = viewH - titleHeight - legendReserveH;
10912
+ const scaleX = (viewW - DIAGRAM_PADDING5 * 2) / diagramW;
10913
+ const scaleY = (availH - DIAGRAM_PADDING5 * 2) / diagramH;
10914
+ scale = Math.min(MAX_SCALE4, scaleX, scaleY);
10915
+ const scaledW = diagramW * scale;
10916
+ offsetX = (viewW - scaledW) / 2;
10917
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10918
+ } else {
10919
+ viewW = naturalW;
10920
+ viewH = naturalH;
10921
+ scale = 1;
10922
+ offsetX = DIAGRAM_PADDING5;
10923
+ offsetY = titleHeight + DIAGRAM_PADDING5;
10924
+ }
10925
+ if (viewW <= 0 || viewH <= 0) return;
10926
+ 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);
10711
10927
  if (parsed.title) {
10712
- 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);
10928
+ 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);
10713
10929
  if (parsed.titleLineNumber) {
10714
10930
  titleEl.attr("data-line-number", parsed.titleLineNumber);
10715
10931
  if (onClickItem) {
@@ -10723,6 +10939,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10723
10939
  }
10724
10940
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
10725
10941
  const seriesColors2 = getSeriesColors(palette);
10942
+ const semanticRoles = useSemanticColors ? classifyEREntities(parsed.tables, parsed.relationships) : null;
10943
+ const semanticActive = semanticRoles !== null && (semanticColorsActive ?? true);
10726
10944
  const useLabels = parsed.options.notation === "labels";
10727
10945
  for (const edge of layout.edges) {
10728
10946
  if (edge.points.length < 2) continue;
@@ -10762,7 +10980,8 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10762
10980
  for (let ni = 0; ni < layout.nodes.length; ni++) {
10763
10981
  const node = layout.nodes[ni];
10764
10982
  const tagColor = resolveTagColor(node.metadata, parsed.tagGroups, activeTagGroup ?? null);
10765
- const nodeColor2 = node.color ?? tagColor ?? seriesColors2[ni % seriesColors2.length];
10983
+ const semanticColor = semanticActive ? palette.colors[ROLE_COLORS[semanticRoles.get(node.id) ?? "unclassified"]] : semanticRoles ? palette.primary : void 0;
10984
+ const nodeColor2 = node.color ?? tagColor ?? semanticColor ?? seriesColors2[ni % seriesColors2.length];
10766
10985
  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);
10767
10986
  if (activeTagGroup) {
10768
10987
  const tagKey = activeTagGroup.toLowerCase();
@@ -10771,6 +10990,10 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10771
10990
  nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
10772
10991
  }
10773
10992
  }
10993
+ if (semanticRoles) {
10994
+ const role = semanticRoles.get(node.id);
10995
+ if (role) nodeG.attr("data-er-role", role);
10996
+ }
10774
10997
  if (onClickItem) {
10775
10998
  nodeG.style("cursor", "pointer").on("click", () => {
10776
10999
  onClickItem(node.lineNumber);
@@ -10803,32 +11026,87 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10803
11026
  }
10804
11027
  }
10805
11028
  if (parsed.tagGroups.length > 0) {
10806
- const LEGEND_Y_PAD = 16;
10807
- const LEGEND_PILL_H = 22;
10808
- const LEGEND_PILL_RX = 11;
10809
- const LEGEND_PILL_PAD9 = 10;
11029
+ const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
11030
+ const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
10810
11031
  const LEGEND_GAP2 = 8;
10811
- const LEGEND_FONT_SIZE2 = 11;
10812
- const LEGEND_GROUP_GAP7 = 16;
10813
11032
  const legendG = svg.append("g").attr("class", "er-tag-legend");
11033
+ if (activeTagGroup) {
11034
+ legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
11035
+ }
10814
11036
  let legendX = DIAGRAM_PADDING5;
10815
- let legendY = height - DIAGRAM_PADDING5;
11037
+ let legendY = viewH - DIAGRAM_PADDING5;
10816
11038
  for (const group of parsed.tagGroups) {
10817
11039
  const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
10818
- 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}:`);
11040
+ 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}:`);
10819
11041
  const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
10820
11042
  legendX += labelWidth;
10821
11043
  for (const entry of group.entries) {
10822
11044
  const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
10823
- const tmpText = legendG.append("text").attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(entry.value);
11045
+ const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
10824
11046
  const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
10825
11047
  tmpText.remove();
10826
- const pillW = textW + LEGEND_PILL_PAD9 * 2;
11048
+ const pillW = textW + LEGEND_PILL_PAD * 2;
10827
11049
  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);
10828
- 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);
11050
+ 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);
10829
11051
  legendX += pillW + LEGEND_GAP2;
10830
11052
  }
10831
- legendX += LEGEND_GROUP_GAP7;
11053
+ legendX += LEGEND_GROUP_GAP;
11054
+ }
11055
+ }
11056
+ if (semanticRoles) {
11057
+ const presentRoles = ROLE_ORDER.filter((role) => {
11058
+ for (const r of semanticRoles.values()) {
11059
+ if (r === role) return true;
11060
+ }
11061
+ return false;
11062
+ });
11063
+ if (presentRoles.length > 0) {
11064
+ const measureLabelW = (text, fontSize) => {
11065
+ const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
11066
+ const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
11067
+ dummy.remove();
11068
+ return measured > 0 ? measured : text.length * fontSize * 0.6;
11069
+ };
11070
+ const labelWidths = /* @__PURE__ */ new Map();
11071
+ for (const role of presentRoles) {
11072
+ labelWidths.set(role, measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE));
11073
+ }
11074
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
11075
+ const groupName = "Role";
11076
+ const pillWidth = groupName.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
11077
+ const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
11078
+ let totalWidth;
11079
+ let entriesWidth = 0;
11080
+ if (semanticActive) {
11081
+ for (const role of presentRoles) {
11082
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11083
+ }
11084
+ totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + LEGEND_ENTRY_TRAIL + entriesWidth;
11085
+ } else {
11086
+ totalWidth = pillWidth;
11087
+ }
11088
+ const legendX = (viewW - totalWidth) / 2;
11089
+ const legendY = viewH - DIAGRAM_PADDING5 - LEGEND_HEIGHT;
11090
+ const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
11091
+ if (semanticActive) {
11092
+ semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11093
+ 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);
11094
+ 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);
11095
+ 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);
11096
+ let entryX = LEGEND_CAPSULE_PAD + pillWidth + LEGEND_ENTRY_TRAIL;
11097
+ for (const role of presentRoles) {
11098
+ const label = ROLE_LABELS[role];
11099
+ const roleColor = palette.colors[ROLE_COLORS[role]];
11100
+ const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
11101
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
11102
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
11103
+ 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);
11104
+ entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
11105
+ }
11106
+ } else {
11107
+ semanticLegendG.append("rect").attr("width", pillWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
11108
+ 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);
11109
+ }
10832
11110
  }
10833
11111
  }
10834
11112
  }
@@ -10875,8 +11153,10 @@ var init_renderer5 = __esm({
10875
11153
  init_color_utils();
10876
11154
  init_palettes();
10877
11155
  init_tag_groups();
11156
+ init_legend_constants();
10878
11157
  init_parser3();
10879
11158
  init_layout4();
11159
+ init_classify();
10880
11160
  DIAGRAM_PADDING5 = 20;
10881
11161
  MAX_SCALE4 = 3;
10882
11162
  TABLE_FONT_SIZE = 13;
@@ -11005,9 +11285,10 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11005
11285
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
11006
11286
  const dagrePoints = dagreEdge?.points ?? [];
11007
11287
  const hasIntermediateRank = allNodeX.some((x) => x > src.x + 20 && x < tgt.x - 20);
11008
- const step = Math.min((enterX - exitX) * 0.15, 20);
11288
+ const step = Math.max(0, Math.min((enterX - exitX) * 0.15, 20));
11009
11289
  const isBackEdge = tgt.x < src.x - 5;
11010
- const isYDisplaced = !isBackEdge && Math.abs(tgt.y - src.y) > NODESEP;
11290
+ const isTopExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y < src.y - NODESEP;
11291
+ const isBottomExit = !isBackEdge && tgt.x > src.x && !hasIntermediateRank && tgt.y > src.y + NODESEP;
11011
11292
  let points;
11012
11293
  if (isBackEdge) {
11013
11294
  const routeAbove = Math.min(src.y, tgt.y) > avgNodeY;
@@ -11017,28 +11298,43 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11017
11298
  const spreadDir = avgNodeX < rawMidX ? 1 : -1;
11018
11299
  const unclamped = Math.abs(src.x - tgt.x) < NODE_WIDTH ? rawMidX + spreadDir * BACK_EDGE_MIN_SPREAD : rawMidX;
11019
11300
  const midX = Math.min(src.x, Math.max(tgt.x, unclamped));
11301
+ const srcDepart = Math.max(midX + 1, src.x - TOP_EXIT_STEP);
11302
+ const tgtApproach = Math.min(midX - 1, tgt.x + TOP_EXIT_STEP);
11020
11303
  if (routeAbove) {
11021
11304
  const arcY = Math.min(src.y - srcHalfH, tgt.y - tgtHalfH) - BACK_EDGE_MARGIN;
11022
11305
  points = [
11023
11306
  { x: src.x, y: src.y - srcHalfH },
11307
+ { x: srcDepart, y: src.y - srcHalfH - TOP_EXIT_STEP },
11024
11308
  { x: midX, y: arcY },
11309
+ { x: tgtApproach, y: tgt.y - tgtHalfH - TOP_EXIT_STEP },
11025
11310
  { x: tgt.x, y: tgt.y - tgtHalfH }
11026
11311
  ];
11027
11312
  } else {
11028
11313
  const arcY = Math.max(src.y + srcHalfH, tgt.y + tgtHalfH) + BACK_EDGE_MARGIN;
11029
11314
  points = [
11030
11315
  { x: src.x, y: src.y + srcHalfH },
11316
+ { x: srcDepart, y: src.y + srcHalfH + TOP_EXIT_STEP },
11031
11317
  { x: midX, y: arcY },
11318
+ { x: tgtApproach, y: tgt.y + tgtHalfH + TOP_EXIT_STEP },
11032
11319
  { x: tgt.x, y: tgt.y + tgtHalfH }
11033
11320
  ];
11034
11321
  }
11035
- } else if (isYDisplaced) {
11036
- const exitY = tgt.y > src.y + NODESEP ? src.y + src.height / 2 : src.y - src.height / 2;
11037
- const midX = Math.max(src.x + 1, (src.x + enterX) / 2);
11038
- const midY = (exitY + tgt.y) / 2;
11322
+ } else if (isTopExit) {
11323
+ const exitY = src.y - src.height / 2;
11324
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11039
11325
  points = [
11040
11326
  { x: src.x, y: exitY },
11041
- { x: midX, y: midY },
11327
+ { x: p1x, y: exitY - TOP_EXIT_STEP },
11328
+ { x: enterX - step, y: tgt.y + yOffset },
11329
+ { x: enterX, y: tgt.y }
11330
+ ];
11331
+ } else if (isBottomExit) {
11332
+ const exitY = src.y + src.height / 2;
11333
+ const p1x = Math.min(Math.max(src.x, src.x + yOffset + TOP_EXIT_STEP), (src.x + enterX) / 2 - 1);
11334
+ points = [
11335
+ { x: src.x, y: exitY },
11336
+ { x: p1x, y: exitY + TOP_EXIT_STEP },
11337
+ { x: enterX - step, y: tgt.y + yOffset },
11042
11338
  { x: enterX, y: tgt.y }
11043
11339
  ];
11044
11340
  } else if (tgt.x > src.x && !hasIntermediateRank) {
@@ -11136,7 +11432,7 @@ function layoutInitiativeStatus(parsed, collapseResult) {
11136
11432
  totalHeight += 40;
11137
11433
  return { nodes: layoutNodes, edges: layoutEdges, groups: layoutGroups, width: totalWidth, height: totalHeight };
11138
11434
  }
11139
- var 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;
11435
+ var 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;
11140
11436
  var init_layout5 = __esm({
11141
11437
  "src/initiative-status/layout.ts"() {
11142
11438
  "use strict";
@@ -11152,6 +11448,7 @@ var init_layout5 = __esm({
11152
11448
  MAX_PARALLEL_EDGES = 5;
11153
11449
  BACK_EDGE_MARGIN = 40;
11154
11450
  BACK_EDGE_MIN_SPREAD = Math.round(NODE_WIDTH * 0.75);
11451
+ TOP_EXIT_STEP = 10;
11155
11452
  CHAR_WIDTH_RATIO = 0.6;
11156
11453
  NODE_FONT_SIZE = 13;
11157
11454
  NODE_TEXT_PADDING = 12;
@@ -11651,7 +11948,7 @@ __export(layout_exports6, {
11651
11948
  rollUpContextRelationships: () => rollUpContextRelationships
11652
11949
  });
11653
11950
  import dagre5 from "@dagrejs/dagre";
11654
- function computeEdgePenalty(edgeList, nodePositions, degrees) {
11951
+ function computeEdgePenalty(edgeList, nodePositions, degrees, nodeGeometry) {
11655
11952
  let penalty = 0;
11656
11953
  for (const edge of edgeList) {
11657
11954
  const sx = nodePositions.get(edge.source);
@@ -11661,6 +11958,32 @@ function computeEdgePenalty(edgeList, nodePositions, degrees) {
11661
11958
  const weight = Math.min(degrees.get(edge.source) ?? 1, degrees.get(edge.target) ?? 1);
11662
11959
  penalty += dist * weight;
11663
11960
  }
11961
+ if (nodeGeometry) {
11962
+ for (const edge of edgeList) {
11963
+ const geomA = nodeGeometry.get(edge.source);
11964
+ const geomB = nodeGeometry.get(edge.target);
11965
+ if (!geomA || !geomB) continue;
11966
+ const ax = nodePositions.get(edge.source) ?? 0;
11967
+ const bx = nodePositions.get(edge.target) ?? 0;
11968
+ const ay = geomA.y;
11969
+ const by = geomB.y;
11970
+ if (ay === by) continue;
11971
+ const edgeMinX = Math.min(ax, bx);
11972
+ const edgeMaxX = Math.max(ax, bx);
11973
+ const edgeMinY = Math.min(ay, by);
11974
+ const edgeMaxY = Math.max(ay, by);
11975
+ for (const [name, geomC] of nodeGeometry) {
11976
+ if (name === edge.source || name === edge.target) continue;
11977
+ const cx = nodePositions.get(name) ?? 0;
11978
+ const cy = geomC.y;
11979
+ const hw = geomC.width / 2;
11980
+ const hh = geomC.height / 2;
11981
+ if (cx + hw > edgeMinX && cx - hw < edgeMaxX && cy + hh > edgeMinY && cy - hh < edgeMaxY) {
11982
+ penalty += EDGE_NODE_COLLISION_WEIGHT;
11983
+ }
11984
+ }
11985
+ }
11986
+ }
11664
11987
  return penalty;
11665
11988
  }
11666
11989
  function reduceCrossings(g, edgeList, nodeGroupMap) {
@@ -11670,6 +11993,11 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11670
11993
  degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
11671
11994
  degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
11672
11995
  }
11996
+ const nodeGeometry = /* @__PURE__ */ new Map();
11997
+ for (const name of g.nodes()) {
11998
+ const pos = g.node(name);
11999
+ if (pos) nodeGeometry.set(name, { y: pos.y, width: pos.width, height: pos.height });
12000
+ }
11673
12001
  const rankMap = /* @__PURE__ */ new Map();
11674
12002
  for (const name of g.nodes()) {
11675
12003
  const pos = g.node(name);
@@ -11712,7 +12040,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11712
12040
  const pos = g.node(name);
11713
12041
  if (pos) basePositions.set(name, pos.x);
11714
12042
  }
11715
- const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees);
12043
+ const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees, nodeGeometry);
11716
12044
  let bestPerm = [...partition];
11717
12045
  let bestPenalty = currentPenalty;
11718
12046
  if (partition.length <= 8) {
@@ -11722,7 +12050,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11722
12050
  for (let i = 0; i < perm.length; i++) {
11723
12051
  testPositions.set(perm[i], xSlots[i]);
11724
12052
  }
11725
- const penalty = computeEdgePenalty(edgeList, testPositions, degrees);
12053
+ const penalty = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11726
12054
  if (penalty < bestPenalty) {
11727
12055
  bestPenalty = penalty;
11728
12056
  bestPerm = [...perm];
@@ -11740,13 +12068,13 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
11740
12068
  for (let k = 0; k < workingOrder.length; k++) {
11741
12069
  testPositions.set(workingOrder[k], xSlots[k]);
11742
12070
  }
11743
- const before = computeEdgePenalty(edgeList, testPositions, degrees);
12071
+ const before = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
11744
12072
  [workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1], workingOrder[i]];
11745
12073
  const testPositions2 = new Map(basePositions);
11746
12074
  for (let k = 0; k < workingOrder.length; k++) {
11747
12075
  testPositions2.set(workingOrder[k], xSlots[k]);
11748
12076
  }
11749
- const after = computeEdgePenalty(edgeList, testPositions2, degrees);
12077
+ const after = computeEdgePenalty(edgeList, testPositions2, degrees, nodeGeometry);
11750
12078
  if (after < before) {
11751
12079
  improved = true;
11752
12080
  if (after < bestPenalty) {
@@ -11974,31 +12302,27 @@ function computeC4NodeDimensions(el, options) {
11974
12302
  height += CARD_V_PAD3;
11975
12303
  return { width, height };
11976
12304
  }
11977
- function computeLegendGroups3(tagGroups, usedValuesByGroup) {
12305
+ function computeLegendGroups3(tagGroups) {
11978
12306
  const result = [];
11979
12307
  for (const group of tagGroups) {
11980
12308
  const entries = [];
11981
12309
  for (const entry of group.entries) {
11982
- if (usedValuesByGroup) {
11983
- const used = usedValuesByGroup.get(group.name.toLowerCase());
11984
- if (!used?.has(entry.value.toLowerCase())) continue;
11985
- }
11986
12310
  entries.push({ value: entry.value, color: entry.color });
11987
12311
  }
11988
12312
  if (entries.length === 0) continue;
11989
- const nameW = group.name.length * LEGEND_PILL_FONT_W5 + LEGEND_PILL_PAD5 * 2;
11990
- let capsuleW = LEGEND_CAPSULE_PAD5;
12313
+ const nameW = group.name.length * LEGEND_PILL_FONT_W4 + LEGEND_PILL_PAD4 * 2;
12314
+ let capsuleW = LEGEND_CAPSULE_PAD4;
11991
12315
  for (const e of entries) {
11992
- capsuleW += LEGEND_DOT_R6 * 2 + LEGEND_ENTRY_DOT_GAP5 + e.value.length * LEGEND_ENTRY_FONT_W5 + LEGEND_ENTRY_TRAIL5;
12316
+ capsuleW += LEGEND_DOT_R4 * 2 + LEGEND_ENTRY_DOT_GAP4 + e.value.length * LEGEND_ENTRY_FONT_W5 + LEGEND_ENTRY_TRAIL4;
11993
12317
  }
11994
- capsuleW += LEGEND_CAPSULE_PAD5;
12318
+ capsuleW += LEGEND_CAPSULE_PAD4;
11995
12319
  result.push({
11996
12320
  name: group.name,
11997
12321
  entries,
11998
12322
  x: 0,
11999
12323
  y: 0,
12000
12324
  width: nameW + capsuleW,
12001
- height: LEGEND_HEIGHT6
12325
+ height: LEGEND_HEIGHT4
12002
12326
  });
12003
12327
  }
12004
12328
  return result;
@@ -12118,18 +12442,7 @@ function layoutC4Context(parsed, activeTagGroup) {
12118
12442
  }
12119
12443
  let totalWidth = nodes.length > 0 ? maxX - minX + MARGIN3 * 2 : 0;
12120
12444
  let totalHeight = nodes.length > 0 ? maxY - minY + MARGIN3 * 2 : 0;
12121
- const usedValuesByGroup = /* @__PURE__ */ new Map();
12122
- for (const el of contextElements) {
12123
- for (const group of parsed.tagGroups) {
12124
- const key = group.name.toLowerCase();
12125
- const val = el.metadata[key];
12126
- if (val) {
12127
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
12128
- usedValuesByGroup.get(key).add(val.toLowerCase());
12129
- }
12130
- }
12131
- }
12132
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
12445
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
12133
12446
  if (legendGroups.length > 0) {
12134
12447
  const legendY = totalHeight + MARGIN3;
12135
12448
  let legendX = MARGIN3;
@@ -12139,7 +12452,7 @@ function layoutC4Context(parsed, activeTagGroup) {
12139
12452
  legendX += lg.width + 12;
12140
12453
  }
12141
12454
  const legendRight = legendX;
12142
- const legendBottom = legendY + LEGEND_HEIGHT6;
12455
+ const legendBottom = legendY + LEGEND_HEIGHT4;
12143
12456
  if (legendRight > totalWidth) totalWidth = legendRight;
12144
12457
  if (legendBottom > totalHeight) totalHeight = legendBottom;
12145
12458
  }
@@ -12445,18 +12758,7 @@ function layoutC4Containers(parsed, systemName, activeTagGroup) {
12445
12758
  }
12446
12759
  let totalWidth = maxX - minX + MARGIN3 * 2;
12447
12760
  let totalHeight = maxY - minY + MARGIN3 * 2;
12448
- const usedValuesByGroup = /* @__PURE__ */ new Map();
12449
- for (const el of [...containers, ...externals]) {
12450
- for (const group of parsed.tagGroups) {
12451
- const key = group.name.toLowerCase();
12452
- const val = el.metadata[key];
12453
- if (val) {
12454
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
12455
- usedValuesByGroup.get(key).add(val.toLowerCase());
12456
- }
12457
- }
12458
- }
12459
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
12761
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
12460
12762
  if (legendGroups.length > 0) {
12461
12763
  const legendY = totalHeight + MARGIN3;
12462
12764
  let legendX = MARGIN3;
@@ -12466,7 +12768,7 @@ function layoutC4Containers(parsed, systemName, activeTagGroup) {
12466
12768
  legendX += lg.width + 12;
12467
12769
  }
12468
12770
  const legendRight = legendX;
12469
- const legendBottom = legendY + LEGEND_HEIGHT6;
12771
+ const legendBottom = legendY + LEGEND_HEIGHT4;
12470
12772
  if (legendRight > totalWidth) totalWidth = legendRight;
12471
12773
  if (legendBottom > totalHeight) totalHeight = legendBottom;
12472
12774
  }
@@ -12821,21 +13123,7 @@ function layoutC4Components(parsed, systemName, containerName, activeTagGroup) {
12821
13123
  }
12822
13124
  let totalWidth = maxX - minX + MARGIN3 * 2;
12823
13125
  let totalHeight = maxY - minY + MARGIN3 * 2;
12824
- const usedValuesByGroup = /* @__PURE__ */ new Map();
12825
- for (const el of [...components, ...externals]) {
12826
- for (const group of parsed.tagGroups) {
12827
- const key = group.name.toLowerCase();
12828
- let val = el.metadata[key];
12829
- if (!val && components.includes(el)) {
12830
- val = targetContainer.metadata[key] ?? system.metadata[key];
12831
- }
12832
- if (val) {
12833
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
12834
- usedValuesByGroup.get(key).add(val.toLowerCase());
12835
- }
12836
- }
12837
- }
12838
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
13126
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
12839
13127
  if (legendGroups.length > 0) {
12840
13128
  const legendY = totalHeight + MARGIN3;
12841
13129
  let legendX = MARGIN3;
@@ -12845,7 +13133,7 @@ function layoutC4Components(parsed, systemName, containerName, activeTagGroup) {
12845
13133
  legendX += lg.width + 12;
12846
13134
  }
12847
13135
  const legendRight = legendX;
12848
- const legendBottom = legendY + LEGEND_HEIGHT6;
13136
+ const legendBottom = legendY + LEGEND_HEIGHT4;
12849
13137
  if (legendRight > totalWidth) totalWidth = legendRight;
12850
13138
  if (legendBottom > totalHeight) totalHeight = legendBottom;
12851
13139
  }
@@ -13090,18 +13378,7 @@ function layoutC4Deployment(parsed, activeTagGroup) {
13090
13378
  }
13091
13379
  let totalWidth = maxX - minX + MARGIN3 * 2;
13092
13380
  let totalHeight = maxY - minY + MARGIN3 * 2;
13093
- const usedValuesByGroup = /* @__PURE__ */ new Map();
13094
- for (const r of refEntries) {
13095
- for (const group of parsed.tagGroups) {
13096
- const key = group.name.toLowerCase();
13097
- const val = r.element.metadata[key];
13098
- if (val) {
13099
- if (!usedValuesByGroup.has(key)) usedValuesByGroup.set(key, /* @__PURE__ */ new Set());
13100
- usedValuesByGroup.get(key).add(val.toLowerCase());
13101
- }
13102
- }
13103
- }
13104
- const legendGroups = computeLegendGroups3(parsed.tagGroups, usedValuesByGroup);
13381
+ const legendGroups = computeLegendGroups3(parsed.tagGroups);
13105
13382
  if (legendGroups.length > 0) {
13106
13383
  const legendY = totalHeight + MARGIN3;
13107
13384
  let legendX = MARGIN3;
@@ -13111,13 +13388,13 @@ function layoutC4Deployment(parsed, activeTagGroup) {
13111
13388
  legendX += lg.width + 12;
13112
13389
  }
13113
13390
  const legendRight = legendX;
13114
- const legendBottom = legendY + LEGEND_HEIGHT6;
13391
+ const legendBottom = legendY + LEGEND_HEIGHT4;
13115
13392
  if (legendRight > totalWidth) totalWidth = legendRight;
13116
13393
  if (legendBottom > totalHeight) totalHeight = legendBottom;
13117
13394
  }
13118
13395
  return { nodes, edges, legend: legendGroups, groupBoundaries, width: totalWidth, height: totalHeight };
13119
13396
  }
13120
- var 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;
13397
+ var 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;
13121
13398
  var init_layout6 = __esm({
13122
13399
  "src/c4/layout.ts"() {
13123
13400
  "use strict";
@@ -13136,16 +13413,17 @@ var init_layout6 = __esm({
13136
13413
  MARGIN3 = 40;
13137
13414
  BOUNDARY_PAD = 40;
13138
13415
  GROUP_BOUNDARY_PAD = 24;
13139
- LEGEND_HEIGHT6 = 28;
13140
- LEGEND_PILL_FONT_SIZE3 = 11;
13141
- LEGEND_PILL_FONT_W5 = LEGEND_PILL_FONT_SIZE3 * 0.6;
13142
- LEGEND_PILL_PAD5 = 16;
13143
- LEGEND_DOT_R6 = 4;
13144
- LEGEND_ENTRY_FONT_SIZE4 = 10;
13145
- LEGEND_ENTRY_FONT_W5 = LEGEND_ENTRY_FONT_SIZE4 * 0.6;
13146
- LEGEND_ENTRY_DOT_GAP5 = 4;
13147
- LEGEND_ENTRY_TRAIL5 = 8;
13148
- LEGEND_CAPSULE_PAD5 = 4;
13416
+ LEGEND_HEIGHT4 = 28;
13417
+ LEGEND_PILL_FONT_SIZE2 = 11;
13418
+ LEGEND_PILL_FONT_W4 = LEGEND_PILL_FONT_SIZE2 * 0.6;
13419
+ LEGEND_PILL_PAD4 = 16;
13420
+ LEGEND_DOT_R4 = 4;
13421
+ LEGEND_ENTRY_FONT_SIZE2 = 10;
13422
+ LEGEND_ENTRY_FONT_W5 = LEGEND_ENTRY_FONT_SIZE2 * 0.6;
13423
+ LEGEND_ENTRY_DOT_GAP4 = 4;
13424
+ LEGEND_ENTRY_TRAIL4 = 8;
13425
+ LEGEND_CAPSULE_PAD4 = 4;
13426
+ EDGE_NODE_COLLISION_WEIGHT = 5e3;
13149
13427
  META_EXCLUDE_KEYS = /* @__PURE__ */ new Set(["description", "tech", "technology", "is a"]);
13150
13428
  }
13151
13429
  });
@@ -13223,8 +13501,14 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
13223
13501
  if (width <= 0 || height <= 0) return;
13224
13502
  const titleHeight = parsed.title ? TITLE_HEIGHT4 + 10 : 0;
13225
13503
  const diagramW = layout.width;
13226
- const diagramH = layout.height;
13227
- const availH = height - titleHeight;
13504
+ const hasLegend = layout.legend.length > 0;
13505
+ const C4_LAYOUT_MARGIN = 40;
13506
+ const LEGEND_FIXED_GAP4 = 8;
13507
+ const fixedLegend = !exportDims && hasLegend;
13508
+ const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
13509
+ const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP4 : 0;
13510
+ const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
13511
+ const availH = height - titleHeight - legendReserveH;
13228
13512
  const scaleX = (width - DIAGRAM_PADDING7 * 2) / diagramW;
13229
13513
  const scaleY = (availH - DIAGRAM_PADDING7 * 2) / diagramH;
13230
13514
  const scale = Math.min(MAX_SCALE6, scaleX, scaleY);
@@ -13296,6 +13580,20 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
13296
13580
  }
13297
13581
  for (const node of layout.nodes) {
13298
13582
  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);
13583
+ if (activeTagGroup) {
13584
+ const tagKey = activeTagGroup.toLowerCase();
13585
+ const tagValue = node.metadata[tagKey];
13586
+ if (tagValue) {
13587
+ nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
13588
+ } else {
13589
+ const tagGroup = parsed.tagGroups.find(
13590
+ (g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
13591
+ );
13592
+ if (tagGroup?.defaultValue) {
13593
+ nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
13594
+ }
13595
+ }
13596
+ }
13299
13597
  if (node.importPath) {
13300
13598
  nodeG.attr("data-import-path", node.importPath);
13301
13599
  }
@@ -13348,36 +13646,12 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
13348
13646
  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");
13349
13647
  }
13350
13648
  }
13351
- if (!exportDims) {
13352
- for (const group of layout.legend) {
13353
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
13354
- if (activeTagGroup != null && !isActive) continue;
13355
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13356
- const pillLabel = group.name;
13357
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W6 + LEGEND_PILL_PAD6;
13358
- 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");
13359
- if (isActive) {
13360
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT7).attr("rx", LEGEND_HEIGHT7 / 2).attr("fill", groupBg);
13361
- }
13362
- const pillX = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13363
- const pillY = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13364
- const pillH = LEGEND_HEIGHT7 - (isActive ? LEGEND_CAPSULE_PAD6 * 2 : 0);
13365
- 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);
13366
- if (isActive) {
13367
- 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);
13368
- }
13369
- 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);
13370
- if (isActive) {
13371
- let entryX = pillX + pillWidth + 4;
13372
- for (const entry of group.entries) {
13373
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13374
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R7).attr("cy", LEGEND_HEIGHT7 / 2).attr("r", LEGEND_DOT_R7).attr("fill", entry.color);
13375
- const textX = entryX + LEGEND_DOT_R7 * 2 + LEGEND_ENTRY_DOT_GAP6;
13376
- 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);
13377
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W6 + LEGEND_ENTRY_TRAIL6;
13378
- }
13379
- }
13649
+ if (hasLegend) {
13650
+ 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");
13651
+ if (activeTagGroup) {
13652
+ legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
13380
13653
  }
13654
+ renderLegend2(legendParent, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
13381
13655
  }
13382
13656
  }
13383
13657
  function renderC4ContextForExport(content, theme, palette) {
@@ -13665,33 +13939,47 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
13665
13939
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
13666
13940
  }
13667
13941
  }
13668
- function renderLegend2(contentG, layout, palette, isDark, activeTagGroup) {
13669
- for (const group of layout.legend) {
13942
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
13943
+ const visibleGroups = activeTagGroup != null ? layout.legend.filter((g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()) : layout.legend;
13944
+ const pillWidthOf = (g) => g.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
13945
+ const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
13946
+ let fixedPositions = null;
13947
+ if (fixedWidth != null && visibleGroups.length > 0) {
13948
+ fixedPositions = /* @__PURE__ */ new Map();
13949
+ const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13950
+ let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
13951
+ for (const g of visibleGroups) {
13952
+ fixedPositions.set(g.name, cx);
13953
+ cx += effectiveW(g) + LEGEND_GROUP_GAP;
13954
+ }
13955
+ }
13956
+ for (const group of visibleGroups) {
13670
13957
  const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
13671
- if (activeTagGroup != null && !isActive) continue;
13672
13958
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13673
13959
  const pillLabel = group.name;
13674
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W6 + LEGEND_PILL_PAD6;
13675
- 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");
13960
+ const pillWidth = pillWidthOf(group);
13961
+ const gX = fixedPositions?.get(group.name) ?? group.x;
13962
+ const gY = fixedPositions != null ? 0 : group.y;
13963
+ 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");
13676
13964
  if (isActive) {
13677
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT7).attr("rx", LEGEND_HEIGHT7 / 2).attr("fill", groupBg);
13965
+ gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13678
13966
  }
13679
- const pillX = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13680
- const pillY = isActive ? LEGEND_CAPSULE_PAD6 : 0;
13681
- const pillH = LEGEND_HEIGHT7 - (isActive ? LEGEND_CAPSULE_PAD6 * 2 : 0);
13967
+ const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
13968
+ const pillY = isActive ? LEGEND_CAPSULE_PAD : 0;
13969
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
13682
13970
  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);
13683
13971
  if (isActive) {
13684
13972
  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);
13685
13973
  }
13686
- 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);
13974
+ 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);
13687
13975
  if (isActive) {
13688
13976
  let entryX = pillX + pillWidth + 4;
13689
13977
  for (const entry of group.entries) {
13690
13978
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13691
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R7).attr("cy", LEGEND_HEIGHT7 / 2).attr("r", LEGEND_DOT_R7).attr("fill", entry.color);
13692
- const textX = entryX + LEGEND_DOT_R7 * 2 + LEGEND_ENTRY_DOT_GAP6;
13693
- 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);
13694
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W6 + LEGEND_ENTRY_TRAIL6;
13979
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13980
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
13981
+ 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);
13982
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
13695
13983
  }
13696
13984
  }
13697
13985
  }
@@ -13703,8 +13991,14 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
13703
13991
  if (width <= 0 || height <= 0) return;
13704
13992
  const titleHeight = parsed.title ? TITLE_HEIGHT4 + 10 : 0;
13705
13993
  const diagramW = layout.width;
13706
- const diagramH = layout.height;
13707
- const availH = height - titleHeight;
13994
+ const hasLegend = layout.legend.length > 0;
13995
+ const C4_LAYOUT_MARGIN = 40;
13996
+ const LEGEND_FIXED_GAP4 = 8;
13997
+ const fixedLegend = !exportDims && hasLegend;
13998
+ const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
13999
+ const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP4 : 0;
14000
+ const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
14001
+ const availH = height - titleHeight - legendReserveH;
13708
14002
  const scaleX = (width - DIAGRAM_PADDING7 * 2) / diagramW;
13709
14003
  const scaleY = (availH - DIAGRAM_PADDING7 * 2) / diagramH;
13710
14004
  const scale = Math.min(MAX_SCALE6, scaleX, scaleY);
@@ -13775,6 +14069,20 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
13775
14069
  renderEdges(contentG, layout.edges, palette, onClickItem, boundaryLabelObstacles);
13776
14070
  for (const node of layout.nodes) {
13777
14071
  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);
14072
+ if (activeTagGroup) {
14073
+ const tagKey = activeTagGroup.toLowerCase();
14074
+ const tagValue = node.metadata[tagKey];
14075
+ if (tagValue) {
14076
+ nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
14077
+ } else {
14078
+ const tagGroup = parsed.tagGroups.find(
14079
+ (g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
14080
+ );
14081
+ if (tagGroup?.defaultValue) {
14082
+ nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
14083
+ }
14084
+ }
14085
+ }
13778
14086
  if (node.shape) {
13779
14087
  nodeG.attr("data-shape", node.shape);
13780
14088
  }
@@ -13860,8 +14168,12 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
13860
14168
  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");
13861
14169
  }
13862
14170
  }
13863
- if (!exportDims) {
13864
- renderLegend2(contentG, layout, palette, isDark, activeTagGroup);
14171
+ if (hasLegend) {
14172
+ 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");
14173
+ if (activeTagGroup) {
14174
+ legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
14175
+ }
14176
+ renderLegend2(legendParent, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
13865
14177
  }
13866
14178
  }
13867
14179
  function renderC4ContainersForExport(content, systemName, theme, palette) {
@@ -13963,7 +14275,7 @@ function renderC4DeploymentForExport(content, theme, palette) {
13963
14275
  document.body.removeChild(el);
13964
14276
  }
13965
14277
  }
13966
- var 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;
14278
+ var 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;
13967
14279
  var init_renderer7 = __esm({
13968
14280
  "src/c4/renderer.ts"() {
13969
14281
  "use strict";
@@ -13972,6 +14284,7 @@ var init_renderer7 = __esm({
13972
14284
  init_inline_markdown();
13973
14285
  init_parser6();
13974
14286
  init_layout6();
14287
+ init_legend_constants();
13975
14288
  DIAGRAM_PADDING7 = 20;
13976
14289
  MAX_SCALE6 = 3;
13977
14290
  TITLE_HEIGHT4 = 30;
@@ -14004,16 +14317,6 @@ var init_renderer7 = __esm({
14004
14317
  PERSON_LEG_SPAN = 7;
14005
14318
  PERSON_ICON_W = PERSON_ARM_SPAN * 2;
14006
14319
  PERSON_SW = 1.5;
14007
- LEGEND_HEIGHT7 = 28;
14008
- LEGEND_PILL_FONT_SIZE4 = 11;
14009
- LEGEND_PILL_FONT_W6 = LEGEND_PILL_FONT_SIZE4 * 0.6;
14010
- LEGEND_PILL_PAD6 = 16;
14011
- LEGEND_DOT_R7 = 4;
14012
- LEGEND_ENTRY_FONT_SIZE5 = 10;
14013
- LEGEND_ENTRY_FONT_W6 = LEGEND_ENTRY_FONT_SIZE5 * 0.6;
14014
- LEGEND_ENTRY_DOT_GAP6 = 4;
14015
- LEGEND_ENTRY_TRAIL6 = 8;
14016
- LEGEND_CAPSULE_PAD6 = 4;
14017
14320
  lineGenerator5 = d3Shape5.line().x((d) => d.x).y((d) => d.y).curve(d3Shape5.curveBasis);
14018
14321
  }
14019
14322
  });
@@ -14869,23 +15172,6 @@ function computeInfra(parsed, params = {}) {
14869
15172
  const defaultLatencyMs = parseFloat(parsed.options["default-latency-ms"] ?? "") || 0;
14870
15173
  const defaultUptime = parseFloat(parsed.options["default-uptime"] ?? "") || 100;
14871
15174
  let effectiveNodes = parsed.nodes;
14872
- if (params.scenario) {
14873
- const overrides = params.scenario.overrides;
14874
- effectiveNodes = parsed.nodes.map((node) => {
14875
- const nodeOverrides = overrides[node.id];
14876
- if (!nodeOverrides) return node;
14877
- const props = node.properties.map((p) => {
14878
- const ov = nodeOverrides[p.key];
14879
- return ov != null ? { ...p, value: ov } : p;
14880
- });
14881
- for (const [key, val] of Object.entries(nodeOverrides)) {
14882
- if (!props.some((p) => p.key === key)) {
14883
- props.push({ key, value: val, lineNumber: node.lineNumber });
14884
- }
14885
- }
14886
- return { ...node, properties: props };
14887
- });
14888
- }
14889
15175
  if (params.propertyOverrides) {
14890
15176
  const propOv = params.propertyOverrides;
14891
15177
  effectiveNodes = effectiveNodes.map((node) => {
@@ -15322,6 +15608,7 @@ var init_compute = __esm({
15322
15608
  // src/infra/layout.ts
15323
15609
  var layout_exports8 = {};
15324
15610
  __export(layout_exports8, {
15611
+ fixEdgeWaypoints: () => fixEdgeWaypoints,
15325
15612
  layoutInfra: () => layoutInfra,
15326
15613
  separateGroups: () => separateGroups
15327
15614
  });
@@ -15506,6 +15793,8 @@ function formatUptime(fraction) {
15506
15793
  return `${pct.toFixed(1)}%`;
15507
15794
  }
15508
15795
  function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15796
+ const groupDeltas = /* @__PURE__ */ new Map();
15797
+ let converged = false;
15509
15798
  for (let iter = 0; iter < maxIterations; iter++) {
15510
15799
  let anyOverlap = false;
15511
15800
  for (let i = 0; i < groups.length; i++) {
@@ -15523,6 +15812,9 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15523
15812
  const groupToShift = aCenter <= bCenter ? gb : ga;
15524
15813
  if (isLR) groupToShift.y += shift;
15525
15814
  else groupToShift.x += shift;
15815
+ const prev = groupDeltas.get(groupToShift.id) ?? { dx: 0, dy: 0 };
15816
+ if (isLR) groupDeltas.set(groupToShift.id, { dx: prev.dx, dy: prev.dy + shift });
15817
+ else groupDeltas.set(groupToShift.id, { dx: prev.dx + shift, dy: prev.dy });
15526
15818
  for (const node of nodes) {
15527
15819
  if (node.groupId === groupToShift.id) {
15528
15820
  if (isLR) node.y += shift;
@@ -15531,19 +15823,48 @@ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15531
15823
  }
15532
15824
  }
15533
15825
  }
15534
- if (!anyOverlap) break;
15826
+ if (!anyOverlap) {
15827
+ converged = true;
15828
+ break;
15829
+ }
15830
+ }
15831
+ if (!converged && maxIterations > 0) {
15832
+ console.warn(`separateGroups: hit maxIterations (${maxIterations}) without fully resolving all group overlaps`);
15833
+ }
15834
+ return groupDeltas;
15835
+ }
15836
+ function fixEdgeWaypoints(edges, nodes, groupDeltas) {
15837
+ if (groupDeltas.size === 0) return;
15838
+ const nodeToGroup = /* @__PURE__ */ new Map();
15839
+ for (const node of nodes) nodeToGroup.set(node.id, node.groupId);
15840
+ for (const edge of edges) {
15841
+ const srcGroup = nodeToGroup.get(edge.sourceId) ?? null;
15842
+ const tgtGroup = nodeToGroup.get(edge.targetId) ?? null;
15843
+ const srcDelta = srcGroup ? groupDeltas.get(srcGroup) : void 0;
15844
+ const tgtDelta = tgtGroup ? groupDeltas.get(tgtGroup) : void 0;
15845
+ if (!srcDelta && !tgtDelta) continue;
15846
+ if (srcDelta && tgtDelta && srcGroup !== tgtGroup) {
15847
+ edge.points = [];
15848
+ continue;
15849
+ }
15850
+ const delta = srcDelta ?? tgtDelta;
15851
+ for (const pt of edge.points) {
15852
+ pt.x += delta.dx;
15853
+ pt.y += delta.dy;
15854
+ }
15535
15855
  }
15536
15856
  }
15537
15857
  function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15538
15858
  if (computed.nodes.length === 0) {
15539
- return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15859
+ return { nodes: [], edges: [], groups: [], options: {}, direction: computed.direction, width: 0, height: 0 };
15540
15860
  }
15861
+ const isLR = computed.direction !== "TB";
15541
15862
  const g = new dagre7.graphlib.Graph();
15542
15863
  g.setGraph({
15543
15864
  rankdir: computed.direction === "TB" ? "TB" : "LR",
15544
- nodesep: 50,
15545
- ranksep: 100,
15546
- edgesep: 20
15865
+ nodesep: isLR ? 70 : 60,
15866
+ ranksep: isLR ? 150 : 120,
15867
+ edgesep: 30
15547
15868
  });
15548
15869
  g.setDefaultEdgeLabel(() => ({}));
15549
15870
  const groupedNodeIds = /* @__PURE__ */ new Set();
@@ -15551,7 +15872,6 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15551
15872
  if (node.groupId) groupedNodeIds.add(node.id);
15552
15873
  }
15553
15874
  const GROUP_INFLATE = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15554
- const isLR = computed.direction !== "TB";
15555
15875
  const widthMap = /* @__PURE__ */ new Map();
15556
15876
  const heightMap = /* @__PURE__ */ new Map();
15557
15877
  for (const node of computed.nodes) {
@@ -15686,7 +16006,8 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15686
16006
  lineNumber: group.lineNumber
15687
16007
  };
15688
16008
  });
15689
- separateGroups(layoutGroups, layoutNodes, isLR);
16009
+ const groupDeltas = separateGroups(layoutGroups, layoutNodes, isLR);
16010
+ fixEdgeWaypoints(layoutEdges, layoutNodes, groupDeltas);
15690
16011
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
15691
16012
  for (const node of layoutNodes) {
15692
16013
  const left = node.x - node.width / 2;
@@ -15744,6 +16065,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15744
16065
  edges: layoutEdges,
15745
16066
  groups: layoutGroups,
15746
16067
  options: computed.options,
16068
+ direction: computed.direction,
15747
16069
  width: totalWidth,
15748
16070
  height: totalHeight
15749
16071
  };
@@ -15785,23 +16107,23 @@ var init_layout8 = __esm({
15785
16107
  ]);
15786
16108
  DISPLAY_NAMES = {
15787
16109
  "cache-hit": "cache hit",
15788
- "firewall-block": "fw block",
16110
+ "firewall-block": "firewall block",
15789
16111
  "ratelimit-rps": "rate limit RPS",
15790
16112
  "latency-ms": "latency",
15791
16113
  "uptime": "uptime",
15792
16114
  "instances": "instances",
15793
16115
  "max-rps": "max RPS",
15794
- "cb-error-threshold": "CB error",
15795
- "cb-latency-threshold-ms": "CB latency",
16116
+ "cb-error-threshold": "CB error threshold",
16117
+ "cb-latency-threshold-ms": "CB latency threshold",
15796
16118
  "concurrency": "concurrency",
15797
16119
  "duration-ms": "duration",
15798
16120
  "cold-start-ms": "cold start",
15799
16121
  "buffer": "buffer",
15800
- "drain-rate": "drain",
16122
+ "drain-rate": "drain rate",
15801
16123
  "retention-hours": "retention",
15802
16124
  "partitions": "partitions"
15803
16125
  };
15804
- GROUP_GAP = 24;
16126
+ GROUP_GAP = GROUP_PADDING3 * 2 + GROUP_HEADER_HEIGHT;
15805
16127
  }
15806
16128
  });
15807
16129
 
@@ -15876,6 +16198,236 @@ function resolveNodeSlo(node, diagramOptions) {
15876
16198
  if (availThreshold == null && latencyP90 == null) return null;
15877
16199
  return { availThreshold, latencyP90, warningMargin };
15878
16200
  }
16201
+ function buildPathD(pts, direction) {
16202
+ const gen = d3Shape7.line().x((d) => d.x).y((d) => d.y);
16203
+ if (pts.length <= 2) {
16204
+ gen.curve(direction === "TB" ? d3Shape7.curveBumpY : d3Shape7.curveBumpX);
16205
+ } else {
16206
+ gen.curve(d3Shape7.curveCatmullRom.alpha(0.5));
16207
+ }
16208
+ return gen(pts) ?? "";
16209
+ }
16210
+ function computePortPts(edges, nodeMap, direction) {
16211
+ const srcPts = /* @__PURE__ */ new Map();
16212
+ const tgtPts = /* @__PURE__ */ new Map();
16213
+ const PAD = 0.1;
16214
+ const activeEdges = edges.filter((e) => e.points.length > 0);
16215
+ const bySource = /* @__PURE__ */ new Map();
16216
+ for (const e of activeEdges) {
16217
+ if (!bySource.has(e.sourceId)) bySource.set(e.sourceId, []);
16218
+ bySource.get(e.sourceId).push(e);
16219
+ }
16220
+ for (const [sourceId, es] of bySource) {
16221
+ if (es.length < 2) continue;
16222
+ const source = nodeMap.get(sourceId);
16223
+ if (!source) continue;
16224
+ 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);
16225
+ const n = sorted.length;
16226
+ for (let i = 0; i < n; i++) {
16227
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16228
+ const { e, t } = sorted[i];
16229
+ const isBackward = direction === "LR" ? t.x < source.x : t.y < source.y;
16230
+ if (direction === "LR") {
16231
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16232
+ x: isBackward ? source.x - source.width / 2 : source.x + source.width / 2,
16233
+ y: source.y - source.height / 2 + frac * source.height
16234
+ });
16235
+ } else {
16236
+ srcPts.set(`${e.sourceId}:${e.targetId}`, {
16237
+ x: source.x - source.width / 2 + frac * source.width,
16238
+ y: isBackward ? source.y - source.height / 2 : source.y + source.height / 2
16239
+ });
16240
+ }
16241
+ }
16242
+ }
16243
+ const byTarget = /* @__PURE__ */ new Map();
16244
+ for (const e of activeEdges) {
16245
+ if (!byTarget.has(e.targetId)) byTarget.set(e.targetId, []);
16246
+ byTarget.get(e.targetId).push(e);
16247
+ }
16248
+ for (const [targetId, es] of byTarget) {
16249
+ if (es.length < 2) continue;
16250
+ const target = nodeMap.get(targetId);
16251
+ if (!target) continue;
16252
+ 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);
16253
+ const n = sorted.length;
16254
+ for (let i = 0; i < n; i++) {
16255
+ const frac = n === 1 ? 0.5 : PAD + (1 - 2 * PAD) * i / (n - 1);
16256
+ const { e, s } = sorted[i];
16257
+ const isBackward = direction === "LR" ? target.x < s.x : target.y < s.y;
16258
+ if (direction === "LR") {
16259
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16260
+ x: isBackward ? target.x + target.width / 2 : target.x - target.width / 2,
16261
+ y: target.y - target.height / 2 + frac * target.height
16262
+ });
16263
+ } else {
16264
+ tgtPts.set(`${e.sourceId}:${e.targetId}`, {
16265
+ x: target.x - target.width / 2 + frac * target.width,
16266
+ y: isBackward ? target.y + target.height / 2 : target.y - target.height / 2
16267
+ });
16268
+ }
16269
+ }
16270
+ }
16271
+ return { srcPts, tgtPts };
16272
+ }
16273
+ function findRoutingLane(blocking, targetY, margin) {
16274
+ const MERGE_SLOP = 4;
16275
+ const sorted = [...blocking].sort((a, b) => a.y + a.height / 2 - (b.y + b.height / 2));
16276
+ const merged = [];
16277
+ for (const r of sorted) {
16278
+ const lo = r.y - MERGE_SLOP;
16279
+ const hi = r.y + r.height + MERGE_SLOP;
16280
+ if (merged.length && lo <= merged[merged.length - 1][1]) {
16281
+ merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], hi);
16282
+ } else {
16283
+ merged.push([lo, hi]);
16284
+ }
16285
+ }
16286
+ if (merged.length === 0) return targetY;
16287
+ const MIN_GAP = 10;
16288
+ const candidates = [
16289
+ merged[0][0] - margin,
16290
+ // above all blocking rects
16291
+ merged[merged.length - 1][1] + margin
16292
+ // below all blocking rects
16293
+ ];
16294
+ for (let i = 0; i < merged.length - 1; i++) {
16295
+ const gapLo = merged[i][1];
16296
+ const gapHi = merged[i + 1][0];
16297
+ if (gapHi - gapLo >= MIN_GAP) {
16298
+ candidates.push((gapLo + gapHi) / 2);
16299
+ }
16300
+ }
16301
+ return candidates.reduce(
16302
+ (best, c) => Math.abs(c - targetY) < Math.abs(best - targetY) ? c : best,
16303
+ candidates[0]
16304
+ );
16305
+ }
16306
+ function segmentIntersectsRect(p1, p2, rect) {
16307
+ const { x: rx, y: ry, width: rw, height: rh } = rect;
16308
+ const rr = rx + rw;
16309
+ const rb = ry + rh;
16310
+ const inRect = (p) => p.x >= rx && p.x <= rr && p.y >= ry && p.y <= rb;
16311
+ if (inRect(p1) || inRect(p2)) return true;
16312
+ if (Math.max(p1.x, p2.x) < rx || Math.min(p1.x, p2.x) > rr) return false;
16313
+ if (Math.max(p1.y, p2.y) < ry || Math.min(p1.y, p2.y) > rb) return false;
16314
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
16315
+ const crosses = (a, b) => {
16316
+ const d1 = cross(a, b, p1);
16317
+ const d2 = cross(a, b, p2);
16318
+ const d3 = cross(p1, p2, a);
16319
+ const d4 = cross(p1, p2, b);
16320
+ return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
16321
+ };
16322
+ const tl = { x: rx, y: ry };
16323
+ const tr = { x: rr, y: ry };
16324
+ const br = { x: rr, y: rb };
16325
+ const bl = { x: rx, y: rb };
16326
+ return crosses(tl, tr) || crosses(tr, br) || crosses(br, bl) || crosses(bl, tl);
16327
+ }
16328
+ function curveIntersectsRect(sc, tc, rect, direction) {
16329
+ if (direction === "LR") {
16330
+ const midX = (sc.x + tc.x) / 2;
16331
+ const m1 = { x: midX, y: sc.y };
16332
+ const m2 = { x: midX, y: tc.y };
16333
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16334
+ } else {
16335
+ const midY = (sc.y + tc.y) / 2;
16336
+ const m1 = { x: sc.x, y: midY };
16337
+ const m2 = { x: tc.x, y: midY };
16338
+ return segmentIntersectsRect(sc, m1, rect) || segmentIntersectsRect(m1, m2, rect) || segmentIntersectsRect(m2, tc, rect);
16339
+ }
16340
+ }
16341
+ function edgeWaypoints(source, target, groups, nodes, direction, margin = 30, srcExitPt, tgtEnterPt) {
16342
+ const sc = { x: source.x, y: source.y };
16343
+ const tc = { x: target.x, y: target.y };
16344
+ const isBackward = direction === "LR" ? tc.x < sc.x : tc.y < sc.y;
16345
+ if (isBackward) {
16346
+ if (direction === "LR") {
16347
+ const xBandObs = [];
16348
+ for (const g of groups) {
16349
+ if (g.x + g.width < tc.x - margin || g.x > sc.x + margin) continue;
16350
+ xBandObs.push({ x: g.x, y: g.y, width: g.width, height: g.height });
16351
+ }
16352
+ for (const n of nodes) {
16353
+ if (n.id === source.id || n.id === target.id) continue;
16354
+ const nLeft = n.x - n.width / 2;
16355
+ const nRight = n.x + n.width / 2;
16356
+ if (nRight < tc.x - margin || nLeft > sc.x + margin) continue;
16357
+ xBandObs.push({ x: nLeft, y: n.y - n.height / 2, width: n.width, height: n.height });
16358
+ }
16359
+ const midY = (sc.y + tc.y) / 2;
16360
+ const routeY2 = xBandObs.length > 0 ? findRoutingLane(xBandObs, midY, margin) : midY;
16361
+ const exitBorder = srcExitPt ?? nodeBorderPoint(source, { x: sc.x, y: routeY2 });
16362
+ const exitPt2 = { x: exitBorder.x, y: routeY2 };
16363
+ const enterPt2 = { x: tc.x, y: routeY2 };
16364
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, enterPt2);
16365
+ return srcExitPt ? [srcExitPt, exitPt2, enterPt2, tp2] : [exitBorder, exitPt2, enterPt2, tp2];
16366
+ } else {
16367
+ const yBandObs = [];
16368
+ for (const g of groups) {
16369
+ if (g.y + g.height < tc.y - margin || g.y > sc.y + margin) continue;
16370
+ yBandObs.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 nTop = n.y - n.height / 2;
16375
+ const nBot = n.y + n.height / 2;
16376
+ if (nBot < tc.y - margin || nTop > sc.y + margin) continue;
16377
+ yBandObs.push({ x: n.x - n.width / 2, y: nTop, width: n.width, height: n.height });
16378
+ }
16379
+ const rotated = yBandObs.map((r) => ({ x: r.y, y: r.x, width: r.height, height: r.width }));
16380
+ const midX = (sc.x + tc.x) / 2;
16381
+ const routeX = rotated.length > 0 ? findRoutingLane(rotated, midX, margin) : midX;
16382
+ const exitPt2 = srcExitPt ?? { x: routeX, y: sc.y };
16383
+ const enterPt2 = { x: routeX, y: tc.y };
16384
+ return [
16385
+ srcExitPt ?? nodeBorderPoint(source, exitPt2),
16386
+ exitPt2,
16387
+ enterPt2,
16388
+ tgtEnterPt ?? nodeBorderPoint(target, enterPt2)
16389
+ ];
16390
+ }
16391
+ }
16392
+ const blocking = [];
16393
+ const blockingGroupIds = /* @__PURE__ */ new Set();
16394
+ const pathSrc = srcExitPt ?? sc;
16395
+ const pathTgt = tgtEnterPt ?? tc;
16396
+ for (const g of groups) {
16397
+ if (g.id === source.groupId || g.id === target.groupId) continue;
16398
+ const gRect = { x: g.x, y: g.y, width: g.width, height: g.height };
16399
+ if (curveIntersectsRect(pathSrc, pathTgt, gRect, direction)) {
16400
+ blocking.push(gRect);
16401
+ blockingGroupIds.add(g.id);
16402
+ }
16403
+ }
16404
+ for (const n of nodes) {
16405
+ if (n.id === source.id || n.id === target.id) continue;
16406
+ if (n.groupId && (n.groupId === source.groupId || n.groupId === target.groupId)) continue;
16407
+ if (n.groupId && blockingGroupIds.has(n.groupId)) continue;
16408
+ const nodeRect = { x: n.x - n.width / 2, y: n.y - n.height / 2, width: n.width, height: n.height };
16409
+ if (curveIntersectsRect(pathSrc, pathTgt, nodeRect, direction)) {
16410
+ blocking.push(nodeRect);
16411
+ }
16412
+ }
16413
+ if (blocking.length === 0) {
16414
+ const sp = srcExitPt ?? nodeBorderPoint(source, tc);
16415
+ const tp2 = tgtEnterPt ?? nodeBorderPoint(target, sp);
16416
+ return [sp, tp2];
16417
+ }
16418
+ const obsLeft = Math.min(...blocking.map((o) => o.x));
16419
+ const obsRight = Math.max(...blocking.map((o) => o.x + o.width));
16420
+ const routeY = findRoutingLane(blocking, tc.y, margin);
16421
+ const exitX = direction === "LR" ? Math.max(sc.x, obsLeft - margin) : obsLeft - margin;
16422
+ const enterX = direction === "LR" ? Math.min(tc.x, obsRight + margin) : obsRight + margin;
16423
+ const exitPt = { x: exitX, y: routeY };
16424
+ const enterPt = { x: enterX, y: routeY };
16425
+ const tp = tgtEnterPt ?? nodeBorderPoint(target, enterPt);
16426
+ if (srcExitPt) {
16427
+ return [srcExitPt, exitPt, enterPt, tp];
16428
+ }
16429
+ return [nodeBorderPoint(source, exitPt), exitPt, enterPt, tp];
16430
+ }
15879
16431
  function nodeBorderPoint(node, target) {
15880
16432
  const hw = node.width / 2;
15881
16433
  const hh = node.height / 2;
@@ -16159,33 +16711,29 @@ function renderGroups(svg, groups, palette, isDark) {
16159
16711
  }
16160
16712
  }
16161
16713
  }
16162
- function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16714
+ function renderEdgePaths(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16163
16715
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16164
16716
  const maxRps = Math.max(...edges.map((e) => e.computedRps), 1);
16717
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16165
16718
  for (const edge of edges) {
16166
16719
  if (edge.points.length === 0) continue;
16167
16720
  const targetNode = nodeMap.get(edge.targetId);
16168
16721
  const sourceNode = nodeMap.get(edge.sourceId);
16169
16722
  const color = edgeColor(edge, palette);
16170
16723
  const strokeW = edgeWidth();
16171
- let pts = edge.points;
16172
- if (sourceNode && targetNode && pts.length >= 2) {
16173
- const first = pts[0];
16174
- const distFirstToSource = (first.x - sourceNode.x) ** 2 + (first.y - sourceNode.y) ** 2;
16175
- const distFirstToTarget = (first.x - targetNode.x) ** 2 + (first.y - targetNode.y) ** 2;
16176
- if (distFirstToTarget < distFirstToSource) {
16177
- pts = [...pts].reverse();
16178
- }
16179
- }
16180
- if (sourceNode && pts.length > 0) {
16181
- const bp = nodeBorderPoint(sourceNode, pts[0]);
16182
- pts = [bp, ...pts];
16183
- }
16184
- if (targetNode && pts.length > 0) {
16185
- const bp = nodeBorderPoint(targetNode, pts[pts.length - 1]);
16186
- pts = [...pts, bp];
16187
- }
16188
- const pathD = lineGenerator7(pts) ?? "";
16724
+ if (!sourceNode || !targetNode) continue;
16725
+ const key = `${edge.sourceId}:${edge.targetId}`;
16726
+ const pts = edgeWaypoints(
16727
+ sourceNode,
16728
+ targetNode,
16729
+ groups,
16730
+ nodes,
16731
+ direction,
16732
+ 30,
16733
+ srcPts.get(key),
16734
+ tgtPts.get(key)
16735
+ );
16736
+ const pathD = buildPathD(pts, direction);
16189
16737
  const edgeG = svg.append("g").attr("class", "infra-edge").attr("data-line-number", edge.lineNumber);
16190
16738
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", color).attr("stroke-width", strokeW);
16191
16739
  if (animate && edge.computedRps > 0) {
@@ -16200,19 +16748,34 @@ function renderEdgePaths(svg, edges, nodes, palette, isDark, animate) {
16200
16748
  }
16201
16749
  }
16202
16750
  }
16203
- function renderEdgeLabels(svg, edges, palette, isDark, animate) {
16751
+ function renderEdgeLabels(svg, edges, nodes, groups, palette, isDark, animate, direction) {
16752
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
16753
+ const { srcPts, tgtPts } = computePortPts(edges, nodeMap, direction);
16204
16754
  for (const edge of edges) {
16205
16755
  if (edge.points.length === 0) continue;
16206
16756
  if (!edge.label) continue;
16207
- const midIdx = Math.floor(edge.points.length / 2);
16208
- const midPt = edge.points[midIdx];
16757
+ const sourceNode = nodeMap.get(edge.sourceId);
16758
+ const targetNode = nodeMap.get(edge.targetId);
16759
+ if (!sourceNode || !targetNode) continue;
16760
+ const key = `${edge.sourceId}:${edge.targetId}`;
16761
+ const wps = edgeWaypoints(
16762
+ sourceNode,
16763
+ targetNode,
16764
+ groups,
16765
+ nodes,
16766
+ direction,
16767
+ 30,
16768
+ srcPts.get(key),
16769
+ tgtPts.get(key)
16770
+ );
16771
+ const midPt = wps[Math.floor(wps.length / 2)];
16209
16772
  const labelText = edge.label;
16210
16773
  const g = svg.append("g").attr("class", animate ? "infra-edge-label" : "");
16211
16774
  const textWidth = labelText.length * 6.5 + 8;
16212
16775
  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);
16213
16776
  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);
16214
16777
  if (animate) {
16215
- const pathD = lineGenerator7(edge.points) ?? "";
16778
+ const pathD = buildPathD(wps, direction);
16216
16779
  g.insert("path", ":first-child").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", 20);
16217
16780
  }
16218
16781
  }
@@ -16484,16 +17047,16 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
16484
17047
  color: r.color,
16485
17048
  key: r.name.toLowerCase().replace(/\s+/g, "-")
16486
17049
  }));
16487
- const pillWidth = "Capabilities".length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
17050
+ const pillWidth = "Capabilities".length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
16488
17051
  let entriesWidth = 0;
16489
17052
  for (const e of entries) {
16490
- entriesWidth += LEGEND_DOT_R8 * 2 + LEGEND_ENTRY_DOT_GAP7 + e.value.length * LEGEND_ENTRY_FONT_W7 + LEGEND_ENTRY_TRAIL7;
17053
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
16491
17054
  }
16492
17055
  groups.push({
16493
17056
  name: "Capabilities",
16494
17057
  type: "role",
16495
17058
  entries,
16496
- width: LEGEND_CAPSULE_PAD7 * 2 + pillWidth + 4 + entriesWidth,
17059
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth,
16497
17060
  minifiedWidth: pillWidth
16498
17061
  });
16499
17062
  }
@@ -16509,123 +17072,72 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
16509
17072
  }
16510
17073
  }
16511
17074
  if (entries.length === 0) continue;
16512
- const pillWidth = tg.name.length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
17075
+ const pillWidth = tg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
16513
17076
  let entriesWidth = 0;
16514
17077
  for (const e of entries) {
16515
- entriesWidth += LEGEND_DOT_R8 * 2 + LEGEND_ENTRY_DOT_GAP7 + e.value.length * LEGEND_ENTRY_FONT_W7 + LEGEND_ENTRY_TRAIL7;
17078
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
16516
17079
  }
16517
17080
  groups.push({
16518
17081
  name: tg.name,
16519
17082
  type: "tag",
16520
17083
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
16521
17084
  entries,
16522
- width: LEGEND_CAPSULE_PAD7 * 2 + pillWidth + 4 + entriesWidth,
17085
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth,
16523
17086
  minifiedWidth: pillWidth
16524
17087
  });
16525
17088
  }
16526
17089
  return groups;
16527
17090
  }
16528
- function computePlaybackWidth(playback) {
16529
- if (!playback) return 0;
16530
- const pillWidth = "Playback".length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
16531
- if (!playback.expanded) return pillWidth;
16532
- let entriesW = 8;
16533
- entriesW += LEGEND_PILL_FONT_SIZE5 * 0.8 + 6;
16534
- for (const s of playback.speedOptions) {
16535
- entriesW += `${s}x`.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
16536
- }
16537
- return LEGEND_CAPSULE_PAD7 * 2 + pillWidth + entriesW;
16538
- }
16539
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
16540
- if (legendGroups.length === 0 && !playback) return;
17091
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup) {
17092
+ if (legendGroups.length === 0) return;
16541
17093
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
17094
+ if (activeGroup) {
17095
+ legendG.attr("data-legend-active", activeGroup.toLowerCase());
17096
+ }
16542
17097
  const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
16543
- const playbackW = computePlaybackWidth(playback);
16544
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP5 : 0;
16545
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP5 + trailingGaps + playbackW;
17098
+ const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP;
16546
17099
  let cursorX = (totalWidth - totalLegendW) / 2;
16547
17100
  for (const group of legendGroups) {
16548
17101
  const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
16549
17102
  const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
16550
17103
  const pillLabel = group.name;
16551
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
16552
- 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");
17104
+ const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
17105
+ 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");
16553
17106
  if (isActive) {
16554
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT8).attr("rx", LEGEND_HEIGHT8 / 2).attr("fill", groupBg);
17107
+ gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
16555
17108
  }
16556
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD7 : 0;
16557
- const pillYOff = isActive ? LEGEND_CAPSULE_PAD7 : 0;
16558
- const pillH = LEGEND_HEIGHT8 - (isActive ? LEGEND_CAPSULE_PAD7 * 2 : 0);
17109
+ const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
17110
+ const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
17111
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
16559
17112
  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);
16560
17113
  if (isActive) {
16561
- 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);
17114
+ 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);
16562
17115
  }
16563
- 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);
17116
+ 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);
16564
17117
  if (isActive) {
16565
17118
  let entryX = pillXOff + pillWidth + 4;
16566
17119
  for (const entry of group.entries) {
16567
- 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");
16568
- if (group.type === "tag" && group.tagKey) {
16569
- entryG.attr("data-legend-tag-group", group.tagKey);
16570
- }
16571
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R8).attr("cy", LEGEND_HEIGHT8 / 2).attr("r", LEGEND_DOT_R8).attr("fill", entry.color);
16572
- const textX = entryX + LEGEND_DOT_R8 * 2 + LEGEND_ENTRY_DOT_GAP7;
16573
- 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);
16574
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W7 + LEGEND_ENTRY_TRAIL7;
17120
+ 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");
17121
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17122
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17123
+ 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);
17124
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
16575
17125
  }
16576
17126
  }
16577
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP5;
17127
+ cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
16578
17128
  }
16579
- if (playback) {
16580
- const isExpanded = playback.expanded;
16581
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
16582
- const pillLabel = "Playback";
16583
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W7 + LEGEND_PILL_PAD7;
16584
- const fullW = computePlaybackWidth(playback);
16585
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
16586
- if (isExpanded) {
16587
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT8).attr("rx", LEGEND_HEIGHT8 / 2).attr("fill", groupBg);
16588
- }
16589
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD7 : 0;
16590
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD7 : 0;
16591
- const pillH = LEGEND_HEIGHT8 - (isExpanded ? LEGEND_CAPSULE_PAD7 * 2 : 0);
16592
- 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);
16593
- if (isExpanded) {
16594
- 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);
16595
- }
16596
- 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);
16597
- if (isExpanded) {
16598
- let entryX = pillXOff + pillWidth + 8;
16599
- const entryY = LEGEND_HEIGHT8 / 2 + LEGEND_ENTRY_FONT_SIZE6 / 2 - 1;
16600
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
16601
- 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);
16602
- entryX += LEGEND_PILL_FONT_SIZE5 * 0.8 + 6;
16603
- for (const s of playback.speedOptions) {
16604
- const label = `${s}x`;
16605
- const isActive = playback.speed === s;
16606
- const slotW = label.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2;
16607
- const badgeH = LEGEND_ENTRY_FONT_SIZE6 + SPEED_BADGE_V_PAD * 2;
16608
- const badgeY = (LEGEND_HEIGHT8 - badgeH) / 2;
16609
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
16610
- 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");
16611
- 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);
16612
- entryX += slotW + SPEED_BADGE_GAP;
16613
- }
16614
- }
16615
- cursorX += fullW + LEGEND_GROUP_GAP5;
16616
- }
16617
- }
16618
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
17129
+ }
17130
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, _playback, expandedNodeIds, exportMode, collapsedNodes) {
16619
17131
  d3Selection9.select(container).selectAll(":not([data-d3-tooltip])").remove();
16620
17132
  const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette, layout.edges);
16621
- const hasLegend = legendGroups.length > 0 || !!playback;
17133
+ const hasLegend = legendGroups.length > 0;
16622
17134
  const fixedLegend = !exportMode && hasLegend;
16623
- const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT8 : 0;
17135
+ const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT : 0;
16624
17136
  const titleOffset = title ? 40 : 0;
16625
17137
  const totalWidth = layout.width;
16626
17138
  const totalHeight = layout.height + titleOffset + legendOffset;
16627
17139
  const shouldAnimate = animate !== false;
16628
- 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");
17140
+ 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");
16629
17141
  if (shouldAnimate) {
16630
17142
  rootSvg.append("style").text(`
16631
17143
  @keyframes infra-pulse-warning {
@@ -16673,7 +17185,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16673
17185
  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);
16674
17186
  }
16675
17187
  renderGroups(svg, layout.groups, palette, isDark);
16676
- renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
17188
+ renderEdgePaths(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16677
17189
  const fanoutSourceIds = collectFanoutSourceIds(layout.edges);
16678
17190
  const scaledGroupIds = new Set(
16679
17191
  layout.groups.filter((g) => {
@@ -16685,14 +17197,14 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16685
17197
  if (shouldAnimate) {
16686
17198
  renderRejectParticles(svg, layout.nodes);
16687
17199
  }
16688
- renderEdgeLabels(svg, layout.edges, palette, isDark, shouldAnimate);
17200
+ renderEdgeLabels(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
16689
17201
  if (hasLegend) {
16690
17202
  if (fixedLegend) {
16691
17203
  const containerWidth = container.clientWidth || totalWidth;
16692
- 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");
16693
- renderLegend3(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP3 / 2, palette, isDark, activeGroup ?? null, playback ?? void 0);
17204
+ 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");
17205
+ renderLegend3(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP3 / 2, palette, isDark, activeGroup ?? null);
16694
17206
  } else {
16695
- renderLegend3(rootSvg, legendGroups, totalWidth, titleOffset + layout.height + 4, palette, isDark, activeGroup ?? null, playback ?? void 0);
17207
+ renderLegend3(rootSvg, legendGroups, totalWidth, titleOffset + layout.height + 4, palette, isDark, activeGroup ?? null);
16696
17208
  }
16697
17209
  }
16698
17210
  }
@@ -16703,7 +17215,7 @@ function parseAndLayoutInfra(content) {
16703
17215
  const layout = layoutInfra(computed);
16704
17216
  return { parsed, computed, layout };
16705
17217
  }
16706
- var 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;
17218
+ var 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;
16707
17219
  var init_renderer8 = __esm({
16708
17220
  "src/infra/renderer.ts"() {
16709
17221
  "use strict";
@@ -16714,6 +17226,7 @@ var init_renderer8 = __esm({
16714
17226
  init_parser9();
16715
17227
  init_compute();
16716
17228
  init_layout8();
17229
+ init_legend_constants();
16717
17230
  NODE_FONT_SIZE4 = 13;
16718
17231
  META_FONT_SIZE4 = 10;
16719
17232
  META_LINE_HEIGHT8 = 14;
@@ -16729,21 +17242,7 @@ var init_renderer8 = __esm({
16729
17242
  NODE_PAD_BOTTOM2 = 10;
16730
17243
  COLLAPSE_BAR_HEIGHT5 = 6;
16731
17244
  COLLAPSE_BAR_INSET2 = 0;
16732
- LEGEND_HEIGHT8 = 28;
16733
- LEGEND_PILL_PAD7 = 16;
16734
- LEGEND_PILL_FONT_SIZE5 = 11;
16735
- LEGEND_PILL_FONT_W7 = LEGEND_PILL_FONT_SIZE5 * 0.6;
16736
- LEGEND_CAPSULE_PAD7 = 4;
16737
- LEGEND_DOT_R8 = 4;
16738
- LEGEND_ENTRY_FONT_SIZE6 = 10;
16739
- LEGEND_ENTRY_FONT_W7 = LEGEND_ENTRY_FONT_SIZE6 * 0.6;
16740
- LEGEND_ENTRY_DOT_GAP7 = 4;
16741
- LEGEND_ENTRY_TRAIL7 = 8;
16742
- LEGEND_GROUP_GAP5 = 12;
16743
17245
  LEGEND_FIXED_GAP3 = 16;
16744
- SPEED_BADGE_H_PAD = 5;
16745
- SPEED_BADGE_V_PAD = 3;
16746
- SPEED_BADGE_GAP = 6;
16747
17246
  COLOR_HEALTHY = "#22c55e";
16748
17247
  COLOR_WARNING = "#eab308";
16749
17248
  COLOR_OVERLOADED = "#ef4444";
@@ -16760,7 +17259,6 @@ var init_renderer8 = __esm({
16760
17259
  REJECT_DURATION_MAX = 3;
16761
17260
  REJECT_COUNT_MIN = 1;
16762
17261
  REJECT_COUNT_MAX = 3;
16763
- lineGenerator7 = d3Shape7.line().x((d) => d.x).y((d) => d.y).curve(d3Shape7.curveBasis);
16764
17262
  PROP_DISPLAY = {
16765
17263
  "cache-hit": "cache hit",
16766
17264
  "firewall-block": "firewall block",
@@ -16945,7 +17443,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
16945
17443
  }
16946
17444
  }
16947
17445
  } else if (edge.points.length >= 2) {
16948
- const pathD = lineGenerator8(edge.points);
17446
+ const pathD = lineGenerator7(edge.points);
16949
17447
  if (pathD) {
16950
17448
  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");
16951
17449
  }
@@ -17009,7 +17507,7 @@ function renderStateForExport(content, theme, palette) {
17009
17507
  document.body.removeChild(container);
17010
17508
  }
17011
17509
  }
17012
- var 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;
17510
+ var 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;
17013
17511
  var init_state_renderer = __esm({
17014
17512
  "src/graph/state-renderer.ts"() {
17015
17513
  "use strict";
@@ -17029,7 +17527,7 @@ var init_state_renderer = __esm({
17029
17527
  PSEUDOSTATE_RADIUS = 10;
17030
17528
  STATE_CORNER_RADIUS = 10;
17031
17529
  GROUP_EXTRA_PADDING2 = 12;
17032
- lineGenerator8 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
17530
+ lineGenerator7 = d3Shape8.line().x((d) => d.x).y((d) => d.y).curve(d3Shape8.curveBasis);
17033
17531
  }
17034
17532
  });
17035
17533
 
@@ -17764,9 +18262,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17764
18262
  const GROUP_PADDING_BOTTOM = 8;
17765
18263
  const GROUP_LABEL_SIZE = 11;
17766
18264
  const titleOffset = title ? TITLE_HEIGHT5 : 0;
17767
- const legendOffset = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT9 + LEGEND_BOTTOM_GAP : 0;
17768
18265
  const groupOffset = groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
17769
- const participantStartY = TOP_MARGIN + titleOffset + legendOffset + PARTICIPANT_Y_OFFSET + groupOffset;
18266
+ const participantStartY = TOP_MARGIN + titleOffset + PARTICIPANT_Y_OFFSET + groupOffset;
17770
18267
  const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
17771
18268
  const hasActors = participants.some((p) => p.type === "actor");
17772
18269
  const messageStartOffset = MESSAGE_START_OFFSET + (hasActors ? 20 : 0);
@@ -17848,7 +18345,9 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17848
18345
  participants.length * PARTICIPANT_GAP,
17849
18346
  PARTICIPANT_BOX_WIDTH + 40
17850
18347
  );
17851
- const totalHeight = participantStartY + PARTICIPANT_BOX_HEIGHT + Math.max(lifelineLength, 40) + 40;
18348
+ const contentHeight = participantStartY + PARTICIPANT_BOX_HEIGHT + Math.max(lifelineLength, 40) + 40;
18349
+ const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
18350
+ const totalHeight = contentHeight + legendSpace;
17852
18351
  const containerWidth = options?.exportWidth ?? container.getBoundingClientRect().width;
17853
18352
  const svgWidth = Math.max(totalWidth, containerWidth);
17854
18353
  const diagramWidth = participants.length * PARTICIPANT_GAP;
@@ -17906,13 +18405,13 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17906
18405
  }
17907
18406
  }
17908
18407
  if (parsed.tagGroups.length > 0) {
17909
- const legendY = TOP_MARGIN + titleOffset;
18408
+ const legendY = contentHeight;
17910
18409
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17911
18410
  const legendItems = [];
17912
18411
  for (const tg of parsed.tagGroups) {
17913
18412
  if (tg.entries.length === 0) continue;
17914
18413
  const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
17915
- const pillWidth = tg.name.length * LEGEND_PILL_FONT_W8 + LEGEND_PILL_PAD8;
18414
+ const pillWidth = tg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
17916
18415
  const entries = tg.entries.map((e) => ({
17917
18416
  value: e.value,
17918
18417
  color: resolveColor(e.color)
@@ -17921,38 +18420,42 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
17921
18420
  if (isActive) {
17922
18421
  let entriesWidth = 0;
17923
18422
  for (const entry of entries) {
17924
- entriesWidth += LEGEND_DOT_R9 * 2 + LEGEND_ENTRY_DOT_GAP8 + entry.value.length * LEGEND_ENTRY_FONT_W8 + LEGEND_ENTRY_TRAIL8;
18423
+ entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
17925
18424
  }
17926
- totalWidth2 = LEGEND_CAPSULE_PAD8 * 2 + pillWidth + 4 + entriesWidth;
18425
+ totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
17927
18426
  }
17928
18427
  legendItems.push({ group: tg, isActive, pillWidth, totalWidth: totalWidth2, entries });
17929
18428
  }
17930
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP6;
18429
+ const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
17931
18430
  let legendX = (svgWidth - totalLegendWidth) / 2;
18431
+ const legendContainer = svg.append("g").attr("class", "sequence-legend");
18432
+ if (activeTagGroup) {
18433
+ legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
18434
+ }
17932
18435
  for (const item of legendItems) {
17933
- 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");
18436
+ 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");
17934
18437
  if (item.isActive) {
17935
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT9).attr("rx", LEGEND_HEIGHT9 / 2).attr("fill", groupBg);
18438
+ gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17936
18439
  }
17937
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD8 : 0;
17938
- const pillYOff = item.isActive ? LEGEND_CAPSULE_PAD8 : 0;
17939
- const pillH = LEGEND_HEIGHT9 - (item.isActive ? LEGEND_CAPSULE_PAD8 * 2 : 0);
18440
+ const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
18441
+ const pillYOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
18442
+ const pillH = LEGEND_HEIGHT - (item.isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
17940
18443
  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);
17941
18444
  if (item.isActive) {
17942
18445
  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);
17943
18446
  }
17944
- 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);
18447
+ 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);
17945
18448
  if (item.isActive) {
17946
18449
  let entryX = pillXOff + item.pillWidth + 4;
17947
18450
  for (const entry of item.entries) {
17948
18451
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17949
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R9).attr("cy", LEGEND_HEIGHT9 / 2).attr("r", LEGEND_DOT_R9).attr("fill", entry.color);
17950
- const textX = entryX + LEGEND_DOT_R9 * 2 + LEGEND_ENTRY_DOT_GAP8;
17951
- 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);
17952
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W8 + LEGEND_ENTRY_TRAIL8;
18452
+ entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
18453
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
18454
+ 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);
18455
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
17953
18456
  }
17954
18457
  }
17955
- legendX += item.totalWidth + LEGEND_GROUP_GAP6;
18458
+ legendX += item.totalWidth + LEGEND_GROUP_GAP;
17956
18459
  }
17957
18460
  }
17958
18461
  for (const group of groups) {
@@ -18473,7 +18976,7 @@ function renderParticipant(svg, participant, cx, cy, palette, isDark, color, tag
18473
18976
  });
18474
18977
  }
18475
18978
  }
18476
- var 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;
18979
+ var 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;
18477
18980
  var init_renderer9 = __esm({
18478
18981
  "src/sequence/renderer.ts"() {
18479
18982
  "use strict";
@@ -18483,6 +18986,7 @@ var init_renderer9 = __esm({
18483
18986
  init_colors();
18484
18987
  init_parser();
18485
18988
  init_tag_resolution();
18989
+ init_legend_constants();
18486
18990
  PARTICIPANT_GAP = 160;
18487
18991
  PARTICIPANT_BOX_WIDTH = 120;
18488
18992
  PARTICIPANT_BOX_HEIGHT = 50;
@@ -18504,18 +19008,6 @@ var init_renderer9 = __esm({
18504
19008
  NOTE_CHARS_PER_LINE = Math.floor((NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
18505
19009
  COLLAPSED_NOTE_H = 20;
18506
19010
  COLLAPSED_NOTE_W = 40;
18507
- LEGEND_HEIGHT9 = 28;
18508
- LEGEND_PILL_PAD8 = 16;
18509
- LEGEND_PILL_FONT_SIZE6 = 11;
18510
- LEGEND_PILL_FONT_W8 = LEGEND_PILL_FONT_SIZE6 * 0.6;
18511
- LEGEND_CAPSULE_PAD8 = 4;
18512
- LEGEND_DOT_R9 = 4;
18513
- LEGEND_ENTRY_FONT_SIZE7 = 10;
18514
- LEGEND_ENTRY_FONT_W8 = LEGEND_ENTRY_FONT_SIZE7 * 0.6;
18515
- LEGEND_ENTRY_DOT_GAP8 = 4;
18516
- LEGEND_ENTRY_TRAIL8 = 8;
18517
- LEGEND_GROUP_GAP6 = 12;
18518
- LEGEND_BOTTOM_GAP = 8;
18519
19011
  LABEL_CHAR_WIDTH = 7.5;
18520
19012
  LABEL_MAX_CHARS = Math.floor((PARTICIPANT_BOX_WIDTH - 10) / LABEL_CHAR_WIDTH);
18521
19013
  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);
@@ -20128,9 +20620,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20128
20620
  const scaleMargin = timelineScale ? 40 : 0;
20129
20621
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
20130
20622
  const margin = {
20131
- top: 104 + markerMargin + tagLegendReserve,
20623
+ top: 104 + markerMargin,
20132
20624
  right: 40 + scaleMargin,
20133
- bottom: 40,
20625
+ bottom: 40 + tagLegendReserve,
20134
20626
  left: 60 + scaleMargin
20135
20627
  };
20136
20628
  const innerWidth = width - margin.left - margin.right;
@@ -20237,9 +20729,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20237
20729
  const scaleMargin = timelineScale ? 40 : 0;
20238
20730
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
20239
20731
  const margin = {
20240
- top: 104 + markerMargin + tagLegendReserve,
20732
+ top: 104 + markerMargin,
20241
20733
  right: 200,
20242
- bottom: 40,
20734
+ bottom: 40 + tagLegendReserve,
20243
20735
  left: 60 + scaleMargin
20244
20736
  };
20245
20737
  const innerWidth = width - margin.left - margin.right;
@@ -20376,9 +20868,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20376
20868
  const dynamicLeftMargin = Math.max(120, maxGroupNameLen * 7 + 30);
20377
20869
  const baseTopMargin = title ? 50 : 20;
20378
20870
  const margin = {
20379
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
20871
+ top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin,
20380
20872
  right: 40,
20381
- bottom: 40 + scaleMargin,
20873
+ bottom: 40 + scaleMargin + tagLegendReserve,
20382
20874
  left: dynamicLeftMargin
20383
20875
  };
20384
20876
  const innerWidth = width - margin.left - margin.right;
@@ -20522,9 +21014,9 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20522
21014
  const scaleMargin = timelineScale ? 24 : 0;
20523
21015
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
20524
21016
  const margin = {
20525
- top: 104 + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
21017
+ top: 104 + (timelineScale ? 40 : 0) + markerMargin,
20526
21018
  right: 40,
20527
- bottom: 40 + scaleMargin,
21019
+ bottom: 40 + scaleMargin + tagLegendReserve,
20528
21020
  left: 60
20529
21021
  };
20530
21022
  const innerWidth = width - margin.left - margin.right;
@@ -20655,17 +21147,17 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20655
21147
  });
20656
21148
  }
20657
21149
  if (parsed.timelineTagGroups.length > 0) {
20658
- const LG_HEIGHT = 28;
20659
- const LG_PILL_PAD = 16;
20660
- const LG_PILL_FONT_SIZE = 11;
20661
- const LG_PILL_FONT_W = LG_PILL_FONT_SIZE * 0.6;
20662
- const LG_CAPSULE_PAD = 4;
20663
- const LG_DOT_R = 4;
20664
- const LG_ENTRY_FONT_SIZE = 10;
20665
- const LG_ENTRY_FONT_W = LG_ENTRY_FONT_SIZE * 0.6;
20666
- const LG_ENTRY_DOT_GAP = 4;
20667
- const LG_ENTRY_TRAIL = 8;
20668
- const LG_GROUP_GAP = 12;
21150
+ const LG_HEIGHT = LEGEND_HEIGHT;
21151
+ const LG_PILL_PAD = LEGEND_PILL_PAD;
21152
+ const LG_PILL_FONT_SIZE = LEGEND_PILL_FONT_SIZE;
21153
+ const LG_PILL_FONT_W = LEGEND_PILL_FONT_W;
21154
+ const LG_CAPSULE_PAD = LEGEND_CAPSULE_PAD;
21155
+ const LG_DOT_R = LEGEND_DOT_R;
21156
+ const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
21157
+ const LG_ENTRY_FONT_W = LEGEND_ENTRY_FONT_W;
21158
+ const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
21159
+ const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
21160
+ const LG_GROUP_GAP = LEGEND_GROUP_GAP;
20669
21161
  const LG_ICON_W = 20;
20670
21162
  const mainSvg = d3Selection12.select(container).select("svg");
20671
21163
  const mainG = mainSvg.select("g");
@@ -20698,6 +21190,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20698
21190
  );
20699
21191
  }, drawLegend2 = function() {
20700
21192
  mainSvg.selectAll(".tl-tag-legend-group").remove();
21193
+ mainSvg.selectAll(".tl-tag-legend-container").remove();
20701
21194
  const effectiveColorKey = (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
20702
21195
  const visibleGroups = viewMode ? legendGroups.filter(
20703
21196
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
@@ -20708,13 +21201,17 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20708
21201
  return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
20709
21202
  }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
20710
21203
  let cx = (width - totalW) / 2;
21204
+ const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
21205
+ if (currentActiveGroup) {
21206
+ legendContainer.attr("data-legend-active", currentActiveGroup.toLowerCase());
21207
+ }
20711
21208
  for (const lg of visibleGroups) {
20712
21209
  const groupKey = lg.group.name.toLowerCase();
20713
21210
  const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
20714
21211
  const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
20715
21212
  const pillLabel = lg.group.name;
20716
21213
  const pillWidth = pillLabel.length * LG_PILL_FONT_W + LG_PILL_PAD;
20717
- 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__");
21214
+ 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__");
20718
21215
  if (!viewMode) {
20719
21216
  gEl.style("cursor", "pointer").on("click", () => {
20720
21217
  currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
@@ -20804,7 +21301,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20804
21301
  });
20805
21302
  };
20806
21303
  var drawSwimlaneIcon = drawSwimlaneIcon2, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
20807
- const legendY = title ? 50 : 10;
21304
+ const legendY = height - LG_HEIGHT - 4;
20808
21305
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20809
21306
  const legendGroups = parsed.timelineTagGroups.map((g) => {
20810
21307
  const pillW = g.name.length * LG_PILL_FONT_W + LG_PILL_PAD;
@@ -21575,7 +22072,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21575
22072
  const orgParsed = parseOrg2(content, effectivePalette2);
21576
22073
  if (orgParsed.error) return "";
21577
22074
  const collapsedNodes = orgExportState?.collapsedNodes;
21578
- const activeTagGroup = orgExportState?.activeTagGroup ?? null;
22075
+ const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
21579
22076
  const hiddenAttributes = orgExportState?.hiddenAttributes;
21580
22077
  const { parsed: effectiveParsed, hiddenCounts } = collapsedNodes && collapsedNodes.size > 0 ? collapseOrgTree2(orgParsed, collapsedNodes) : { parsed: orgParsed, hiddenCounts: /* @__PURE__ */ new Map() };
21581
22078
  const orgLayout = layoutOrg2(
@@ -21604,7 +22101,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21604
22101
  const sitemapParsed = parseSitemap2(content, effectivePalette2);
21605
22102
  if (sitemapParsed.error || sitemapParsed.roots.length === 0) return "";
21606
22103
  const collapsedNodes = orgExportState?.collapsedNodes;
21607
- const activeTagGroup = orgExportState?.activeTagGroup ?? null;
22104
+ const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
21608
22105
  const hiddenAttributes = orgExportState?.hiddenAttributes;
21609
22106
  const { parsed: effectiveParsed, hiddenCounts } = collapsedNodes && collapsedNodes.size > 0 ? collapseSitemapTree2(sitemapParsed, collapsedNodes) : { parsed: sitemapParsed, hiddenCounts: /* @__PURE__ */ new Map() };
21610
22107
  const sitemapLayout = layoutSitemap2(
@@ -21632,7 +22129,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21632
22129
  container2.style.position = "absolute";
21633
22130
  container2.style.left = "-9999px";
21634
22131
  document.body.appendChild(container2);
21635
- renderKanban2(container2, kanbanParsed, effectivePalette2, theme === "dark");
22132
+ renderKanban2(container2, kanbanParsed, effectivePalette2, theme === "dark", void 0, void 0, options?.tagGroup);
21636
22133
  return finalizeSvgExport(container2, theme, effectivePalette2, options);
21637
22134
  }
21638
22135
  if (detectedType === "class") {
@@ -21664,7 +22161,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21664
22161
  const exportWidth = erLayout.width + PADDING * 2;
21665
22162
  const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
21666
22163
  const container2 = createExportContainer(exportWidth, exportHeight);
21667
- renderERDiagram2(container2, erParsed, erLayout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight });
22164
+ renderERDiagram2(container2, erParsed, erLayout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight }, options?.tagGroup);
21668
22165
  return finalizeSvgExport(container2, theme, effectivePalette2, options);
21669
22166
  }
21670
22167
  if (detectedType === "initiative-status") {
@@ -21701,7 +22198,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21701
22198
  const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
21702
22199
  const container2 = createExportContainer(exportWidth, exportHeight);
21703
22200
  const renderFn = c4Level === "deployment" || c4Level === "components" && c4System && c4Container || c4Level === "containers" && c4System ? renderC4Containers2 : renderC4Context2;
21704
- renderFn(container2, c4Parsed, c4Layout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight });
22201
+ renderFn(container2, c4Parsed, c4Layout, effectivePalette2, theme === "dark", void 0, { width: exportWidth, height: exportHeight }, options?.tagGroup);
21705
22202
  return finalizeSvgExport(container2, theme, effectivePalette2, options);
21706
22203
  }
21707
22204
  if (detectedType === "flowchart") {
@@ -21724,16 +22221,16 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21724
22221
  const effectivePalette2 = await resolveExportPalette(theme, palette);
21725
22222
  const infraParsed = parseInfra2(content);
21726
22223
  if (infraParsed.error || infraParsed.nodes.length === 0) return "";
21727
- const selectedScenario = options?.scenario ? infraParsed.scenarios.find((s) => s.name.toLowerCase() === options.scenario.toLowerCase()) ?? null : null;
21728
- const infraComputed = computeInfra2(infraParsed, selectedScenario ? { scenario: selectedScenario } : {});
22224
+ const infraComputed = computeInfra2(infraParsed);
21729
22225
  const infraLayout = layoutInfra2(infraComputed);
22226
+ const activeTagGroup = options?.tagGroup ?? null;
21730
22227
  const titleOffset = infraParsed.title ? 40 : 0;
21731
22228
  const legendGroups = computeInfraLegendGroups2(infraLayout.nodes, infraParsed.tagGroups, effectivePalette2);
21732
22229
  const legendOffset = legendGroups.length > 0 ? 28 : 0;
21733
22230
  const exportWidth = infraLayout.width;
21734
22231
  const exportHeight = infraLayout.height + titleOffset + legendOffset;
21735
22232
  const container2 = createExportContainer(exportWidth, exportHeight);
21736
- renderInfra2(container2, infraLayout, effectivePalette2, theme === "dark", infraParsed.title, infraParsed.titleLineNumber, infraParsed.tagGroups, null, false, null, null, true);
22233
+ renderInfra2(container2, infraLayout, effectivePalette2, theme === "dark", infraParsed.title, infraParsed.titleLineNumber, infraParsed.tagGroups, activeTagGroup, false, null, null, true);
21737
22234
  const infraSvg = container2.querySelector("svg");
21738
22235
  if (infraSvg) {
21739
22236
  infraSvg.setAttribute("width", String(exportWidth));
@@ -21777,7 +22274,8 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21777
22274
  const seqParsed = parseSequenceDgmo2(content);
21778
22275
  if (seqParsed.error || seqParsed.participants.length === 0) return "";
21779
22276
  renderSequenceDiagram2(container, seqParsed, effectivePalette, isDark, void 0, {
21780
- exportWidth: EXPORT_WIDTH
22277
+ exportWidth: EXPORT_WIDTH,
22278
+ activeTagGroup: options?.tagGroup
21781
22279
  });
21782
22280
  } else if (parsed.type === "wordcloud") {
21783
22281
  await renderWordCloudAsync(container, parsed, effectivePalette, isDark, dims);
@@ -21791,7 +22289,7 @@ async function renderForExport(content, theme, palette, orgExportState, options)
21791
22289
  isDark,
21792
22290
  void 0,
21793
22291
  dims,
21794
- orgExportState?.activeTagGroup,
22292
+ orgExportState?.activeTagGroup ?? options?.tagGroup,
21795
22293
  orgExportState?.swimlaneTagGroup
21796
22294
  );
21797
22295
  } else if (parsed.type === "venn") {
@@ -21815,6 +22313,7 @@ var init_d3 = __esm({
21815
22313
  init_diagnostics();
21816
22314
  init_parsing();
21817
22315
  init_tag_groups();
22316
+ init_legend_constants();
21818
22317
  DEFAULT_CLOUD_OPTIONS = {
21819
22318
  rotate: "none",
21820
22319
  max: 0,
@@ -21990,7 +22489,7 @@ async function render(content, options) {
21990
22489
  c4Level: options?.c4Level,
21991
22490
  c4System: options?.c4System,
21992
22491
  c4Container: options?.c4Container,
21993
- scenario: options?.scenario
22492
+ tagGroup: options?.tagGroup
21994
22493
  });
21995
22494
  }
21996
22495