@preact/signals-devtools-ui 0.3.0 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preact/signals-devtools-ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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.3.0"
47
+ "@preact/signals-devtools-adapter": "0.4.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^20.0.0",
@@ -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.6.1"
57
+ "@preact/signals": "2.8.0"
58
58
  },
59
59
  "publishConfig": {
60
60
  "access": "public",
@@ -212,7 +212,8 @@ export function GraphVisualization() {
212
212
  if (!update.signalId) continue;
213
213
  if (!showDisposed && disposed.has(update.signalId)) continue;
214
214
 
215
- const type: "signal" | "computed" | "effect" = update.signalType;
215
+ const type: "signal" | "computed" | "effect" | "component" =
216
+ update.signalType;
216
217
 
217
218
  if (!nodes.has(update.signalId)) {
218
219
  nodes.set(update.signalId, {
@@ -424,6 +425,9 @@ export function GraphVisualization() {
424
425
  case "effect":
425
426
  lines.push(` ${id}([${name}])`);
426
427
  break;
428
+ case "component":
429
+ lines.push(` ${id}{{${name}}}`);
430
+ break;
427
431
  }
428
432
  });
429
433
 
@@ -489,7 +493,7 @@ export function GraphVisualization() {
489
493
  refY="3"
490
494
  orient="auto"
491
495
  >
492
- <polygon points="0 0, 8 3, 0 6" fill="#94a3b8" />
496
+ <polygon points="0 0, 8 3, 0 6" className="graph-arrowhead" />
493
497
  </marker>
494
498
  </defs>
495
499
 
@@ -635,26 +639,21 @@ export function GraphVisualization() {
635
639
 
636
640
  <div className="graph-legend">
637
641
  <div className="legend-item">
638
- <div
639
- className="legend-color"
640
- style={{ backgroundColor: "#2196f3" }}
641
- ></div>
642
+ <div className="legend-color signal"></div>
642
643
  <span>Signal</span>
643
644
  </div>
644
645
  <div className="legend-item">
645
- <div
646
- className="legend-color"
647
- style={{ backgroundColor: "#ff9800" }}
648
- ></div>
646
+ <div className="legend-color computed"></div>
649
647
  <span>Computed</span>
650
648
  </div>
651
649
  <div className="legend-item">
652
- <div
653
- className="legend-color"
654
- style={{ backgroundColor: "#4caf50" }}
655
- ></div>
650
+ <div className="legend-color effect"></div>
656
651
  <span>Effect</span>
657
652
  </div>
653
+ <div className="legend-item">
654
+ <div className="legend-color component"></div>
655
+ <span>Component</span>
656
+ </div>
658
657
  </div>
659
658
  </div>
660
659
  </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;
@@ -23,6 +36,13 @@ export function Header() {
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]}`}
43
+ >
44
+ {themeIcons[themeStore.theme]} {themeLabels[themeStore.theme]}
45
+ </button>
26
46
  {onClear && <Button onClick={onClear}>Clear</Button>}
27
47
  {onTogglePause && (
28
48
  <Button onClick={onTogglePause} active={updatesStore.isPaused.value}>
@@ -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
@@ -1,4 +1,4 @@
1
- import { signal, computed } from "@preact/signals";
1
+ import { signal, computed, effect } from "@preact/signals";
2
2
  import type {
3
3
  DevToolsAdapter,
4
4
  ConnectionStatus,
@@ -8,11 +8,14 @@ import type {
8
8
  DependencyInfo,
9
9
  } from "@preact/signals-devtools-adapter";
10
10
 
11
+ export type ThemeMode = "auto" | "light" | "dark";
12
+
11
13
  export interface DevToolsContext {
12
14
  adapter: DevToolsAdapter;
13
15
  connectionStore: ReturnType<typeof createConnectionStore>;
14
16
  updatesStore: ReturnType<typeof createUpdatesStore>;
15
17
  settingsStore: ReturnType<typeof createSettingsStore>;
18
+ themeStore: ReturnType<typeof createThemeStore>;
16
19
  }
17
20
 
18
21
  let currentContext: DevToolsContext | null = null;
@@ -65,8 +68,8 @@ export function createConnectionStore(adapter: DevToolsAdapter) {
65
68
  }
66
69
 
67
70
  export interface SignalUpdate {
68
- type: "update" | "effect";
69
- signalType: "signal" | "computed" | "effect";
71
+ type: "update" | "effect" | "component";
72
+ signalType: "signal" | "computed" | "effect" | "component";
70
73
  signalName: string;
71
74
  signalId?: string;
72
75
  prevValue?: any;
@@ -325,16 +328,85 @@ export function createSettingsStore(adapter: DevToolsAdapter) {
325
328
  };
326
329
  }
327
330
 
331
+ const THEME_STORAGE_KEY = "signals-devtools-theme";
332
+
333
+ export function createThemeStore() {
334
+ const stored = (() => {
335
+ try {
336
+ const val = localStorage.getItem(THEME_STORAGE_KEY);
337
+ if (val === "light" || val === "dark" || val === "auto") return val;
338
+ } catch {
339
+ // localStorage unavailable
340
+ }
341
+ return "auto" as ThemeMode;
342
+ })();
343
+
344
+ const theme = signal<ThemeMode>(stored);
345
+
346
+ const mediaQuery =
347
+ typeof window !== "undefined"
348
+ ? window.matchMedia("(prefers-color-scheme: dark)")
349
+ : null;
350
+ const systemIsDark = signal(mediaQuery?.matches ?? false);
351
+
352
+ if (mediaQuery) {
353
+ const handler = (e: MediaQueryListEvent) => {
354
+ systemIsDark.value = e.matches;
355
+ };
356
+ mediaQuery.addEventListener("change", handler);
357
+ }
358
+
359
+ const resolvedTheme = computed<"light" | "dark">(() =>
360
+ theme.value === "auto"
361
+ ? systemIsDark.value
362
+ ? "dark"
363
+ : "light"
364
+ : theme.value
365
+ );
366
+
367
+ // Apply data-theme attribute to the devtools container
368
+ effect(() => {
369
+ const resolved = resolvedTheme.value;
370
+ const el = document.querySelector(".signals-devtools");
371
+ if (el instanceof HTMLElement) {
372
+ el.dataset.theme = resolved;
373
+ }
374
+ });
375
+
376
+ const toggleTheme = () => {
377
+ const order: ThemeMode[] = ["auto", "light", "dark"];
378
+ const idx = order.indexOf(theme.value);
379
+ theme.value = order[(idx + 1) % order.length];
380
+ try {
381
+ localStorage.setItem(THEME_STORAGE_KEY, theme.value);
382
+ } catch {
383
+ // localStorage unavailable
384
+ }
385
+ };
386
+
387
+ return {
388
+ get theme() {
389
+ return theme.value;
390
+ },
391
+ get resolvedTheme() {
392
+ return resolvedTheme.value;
393
+ },
394
+ toggleTheme,
395
+ };
396
+ }
397
+
328
398
  export function initDevTools(adapter: DevToolsAdapter): DevToolsContext {
329
399
  const settingsStore = createSettingsStore(adapter);
330
400
  const updatesStore = createUpdatesStore(adapter, settingsStore);
331
401
  const connectionStore = createConnectionStore(adapter);
402
+ const themeStore = createThemeStore();
332
403
 
333
404
  currentContext = {
334
405
  adapter,
335
406
  connectionStore,
336
407
  updatesStore,
337
408
  settingsStore,
409
+ themeStore,
338
410
  };
339
411
 
340
412
  return currentContext;