@diagrammo/dgmo 0.8.20 → 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 (110) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/cli.cjs +142 -90
  4. package/dist/editor.cjs +30 -4
  5. package/dist/editor.cjs.map +1 -1
  6. package/dist/editor.js +30 -4
  7. package/dist/editor.js.map +1 -1
  8. package/dist/highlight.cjs +25 -3
  9. package/dist/highlight.cjs.map +1 -1
  10. package/dist/highlight.js +25 -3
  11. package/dist/highlight.js.map +1 -1
  12. package/dist/index.cjs +21201 -12886
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +646 -89
  15. package/dist/index.d.ts +646 -89
  16. package/dist/index.js +21178 -12889
  17. package/dist/index.js.map +1 -1
  18. package/docs/guide/chart-mindmap.md +198 -0
  19. package/docs/guide/chart-sequence.md +23 -1
  20. package/docs/guide/chart-sitemap.md +18 -1
  21. package/docs/guide/chart-tech-radar.md +219 -0
  22. package/docs/guide/chart-wireframe.md +100 -0
  23. package/docs/guide/index.md +8 -0
  24. package/docs/guide/registry.json +1 -0
  25. package/docs/language-reference.md +249 -4
  26. package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
  27. package/gallery/fixtures/c4-full.dgmo +2 -2
  28. package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
  29. package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
  30. package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
  31. package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
  32. package/gallery/fixtures/gantt-full.dgmo +2 -2
  33. package/gallery/fixtures/gantt.dgmo +2 -2
  34. package/gallery/fixtures/infra-full.dgmo +2 -2
  35. package/gallery/fixtures/infra.dgmo +1 -1
  36. package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
  37. package/gallery/fixtures/sequence-tags.dgmo +2 -2
  38. package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
  39. package/gallery/fixtures/tech-radar.dgmo +36 -0
  40. package/gallery/fixtures/timeline.dgmo +1 -1
  41. package/package.json +1 -1
  42. package/src/boxes-and-lines/collapse.ts +21 -3
  43. package/src/boxes-and-lines/layout.ts +360 -42
  44. package/src/boxes-and-lines/parser.ts +94 -11
  45. package/src/boxes-and-lines/renderer.ts +371 -114
  46. package/src/boxes-and-lines/types.ts +2 -1
  47. package/src/c4/layout.ts +8 -8
  48. package/src/c4/parser.ts +35 -2
  49. package/src/c4/renderer.ts +19 -3
  50. package/src/c4/types.ts +1 -0
  51. package/src/chart.ts +14 -7
  52. package/src/completion.ts +253 -0
  53. package/src/cycle/layout.ts +732 -0
  54. package/src/cycle/parser.ts +352 -0
  55. package/src/cycle/renderer.ts +539 -0
  56. package/src/cycle/types.ts +77 -0
  57. package/src/d3.ts +240 -40
  58. package/src/dgmo-router.ts +15 -0
  59. package/src/echarts.ts +7 -4
  60. package/src/editor/dgmo.grammar +5 -1
  61. package/src/editor/dgmo.grammar.js +1 -1
  62. package/src/editor/keywords.ts +26 -0
  63. package/src/gantt/parser.ts +2 -8
  64. package/src/graph/flowchart-parser.ts +15 -21
  65. package/src/graph/layout.ts +73 -9
  66. package/src/graph/state-collapse.ts +78 -0
  67. package/src/graph/state-parser.ts +5 -10
  68. package/src/graph/state-renderer.ts +139 -34
  69. package/src/index.ts +78 -0
  70. package/src/infra/layout.ts +218 -74
  71. package/src/infra/parser.ts +30 -6
  72. package/src/infra/renderer.ts +14 -8
  73. package/src/infra/types.ts +10 -3
  74. package/src/journey-map/layout.ts +386 -0
  75. package/src/journey-map/parser.ts +540 -0
  76. package/src/journey-map/renderer.ts +1456 -0
  77. package/src/journey-map/types.ts +47 -0
  78. package/src/kanban/parser.ts +3 -10
  79. package/src/kanban/renderer.ts +325 -63
  80. package/src/mindmap/collapse.ts +88 -0
  81. package/src/mindmap/layout.ts +605 -0
  82. package/src/mindmap/parser.ts +373 -0
  83. package/src/mindmap/renderer.ts +544 -0
  84. package/src/mindmap/text-wrap.ts +217 -0
  85. package/src/mindmap/types.ts +55 -0
  86. package/src/org/parser.ts +2 -6
  87. package/src/render.ts +18 -21
  88. package/src/sequence/renderer.ts +273 -56
  89. package/src/sharing.ts +3 -0
  90. package/src/sitemap/layout.ts +56 -18
  91. package/src/sitemap/parser.ts +26 -17
  92. package/src/sitemap/renderer.ts +34 -0
  93. package/src/sitemap/types.ts +1 -0
  94. package/src/tech-radar/index.ts +14 -0
  95. package/src/tech-radar/interactive.ts +1058 -0
  96. package/src/tech-radar/layout.ts +190 -0
  97. package/src/tech-radar/parser.ts +385 -0
  98. package/src/tech-radar/renderer.ts +1159 -0
  99. package/src/tech-radar/shared.ts +187 -0
  100. package/src/tech-radar/types.ts +81 -0
  101. package/src/utils/description-helpers.ts +33 -0
  102. package/src/utils/export-container.ts +3 -2
  103. package/src/utils/legend-d3.ts +1 -0
  104. package/src/utils/legend-layout.ts +5 -3
  105. package/src/utils/parsing.ts +48 -7
  106. package/src/utils/tag-groups.ts +46 -60
  107. package/src/wireframe/layout.ts +460 -0
  108. package/src/wireframe/parser.ts +956 -0
  109. package/src/wireframe/renderer.ts +1293 -0
  110. package/src/wireframe/types.ts +110 -0
@@ -0,0 +1,460 @@
1
+ // ============================================================
2
+ // Wireframe Diagram Layout Engine (Document Flow)
3
+ // ============================================================
4
+
5
+ import type {
6
+ WireframeElement,
7
+ ParsedWireframe,
8
+ WireframeFormFactor,
9
+ } from './types';
10
+
11
+ // ============================================================
12
+ // Constants
13
+ // ============================================================
14
+
15
+ const DESKTOP_WIDTH = 1200;
16
+ const MOBILE_WIDTH = 375;
17
+
18
+ const CHAR_WIDTH = 7.5;
19
+ const LABEL_PADDING = 16;
20
+
21
+ /** Element type-specific heights */
22
+ const ELEMENT_HEIGHTS: Record<string, number> = {
23
+ textInput: 36,
24
+ button: 40,
25
+ dropdown: 36,
26
+ checkbox: 24,
27
+ radio: 24,
28
+ heading: 48,
29
+ subheading: 36,
30
+ divider: 24,
31
+ text: 22,
32
+ listItem: 24,
33
+ image: 120,
34
+ progress: 28,
35
+ chart: 160,
36
+ alert: 40,
37
+ nav: 44,
38
+ tabs: 44,
39
+ };
40
+
41
+ /** Spacing after specific element types */
42
+ const SPACING_AFTER: Record<string, number> = {
43
+ heading: 16,
44
+ subheading: 12,
45
+ divider: 12,
46
+ textInput: 8,
47
+ dropdown: 8,
48
+ checkbox: 8,
49
+ radio: 8,
50
+ button: 8,
51
+ text: 12,
52
+ listItem: 4,
53
+ group: 16,
54
+ image: 12,
55
+ progress: 12,
56
+ chart: 16,
57
+ alert: 12,
58
+ nav: 12,
59
+ tabs: 12,
60
+ table: 16,
61
+ };
62
+
63
+ const GROUP_PADDING_TOP_WITH_LABEL = 32; // label + top padding
64
+ const GROUP_PADDING_TOP_NO_LABEL = 10; // just content padding
65
+ const GROUP_PADDING_BOTTOM = 12;
66
+
67
+ /** Resolved at layout time based on showGroupLabels option */
68
+ let GROUP_PADDING_TOP = GROUP_PADDING_TOP_WITH_LABEL;
69
+ const GROUP_PADDING_X = 12;
70
+ const FRAME_PADDING = 20;
71
+ const TITLE_HEIGHT = 40;
72
+
73
+ /** Smart sizing: recognized region name prefixes → width fraction */
74
+ const REGION_SIZES: Record<string, number> = {
75
+ sidebar: 0.25,
76
+ side: 0.25,
77
+ left: 0.25,
78
+ right: 0.25,
79
+ main: 0, // fill remaining
80
+ content: 0, // fill remaining
81
+ center: 0, // fill remaining
82
+ };
83
+
84
+ /** Full-width regions (thin strips) */
85
+ const FULL_WIDTH_REGIONS = new Set(['header', 'top', 'footer', 'bottom']);
86
+ const HEADER_REGION_HEIGHT = 60;
87
+ const FOOTER_REGION_HEIGHT = 40;
88
+
89
+ // ============================================================
90
+ // Layout Types
91
+ // ============================================================
92
+
93
+ export interface WireframeLayoutNode {
94
+ id: string;
95
+ x: number;
96
+ y: number;
97
+ width: number;
98
+ height: number;
99
+ element: WireframeElement;
100
+ children: WireframeLayoutNode[];
101
+ /** For label-field pairs: the x offset where fields align */
102
+ fieldAlignX?: number;
103
+ }
104
+
105
+ export interface WireframeLayout {
106
+ width: number;
107
+ height: number;
108
+ titleHeight: number;
109
+ nodes: WireframeLayoutNode[];
110
+ modalNodes: WireframeLayoutNode[];
111
+ }
112
+
113
+ // ============================================================
114
+ // Layout Engine
115
+ // ============================================================
116
+
117
+ export function layoutWireframe(
118
+ parsed: ParsedWireframe,
119
+ _options?: Record<string, string>,
120
+ overrideWidth?: number,
121
+ showGroupLabels = true
122
+ ): WireframeLayout {
123
+ GROUP_PADDING_TOP = showGroupLabels
124
+ ? GROUP_PADDING_TOP_WITH_LABEL
125
+ : GROUP_PADDING_TOP_NO_LABEL;
126
+ const defaultWidth =
127
+ parsed.formFactor === 'mobile' ? MOBILE_WIDTH : DESKTOP_WIDTH;
128
+ const frameWidth = overrideWidth ?? defaultWidth;
129
+ const titleHeight = parsed.title ? TITLE_HEIGHT : 0;
130
+
131
+ const contentWidth = frameWidth - FRAME_PADDING * 2;
132
+
133
+ // Layout top-level roots
134
+ const nodes = layoutTopLevel(parsed.roots, contentWidth, parsed.formFactor);
135
+
136
+ // Calculate total height from positioned nodes
137
+ let maxY = 0;
138
+ for (const n of nodes) {
139
+ maxY = Math.max(maxY, n.y + n.height);
140
+ }
141
+
142
+ // Layout modals below main content
143
+ const modalNodes: WireframeLayoutNode[] = [];
144
+ let modalY = maxY + 24;
145
+ for (const modal of parsed.modals) {
146
+ const modalWidth = Math.min(contentWidth * 0.7, 600);
147
+ const modalX = (contentWidth - modalWidth) / 2;
148
+ const node = layoutElement(modal, modalX, modalY, modalWidth);
149
+ modalNodes.push(node);
150
+ modalY += node.height + 24;
151
+ }
152
+
153
+ const totalHeight =
154
+ (modalNodes.length > 0
155
+ ? modalNodes[modalNodes.length - 1].y +
156
+ modalNodes[modalNodes.length - 1].height
157
+ : maxY) +
158
+ FRAME_PADDING * 2 +
159
+ titleHeight;
160
+
161
+ return {
162
+ width: frameWidth,
163
+ height: totalHeight,
164
+ titleHeight,
165
+ nodes,
166
+ modalNodes,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Layout top-level roots: separate into full-width regions (header/footer)
172
+ * and horizontal siblings, then stack vertically.
173
+ */
174
+ function layoutTopLevel(
175
+ roots: WireframeElement[],
176
+ contentWidth: number,
177
+ formFactor: WireframeFormFactor
178
+ ): WireframeLayoutNode[] {
179
+ const result: WireframeLayoutNode[] = [];
180
+ let y = 0;
181
+
182
+ // Classify roots into rows
183
+ const rows: WireframeElement[][] = [];
184
+ let currentRow: WireframeElement[] = [];
185
+
186
+ for (const root of roots) {
187
+ const regionName = root.label.toLowerCase().split(/\s+/)[0];
188
+ if (FULL_WIDTH_REGIONS.has(regionName) || formFactor === 'mobile') {
189
+ // Full-width: flush current row first, then add as its own row
190
+ if (currentRow.length > 0) {
191
+ rows.push(currentRow);
192
+ currentRow = [];
193
+ }
194
+ rows.push([root]);
195
+ } else {
196
+ currentRow.push(root);
197
+ }
198
+ }
199
+ if (currentRow.length > 0) rows.push(currentRow);
200
+
201
+ // Layout each row
202
+ for (const row of rows) {
203
+ if (row.length === 1 && formFactor !== 'mobile') {
204
+ const el = row[0];
205
+ const regionName = el.label.toLowerCase().split(/\s+/)[0];
206
+
207
+ if (FULL_WIDTH_REGIONS.has(regionName)) {
208
+ // Full-width strip
209
+ const node = layoutElement(el, 0, y, contentWidth);
210
+ // Enforce min height for header/footer
211
+ if (regionName === 'header' || regionName === 'top') {
212
+ node.height = Math.max(node.height, HEADER_REGION_HEIGHT);
213
+ } else {
214
+ node.height = Math.max(node.height, FOOTER_REGION_HEIGHT);
215
+ }
216
+ result.push(node);
217
+ y += node.height + 16;
218
+ continue;
219
+ }
220
+ }
221
+
222
+ if (formFactor === 'mobile' || row.length === 1) {
223
+ // Stack vertically
224
+ for (const el of row) {
225
+ const node = layoutElement(el, 0, y, contentWidth);
226
+ result.push(node);
227
+ y += node.height + 16;
228
+ }
229
+ } else {
230
+ // Horizontal siblings — smart sizing
231
+ const allocated = allocateHorizontalWidths(row, contentWidth);
232
+ let x = 0;
233
+ let maxHeight = 0;
234
+ const rowNodes: WireframeLayoutNode[] = [];
235
+
236
+ for (let i = 0; i < row.length; i++) {
237
+ const w = allocated[i];
238
+ const node = layoutElement(row[i], x, y, w);
239
+ rowNodes.push(node);
240
+ maxHeight = Math.max(maxHeight, node.height);
241
+ x += w + 12; // gap between horizontal siblings
242
+ }
243
+
244
+ // Equalize heights (tallest wins, F4)
245
+ for (const n of rowNodes) {
246
+ n.height = maxHeight;
247
+ }
248
+
249
+ result.push(...rowNodes);
250
+ y += maxHeight + 16;
251
+ }
252
+ }
253
+
254
+ return result;
255
+ }
256
+
257
+ /**
258
+ * Allocate horizontal widths for side-by-side regions using smart sizing.
259
+ */
260
+ function allocateHorizontalWidths(
261
+ elements: WireframeElement[],
262
+ totalWidth: number
263
+ ): number[] {
264
+ const gap = 12;
265
+ const totalGaps = (elements.length - 1) * gap;
266
+ const available = totalWidth - totalGaps;
267
+
268
+ const widths: number[] = new Array(elements.length).fill(0);
269
+ let allocated = 0;
270
+ let fillCount = 0;
271
+
272
+ for (let i = 0; i < elements.length; i++) {
273
+ const name = elements[i].label.toLowerCase().split(/\s+/)[0];
274
+ const fraction = REGION_SIZES[name];
275
+
276
+ if (fraction !== undefined && fraction > 0) {
277
+ widths[i] = available * fraction;
278
+ allocated += widths[i];
279
+ } else if (fraction === 0) {
280
+ // Fill remaining
281
+ fillCount++;
282
+ } else {
283
+ // Unknown — will be equal-split
284
+ fillCount++;
285
+ }
286
+ }
287
+
288
+ // Distribute remaining to fill/unknown regions
289
+ if (fillCount > 0) {
290
+ const remaining = Math.max(0, available - allocated);
291
+ const each = remaining / fillCount;
292
+ for (let i = 0; i < widths.length; i++) {
293
+ if (widths[i] === 0) widths[i] = Math.max(40, each);
294
+ }
295
+ }
296
+
297
+ // Clamp all widths to minimum
298
+ for (let i = 0; i < widths.length; i++) {
299
+ widths[i] = Math.max(40, widths[i]);
300
+ }
301
+
302
+ return widths;
303
+ }
304
+
305
+ /**
306
+ * Layout a single element and its children recursively.
307
+ */
308
+ function layoutElement(
309
+ el: WireframeElement,
310
+ x: number,
311
+ y: number,
312
+ width: number
313
+ ): WireframeLayoutNode {
314
+ const node: WireframeLayoutNode = {
315
+ id: el.id,
316
+ x,
317
+ y,
318
+ width,
319
+ height: 0,
320
+ element: el,
321
+ children: [],
322
+ };
323
+
324
+ // Nav and tabs render children internally as a horizontal bar — treat as leaf for layout
325
+ const selfRendered = el.type === 'nav' || el.type === 'tabs';
326
+
327
+ if (!el.isContainer || el.children.length === 0 || selfRendered) {
328
+ // Leaf element — use type-specific height
329
+ node.height = getElementHeight(el);
330
+ return node;
331
+ }
332
+
333
+ // Container — layout children
334
+ const isInlineRow =
335
+ el.metadata._inlineRow === 'true' || el.metadata._labelField === 'true';
336
+ const padTop = isInlineRow ? 0 : GROUP_PADDING_TOP;
337
+ const padBottom = isInlineRow ? 0 : GROUP_PADDING_BOTTOM;
338
+ const padX = isInlineRow ? 0 : GROUP_PADDING_X;
339
+ const innerWidth = width - padX * 2;
340
+ const innerX = padX;
341
+
342
+ if (el.orientation === 'horizontal') {
343
+ // Horizontal layout: children in a row
344
+ const childWidths = allocateEqualWidths(el.children, innerWidth);
345
+ let cx = innerX;
346
+ let maxChildHeight = 0;
347
+
348
+ for (let i = 0; i < el.children.length; i++) {
349
+ const child = el.children[i];
350
+ const cw = childWidths[i];
351
+ const childNode = layoutElement(child, cx, padTop, cw);
352
+ node.children.push(childNode);
353
+ maxChildHeight = Math.max(maxChildHeight, childNode.height);
354
+ cx += cw + 8;
355
+ }
356
+
357
+ // Equalize child heights
358
+ for (const cn of node.children) {
359
+ cn.height = maxChildHeight;
360
+ }
361
+
362
+ node.height = padTop + maxChildHeight + padBottom;
363
+ } else {
364
+ // Vertical layout: stack children
365
+ let cy = padTop;
366
+
367
+ // Check for label-field auto-alignment (ADR-4)
368
+ const fieldAlignX = computeFieldAlignX(el.children);
369
+ if (fieldAlignX > 0) {
370
+ node.fieldAlignX = fieldAlignX;
371
+ }
372
+
373
+ for (const child of el.children) {
374
+ const childNode = layoutElement(child, innerX, cy, innerWidth);
375
+ node.children.push(childNode);
376
+ cy += childNode.height + getSpacingAfter(child);
377
+ }
378
+
379
+ node.height = cy + padBottom;
380
+ }
381
+
382
+ // Collapsed state: only show header
383
+ if (el.states.includes('collapsed')) {
384
+ node.height = GROUP_PADDING_TOP;
385
+ node.children = [];
386
+ }
387
+
388
+ return node;
389
+ }
390
+
391
+ function allocateEqualWidths(
392
+ children: WireframeElement[],
393
+ totalWidth: number
394
+ ): number[] {
395
+ const gap = 8;
396
+ const totalGaps = (children.length - 1) * gap;
397
+ const each = Math.max(40, (totalWidth - totalGaps) / children.length);
398
+ return children.map(() => each);
399
+ }
400
+
401
+ function getElementHeight(el: WireframeElement): number {
402
+ if (el.type === 'heading') {
403
+ return el.headingLevel === 2
404
+ ? (ELEMENT_HEIGHTS.subheading ?? 36)
405
+ : (ELEMENT_HEIGHTS.heading ?? 48);
406
+ }
407
+
408
+ if (el.type === 'textInput' && el.fieldVariant === 'textarea') {
409
+ return 80;
410
+ }
411
+
412
+ if (el.type === 'table') {
413
+ const headerH = 32;
414
+ const rowH = 28;
415
+ const rows = el.tableData?.length || el.tableRows || 0;
416
+ return headerH + rows * rowH + 8;
417
+ }
418
+
419
+ // Image hints affect height
420
+ if (el.type === 'image') {
421
+ if (el.imageHint === 'round') return 80;
422
+ if (el.imageHint === 'wide') return 80;
423
+ return ELEMENT_HEIGHTS.image ?? 120;
424
+ }
425
+
426
+ // Label-field wrapper
427
+ if (el.metadata._labelField === 'true') {
428
+ return 36; // input height
429
+ }
430
+
431
+ return ELEMENT_HEIGHTS[el.type] ?? 24;
432
+ }
433
+
434
+ function getSpacingAfter(el: WireframeElement): number {
435
+ if (el.type === 'heading' && el.headingLevel === 2) {
436
+ return SPACING_AFTER.subheading ?? 12;
437
+ }
438
+ return SPACING_AFTER[el.type] ?? 8;
439
+ }
440
+
441
+ /**
442
+ * Compute auto-alignment X offset for label-field pairs in a group (ADR-4, EC8).
443
+ * Returns 0 if no label-field pattern detected.
444
+ */
445
+ function computeFieldAlignX(children: WireframeElement[]): number {
446
+ let maxLabelWidth = 0;
447
+ let labelFieldCount = 0;
448
+
449
+ for (const child of children) {
450
+ if (child.metadata._labelField === 'true' && child.children.length >= 2) {
451
+ const labelEl = child.children[0];
452
+ const labelWidth = labelEl.label.length * CHAR_WIDTH;
453
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
454
+ labelFieldCount++;
455
+ }
456
+ }
457
+
458
+ if (labelFieldCount < 2) return 0;
459
+ return maxLabelWidth + LABEL_PADDING;
460
+ }