@epoch32/visage 0.0.0-stage → 1.0.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.
Files changed (135) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/src/api/index.js +4002 -0
  4. package/dist/src/charts/index.js +2717 -0
  5. package/dist/src/devtools/index.js +4677 -0
  6. package/dist/src/dom/index.js +1550 -0
  7. package/dist/src/form/index.js +1415 -0
  8. package/dist/src/index.js +11826 -0
  9. package/dist/src/jsx-runtime/index.js +1630 -0
  10. package/dist/src/query/index.js +2422 -0
  11. package/dist/src/router/index.js +1856 -0
  12. package/dist/src/rpc/index.js +3484 -0
  13. package/dist/src/server/index.js +1173 -0
  14. package/dist/src/signals/index.js +1077 -0
  15. package/dist/src/style/index.js +2340 -0
  16. package/dist/src/template/index.js +1149 -0
  17. package/dist/src/ui/index.js +4155 -0
  18. package/dist/src/virtual/index.js +1955 -0
  19. package/dist/src/vite/index.js +984 -0
  20. package/package.json +140 -4
  21. package/src/api/client.ts +371 -0
  22. package/src/api/file-router.ts +183 -0
  23. package/src/api/handler.ts +326 -0
  24. package/src/api/index.ts +71 -0
  25. package/src/api/middleware.ts +54 -0
  26. package/src/api/pragma.ts +155 -0
  27. package/src/api/procedure.ts +172 -0
  28. package/src/api/types.ts +249 -0
  29. package/src/charts/Area.tsx +111 -0
  30. package/src/charts/Axis.tsx +126 -0
  31. package/src/charts/Bar.tsx +86 -0
  32. package/src/charts/CartesianGrid.tsx +72 -0
  33. package/src/charts/Chart.tsx +269 -0
  34. package/src/charts/Cursor.tsx +42 -0
  35. package/src/charts/Legend.tsx +56 -0
  36. package/src/charts/Line.tsx +106 -0
  37. package/src/charts/Pie.tsx +88 -0
  38. package/src/charts/Tooltip.tsx +127 -0
  39. package/src/charts/context.ts +30 -0
  40. package/src/charts/index.ts +14 -0
  41. package/src/charts/path.ts +155 -0
  42. package/src/charts/scale.ts +133 -0
  43. package/src/charts/types.ts +50 -0
  44. package/src/devtools/DomTab.tsx +134 -0
  45. package/src/devtools/GraphInspector.tsx +196 -0
  46. package/src/devtools/GraphTab.tsx +315 -0
  47. package/src/devtools/QueryTab.tsx +200 -0
  48. package/src/devtools/RpcTab.tsx +190 -0
  49. package/src/devtools/SignalsTab.tsx +129 -0
  50. package/src/devtools/component.tsx +243 -0
  51. package/src/devtools/highlighter.ts +134 -0
  52. package/src/devtools/hub.ts +175 -0
  53. package/src/devtools/index.ts +20 -0
  54. package/src/devtools/styles.ts +313 -0
  55. package/src/devtools/types.ts +45 -0
  56. package/src/dom/async.ts +232 -0
  57. package/src/dom/control-flow.ts +353 -0
  58. package/src/dom/index.ts +32 -0
  59. package/src/dom/lazy.ts +82 -0
  60. package/src/dom/render.ts +286 -0
  61. package/src/dom/resource.ts +120 -0
  62. package/src/form/form.ts +218 -0
  63. package/src/form/index.ts +17 -0
  64. package/src/form/schema.ts +299 -0
  65. package/src/index.ts +413 -0
  66. package/src/jsx-dev-runtime/index.ts +1 -0
  67. package/src/jsx-runtime/index.ts +112 -0
  68. package/src/jsx-runtime/jsx.d.ts +278 -0
  69. package/src/query/create-mutation.ts +64 -0
  70. package/src/query/create-query.ts +426 -0
  71. package/src/query/focus-manager.ts +68 -0
  72. package/src/query/index.ts +63 -0
  73. package/src/query/mutation-cache.ts +138 -0
  74. package/src/query/online-manager.ts +67 -0
  75. package/src/query/query-cache.ts +369 -0
  76. package/src/query/query-client.ts +248 -0
  77. package/src/query/types.ts +221 -0
  78. package/src/query/utils.ts +132 -0
  79. package/src/router/index.ts +19 -0
  80. package/src/router/router.tsx +352 -0
  81. package/src/rpc/client.ts +317 -0
  82. package/src/rpc/error.ts +70 -0
  83. package/src/rpc/index.ts +52 -0
  84. package/src/rpc/procedure.ts +88 -0
  85. package/src/rpc/router.ts +105 -0
  86. package/src/rpc/server.ts +511 -0
  87. package/src/rpc/types.ts +328 -0
  88. package/src/server/hydrate.ts +21 -0
  89. package/src/server/index.ts +10 -0
  90. package/src/server/render-to-stream.ts +31 -0
  91. package/src/server/render-to-string.ts +125 -0
  92. package/src/signals/computed.ts +217 -0
  93. package/src/signals/context.ts +89 -0
  94. package/src/signals/effect.ts +161 -0
  95. package/src/signals/index.ts +57 -0
  96. package/src/signals/scope.ts +132 -0
  97. package/src/signals/signal.ts +203 -0
  98. package/src/signals/store.ts +163 -0
  99. package/src/signals/system.ts +124 -0
  100. package/src/signals/types.ts +64 -0
  101. package/src/style/component.tsx +122 -0
  102. package/src/style/create-style.ts +229 -0
  103. package/src/style/cx.ts +75 -0
  104. package/src/style/hash.ts +19 -0
  105. package/src/style/index.ts +8 -0
  106. package/src/style/layout.tsx +406 -0
  107. package/src/style/manager.ts +87 -0
  108. package/src/style/parser.ts +257 -0
  109. package/src/style/scale.ts +237 -0
  110. package/src/template/index.ts +4 -0
  111. package/src/template/template.ts +126 -0
  112. package/src/ui/accordion.tsx +286 -0
  113. package/src/ui/command-dialog.tsx +49 -0
  114. package/src/ui/command.tsx +400 -0
  115. package/src/ui/data-table.tsx +322 -0
  116. package/src/ui/dialog.tsx +362 -0
  117. package/src/ui/dropdown-menu.tsx +381 -0
  118. package/src/ui/icon.tsx +69 -0
  119. package/src/ui/index.ts +138 -0
  120. package/src/ui/layout.tsx +13 -0
  121. package/src/ui/popover.tsx +249 -0
  122. package/src/ui/switch.tsx +114 -0
  123. package/src/ui/tabs.tsx +297 -0
  124. package/src/ui/toast.tsx +280 -0
  125. package/src/ui/tooltip.tsx +262 -0
  126. package/src/ui/utils.ts +110 -0
  127. package/src/virtual/index.ts +12 -0
  128. package/src/virtual/virtual-list.tsx +185 -0
  129. package/src/virtual/virtualizer.ts +274 -0
  130. package/src/vite/hmr.ts +38 -0
  131. package/src/vite/index.ts +3 -0
  132. package/src/vite/lucide.ts +124 -0
  133. package/src/vite/plugin.ts +102 -0
  134. package/src/vite/rpc-middleware.ts +82 -0
  135. package/src/vite/types.ts +65 -0
@@ -0,0 +1,134 @@
1
+ import { Switch } from "../ui/switch";
2
+ import type { DevToolsHub } from "./hub";
3
+
4
+ interface DomTabProps {
5
+ hub: DevToolsHub;
6
+ }
7
+
8
+ export function DomTab(props: DomTabProps) {
9
+ return (
10
+ <div style={{ flex: 1, padding: "24px", overflowY: "auto", background: "#090d16" }}>
11
+ <div style={{ maxWidth: "700px", margin: "0 auto" }}>
12
+ <div
13
+ style={{
14
+ display: "flex",
15
+ alignItems: "center",
16
+ justifyContent: "space-between",
17
+ marginBottom: "20px"
18
+ }}
19
+ >
20
+ <div>
21
+ <h3
22
+ style={{ margin: "0 0 6px 0", color: "#f8fafc", fontSize: "16px", fontWeight: 700 }}
23
+ >
24
+ Real-Time DOM Mutation Highlighter
25
+ </h3>
26
+ <p style={{ margin: 0, color: "#94a3b8", fontSize: "13px", lineHeight: 1.5 }}>
27
+ Visage uses zero Virtual DOM and fine-grained reactivity. Only exact text nodes and
28
+ attributes update in-place.
29
+ </p>
30
+ </div>
31
+ <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
32
+ <span
33
+ style={{
34
+ fontSize: "11px",
35
+ fontWeight: 700,
36
+ letterSpacing: "0.04em",
37
+ color: () => (props.hub.highlightDOM() ? "#10b981" : "#64748b")
38
+ }}
39
+ >
40
+ {() => (props.hub.highlightDOM() ? "ENABLED" : "DISABLED")}
41
+ </span>
42
+ <Switch
43
+ checked={props.hub.highlightDOM}
44
+ onCheckedChange={(val) => props.hub.highlightDOM.set(val)}
45
+ />
46
+ </div>
47
+ </div>
48
+
49
+ <div
50
+ style={{
51
+ background: "#0c111d",
52
+ border: "1px solid #1e293b",
53
+ borderRadius: "8px",
54
+ padding: "16px",
55
+ marginBottom: "20px"
56
+ }}
57
+ >
58
+ <h4 style={{ margin: "0 0 10px 0", color: "#38bdf8", fontSize: "13px", fontWeight: 600 }}>
59
+ Usage Instructions:
60
+ </h4>
61
+ <ul
62
+ style={{
63
+ margin: 0,
64
+ paddingLeft: "20px",
65
+ color: "#cbd5e1",
66
+ fontSize: "13px",
67
+ lineHeight: 1.6
68
+ }}
69
+ >
70
+ <li>Toggle the switch above to enable visual flashing outlines across your app.</li>
71
+ <li>Interact with buttons, form inputs, tabs, and modals in your application.</li>
72
+ <li>
73
+ Notice that <strong>only the exact updated node flashes</strong> without re-rendering
74
+ the whole component or tree.
75
+ </li>
76
+ </ul>
77
+ </div>
78
+
79
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}>
80
+ <div
81
+ style={{
82
+ background: "#0c111d",
83
+ border: "1px solid #1e293b",
84
+ borderRadius: "8px",
85
+ padding: "14px"
86
+ }}
87
+ >
88
+ <div
89
+ style={{
90
+ fontWeight: 700,
91
+ color: "#f87171",
92
+ marginBottom: "6px",
93
+ fontSize: "12px",
94
+ textTransform: "uppercase",
95
+ letterSpacing: "0.04em"
96
+ }}
97
+ >
98
+ Virtual DOM Model
99
+ </div>
100
+ <p style={{ margin: 0, color: "#94a3b8", fontSize: "12px", lineHeight: 1.5 }}>
101
+ State changes re-run entire component functions, allocate temporary Virtual DOM nodes,
102
+ and diff tree structures.
103
+ </p>
104
+ </div>
105
+ <div
106
+ style={{
107
+ background: "#0c111d",
108
+ border: "1px solid #1e293b",
109
+ borderRadius: "8px",
110
+ padding: "14px"
111
+ }}
112
+ >
113
+ <div
114
+ style={{
115
+ fontWeight: 700,
116
+ color: "#34d399",
117
+ marginBottom: "6px",
118
+ fontSize: "12px",
119
+ textTransform: "uppercase",
120
+ letterSpacing: "0.04em"
121
+ }}
122
+ >
123
+ Visage Reactive Engine
124
+ </div>
125
+ <p style={{ margin: 0, color: "#94a3b8", fontSize: "12px", lineHeight: 1.5 }}>
126
+ Component functions execute once during mounting. Signals push direct in-place
127
+ mutations at native DOM speed.
128
+ </p>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,196 @@
1
+ import { Show } from "../dom";
2
+ import type { WritableSignal } from "../signals";
3
+ import type { SignalRecord } from "./types";
4
+
5
+ interface GraphInspectorProps {
6
+ selectedNode: () => SignalRecord | null;
7
+ selectedId: WritableSignal<string | null>;
8
+ getDependencies: (node: SignalRecord) => SignalRecord[];
9
+ getDependents: (node: SignalRecord) => SignalRecord[];
10
+ formatJSON: (val: any) => string;
11
+ }
12
+
13
+ export function GraphInspector(props: GraphInspectorProps) {
14
+ return (
15
+ <div
16
+ style={{
17
+ width: "320px",
18
+ borderLeft: "1px solid #1e293b",
19
+ background: "#090d16",
20
+ padding: "16px",
21
+ overflowY: "auto"
22
+ }}
23
+ >
24
+ <Show
25
+ when={props.selectedNode}
26
+ fallback={
27
+ <div
28
+ style={{
29
+ color: "#64748b",
30
+ textAlign: "center",
31
+ marginTop: "30px",
32
+ fontSize: "12px",
33
+ lineHeight: 1.6
34
+ }}
35
+ >
36
+ <p>
37
+ Select any DAG node to inspect live dependencies, downstream subscribers, and reactive
38
+ version history.
39
+ </p>
40
+ <div
41
+ style={{
42
+ marginTop: "16px",
43
+ padding: "12px",
44
+ background: "#0f172a",
45
+ borderRadius: "6px",
46
+ textAlign: "left"
47
+ }}
48
+ >
49
+ <div
50
+ style={{ color: "#94a3b8", fontWeight: 600, marginBottom: "4px", fontSize: "11px" }}
51
+ >
52
+ Legend:
53
+ </div>
54
+ <div style={{ color: "#38bdf8", fontSize: "11px" }}>
55
+ [SIG] Upstream Source Signals
56
+ </div>
57
+ <div style={{ color: "#c084fc", fontSize: "11px" }}>[CMP] Derived Computeds</div>
58
+ </div>
59
+ </div>
60
+ }
61
+ >
62
+ {(node: () => SignalRecord) => {
63
+ const deps = () => props.getDependencies(node());
64
+ const targets = () => props.getDependents(node());
65
+
66
+ return (
67
+ <div>
68
+ <div
69
+ style={{
70
+ display: "flex",
71
+ justifyContent: "space-between",
72
+ alignItems: "flex-start",
73
+ marginBottom: "12px"
74
+ }}
75
+ >
76
+ <div>
77
+ <h4 style={{ margin: "0 0 2px 0", color: "#f1f5f9", fontSize: "15px" }}>
78
+ {() => node().name}
79
+ </h4>
80
+ <span style={{ fontSize: "11px", color: "#64748b" }}>ID: {() => node().id}</span>
81
+ </div>
82
+ <button
83
+ type="button"
84
+ class="visage-devtools-btn"
85
+ onClick={() => props.selectedId.set(null)}
86
+ title="Clear Selection"
87
+ >
88
+ Clear
89
+ </button>
90
+ </div>
91
+
92
+ <div style={{ display: "flex", gap: "6px", marginBottom: "16px" }}>
93
+ <span
94
+ class="visage-devtools-badge"
95
+ style={{
96
+ background: () => (node().type === "computed" ? "#581c87" : "#0369a1"),
97
+ color: "#fff"
98
+ }}
99
+ >
100
+ {() => node().type.toUpperCase()}
101
+ </span>
102
+ <span class="visage-devtools-badge">VERSION: {() => node().version()}</span>
103
+ </div>
104
+
105
+ {/* Current Value */}
106
+ <div style={{ fontSize: "12px", color: "#94a3b8", marginBottom: "6px" }}>
107
+ Live Value:
108
+ </div>
109
+ <pre class="visage-devtools-json" style={{ marginBottom: "16px" }}>
110
+ {() => props.formatJSON(node().getValue())}
111
+ </pre>
112
+
113
+ {/* Upstream Dependencies (Inputs) */}
114
+ <div style={{ marginBottom: "16px" }}>
115
+ <div
116
+ style={{
117
+ fontSize: "11px",
118
+ fontWeight: 700,
119
+ textTransform: "uppercase",
120
+ color: "#38bdf8",
121
+ marginBottom: "6px"
122
+ }}
123
+ >
124
+ Upstream Inputs ({() => deps().length})
125
+ </div>
126
+ {() =>
127
+ deps().length > 0 ? (
128
+ <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
129
+ {deps().map((d) => (
130
+ <div
131
+ class="visage-graph-pill source"
132
+ style={{
133
+ width: "100%",
134
+ justifyContent: "space-between",
135
+ padding: "6px 8px"
136
+ }}
137
+ onClick={() => props.selectedId.set(d.id)}
138
+ >
139
+ <span>{d.name}</span>
140
+ <span style={{ color: "#94a3b8" }}>v{d.version()}</span>
141
+ </div>
142
+ ))}
143
+ </div>
144
+ ) : (
145
+ <div style={{ fontSize: "11px", color: "#64748b" }}>
146
+ None (Root Reactive Source)
147
+ </div>
148
+ )
149
+ }
150
+ </div>
151
+
152
+ {/* Downstream Consumers (Outputs) */}
153
+ <div>
154
+ <div
155
+ style={{
156
+ fontSize: "11px",
157
+ fontWeight: 700,
158
+ textTransform: "uppercase",
159
+ color: "#c084fc",
160
+ marginBottom: "6px"
161
+ }}
162
+ >
163
+ Downstream Consumers ({() => targets().length})
164
+ </div>
165
+ {() =>
166
+ targets().length > 0 ? (
167
+ <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
168
+ {targets().map((t) => (
169
+ <div
170
+ class="visage-graph-pill target"
171
+ style={{
172
+ width: "100%",
173
+ justifyContent: "space-between",
174
+ padding: "6px 8px"
175
+ }}
176
+ onClick={() => props.selectedId.set(t.id)}
177
+ >
178
+ <span>{t.name}</span>
179
+ <span style={{ color: "#94a3b8" }}>v{t.version()}</span>
180
+ </div>
181
+ ))}
182
+ </div>
183
+ ) : (
184
+ <div style={{ fontSize: "11px", color: "#64748b" }}>
185
+ None (Leaf / UI Consumer)
186
+ </div>
187
+ )
188
+ }
189
+ </div>
190
+ </div>
191
+ );
192
+ }}
193
+ </Show>
194
+ </div>
195
+ );
196
+ }
@@ -0,0 +1,315 @@
1
+ import { For } from "../dom";
2
+ import { createSignal, type WritableSignal } from "../signals";
3
+ import { GraphInspector } from "./GraphInspector";
4
+ import type { DevToolsHub } from "./hub";
5
+ import type { SignalRecord } from "./types";
6
+
7
+ interface GraphTabProps {
8
+ hub: DevToolsHub;
9
+ selectedId: WritableSignal<string | null>;
10
+ searchQuery: WritableSignal<string>;
11
+ formatJSON: (val: any) => string;
12
+ }
13
+
14
+ export function GraphTab(props: GraphTabProps) {
15
+ const [filterType, setFilterType] = createSignal<"all" | "signal" | "computed">("all", {
16
+ internal: true
17
+ });
18
+ const [hoveredId, setHoveredId] = createSignal<string | null>(null, { internal: true });
19
+
20
+ const allSignals = () => props.hub.signalsList();
21
+
22
+ const getDependencies = (node: SignalRecord): SignalRecord[] => {
23
+ const depIds = node.dependencies ? node.dependencies() : [];
24
+ if (depIds.length === 0) return [];
25
+ return allSignals().filter((s) => depIds.includes(s.id));
26
+ };
27
+
28
+ const getDependents = (node: SignalRecord): SignalRecord[] => {
29
+ const directDependents = node.dependents ? node.dependents() : [];
30
+ return allSignals().filter(
31
+ (s) => directDependents.includes(s.id) || s.dependencies?.().includes(node.id)
32
+ );
33
+ };
34
+
35
+ const activeFocusId = () => hoveredId() ?? props.selectedId();
36
+
37
+ const focusedDeps = () => {
38
+ const id = activeFocusId();
39
+ if (!id) return [];
40
+ const node = allSignals().find((s) => s.id === id);
41
+ if (!node) return [];
42
+ return getDependencies(node).map((s) => s.id);
43
+ };
44
+
45
+ const focusedTargets = () => {
46
+ const id = activeFocusId();
47
+ if (!id) return [];
48
+ const node = allSignals().find((s) => s.id === id);
49
+ if (!node) return [];
50
+ return getDependents(node).map((s) => s.id);
51
+ };
52
+
53
+ const filteredSignals = () =>
54
+ allSignals().filter((s) => {
55
+ const q = props.searchQuery().toLowerCase().trim();
56
+ const matchQuery = !q || s.name.toLowerCase().includes(q) || s.id.toLowerCase().includes(q);
57
+ const matchType = filterType() === "all" || s.type === filterType();
58
+ return matchQuery && matchType;
59
+ });
60
+
61
+ const stateSignals = () => filteredSignals().filter((s) => s.type === "signal");
62
+ const computedSignals = () => filteredSignals().filter((s) => s.type === "computed");
63
+
64
+ const selectedNode = () => {
65
+ const id = props.selectedId();
66
+ if (!id) return null;
67
+ return allSignals().find((s) => s.id === id) ?? null;
68
+ };
69
+
70
+ return (
71
+ <div class="visage-graph-container">
72
+ {/* Top Toolbar */}
73
+ <div class="visage-graph-toolbar">
74
+ <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
75
+ <input
76
+ class="visage-devtools-input"
77
+ style={{ width: "200px" }}
78
+ placeholder="Filter DAG graph..."
79
+ value={props.searchQuery}
80
+ onInput={(e: any) => props.searchQuery.set(e.target.value)}
81
+ />
82
+
83
+ <div style={{ display: "flex", gap: "4px" }}>
84
+ <button
85
+ type="button"
86
+ class={() => `visage-devtools-btn ${filterType() === "all" ? "active" : ""}`}
87
+ onClick={() => setFilterType("all")}
88
+ >
89
+ All ({() => filteredSignals().length})
90
+ </button>
91
+ <button
92
+ type="button"
93
+ class={() => `visage-devtools-btn ${filterType() === "signal" ? "active" : ""}`}
94
+ onClick={() => setFilterType("signal")}
95
+ >
96
+ Signals ({() => stateSignals().length})
97
+ </button>
98
+ <button
99
+ type="button"
100
+ class={() => `visage-devtools-btn ${filterType() === "computed" ? "active" : ""}`}
101
+ onClick={() => setFilterType("computed")}
102
+ >
103
+ Computeds ({() => computedSignals().length})
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ <div
109
+ style={{
110
+ fontSize: "11px",
111
+ color: "#64748b",
112
+ display: "flex",
113
+ gap: "14px",
114
+ alignItems: "center"
115
+ }}
116
+ >
117
+ <span style={{ color: "#38bdf8" }}>[SIG] Upstream</span>
118
+ <span style={{ color: "#c084fc" }}>[CMP] Derived</span>
119
+ <span style={{ color: "#10b981" }}>Glitch-Free DAG</span>
120
+ </div>
121
+ </div>
122
+
123
+ {/* Main Reactive Canvas */}
124
+ <div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
125
+ <div class="visage-graph-canvas">
126
+ {/* Column 1: Root Reactive State (Sources) */}
127
+ <div class="visage-graph-column">
128
+ <div class="visage-graph-col-header">
129
+ <span class="visage-graph-pulse" />
130
+ <span>Reactive Sources ({() => stateSignals().length})</span>
131
+ </div>
132
+
133
+ <For each={stateSignals}>
134
+ {(node: SignalRecord) => {
135
+ const isSelected = () => props.selectedId() === node.id;
136
+ const isDep = () => focusedDeps().includes(node.id);
137
+ const isTarget = () => focusedTargets().includes(node.id);
138
+ const isDimmed = () =>
139
+ activeFocusId() !== null && !isSelected() && !isDep() && !isTarget();
140
+
141
+ const dependents = () => getDependents(node);
142
+
143
+ return (
144
+ <div
145
+ class={() =>
146
+ `visage-graph-node ${isSelected() ? "selected" : ""} ${isDep() ? "is-dependency" : ""} ${isTarget() ? "is-dependent" : ""} ${isDimmed() ? "is-dimmed" : ""}`
147
+ }
148
+ onClick={() => props.selectedId.set(node.id)}
149
+ onMouseEnter={() => setHoveredId(node.id)}
150
+ onMouseLeave={() => setHoveredId(null)}
151
+ >
152
+ <div class="visage-graph-node-title">
153
+ <span
154
+ style={{
155
+ color: "#f8fafc",
156
+ display: "inline-flex",
157
+ alignItems: "center",
158
+ gap: "6px"
159
+ }}
160
+ >
161
+ <span
162
+ style={{
163
+ fontSize: "9px",
164
+ fontWeight: 700,
165
+ padding: "1px 4px",
166
+ borderRadius: "3px",
167
+ background: "#082f49",
168
+ color: "#38bdf8",
169
+ border: "1px solid #0369a1"
170
+ }}
171
+ >
172
+ SIG
173
+ </span>
174
+ <span>{node.name}</span>
175
+ </span>
176
+ <span class="visage-devtools-badge">v{() => node.version()}</span>
177
+ </div>
178
+
179
+ <div class="visage-graph-node-val">{() => JSON.stringify(node.getValue())}</div>
180
+
181
+ <div
182
+ style={{ marginTop: "8px", display: "flex", flexWrap: "wrap", gap: "4px" }}
183
+ >
184
+ {() =>
185
+ dependents().length > 0 ? (
186
+ <span style={{ fontSize: "10px", color: "#94a3b8" }}>
187
+ {dependents().length} consumer{dependents().length > 1 ? "s" : ""}
188
+ </span>
189
+ ) : (
190
+ <span style={{ fontSize: "10px", color: "#475569" }}>Root State</span>
191
+ )
192
+ }
193
+ </div>
194
+ </div>
195
+ );
196
+ }}
197
+ </For>
198
+ </div>
199
+
200
+ {/* Connectors Visual Divider */}
201
+ <div
202
+ style={{
203
+ display: "flex",
204
+ flexDirection: "column",
205
+ justifyContent: "center",
206
+ alignItems: "center",
207
+ color: "#334155",
208
+ fontSize: "14px",
209
+ fontWeight: 700,
210
+ padding: "0 10px"
211
+ }}
212
+ >
213
+ <span>&gt;</span>
214
+ </div>
215
+
216
+ {/* Column 2: Derived Computeds */}
217
+ <div class="visage-graph-column">
218
+ <div class="visage-graph-col-header">
219
+ <span class="visage-graph-pulse computed" />
220
+ <span>Derived Computeds ({() => computedSignals().length})</span>
221
+ </div>
222
+
223
+ <For each={computedSignals}>
224
+ {(node: SignalRecord) => {
225
+ const isSelected = () => props.selectedId() === node.id;
226
+ const isDep = () => focusedDeps().includes(node.id);
227
+ const isTarget = () => focusedTargets().includes(node.id);
228
+ const isDimmed = () =>
229
+ activeFocusId() !== null && !isSelected() && !isDep() && !isTarget();
230
+
231
+ const deps = () => getDependencies(node);
232
+
233
+ return (
234
+ <div
235
+ class={() =>
236
+ `visage-graph-node ${isSelected() ? "selected" : ""} ${isDep() ? "is-dependency" : ""} ${isTarget() ? "is-dependent" : ""} ${isDimmed() ? "is-dimmed" : ""}`
237
+ }
238
+ onClick={() => props.selectedId.set(node.id)}
239
+ onMouseEnter={() => setHoveredId(node.id)}
240
+ onMouseLeave={() => setHoveredId(null)}
241
+ >
242
+ <div class="visage-graph-node-title">
243
+ <span
244
+ style={{
245
+ color: "#f8fafc",
246
+ display: "inline-flex",
247
+ alignItems: "center",
248
+ gap: "6px"
249
+ }}
250
+ >
251
+ <span
252
+ style={{
253
+ fontSize: "9px",
254
+ fontWeight: 700,
255
+ padding: "1px 4px",
256
+ borderRadius: "3px",
257
+ background: "#3b0764",
258
+ color: "#c084fc",
259
+ border: "1px solid #581c87"
260
+ }}
261
+ >
262
+ CMP
263
+ </span>
264
+ <span>{node.name}</span>
265
+ </span>
266
+ <span
267
+ class="visage-devtools-badge"
268
+ style={{ background: "#581c87", color: "#fff" }}
269
+ >
270
+ v{() => node.version()}
271
+ </span>
272
+ </div>
273
+
274
+ <div class="visage-graph-node-val">{() => JSON.stringify(node.getValue())}</div>
275
+
276
+ <div
277
+ style={{ marginTop: "8px", display: "flex", flexWrap: "wrap", gap: "4px" }}
278
+ >
279
+ {() =>
280
+ deps().length > 0 ? (
281
+ deps().map((d) => (
282
+ <span
283
+ class="visage-graph-pill source"
284
+ onClick={(e: MouseEvent) => {
285
+ e.stopPropagation();
286
+ props.selectedId.set(d.id);
287
+ }}
288
+ >
289
+ in: {d.name}
290
+ </span>
291
+ ))
292
+ ) : (
293
+ <span style={{ fontSize: "10px", color: "#64748b" }}>Memoized</span>
294
+ )
295
+ }
296
+ </div>
297
+ </div>
298
+ );
299
+ }}
300
+ </For>
301
+ </div>
302
+ </div>
303
+
304
+ {/* Modular Node Detail Side Inspector */}
305
+ <GraphInspector
306
+ selectedNode={selectedNode}
307
+ selectedId={props.selectedId}
308
+ getDependencies={getDependencies}
309
+ getDependents={getDependents}
310
+ formatJSON={props.formatJSON}
311
+ />
312
+ </div>
313
+ </div>
314
+ );
315
+ }