@transitrix/diagrams 1.12.0 → 1.12.1
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.
- package/dist/capability-map/CapabilityCardNode.d.ts +1 -0
- package/dist/capability-map/CapabilityCardNode.js +5 -4
- package/dist/capability-map/CapabilityMapView.js +1 -0
- package/dist/capability-map/dsm-layout.js +5 -4
- package/dist/capability-map/dsm-schema.d.ts +1 -0
- package/dist/goals/GoalNode.d.ts +1 -0
- package/dist/goals/GoalNode.js +5 -4
- package/dist/goals/GoalTreeView.js +1 -0
- package/dist/goals/layout.js +7 -5
- package/dist/goals/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/capability-map/CapabilityCardNode.tsx +8 -6
- package/src/capability-map/CapabilityMapView.tsx +1 -0
- package/src/capability-map/dsm-layout.ts +5 -4
- package/src/capability-map/dsm-schema.ts +1 -0
- package/src/goals/GoalNode.tsx +8 -6
- package/src/goals/GoalTreeView.tsx +1 -0
- package/src/goals/layout.ts +7 -5
- package/src/goals/types.ts +2 -0
|
@@ -9,7 +9,7 @@ function latestMaturityLevel(cap) {
|
|
|
9
9
|
return [...cap.maturity].sort((a, b) => b.date.localeCompare(a.date))[0].level;
|
|
10
10
|
}
|
|
11
11
|
const CapabilityCardNode = memo(({ data }) => {
|
|
12
|
-
const { capability, theme, maturityColours, readOnly, isDropTarget, hasHiddenChildren, isCollapsed } = data;
|
|
12
|
+
const { capability, theme, maturityColours, readOnly, isDropTarget, hasChildren, hasHiddenChildren, isCollapsed } = data;
|
|
13
13
|
const maturity = latestMaturityLevel(capability);
|
|
14
14
|
const dotColour = maturity !== undefined ? maturityColours[maturity] : undefined;
|
|
15
15
|
return (_jsxs("div", { style: {
|
|
@@ -53,14 +53,15 @@ const CapabilityCardNode = memo(({ data }) => {
|
|
|
53
53
|
position: 'absolute', bottom: -8, right: -8, width: 16, height: 16,
|
|
54
54
|
borderRadius: '50%', border: '1px solid #fca5a5', background: '#ffffff',
|
|
55
55
|
color: '#dc2626', fontSize: 11, lineHeight: '14px', padding: 0, cursor: 'pointer',
|
|
56
|
-
}, children: "\u00D7" })), hasHiddenChildren && (_jsx("button", { type: "button", "aria-label": isCollapsed ? 'Expand branch' : 'Collapse branch', title: isCollapsed ? 'Expand branch' : 'Collapse branch', onClick: (e) => {
|
|
56
|
+
}, children: "\u00D7" })), (hasChildren || hasHiddenChildren) && (_jsx("button", { type: "button", "aria-label": isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch', title: isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch', onClick: (e) => {
|
|
57
57
|
e.stopPropagation();
|
|
58
58
|
data.onToggleCollapse(capability.id);
|
|
59
59
|
}, style: {
|
|
60
60
|
position: 'absolute', top: '50%', right: -8, transform: 'translateY(-50%)',
|
|
61
61
|
width: 16, height: 16, borderRadius: '50%', border: `1px solid ${theme.cardBorderColor}`,
|
|
62
|
-
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
63
|
-
|
|
62
|
+
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
63
|
+
cursor: 'pointer', zIndex: 10,
|
|
64
|
+
}, children: isCollapsed || hasHiddenChildren ? '+' : '−' }))] }));
|
|
64
65
|
});
|
|
65
66
|
CapabilityCardNode.displayName = 'CapabilityCardNode';
|
|
66
67
|
export default CapabilityCardNode;
|
|
@@ -101,6 +101,7 @@ function CapabilityMapViewInner({ map, layout: layoutProp, layoutOptions, theme:
|
|
|
101
101
|
maturityColours,
|
|
102
102
|
readOnly,
|
|
103
103
|
isDropTarget: hoveredId === n.id,
|
|
104
|
+
hasChildren: n.hasChildren,
|
|
104
105
|
hasHiddenChildren: n.hasHiddenChildren,
|
|
105
106
|
isCollapsed: collapsedIds.has(n.id),
|
|
106
107
|
onAddChild,
|
|
@@ -68,11 +68,12 @@ export function layoutCapabilityMap(map, options = {}) {
|
|
|
68
68
|
const cap = byId.get(id);
|
|
69
69
|
const allChildIds = children.get(id) ?? [];
|
|
70
70
|
const childIds = allChildIds.filter((c) => !hiddenSubtrees.has(c));
|
|
71
|
+
const hasChildren = allChildIds.length > 0;
|
|
71
72
|
const hasHiddenChildren = childIds.length < allChildIds.length;
|
|
72
73
|
if (childIds.length === 0) {
|
|
73
74
|
const y = getNextY(col);
|
|
74
75
|
advanceY(col, nodeHeight + nodeSep);
|
|
75
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: cap, hasHiddenChildren });
|
|
76
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: cap, hasChildren, hasHiddenChildren });
|
|
76
77
|
return { top: y, bottom: y + nodeHeight };
|
|
77
78
|
}
|
|
78
79
|
const spans = childIds.map((cid) => placeSubtree(cid, col + 1));
|
|
@@ -82,7 +83,7 @@ export function layoutCapabilityMap(map, options = {}) {
|
|
|
82
83
|
const curColY = getNextY(col);
|
|
83
84
|
const finalY = Math.max(parentY, curColY);
|
|
84
85
|
advanceY(col, finalY - curColY + nodeHeight + nodeSep);
|
|
85
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: cap, hasHiddenChildren });
|
|
86
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: cap, hasChildren, hasHiddenChildren });
|
|
86
87
|
for (const cid of childIds)
|
|
87
88
|
edges.push({ source: id, target: cid });
|
|
88
89
|
return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + nodeHeight, spanBottom) };
|
|
@@ -95,7 +96,7 @@ export function layoutCapabilityMap(map, options = {}) {
|
|
|
95
96
|
columnNextY.set(col, y + nodeSep);
|
|
96
97
|
}
|
|
97
98
|
if (nodes.length === 0) {
|
|
98
|
-
const rootNode = { id: 0, x: 0, y: 0, width: ROOT_WIDTH, height: nodeHeight, data: null, hasHiddenChildren: false };
|
|
99
|
+
const rootNode = { id: 0, x: 0, y: 0, width: ROOT_WIDTH, height: nodeHeight, data: null, hasChildren: false, hasHiddenChildren: false };
|
|
99
100
|
return { rootNode, nodes: [], edges: [], bounds: { x: 0, y: 0, width: ROOT_WIDTH, height: nodeHeight } };
|
|
100
101
|
}
|
|
101
102
|
const minX = Math.min(...nodes.map((n) => n.x));
|
|
@@ -107,7 +108,7 @@ export function layoutCapabilityMap(map, options = {}) {
|
|
|
107
108
|
const rootTop = Math.min(...l1Nodes.map((n) => n.y));
|
|
108
109
|
const rootBottom = Math.max(...l1Nodes.map((n) => n.y + n.height));
|
|
109
110
|
const rootY = rootTop + (rootBottom - rootTop) / 2 - nodeHeight / 2;
|
|
110
|
-
const rootNode = { id: 0, x: 0, y: rootY, width: ROOT_WIDTH, height: nodeHeight, data: null, hasHiddenChildren: false };
|
|
111
|
+
const rootNode = { id: 0, x: 0, y: rootY, width: ROOT_WIDTH, height: nodeHeight, data: null, hasChildren: l1Ids.length > 0, hasHiddenChildren: false };
|
|
111
112
|
const rootEdges = l1Ids
|
|
112
113
|
.filter((id) => !hiddenSubtrees.has(id))
|
|
113
114
|
.map((id) => ({ source: 'root', target: id }));
|
package/dist/goals/GoalNode.d.ts
CHANGED
package/dist/goals/GoalNode.js
CHANGED
|
@@ -13,7 +13,7 @@ function impactSign(f) {
|
|
|
13
13
|
return 'negative';
|
|
14
14
|
}
|
|
15
15
|
const GoalNode = memo(({ data }) => {
|
|
16
|
-
const { goal, theme, readOnly, isDropTarget, hasHiddenChildren, isCollapsed } = data;
|
|
16
|
+
const { goal, theme, readOnly, isDropTarget, hasChildren, hasHiddenChildren, isCollapsed } = data;
|
|
17
17
|
const [openPanel, setOpenPanel] = useState(null);
|
|
18
18
|
const positive = (goal.factors ?? []).filter((f) => impactSign(f) !== 'negative');
|
|
19
19
|
const negative = (goal.factors ?? []).filter((f) => impactSign(f) !== 'positive');
|
|
@@ -83,14 +83,15 @@ const GoalNode = memo(({ data }) => {
|
|
|
83
83
|
position: 'absolute', bottom: -8, right: -8, width: 16, height: 16,
|
|
84
84
|
borderRadius: '50%', border: '1px solid #fca5a5', background: '#ffffff',
|
|
85
85
|
color: '#dc2626', fontSize: 11, lineHeight: '14px', padding: 0, cursor: 'pointer',
|
|
86
|
-
}, children: "\u00D7" })), hasHiddenChildren && (_jsx("button", { type: "button", "aria-label": isCollapsed ? 'Expand branch' : 'Collapse branch', title: isCollapsed ? 'Expand branch' : 'Collapse branch', onClick: (e) => {
|
|
86
|
+
}, children: "\u00D7" })), (hasChildren || hasHiddenChildren) && (_jsx("button", { type: "button", "aria-label": isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch', title: isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch', onClick: (e) => {
|
|
87
87
|
e.stopPropagation();
|
|
88
88
|
data.onToggleCollapse(goal.id);
|
|
89
89
|
}, style: {
|
|
90
90
|
position: 'absolute', top: '50%', right: -8, transform: 'translateY(-50%)',
|
|
91
91
|
width: 16, height: 16, borderRadius: '50%', border: `1px solid ${theme.cardBorderColor}`,
|
|
92
|
-
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
93
|
-
|
|
92
|
+
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
93
|
+
cursor: 'pointer', zIndex: 10,
|
|
94
|
+
}, children: isCollapsed || hasHiddenChildren ? '+' : '−' }))] }));
|
|
94
95
|
});
|
|
95
96
|
GoalNode.displayName = 'GoalNode';
|
|
96
97
|
export default GoalNode;
|
|
@@ -122,6 +122,7 @@ function GoalTreeViewInner({ tree, layout: layoutProp, layoutOptions, theme: the
|
|
|
122
122
|
theme,
|
|
123
123
|
readOnly,
|
|
124
124
|
isDropTarget: hoveredId === n.id,
|
|
125
|
+
hasChildren: n.hasChildren,
|
|
125
126
|
hasHiddenChildren: n.hasHiddenChildren,
|
|
126
127
|
isCollapsed: collapsedIds.has(n.id),
|
|
127
128
|
onAddChild,
|
package/dist/goals/layout.js
CHANGED
|
@@ -100,14 +100,16 @@ export function layoutGoalTree(tree, options = {}) {
|
|
|
100
100
|
return { top: 0, bottom: 0 };
|
|
101
101
|
placed.add(id);
|
|
102
102
|
const col = levelToCol.get(goal.level) ?? 0;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
103
|
+
const allChildIds = children.get(id) ?? [];
|
|
104
|
+
const childIds = allChildIds.filter(c => !hiddenSubtrees.has(c) && !placed.has(c));
|
|
105
|
+
const isCollapsedRoot = hiddenSet.has(id) && allChildIds.length > 0;
|
|
106
|
+
const hasChildren = allChildIds.length > 0;
|
|
107
|
+
const hasHiddenChildren = childIds.length < allChildIds.length;
|
|
106
108
|
if (childIds.length === 0) {
|
|
107
109
|
// Leaf: place at current column Y
|
|
108
110
|
const y = getNextY(col);
|
|
109
111
|
advanceY(col, nodeHeight + nodeSep);
|
|
110
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasHiddenChildren });
|
|
112
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasChildren, hasHiddenChildren });
|
|
111
113
|
return { top: y, bottom: y + nodeHeight };
|
|
112
114
|
}
|
|
113
115
|
// Place children first to get their span
|
|
@@ -122,7 +124,7 @@ export function layoutGoalTree(tree, options = {}) {
|
|
|
122
124
|
const curColY = getNextY(col);
|
|
123
125
|
const finalY = Math.max(parentY, curColY);
|
|
124
126
|
advanceY(col, finalY - curColY + nodeHeight + nodeSep);
|
|
125
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasHiddenChildren });
|
|
127
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasChildren, hasHiddenChildren });
|
|
126
128
|
for (const cid of childIds) {
|
|
127
129
|
edges.push({ source: id, target: cid });
|
|
128
130
|
}
|
package/dist/goals/types.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface LaidOutNode {
|
|
|
55
55
|
height: number;
|
|
56
56
|
data: Goal;
|
|
57
57
|
isCollapsedRoot: boolean;
|
|
58
|
+
/** True when this node has at least one child in the source tree (visible or hidden). */
|
|
59
|
+
hasChildren: boolean;
|
|
58
60
|
hasHiddenChildren: boolean;
|
|
59
61
|
}
|
|
60
62
|
export interface LaidOutEdge {
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ export interface CapabilityCardNodeData {
|
|
|
17
17
|
maturityColours: Record<number, string>;
|
|
18
18
|
readOnly: boolean;
|
|
19
19
|
isDropTarget: boolean;
|
|
20
|
+
hasChildren: boolean;
|
|
20
21
|
hasHiddenChildren: boolean;
|
|
21
22
|
isCollapsed: boolean;
|
|
22
23
|
onAddChild: (parentId: number) => void;
|
|
@@ -25,7 +26,7 @@ export interface CapabilityCardNodeData {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const CapabilityCardNode = memo(({ data }: { data: CapabilityCardNodeData }) => {
|
|
28
|
-
const { capability, theme, maturityColours, readOnly, isDropTarget, hasHiddenChildren, isCollapsed } = data;
|
|
29
|
+
const { capability, theme, maturityColours, readOnly, isDropTarget, hasChildren, hasHiddenChildren, isCollapsed } = data;
|
|
29
30
|
const maturity = latestMaturityLevel(capability);
|
|
30
31
|
const dotColour = maturity !== undefined ? maturityColours[maturity] : undefined;
|
|
31
32
|
|
|
@@ -119,11 +120,11 @@ const CapabilityCardNode = memo(({ data }: { data: CapabilityCardNodeData }) =>
|
|
|
119
120
|
</button>
|
|
120
121
|
)}
|
|
121
122
|
|
|
122
|
-
{hasHiddenChildren && (
|
|
123
|
+
{(hasChildren || hasHiddenChildren) && (
|
|
123
124
|
<button
|
|
124
125
|
type="button"
|
|
125
|
-
aria-label={isCollapsed ? 'Expand branch' : 'Collapse branch'}
|
|
126
|
-
title={isCollapsed ? 'Expand branch' : 'Collapse branch'}
|
|
126
|
+
aria-label={isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch'}
|
|
127
|
+
title={isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch'}
|
|
127
128
|
onClick={(e) => {
|
|
128
129
|
e.stopPropagation();
|
|
129
130
|
data.onToggleCollapse(capability.id);
|
|
@@ -131,10 +132,11 @@ const CapabilityCardNode = memo(({ data }: { data: CapabilityCardNodeData }) =>
|
|
|
131
132
|
style={{
|
|
132
133
|
position: 'absolute', top: '50%', right: -8, transform: 'translateY(-50%)',
|
|
133
134
|
width: 16, height: 16, borderRadius: '50%', border: `1px solid ${theme.cardBorderColor}`,
|
|
134
|
-
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
135
|
+
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
136
|
+
cursor: 'pointer', zIndex: 10,
|
|
135
137
|
}}
|
|
136
138
|
>
|
|
137
|
-
{isCollapsed ? '+' : '−'}
|
|
139
|
+
{isCollapsed || hasHiddenChildren ? '+' : '−'}
|
|
138
140
|
</button>
|
|
139
141
|
)}
|
|
140
142
|
</div>
|
|
@@ -79,12 +79,13 @@ export function layoutCapabilityMap(map: CapabilityMap, options: LayoutOptions =
|
|
|
79
79
|
const cap = byId.get(id)!;
|
|
80
80
|
const allChildIds = children.get(id) ?? [];
|
|
81
81
|
const childIds = allChildIds.filter((c) => !hiddenSubtrees.has(c));
|
|
82
|
+
const hasChildren = allChildIds.length > 0;
|
|
82
83
|
const hasHiddenChildren = childIds.length < allChildIds.length;
|
|
83
84
|
|
|
84
85
|
if (childIds.length === 0) {
|
|
85
86
|
const y = getNextY(col);
|
|
86
87
|
advanceY(col, nodeHeight + nodeSep);
|
|
87
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: cap, hasHiddenChildren });
|
|
88
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: cap, hasChildren, hasHiddenChildren });
|
|
88
89
|
return { top: y, bottom: y + nodeHeight };
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -96,7 +97,7 @@ export function layoutCapabilityMap(map: CapabilityMap, options: LayoutOptions =
|
|
|
96
97
|
const finalY = Math.max(parentY, curColY);
|
|
97
98
|
advanceY(col, finalY - curColY + nodeHeight + nodeSep);
|
|
98
99
|
|
|
99
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: cap, hasHiddenChildren });
|
|
100
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: cap, hasChildren, hasHiddenChildren });
|
|
100
101
|
for (const cid of childIds) edges.push({ source: id, target: cid });
|
|
101
102
|
return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + nodeHeight, spanBottom) };
|
|
102
103
|
}
|
|
@@ -108,7 +109,7 @@ export function layoutCapabilityMap(map: CapabilityMap, options: LayoutOptions =
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
if (nodes.length === 0) {
|
|
111
|
-
const rootNode: LaidOutNode = { id: 0, x: 0, y: 0, width: ROOT_WIDTH, height: nodeHeight, data: null, hasHiddenChildren: false };
|
|
112
|
+
const rootNode: LaidOutNode = { id: 0, x: 0, y: 0, width: ROOT_WIDTH, height: nodeHeight, data: null, hasChildren: false, hasHiddenChildren: false };
|
|
112
113
|
return { rootNode, nodes: [], edges: [], bounds: { x: 0, y: 0, width: ROOT_WIDTH, height: nodeHeight } };
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -122,7 +123,7 @@ export function layoutCapabilityMap(map: CapabilityMap, options: LayoutOptions =
|
|
|
122
123
|
const rootTop = Math.min(...l1Nodes.map((n) => n.y));
|
|
123
124
|
const rootBottom = Math.max(...l1Nodes.map((n) => n.y + n.height));
|
|
124
125
|
const rootY = rootTop + (rootBottom - rootTop) / 2 - nodeHeight / 2;
|
|
125
|
-
const rootNode: LaidOutNode = { id: 0, x: 0, y: rootY, width: ROOT_WIDTH, height: nodeHeight, data: null, hasHiddenChildren: false };
|
|
126
|
+
const rootNode: LaidOutNode = { id: 0, x: 0, y: rootY, width: ROOT_WIDTH, height: nodeHeight, data: null, hasChildren: l1Ids.length > 0, hasHiddenChildren: false };
|
|
126
127
|
|
|
127
128
|
const rootEdges: LaidOutEdge[] = l1Ids
|
|
128
129
|
.filter((id) => !hiddenSubtrees.has(id))
|
package/src/goals/GoalNode.tsx
CHANGED
|
@@ -19,6 +19,7 @@ export interface GoalNodeData {
|
|
|
19
19
|
theme: Required<ThemeTokens>;
|
|
20
20
|
readOnly: boolean;
|
|
21
21
|
isDropTarget: boolean;
|
|
22
|
+
hasChildren: boolean;
|
|
22
23
|
hasHiddenChildren: boolean;
|
|
23
24
|
isCollapsed: boolean;
|
|
24
25
|
onAddChild: (parentId: number, parentLevel: number) => void;
|
|
@@ -28,7 +29,7 @@ export interface GoalNodeData {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const GoalNode = memo(({ data }: { data: GoalNodeData }) => {
|
|
31
|
-
const { goal, theme, readOnly, isDropTarget, hasHiddenChildren, isCollapsed } = data;
|
|
32
|
+
const { goal, theme, readOnly, isDropTarget, hasChildren, hasHiddenChildren, isCollapsed } = data;
|
|
32
33
|
const [openPanel, setOpenPanel] = useState<'positive' | 'negative' | null>(null);
|
|
33
34
|
|
|
34
35
|
const positive = (goal.factors ?? []).filter((f) => impactSign(f) !== 'negative');
|
|
@@ -192,11 +193,11 @@ const GoalNode = memo(({ data }: { data: GoalNodeData }) => {
|
|
|
192
193
|
</button>
|
|
193
194
|
)}
|
|
194
195
|
|
|
195
|
-
{hasHiddenChildren && (
|
|
196
|
+
{(hasChildren || hasHiddenChildren) && (
|
|
196
197
|
<button
|
|
197
198
|
type="button"
|
|
198
|
-
aria-label={isCollapsed ? 'Expand branch' : 'Collapse branch'}
|
|
199
|
-
title={isCollapsed ? 'Expand branch' : 'Collapse branch'}
|
|
199
|
+
aria-label={isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch'}
|
|
200
|
+
title={isCollapsed || hasHiddenChildren ? 'Expand branch' : 'Collapse branch'}
|
|
200
201
|
onClick={(e) => {
|
|
201
202
|
e.stopPropagation();
|
|
202
203
|
data.onToggleCollapse(goal.id);
|
|
@@ -204,10 +205,11 @@ const GoalNode = memo(({ data }: { data: GoalNodeData }) => {
|
|
|
204
205
|
style={{
|
|
205
206
|
position: 'absolute', top: '50%', right: -8, transform: 'translateY(-50%)',
|
|
206
207
|
width: 16, height: 16, borderRadius: '50%', border: `1px solid ${theme.cardBorderColor}`,
|
|
207
|
-
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
208
|
+
background: '#ffffff', color: theme.cardBorderColor, fontSize: 11, lineHeight: '14px', padding: 0,
|
|
209
|
+
cursor: 'pointer', zIndex: 10,
|
|
208
210
|
}}
|
|
209
211
|
>
|
|
210
|
-
{isCollapsed ? '+' : '−'}
|
|
212
|
+
{isCollapsed || hasHiddenChildren ? '+' : '−'}
|
|
211
213
|
</button>
|
|
212
214
|
)}
|
|
213
215
|
</div>
|
package/src/goals/layout.ts
CHANGED
|
@@ -114,15 +114,17 @@ export function layoutGoalTree(tree: GoalTree, options: LayoutOptions = {}): Goa
|
|
|
114
114
|
placed.add(id);
|
|
115
115
|
|
|
116
116
|
const col = levelToCol.get(goal.level) ?? 0;
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
117
|
+
const allChildIds = children.get(id) ?? [];
|
|
118
|
+
const childIds = allChildIds.filter(c => !hiddenSubtrees.has(c) && !placed.has(c));
|
|
119
|
+
const isCollapsedRoot = hiddenSet.has(id) && allChildIds.length > 0;
|
|
120
|
+
const hasChildren = allChildIds.length > 0;
|
|
121
|
+
const hasHiddenChildren = childIds.length < allChildIds.length;
|
|
120
122
|
|
|
121
123
|
if (childIds.length === 0) {
|
|
122
124
|
// Leaf: place at current column Y
|
|
123
125
|
const y = getNextY(col);
|
|
124
126
|
advanceY(col, nodeHeight + nodeSep);
|
|
125
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasHiddenChildren });
|
|
127
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasChildren, hasHiddenChildren });
|
|
126
128
|
return { top: y, bottom: y + nodeHeight };
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -141,7 +143,7 @@ export function layoutGoalTree(tree: GoalTree, options: LayoutOptions = {}): Goa
|
|
|
141
143
|
const finalY = Math.max(parentY, curColY);
|
|
142
144
|
advanceY(col, finalY - curColY + nodeHeight + nodeSep);
|
|
143
145
|
|
|
144
|
-
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasHiddenChildren });
|
|
146
|
+
nodes.push({ id, x: col * (nodeWidth + rankSep), y: finalY, width: nodeWidth, height: nodeHeight, data: goal, isCollapsedRoot, hasChildren, hasHiddenChildren });
|
|
145
147
|
|
|
146
148
|
for (const cid of childIds) {
|
|
147
149
|
edges.push({ source: id, target: cid });
|
package/src/goals/types.ts
CHANGED