@diagrammo/dgmo 0.8.20 → 0.8.21

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 (53) hide show
  1. package/dist/cli.cjs +92 -90
  2. package/dist/editor.cjs +13 -1
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.js +13 -1
  5. package/dist/editor.js.map +1 -1
  6. package/dist/highlight.cjs +13 -1
  7. package/dist/highlight.cjs.map +1 -1
  8. package/dist/highlight.js +13 -1
  9. package/dist/highlight.js.map +1 -1
  10. package/dist/index.cjs +4144 -940
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +318 -84
  13. package/dist/index.d.ts +318 -84
  14. package/dist/index.js +4132 -938
  15. package/dist/index.js.map +1 -1
  16. package/docs/guide/chart-mindmap.md +198 -0
  17. package/docs/guide/chart-sequence.md +23 -1
  18. package/docs/guide/chart-wireframe.md +100 -0
  19. package/docs/guide/index.md +8 -0
  20. package/docs/language-reference.md +137 -2
  21. package/package.json +1 -1
  22. package/src/boxes-and-lines/collapse.ts +21 -3
  23. package/src/boxes-and-lines/layout.ts +51 -9
  24. package/src/boxes-and-lines/parser.ts +8 -1
  25. package/src/boxes-and-lines/renderer.ts +121 -23
  26. package/src/boxes-and-lines/types.ts +1 -0
  27. package/src/completion.ts +26 -0
  28. package/src/d3.ts +153 -32
  29. package/src/dgmo-router.ts +6 -0
  30. package/src/editor/keywords.ts +12 -0
  31. package/src/graph/layout.ts +73 -9
  32. package/src/graph/state-collapse.ts +78 -0
  33. package/src/graph/state-renderer.ts +139 -34
  34. package/src/index.ts +28 -0
  35. package/src/kanban/renderer.ts +303 -57
  36. package/src/mindmap/collapse.ts +88 -0
  37. package/src/mindmap/layout.ts +605 -0
  38. package/src/mindmap/parser.ts +379 -0
  39. package/src/mindmap/renderer.ts +543 -0
  40. package/src/mindmap/text-wrap.ts +207 -0
  41. package/src/mindmap/types.ts +55 -0
  42. package/src/render.ts +18 -21
  43. package/src/sequence/renderer.ts +129 -18
  44. package/src/sharing.ts +2 -0
  45. package/src/sitemap/layout.ts +35 -12
  46. package/src/utils/export-container.ts +3 -2
  47. package/src/utils/legend-d3.ts +1 -0
  48. package/src/utils/legend-layout.ts +2 -2
  49. package/src/utils/parsing.ts +2 -0
  50. package/src/wireframe/layout.ts +460 -0
  51. package/src/wireframe/parser.ts +956 -0
  52. package/src/wireframe/renderer.ts +1293 -0
  53. package/src/wireframe/types.ts +110 -0
@@ -0,0 +1,198 @@
1
+ # Mind Map
2
+
3
+ ```dgmo
4
+ mindmap Product Strategy
5
+
6
+ Research
7
+ User Interviews
8
+ Surveys | description: Quarterly NPS survey
9
+ Focus Groups
10
+ Competitor Analysis
11
+ Feature Matrix
12
+ Pricing Review
13
+ Development
14
+ MVP Features
15
+ Auth System
16
+ description: Handle login, signup, OAuth flows
17
+ Dashboard
18
+ Nice-to-haves | collapsed: true
19
+ Dark Mode
20
+ Export PDF
21
+ Go-to-Market
22
+ Launch Plan
23
+ Blog Post
24
+ Demo Video | description: 2-min product walkthrough
25
+ ```
26
+
27
+ ## Overview
28
+
29
+ Mind maps visualize hierarchical information radiating from a central concept. Each branch represents a subtopic, with deeper nesting for finer detail. Mind maps are useful for brainstorming, project planning, knowledge organization, and breaking down complex topics.
30
+
31
+ **Interactive features:** Click nodes to navigate to their source line. Collapse and expand subtrees by clicking nodes with children. Toggle tag group coloring and depth-based coloring via the legend controls.
32
+
33
+ ## Syntax
34
+
35
+ ```
36
+ mindmap Title
37
+
38
+ Node
39
+ Child Node
40
+ Grandchild
41
+ Another Child
42
+ ```
43
+
44
+ The first line declares the chart type. The title becomes the root node. Indentation establishes the parent-child hierarchy.
45
+
46
+ ## Nodes
47
+
48
+ Every non-blank, non-directive line is a node. Nesting is set by indentation:
49
+
50
+ ```
51
+ mindmap Root
52
+ Level 1a
53
+ Level 2a
54
+ Level 2b
55
+ Level 1b
56
+ ```
57
+
58
+ ### Node Colors
59
+
60
+ Add a color suffix in parentheses:
61
+
62
+ ```
63
+ Important Topic(red)
64
+ Sub-topic(blue)
65
+ ```
66
+
67
+ ### Descriptions
68
+
69
+ Nodes can have descriptions shown as secondary text. Two forms:
70
+
71
+ ```
72
+ // Pipe form (single line)
73
+ Surveys | description: Quarterly NPS survey
74
+
75
+ // Indented form (before children)
76
+ Auth System
77
+ description: Handle login, signup, and OAuth flows
78
+ Login Page
79
+ ```
80
+
81
+ ### Collapse Default
82
+
83
+ Set a node's default collapsed state with pipe metadata:
84
+
85
+ ```
86
+ Nice-to-haves | collapsed: true
87
+ Dark Mode
88
+ Export PDF
89
+ ```
90
+
91
+ Collapsed nodes show a drill-bar indicating hidden children. Click to expand.
92
+
93
+ ## Multi-Root
94
+
95
+ Omit the title to create multiple independent root trees:
96
+
97
+ ```
98
+ mindmap
99
+
100
+ Q1 Goals
101
+ Ship MVP
102
+ Hire designers
103
+ Q2 Goals
104
+ Launch marketing
105
+ Expand team
106
+ ```
107
+
108
+ The diagram title is inferred from the first root's label.
109
+
110
+ ## Pipe Metadata
111
+
112
+ Attach metadata after `|` with comma-separated `key: value` pairs:
113
+
114
+ ```
115
+ Node | priority: High, status: Done
116
+ ```
117
+
118
+ Tag values, descriptions, and `collapsed` are all set via pipe metadata.
119
+
120
+ ## Tag Groups
121
+
122
+ Tag groups define color-coded categories. They appear before content and follow the standard tag syntax:
123
+
124
+ ```
125
+ mindmap Root
126
+
127
+ tag Priority p
128
+ High(red)
129
+ Medium(yellow)
130
+ Low(green)
131
+
132
+ Task A | p: High
133
+ Task B | p: Low
134
+ ```
135
+
136
+ - The alias (`p`) provides a shorthand for metadata keys
137
+ - The active tag group colors nodes by their tag value
138
+ - Click a tag group name in the legend to activate or deactivate it
139
+
140
+ ## Options
141
+
142
+ | Option | Effect |
143
+ |--------|--------|
144
+ | `active-tag GroupName` | Set the default active tag group |
145
+ | `hide-descriptions` | Hide description text on all nodes |
146
+
147
+ Options are placed on their own line before content:
148
+
149
+ ```
150
+ mindmap Root
151
+
152
+ tag Priority p
153
+ High(red)
154
+ Low(green)
155
+
156
+ active-tag Priority
157
+ hide-descriptions
158
+
159
+ Task A | p: High
160
+ ```
161
+
162
+ ## Complete Example
163
+
164
+ ```dgmo
165
+ mindmap Product Strategy(blue)
166
+
167
+ tag Priority p
168
+ High(red)
169
+ Medium(yellow)
170
+ Low(green)
171
+
172
+ tag Department d
173
+ Engineering(blue)
174
+ Design(purple)
175
+ Marketing(orange)
176
+
177
+ active-tag Priority
178
+
179
+ Research | d: Marketing
180
+ User Interviews | p: High
181
+ Surveys | description: Quarterly NPS survey
182
+ Focus Groups
183
+ Competitor Analysis | d: Engineering
184
+ Feature Matrix
185
+ Pricing Review
186
+ Development | p: High, d: Engineering
187
+ MVP Features
188
+ Auth System
189
+ description: Handle login, signup, OAuth flows
190
+ Dashboard
191
+ Nice-to-haves | p: Low, collapsed: true
192
+ Dark Mode
193
+ Export PDF
194
+ Go-to-Market | d: Marketing
195
+ Launch Plan
196
+ Blog Post
197
+ Demo Video | description: 2-min product walkthrough
198
+ ```
@@ -103,7 +103,29 @@ PaymentGW is a networking aka "Payment Gateway"
103
103
  OrderDB position -1
104
104
  ```
105
105
 
106
- Position `0` is leftmost, `-1` is rightmost. Unpositioned participants appear in first-mention order.
106
+ ## Participant Ordering
107
+
108
+ Participants are laid out left-to-right based on **first appearance in messages** — the first participant mentioned gets the leftmost column.
109
+
110
+ **Position values:**
111
+ - `0` = leftmost, `1` = second from left, etc.
112
+ - `-1` = rightmost, `-2` = second from right
113
+ - If two participants target the same slot, the later one shifts to the nearest free position
114
+
115
+ **Groups affect ordering:** members of the same group always stay adjacent (see [Groups](#groups) below). The group is placed where its first member would naturally appear.
116
+
117
+ **Priority (highest wins):**
118
+ 1. Explicit `position N`
119
+ 2. Group adjacency
120
+ 3. First appearance in messages
121
+
122
+ ```
123
+ // Example: force the database to the far right
124
+ sequence
125
+ User -placeOrder-> OrderService
126
+ OrderService -save-> OrderDB
127
+ OrderDB position -1
128
+ ```
107
129
 
108
130
  ## Messages
109
131
 
@@ -0,0 +1,100 @@
1
+ # Wireframe
2
+
3
+ ```dgmo
4
+ wireframe Login Page
5
+
6
+ [Header]
7
+ Acme App
8
+ nav
9
+ Home | active
10
+ About
11
+ Pricing
12
+
13
+ [Main]
14
+ # Sign In
15
+
16
+ Email [user@example.com]
17
+ Password [****] | password
18
+
19
+ <x> Remember me
20
+
21
+ (Sign In)
22
+ (Forgot Password?) | ghost
23
+
24
+ ---
25
+
26
+ New here? (Create Account) | ghost
27
+ ```
28
+
29
+ ## Overview
30
+
31
+ Wireframe diagrams use **visual-mnemonic syntax** where bracket characters communicate the element type. The source text reads as a wireframe even before rendering.
32
+
33
+ ## Elements
34
+
35
+ | Syntax | Element |
36
+ |--------|---------|
37
+ | `[text]` (no children) | Text input |
38
+ | `[Name]` (with children) | Group / region |
39
+ | `(Label)` | Button |
40
+ | `{A \| B \| C}` | Dropdown |
41
+ | `<x>` / `< >` | Checkbox |
42
+ | `(*) Label` / `( ) Label` | Radio button |
43
+ | `# Text` | Heading |
44
+ | `## Text` | Subheading |
45
+ | `---` | Divider |
46
+ | `- text` | List item |
47
+
48
+ ## Keywords
49
+
50
+ `nav`, `tabs`, `table`, `image`, `modal`, `skeleton`, `alert`, `progress`, `chart`
51
+
52
+ - `image round` / `image wide` — shape hints
53
+ - `chart line` / `chart bar` / `chart pie` — chart silhouettes
54
+ - `progress 60` — percentage bar
55
+ - `table 5x4` — skeleton table with dimensions
56
+
57
+ ## States
58
+
59
+ Pipe metadata flags on elements:
60
+
61
+ ```
62
+ (Submit) | disabled
63
+ (Delete) | destructive
64
+ (Cancel) | ghost
65
+ [Email] | password
66
+ [Notes] | textarea
67
+ <x> Dark mode | toggle
68
+ [Cards] | horizontal
69
+ ```
70
+
71
+ ## Layout
72
+
73
+ - **Desktop** (default): 1200px, top-level regions side-by-side
74
+ - **Mobile**: `mobile` keyword, 375px vertical stacking
75
+ - **Smart sizing**: `sidebar` gets ~25%, `main` fills remaining
76
+ - `| horizontal` on any group arranges children in a row
77
+
78
+ ## Multi-Element Lines
79
+
80
+ Two or more spaces between elements on one line:
81
+
82
+ ```
83
+ Email [user@example.com] // label + field
84
+ (-) 1 (+) // inline stepper
85
+ ```
86
+
87
+ ## Tables
88
+
89
+ ```dgmo
90
+ wireframe Data
91
+
92
+ table
93
+ Name, Email, Role
94
+ John, john@, Admin
95
+ Sally, sally@, Editor
96
+
97
+ // Skeleton shorthand:
98
+ table 5x4
99
+ Name, Email, Role, Status
100
+ ```
@@ -4,6 +4,14 @@ Diagrammo is a diagram editor for creating charts and diagrams with a simple pla
4
4
 
5
5
  Learn more at **[diagrammo.app](https://diagrammo.app)**.
6
6
 
7
+ ## DGMO and Diagrammo
8
+
9
+ **DGMO** is the plain-text markup language you write. **Diagrammo** is the app you write it in.
10
+
11
+ Think of it like Markdown and your editor — Markdown is the syntax, and you can write it anywhere. DGMO works the same way: it's a `.dgmo` text file that describes a diagram. You can create and edit `.dgmo` files in the Diagrammo desktop app, the online editor, Obsidian (via plugin), or any text editor. The `dgmo` CLI renders them from the terminal, and the `@diagrammo/dgmo` npm package lets you render them programmatically.
12
+
13
+ The name "DGMO" is shorthand for "Diagrammo" — shorter to type, easier to use as a file extension and command name.
14
+
7
15
  ## Getting Started
8
16
 
9
17
  - **Create a new file** using the file tree on the left, or press **Cmd + N**
@@ -20,7 +20,8 @@
20
20
  14. [Timeline Diagrams](#14-timeline-diagrams)
21
21
  15. [Data Charts](#15-data-charts)
22
22
  16. [Visualizations](#16-visualizations)
23
- 17. [Colon Usage Summary](#17-colon-usage-summary)
23
+ 17. [Wireframe Diagrams](#17-wireframe-diagrams)
24
+ 18. [Colon Usage Summary](#18-colon-usage-summary)
24
25
 
25
26
  ---
26
27
 
@@ -1326,7 +1327,141 @@ Navigator 0.85 0.8
1326
1327
 
1327
1328
  ---
1328
1329
 
1329
- ## 17. Colon Usage Summary
1330
+ ## 17. Wireframe Diagrams
1331
+
1332
+ Wireframe diagrams use **visual-mnemonic syntax** where bracket characters communicate element type.
1333
+
1334
+ ### Declaration
1335
+
1336
+ ```
1337
+ wireframe Page Title
1338
+ ```
1339
+
1340
+ ### Form Factor
1341
+
1342
+ ```
1343
+ mobile
1344
+ ```
1345
+
1346
+ Switches to narrow vertical layout (375px). Desktop (1200px, horizontal regions) is the default.
1347
+
1348
+ ### Visual-Mnemonic Elements
1349
+
1350
+ | Syntax | Element | Example |
1351
+ |--------|---------|---------|
1352
+ | `[text]` (leaf) | Text input | `[Email address]` |
1353
+ | `[Name]` (with children) | Group/region | `[Sidebar]` + indented children |
1354
+ | `(Label)` | Button | `(Submit)` |
1355
+ | `{A \| B \| C}` | Dropdown/select | `{Small \| Medium \| Large}` |
1356
+ | `<x>` / `< >` | Checkbox | `<x> Remember me` |
1357
+ | `(*) Label` / `( ) Label` | Radio button | `(*) Option A` |
1358
+ | `# Text` / `## Text` | Heading | `# Sign In` |
1359
+ | `---` | Divider | `---` |
1360
+ | `- text` | List item | `- Electronics` |
1361
+ | Bare text | Text/paragraph | `Welcome to our app` |
1362
+
1363
+ ### Keyword Elements
1364
+
1365
+ | Keyword | Type | Parameters |
1366
+ |---------|------|------------|
1367
+ | `nav` | Block | Children are nav items |
1368
+ | `tabs` | Block | Children are tab labels |
1369
+ | `table` | Block | Comma-separated rows; first = header |
1370
+ | `table RxC` | Skeleton table | `table 5x4` + optional header row |
1371
+ | `image` | Leaf | `round`, `wide` hints |
1372
+ | `modal Title` | Block | Rendered as separate panel below |
1373
+ | `skeleton` | Block | Children render as grey placeholders |
1374
+ | `alert` | Block | Optional semantic state |
1375
+ | `progress N` | Leaf | Value 0-100: `progress 60` |
1376
+ | `chart type` | Leaf | `chart line`, `chart bar`, `chart pie` |
1377
+
1378
+ ### Pipe Metadata (States)
1379
+
1380
+ Wireframe uses flag keywords (not `key: value`):
1381
+
1382
+ ```
1383
+ (Submit) | disabled
1384
+ (Delete) | destructive
1385
+ (Cancel) | ghost
1386
+ [Email] | password
1387
+ [Notes] | textarea
1388
+ [Cards] | horizontal
1389
+ [Advanced] | collapsed
1390
+ [Messages] | scrollable
1391
+ <x> Dark mode | toggle
1392
+ ```
1393
+
1394
+ Available states: `disabled`, `active`, `selected`, `empty`, `ghost`, `destructive`, `success`, `warning`, `info`, `scrollable`, `collapsed`, `toggle`, `password`, `textarea`, `horizontal`, `primary`.
1395
+
1396
+ Free-text annotations after states: `[Email] | required, validates email format`.
1397
+
1398
+ ### Multi-Element Lines
1399
+
1400
+ Two or more spaces between segments create separate elements:
1401
+
1402
+ ```
1403
+ Email [user@example.com] // label + field (2 segments)
1404
+ (-) 1 (+) // 3 inline items
1405
+ $299.99 ~~$349.99~~ // 2 inline texts
1406
+ ```
1407
+
1408
+ - **2 segments** (bare text + element): label-for-element pairing
1409
+ - **3+ segments**: inline items, no label pairing
1410
+ - **Single space = same element**: `Cart (3)` is one text element
1411
+
1412
+ ### Group Disambiguation
1413
+
1414
+ - `[Name]` with indented children = group/container
1415
+ - `[Name]` with no children = text input
1416
+ - `[Name] | horizontal/scrollable/collapsed` = group (even without children)
1417
+
1418
+ ### Table Syntax
1419
+
1420
+ Explicit rows (comma-separated, first row = header):
1421
+
1422
+ ```
1423
+ table
1424
+ Name, Email, Role
1425
+ John, john@, Admin
1426
+ Sally, sally@, Editor
1427
+ ```
1428
+
1429
+ Skeleton shorthand:
1430
+
1431
+ ```
1432
+ table 5x4
1433
+ Name, Email, Role, Status
1434
+ ```
1435
+
1436
+ ### Layout Model
1437
+
1438
+ - Desktop: 1200px wide, top-level regions arrange horizontally
1439
+ - Mobile: 375px wide, all regions stack vertically
1440
+ - Smart sizing: `sidebar` → ~25%, `main`/`content` → fill, `header`/`footer` → full width
1441
+ - `| horizontal` on groups arranges children in a row
1442
+
1443
+ ### Example
1444
+
1445
+ ```
1446
+ wireframe Login Page
1447
+
1448
+ [Header]
1449
+ nav
1450
+ Home | active
1451
+ Settings
1452
+
1453
+ [Main]
1454
+ # Sign In
1455
+ Email [user@example.com]
1456
+ Password [****] | password
1457
+ <x> Remember me
1458
+ (Sign In)
1459
+ (Forgot Password?) | ghost
1460
+ ```
1461
+
1462
+ ---
1463
+
1464
+ ## 18. Colon Usage Summary
1330
1465
 
1331
1466
  ### Constructs Where Colons Are REQUIRED
1332
1467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.20",
3
+ "version": "0.8.21",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,12 +40,28 @@ export function collapseBoxesAndLines(
40
40
  const nodeToGroup = new Map<string, string>();
41
41
  const collapsedChildCounts = new Map<string, number>();
42
42
 
43
+ // Recursively collect all descendants of a group (including sub-group children)
44
+ function collectDescendants(groupLabel: string): string[] {
45
+ const group = groupByLabel.get(groupLabel);
46
+ if (!group) return [];
47
+ const descendants: string[] = [];
48
+ for (const child of group.children) {
49
+ descendants.push(child);
50
+ // If child is itself a group, collect its descendants too
51
+ if (groupByLabel.has(child)) {
52
+ descendants.push(...collectDescendants(child));
53
+ }
54
+ }
55
+ return descendants;
56
+ }
57
+
43
58
  for (const groupLabel of collapsedGroups) {
44
59
  const group = groupByLabel.get(groupLabel);
45
60
  if (!group) continue;
46
61
  const groupId = `__group_${groupLabel}`;
47
62
 
48
- for (const child of group.children) {
63
+ const allDescendants = collectDescendants(groupLabel);
64
+ for (const child of allDescendants) {
49
65
  nodeToGroup.set(child, groupId);
50
66
  }
51
67
  collapsedChildCounts.set(groupLabel, group.children.length);
@@ -67,8 +83,10 @@ export function collapseBoxesAndLines(
67
83
  edges.push({ ...edge, source: src, target: tgt });
68
84
  }
69
85
 
70
- // Keep only groups that are not collapsed
71
- const groups = parsed.groups.filter((g) => !collapsedGroups.has(g.label));
86
+ // Keep only groups that are not collapsed and not inside a collapsed group
87
+ const groups = parsed.groups.filter(
88
+ (g) => !collapsedGroups.has(g.label) && !nodeToGroup.has(g.label)
89
+ );
72
90
 
73
91
  return {
74
92
  parsed: { ...parsed, nodes, edges, groups },
@@ -3,7 +3,7 @@
3
3
  // ============================================================
4
4
 
5
5
  import dagre from '@dagrejs/dagre';
6
- import type { ParsedBoxesAndLines, BLNode } from './types';
6
+ import type { ParsedBoxesAndLines, BLNode, BLGroup } from './types';
7
7
 
8
8
  /**
9
9
  * Clip a point at (cx, cy) to the border of a rectangle centered at (cx, cy)
@@ -38,8 +38,7 @@ const CONTAINER_PAD_X = 30;
38
38
  const CONTAINER_PAD_TOP = 40;
39
39
  const CONTAINER_PAD_BOTTOM = 24;
40
40
  const MAX_PARALLEL_EDGES = 5;
41
- const PARALLEL_SPACING = 12;
42
- const PARALLEL_EDGE_MARGIN = 10;
41
+ const PARALLEL_SPACING = 22;
43
42
 
44
43
  // ── Result types ───────────────────────────────────────────
45
44
 
@@ -116,12 +115,23 @@ export function layoutBoxesAndLines(
116
115
  });
117
116
  g.setDefaultEdgeLabel(() => ({}));
118
117
 
119
- // Determine which groups are collapsed
118
+ // Determine which groups are collapsed (but not hidden inside a collapsed parent)
120
119
  const collapsedGroupLabels = new Set<string>();
121
120
  if (collapseInfo) {
121
+ // Build set of all groups that are missing from parsed (collapsed or hidden)
122
+ const missingGroups = new Set<string>();
122
123
  for (const og of collapseInfo.originalGroups) {
123
124
  if (!parsed.groups.some((g) => g.label === og.label)) {
124
- collapsedGroupLabels.add(og.label);
125
+ missingGroups.add(og.label);
126
+ }
127
+ }
128
+ // Only show a collapsed group as a node if its parent is NOT also missing
129
+ // (i.e., it's a directly collapsed group, not one hidden inside a collapsed parent)
130
+ for (const label of missingGroups) {
131
+ const og = collapseInfo.originalGroups.find((g) => g.label === label);
132
+ const parentLabel = og?.parentGroup;
133
+ if (!parentLabel || !missingGroups.has(parentLabel)) {
134
+ collapsedGroupLabels.add(label);
125
135
  }
126
136
  }
127
137
  }
@@ -147,6 +157,25 @@ export function layoutBoxesAndLines(
147
157
  });
148
158
  }
149
159
 
160
+ // Re-establish parent relationships for collapsed groups
161
+ // (must run AFTER expanded groups are added to the graph)
162
+ const originalGroupByLabel = new Map<string, BLGroup>();
163
+ if (collapseInfo) {
164
+ for (const og of collapseInfo.originalGroups) {
165
+ originalGroupByLabel.set(og.label, og);
166
+ }
167
+ }
168
+ for (const label of collapsedGroupLabels) {
169
+ const og = originalGroupByLabel.get(label);
170
+ if (og?.parentGroup && !collapsedGroupLabels.has(og.parentGroup)) {
171
+ const gid = `__group_${label}`;
172
+ const parentGid = `__group_${og.parentGroup}`;
173
+ if (g.hasNode(parentGid)) {
174
+ g.setParent(gid, parentGid);
175
+ }
176
+ }
177
+ }
178
+
150
179
  // Add nodes
151
180
  for (const node of parsed.nodes) {
152
181
  const size = computeNodeSize(node);
@@ -157,10 +186,26 @@ export function layoutBoxesAndLines(
157
186
  });
158
187
  }
159
188
 
189
+ // Set parent relationships for nested groups
190
+ for (const group of parsed.groups) {
191
+ if (group.parentGroup) {
192
+ const childGid = `__group_${group.label}`;
193
+ const parentGid = `__group_${group.parentGroup}`;
194
+ if (g.hasNode(childGid) && g.hasNode(parentGid)) {
195
+ g.setParent(childGid, parentGid);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Build set of group labels for skip-check below
201
+ const groupLabelSet = new Set(parsed.groups.map((gr) => gr.label));
202
+
160
203
  // Set parent relationships for nodes in groups
161
204
  for (const group of parsed.groups) {
162
205
  const gid = `__group_${group.label}`;
163
206
  for (const child of group.children) {
207
+ // Skip children that are sub-groups — their parent is set above
208
+ if (groupLabelSet.has(child)) continue;
164
209
  if (g.hasNode(child)) {
165
210
  g.setParent(child, gid);
166
211
  }
@@ -263,10 +308,7 @@ export function layoutBoxesAndLines(
263
308
  edgeParallelCounts[idx] = 0;
264
309
  }
265
310
  if (capped.length < 2) continue;
266
- const effectiveSpacing = Math.min(
267
- PARALLEL_SPACING,
268
- (60 - PARALLEL_EDGE_MARGIN) / (capped.length - 1)
269
- );
311
+ const effectiveSpacing = PARALLEL_SPACING;
270
312
  for (let j = 0; j < capped.length; j++) {
271
313
  edgeYOffsets[capped[j]] =
272
314
  (j - (capped.length - 1) / 2) * effectiveSpacing;
@@ -20,7 +20,7 @@ import {
20
20
  OPTION_NOCOLON_RE,
21
21
  } from '../utils/parsing';
22
22
 
23
- const MAX_GROUP_DEPTH = 1;
23
+ const MAX_GROUP_DEPTH = 2;
24
24
 
25
25
  /** Boxes-and-lines requires explicit first line — no heuristic detection. */
26
26
  export function looksLikeBoxesAndLines(_content: string): boolean {
@@ -387,13 +387,20 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
387
387
  }
388
388
  }
389
389
 
390
+ const parentGs = currentGroupState();
390
391
  const group: BLGroup = {
391
392
  label,
392
393
  children: [],
393
394
  lineNumber: lineNum,
394
395
  metadata: groupMeta,
396
+ ...(parentGs ? { parentGroup: parentGs.group.label } : {}),
395
397
  };
396
398
 
399
+ // Add nested group as child of parent group
400
+ if (parentGs && indent > parentGs.indent) {
401
+ parentGs.group.children.push(label);
402
+ }
403
+
397
404
  groupLabels.add(label);
398
405
  groupStack.push({ group, indent, depth: currentDepth });
399
406
  lastNodeLabel = label;