@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.
- package/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- 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>></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
|
+
}
|