@preact/signals-devtools-ui 0.4.0 → 0.4.2
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 +12 -0
- package/dist/devtools-ui.d.ts +1 -1
- 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 +400 -101
- package/package.json +2 -2
- package/src/DevToolsPanel.tsx +1 -1
- package/src/components/Graph.tsx +6 -18
- package/src/components/Header.tsx +24 -3
- package/src/components/SettingsPanel.tsx +3 -4
- package/src/context.ts +28 -314
- package/src/index.ts +3 -3
- package/src/models/ConnectionModel.ts +38 -0
- package/src/models/SettingsModel.ts +40 -0
- package/src/models/ThemeModel.ts +70 -0
- package/src/models/UpdatesModel.ts +228 -0
- package/src/models/index.ts +11 -0
- package/src/styles.css +400 -101
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preact/signals-devtools-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "DevTools UI components for @preact/signals",
|
|
6
6
|
"keywords": [
|
|
@@ -54,7 +54,7 @@
|
|
|
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.8.1"
|
|
58
58
|
},
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public",
|
package/src/DevToolsPanel.tsx
CHANGED
package/src/components/Graph.tsx
CHANGED
|
@@ -197,7 +197,7 @@ export function GraphVisualization() {
|
|
|
197
197
|
const graphData = useComputed<GraphData>(() => {
|
|
198
198
|
const rawUpdates = updates.value;
|
|
199
199
|
const disposed = disposedSignalIds.value;
|
|
200
|
-
const showDisposed = settingsStore.showDisposedSignals;
|
|
200
|
+
const showDisposed = settingsStore.showDisposedSignals.value;
|
|
201
201
|
|
|
202
202
|
if (!rawUpdates || rawUpdates.length === 0) return { nodes: [], links: [] };
|
|
203
203
|
|
|
@@ -493,7 +493,7 @@ export function GraphVisualization() {
|
|
|
493
493
|
refY="3"
|
|
494
494
|
orient="auto"
|
|
495
495
|
>
|
|
496
|
-
<polygon points="0 0, 8 3, 0 6"
|
|
496
|
+
<polygon points="0 0, 8 3, 0 6" className="graph-arrowhead" />
|
|
497
497
|
</marker>
|
|
498
498
|
</defs>
|
|
499
499
|
|
|
@@ -639,31 +639,19 @@ export function GraphVisualization() {
|
|
|
639
639
|
|
|
640
640
|
<div className="graph-legend">
|
|
641
641
|
<div className="legend-item">
|
|
642
|
-
<div
|
|
643
|
-
className="legend-color"
|
|
644
|
-
style={{ backgroundColor: "#2196f3" }}
|
|
645
|
-
></div>
|
|
642
|
+
<div className="legend-color signal"></div>
|
|
646
643
|
<span>Signal</span>
|
|
647
644
|
</div>
|
|
648
645
|
<div className="legend-item">
|
|
649
|
-
<div
|
|
650
|
-
className="legend-color"
|
|
651
|
-
style={{ backgroundColor: "#ff9800" }}
|
|
652
|
-
></div>
|
|
646
|
+
<div className="legend-color computed"></div>
|
|
653
647
|
<span>Computed</span>
|
|
654
648
|
</div>
|
|
655
649
|
<div className="legend-item">
|
|
656
|
-
<div
|
|
657
|
-
className="legend-color"
|
|
658
|
-
style={{ backgroundColor: "#4caf50" }}
|
|
659
|
-
></div>
|
|
650
|
+
<div className="legend-color effect"></div>
|
|
660
651
|
<span>Effect</span>
|
|
661
652
|
</div>
|
|
662
653
|
<div className="legend-item">
|
|
663
|
-
<div
|
|
664
|
-
className="legend-color"
|
|
665
|
-
style={{ backgroundColor: "#9c27b0" }}
|
|
666
|
-
></div>
|
|
654
|
+
<div className="legend-color component"></div>
|
|
667
655
|
<span>Component</span>
|
|
668
656
|
</div>
|
|
669
657
|
</div>
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { StatusIndicator } from "./StatusIndicator";
|
|
2
2
|
import { Button } from "./Button";
|
|
3
3
|
import { getContext } from "../context";
|
|
4
|
+
import type { ThemeMode } from "../context";
|
|
5
|
+
|
|
6
|
+
const themeLabels: Record<ThemeMode, string> = {
|
|
7
|
+
auto: "Auto",
|
|
8
|
+
light: "Light",
|
|
9
|
+
dark: "Dark",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const themeIcons: Record<ThemeMode, string> = {
|
|
13
|
+
auto: "\u25D1",
|
|
14
|
+
light: "\u2600",
|
|
15
|
+
dark: "\u263E",
|
|
16
|
+
};
|
|
4
17
|
|
|
5
18
|
export function Header() {
|
|
6
|
-
const { connectionStore, updatesStore } = getContext();
|
|
19
|
+
const { connectionStore, updatesStore, themeStore } = getContext();
|
|
7
20
|
|
|
8
21
|
const onTogglePause = () => {
|
|
9
22
|
updatesStore.isPaused.value = !updatesStore.isPaused.value;
|
|
@@ -18,11 +31,19 @@ export function Header() {
|
|
|
18
31
|
<div className="header-title">
|
|
19
32
|
<h1>Signals</h1>
|
|
20
33
|
<StatusIndicator
|
|
21
|
-
status={connectionStore.status}
|
|
22
|
-
message={connectionStore.message}
|
|
34
|
+
status={connectionStore.status.value}
|
|
35
|
+
message={connectionStore.message.value}
|
|
23
36
|
/>
|
|
24
37
|
</div>
|
|
25
38
|
<div className="header-controls">
|
|
39
|
+
<button
|
|
40
|
+
className="theme-toggle"
|
|
41
|
+
onClick={themeStore.toggleTheme}
|
|
42
|
+
title={`Theme: ${themeLabels[themeStore.theme.value]}`}
|
|
43
|
+
>
|
|
44
|
+
{themeIcons[themeStore.theme.value]}{" "}
|
|
45
|
+
{themeLabels[themeStore.theme.value]}
|
|
46
|
+
</button>
|
|
26
47
|
{onClear && <Button onClick={onClear}>Clear</Button>}
|
|
27
48
|
{onTogglePause && (
|
|
28
49
|
<Button onClick={onTogglePause} active={updatesStore.isPaused.value}>
|
|
@@ -9,12 +9,11 @@ export function SettingsPanel() {
|
|
|
9
9
|
const popover = useRef<HTMLDivElement>(null);
|
|
10
10
|
|
|
11
11
|
const onApply = settingsStore.applySettings;
|
|
12
|
-
const settings = settingsStore.settings;
|
|
13
12
|
|
|
14
|
-
const localSettings = useSignal<Settings>(settings);
|
|
13
|
+
const localSettings = useSignal<Settings>(settingsStore.settings.value);
|
|
15
14
|
|
|
16
15
|
useSignalEffect(() => {
|
|
17
|
-
localSettings.value = settingsStore.settings;
|
|
16
|
+
localSettings.value = settingsStore.settings.value;
|
|
18
17
|
});
|
|
19
18
|
|
|
20
19
|
const handleApply = () => {
|
|
@@ -132,7 +131,7 @@ export function SettingsPanel() {
|
|
|
132
131
|
<label>
|
|
133
132
|
<input
|
|
134
133
|
type="checkbox"
|
|
135
|
-
checked={settingsStore.showDisposedSignals}
|
|
134
|
+
checked={settingsStore.showDisposedSignals.value}
|
|
136
135
|
onChange={() => settingsStore.toggleShowDisposedSignals()}
|
|
137
136
|
/>
|
|
138
137
|
Show disposed signals in graph
|
package/src/context.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import type { DevToolsAdapter } from "@preact/signals-devtools-adapter";
|
|
2
|
+
import { ConnectionModel } from "./models/ConnectionModel";
|
|
3
|
+
import { SettingsModel } from "./models/SettingsModel";
|
|
4
|
+
import { ThemeModel } from "./models/ThemeModel";
|
|
5
|
+
import { UpdatesModel } from "./models/UpdatesModel";
|
|
6
|
+
|
|
7
|
+
export { ConnectionModel, SettingsModel, ThemeModel, UpdatesModel };
|
|
8
|
+
export type {
|
|
9
|
+
SignalUpdate,
|
|
10
|
+
Divider,
|
|
11
|
+
UpdateTreeNode,
|
|
12
|
+
UpdateTreeNodeSingle,
|
|
13
|
+
UpdateTreeNodeGroup,
|
|
14
|
+
} from "./models/UpdatesModel";
|
|
15
|
+
export type { ThemeMode } from "./models/ThemeModel";
|
|
10
16
|
|
|
11
17
|
export interface DevToolsContext {
|
|
12
18
|
adapter: DevToolsAdapter;
|
|
13
|
-
connectionStore:
|
|
14
|
-
updatesStore:
|
|
15
|
-
settingsStore:
|
|
19
|
+
connectionStore: InstanceType<typeof ConnectionModel>;
|
|
20
|
+
updatesStore: InstanceType<typeof UpdatesModel>;
|
|
21
|
+
settingsStore: InstanceType<typeof SettingsModel>;
|
|
22
|
+
themeStore: InstanceType<typeof ThemeModel>;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
let currentContext: DevToolsContext | null = null;
|
|
@@ -26,315 +33,18 @@ export function getContext(): DevToolsContext {
|
|
|
26
33
|
return currentContext;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
export function createConnectionStore(adapter: DevToolsAdapter) {
|
|
30
|
-
const status = signal<ConnectionStatusType>("connecting");
|
|
31
|
-
const message = signal<string>("Connecting...");
|
|
32
|
-
const isConnected = signal(false);
|
|
33
|
-
|
|
34
|
-
// Listen to adapter events
|
|
35
|
-
adapter.on(
|
|
36
|
-
"connectionStatusChanged",
|
|
37
|
-
(connectionStatus: ConnectionStatus) => {
|
|
38
|
-
status.value = connectionStatus.status;
|
|
39
|
-
message.value = connectionStatus.message;
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
adapter.on("signalsAvailable", (available: boolean) => {
|
|
44
|
-
isConnected.value = available;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const refreshConnection = () => {
|
|
48
|
-
status.value = "connecting";
|
|
49
|
-
message.value = "Connecting...";
|
|
50
|
-
adapter.requestState();
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
get status() {
|
|
55
|
-
return status.value;
|
|
56
|
-
},
|
|
57
|
-
get message() {
|
|
58
|
-
return message.value;
|
|
59
|
-
},
|
|
60
|
-
get isConnected() {
|
|
61
|
-
return isConnected.value;
|
|
62
|
-
},
|
|
63
|
-
refreshConnection,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface SignalUpdate {
|
|
68
|
-
type: "update" | "effect" | "component";
|
|
69
|
-
signalType: "signal" | "computed" | "effect" | "component";
|
|
70
|
-
signalName: string;
|
|
71
|
-
signalId?: string;
|
|
72
|
-
prevValue?: any;
|
|
73
|
-
newValue?: any;
|
|
74
|
-
timestamp?: number;
|
|
75
|
-
receivedAt: number;
|
|
76
|
-
depth?: number;
|
|
77
|
-
subscribedTo?: string;
|
|
78
|
-
/** All dependencies this computed/effect currently depends on (with rich info) */
|
|
79
|
-
allDependencies?: DependencyInfo[];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export type Divider = { type: "divider" };
|
|
83
|
-
|
|
84
|
-
export interface UpdateTreeNodeBase {
|
|
85
|
-
id: string;
|
|
86
|
-
update: SignalUpdate;
|
|
87
|
-
children: UpdateTreeNode[];
|
|
88
|
-
depth: number;
|
|
89
|
-
hasChildren: boolean;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface UpdateTreeNodeSingle extends UpdateTreeNodeBase {
|
|
93
|
-
type: "single";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface UpdateTreeNodeGroup extends UpdateTreeNodeBase {
|
|
97
|
-
type: "group";
|
|
98
|
-
count: number;
|
|
99
|
-
firstUpdate: SignalUpdate;
|
|
100
|
-
firstChildren: UpdateTreeNode[];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export type UpdateTreeNode = UpdateTreeNodeGroup | UpdateTreeNodeSingle;
|
|
104
|
-
|
|
105
|
-
const nodesAreEqual = (a: UpdateTreeNode, b: UpdateTreeNode): boolean => {
|
|
106
|
-
return (
|
|
107
|
-
a.update.signalId === b.update.signalId &&
|
|
108
|
-
a.children.length === b.children.length &&
|
|
109
|
-
a.children.every((child, index) => nodesAreEqual(child, b.children[index]))
|
|
110
|
-
);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const collapseTree = (nodes: UpdateTreeNodeSingle[]): UpdateTreeNode[] => {
|
|
114
|
-
const tree: UpdateTreeNode[] = [];
|
|
115
|
-
let lastNode: UpdateTreeNode | undefined;
|
|
116
|
-
|
|
117
|
-
for (const node of nodes) {
|
|
118
|
-
if (lastNode && nodesAreEqual(lastNode, node)) {
|
|
119
|
-
if (lastNode.type !== "group") {
|
|
120
|
-
tree.pop();
|
|
121
|
-
lastNode = {
|
|
122
|
-
...lastNode,
|
|
123
|
-
type: "group",
|
|
124
|
-
count: 2,
|
|
125
|
-
firstUpdate: node.update,
|
|
126
|
-
firstChildren: node.children,
|
|
127
|
-
};
|
|
128
|
-
tree.push(lastNode);
|
|
129
|
-
} else {
|
|
130
|
-
lastNode.count++;
|
|
131
|
-
lastNode.firstUpdate = node.update;
|
|
132
|
-
lastNode.firstChildren = node.children;
|
|
133
|
-
}
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
tree.push(node);
|
|
137
|
-
lastNode = node;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return tree;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
export function createUpdatesStore(
|
|
144
|
-
adapter: DevToolsAdapter,
|
|
145
|
-
settingsStore: ReturnType<typeof createSettingsStore>
|
|
146
|
-
) {
|
|
147
|
-
const updates = signal<(SignalUpdate | Divider)[]>([]);
|
|
148
|
-
const isPaused = signal<boolean>(false);
|
|
149
|
-
const disposedSignalIds = signal<Set<string>>(new Set());
|
|
150
|
-
|
|
151
|
-
const addUpdate = (
|
|
152
|
-
update: SignalUpdate | Divider | Array<SignalUpdate | Divider>
|
|
153
|
-
) => {
|
|
154
|
-
if (Array.isArray(update)) {
|
|
155
|
-
update.forEach(item => {
|
|
156
|
-
if (item.type !== "divider") item.receivedAt = Date.now();
|
|
157
|
-
});
|
|
158
|
-
} else if (update.type === "update") {
|
|
159
|
-
update.receivedAt = Date.now();
|
|
160
|
-
}
|
|
161
|
-
updates.value = [
|
|
162
|
-
...updates.value,
|
|
163
|
-
...(Array.isArray(update) ? update : [update]),
|
|
164
|
-
];
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const addDisposal = (disposal: SignalDisposed | SignalDisposed[]) => {
|
|
168
|
-
const disposals = Array.isArray(disposal) ? disposal : [disposal];
|
|
169
|
-
const newDisposed = new Set(disposedSignalIds.value);
|
|
170
|
-
for (const d of disposals) {
|
|
171
|
-
if (d.signalId) {
|
|
172
|
-
newDisposed.add(d.signalId);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
disposedSignalIds.value = newDisposed;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const hasUpdates = computed(() => updates.value.length > 0);
|
|
179
|
-
|
|
180
|
-
const signalCounts = computed(() => {
|
|
181
|
-
const counts = new Map<string, number>();
|
|
182
|
-
updates.value.forEach(update => {
|
|
183
|
-
if (update.type === "divider") return;
|
|
184
|
-
const signalName = update.signalName || "Unknown";
|
|
185
|
-
counts.set(signalName, (counts.get(signalName) || 0) + 1);
|
|
186
|
-
});
|
|
187
|
-
return counts;
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const updateTree = computed(() => {
|
|
191
|
-
const buildTree = (
|
|
192
|
-
updates: (SignalUpdate | Divider)[]
|
|
193
|
-
): UpdateTreeNodeSingle[] => {
|
|
194
|
-
const tree: UpdateTreeNodeSingle[] = [];
|
|
195
|
-
const stack: UpdateTreeNodeSingle[] = [];
|
|
196
|
-
|
|
197
|
-
const recentUpdates = updates.slice(-100).reverse();
|
|
198
|
-
|
|
199
|
-
for (let i = 0; i < recentUpdates.length; i++) {
|
|
200
|
-
const item = recentUpdates[i];
|
|
201
|
-
|
|
202
|
-
if (item.type === "divider") {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const update = item as SignalUpdate;
|
|
207
|
-
const depth = update.depth || 0;
|
|
208
|
-
|
|
209
|
-
const nodeId = `${update.signalName}-${update.receivedAt}-${i}`;
|
|
210
|
-
|
|
211
|
-
const node: UpdateTreeNodeSingle = {
|
|
212
|
-
type: "single",
|
|
213
|
-
id: nodeId,
|
|
214
|
-
update,
|
|
215
|
-
children: [],
|
|
216
|
-
depth,
|
|
217
|
-
hasChildren: false,
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
221
|
-
stack.pop();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (stack.length === 0) {
|
|
225
|
-
tree.push(node);
|
|
226
|
-
} else {
|
|
227
|
-
const parent = stack[stack.length - 1];
|
|
228
|
-
parent.children.push(node);
|
|
229
|
-
parent.hasChildren = true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
stack.push(node);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return tree;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
return buildTree(updates.value);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const clearUpdates = () => {
|
|
242
|
-
updates.value = [];
|
|
243
|
-
disposedSignalIds.value = new Set();
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// Listen to adapter events
|
|
247
|
-
adapter.on("signalUpdate", (signalUpdates: SignalUpdate[]) => {
|
|
248
|
-
if (isPaused.value) return;
|
|
249
|
-
|
|
250
|
-
const updatesArray: Array<SignalUpdate | Divider> = [
|
|
251
|
-
...signalUpdates,
|
|
252
|
-
].reverse();
|
|
253
|
-
updatesArray.push({ type: "divider" });
|
|
254
|
-
|
|
255
|
-
addUpdate(updatesArray);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// Listen to disposal events
|
|
259
|
-
adapter.on("signalDisposed", (disposals: SignalDisposed[]) => {
|
|
260
|
-
if (isPaused.value) return;
|
|
261
|
-
addDisposal(disposals);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const collapsedUpdateTree = computed(() => {
|
|
265
|
-
const updateTreeValue = updateTree.value;
|
|
266
|
-
if (settingsStore.settings.grouped) {
|
|
267
|
-
return collapseTree(updateTreeValue);
|
|
268
|
-
}
|
|
269
|
-
return updateTreeValue;
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
updates,
|
|
274
|
-
updateTree,
|
|
275
|
-
collapsedUpdateTree,
|
|
276
|
-
totalUpdates: computed(() => Object.keys(updateTree.value).length),
|
|
277
|
-
signalCounts,
|
|
278
|
-
disposedSignalIds,
|
|
279
|
-
addUpdate,
|
|
280
|
-
clearUpdates,
|
|
281
|
-
hasUpdates,
|
|
282
|
-
isPaused,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function createSettingsStore(adapter: DevToolsAdapter) {
|
|
287
|
-
const settings = signal<Settings>({
|
|
288
|
-
enabled: true,
|
|
289
|
-
grouped: true,
|
|
290
|
-
consoleLogging: true,
|
|
291
|
-
maxUpdatesPerSecond: 60,
|
|
292
|
-
filterPatterns: [],
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const showDisposedSignals = signal<boolean>(false);
|
|
296
|
-
|
|
297
|
-
const applySettings = (newSettings: Settings) => {
|
|
298
|
-
settings.value = newSettings;
|
|
299
|
-
adapter.sendConfig(newSettings);
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const toggleShowDisposedSignals = () => {
|
|
303
|
-
showDisposedSignals.value = !showDisposedSignals.value;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// Listen to adapter events
|
|
307
|
-
adapter.on("configReceived", (config: { settings?: Settings }) => {
|
|
308
|
-
if (config.settings) {
|
|
309
|
-
settings.value = config.settings;
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
get settings() {
|
|
315
|
-
return settings.value;
|
|
316
|
-
},
|
|
317
|
-
get showDisposedSignals() {
|
|
318
|
-
return showDisposedSignals.value;
|
|
319
|
-
},
|
|
320
|
-
set settings(newSettings: Settings) {
|
|
321
|
-
settings.value = newSettings;
|
|
322
|
-
},
|
|
323
|
-
applySettings,
|
|
324
|
-
toggleShowDisposedSignals,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
36
|
export function initDevTools(adapter: DevToolsAdapter): DevToolsContext {
|
|
329
|
-
const settingsStore =
|
|
330
|
-
const updatesStore =
|
|
331
|
-
const connectionStore =
|
|
37
|
+
const settingsStore = new SettingsModel(adapter);
|
|
38
|
+
const updatesStore = new UpdatesModel(adapter, settingsStore);
|
|
39
|
+
const connectionStore = new ConnectionModel(adapter);
|
|
40
|
+
const themeStore = new ThemeModel();
|
|
332
41
|
|
|
333
42
|
currentContext = {
|
|
334
43
|
adapter,
|
|
335
44
|
connectionStore,
|
|
336
45
|
updatesStore,
|
|
337
46
|
settingsStore,
|
|
47
|
+
themeStore,
|
|
338
48
|
};
|
|
339
49
|
|
|
340
50
|
return currentContext;
|
|
@@ -342,6 +52,10 @@ export function initDevTools(adapter: DevToolsAdapter): DevToolsContext {
|
|
|
342
52
|
|
|
343
53
|
export function destroyDevTools(): void {
|
|
344
54
|
if (currentContext) {
|
|
55
|
+
currentContext.connectionStore[Symbol.dispose]();
|
|
56
|
+
currentContext.updatesStore[Symbol.dispose]();
|
|
57
|
+
currentContext.settingsStore[Symbol.dispose]();
|
|
58
|
+
currentContext.themeStore[Symbol.dispose]();
|
|
345
59
|
currentContext.adapter.disconnect();
|
|
346
60
|
currentContext = null;
|
|
347
61
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,9 +11,9 @@ export {
|
|
|
11
11
|
initDevTools,
|
|
12
12
|
destroyDevTools,
|
|
13
13
|
getContext,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
ConnectionModel,
|
|
15
|
+
UpdatesModel,
|
|
16
|
+
SettingsModel,
|
|
17
17
|
type DevToolsContext,
|
|
18
18
|
type SignalUpdate,
|
|
19
19
|
type UpdateTreeNode,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { signal, createModel } from "@preact/signals";
|
|
2
|
+
import type {
|
|
3
|
+
DevToolsAdapter,
|
|
4
|
+
ConnectionStatus,
|
|
5
|
+
ConnectionStatusType,
|
|
6
|
+
} from "@preact/signals-devtools-adapter";
|
|
7
|
+
|
|
8
|
+
export const ConnectionModel = createModel((adapter: DevToolsAdapter) => {
|
|
9
|
+
const status = signal<ConnectionStatusType>("connecting");
|
|
10
|
+
const message = signal<string>("Connecting...");
|
|
11
|
+
const isConnected = signal(false);
|
|
12
|
+
|
|
13
|
+
// Listen to adapter events
|
|
14
|
+
adapter.on(
|
|
15
|
+
"connectionStatusChanged",
|
|
16
|
+
(connectionStatus: ConnectionStatus) => {
|
|
17
|
+
status.value = connectionStatus.status;
|
|
18
|
+
message.value = connectionStatus.message;
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
adapter.on("signalsAvailable", (available: boolean) => {
|
|
23
|
+
isConnected.value = available;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const refreshConnection = () => {
|
|
27
|
+
status.value = "connecting";
|
|
28
|
+
message.value = "Connecting...";
|
|
29
|
+
adapter.requestState();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
status,
|
|
34
|
+
message,
|
|
35
|
+
isConnected,
|
|
36
|
+
refreshConnection,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { signal, createModel } from "@preact/signals";
|
|
2
|
+
import type {
|
|
3
|
+
DevToolsAdapter,
|
|
4
|
+
Settings,
|
|
5
|
+
} from "@preact/signals-devtools-adapter";
|
|
6
|
+
|
|
7
|
+
export const SettingsModel = createModel((adapter: DevToolsAdapter) => {
|
|
8
|
+
const settings = signal<Settings>({
|
|
9
|
+
enabled: true,
|
|
10
|
+
grouped: true,
|
|
11
|
+
consoleLogging: true,
|
|
12
|
+
maxUpdatesPerSecond: 60,
|
|
13
|
+
filterPatterns: [],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const showDisposedSignals = signal<boolean>(false);
|
|
17
|
+
|
|
18
|
+
const applySettings = (newSettings: Settings) => {
|
|
19
|
+
settings.value = newSettings;
|
|
20
|
+
adapter.sendConfig(newSettings);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const toggleShowDisposedSignals = () => {
|
|
24
|
+
showDisposedSignals.value = !showDisposedSignals.value;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Listen to adapter events
|
|
28
|
+
adapter.on("configReceived", (config: { settings?: Settings }) => {
|
|
29
|
+
if (config.settings) {
|
|
30
|
+
settings.value = config.settings;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
settings,
|
|
36
|
+
showDisposedSignals,
|
|
37
|
+
applySettings,
|
|
38
|
+
toggleShowDisposedSignals,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { signal, computed, effect, createModel } from "@preact/signals";
|
|
2
|
+
|
|
3
|
+
export type ThemeMode = "auto" | "light" | "dark";
|
|
4
|
+
|
|
5
|
+
const THEME_STORAGE_KEY = "signals-devtools-theme";
|
|
6
|
+
|
|
7
|
+
export const ThemeModel = createModel(() => {
|
|
8
|
+
const stored = (() => {
|
|
9
|
+
try {
|
|
10
|
+
const val = localStorage.getItem(THEME_STORAGE_KEY);
|
|
11
|
+
if (val === "light" || val === "dark" || val === "auto") return val;
|
|
12
|
+
} catch {
|
|
13
|
+
// localStorage unavailable
|
|
14
|
+
}
|
|
15
|
+
return "auto" as ThemeMode;
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
const theme = signal<ThemeMode>(stored);
|
|
19
|
+
|
|
20
|
+
const mediaQuery =
|
|
21
|
+
typeof window !== "undefined"
|
|
22
|
+
? window.matchMedia("(prefers-color-scheme: dark)")
|
|
23
|
+
: null;
|
|
24
|
+
const systemIsDark = signal(mediaQuery?.matches ?? false);
|
|
25
|
+
|
|
26
|
+
effect(() => {
|
|
27
|
+
if (!mediaQuery) return;
|
|
28
|
+
|
|
29
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
30
|
+
systemIsDark.value = e.matches;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
mediaQuery.addEventListener("change", handler);
|
|
34
|
+
return () => mediaQuery.removeEventListener("change", handler);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const resolvedTheme = computed<"light" | "dark">(() =>
|
|
38
|
+
theme.value === "auto"
|
|
39
|
+
? systemIsDark.value
|
|
40
|
+
? "dark"
|
|
41
|
+
: "light"
|
|
42
|
+
: theme.value
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Apply data-theme attribute to the devtools container
|
|
46
|
+
effect(() => {
|
|
47
|
+
const resolved = resolvedTheme.value;
|
|
48
|
+
const el = document.querySelector(".signals-devtools");
|
|
49
|
+
if (el instanceof HTMLElement) {
|
|
50
|
+
el.dataset.theme = resolved;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const toggleTheme = () => {
|
|
55
|
+
const order: ThemeMode[] = ["auto", "light", "dark"];
|
|
56
|
+
const idx = order.indexOf(theme.value);
|
|
57
|
+
theme.value = order[(idx + 1) % order.length];
|
|
58
|
+
try {
|
|
59
|
+
localStorage.setItem(THEME_STORAGE_KEY, theme.value);
|
|
60
|
+
} catch {
|
|
61
|
+
// localStorage unavailable
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
theme,
|
|
67
|
+
resolvedTheme,
|
|
68
|
+
toggleTheme,
|
|
69
|
+
};
|
|
70
|
+
});
|