@preact/signals-devtools-ui 0.2.0 → 0.3.0
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/CHANGELOG.md +13 -0
- package/dist/devtools-ui.js +1 -1
- package/dist/devtools-ui.js.map +1 -1
- package/dist/devtools-ui.min.js +1 -1
- package/dist/devtools-ui.min.js.map +1 -1
- package/dist/devtools-ui.mjs +1 -1
- package/dist/devtools-ui.mjs.map +1 -1
- package/dist/devtools-ui.module.js +1 -1
- package/dist/devtools-ui.module.js.map +1 -1
- package/dist/styles.css +4 -1
- package/package.json +3 -2
- package/src/DevToolsPanel.tsx +1 -1
- package/src/components/Graph.tsx +204 -38
- package/src/context.ts +3 -0
- package/src/styles.css +4 -1
- package/src/types.ts +1 -0
package/dist/styles.css
CHANGED
|
@@ -153,7 +153,6 @@ body {
|
|
|
153
153
|
border-radius: 8px;
|
|
154
154
|
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
155
155
|
z-index: 1000;
|
|
156
|
-
max-height: 400px;
|
|
157
156
|
overflow-y: auto;
|
|
158
157
|
}
|
|
159
158
|
|
|
@@ -346,6 +345,10 @@ body {
|
|
|
346
345
|
margin-bottom: 4px;
|
|
347
346
|
}
|
|
348
347
|
|
|
348
|
+
.signals-devtools {
|
|
349
|
+
position: relative;
|
|
350
|
+
}
|
|
351
|
+
|
|
349
352
|
.update-count {
|
|
350
353
|
display: inline-block;
|
|
351
354
|
padding: 0 6px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preact/signals-devtools-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "DevTools UI components for @preact/signals",
|
|
6
6
|
"keywords": [
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"README.md"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@preact/signals-devtools-adapter": "0.
|
|
47
|
+
"@preact/signals-devtools-adapter": "0.3.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^20.0.0",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@preact/signals": "2.6.1"
|
|
58
58
|
},
|
|
59
59
|
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
60
61
|
"provenance": true
|
|
61
62
|
},
|
|
62
63
|
"scripts": {
|
package/src/DevToolsPanel.tsx
CHANGED
package/src/components/Graph.tsx
CHANGED
|
@@ -24,7 +24,6 @@ export function GraphVisualization() {
|
|
|
24
24
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
25
25
|
const exportMenuRef = useRef<HTMLDivElement>(null);
|
|
26
26
|
|
|
27
|
-
// Pan and zoom state using signals
|
|
28
27
|
const panOffset = useSignal({ x: 0, y: 0 });
|
|
29
28
|
const zoom = useSignal(1);
|
|
30
29
|
const isPanning = useSignal(false);
|
|
@@ -51,7 +50,150 @@ export function GraphVisualization() {
|
|
|
51
50
|
};
|
|
52
51
|
}, []);
|
|
53
52
|
|
|
54
|
-
//
|
|
53
|
+
// Improved topological sort with proper layering
|
|
54
|
+
const computeNodeLayers = (
|
|
55
|
+
nodes: Map<string, GraphNode>,
|
|
56
|
+
links: GraphLink[]
|
|
57
|
+
): Map<string, number> => {
|
|
58
|
+
const layers = new Map<string, number>();
|
|
59
|
+
const adjacency = new Map<string, Set<string>>();
|
|
60
|
+
const inDegree = new Map<string, number>();
|
|
61
|
+
|
|
62
|
+
// Initialize adjacency list and in-degrees
|
|
63
|
+
nodes.forEach((_, id) => {
|
|
64
|
+
adjacency.set(id, new Set());
|
|
65
|
+
inDegree.set(id, 0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Build adjacency list and calculate in-degrees
|
|
69
|
+
links.forEach(link => {
|
|
70
|
+
adjacency.get(link.source)?.add(link.target);
|
|
71
|
+
inDegree.set(link.target, (inDegree.get(link.target) || 0) + 1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// BFS-based layering (Kahn's algorithm with layers)
|
|
75
|
+
const queue: string[] = [];
|
|
76
|
+
|
|
77
|
+
// Start with nodes that have no dependencies (in-degree = 0)
|
|
78
|
+
nodes.forEach((_, id) => {
|
|
79
|
+
if (inDegree.get(id) === 0) {
|
|
80
|
+
queue.push(id);
|
|
81
|
+
layers.set(id, 0);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
while (queue.length > 0) {
|
|
86
|
+
const nodeId = queue.shift()!;
|
|
87
|
+
const currentLayer = layers.get(nodeId)!;
|
|
88
|
+
|
|
89
|
+
// Process all nodes that depend on this node
|
|
90
|
+
adjacency.get(nodeId)?.forEach(targetId => {
|
|
91
|
+
// Update target's layer to be at least one more than current
|
|
92
|
+
const targetLayer = layers.get(targetId) ?? 0;
|
|
93
|
+
layers.set(targetId, Math.max(targetLayer, currentLayer + 1));
|
|
94
|
+
|
|
95
|
+
// Decrease in-degree and add to queue if all dependencies processed
|
|
96
|
+
const newInDegree = (inDegree.get(targetId) || 0) - 1;
|
|
97
|
+
inDegree.set(targetId, newInDegree);
|
|
98
|
+
|
|
99
|
+
if (newInDegree === 0) {
|
|
100
|
+
queue.push(targetId);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return layers;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Reduce edge crossings within layers using barycenter heuristic
|
|
109
|
+
const minimizeCrossings = (
|
|
110
|
+
nodesByLayer: Map<number, GraphNode[]>,
|
|
111
|
+
links: GraphLink[]
|
|
112
|
+
): void => {
|
|
113
|
+
const layers = Array.from(nodesByLayer.keys()).sort((a, b) => a - b);
|
|
114
|
+
|
|
115
|
+
// Build adjacency maps for quick lookup
|
|
116
|
+
const targets = new Map<string, string[]>();
|
|
117
|
+
const sources = new Map<string, string[]>();
|
|
118
|
+
|
|
119
|
+
links.forEach(link => {
|
|
120
|
+
if (!targets.has(link.source)) targets.set(link.source, []);
|
|
121
|
+
if (!sources.has(link.target)) sources.set(link.target, []);
|
|
122
|
+
targets.get(link.source)!.push(link.target);
|
|
123
|
+
sources.get(link.target)!.push(link.source);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Create position maps for quick lookup
|
|
127
|
+
const nodePositions = new Map<string, number>();
|
|
128
|
+
|
|
129
|
+
// Multiple passes to reduce crossings
|
|
130
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
131
|
+
// Forward pass: order based on predecessors
|
|
132
|
+
for (let i = 0; i < layers.length; i++) {
|
|
133
|
+
const layer = layers[i];
|
|
134
|
+
const nodes = nodesByLayer.get(layer)!;
|
|
135
|
+
|
|
136
|
+
// Update position map for current layer
|
|
137
|
+
nodes.forEach((node, idx) => {
|
|
138
|
+
nodePositions.set(node.id, idx);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (i === 0) continue; // Skip first layer
|
|
142
|
+
|
|
143
|
+
// Calculate barycenter for each node based on predecessors
|
|
144
|
+
const barycenters = nodes.map(node => {
|
|
145
|
+
const preds = sources.get(node.id) || [];
|
|
146
|
+
if (preds.length === 0) return 0;
|
|
147
|
+
|
|
148
|
+
const sum = preds.reduce((acc, predId) => {
|
|
149
|
+
return acc + (nodePositions.get(predId) ?? 0);
|
|
150
|
+
}, 0);
|
|
151
|
+
return sum / preds.length;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Sort nodes by barycenter
|
|
155
|
+
const sorted = nodes
|
|
156
|
+
.map((node, idx) => ({ node, barycenter: barycenters[idx] }))
|
|
157
|
+
.sort((a, b) => a.barycenter - b.barycenter)
|
|
158
|
+
.map(item => item.node);
|
|
159
|
+
|
|
160
|
+
nodesByLayer.set(layer, sorted);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Backward pass: order based on successors
|
|
164
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
165
|
+
const layer = layers[i];
|
|
166
|
+
const nodes = nodesByLayer.get(layer)!;
|
|
167
|
+
|
|
168
|
+
// Update position map for current layer
|
|
169
|
+
nodes.forEach((node, idx) => {
|
|
170
|
+
nodePositions.set(node.id, idx);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (i === layers.length - 1) continue; // Skip last layer
|
|
174
|
+
|
|
175
|
+
// Calculate barycenter for each node based on successors
|
|
176
|
+
const barycenters = nodes.map(node => {
|
|
177
|
+
const succs = targets.get(node.id) || [];
|
|
178
|
+
if (succs.length === 0) return 0;
|
|
179
|
+
|
|
180
|
+
const sum = succs.reduce((acc, succId) => {
|
|
181
|
+
return acc + (nodePositions.get(succId) ?? 0);
|
|
182
|
+
}, 0);
|
|
183
|
+
return sum / succs.length;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Sort nodes by barycenter
|
|
187
|
+
const sorted = nodes
|
|
188
|
+
.map((node, idx) => ({ node, barycenter: barycenters[idx] }))
|
|
189
|
+
.sort((a, b) => a.barycenter - b.barycenter)
|
|
190
|
+
.map(item => item.node);
|
|
191
|
+
|
|
192
|
+
nodesByLayer.set(layer, sorted);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
55
197
|
const graphData = useComputed<GraphData>(() => {
|
|
56
198
|
const rawUpdates = updates.value;
|
|
57
199
|
const disposed = disposedSignalIds.value;
|
|
@@ -62,19 +204,15 @@ export function GraphVisualization() {
|
|
|
62
204
|
const nodes = new Map<string, GraphNode>();
|
|
63
205
|
const links = new Map<string, GraphLink>();
|
|
64
206
|
|
|
65
|
-
// Process updates to build graph structure
|
|
66
207
|
const signalUpdates = rawUpdates.filter(
|
|
67
208
|
update => update.type !== "divider"
|
|
68
209
|
) as SignalUpdate[];
|
|
69
210
|
|
|
70
211
|
for (const update of signalUpdates) {
|
|
71
212
|
if (!update.signalId) continue;
|
|
72
|
-
|
|
73
|
-
// Skip disposed signals unless showDisposed is enabled
|
|
74
213
|
if (!showDisposed && disposed.has(update.signalId)) continue;
|
|
75
214
|
|
|
76
215
|
const type: "signal" | "computed" | "effect" = update.signalType;
|
|
77
|
-
const currentDepth = update.depth || 0;
|
|
78
216
|
|
|
79
217
|
if (!nodes.has(update.signalId)) {
|
|
80
218
|
nodes.set(update.signalId, {
|
|
@@ -83,12 +221,35 @@ export function GraphVisualization() {
|
|
|
83
221
|
type,
|
|
84
222
|
x: 0,
|
|
85
223
|
y: 0,
|
|
86
|
-
depth:
|
|
224
|
+
depth: 0, // Will be recalculated
|
|
87
225
|
});
|
|
88
226
|
}
|
|
89
227
|
|
|
90
|
-
if (update.
|
|
91
|
-
|
|
228
|
+
if (update.allDependencies && update.allDependencies.length > 0) {
|
|
229
|
+
for (const dep of update.allDependencies) {
|
|
230
|
+
const sourceDisposed = !showDisposed && disposed.has(dep.id);
|
|
231
|
+
if (sourceDisposed) continue;
|
|
232
|
+
|
|
233
|
+
if (!nodes.has(dep.id)) {
|
|
234
|
+
nodes.set(dep.id, {
|
|
235
|
+
id: dep.id,
|
|
236
|
+
name: dep.name,
|
|
237
|
+
type: dep.type,
|
|
238
|
+
x: 0,
|
|
239
|
+
y: 0,
|
|
240
|
+
depth: 0,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const linkKey = `${dep.id}->${update.signalId}`;
|
|
245
|
+
if (!links.has(linkKey)) {
|
|
246
|
+
links.set(linkKey, {
|
|
247
|
+
source: dep.id,
|
|
248
|
+
target: update.signalId,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else if (update.subscribedTo) {
|
|
92
253
|
const sourceDisposed =
|
|
93
254
|
!showDisposed && disposed.has(update.subscribedTo);
|
|
94
255
|
if (sourceDisposed) continue;
|
|
@@ -103,41 +264,50 @@ export function GraphVisualization() {
|
|
|
103
264
|
}
|
|
104
265
|
}
|
|
105
266
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const startX = 100;
|
|
111
|
-
const startY = 80;
|
|
267
|
+
const allLinks = Array.from(links.values());
|
|
268
|
+
|
|
269
|
+
// Compute proper layers using topological sort
|
|
270
|
+
const nodeLayers = computeNodeLayers(nodes, allLinks);
|
|
112
271
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
272
|
+
// Update node depths based on computed layers
|
|
273
|
+
nodes.forEach((node, id) => {
|
|
274
|
+
node.depth = nodeLayers.get(id) ?? 0;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Group nodes by layer
|
|
278
|
+
const nodesByLayer = new Map<number, GraphNode[]>();
|
|
279
|
+
nodes.forEach(node => {
|
|
280
|
+
if (!nodesByLayer.has(node.depth)) {
|
|
281
|
+
nodesByLayer.set(node.depth, []);
|
|
118
282
|
}
|
|
119
|
-
|
|
283
|
+
nodesByLayer.get(node.depth)!.push(node);
|
|
120
284
|
});
|
|
121
285
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
286
|
+
// Minimize edge crossings
|
|
287
|
+
minimizeCrossings(nodesByLayer, allLinks);
|
|
288
|
+
|
|
289
|
+
// Layout nodes with proper spacing
|
|
290
|
+
const nodeSpacing = 120;
|
|
291
|
+
const layerSpacing = 250;
|
|
292
|
+
const startX = 100;
|
|
293
|
+
const startY = 80;
|
|
294
|
+
|
|
295
|
+
nodesByLayer.forEach((layerNodes, layer) => {
|
|
296
|
+
const layerHeight = (layerNodes.length - 1) * nodeSpacing;
|
|
297
|
+
const layerStartY = startY - layerHeight / 2;
|
|
127
298
|
|
|
128
|
-
|
|
129
|
-
node.x = startX +
|
|
130
|
-
node.y =
|
|
299
|
+
layerNodes.forEach((node, index) => {
|
|
300
|
+
node.x = startX + layer * layerSpacing;
|
|
301
|
+
node.y = layerStartY + index * nodeSpacing + nodesByLayer.size * 50;
|
|
131
302
|
});
|
|
132
303
|
});
|
|
133
304
|
|
|
134
305
|
return {
|
|
135
|
-
nodes:
|
|
136
|
-
links:
|
|
306
|
+
nodes: Array.from(nodes.values()),
|
|
307
|
+
links: allLinks,
|
|
137
308
|
};
|
|
138
309
|
});
|
|
139
310
|
|
|
140
|
-
// Mouse event handlers for panning
|
|
141
311
|
const handleMouseDown = (e: MouseEvent) => {
|
|
142
312
|
if (e.button !== 0) return;
|
|
143
313
|
isPanning.value = true;
|
|
@@ -169,7 +339,6 @@ export function GraphVisualization() {
|
|
|
169
339
|
const mouseX = e.clientX - rect.left;
|
|
170
340
|
const mouseY = e.clientY - rect.top;
|
|
171
341
|
|
|
172
|
-
// Smoother zoom: smaller delta for less aggressive scrolling
|
|
173
342
|
const delta = e.deltaY > 0 ? 0.96 : 1.04;
|
|
174
343
|
const newZoom = Math.min(Math.max(0.1, zoom.value * delta), 5);
|
|
175
344
|
|
|
@@ -194,16 +363,14 @@ export function GraphVisualization() {
|
|
|
194
363
|
const mermaidIdPattern = /[^a-zA-Z0-9]/g;
|
|
195
364
|
const computeMermaidId = (id: string) => id.replace(mermaidIdPattern, "_");
|
|
196
365
|
|
|
197
|
-
// Calculate node radius based on name length
|
|
198
366
|
const getNodeRadius = (node: GraphNode) => {
|
|
199
367
|
const baseRadius = 30;
|
|
200
|
-
const charWidth = 6.5;
|
|
368
|
+
const charWidth = 6.5;
|
|
201
369
|
const padding = 16;
|
|
202
370
|
const textWidth = node.name.length * charWidth + padding;
|
|
203
371
|
return Math.max(baseRadius, Math.min(textWidth / 2, 70));
|
|
204
372
|
};
|
|
205
373
|
|
|
206
|
-
// Handle node hover for tooltip
|
|
207
374
|
const handleNodeMouseEnter = (node: GraphNode, e: MouseEvent) => {
|
|
208
375
|
hoveredNode.value = node;
|
|
209
376
|
const container = containerRef.current;
|
|
@@ -344,7 +511,7 @@ export function GraphVisualization() {
|
|
|
344
511
|
const targetRadius = getNodeRadius(targetNode);
|
|
345
512
|
const sourceX = sourceNode.x + sourceRadius;
|
|
346
513
|
const sourceY = sourceNode.y;
|
|
347
|
-
const targetX = targetNode.x - targetRadius - 8;
|
|
514
|
+
const targetX = targetNode.x - targetRadius - 8;
|
|
348
515
|
const targetY = targetNode.y;
|
|
349
516
|
|
|
350
517
|
const midX = sourceX + (targetX - sourceX) * 0.5;
|
|
@@ -365,7 +532,6 @@ export function GraphVisualization() {
|
|
|
365
532
|
<g className="nodes">
|
|
366
533
|
{graphData.value.nodes.map(node => {
|
|
367
534
|
const radius = getNodeRadius(node);
|
|
368
|
-
// Calculate max chars based on radius
|
|
369
535
|
const maxChars = Math.floor((radius * 2 - 16) / 6.5);
|
|
370
536
|
const displayName =
|
|
371
537
|
node.name.length > maxChars
|
package/src/context.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ConnectionStatusType,
|
|
6
6
|
Settings,
|
|
7
7
|
SignalDisposed,
|
|
8
|
+
DependencyInfo,
|
|
8
9
|
} from "@preact/signals-devtools-adapter";
|
|
9
10
|
|
|
10
11
|
export interface DevToolsContext {
|
|
@@ -74,6 +75,8 @@ export interface SignalUpdate {
|
|
|
74
75
|
receivedAt: number;
|
|
75
76
|
depth?: number;
|
|
76
77
|
subscribedTo?: string;
|
|
78
|
+
/** All dependencies this computed/effect currently depends on (with rich info) */
|
|
79
|
+
allDependencies?: DependencyInfo[];
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export type Divider = { type: "divider" };
|
package/src/styles.css
CHANGED
|
@@ -153,7 +153,6 @@ body {
|
|
|
153
153
|
border-radius: 8px;
|
|
154
154
|
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
155
155
|
z-index: 1000;
|
|
156
|
-
max-height: 400px;
|
|
157
156
|
overflow-y: auto;
|
|
158
157
|
}
|
|
159
158
|
|
|
@@ -346,6 +345,10 @@ body {
|
|
|
346
345
|
margin-bottom: 4px;
|
|
347
346
|
}
|
|
348
347
|
|
|
348
|
+
.signals-devtools {
|
|
349
|
+
position: relative;
|
|
350
|
+
}
|
|
351
|
+
|
|
349
352
|
.update-count {
|
|
350
353
|
display: inline-block;
|
|
351
354
|
padding: 0 6px;
|