@diagrammo/dgmo 0.31.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +4 -1
- package/.github/copilot-instructions.md +4 -1
- package/.windsurfrules +4 -1
- package/SKILL.md +4 -1
- package/dist/advanced.cjs +1297 -358
- package/dist/advanced.d.cts +117 -15
- package/dist/advanced.d.ts +117 -15
- package/dist/advanced.js +1291 -358
- package/dist/auto.cjs +1087 -316
- package/dist/auto.js +98 -98
- package/dist/auto.mjs +1087 -316
- package/dist/cli.cjs +140 -140
- package/dist/index.cjs +1090 -397
- package/dist/index.js +1090 -397
- package/docs/ai-integration.md +4 -1
- package/docs/language-reference.md +282 -27
- package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
- package/gallery/fixtures/c4-full.dgmo +4 -5
- package/gallery/fixtures/c4.dgmo +2 -3
- package/package.json +7 -1
- package/src/advanced.ts +7 -0
- package/src/boxes-and-lines/focus.ts +257 -0
- package/src/boxes-and-lines/layout-search.ts +131 -65
- package/src/boxes-and-lines/layout.ts +7 -1
- package/src/boxes-and-lines/parser.ts +19 -4
- package/src/boxes-and-lines/renderer.ts +54 -3
- package/src/c4/parser.ts +8 -7
- package/src/chart-type-registry.ts +129 -4
- package/src/chart-types.ts +4 -4
- package/src/chart.ts +18 -1
- package/src/colors.ts +225 -2
- package/src/cycle/parser.ts +2 -7
- package/src/d3.ts +67 -54
- package/src/diagnostics.ts +17 -0
- package/src/dimensions.ts +9 -13
- package/src/echarts.ts +42 -14
- package/src/er/parser.ts +6 -1
- package/src/gantt/parser.ts +44 -7
- package/src/graph/flowchart-parser.ts +77 -3
- package/src/graph/state-renderer.ts +2 -2
- package/src/infra/parser.ts +80 -0
- package/src/journey-map/parser.ts +8 -7
- package/src/kanban/parser.ts +8 -7
- package/src/map/context-labels.ts +134 -27
- package/src/map/geo.ts +10 -2
- package/src/map/layout.ts +259 -4
- package/src/map/parser.ts +2 -0
- package/src/map/renderer.ts +22 -11
- package/src/map/resolver.ts +68 -19
- package/src/mindmap/parser.ts +15 -7
- package/src/mindmap/renderer.ts +50 -12
- package/src/org/parser.ts +8 -7
- package/src/org/renderer.ts +22 -7
- package/src/palettes/color-utils.ts +12 -2
- package/src/palettes/index.ts +1 -0
- package/src/pert/renderer.ts +2 -2
- package/src/pyramid/parser.ts +2 -7
- package/src/quadrant/renderer.ts +2 -2
- package/src/raci/parser.ts +2 -7
- package/src/raci/renderer.ts +4 -4
- package/src/ring/parser.ts +2 -7
- package/src/sequence/parser.ts +18 -7
- package/src/sequence/renderer.ts +4 -4
- package/src/sitemap/parser.ts +8 -7
- package/src/sitemap/renderer.ts +2 -2
- package/src/tech-radar/parser.ts +2 -7
- package/src/timeline/renderer.ts +15 -5
- package/src/utils/parsing.ts +13 -1
- package/src/utils/scaling.ts +38 -81
- package/src/utils/tag-groups.ts +38 -0
- package/src/visualizations/parse.ts +6 -1
- package/src/wireframe/parser.ts +6 -1
package/src/map/resolver.ts
CHANGED
|
@@ -68,12 +68,44 @@ const POI_ZOOM_FLOOR_DEG = 7;
|
|
|
68
68
|
// single POI near the edge of a tall/wide country (e.g. Cartagena at the north
|
|
69
69
|
// tip of Colombia) would otherwise drag the frame to that country's far edge —
|
|
70
70
|
// all the way to the Amazon, ~15° below the southernmost dot. Clamp the container
|
|
71
|
-
// union so it reveals at most
|
|
72
|
-
// cluster on each side: northern Colombia stays for orientation,
|
|
73
|
-
// interior is cropped.
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
71
|
+
// union so it reveals at most `containerOvershoot(cluster)` degrees of container
|
|
72
|
+
// BEYOND the POI cluster on each side: northern Colombia stays for orientation,
|
|
73
|
+
// the empty interior is cropped.
|
|
74
|
+
//
|
|
75
|
+
// For NON-US clusters the overshoot SHRINKS as the cluster grows (tuned
|
|
76
|
+
// 2026-06-19). A tiny cluster has no context of its own, so it keeps the full MAX
|
|
77
|
+
// (8°) — enough to reveal its whole modest container (e.g. a small European
|
|
78
|
+
// country). A LARGE cluster already supplies its own context, so a fixed 8° just
|
|
79
|
+
// padded a huge container with empty land (a Ukraine/Russia strike map framed
|
|
80
|
+
// ~2.4× the cluster, a wide dead band above the dots). Linearly decaying the
|
|
81
|
+
// overshoot to MIN (3°) tightens big clusters (~1.7× cluster) without cropping
|
|
82
|
+
// small ones. The POI_ZOOM_FLOOR_DEG floor still guards the lower bound.
|
|
83
|
+
//
|
|
84
|
+
// US-ORIENTED maps are EXEMPT (keep the flat MAX): the national-vs-regional
|
|
85
|
+
// projection gate (US_NATIONAL_LON_SPAN, albers-usa vs regional Mercator) is
|
|
86
|
+
// calibrated against the 8°-overshoot frame span — a coast-to-Caribbean US cruise
|
|
87
|
+
// route clears the national threshold only because the west overshoot reaches ~8°
|
|
88
|
+
// past Denver. Shrinking it there would silently flip such maps off albers-usa.
|
|
89
|
+
// US containers (a state, CONUS) aren't the huge-empty-container problem anyway.
|
|
90
|
+
const CONTAINER_OVERSHOOT_MAX = 8;
|
|
91
|
+
const CONTAINER_OVERSHOOT_MIN = 3;
|
|
92
|
+
// Degrees of overshoot shed per degree of cluster span (larger span ⇒ less slack).
|
|
93
|
+
const CONTAINER_OVERSHOOT_DECAY = 0.3;
|
|
94
|
+
|
|
95
|
+
/** Per-cluster container overshoot (deg). US-oriented maps keep the flat MAX (the
|
|
96
|
+
* albers-usa national gate is calibrated to it); other maps get full MAX for a
|
|
97
|
+
* tight cluster, decaying to MIN for a large one. `span` = the cluster's larger
|
|
98
|
+
* lon/lat extent. */
|
|
99
|
+
function containerOvershoot(span: number, usOriented: boolean): number {
|
|
100
|
+
if (usOriented) return CONTAINER_OVERSHOOT_MAX;
|
|
101
|
+
return Math.max(
|
|
102
|
+
CONTAINER_OVERSHOOT_MIN,
|
|
103
|
+
Math.min(
|
|
104
|
+
CONTAINER_OVERSHOOT_MAX,
|
|
105
|
+
CONTAINER_OVERSHOOT_MAX - CONTAINER_OVERSHOOT_DECAY * span
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
77
109
|
// Above this longitudinal span a US POI-only extent is "national" — use the
|
|
78
110
|
// albers-usa composite (CONUS conic + AK/HI insets) instead of regional Mercator.
|
|
79
111
|
// CONUS spans ≈58° lon; 48° is "most of the country". Tunable.
|
|
@@ -907,7 +939,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
907
939
|
const containerUnion = unionExtent(containerBoxes, points);
|
|
908
940
|
if (containerUnion)
|
|
909
941
|
extent = pad(
|
|
910
|
-
clampContainerToCluster(containerUnion, points),
|
|
942
|
+
clampContainerToCluster(containerUnion, points, usOriented),
|
|
911
943
|
PAD_FRACTION
|
|
912
944
|
);
|
|
913
945
|
}
|
|
@@ -1079,25 +1111,42 @@ function mostCommonCountry(
|
|
|
1079
1111
|
/** Asymmetric container clamp (R-poi-region overshoot guard). Container framing
|
|
1080
1112
|
* reveals the region(s) holding the POIs, but one POI at the edge of a tall/wide
|
|
1081
1113
|
* country drags the frame to that country's far edge. Cap how far the frame
|
|
1082
|
-
* extends BEYOND the POI cluster on each side at CONTAINER_OVERSHOOT_DEG
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1114
|
+
* extends BEYOND the POI cluster on each side at CONTAINER_OVERSHOOT_DEG, while
|
|
1115
|
+
* letting a genuinely tighter container edge still bound the frame (so a small
|
|
1116
|
+
* country shows whole, but a cluster inside a giant one stays on the cluster).
|
|
1117
|
+
*
|
|
1118
|
+
* Each longitude side clamps independently. A container edge is a usable outer
|
|
1119
|
+
* bound only when it sits within the normal [-180, 180] range AND on the correct
|
|
1120
|
+
* side of the cluster; an antimeridian-crossing container (Russia, Fiji, NZ, the
|
|
1121
|
+
* US via the Aleutians) reports a degenerate east (> 180, or numerically < its
|
|
1122
|
+
* own west), so that side falls back to cluster ± overshoot instead of skipping
|
|
1123
|
+
* the clamp entirely (which previously blew a western-Russia cluster out to a
|
|
1124
|
+
* world frame). Latitude never wraps, so it always clamps. Assumes the POI
|
|
1125
|
+
* cluster itself does not straddle the seam — true for any regional cluster. */
|
|
1087
1126
|
function clampContainerToCluster(
|
|
1088
1127
|
container: GeoExtent,
|
|
1089
|
-
points: Array<[number, number]
|
|
1128
|
+
points: Array<[number, number]>,
|
|
1129
|
+
usOriented: boolean
|
|
1090
1130
|
): GeoExtent {
|
|
1091
1131
|
const poi = unionExtent([], points);
|
|
1092
1132
|
if (!poi) return container;
|
|
1093
1133
|
let [[west, south], [east, north]] = container;
|
|
1094
1134
|
const [[pWest, pSouth], [pEast, pNorth]] = poi;
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1135
|
+
// Overshoot shrinks with cluster size for non-US maps (see containerOvershoot):
|
|
1136
|
+
// a big cluster already orients itself, so it gets less surrounding slack than a
|
|
1137
|
+
// tiny one. US-oriented maps keep the flat MAX (the albers-usa gate needs it).
|
|
1138
|
+
const over = containerOvershoot(
|
|
1139
|
+
Math.max(pEast - pWest, pNorth - pSouth),
|
|
1140
|
+
usOriented
|
|
1141
|
+
);
|
|
1142
|
+
south = Math.max(south, pSouth - over);
|
|
1143
|
+
north = Math.min(north, pNorth + over);
|
|
1144
|
+
const wOver = pWest - over;
|
|
1145
|
+
const eOver = pEast + over;
|
|
1146
|
+
// West edge usable iff in range and not east of the cluster's west.
|
|
1147
|
+
west = west >= -180 && west <= pWest ? Math.max(west, wOver) : wOver;
|
|
1148
|
+
// East edge usable iff in range and not west of the cluster's east.
|
|
1149
|
+
east = east <= 180 && east >= pEast ? Math.min(east, eOver) : eOver;
|
|
1101
1150
|
return [
|
|
1102
1151
|
[west, south],
|
|
1103
1152
|
[east, north],
|
package/src/mindmap/parser.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
descriptionBareRemovedMessage,
|
|
4
4
|
formatDgmoError,
|
|
5
5
|
makeDgmoError,
|
|
6
|
+
makeFail,
|
|
6
7
|
METADATA_DIAGNOSTIC_CODES,
|
|
7
8
|
pipeOperatorRemovedMessage,
|
|
8
9
|
suggest,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
validateTagGroupNames,
|
|
17
18
|
stripDefaultModifier,
|
|
18
19
|
finalizeAutoTagColors,
|
|
20
|
+
cascadeTagMetadata,
|
|
19
21
|
AUTO_TAG_COLOR_SENTINEL,
|
|
20
22
|
} from '../utils/tag-groups';
|
|
21
23
|
import {
|
|
@@ -60,12 +62,7 @@ export function parseMindmap(
|
|
|
60
62
|
error: null,
|
|
61
63
|
};
|
|
62
64
|
|
|
63
|
-
const fail = (
|
|
64
|
-
const diag = makeDgmoError(line, message);
|
|
65
|
-
result.diagnostics.push(diag);
|
|
66
|
-
result.error = formatDgmoError(diag);
|
|
67
|
-
return result;
|
|
68
|
-
};
|
|
65
|
+
const fail = makeFail(result);
|
|
69
66
|
|
|
70
67
|
const pushError = (line: number, message: string): void => {
|
|
71
68
|
const diag = makeDgmoError(line, message);
|
|
@@ -210,7 +207,12 @@ export function parseMindmap(
|
|
|
210
207
|
const indent = measureIndent(line);
|
|
211
208
|
if (indent > 0) {
|
|
212
209
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
213
|
-
const { label, color } = extractColor(
|
|
210
|
+
const { label, color } = extractColor(
|
|
211
|
+
cleanEntry,
|
|
212
|
+
palette,
|
|
213
|
+
result.diagnostics,
|
|
214
|
+
lineNumber
|
|
215
|
+
);
|
|
214
216
|
// Bare value (no explicit color) → keep it; finalized below.
|
|
215
217
|
if (isDefault) {
|
|
216
218
|
currentTagGroup.defaultValue = label;
|
|
@@ -307,6 +309,12 @@ export function parseMindmap(
|
|
|
307
309
|
collectAll(result.roots);
|
|
308
310
|
validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
|
|
309
311
|
validateTagGroupNames(result.tagGroups, pushWarning);
|
|
312
|
+
|
|
313
|
+
// Cascade explicit tag values down the tree so sub-nodes inherit a tagged
|
|
314
|
+
// ancestor's value (overridable per-node). Runs after validation (so we
|
|
315
|
+
// don't double-warn on inherited values) and before the layout's
|
|
316
|
+
// global-default injection (so an inherited value wins over the default).
|
|
317
|
+
cascadeTagMetadata(result.roots, result.tagGroups);
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
// Check for empty mindmap
|
package/src/mindmap/renderer.ts
CHANGED
|
@@ -149,9 +149,16 @@ export function renderMindmap(
|
|
|
149
149
|
const availHeight =
|
|
150
150
|
containerHeight - DIAGRAM_PADDING * 2 - legendReserve - titleReserve;
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
// Fit to BOTH axes so a tall tree shrinks to fit a short canvas instead of
|
|
153
|
+
// overflowing vertically (export sizes its own canvas, so it stays identity).
|
|
154
|
+
let ctx = isExport
|
|
153
155
|
? ScaleContext.identity()
|
|
154
|
-
: ScaleContext.
|
|
156
|
+
: ScaleContext.fromBox(
|
|
157
|
+
availWidth,
|
|
158
|
+
layout.width,
|
|
159
|
+
availHeight,
|
|
160
|
+
layout.height
|
|
161
|
+
);
|
|
155
162
|
|
|
156
163
|
let renderLayout = layout;
|
|
157
164
|
if (ctx.factor < 1) {
|
|
@@ -161,25 +168,56 @@ export function renderMindmap(
|
|
|
161
168
|
hiddenCounts.set(n.id, n.hiddenCount);
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
const relayout = (c: ScaleContext): MindmapLayoutResult =>
|
|
172
|
+
layoutMindmap(parsed, palette, {
|
|
173
|
+
interactive: !isExport,
|
|
174
|
+
...(hiddenCounts.size > 0 && { hiddenCounts }),
|
|
175
|
+
activeTagGroup: activeTagGroup ?? null,
|
|
176
|
+
...(hideDescriptions !== undefined && { hideDescriptions }),
|
|
177
|
+
ctx: c,
|
|
178
|
+
});
|
|
179
|
+
renderLayout = relayout(ctx);
|
|
180
|
+
// Scaling is non-linear, so one pass can still overflow. Re-measure the
|
|
181
|
+
// laid-out result and tighten until it fits both axes or hits the floor.
|
|
182
|
+
for (let i = 0; i < 3 && !ctx.isBelowFloor; i++) {
|
|
183
|
+
const refit = Math.min(
|
|
184
|
+
availWidth / renderLayout.width,
|
|
185
|
+
availHeight / renderLayout.height
|
|
186
|
+
);
|
|
187
|
+
if (refit >= 0.999) break; // already fits
|
|
188
|
+
ctx = ScaleContext.fromFactor(ctx.factor * refit);
|
|
189
|
+
renderLayout = relayout(ctx);
|
|
190
|
+
}
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
|
|
193
|
+
// Re-layout keeps text readable but is floor-limited, so a dense tree can
|
|
194
|
+
// still exceed the canvas. Apply a final uniform scale as a hard guarantee
|
|
195
|
+
// that the diagram always fits within the canvas (no overflow), regardless
|
|
196
|
+
// of how small the canvas is. Export sizes its own canvas, so this is a no-op
|
|
197
|
+
// there (fitScale === 1).
|
|
198
|
+
const fitScale = isExport
|
|
199
|
+
? 1
|
|
200
|
+
: Math.min(
|
|
201
|
+
1,
|
|
202
|
+
renderLayout.width > 0 ? availWidth / renderLayout.width : 1,
|
|
203
|
+
renderLayout.height > 0 ? availHeight / renderLayout.height : 1
|
|
204
|
+
);
|
|
205
|
+
const scaledWidth = renderLayout.width * fitScale;
|
|
206
|
+
const scaledHeight = renderLayout.height * fitScale;
|
|
207
|
+
|
|
208
|
+
const offsetX = Math.max(0, (availWidth - scaledWidth) / 2);
|
|
174
209
|
const offsetY =
|
|
175
210
|
DIAGRAM_PADDING +
|
|
176
211
|
legendReserve +
|
|
177
212
|
titleReserve +
|
|
178
|
-
Math.max(0, (availHeight -
|
|
213
|
+
Math.max(0, (availHeight - scaledHeight) / 2);
|
|
179
214
|
|
|
180
215
|
const mainG = svg
|
|
181
216
|
.append('g')
|
|
182
|
-
.attr(
|
|
217
|
+
.attr(
|
|
218
|
+
'transform',
|
|
219
|
+
`translate(${offsetX}, ${offsetY})${fitScale < 1 ? ` scale(${fitScale})` : ''}`
|
|
220
|
+
);
|
|
183
221
|
|
|
184
222
|
if (ctx.isBelowFloor) {
|
|
185
223
|
svg.attr('width', '100%');
|
package/src/org/parser.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { DgmoError } from '../diagnostics';
|
|
|
3
3
|
import {
|
|
4
4
|
formatDgmoError,
|
|
5
5
|
makeDgmoError,
|
|
6
|
+
makeFail,
|
|
6
7
|
METADATA_DIAGNOSTIC_CODES,
|
|
7
8
|
pipeOperatorRemovedMessage,
|
|
8
9
|
suggest,
|
|
@@ -107,12 +108,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
107
108
|
error: null,
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
const fail = (
|
|
111
|
-
const diag = makeDgmoError(line, message);
|
|
112
|
-
result.diagnostics.push(diag);
|
|
113
|
-
result.error = formatDgmoError(diag);
|
|
114
|
-
return result;
|
|
115
|
-
};
|
|
111
|
+
const fail = makeFail(result);
|
|
116
112
|
|
|
117
113
|
/** Push a recoverable error and continue parsing. */
|
|
118
114
|
const pushError = (line: number, message: string): void => {
|
|
@@ -261,7 +257,12 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
261
257
|
const indent = measureIndent(line);
|
|
262
258
|
if (indent > 0) {
|
|
263
259
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
264
|
-
const { label, color } = extractColor(
|
|
260
|
+
const { label, color } = extractColor(
|
|
261
|
+
cleanEntry,
|
|
262
|
+
palette,
|
|
263
|
+
result.diagnostics,
|
|
264
|
+
lineNumber
|
|
265
|
+
);
|
|
265
266
|
// Bare value (no explicit color) → keep it; the post-parse
|
|
266
267
|
// finalize pass assigns a deterministic palette color.
|
|
267
268
|
if (isDefault) {
|
package/src/org/renderer.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
} from '../utils/export-container';
|
|
11
11
|
import { ScaleContext } from '../utils/scaling';
|
|
12
12
|
import type { PaletteColors } from '../palettes';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
contrastText,
|
|
15
|
+
mix,
|
|
16
|
+
shapeFill,
|
|
17
|
+
themeBaseBg,
|
|
18
|
+
} from '../palettes/color-utils';
|
|
14
19
|
import { resolveTagColor } from '../utils/tag-groups';
|
|
15
20
|
import type { ParsedOrg } from './parser';
|
|
16
21
|
import type { OrgLayoutResult } from './layout';
|
|
@@ -93,7 +98,7 @@ function containerFill(
|
|
|
93
98
|
nodeColor?: string
|
|
94
99
|
): string {
|
|
95
100
|
if (nodeColor) {
|
|
96
|
-
return mix(nodeColor,
|
|
101
|
+
return mix(nodeColor, themeBaseBg(palette, isDark), 10);
|
|
97
102
|
}
|
|
98
103
|
return mix(palette.surface, palette.bg, 40);
|
|
99
104
|
}
|
|
@@ -397,6 +402,13 @@ export function renderOrg(
|
|
|
397
402
|
.attr('height', iconSize + 6)
|
|
398
403
|
.attr('fill', 'transparent');
|
|
399
404
|
|
|
405
|
+
// Use the container's contrast text color so it stays legible on darker
|
|
406
|
+
// fills, mirroring the node icon.
|
|
407
|
+
const iconColor = contrastText(
|
|
408
|
+
fill,
|
|
409
|
+
palette.textOnFillLight,
|
|
410
|
+
palette.textOnFillDark
|
|
411
|
+
);
|
|
400
412
|
const cx = iconSize / 2;
|
|
401
413
|
const cy = iconSize / 2;
|
|
402
414
|
focusG
|
|
@@ -405,14 +417,14 @@ export function renderOrg(
|
|
|
405
417
|
.attr('cy', cy)
|
|
406
418
|
.attr('r', iconSize / 2 - 1)
|
|
407
419
|
.attr('fill', 'none')
|
|
408
|
-
.attr('stroke',
|
|
420
|
+
.attr('stroke', iconColor)
|
|
409
421
|
.attr('stroke-width', 1.5);
|
|
410
422
|
focusG
|
|
411
423
|
.append('circle')
|
|
412
424
|
.attr('cx', cx)
|
|
413
425
|
.attr('cy', cy)
|
|
414
426
|
.attr('r', 2)
|
|
415
|
-
.attr('fill',
|
|
427
|
+
.attr('fill', iconColor);
|
|
416
428
|
}
|
|
417
429
|
}
|
|
418
430
|
|
|
@@ -558,7 +570,10 @@ export function renderOrg(
|
|
|
558
570
|
.attr('height', iconSize + 6)
|
|
559
571
|
.attr('fill', 'transparent');
|
|
560
572
|
|
|
561
|
-
// Scope/target icon: outer circle + inner dot
|
|
573
|
+
// Scope/target icon: outer circle + inner dot. Use the card's contrast
|
|
574
|
+
// text color so it stays legible on solid-fill dark cards, not just the
|
|
575
|
+
// light default surface.
|
|
576
|
+
const iconColor = labelColor;
|
|
562
577
|
const cx = iconSize / 2;
|
|
563
578
|
const cy = iconSize / 2;
|
|
564
579
|
focusG
|
|
@@ -567,14 +582,14 @@ export function renderOrg(
|
|
|
567
582
|
.attr('cy', cy)
|
|
568
583
|
.attr('r', iconSize / 2 - 1)
|
|
569
584
|
.attr('fill', 'none')
|
|
570
|
-
.attr('stroke',
|
|
585
|
+
.attr('stroke', iconColor)
|
|
571
586
|
.attr('stroke-width', 1.5);
|
|
572
587
|
focusG
|
|
573
588
|
.append('circle')
|
|
574
589
|
.attr('cx', cx)
|
|
575
590
|
.attr('cy', cy)
|
|
576
591
|
.attr('r', 2)
|
|
577
|
-
.attr('fill',
|
|
592
|
+
.attr('fill', iconColor);
|
|
578
593
|
}
|
|
579
594
|
}
|
|
580
595
|
|
|
@@ -305,6 +305,16 @@ export function contrastText(
|
|
|
305
305
|
* `opts.solid` (per `option solid-fill`): bypass the 25% tint and return
|
|
306
306
|
* the raw intent. Opt-in only; default behavior unchanged.
|
|
307
307
|
*/
|
|
308
|
+
/**
|
|
309
|
+
* The theme-aware base background a diagram's tinted shapes blend toward:
|
|
310
|
+
* `surface` in dark, page `bg` in light. Concentrates the
|
|
311
|
+
* `isDark ? palette.surface : palette.bg` pick repeated across ~20 renderers
|
|
312
|
+
* (Story 111.3).
|
|
313
|
+
*/
|
|
314
|
+
export function themeBaseBg(palette: PaletteColors, isDark: boolean): string {
|
|
315
|
+
return isDark ? palette.surface : palette.bg;
|
|
316
|
+
}
|
|
317
|
+
|
|
308
318
|
export function shapeFill(
|
|
309
319
|
palette: PaletteColors,
|
|
310
320
|
intent: string,
|
|
@@ -312,7 +322,7 @@ export function shapeFill(
|
|
|
312
322
|
opts?: { solid?: boolean }
|
|
313
323
|
): string {
|
|
314
324
|
if (opts?.solid) return intent;
|
|
315
|
-
return mix(intent,
|
|
325
|
+
return mix(intent, themeBaseBg(palette, isDark), 25);
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
// ============================================================
|
|
@@ -408,7 +418,7 @@ export function politicalTints(
|
|
|
408
418
|
isDark: boolean
|
|
409
419
|
): string[] {
|
|
410
420
|
if (count <= 0) return [];
|
|
411
|
-
const base =
|
|
421
|
+
const base = themeBaseBg(palette, isDark);
|
|
412
422
|
const c = palette.colors;
|
|
413
423
|
// Land-first: greens/earth tones lead; water-like blue & cyan trail.
|
|
414
424
|
const swatches = [
|
package/src/palettes/index.ts
CHANGED
package/src/pert/renderer.ts
CHANGED
|
@@ -33,7 +33,7 @@ import * as d3Selection from 'd3-selection';
|
|
|
33
33
|
import * as d3Shape from 'd3-shape';
|
|
34
34
|
import { FONT_FAMILY } from '../fonts';
|
|
35
35
|
import type { PaletteColors } from '../palettes';
|
|
36
|
-
import { contrastText, mix, shapeFill } from '../palettes/color-utils';
|
|
36
|
+
import { contrastText, mix, shapeFill, themeBaseBg } from '../palettes/color-utils';
|
|
37
37
|
import { ScaleContext } from '../utils/scaling';
|
|
38
38
|
import {
|
|
39
39
|
measureText,
|
|
@@ -108,7 +108,7 @@ function analysisBlockChrome(
|
|
|
108
108
|
palette: PaletteColors,
|
|
109
109
|
isDark: boolean
|
|
110
110
|
): { fill: string; stroke: string } {
|
|
111
|
-
const surfaceBg =
|
|
111
|
+
const surfaceBg = themeBaseBg(palette, isDark);
|
|
112
112
|
return {
|
|
113
113
|
fill: mix(palette.surface, palette.bg, 40),
|
|
114
114
|
stroke: mix(palette.textMuted, surfaceBg, 35),
|
package/src/pyramid/parser.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
bareDescriptionRemovedMessage,
|
|
7
|
-
formatDgmoError,
|
|
8
7
|
makeDgmoError,
|
|
8
|
+
makeFail,
|
|
9
9
|
METADATA_DIAGNOSTIC_CODES,
|
|
10
10
|
pipeOperatorRemovedMessage,
|
|
11
11
|
} from '../diagnostics';
|
|
@@ -61,12 +61,7 @@ export function parsePyramid(content: string): ParsedPyramid {
|
|
|
61
61
|
let headerParsed = false;
|
|
62
62
|
let currentLayer: Writable<PyramidLayer> | null = null;
|
|
63
63
|
|
|
64
|
-
const fail = (
|
|
65
|
-
const diag = makeDgmoError(line, message);
|
|
66
|
-
result.diagnostics.push(diag);
|
|
67
|
-
result.error = formatDgmoError(diag);
|
|
68
|
-
return result;
|
|
69
|
-
};
|
|
64
|
+
const fail = makeFail(result);
|
|
70
65
|
|
|
71
66
|
const warn = (
|
|
72
67
|
line: number,
|
package/src/quadrant/renderer.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { ScaleContext } from '../utils/scaling';
|
|
|
11
11
|
import { initD3Chart, renderChartTitle } from '../utils/d3-helpers';
|
|
12
12
|
import type { ParsedQuadrant, QuadrantLabel } from '../visualizations/types';
|
|
13
13
|
import type { PaletteColors } from '../palettes';
|
|
14
|
-
import { mix } from '../palettes/color-utils';
|
|
14
|
+
import { mix, themeBaseBg } from '../palettes/color-utils';
|
|
15
15
|
|
|
16
16
|
// Quadrant Chart Renderer
|
|
17
17
|
// ============================================================
|
|
@@ -99,7 +99,7 @@ export function renderQuadrant(
|
|
|
99
99
|
.append('g')
|
|
100
100
|
.attr('transform', `translate(${margin.left}, ${margin.top})`);
|
|
101
101
|
|
|
102
|
-
const bg =
|
|
102
|
+
const bg = themeBaseBg(palette, isDark);
|
|
103
103
|
|
|
104
104
|
// Full palette color for a quadrant (used for border and label tinting)
|
|
105
105
|
const getQuadrantColor = (
|
package/src/raci/parser.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// See `docs/dgmo-language-spec.md` § "RACI Matrix".
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
-
formatDgmoError,
|
|
23
22
|
makeDgmoError,
|
|
23
|
+
makeFail,
|
|
24
24
|
METADATA_DIAGNOSTIC_CODES,
|
|
25
25
|
pipeOperatorRemovedMessage,
|
|
26
26
|
suggest,
|
|
@@ -181,12 +181,7 @@ export function parseRaci(
|
|
|
181
181
|
error: null,
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
-
const fail = (
|
|
185
|
-
const diag = makeDgmoError(line, message);
|
|
186
|
-
result.diagnostics.push(diag);
|
|
187
|
-
result.error = formatDgmoError(diag);
|
|
188
|
-
return result;
|
|
189
|
-
};
|
|
184
|
+
const fail = makeFail(result);
|
|
190
185
|
|
|
191
186
|
const warn = (line: number, message: string, code?: string): void => {
|
|
192
187
|
result.diagnostics.push(makeDgmoError(line, message, 'warning', code));
|
package/src/raci/renderer.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
TITLE_FONT_WEIGHT,
|
|
31
31
|
TITLE_Y,
|
|
32
32
|
} from '../utils/title-constants';
|
|
33
|
-
import { contrastText, mix } from '../palettes/color-utils';
|
|
33
|
+
import { contrastText, mix, themeBaseBg } from '../palettes/color-utils';
|
|
34
34
|
import type { PaletteColors } from '../palettes';
|
|
35
35
|
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
36
36
|
import type {
|
|
@@ -394,7 +394,7 @@ export function renderRaci(
|
|
|
394
394
|
if (tasksAll.length === 0 && parsed.phases.length === 0) return;
|
|
395
395
|
|
|
396
396
|
const solid = parsed.options['solid-fill'] === 'on';
|
|
397
|
-
const surfaceBg =
|
|
397
|
+
const surfaceBg = themeBaseBg(palette, isDark);
|
|
398
398
|
|
|
399
399
|
// --- ScaleContext: differential scaling ---
|
|
400
400
|
const roleCount = Math.max(1, parsed.roles.length);
|
|
@@ -679,10 +679,10 @@ export function renderRaci(
|
|
|
679
679
|
// each column has a subtle visual identity instead of every column
|
|
680
680
|
// reading as the same neutral gray.
|
|
681
681
|
const roleColor = parsed.roleColors[i] ?? autoAccent(i, palette);
|
|
682
|
-
const bodyFill = mix(roleColor,
|
|
682
|
+
const bodyFill = mix(roleColor, themeBaseBg(palette, isDark), 16);
|
|
683
683
|
const headerFill = mix(
|
|
684
684
|
roleColor,
|
|
685
|
-
|
|
685
|
+
themeBaseBg(palette, isDark),
|
|
686
686
|
30
|
|
687
687
|
);
|
|
688
688
|
const colG = columnsG
|
package/src/ring/parser.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
bareDescriptionRemovedMessage,
|
|
7
|
-
formatDgmoError,
|
|
8
7
|
makeDgmoError,
|
|
8
|
+
makeFail,
|
|
9
9
|
METADATA_DIAGNOSTIC_CODES,
|
|
10
10
|
pipeOperatorRemovedMessage,
|
|
11
11
|
suggest,
|
|
@@ -48,12 +48,7 @@ export function parseRing(content: string): ParsedRing {
|
|
|
48
48
|
let headerParsed = false;
|
|
49
49
|
let currentLayer: Writable<RingLayer> | null = null;
|
|
50
50
|
|
|
51
|
-
const fail = (
|
|
52
|
-
const diag = makeDgmoError(line, message);
|
|
53
|
-
result.diagnostics.push(diag);
|
|
54
|
-
result.error = formatDgmoError(diag);
|
|
55
|
-
return result;
|
|
56
|
-
};
|
|
51
|
+
const fail = makeFail(result);
|
|
57
52
|
|
|
58
53
|
const warn = (
|
|
59
54
|
line: number,
|
package/src/sequence/parser.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
akaRemovedMessage,
|
|
11
11
|
formatDgmoError,
|
|
12
12
|
makeDgmoError,
|
|
13
|
+
makeFail,
|
|
13
14
|
METADATA_DIAGNOSTIC_CODES,
|
|
14
15
|
NAME_DIAGNOSTIC_CODES,
|
|
15
16
|
nameMergedMessage,
|
|
@@ -528,12 +529,7 @@ export function parseSequenceDgmo(
|
|
|
528
529
|
return nameAliasMap.get(trimmed) ?? trimmed;
|
|
529
530
|
};
|
|
530
531
|
|
|
531
|
-
const fail = (
|
|
532
|
-
const diag = makeDgmoError(line, message);
|
|
533
|
-
result.diagnostics.push(diag);
|
|
534
|
-
result.error = formatDgmoError(diag);
|
|
535
|
-
return result;
|
|
536
|
-
};
|
|
532
|
+
const fail = makeFail(result);
|
|
537
533
|
|
|
538
534
|
/** Push a recoverable error and continue parsing. */
|
|
539
535
|
const pushError = (line: number, message: string): void => {
|
|
@@ -554,6 +550,13 @@ export function parseSequenceDgmo(
|
|
|
554
550
|
const lines = content.split('\n');
|
|
555
551
|
let hasExplicitChart = false;
|
|
556
552
|
let contentStarted = false;
|
|
553
|
+
// Whether the message body has begun (first message, section, block, or note).
|
|
554
|
+
// Unlike `contentStarted` — which any declaration trips to close the
|
|
555
|
+
// options/tag-group "headers first" window — `bodyStarted` stays false through
|
|
556
|
+
// the entire declaration preamble so bare and typed participant declarations
|
|
557
|
+
// can be freely interleaved. It gates bare-name declarations: once real body
|
|
558
|
+
// content appears, a bare word is treated as a stray line, not a participant.
|
|
559
|
+
let bodyStarted = false;
|
|
557
560
|
let firstLineIndex = -1; // line index of the `sequence [Title]` first line (to skip in main loop)
|
|
558
561
|
|
|
559
562
|
// Handle first non-empty, non-comment line for `sequence Title` syntax
|
|
@@ -997,6 +1000,7 @@ export function parseSequenceDgmo(
|
|
|
997
1000
|
);
|
|
998
1001
|
}
|
|
999
1002
|
contentStarted = true;
|
|
1003
|
+
bodyStarted = true;
|
|
1000
1004
|
const section: SequenceSection = {
|
|
1001
1005
|
kind: 'section',
|
|
1002
1006
|
// Capture group 1 guaranteed present after successful match.
|
|
@@ -1301,7 +1305,7 @@ export function parseSequenceDgmo(
|
|
|
1301
1305
|
if (
|
|
1302
1306
|
/^\S+$/.test(bareCore) &&
|
|
1303
1307
|
!ARROW_PATTERN.test(bareCore) &&
|
|
1304
|
-
(inGroup || !
|
|
1308
|
+
(inGroup || !bodyStarted || bareMeta)
|
|
1305
1309
|
) {
|
|
1306
1310
|
contentStarted = true;
|
|
1307
1311
|
const id = bareCore;
|
|
@@ -1369,6 +1373,7 @@ export function parseSequenceDgmo(
|
|
|
1369
1373
|
}
|
|
1370
1374
|
if (labeledArrow) {
|
|
1371
1375
|
contentStarted = true;
|
|
1376
|
+
bodyStarted = true;
|
|
1372
1377
|
const { from, to, label: rawLabel, async: isAsync } = labeledArrow;
|
|
1373
1378
|
const fromKey = addParticipant(resolveAlias(from), lineNumber);
|
|
1374
1379
|
const toKey = addParticipant(resolveAlias(to), lineNumber);
|
|
@@ -1445,6 +1450,7 @@ export function parseSequenceDgmo(
|
|
|
1445
1450
|
const bareCall = bareCallSync || bareCallAsync;
|
|
1446
1451
|
if (bareCall) {
|
|
1447
1452
|
contentStarted = true;
|
|
1453
|
+
bodyStarted = true;
|
|
1448
1454
|
// Capture groups 1 and 2 guaranteed present after successful match.
|
|
1449
1455
|
const from = bareCall[1]!;
|
|
1450
1456
|
const to = bareCall[2]!;
|
|
@@ -1470,6 +1476,7 @@ export function parseSequenceDgmo(
|
|
|
1470
1476
|
const ifMatch = trimmed.match(/^if\s+(.+)$/i);
|
|
1471
1477
|
if (ifMatch) {
|
|
1472
1478
|
contentStarted = true;
|
|
1479
|
+
bodyStarted = true;
|
|
1473
1480
|
const block: Writable<SequenceBlock> = {
|
|
1474
1481
|
kind: 'block',
|
|
1475
1482
|
type: 'if',
|
|
@@ -1488,6 +1495,7 @@ export function parseSequenceDgmo(
|
|
|
1488
1495
|
const loopMatch = trimmed.match(/^loop\s+(.+)$/i);
|
|
1489
1496
|
if (loopMatch) {
|
|
1490
1497
|
contentStarted = true;
|
|
1498
|
+
bodyStarted = true;
|
|
1491
1499
|
const block: Writable<SequenceBlock> = {
|
|
1492
1500
|
kind: 'block',
|
|
1493
1501
|
type: 'loop',
|
|
@@ -1506,6 +1514,7 @@ export function parseSequenceDgmo(
|
|
|
1506
1514
|
const parallelMatch = trimmed.match(/^parallel(?:\s+(.+))?$/i);
|
|
1507
1515
|
if (parallelMatch) {
|
|
1508
1516
|
contentStarted = true;
|
|
1517
|
+
bodyStarted = true;
|
|
1509
1518
|
const block: Writable<SequenceBlock> = {
|
|
1510
1519
|
kind: 'block',
|
|
1511
1520
|
type: 'parallel',
|
|
@@ -1629,6 +1638,7 @@ export function parseSequenceDgmo(
|
|
|
1629
1638
|
lineNumber,
|
|
1630
1639
|
endLineNumber: lineNumber,
|
|
1631
1640
|
};
|
|
1641
|
+
bodyStarted = true;
|
|
1632
1642
|
currentContainer().push(note);
|
|
1633
1643
|
continue;
|
|
1634
1644
|
}
|
|
@@ -1654,6 +1664,7 @@ export function parseSequenceDgmo(
|
|
|
1654
1664
|
lineNumber,
|
|
1655
1665
|
endLineNumber: i + 1, // i has advanced past the body lines (1-based)
|
|
1656
1666
|
};
|
|
1667
|
+
bodyStarted = true;
|
|
1657
1668
|
currentContainer().push(note);
|
|
1658
1669
|
continue;
|
|
1659
1670
|
}
|