@diagrammo/dgmo 0.33.0 → 0.35.0

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.
Files changed (59) hide show
  1. package/.claude/commands/dgmo.md +17 -32
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +1 -1
  5. package/dist/advanced.cjs +791 -614
  6. package/dist/advanced.d.cts +8 -51
  7. package/dist/advanced.d.ts +8 -51
  8. package/dist/advanced.js +791 -615
  9. package/dist/auto.cjs +793 -292
  10. package/dist/auto.js +95 -95
  11. package/dist/auto.mjs +793 -292
  12. package/dist/cli.cjs +145 -328
  13. package/dist/index.cjs +792 -291
  14. package/dist/index.js +792 -291
  15. package/docs/ai-integration.md +46 -26
  16. package/docs/language-reference.md +2 -2
  17. package/package.json +3 -3
  18. package/src/advanced.ts +0 -12
  19. package/src/boxes-and-lines/label-placement.ts +339 -0
  20. package/src/boxes-and-lines/layout-search.ts +16 -2
  21. package/src/boxes-and-lines/layout.ts +32 -7
  22. package/src/boxes-and-lines/renderer.ts +67 -139
  23. package/src/c4/parser.ts +15 -6
  24. package/src/chart.ts +1 -1
  25. package/src/class/parser.ts +6 -2
  26. package/src/cli-banner.ts +2 -3
  27. package/src/cli.ts +539 -1223
  28. package/src/cycle/parser.ts +4 -1
  29. package/src/dgmo-router.ts +16 -2
  30. package/src/diagnostics.ts +0 -0
  31. package/src/echarts.ts +5 -1
  32. package/src/er/parser.ts +5 -1
  33. package/src/gantt/parser.ts +13 -4
  34. package/src/graph/flowchart-parser.ts +5 -1
  35. package/src/graph/state-parser.ts +5 -1
  36. package/src/infra/parser.ts +10 -6
  37. package/src/journey-map/parser.ts +5 -2
  38. package/src/kanban/parser.ts +6 -2
  39. package/src/map/context-labels.ts +29 -19
  40. package/src/map/layout.ts +403 -108
  41. package/src/map/legend-band.ts +28 -0
  42. package/src/map/parser.ts +13 -4
  43. package/src/map/renderer.ts +74 -23
  44. package/src/map/resolver.ts +2 -2
  45. package/src/org/parser.ts +8 -2
  46. package/src/pyramid/parser.ts +4 -1
  47. package/src/raci/parser.ts +4 -1
  48. package/src/ring/parser.ts +4 -1
  49. package/src/sequence/parser.ts +6 -3
  50. package/src/sitemap/parser.ts +12 -3
  51. package/src/tech-radar/parser.ts +6 -3
  52. package/src/utils/parsing.ts +2 -2
  53. package/src/utils/tag-groups.ts +3 -3
  54. package/src/visualizations/parse.ts +9 -3
  55. package/src/migrate/data-comma.ts +0 -117
  56. package/src/migrate/embedded.ts +0 -238
  57. package/src/migrate/index.ts +0 -295
  58. package/src/migrate/line-classifier.ts +0 -128
  59. package/src/migrate/line-transformer.ts +0 -218
package/dist/index.js CHANGED
@@ -35,6 +35,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
  function makeDgmoError(line11, message, severity = "error", code) {
36
36
  return code !== void 0 ? { line: line11, message, severity, code } : { line: line11, message, severity };
37
37
  }
38
+ function dedupeDiagnostics(diagnostics) {
39
+ const seen = /* @__PURE__ */ new Set();
40
+ const out = [];
41
+ for (const d of diagnostics) {
42
+ const key = `${d.line}\0${d.column ?? ""}\0${d.severity}\0${d.code ?? ""}\0${d.message}`;
43
+ if (seen.has(key)) continue;
44
+ seen.add(key);
45
+ out.push(d);
46
+ }
47
+ return out;
48
+ }
38
49
  function formatDgmoError(err) {
39
50
  return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
40
51
  }
@@ -92,7 +103,7 @@ function vennAliasKeywordRemovedMessage(args) {
92
103
  return `Venn 'alias' keyword was removed. Use 'as' instead \u2014 '${args.name} as ${args.alias}'.`;
93
104
  }
94
105
  function pipeOperatorRemovedMessage() {
95
- return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'. Run 'dgmo migrate'`;
106
+ return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'`;
96
107
  }
97
108
  function ganttBarePercentRemovedMessage(percent) {
98
109
  return `Bare-percent progress shorthand '| ${percent}' was removed. Use 'progress: ${percent.replace(/%$/, "")}' (integer 0\u2013100, no '%').`;
@@ -1148,7 +1159,7 @@ var init_parsing = __esm({
1148
1159
  "no-title",
1149
1160
  "no-notes"
1150
1161
  ]);
1151
- MULTIPLE_PIPE_ERROR = 'Use a single "|" to start metadata, then separate items with commas.';
1162
+ MULTIPLE_PIPE_ERROR = "'|' is no longer metadata syntax \u2014 put metadata on the same line as 'key: value, key2: value2' (no pipes), per \xA71.4.";
1152
1163
  RESERVED_KEY_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
1153
1164
  AS_ALIAS_RE = /\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})$/;
1154
1165
  POTENTIAL_META_RE = /\b([\w-]+)\s*:/;
@@ -1329,12 +1340,10 @@ function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1329
1340
  );
1330
1341
  if (!isPrefix) {
1331
1342
  const defined = group.entries.map((e) => e.value);
1332
- let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1343
+ let msg = `Unknown value '${value}' for tag group '${group.name}' \u2014 use one of: ${defined.join(", ")}`;
1333
1344
  const hint = suggestFn?.(value, defined);
1334
1345
  if (hint) {
1335
1346
  msg += `. ${hint}`;
1336
- } else {
1337
- msg += ` \u2014 defined values: ${defined.join(", ")}`;
1338
1347
  }
1339
1348
  pushWarning(entity.lineNumber, msg);
1340
1349
  }
@@ -2110,7 +2119,10 @@ function parseVisualizationFull(content, palette) {
2110
2119
  lineNumber
2111
2120
  });
2112
2121
  } else {
2113
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
2122
+ warn(
2123
+ lineNumber,
2124
+ `Unrecognized era entry: "${line11}" \u2014 use '<start> -> <end> Label [color]' (e.g. '1716 -> 1717 Reign').`
2125
+ );
2114
2126
  }
2115
2127
  continue;
2116
2128
  }
@@ -2138,7 +2150,10 @@ function parseVisualizationFull(content, palette) {
2138
2150
  lineNumber
2139
2151
  });
2140
2152
  } else {
2141
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
2153
+ warn(
2154
+ lineNumber,
2155
+ `Unrecognized marker entry: "${line11}" \u2014 use '<date> Label [color]' (e.g. '1776 Independence').`
2156
+ );
2142
2157
  }
2143
2158
  continue;
2144
2159
  }
@@ -2816,7 +2831,7 @@ function parseVisualizationFull(content, palette) {
2816
2831
  result.diagnostics.push(
2817
2832
  makeDgmoError(
2818
2833
  ov.lineNumber,
2819
- `Intersection references unknown set or alias "${ref}"`
2834
+ `Intersection references unknown set or alias "${ref}" \u2014 declare the set first on its own line ('Name [as <alias>] [color]', e.g. 'Navigation as nav blue'), then reference it by that name or alias. (\xA716.4)`
2820
2835
  )
2821
2836
  );
2822
2837
  if (!result.error)
@@ -6260,10 +6275,10 @@ function parseSequenceDgmo(content, palette) {
6260
6275
  const rawInside = fallbackMatch[1];
6261
6276
  const pipeIdx = rawInside.indexOf("|");
6262
6277
  const cleanName = rawInside.substring(0, pipeIdx).trim().replace(/\([^)]*\)$/, "").trim();
6263
- const metaPart = rawInside.substring(pipeIdx).trim();
6278
+ const metaPart = rawInside.substring(pipeIdx).replace(/^\|\s*/, "").trim();
6264
6279
  pushError(
6265
6280
  lineNumber,
6266
- `Pipe metadata must go outside brackets \u2014 use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`
6281
+ `Metadata goes after the bracket with no pipe \u2014 use '[${cleanName}] ${metaPart}' (e.g. '[Backend] t: Product'), not '[${rawInside.trim()}]'`
6267
6282
  );
6268
6283
  continue;
6269
6284
  }
@@ -6873,7 +6888,7 @@ function parseSequenceDgmo(content, palette) {
6873
6888
  }
6874
6889
  pushWarning(
6875
6890
  lineNumber,
6876
- `Unexpected line: '${trimmed}'. Expected a message (A -> B), participant, section (== Name ==), or block keyword (if/loop/parallel).`
6891
+ `Unexpected line: '${trimmed}'. Expected a message ('A -> B' or 'A -label-> B'), participant ('Name' or 'Name is a actor'), note ('note right of A Text'), section ('== Name =='), or block ('if/loop/parallel <label>').`
6877
6892
  );
6878
6893
  }
6879
6894
  if (!hasExplicitChart && result.messages.length === 0) {
@@ -8035,7 +8050,11 @@ function parseFlowchart(content, palette) {
8035
8050
  const value = optMatch[2].trim();
8036
8051
  if (key === "no-color") {
8037
8052
  result.diagnostics.push(
8038
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8053
+ makeDgmoError(
8054
+ lineNumber,
8055
+ '"no-color" was removed \u2014 node colors are assigned automatically by shape; delete this line.',
8056
+ "warning"
8057
+ )
8039
8058
  );
8040
8059
  continue;
8041
8060
  }
@@ -8364,7 +8383,11 @@ function parseState(content, palette) {
8364
8383
  const value = optMatch[2].trim();
8365
8384
  if (key === "no-color") {
8366
8385
  result.diagnostics.push(
8367
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8386
+ makeDgmoError(
8387
+ lineNumber,
8388
+ '"no-color" was removed \u2014 state colors are assigned automatically; delete this line.',
8389
+ "warning"
8390
+ )
8368
8391
  );
8369
8392
  continue;
8370
8393
  }
@@ -8679,7 +8702,7 @@ function parseClassDiagram(content, palette) {
8679
8702
  result.diagnostics.push(
8680
8703
  makeDgmoError(
8681
8704
  lineNumber,
8682
- '"no-auto-color" has been removed.',
8705
+ '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.',
8683
8706
  "warning"
8684
8707
  )
8685
8708
  );
@@ -8794,7 +8817,11 @@ function parseClassDiagram(content, palette) {
8794
8817
  continue;
8795
8818
  }
8796
8819
  result.diagnostics.push(
8797
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
8820
+ makeDgmoError(
8821
+ lineNumber,
8822
+ `Unexpected line: '${trimmed}'. Classes start uppercase at indent 0 ('Ship'); members are indented and colon-typed ('+ name: string', '+ sail(): void'); relationships are indented arrows ('--|> Vessel'). (\xA710)`,
8823
+ "warning"
8824
+ )
8798
8825
  );
8799
8826
  }
8800
8827
  if (notes.length > 0) {
@@ -9272,7 +9299,11 @@ function parseERDiagram(content, palette) {
9272
9299
  continue;
9273
9300
  }
9274
9301
  result.diagnostics.push(
9275
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
9302
+ makeDgmoError(
9303
+ lineNumber,
9304
+ `Unexpected line: '${trimmed}'. A table is a bare name at indent 0 ('users'); columns are indented and SPACE-separated, no colon ('id int pk'); relationships are indented ('1--* orders'). (\xA79)`,
9305
+ "warning"
9306
+ )
9276
9307
  );
9277
9308
  }
9278
9309
  if (result.tables.length === 0 && !result.error) {
@@ -9616,7 +9647,10 @@ function parseOrg(content, palette) {
9616
9647
  const value = metadataMatch[2].trim();
9617
9648
  const parent = findMetadataParent(indent, indentStack);
9618
9649
  if (!parent) {
9619
- pushError(lineNumber, "Metadata has no parent node");
9650
+ pushError(
9651
+ lineNumber,
9652
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9653
+ );
9620
9654
  } else {
9621
9655
  parent.metadata = { ...parent.metadata, [key]: value };
9622
9656
  }
@@ -9634,7 +9668,10 @@ function parseOrg(content, palette) {
9634
9668
  );
9635
9669
  attachNode(node, indent, indentStack, result);
9636
9670
  } else {
9637
- pushError(lineNumber, "Metadata has no parent node");
9671
+ pushError(
9672
+ lineNumber,
9673
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9674
+ );
9638
9675
  }
9639
9676
  } else {
9640
9677
  const node = parseNodeLabel(
@@ -9983,7 +10020,10 @@ function parseSitemap(content, palette) {
9983
10020
  if (arrowInfo) {
9984
10021
  const source = findParentNode(indent, indentStack);
9985
10022
  if (!source) {
9986
- pushError(lineNumber, "Arrow has no source node");
10023
+ pushError(
10024
+ lineNumber,
10025
+ "Arrow '-> target' must be indented under its source page \u2014 no page precedes it."
10026
+ );
9987
10027
  } else {
9988
10028
  deferredArrows.push({
9989
10029
  sourceNode: source,
@@ -10053,7 +10093,10 @@ function parseSitemap(content, palette) {
10053
10093
  const value = metadataMatch[2].trim();
10054
10094
  const parent = findParentNode(indent, indentStack);
10055
10095
  if (!parent) {
10056
- pushError(lineNumber, "Metadata has no parent node");
10096
+ pushError(
10097
+ lineNumber,
10098
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10099
+ );
10057
10100
  } else {
10058
10101
  parent.metadata = { ...parent.metadata, [key]: value };
10059
10102
  }
@@ -10073,7 +10116,10 @@ function parseSitemap(content, palette) {
10073
10116
  const key = normalizeName(node.label);
10074
10117
  labelToNode.set(key, node);
10075
10118
  } else {
10076
- pushError(lineNumber, "Metadata has no parent node");
10119
+ pushError(
10120
+ lineNumber,
10121
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10122
+ );
10077
10123
  }
10078
10124
  } else {
10079
10125
  const descResult = tryStripDescriptionKeyword(trimmed);
@@ -11891,7 +11937,7 @@ function parseChart(content, palette) {
11891
11937
  result.diagnostics.push(
11892
11938
  makeDgmoError(
11893
11939
  lineNumber,
11894
- `'${trimmed}' \u2014 ## syntax is no longer supported. Use [Group] containers instead`
11940
+ `'${trimmed}' removed \u2014 use a bracket container '[${trimmed.replace(/^#+\s*/, "")}]' instead (## headers are no longer supported).`
11895
11941
  )
11896
11942
  );
11897
11943
  continue;
@@ -12870,7 +12916,11 @@ function parseExtendedChartFull(content, palette) {
12870
12916
  continue;
12871
12917
  }
12872
12918
  result.diagnostics.push(
12873
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
12919
+ makeDgmoError(
12920
+ lineNumber,
12921
+ `Unexpected line: '${trimmed}'. Expected a data row ('Label value', space-separated) or a known option.`,
12922
+ "warning"
12923
+ )
12874
12924
  );
12875
12925
  }
12876
12926
  const warn = (line11, message) => {
@@ -15228,7 +15278,10 @@ function parseKanban(content, palette) {
15228
15278
  continue;
15229
15279
  }
15230
15280
  if (!currentColumn) {
15231
- warn(lineNumber, "Card line found before any column");
15281
+ warn(
15282
+ lineNumber,
15283
+ "Card line found before any column. Declare a column first with '[Column Name]', then indent cards beneath it. (\xA711)"
15284
+ );
15232
15285
  continue;
15233
15286
  }
15234
15287
  if (currentCard && indent > cardBaseIndent) {
@@ -15367,7 +15420,7 @@ var init_parser13 = __esm({
15367
15420
  KNOWN_OPTIONS4 = /* @__PURE__ */ new Set(["hide", "active-tag"]);
15368
15421
  KNOWN_BOOLEANS3 = /* @__PURE__ */ new Set(["solid-fill", "no-title"]);
15369
15422
  REMOVED_BOOLEANS = {
15370
- "no-auto-color": '"no-auto-color" has been removed.'
15423
+ "no-auto-color": '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.'
15371
15424
  };
15372
15425
  }
15373
15426
  });
@@ -15604,7 +15657,7 @@ function parseC4(content, palette) {
15604
15657
  } else {
15605
15658
  pushError(
15606
15659
  lineNumber,
15607
- `"container ${refName}" must be inside a deployment node`
15660
+ `"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
15608
15661
  );
15609
15662
  }
15610
15663
  continue;
@@ -15659,7 +15712,10 @@ function parseC4(content, palette) {
15659
15712
  indent
15660
15713
  });
15661
15714
  } else {
15662
- pushError(lineNumber, `"${sectionType}" must be inside an element`);
15715
+ pushError(
15716
+ lineNumber,
15717
+ `"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
15718
+ );
15663
15719
  }
15664
15720
  continue;
15665
15721
  }
@@ -15686,7 +15742,10 @@ function parseC4(content, palette) {
15686
15742
  indent
15687
15743
  });
15688
15744
  } else {
15689
- pushError(lineNumber, `Group [${groupName}] must be inside an element`);
15745
+ pushError(
15746
+ lineNumber,
15747
+ `Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
15748
+ );
15690
15749
  }
15691
15750
  continue;
15692
15751
  }
@@ -16016,7 +16075,10 @@ function parseC4(content, palette) {
16016
16075
  }
16017
16076
  desc.push(descResult.text);
16018
16077
  } else {
16019
- pushError(lineNumber, `Unexpected content: "${trimmed}"`);
16078
+ pushError(
16079
+ lineNumber,
16080
+ `Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (\xA78)`
16081
+ );
16020
16082
  }
16021
16083
  }
16022
16084
  finalizeAutoTagColors(result.tagGroups, palette);
@@ -16078,7 +16140,7 @@ function validateRelationshipTargets(result, knownNames, pushWarning) {
16078
16140
  for (const el of elements) {
16079
16141
  for (const rel of el.relationships) {
16080
16142
  if (!knownNames.has(normalizeName(rel.target))) {
16081
- let msg = `Relationship target "${rel.target}" not found`;
16143
+ let msg = `Relationship target "${rel.target}" not found \u2014 declare it first (e.g. '${rel.target} is a container'), then reference it in the arrow`;
16082
16144
  const hint = suggest(normalizeName(rel.target), allNames);
16083
16145
  if (hint) msg += `. ${hint}`;
16084
16146
  pushWarning(rel.lineNumber, msg, "warning");
@@ -16106,7 +16168,7 @@ function validateDeploymentRefs(result, knownNames, pushWarning) {
16106
16168
  for (const node of nodes) {
16107
16169
  for (const ref of node.containerRefs) {
16108
16170
  if (!knownNames.has(normalizeName(ref))) {
16109
- let msg = `Deployment reference "container ${ref}" not found`;
16171
+ let msg = `Deployment reference "container ${ref}" not found \u2014 declare the container element first, then reference it under the deployment node`;
16110
16172
  const hint = suggest(normalizeName(ref), allNames);
16111
16173
  if (hint) msg += `. ${hint}`;
16112
16174
  pushWarning(node.lineNumber, msg, "warning");
@@ -16825,7 +16887,7 @@ function parseInfra(content) {
16825
16887
  }
16826
16888
  warn(
16827
16889
  lineNumber,
16828
- `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
16890
+ `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}' \u2014 a value is a bare word with an optional trailing color (e.g. 'Production red'); no ':' or '|'.`
16829
16891
  );
16830
16892
  continue;
16831
16893
  }
@@ -16898,7 +16960,7 @@ function parseInfra(content) {
16898
16960
  const n = deprecatedFanout[1];
16899
16961
  setError(
16900
16962
  lineNumber,
16901
- `'x${n}' fanout syntax is no longer supported \u2014 use '| fanout: ${n}' instead`
16963
+ `'x${n}' fanout syntax is no longer supported \u2014 use 'fanout: ${n}' instead`
16902
16964
  );
16903
16965
  continue;
16904
16966
  }
@@ -17072,8 +17134,10 @@ function parseInfra(content) {
17072
17134
  const hint = suggest(key, allKeys);
17073
17135
  const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
17074
17136
  if (hint || valueLooksNumeric) {
17075
- let msg = `Unknown property '${key}'.`;
17137
+ let msg = `Unknown infra property '${key}'.`;
17076
17138
  if (hint) msg += ` ${hint}`;
17139
+ else
17140
+ msg += ` Properties are colon-keyed 'key: value' from the known schema (e.g. latency-ms, max-rps, cache-hit, instances, concurrency).`;
17077
17141
  warn(lineNumber, msg);
17078
17142
  } else if (!currentNode.isEdge) {
17079
17143
  pushDescription(currentNode, trimmed);
@@ -17084,7 +17148,7 @@ function parseInfra(content) {
17084
17148
  if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
17085
17149
  warn(
17086
17150
  lineNumber,
17087
- `Property '${key}' is only valid on the entry point (Edge/Internet).`
17151
+ `Property '${key}' belongs on the entry point \u2014 declare it indented under an 'internet' or 'edge' node, not on '${currentNode.label}'.`
17088
17152
  );
17089
17153
  }
17090
17154
  const value = parsePropertyValue(rawVal);
@@ -17191,7 +17255,7 @@ function parseInfra(content) {
17191
17255
  }
17192
17256
  warn(
17193
17257
  lineNumber,
17194
- currentGroup ? `Unexpected line: '${trimmed}'. Expected an indented component name.` : `Unexpected line: '${trimmed}'. Expected a component name, [Group Name], tag group, or option.`
17258
+ currentGroup ? `Unexpected line: '${trimmed}'. Inside a group, indent a component name under the '[Group]'.` : `Unexpected line: '${trimmed}'. Expected a node ('Api Gateway'), a group ('[Backend]'), a tag group ('tag Tier as t'), or an option ('default-rps 100'); node properties go indented under a node as 'key: value'.`
17195
17259
  );
17196
17260
  }
17197
17261
  finishCurrentNode();
@@ -17567,7 +17631,10 @@ function parseGantt(content, palette) {
17567
17631
  continue;
17568
17632
  }
17569
17633
  if (COMMENT_RE.test(line11)) continue;
17570
- warn(lineNumber, `Unrecognized holiday entry: "${line11}"`);
17634
+ warn(
17635
+ lineNumber,
17636
+ `Unrecognized holiday entry: "${line11}". Use a date '2024-02-19 Presidents Day' or a range '2024-05-27 -> 2024-05-29 Memorial Weekend'. (\xA713.3)`
17637
+ );
17571
17638
  continue;
17572
17639
  }
17573
17640
  }
@@ -17593,7 +17660,10 @@ function parseGantt(content, palette) {
17593
17660
  lineNumber
17594
17661
  });
17595
17662
  } else {
17596
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
17663
+ warn(
17664
+ lineNumber,
17665
+ `Unrecognized era entry: "${line11}". Use 'YYYY-MM-DD -> YYYY-MM-DD Label [color]' (e.g. '2026-04-06 -> 2026-04-10 Conference purple'). (\xA713.5)`
17666
+ );
17597
17667
  }
17598
17668
  continue;
17599
17669
  }
@@ -17619,7 +17689,10 @@ function parseGantt(content, palette) {
17619
17689
  lineNumber
17620
17690
  });
17621
17691
  } else {
17622
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
17692
+ warn(
17693
+ lineNumber,
17694
+ `Unrecognized marker entry: "${line11}". Use 'YYYY-MM-DD Label [color]' (e.g. '2026-03-27 Board Review' or '2026-06-15 Release green'). (\xA713.6)`
17695
+ );
17623
17696
  }
17624
17697
  continue;
17625
17698
  }
@@ -17785,7 +17858,7 @@ function parseGantt(content, palette) {
17785
17858
  } else {
17786
17859
  softError(
17787
17860
  lineNumber,
17788
- `Arrow reference "${content2.name}" has no parent task context.`
17861
+ `Arrow '-> ${content2.name}' has no source task \u2014 indent it under the task it follows (e.g. 'Design 5bd' then indented '-> ${content2.name} 3bd'). (\xA713)`
17789
17862
  );
17790
17863
  }
17791
17864
  }
@@ -19005,7 +19078,10 @@ function parseMap(content, palette) {
19005
19078
  function handleTag(trimmed, line11) {
19006
19079
  const m = matchTagBlockHeading(trimmed);
19007
19080
  if (!m) {
19008
- pushError(line11, `Malformed tag declaration: "${trimmed}".`);
19081
+ pushError(
19082
+ line11,
19083
+ `Malformed tag declaration: "${trimmed}" \u2014 write 'tag <Group>' (optionally 'tag <Group> as <alias>'), e.g. 'tag Market as m', with indented 'Value color' entries below.`
19084
+ );
19009
19085
  return;
19010
19086
  }
19011
19087
  emitTagLegacyDiagnostic(m, line11, diagnostics);
@@ -19117,7 +19193,7 @@ function parseMap(content, palette) {
19117
19193
  if (!pos || pos.kind === "name" && !pos.name) {
19118
19194
  pushError(
19119
19195
  line11,
19120
- "route requires an origin: `route <origin> [style: arc]`."
19196
+ "route needs an origin place \u2014 `route <origin>` then indented legs (`-> dest`); origin may be a city name, IATA code, or `<lat> <lon>`. (\xA724B)"
19121
19197
  );
19122
19198
  return;
19123
19199
  }
@@ -19180,7 +19256,10 @@ function parseMap(content, palette) {
19180
19256
  function handleEdges(trimmed, line11) {
19181
19257
  const parts = trimmed.split(ARROW_SPLIT);
19182
19258
  if (parts.length < 3) {
19183
- pushError(line11, `Malformed edge: "${trimmed}".`);
19259
+ pushError(
19260
+ line11,
19261
+ `Malformed edge: "${trimmed}" \u2014 an edge is 'A -> B' (or 'A -label-> B', '~>' for arc, '--' undirected), both endpoints non-empty. (\xA724B)`
19262
+ );
19184
19263
  return;
19185
19264
  }
19186
19265
  const endpoints = [];
@@ -19203,7 +19282,10 @@ function parseMap(content, palette) {
19203
19282
  const from = endpoints[k];
19204
19283
  const to = endpoints[k + 1];
19205
19284
  if (!from || !to) {
19206
- pushError(line11, `Edge has an empty endpoint: "${trimmed}".`);
19285
+ pushError(
19286
+ line11,
19287
+ `Edge has an empty endpoint: "${trimmed}" \u2014 both sides of the arrow need a place, e.g. 'Paris -> Berlin'. (\xA724B)`
19288
+ );
19207
19289
  continue;
19208
19290
  }
19209
19291
  const isLast = k === links.length - 1;
@@ -21322,7 +21404,10 @@ function parseTechRadar(content) {
21322
21404
  }
21323
21405
  if (indent === 0 && trimmed.toLowerCase() === "rings") {
21324
21406
  if (result.rings.length > 0) {
21325
- warn(lineNumber, 'Duplicate "rings" block \u2014 using last one');
21407
+ warn(
21408
+ lineNumber,
21409
+ 'Duplicate "rings" block \u2014 only the last is kept; merge all ring names into one "rings" block.'
21410
+ );
21326
21411
  result.rings = [];
21327
21412
  }
21328
21413
  inRingsBlock = true;
@@ -21541,7 +21626,7 @@ function parseTechRadar(content) {
21541
21626
  result.diagnostics.push(
21542
21627
  makeDgmoError(
21543
21628
  lineNumber,
21544
- `Blip "${trimmed}" has no ring assignment. Place it under a ring section header or use: ${trimmed} | ring: RingName`
21629
+ `Blip "${trimmed}" has no ring. Put it under a ring section header, or add a colon-keyed ring \u2014 '${trimmed} ring: RingName' (per \xA720).`
21545
21630
  )
21546
21631
  );
21547
21632
  continue;
@@ -21550,7 +21635,7 @@ function parseTechRadar(content) {
21550
21635
  if (!trimmed.includes("|") && headerParsed && result.rings.length > 0) {
21551
21636
  warn(
21552
21637
  lineNumber,
21553
- `Unrecognized line "${trimmed}". Quadrant headers require pipe metadata: ${trimmed} | quadrant: top-left`
21638
+ `Unrecognized line "${trimmed}". A quadrant header is 'Name quadrant: <top-left|top-right|bottom-left|bottom-right>' (per \xA720).`
21554
21639
  );
21555
21640
  }
21556
21641
  }
@@ -21843,7 +21928,10 @@ function parseCycle(content) {
21843
21928
  currentNode.description.push(descLine);
21844
21929
  continue;
21845
21930
  }
21846
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
21931
+ warn(
21932
+ lineNum,
21933
+ `Indented line "${trimmed}" has no step above it \u2014 add a step on a non-indented line first, then indent its details. (\xA721)`
21934
+ );
21847
21935
  continue;
21848
21936
  }
21849
21937
  }
@@ -22146,7 +22234,7 @@ function parseJourneyMap(content, palette) {
22146
22234
  if (trimmed.includes("|") || !PHASE_RE.test(trimmed)) {
22147
22235
  warn(
22148
22236
  lineNumber,
22149
- "Steps outside phases will be ignored when phases are present"
22237
+ `Step "${trimmed}" sits outside any phase and is ignored \u2014 indent it under a '[Phase]' header (per \xA722).`
22150
22238
  );
22151
22239
  continue;
22152
22240
  }
@@ -22203,7 +22291,10 @@ function parseJourneyMap(content, palette) {
22203
22291
  continue;
22204
22292
  }
22205
22293
  if (indent > 0 && !currentPhase && hasPhases) {
22206
- warn(lineNumber, `Unexpected indented line outside of a phase`);
22294
+ warn(
22295
+ lineNumber,
22296
+ `Indented line outside any phase \u2014 add a '[Phase]' header above it, or unindent it to a top-level step. (\xA722)`
22297
+ );
22207
22298
  }
22208
22299
  }
22209
22300
  if (currentStep) {
@@ -22549,7 +22640,10 @@ function parsePyramid(content) {
22549
22640
  continue;
22550
22641
  }
22551
22642
  if (!currentLayer) {
22552
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22643
+ warn(
22644
+ lineNum,
22645
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA723)`
22646
+ );
22553
22647
  continue;
22554
22648
  }
22555
22649
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -22723,7 +22817,10 @@ function parseRing(content) {
22723
22817
  continue;
22724
22818
  }
22725
22819
  if (!currentLayer) {
22726
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22820
+ warn(
22821
+ lineNum,
22822
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA724)`
22823
+ );
22727
22824
  continue;
22728
22825
  }
22729
22826
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -23449,7 +23546,10 @@ function parseRaci(content, palette) {
23449
23546
  continue;
23450
23547
  }
23451
23548
  if (bodyStarted) {
23452
- errorAt(lineNumber, `Unexpected line: '${trimmed}'.`);
23549
+ errorAt(
23550
+ lineNumber,
23551
+ `Unexpected line: '${trimmed}'. In the body expect a '[Phase]' header, an indented task name, or an assignment 'Role: A R C I' under a task. (\xA724A)`
23552
+ );
23453
23553
  } else {
23454
23554
  warn(lineNumber, `Unrecognized directive: '${trimmed}'.`);
23455
23555
  }
@@ -23996,6 +24096,13 @@ function getAllChartTypes() {
23996
24096
  return chartTypes.map((c) => c.id);
23997
24097
  }
23998
24098
  function parseDgmo(content) {
24099
+ const result = parseDgmoUndeduped(content);
24100
+ return {
24101
+ diagnostics: dedupeDiagnostics(result.diagnostics),
24102
+ chartType: result.chartType
24103
+ };
24104
+ }
24105
+ function parseDgmoUndeduped(content) {
23999
24106
  const chartType = parseDgmoChartType(content);
24000
24107
  if (!chartType) {
24001
24108
  const colonDiag = detectColonChartType(content);
@@ -29062,63 +29169,6 @@ function ensureArrowMarkers(defs, colors) {
29062
29169
  ).attr("fill", color);
29063
29170
  }
29064
29171
  }
29065
- function edgePolylineMidpoint(points) {
29066
- if (points.length === 0) return { x: 0, y: 0 };
29067
- if (points.length === 1) return { x: points[0].x, y: points[0].y };
29068
- let total = 0;
29069
- const segLen = [];
29070
- for (let k = 1; k < points.length; k++) {
29071
- const len = Math.hypot(
29072
- points[k].x - points[k - 1].x,
29073
- points[k].y - points[k - 1].y
29074
- );
29075
- segLen.push(len);
29076
- total += len;
29077
- }
29078
- let half = total / 2;
29079
- for (let k = 1; k < points.length; k++) {
29080
- const len = segLen[k - 1];
29081
- if (half <= len || k === points.length - 1) {
29082
- const t = len === 0 ? 0 : Math.min(1, half / len);
29083
- return {
29084
- x: points[k - 1].x + (points[k].x - points[k - 1].x) * t,
29085
- y: points[k - 1].y + (points[k].y - points[k - 1].y) * t
29086
- };
29087
- }
29088
- half -= len;
29089
- }
29090
- const last = points[points.length - 1];
29091
- return { x: last.x, y: last.y };
29092
- }
29093
- function resolveEdgeLabelOverlaps(labels) {
29094
- const MAX_PASSES = 8;
29095
- const PAD = 4;
29096
- for (let pass = 0; pass < MAX_PASSES; pass++) {
29097
- let moved = false;
29098
- for (let i = 0; i < labels.length; i++) {
29099
- for (let j = i + 1; j < labels.length; j++) {
29100
- const a = labels[i];
29101
- const b = labels[j];
29102
- const dx = Math.abs(a.x - b.x);
29103
- const dy = Math.abs(a.y - b.y);
29104
- const overlapX = (a.width + b.width) / 2 + PAD - dx;
29105
- const overlapY = (a.height + b.height) / 2 + PAD - dy;
29106
- if (overlapX > 0 && overlapY > 0) {
29107
- const shift = overlapY / 2 + 1;
29108
- if (a.y < b.y) {
29109
- a.y -= shift;
29110
- b.y += shift;
29111
- } else {
29112
- a.y += shift;
29113
- b.y -= shift;
29114
- }
29115
- moved = true;
29116
- }
29117
- }
29118
- }
29119
- if (!moved) break;
29120
- }
29121
- }
29122
29172
  function renderBoxesAndLines(container, parsed, layout, palette, isDark, options) {
29123
29173
  const {
29124
29174
  onClickItem,
@@ -29325,8 +29375,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29325
29375
  groupG.append("text").attr("class", "bl-group-label").attr("x", gx + group.width / 2).attr("y", gy + 18).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", sGroupLabelFontSize).attr("font-weight", "600").attr("fill", palette.text).text(group.label);
29326
29376
  }
29327
29377
  }
29328
- const labelPositions = [];
29329
- const edgeGroups = /* @__PURE__ */ new Map();
29330
29378
  for (let i = 0; i < layout.edges.length; i++) {
29331
29379
  const le = layout.edges[i];
29332
29380
  const color = edgeColorMap.get(i) ?? palette.textMuted;
@@ -29345,7 +29393,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29345
29393
  const nodeLayout = layoutNodeMap.get(le.source);
29346
29394
  if (nodeLayout) {
29347
29395
  const edgeG2 = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
29348
- edgeGroups.set(i, edgeG2);
29349
29396
  const markerId2 = `bl-arrow-${color.replace("#", "")}`;
29350
29397
  const cx = nodeLayout.x;
29351
29398
  const cy = nodeLayout.y;
@@ -29394,7 +29441,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29394
29441
  }
29395
29442
  if (points.length < 2) continue;
29396
29443
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
29397
- edgeGroups.set(i, edgeG);
29398
29444
  const markerId = `bl-arrow-${color.replace("#", "")}`;
29399
29445
  const gen = le.straight ? lineGeneratorStraight : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
29400
29446
  const path = edgeG.append("path").attr("class", "bl-edge").attr("d", gen(points) ?? "").attr("fill", "none").attr("stroke", color).attr("stroke-width", sEdgeStrokeWidth).attr("marker-end", `url(#${markerId})`);
@@ -29402,31 +29448,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29402
29448
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
29403
29449
  path.attr("marker-start", `url(#${revId})`);
29404
29450
  }
29405
- if (le.label && le.points.length > 0) {
29406
- const lw = measureText(le.label, sEdgeLabelFontSize);
29407
- const labelH = sEdgeLabelFontSize + 6;
29408
- const mid = edgePolylineMidpoint(le.points);
29409
- let ly = mid.y;
29410
- if (le.parallelCount > 1 && le.yOffset !== 0) {
29411
- ly += (le.yOffset < 0 ? -labelH : labelH) * 0.5;
29412
- }
29413
- labelPositions.push({
29414
- x: mid.x,
29415
- y: ly,
29416
- width: lw + 8,
29417
- height: labelH,
29418
- idx: i
29419
- });
29420
- }
29421
- }
29422
- resolveEdgeLabelOverlaps(labelPositions);
29423
- for (const lp of labelPositions) {
29424
- const le = layout.edges[lp.idx];
29425
- if (!le.label) continue;
29426
- const edgeG = edgeGroups.get(lp.idx);
29427
- const target = edgeG ?? diagramG;
29428
- target.append("rect").attr("x", lp.x - lp.width / 2).attr("y", lp.y - lp.height / 2).attr("width", lp.width).attr("height", lp.height).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
29429
- target.append("text").attr("x", lp.x).attr("y", lp.y + sEdgeLabelFontSize / 3).attr("text-anchor", "middle").attr("font-size", sEdgeLabelFontSize).attr("fill", palette.textMuted).text(le.label);
29430
29451
  }
29431
29452
  for (const ln of layout.nodes) {
29432
29453
  const node = nodeMap.get(ln.label);
@@ -29477,8 +29498,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29477
29498
  nodeG.append("rect").attr("x", x).attr("y", y).attr("width", ln.width).attr("height", ln.height).attr("rx", NODE_RX).attr("ry", NODE_RX).attr("fill", colors.fill).attr("stroke", colors.stroke).attr("stroke-width", sNodeStrokeWidth);
29478
29499
  const desc = node.description;
29479
29500
  if (desc && desc.length > 0 && !hideDescriptions) {
29480
- const MAX_LABEL_LINES3 = 3;
29481
- const fitted = fitLabelToHeader(node.label, ln.width, MAX_LABEL_LINES3);
29501
+ const MAX_LABEL_LINES4 = 3;
29502
+ const fitted = fitLabelToHeader(node.label, ln.width, MAX_LABEL_LINES4);
29482
29503
  const labelLines = fitted.lines;
29483
29504
  const labelLineH = fitted.fontSize * 1.3;
29484
29505
  const labelTotalH = labelLines.length * labelLineH;
@@ -29612,6 +29633,35 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29612
29633
  }
29613
29634
  }
29614
29635
  }
29636
+ const labelLayer = diagramG.append("g").attr("class", "bl-edge-labels");
29637
+ const labelLineHeight = sEdgeLabelFontSize * 1.3;
29638
+ for (const le of layout.edges) {
29639
+ if (!le.labelLines || le.labelLines.length === 0) continue;
29640
+ if (le.labelX === void 0 || le.labelY === void 0) continue;
29641
+ const lx = le.labelX;
29642
+ const ly = le.labelY;
29643
+ if (hidden.size > 0) {
29644
+ let isHidden = false;
29645
+ for (const [groupKey, hiddenVals] of hidden) {
29646
+ const val = le.metadata[groupKey];
29647
+ if (val && hiddenVals.has(val.toLowerCase())) {
29648
+ isHidden = true;
29649
+ break;
29650
+ }
29651
+ }
29652
+ if (isHidden) continue;
29653
+ }
29654
+ const lw = le.labelWidth ?? 0;
29655
+ const lh = le.labelHeight ?? sEdgeLabelFontSize + 6;
29656
+ const lines = le.labelLines;
29657
+ const labelG = labelLayer.append("g").attr("class", "bl-edge-label").attr("data-line-number", String(le.lineNumber));
29658
+ labelG.append("rect").attr("x", lx - lw / 2).attr("y", ly - lh / 2).attr("width", lw).attr("height", lh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.72);
29659
+ const text = labelG.append("text").attr("x", lx).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", sEdgeLabelFontSize).attr("fill", palette.textMuted);
29660
+ const firstY = ly - (lines.length - 1) / 2 * labelLineHeight + sEdgeLabelFontSize / 3;
29661
+ lines.forEach((line11, k) => {
29662
+ text.append("tspan").attr("x", lx).attr("y", firstY + k * labelLineHeight).text(line11);
29663
+ });
29664
+ }
29615
29665
  const hasDescriptions = parsed.nodes.some(
29616
29666
  (n) => n.description && n.description.length > 0
29617
29667
  );
@@ -29726,6 +29776,222 @@ var init_renderer6 = __esm({
29726
29776
  }
29727
29777
  });
29728
29778
 
29779
+ // src/boxes-and-lines/label-placement.ts
29780
+ function measureEdgeLabel(label, fontSize = EDGE_LABEL_FONT_SIZE6, maxWidth = LABEL_MAX_WIDTH) {
29781
+ let lines = wrapTextToWidth(label, fontSize, maxWidth, { hardBreak: true });
29782
+ if (lines.length > MAX_LABEL_LINES) {
29783
+ const kept = lines.slice(0, MAX_LABEL_LINES - 1);
29784
+ const rest = lines.slice(MAX_LABEL_LINES - 1).join(" ");
29785
+ kept.push(truncateText(rest, fontSize, maxWidth));
29786
+ lines = kept;
29787
+ }
29788
+ let textW = 0;
29789
+ for (const ln of lines) {
29790
+ const w = measureText(ln, fontSize);
29791
+ if (w > textW) textW = w;
29792
+ }
29793
+ return {
29794
+ lines,
29795
+ width: textW + 2 * H_PAD,
29796
+ height: lines.length * fontSize * LABEL_LINE_HEIGHT + 2 * V_PAD
29797
+ };
29798
+ }
29799
+ function edgePolylineMidpoint(points) {
29800
+ return pointAtArcFraction(points, 0.5).p;
29801
+ }
29802
+ function pointAtArcFraction(points, t) {
29803
+ if (points.length === 0) return { p: { x: 0, y: 0 }, nx: 0, ny: 1 };
29804
+ if (points.length === 1)
29805
+ return { p: { x: points[0].x, y: points[0].y }, nx: 0, ny: 1 };
29806
+ const segLen = [];
29807
+ let total = 0;
29808
+ for (let k = 1; k < points.length; k++) {
29809
+ const len = Math.hypot(
29810
+ points[k].x - points[k - 1].x,
29811
+ points[k].y - points[k - 1].y
29812
+ );
29813
+ segLen.push(len);
29814
+ total += len;
29815
+ }
29816
+ let want = total * Math.max(0, Math.min(1, t));
29817
+ for (let k = 1; k < points.length; k++) {
29818
+ const len = segLen[k - 1];
29819
+ if (want <= len || k === points.length - 1) {
29820
+ const f = len === 0 ? 0 : Math.min(1, want / len);
29821
+ const ax = points[k - 1].x;
29822
+ const ay = points[k - 1].y;
29823
+ const bx = points[k].x;
29824
+ const by = points[k].y;
29825
+ const dx = bx - ax;
29826
+ const dy = by - ay;
29827
+ const dlen = Math.hypot(dx, dy) || 1;
29828
+ return {
29829
+ p: { x: ax + dx * f, y: ay + dy * f },
29830
+ nx: -dy / dlen,
29831
+ ny: dx / dlen
29832
+ };
29833
+ }
29834
+ want -= len;
29835
+ }
29836
+ const last = points[points.length - 1];
29837
+ return { p: { x: last.x, y: last.y }, nx: 0, ny: 1 };
29838
+ }
29839
+ function rectFromCenter(cx, cy, w, h) {
29840
+ return {
29841
+ minX: cx - w / 2,
29842
+ minY: cy - h / 2,
29843
+ maxX: cx + w / 2,
29844
+ maxY: cy + h / 2
29845
+ };
29846
+ }
29847
+ function overlapsAny(cx, cy, w, h, obstacles, pad2) {
29848
+ const minX = cx - w / 2 - pad2;
29849
+ const minY = cy - h / 2 - pad2;
29850
+ const maxX = cx + w / 2 + pad2;
29851
+ const maxY = cy + h / 2 + pad2;
29852
+ for (const o of obstacles) {
29853
+ if (minX < o.maxX && maxX > o.minX && minY < o.maxY && maxY > o.minY)
29854
+ return true;
29855
+ }
29856
+ return false;
29857
+ }
29858
+ function slideFractions() {
29859
+ const out = [0.5];
29860
+ for (let k = 1; k <= SLIDE_SAMPLES; k++) {
29861
+ const off = k * 0.08;
29862
+ if (0.5 - off >= 0.12) out.push(0.5 - off);
29863
+ if (0.5 + off <= 0.88) out.push(0.5 + off);
29864
+ }
29865
+ return out;
29866
+ }
29867
+ function findClearPosition(w, h, points, obstacles) {
29868
+ const ts = slideFractions();
29869
+ for (const t of ts) {
29870
+ const { p } = pointAtArcFraction(points, t);
29871
+ if (!overlapsAny(p.x, p.y, w, h, obstacles, BOX_CLEAR_PAD)) return p;
29872
+ }
29873
+ for (let mag = PERP_STEP; mag <= PERP_MAX; mag += PERP_STEP) {
29874
+ for (const t of ts) {
29875
+ const { p, nx, ny } = pointAtArcFraction(points, t);
29876
+ for (const sign of [-1, 1]) {
29877
+ const x = p.x + nx * mag * sign;
29878
+ const y = p.y + ny * mag * sign;
29879
+ if (!overlapsAny(x, y, w, h, obstacles, BOX_CLEAR_PAD)) return { x, y };
29880
+ }
29881
+ }
29882
+ }
29883
+ return null;
29884
+ }
29885
+ function resolveLabelOverlaps(boxes) {
29886
+ const MAX_PASSES = 8;
29887
+ const PAD = 4;
29888
+ for (let pass = 0; pass < MAX_PASSES; pass++) {
29889
+ let moved = false;
29890
+ for (let i = 0; i < boxes.length; i++) {
29891
+ for (let j = i + 1; j < boxes.length; j++) {
29892
+ const a = boxes[i];
29893
+ const b = boxes[j];
29894
+ const dx = Math.abs(a.cx - b.cx);
29895
+ const dy = Math.abs(a.cy - b.cy);
29896
+ const overlapX = (a.w + b.w) / 2 + PAD - dx;
29897
+ const overlapY = (a.h + b.h) / 2 + PAD - dy;
29898
+ if (overlapX > 0 && overlapY > 0) {
29899
+ const shift = overlapY / 2 + 1;
29900
+ if (a.cy < b.cy) {
29901
+ a.cy -= shift;
29902
+ b.cy += shift;
29903
+ } else {
29904
+ a.cy += shift;
29905
+ b.cy -= shift;
29906
+ }
29907
+ moved = true;
29908
+ }
29909
+ }
29910
+ }
29911
+ if (!moved) break;
29912
+ }
29913
+ }
29914
+ function placeEdgeLabels(layout, opts) {
29915
+ const fontSize = opts?.fontSize ?? EDGE_LABEL_FONT_SIZE6;
29916
+ const maxWidth = opts?.maxWidth ?? LABEL_MAX_WIDTH;
29917
+ const obstacles = [];
29918
+ for (const n of layout.nodes)
29919
+ obstacles.push(rectFromCenter(n.x, n.y, n.width, n.height));
29920
+ for (const g of layout.groups)
29921
+ if (g.collapsed)
29922
+ obstacles.push(rectFromCenter(g.x, g.y, g.width, g.height));
29923
+ const boxes = [];
29924
+ layout.edges.forEach((e, idx) => {
29925
+ if (!e.label || e.points.length < 2) return;
29926
+ const m = measureEdgeLabel(e.label, fontSize, maxWidth);
29927
+ const mid = edgePolylineMidpoint(e.points);
29928
+ let cy = mid.y;
29929
+ if (e.parallelCount > 1 && e.yOffset !== 0)
29930
+ cy += (e.yOffset < 0 ? -1 : 1) * (m.height / 2);
29931
+ boxes.push({
29932
+ edgeIdx: idx,
29933
+ cx: mid.x,
29934
+ cy,
29935
+ w: m.width,
29936
+ h: m.height,
29937
+ lines: m.lines,
29938
+ resolved: true
29939
+ });
29940
+ });
29941
+ for (const box of boxes) {
29942
+ if (!overlapsAny(box.cx, box.cy, box.w, box.h, obstacles, BOX_CLEAR_PAD))
29943
+ continue;
29944
+ const e = layout.edges[box.edgeIdx];
29945
+ const clear = findClearPosition(box.w, box.h, e.points, obstacles);
29946
+ if (clear) {
29947
+ box.cx = clear.x;
29948
+ box.cy = clear.y;
29949
+ } else {
29950
+ box.resolved = false;
29951
+ }
29952
+ }
29953
+ resolveLabelOverlaps(boxes);
29954
+ for (const box of boxes) {
29955
+ if (overlapsAny(box.cx, box.cy, box.w, box.h, obstacles, BOX_CLEAR_PAD))
29956
+ box.resolved = false;
29957
+ }
29958
+ const boxByIdx = /* @__PURE__ */ new Map();
29959
+ for (const b of boxes) boxByIdx.set(b.edgeIdx, b);
29960
+ const edges = layout.edges.map((e, idx) => {
29961
+ const b = boxByIdx.get(idx);
29962
+ if (!b) return e;
29963
+ return {
29964
+ ...e,
29965
+ labelX: b.cx,
29966
+ labelY: b.cy,
29967
+ labelWidth: b.w,
29968
+ labelHeight: b.h,
29969
+ labelLines: b.lines
29970
+ };
29971
+ });
29972
+ return {
29973
+ layout: { ...layout, edges },
29974
+ unresolved: boxes.filter((b) => !b.resolved).map((b) => b.edgeIdx)
29975
+ };
29976
+ }
29977
+ var EDGE_LABEL_FONT_SIZE6, LABEL_MAX_WIDTH, MAX_LABEL_LINES, LABEL_LINE_HEIGHT, H_PAD, V_PAD, BOX_CLEAR_PAD, PERP_STEP, PERP_MAX, SLIDE_SAMPLES;
29978
+ var init_label_placement = __esm({
29979
+ "src/boxes-and-lines/label-placement.ts"() {
29980
+ "use strict";
29981
+ init_text_measure();
29982
+ EDGE_LABEL_FONT_SIZE6 = 11;
29983
+ LABEL_MAX_WIDTH = 160;
29984
+ MAX_LABEL_LINES = 3;
29985
+ LABEL_LINE_HEIGHT = 1.3;
29986
+ H_PAD = 6;
29987
+ V_PAD = 3;
29988
+ BOX_CLEAR_PAD = 4;
29989
+ PERP_STEP = 8;
29990
+ PERP_MAX = 40;
29991
+ SLIDE_SAMPLES = 9;
29992
+ }
29993
+ });
29994
+
29729
29995
  // src/boxes-and-lines/layout-layered.ts
29730
29996
  function rng(s) {
29731
29997
  return () => {
@@ -31515,6 +31781,7 @@ function edgeLength(layout) {
31515
31781
  }
31516
31782
  async function layoutBoxesAndLinesSearch(parsed, collapseInfo, opts) {
31517
31783
  const hideDescriptions = opts?.hideDescriptions ?? false;
31784
+ const reserveEdgeLabels = opts?.reserveEdgeLabels ?? false;
31518
31785
  const onProgress = opts?.onProgress;
31519
31786
  const tick = onProgress ? () => new Promise((r) => setTimeout(r)) : () => void 0;
31520
31787
  const collapsedGroupLabels = /* @__PURE__ */ new Set();
@@ -31807,8 +32074,14 @@ async function layoutBoxesAndLinesSearch(parsed, collapseInfo, opts) {
31807
32074
  for (const e of ord(parsed.edges)) {
31808
32075
  const s = srcRep(e.source);
31809
32076
  const t = tgtRep(e.target);
31810
- if (s !== t && g.hasNode(s) && g.hasNode(t))
31811
- g.setEdge(s, t, {}, edgeKey(e));
32077
+ if (s !== t && g.hasNode(s) && g.hasNode(t)) {
32078
+ let edgeLabel = {};
32079
+ if (reserveEdgeLabels && e.label) {
32080
+ const m = measureEdgeLabel(e.label, EDGE_LABEL_FONT_SIZE6);
32081
+ edgeLabel = { width: m.width, height: m.height, labelpos: "c" };
32082
+ }
32083
+ g.setEdge(s, t, edgeLabel, edgeKey(e));
32084
+ }
31812
32085
  }
31813
32086
  dagre4.layout(g);
31814
32087
  const nodes = parsed.nodes.map((n2) => {
@@ -32037,6 +32310,7 @@ var init_layout_search = __esm({
32037
32310
  init_layout5();
32038
32311
  init_layout_layered();
32039
32312
  init_layout_grouped();
32313
+ init_label_placement();
32040
32314
  DEFAULT_LAMBDA = 4;
32041
32315
  ESCALATE_THRESHOLD = 4;
32042
32316
  ESCALATE_MAX_N = 45;
@@ -32095,9 +32369,9 @@ function estimateLabelLines(label, nodeWidth = NODE_WIDTH) {
32095
32369
  current = word;
32096
32370
  }
32097
32371
  }
32098
- if (lines <= MAX_LABEL_LINES) return Math.min(lines, MAX_LABEL_LINES);
32372
+ if (lines <= MAX_LABEL_LINES2) return Math.min(lines, MAX_LABEL_LINES2);
32099
32373
  }
32100
- return MAX_LABEL_LINES;
32374
+ return MAX_LABEL_LINES2;
32101
32375
  }
32102
32376
  function computeNodeSize(node, reserveValueRow) {
32103
32377
  if (!node.description || node.description.length === 0) {
@@ -32105,7 +32379,7 @@ function computeNodeSize(node, reserveValueRow) {
32105
32379
  }
32106
32380
  const w = DESC_NODE_WIDTH;
32107
32381
  const labelLines = estimateLabelLines(node.label, w);
32108
- const labelHeight = labelLines * 13 * LABEL_LINE_HEIGHT + LABEL_PAD;
32382
+ const labelHeight = labelLines * 13 * LABEL_LINE_HEIGHT2 + LABEL_PAD;
32109
32383
  const maxTextWidth = w - 24;
32110
32384
  let totalRenderedLines = 0;
32111
32385
  for (const line11 of node.description) {
@@ -32129,7 +32403,7 @@ function computeNodeSize(node, reserveValueRow) {
32129
32403
  }
32130
32404
  async function layoutBoxesAndLines(parsed, collapseInfo, layoutOptions) {
32131
32405
  const { layoutBoxesAndLinesSearch: layoutBoxesAndLinesSearch2 } = await Promise.resolve().then(() => (init_layout_search(), layout_search_exports));
32132
- const searched = await layoutBoxesAndLinesSearch2(parsed, collapseInfo, {
32406
+ const searchOpts = {
32133
32407
  ...layoutOptions?.hideDescriptions !== void 0 && {
32134
32408
  hideDescriptions: layoutOptions.hideDescriptions
32135
32409
  },
@@ -32139,12 +32413,23 @@ async function layoutBoxesAndLines(parsed, collapseInfo, layoutOptions) {
32139
32413
  ...layoutOptions?.onProgress !== void 0 && {
32140
32414
  onProgress: layoutOptions.onProgress
32141
32415
  }
32142
- });
32143
- return attachNotes(
32144
- applyParallelEdgeOffsets(searched),
32416
+ };
32417
+ const searched = await layoutBoxesAndLinesSearch2(
32145
32418
  parsed,
32146
- layoutOptions?.collapsedNotes
32147
- );
32419
+ collapseInfo,
32420
+ searchOpts
32421
+ );
32422
+ let placed = placeEdgeLabels(applyParallelEdgeOffsets(searched));
32423
+ if (placed.unresolved.length > 0) {
32424
+ const relaid = await layoutBoxesAndLinesSearch2(parsed, collapseInfo, {
32425
+ ...searchOpts,
32426
+ reserveEdgeLabels: true
32427
+ });
32428
+ const relaidPlaced = placeEdgeLabels(applyParallelEdgeOffsets(relaid));
32429
+ if (relaidPlaced.unresolved.length < placed.unresolved.length)
32430
+ placed = relaidPlaced;
32431
+ }
32432
+ return attachNotes(placed.layout, parsed, layoutOptions?.collapsedNotes);
32148
32433
  }
32149
32434
  function attachNotes(layout, parsed, collapsedNotes) {
32150
32435
  const notesSuppressed = parsed.options?.["no-notes"] === "on";
@@ -32212,6 +32497,7 @@ function attachNotes(layout, parsed, collapsedNotes) {
32212
32497
  points: e.points.map((pt) => ({ x: pt.x + shiftX, y: pt.y + shiftY })),
32213
32498
  ...e.labelX !== void 0 && { labelX: e.labelX + shiftX },
32214
32499
  ...e.labelY !== void 0 && { labelY: e.labelY + shiftY }
32500
+ // labelWidth/labelHeight/labelLines are shift-invariant — carried via spread.
32215
32501
  })) : layout.edges;
32216
32502
  const finalGroups = shifted ? layout.groups.map((grp) => ({
32217
32503
  ...grp,
@@ -32256,11 +32542,12 @@ function applyParallelEdgeOffsets(layout) {
32256
32542
  }))
32257
32543
  };
32258
32544
  }
32259
- var MARGIN5, MAX_PARALLEL_EDGES, PARALLEL_SPACING, PHI, NODE_HEIGHT, NODE_WIDTH, DESC_NODE_WIDTH, DESC_FONT_SIZE2, DESC_LINE_HEIGHT2, DESC_PADDING, SEPARATOR_GAP4, MAX_DESC_LINES2, MAX_LABEL_LINES, LABEL_LINE_HEIGHT, LABEL_PAD, VALUE_ROW_FONT, VALUE_ROW_H;
32545
+ var MARGIN5, MAX_PARALLEL_EDGES, PARALLEL_SPACING, PHI, NODE_HEIGHT, NODE_WIDTH, DESC_NODE_WIDTH, DESC_FONT_SIZE2, DESC_LINE_HEIGHT2, DESC_PADDING, SEPARATOR_GAP4, MAX_DESC_LINES2, MAX_LABEL_LINES2, LABEL_LINE_HEIGHT2, LABEL_PAD, VALUE_ROW_FONT, VALUE_ROW_H;
32260
32546
  var init_layout5 = __esm({
32261
32547
  "src/boxes-and-lines/layout.ts"() {
32262
32548
  "use strict";
32263
32549
  init_text_measure();
32550
+ init_label_placement();
32264
32551
  init_notes();
32265
32552
  MARGIN5 = 40;
32266
32553
  MAX_PARALLEL_EDGES = 5;
@@ -32274,8 +32561,8 @@ var init_layout5 = __esm({
32274
32561
  DESC_PADDING = 8;
32275
32562
  SEPARATOR_GAP4 = 4;
32276
32563
  MAX_DESC_LINES2 = 6;
32277
- MAX_LABEL_LINES = 3;
32278
- LABEL_LINE_HEIGHT = 1.3;
32564
+ MAX_LABEL_LINES2 = 3;
32565
+ LABEL_LINE_HEIGHT2 = 1.3;
32279
32566
  LABEL_PAD = 12;
32280
32567
  VALUE_ROW_FONT = 11;
32281
32568
  VALUE_ROW_H = SEPARATOR_GAP4 + VALUE_ROW_FONT * DESC_LINE_HEIGHT2 + DESC_PADDING;
@@ -32294,7 +32581,7 @@ function tokenize(text) {
32294
32581
  return tokens;
32295
32582
  }
32296
32583
  function tryWrap(tokens, maxWidth, fontSize, maxLines) {
32297
- const availWidth = maxWidth - H_PAD;
32584
+ const availWidth = maxWidth - H_PAD2;
32298
32585
  const lines = [];
32299
32586
  let currentLine = "";
32300
32587
  for (const token of tokens) {
@@ -32319,7 +32606,7 @@ function tryWrap(tokens, maxWidth, fontSize, maxLines) {
32319
32606
  return lines;
32320
32607
  }
32321
32608
  function truncateLastLine(lines, maxWidth, fontSize) {
32322
- const availWidth = maxWidth - H_PAD;
32609
+ const availWidth = maxWidth - H_PAD2;
32323
32610
  const result = [...lines];
32324
32611
  const last = result[result.length - 1];
32325
32612
  if (measureText(last, fontSize) > availWidth) {
@@ -32327,7 +32614,7 @@ function truncateLastLine(lines, maxWidth, fontSize) {
32327
32614
  }
32328
32615
  return result;
32329
32616
  }
32330
- function wrapText(text, maxWidth, baseFontSize, minFontSize, maxLines = MAX_LABEL_LINES2) {
32617
+ function wrapText(text, maxWidth, baseFontSize, minFontSize, maxLines = MAX_LABEL_LINES3) {
32331
32618
  if (!text) return { lines: [""], fontSize: baseFontSize };
32332
32619
  const tokens = tokenize(text);
32333
32620
  for (let fs = baseFontSize; fs >= minFontSize; fs--) {
@@ -32342,7 +32629,7 @@ function wrapText(text, maxWidth, baseFontSize, minFontSize, maxLines = MAX_LABE
32342
32629
  if (allLines.length > maxLines) {
32343
32630
  const last = truncated[truncated.length - 1];
32344
32631
  if (!last.endsWith("\u2026")) {
32345
- const availWidth = maxWidth - H_PAD;
32632
+ const availWidth = maxWidth - H_PAD2;
32346
32633
  if (measureText(last + "\u2026", minFontSize) > availWidth) {
32347
32634
  truncated[truncated.length - 1] = truncateText(
32348
32635
  last,
@@ -32369,7 +32656,7 @@ function computeNodeText(label, description, depth, nodeWidth, hideDescriptions)
32369
32656
  nodeWidth,
32370
32657
  baseFontSize,
32371
32658
  MIN_FONT_SIZE,
32372
- MAX_LABEL_LINES2
32659
+ MAX_LABEL_LINES3
32373
32660
  );
32374
32661
  const descLines = [];
32375
32662
  let descFontSize = DESC_FONT_SIZE3;
@@ -32397,14 +32684,14 @@ function computeNodeText(label, description, depth, nodeWidth, hideDescriptions)
32397
32684
  descFontSize
32398
32685
  };
32399
32686
  }
32400
- var H_PAD, MAX_LABEL_LINES2, MAX_DESC_LINES3, ROOT_FONT_SIZE, MIN_FONT_SIZE, FONT_STEP, DESC_FONT_SIZE3;
32687
+ var H_PAD2, MAX_LABEL_LINES3, MAX_DESC_LINES3, ROOT_FONT_SIZE, MIN_FONT_SIZE, FONT_STEP, DESC_FONT_SIZE3;
32401
32688
  var init_text_wrap = __esm({
32402
32689
  "src/mindmap/text-wrap.ts"() {
32403
32690
  "use strict";
32404
32691
  init_description_helpers();
32405
32692
  init_text_measure();
32406
- H_PAD = 16;
32407
- MAX_LABEL_LINES2 = 3;
32693
+ H_PAD2 = 16;
32694
+ MAX_LABEL_LINES3 = 3;
32408
32695
  MAX_DESC_LINES3 = 2;
32409
32696
  ROOT_FONT_SIZE = 17;
32410
32697
  MIN_FONT_SIZE = 9;
@@ -32791,7 +33078,7 @@ function nodeHeightS(node, hideDescriptions, sc) {
32791
33078
  hideDescriptions
32792
33079
  );
32793
33080
  const labelLineCount = text.labelLines.length;
32794
- const labelH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT : LABEL_LINE_HEIGHT2 * labelLineCount;
33081
+ const labelH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT : LABEL_LINE_HEIGHT3 * labelLineCount;
32795
33082
  let h = labelH + NODE_V_PAD;
32796
33083
  if (text.descLines.length > 0) {
32797
33084
  h += DESC_LINE_HEIGHT3 * text.descLines.length + 4;
@@ -32811,7 +33098,7 @@ function populateDepthCache(roots) {
32811
33098
  };
32812
33099
  walk(roots, 0);
32813
33100
  }
32814
- var ROOT_WIDTH, DEPTH1_WIDTH, LEAF_WIDTH, SINGLE_LABEL_HEIGHT, LABEL_LINE_HEIGHT2, DESC_LINE_HEIGHT3, NODE_V_PAD, H_GAP2, V_GAP2, MARGIN6, MULTI_ROOT_GAP, nodeDepthCache;
33101
+ var ROOT_WIDTH, DEPTH1_WIDTH, LEAF_WIDTH, SINGLE_LABEL_HEIGHT, LABEL_LINE_HEIGHT3, DESC_LINE_HEIGHT3, NODE_V_PAD, H_GAP2, V_GAP2, MARGIN6, MULTI_ROOT_GAP, nodeDepthCache;
32815
33102
  var init_layout6 = __esm({
32816
33103
  "src/mindmap/layout.ts"() {
32817
33104
  "use strict";
@@ -32822,7 +33109,7 @@ var init_layout6 = __esm({
32822
33109
  DEPTH1_WIDTH = 150;
32823
33110
  LEAF_WIDTH = 120;
32824
33111
  SINGLE_LABEL_HEIGHT = 28;
32825
- LABEL_LINE_HEIGHT2 = 18;
33112
+ LABEL_LINE_HEIGHT3 = 18;
32826
33113
  DESC_LINE_HEIGHT3 = 14;
32827
33114
  NODE_V_PAD = 10;
32828
33115
  H_GAP2 = 40;
@@ -33114,18 +33401,18 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
33114
33401
  descFontSize
33115
33402
  } = textLayout;
33116
33403
  const labelLineCount = labelLines.length;
33117
- const labelZoneH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT2 : LABEL_LINE_HEIGHT3 * labelLineCount;
33404
+ const labelZoneH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT2 : LABEL_LINE_HEIGHT4 * labelLineCount;
33118
33405
  const labelZoneHeight = showDesc ? labelZoneH : node.height;
33119
33406
  const centerX = node.x + node.width / 2;
33120
33407
  if (labelLineCount <= 1) {
33121
33408
  const labelY = node.y + labelZoneHeight / 2 + fontSize * 0.35;
33122
33409
  nodeG.append("text").attr("x", centerX).attr("y", labelY).attr("text-anchor", "middle").attr("font-size", fontSize).attr("font-weight", isRoot ? "bold" : "normal").attr("fill", onFillText).text(labelLines[0]);
33123
33410
  } else {
33124
- const blockH = LABEL_LINE_HEIGHT3 * (labelLineCount - 1);
33411
+ const blockH = LABEL_LINE_HEIGHT4 * (labelLineCount - 1);
33125
33412
  const firstBaselineY = node.y + labelZoneHeight / 2 - blockH / 2 + fontSize * 0.35;
33126
33413
  const textEl = nodeG.append("text").attr("x", centerX).attr("text-anchor", "middle").attr("font-size", fontSize).attr("font-weight", isRoot ? "bold" : "normal").attr("fill", onFillText);
33127
33414
  for (let i = 0; i < labelLines.length; i++) {
33128
- textEl.append("tspan").attr("x", centerX).attr("y", firstBaselineY + i * LABEL_LINE_HEIGHT3).text(labelLines[i]);
33415
+ textEl.append("tspan").attr("x", centerX).attr("y", firstBaselineY + i * LABEL_LINE_HEIGHT4).text(labelLines[i]);
33129
33416
  }
33130
33417
  }
33131
33418
  if (labelLines.length > 1 || labelLines[0] !== node.label) {
@@ -33200,7 +33487,7 @@ function renderMindmapForExport(content, theme, palette) {
33200
33487
  return extractExportSvg(container, theme);
33201
33488
  });
33202
33489
  }
33203
- var DIAGRAM_PADDING7, TITLE_HEIGHT4, SINGLE_LABEL_HEIGHT2, LABEL_LINE_HEIGHT3, DESC_LINE_HEIGHT4, NODE_RADIUS, ROOT_STROKE_WIDTH, DEPTH_COLOR_KEYS;
33490
+ var DIAGRAM_PADDING7, TITLE_HEIGHT4, SINGLE_LABEL_HEIGHT2, LABEL_LINE_HEIGHT4, DESC_LINE_HEIGHT4, NODE_RADIUS, ROOT_STROKE_WIDTH, DEPTH_COLOR_KEYS;
33204
33491
  var init_renderer7 = __esm({
33205
33492
  "src/mindmap/renderer.ts"() {
33206
33493
  "use strict";
@@ -33222,7 +33509,7 @@ var init_renderer7 = __esm({
33222
33509
  DIAGRAM_PADDING7 = 20;
33223
33510
  TITLE_HEIGHT4 = 30;
33224
33511
  SINGLE_LABEL_HEIGHT2 = 28;
33225
- LABEL_LINE_HEIGHT3 = 18;
33512
+ LABEL_LINE_HEIGHT4 = 18;
33226
33513
  DESC_LINE_HEIGHT4 = 14;
33227
33514
  NODE_RADIUS = 6;
33228
33515
  ROOT_STROKE_WIDTH = 2.5;
@@ -35891,7 +36178,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
35891
36178
  const labelText = edge.label ?? "";
35892
36179
  const techText = edge.technology ? `[${edge.technology}]` : "";
35893
36180
  const textW = Math.max(
35894
- measureText(labelText, EDGE_LABEL_FONT_SIZE6),
36181
+ measureText(labelText, EDGE_LABEL_FONT_SIZE7),
35895
36182
  measureText(techText, TECH_FONT_SIZE)
35896
36183
  );
35897
36184
  const bgW = textW + 12;
@@ -35902,7 +36189,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
35902
36189
  textY = midPt.y - 4;
35903
36190
  }
35904
36191
  if (labelText) {
35905
- edgeG.append("text").attr("x", midPt.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", edgeColor3).attr("font-size", EDGE_LABEL_FONT_SIZE6).attr("class", "c4-edge-label").text(labelText);
36192
+ edgeG.append("text").attr("x", midPt.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", edgeColor3).attr("font-size", EDGE_LABEL_FONT_SIZE7).attr("class", "c4-edge-label").text(labelText);
35906
36193
  }
35907
36194
  if (techText) {
35908
36195
  edgeG.append("text").attr("x", midPt.x).attr("y", labelText ? textY + 18 : textY + 4).attr("text-anchor", "middle").attr("fill", edgeColor3).attr("font-size", TECH_FONT_SIZE).attr("font-style", "italic").attr("class", "c4-edge-tech").text(techText);
@@ -36086,7 +36373,7 @@ function renderEdges(contentG, edges, palette, onClickItem, obstacleRects) {
36086
36373
  const labelText = edge.label ?? "";
36087
36374
  const techText = edge.technology ? `[${edge.technology}]` : "";
36088
36375
  const textW = Math.max(
36089
- measureText(labelText, EDGE_LABEL_FONT_SIZE6),
36376
+ measureText(labelText, EDGE_LABEL_FONT_SIZE7),
36090
36377
  measureText(techText, TECH_FONT_SIZE)
36091
36378
  );
36092
36379
  const bgW = textW + 12;
@@ -36104,7 +36391,7 @@ function renderEdges(contentG, edges, palette, onClickItem, obstacleRects) {
36104
36391
  });
36105
36392
  }
36106
36393
  }
36107
- placeEdgeLabels(pendingLabels, edges, obstacleRects);
36394
+ placeEdgeLabels2(pendingLabels, edges, obstacleRects);
36108
36395
  for (const lbl of pendingLabels) {
36109
36396
  lbl.edgeG.append("rect").attr("x", lbl.x - lbl.bgW / 2).attr("y", lbl.y - lbl.bgH / 2).attr("width", lbl.bgW).attr("height", lbl.bgH).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.9).attr("class", "c4-edge-label-bg");
36110
36397
  let textY = lbl.y;
@@ -36112,7 +36399,7 @@ function renderEdges(contentG, edges, palette, onClickItem, obstacleRects) {
36112
36399
  textY = lbl.y - 4;
36113
36400
  }
36114
36401
  if (lbl.labelText) {
36115
- lbl.edgeG.append("text").attr("x", lbl.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", lbl.edgeColor).attr("font-size", EDGE_LABEL_FONT_SIZE6).attr("class", "c4-edge-label").text(lbl.labelText);
36402
+ lbl.edgeG.append("text").attr("x", lbl.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", lbl.edgeColor).attr("font-size", EDGE_LABEL_FONT_SIZE7).attr("class", "c4-edge-label").text(lbl.labelText);
36116
36403
  }
36117
36404
  if (lbl.techText) {
36118
36405
  lbl.edgeG.append("text").attr("x", lbl.x).attr("y", lbl.labelText ? textY + 18 : textY + 4).attr("text-anchor", "middle").attr("fill", lbl.edgeColor).attr("font-size", TECH_FONT_SIZE).attr("font-style", "italic").attr("class", "c4-edge-tech").text(lbl.techText);
@@ -36193,7 +36480,7 @@ function tangentAt(points, t) {
36193
36480
  }
36194
36481
  return { x: 0, y: 1 };
36195
36482
  }
36196
- function placeEdgeLabels(labels, edges, obstacleRects) {
36483
+ function placeEdgeLabels2(labels, edges, obstacleRects) {
36197
36484
  if (labels.length === 0) return;
36198
36485
  const allPaths = edges.map((e) => e.points);
36199
36486
  const placedRects = [];
@@ -36731,7 +37018,7 @@ function renderC4DeploymentForExport(content, theme, palette) {
36731
37018
  document.body.removeChild(el);
36732
37019
  }
36733
37020
  }
36734
- var DIAGRAM_PADDING8, MAX_SCALE5, TITLE_HEIGHT6, TYPE_FONT_SIZE, NAME_FONT_SIZE2, DESC_FONT_SIZE5, DESC_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE6, TECH_FONT_SIZE, CARD_H_PAD4, CARD_V_PAD4, TYPE_LABEL_HEIGHT2, DIVIDER_GAP2, NAME_HEIGHT2, 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, lineGenerator4;
37021
+ var DIAGRAM_PADDING8, MAX_SCALE5, TITLE_HEIGHT6, TYPE_FONT_SIZE, NAME_FONT_SIZE2, DESC_FONT_SIZE5, DESC_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE7, TECH_FONT_SIZE, CARD_H_PAD4, CARD_V_PAD4, TYPE_LABEL_HEIGHT2, DIVIDER_GAP2, NAME_HEIGHT2, 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, lineGenerator4;
36735
37022
  var init_renderer9 = __esm({
36736
37023
  "src/c4/renderer.ts"() {
36737
37024
  "use strict";
@@ -36754,7 +37041,7 @@ var init_renderer9 = __esm({
36754
37041
  NAME_FONT_SIZE2 = 14;
36755
37042
  DESC_FONT_SIZE5 = 11;
36756
37043
  DESC_LINE_HEIGHT6 = 16;
36757
- EDGE_LABEL_FONT_SIZE6 = 11;
37044
+ EDGE_LABEL_FONT_SIZE7 = 11;
36758
37045
  TECH_FONT_SIZE = 10;
36759
37046
  CARD_H_PAD4 = 20;
36760
37047
  CARD_V_PAD4 = 14;
@@ -37379,7 +37666,7 @@ function renderFlowchart(container, graph, layout, palette, isDark, onClickItem,
37379
37666
  const sTitleFontSize = ctx.text(TITLE_FONT_SIZE);
37380
37667
  const sTitleY = ctx.structural(TITLE_Y);
37381
37668
  const sNodeFontSize = ctx.text(NODE_FONT_SIZE2);
37382
- const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE7);
37669
+ const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE8);
37383
37670
  const sEdgeStrokeWidth = ctx.structural(EDGE_STROKE_WIDTH);
37384
37671
  const sNodeStrokeWidth = ctx.structural(NODE_STROKE_WIDTH);
37385
37672
  const sArrowheadW = ctx.structural(ARROWHEAD_W3);
@@ -37605,7 +37892,7 @@ function renderFlowchartForExport(content, theme, palette) {
37605
37892
  document.body.removeChild(container);
37606
37893
  }
37607
37894
  }
37608
- var DIAGRAM_PADDING9, MAX_SCALE6, NODE_FONT_SIZE2, EDGE_LABEL_FONT_SIZE7, ARROWHEAD_W3, ARROWHEAD_H3, IO_SKEW, SUBROUTINE_INSET, DOC_WAVE_HEIGHT;
37895
+ var DIAGRAM_PADDING9, MAX_SCALE6, NODE_FONT_SIZE2, EDGE_LABEL_FONT_SIZE8, ARROWHEAD_W3, ARROWHEAD_H3, IO_SKEW, SUBROUTINE_INSET, DOC_WAVE_HEIGHT;
37609
37896
  var init_flowchart_renderer = __esm({
37610
37897
  "src/graph/flowchart-renderer.ts"() {
37611
37898
  "use strict";
@@ -37624,7 +37911,7 @@ var init_flowchart_renderer = __esm({
37624
37911
  DIAGRAM_PADDING9 = 20;
37625
37912
  MAX_SCALE6 = 3;
37626
37913
  NODE_FONT_SIZE2 = 13;
37627
- EDGE_LABEL_FONT_SIZE7 = 11;
37914
+ EDGE_LABEL_FONT_SIZE8 = 11;
37628
37915
  ARROWHEAD_W3 = 10;
37629
37916
  ARROWHEAD_H3 = 7;
37630
37917
  IO_SKEW = 15;
@@ -39059,7 +39346,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
39059
39346
  const midIdx = Math.floor(edge.points.length / 2);
39060
39347
  const midPt = edge.points[midIdx];
39061
39348
  if (midPt) {
39062
- const halfWidth = (measureText(edge.label, EDGE_LABEL_FONT_SIZE8) + 8) / 2;
39349
+ const halfWidth = (measureText(edge.label, EDGE_LABEL_FONT_SIZE9) + 8) / 2;
39063
39350
  if (midPt.x - halfWidth < minX) minX = midPt.x - halfWidth;
39064
39351
  if (midPt.x + halfWidth > maxX) maxX = midPt.x + halfWidth;
39065
39352
  }
@@ -39093,7 +39380,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
39093
39380
  height: totalHeight
39094
39381
  };
39095
39382
  }
39096
- var MIN_NODE_WIDTH2, NODE_HEADER_HEIGHT, META_LINE_HEIGHT5, NODE_SEPARATOR_GAP, NODE_PAD_BOTTOM, ROLE_DOT_ROW, COLLAPSE_BAR_HEIGHT3, NODE_FONT_SIZE3, META_FONT_SIZE5, EDGE_LABEL_FONT_SIZE8, PADDING_X3, GROUP_PADDING2, GROUP_HEADER_HEIGHT, EDGE_MARGIN, DISPLAY_KEYS, DISPLAY_NAMES, GROUP_GAP2;
39383
+ var MIN_NODE_WIDTH2, NODE_HEADER_HEIGHT, META_LINE_HEIGHT5, NODE_SEPARATOR_GAP, NODE_PAD_BOTTOM, ROLE_DOT_ROW, COLLAPSE_BAR_HEIGHT3, NODE_FONT_SIZE3, META_FONT_SIZE5, EDGE_LABEL_FONT_SIZE9, PADDING_X3, GROUP_PADDING2, GROUP_HEADER_HEIGHT, EDGE_MARGIN, DISPLAY_KEYS, DISPLAY_NAMES, GROUP_GAP2;
39097
39384
  var init_layout10 = __esm({
39098
39385
  "src/infra/layout.ts"() {
39099
39386
  "use strict";
@@ -39107,7 +39394,7 @@ var init_layout10 = __esm({
39107
39394
  COLLAPSE_BAR_HEIGHT3 = 6;
39108
39395
  NODE_FONT_SIZE3 = 13;
39109
39396
  META_FONT_SIZE5 = 10;
39110
- EDGE_LABEL_FONT_SIZE8 = 11;
39397
+ EDGE_LABEL_FONT_SIZE9 = 11;
39111
39398
  PADDING_X3 = 24;
39112
39399
  GROUP_PADDING2 = 20;
39113
39400
  GROUP_HEADER_HEIGHT = 24;
@@ -39214,7 +39501,7 @@ function buildScaledConstants(ctx) {
39214
39501
  sNodeFontSize: ctx.text(NODE_FONT_SIZE4),
39215
39502
  sMetaFontSize: ctx.text(META_FONT_SIZE6),
39216
39503
  sMetaLineHeight: ctx.structural(META_LINE_HEIGHT6),
39217
- sEdgeLabelFontSize: ctx.text(EDGE_LABEL_FONT_SIZE9),
39504
+ sEdgeLabelFontSize: ctx.text(EDGE_LABEL_FONT_SIZE10),
39218
39505
  sGroupLabelFontSize: ctx.text(GROUP_LABEL_FONT_SIZE2),
39219
39506
  sNodeBorderRadius: ctx.structural(NODE_BORDER_RADIUS),
39220
39507
  sEdgeStrokeWidth: ctx.structural(EDGE_STROKE_WIDTH),
@@ -39943,7 +40230,7 @@ function renderEdgeLabels(svg, edges, nodes, groups, palette, _isDark, animate,
39943
40230
  if (!midPt) continue;
39944
40231
  const labelText = edge.label;
39945
40232
  const g = svg.append("g").attr("class", animate ? "infra-edge-label" : "");
39946
- const textWidth = measureText(labelText, EDGE_LABEL_FONT_SIZE9) + 8;
40233
+ const textWidth = measureText(labelText, EDGE_LABEL_FONT_SIZE10) + 8;
39947
40234
  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);
39948
40235
  g.append("text").attr("x", midPt.x).attr("y", midPt.y + 4).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", sc.sEdgeLabelFontSize).attr("fill", palette.textMuted).text(labelText);
39949
40236
  if (animate) {
@@ -40553,7 +40840,7 @@ function parseAndLayoutInfra(content) {
40553
40840
  const layout = layoutInfra(computed);
40554
40841
  return { parsed, computed, layout };
40555
40842
  }
40556
- var NODE_FONT_SIZE4, META_FONT_SIZE6, META_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE9, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, 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_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, PROP_DISPLAY, DESC_MAX_CHARS, DESC_MAX_WIDTH, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
40843
+ var NODE_FONT_SIZE4, META_FONT_SIZE6, META_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE10, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, 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_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, PROP_DISPLAY, DESC_MAX_CHARS, DESC_MAX_WIDTH, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
40557
40844
  var init_renderer10 = __esm({
40558
40845
  "src/infra/renderer.ts"() {
40559
40846
  "use strict";
@@ -40577,7 +40864,7 @@ var init_renderer10 = __esm({
40577
40864
  NODE_FONT_SIZE4 = 13;
40578
40865
  META_FONT_SIZE6 = 10;
40579
40866
  META_LINE_HEIGHT6 = 14;
40580
- EDGE_LABEL_FONT_SIZE9 = 11;
40867
+ EDGE_LABEL_FONT_SIZE10 = 11;
40581
40868
  GROUP_LABEL_FONT_SIZE2 = 14;
40582
40869
  NODE_BORDER_RADIUS = 8;
40583
40870
  OVERLOAD_STROKE_WIDTH = 3;
@@ -46966,7 +47253,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
46966
47253
  const sTitleFontSize = ctx.text(TITLE_FONT_SIZE);
46967
47254
  const sTitleY = ctx.structural(TITLE_Y);
46968
47255
  const sNodeFontSize = ctx.text(NODE_FONT_SIZE6);
46969
- const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE10);
47256
+ const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE11);
46970
47257
  const sGroupLabelFontSize = ctx.text(GROUP_LABEL_FONT_SIZE3);
46971
47258
  const sEdgeStrokeWidth = ctx.structural(EDGE_STROKE_WIDTH);
46972
47259
  const sNodeStrokeWidth = ctx.structural(NODE_STROKE_WIDTH);
@@ -47246,7 +47533,7 @@ function renderStateForExport(content, theme, palette) {
47246
47533
  document.body.removeChild(container);
47247
47534
  }
47248
47535
  }
47249
- var DIAGRAM_PADDING12, MAX_SCALE7, NODE_FONT_SIZE6, EDGE_LABEL_FONT_SIZE10, GROUP_LABEL_FONT_SIZE3, ARROWHEAD_W5, ARROWHEAD_H5, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING;
47536
+ var DIAGRAM_PADDING12, MAX_SCALE7, NODE_FONT_SIZE6, EDGE_LABEL_FONT_SIZE11, GROUP_LABEL_FONT_SIZE3, ARROWHEAD_W5, ARROWHEAD_H5, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING;
47250
47537
  var init_state_renderer = __esm({
47251
47538
  "src/graph/state-renderer.ts"() {
47252
47539
  "use strict";
@@ -47265,7 +47552,7 @@ var init_state_renderer = __esm({
47265
47552
  DIAGRAM_PADDING12 = 20;
47266
47553
  MAX_SCALE7 = 3;
47267
47554
  NODE_FONT_SIZE6 = 13;
47268
- EDGE_LABEL_FONT_SIZE10 = 11;
47555
+ EDGE_LABEL_FONT_SIZE11 = 11;
47269
47556
  GROUP_LABEL_FONT_SIZE3 = 11;
47270
47557
  ARROWHEAD_W5 = 10;
47271
47558
  ARROWHEAD_H5 = 7;
@@ -50087,10 +50374,10 @@ function renderedDescNodeHeight(numLines, scale) {
50087
50374
  return headerH + textBlockH + bottomPad;
50088
50375
  }
50089
50376
  function wrapEdgeLabelText(label, description, maxWidth = EDGE_LABEL_MAX_WIDTH) {
50090
- const labelLines = label ? wrapTextToWidth(label, EDGE_LABEL_FONT_SIZE11, maxWidth) : [];
50377
+ const labelLines = label ? wrapTextToWidth(label, EDGE_LABEL_FONT_SIZE12, maxWidth) : [];
50091
50378
  const descLines = [];
50092
50379
  for (const d of description) {
50093
- descLines.push(...wrapTextToWidth(d, EDGE_LABEL_FONT_SIZE11, maxWidth));
50380
+ descLines.push(...wrapTextToWidth(d, EDGE_LABEL_FONT_SIZE12, maxWidth));
50094
50381
  }
50095
50382
  return { labelLines, descLines };
50096
50383
  }
@@ -50228,9 +50515,9 @@ function computeEdgePaths(layoutNodes, parsed, cx, cy, radius, isClockwise) {
50228
50515
  const lineCount = labelLines.length + descLines.length;
50229
50516
  let labelPxW = 0;
50230
50517
  for (const l of labelLines)
50231
- labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE11));
50518
+ labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE12));
50232
50519
  for (const l of descLines)
50233
- labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE11));
50520
+ labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE12));
50234
50521
  const { labelX, labelY, labelAngle } = computeEdgeLabelPosition(
50235
50522
  midAngle,
50236
50523
  radius,
@@ -50332,9 +50619,9 @@ function fitToCanvas(nodes, edges, parsed, _cx, _cy, radius, width, height, _isC
50332
50619
  );
50333
50620
  let textWidth = 0;
50334
50621
  for (const l of labelLines)
50335
- textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE11));
50622
+ textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE12));
50336
50623
  for (const l of descLines)
50337
- textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE11));
50624
+ textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE12));
50338
50625
  if (textWidth === 0) continue;
50339
50626
  const normAngle = (le.labelAngle % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
50340
50627
  const isRight = normAngle < Math.PI * 0.4 || normAngle > Math.PI * 1.6;
@@ -50408,7 +50695,7 @@ function buildEdgeArc(src, tgt, cx, cy, radius, isClockwise, arrowLength = 0) {
50408
50695
  const midAngle = srcExitAngle + dir * visibleSweep / 2;
50409
50696
  return { path, midAngle };
50410
50697
  }
50411
- var MIN_ARC_ANGLE, LABEL_FONT_SIZE4, CIRCLE_LABEL_FONT_SIZE, DESC_FONT_SIZE6, EDGE_LABEL_FONT_SIZE11, MIN_NODE_WIDTH4, MAX_NODE_WIDTH3, DESC_MIN_WIDTH, DESC_WIDTH_STEP, DESC_TARGET_RATIO, PLAIN_NODE_HEIGHT, HEADER_HEIGHT4, DESC_LINE_HEIGHT7, DESC_PAD_Y, NODE_PAD_X, MIN_CIRCLE_RADIUS, CIRCLE_PAD, RENDERER_DESC_FONT, RENDERER_DESC_FONT_MIN, RENDERER_DESC_LINE_H, RENDERER_DESC_LINE_H_MIN, EDGE_LABEL_MAX_WIDTH;
50698
+ var MIN_ARC_ANGLE, LABEL_FONT_SIZE4, CIRCLE_LABEL_FONT_SIZE, DESC_FONT_SIZE6, EDGE_LABEL_FONT_SIZE12, MIN_NODE_WIDTH4, MAX_NODE_WIDTH3, DESC_MIN_WIDTH, DESC_WIDTH_STEP, DESC_TARGET_RATIO, PLAIN_NODE_HEIGHT, HEADER_HEIGHT4, DESC_LINE_HEIGHT7, DESC_PAD_Y, NODE_PAD_X, MIN_CIRCLE_RADIUS, CIRCLE_PAD, RENDERER_DESC_FONT, RENDERER_DESC_FONT_MIN, RENDERER_DESC_LINE_H, RENDERER_DESC_LINE_H_MIN, EDGE_LABEL_MAX_WIDTH;
50412
50699
  var init_layout14 = __esm({
50413
50700
  "src/cycle/layout.ts"() {
50414
50701
  "use strict";
@@ -50419,7 +50706,7 @@ var init_layout14 = __esm({
50419
50706
  LABEL_FONT_SIZE4 = 13;
50420
50707
  CIRCLE_LABEL_FONT_SIZE = 16;
50421
50708
  DESC_FONT_SIZE6 = 11;
50422
- EDGE_LABEL_FONT_SIZE11 = 11;
50709
+ EDGE_LABEL_FONT_SIZE12 = 11;
50423
50710
  MIN_NODE_WIDTH4 = 70;
50424
50711
  MAX_NODE_WIDTH3 = 260;
50425
50712
  DESC_MIN_WIDTH = 140;
@@ -50565,7 +50852,7 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
50565
50852
  const scaledNodeFont = Math.max(6, Math.round(NODE_FONT_SIZE7 * layout.scale));
50566
50853
  const scaledEdgeLabelFont = Math.max(
50567
50854
  6,
50568
- Math.round(EDGE_LABEL_FONT_SIZE12 * layout.scale)
50855
+ Math.round(EDGE_LABEL_FONT_SIZE13 * layout.scale)
50569
50856
  );
50570
50857
  const CIRCLE_LABEL_FONT_SIZE2 = 16;
50571
50858
  const scaledCircleLabelFont = Math.max(
@@ -50728,7 +51015,7 @@ function ensureArrowMarker(defs, color, strokeWidth) {
50728
51015
  const mh = Math.max(1.5, mw * 0.5);
50729
51016
  defs.append("marker").attr("id", id).attr("viewBox", `0 0 ${mw} ${mh}`).attr("refX", mw * 0.1).attr("refY", mh / 2).attr("markerWidth", mw).attr("markerHeight", mh).attr("orient", "auto").append("polygon").attr("points", `0,0 ${mw},${mh / 2} 0,${mh}`).attr("fill", color);
50730
51017
  }
50731
- var NODE_FONT_SIZE7, DESC_FONT_SIZE7, EDGE_LABEL_FONT_SIZE12, DESC_LINE_HEIGHT8, TITLE_AREA_HEIGHT;
51018
+ var NODE_FONT_SIZE7, DESC_FONT_SIZE7, EDGE_LABEL_FONT_SIZE13, DESC_LINE_HEIGHT8, TITLE_AREA_HEIGHT;
50732
51019
  var init_renderer15 = __esm({
50733
51020
  "src/cycle/renderer.ts"() {
50734
51021
  "use strict";
@@ -50745,7 +51032,7 @@ var init_renderer15 = __esm({
50745
51032
  init_text_measure();
50746
51033
  NODE_FONT_SIZE7 = 13;
50747
51034
  DESC_FONT_SIZE7 = 11;
50748
- EDGE_LABEL_FONT_SIZE12 = 11;
51035
+ EDGE_LABEL_FONT_SIZE13 = 11;
50749
51036
  DESC_LINE_HEIGHT8 = 15;
50750
51037
  TITLE_AREA_HEIGHT = 50;
50751
51038
  }
@@ -51143,7 +51430,7 @@ function resolveMap(parsed, data) {
51143
51430
  const hint = suggest(r.name, allNames);
51144
51431
  err(
51145
51432
  r.lineNumber,
51146
- `Unknown subdivision "${r.name}".${hint ? " " + hint : ""}`,
51433
+ `Unknown region "${r.name}" \u2014 not a known country or US state.${hint ? " " + hint : ""} Search the exact token with \`dgmo map-search "${r.name}"\` (or the lookup_map_location tool), use an ISO code (e.g. FR, US-CA), or coordinates.`,
51147
51434
  "E_MAP_UNKNOWN_SUBDIVISION"
51148
51435
  );
51149
51436
  continue;
@@ -51223,7 +51510,7 @@ function resolveMap(parsed, data) {
51223
51510
  const hint = suggest(name, cityNames);
51224
51511
  err(
51225
51512
  line11,
51226
- `Unknown place "${name}" (not in the gazetteer; use coordinates).${hint ? " " + hint : ""}`,
51513
+ `Unknown place "${name}" \u2014 not in the gazetteer.${hint ? " " + hint : ""} Search the exact token with \`dgmo map-search "${name}"\` (or the lookup_map_location tool), or use coordinates \`poi <lat> <lon>\`.`,
51227
51514
  "E_MAP_UNKNOWN_PLACE"
51228
51515
  );
51229
51516
  return { kind: "miss" };
@@ -51765,6 +52052,21 @@ function mapLegendBand(legend, opts) {
51765
52052
  if (height <= 0) return 0;
51766
52053
  return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
51767
52054
  }
52055
+ function mapLegendBox(legend, opts) {
52056
+ if (!legend) return null;
52057
+ const groups = mapLegendGroups(legend);
52058
+ if (groups.length === 0) return null;
52059
+ const config = mapLegendConfig(groups, opts.mode);
52060
+ const state = { activeGroup: legend.activeGroup };
52061
+ const { width, height } = computeLegendLayout(config, state, opts.width);
52062
+ if (height <= 0 || width <= 0) return null;
52063
+ return {
52064
+ x: (opts.width - width) / 2,
52065
+ y: mapLegendTop(opts.hasTitle, opts.hasSubtitle),
52066
+ width,
52067
+ height
52068
+ };
52069
+ }
51768
52070
  var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
51769
52071
  var init_legend_band = __esm({
51770
52072
  "src/map/legend-band.ts"() {
@@ -51889,6 +52191,8 @@ function placeContextLabels(args) {
51889
52191
  waterBodies,
51890
52192
  countries,
51891
52193
  palette,
52194
+ countryTone,
52195
+ waterTone,
51892
52196
  project,
51893
52197
  collides,
51894
52198
  contentPoints,
@@ -51898,8 +52202,8 @@ function placeContextLabels(args) {
51898
52202
  const band = tierBand(Math.max(dLonSpan, dLatSpan));
51899
52203
  const budget = labelBudget(width, height, band);
51900
52204
  if (budget <= 0) return [];
51901
- const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
51902
- const countryColor = palette.textMuted;
52205
+ const waterColor = waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
52206
+ const countryColor = countryTone ?? palette.textMuted;
51903
52207
  const haloColor = palette.bg;
51904
52208
  const candidates = [];
51905
52209
  const center = [width / 2, height / 2];
@@ -51988,8 +52292,7 @@ function placeContextLabels(args) {
51988
52292
  )
51989
52293
  );
51990
52294
  const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
51991
- const fade = Math.round(t * COUNTRY_FADE_MAX);
51992
- const color = fade > 0 ? mix(countryColor, palette.bg, 100 - fade) : countryColor;
52295
+ const color = countryColor;
51993
52296
  const text = c.name;
51994
52297
  const tw = labelWidth(text, 0, fontSize);
51995
52298
  if (tw > w || fontSize + 2 * PADY > h) continue;
@@ -52083,7 +52386,7 @@ function placeContextLabels(args) {
52083
52386
  }
52084
52387
  return placed;
52085
52388
  }
52086
- var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, COUNTRY_FONT_MAX, COUNTRY_SIZE_FRAC_MIN, COUNTRY_SIZE_FRAC_MAX, COUNTRY_FADE_MAX, MAX_COUNTRY_POSITIONS, COUNTRY_POS_GRID, COUNTRY_POS_TOPN_MARGIN, KIND_ORDER;
52389
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, COUNTRY_FONT_MAX, COUNTRY_SIZE_FRAC_MIN, COUNTRY_SIZE_FRAC_MAX, MAX_COUNTRY_POSITIONS, COUNTRY_POS_GRID, COUNTRY_POS_TOPN_MARGIN, KIND_ORDER;
52087
52390
  var init_context_labels = __esm({
52088
52391
  "src/map/context-labels.ts"() {
52089
52392
  "use strict";
@@ -52100,7 +52403,6 @@ var init_context_labels = __esm({
52100
52403
  COUNTRY_FONT_MAX = 22;
52101
52404
  COUNTRY_SIZE_FRAC_MIN = 0.06;
52102
52405
  COUNTRY_SIZE_FRAC_MAX = 0.32;
52103
- COUNTRY_FADE_MAX = 45;
52104
52406
  MAX_COUNTRY_POSITIONS = 4;
52105
52407
  COUNTRY_POS_GRID = 5;
52106
52408
  COUNTRY_POS_TOPN_MARGIN = 3;
@@ -52238,36 +52540,23 @@ function countryLabelPositions(args) {
52238
52540
  return n;
52239
52541
  };
52240
52542
  const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
52241
- const pool = [...kept];
52242
- pool.sort((a, b) => {
52243
- const d = interiorness(b) - interiorness(a);
52244
- return d !== 0 ? d : dist2ToCentre(a) - dist2ToCentre(b);
52245
- });
52246
- const ordered = [pool.shift()];
52247
- while (ordered.length < MAX_COUNTRY_POSITIONS && pool.length) {
52248
- let bestIdx = 0;
52249
- let bestMin = -1;
52250
- for (let k = 0; k < pool.length; k++) {
52251
- const c = pool[k];
52252
- let minD = Infinity;
52253
- for (const o of ordered) {
52254
- const d = (c.sx - o.sx) ** 2 + (c.sy - o.sy) ** 2;
52255
- if (d < minD) minD = d;
52256
- }
52257
- if (minD > bestMin) {
52258
- bestMin = minD;
52259
- bestIdx = k;
52260
- }
52543
+ const centreLon = kept.reduce((s, c) => s + c.lon, 0) / kept.length;
52544
+ const centreLat = kept.reduce((s, c) => s + c.lat, 0) / kept.length;
52545
+ const centreScreen = project(centreLon, centreLat);
52546
+ const lead = pointInGeometry(geometry, centreLon, centreLat) && centreScreen && Number.isFinite(centreScreen[0]) && Number.isFinite(centreScreen[1]) && centreScreen[0] >= 0 && centreScreen[0] <= width && centreScreen[1] >= 0 && centreScreen[1] <= height ? [
52547
+ {
52548
+ lonLat: [centreLon, centreLat],
52549
+ screen: [centreScreen[0], centreScreen[1]]
52261
52550
  }
52262
- ordered.push(pool.splice(bestIdx, 1)[0]);
52263
- }
52264
- const grid = ordered.map((c) => ({
52551
+ ] : [];
52552
+ const grid = [...kept].sort(
52553
+ (a, b) => interiorness(b) - interiorness(a) || dist2ToCentre(a) - dist2ToCentre(b)
52554
+ ).map((c) => ({
52265
52555
  lonLat: [c.lon, c.lat],
52266
52556
  screen: [c.sx, c.sy]
52267
52557
  }));
52268
52558
  const curatedPos = curated ? mkCurated(curated, project) : [];
52269
- const out = [...curatedPos, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52270
- return out;
52559
+ return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52271
52560
  }
52272
52561
  function mkCurated(curated, project) {
52273
52562
  if (!curated) return [];
@@ -52533,6 +52822,25 @@ function layoutMap(resolvedIn, data, size, opts) {
52533
52822
  palette.bg,
52534
52823
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
52535
52824
  );
52825
+ const contrastFloorTone = (tone, ...substrates) => {
52826
+ const clears = (t) => substrates.every((s) => contrastRatio(t, s) >= REGION_LABEL_HALO_RATIO);
52827
+ if (clears(tone)) return tone;
52828
+ for (let s = 10; s <= 100; s += 10) {
52829
+ const t = mix(palette.text, tone, s);
52830
+ if (clears(t)) return t;
52831
+ }
52832
+ return palette.text;
52833
+ };
52834
+ const backdropLandTone = contrastFloorTone(
52835
+ palette.textMuted,
52836
+ neutralFill,
52837
+ water
52838
+ );
52839
+ const backdropWaterTone = contrastFloorTone(
52840
+ mix(palette.colors.blue, palette.textMuted, 50),
52841
+ water,
52842
+ neutralFill
52843
+ );
52536
52844
  const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
52537
52845
  const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
52538
52846
  const colorByIso = /* @__PURE__ */ new Map();
@@ -52647,6 +52955,33 @@ function layoutMap(resolvedIn, data, size, opts) {
52647
52955
  hasSubtitle: Boolean(resolved.subtitle)
52648
52956
  });
52649
52957
  if (legendBand > topPad) topPad = legendBand;
52958
+ const topReserved = [];
52959
+ if (shownTitle) {
52960
+ const lines = resolved.subtitle ? 2 : 1;
52961
+ const tw = Math.max(
52962
+ measureLegendText(shownTitle, TITLE_FONT_SIZE),
52963
+ resolved.subtitle ? measureLegendText(resolved.subtitle, TITLE_FONT_SIZE) : 0
52964
+ ) + 2 * TITLE_FONT_SIZE;
52965
+ topReserved.push({
52966
+ x: width / 2 - tw / 2,
52967
+ y: TITLE_Y - TITLE_FONT_SIZE,
52968
+ w: tw,
52969
+ h: TITLE_FONT_SIZE * (lines + 0.5)
52970
+ });
52971
+ }
52972
+ const legendBox = mapLegendBox(legend, {
52973
+ width,
52974
+ mode: opts.legendMode ?? "preview",
52975
+ hasTitle: Boolean(shownTitle),
52976
+ hasSubtitle: Boolean(resolved.subtitle)
52977
+ });
52978
+ if (legendBox)
52979
+ topReserved.push({
52980
+ x: legendBox.x,
52981
+ y: legendBox.y,
52982
+ w: legendBox.width,
52983
+ h: legendBox.height
52984
+ });
52650
52985
  const reserve = opts._calloutReserve;
52651
52986
  const fitLeft = FIT_PAD + (reserve?.left ?? 0);
52652
52987
  const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
@@ -53376,6 +53711,19 @@ function layoutMap(resolvedIn, data, size, opts) {
53376
53711
  const by = b.cy - (b.cy - py) / tb * trimB;
53377
53712
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
53378
53713
  };
53714
+ const legLabelPoint = (a, b, curved, offset) => {
53715
+ const mx = (a.cx + b.cx) / 2;
53716
+ const my = (a.cy + b.cy) / 2;
53717
+ if (!curved && offset === 0) return { x: mx, y: my - 4 };
53718
+ const dx = b.cx - a.cx;
53719
+ const dy = b.cy - a.cy;
53720
+ const len = Math.hypot(dx, dy) || 1;
53721
+ const nx = -dy / len;
53722
+ const ny = dx / len;
53723
+ const bow = offset !== 0 ? offset : len * ARC_CURVE_FRAC;
53724
+ const off = bow * 0.5 + Math.sign(bow || 1) * 8;
53725
+ return { x: mx + nx * off, y: my + ny * off };
53726
+ };
53379
53727
  const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
53380
53728
  const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
53381
53729
  const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
@@ -53389,13 +53737,12 @@ function layoutMap(resolvedIn, data, size, opts) {
53389
53737
  const a = poiScreen.get(leg.fromId);
53390
53738
  const b = poiScreen.get(leg.toId);
53391
53739
  if (!a || !b) continue;
53392
- const mx = (a.cx + b.cx) / 2;
53393
- const my = (a.cy + b.cy) / 2;
53740
+ const lp = legLabelPoint(a, b, leg.style === "arc", 0);
53394
53741
  const bow = {
53395
53742
  curved: leg.style === "arc",
53396
53743
  offset: 0,
53397
- labelX: mx,
53398
- labelY: my - 4
53744
+ labelX: lp.x,
53745
+ labelY: lp.y
53399
53746
  };
53400
53747
  const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53401
53748
  legs.push({
@@ -53442,13 +53789,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53442
53789
  const b = poiScreen.get(e.toId);
53443
53790
  if (!a || !b) return;
53444
53791
  const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
53445
- const mx = (a.cx + b.cx) / 2;
53446
- const my = (a.cy + b.cy) / 2;
53792
+ const curved = e.style === "arc" || n > 1;
53793
+ const lp = legLabelPoint(a, b, curved, fanOffset);
53447
53794
  const bow = {
53448
- curved: e.style === "arc" || n > 1,
53795
+ curved,
53449
53796
  offset: fanOffset,
53450
- labelX: mx,
53451
- labelY: my - 4
53797
+ labelX: lp.x,
53798
+ labelY: lp.y
53452
53799
  };
53453
53800
  const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53454
53801
  legs.push({
@@ -53472,7 +53819,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53472
53819
  });
53473
53820
  }
53474
53821
  const labels = [];
53475
- const obstacles = [];
53822
+ const obstacles = [...topReserved];
53476
53823
  const regionLabelGuards = [];
53477
53824
  const markers = pois.map((p) => ({
53478
53825
  cx: p.cx,
@@ -53510,6 +53857,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53510
53857
  }
53511
53858
  const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
53512
53859
  const showRegionLabels = resolved.directives.noRegionLabels !== true;
53860
+ const labeledRegionIds = /* @__PURE__ */ new Set();
53513
53861
  const showRegionValues = resolved.directives.noRegionValue !== true && activeIsScore;
53514
53862
  const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
53515
53863
  const isCompact = width < COMPACT_WIDTH_PX;
@@ -53530,7 +53878,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53530
53878
  const REGION_FONT_MAX_ORIENT = 22;
53531
53879
  const REGION_SIZE_FRAC_MIN = 0.06;
53532
53880
  const REGION_SIZE_FRAC_MAX = 0.32;
53533
- const REGION_FADE_ORIENT = 45;
53881
+ const SUBJECT_FONT_MAX = 18;
53882
+ const SUBJECT_MIN_PROMINENCE = 13;
53883
+ const SUBJECT_LEADER_FONT = 15;
53534
53884
  const canvasLinear = Math.sqrt(Math.max(1, width * height));
53535
53885
  const sizeT = (boxW, boxH) => {
53536
53886
  const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
@@ -53542,9 +53892,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53542
53892
  )
53543
53893
  );
53544
53894
  };
53545
- const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, fade = 0) => {
53895
+ const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
53546
53896
  const { color: baseColor, haloColor } = labelOnFill(fill2);
53547
- const color = fade > 0 ? mix(baseColor, palette.bg, fade) : baseColor;
53897
+ const color = colorOverride ?? baseColor;
53548
53898
  const vf = Math.round(fontSize * 0.82);
53549
53899
  const halfW = Math.max(
53550
53900
  measureLegendText(text, fontSize),
@@ -53559,7 +53909,11 @@ function layoutMap(resolvedIn, data, size, opts) {
53559
53909
  text,
53560
53910
  anchor: "middle",
53561
53911
  color,
53562
- halo: overflows,
53912
+ // A backdrop label (colorOverride) NEVER haloes: the placement gate above
53913
+ // guarantees it sits on a floored-readable substrate (own fill, neutral
53914
+ // land, foreign land, or water), so a halo is pure noise. Data labels keep
53915
+ // the containment-gated halo until the role-tier pass (Phase 3).
53916
+ halo: colorOverride ? false : overflows,
53563
53917
  haloColor,
53564
53918
  ...fontSize !== FONT2 && { fontSize },
53565
53919
  ...valueLine !== void 0 && { valueLine },
@@ -53595,7 +53949,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53595
53949
  if (!c || !Number.isFinite(c[0])) return null;
53596
53950
  return { r, c, boxW, boxH, area: boxW * boxH, candidates };
53597
53951
  }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
53598
- const placedRegionRects = [];
53952
+ const placedRegionRects = [...topReserved];
53599
53953
  const placedLeaders = [];
53600
53954
  const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
53601
53955
  const POI_LABEL_PAD = 14;
@@ -53608,9 +53962,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53608
53962
  const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
53609
53963
  const STEP = 4;
53610
53964
  const HOP_GAP = 9;
53611
- const tryShortHopCallout = (cx, cy, fill2, name, value) => {
53612
- const chipW = stackW(name, value);
53613
- const chipH = stackH(value !== void 0);
53965
+ const tryShortHopCallout = (cx, cy, fill2, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
53966
+ const chipW = stackW(name, value, font);
53967
+ const chipH = stackH(value !== void 0, font);
53614
53968
  const dirs = [
53615
53969
  [0, -1],
53616
53970
  // north (over Canada)
@@ -53626,7 +53980,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53626
53980
  let ex = cx;
53627
53981
  let ey = cy;
53628
53982
  let steps = 0;
53629
- const maxSteps = Math.ceil(SHORT_HOP_MAX / STEP) + 1;
53983
+ const maxSteps = Math.ceil(maxLen / STEP) + 1;
53630
53984
  while (steps < maxSteps && fillAt(ex, ey) === fill2) {
53631
53985
  ex += dx * STEP;
53632
53986
  ey += dy * STEP;
@@ -53634,8 +53988,8 @@ function layoutMap(resolvedIn, data, size, opts) {
53634
53988
  }
53635
53989
  if (fillAt(ex, ey) === fill2) continue;
53636
53990
  const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
53637
- const ccx = ex + dx * (HOP_GAP + halfAlong);
53638
- const ccy = ey + dy * (HOP_GAP + halfAlong);
53991
+ const ccx = ex + dx * (gap + halfAlong);
53992
+ const ccy = ey + dy * (gap + halfAlong);
53639
53993
  const rect = {
53640
53994
  x: ccx - chipW / 2,
53641
53995
  y: ccy - chipH / 2,
@@ -53660,7 +54014,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53660
54014
  const innerX = ccx - dx * (chipW / 2);
53661
54015
  const innerY = ccy - dy * (chipH / 2);
53662
54016
  const len = Math.hypot(innerX - cx, innerY - cy);
53663
- if (len > SHORT_HOP_MAX) continue;
54017
+ if (len > maxLen) continue;
53664
54018
  if (placedLeaders.some(
53665
54019
  (l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
53666
54020
  ))
@@ -53684,6 +54038,78 @@ function layoutMap(resolvedIn, data, size, opts) {
53684
54038
  const valStr = regionValueStr(r.value);
53685
54039
  const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
53686
54040
  const fitsPois = (rect) => !poiObstacles.some((o) => rectsOverlap(rect, o));
54041
+ const isSubject = regionById.has(r.id);
54042
+ const extremesOnFill = (ax, ay, text) => {
54043
+ const halfW = measureLegendText(text, FONT2) / 2;
54044
+ return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
54045
+ };
54046
+ if (isSubject && valStr === void 0 && r.label !== void 0) {
54047
+ const name = candidates[0];
54048
+ let best = -1;
54049
+ for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
54050
+ if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
54051
+ continue;
54052
+ const rect = regionLabelRect(c[0], c[1], name, void 0, f);
54053
+ if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
54054
+ const halfW = measureLegendText(name, f) / 2;
54055
+ if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
54056
+ continue;
54057
+ best = f;
54058
+ break;
54059
+ }
54060
+ if (best >= SUBJECT_MIN_PROMINENCE) {
54061
+ const rect = regionLabelRect(c[0], c[1], name, void 0, best);
54062
+ placedRegionRects.push(rect);
54063
+ pushRegionLabel(
54064
+ c[0],
54065
+ c[1],
54066
+ name,
54067
+ r.fill,
54068
+ r.lineNumber,
54069
+ void 0,
54070
+ best
54071
+ );
54072
+ labeledRegionIds.add(r.id);
54073
+ regionLabelGuards.push({ label: labels[labels.length - 1], rect });
54074
+ } else {
54075
+ const hop = tryShortHopCallout(
54076
+ c[0],
54077
+ c[1],
54078
+ r.fill,
54079
+ r.label,
54080
+ void 0,
54081
+ SUBJECT_LEADER_FONT,
54082
+ HOP_GAP + SUBJECT_LEADER_FONT,
54083
+ SHORT_HOP_MAX * 2.2
54084
+ );
54085
+ if (hop) {
54086
+ placedRegionRects.push(hop.rect);
54087
+ placedLeaders.push(hop.leader);
54088
+ const dark = mix(r.fill, palette.text, 60);
54089
+ labels.push({
54090
+ x: hop.x,
54091
+ y: hop.y,
54092
+ text: r.label,
54093
+ anchor: "middle",
54094
+ color: palette.text,
54095
+ halo: false,
54096
+ haloColor: palette.bg,
54097
+ fontSize: SUBJECT_LEADER_FONT,
54098
+ leader: {
54099
+ x1: hop.leader[0],
54100
+ y1: hop.leader[1],
54101
+ x2: hop.leader[2],
54102
+ y2: hop.leader[3]
54103
+ },
54104
+ leaderColor: dark,
54105
+ calloutDot: { x: c[0], y: c[1], color: dark },
54106
+ lineNumber: r.lineNumber
54107
+ });
54108
+ labeledRegionIds.add(r.id);
54109
+ }
54110
+ }
54111
+ continue;
54112
+ }
53687
54113
  const seekAnchors = [
53688
54114
  { x: c[0], y: c[1], guard: false }
53689
54115
  ];
@@ -53706,13 +54132,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53706
54132
  const nameRect = regionLabelRect(a.x, a.y, t);
53707
54133
  if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
53708
54134
  const stackRect = regionLabelRect(a.x, a.y, t, valStr);
53709
- if (fitsRegions(stackRect) && fitsPois(nameRect)) {
54135
+ if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
53710
54136
  chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
53711
54137
  break;
53712
54138
  }
53713
54139
  }
53714
54140
  if (labelW(t) <= boxW && labelH <= boxH) {
53715
- if (fitsRegions(nameRect) && fitsPois(nameRect)) {
54141
+ if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
53716
54142
  chosen = { text: t, ax: a.x, ay: a.y };
53717
54143
  break;
53718
54144
  }
@@ -53720,7 +54146,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53720
54146
  }
53721
54147
  if (chosen) break;
53722
54148
  }
53723
- if (chosen === void 0 && valStr && r.label !== void 0) {
54149
+ if (chosen === void 0 && r.label !== void 0 && isSubject) {
53724
54150
  const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
53725
54151
  if (hop) {
53726
54152
  placedRegionRects.push(hop.rect);
@@ -53732,9 +54158,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53732
54158
  text: r.label,
53733
54159
  anchor: "middle",
53734
54160
  color: palette.text,
53735
- halo: true,
54161
+ halo: false,
53736
54162
  haloColor: palette.bg,
53737
- valueLine: valStr,
54163
+ ...valStr !== void 0 && { valueLine: valStr },
53738
54164
  leader: {
53739
54165
  x1: hop.leader[0],
53740
54166
  y1: hop.leader[1],
@@ -53745,20 +54171,28 @@ function layoutMap(resolvedIn, data, size, opts) {
53745
54171
  calloutDot: { x: c[0], y: c[1], color: dark },
53746
54172
  lineNumber: r.lineNumber
53747
54173
  });
54174
+ labeledRegionIds.add(r.id);
53748
54175
  }
53749
54176
  continue;
53750
54177
  }
53751
54178
  if (chosen === void 0) continue;
53752
54179
  const isOrient = r.value === void 0 && r.layer === "base";
53753
54180
  let font = FONT2;
53754
- let fade = 0;
53755
54181
  if (isOrient) {
53756
54182
  const growT = sizeT(boxW, boxH);
53757
54183
  const desiredFont = Math.round(
53758
54184
  FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
53759
54185
  );
53760
54186
  const hasVal = chosen.valueLine !== void 0;
53761
- for (let f = desiredFont; f > FONT2; f--) {
54187
+ const extremesClean = (f) => {
54188
+ const halfW = measureLegendText(chosen.text, f) / 2;
54189
+ return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
54190
+ const fAt = fillAt(sx, chosen.ay);
54191
+ return fAt === r.fill || isEmptyFill(fAt);
54192
+ });
54193
+ };
54194
+ let fit = -1;
54195
+ for (let f = desiredFont; f >= FONT2; f--) {
53762
54196
  if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
53763
54197
  continue;
53764
54198
  const gRect = regionLabelRect(
@@ -53775,14 +54209,18 @@ function layoutMap(resolvedIn, data, size, opts) {
53775
54209
  void 0,
53776
54210
  f
53777
54211
  );
53778
- if (!fitsRegions(gRect) || !fitsPois(gName)) continue;
53779
- const halfW = measureLegendText(chosen.text, f) / 2;
53780
- if (fillAt(chosen.ax - halfW, chosen.ay) !== r.fill || fillAt(chosen.ax + halfW, chosen.ay) !== r.fill)
54212
+ if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
53781
54213
  continue;
53782
- font = f;
54214
+ if (!extremesClean(f)) continue;
54215
+ fit = f;
53783
54216
  break;
53784
54217
  }
53785
- fade = font > FONT2 ? Math.round(growT * REGION_FADE_ORIENT) : 0;
54218
+ if (fit < 0) {
54219
+ if (!frameContainers.has(r.id)) continue;
54220
+ font = FONT2;
54221
+ } else {
54222
+ font = fit;
54223
+ }
53786
54224
  }
53787
54225
  const rRect = regionLabelRect(
53788
54226
  chosen.ax,
@@ -53800,8 +54238,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53800
54238
  r.lineNumber,
53801
54239
  chosen.valueLine,
53802
54240
  font,
53803
- fade
54241
+ isOrient ? backdropLandTone : void 0
53804
54242
  );
54243
+ labeledRegionIds.add(r.id);
53805
54244
  regionLabelGuards.push({
53806
54245
  label: labels[labels.length - 1],
53807
54246
  rect: rRect
@@ -53819,6 +54258,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53819
54258
  seed.lineNumber,
53820
54259
  valStr
53821
54260
  );
54261
+ labeledRegionIds.add(seed.iso);
53822
54262
  regionLabelGuards.push({
53823
54263
  label: labels[labels.length - 1],
53824
54264
  rect: regionLabelRect(seed.x, seed.y, text, valStr)
@@ -54250,13 +54690,30 @@ function layoutMap(resolvedIn, data, size, opts) {
54250
54690
  const nx = -dy / len;
54251
54691
  const ny = dx / len;
54252
54692
  const perp = labelH + 2;
54693
+ const pm = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
54694
+ lg.d
54695
+ );
54696
+ const pointAt = (t) => {
54697
+ if (pm?.[5] !== void 0) {
54698
+ const u = 1 - t;
54699
+ return [
54700
+ u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
54701
+ u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
54702
+ ];
54703
+ }
54704
+ if (pm?.[3] !== void 0)
54705
+ return [
54706
+ +pm[1] + (+pm[3] - +pm[1]) * t,
54707
+ +pm[2] + (+pm[4] - +pm[2]) * t
54708
+ ];
54709
+ return [a.cx + dx * t, a.cy + dy * t];
54710
+ };
54253
54711
  const candidates = [];
54254
54712
  for (const off of [0, perp, -perp, 2 * perp, -2 * perp])
54255
- for (const t of T_LIST)
54256
- candidates.push([
54257
- a.cx + dx * t + nx * off,
54258
- a.cy + dy * t + ny * off - 4
54259
- ]);
54713
+ for (const t of T_LIST) {
54714
+ const [bx, by] = pointAt(t);
54715
+ candidates.push([bx + nx * off, by + ny * off - 4]);
54716
+ }
54260
54717
  const clean = ([cx2, cy2]) => {
54261
54718
  const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
54262
54719
  if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
@@ -54266,10 +54723,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54266
54723
  if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
54267
54724
  return true;
54268
54725
  };
54269
- const [cx, cy] = candidates.find(clean) ?? [
54270
- a.cx + dx * 0.5,
54271
- a.cy + dy * 0.5 - 4
54272
- ];
54726
+ const mid = pointAt(0.5);
54727
+ const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
54273
54728
  const style = labelOnFill(fillAt(cx, cy));
54274
54729
  legs[i] = {
54275
54730
  ...lg,
@@ -54297,7 +54752,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54297
54752
  const rawCountries = [];
54298
54753
  for (const f of worldLayer.values()) {
54299
54754
  const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
54300
- if (!iso || regionById.has(iso)) continue;
54755
+ if (!iso || regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54301
54756
  let hasReferencedSub = false;
54302
54757
  for (const k of regionById.keys())
54303
54758
  if (k.startsWith(iso + "-")) {
@@ -54371,7 +54826,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54371
54826
  );
54372
54827
  if (usLayer && framedStateContainers) {
54373
54828
  for (const [iso, f] of usLayer) {
54374
- if (regionById.has(iso)) continue;
54829
+ if (regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54375
54830
  const viewF = cullFeatureToView(f);
54376
54831
  if (!viewF) continue;
54377
54832
  const b = path.bounds(viewF);
@@ -54395,6 +54850,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54395
54850
  waterBodies: data.waterBodies,
54396
54851
  countries: countryCandidates,
54397
54852
  palette,
54853
+ // One muted, contrast-floored tone shared with the orientation backdrop
54854
+ // (region pass) so a context country (Canada) and an orientation region
54855
+ // (Minnesota) read identically — never below the WCAG-AA floor, no fade.
54856
+ countryTone: backdropLandTone,
54857
+ waterTone: backdropWaterTone,
54398
54858
  project,
54399
54859
  collides,
54400
54860
  contentPoints,
@@ -54948,19 +55408,60 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
54948
55408
  sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
54949
55409
  };
54950
55410
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
54951
- layout.legs.forEach((leg, i) => {
54952
- const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
55411
+ const legEnd = (d) => {
55412
+ const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
55413
+ d
55414
+ );
55415
+ if (!m) return null;
55416
+ const x0 = +m[1];
55417
+ const y0 = +m[2];
55418
+ let ex;
55419
+ let ey;
55420
+ let fromX;
55421
+ let fromY;
55422
+ if (m[3] !== void 0) {
55423
+ ex = +m[3];
55424
+ ey = +m[4];
55425
+ fromX = x0;
55426
+ fromY = y0;
55427
+ } else {
55428
+ ex = +m[7];
55429
+ ey = +m[8];
55430
+ fromX = +m[5];
55431
+ fromY = +m[6];
55432
+ }
55433
+ const dx = ex - fromX;
55434
+ const dy = ey - fromY;
55435
+ const len = Math.hypot(dx, dy);
55436
+ if (!(len > 0)) return null;
55437
+ return { ex, ey, tx: dx / len, ty: dy / len };
55438
+ };
55439
+ layout.legs.forEach((leg) => {
55440
+ const end = leg.arrow ? legEnd(leg.d) : null;
55441
+ let drawD = leg.d;
55442
+ let arrowTri = null;
55443
+ if (end) {
55444
+ const aLen = arrowSize(leg.width);
55445
+ const aHalf = Math.max(aLen * 0.5, leg.width * 0.6 + 1);
55446
+ const bx = end.ex - end.tx * aLen;
55447
+ const by = end.ey - end.ty * aLen;
55448
+ const px = -end.ty;
55449
+ const py = end.tx;
55450
+ arrowTri = `M${bx + px * aHalf},${by + py * aHalf}L${end.ex},${end.ey}L${bx - px * aHalf},${by - py * aHalf}z`;
55451
+ drawD = leg.d.replace(
55452
+ /(-?[\d.]+),(-?[\d.]+)$/,
55453
+ `${bx.toFixed(2)},${by.toFixed(2)}`
55454
+ );
55455
+ }
55456
+ const p = gLegs.append("path").attr("d", drawD).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
54953
55457
  if (leg.tags) {
54954
55458
  for (const [group, value] of Object.entries(leg.tags)) {
54955
55459
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
54956
55460
  }
54957
55461
  }
54958
55462
  wireSync(p, leg.lineNumber);
54959
- if (leg.arrow) {
54960
- const id = nid(`dgmo-map-arrow-${i}`);
54961
- const s = arrowSize(leg.width);
54962
- defs.append("marker").attr("id", id).attr("viewBox", "0 0 10 10").attr("refX", 10).attr("refY", 5).attr("markerUnits", "userSpaceOnUse").attr("markerWidth", s).attr("markerHeight", s).attr("orient", "auto-start-reverse").append("path").attr("d", "M0,0L10,5L0,10z").attr("fill", leg.color);
54963
- p.attr("marker-end", `url(#${id})`);
55463
+ if (arrowTri) {
55464
+ gLegs.append("path").attr("d", arrowTri).attr("fill", leg.color).attr("stroke", leg.color).attr("stroke-width", 1).attr("stroke-linejoin", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
54964
55465
  }
54965
55466
  if (leg.label !== void 0 && leg.labelX !== void 0) {
54966
55467
  const lt = emitText(
@@ -58587,9 +59088,9 @@ var init_renderer20 = __esm({
58587
59088
  }
58588
59089
  });
58589
59090
 
58590
- // node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
59091
+ // ../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
58591
59092
  var require_lz_string = __commonJS({
58592
- "node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports, module) {
59093
+ "../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports, module) {
58593
59094
  "use strict";
58594
59095
  var LZString = (function() {
58595
59096
  var f = String.fromCharCode;