@diagrammo/dgmo 0.8.21 → 0.8.22

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 (93) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/cli.cjs +143 -93
  4. package/dist/editor.cjs +17 -3
  5. package/dist/editor.cjs.map +1 -1
  6. package/dist/editor.js +17 -3
  7. package/dist/editor.js.map +1 -1
  8. package/dist/highlight.cjs +12 -2
  9. package/dist/highlight.cjs.map +1 -1
  10. package/dist/highlight.js +12 -2
  11. package/dist/highlight.js.map +1 -1
  12. package/dist/index.cjs +19997 -14886
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +331 -8
  15. package/dist/index.d.ts +331 -8
  16. package/dist/index.js +19984 -14889
  17. package/dist/index.js.map +1 -1
  18. package/docs/guide/chart-sitemap.md +18 -1
  19. package/docs/guide/chart-tech-radar.md +219 -0
  20. package/docs/guide/registry.json +1 -0
  21. package/docs/language-reference.md +116 -6
  22. package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
  23. package/gallery/fixtures/c4-full.dgmo +2 -2
  24. package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
  25. package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
  26. package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
  27. package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
  28. package/gallery/fixtures/gantt-full.dgmo +2 -2
  29. package/gallery/fixtures/gantt.dgmo +2 -2
  30. package/gallery/fixtures/infra-full.dgmo +2 -2
  31. package/gallery/fixtures/infra.dgmo +1 -1
  32. package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
  33. package/gallery/fixtures/sequence-tags.dgmo +2 -2
  34. package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
  35. package/gallery/fixtures/tech-radar.dgmo +36 -0
  36. package/gallery/fixtures/timeline.dgmo +1 -1
  37. package/package.json +1 -1
  38. package/src/boxes-and-lines/layout.ts +309 -33
  39. package/src/boxes-and-lines/parser.ts +86 -10
  40. package/src/boxes-and-lines/renderer.ts +250 -91
  41. package/src/boxes-and-lines/types.ts +1 -1
  42. package/src/c4/layout.ts +8 -8
  43. package/src/c4/parser.ts +35 -2
  44. package/src/c4/renderer.ts +19 -3
  45. package/src/c4/types.ts +1 -0
  46. package/src/chart.ts +14 -7
  47. package/src/completion.ts +227 -0
  48. package/src/cycle/layout.ts +732 -0
  49. package/src/cycle/parser.ts +352 -0
  50. package/src/cycle/renderer.ts +539 -0
  51. package/src/cycle/types.ts +77 -0
  52. package/src/d3.ts +87 -8
  53. package/src/dgmo-router.ts +9 -0
  54. package/src/echarts.ts +7 -4
  55. package/src/editor/dgmo.grammar +5 -1
  56. package/src/editor/dgmo.grammar.js +1 -1
  57. package/src/editor/keywords.ts +14 -0
  58. package/src/gantt/parser.ts +2 -8
  59. package/src/graph/flowchart-parser.ts +15 -21
  60. package/src/graph/state-parser.ts +5 -10
  61. package/src/index.ts +50 -0
  62. package/src/infra/layout.ts +218 -74
  63. package/src/infra/parser.ts +30 -6
  64. package/src/infra/renderer.ts +14 -8
  65. package/src/infra/types.ts +10 -3
  66. package/src/journey-map/layout.ts +386 -0
  67. package/src/journey-map/parser.ts +540 -0
  68. package/src/journey-map/renderer.ts +1456 -0
  69. package/src/journey-map/types.ts +47 -0
  70. package/src/kanban/parser.ts +3 -10
  71. package/src/kanban/renderer.ts +31 -15
  72. package/src/mindmap/parser.ts +12 -18
  73. package/src/mindmap/renderer.ts +14 -13
  74. package/src/mindmap/text-wrap.ts +22 -12
  75. package/src/mindmap/types.ts +2 -2
  76. package/src/org/parser.ts +2 -6
  77. package/src/sequence/renderer.ts +144 -38
  78. package/src/sharing.ts +1 -0
  79. package/src/sitemap/layout.ts +21 -6
  80. package/src/sitemap/parser.ts +26 -17
  81. package/src/sitemap/renderer.ts +34 -0
  82. package/src/sitemap/types.ts +1 -0
  83. package/src/tech-radar/index.ts +14 -0
  84. package/src/tech-radar/interactive.ts +1058 -0
  85. package/src/tech-radar/layout.ts +190 -0
  86. package/src/tech-radar/parser.ts +385 -0
  87. package/src/tech-radar/renderer.ts +1159 -0
  88. package/src/tech-radar/shared.ts +187 -0
  89. package/src/tech-radar/types.ts +81 -0
  90. package/src/utils/description-helpers.ts +33 -0
  91. package/src/utils/legend-layout.ts +3 -1
  92. package/src/utils/parsing.ts +46 -7
  93. package/src/utils/tag-groups.ts +46 -60
@@ -0,0 +1,386 @@
1
+ import type { PaletteColors } from '../palettes';
2
+ import { mix } from '../palettes/color-utils';
3
+ import type {
4
+ ParsedJourneyMap,
5
+ JourneyMapPhase,
6
+ JourneyMapStep,
7
+ } from './types';
8
+
9
+ // ============================================================
10
+ // Layout Types
11
+ // ============================================================
12
+
13
+ export interface CurvePoint {
14
+ x: number;
15
+ y: number;
16
+ score: number;
17
+ emotionLabel?: string;
18
+ stepIndex: number;
19
+ }
20
+
21
+ export interface StepLayout {
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ step: JourneyMapStep;
27
+ color: string;
28
+ }
29
+
30
+ export interface PhaseLayout {
31
+ x: number;
32
+ y: number;
33
+ width: number;
34
+ height: number;
35
+ phase: JourneyMapPhase;
36
+ headerColor: string;
37
+ stepLayouts: StepLayout[];
38
+ }
39
+
40
+ export interface JourneyMapLayout {
41
+ phases: PhaseLayout[];
42
+ flatStepLayouts: StepLayout[];
43
+ curvePoints: CurvePoint[];
44
+ totalWidth: number;
45
+ totalHeight: number;
46
+ curveAreaTop: number;
47
+ curveAreaBottom: number;
48
+ cardAreaTop: number;
49
+ personaHeight: number;
50
+ titleHeight: number;
51
+ /** Whether any step has thought annotations */
52
+ hasThoughts: boolean;
53
+ }
54
+
55
+ // ============================================================
56
+ // Constants
57
+ // ============================================================
58
+
59
+ const PADDING = 24;
60
+ const TITLE_HEIGHT = 36;
61
+ const PERSONA_HEIGHT = 48;
62
+ const CURVE_AREA_HEIGHT = 260;
63
+ const CARD_GAP = 8;
64
+ const STEP_CARD_WIDTH = 190;
65
+ const CARD_HEADER_HEIGHT = 24;
66
+ const CARD_META_LINE_HEIGHT = 14;
67
+ const PHASE_HEADER_HEIGHT = 36;
68
+ const CARD_PADDING_X = 10;
69
+ const CARD_PADDING_Y = 6;
70
+ const ANNO_ICON_SIZE = 10;
71
+ const ANNO_ICON_GAP = 4;
72
+ export const TAG_STRIP_HEIGHT = 18;
73
+ const PHASE_GAP = 16;
74
+ const COLUMN_PADDING = 12;
75
+ const FACE_ICON_SIZE = 20;
76
+
77
+ // ============================================================
78
+ // Score-to-color
79
+ // ============================================================
80
+
81
+ export function scoreToColor(score: number, palette: PaletteColors): string {
82
+ // 5=green, 1=red — interpolate
83
+ const t = ((5 - score) / 4) * 100;
84
+ return mix(palette.colors.red, palette.colors.green, t);
85
+ }
86
+
87
+ // ============================================================
88
+ // Layout Engine
89
+ // ============================================================
90
+
91
+ export const COLLAPSED_PHASE_WIDTH = 60;
92
+
93
+ export function layoutJourneyMap(
94
+ parsed: ParsedJourneyMap,
95
+ palette: PaletteColors,
96
+ options?: {
97
+ exportDims?: { width: number; height: number };
98
+ collapsedPhases?: Set<string>;
99
+ }
100
+ ): JourneyMapLayout {
101
+ const hasTitle = !!parsed.title;
102
+ const hasPersona = !!parsed.persona;
103
+ const hasPhases = parsed.phases.length > 0;
104
+
105
+ const titleHeight = hasTitle ? TITLE_HEIGHT : 0;
106
+ const personaHeight = hasPersona ? PERSONA_HEIGHT : 0;
107
+
108
+ // Thought bubbles render as overlays on hover — no reserved vertical space
109
+ const allStepsForThoughts = hasPhases
110
+ ? parsed.phases.flatMap((p) => p.steps)
111
+ : parsed.steps;
112
+ const hasThoughts = allStepsForThoughts.some((s) =>
113
+ s.annotations.some((a) => a.type === 'thought')
114
+ );
115
+
116
+ const curveAreaTop = PADDING + titleHeight + personaHeight;
117
+ const curveAreaBottom = curveAreaTop + CURVE_AREA_HEIGHT;
118
+ const cardAreaTop = curveAreaBottom + PADDING;
119
+
120
+ const allSteps = hasPhases
121
+ ? parsed.phases.flatMap((p) => p.steps)
122
+ : parsed.steps;
123
+
124
+ // Compute step card heights based on content (matches kanban card sizing)
125
+ const annoIconIndent = ANNO_ICON_SIZE + ANNO_ICON_GAP;
126
+ const annoTextW = STEP_CARD_WIDTH - CARD_PADDING_X * 2 - annoIconIndent;
127
+ const descTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
128
+ const charWidth = 4.8; // average char width at FONT_SIZE_META (10px)
129
+
130
+ const titleTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
131
+ const titleCharWidth = 6.5; // average char width at FONT_SIZE_STEP (12px)
132
+ const TITLE_LINE_HEIGHT = 16;
133
+
134
+ const stepHeights = allSteps.map((step) => {
135
+ const titleLines = wrapLineCount(
136
+ step.title,
137
+ titleTextWidth,
138
+ titleCharWidth
139
+ );
140
+ let h = CARD_PADDING_Y + titleLines * TITLE_LINE_HEIGHT + CARD_PADDING_Y;
141
+ const cardAnnos = step.annotations;
142
+ let contentLines = 0;
143
+ // Description may wrap
144
+ if (step.description) {
145
+ contentLines += wrapLineCount(step.description, descTextWidth, charWidth);
146
+ }
147
+ // Annotations: all lines indented past icon
148
+ for (const anno of cardAnnos) {
149
+ contentLines += wrapLineCount(anno.text, annoTextW, charWidth);
150
+ }
151
+ if (contentLines > 0) {
152
+ h += contentLines * CARD_META_LINE_HEIGHT + 4; // 4px bottom padding
153
+ }
154
+ return h;
155
+ });
156
+
157
+ const minCardHeight = CARD_HEADER_HEIGHT + CARD_META_LINE_HEIGHT;
158
+ const maxCardHeight = Math.max(minCardHeight, ...stepHeights);
159
+
160
+ // Check if any step has tags — if so, reserve space for the tag strip above cards
161
+ const hasTags = allSteps.some((s) => Object.keys(s.tags).length > 0);
162
+ const tagStripOffset = hasTags ? TAG_STRIP_HEIGHT + 6 : 0;
163
+
164
+ // Layout phases or flat steps
165
+ const phaseLayouts: PhaseLayout[] = [];
166
+ const flatStepLayouts: StepLayout[] = [];
167
+ const curvePoints: CurvePoint[] = [];
168
+
169
+ let globalStepIndex = 0;
170
+
171
+ const collapsed = options?.collapsedPhases ?? new Set<string>();
172
+
173
+ if (hasPhases) {
174
+ let phaseX = PADDING;
175
+
176
+ for (const phase of parsed.phases) {
177
+ const isCollapsed = collapsed.has(phase.name);
178
+ const stepCount = Math.max(phase.steps.length, 1);
179
+ const phaseWidth = isCollapsed
180
+ ? STEP_CARD_WIDTH + COLUMN_PADDING * 2
181
+ : stepCount * STEP_CARD_WIDTH +
182
+ (stepCount - 1) * CARD_GAP +
183
+ COLUMN_PADDING * 2;
184
+
185
+ const stepLayouts: StepLayout[] = [];
186
+
187
+ if (!isCollapsed) {
188
+ let stepX = phaseX + COLUMN_PADDING;
189
+
190
+ for (let si = 0; si < phase.steps.length; si++) {
191
+ const step = phase.steps[si];
192
+ const color =
193
+ step.score !== undefined
194
+ ? scoreToColor(step.score, palette)
195
+ : palette.surface;
196
+
197
+ const sl: StepLayout = {
198
+ x: stepX,
199
+ y: cardAreaTop + PHASE_HEADER_HEIGHT + CARD_GAP + tagStripOffset,
200
+ width: STEP_CARD_WIDTH,
201
+ height: maxCardHeight,
202
+ step,
203
+ color,
204
+ };
205
+ stepLayouts.push(sl);
206
+
207
+ // Curve point
208
+ if (step.score !== undefined) {
209
+ const curveX = stepX + STEP_CARD_WIDTH / 2;
210
+ const curveY =
211
+ curveAreaBottom -
212
+ ((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
213
+ 10;
214
+ curvePoints.push({
215
+ x: curveX,
216
+ y: curveY,
217
+ score: step.score,
218
+ emotionLabel: step.emotionLabel,
219
+ stepIndex: globalStepIndex,
220
+ });
221
+ }
222
+
223
+ stepX += STEP_CARD_WIDTH + CARD_GAP;
224
+ globalStepIndex++;
225
+ }
226
+ } else {
227
+ // Collapsed: spread curve points across the compressed column width
228
+ const stepCount = phase.steps.length;
229
+ const padX = COLUMN_PADDING + FACE_ICON_SIZE;
230
+ const availW = phaseWidth - padX * 2;
231
+ for (let si = 0; si < stepCount; si++) {
232
+ const step = phase.steps[si];
233
+ if (step.score !== undefined) {
234
+ const curveX =
235
+ stepCount === 1
236
+ ? phaseX + phaseWidth / 2
237
+ : phaseX + padX + (si / (stepCount - 1)) * availW;
238
+ const curveY =
239
+ curveAreaBottom -
240
+ ((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
241
+ 10;
242
+ curvePoints.push({
243
+ x: curveX,
244
+ y: curveY,
245
+ score: step.score,
246
+ emotionLabel: step.emotionLabel,
247
+ stepIndex: globalStepIndex,
248
+ });
249
+ }
250
+ globalStepIndex++;
251
+ }
252
+ }
253
+
254
+ // Phase header color from average score
255
+ const scoredSteps = phase.steps.filter((s) => s.score !== undefined);
256
+ const avgScore =
257
+ scoredSteps.length > 0
258
+ ? scoredSteps.reduce((sum, s) => sum + s.score!, 0) /
259
+ scoredSteps.length
260
+ : 3;
261
+ const headerColor = mix(scoreToColor(avgScore, palette), palette.bg, 25);
262
+
263
+ const COLLAPSED_CARD_H = 26;
264
+ const COLLAPSED_GAP = 6;
265
+ const phaseHeight = isCollapsed
266
+ ? PHASE_HEADER_HEIGHT +
267
+ CARD_GAP +
268
+ phase.steps.length * (COLLAPSED_CARD_H + COLLAPSED_GAP) +
269
+ CARD_GAP
270
+ : PHASE_HEADER_HEIGHT +
271
+ CARD_GAP +
272
+ tagStripOffset +
273
+ maxCardHeight +
274
+ CARD_GAP;
275
+
276
+ phaseLayouts.push({
277
+ x: phaseX,
278
+ y: cardAreaTop,
279
+ width: phaseWidth,
280
+ height: phaseHeight,
281
+ phase,
282
+ headerColor,
283
+ stepLayouts,
284
+ });
285
+
286
+ phaseX += phaseWidth + PHASE_GAP;
287
+ }
288
+ } else {
289
+ // Flat mode
290
+ let stepX = PADDING;
291
+
292
+ for (let si = 0; si < parsed.steps.length; si++) {
293
+ const step = parsed.steps[si];
294
+ const color =
295
+ step.score !== undefined
296
+ ? scoreToColor(step.score, palette)
297
+ : palette.surface;
298
+
299
+ flatStepLayouts.push({
300
+ x: stepX,
301
+ y: cardAreaTop + CARD_GAP + tagStripOffset,
302
+ width: STEP_CARD_WIDTH,
303
+ height: maxCardHeight,
304
+ step,
305
+ color,
306
+ });
307
+
308
+ if (step.score !== undefined) {
309
+ const curveX = stepX + STEP_CARD_WIDTH / 2;
310
+ const curveY =
311
+ curveAreaBottom -
312
+ ((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 20) -
313
+ 10;
314
+ curvePoints.push({
315
+ x: curveX,
316
+ y: curveY,
317
+ score: step.score,
318
+ emotionLabel: step.emotionLabel,
319
+ stepIndex: si,
320
+ });
321
+ }
322
+
323
+ stepX += STEP_CARD_WIDTH + CARD_GAP;
324
+ globalStepIndex++;
325
+ }
326
+ }
327
+
328
+ // Compute total dimensions
329
+ const rightEdge = hasPhases
330
+ ? phaseLayouts.length > 0
331
+ ? phaseLayouts[phaseLayouts.length - 1].x +
332
+ phaseLayouts[phaseLayouts.length - 1].width +
333
+ PADDING
334
+ : PADDING * 2
335
+ : flatStepLayouts.length > 0
336
+ ? flatStepLayouts[flatStepLayouts.length - 1].x +
337
+ STEP_CARD_WIDTH +
338
+ PADDING
339
+ : PADDING * 2;
340
+
341
+ const bottomEdge = hasPhases
342
+ ? phaseLayouts.length > 0
343
+ ? phaseLayouts[0].y + phaseLayouts[0].height + PADDING + 40
344
+ : cardAreaTop + PADDING
345
+ : cardAreaTop + CARD_GAP + tagStripOffset + maxCardHeight + PADDING + 40;
346
+
347
+ // Add space for score legend at bottom
348
+ const totalWidth = Math.max(rightEdge, 400);
349
+ const totalHeight = bottomEdge;
350
+
351
+ return {
352
+ phases: phaseLayouts,
353
+ flatStepLayouts,
354
+ curvePoints,
355
+ totalWidth,
356
+ totalHeight,
357
+ curveAreaTop,
358
+ curveAreaBottom,
359
+ cardAreaTop,
360
+ personaHeight,
361
+ titleHeight,
362
+ hasThoughts,
363
+ };
364
+ }
365
+
366
+ /** Count how many visual lines a text string will occupy when wrapped. */
367
+ function wrapLineCount(
368
+ text: string,
369
+ maxWidth: number,
370
+ charWidth: number
371
+ ): number {
372
+ const maxChars = Math.max(1, Math.floor(maxWidth / charWidth));
373
+ const words = text.split(/\s+/);
374
+ let lines = 1;
375
+ let currentLen = 0;
376
+ for (const word of words) {
377
+ const needed = currentLen > 0 ? word.length + 1 : word.length;
378
+ if (currentLen + needed > maxChars && currentLen > 0) {
379
+ lines++;
380
+ currentLen = word.length;
381
+ } else {
382
+ currentLen += needed;
383
+ }
384
+ }
385
+ return lines;
386
+ }