@diagrammo/dgmo 0.2.22 → 0.2.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.
@@ -0,0 +1,43 @@
1
+ // ============================================================
2
+ // Initiative Status Diagram — Types
3
+ // ============================================================
4
+
5
+ import type { DgmoError } from '../diagnostics';
6
+ import type { ParticipantType } from '../sequence/parser';
7
+
8
+ export type InitiativeStatus = 'done' | 'wip' | 'todo' | 'na' | null;
9
+
10
+ export const VALID_STATUSES: readonly string[] = ['done', 'wip', 'todo', 'na'];
11
+
12
+ export interface ISNode {
13
+ label: string;
14
+ status: InitiativeStatus;
15
+ shape: ParticipantType;
16
+ lineNumber: number;
17
+ }
18
+
19
+ export interface ISEdge {
20
+ source: string; // node label
21
+ target: string; // node label
22
+ label?: string; // e.g. "getUser"
23
+ status: InitiativeStatus;
24
+ lineNumber: number;
25
+ }
26
+
27
+ export interface ISGroup {
28
+ label: string;
29
+ nodeLabels: string[];
30
+ lineNumber: number;
31
+ }
32
+
33
+ export interface ParsedInitiativeStatus {
34
+ type: 'initiative-status';
35
+ title: string | null;
36
+ titleLineNumber: number | null;
37
+ nodes: ISNode[];
38
+ edges: ISEdge[];
39
+ groups: ISGroup[];
40
+ options: Record<string, string>;
41
+ diagnostics: DgmoError[];
42
+ error?: string;
43
+ }
@@ -347,7 +347,12 @@ export function renderKanban(
347
347
  if (isActive) {
348
348
  let entryX = pillX + pillWidth + 4;
349
349
  for (const entry of group.entries) {
350
- svg
350
+ const entryG = svg
351
+ .append('g')
352
+ .attr('data-legend-entry', entry.value.toLowerCase())
353
+ .style('cursor', 'pointer');
354
+
355
+ entryG
351
356
  .append('circle')
352
357
  .attr('cx', entryX + LEGEND_DOT_R)
353
358
  .attr('cy', legendY + LEGEND_HEIGHT / 2)
@@ -355,7 +360,7 @@ export function renderKanban(
355
360
  .attr('fill', entry.color);
356
361
 
357
362
  const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
358
- svg
363
+ entryG
359
364
  .append('text')
360
365
  .attr('x', entryTextX)
361
366
  .attr('y', legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
@@ -466,6 +471,19 @@ export function renderKanban(
466
471
  .attr('data-card-id', card.id)
467
472
  .attr('data-line-number', card.lineNumber);
468
473
 
474
+ // Expose active tag group value for legend-entry hover dimming
475
+ if (activeTagGroup) {
476
+ const tagKey = activeTagGroup.toLowerCase();
477
+ const tagValue = card.tags[tagKey];
478
+ const group = parsed.tagGroups.find(
479
+ (tg) => tg.name.toLowerCase() === tagKey
480
+ );
481
+ const value = tagValue ?? group?.defaultValue;
482
+ if (value) {
483
+ cg.attr(`data-tag-${tagKey}`, value.toLowerCase());
484
+ }
485
+ }
486
+
469
487
  const cx = colLayout.x + cardLayout.x;
470
488
  const cy = colLayout.y + cardLayout.y;
471
489
 
package/src/org/layout.ts CHANGED
@@ -52,7 +52,6 @@ export interface OrgContainerBounds {
52
52
  export interface OrgLegendEntry {
53
53
  value: string;
54
54
  color: string;
55
- isDefault?: boolean;
56
55
  }
57
56
 
58
57
  export interface OrgLegendGroup {
@@ -272,29 +271,34 @@ function centerHeavyChildren(node: TreeNode): void {
272
271
  // Layout
273
272
  // ============================================================
274
273
 
275
- function computeLegendGroups(tagGroups: OrgTagGroup[], _showEyeIcons: boolean): OrgLegendGroup[] {
274
+ function computeLegendGroups(
275
+ tagGroups: OrgTagGroup[],
276
+ _showEyeIcons: boolean,
277
+ usedValuesByGroup?: Map<string, Set<string>>
278
+ ): OrgLegendGroup[] {
276
279
  const groups: OrgLegendGroup[] = [];
277
280
 
278
281
  for (const group of tagGroups) {
279
282
  if (group.entries.length === 0) continue;
280
283
 
281
- // Pill label includes alias if present (e.g., "Rank (r)")
282
- const pillLabel = group.alias ? `${group.name} (${group.alias})` : group.name;
283
- const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
284
- // Minified pill shows just the group name (no alias)
285
- const minPillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
284
+ // Filter entries to only values actually used by nodes (if provided)
285
+ const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
286
+ const visibleEntries = usedValues
287
+ ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase()))
288
+ : group.entries;
289
+ if (visibleEntries.length === 0) continue;
290
+
291
+ // Pill label shows just the group name (alias is for DSL shorthand only)
292
+ const pillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
293
+ const minPillWidth = pillWidth;
286
294
 
287
295
  // Capsule: pad + pill + gap + entries + pad
288
- const isDefaultValue = group.defaultValue?.toLowerCase();
289
296
  let entriesWidth = 0;
290
- for (const entry of group.entries) {
291
- const entryLabel = isDefaultValue === entry.value.toLowerCase()
292
- ? `${entry.value} (default)`
293
- : entry.value;
297
+ for (const entry of visibleEntries) {
294
298
  entriesWidth +=
295
299
  LEGEND_DOT_R * 2 +
296
300
  LEGEND_ENTRY_DOT_GAP +
297
- entryLabel.length * LEGEND_ENTRY_FONT_W +
301
+ entry.value.length * LEGEND_ENTRY_FONT_W +
298
302
  LEGEND_ENTRY_TRAIL;
299
303
  }
300
304
  const capsuleWidth =
@@ -303,10 +307,9 @@ function computeLegendGroups(tagGroups: OrgTagGroup[], _showEyeIcons: boolean):
303
307
  groups.push({
304
308
  name: group.name,
305
309
  alias: group.alias,
306
- entries: group.entries.map((e) => ({
310
+ entries: visibleEntries.map((e) => ({
307
311
  value: e.value,
308
312
  color: e.color,
309
- isDefault: group.defaultValue?.toLowerCase() === e.value.toLowerCase() || undefined,
310
313
  })),
311
314
  x: 0,
312
315
  y: 0,
@@ -1101,9 +1104,24 @@ export function layoutOrg(
1101
1104
  const totalWidth = finalMaxX - finalMinX + MARGIN * 2;
1102
1105
  const totalHeight = finalMaxY - minY + MARGIN * 2;
1103
1106
 
1107
+ // Collect which tag group values are actually used by nodes
1108
+ const usedValuesByGroup = new Map<string, Set<string>>();
1109
+ for (const group of parsed.tagGroups) {
1110
+ const key = group.name.toLowerCase();
1111
+ const used = new Set<string>();
1112
+ const walk = (node: OrgNode) => {
1113
+ if (!node.isContainer && node.metadata[key]) {
1114
+ used.add(node.metadata[key].toLowerCase());
1115
+ }
1116
+ for (const child of node.children) walk(child);
1117
+ };
1118
+ for (const root of parsed.roots) walk(root);
1119
+ usedValuesByGroup.set(key, used);
1120
+ }
1121
+
1104
1122
  // Compute legend for tag groups
1105
1123
  const showEyeIcons = hiddenAttributes !== undefined;
1106
- const legendGroups = computeLegendGroups(parsed.tagGroups, showEyeIcons);
1124
+ const legendGroups = computeLegendGroups(parsed.tagGroups, showEyeIcons, usedValuesByGroup);
1107
1125
  let finalWidth = totalWidth;
1108
1126
  let finalHeight = totalHeight;
1109
1127
 
@@ -77,14 +77,12 @@ function nodeFill(
77
77
  isDark: boolean,
78
78
  nodeColor?: string
79
79
  ): string {
80
- if (nodeColor) {
81
- return mix(nodeColor, isDark ? palette.surface : palette.bg, 25);
82
- }
83
- return mix(palette.primary, isDark ? palette.surface : palette.bg, 15);
80
+ const color = nodeColor ?? palette.primary;
81
+ return mix(color, isDark ? palette.surface : palette.bg, 25);
84
82
  }
85
83
 
86
84
  function nodeStroke(palette: PaletteColors, nodeColor?: string): string {
87
- return nodeColor ?? palette.textMuted;
85
+ return nodeColor ?? palette.primary;
88
86
  }
89
87
 
90
88
  function containerFill(
@@ -206,6 +204,15 @@ export function renderOrg(
206
204
  .attr('class', 'org-container')
207
205
  .attr('data-line-number', String(c.lineNumber)) as GSelection;
208
206
 
207
+ // Expose active tag group value for legend-entry hover dimming
208
+ if (activeTagGroup) {
209
+ const tagKey = activeTagGroup.toLowerCase();
210
+ const metaValue = c.metadata[tagKey];
211
+ if (metaValue) {
212
+ cG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
213
+ }
214
+ }
215
+
209
216
  // Toggle attribute for containers that have (or had) children
210
217
  if (c.hasChildren) {
211
218
  cG.attr('data-node-toggle', c.nodeId)
@@ -333,6 +340,15 @@ export function renderOrg(
333
340
  .attr('class', 'org-node')
334
341
  .attr('data-line-number', String(node.lineNumber)) as GSelection;
335
342
 
343
+ // Expose active tag group value for legend-entry hover dimming
344
+ if (activeTagGroup) {
345
+ const tagKey = activeTagGroup.toLowerCase();
346
+ const metaValue = node.metadata[tagKey];
347
+ if (metaValue) {
348
+ nodeG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
349
+ }
350
+ }
351
+
336
352
  // Toggle attribute for nodes that have (or had) children
337
353
  if (node.hasChildren) {
338
354
  nodeG
@@ -464,8 +480,8 @@ export function renderOrg(
464
480
  ? mix(palette.surface, palette.bg, 50)
465
481
  : mix(palette.surface, palette.bg, 30);
466
482
 
467
- // Pill label: include alias when expanded (e.g., "Rank (r)")
468
- const pillLabel = isActive && group.alias ? `${group.name} (${group.alias})` : group.name;
483
+ // Pill label: just the group name (alias is for DSL shorthand only)
484
+ const pillLabel = group.name;
469
485
  const pillWidth =
470
486
  pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
471
487
 
@@ -529,7 +545,12 @@ export function renderOrg(
529
545
  if (isActive) {
530
546
  let entryX = pillX + pillWidth + 4;
531
547
  for (const entry of group.entries) {
532
- gEl
548
+ const entryG = gEl
549
+ .append('g')
550
+ .attr('data-legend-entry', entry.value.toLowerCase())
551
+ .style('cursor', 'pointer');
552
+
553
+ entryG
533
554
  .append('circle')
534
555
  .attr('cx', entryX + LEGEND_DOT_R)
535
556
  .attr('cy', LEGEND_HEIGHT / 2)
@@ -537,8 +558,8 @@ export function renderOrg(
537
558
  .attr('fill', entry.color);
538
559
 
539
560
  const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
540
- const entryLabel = entry.isDefault ? `${entry.value} (default)` : entry.value;
541
- gEl
561
+ const entryLabel = entry.value;
562
+ entryG
542
563
  .append('text')
543
564
  .attr('x', textX)
544
565
  .attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
@@ -200,6 +200,7 @@ async function resolveFile(
200
200
  const bodyStartIndex = findBodyStart(lines);
201
201
 
202
202
  // Collect header lines (chart:, title:, options, tags:)
203
+ let tagsLineNumber = 0; // 1-based line number of the tags: directive
203
204
  for (let i = 0; i < bodyStartIndex; i++) {
204
205
  const trimmed = lines[i].trim();
205
206
  if (trimmed === '' || trimmed.startsWith('//')) {
@@ -212,6 +213,7 @@ async function resolveFile(
212
213
  const tagsMatch = trimmed.match(TAGS_RE);
213
214
  if (tagsMatch) {
214
215
  tagsDirective = tagsMatch[1].trim();
216
+ tagsLineNumber = i + 1; // 1-based
215
217
  continue;
216
218
  }
217
219
 
@@ -228,7 +230,7 @@ async function resolveFile(
228
230
  tagsFileGroups = extractTagGroups(tagsLines);
229
231
  } catch {
230
232
  diagnostics.push(
231
- makeDgmoError(0, `Tags file not found: ${tagsDirective}`)
233
+ makeDgmoError(tagsLineNumber, `Tags file not found: ${tagsDirective}`)
232
234
  );
233
235
  }
234
236
  }
package/src/render.ts CHANGED
@@ -49,6 +49,9 @@ export async function render(
49
49
  theme?: 'light' | 'dark' | 'transparent';
50
50
  palette?: string;
51
51
  branding?: boolean;
52
+ c4Level?: 'context' | 'containers' | 'components' | 'deployment';
53
+ c4System?: string;
54
+ c4Container?: string;
52
55
  },
53
56
  ): Promise<string> {
54
57
  const theme = options?.theme ?? 'light';
@@ -66,5 +69,10 @@ export async function render(
66
69
 
67
70
  // D3 and unknown/null frameworks both go through D3 renderer
68
71
  await ensureDom();
69
- return renderD3ForExport(content, theme, paletteColors, undefined, { branding });
72
+ return renderD3ForExport(content, theme, paletteColors, undefined, {
73
+ branding,
74
+ c4Level: options?.c4Level,
75
+ c4System: options?.c4System,
76
+ c4Container: options?.c4Container,
77
+ });
70
78
  }
@@ -203,6 +203,7 @@ const PARTICIPANT_RULES: readonly InferenceRule[] = [
203
203
  { pattern: /User$/i, type: 'actor' },
204
204
  { pattern: /Actor$/i, type: 'actor' },
205
205
  { pattern: /Analyst$/i, type: 'actor' },
206
+ { pattern: /Staff$/i, type: 'actor' },
206
207
 
207
208
  // ── 7. Frontend patterns ────────────────────────────────
208
209
  { pattern: /App$/i, type: 'frontend' },
@@ -1925,7 +1925,8 @@ export function renderSequenceDiagram(
1925
1925
  'data-line-number',
1926
1926
  String(messages[step.messageIndex].lineNumber)
1927
1927
  )
1928
- .attr('data-msg-index', String(step.messageIndex));
1928
+ .attr('data-msg-index', String(step.messageIndex))
1929
+ .attr('data-step-index', String(i));
1929
1930
 
1930
1931
  if (step.label) {
1931
1932
  const labelEl = svg
@@ -1940,7 +1941,8 @@ export function renderSequenceDiagram(
1940
1941
  'data-line-number',
1941
1942
  String(messages[step.messageIndex].lineNumber)
1942
1943
  )
1943
- .attr('data-msg-index', String(step.messageIndex));
1944
+ .attr('data-msg-index', String(step.messageIndex))
1945
+ .attr('data-step-index', String(i));
1944
1946
  renderInlineText(labelEl, step.label, palette);
1945
1947
  }
1946
1948
  } else {
@@ -1966,7 +1968,8 @@ export function renderSequenceDiagram(
1966
1968
  'data-line-number',
1967
1969
  String(messages[step.messageIndex].lineNumber)
1968
1970
  )
1969
- .attr('data-msg-index', String(step.messageIndex));
1971
+ .attr('data-msg-index', String(step.messageIndex))
1972
+ .attr('data-step-index', String(i));
1970
1973
 
1971
1974
  if (step.label) {
1972
1975
  const midX = (x1 + x2) / 2;
@@ -1982,7 +1985,8 @@ export function renderSequenceDiagram(
1982
1985
  'data-line-number',
1983
1986
  String(messages[step.messageIndex].lineNumber)
1984
1987
  )
1985
- .attr('data-msg-index', String(step.messageIndex));
1988
+ .attr('data-msg-index', String(step.messageIndex))
1989
+ .attr('data-step-index', String(i));
1986
1990
  renderInlineText(labelEl, step.label, palette);
1987
1991
  }
1988
1992
  }
@@ -2011,7 +2015,8 @@ export function renderSequenceDiagram(
2011
2015
  'data-line-number',
2012
2016
  String(messages[step.messageIndex].lineNumber)
2013
2017
  )
2014
- .attr('data-msg-index', String(step.messageIndex));
2018
+ .attr('data-msg-index', String(step.messageIndex))
2019
+ .attr('data-step-index', String(i));
2015
2020
 
2016
2021
  if (step.label) {
2017
2022
  const midX = (x1 + x2) / 2;
@@ -2027,7 +2032,8 @@ export function renderSequenceDiagram(
2027
2032
  'data-line-number',
2028
2033
  String(messages[step.messageIndex].lineNumber)
2029
2034
  )
2030
- .attr('data-msg-index', String(step.messageIndex));
2035
+ .attr('data-msg-index', String(step.messageIndex))
2036
+ .attr('data-step-index', String(i));
2031
2037
  renderInlineText(labelEl, step.label, palette);
2032
2038
  }
2033
2039
  }