what-devtools 0.6.0 → 0.6.2

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/src/DevPanel.jsx CHANGED
@@ -1,402 +1,175 @@
1
1
  /**
2
2
  * What Framework DevPanel
3
3
  *
4
- * A drop-in floating UI panel that shows live signal values,
5
- * active effects, and mounted components during development.
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
- *
14
- * Usage:
15
- * import { DevPanel } from 'what-devtools/panel';
16
- * // Add to your app:
17
- * <DevPanel />
18
- *
19
- * Works WITHOUT MCP devtools connected.
4
+ * A small floating UI panel for browser-based devtools tests and local debugging.
5
+ * It is intentionally implemented without JSX so the devtools package does not
6
+ * depend on compiler fragment behavior to render its own diagnostics UI.
20
7
  */
21
8
 
22
- import { signal, effect, onCleanup } from 'what-core';
9
+ import { onCleanup } from 'what-core';
23
10
  import { subscribe, getSnapshot, getErrors, installDevTools } from './index.js';
24
11
 
12
+ const MONO = 'ui-monospace,SFMono-Regular,Menlo,monospace';
13
+
25
14
  export function DevPanel() {
26
- // Auto-install devtools if not already done
27
15
  installDevTools();
28
16
 
29
- const isOpen = signal(false);
30
- const activeTab = signal('overview');
31
- const snapshot = signal(getSnapshot());
32
- const recentErrors = signal(getErrors());
17
+ if (typeof document === 'undefined') return null;
18
+
19
+ let activeTab = 'signals';
20
+ let isOpen = false;
21
+
22
+ const root = document.createDocumentFragment();
23
+ const toggle = document.createElement('button');
24
+ toggle.type = 'button';
25
+ toggle.textContent = 'W';
26
+ toggle.title = 'What Framework DevTools (Ctrl+Shift+D)';
27
+ toggle.setAttribute('style',
28
+ 'position:fixed;bottom:12px;right:12px;z-index:99999;width:36px;height:36px;' +
29
+ 'border-radius:8px;border:1px solid #2a2a4a;background:linear-gradient(135deg,#2563eb,#1d4ed8);' +
30
+ `color:#fff;font-weight:800;font-size:14px;cursor:pointer;font-family:${MONO};` +
31
+ 'box-shadow:0 4px 12px rgba(37,99,235,0.3);'
32
+ );
33
33
 
34
- // Subscribe to devtools events and refresh
35
- const unsub = subscribe((event) => {
36
- snapshot(getSnapshot());
37
- if (event === 'error:captured') {
38
- recentErrors(getErrors());
39
- }
40
- });
34
+ const panel = document.createElement('div');
35
+ panel.setAttribute('style',
36
+ 'position:fixed;bottom:0;right:0;width:380px;max-height:55vh;z-index:99998;' +
37
+ `font-family:${MONO};font-size:12px;background:#1a1a2e;color:#e0e0e0;` +
38
+ 'border:1px solid #2a2a4a;border-radius:12px 0 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);' +
39
+ 'display:none;flex-direction:column;overflow:hidden;'
40
+ );
41
41
 
42
- // Poll every 500ms for signal value changes (cheap -- just reads .peek())
43
- const interval = setInterval(() => {
44
- snapshot(getSnapshot());
45
- recentErrors(getErrors());
46
- }, 500);
42
+ root.append(toggle, panel);
43
+
44
+ function setOpen(next) {
45
+ isOpen = next;
46
+ panel.style.display = isOpen ? 'flex' : 'none';
47
+ if (isOpen) renderPanel();
48
+ }
49
+
50
+ toggle.addEventListener('click', () => setOpen(!isOpen));
47
51
 
48
- // Keyboard shortcut: Ctrl+Shift+D / Cmd+Shift+D
49
52
  const onKeyDown = (e) => {
50
53
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') {
51
54
  e.preventDefault();
52
- isOpen((v) => !v);
55
+ setOpen(!isOpen);
53
56
  }
54
57
  };
55
- if (typeof document !== 'undefined') {
56
- document.addEventListener('keydown', onKeyDown);
57
- }
58
+ document.addEventListener('keydown', onKeyDown);
59
+
60
+ const unsub = subscribe(() => {
61
+ if (isOpen) renderPanel();
62
+ });
63
+ const interval = setInterval(() => {
64
+ if (isOpen) renderPanel();
65
+ }, 500);
58
66
 
59
67
  onCleanup(() => {
60
68
  unsub();
61
69
  clearInterval(interval);
62
- if (typeof document !== 'undefined') {
63
- document.removeEventListener('keydown', onKeyDown);
64
- }
70
+ document.removeEventListener('keydown', onKeyDown);
65
71
  });
66
72
 
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
- };
73
+ function renderPanel() {
74
+ panel.replaceChildren(renderHeader(), renderTabs(), renderContent());
75
+ }
80
76
 
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;';
77
+ function renderHeader() {
78
+ const header = document.createElement('div');
79
+ header.setAttribute('style', 'display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;');
87
80
 
88
- const tabStyle = (tab) => () => {
89
- const isActive = activeTab() === tab;
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
- );
97
- };
81
+ const title = document.createElement('span');
82
+ title.textContent = 'What DevTools';
83
+ title.setAttribute('style', 'font-weight:700;font-size:12px;color:#818cf8;');
98
84
 
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();
85
+ const close = document.createElement('button');
86
+ close.type = 'button';
87
+ close.textContent = 'x';
88
+ close.setAttribute('style', 'background:none;border:none;color:#6a6a8a;cursor:pointer;font-size:14px;');
89
+ close.addEventListener('click', () => setOpen(false));
105
90
 
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>
91
+ header.append(title, close);
92
+ return header;
93
+ }
121
94
 
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>
95
+ function renderTabs() {
96
+ const tabs = document.createElement('div');
97
+ tabs.setAttribute('style', 'display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid #2a2a4a;flex-wrap:wrap;');
98
+ for (const tab of ['signals', 'effects', 'components', 'errors']) {
99
+ const button = document.createElement('button');
100
+ button.type = 'button';
101
+ button.textContent = tabLabel(tab);
102
+ button.setAttribute('style', tabStyle(tab));
103
+ button.addEventListener('click', () => {
104
+ activeTab = tab;
105
+ renderPanel();
106
+ });
107
+ tabs.append(button);
108
+ }
109
+ return tabs;
110
+ }
149
111
 
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
- };
112
+ function tabLabel(tab) {
113
+ const snapshot = getSnapshot();
114
+ if (tab === 'signals') return `Signals (${snapshot.signals.length})`;
115
+ if (tab === 'effects') return `Effects (${snapshot.effects.length})`;
116
+ if (tab === 'components') return `Components (${snapshot.components.length})`;
117
+ return `Errors (${getErrors().length})`;
118
+ }
178
119
 
179
- // --- Tab: Signals ---
180
- const renderSignals = () => {
181
- const data = snapshot();
182
- if (!data.signals.length) {
183
- return (
184
- <div style="padding:12px;color:#4a4a6a;">No signals tracked</div>
185
- );
186
- }
187
- return (
188
- <div style="padding:8px;">
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
- >
194
- <span style="color:#818cf8;">{s.name}</span>
195
- <span style="color:#a0a0c0;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
196
- {formatValue(s.value)}
197
- </span>
198
- </div>
199
- ))}
200
- </div>
201
- );
202
- };
120
+ function tabStyle(tab) {
121
+ const selected = activeTab === tab;
122
+ return 'padding:6px 10px;border:none;background:' + (selected ? '#2a2a4a' : 'transparent') +
123
+ ';color:' + (selected ? '#fff' : '#6a6a8a') +
124
+ `;cursor:pointer;font-family:${MONO};font-size:11px;font-weight:600;border-radius:4px;`;
125
+ }
203
126
 
204
- // --- Tab: Effects ---
205
- const renderEffects = () => {
206
- const data = snapshot();
207
- if (!data.effects.length) {
208
- return (
209
- <div style="padding:12px;color:#4a4a6a;">No effects tracked</div>
210
- );
127
+ function renderContent() {
128
+ const content = document.createElement('div');
129
+ content.setAttribute('style', 'overflow-y:auto;flex:1;padding:8px;');
130
+ const snapshot = getSnapshot();
131
+
132
+ if (activeTab === 'signals') {
133
+ renderRows(content, snapshot.signals, (signal) => [signal.name, formatValue(signal.value)], '#818cf8');
134
+ } else if (activeTab === 'effects') {
135
+ renderRows(content, snapshot.effects, (effect) => [effect.name, `runs: ${effect.runCount || 0}`], '#fbbf24');
136
+ } else if (activeTab === 'components') {
137
+ renderRows(content, snapshot.components, (component) => [`<${component.name} />`, ''], '#34d399');
138
+ } else {
139
+ renderRows(content, getErrors(), (error) => [`[${error.type}]`, error.message], '#f87171');
211
140
  }
212
- return (
213
- <div style="padding:8px;">
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
- >
219
- <span style="color:#fbbf24;">{e.name}</span>
220
- <span style="color:#6a6a8a;font-size:10px;">
221
- runs: {e.runCount || 0}
222
- </span>
223
- </div>
224
- ))}
225
- </div>
226
- );
227
- };
228
141
 
229
- // --- Tab: Components ---
230
- const renderComponents = () => {
231
- const data = snapshot();
232
- if (!data.components.length) {
233
- return (
234
- <div style="padding:12px;color:#4a4a6a;">No components tracked</div>
235
- );
142
+ if (!content.childNodes.length) {
143
+ content.textContent = `No ${activeTab} tracked`;
144
+ content.style.color = '#4a4a6a';
145
+ content.style.padding = '12px';
236
146
  }
237
- return (
238
- <div style="padding:8px;">
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>
247
- </div>
248
- ))}
249
- </div>
250
- );
251
- };
147
+ return content;
148
+ }
252
149
 
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
- );
150
+ function renderRows(parent, rows, mapRow, color) {
151
+ for (const row of rows) {
152
+ const [leftText, rightText] = mapRow(row);
153
+ const item = document.createElement('div');
154
+ item.setAttribute('style', 'display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-bottom:1px solid #2a2a4a;gap:12px;');
155
+ const left = document.createElement('span');
156
+ left.textContent = leftText;
157
+ left.setAttribute('style', `color:${color};`);
158
+ const right = document.createElement('span');
159
+ right.textContent = rightText;
160
+ right.setAttribute('style', 'color:#a0a0c0;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;');
161
+ item.append(left, right);
162
+ parent.append(item);
260
163
  }
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
-
293
- return (
294
- <>
295
- {/* Toggle button with health indicator */}
296
- <button
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)"
311
- >
312
- W
313
- </button>
314
-
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>
340
- </div>
341
-
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>
164
+ }
375
165
 
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>
388
- </div>
389
- ) : null
390
- }
391
- </>
392
- );
166
+ return root;
393
167
  }
394
168
 
395
169
  function formatValue(value) {
396
170
  if (value === null) return 'null';
397
171
  if (value === undefined) return 'undefined';
398
- if (typeof value === 'string')
399
- return `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"`;
172
+ if (typeof value === 'string') return `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"`;
400
173
  if (typeof value === 'object') {
401
174
  try {
402
175
  const str = JSON.stringify(value);
package/src/index.js CHANGED
@@ -31,6 +31,10 @@ const subsToSignalId = new WeakMap();
31
31
  const errors = [];
32
32
  const MAX_ERRORS = 100;
33
33
 
34
+ // Hydration mismatch log (capped at 50)
35
+ const hydrationMismatches = [];
36
+ const MAX_HYDRATION_MISMATCHES = 50;
37
+
34
38
  // Event listeners for the DevPanel
35
39
  const listeners = new Set();
36
40
 
@@ -226,12 +230,15 @@ export function unregisterEffect(e) {
226
230
  /**
227
231
  * Capture a runtime error.
228
232
  */
229
- function captureError(err, context) {
233
+ export function captureError(err, typeOrContext, context) {
234
+ const resolvedContext = typeof typeOrContext === 'string'
235
+ ? { ...(context || {}), type: typeOrContext }
236
+ : (typeOrContext || context || {});
230
237
  const entry = {
231
238
  message: err?.message || String(err),
232
239
  stack: err?.stack || null,
233
- type: context?.type || 'unknown',
234
- effectId: context?.effect?._devId || null,
240
+ type: resolvedContext?.type || 'unknown',
241
+ effectId: resolvedContext?.effect?._devId || null,
235
242
  timestamp: Date.now(),
236
243
  };
237
244
  errors.push(entry);
@@ -314,6 +321,7 @@ export function getSnapshot(opts = {}) {
314
321
  effects: effectList,
315
322
  components: componentList,
316
323
  errors: errors.slice(),
324
+ hydrationMismatches: hydrationMismatches.slice(),
317
325
  };
318
326
  }
319
327
 
@@ -328,6 +336,32 @@ export function getErrors(opts = {}) {
328
336
  return errors.slice();
329
337
  }
330
338
 
339
+ /**
340
+ * Get captured hydration mismatches.
341
+ * @param {object} [opts]
342
+ * @param {number} [opts.since] - Only mismatches after this timestamp
343
+ */
344
+ export function getHydrationMismatches(opts = {}) {
345
+ const { since } = opts;
346
+ if (since) return hydrationMismatches.filter(m => m.timestamp > since);
347
+ return hydrationMismatches.slice();
348
+ }
349
+
350
+ /**
351
+ * Reset devtools registries and captured logs.
352
+ */
353
+ export function resetDevTools() {
354
+ signals.clear();
355
+ effects.clear();
356
+ components.clear();
357
+ errors.length = 0;
358
+ hydrationMismatches.length = 0;
359
+ listeners.clear();
360
+ signalId = 0;
361
+ effectId = 0;
362
+ componentId = 0;
363
+ }
364
+
331
365
  /**
332
366
  * Install devtools. Call once at app startup.
333
367
  * Wires into what-core's __DEV__ hooks and exposes `window.__WHAT_DEVTOOLS__`.
@@ -341,10 +375,24 @@ export function installDevTools(core) {
341
375
  const hooks = {
342
376
  onSignalCreate: (sig) => registerSignal(sig),
343
377
  onSignalUpdate: (sig) => notifySignalUpdate(sig),
378
+ onSignalDispose: (sig) => unregisterSignal(sig),
344
379
  onEffectCreate: (e) => registerEffect(e),
345
380
  onEffectDispose: (e) => unregisterEffect(e),
346
381
  onEffectRun: (e) => trackEffectRun(e),
347
382
  onError: (err, context) => captureError(err, context),
383
+ onHydrationMismatch: (info) => {
384
+ const entry = {
385
+ type: 'hydration_mismatch',
386
+ component: info.component,
387
+ expected: info.expected,
388
+ actual: info.actual,
389
+ mismatchCount: info.mismatchCount,
390
+ timestamp: Date.now(),
391
+ };
392
+ hydrationMismatches.push(entry);
393
+ if (hydrationMismatches.length > MAX_HYDRATION_MISMATCHES) hydrationMismatches.shift();
394
+ emit('hydration:mismatch', entry);
395
+ },
348
396
  onComponentMount: (ctx) => {
349
397
  const name = ctx.Component?.displayName || ctx.Component?.name || 'Anonymous';
350
398
  const parentDevId = ctx._parentCtx?._devId || null;
@@ -362,11 +410,13 @@ export function installDevTools(core) {
362
410
  if (typeof window !== 'undefined') window.__WHAT_CORE__ = core;
363
411
  } else {
364
412
  try {
365
- import('what-core').then(mod => {
413
+ import('what-core/devtools').then(mod => {
366
414
  if (mod.__setDevToolsHooks) mod.__setDevToolsHooks(hooks);
367
- if (typeof window !== 'undefined') window.__WHAT_CORE__ = mod;
368
- }).catch(() => {});
369
- } catch {}
415
+ if (typeof window !== 'undefined') window.__WHAT_CORE_DEVTOOLS__ = mod;
416
+ }).catch((error) => warnDevToolsImportFailure(error));
417
+ } catch (error) {
418
+ warnDevToolsImportFailure(error);
419
+ }
370
420
  }
371
421
 
372
422
  if (typeof window !== 'undefined') {
@@ -375,13 +425,26 @@ export function installDevTools(core) {
375
425
  get effects() { return getSnapshot().effects; },
376
426
  get components() { return getSnapshot().components; },
377
427
  get errors() { return getErrors(); },
428
+ get hydrationMismatches() { return getHydrationMismatches(); },
378
429
  getSnapshot,
379
430
  getErrors,
431
+ getHydrationMismatches,
380
432
  subscribe,
381
433
  safeSerialize,
382
- _registries: { signals, effects, components, errors },
434
+ captureError,
435
+ resetDevTools,
436
+ _registries: { signals, effects, components, errors, hydrationMismatches },
383
437
  };
384
438
  }
385
439
  }
386
440
 
387
- export { signals, effects, components, errors };
441
+ export { signals, effects, components, errors, hydrationMismatches };
442
+
443
+ function warnDevToolsImportFailure(error) {
444
+ const isDev = typeof process === 'undefined' || process.env?.NODE_ENV !== 'production';
445
+ if (!isDev || typeof console === 'undefined') return;
446
+ console.warn(
447
+ '[what-devtools] Could not import what-core/devtools. Pass installDevTools({ __setDevToolsHooks }) or verify package subpath exports.',
448
+ error
449
+ );
450
+ }