@transitrix/diagrams 1.5.0 → 1.7.6

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 (148) hide show
  1. package/dist/capability-map/CapabilityCardNode.d.ts +19 -0
  2. package/dist/capability-map/CapabilityCardNode.js +66 -0
  3. package/dist/capability-map/CapabilityMapView.d.ts +59 -0
  4. package/dist/capability-map/CapabilityMapView.js +204 -0
  5. package/dist/capability-map/CapabilityRootNode.d.ts +12 -0
  6. package/dist/capability-map/CapabilityRootNode.js +23 -0
  7. package/dist/capability-map/__tests__/CapabilityMapView.test.d.ts +1 -0
  8. package/dist/capability-map/__tests__/CapabilityMapView.test.js +217 -0
  9. package/dist/capability-map/__tests__/address.test.d.ts +1 -0
  10. package/dist/capability-map/__tests__/address.test.js +49 -0
  11. package/dist/capability-map/__tests__/dsm-layout.test.d.ts +1 -0
  12. package/dist/capability-map/__tests__/dsm-layout.test.js +46 -0
  13. package/dist/capability-map/__tests__/dsm-mutations.test.d.ts +1 -0
  14. package/dist/capability-map/__tests__/dsm-mutations.test.js +125 -0
  15. package/dist/capability-map/__tests__/dsm-validate.test.d.ts +1 -0
  16. package/dist/capability-map/__tests__/dsm-validate.test.js +56 -0
  17. package/dist/capability-map/address.d.ts +12 -0
  18. package/dist/capability-map/address.js +48 -0
  19. package/dist/capability-map/dsm-layout.d.ts +9 -0
  20. package/dist/capability-map/dsm-layout.js +125 -0
  21. package/dist/capability-map/dsm-mutations.d.ts +28 -0
  22. package/dist/capability-map/dsm-mutations.js +196 -0
  23. package/dist/capability-map/dsm-schema.d.ts +71 -0
  24. package/dist/capability-map/dsm-schema.js +14 -0
  25. package/dist/capability-map/dsm-theme.d.ts +20 -0
  26. package/dist/capability-map/dsm-theme.js +36 -0
  27. package/dist/capability-map/dsm-validate.d.ts +2 -0
  28. package/dist/capability-map/dsm-validate.js +93 -0
  29. package/dist/capability-map/index.d.ts +9 -0
  30. package/dist/capability-map/index.js +6 -0
  31. package/dist/codex/__tests__/example.test.d.ts +1 -0
  32. package/dist/codex/__tests__/example.test.js +33 -0
  33. package/dist/codex/__tests__/validate.test.d.ts +1 -0
  34. package/dist/codex/__tests__/validate.test.js +68 -0
  35. package/dist/codex/index.d.ts +4 -0
  36. package/dist/codex/index.js +2 -0
  37. package/dist/codex/types.d.ts +7 -0
  38. package/dist/codex/types.js +8 -0
  39. package/dist/codex/validate.d.ts +11 -0
  40. package/dist/codex/validate.js +103 -0
  41. package/dist/compliance/__tests__/canon-catalog.test.d.ts +1 -0
  42. package/dist/compliance/__tests__/canon-catalog.test.js +60 -0
  43. package/dist/compliance/__tests__/trace.test.d.ts +1 -0
  44. package/dist/compliance/__tests__/trace.test.js +144 -0
  45. package/dist/compliance/__tests__/view-resolution.test.d.ts +1 -0
  46. package/dist/compliance/__tests__/view-resolution.test.js +59 -0
  47. package/dist/compliance/canon-catalog.d.ts +19 -0
  48. package/dist/compliance/canon-catalog.js +59 -0
  49. package/dist/compliance/classify.js +7 -1
  50. package/dist/compliance/coverage-metric.d.ts +12 -0
  51. package/dist/compliance/coverage-metric.js +35 -0
  52. package/dist/compliance/impact.d.ts +7 -1
  53. package/dist/compliance/impact.js +53 -0
  54. package/dist/compliance/index.d.ts +8 -5
  55. package/dist/compliance/index.js +4 -2
  56. package/dist/compliance/reverse-index.js +4 -1
  57. package/dist/compliance/trace.d.ts +26 -0
  58. package/dist/compliance/trace.js +89 -0
  59. package/dist/compliance/types.d.ts +94 -1
  60. package/dist/constraint/__tests__/example.test.d.ts +1 -0
  61. package/dist/constraint/__tests__/example.test.js +14 -0
  62. package/dist/constraint/__tests__/validate.test.d.ts +1 -0
  63. package/dist/constraint/__tests__/validate.test.js +58 -0
  64. package/dist/constraint/index.d.ts +4 -0
  65. package/dist/constraint/index.js +2 -0
  66. package/dist/constraint/types.d.ts +4 -0
  67. package/dist/constraint/types.js +2 -0
  68. package/dist/constraint/validate.d.ts +7 -0
  69. package/dist/constraint/validate.js +115 -0
  70. package/dist/fgca/__tests__/parse-canonical.test.js +31 -0
  71. package/dist/fgca/parse-canonical.d.ts +1 -1
  72. package/dist/fgca/parse-canonical.js +18 -4
  73. package/dist/goals/GoalNode.d.ts +19 -0
  74. package/dist/goals/GoalNode.js +96 -0
  75. package/dist/goals/GoalTreeView.d.ts +59 -0
  76. package/dist/goals/GoalTreeView.js +245 -0
  77. package/dist/goals/__tests__/GoalTreeView.test.d.ts +1 -0
  78. package/dist/goals/__tests__/GoalTreeView.test.js +337 -0
  79. package/dist/goals/__tests__/mutations.test.js +39 -0
  80. package/dist/goals/__tests__/parse-canonical.test.js +2 -0
  81. package/dist/goals/index.d.ts +3 -0
  82. package/dist/goals/index.js +1 -0
  83. package/dist/goals/layout.js +2 -1
  84. package/dist/goals/mutations.js +6 -6
  85. package/dist/goals/parse-canonical.d.ts +5 -0
  86. package/dist/goals/parse-canonical.js +5 -0
  87. package/dist/goals/theme.d.ts +22 -0
  88. package/dist/goals/theme.js +32 -0
  89. package/dist/goals/types.d.ts +2 -0
  90. package/dist/index.d.ts +2 -1
  91. package/dist/index.js +1 -1
  92. package/dist/repo-validate/types.js +2 -3
  93. package/dist/webview/__tests__/render-goals.test.js +17 -3
  94. package/dist/webview/render-goals.js +2 -1
  95. package/package.json +4 -1
  96. package/src/capability-map/CapabilityCardNode.tsx +146 -0
  97. package/src/capability-map/CapabilityMapView.tsx +331 -0
  98. package/src/capability-map/CapabilityRootNode.tsx +38 -0
  99. package/src/capability-map/__tests__/CapabilityMapView.test.tsx +232 -0
  100. package/src/capability-map/__tests__/address.test.ts +59 -0
  101. package/src/capability-map/__tests__/dsm-layout.test.ts +52 -0
  102. package/src/capability-map/__tests__/dsm-mutations.test.ts +141 -0
  103. package/src/capability-map/__tests__/dsm-validate.test.ts +65 -0
  104. package/src/capability-map/address.ts +48 -0
  105. package/src/capability-map/dsm-layout.ts +142 -0
  106. package/src/capability-map/dsm-mutations.ts +208 -0
  107. package/src/capability-map/dsm-schema.ts +80 -0
  108. package/src/capability-map/dsm-theme.ts +53 -0
  109. package/src/capability-map/dsm-validate.ts +103 -0
  110. package/src/capability-map/index.ts +35 -0
  111. package/src/codex/__tests__/example.test.ts +40 -0
  112. package/src/codex/__tests__/validate.test.ts +84 -0
  113. package/src/codex/index.ts +4 -0
  114. package/src/codex/types.ts +13 -0
  115. package/src/codex/validate.ts +125 -0
  116. package/src/compliance/__tests__/canon-catalog.test.ts +68 -0
  117. package/src/compliance/__tests__/trace.test.ts +157 -0
  118. package/src/compliance/__tests__/view-resolution.test.ts +80 -0
  119. package/src/compliance/canon-catalog.ts +76 -0
  120. package/src/compliance/classify.ts +7 -1
  121. package/src/compliance/coverage-metric.ts +50 -0
  122. package/src/compliance/impact.ts +67 -1
  123. package/src/compliance/index.ts +16 -1
  124. package/src/compliance/reverse-index.ts +3 -1
  125. package/src/compliance/trace.ts +118 -0
  126. package/src/compliance/types.ts +101 -1
  127. package/src/constraint/__tests__/example.test.ts +19 -0
  128. package/src/constraint/__tests__/validate.test.ts +68 -0
  129. package/src/constraint/index.ts +4 -0
  130. package/src/constraint/types.ts +5 -0
  131. package/src/constraint/validate.ts +131 -0
  132. package/src/fgca/__tests__/parse-canonical.test.ts +34 -0
  133. package/src/fgca/parse-canonical.ts +17 -4
  134. package/src/goals/GoalNode.tsx +219 -0
  135. package/src/goals/GoalTreeView.tsx +384 -0
  136. package/src/goals/__tests__/GoalTreeView.test.tsx +361 -0
  137. package/src/goals/__tests__/mutations.test.ts +42 -0
  138. package/src/goals/__tests__/parse-canonical.test.ts +2 -0
  139. package/src/goals/index.ts +4 -0
  140. package/src/goals/layout.ts +2 -1
  141. package/src/goals/mutations.ts +6 -6
  142. package/src/goals/parse-canonical.ts +6 -0
  143. package/src/goals/theme.ts +54 -0
  144. package/src/goals/types.ts +2 -0
  145. package/src/index.ts +29 -1
  146. package/src/repo-validate/types.ts +2 -3
  147. package/src/webview/__tests__/render-goals.test.ts +17 -3
  148. package/src/webview/render-goals.ts +2 -1
@@ -16,12 +16,12 @@ function getTypeForLevel(tree, level) {
16
16
  const gt = tree.goal_types.find(t => t.level === level);
17
17
  return gt?.name ?? '';
18
18
  }
19
- function updateDescendantLevels(goals, id, levelDelta) {
20
- for (const g of goals) {
19
+ function updateDescendantLevels(clone, id, levelDelta, maxLevel) {
20
+ for (const g of clone.goals) {
21
21
  if (g.parent_id === id) {
22
- g.level += levelDelta;
23
- g.type = '';
24
- updateDescendantLevels(goals, g.id, levelDelta);
22
+ g.level = Math.min(g.level + levelDelta, maxLevel);
23
+ g.type = getTypeForLevel(clone, g.level);
24
+ updateDescendantLevels(clone, g.id, levelDelta, maxLevel);
25
25
  }
26
26
  }
27
27
  }
@@ -67,7 +67,7 @@ export function reparent(tree, sourceId, targetId) {
67
67
  source.level = newLevel;
68
68
  source.type = getTypeForLevel(clone, newLevel);
69
69
  if (levelDelta !== 0)
70
- updateDescendantLevels(clone.goals, sourceId, levelDelta);
70
+ updateDescendantLevels(clone, sourceId, levelDelta, maxLevel);
71
71
  const v = validateGoalTree(clone);
72
72
  if (!v.valid)
73
73
  return refusalError(v.errors[0]?.message ?? 'Validation failed after reparent');
@@ -26,4 +26,9 @@ export interface CanonicalGoalsResult extends ValidationResult {
26
26
  /** On success, the internal-form GoalTree derived from the canonical input. */
27
27
  parsed?: GoalTree;
28
28
  }
29
+ /** Display id for previews — canonical `GOAL-…` when present, else internal numeric id. */
30
+ export declare function displayGoalId(goal: {
31
+ id: number;
32
+ canonical_id?: string;
33
+ }): string;
29
34
  export declare function parseCanonicalGoals(input: unknown): CanonicalGoalsResult;
@@ -22,6 +22,10 @@
22
22
  */
23
23
  const GOAL_ID_RE = /^GOAL(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
24
24
  const GOALS_DOC_ID_RE = /^GOALS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
25
+ /** Display id for previews — canonical `GOAL-…` when present, else internal numeric id. */
26
+ export function displayGoalId(goal) {
27
+ return goal.canonical_id ?? String(goal.id);
28
+ }
25
29
  function isNonEmptyString(v) {
26
30
  return typeof v === 'string' && v.trim().length > 0;
27
31
  }
@@ -240,6 +244,7 @@ export function parseCanonicalGoals(input) {
240
244
  const parentId = isNonEmptyString(parent) && idToInternal.has(parent) ? idToInternal.get(parent) : 0;
241
245
  return {
242
246
  id: idToInternal.get(o['id']),
247
+ canonical_id: o['id'],
243
248
  name: o['name'],
244
249
  type: o['type'],
245
250
  level: o['level'],
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Theme tokens for GoalTreeView — replaces DSM's Redux selectors
3
+ * (selectDiagramParams, selectGoalLevelColors) with a plain prop. Hosts
4
+ * that already carry Transitrix's --ts-* CSS custom properties (Studio)
5
+ * can pass those through; hosts with their own theme store (DSM) map
6
+ * their own values onto this shape.
7
+ */
8
+ export interface ThemeTokens {
9
+ /** Card fill colour per goal level (0..7). Falls back to DEFAULT_GOAL_LEVEL_COLORS for missing levels. */
10
+ goalLevelColors?: Record<number, string>;
11
+ cardBorderColor?: string;
12
+ cardBorderWidth?: number;
13
+ cardTextColor?: string;
14
+ cardMetaColor?: string;
15
+ edgeColor?: string;
16
+ edgeWidth?: number;
17
+ /** Border colour a card takes on while it's a highlighted drop target during drag. */
18
+ dropTargetBorderColor?: string;
19
+ }
20
+ export declare const DEFAULT_GOAL_LEVEL_COLORS: Record<number, string>;
21
+ export declare const DEFAULT_THEME: Required<ThemeTokens>;
22
+ export declare function resolveTheme(theme?: ThemeTokens): Required<ThemeTokens>;
@@ -0,0 +1,32 @@
1
+ export const DEFAULT_GOAL_LEVEL_COLORS = {
2
+ 0: '#e0e7ff',
3
+ 1: '#c7d2fe',
4
+ 2: '#a5b4fc',
5
+ 3: '#93c5fd',
6
+ 4: '#bae6fd',
7
+ 5: '#bbf7d0',
8
+ 6: '#fef08a',
9
+ 7: '#fed7aa',
10
+ };
11
+ export const DEFAULT_THEME = {
12
+ goalLevelColors: DEFAULT_GOAL_LEVEL_COLORS,
13
+ cardBorderColor: '#94a3b8',
14
+ cardBorderWidth: 1,
15
+ cardTextColor: '#0f172a',
16
+ cardMetaColor: '#64748b',
17
+ edgeColor: '#94a3b8',
18
+ edgeWidth: 1.5,
19
+ dropTargetBorderColor: '#2563eb',
20
+ };
21
+ export function resolveTheme(theme) {
22
+ return {
23
+ goalLevelColors: theme?.goalLevelColors ?? DEFAULT_THEME.goalLevelColors,
24
+ cardBorderColor: theme?.cardBorderColor ?? DEFAULT_THEME.cardBorderColor,
25
+ cardBorderWidth: theme?.cardBorderWidth ?? DEFAULT_THEME.cardBorderWidth,
26
+ cardTextColor: theme?.cardTextColor ?? DEFAULT_THEME.cardTextColor,
27
+ cardMetaColor: theme?.cardMetaColor ?? DEFAULT_THEME.cardMetaColor,
28
+ edgeColor: theme?.edgeColor ?? DEFAULT_THEME.edgeColor,
29
+ edgeWidth: theme?.edgeWidth ?? DEFAULT_THEME.edgeWidth,
30
+ dropTargetBorderColor: theme?.dropTargetBorderColor ?? DEFAULT_THEME.dropTargetBorderColor,
31
+ };
32
+ }
@@ -8,6 +8,8 @@ export interface Factor {
8
8
  }
9
9
  export interface Goal {
10
10
  id: number;
11
+ /** Canonical typed id from the YAML (`GOAL-…`) when parsed from canonical form. */
12
+ canonical_id?: string;
11
13
  name: string;
12
14
  type: string;
13
15
  level: number;
package/dist/index.d.ts CHANGED
@@ -6,7 +6,8 @@ export * from "./geometry";
6
6
  export * from "./applications/index";
7
7
  export * from "./assertion/index";
8
8
  export * from "./blocks/index";
9
- export * from "./capability-map/index";
9
+ export type { CapabilityType, CapabilityNode, CapabilityMapHeader, CapabilityMapFile, CapabilityTreeNode, CapabilityTreeEdge, CapabilityTreeLevelCounts, CapabilityTreeLayout, RenderCapabilityTreeOptions, } from "./capability-map/index";
10
+ export { validateCapabilityMap, layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_RANK_SEP, TREE_NODE_SEP, renderCapabilityTreeSvg, } from "./capability-map/index";
10
11
  export * from "./change/index";
11
12
  export * from "./compliance/index";
12
13
  export * from "./compliance-matrix/index";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ export * from "./geometry";
6
6
  export * from "./applications/index";
7
7
  export * from "./assertion/index";
8
8
  export * from "./blocks/index";
9
- export * from "./capability-map/index";
9
+ export { validateCapabilityMap, layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_RANK_SEP, TREE_NODE_SEP, renderCapabilityTreeSvg, } from "./capability-map/index";
10
10
  export * from "./change/index";
11
11
  export * from "./compliance/index";
12
12
  export * from "./compliance-matrix/index";
@@ -4,9 +4,8 @@
4
4
  // referential integrity, atomicity, ID uniqueness, policy — the checks that the
5
5
  // methodology's Python `.validators/lint.py` owns today. This converges those
6
6
  // checks onto the shared TypeScript model so there is a single validation
7
- // runtime (methodology ADR
8
- // `2026-06-11-validation-two-axis-model.md`; Studio referencing ADR
9
- // `docs/decisions/2026-06-11-validation-runtime-convergence.md`).
7
+ // runtime (methodology ADR "Validation Two-Axis Model"; Studio decision
8
+ // "Validation Runtime Convergence").
10
9
  //
11
10
  // The finding shape is deliberately minimal — `{ scope, id, message }`. The
12
11
  // richer `target` / `category` reporting taxonomy is explicitly DEFERRED per the
@@ -58,8 +58,22 @@ describe('renderGoalsSvg', () => {
58
58
  expect(svg).toContain('width="0"');
59
59
  expect(svg).toContain('height="0"');
60
60
  });
61
- it('omits the title block when treeName is missing', () => {
62
- const svg = renderGoalsSvg(SIMPLE_TREE);
63
- expect(svg).not.toContain('Goal tree —');
61
+ it('shows canonical goal ids in node labels when present', () => {
62
+ const tree = {
63
+ goal_types: [{ name: 'Strategy', level: 0 }],
64
+ goals: [
65
+ {
66
+ id: 1,
67
+ canonical_id: 'GOAL-REVENUE-1',
68
+ name: 'Triple revenue',
69
+ type: 'Strategy',
70
+ level: 0,
71
+ parent_id: 0,
72
+ },
73
+ ],
74
+ };
75
+ const svg = renderGoalsSvg(tree);
76
+ expect(svg).toContain('GOAL-REVENUE-1');
77
+ expect(svg).not.toMatch(/>\s*1\s*</);
64
78
  });
65
79
  });
@@ -11,6 +11,7 @@
11
11
  * Step 4 will follow the same shape for the remaining ten notations.
12
12
  */
13
13
  import { layoutGoalTree } from '../goals/layout.js';
14
+ import { displayGoalId } from '../goals/parse-canonical.js';
14
15
  import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
15
16
  import { generateSvgEmbedCss } from '../theme/index.js';
16
17
  import { escXml } from './render-util.js';
@@ -102,7 +103,7 @@ export function renderGoalsLayoutSvg(layout, options = {}) {
102
103
  const typeY = twoLines ? y + 43 : y + 35;
103
104
  const idY = twoLines ? y + 59 : y + 55;
104
105
  const typeLabel = n.data.type ?? '';
105
- const idText = String(n.data.id);
106
+ const idText = displayGoalId(n.data);
106
107
  return `<g>
107
108
  <rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="${ENTITY_NODE_RX}"/>
108
109
  <text class="text-primary" x="${x + n.width / 2}" y="${nameY1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>${twoLines ? `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.5.0",
3
+ "version": "1.7.6",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -43,8 +43,11 @@
43
43
  "reactflow": "^11.0.0"
44
44
  },
45
45
  "devDependencies": {
46
+ "@testing-library/dom": "^10.4.1",
47
+ "@testing-library/react": "^16.3.2",
46
48
  "@types/react": "^18.3.28",
47
49
  "@types/react-dom": "^19.2.3",
50
+ "jsdom": "^29.1.1",
48
51
  "react": "^19.2.6",
49
52
  "react-dom": "^19.2.6",
50
53
  "reactflow": "^11.11.4",
@@ -0,0 +1,146 @@
1
+ import React, { memo } from 'react';
2
+ import { Handle, Position } from 'reactflow';
3
+ import type { Capability } from './dsm-schema.js';
4
+ import type { ThemeTokens } from './dsm-theme.js';
5
+
6
+ const CARD_WIDTH = 250;
7
+ const CARD_HEIGHT = 64;
8
+
9
+ function latestMaturityLevel(cap: Capability): number | undefined {
10
+ if (!cap.maturity || cap.maturity.length === 0) return undefined;
11
+ return [...cap.maturity].sort((a, b) => b.date.localeCompare(a.date))[0].level;
12
+ }
13
+
14
+ export interface CapabilityCardNodeData {
15
+ capability: Capability;
16
+ theme: Required<ThemeTokens>;
17
+ maturityColours: Record<number, string>;
18
+ readOnly: boolean;
19
+ isDropTarget: boolean;
20
+ hasHiddenChildren: boolean;
21
+ isCollapsed: boolean;
22
+ onAddChild: (parentId: number) => void;
23
+ onDelete: (id: number) => void;
24
+ onToggleCollapse: (id: number) => void;
25
+ }
26
+
27
+ const CapabilityCardNode = memo(({ data }: { data: CapabilityCardNodeData }) => {
28
+ const { capability, theme, maturityColours, readOnly, isDropTarget, hasHiddenChildren, isCollapsed } = data;
29
+ const maturity = latestMaturityLevel(capability);
30
+ const dotColour = maturity !== undefined ? maturityColours[maturity] : undefined;
31
+
32
+ return (
33
+ <div
34
+ style={{
35
+ position: 'relative',
36
+ width: CARD_WIDTH,
37
+ height: CARD_HEIGHT,
38
+ background: theme.cardFill,
39
+ borderRadius: 8,
40
+ borderStyle: 'solid',
41
+ borderWidth: isDropTarget ? 2 : theme.cardBorderWidth,
42
+ borderColor: isDropTarget ? theme.dropTargetBorderColor : theme.cardBorderColor,
43
+ boxShadow: '0 1px 4px rgba(0,0,0,0.12)',
44
+ padding: '6px 10px',
45
+ display: 'flex',
46
+ flexDirection: 'column',
47
+ justifyContent: 'center',
48
+ }}
49
+ >
50
+ <Handle type="target" position={Position.Left} style={{ opacity: 0 }} />
51
+ <Handle type="source" position={Position.Right} style={{ opacity: 0 }} />
52
+
53
+ {dotColour && (
54
+ <span
55
+ title={`Maturity ${maturity}`}
56
+ aria-label={`Maturity ${maturity}`}
57
+ style={{
58
+ position: 'absolute', top: 6, left: 6, width: 8, height: 8, borderRadius: '50%', background: dotColour,
59
+ }}
60
+ />
61
+ )}
62
+
63
+ <div
64
+ title={capability.name}
65
+ style={{
66
+ fontWeight: 600,
67
+ fontSize: 12,
68
+ color: theme.cardTextColor,
69
+ marginLeft: dotColour ? 12 : 0,
70
+ overflow: 'hidden',
71
+ display: '-webkit-box',
72
+ WebkitLineClamp: 2,
73
+ WebkitBoxOrient: 'vertical',
74
+ wordBreak: 'break-word',
75
+ lineHeight: 1.2,
76
+ }}
77
+ >
78
+ {capability.name}
79
+ </div>
80
+ <div style={{ fontSize: 10, color: theme.cardMetaColor, marginTop: 2, marginLeft: dotColour ? 12 : 0 }}>
81
+ {capability.address}
82
+ </div>
83
+
84
+ {!readOnly && (
85
+ <button
86
+ type="button"
87
+ aria-label="Add child capability"
88
+ title="Add child capability"
89
+ onClick={(e) => {
90
+ e.stopPropagation();
91
+ data.onAddChild(capability.id);
92
+ }}
93
+ style={{
94
+ position: 'absolute', top: -8, right: -8, width: 16, height: 16,
95
+ borderRadius: '50%', border: '1px solid #86efac', background: '#ffffff',
96
+ color: '#16a34a', fontSize: 11, lineHeight: '14px', padding: 0, cursor: 'pointer',
97
+ }}
98
+ >
99
+ +
100
+ </button>
101
+ )}
102
+
103
+ {!readOnly && (
104
+ <button
105
+ type="button"
106
+ aria-label="Delete capability"
107
+ title="Delete capability and descendants"
108
+ onClick={(e) => {
109
+ e.stopPropagation();
110
+ data.onDelete(capability.id);
111
+ }}
112
+ style={{
113
+ position: 'absolute', bottom: -8, right: -8, width: 16, height: 16,
114
+ borderRadius: '50%', border: '1px solid #fca5a5', background: '#ffffff',
115
+ color: '#dc2626', fontSize: 11, lineHeight: '14px', padding: 0, cursor: 'pointer',
116
+ }}
117
+ >
118
+ &times;
119
+ </button>
120
+ )}
121
+
122
+ {hasHiddenChildren && (
123
+ <button
124
+ type="button"
125
+ aria-label={isCollapsed ? 'Expand branch' : 'Collapse branch'}
126
+ title={isCollapsed ? 'Expand branch' : 'Collapse branch'}
127
+ onClick={(e) => {
128
+ e.stopPropagation();
129
+ data.onToggleCollapse(capability.id);
130
+ }}
131
+ style={{
132
+ position: 'absolute', top: '50%', right: -8, transform: 'translateY(-50%)',
133
+ width: 16, height: 16, borderRadius: '50%', border: `1px solid ${theme.cardBorderColor}`,
134
+ background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0, cursor: 'pointer',
135
+ }}
136
+ >
137
+ {isCollapsed ? '+' : '−'}
138
+ </button>
139
+ )}
140
+ </div>
141
+ );
142
+ });
143
+
144
+ CapabilityCardNode.displayName = 'CapabilityCardNode';
145
+
146
+ export default CapabilityCardNode;