@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/dist/styles.css CHANGED
@@ -7,7 +7,8 @@
7
7
  }
8
8
 
9
9
  body {
10
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
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
- width: 100%;
58
- height: 2px;
59
- background: #e0e0e0;
60
- margin: 8px 0;
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%, 100% { opacity: 1; }
90
- 50% { opacity: 0.5; }
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
- margin-right: 8px;
348
+ margin-right: 8px;
320
349
  }
321
350
 
322
351
  .component-list {
323
- list-style: none;
324
- display: flex;
325
- padding: 0;
326
- margin: 0;
327
- font-family: monospace;
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: transform 0.15s ease, filter 0.15s ease;
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: stroke 0.2s, stroke-width 0.2s;
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.2.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.2.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.6.1"
57
+ "@preact/signals": "2.7.0"
58
58
  },
59
59
  "publishConfig": {
60
+ "access": "public",
60
61
  "provenance": true
61
62
  },
62
63
  "scripts": {
@@ -23,7 +23,7 @@ export function DevToolsPanel({
23
23
  const activeTab = useSignal<"updates" | "graph">(initialTab);
24
24
 
25
25
  return (
26
- <div id="app" className="signals-devtools">
26
+ <div className="signals-devtools">
27
27
  {!hideHeader && <Header />}
28
28
 
29
29
  <SettingsPanel />
@@ -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
- // Build graph data from updates signal using a computed
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" = update.signalType;
77
- const currentDepth = update.depth || 0;
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: currentDepth,
225
+ depth: 0, // Will be recalculated
87
226
  });
88
227
  }
89
228
 
90
- if (update.subscribedTo) {
91
- // Also skip links to/from disposed signals
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
- // Simple depth-based layout
107
- const allNodes = Array.from(nodes.values());
108
- const nodeSpacing = 120;
109
- const depthSpacing = 250;
110
- const startX = 100;
111
- const startY = 80;
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 depth
114
- const nodesByDepth = new Map<number, GraphNode[]>();
115
- allNodes.forEach(node => {
116
- if (!nodesByDepth.has(node.depth)) {
117
- nodesByDepth.set(node.depth, []);
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
- nodesByDepth.get(node.depth)!.push(node);
284
+ nodesByLayer.get(node.depth)!.push(node);
120
285
  });
121
286
 
122
- // Layout nodes by depth, centering each depth level vertically
123
- const maxDepth = Math.max(...allNodes.map(n => n.depth));
124
- nodesByDepth.forEach((depthNodes, depth) => {
125
- const depthHeight = (depthNodes.length - 1) * nodeSpacing;
126
- const depthStartY = startY + maxDepth * 100 - depthHeight / 2;
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
- depthNodes.forEach((node, index) => {
129
- node.x = startX + depth * depthSpacing;
130
- node.y = depthStartY + index * nodeSpacing;
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: allNodes,
136
- links: Array.from(links.values()),
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; // Approximate width per character
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; // Extra space for arrow
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
- ↪️ {update.signalName}
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" };