@preact/signals-devtools-ui 0.2.0 → 0.4.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 +24 -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 +66 -30
- package/package.json +4 -3
- package/src/DevToolsPanel.tsx +1 -1
- package/src/components/Graph.tsx +216 -39
- package/src/components/UpdateItem.tsx +5 -2
- package/src/context.ts +5 -2
- package/src/styles.css +66 -30
- package/src/types.ts +2 -1
package/dist/styles.css
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
body {
|
|
10
|
-
font-family:
|
|
10
|
+
font-family:
|
|
11
|
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
11
12
|
font-size: 13px;
|
|
12
13
|
line-height: 1.4;
|
|
13
14
|
color: #333;
|
|
@@ -54,10 +55,10 @@ body {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
.divider {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 2px;
|
|
60
|
+
background: #e0e0e0;
|
|
61
|
+
margin: 8px 0;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
.status-indicator {
|
|
@@ -86,8 +87,13 @@ body {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
@keyframes pulse {
|
|
89
|
-
0%,
|
|
90
|
-
|
|
90
|
+
0%,
|
|
91
|
+
100% {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
}
|
|
94
|
+
50% {
|
|
95
|
+
opacity: 0.5;
|
|
96
|
+
}
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
.header-controls {
|
|
@@ -151,9 +157,8 @@ body {
|
|
|
151
157
|
background: white;
|
|
152
158
|
border: 1px solid #d0d0d0;
|
|
153
159
|
border-radius: 8px;
|
|
154
|
-
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
160
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
155
161
|
z-index: 1000;
|
|
156
|
-
max-height: 400px;
|
|
157
162
|
overflow-y: auto;
|
|
158
163
|
}
|
|
159
164
|
|
|
@@ -315,16 +320,40 @@ body {
|
|
|
315
320
|
border-left: 4px solid #ff9800;
|
|
316
321
|
}
|
|
317
322
|
|
|
323
|
+
.update-item.component {
|
|
324
|
+
border-left: 4px solid #4caf50;
|
|
325
|
+
background-color: #f8fff8;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.update-type-badge {
|
|
329
|
+
font-size: 10px;
|
|
330
|
+
padding: 2px 6px;
|
|
331
|
+
border-radius: 3px;
|
|
332
|
+
background: #e0e0e0;
|
|
333
|
+
color: #666;
|
|
334
|
+
margin-left: 8px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.update-item.component .update-type-badge {
|
|
338
|
+
background: #e8f5e9;
|
|
339
|
+
color: #2e7d32;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.update-item.effect .update-type-badge {
|
|
343
|
+
background: #fff3e0;
|
|
344
|
+
color: #e65100;
|
|
345
|
+
}
|
|
346
|
+
|
|
318
347
|
.component-name-header {
|
|
319
|
-
|
|
348
|
+
margin-right: 8px;
|
|
320
349
|
}
|
|
321
350
|
|
|
322
351
|
.component-list {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
352
|
+
list-style: none;
|
|
353
|
+
display: flex;
|
|
354
|
+
padding: 0;
|
|
355
|
+
margin: 0;
|
|
356
|
+
font-family: monospace;
|
|
328
357
|
background: #f8f9fa;
|
|
329
358
|
padding: 4px 6px;
|
|
330
359
|
border-radius: 3px;
|
|
@@ -346,6 +375,10 @@ body {
|
|
|
346
375
|
margin-bottom: 4px;
|
|
347
376
|
}
|
|
348
377
|
|
|
378
|
+
.signals-devtools {
|
|
379
|
+
position: relative;
|
|
380
|
+
}
|
|
381
|
+
|
|
349
382
|
.update-count {
|
|
350
383
|
display: inline-block;
|
|
351
384
|
padding: 0 6px;
|
|
@@ -436,8 +469,7 @@ body {
|
|
|
436
469
|
position: relative;
|
|
437
470
|
background:
|
|
438
471
|
linear-gradient(90deg, #e5e7eb 1px, transparent 1px),
|
|
439
|
-
linear-gradient(180deg, #e5e7eb 1px, transparent 1px),
|
|
440
|
-
#f8fafc;
|
|
472
|
+
linear-gradient(180deg, #e5e7eb 1px, transparent 1px), #f8fafc;
|
|
441
473
|
background-size: 40px 40px;
|
|
442
474
|
overflow: hidden;
|
|
443
475
|
user-select: none;
|
|
@@ -469,7 +501,7 @@ body {
|
|
|
469
501
|
font-size: 12px;
|
|
470
502
|
font-weight: 500;
|
|
471
503
|
cursor: pointer;
|
|
472
|
-
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
|
504
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
473
505
|
transition: all 0.15s ease;
|
|
474
506
|
}
|
|
475
507
|
|
|
@@ -477,7 +509,7 @@ body {
|
|
|
477
509
|
.graph-export-button:hover {
|
|
478
510
|
background: #fff;
|
|
479
511
|
border-color: #cbd5e1;
|
|
480
|
-
box-shadow: 0 4px 8px rgba(0,0,0,0.12);
|
|
512
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
|
481
513
|
transform: translateY(-1px);
|
|
482
514
|
}
|
|
483
515
|
|
|
@@ -499,7 +531,7 @@ body {
|
|
|
499
531
|
background: white;
|
|
500
532
|
border: 1px solid #e0e0e0;
|
|
501
533
|
border-radius: 4px;
|
|
502
|
-
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
|
534
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
503
535
|
overflow: hidden;
|
|
504
536
|
min-width: 180px;
|
|
505
537
|
}
|
|
@@ -539,21 +571,23 @@ body {
|
|
|
539
571
|
font-size: 11px;
|
|
540
572
|
font-weight: 600;
|
|
541
573
|
font-family: monospace;
|
|
542
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
574
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
543
575
|
min-width: 48px;
|
|
544
576
|
text-align: center;
|
|
545
577
|
}
|
|
546
578
|
|
|
547
579
|
.graph-node {
|
|
548
580
|
cursor: pointer;
|
|
549
|
-
transition:
|
|
581
|
+
transition:
|
|
582
|
+
transform 0.15s ease,
|
|
583
|
+
filter 0.15s ease;
|
|
550
584
|
stroke: #fff;
|
|
551
585
|
stroke-width: 2.5;
|
|
552
|
-
filter: drop-shadow(0 3px 6px rgba(0,0,0,0.15));
|
|
586
|
+
filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.15));
|
|
553
587
|
}
|
|
554
588
|
|
|
555
589
|
.graph-node-group.hovered .graph-node {
|
|
556
|
-
filter: brightness(1.15) drop-shadow(0 4px 12px rgba(0,0,0,0.25));
|
|
590
|
+
filter: brightness(1.15) drop-shadow(0 4px 12px rgba(0, 0, 0, 0.25));
|
|
557
591
|
stroke-width: 3;
|
|
558
592
|
}
|
|
559
593
|
|
|
@@ -577,7 +611,9 @@ body {
|
|
|
577
611
|
stroke: #94a3b8;
|
|
578
612
|
stroke-width: 2;
|
|
579
613
|
fill: none;
|
|
580
|
-
transition:
|
|
614
|
+
transition:
|
|
615
|
+
stroke 0.2s,
|
|
616
|
+
stroke-width 0.2s;
|
|
581
617
|
}
|
|
582
618
|
|
|
583
619
|
.graph-link.highlighted {
|
|
@@ -592,7 +628,7 @@ body {
|
|
|
592
628
|
font-size: 12px;
|
|
593
629
|
font-weight: 600;
|
|
594
630
|
pointer-events: none;
|
|
595
|
-
text-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
631
|
+
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
|
596
632
|
letter-spacing: 0.2px;
|
|
597
633
|
}
|
|
598
634
|
|
|
@@ -604,7 +640,7 @@ body {
|
|
|
604
640
|
padding: 10px 14px;
|
|
605
641
|
border-radius: 8px;
|
|
606
642
|
font-size: 12px;
|
|
607
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
|
643
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
|
608
644
|
z-index: 1000;
|
|
609
645
|
pointer-events: none;
|
|
610
646
|
transform: translateY(-50%);
|
|
@@ -676,7 +712,7 @@ body {
|
|
|
676
712
|
border-radius: 8px;
|
|
677
713
|
padding: 14px 16px;
|
|
678
714
|
font-size: 12px;
|
|
679
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
715
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
680
716
|
}
|
|
681
717
|
|
|
682
718
|
.legend-item {
|
|
@@ -694,7 +730,7 @@ body {
|
|
|
694
730
|
width: 14px;
|
|
695
731
|
height: 14px;
|
|
696
732
|
border-radius: 50%;
|
|
697
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
|
|
733
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
698
734
|
}
|
|
699
735
|
|
|
700
736
|
.component-boundary {
|
|
@@ -730,7 +766,7 @@ body {
|
|
|
730
766
|
border-radius: 4px;
|
|
731
767
|
font-size: 13px;
|
|
732
768
|
font-weight: 500;
|
|
733
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
769
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
734
770
|
z-index: 1000;
|
|
735
771
|
animation: slideInFade 0.3s ease-out;
|
|
736
772
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preact/signals-devtools-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^20.0.0",
|
|
@@ -54,9 +54,10 @@
|
|
|
54
54
|
"typescript": "~5.8.3",
|
|
55
55
|
"vite": "^7.0.0",
|
|
56
56
|
"vitest": "^4.0.17",
|
|
57
|
-
"@preact/signals": "2.
|
|
57
|
+
"@preact/signals": "2.7.0"
|
|
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,16 @@ 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
|
-
const type: "signal" | "computed" | "effect" =
|
|
77
|
-
|
|
215
|
+
const type: "signal" | "computed" | "effect" | "component" =
|
|
216
|
+
update.signalType;
|
|
78
217
|
|
|
79
218
|
if (!nodes.has(update.signalId)) {
|
|
80
219
|
nodes.set(update.signalId, {
|
|
@@ -83,12 +222,35 @@ export function GraphVisualization() {
|
|
|
83
222
|
type,
|
|
84
223
|
x: 0,
|
|
85
224
|
y: 0,
|
|
86
|
-
depth:
|
|
225
|
+
depth: 0, // Will be recalculated
|
|
87
226
|
});
|
|
88
227
|
}
|
|
89
228
|
|
|
90
|
-
if (update.
|
|
91
|
-
|
|
229
|
+
if (update.allDependencies && update.allDependencies.length > 0) {
|
|
230
|
+
for (const dep of update.allDependencies) {
|
|
231
|
+
const sourceDisposed = !showDisposed && disposed.has(dep.id);
|
|
232
|
+
if (sourceDisposed) continue;
|
|
233
|
+
|
|
234
|
+
if (!nodes.has(dep.id)) {
|
|
235
|
+
nodes.set(dep.id, {
|
|
236
|
+
id: dep.id,
|
|
237
|
+
name: dep.name,
|
|
238
|
+
type: dep.type,
|
|
239
|
+
x: 0,
|
|
240
|
+
y: 0,
|
|
241
|
+
depth: 0,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const linkKey = `${dep.id}->${update.signalId}`;
|
|
246
|
+
if (!links.has(linkKey)) {
|
|
247
|
+
links.set(linkKey, {
|
|
248
|
+
source: dep.id,
|
|
249
|
+
target: update.signalId,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else if (update.subscribedTo) {
|
|
92
254
|
const sourceDisposed =
|
|
93
255
|
!showDisposed && disposed.has(update.subscribedTo);
|
|
94
256
|
if (sourceDisposed) continue;
|
|
@@ -103,41 +265,50 @@ export function GraphVisualization() {
|
|
|
103
265
|
}
|
|
104
266
|
}
|
|
105
267
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
268
|
+
const allLinks = Array.from(links.values());
|
|
269
|
+
|
|
270
|
+
// Compute proper layers using topological sort
|
|
271
|
+
const nodeLayers = computeNodeLayers(nodes, allLinks);
|
|
272
|
+
|
|
273
|
+
// Update node depths based on computed layers
|
|
274
|
+
nodes.forEach((node, id) => {
|
|
275
|
+
node.depth = nodeLayers.get(id) ?? 0;
|
|
276
|
+
});
|
|
112
277
|
|
|
113
|
-
// Group nodes by
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
if (!
|
|
117
|
-
|
|
278
|
+
// Group nodes by layer
|
|
279
|
+
const nodesByLayer = new Map<number, GraphNode[]>();
|
|
280
|
+
nodes.forEach(node => {
|
|
281
|
+
if (!nodesByLayer.has(node.depth)) {
|
|
282
|
+
nodesByLayer.set(node.depth, []);
|
|
118
283
|
}
|
|
119
|
-
|
|
284
|
+
nodesByLayer.get(node.depth)!.push(node);
|
|
120
285
|
});
|
|
121
286
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
287
|
+
// Minimize edge crossings
|
|
288
|
+
minimizeCrossings(nodesByLayer, allLinks);
|
|
289
|
+
|
|
290
|
+
// Layout nodes with proper spacing
|
|
291
|
+
const nodeSpacing = 120;
|
|
292
|
+
const layerSpacing = 250;
|
|
293
|
+
const startX = 100;
|
|
294
|
+
const startY = 80;
|
|
127
295
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
296
|
+
nodesByLayer.forEach((layerNodes, layer) => {
|
|
297
|
+
const layerHeight = (layerNodes.length - 1) * nodeSpacing;
|
|
298
|
+
const layerStartY = startY - layerHeight / 2;
|
|
299
|
+
|
|
300
|
+
layerNodes.forEach((node, index) => {
|
|
301
|
+
node.x = startX + layer * layerSpacing;
|
|
302
|
+
node.y = layerStartY + index * nodeSpacing + nodesByLayer.size * 50;
|
|
131
303
|
});
|
|
132
304
|
});
|
|
133
305
|
|
|
134
306
|
return {
|
|
135
|
-
nodes:
|
|
136
|
-
links:
|
|
307
|
+
nodes: Array.from(nodes.values()),
|
|
308
|
+
links: allLinks,
|
|
137
309
|
};
|
|
138
310
|
});
|
|
139
311
|
|
|
140
|
-
// Mouse event handlers for panning
|
|
141
312
|
const handleMouseDown = (e: MouseEvent) => {
|
|
142
313
|
if (e.button !== 0) return;
|
|
143
314
|
isPanning.value = true;
|
|
@@ -169,7 +340,6 @@ export function GraphVisualization() {
|
|
|
169
340
|
const mouseX = e.clientX - rect.left;
|
|
170
341
|
const mouseY = e.clientY - rect.top;
|
|
171
342
|
|
|
172
|
-
// Smoother zoom: smaller delta for less aggressive scrolling
|
|
173
343
|
const delta = e.deltaY > 0 ? 0.96 : 1.04;
|
|
174
344
|
const newZoom = Math.min(Math.max(0.1, zoom.value * delta), 5);
|
|
175
345
|
|
|
@@ -194,16 +364,14 @@ export function GraphVisualization() {
|
|
|
194
364
|
const mermaidIdPattern = /[^a-zA-Z0-9]/g;
|
|
195
365
|
const computeMermaidId = (id: string) => id.replace(mermaidIdPattern, "_");
|
|
196
366
|
|
|
197
|
-
// Calculate node radius based on name length
|
|
198
367
|
const getNodeRadius = (node: GraphNode) => {
|
|
199
368
|
const baseRadius = 30;
|
|
200
|
-
const charWidth = 6.5;
|
|
369
|
+
const charWidth = 6.5;
|
|
201
370
|
const padding = 16;
|
|
202
371
|
const textWidth = node.name.length * charWidth + padding;
|
|
203
372
|
return Math.max(baseRadius, Math.min(textWidth / 2, 70));
|
|
204
373
|
};
|
|
205
374
|
|
|
206
|
-
// Handle node hover for tooltip
|
|
207
375
|
const handleNodeMouseEnter = (node: GraphNode, e: MouseEvent) => {
|
|
208
376
|
hoveredNode.value = node;
|
|
209
377
|
const container = containerRef.current;
|
|
@@ -257,6 +425,9 @@ export function GraphVisualization() {
|
|
|
257
425
|
case "effect":
|
|
258
426
|
lines.push(` ${id}([${name}])`);
|
|
259
427
|
break;
|
|
428
|
+
case "component":
|
|
429
|
+
lines.push(` ${id}{{${name}}}`);
|
|
430
|
+
break;
|
|
260
431
|
}
|
|
261
432
|
});
|
|
262
433
|
|
|
@@ -344,7 +515,7 @@ export function GraphVisualization() {
|
|
|
344
515
|
const targetRadius = getNodeRadius(targetNode);
|
|
345
516
|
const sourceX = sourceNode.x + sourceRadius;
|
|
346
517
|
const sourceY = sourceNode.y;
|
|
347
|
-
const targetX = targetNode.x - targetRadius - 8;
|
|
518
|
+
const targetX = targetNode.x - targetRadius - 8;
|
|
348
519
|
const targetY = targetNode.y;
|
|
349
520
|
|
|
350
521
|
const midX = sourceX + (targetX - sourceX) * 0.5;
|
|
@@ -365,7 +536,6 @@ export function GraphVisualization() {
|
|
|
365
536
|
<g className="nodes">
|
|
366
537
|
{graphData.value.nodes.map(node => {
|
|
367
538
|
const radius = getNodeRadius(node);
|
|
368
|
-
// Calculate max chars based on radius
|
|
369
539
|
const maxChars = Math.floor((radius * 2 - 16) / 6.5);
|
|
370
540
|
const displayName =
|
|
371
541
|
node.name.length > maxChars
|
|
@@ -489,6 +659,13 @@ export function GraphVisualization() {
|
|
|
489
659
|
></div>
|
|
490
660
|
<span>Effect</span>
|
|
491
661
|
</div>
|
|
662
|
+
<div className="legend-item">
|
|
663
|
+
<div
|
|
664
|
+
className="legend-color"
|
|
665
|
+
style={{ backgroundColor: "#9c27b0" }}
|
|
666
|
+
></div>
|
|
667
|
+
<span>Component</span>
|
|
668
|
+
</div>
|
|
492
669
|
</div>
|
|
493
670
|
</div>
|
|
494
671
|
</div>
|
|
@@ -31,14 +31,17 @@ export function UpdateItem({ update, count, firstUpdate }: UpdateItemProps) {
|
|
|
31
31
|
</span>
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
-
if (update.type === "effect") {
|
|
34
|
+
if (update.type === "effect" || update.type === "component") {
|
|
35
|
+
const icon = update.type === "component" ? "🔄" : "↪️";
|
|
36
|
+
const label = update.type === "component" ? "Component render" : "Effect";
|
|
35
37
|
return (
|
|
36
38
|
<div className={`update-item ${update.type}`}>
|
|
37
39
|
<div className="update-header">
|
|
38
40
|
<span className="signal-name">
|
|
39
|
-
|
|
41
|
+
{icon} {update.signalName}
|
|
40
42
|
{countLabel}
|
|
41
43
|
</span>
|
|
44
|
+
<span className="update-type-badge">{label}</span>
|
|
42
45
|
<span className="update-time">{time}</span>
|
|
43
46
|
</div>
|
|
44
47
|
</div>
|
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 {
|
|
@@ -64,8 +65,8 @@ export function createConnectionStore(adapter: DevToolsAdapter) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface SignalUpdate {
|
|
67
|
-
type: "update" | "effect";
|
|
68
|
-
signalType: "signal" | "computed" | "effect";
|
|
68
|
+
type: "update" | "effect" | "component";
|
|
69
|
+
signalType: "signal" | "computed" | "effect" | "component";
|
|
69
70
|
signalName: string;
|
|
70
71
|
signalId?: string;
|
|
71
72
|
prevValue?: any;
|
|
@@ -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" };
|