Skip to content

Commit e6eea07

Browse files
committed
Signalify
1 parent 2acad3c commit e6eea07

18 files changed

+1129
-1740
lines changed

docs/demos/index.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "@preact/signals-debug";
22
import { render } from "preact";
33
import { LocationProvider, Router, useLocation, lazy } from "preact-iso";
4-
import { signal, useSignal } from "@preact/signals";
4+
import { signal, useComputed, useSignal } from "@preact/signals";
55
import { setFlashingEnabled, constrainFlashToChildren } from "./render-flasher";
66

77
// disable flashing during initial render:
@@ -10,6 +10,7 @@ setTimeout(setFlashingEnabled, 100, true);
1010

1111
const demos = {
1212
Counter,
13+
Sum,
1314
GlobalCounter,
1415
DuelingCounters,
1516
Nesting: lazy(() => import("./nesting")),
@@ -69,6 +70,39 @@ function Counter() {
6970
);
7071
}
7172

73+
function Sum() {
74+
const a = useSignal(0, { name: "a" });
75+
const b = useSignal(0, { name: "b" });
76+
77+
const sum = useComputed(() => a.value + b.value, { name: "sum" });
78+
79+
return (
80+
<div class="card">
81+
<p>
82+
<label>
83+
A:{" "}
84+
<input
85+
type="number"
86+
value={a}
87+
onInput={e => (a.value = +e.currentTarget.value)}
88+
/>
89+
</label>
90+
</p>
91+
<p>
92+
<label>
93+
B:{" "}
94+
<input
95+
type="number"
96+
value={b}
97+
onInput={e => (b.value = +e.currentTarget.value)}
98+
/>
99+
</label>
100+
</p>
101+
<output>Sum: {sum}</output>
102+
</div>
103+
);
104+
}
105+
72106
const globalCount = signal(0, { name: "global-counter" });
73107
function GlobalCounter({ explain = true }) {
74108
return (

extension/devtools-init.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ chrome.devtools.panels.create(
100100
panelWindow.postMessage(
101101
{
102102
type: "DEVTOOLS_READY",
103-
connected: isConnected,
103+
payload: { connected: isConnected },
104104
},
105105
"*"
106106
);

extension/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
"lint": "web-ext lint --source-dir ."
1313
},
1414
"dependencies": {
15-
"preact": "^10.26.9",
1615
"@preact/signals": "workspace:*",
17-
"@preact/signals-core": "workspace:*"
16+
"@preact/signals-core": "workspace:*",
17+
"preact": "^10.26.9"
1818
},
1919
"devDependencies": {
20+
"@preact/preset-vite": "^2.3.0",
2021
"@types/chrome": "^0.0.270",
2122
"typescript": "^5.8.3",
2223
"vite": "^7.0.0",

extension/src/components/Button.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
interface ButtonProps {
2+
onClick: () => void;
3+
className?: string;
4+
disabled?: boolean;
5+
children: preact.ComponentChildren;
6+
variant?: "primary" | "secondary";
7+
active?: boolean;
8+
}
9+
10+
export function Button({
11+
onClick,
12+
className = "",
13+
disabled = false,
14+
children,
15+
variant = "secondary",
16+
active = false,
17+
}: ButtonProps) {
18+
const baseClass = "btn";
19+
const variantClass = variant === "primary" ? "btn-primary" : "btn-secondary";
20+
const activeClass = active ? "active" : "";
21+
const combinedClassName =
22+
`${baseClass} ${variantClass} ${activeClass} ${className}`.trim();
23+
24+
return (
25+
<button onClick={onClick} className={combinedClassName} disabled={disabled}>
26+
{children}
27+
</button>
28+
);
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Button } from "./Button";
2+
3+
interface EmptyStateProps {
4+
onRefresh: () => void;
5+
title?: string;
6+
description?: string;
7+
buttonText?: string;
8+
}
9+
10+
export function EmptyState({
11+
onRefresh,
12+
title = "No Signals Detected",
13+
description = "Make sure your application is using @preact/signals-debug package.",
14+
buttonText = "Refresh Detection",
15+
}: EmptyStateProps) {
16+
return (
17+
<div className="empty-state">
18+
<div className="empty-state-content">
19+
<h2>{title}</h2>
20+
<p>{description}</p>
21+
<div className="empty-state-actions">
22+
<Button onClick={onRefresh} variant="primary">
23+
{buttonText}
24+
</Button>
25+
</div>
26+
</div>
27+
</div>
28+
);
29+
}

extension/src/components/Graph.tsx

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { useEffect, useRef, useState } from "preact/hooks";
2+
import {
3+
Divider,
4+
GraphData,
5+
GraphLink,
6+
GraphNode,
7+
SignalUpdate,
8+
} from "../types";
9+
10+
export function GraphVisualization({
11+
updates,
12+
}: {
13+
updates: (SignalUpdate | Divider)[];
14+
}) {
15+
const [graphData, setGraphData] = useState<GraphData>({
16+
nodes: [],
17+
links: [],
18+
});
19+
const svgRef = useRef<SVGSVGElement>(null);
20+
21+
// Build graph data from updates
22+
useEffect(() => {
23+
const nodes = new Map<string, GraphNode>();
24+
const links = new Map<string, GraphLink>();
25+
const depthMap = new Map<string, number>();
26+
27+
// Process updates to build graph structure
28+
const signalUpdates = updates.filter(
29+
update => update.type !== "divider"
30+
) as SignalUpdate[];
31+
32+
signalUpdates.forEach(update => {
33+
if (!update.signalId) return;
34+
35+
// Determine signal type
36+
let type: "signal" | "computed" | "effect" = "signal";
37+
if (update.type === "effect") {
38+
type = "effect";
39+
} else if (update.subscribedTo) {
40+
type = "computed";
41+
}
42+
43+
// Track depth
44+
const currentDepth = update.depth || 0;
45+
depthMap.set(update.signalId, currentDepth);
46+
47+
// Add node
48+
if (!nodes.has(update.signalId)) {
49+
nodes.set(update.signalId, {
50+
id: update.signalId,
51+
name: update.signalName,
52+
type,
53+
x: 0,
54+
y: 0,
55+
depth: currentDepth,
56+
});
57+
}
58+
59+
// Add link if this signal is subscribed to another
60+
if (update.subscribedTo) {
61+
const linkKey = `${update.subscribedTo}->${update.signalId}`;
62+
if (!links.has(linkKey)) {
63+
links.set(linkKey, {
64+
source: update.subscribedTo,
65+
target: update.signalId,
66+
});
67+
68+
// Also ensure source node exists
69+
if (!nodes.has(update.subscribedTo)) {
70+
nodes.set(update.subscribedTo, {
71+
id: update.subscribedTo,
72+
name: update.signalName,
73+
type: "signal",
74+
x: 0,
75+
y: 0,
76+
depth: currentDepth - 1,
77+
});
78+
}
79+
}
80+
}
81+
});
82+
83+
// Layout nodes by depth
84+
const nodesByDepth = new Map<number, GraphNode[]>();
85+
nodes.forEach(node => {
86+
if (!nodesByDepth.has(node.depth)) {
87+
nodesByDepth.set(node.depth, []);
88+
}
89+
nodesByDepth.get(node.depth)!.push(node);
90+
});
91+
92+
// Position nodes
93+
const nodeSpacing = 120;
94+
const depthSpacing = 200;
95+
const startX = 100;
96+
const startY = 100;
97+
98+
nodesByDepth.forEach((depthNodes, depth) => {
99+
const totalHeight = (depthNodes.length - 1) * nodeSpacing;
100+
const offsetY = -totalHeight / 2;
101+
102+
depthNodes.forEach((node, index) => {
103+
node.x = startX + depth * depthSpacing;
104+
node.y = startY + offsetY + index * nodeSpacing;
105+
});
106+
});
107+
108+
setGraphData({
109+
nodes: Array.from(nodes.values()),
110+
links: Array.from(links.values()),
111+
});
112+
}, [updates]);
113+
114+
if (graphData.nodes.length === 0) {
115+
return (
116+
<div className="graph-empty">
117+
<div>
118+
<h3>No Signal Dependencies</h3>
119+
<p>
120+
Create some signals with dependencies to see the graph
121+
visualization.
122+
</p>
123+
</div>
124+
</div>
125+
);
126+
}
127+
128+
return (
129+
<div className="graph-container">
130+
<div className="graph-content">
131+
<svg ref={svgRef} className="graph-svg">
132+
{/* Arrow marker definition */}
133+
<defs>
134+
<marker
135+
id="arrowhead"
136+
markerWidth="10"
137+
markerHeight="7"
138+
refX="9"
139+
refY="3.5"
140+
orient="auto"
141+
>
142+
<polygon points="0 0, 10 3.5, 0 7" fill="#666" />
143+
</marker>
144+
</defs>
145+
146+
{/* Links */}
147+
<g className="links">
148+
{graphData.links.map((link, index) => {
149+
const sourceNode = graphData.nodes.find(
150+
n => n.id === link.source
151+
);
152+
const targetNode = graphData.nodes.find(
153+
n => n.id === link.target
154+
);
155+
156+
if (!sourceNode || !targetNode) return null;
157+
158+
return (
159+
<line
160+
key={`link-${index}`}
161+
className="graph-link"
162+
x1={sourceNode.x + 30}
163+
y1={sourceNode.y}
164+
x2={targetNode.x - 30}
165+
y2={targetNode.y}
166+
/>
167+
);
168+
})}
169+
</g>
170+
171+
{/* Nodes */}
172+
<g className="nodes">
173+
{graphData.nodes.map(node => (
174+
<g key={node.id} className="graph-node-group">
175+
<circle
176+
className={`graph-node ${node.type}`}
177+
cx={node.x}
178+
cy={node.y}
179+
r="25"
180+
/>
181+
<text
182+
className="graph-text"
183+
x={node.x}
184+
y={node.y}
185+
textLength="40"
186+
lengthAdjust="spacingAndGlyphs"
187+
>
188+
{node.name.length > 8
189+
? node.name.slice(0, 8) + "..."
190+
: node.name}
191+
</text>
192+
</g>
193+
))}
194+
</g>
195+
</svg>
196+
197+
{/* Legend */}
198+
<div className="graph-legend">
199+
<div className="legend-item">
200+
<div
201+
className="legend-color"
202+
style={{ backgroundColor: "#2196f3" }}
203+
></div>
204+
<span>Signal</span>
205+
</div>
206+
<div className="legend-item">
207+
<div
208+
className="legend-color"
209+
style={{ backgroundColor: "#ff9800" }}
210+
></div>
211+
<span>Computed</span>
212+
</div>
213+
<div className="legend-item">
214+
<div
215+
className="legend-color"
216+
style={{ backgroundColor: "#4caf50" }}
217+
></div>
218+
<span>Effect</span>
219+
</div>
220+
</div>
221+
</div>
222+
</div>
223+
);
224+
}

0 commit comments

Comments
 (0)