@transitrix/diagrams 1.13.2 → 1.13.3

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.
@@ -24,6 +24,11 @@ export interface Activity {
24
24
  resources_cost?: number;
25
25
  effort?: number;
26
26
  link?: string;
27
+ /** Derived display metadata, loaded from the model sidecar. */
28
+ progress?: {
29
+ percent: number;
30
+ computedAt: string;
31
+ };
27
32
  description?: string;
28
33
  delivers_changes?: string[];
29
34
  }
@@ -280,6 +280,7 @@ export function parseCanonicalFGCA(input, outOfScopeGoalIds, outOfScopeFactorIds
280
280
  name: String(el['name'] ?? ''),
281
281
  goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
282
282
  ...(typeVal !== undefined ? { type: typeVal } : {}),
283
+ ...(typeof el['link'] === 'string' ? { link: el['link'] } : {}),
283
284
  ...(progressVal !== undefined ? { progress: progressVal } : {}),
284
285
  };
285
286
  });
@@ -19,6 +19,7 @@ export interface FGCAPreviewChange {
19
19
  activity_ids: Array<number | string>;
20
20
  }
21
21
  export interface FGCAPreviewActivity {
22
+ link?: string;
22
23
  id: number | string;
23
24
  name: string;
24
25
  goal_id?: number | string | null;
@@ -250,7 +250,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
250
250
  ...changes.map(c => ({ id: `change_${c.id}`, label: c.name })),
251
251
  ...virtualChanges.map(v => ({ id: v.id, label: '–', virtual: true })),
252
252
  ],
253
- activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: a.goal_id != null })),
253
+ activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: Boolean(a.link?.trim()) })),
254
254
  };
255
255
  // Build predecessor map: for each node, which node IDs in the previous column
256
256
  // connect to it from the left. Used for barycenter crossing minimization.
@@ -24,6 +24,8 @@ export interface ActivityItem {
24
24
  activity_type_id?: number;
25
25
  /** Canonical values: initiative | programme | project | work_package. Unknown values pass through without error. */
26
26
  type?: string;
27
+ /** Tracker link, independent of goal relationships. */
28
+ link?: string;
27
29
  progress?: {
28
30
  percent: number;
29
31
  computedAt: string;
@@ -0,0 +1,10 @@
1
+ /** Shared completion display contract for Action, DGA and DGCA previews. */
2
+ export interface ActionProgress {
3
+ percent: number;
4
+ computedAt: string;
5
+ }
6
+ export declare function completionPercentState(progress: ActionProgress | undefined, linked: boolean, enabled?: boolean, now?: number): {
7
+ label: string;
8
+ stale: boolean;
9
+ } | undefined;
10
+ export declare function completionPercentSvg(progress: ActionProgress | undefined, linked: boolean, x: number, y: number, enabled?: boolean): string;
@@ -0,0 +1,12 @@
1
+ export function completionPercentState(progress, linked, enabled = true, now = Date.now()) {
2
+ if (!enabled)
3
+ return undefined;
4
+ if (progress && Number.isFinite(progress.percent) && progress.percent >= 0 && progress.percent <= 100 && Number.isFinite(Date.parse(progress.computedAt))) {
5
+ return { label: `${progress.percent}%`, stale: now - Date.parse(progress.computedAt) > 7 * 24 * 60 * 60 * 1000 };
6
+ }
7
+ return linked ? { label: '–%', stale: false } : undefined;
8
+ }
9
+ export function completionPercentSvg(progress, linked, x, y, enabled = true) {
10
+ const state = completionPercentState(progress, linked, enabled);
11
+ return state ? `<text class="text-id completion-percent" x="${x}" y="${y}" text-anchor="end" dominant-baseline="hanging" font-size="10"${state.stale ? ' opacity="0.5"' : ''}>${state.label}</text>` : '';
12
+ }
@@ -16,6 +16,7 @@
16
16
  * in `renderActivitiesNetworkBody` and is shared verbatim with the VS Code
17
17
  * preview's `networkSvg`.
18
18
  */
19
+ import { completionPercentSvg } from './completion-percent.js';
19
20
  import { layoutActivities } from '../activities/layout.js';
20
21
  import { computeCpm } from '../activities/cpm.js';
21
22
  import { horizontalCubicEdgePath, bowedCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
@@ -127,6 +128,7 @@ function activityNodeSvg(n, x, y, isCritical, isMilestone, durLabel) {
127
128
  return [
128
129
  `<rect class="${cls}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="8"/>`,
129
130
  textSvg,
131
+ completionPercentSvg(n.data.progress, Boolean(n.data.link?.trim()), x + n.width - 8, y + 6),
130
132
  durLabel ? `<text class="text-secondary" x="${x + n.width - 8}" y="${y + n.height - 8}" text-anchor="end">${durLabel}</text>` : '',
131
133
  ].filter(Boolean).join('\n');
132
134
  }
@@ -1,17 +1,3 @@
1
- /**
2
- * Browser-safe SVG renderer for the canonical DGCA and DGA notations.
3
- *
4
- * Step 4 of the IntelliJ epic (ADR 0001): the webview bundle must turn a
5
- * validated DGCA/DGA document into renderable SVG so JCEF can drop it into the
6
- * preview panel. The VS Code path lives in `extension/src/dgca-preview.ts` and
7
- * pulls in VS Code-specific concerns (themes, title block, save dialogs,
8
- * spacing/scope controls); this module is the host-neutral subset — pure
9
- * column layout → SVG with no VS Code APIs, no `node:fs`, no `node:path`.
10
- *
11
- * DGCA and DGA share the same renderer; DGA only differs by hiding the
12
- * `change` column (`hideChanges` / the layout's Goal → Activity collapse),
13
- * exactly as the extension preview does.
14
- */
15
1
  import { layoutFGCAPreview } from '../fgca/preview-layout.js';
16
2
  import type { FGCADoc } from '../fgca/validate.js';
17
3
  import { type EdgeStyle } from '../edge-path.js';
@@ -12,6 +12,7 @@
12
12
  * `change` column (`hideChanges` / the layout's Goal → Activity collapse),
13
13
  * exactly as the extension preview does.
14
14
  */
15
+ import { completionPercentSvg } from './completion-percent.js';
15
16
  import { layoutFGCAPreview, FGCA_NODE_W, FGCA_NODE_H, FGCA_HEADER_H as HEADER_H, FGCA_PAD as PAD, } from '../fgca/preview-layout.js';
16
17
  import { previewEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
17
18
  import { parseNodeSizePreset, resolveDgcaNodeSize } from '../node-size-presets.js';
@@ -39,7 +40,6 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
39
40
  const edgeStyle = bodyOptions.edgeStyle;
40
41
  const showCompletionPercent = bodyOptions.showCompletionPercent ?? true;
41
42
  const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
42
- const STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
43
43
  const headerSvg = columns
44
44
  .map(({ col, x }) => `<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`)
45
45
  .join('\n');
@@ -69,20 +69,8 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
69
69
  const typeBadge = n.type && n.col === 'activity'
70
70
  ? `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 10}" text-anchor="end" dominant-baseline="hanging" font-size="10">${escXml(n.type)}</text>`
71
71
  : '';
72
- const progressBadge = showCompletionPercent && n.col === 'activity'
73
- ? (() => {
74
- if (n.progress) {
75
- const computedAt = new Date(n.progress.computedAt);
76
- const now = new Date();
77
- const isStale = now.getTime() - computedAt.getTime() > STALENESS_THRESHOLD_MS;
78
- const opacity = isStale ? ' opacity="0.5"' : '';
79
- return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10"${opacity}>${escXml(n.progress.percent + '%')}</text>`;
80
- }
81
- if (n.linked) {
82
- return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10">–%</text>`;
83
- }
84
- return '';
85
- })()
72
+ const progressBadge = n.col === 'activity'
73
+ ? completionPercentSvg(n.progress, Boolean(n.linked), n.x + nodeWidth - 6, n.y + 22, showCompletionPercent)
86
74
  : '';
87
75
  return [
88
76
  `<g>`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.13.2",
3
+ "version": "1.13.3",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -52,13 +52,13 @@
52
52
  "reactflow": "^11.0.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@testing-library/dom": "^10.4.1",
55
+ "@testing-library/dom": "^10.4.2",
56
56
  "@testing-library/react": "^16.3.3",
57
57
  "@types/react": "^19.2.17",
58
- "@types/react-dom": "^19.2.5",
58
+ "@types/react-dom": "^19.3.0",
59
59
  "jsdom": "^29.1.1",
60
60
  "react": "^19.2.8",
61
- "react-dom": "^19.2.8",
61
+ "react-dom": "^19.3.0",
62
62
  "reactflow": "^11.11.4",
63
63
  "typescript": "^5.0.0",
64
64
  "vitest": "^4.1.11"
@@ -33,6 +33,8 @@ export interface Activity {
33
33
  resources_cost?: number;
34
34
  effort?: number;
35
35
  link?: string;
36
+ /** Derived display metadata, loaded from the model sidecar. */
37
+ progress?: { percent: number; computedAt: string };
36
38
  description?: string;
37
39
  delivers_changes?: string[];
38
40
  }
@@ -332,6 +332,7 @@ export function parseCanonicalFGCA(
332
332
  name: String(el!['name'] ?? ''),
333
333
  goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
334
334
  ...(typeVal !== undefined ? { type: typeVal } : {}),
335
+ ...(typeof el!['link'] === 'string' ? { link: el!['link'] } : {}),
335
336
  ...(progressVal !== undefined ? { progress: progressVal } : {}),
336
337
  };
337
338
  });
@@ -30,6 +30,7 @@ export interface FGCAPreviewChange {
30
30
  activity_ids: Array<number | string>;
31
31
  }
32
32
  export interface FGCAPreviewActivity {
33
+ link?: string;
33
34
  id: number | string;
34
35
  name: string;
35
36
  goal_id?: number | string | null;
@@ -387,7 +388,7 @@ export function layoutFGCAPreview(
387
388
  ...changes.map(c => ({ id: `change_${c.id}`, label: c.name })),
388
389
  ...virtualChanges.map(v => ({ id: v.id, label: '–', virtual: true })),
389
390
  ],
390
- activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: a.goal_id != null })),
391
+ activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: Boolean(a.link?.trim()) })),
391
392
  };
392
393
 
393
394
  // Build predecessor map: for each node, which node IDs in the previous column
package/src/fgca/types.ts CHANGED
@@ -25,6 +25,8 @@ export interface ActivityItem {
25
25
  activity_type_id?: number;
26
26
  /** Canonical values: initiative | programme | project | work_package. Unknown values pass through without error. */
27
27
  type?: string;
28
+ /** Tracker link, independent of goal relationships. */
29
+ link?: string;
28
30
  progress?: {
29
31
  percent: number;
30
32
  computedAt: string;
@@ -0,0 +1,18 @@
1
+ /** Shared completion display contract for Action, DGA and DGCA previews. */
2
+ export interface ActionProgress {
3
+ percent: number;
4
+ computedAt: string;
5
+ }
6
+
7
+ export function completionPercentState(progress: ActionProgress | undefined, linked: boolean, enabled = true, now = Date.now()): { label: string; stale: boolean } | undefined {
8
+ if (!enabled) return undefined;
9
+ if (progress && Number.isFinite(progress.percent) && progress.percent >= 0 && progress.percent <= 100 && Number.isFinite(Date.parse(progress.computedAt))) {
10
+ return { label: `${progress.percent}%`, stale: now - Date.parse(progress.computedAt) > 7 * 24 * 60 * 60 * 1000 };
11
+ }
12
+ return linked ? { label: '–%', stale: false } : undefined;
13
+ }
14
+
15
+ export function completionPercentSvg(progress: ActionProgress | undefined, linked: boolean, x: number, y: number, enabled = true): string {
16
+ const state = completionPercentState(progress, linked, enabled);
17
+ return state ? `<text class="text-id completion-percent" x="${x}" y="${y}" text-anchor="end" dominant-baseline="hanging" font-size="10"${state.stale ? ' opacity="0.5"' : ''}>${state.label}</text>` : '';
18
+ }
@@ -16,6 +16,7 @@
16
16
  * in `renderActivitiesNetworkBody` and is shared verbatim with the VS Code
17
17
  * preview's `networkSvg`.
18
18
  */
19
+ import { completionPercentSvg } from './completion-percent.js';
19
20
  import { layoutActivities } from '../activities/layout.js';
20
21
  import { computeCpm } from '../activities/cpm.js';
21
22
  import type {
@@ -147,6 +148,7 @@ function activityNodeSvg(
147
148
  return [
148
149
  `<rect class="${cls}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="8"/>`,
149
150
  textSvg,
151
+ completionPercentSvg(n.data.progress, Boolean(n.data.link?.trim()), x + n.width - 8, y + 6),
150
152
  durLabel ? `<text class="text-secondary" x="${x + n.width - 8}" y="${y + n.height - 8}" text-anchor="end">${durLabel}</text>` : '',
151
153
  ].filter(Boolean).join('\n');
152
154
  }
@@ -12,6 +12,7 @@
12
12
  * `change` column (`hideChanges` / the layout's Goal → Activity collapse),
13
13
  * exactly as the extension preview does.
14
14
  */
15
+ import { completionPercentSvg } from './completion-percent.js';
15
16
  import {
16
17
  layoutFGCAPreview,
17
18
  FGCA_NODE_W,
@@ -65,7 +66,6 @@ export function renderFgcaBody(
65
66
  const edgeStyle = bodyOptions.edgeStyle;
66
67
  const showCompletionPercent = bodyOptions.showCompletionPercent ?? true;
67
68
  const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
68
- const STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
69
69
 
70
70
  const headerSvg = columns
71
71
  .map(({ col, x }) =>
@@ -103,20 +103,8 @@ export function renderFgcaBody(
103
103
  const typeBadge = n.type && n.col === 'activity'
104
104
  ? `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 10}" text-anchor="end" dominant-baseline="hanging" font-size="10">${escXml(n.type)}</text>`
105
105
  : '';
106
- const progressBadge = showCompletionPercent && n.col === 'activity'
107
- ? (() => {
108
- if (n.progress) {
109
- const computedAt = new Date(n.progress.computedAt);
110
- const now = new Date();
111
- const isStale = now.getTime() - computedAt.getTime() > STALENESS_THRESHOLD_MS;
112
- const opacity = isStale ? ' opacity="0.5"' : '';
113
- return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10"${opacity}>${escXml(n.progress.percent + '%')}</text>`;
114
- }
115
- if (n.linked) {
116
- return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10">–%</text>`;
117
- }
118
- return '';
119
- })()
106
+ const progressBadge = n.col === 'activity'
107
+ ? completionPercentSvg(n.progress, Boolean(n.linked), n.x + nodeWidth - 6, n.y + 22, showCompletionPercent)
120
108
  : '';
121
109
  return [
122
110
  `<g>`,