@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,243 @@
1
+ import { onCleanup, signal } from "../signals";
2
+ import { Style } from "../style";
3
+ import { Tabs } from "../ui/tabs";
4
+ import { Tooltip } from "../ui/tooltip";
5
+ import { DomTab } from "./DomTab";
6
+ import { GraphTab } from "./GraphTab";
7
+ import { setupDomMutationHighlighter } from "./highlighter";
8
+ import { devtoolsHub } from "./hub";
9
+ import { QueryTab } from "./QueryTab";
10
+ import { RpcTab } from "./RpcTab";
11
+ import { SignalsTab } from "./SignalsTab";
12
+ import { devtoolsStyles } from "./styles";
13
+ import type { DevToolsOptions, DevToolsTab } from "./types";
14
+
15
+ export function VisageDevTools(props: DevToolsOptions = {}) {
16
+ if (typeof window === "undefined") {
17
+ return null;
18
+ }
19
+
20
+ const hub = devtoolsHub;
21
+ if (typeof window !== "undefined" && !(window as any).__VISAGE_DEVTOOLS__) {
22
+ (window as any).__VISAGE_DEVTOOLS__ = hub;
23
+ }
24
+ if (props.initialOpen !== undefined) {
25
+ hub.isOpen.set(props.initialOpen);
26
+ }
27
+ if (props.initialTab !== undefined) {
28
+ hub.activeTab.set(props.initialTab);
29
+ }
30
+ if (props.highlightDOM !== undefined) {
31
+ hub.highlightDOM.set(props.highlightDOM);
32
+ }
33
+
34
+ // Set up real-time visual DOM mutation highlighting
35
+ setupDomMutationHighlighter(() => hub.highlightDOM());
36
+
37
+ const selectedId = signal<string | null>(null, { internal: true });
38
+ const searchQuery = signal("", { internal: true });
39
+
40
+ // Keyboard shortcut: Shift + Alt + V to toggle DevTools
41
+ if (typeof window !== "undefined") {
42
+ const handleKeyDown = (e: KeyboardEvent) => {
43
+ if (e.shiftKey && e.altKey && e.key.toLowerCase() === "v") {
44
+ e.preventDefault();
45
+ hub.toggle();
46
+ }
47
+ };
48
+ window.addEventListener("keydown", handleKeyDown);
49
+ onCleanup(() => window.removeEventListener("keydown", handleKeyDown));
50
+ }
51
+
52
+ const formatJSON = (val: any) => {
53
+ try {
54
+ if (val === undefined) return "undefined";
55
+ return JSON.stringify(val, null, 2);
56
+ } catch {
57
+ return String(val);
58
+ }
59
+ };
60
+
61
+ return (
62
+ <div class="visage-devtools-root">
63
+ <Style>{devtoolsStyles}</Style>
64
+
65
+ {/* Floating Toggle Launcher Button with Tooltip */}
66
+ {() =>
67
+ !hub.isOpen() ? (
68
+ <Tooltip content="Press Shift+Alt+V to toggle Visage DevTools" side="top">
69
+ <button
70
+ type="button"
71
+ class="visage-devtools-trigger"
72
+ onClick={() => hub.toggle()}
73
+ aria-label="Open Visage DevTools"
74
+ >
75
+ <svg
76
+ aria-hidden="true"
77
+ width="14"
78
+ height="14"
79
+ viewBox="0 0 24 24"
80
+ fill="none"
81
+ stroke="currentColor"
82
+ stroke-width="2"
83
+ stroke-linecap="round"
84
+ stroke-linejoin="round"
85
+ >
86
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
87
+ </svg>
88
+ <span>DevTools</span>
89
+ </button>
90
+ </Tooltip>
91
+ ) : (
92
+ <div class="visage-devtools-drawer" role="dialog" aria-label="Visage In-Browser DevTools">
93
+ {/* Header with Navigation Tabs */}
94
+ <div class="visage-devtools-header">
95
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
96
+ <svg
97
+ aria-hidden="true"
98
+ width="14"
99
+ height="14"
100
+ viewBox="0 0 24 24"
101
+ fill="none"
102
+ stroke="#38bdf8"
103
+ stroke-width="2"
104
+ stroke-linecap="round"
105
+ stroke-linejoin="round"
106
+ >
107
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
108
+ </svg>
109
+ <strong
110
+ style={{
111
+ fontSize: "12px",
112
+ letterSpacing: "0.04em",
113
+ color: "#f8fafc",
114
+ fontWeight: 700
115
+ }}
116
+ >
117
+ VISAGE
118
+ </strong>
119
+ </div>
120
+
121
+ {/* DevTools Tab Bar powered by visage/ui <Tabs> */}
122
+ <Tabs
123
+ value={() => hub.activeTab()}
124
+ onValueChange={(val) => {
125
+ hub.setTab(val as DevToolsTab);
126
+ selectedId.set(null);
127
+ }}
128
+ listClass="visage-devtools-tabs"
129
+ items={[
130
+ {
131
+ value: "signals",
132
+ label: () => (
133
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "6px" }}>
134
+ <span>Signals</span>
135
+ <span class="visage-devtools-badge">{() => hub.signalsList().length}</span>
136
+ </span>
137
+ )
138
+ },
139
+ {
140
+ value: "graph",
141
+ label: () => (
142
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "6px" }}>
143
+ <span>DAG Graph</span>
144
+ <span
145
+ class="visage-devtools-badge"
146
+ style={{ background: "#581c87", color: "#fff" }}
147
+ >
148
+ LIVE
149
+ </span>
150
+ </span>
151
+ )
152
+ },
153
+ {
154
+ value: "query",
155
+ label: () => (
156
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "6px" }}>
157
+ <span>Query Cache</span>
158
+ <span class="visage-devtools-badge">{() => hub.queriesList().length}</span>
159
+ </span>
160
+ )
161
+ },
162
+ {
163
+ value: "rpc",
164
+ label: () => (
165
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "6px" }}>
166
+ <span>RPC</span>
167
+ <span class="visage-devtools-badge">{() => hub.rpcLogs().length}</span>
168
+ </span>
169
+ )
170
+ },
171
+ {
172
+ value: "dom",
173
+ label: "DOM Inspector"
174
+ }
175
+ ]}
176
+ />
177
+
178
+ {/* Close Button */}
179
+ <div class="visage-devtools-actions">
180
+ <button
181
+ type="button"
182
+ class="visage-devtools-btn"
183
+ onClick={() => hub.isOpen.set(false)}
184
+ title="Close DevTools (Shift+Alt+V)"
185
+ >
186
+ Close
187
+ </button>
188
+ </div>
189
+ </div>
190
+
191
+ {/* DevTools Body Switch */}
192
+ <div class="visage-devtools-body">
193
+ {() => {
194
+ const tab = hub.activeTab();
195
+ if (tab === "signals") {
196
+ return (
197
+ <SignalsTab
198
+ hub={hub}
199
+ selectedId={selectedId}
200
+ searchQuery={searchQuery}
201
+ formatJSON={formatJSON}
202
+ />
203
+ );
204
+ }
205
+ if (tab === "graph") {
206
+ return (
207
+ <GraphTab
208
+ hub={hub}
209
+ selectedId={selectedId}
210
+ searchQuery={searchQuery}
211
+ formatJSON={formatJSON}
212
+ />
213
+ );
214
+ }
215
+ if (tab === "query") {
216
+ return (
217
+ <QueryTab
218
+ hub={hub}
219
+ selectedId={selectedId}
220
+ searchQuery={searchQuery}
221
+ formatJSON={formatJSON}
222
+ />
223
+ );
224
+ }
225
+ if (tab === "rpc") {
226
+ return (
227
+ <RpcTab
228
+ hub={hub}
229
+ selectedId={selectedId}
230
+ searchQuery={searchQuery}
231
+ formatJSON={formatJSON}
232
+ />
233
+ );
234
+ }
235
+ return <DomTab hub={hub} />;
236
+ }}
237
+ </div>
238
+ </div>
239
+ )
240
+ }
241
+ </div>
242
+ );
243
+ }
@@ -0,0 +1,134 @@
1
+ import { effect, onCleanup } from "../signals";
2
+
3
+ /**
4
+ * Visual DOM element mutation highlighter for Visage DevTools
5
+ */
6
+ let styleInjected = false;
7
+ const activeTimeouts = new WeakMap<Element, ReturnType<typeof setTimeout>>();
8
+
9
+ function injectHighlighterStyles() {
10
+ if (styleInjected || typeof document === "undefined") return;
11
+ const style = document.createElement("style");
12
+ style.id = "visage-devtools-highlighter";
13
+ style.textContent = `
14
+ @keyframes visage-devtools-flash {
15
+ 0% {
16
+ outline: 2px solid rgba(56, 189, 248, 0.9);
17
+ box-shadow: 0 0 12px rgba(56, 189, 248, 0.45);
18
+ }
19
+ 100% {
20
+ outline: 2px solid transparent;
21
+ box-shadow: none;
22
+ }
23
+ }
24
+ .visage-dev-highlight {
25
+ outline-offset: -1px;
26
+ animation: visage-devtools-flash 0.5s ease-out !important;
27
+ }
28
+ `;
29
+ document.head.appendChild(style);
30
+ styleInjected = true;
31
+ }
32
+
33
+ export function highlightElement(element: Element | null) {
34
+ if (!element || typeof document === "undefined" || !(element instanceof Element)) return;
35
+ injectHighlighterStyles();
36
+
37
+ const prevTimeout = activeTimeouts.get(element);
38
+ if (prevTimeout) {
39
+ clearTimeout(prevTimeout);
40
+ }
41
+
42
+ element.classList.remove("visage-dev-highlight");
43
+ // Force reflow
44
+ void (element as HTMLElement).offsetWidth;
45
+ element.classList.add("visage-dev-highlight");
46
+
47
+ const timeout = setTimeout(() => {
48
+ element.classList.remove("visage-dev-highlight");
49
+ activeTimeouts.delete(element);
50
+ }, 500);
51
+
52
+ activeTimeouts.set(element, timeout);
53
+ }
54
+
55
+ let observer: MutationObserver | null = null;
56
+
57
+ export function setupDomMutationHighlighter(enabledGetter: () => boolean) {
58
+ if (typeof window === "undefined" || typeof MutationObserver === "undefined") return;
59
+
60
+ const startObserver = () => {
61
+ if (observer) return;
62
+ injectHighlighterStyles();
63
+ observer = new MutationObserver((mutations) => {
64
+ if (!enabledGetter()) return;
65
+ const seen = new Set<Element>();
66
+ for (const mutation of mutations) {
67
+ const target = mutation.target as Node;
68
+ let el: Element | null = null;
69
+ if (target instanceof Element) {
70
+ el = target;
71
+ } else if (target.parentElement) {
72
+ el = target.parentElement;
73
+ }
74
+
75
+ if (!el) continue;
76
+
77
+ // Skip DevTools UI itself
78
+ if (el.closest?.(".visage-devtools-root") || el.closest?.("#visage-devtools-highlighter")) {
79
+ continue;
80
+ }
81
+
82
+ // Avoid infinite loops when adding/removing the highlight class
83
+ if (mutation.type === "attributes" && mutation.attributeName === "class") {
84
+ const oldCls = (mutation.oldValue || "")
85
+ .replace(/\bvisage-dev-highlight\b/g, "")
86
+ .replace(/\s+/g, " ")
87
+ .trim();
88
+ const newCls = (el.getAttribute("class") || "")
89
+ .replace(/\bvisage-dev-highlight\b/g, "")
90
+ .replace(/\s+/g, " ")
91
+ .trim();
92
+ if (oldCls === newCls) {
93
+ continue;
94
+ }
95
+ }
96
+
97
+ if (!seen.has(el)) {
98
+ seen.add(el);
99
+ highlightElement(el);
100
+ }
101
+ }
102
+ });
103
+
104
+ const targetNode = document.body || document.documentElement;
105
+ if (targetNode) {
106
+ observer.observe(targetNode, {
107
+ childList: true,
108
+ attributes: true,
109
+ attributeOldValue: true,
110
+ characterData: true,
111
+ subtree: true
112
+ });
113
+ }
114
+ };
115
+
116
+ const stopObserver = () => {
117
+ if (observer) {
118
+ observer.disconnect();
119
+ observer = null;
120
+ }
121
+ };
122
+
123
+ effect(() => {
124
+ if (enabledGetter()) {
125
+ startObserver();
126
+ } else {
127
+ stopObserver();
128
+ }
129
+ });
130
+
131
+ onCleanup(() => {
132
+ stopObserver();
133
+ });
134
+ }
@@ -0,0 +1,175 @@
1
+ import type { QueryClient } from "../query/query-client";
2
+ import { signal } from "../signals";
3
+ import type { DevToolsTab, QueryRecord, RpcRecord, SignalRecord } from "./types";
4
+
5
+ export class DevToolsHub {
6
+ readonly isOpen = signal(false, { internal: true });
7
+ readonly activeTab = signal<DevToolsTab>("signals", { internal: true });
8
+ readonly highlightDOM = signal(false, { internal: true });
9
+ readonly searchQuery = signal("", { internal: true });
10
+
11
+ // Store active records
12
+ private _signalsMap = new Map<string, SignalRecord>();
13
+ readonly signalsList = signal<SignalRecord[]>([], { internal: true });
14
+
15
+ private _queriesMap = new Map<string, QueryRecord>();
16
+ readonly queriesList = signal<QueryRecord[]>([], { internal: true });
17
+
18
+ readonly rpcLogs = signal<RpcRecord[]>([], { internal: true });
19
+ private _queryClients = new Set<QueryClient>();
20
+
21
+ private _signalCounter = 0;
22
+
23
+ constructor() {
24
+ if (typeof window !== "undefined") {
25
+ (window as any).__VISAGE_DEVTOOLS__ = this;
26
+ }
27
+ }
28
+
29
+ toggle() {
30
+ this.isOpen.update((v) => !v);
31
+ }
32
+
33
+ setTab(tab: DevToolsTab) {
34
+ this.activeTab.set(tab);
35
+ }
36
+
37
+ // --- Signal Tracking ---
38
+ registerSignal(options: {
39
+ id?: string;
40
+ name?: string;
41
+ type?: "signal" | "computed";
42
+ getValue: () => any;
43
+ version: () => number;
44
+ dependencies?: () => string[];
45
+ dependents?: () => string[];
46
+ subscribersCount?: () => number;
47
+ }): string {
48
+ const id = options.id ?? `sig_${++this._signalCounter}`;
49
+ const name = options.name ?? id;
50
+ const type = options.type ?? "signal";
51
+
52
+ const record: SignalRecord = {
53
+ id,
54
+ name,
55
+ type,
56
+ getValue: options.getValue,
57
+ version: options.version,
58
+ dependencies: options.dependencies,
59
+ dependents: options.dependents,
60
+ subscribersCount: options.subscribersCount,
61
+ lastUpdatedAt: Date.now()
62
+ };
63
+
64
+ this._signalsMap.set(id, record);
65
+ this.refreshSignals();
66
+ return id;
67
+ }
68
+
69
+ unregisterSignal(id: string) {
70
+ if (this._signalsMap.delete(id)) {
71
+ this.refreshSignals();
72
+ }
73
+ }
74
+
75
+ clearSignals() {
76
+ this._signalsMap.clear();
77
+ this._signalCounter = 0;
78
+ this.refreshSignals();
79
+ }
80
+
81
+ clearQueries() {
82
+ this._queriesMap.clear();
83
+ this.queriesList.set([]);
84
+ }
85
+
86
+ clearAll() {
87
+ this.clearSignals();
88
+ this.clearQueries();
89
+ this.clearLogs();
90
+ }
91
+
92
+ private refreshSignals() {
93
+ this.signalsList.set(Array.from(this._signalsMap.values()));
94
+ }
95
+
96
+ // --- Query Cache Tracking ---
97
+ attachQueryClient(client: QueryClient) {
98
+ if (this._queryClients.has(client)) return;
99
+ this._queryClients.add(client);
100
+
101
+ const syncQueries = () => {
102
+ const cache = client.getQueryCache();
103
+ const allQueries = cache.getAll();
104
+ const records: QueryRecord[] = allQueries.map((q) => ({
105
+ hash: q.queryHash,
106
+ key: q.queryKey,
107
+ status: () => q.status(),
108
+ fetchStatus: () => q.fetchStatus(),
109
+ data: () => q.data(),
110
+ error: () => q.error(),
111
+ dataUpdatedAt: () => q.dataUpdatedAt(),
112
+ refetch: () => q.fetch(),
113
+ invalidate: () => q.invalidate()
114
+ }));
115
+
116
+ this._queriesMap.clear();
117
+ for (const r of records) {
118
+ this._queriesMap.set(r.hash, r);
119
+ }
120
+ this.queriesList.set(records);
121
+ };
122
+
123
+ client.getQueryCache().subscribe(() => {
124
+ syncQueries();
125
+ });
126
+
127
+ syncQueries();
128
+ }
129
+
130
+ // --- RPC & SSE Logging ---
131
+ logRpcCall(record: Omit<RpcRecord, "status" | "startTime"> & { status?: RpcRecord["status"] }) {
132
+ const fullRecord: RpcRecord = {
133
+ ...record,
134
+ status: record.status ?? "pending",
135
+ startTime: Date.now(),
136
+ events: []
137
+ };
138
+
139
+ this.rpcLogs.update((prev) => [fullRecord, ...prev.slice(0, 99)]);
140
+ return fullRecord.id;
141
+ }
142
+
143
+ updateRpcCall(id: string, updates: Partial<RpcRecord>) {
144
+ this.rpcLogs.update((prev) =>
145
+ prev.map((item) => {
146
+ if (item.id === id) {
147
+ const duration =
148
+ updates.duration ?? (updates.status ? Date.now() - item.startTime : item.duration);
149
+ return { ...item, ...updates, duration };
150
+ }
151
+ return item;
152
+ })
153
+ );
154
+ }
155
+
156
+ logRpcEvent(id: string, data: any) {
157
+ this.rpcLogs.update((prev) =>
158
+ prev.map((item) => {
159
+ if (item.id === id) {
160
+ const events = item.events
161
+ ? [...item.events, { timestamp: Date.now(), data }]
162
+ : [{ timestamp: Date.now(), data }];
163
+ return { ...item, events, status: "streaming" as const };
164
+ }
165
+ return item;
166
+ })
167
+ );
168
+ }
169
+
170
+ clearLogs() {
171
+ this.rpcLogs.set([]);
172
+ }
173
+ }
174
+
175
+ export const devtoolsHub = new DevToolsHub();
@@ -0,0 +1,20 @@
1
+ import type { QueryClient } from "../query/query-client";
2
+ import { devtoolsHub } from "./hub";
3
+
4
+ export { VisageDevTools } from "./component";
5
+ export { highlightElement } from "./highlighter";
6
+ export { DevToolsHub, devtoolsHub } from "./hub";
7
+ export type {
8
+ DevToolsOptions,
9
+ DevToolsTab,
10
+ QueryRecord,
11
+ RpcRecord,
12
+ SignalRecord
13
+ } from "./types";
14
+
15
+ /**
16
+ * Connect QueryClient or RPC client to Visage DevTools
17
+ */
18
+ export function connectDevTools(client: QueryClient) {
19
+ devtoolsHub.attachQueryClient(client);
20
+ }