@diagrammo/dgmo 0.34.0 → 0.36.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 (55) hide show
  1. package/.claude/commands/dgmo.md +41 -43
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +12 -7
  5. package/dist/advanced.cjs +460 -463
  6. package/dist/advanced.d.cts +2 -51
  7. package/dist/advanced.d.ts +2 -51
  8. package/dist/advanced.js +460 -464
  9. package/dist/auto.cjs +461 -140
  10. package/dist/auto.js +91 -91
  11. package/dist/auto.mjs +461 -140
  12. package/dist/cli.cjs +143 -326
  13. package/dist/index.cjs +460 -139
  14. package/dist/index.js +460 -139
  15. package/docs/ai-integration.md +47 -25
  16. package/docs/language-reference.md +2 -2
  17. package/package.json +2 -2
  18. package/src/advanced.ts +0 -12
  19. package/src/c4/parser.ts +15 -6
  20. package/src/chart.ts +1 -1
  21. package/src/class/parser.ts +6 -2
  22. package/src/cli-banner.ts +2 -3
  23. package/src/cli.ts +539 -1223
  24. package/src/cycle/parser.ts +4 -1
  25. package/src/dgmo-router.ts +16 -2
  26. package/src/diagnostics.ts +0 -0
  27. package/src/echarts.ts +5 -1
  28. package/src/er/parser.ts +5 -1
  29. package/src/gantt/parser.ts +13 -4
  30. package/src/graph/flowchart-parser.ts +5 -1
  31. package/src/graph/state-parser.ts +5 -1
  32. package/src/infra/parser.ts +10 -6
  33. package/src/journey-map/parser.ts +5 -2
  34. package/src/kanban/parser.ts +6 -2
  35. package/src/map/context-labels.ts +29 -19
  36. package/src/map/layout.ts +403 -108
  37. package/src/map/legend-band.ts +28 -0
  38. package/src/map/parser.ts +13 -4
  39. package/src/map/renderer.ts +74 -23
  40. package/src/map/resolver.ts +2 -2
  41. package/src/org/parser.ts +8 -2
  42. package/src/pyramid/parser.ts +4 -1
  43. package/src/raci/parser.ts +4 -1
  44. package/src/ring/parser.ts +4 -1
  45. package/src/sequence/parser.ts +6 -3
  46. package/src/sitemap/parser.ts +12 -3
  47. package/src/tech-radar/parser.ts +6 -3
  48. package/src/utils/parsing.ts +2 -2
  49. package/src/utils/tag-groups.ts +3 -3
  50. package/src/visualizations/parse.ts +9 -3
  51. package/src/migrate/data-comma.ts +0 -117
  52. package/src/migrate/embedded.ts +0 -238
  53. package/src/migrate/index.ts +0 -295
  54. package/src/migrate/line-classifier.ts +0 -128
  55. 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);
@@ -51323,7 +51430,7 @@ function resolveMap(parsed, data) {
51323
51430
  const hint = suggest(r.name, allNames);
51324
51431
  err(
51325
51432
  r.lineNumber,
51326
- `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.`,
51327
51434
  "E_MAP_UNKNOWN_SUBDIVISION"
51328
51435
  );
51329
51436
  continue;
@@ -51403,7 +51510,7 @@ function resolveMap(parsed, data) {
51403
51510
  const hint = suggest(name, cityNames);
51404
51511
  err(
51405
51512
  line11,
51406
- `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>\`.`,
51407
51514
  "E_MAP_UNKNOWN_PLACE"
51408
51515
  );
51409
51516
  return { kind: "miss" };
@@ -51945,6 +52052,21 @@ function mapLegendBand(legend, opts) {
51945
52052
  if (height <= 0) return 0;
51946
52053
  return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
51947
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
+ }
51948
52070
  var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
51949
52071
  var init_legend_band = __esm({
51950
52072
  "src/map/legend-band.ts"() {
@@ -52069,6 +52191,8 @@ function placeContextLabels(args) {
52069
52191
  waterBodies,
52070
52192
  countries,
52071
52193
  palette,
52194
+ countryTone,
52195
+ waterTone,
52072
52196
  project,
52073
52197
  collides,
52074
52198
  contentPoints,
@@ -52078,8 +52202,8 @@ function placeContextLabels(args) {
52078
52202
  const band = tierBand(Math.max(dLonSpan, dLatSpan));
52079
52203
  const budget = labelBudget(width, height, band);
52080
52204
  if (budget <= 0) return [];
52081
- const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
52082
- const countryColor = palette.textMuted;
52205
+ const waterColor = waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
52206
+ const countryColor = countryTone ?? palette.textMuted;
52083
52207
  const haloColor = palette.bg;
52084
52208
  const candidates = [];
52085
52209
  const center = [width / 2, height / 2];
@@ -52168,8 +52292,7 @@ function placeContextLabels(args) {
52168
52292
  )
52169
52293
  );
52170
52294
  const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
52171
- const fade = Math.round(t * COUNTRY_FADE_MAX);
52172
- const color = fade > 0 ? mix(countryColor, palette.bg, 100 - fade) : countryColor;
52295
+ const color = countryColor;
52173
52296
  const text = c.name;
52174
52297
  const tw = labelWidth(text, 0, fontSize);
52175
52298
  if (tw > w || fontSize + 2 * PADY > h) continue;
@@ -52263,7 +52386,7 @@ function placeContextLabels(args) {
52263
52386
  }
52264
52387
  return placed;
52265
52388
  }
52266
- 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;
52267
52390
  var init_context_labels = __esm({
52268
52391
  "src/map/context-labels.ts"() {
52269
52392
  "use strict";
@@ -52280,7 +52403,6 @@ var init_context_labels = __esm({
52280
52403
  COUNTRY_FONT_MAX = 22;
52281
52404
  COUNTRY_SIZE_FRAC_MIN = 0.06;
52282
52405
  COUNTRY_SIZE_FRAC_MAX = 0.32;
52283
- COUNTRY_FADE_MAX = 45;
52284
52406
  MAX_COUNTRY_POSITIONS = 4;
52285
52407
  COUNTRY_POS_GRID = 5;
52286
52408
  COUNTRY_POS_TOPN_MARGIN = 3;
@@ -52418,36 +52540,23 @@ function countryLabelPositions(args) {
52418
52540
  return n;
52419
52541
  };
52420
52542
  const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
52421
- const pool = [...kept];
52422
- pool.sort((a, b) => {
52423
- const d = interiorness(b) - interiorness(a);
52424
- return d !== 0 ? d : dist2ToCentre(a) - dist2ToCentre(b);
52425
- });
52426
- const ordered = [pool.shift()];
52427
- while (ordered.length < MAX_COUNTRY_POSITIONS && pool.length) {
52428
- let bestIdx = 0;
52429
- let bestMin = -1;
52430
- for (let k = 0; k < pool.length; k++) {
52431
- const c = pool[k];
52432
- let minD = Infinity;
52433
- for (const o of ordered) {
52434
- const d = (c.sx - o.sx) ** 2 + (c.sy - o.sy) ** 2;
52435
- if (d < minD) minD = d;
52436
- }
52437
- if (minD > bestMin) {
52438
- bestMin = minD;
52439
- bestIdx = k;
52440
- }
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]]
52441
52550
  }
52442
- ordered.push(pool.splice(bestIdx, 1)[0]);
52443
- }
52444
- 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) => ({
52445
52555
  lonLat: [c.lon, c.lat],
52446
52556
  screen: [c.sx, c.sy]
52447
52557
  }));
52448
52558
  const curatedPos = curated ? mkCurated(curated, project) : [];
52449
- const out = [...curatedPos, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52450
- return out;
52559
+ return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52451
52560
  }
52452
52561
  function mkCurated(curated, project) {
52453
52562
  if (!curated) return [];
@@ -52713,6 +52822,25 @@ function layoutMap(resolvedIn, data, size, opts) {
52713
52822
  palette.bg,
52714
52823
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
52715
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
+ );
52716
52844
  const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
52717
52845
  const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
52718
52846
  const colorByIso = /* @__PURE__ */ new Map();
@@ -52827,6 +52955,33 @@ function layoutMap(resolvedIn, data, size, opts) {
52827
52955
  hasSubtitle: Boolean(resolved.subtitle)
52828
52956
  });
52829
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
+ });
52830
52985
  const reserve = opts._calloutReserve;
52831
52986
  const fitLeft = FIT_PAD + (reserve?.left ?? 0);
52832
52987
  const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
@@ -53556,6 +53711,19 @@ function layoutMap(resolvedIn, data, size, opts) {
53556
53711
  const by = b.cy - (b.cy - py) / tb * trimB;
53557
53712
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
53558
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
+ };
53559
53727
  const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
53560
53728
  const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
53561
53729
  const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
@@ -53569,13 +53737,12 @@ function layoutMap(resolvedIn, data, size, opts) {
53569
53737
  const a = poiScreen.get(leg.fromId);
53570
53738
  const b = poiScreen.get(leg.toId);
53571
53739
  if (!a || !b) continue;
53572
- const mx = (a.cx + b.cx) / 2;
53573
- const my = (a.cy + b.cy) / 2;
53740
+ const lp = legLabelPoint(a, b, leg.style === "arc", 0);
53574
53741
  const bow = {
53575
53742
  curved: leg.style === "arc",
53576
53743
  offset: 0,
53577
- labelX: mx,
53578
- labelY: my - 4
53744
+ labelX: lp.x,
53745
+ labelY: lp.y
53579
53746
  };
53580
53747
  const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53581
53748
  legs.push({
@@ -53622,13 +53789,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53622
53789
  const b = poiScreen.get(e.toId);
53623
53790
  if (!a || !b) return;
53624
53791
  const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
53625
- const mx = (a.cx + b.cx) / 2;
53626
- const my = (a.cy + b.cy) / 2;
53792
+ const curved = e.style === "arc" || n > 1;
53793
+ const lp = legLabelPoint(a, b, curved, fanOffset);
53627
53794
  const bow = {
53628
- curved: e.style === "arc" || n > 1,
53795
+ curved,
53629
53796
  offset: fanOffset,
53630
- labelX: mx,
53631
- labelY: my - 4
53797
+ labelX: lp.x,
53798
+ labelY: lp.y
53632
53799
  };
53633
53800
  const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53634
53801
  legs.push({
@@ -53652,7 +53819,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53652
53819
  });
53653
53820
  }
53654
53821
  const labels = [];
53655
- const obstacles = [];
53822
+ const obstacles = [...topReserved];
53656
53823
  const regionLabelGuards = [];
53657
53824
  const markers = pois.map((p) => ({
53658
53825
  cx: p.cx,
@@ -53690,6 +53857,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53690
53857
  }
53691
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));
53692
53859
  const showRegionLabels = resolved.directives.noRegionLabels !== true;
53860
+ const labeledRegionIds = /* @__PURE__ */ new Set();
53693
53861
  const showRegionValues = resolved.directives.noRegionValue !== true && activeIsScore;
53694
53862
  const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
53695
53863
  const isCompact = width < COMPACT_WIDTH_PX;
@@ -53710,7 +53878,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53710
53878
  const REGION_FONT_MAX_ORIENT = 22;
53711
53879
  const REGION_SIZE_FRAC_MIN = 0.06;
53712
53880
  const REGION_SIZE_FRAC_MAX = 0.32;
53713
- const REGION_FADE_ORIENT = 45;
53881
+ const SUBJECT_FONT_MAX = 18;
53882
+ const SUBJECT_MIN_PROMINENCE = 13;
53883
+ const SUBJECT_LEADER_FONT = 15;
53714
53884
  const canvasLinear = Math.sqrt(Math.max(1, width * height));
53715
53885
  const sizeT = (boxW, boxH) => {
53716
53886
  const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
@@ -53722,9 +53892,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53722
53892
  )
53723
53893
  );
53724
53894
  };
53725
- const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, fade = 0) => {
53895
+ const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
53726
53896
  const { color: baseColor, haloColor } = labelOnFill(fill2);
53727
- const color = fade > 0 ? mix(baseColor, palette.bg, fade) : baseColor;
53897
+ const color = colorOverride ?? baseColor;
53728
53898
  const vf = Math.round(fontSize * 0.82);
53729
53899
  const halfW = Math.max(
53730
53900
  measureLegendText(text, fontSize),
@@ -53739,7 +53909,11 @@ function layoutMap(resolvedIn, data, size, opts) {
53739
53909
  text,
53740
53910
  anchor: "middle",
53741
53911
  color,
53742
- 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,
53743
53917
  haloColor,
53744
53918
  ...fontSize !== FONT2 && { fontSize },
53745
53919
  ...valueLine !== void 0 && { valueLine },
@@ -53775,7 +53949,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53775
53949
  if (!c || !Number.isFinite(c[0])) return null;
53776
53950
  return { r, c, boxW, boxH, area: boxW * boxH, candidates };
53777
53951
  }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
53778
- const placedRegionRects = [];
53952
+ const placedRegionRects = [...topReserved];
53779
53953
  const placedLeaders = [];
53780
53954
  const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
53781
53955
  const POI_LABEL_PAD = 14;
@@ -53788,9 +53962,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53788
53962
  const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
53789
53963
  const STEP = 4;
53790
53964
  const HOP_GAP = 9;
53791
- const tryShortHopCallout = (cx, cy, fill2, name, value) => {
53792
- const chipW = stackW(name, value);
53793
- 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);
53794
53968
  const dirs = [
53795
53969
  [0, -1],
53796
53970
  // north (over Canada)
@@ -53806,7 +53980,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53806
53980
  let ex = cx;
53807
53981
  let ey = cy;
53808
53982
  let steps = 0;
53809
- const maxSteps = Math.ceil(SHORT_HOP_MAX / STEP) + 1;
53983
+ const maxSteps = Math.ceil(maxLen / STEP) + 1;
53810
53984
  while (steps < maxSteps && fillAt(ex, ey) === fill2) {
53811
53985
  ex += dx * STEP;
53812
53986
  ey += dy * STEP;
@@ -53814,8 +53988,8 @@ function layoutMap(resolvedIn, data, size, opts) {
53814
53988
  }
53815
53989
  if (fillAt(ex, ey) === fill2) continue;
53816
53990
  const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
53817
- const ccx = ex + dx * (HOP_GAP + halfAlong);
53818
- const ccy = ey + dy * (HOP_GAP + halfAlong);
53991
+ const ccx = ex + dx * (gap + halfAlong);
53992
+ const ccy = ey + dy * (gap + halfAlong);
53819
53993
  const rect = {
53820
53994
  x: ccx - chipW / 2,
53821
53995
  y: ccy - chipH / 2,
@@ -53840,7 +54014,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53840
54014
  const innerX = ccx - dx * (chipW / 2);
53841
54015
  const innerY = ccy - dy * (chipH / 2);
53842
54016
  const len = Math.hypot(innerX - cx, innerY - cy);
53843
- if (len > SHORT_HOP_MAX) continue;
54017
+ if (len > maxLen) continue;
53844
54018
  if (placedLeaders.some(
53845
54019
  (l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
53846
54020
  ))
@@ -53864,6 +54038,78 @@ function layoutMap(resolvedIn, data, size, opts) {
53864
54038
  const valStr = regionValueStr(r.value);
53865
54039
  const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
53866
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
+ }
53867
54113
  const seekAnchors = [
53868
54114
  { x: c[0], y: c[1], guard: false }
53869
54115
  ];
@@ -53886,13 +54132,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53886
54132
  const nameRect = regionLabelRect(a.x, a.y, t);
53887
54133
  if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
53888
54134
  const stackRect = regionLabelRect(a.x, a.y, t, valStr);
53889
- if (fitsRegions(stackRect) && fitsPois(nameRect)) {
54135
+ if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
53890
54136
  chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
53891
54137
  break;
53892
54138
  }
53893
54139
  }
53894
54140
  if (labelW(t) <= boxW && labelH <= boxH) {
53895
- if (fitsRegions(nameRect) && fitsPois(nameRect)) {
54141
+ if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
53896
54142
  chosen = { text: t, ax: a.x, ay: a.y };
53897
54143
  break;
53898
54144
  }
@@ -53900,7 +54146,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53900
54146
  }
53901
54147
  if (chosen) break;
53902
54148
  }
53903
- if (chosen === void 0 && valStr && r.label !== void 0) {
54149
+ if (chosen === void 0 && r.label !== void 0 && isSubject) {
53904
54150
  const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
53905
54151
  if (hop) {
53906
54152
  placedRegionRects.push(hop.rect);
@@ -53912,9 +54158,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53912
54158
  text: r.label,
53913
54159
  anchor: "middle",
53914
54160
  color: palette.text,
53915
- halo: true,
54161
+ halo: false,
53916
54162
  haloColor: palette.bg,
53917
- valueLine: valStr,
54163
+ ...valStr !== void 0 && { valueLine: valStr },
53918
54164
  leader: {
53919
54165
  x1: hop.leader[0],
53920
54166
  y1: hop.leader[1],
@@ -53925,20 +54171,28 @@ function layoutMap(resolvedIn, data, size, opts) {
53925
54171
  calloutDot: { x: c[0], y: c[1], color: dark },
53926
54172
  lineNumber: r.lineNumber
53927
54173
  });
54174
+ labeledRegionIds.add(r.id);
53928
54175
  }
53929
54176
  continue;
53930
54177
  }
53931
54178
  if (chosen === void 0) continue;
53932
54179
  const isOrient = r.value === void 0 && r.layer === "base";
53933
54180
  let font = FONT2;
53934
- let fade = 0;
53935
54181
  if (isOrient) {
53936
54182
  const growT = sizeT(boxW, boxH);
53937
54183
  const desiredFont = Math.round(
53938
54184
  FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
53939
54185
  );
53940
54186
  const hasVal = chosen.valueLine !== void 0;
53941
- 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--) {
53942
54196
  if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
53943
54197
  continue;
53944
54198
  const gRect = regionLabelRect(
@@ -53955,14 +54209,18 @@ function layoutMap(resolvedIn, data, size, opts) {
53955
54209
  void 0,
53956
54210
  f
53957
54211
  );
53958
- if (!fitsRegions(gRect) || !fitsPois(gName)) continue;
53959
- const halfW = measureLegendText(chosen.text, f) / 2;
53960
- 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))
53961
54213
  continue;
53962
- font = f;
54214
+ if (!extremesClean(f)) continue;
54215
+ fit = f;
53963
54216
  break;
53964
54217
  }
53965
- 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
+ }
53966
54224
  }
53967
54225
  const rRect = regionLabelRect(
53968
54226
  chosen.ax,
@@ -53980,8 +54238,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53980
54238
  r.lineNumber,
53981
54239
  chosen.valueLine,
53982
54240
  font,
53983
- fade
54241
+ isOrient ? backdropLandTone : void 0
53984
54242
  );
54243
+ labeledRegionIds.add(r.id);
53985
54244
  regionLabelGuards.push({
53986
54245
  label: labels[labels.length - 1],
53987
54246
  rect: rRect
@@ -53999,6 +54258,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53999
54258
  seed.lineNumber,
54000
54259
  valStr
54001
54260
  );
54261
+ labeledRegionIds.add(seed.iso);
54002
54262
  regionLabelGuards.push({
54003
54263
  label: labels[labels.length - 1],
54004
54264
  rect: regionLabelRect(seed.x, seed.y, text, valStr)
@@ -54430,13 +54690,30 @@ function layoutMap(resolvedIn, data, size, opts) {
54430
54690
  const nx = -dy / len;
54431
54691
  const ny = dx / len;
54432
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
+ };
54433
54711
  const candidates = [];
54434
54712
  for (const off of [0, perp, -perp, 2 * perp, -2 * perp])
54435
- for (const t of T_LIST)
54436
- candidates.push([
54437
- a.cx + dx * t + nx * off,
54438
- a.cy + dy * t + ny * off - 4
54439
- ]);
54713
+ for (const t of T_LIST) {
54714
+ const [bx, by] = pointAt(t);
54715
+ candidates.push([bx + nx * off, by + ny * off - 4]);
54716
+ }
54440
54717
  const clean = ([cx2, cy2]) => {
54441
54718
  const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
54442
54719
  if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
@@ -54446,10 +54723,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54446
54723
  if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
54447
54724
  return true;
54448
54725
  };
54449
- const [cx, cy] = candidates.find(clean) ?? [
54450
- a.cx + dx * 0.5,
54451
- a.cy + dy * 0.5 - 4
54452
- ];
54726
+ const mid = pointAt(0.5);
54727
+ const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
54453
54728
  const style = labelOnFill(fillAt(cx, cy));
54454
54729
  legs[i] = {
54455
54730
  ...lg,
@@ -54477,7 +54752,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54477
54752
  const rawCountries = [];
54478
54753
  for (const f of worldLayer.values()) {
54479
54754
  const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
54480
- if (!iso || regionById.has(iso)) continue;
54755
+ if (!iso || regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54481
54756
  let hasReferencedSub = false;
54482
54757
  for (const k of regionById.keys())
54483
54758
  if (k.startsWith(iso + "-")) {
@@ -54551,7 +54826,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54551
54826
  );
54552
54827
  if (usLayer && framedStateContainers) {
54553
54828
  for (const [iso, f] of usLayer) {
54554
- if (regionById.has(iso)) continue;
54829
+ if (regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54555
54830
  const viewF = cullFeatureToView(f);
54556
54831
  if (!viewF) continue;
54557
54832
  const b = path.bounds(viewF);
@@ -54575,6 +54850,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54575
54850
  waterBodies: data.waterBodies,
54576
54851
  countries: countryCandidates,
54577
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,
54578
54858
  project,
54579
54859
  collides,
54580
54860
  contentPoints,
@@ -55128,19 +55408,60 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
55128
55408
  sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
55129
55409
  };
55130
55410
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
55131
- layout.legs.forEach((leg, i) => {
55132
- 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);
55133
55457
  if (leg.tags) {
55134
55458
  for (const [group, value] of Object.entries(leg.tags)) {
55135
55459
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
55136
55460
  }
55137
55461
  }
55138
55462
  wireSync(p, leg.lineNumber);
55139
- if (leg.arrow) {
55140
- const id = nid(`dgmo-map-arrow-${i}`);
55141
- const s = arrowSize(leg.width);
55142
- 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);
55143
- 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);
55144
55465
  }
55145
55466
  if (leg.label !== void 0 && leg.labelX !== void 0) {
55146
55467
  const lt = emitText(