what-devtools 0.5.5 → 0.7.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/README.md CHANGED
@@ -83,7 +83,7 @@ import DevPanel from 'what-devtools/panel';
83
83
  ## Links
84
84
 
85
85
  - [Documentation](https://whatfw.com)
86
- - [GitHub](https://github.com/CelsianJs/whatfw)
86
+ - [GitHub](https://github.com/CelsianJs/what-framework)
87
87
 
88
88
  ## License
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-devtools",
3
- "version": "0.5.5",
3
+ "version": "0.7.0",
4
4
  "description": "Dev tools for What Framework — signal inspector, component tree, effect graph",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -20,16 +20,16 @@
20
20
  "inspector"
21
21
  ],
22
22
  "peerDependencies": {
23
- "what-core": "^0.5.3"
23
+ "what-core": "^0.7.0"
24
24
  },
25
25
  "author": "ZVN DEV (https://zvndev.com)",
26
26
  "license": "MIT",
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/CelsianJs/whatfw"
29
+ "url": "https://github.com/CelsianJs/what-framework"
30
30
  },
31
31
  "bugs": {
32
- "url": "https://github.com/CelsianJs/whatfw/issues"
32
+ "url": "https://github.com/CelsianJs/what-framework/issues"
33
33
  },
34
34
  "homepage": "https://whatfw.com"
35
35
  }
package/src/DevPanel.jsx CHANGED
@@ -4,57 +4,193 @@
4
4
  * A drop-in floating UI panel that shows live signal values,
5
5
  * active effects, and mounted components during development.
6
6
  *
7
+ * Features:
8
+ * - Signal count, effect count, component count
9
+ * - Recent errors with structured output
10
+ * - Real-time signal watcher
11
+ * - Health indicator (green/yellow/red)
12
+ * - Toggle via Ctrl+Shift+D / Cmd+Shift+D
13
+ *
7
14
  * Usage:
8
15
  * import { DevPanel } from 'what-devtools/panel';
9
16
  * // Add to your app:
10
17
  * <DevPanel />
11
18
  *
12
- * The panel is draggable and can be collapsed. It auto-updates
13
- * when signals change.
19
+ * Works WITHOUT MCP devtools connected.
14
20
  */
15
21
 
16
22
  import { signal, effect, onCleanup } from 'what-core';
17
- import { subscribe, getSnapshot, installDevTools } from './index.js';
23
+ import { subscribe, getSnapshot, getErrors, installDevTools } from './index.js';
18
24
 
19
25
  export function DevPanel() {
20
26
  // Auto-install devtools if not already done
21
27
  installDevTools();
22
28
 
23
29
  const isOpen = signal(false);
24
- const activeTab = signal('signals');
30
+ const activeTab = signal('overview');
25
31
  const snapshot = signal(getSnapshot());
32
+ const recentErrors = signal(getErrors());
26
33
 
27
34
  // Subscribe to devtools events and refresh
28
- const unsub = subscribe(() => {
35
+ const unsub = subscribe((event) => {
29
36
  snapshot(getSnapshot());
37
+ if (event === 'error:captured') {
38
+ recentErrors(getErrors());
39
+ }
30
40
  });
31
41
 
32
- // Also poll every 500ms for signal value changes (cheap just reads .peek())
42
+ // Poll every 500ms for signal value changes (cheap -- just reads .peek())
33
43
  const interval = setInterval(() => {
34
44
  snapshot(getSnapshot());
45
+ recentErrors(getErrors());
35
46
  }, 500);
36
47
 
48
+ // Keyboard shortcut: Ctrl+Shift+D / Cmd+Shift+D
49
+ const onKeyDown = (e) => {
50
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') {
51
+ e.preventDefault();
52
+ isOpen((v) => !v);
53
+ }
54
+ };
55
+ if (typeof document !== 'undefined') {
56
+ document.addEventListener('keydown', onKeyDown);
57
+ }
58
+
37
59
  onCleanup(() => {
38
60
  unsub();
39
61
  clearInterval(interval);
62
+ if (typeof document !== 'undefined') {
63
+ document.removeEventListener('keydown', onKeyDown);
64
+ }
40
65
  });
41
66
 
42
- const PANEL_STYLE = 'position:fixed;bottom:0;right:0;width:360px;max-height:50vh;z-index:99998;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:#1a1a2e;color:#e0e0e0;border:1px solid #2a2a4a;border-radius:12px 0 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);display:flex;flex-direction:column;overflow:hidden;';
67
+ // --- Health indicator ---
68
+ const getHealth = () => {
69
+ const data = snapshot();
70
+ const errs = recentErrors();
71
+ const recentErrCount = errs.filter(
72
+ (e) => Date.now() - e.timestamp < 30000
73
+ ).length;
74
+
75
+ if (recentErrCount > 0) return { color: '#ef4444', label: 'Errors detected' };
76
+ if (data.effects.length > 100) return { color: '#eab308', label: 'Many effects' };
77
+ if (data.signals.length > 200) return { color: '#eab308', label: 'Many signals' };
78
+ return { color: '#22c55e', label: 'Healthy' };
79
+ };
80
+
81
+ const PANEL_STYLE =
82
+ 'position:fixed;bottom:0;right:0;width:380px;max-height:55vh;z-index:99998;' +
83
+ 'font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;' +
84
+ 'background:#1a1a2e;color:#e0e0e0;border:1px solid #2a2a4a;' +
85
+ 'border-radius:12px 0 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);' +
86
+ 'display:flex;flex-direction:column;overflow:hidden;';
43
87
 
44
88
  const tabStyle = (tab) => () => {
45
89
  const isActive = activeTab() === tab;
46
- return `padding:6px 12px;border:none;background:${isActive ? '#2a2a4a' : 'transparent'};color:${isActive ? '#fff' : '#6a6a8a'};cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;border-radius:4px;`;
90
+ return (
91
+ 'padding:6px 10px;border:none;background:' +
92
+ (isActive ? '#2a2a4a' : 'transparent') +
93
+ ';color:' +
94
+ (isActive ? '#fff' : '#6a6a8a') +
95
+ ';cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;border-radius:4px;'
96
+ );
47
97
  };
48
98
 
99
+ // --- Tab: Overview ---
100
+ const renderOverview = () => {
101
+ const data = snapshot();
102
+ const health = getHealth();
103
+ const errs = recentErrors();
104
+ const recentErrs = errs.slice(-3).reverse();
105
+
106
+ return (
107
+ <div style="padding:12px;">
108
+ {/* Health indicator */}
109
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:8px;background:#0d0d1a;border-radius:6px;">
110
+ <div
111
+ style={() =>
112
+ 'width:10px;height:10px;border-radius:50%;background:' +
113
+ getHealth().color +
114
+ ';'
115
+ }
116
+ />
117
+ <span style={() => 'color:' + getHealth().color + ';font-weight:600;'}>
118
+ {() => getHealth().label}
119
+ </span>
120
+ </div>
121
+
122
+ {/* Counts */}
123
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:12px;">
124
+ <div style="text-align:center;padding:8px;background:#0d0d1a;border-radius:6px;">
125
+ <div style="font-size:18px;font-weight:700;color:#818cf8;">
126
+ {() => snapshot().signals.length}
127
+ </div>
128
+ <div style="font-size:10px;color:#6a6a8a;margin-top:2px;">
129
+ Signals
130
+ </div>
131
+ </div>
132
+ <div style="text-align:center;padding:8px;background:#0d0d1a;border-radius:6px;">
133
+ <div style="font-size:18px;font-weight:700;color:#fbbf24;">
134
+ {() => snapshot().effects.length}
135
+ </div>
136
+ <div style="font-size:10px;color:#6a6a8a;margin-top:2px;">
137
+ Effects
138
+ </div>
139
+ </div>
140
+ <div style="text-align:center;padding:8px;background:#0d0d1a;border-radius:6px;">
141
+ <div style="font-size:18px;font-weight:700;color:#34d399;">
142
+ {() => snapshot().components.length}
143
+ </div>
144
+ <div style="font-size:10px;color:#6a6a8a;margin-top:2px;">
145
+ Components
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ {/* Recent errors */}
151
+ {() => {
152
+ const errs = recentErrors();
153
+ if (errs.length === 0) return null;
154
+ const recent = errs.slice(-3).reverse();
155
+ return (
156
+ <div style="margin-top:8px;">
157
+ <div style="font-size:11px;font-weight:600;color:#f87171;margin-bottom:6px;">
158
+ Recent Errors ({errs.length})
159
+ </div>
160
+ {recent.map((e, i) => (
161
+ <div
162
+ key={i}
163
+ style="padding:6px 8px;background:#1c0a0a;border:1px solid #3b1219;border-radius:4px;margin-bottom:4px;font-size:11px;color:#fca5a5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
164
+ title={e.message}
165
+ >
166
+ <span style="color:#6a6a8a;">
167
+ [{e.type}]
168
+ </span>{' '}
169
+ {e.message}
170
+ </div>
171
+ ))}
172
+ </div>
173
+ );
174
+ }}
175
+ </div>
176
+ );
177
+ };
178
+
179
+ // --- Tab: Signals ---
49
180
  const renderSignals = () => {
50
181
  const data = snapshot();
51
182
  if (!data.signals.length) {
52
- return <div style="padding:12px;color:#4a4a6a;">No signals tracked</div>;
183
+ return (
184
+ <div style="padding:12px;color:#4a4a6a;">No signals tracked</div>
185
+ );
53
186
  }
54
187
  return (
55
188
  <div style="padding:8px;">
56
- {data.signals.map(s => (
57
- <div key={s.id} style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-bottom:1px solid #2a2a4a;">
189
+ {data.signals.map((s) => (
190
+ <div
191
+ key={s.id}
192
+ style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-bottom:1px solid #2a2a4a;"
193
+ >
58
194
  <span style="color:#818cf8;">{s.name}</span>
59
195
  <span style="color:#a0a0c0;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
60
196
  {formatValue(s.value)}
@@ -65,89 +201,193 @@ export function DevPanel() {
65
201
  );
66
202
  };
67
203
 
204
+ // --- Tab: Effects ---
68
205
  const renderEffects = () => {
69
206
  const data = snapshot();
70
207
  if (!data.effects.length) {
71
- return <div style="padding:12px;color:#4a4a6a;">No effects tracked</div>;
208
+ return (
209
+ <div style="padding:12px;color:#4a4a6a;">No effects tracked</div>
210
+ );
72
211
  }
73
212
  return (
74
213
  <div style="padding:8px;">
75
- {data.effects.map(e => (
76
- <div key={e.id} style="padding:4px 8px;border-bottom:1px solid #2a2a4a;">
214
+ {data.effects.map((e) => (
215
+ <div
216
+ key={e.id}
217
+ style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-bottom:1px solid #2a2a4a;"
218
+ >
77
219
  <span style="color:#fbbf24;">{e.name}</span>
220
+ <span style="color:#6a6a8a;font-size:10px;">
221
+ runs: {e.runCount || 0}
222
+ </span>
78
223
  </div>
79
224
  ))}
80
225
  </div>
81
226
  );
82
227
  };
83
228
 
229
+ // --- Tab: Components ---
84
230
  const renderComponents = () => {
85
231
  const data = snapshot();
86
232
  if (!data.components.length) {
87
- return <div style="padding:12px;color:#4a4a6a;">No components tracked</div>;
233
+ return (
234
+ <div style="padding:12px;color:#4a4a6a;">No components tracked</div>
235
+ );
88
236
  }
89
237
  return (
90
238
  <div style="padding:8px;">
91
- {data.components.map(c => (
92
- <div key={c.id} style="padding:4px 8px;border-bottom:1px solid #2a2a4a;">
93
- <span style="color:#34d399;">&lt;{c.name} /&gt;</span>
239
+ {data.components.map((c) => (
240
+ <div
241
+ key={c.id}
242
+ style="padding:4px 8px;border-bottom:1px solid #2a2a4a;"
243
+ >
244
+ <span style="color:#34d399;">
245
+ &lt;{c.name} /&gt;
246
+ </span>
94
247
  </div>
95
248
  ))}
96
249
  </div>
97
250
  );
98
251
  };
99
252
 
253
+ // --- Tab: Errors ---
254
+ const renderErrors = () => {
255
+ const errs = recentErrors();
256
+ if (!errs.length) {
257
+ return (
258
+ <div style="padding:12px;color:#4a4a6a;">No errors captured</div>
259
+ );
260
+ }
261
+ return (
262
+ <div style="padding:8px;">
263
+ {errs
264
+ .slice()
265
+ .reverse()
266
+ .map((e, i) => (
267
+ <div
268
+ key={i}
269
+ style="padding:8px;background:#0d0d1a;border:1px solid #2a2a4a;border-radius:6px;margin-bottom:6px;"
270
+ >
271
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
272
+ <span style="color:#f87171;font-weight:600;font-size:11px;">
273
+ [{e.type}]
274
+ </span>
275
+ <span style="color:#4a4a6a;font-size:10px;">
276
+ {new Date(e.timestamp).toLocaleTimeString()}
277
+ </span>
278
+ </div>
279
+ <div style="color:#e0e0e0;font-size:11px;white-space:pre-wrap;word-break:break-all;">
280
+ {e.message}
281
+ </div>
282
+ {e.stack ? (
283
+ <div style="color:#4a4a6a;font-size:10px;margin-top:4px;white-space:pre-wrap;max-height:60px;overflow:hidden;">
284
+ {e.stack.split('\n').slice(0, 3).join('\n')}
285
+ </div>
286
+ ) : null}
287
+ </div>
288
+ ))}
289
+ </div>
290
+ );
291
+ };
292
+
100
293
  return (
101
294
  <>
102
- {/* Toggle button */}
295
+ {/* Toggle button with health indicator */}
103
296
  <button
104
- onclick={() => isOpen(v => !v)}
105
- style="position:fixed;bottom:12px;right:12px;z-index:99999;width:36px;height:36px;border-radius:8px;border:1px solid #2a2a4a;background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;font-weight:800;font-size:14px;cursor:pointer;font-family:ui-monospace,monospace;box-shadow:0 4px 12px rgba(37,99,235,0.3);"
106
- title="What Framework DevTools"
297
+ onclick={() => isOpen((v) => !v)}
298
+ style={() => {
299
+ const health = getHealth();
300
+ return (
301
+ 'position:fixed;bottom:12px;right:12px;z-index:99999;width:36px;height:36px;' +
302
+ 'border-radius:8px;border:1px solid #2a2a4a;' +
303
+ 'background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;' +
304
+ 'font-weight:800;font-size:14px;cursor:pointer;' +
305
+ 'font-family:ui-monospace,monospace;' +
306
+ 'box-shadow:0 4px 12px rgba(37,99,235,0.3),' +
307
+ '0 0 0 2px ' + health.color + ';'
308
+ );
309
+ }}
310
+ title="What Framework DevTools (Ctrl+Shift+D)"
107
311
  >
108
312
  W
109
313
  </button>
110
314
 
111
- {/* Panel conditionally rendered */}
112
- {() => isOpen() ? (
113
- <div style={PANEL_STYLE}>
114
- {/* Header */}
115
- <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;">
116
- <div style="display:flex;align-items:center;gap:8px;">
117
- <span style="font-weight:700;font-size:12px;color:#818cf8;">What DevTools</span>
315
+ {/* Panel -- conditionally rendered */}
316
+ {() =>
317
+ isOpen() ? (
318
+ <div style={PANEL_STYLE}>
319
+ {/* Header */}
320
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;">
321
+ <div style="display:flex;align-items:center;gap:8px;">
322
+ <span style="font-weight:700;font-size:12px;color:#818cf8;">
323
+ What DevTools
324
+ </span>
325
+ <div
326
+ style={() =>
327
+ 'width:8px;height:8px;border-radius:50%;background:' +
328
+ getHealth().color +
329
+ ';'
330
+ }
331
+ title={() => getHealth().label}
332
+ />
333
+ </div>
334
+ <button
335
+ onclick={() => isOpen(false)}
336
+ style="background:none;border:none;color:#6a6a8a;cursor:pointer;font-size:14px;"
337
+ >
338
+ x
339
+ </button>
118
340
  </div>
119
- <button
120
- onclick={() => isOpen(false)}
121
- style="background:none;border:none;color:#6a6a8a;cursor:pointer;font-size:14px;"
122
- >
123
- x
124
- </button>
125
- </div>
126
341
 
127
- {/* Tabs */}
128
- <div style="display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #2a2a4a;">
129
- <button style={tabStyle('signals')} onclick={() => activeTab('signals')}>
130
- Signals ({() => snapshot().signals.length})
131
- </button>
132
- <button style={tabStyle('effects')} onclick={() => activeTab('effects')}>
133
- Effects ({() => snapshot().effects.length})
134
- </button>
135
- <button style={tabStyle('components')} onclick={() => activeTab('components')}>
136
- Components ({() => snapshot().components.length})
137
- </button>
138
- </div>
342
+ {/* Tabs */}
343
+ <div style="display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid #2a2a4a;flex-wrap:wrap;">
344
+ <button
345
+ style={tabStyle('overview')}
346
+ onclick={() => activeTab('overview')}
347
+ >
348
+ Overview
349
+ </button>
350
+ <button
351
+ style={tabStyle('signals')}
352
+ onclick={() => activeTab('signals')}
353
+ >
354
+ Signals ({() => snapshot().signals.length})
355
+ </button>
356
+ <button
357
+ style={tabStyle('effects')}
358
+ onclick={() => activeTab('effects')}
359
+ >
360
+ Effects ({() => snapshot().effects.length})
361
+ </button>
362
+ <button
363
+ style={tabStyle('components')}
364
+ onclick={() => activeTab('components')}
365
+ >
366
+ Components ({() => snapshot().components.length})
367
+ </button>
368
+ <button
369
+ style={tabStyle('errors')}
370
+ onclick={() => activeTab('errors')}
371
+ >
372
+ Errors ({() => recentErrors().length})
373
+ </button>
374
+ </div>
139
375
 
140
- {/* Content */}
141
- <div style="overflow-y:auto;flex:1;">
142
- {() => {
143
- const tab = activeTab();
144
- if (tab === 'signals') return renderSignals();
145
- if (tab === 'effects') return renderEffects();
146
- return renderComponents();
147
- }}
376
+ {/* Content */}
377
+ <div style="overflow-y:auto;flex:1;">
378
+ {() => {
379
+ const tab = activeTab();
380
+ if (tab === 'overview') return renderOverview();
381
+ if (tab === 'signals') return renderSignals();
382
+ if (tab === 'effects') return renderEffects();
383
+ if (tab === 'components') return renderComponents();
384
+ if (tab === 'errors') return renderErrors();
385
+ return renderOverview();
386
+ }}
387
+ </div>
148
388
  </div>
149
- </div>
150
- ) : null}
389
+ ) : null
390
+ }
151
391
  </>
152
392
  );
153
393
  }
@@ -155,7 +395,8 @@ export function DevPanel() {
155
395
  function formatValue(value) {
156
396
  if (value === null) return 'null';
157
397
  if (value === undefined) return 'undefined';
158
- if (typeof value === 'string') return `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"`;
398
+ if (typeof value === 'string')
399
+ return `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"`;
159
400
  if (typeof value === 'object') {
160
401
  try {
161
402
  const str = JSON.stringify(value);