@diagrammo/dgmo 0.34.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 (55) 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 +462 -465
  6. package/dist/advanced.d.cts +2 -51
  7. package/dist/advanced.d.ts +2 -51
  8. package/dist/advanced.js +462 -466
  9. package/dist/auto.cjs +463 -142
  10. package/dist/auto.js +91 -91
  11. package/dist/auto.mjs +463 -142
  12. package/dist/cli.cjs +143 -326
  13. package/dist/index.cjs +462 -141
  14. package/dist/index.js +462 -141
  15. package/docs/ai-integration.md +46 -26
  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.cjs CHANGED
@@ -37,6 +37,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
37
37
  function makeDgmoError(line11, message, severity = "error", code) {
38
38
  return code !== void 0 ? { line: line11, message, severity, code } : { line: line11, message, severity };
39
39
  }
40
+ function dedupeDiagnostics(diagnostics) {
41
+ const seen = /* @__PURE__ */ new Set();
42
+ const out = [];
43
+ for (const d of diagnostics) {
44
+ const key = `${d.line}\0${d.column ?? ""}\0${d.severity}\0${d.code ?? ""}\0${d.message}`;
45
+ if (seen.has(key)) continue;
46
+ seen.add(key);
47
+ out.push(d);
48
+ }
49
+ return out;
50
+ }
40
51
  function formatDgmoError(err) {
41
52
  return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
42
53
  }
@@ -94,7 +105,7 @@ function vennAliasKeywordRemovedMessage(args) {
94
105
  return `Venn 'alias' keyword was removed. Use 'as' instead \u2014 '${args.name} as ${args.alias}'.`;
95
106
  }
96
107
  function pipeOperatorRemovedMessage() {
97
- return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'. Run 'dgmo migrate'`;
108
+ return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'`;
98
109
  }
99
110
  function ganttBarePercentRemovedMessage(percent) {
100
111
  return `Bare-percent progress shorthand '| ${percent}' was removed. Use 'progress: ${percent.replace(/%$/, "")}' (integer 0\u2013100, no '%').`;
@@ -1150,7 +1161,7 @@ var init_parsing = __esm({
1150
1161
  "no-title",
1151
1162
  "no-notes"
1152
1163
  ]);
1153
- MULTIPLE_PIPE_ERROR = 'Use a single "|" to start metadata, then separate items with commas.';
1164
+ 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.";
1154
1165
  RESERVED_KEY_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
1155
1166
  AS_ALIAS_RE = /\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})$/;
1156
1167
  POTENTIAL_META_RE = /\b([\w-]+)\s*:/;
@@ -1331,12 +1342,10 @@ function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1331
1342
  );
1332
1343
  if (!isPrefix) {
1333
1344
  const defined = group.entries.map((e) => e.value);
1334
- let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1345
+ let msg = `Unknown value '${value}' for tag group '${group.name}' \u2014 use one of: ${defined.join(", ")}`;
1335
1346
  const hint = suggestFn?.(value, defined);
1336
1347
  if (hint) {
1337
1348
  msg += `. ${hint}`;
1338
- } else {
1339
- msg += ` \u2014 defined values: ${defined.join(", ")}`;
1340
1349
  }
1341
1350
  pushWarning(entity.lineNumber, msg);
1342
1351
  }
@@ -2112,7 +2121,10 @@ function parseVisualizationFull(content, palette) {
2112
2121
  lineNumber
2113
2122
  });
2114
2123
  } else {
2115
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
2124
+ warn(
2125
+ lineNumber,
2126
+ `Unrecognized era entry: "${line11}" \u2014 use '<start> -> <end> Label [color]' (e.g. '1716 -> 1717 Reign').`
2127
+ );
2116
2128
  }
2117
2129
  continue;
2118
2130
  }
@@ -2140,7 +2152,10 @@ function parseVisualizationFull(content, palette) {
2140
2152
  lineNumber
2141
2153
  });
2142
2154
  } else {
2143
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
2155
+ warn(
2156
+ lineNumber,
2157
+ `Unrecognized marker entry: "${line11}" \u2014 use '<date> Label [color]' (e.g. '1776 Independence').`
2158
+ );
2144
2159
  }
2145
2160
  continue;
2146
2161
  }
@@ -2818,7 +2833,7 @@ function parseVisualizationFull(content, palette) {
2818
2833
  result.diagnostics.push(
2819
2834
  makeDgmoError(
2820
2835
  ov.lineNumber,
2821
- `Intersection references unknown set or alias "${ref}"`
2836
+ `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)`
2822
2837
  )
2823
2838
  );
2824
2839
  if (!result.error)
@@ -6262,10 +6277,10 @@ function parseSequenceDgmo(content, palette) {
6262
6277
  const rawInside = fallbackMatch[1];
6263
6278
  const pipeIdx = rawInside.indexOf("|");
6264
6279
  const cleanName = rawInside.substring(0, pipeIdx).trim().replace(/\([^)]*\)$/, "").trim();
6265
- const metaPart = rawInside.substring(pipeIdx).trim();
6280
+ const metaPart = rawInside.substring(pipeIdx).replace(/^\|\s*/, "").trim();
6266
6281
  pushError(
6267
6282
  lineNumber,
6268
- `Pipe metadata must go outside brackets \u2014 use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`
6283
+ `Metadata goes after the bracket with no pipe \u2014 use '[${cleanName}] ${metaPart}' (e.g. '[Backend] t: Product'), not '[${rawInside.trim()}]'`
6269
6284
  );
6270
6285
  continue;
6271
6286
  }
@@ -6875,7 +6890,7 @@ function parseSequenceDgmo(content, palette) {
6875
6890
  }
6876
6891
  pushWarning(
6877
6892
  lineNumber,
6878
- `Unexpected line: '${trimmed}'. Expected a message (A -> B), participant, section (== Name ==), or block keyword (if/loop/parallel).`
6893
+ `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>').`
6879
6894
  );
6880
6895
  }
6881
6896
  if (!hasExplicitChart && result.messages.length === 0) {
@@ -8037,7 +8052,11 @@ function parseFlowchart(content, palette) {
8037
8052
  const value = optMatch[2].trim();
8038
8053
  if (key === "no-color") {
8039
8054
  result.diagnostics.push(
8040
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8055
+ makeDgmoError(
8056
+ lineNumber,
8057
+ '"no-color" was removed \u2014 node colors are assigned automatically by shape; delete this line.',
8058
+ "warning"
8059
+ )
8041
8060
  );
8042
8061
  continue;
8043
8062
  }
@@ -8366,7 +8385,11 @@ function parseState(content, palette) {
8366
8385
  const value = optMatch[2].trim();
8367
8386
  if (key === "no-color") {
8368
8387
  result.diagnostics.push(
8369
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8388
+ makeDgmoError(
8389
+ lineNumber,
8390
+ '"no-color" was removed \u2014 state colors are assigned automatically; delete this line.',
8391
+ "warning"
8392
+ )
8370
8393
  );
8371
8394
  continue;
8372
8395
  }
@@ -8681,7 +8704,7 @@ function parseClassDiagram(content, palette) {
8681
8704
  result.diagnostics.push(
8682
8705
  makeDgmoError(
8683
8706
  lineNumber,
8684
- '"no-auto-color" has been removed.',
8707
+ '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.',
8685
8708
  "warning"
8686
8709
  )
8687
8710
  );
@@ -8796,7 +8819,11 @@ function parseClassDiagram(content, palette) {
8796
8819
  continue;
8797
8820
  }
8798
8821
  result.diagnostics.push(
8799
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
8822
+ makeDgmoError(
8823
+ lineNumber,
8824
+ `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)`,
8825
+ "warning"
8826
+ )
8800
8827
  );
8801
8828
  }
8802
8829
  if (notes.length > 0) {
@@ -9274,7 +9301,11 @@ function parseERDiagram(content, palette) {
9274
9301
  continue;
9275
9302
  }
9276
9303
  result.diagnostics.push(
9277
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
9304
+ makeDgmoError(
9305
+ lineNumber,
9306
+ `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)`,
9307
+ "warning"
9308
+ )
9278
9309
  );
9279
9310
  }
9280
9311
  if (result.tables.length === 0 && !result.error) {
@@ -9618,7 +9649,10 @@ function parseOrg(content, palette) {
9618
9649
  const value = metadataMatch[2].trim();
9619
9650
  const parent = findMetadataParent(indent, indentStack);
9620
9651
  if (!parent) {
9621
- pushError(lineNumber, "Metadata has no parent node");
9652
+ pushError(
9653
+ lineNumber,
9654
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9655
+ );
9622
9656
  } else {
9623
9657
  parent.metadata = { ...parent.metadata, [key]: value };
9624
9658
  }
@@ -9636,7 +9670,10 @@ function parseOrg(content, palette) {
9636
9670
  );
9637
9671
  attachNode(node, indent, indentStack, result);
9638
9672
  } else {
9639
- pushError(lineNumber, "Metadata has no parent node");
9673
+ pushError(
9674
+ lineNumber,
9675
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9676
+ );
9640
9677
  }
9641
9678
  } else {
9642
9679
  const node = parseNodeLabel(
@@ -9985,7 +10022,10 @@ function parseSitemap(content, palette) {
9985
10022
  if (arrowInfo) {
9986
10023
  const source = findParentNode(indent, indentStack);
9987
10024
  if (!source) {
9988
- pushError(lineNumber, "Arrow has no source node");
10025
+ pushError(
10026
+ lineNumber,
10027
+ "Arrow '-> target' must be indented under its source page \u2014 no page precedes it."
10028
+ );
9989
10029
  } else {
9990
10030
  deferredArrows.push({
9991
10031
  sourceNode: source,
@@ -10055,7 +10095,10 @@ function parseSitemap(content, palette) {
10055
10095
  const value = metadataMatch[2].trim();
10056
10096
  const parent = findParentNode(indent, indentStack);
10057
10097
  if (!parent) {
10058
- pushError(lineNumber, "Metadata has no parent node");
10098
+ pushError(
10099
+ lineNumber,
10100
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10101
+ );
10059
10102
  } else {
10060
10103
  parent.metadata = { ...parent.metadata, [key]: value };
10061
10104
  }
@@ -10075,7 +10118,10 @@ function parseSitemap(content, palette) {
10075
10118
  const key = normalizeName(node.label);
10076
10119
  labelToNode.set(key, node);
10077
10120
  } else {
10078
- pushError(lineNumber, "Metadata has no parent node");
10121
+ pushError(
10122
+ lineNumber,
10123
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10124
+ );
10079
10125
  }
10080
10126
  } else {
10081
10127
  const descResult = tryStripDescriptionKeyword(trimmed);
@@ -11893,7 +11939,7 @@ function parseChart(content, palette) {
11893
11939
  result.diagnostics.push(
11894
11940
  makeDgmoError(
11895
11941
  lineNumber,
11896
- `'${trimmed}' \u2014 ## syntax is no longer supported. Use [Group] containers instead`
11942
+ `'${trimmed}' removed \u2014 use a bracket container '[${trimmed.replace(/^#+\s*/, "")}]' instead (## headers are no longer supported).`
11897
11943
  )
11898
11944
  );
11899
11945
  continue;
@@ -12850,7 +12896,11 @@ function parseExtendedChartFull(content, palette) {
12850
12896
  continue;
12851
12897
  }
12852
12898
  result.diagnostics.push(
12853
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
12899
+ makeDgmoError(
12900
+ lineNumber,
12901
+ `Unexpected line: '${trimmed}'. Expected a data row ('Label value', space-separated) or a known option.`,
12902
+ "warning"
12903
+ )
12854
12904
  );
12855
12905
  }
12856
12906
  const warn = (line11, message) => {
@@ -15212,7 +15262,10 @@ function parseKanban(content, palette) {
15212
15262
  continue;
15213
15263
  }
15214
15264
  if (!currentColumn) {
15215
- warn(lineNumber, "Card line found before any column");
15265
+ warn(
15266
+ lineNumber,
15267
+ "Card line found before any column. Declare a column first with '[Column Name]', then indent cards beneath it. (\xA711)"
15268
+ );
15216
15269
  continue;
15217
15270
  }
15218
15271
  if (currentCard && indent > cardBaseIndent) {
@@ -15351,7 +15404,7 @@ var init_parser13 = __esm({
15351
15404
  KNOWN_OPTIONS4 = /* @__PURE__ */ new Set(["hide", "active-tag"]);
15352
15405
  KNOWN_BOOLEANS3 = /* @__PURE__ */ new Set(["solid-fill", "no-title"]);
15353
15406
  REMOVED_BOOLEANS = {
15354
- "no-auto-color": '"no-auto-color" has been removed.'
15407
+ "no-auto-color": '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.'
15355
15408
  };
15356
15409
  }
15357
15410
  });
@@ -15588,7 +15641,7 @@ function parseC4(content, palette) {
15588
15641
  } else {
15589
15642
  pushError(
15590
15643
  lineNumber,
15591
- `"container ${refName}" must be inside a deployment node`
15644
+ `"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
15592
15645
  );
15593
15646
  }
15594
15647
  continue;
@@ -15643,7 +15696,10 @@ function parseC4(content, palette) {
15643
15696
  indent
15644
15697
  });
15645
15698
  } else {
15646
- pushError(lineNumber, `"${sectionType}" must be inside an element`);
15699
+ pushError(
15700
+ lineNumber,
15701
+ `"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
15702
+ );
15647
15703
  }
15648
15704
  continue;
15649
15705
  }
@@ -15670,7 +15726,10 @@ function parseC4(content, palette) {
15670
15726
  indent
15671
15727
  });
15672
15728
  } else {
15673
- pushError(lineNumber, `Group [${groupName}] must be inside an element`);
15729
+ pushError(
15730
+ lineNumber,
15731
+ `Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
15732
+ );
15674
15733
  }
15675
15734
  continue;
15676
15735
  }
@@ -16000,7 +16059,10 @@ function parseC4(content, palette) {
16000
16059
  }
16001
16060
  desc.push(descResult.text);
16002
16061
  } else {
16003
- pushError(lineNumber, `Unexpected content: "${trimmed}"`);
16062
+ pushError(
16063
+ lineNumber,
16064
+ `Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (\xA78)`
16065
+ );
16004
16066
  }
16005
16067
  }
16006
16068
  finalizeAutoTagColors(result.tagGroups, palette);
@@ -16062,7 +16124,7 @@ function validateRelationshipTargets(result, knownNames, pushWarning) {
16062
16124
  for (const el of elements) {
16063
16125
  for (const rel of el.relationships) {
16064
16126
  if (!knownNames.has(normalizeName(rel.target))) {
16065
- let msg = `Relationship target "${rel.target}" not found`;
16127
+ 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`;
16066
16128
  const hint = suggest(normalizeName(rel.target), allNames);
16067
16129
  if (hint) msg += `. ${hint}`;
16068
16130
  pushWarning(rel.lineNumber, msg, "warning");
@@ -16090,7 +16152,7 @@ function validateDeploymentRefs(result, knownNames, pushWarning) {
16090
16152
  for (const node of nodes) {
16091
16153
  for (const ref of node.containerRefs) {
16092
16154
  if (!knownNames.has(normalizeName(ref))) {
16093
- let msg = `Deployment reference "container ${ref}" not found`;
16155
+ let msg = `Deployment reference "container ${ref}" not found \u2014 declare the container element first, then reference it under the deployment node`;
16094
16156
  const hint = suggest(normalizeName(ref), allNames);
16095
16157
  if (hint) msg += `. ${hint}`;
16096
16158
  pushWarning(node.lineNumber, msg, "warning");
@@ -16809,7 +16871,7 @@ function parseInfra(content) {
16809
16871
  }
16810
16872
  warn(
16811
16873
  lineNumber,
16812
- `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
16874
+ `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 '|'.`
16813
16875
  );
16814
16876
  continue;
16815
16877
  }
@@ -16882,7 +16944,7 @@ function parseInfra(content) {
16882
16944
  const n = deprecatedFanout[1];
16883
16945
  setError(
16884
16946
  lineNumber,
16885
- `'x${n}' fanout syntax is no longer supported \u2014 use '| fanout: ${n}' instead`
16947
+ `'x${n}' fanout syntax is no longer supported \u2014 use 'fanout: ${n}' instead`
16886
16948
  );
16887
16949
  continue;
16888
16950
  }
@@ -17056,8 +17118,10 @@ function parseInfra(content) {
17056
17118
  const hint = suggest(key, allKeys);
17057
17119
  const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
17058
17120
  if (hint || valueLooksNumeric) {
17059
- let msg = `Unknown property '${key}'.`;
17121
+ let msg = `Unknown infra property '${key}'.`;
17060
17122
  if (hint) msg += ` ${hint}`;
17123
+ else
17124
+ msg += ` Properties are colon-keyed 'key: value' from the known schema (e.g. latency-ms, max-rps, cache-hit, instances, concurrency).`;
17061
17125
  warn(lineNumber, msg);
17062
17126
  } else if (!currentNode.isEdge) {
17063
17127
  pushDescription(currentNode, trimmed);
@@ -17068,7 +17132,7 @@ function parseInfra(content) {
17068
17132
  if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
17069
17133
  warn(
17070
17134
  lineNumber,
17071
- `Property '${key}' is only valid on the entry point (Edge/Internet).`
17135
+ `Property '${key}' belongs on the entry point \u2014 declare it indented under an 'internet' or 'edge' node, not on '${currentNode.label}'.`
17072
17136
  );
17073
17137
  }
17074
17138
  const value = parsePropertyValue(rawVal);
@@ -17175,7 +17239,7 @@ function parseInfra(content) {
17175
17239
  }
17176
17240
  warn(
17177
17241
  lineNumber,
17178
- currentGroup ? `Unexpected line: '${trimmed}'. Expected an indented component name.` : `Unexpected line: '${trimmed}'. Expected a component name, [Group Name], tag group, or option.`
17242
+ 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'.`
17179
17243
  );
17180
17244
  }
17181
17245
  finishCurrentNode();
@@ -17551,7 +17615,10 @@ function parseGantt(content, palette) {
17551
17615
  continue;
17552
17616
  }
17553
17617
  if (COMMENT_RE.test(line11)) continue;
17554
- warn(lineNumber, `Unrecognized holiday entry: "${line11}"`);
17618
+ warn(
17619
+ lineNumber,
17620
+ `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)`
17621
+ );
17555
17622
  continue;
17556
17623
  }
17557
17624
  }
@@ -17577,7 +17644,10 @@ function parseGantt(content, palette) {
17577
17644
  lineNumber
17578
17645
  });
17579
17646
  } else {
17580
- warn(lineNumber, `Unrecognized era entry: "${line11}"`);
17647
+ warn(
17648
+ lineNumber,
17649
+ `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)`
17650
+ );
17581
17651
  }
17582
17652
  continue;
17583
17653
  }
@@ -17603,7 +17673,10 @@ function parseGantt(content, palette) {
17603
17673
  lineNumber
17604
17674
  });
17605
17675
  } else {
17606
- warn(lineNumber, `Unrecognized marker entry: "${line11}"`);
17676
+ warn(
17677
+ lineNumber,
17678
+ `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)`
17679
+ );
17607
17680
  }
17608
17681
  continue;
17609
17682
  }
@@ -17769,7 +17842,7 @@ function parseGantt(content, palette) {
17769
17842
  } else {
17770
17843
  softError(
17771
17844
  lineNumber,
17772
- `Arrow reference "${content2.name}" has no parent task context.`
17845
+ `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)`
17773
17846
  );
17774
17847
  }
17775
17848
  }
@@ -18989,7 +19062,10 @@ function parseMap(content, palette) {
18989
19062
  function handleTag(trimmed, line11) {
18990
19063
  const m = matchTagBlockHeading(trimmed);
18991
19064
  if (!m) {
18992
- pushError(line11, `Malformed tag declaration: "${trimmed}".`);
19065
+ pushError(
19066
+ line11,
19067
+ `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.`
19068
+ );
18993
19069
  return;
18994
19070
  }
18995
19071
  emitTagLegacyDiagnostic(m, line11, diagnostics);
@@ -19101,7 +19177,7 @@ function parseMap(content, palette) {
19101
19177
  if (!pos || pos.kind === "name" && !pos.name) {
19102
19178
  pushError(
19103
19179
  line11,
19104
- "route requires an origin: `route <origin> [style: arc]`."
19180
+ "route needs an origin place \u2014 `route <origin>` then indented legs (`-> dest`); origin may be a city name, IATA code, or `<lat> <lon>`. (\xA724B)"
19105
19181
  );
19106
19182
  return;
19107
19183
  }
@@ -19164,7 +19240,10 @@ function parseMap(content, palette) {
19164
19240
  function handleEdges(trimmed, line11) {
19165
19241
  const parts = trimmed.split(ARROW_SPLIT);
19166
19242
  if (parts.length < 3) {
19167
- pushError(line11, `Malformed edge: "${trimmed}".`);
19243
+ pushError(
19244
+ line11,
19245
+ `Malformed edge: "${trimmed}" \u2014 an edge is 'A -> B' (or 'A -label-> B', '~>' for arc, '--' undirected), both endpoints non-empty. (\xA724B)`
19246
+ );
19168
19247
  return;
19169
19248
  }
19170
19249
  const endpoints = [];
@@ -19187,7 +19266,10 @@ function parseMap(content, palette) {
19187
19266
  const from = endpoints[k];
19188
19267
  const to = endpoints[k + 1];
19189
19268
  if (!from || !to) {
19190
- pushError(line11, `Edge has an empty endpoint: "${trimmed}".`);
19269
+ pushError(
19270
+ line11,
19271
+ `Edge has an empty endpoint: "${trimmed}" \u2014 both sides of the arrow need a place, e.g. 'Paris -> Berlin'. (\xA724B)`
19272
+ );
19191
19273
  continue;
19192
19274
  }
19193
19275
  const isLast = k === links.length - 1;
@@ -21306,7 +21388,10 @@ function parseTechRadar(content) {
21306
21388
  }
21307
21389
  if (indent === 0 && trimmed.toLowerCase() === "rings") {
21308
21390
  if (result.rings.length > 0) {
21309
- warn(lineNumber, 'Duplicate "rings" block \u2014 using last one');
21391
+ warn(
21392
+ lineNumber,
21393
+ 'Duplicate "rings" block \u2014 only the last is kept; merge all ring names into one "rings" block.'
21394
+ );
21310
21395
  result.rings = [];
21311
21396
  }
21312
21397
  inRingsBlock = true;
@@ -21525,7 +21610,7 @@ function parseTechRadar(content) {
21525
21610
  result.diagnostics.push(
21526
21611
  makeDgmoError(
21527
21612
  lineNumber,
21528
- `Blip "${trimmed}" has no ring assignment. Place it under a ring section header or use: ${trimmed} | ring: RingName`
21613
+ `Blip "${trimmed}" has no ring. Put it under a ring section header, or add a colon-keyed ring \u2014 '${trimmed} ring: RingName' (per \xA720).`
21529
21614
  )
21530
21615
  );
21531
21616
  continue;
@@ -21534,7 +21619,7 @@ function parseTechRadar(content) {
21534
21619
  if (!trimmed.includes("|") && headerParsed && result.rings.length > 0) {
21535
21620
  warn(
21536
21621
  lineNumber,
21537
- `Unrecognized line "${trimmed}". Quadrant headers require pipe metadata: ${trimmed} | quadrant: top-left`
21622
+ `Unrecognized line "${trimmed}". A quadrant header is 'Name quadrant: <top-left|top-right|bottom-left|bottom-right>' (per \xA720).`
21538
21623
  );
21539
21624
  }
21540
21625
  }
@@ -21827,7 +21912,10 @@ function parseCycle(content) {
21827
21912
  currentNode.description.push(descLine);
21828
21913
  continue;
21829
21914
  }
21830
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
21915
+ warn(
21916
+ lineNum,
21917
+ `Indented line "${trimmed}" has no step above it \u2014 add a step on a non-indented line first, then indent its details. (\xA721)`
21918
+ );
21831
21919
  continue;
21832
21920
  }
21833
21921
  }
@@ -22130,7 +22218,7 @@ function parseJourneyMap(content, palette) {
22130
22218
  if (trimmed.includes("|") || !PHASE_RE.test(trimmed)) {
22131
22219
  warn(
22132
22220
  lineNumber,
22133
- "Steps outside phases will be ignored when phases are present"
22221
+ `Step "${trimmed}" sits outside any phase and is ignored \u2014 indent it under a '[Phase]' header (per \xA722).`
22134
22222
  );
22135
22223
  continue;
22136
22224
  }
@@ -22187,7 +22275,10 @@ function parseJourneyMap(content, palette) {
22187
22275
  continue;
22188
22276
  }
22189
22277
  if (indent > 0 && !currentPhase && hasPhases) {
22190
- warn(lineNumber, `Unexpected indented line outside of a phase`);
22278
+ warn(
22279
+ lineNumber,
22280
+ `Indented line outside any phase \u2014 add a '[Phase]' header above it, or unindent it to a top-level step. (\xA722)`
22281
+ );
22191
22282
  }
22192
22283
  }
22193
22284
  if (currentStep) {
@@ -22533,7 +22624,10 @@ function parsePyramid(content) {
22533
22624
  continue;
22534
22625
  }
22535
22626
  if (!currentLayer) {
22536
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22627
+ warn(
22628
+ lineNum,
22629
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA723)`
22630
+ );
22537
22631
  continue;
22538
22632
  }
22539
22633
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -22707,7 +22801,10 @@ function parseRing(content) {
22707
22801
  continue;
22708
22802
  }
22709
22803
  if (!currentLayer) {
22710
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
22804
+ warn(
22805
+ lineNum,
22806
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA724)`
22807
+ );
22711
22808
  continue;
22712
22809
  }
22713
22810
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -23433,7 +23530,10 @@ function parseRaci(content, palette) {
23433
23530
  continue;
23434
23531
  }
23435
23532
  if (bodyStarted) {
23436
- errorAt(lineNumber, `Unexpected line: '${trimmed}'.`);
23533
+ errorAt(
23534
+ lineNumber,
23535
+ `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)`
23536
+ );
23437
23537
  } else {
23438
23538
  warn(lineNumber, `Unrecognized directive: '${trimmed}'.`);
23439
23539
  }
@@ -23980,6 +24080,13 @@ function getAllChartTypes() {
23980
24080
  return chartTypes.map((c) => c.id);
23981
24081
  }
23982
24082
  function parseDgmo(content) {
24083
+ const result = parseDgmoUndeduped(content);
24084
+ return {
24085
+ diagnostics: dedupeDiagnostics(result.diagnostics),
24086
+ chartType: result.chartType
24087
+ };
24088
+ }
24089
+ function parseDgmoUndeduped(content) {
23983
24090
  const chartType = parseDgmoChartType(content);
23984
24091
  if (!chartType) {
23985
24092
  const colonDiag = detectColonChartType(content);
@@ -51307,7 +51414,7 @@ function resolveMap(parsed, data) {
51307
51414
  const hint = suggest(r.name, allNames);
51308
51415
  err(
51309
51416
  r.lineNumber,
51310
- `Unknown subdivision "${r.name}".${hint ? " " + hint : ""}`,
51417
+ `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.`,
51311
51418
  "E_MAP_UNKNOWN_SUBDIVISION"
51312
51419
  );
51313
51420
  continue;
@@ -51387,7 +51494,7 @@ function resolveMap(parsed, data) {
51387
51494
  const hint = suggest(name, cityNames);
51388
51495
  err(
51389
51496
  line11,
51390
- `Unknown place "${name}" (not in the gazetteer; use coordinates).${hint ? " " + hint : ""}`,
51497
+ `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>\`.`,
51391
51498
  "E_MAP_UNKNOWN_PLACE"
51392
51499
  );
51393
51500
  return { kind: "miss" };
@@ -51929,6 +52036,21 @@ function mapLegendBand(legend, opts) {
51929
52036
  if (height <= 0) return 0;
51930
52037
  return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
51931
52038
  }
52039
+ function mapLegendBox(legend, opts) {
52040
+ if (!legend) return null;
52041
+ const groups = mapLegendGroups(legend);
52042
+ if (groups.length === 0) return null;
52043
+ const config = mapLegendConfig(groups, opts.mode);
52044
+ const state = { activeGroup: legend.activeGroup };
52045
+ const { width, height } = computeLegendLayout(config, state, opts.width);
52046
+ if (height <= 0 || width <= 0) return null;
52047
+ return {
52048
+ x: (opts.width - width) / 2,
52049
+ y: mapLegendTop(opts.hasTitle, opts.hasSubtitle),
52050
+ width,
52051
+ height
52052
+ };
52053
+ }
51932
52054
  var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
51933
52055
  var init_legend_band = __esm({
51934
52056
  "src/map/legend-band.ts"() {
@@ -52053,6 +52175,8 @@ function placeContextLabels(args) {
52053
52175
  waterBodies,
52054
52176
  countries,
52055
52177
  palette,
52178
+ countryTone,
52179
+ waterTone,
52056
52180
  project,
52057
52181
  collides,
52058
52182
  contentPoints,
@@ -52062,8 +52186,8 @@ function placeContextLabels(args) {
52062
52186
  const band = tierBand(Math.max(dLonSpan, dLatSpan));
52063
52187
  const budget = labelBudget(width, height, band);
52064
52188
  if (budget <= 0) return [];
52065
- const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
52066
- const countryColor = palette.textMuted;
52189
+ const waterColor = waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
52190
+ const countryColor = countryTone ?? palette.textMuted;
52067
52191
  const haloColor = palette.bg;
52068
52192
  const candidates = [];
52069
52193
  const center = [width / 2, height / 2];
@@ -52152,8 +52276,7 @@ function placeContextLabels(args) {
52152
52276
  )
52153
52277
  );
52154
52278
  const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
52155
- const fade = Math.round(t * COUNTRY_FADE_MAX);
52156
- const color = fade > 0 ? mix(countryColor, palette.bg, 100 - fade) : countryColor;
52279
+ const color = countryColor;
52157
52280
  const text = c.name;
52158
52281
  const tw = labelWidth(text, 0, fontSize);
52159
52282
  if (tw > w || fontSize + 2 * PADY > h) continue;
@@ -52247,7 +52370,7 @@ function placeContextLabels(args) {
52247
52370
  }
52248
52371
  return placed;
52249
52372
  }
52250
- 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;
52373
+ 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;
52251
52374
  var init_context_labels = __esm({
52252
52375
  "src/map/context-labels.ts"() {
52253
52376
  "use strict";
@@ -52264,7 +52387,6 @@ var init_context_labels = __esm({
52264
52387
  COUNTRY_FONT_MAX = 22;
52265
52388
  COUNTRY_SIZE_FRAC_MIN = 0.06;
52266
52389
  COUNTRY_SIZE_FRAC_MAX = 0.32;
52267
- COUNTRY_FADE_MAX = 45;
52268
52390
  MAX_COUNTRY_POSITIONS = 4;
52269
52391
  COUNTRY_POS_GRID = 5;
52270
52392
  COUNTRY_POS_TOPN_MARGIN = 3;
@@ -52391,36 +52513,23 @@ function countryLabelPositions(args) {
52391
52513
  return n;
52392
52514
  };
52393
52515
  const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
52394
- const pool = [...kept];
52395
- pool.sort((a, b) => {
52396
- const d = interiorness(b) - interiorness(a);
52397
- return d !== 0 ? d : dist2ToCentre(a) - dist2ToCentre(b);
52398
- });
52399
- const ordered = [pool.shift()];
52400
- while (ordered.length < MAX_COUNTRY_POSITIONS && pool.length) {
52401
- let bestIdx = 0;
52402
- let bestMin = -1;
52403
- for (let k = 0; k < pool.length; k++) {
52404
- const c = pool[k];
52405
- let minD = Infinity;
52406
- for (const o of ordered) {
52407
- const d = (c.sx - o.sx) ** 2 + (c.sy - o.sy) ** 2;
52408
- if (d < minD) minD = d;
52409
- }
52410
- if (minD > bestMin) {
52411
- bestMin = minD;
52412
- bestIdx = k;
52413
- }
52516
+ const centreLon = kept.reduce((s, c) => s + c.lon, 0) / kept.length;
52517
+ const centreLat = kept.reduce((s, c) => s + c.lat, 0) / kept.length;
52518
+ const centreScreen = project(centreLon, centreLat);
52519
+ 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 ? [
52520
+ {
52521
+ lonLat: [centreLon, centreLat],
52522
+ screen: [centreScreen[0], centreScreen[1]]
52414
52523
  }
52415
- ordered.push(pool.splice(bestIdx, 1)[0]);
52416
- }
52417
- const grid = ordered.map((c) => ({
52524
+ ] : [];
52525
+ const grid = [...kept].sort(
52526
+ (a, b) => interiorness(b) - interiorness(a) || dist2ToCentre(a) - dist2ToCentre(b)
52527
+ ).map((c) => ({
52418
52528
  lonLat: [c.lon, c.lat],
52419
52529
  screen: [c.sx, c.sy]
52420
52530
  }));
52421
52531
  const curatedPos = curated ? mkCurated(curated, project) : [];
52422
- const out = [...curatedPos, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52423
- return out;
52532
+ return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52424
52533
  }
52425
52534
  function mkCurated(curated, project) {
52426
52535
  if (!curated) return [];
@@ -52686,6 +52795,25 @@ function layoutMap(resolvedIn, data, size, opts) {
52686
52795
  palette.bg,
52687
52796
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
52688
52797
  );
52798
+ const contrastFloorTone = (tone, ...substrates) => {
52799
+ const clears = (t) => substrates.every((s) => contrastRatio(t, s) >= REGION_LABEL_HALO_RATIO);
52800
+ if (clears(tone)) return tone;
52801
+ for (let s = 10; s <= 100; s += 10) {
52802
+ const t = mix(palette.text, tone, s);
52803
+ if (clears(t)) return t;
52804
+ }
52805
+ return palette.text;
52806
+ };
52807
+ const backdropLandTone = contrastFloorTone(
52808
+ palette.textMuted,
52809
+ neutralFill,
52810
+ water
52811
+ );
52812
+ const backdropWaterTone = contrastFloorTone(
52813
+ mix(palette.colors.blue, palette.textMuted, 50),
52814
+ water,
52815
+ neutralFill
52816
+ );
52689
52817
  const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
52690
52818
  const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
52691
52819
  const colorByIso = /* @__PURE__ */ new Map();
@@ -52800,6 +52928,33 @@ function layoutMap(resolvedIn, data, size, opts) {
52800
52928
  hasSubtitle: Boolean(resolved.subtitle)
52801
52929
  });
52802
52930
  if (legendBand > topPad) topPad = legendBand;
52931
+ const topReserved = [];
52932
+ if (shownTitle) {
52933
+ const lines = resolved.subtitle ? 2 : 1;
52934
+ const tw = Math.max(
52935
+ measureLegendText(shownTitle, TITLE_FONT_SIZE),
52936
+ resolved.subtitle ? measureLegendText(resolved.subtitle, TITLE_FONT_SIZE) : 0
52937
+ ) + 2 * TITLE_FONT_SIZE;
52938
+ topReserved.push({
52939
+ x: width / 2 - tw / 2,
52940
+ y: TITLE_Y - TITLE_FONT_SIZE,
52941
+ w: tw,
52942
+ h: TITLE_FONT_SIZE * (lines + 0.5)
52943
+ });
52944
+ }
52945
+ const legendBox = mapLegendBox(legend, {
52946
+ width,
52947
+ mode: opts.legendMode ?? "preview",
52948
+ hasTitle: Boolean(shownTitle),
52949
+ hasSubtitle: Boolean(resolved.subtitle)
52950
+ });
52951
+ if (legendBox)
52952
+ topReserved.push({
52953
+ x: legendBox.x,
52954
+ y: legendBox.y,
52955
+ w: legendBox.width,
52956
+ h: legendBox.height
52957
+ });
52803
52958
  const reserve = opts._calloutReserve;
52804
52959
  const fitLeft = FIT_PAD + (reserve?.left ?? 0);
52805
52960
  const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
@@ -53529,6 +53684,19 @@ function layoutMap(resolvedIn, data, size, opts) {
53529
53684
  const by = b.cy - (b.cy - py) / tb * trimB;
53530
53685
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
53531
53686
  };
53687
+ const legLabelPoint = (a, b, curved, offset) => {
53688
+ const mx = (a.cx + b.cx) / 2;
53689
+ const my = (a.cy + b.cy) / 2;
53690
+ if (!curved && offset === 0) return { x: mx, y: my - 4 };
53691
+ const dx = b.cx - a.cx;
53692
+ const dy = b.cy - a.cy;
53693
+ const len = Math.hypot(dx, dy) || 1;
53694
+ const nx = -dy / len;
53695
+ const ny = dx / len;
53696
+ const bow = offset !== 0 ? offset : len * ARC_CURVE_FRAC;
53697
+ const off = bow * 0.5 + Math.sign(bow || 1) * 8;
53698
+ return { x: mx + nx * off, y: my + ny * off };
53699
+ };
53532
53700
  const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
53533
53701
  const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
53534
53702
  const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
@@ -53542,13 +53710,12 @@ function layoutMap(resolvedIn, data, size, opts) {
53542
53710
  const a = poiScreen.get(leg.fromId);
53543
53711
  const b = poiScreen.get(leg.toId);
53544
53712
  if (!a || !b) continue;
53545
- const mx = (a.cx + b.cx) / 2;
53546
- const my = (a.cy + b.cy) / 2;
53713
+ const lp = legLabelPoint(a, b, leg.style === "arc", 0);
53547
53714
  const bow = {
53548
53715
  curved: leg.style === "arc",
53549
53716
  offset: 0,
53550
- labelX: mx,
53551
- labelY: my - 4
53717
+ labelX: lp.x,
53718
+ labelY: lp.y
53552
53719
  };
53553
53720
  const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53554
53721
  legs.push({
@@ -53595,13 +53762,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53595
53762
  const b = poiScreen.get(e.toId);
53596
53763
  if (!a || !b) return;
53597
53764
  const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
53598
- const mx = (a.cx + b.cx) / 2;
53599
- const my = (a.cy + b.cy) / 2;
53765
+ const curved = e.style === "arc" || n > 1;
53766
+ const lp = legLabelPoint(a, b, curved, fanOffset);
53600
53767
  const bow = {
53601
- curved: e.style === "arc" || n > 1,
53768
+ curved,
53602
53769
  offset: fanOffset,
53603
- labelX: mx,
53604
- labelY: my - 4
53770
+ labelX: lp.x,
53771
+ labelY: lp.y
53605
53772
  };
53606
53773
  const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53607
53774
  legs.push({
@@ -53625,7 +53792,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53625
53792
  });
53626
53793
  }
53627
53794
  const labels = [];
53628
- const obstacles = [];
53795
+ const obstacles = [...topReserved];
53629
53796
  const regionLabelGuards = [];
53630
53797
  const markers = pois.map((p) => ({
53631
53798
  cx: p.cx,
@@ -53663,6 +53830,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53663
53830
  }
53664
53831
  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));
53665
53832
  const showRegionLabels = resolved.directives.noRegionLabels !== true;
53833
+ const labeledRegionIds = /* @__PURE__ */ new Set();
53666
53834
  const showRegionValues = resolved.directives.noRegionValue !== true && activeIsScore;
53667
53835
  const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
53668
53836
  const isCompact = width < COMPACT_WIDTH_PX;
@@ -53683,7 +53851,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53683
53851
  const REGION_FONT_MAX_ORIENT = 22;
53684
53852
  const REGION_SIZE_FRAC_MIN = 0.06;
53685
53853
  const REGION_SIZE_FRAC_MAX = 0.32;
53686
- const REGION_FADE_ORIENT = 45;
53854
+ const SUBJECT_FONT_MAX = 18;
53855
+ const SUBJECT_MIN_PROMINENCE = 13;
53856
+ const SUBJECT_LEADER_FONT = 15;
53687
53857
  const canvasLinear = Math.sqrt(Math.max(1, width * height));
53688
53858
  const sizeT = (boxW, boxH) => {
53689
53859
  const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
@@ -53695,9 +53865,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53695
53865
  )
53696
53866
  );
53697
53867
  };
53698
- const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, fade = 0) => {
53868
+ const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
53699
53869
  const { color: baseColor, haloColor } = labelOnFill(fill2);
53700
- const color = fade > 0 ? mix(baseColor, palette.bg, fade) : baseColor;
53870
+ const color = colorOverride ?? baseColor;
53701
53871
  const vf = Math.round(fontSize * 0.82);
53702
53872
  const halfW = Math.max(
53703
53873
  measureLegendText(text, fontSize),
@@ -53712,7 +53882,11 @@ function layoutMap(resolvedIn, data, size, opts) {
53712
53882
  text,
53713
53883
  anchor: "middle",
53714
53884
  color,
53715
- halo: overflows,
53885
+ // A backdrop label (colorOverride) NEVER haloes: the placement gate above
53886
+ // guarantees it sits on a floored-readable substrate (own fill, neutral
53887
+ // land, foreign land, or water), so a halo is pure noise. Data labels keep
53888
+ // the containment-gated halo until the role-tier pass (Phase 3).
53889
+ halo: colorOverride ? false : overflows,
53716
53890
  haloColor,
53717
53891
  ...fontSize !== FONT2 && { fontSize },
53718
53892
  ...valueLine !== void 0 && { valueLine },
@@ -53748,7 +53922,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53748
53922
  if (!c || !Number.isFinite(c[0])) return null;
53749
53923
  return { r, c, boxW, boxH, area: boxW * boxH, candidates };
53750
53924
  }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
53751
- const placedRegionRects = [];
53925
+ const placedRegionRects = [...topReserved];
53752
53926
  const placedLeaders = [];
53753
53927
  const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
53754
53928
  const POI_LABEL_PAD = 14;
@@ -53761,9 +53935,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53761
53935
  const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
53762
53936
  const STEP = 4;
53763
53937
  const HOP_GAP = 9;
53764
- const tryShortHopCallout = (cx, cy, fill2, name, value) => {
53765
- const chipW = stackW(name, value);
53766
- const chipH = stackH(value !== void 0);
53938
+ const tryShortHopCallout = (cx, cy, fill2, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
53939
+ const chipW = stackW(name, value, font);
53940
+ const chipH = stackH(value !== void 0, font);
53767
53941
  const dirs = [
53768
53942
  [0, -1],
53769
53943
  // north (over Canada)
@@ -53779,7 +53953,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53779
53953
  let ex = cx;
53780
53954
  let ey = cy;
53781
53955
  let steps = 0;
53782
- const maxSteps = Math.ceil(SHORT_HOP_MAX / STEP) + 1;
53956
+ const maxSteps = Math.ceil(maxLen / STEP) + 1;
53783
53957
  while (steps < maxSteps && fillAt(ex, ey) === fill2) {
53784
53958
  ex += dx * STEP;
53785
53959
  ey += dy * STEP;
@@ -53787,8 +53961,8 @@ function layoutMap(resolvedIn, data, size, opts) {
53787
53961
  }
53788
53962
  if (fillAt(ex, ey) === fill2) continue;
53789
53963
  const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
53790
- const ccx = ex + dx * (HOP_GAP + halfAlong);
53791
- const ccy = ey + dy * (HOP_GAP + halfAlong);
53964
+ const ccx = ex + dx * (gap + halfAlong);
53965
+ const ccy = ey + dy * (gap + halfAlong);
53792
53966
  const rect = {
53793
53967
  x: ccx - chipW / 2,
53794
53968
  y: ccy - chipH / 2,
@@ -53813,7 +53987,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53813
53987
  const innerX = ccx - dx * (chipW / 2);
53814
53988
  const innerY = ccy - dy * (chipH / 2);
53815
53989
  const len = Math.hypot(innerX - cx, innerY - cy);
53816
- if (len > SHORT_HOP_MAX) continue;
53990
+ if (len > maxLen) continue;
53817
53991
  if (placedLeaders.some(
53818
53992
  (l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
53819
53993
  ))
@@ -53837,6 +54011,78 @@ function layoutMap(resolvedIn, data, size, opts) {
53837
54011
  const valStr = regionValueStr(r.value);
53838
54012
  const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
53839
54013
  const fitsPois = (rect) => !poiObstacles.some((o) => rectsOverlap(rect, o));
54014
+ const isSubject = regionById.has(r.id);
54015
+ const extremesOnFill = (ax, ay, text) => {
54016
+ const halfW = measureLegendText(text, FONT2) / 2;
54017
+ return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
54018
+ };
54019
+ if (isSubject && valStr === void 0 && r.label !== void 0) {
54020
+ const name = candidates[0];
54021
+ let best = -1;
54022
+ for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
54023
+ if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
54024
+ continue;
54025
+ const rect = regionLabelRect(c[0], c[1], name, void 0, f);
54026
+ if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
54027
+ const halfW = measureLegendText(name, f) / 2;
54028
+ if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
54029
+ continue;
54030
+ best = f;
54031
+ break;
54032
+ }
54033
+ if (best >= SUBJECT_MIN_PROMINENCE) {
54034
+ const rect = regionLabelRect(c[0], c[1], name, void 0, best);
54035
+ placedRegionRects.push(rect);
54036
+ pushRegionLabel(
54037
+ c[0],
54038
+ c[1],
54039
+ name,
54040
+ r.fill,
54041
+ r.lineNumber,
54042
+ void 0,
54043
+ best
54044
+ );
54045
+ labeledRegionIds.add(r.id);
54046
+ regionLabelGuards.push({ label: labels[labels.length - 1], rect });
54047
+ } else {
54048
+ const hop = tryShortHopCallout(
54049
+ c[0],
54050
+ c[1],
54051
+ r.fill,
54052
+ r.label,
54053
+ void 0,
54054
+ SUBJECT_LEADER_FONT,
54055
+ HOP_GAP + SUBJECT_LEADER_FONT,
54056
+ SHORT_HOP_MAX * 2.2
54057
+ );
54058
+ if (hop) {
54059
+ placedRegionRects.push(hop.rect);
54060
+ placedLeaders.push(hop.leader);
54061
+ const dark = mix(r.fill, palette.text, 60);
54062
+ labels.push({
54063
+ x: hop.x,
54064
+ y: hop.y,
54065
+ text: r.label,
54066
+ anchor: "middle",
54067
+ color: palette.text,
54068
+ halo: false,
54069
+ haloColor: palette.bg,
54070
+ fontSize: SUBJECT_LEADER_FONT,
54071
+ leader: {
54072
+ x1: hop.leader[0],
54073
+ y1: hop.leader[1],
54074
+ x2: hop.leader[2],
54075
+ y2: hop.leader[3]
54076
+ },
54077
+ leaderColor: dark,
54078
+ calloutDot: { x: c[0], y: c[1], color: dark },
54079
+ lineNumber: r.lineNumber
54080
+ });
54081
+ labeledRegionIds.add(r.id);
54082
+ }
54083
+ }
54084
+ continue;
54085
+ }
53840
54086
  const seekAnchors = [
53841
54087
  { x: c[0], y: c[1], guard: false }
53842
54088
  ];
@@ -53859,13 +54105,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53859
54105
  const nameRect = regionLabelRect(a.x, a.y, t);
53860
54106
  if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
53861
54107
  const stackRect = regionLabelRect(a.x, a.y, t, valStr);
53862
- if (fitsRegions(stackRect) && fitsPois(nameRect)) {
54108
+ if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
53863
54109
  chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
53864
54110
  break;
53865
54111
  }
53866
54112
  }
53867
54113
  if (labelW(t) <= boxW && labelH <= boxH) {
53868
- if (fitsRegions(nameRect) && fitsPois(nameRect)) {
54114
+ if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
53869
54115
  chosen = { text: t, ax: a.x, ay: a.y };
53870
54116
  break;
53871
54117
  }
@@ -53873,7 +54119,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53873
54119
  }
53874
54120
  if (chosen) break;
53875
54121
  }
53876
- if (chosen === void 0 && valStr && r.label !== void 0) {
54122
+ if (chosen === void 0 && r.label !== void 0 && isSubject) {
53877
54123
  const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
53878
54124
  if (hop) {
53879
54125
  placedRegionRects.push(hop.rect);
@@ -53885,9 +54131,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53885
54131
  text: r.label,
53886
54132
  anchor: "middle",
53887
54133
  color: palette.text,
53888
- halo: true,
54134
+ halo: false,
53889
54135
  haloColor: palette.bg,
53890
- valueLine: valStr,
54136
+ ...valStr !== void 0 && { valueLine: valStr },
53891
54137
  leader: {
53892
54138
  x1: hop.leader[0],
53893
54139
  y1: hop.leader[1],
@@ -53898,20 +54144,28 @@ function layoutMap(resolvedIn, data, size, opts) {
53898
54144
  calloutDot: { x: c[0], y: c[1], color: dark },
53899
54145
  lineNumber: r.lineNumber
53900
54146
  });
54147
+ labeledRegionIds.add(r.id);
53901
54148
  }
53902
54149
  continue;
53903
54150
  }
53904
54151
  if (chosen === void 0) continue;
53905
54152
  const isOrient = r.value === void 0 && r.layer === "base";
53906
54153
  let font = FONT2;
53907
- let fade = 0;
53908
54154
  if (isOrient) {
53909
54155
  const growT = sizeT(boxW, boxH);
53910
54156
  const desiredFont = Math.round(
53911
54157
  FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
53912
54158
  );
53913
54159
  const hasVal = chosen.valueLine !== void 0;
53914
- for (let f = desiredFont; f > FONT2; f--) {
54160
+ const extremesClean = (f) => {
54161
+ const halfW = measureLegendText(chosen.text, f) / 2;
54162
+ return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
54163
+ const fAt = fillAt(sx, chosen.ay);
54164
+ return fAt === r.fill || isEmptyFill(fAt);
54165
+ });
54166
+ };
54167
+ let fit = -1;
54168
+ for (let f = desiredFont; f >= FONT2; f--) {
53915
54169
  if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
53916
54170
  continue;
53917
54171
  const gRect = regionLabelRect(
@@ -53928,14 +54182,18 @@ function layoutMap(resolvedIn, data, size, opts) {
53928
54182
  void 0,
53929
54183
  f
53930
54184
  );
53931
- if (!fitsRegions(gRect) || !fitsPois(gName)) continue;
53932
- const halfW = measureLegendText(chosen.text, f) / 2;
53933
- if (fillAt(chosen.ax - halfW, chosen.ay) !== r.fill || fillAt(chosen.ax + halfW, chosen.ay) !== r.fill)
54185
+ if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
53934
54186
  continue;
53935
- font = f;
54187
+ if (!extremesClean(f)) continue;
54188
+ fit = f;
53936
54189
  break;
53937
54190
  }
53938
- fade = font > FONT2 ? Math.round(growT * REGION_FADE_ORIENT) : 0;
54191
+ if (fit < 0) {
54192
+ if (!frameContainers.has(r.id)) continue;
54193
+ font = FONT2;
54194
+ } else {
54195
+ font = fit;
54196
+ }
53939
54197
  }
53940
54198
  const rRect = regionLabelRect(
53941
54199
  chosen.ax,
@@ -53953,8 +54211,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53953
54211
  r.lineNumber,
53954
54212
  chosen.valueLine,
53955
54213
  font,
53956
- fade
54214
+ isOrient ? backdropLandTone : void 0
53957
54215
  );
54216
+ labeledRegionIds.add(r.id);
53958
54217
  regionLabelGuards.push({
53959
54218
  label: labels[labels.length - 1],
53960
54219
  rect: rRect
@@ -53972,6 +54231,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53972
54231
  seed.lineNumber,
53973
54232
  valStr
53974
54233
  );
54234
+ labeledRegionIds.add(seed.iso);
53975
54235
  regionLabelGuards.push({
53976
54236
  label: labels[labels.length - 1],
53977
54237
  rect: regionLabelRect(seed.x, seed.y, text, valStr)
@@ -54403,13 +54663,30 @@ function layoutMap(resolvedIn, data, size, opts) {
54403
54663
  const nx = -dy / len;
54404
54664
  const ny = dx / len;
54405
54665
  const perp = labelH + 2;
54666
+ const pm = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
54667
+ lg.d
54668
+ );
54669
+ const pointAt = (t) => {
54670
+ if (pm?.[5] !== void 0) {
54671
+ const u = 1 - t;
54672
+ return [
54673
+ u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
54674
+ u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
54675
+ ];
54676
+ }
54677
+ if (pm?.[3] !== void 0)
54678
+ return [
54679
+ +pm[1] + (+pm[3] - +pm[1]) * t,
54680
+ +pm[2] + (+pm[4] - +pm[2]) * t
54681
+ ];
54682
+ return [a.cx + dx * t, a.cy + dy * t];
54683
+ };
54406
54684
  const candidates = [];
54407
54685
  for (const off of [0, perp, -perp, 2 * perp, -2 * perp])
54408
- for (const t of T_LIST)
54409
- candidates.push([
54410
- a.cx + dx * t + nx * off,
54411
- a.cy + dy * t + ny * off - 4
54412
- ]);
54686
+ for (const t of T_LIST) {
54687
+ const [bx, by] = pointAt(t);
54688
+ candidates.push([bx + nx * off, by + ny * off - 4]);
54689
+ }
54413
54690
  const clean = ([cx2, cy2]) => {
54414
54691
  const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
54415
54692
  if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
@@ -54419,10 +54696,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54419
54696
  if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
54420
54697
  return true;
54421
54698
  };
54422
- const [cx, cy] = candidates.find(clean) ?? [
54423
- a.cx + dx * 0.5,
54424
- a.cy + dy * 0.5 - 4
54425
- ];
54699
+ const mid = pointAt(0.5);
54700
+ const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
54426
54701
  const style = labelOnFill(fillAt(cx, cy));
54427
54702
  legs[i] = {
54428
54703
  ...lg,
@@ -54450,7 +54725,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54450
54725
  const rawCountries = [];
54451
54726
  for (const f of worldLayer.values()) {
54452
54727
  const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
54453
- if (!iso || regionById.has(iso)) continue;
54728
+ if (!iso || regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54454
54729
  let hasReferencedSub = false;
54455
54730
  for (const k of regionById.keys())
54456
54731
  if (k.startsWith(iso + "-")) {
@@ -54524,7 +54799,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54524
54799
  );
54525
54800
  if (usLayer && framedStateContainers) {
54526
54801
  for (const [iso, f] of usLayer) {
54527
- if (regionById.has(iso)) continue;
54802
+ if (regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54528
54803
  const viewF = cullFeatureToView(f);
54529
54804
  if (!viewF) continue;
54530
54805
  const b = path.bounds(viewF);
@@ -54548,6 +54823,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54548
54823
  waterBodies: data.waterBodies,
54549
54824
  countries: countryCandidates,
54550
54825
  palette,
54826
+ // One muted, contrast-floored tone shared with the orientation backdrop
54827
+ // (region pass) so a context country (Canada) and an orientation region
54828
+ // (Minnesota) read identically — never below the WCAG-AA floor, no fade.
54829
+ countryTone: backdropLandTone,
54830
+ waterTone: backdropWaterTone,
54551
54831
  project,
54552
54832
  collides,
54553
54833
  contentPoints,
@@ -55102,19 +55382,60 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
55102
55382
  sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
55103
55383
  };
55104
55384
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
55105
- layout.legs.forEach((leg, i) => {
55106
- 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);
55385
+ const legEnd = (d) => {
55386
+ const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
55387
+ d
55388
+ );
55389
+ if (!m) return null;
55390
+ const x0 = +m[1];
55391
+ const y0 = +m[2];
55392
+ let ex;
55393
+ let ey;
55394
+ let fromX;
55395
+ let fromY;
55396
+ if (m[3] !== void 0) {
55397
+ ex = +m[3];
55398
+ ey = +m[4];
55399
+ fromX = x0;
55400
+ fromY = y0;
55401
+ } else {
55402
+ ex = +m[7];
55403
+ ey = +m[8];
55404
+ fromX = +m[5];
55405
+ fromY = +m[6];
55406
+ }
55407
+ const dx = ex - fromX;
55408
+ const dy = ey - fromY;
55409
+ const len = Math.hypot(dx, dy);
55410
+ if (!(len > 0)) return null;
55411
+ return { ex, ey, tx: dx / len, ty: dy / len };
55412
+ };
55413
+ layout.legs.forEach((leg) => {
55414
+ const end = leg.arrow ? legEnd(leg.d) : null;
55415
+ let drawD = leg.d;
55416
+ let arrowTri = null;
55417
+ if (end) {
55418
+ const aLen = arrowSize(leg.width);
55419
+ const aHalf = Math.max(aLen * 0.5, leg.width * 0.6 + 1);
55420
+ const bx = end.ex - end.tx * aLen;
55421
+ const by = end.ey - end.ty * aLen;
55422
+ const px = -end.ty;
55423
+ const py = end.tx;
55424
+ arrowTri = `M${bx + px * aHalf},${by + py * aHalf}L${end.ex},${end.ey}L${bx - px * aHalf},${by - py * aHalf}z`;
55425
+ drawD = leg.d.replace(
55426
+ /(-?[\d.]+),(-?[\d.]+)$/,
55427
+ `${bx.toFixed(2)},${by.toFixed(2)}`
55428
+ );
55429
+ }
55430
+ 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);
55107
55431
  if (leg.tags) {
55108
55432
  for (const [group, value] of Object.entries(leg.tags)) {
55109
55433
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
55110
55434
  }
55111
55435
  }
55112
55436
  wireSync(p, leg.lineNumber);
55113
- if (leg.arrow) {
55114
- const id = nid(`dgmo-map-arrow-${i}`);
55115
- const s = arrowSize(leg.width);
55116
- 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);
55117
- p.attr("marker-end", `url(#${id})`);
55437
+ if (arrowTri) {
55438
+ 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);
55118
55439
  }
55119
55440
  if (leg.label !== void 0 && leg.labelX !== void 0) {
55120
55441
  const lt = emitText(
@@ -58743,9 +59064,9 @@ var init_renderer20 = __esm({
58743
59064
  }
58744
59065
  });
58745
59066
 
58746
- // node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
59067
+ // ../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
58747
59068
  var require_lz_string = __commonJS({
58748
- "node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports2, module2) {
59069
+ "../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports2, module2) {
58749
59070
  "use strict";
58750
59071
  var LZString = (function() {
58751
59072
  var f = String.fromCharCode;