@diagrammo/dgmo 0.8.21 → 0.8.23

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 (114) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/cli.cjs +145 -93
  4. package/dist/editor.cjs +20 -3
  5. package/dist/editor.cjs.map +1 -1
  6. package/dist/editor.js +20 -3
  7. package/dist/editor.js.map +1 -1
  8. package/dist/highlight.cjs +15 -2
  9. package/dist/highlight.cjs.map +1 -1
  10. package/dist/highlight.js +15 -2
  11. package/dist/highlight.js.map +1 -1
  12. package/dist/index.cjs +20843 -14937
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +426 -17
  15. package/dist/index.d.ts +426 -17
  16. package/dist/index.js +20795 -14912
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal.cjs +380 -0
  19. package/dist/internal.cjs.map +1 -0
  20. package/dist/internal.d.cts +179 -0
  21. package/dist/internal.d.ts +179 -0
  22. package/dist/internal.js +337 -0
  23. package/dist/internal.js.map +1 -0
  24. package/docs/guide/chart-cycle.md +156 -0
  25. package/docs/guide/chart-journey-map.md +179 -0
  26. package/docs/guide/chart-pyramid.md +111 -0
  27. package/docs/guide/chart-sitemap.md +18 -1
  28. package/docs/guide/chart-tech-radar.md +219 -0
  29. package/docs/guide/registry.json +6 -0
  30. package/docs/language-reference.md +177 -6
  31. package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
  32. package/gallery/fixtures/c4-full.dgmo +2 -2
  33. package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
  34. package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
  35. package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
  36. package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
  37. package/gallery/fixtures/gantt-full.dgmo +2 -2
  38. package/gallery/fixtures/gantt.dgmo +2 -2
  39. package/gallery/fixtures/infra-full.dgmo +2 -2
  40. package/gallery/fixtures/infra.dgmo +1 -1
  41. package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
  42. package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
  43. package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
  44. package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
  45. package/gallery/fixtures/sequence-tags.dgmo +2 -2
  46. package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
  47. package/gallery/fixtures/tech-radar.dgmo +36 -0
  48. package/gallery/fixtures/timeline.dgmo +1 -1
  49. package/package.json +11 -1
  50. package/src/boxes-and-lines/layout.ts +309 -33
  51. package/src/boxes-and-lines/parser.ts +86 -10
  52. package/src/boxes-and-lines/renderer.ts +250 -91
  53. package/src/boxes-and-lines/types.ts +1 -1
  54. package/src/c4/layout.ts +8 -8
  55. package/src/c4/parser.ts +35 -2
  56. package/src/c4/renderer.ts +19 -3
  57. package/src/c4/types.ts +1 -0
  58. package/src/chart.ts +14 -7
  59. package/src/cli.ts +5 -35
  60. package/src/completion.ts +233 -41
  61. package/src/cycle/layout.ts +723 -0
  62. package/src/cycle/parser.ts +352 -0
  63. package/src/cycle/renderer.ts +566 -0
  64. package/src/cycle/types.ts +98 -0
  65. package/src/d3.ts +107 -8
  66. package/src/dgmo-router.ts +82 -3
  67. package/src/echarts.ts +8 -5
  68. package/src/editor/dgmo.grammar +5 -1
  69. package/src/editor/dgmo.grammar.js +1 -1
  70. package/src/editor/keywords.ts +17 -0
  71. package/src/gantt/parser.ts +2 -8
  72. package/src/graph/flowchart-parser.ts +15 -21
  73. package/src/graph/state-parser.ts +5 -10
  74. package/src/index.ts +63 -2
  75. package/src/infra/layout.ts +218 -74
  76. package/src/infra/parser.ts +32 -8
  77. package/src/infra/renderer.ts +14 -8
  78. package/src/infra/types.ts +10 -3
  79. package/src/internal.ts +16 -0
  80. package/src/journey-map/layout.ts +386 -0
  81. package/src/journey-map/parser.ts +540 -0
  82. package/src/journey-map/renderer.ts +1521 -0
  83. package/src/journey-map/types.ts +47 -0
  84. package/src/kanban/parser.ts +3 -10
  85. package/src/kanban/renderer.ts +31 -15
  86. package/src/mindmap/parser.ts +12 -18
  87. package/src/mindmap/renderer.ts +14 -13
  88. package/src/mindmap/text-wrap.ts +22 -12
  89. package/src/mindmap/types.ts +2 -2
  90. package/src/org/collapse.ts +81 -0
  91. package/src/org/parser.ts +2 -6
  92. package/src/org/renderer.ts +212 -4
  93. package/src/pyramid/parser.ts +172 -0
  94. package/src/pyramid/renderer.ts +684 -0
  95. package/src/pyramid/types.ts +28 -0
  96. package/src/render.ts +2 -8
  97. package/src/sequence/parser.ts +62 -20
  98. package/src/sequence/renderer.ts +146 -40
  99. package/src/sharing.ts +1 -0
  100. package/src/sitemap/layout.ts +21 -6
  101. package/src/sitemap/parser.ts +26 -17
  102. package/src/sitemap/renderer.ts +34 -0
  103. package/src/sitemap/types.ts +1 -0
  104. package/src/tech-radar/index.ts +14 -0
  105. package/src/tech-radar/interactive.ts +1112 -0
  106. package/src/tech-radar/layout.ts +190 -0
  107. package/src/tech-radar/parser.ts +385 -0
  108. package/src/tech-radar/renderer.ts +1159 -0
  109. package/src/tech-radar/shared.ts +187 -0
  110. package/src/tech-radar/types.ts +81 -0
  111. package/src/utils/description-helpers.ts +33 -0
  112. package/src/utils/legend-layout.ts +3 -1
  113. package/src/utils/parsing.ts +47 -7
  114. package/src/utils/tag-groups.ts +46 -60
@@ -0,0 +1,47 @@
1
+ import type { DgmoError } from '../diagnostics';
2
+ import type { TagGroup } from '../utils/tag-groups';
3
+
4
+ export interface JourneyMapAnnotation {
5
+ type: 'pain' | 'opportunity' | 'thought';
6
+ text: string;
7
+ }
8
+
9
+ export interface JourneyMapStep {
10
+ id: string;
11
+ title: string;
12
+ score?: number;
13
+ emotionLabel?: string;
14
+ tags: Record<string, string>;
15
+ annotations: JourneyMapAnnotation[];
16
+ description?: string;
17
+ lineNumber: number;
18
+ endLineNumber: number;
19
+ }
20
+
21
+ export interface JourneyMapPhase {
22
+ id: string;
23
+ name: string;
24
+ steps: JourneyMapStep[];
25
+ lineNumber: number;
26
+ }
27
+
28
+ export interface JourneyMapPersona {
29
+ name: string;
30
+ description?: string;
31
+ color?: string;
32
+ lineNumber: number;
33
+ }
34
+
35
+ export interface ParsedJourneyMap {
36
+ type: 'journey-map';
37
+ title?: string;
38
+ titleLineNumber?: number;
39
+ persona?: JourneyMapPersona;
40
+ phases: JourneyMapPhase[];
41
+ /** Flat-mode steps (not inside any phase) */
42
+ steps: JourneyMapStep[];
43
+ tagGroups: TagGroup[];
44
+ options: Record<string, string>;
45
+ diagnostics: DgmoError[];
46
+ error: string | null;
47
+ }
@@ -387,8 +387,8 @@ function parseCardLine(
387
387
  lineNumber: number,
388
388
  counter: number,
389
389
  aliasMap: Map<string, string>,
390
- palette?: PaletteColors,
391
- diagnostics?: import('../diagnostics').DgmoError[]
390
+ _palette?: PaletteColors,
391
+ _diagnostics?: import('../diagnostics').DgmoError[]
392
392
  ): KanbanCard {
393
393
  // Split on first pipe: Title | tag: value, tag: value
394
394
  const pipeIdx = trimmed.indexOf('|');
@@ -402,13 +402,7 @@ function parseCardLine(
402
402
  rawTitle = trimmed;
403
403
  }
404
404
 
405
- // Extract optional color suffix from title
406
- const { label: title, color } = extractColor(
407
- rawTitle,
408
- palette,
409
- diagnostics,
410
- lineNumber
411
- );
405
+ const title = rawTitle;
412
406
 
413
407
  // Parse tags: comma-separated key: value pairs
414
408
  const tags: Record<string, string> = {};
@@ -431,6 +425,5 @@ function parseCardLine(
431
425
  details: [],
432
426
  lineNumber,
433
427
  endLineNumber: lineNumber,
434
- color,
435
428
  };
436
429
  }
@@ -499,17 +499,21 @@ export function renderKanban(
499
499
  .attr('fill', palette.text)
500
500
  .text(col.name);
501
501
 
502
- // WIP limit badge
503
- if (col.wipLimit != null) {
504
- const wipExceeded = col.cards.length > col.wipLimit;
505
- const badgeText = `${col.cards.length}/${col.wipLimit}`;
506
- const nameWidth = col.name.length * COLUMN_HEADER_FONT_SIZE * 0.65;
502
+ // Card count / WIP limit badge (right-aligned)
503
+ {
504
+ const wipExceeded =
505
+ col.wipLimit != null && col.cards.length > col.wipLimit;
506
+ const badgeText =
507
+ col.wipLimit != null
508
+ ? `${col.cards.length}/${col.wipLimit}`
509
+ : String(col.cards.length);
507
510
  g.append('text')
508
- .attr('x', colLayout.x + COLUMN_PADDING + nameWidth + 8)
511
+ .attr('x', colLayout.x + colLayout.width - COLUMN_PADDING)
509
512
  .attr(
510
513
  'y',
511
514
  colLayout.y + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
512
515
  )
516
+ .attr('text-anchor', 'end')
513
517
  .attr('font-size', WIP_FONT_SIZE)
514
518
  .attr('fill', wipExceeded ? palette.colors.red : palette.textMuted)
515
519
  .attr('font-weight', wipExceeded ? 'bold' : 'normal')
@@ -978,6 +982,26 @@ function renderSwimlaneBoard(
978
982
  .attr('font-weight', 'bold')
979
983
  .attr('fill', palette.text)
980
984
  .text(col.name);
985
+
986
+ // Card count (right-aligned)
987
+ const wipExceeded =
988
+ col.wipLimit != null && col.cards.length > col.wipLimit;
989
+ const badgeText =
990
+ col.wipLimit != null
991
+ ? `${col.cards.length}/${col.wipLimit}`
992
+ : String(col.cards.length);
993
+ headerG
994
+ .append('text')
995
+ .attr('x', colInfo.x + colInfo.width - COLUMN_PADDING)
996
+ .attr(
997
+ 'y',
998
+ grid.startY + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
999
+ )
1000
+ .attr('text-anchor', 'end')
1001
+ .attr('font-size', WIP_FONT_SIZE)
1002
+ .attr('fill', wipExceeded ? palette.colors.red : palette.textMuted)
1003
+ .attr('font-weight', wipExceeded ? 'bold' : 'normal')
1004
+ .text(badgeText);
981
1005
  }
982
1006
  }
983
1007
 
@@ -1031,7 +1055,7 @@ function renderSwimlaneBoard(
1031
1055
  .attr('fill', palette.textMuted)
1032
1056
  .text(`${lane.bucket.laneName} (${totalCards})`);
1033
1057
  } else {
1034
- // Expanded: name on first line, count on second
1058
+ // Expanded: name only (count omitted to match app view)
1035
1059
  headerG
1036
1060
  .append('text')
1037
1061
  .attr('x', labelX)
@@ -1040,14 +1064,6 @@ function renderSwimlaneBoard(
1040
1064
  .attr('font-weight', 'bold')
1041
1065
  .attr('fill', lane.bucket.isFallback ? palette.textMuted : palette.text)
1042
1066
  .text(lane.bucket.laneName);
1043
-
1044
- headerG
1045
- .append('text')
1046
- .attr('x', labelX)
1047
- .attr('y', 36)
1048
- .attr('font-size', 10)
1049
- .attr('fill', palette.textMuted)
1050
- .text(`(${totalCards})`);
1051
1067
  }
1052
1068
 
1053
1069
  if (isLaneCollapsed) {
@@ -16,13 +16,12 @@ import {
16
16
  OPTION_NOCOLON_RE,
17
17
  } from '../utils/parsing';
18
18
  import type { MindmapNode, ParsedMindmap } from './types';
19
+ import { tryStripDescriptionKeyword } from '../utils/description-helpers';
19
20
 
20
21
  // ============================================================
21
22
  // Constants
22
23
  // ============================================================
23
24
 
24
- const DESCRIPTION_RE = /^description:\s*(.*)$/i;
25
-
26
25
  /** Known mindmap options (key-value). */
27
26
  const KNOWN_OPTIONS = new Set(['active-tag']);
28
27
 
@@ -111,8 +110,8 @@ export function parseMindmap(
111
110
  return fail(lineNumber, msg);
112
111
  }
113
112
  if (firstLine.title) {
114
- // Title IS the root: extract color from title
115
- const { label, color } = extractColor(firstLine.title, palette);
113
+ // Title IS the root
114
+ const label = firstLine.title;
116
115
  result.title = label;
117
116
  result.titleLineNumber = lineNumber;
118
117
 
@@ -124,7 +123,6 @@ export function parseMindmap(
124
123
  children: [],
125
124
  parentId: null,
126
125
  lineNumber,
127
- color,
128
126
  };
129
127
  result.roots.push(titleRoot);
130
128
  // Push title root onto indent stack at indent -1 so all indent-0 lines become children
@@ -208,14 +206,14 @@ export function parseMindmap(
208
206
 
209
207
  const indent = measureIndent(line);
210
208
 
211
- // Check for indented `description: text` metadata
209
+ // Check for indented `description: text` or `description text` metadata
212
210
  if (indent > 0) {
213
- const descMatch = trimmed.match(DESCRIPTION_RE);
214
- if (descMatch) {
211
+ const descResult = tryStripDescriptionKeyword(trimmed);
212
+ if (descResult.isKeyword) {
215
213
  // Find parent node from indent stack
216
214
  const parent = findMetadataParent(indent, indentStack);
217
215
  if (parent) {
218
- const descValue = descMatch[1].trim();
216
+ const descValue = descResult.text.trim();
219
217
  if (!descValue) {
220
218
  // Empty description: silently skip
221
219
  continue;
@@ -228,10 +226,8 @@ export function parseMindmap(
228
226
  );
229
227
  continue;
230
228
  }
231
- // Only set if pipe description didn't already set it
232
- if (parent.description === undefined) {
233
- parent.description = descValue;
234
- }
229
+ if (!parent.description) parent.description = [];
230
+ parent.description.push(descValue);
235
231
  continue;
236
232
  }
237
233
  }
@@ -300,19 +296,18 @@ function parseNodeLine(
300
296
  warnFn: (line: number, msg: string) => void
301
297
  ): MindmapNode {
302
298
  const segments = trimmed.split('|').map((s) => s.trim());
303
- const rawLabel = segments[0];
304
- const { label, color } = extractColor(rawLabel, palette);
299
+ const label = segments[0];
305
300
 
306
301
  const metadata = parsePipeMetadata(segments, aliasMap, () =>
307
302
  warnFn(lineNumber, MULTIPLE_PIPE_ERROR)
308
303
  );
309
304
 
310
305
  // Extract description from pipe metadata as a dedicated field
311
- let description: string | undefined;
306
+ let description: string[] | undefined;
312
307
  if ('description' in metadata) {
313
308
  const descVal = metadata['description'].trim();
314
309
  if (descVal) {
315
- description = descVal;
310
+ description = [descVal];
316
311
  }
317
312
  delete metadata['description'];
318
313
  }
@@ -332,7 +327,6 @@ function parseNodeLine(
332
327
  children: [],
333
328
  parentId: null,
334
329
  lineNumber,
335
- color,
336
330
  collapsed,
337
331
  };
338
332
  }
@@ -15,6 +15,8 @@ import type { MindmapLayoutResult } from './types';
15
15
  import { parseMindmap } from './parser';
16
16
  import { layoutMindmap } from './layout';
17
17
  import { computeNodeText } from './text-wrap';
18
+ import { renderInlineText } from '../utils/inline-markdown';
19
+ import { preprocessDescriptionLine } from '../utils/description-helpers';
18
20
  import { renderLegendD3 } from '../utils/legend-d3';
19
21
  import type { LegendConfig, LegendState } from '../utils/legend-types';
20
22
  import { LEGEND_HEIGHT, LEGEND_GROUP_GAP } from '../utils/legend-constants';
@@ -419,31 +421,30 @@ export function renderMindmap(
419
421
  .attr('stroke-opacity', 0.3)
420
422
  .attr('stroke-width', 1);
421
423
 
422
- // Description text
424
+ // Description text (with inline markdown + preprocessing)
423
425
  if (descLines.length <= 1) {
424
426
  const descY = separatorY + 4 + descFontSize;
425
- nodeG
427
+ const processed = preprocessDescriptionLine(descLines[0]);
428
+ const textEl = nodeG
426
429
  .append('text')
427
430
  .attr('x', centerX)
428
431
  .attr('y', descY)
429
432
  .attr('text-anchor', 'middle')
430
433
  .attr('font-size', descFontSize)
431
- .attr('fill', palette.textMuted)
432
- .text(descLines[0]);
434
+ .attr('fill', palette.textMuted);
435
+ renderInlineText(textEl, processed, palette, descFontSize);
433
436
  } else {
434
437
  const descStartY = separatorY + 4 + descFontSize;
435
- const descTextEl = nodeG
436
- .append('text')
437
- .attr('x', centerX)
438
- .attr('text-anchor', 'middle')
439
- .attr('font-size', descFontSize)
440
- .attr('fill', palette.textMuted);
441
438
  for (let i = 0; i < descLines.length; i++) {
442
- descTextEl
443
- .append('tspan')
439
+ const processed = preprocessDescriptionLine(descLines[i]);
440
+ const textEl = nodeG
441
+ .append('text')
444
442
  .attr('x', centerX)
445
443
  .attr('y', descStartY + i * DESC_LINE_HEIGHT)
446
- .text(descLines[i]);
444
+ .attr('text-anchor', 'middle')
445
+ .attr('font-size', descFontSize)
446
+ .attr('fill', palette.textMuted);
447
+ renderInlineText(textEl, processed, palette, descFontSize);
447
448
  }
448
449
  }
449
450
  }
@@ -6,6 +6,8 @@
6
6
  // multiple lines. Used by both layout (for sizing) and renderer
7
7
  // (for drawing). Ensures both agree on line breaks and font size.
8
8
 
9
+ import { preprocessDescriptionLine } from '../utils/description-helpers';
10
+
9
11
  const CHAR_WIDTH_RATIO = 0.58; // avg char width / fontSize for Helvetica
10
12
  const H_PAD = 16; // 8px padding each side
11
13
  const MAX_LABEL_LINES = 3;
@@ -170,7 +172,7 @@ interface NodeTextLayout {
170
172
  */
171
173
  export function computeNodeText(
172
174
  label: string,
173
- description: string | undefined,
175
+ description: string[] | undefined,
174
176
  depth: number,
175
177
  nodeWidth: number,
176
178
  hideDescriptions: boolean
@@ -184,18 +186,26 @@ export function computeNodeText(
184
186
  MAX_LABEL_LINES
185
187
  );
186
188
 
187
- let descLines: string[] = [];
189
+ const descLines: string[] = [];
188
190
  let descFontSize = DESC_FONT_SIZE;
189
- if (!hideDescriptions && description) {
190
- const descResult = wrapText(
191
- description,
192
- nodeWidth,
193
- DESC_FONT_SIZE,
194
- DESC_FONT_SIZE, // don't shrink descriptions
195
- MAX_DESC_LINES
196
- );
197
- descLines = descResult.lines;
198
- descFontSize = descResult.fontSize;
191
+ if (!hideDescriptions && description && description.length > 0) {
192
+ // Wrap each description line independently so bullets and line breaks are preserved.
193
+ // Cap total output lines at MAX_DESC_LINES across all input lines.
194
+ let remaining = MAX_DESC_LINES;
195
+ for (const rawLine of description) {
196
+ if (remaining <= 0) break;
197
+ const line = preprocessDescriptionLine(rawLine);
198
+ const lineResult = wrapText(
199
+ line,
200
+ nodeWidth,
201
+ DESC_FONT_SIZE,
202
+ DESC_FONT_SIZE,
203
+ remaining
204
+ );
205
+ descLines.push(...lineResult.lines);
206
+ remaining -= lineResult.lines.length;
207
+ descFontSize = lineResult.fontSize;
208
+ }
199
209
  }
200
210
 
201
211
  return {
@@ -4,7 +4,7 @@ import type { TagGroup } from '../utils/tag-groups.js';
4
4
  export interface MindmapNode {
5
5
  id: string;
6
6
  label: string;
7
- description?: string;
7
+ description?: string[];
8
8
  metadata: Record<string, string>;
9
9
  children: MindmapNode[];
10
10
  parentId: string | null;
@@ -26,7 +26,7 @@ export interface ParsedMindmap {
26
26
  export interface MindmapLayoutNode {
27
27
  id: string;
28
28
  label: string;
29
- description?: string;
29
+ description?: string[];
30
30
  metadata: Record<string, string>;
31
31
  lineNumber: number;
32
32
  color?: string;
@@ -15,6 +15,22 @@ export interface CollapsedOrgResult {
15
15
  hiddenCounts: Map<string, number>;
16
16
  }
17
17
 
18
+ export interface AncestorInfo {
19
+ id: string;
20
+ label: string;
21
+ lineNumber: number;
22
+ color?: string;
23
+ metadata: Record<string, string>;
24
+ isContainer: boolean;
25
+ }
26
+
27
+ export interface FocusOrgResult {
28
+ /** ParsedOrg with only the focused subtree as the single root */
29
+ parsed: ParsedOrg;
30
+ /** Ancestor path from original root → parent of focused node (top-down order) */
31
+ ancestorPath: AncestorInfo[];
32
+ }
33
+
18
34
  // ============================================================
19
35
  // Helpers
20
36
  // ============================================================
@@ -99,3 +115,68 @@ export function collapseOrgTree(
99
115
  hiddenCounts,
100
116
  };
101
117
  }
118
+
119
+ // ============================================================
120
+ // Focus (subtree drill-down)
121
+ // ============================================================
122
+
123
+ /** Find a node by ID and collect the ancestor path leading to it. */
124
+ function findNodeWithPath(
125
+ nodes: OrgNode[],
126
+ targetId: string,
127
+ path: AncestorInfo[]
128
+ ): { node: OrgNode; path: AncestorInfo[] } | null {
129
+ for (const node of nodes) {
130
+ if (node.id === targetId) {
131
+ return { node, path };
132
+ }
133
+ const result = findNodeWithPath(node.children, targetId, [
134
+ ...path,
135
+ {
136
+ id: node.id,
137
+ label: node.label,
138
+ lineNumber: node.lineNumber,
139
+ color: node.color,
140
+ metadata: { ...node.metadata },
141
+ isContainer: node.isContainer,
142
+ },
143
+ ]);
144
+ if (result) return result;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Extract a subtree rooted at `focusNodeId`, returning the focused tree
151
+ * and the ancestor breadcrumb path. Returns null if the node is not found.
152
+ */
153
+ export function focusOrgTree(
154
+ original: ParsedOrg,
155
+ focusNodeId: string
156
+ ): FocusOrgResult | null {
157
+ const found = findNodeWithPath(original.roots, focusNodeId, []);
158
+ if (!found) return null;
159
+
160
+ // If it's already a root, return as-is with empty ancestor path
161
+ const isRoot = original.roots.some((r) => r.id === focusNodeId);
162
+ if (isRoot) {
163
+ return {
164
+ parsed: {
165
+ ...original,
166
+ roots: [cloneNode(found.node)],
167
+ },
168
+ ancestorPath: [],
169
+ };
170
+ }
171
+
172
+ const cloned = cloneNode(found.node);
173
+ cloned.parentId = null;
174
+
175
+ return {
176
+ parsed: {
177
+ ...original,
178
+ roots: [cloned],
179
+ },
180
+ ancestorPath: found.path,
181
+ };
182
+ }
package/src/org/parser.ts CHANGED
@@ -270,8 +270,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
270
270
 
271
271
  if (containerMatch) {
272
272
  // It's a container node
273
- const rawLabel = containerMatch[1].trim();
274
- const { label, color } = extractColor(rawLabel, palette);
273
+ const label = containerMatch[1].trim();
275
274
 
276
275
  containerCounter++;
277
276
  const node: OrgNode = {
@@ -282,7 +281,6 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
282
281
  parentId: null,
283
282
  isContainer: true,
284
283
  lineNumber,
285
- color,
286
284
  };
287
285
 
288
286
  attachNode(node, indent, indentStack, result);
@@ -378,8 +376,7 @@ function parseNodeLabel(
378
376
  // Check for single-line compact metadata: "Alice Park | role: Senior, location: NY"
379
377
  const segments = trimmed.split('|').map((s) => s.trim());
380
378
 
381
- const rawLabel = segments[0];
382
- const { label, color } = extractColor(rawLabel, palette);
379
+ const label = segments[0];
383
380
 
384
381
  const metadata = parsePipeMetadata(
385
382
  segments,
@@ -395,7 +392,6 @@ function parseNodeLabel(
395
392
  parentId: null,
396
393
  isContainer: false,
397
394
  lineNumber,
398
- color,
399
395
  };
400
396
  }
401
397