webspresso 0.0.109 → 0.0.111

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.
@@ -0,0 +1,1239 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Client DevTools Dock & Inspector
5
+ *
6
+ * Integrated development-mode toolbar and inspector for Webspresso client applications:
7
+ * - 🎮 Controllers: Live reactive state, computed values, bound forms, tasks (with manual trigger), watchers
8
+ * - 🧩 Fragments: Live SSR fragments, input parameters, dependsOn tags, on-demand refresh & DOM highlight
9
+ * - 🌐 Network & Causal Chains: Client API timeline, concurrency policies, invalidation-to-patch chains
10
+ * - 💾 Storage: Webspresso namespaced local/session storage viewer and cleaner
11
+ * - 🛠️ Studio & Server: Direct link to Webspresso DB Studio, Admin Panel, and SSE live-reload status
12
+ * - 🔌 Redux DevTools Bridge: Auto-connects to browser Redux DevTools extension if present
13
+ * - ⌨️ Hotkey: Alt+W (or Cmd+Shift+D) toggles the dock
14
+ * - 🖥️ Console API: window.__WEBSPRESSO_DEVTOOLS__
15
+ */
16
+
17
+ const { defaultStorage } = require('./storage');
18
+ const { isArray, isNumber } = require('./utils');
19
+
20
+ const DEVTOOL_HOST_ID = 'webspresso-client-devtools-host';
21
+
22
+ const PKG_VERSION =
23
+ (typeof window !== 'undefined' &&
24
+ window.__WEBSPRESSO_DEV_META__ &&
25
+ window.__WEBSPRESSO_DEV_META__.version) ||
26
+ '0.0.110';
27
+
28
+ /**
29
+ * Connect to Redux DevTools browser extension if installed.
30
+ */
31
+ function setupReduxDevToolsBridge(runtime) {
32
+ if (typeof window === 'undefined' || !window.__REDUX_DEVTOOLS_EXTENSION__) {
33
+ return null;
34
+ }
35
+ try {
36
+ const devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
37
+ name: 'Webspresso Client',
38
+ features: {
39
+ jump: false,
40
+ skip: false,
41
+ reorder: false,
42
+ dispatch: false,
43
+ test: false,
44
+ },
45
+ });
46
+
47
+ function getCombinedState() {
48
+ const stateObj = {};
49
+ const instances = runtime.controllers ? runtime.controllers.getActiveInstances() : [];
50
+ for (const inst of instances) {
51
+ stateObj[inst.name || inst.instanceId] = inst.state ? { ...inst.state } : {};
52
+ }
53
+ return stateObj;
54
+ }
55
+
56
+ devTools.init(getCombinedState());
57
+
58
+ // Dispatches state snapshot when events occur
59
+ if (runtime.debug && typeof runtime.debug.getSnapshot === 'function') {
60
+ const origRecordEvent = runtime.debug.recordEvent;
61
+ if (typeof origRecordEvent === 'function') {
62
+ runtime.debug.recordEvent = function (type, detail) {
63
+ const res = origRecordEvent.call(this, type, detail);
64
+ try {
65
+ devTools.send({ type: `[Webspresso] ${type}`, ...detail }, getCombinedState());
66
+ } catch (_) {}
67
+ return res;
68
+ };
69
+ }
70
+ }
71
+
72
+ return devTools;
73
+ } catch (_) {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Format JSON with syntax-colored HTML or safe text.
80
+ */
81
+ function formatJsonHtml(data) {
82
+ if (data === undefined) return '<span style="color:#71717a">undefined</span>';
83
+ if (data === null) return '<span style="color:#f43f5e">null</span>';
84
+ try {
85
+ const jsonStr = JSON.stringify(data, null, 2);
86
+ return jsonStr
87
+ .replace(/&/g, '&amp;')
88
+ .replace(/</g, '&lt;')
89
+ .replace(/>/g, '&gt;')
90
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => {
91
+ let cls = 'color:#38bdf8;'; // number
92
+ if (/^"/.test(match)) {
93
+ if (/:$/.test(match)) {
94
+ cls = 'color:#a78bfa;font-weight:600;'; // key
95
+ } else {
96
+ cls = 'color:#34d399;'; // string
97
+ }
98
+ } else if (/true|false/.test(match)) {
99
+ cls = 'color:#fbbf24;'; // boolean
100
+ } else if (/null/.test(match)) {
101
+ cls = 'color:#f43f5e;'; // null
102
+ }
103
+ return `<span style="${cls}">${match}</span>`;
104
+ });
105
+ } catch (_) {
106
+ return String(data);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Highlight a DOM element with a temporary pulsing outline.
112
+ */
113
+ function highlightElement(el) {
114
+ if (!el || !el.style) return false;
115
+ try {
116
+ if (typeof el.scrollIntoView === 'function') {
117
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
118
+ }
119
+ const origOutline = el.style.outline;
120
+ const origOutlineOffset = el.style.outlineOffset;
121
+ const origBoxShadow = el.style.boxShadow;
122
+ const origTransition = el.style.transition;
123
+
124
+ el.style.transition = 'outline 0.2s, box-shadow 0.2s';
125
+ el.style.outline = '3px solid #f59e0b';
126
+ el.style.outlineOffset = '4px';
127
+ el.style.boxShadow = '0 0 25px rgba(245, 158, 11, 0.7)';
128
+
129
+ setTimeout(() => {
130
+ try {
131
+ el.style.outline = origOutline;
132
+ el.style.outlineOffset = origOutlineOffset;
133
+ el.style.boxShadow = origBoxShadow;
134
+ el.style.transition = origTransition;
135
+ } catch (_) {}
136
+ }, 2000);
137
+ return true;
138
+ } catch (_) {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Mount the Webspresso Client DevTool onto the document.
145
+ *
146
+ * @param {object} runtime - Webspresso Client Runtime instance
147
+ * @param {object} [options]
148
+ * @param {string} [options.position='bottom-right'] - 'bottom-right' | 'bottom-left'
149
+ * @param {boolean} [options.open=false] - Initial drawer open state
150
+ * @param {number} [options.pollInterval=1500] - Snapshot refresh interval (ms)
151
+ * @param {string} [options.studioUrl='http://localhost:4567'] - URL to Webspresso DB Studio
152
+ * @returns {object} DevTools handle with open(), close(), toggle(), destroy()
153
+ */
154
+ function mountDevtool(runtime, options = {}) {
155
+ if (typeof document === 'undefined') {
156
+ return {
157
+ open: () => {},
158
+ close: () => {},
159
+ toggle: () => {},
160
+ destroy: () => {},
161
+ isOpen: () => false,
162
+ getSnapshot: () => ({}),
163
+ };
164
+ }
165
+
166
+ // Prevent multiple devtool instances on the same page
167
+ const existing = document.getElementById(DEVTOOL_HOST_ID);
168
+ if (existing) {
169
+ if (existing.__webspressoDevtoolInstance) {
170
+ return existing.__webspressoDevtoolInstance;
171
+ }
172
+ existing.remove();
173
+ }
174
+
175
+ const position = options.position || 'bottom-right';
176
+ const isRight = position.includes('right');
177
+ const studioUrl = options.studioUrl || 'http://localhost:4567';
178
+ let metaOverrides =
179
+ options.meta && typeof options.meta === 'object' ? { ...options.meta } : {};
180
+ let isOpen = Boolean(options.open);
181
+ let activeTab = 'controllers'; // 'controllers' | 'fragments' | 'network' | 'storage' | 'studio'
182
+ let selectedControllerId = null;
183
+
184
+ function resolveDevFeatures() {
185
+ const winMeta =
186
+ typeof window !== 'undefined' &&
187
+ window.__WEBSPRESSO_DEV_META__ &&
188
+ typeof window.__WEBSPRESSO_DEV_META__ === 'object'
189
+ ? window.__WEBSPRESSO_DEV_META__
190
+ : {};
191
+ const merged = { ...winMeta, ...metaOverrides };
192
+ const pluginsList = isArray(merged.plugins)
193
+ ? merged.plugins
194
+ : isArray(options.plugins)
195
+ ? options.plugins
196
+ : [];
197
+
198
+ const hasStudio =
199
+ options.hasStudio !== undefined
200
+ ? Boolean(options.hasStudio)
201
+ : merged.hasStudio !== undefined
202
+ ? Boolean(merged.hasStudio)
203
+ : merged.hasDb !== undefined
204
+ ? Boolean(merged.hasDb)
205
+ : Boolean(
206
+ options.studioUrl ||
207
+ pluginsList.includes('studio') ||
208
+ pluginsList.includes('db-studio')
209
+ );
210
+
211
+ const hasAdmin =
212
+ options.hasAdmin !== undefined
213
+ ? Boolean(options.hasAdmin)
214
+ : merged.hasAdmin !== undefined
215
+ ? Boolean(merged.hasAdmin)
216
+ : Boolean(
217
+ options.adminPath ||
218
+ options.adminUrl ||
219
+ pluginsList.includes('admin-panel') ||
220
+ pluginsList.includes('adminPanel') ||
221
+ pluginsList.includes('admin')
222
+ );
223
+
224
+ const adminPath = merged.adminPath || options.adminPath || options.adminUrl || '/_admin';
225
+ return {
226
+ hasStudio,
227
+ hasAdmin,
228
+ adminPath,
229
+ plugins: pluginsList,
230
+ };
231
+ }
232
+
233
+ // Initialize Redux DevTools extension bridge if available
234
+ const reduxBridge = setupReduxDevToolsBridge(runtime);
235
+
236
+ // Host container
237
+ const host = document.createElement('div');
238
+ host.id = DEVTOOL_HOST_ID;
239
+ host.style.cssText = 'position:fixed;z-index:999998;pointer-events:none;';
240
+
241
+ // Use Shadow DOM to completely isolate styles from user CSS
242
+ const shadow = typeof host.attachShadow === 'function' ? host.attachShadow({ mode: 'open' }) : host;
243
+
244
+ // Style block
245
+ const styleEl = document.createElement('style');
246
+ styleEl.textContent = `
247
+ * { box-sizing: border-box; margin: 0; padding: 0; }
248
+ .ws-devtool-root {
249
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
250
+ font-size: 12px;
251
+ color: #f4f4f5;
252
+ line-height: 1.5;
253
+ pointer-events: auto;
254
+ }
255
+ .ws-pill {
256
+ position: fixed;
257
+ bottom: 16px;
258
+ ${isRight ? 'right: 16px;' : 'left: 16px;'}
259
+ background: #18181b;
260
+ border: 1px solid #3f3f46;
261
+ border-radius: 9999px;
262
+ padding: 6px 14px;
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 8px;
266
+ cursor: pointer;
267
+ box-shadow: 0 10px 25px -5px rgba(0,0,0,0.5), 0 0 10px rgba(0,0,0,0.3);
268
+ user-select: none;
269
+ transition: all 0.2s ease;
270
+ z-index: 999998;
271
+ }
272
+ .ws-pill:hover {
273
+ background: #27272a;
274
+ border-color: #f59e0b;
275
+ transform: translateY(-1px);
276
+ box-shadow: 0 12px 30px -5px rgba(245,158,11,0.25);
277
+ }
278
+ .ws-pill-badge {
279
+ display: inline-block;
280
+ padding: 2px 6px;
281
+ border-radius: 9999px;
282
+ font-size: 10px;
283
+ font-weight: 700;
284
+ background: #27272a;
285
+ color: #a1a1aa;
286
+ }
287
+ .ws-dot {
288
+ width: 8px;
289
+ height: 8px;
290
+ border-radius: 50%;
291
+ background: #10b981;
292
+ display: inline-block;
293
+ box-shadow: 0 0 8px #10b981;
294
+ }
295
+ .ws-drawer {
296
+ position: fixed;
297
+ bottom: 16px;
298
+ ${isRight ? 'right: 16px;' : 'left: 16px;'}
299
+ width: min(780px, calc(100vw - 32px));
300
+ height: 520px;
301
+ max-height: calc(100vh - 32px);
302
+ background: #09090b;
303
+ border: 1px solid #27272a;
304
+ border-radius: 12px;
305
+ box-shadow: 0 25px 50px -12px rgba(0,0,0,0.7), 0 0 30px rgba(0,0,0,0.5);
306
+ display: flex;
307
+ flex-direction: column;
308
+ overflow: hidden;
309
+ z-index: 999999;
310
+ }
311
+ .ws-drawer.ws-drawer-enter {
312
+ animation: wsFadeIn 0.15s ease-out;
313
+ }
314
+ @keyframes wsFadeIn {
315
+ from { opacity: 0; transform: scale(0.98) translateY(6px); }
316
+ to { opacity: 1; transform: scale(1) translateY(0); }
317
+ }
318
+ .ws-header {
319
+ background: #121215;
320
+ border-bottom: 1px solid #27272a;
321
+ padding: 10px 14px;
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: space-between;
325
+ gap: 12px;
326
+ user-select: none;
327
+ }
328
+ .ws-title {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 8px;
332
+ font-weight: 700;
333
+ font-size: 13px;
334
+ color: #fbbf24;
335
+ }
336
+ .ws-tabs {
337
+ display: flex;
338
+ gap: 4px;
339
+ overflow-x: auto;
340
+ scrollbar-width: none;
341
+ }
342
+ .ws-tabs::-webkit-scrollbar { display: none; }
343
+ .ws-tab-btn {
344
+ background: transparent;
345
+ border: 1px solid transparent;
346
+ color: #a1a1aa;
347
+ padding: 5px 10px;
348
+ border-radius: 6px;
349
+ cursor: pointer;
350
+ font-family: inherit;
351
+ font-size: 11px;
352
+ font-weight: 600;
353
+ display: flex;
354
+ align-items: center;
355
+ gap: 6px;
356
+ transition: all 0.15s;
357
+ }
358
+ .ws-tab-btn:hover {
359
+ background: #27272a;
360
+ color: #f4f4f5;
361
+ }
362
+ .ws-tab-btn.active {
363
+ background: #27272a;
364
+ border-color: #3f3f46;
365
+ color: #fbbf24;
366
+ }
367
+ .ws-btn {
368
+ background: #27272a;
369
+ border: 1px solid #3f3f46;
370
+ color: #f4f4f5;
371
+ padding: 4px 10px;
372
+ border-radius: 6px;
373
+ cursor: pointer;
374
+ font-family: inherit;
375
+ font-size: 11px;
376
+ font-weight: 600;
377
+ display: inline-flex;
378
+ align-items: center;
379
+ gap: 6px;
380
+ transition: all 0.15s;
381
+ }
382
+ .ws-btn:hover {
383
+ background: #3f3f46;
384
+ border-color: #71717a;
385
+ color: #fff;
386
+ }
387
+ .ws-btn-primary {
388
+ background: #d97706;
389
+ border-color: #f59e0b;
390
+ color: #fff;
391
+ }
392
+ .ws-btn-primary:hover {
393
+ background: #f59e0b;
394
+ }
395
+ .ws-btn-sm {
396
+ padding: 2px 7px;
397
+ font-size: 10px;
398
+ }
399
+ .ws-content {
400
+ flex: 1;
401
+ overflow-y: auto;
402
+ padding: 14px;
403
+ background: #09090b;
404
+ }
405
+ .ws-footer {
406
+ background: #121215;
407
+ border-top: 1px solid #27272a;
408
+ padding: 6px 14px;
409
+ display: flex;
410
+ align-items: center;
411
+ justify-content: space-between;
412
+ font-size: 11px;
413
+ color: #71717a;
414
+ }
415
+ .ws-code-block {
416
+ background: #18181b;
417
+ border: 1px solid #27272a;
418
+ border-radius: 8px;
419
+ padding: 10px;
420
+ font-size: 11px;
421
+ overflow-x: auto;
422
+ white-space: pre-wrap;
423
+ word-break: break-all;
424
+ }
425
+ .ws-card {
426
+ background: #18181b;
427
+ border: 1px solid #27272a;
428
+ border-radius: 8px;
429
+ padding: 12px;
430
+ margin-bottom: 10px;
431
+ }
432
+ .ws-badge {
433
+ display: inline-block;
434
+ padding: 1px 6px;
435
+ border-radius: 4px;
436
+ font-size: 10px;
437
+ font-weight: 700;
438
+ text-transform: uppercase;
439
+ }
440
+ .ws-badge-success { background: #064e3b; color: #34d399; }
441
+ .ws-badge-warning { background: #78350f; color: #fbbf24; }
442
+ .ws-badge-info { background: #1e3a8a; color: #60a5fa; }
443
+ .ws-badge-danger { background: #881337; color: #fb7185; }
444
+ `;
445
+ shadow.appendChild(styleEl);
446
+
447
+ const container = document.createElement('div');
448
+ container.className = 'ws-devtool-root';
449
+ shadow.appendChild(container);
450
+
451
+ // --- Snapshot Data Fetchers ---
452
+ function getControllersSnapshot() {
453
+ if (!runtime.controllers) return [];
454
+ return runtime.controllers.getActiveInstances().map((inst) => ({
455
+ instanceId: inst.instanceId,
456
+ name: inst.name,
457
+ tagName: inst.el ? inst.el.tagName.toLowerCase() : 'div',
458
+ isConnected: Boolean(inst.isConnected),
459
+ state: inst.state ? { ...inst.state } : {},
460
+ el: inst.el,
461
+ actions: inst.actions ? Object.keys(inst.actions) : [],
462
+ }));
463
+ }
464
+
465
+ function getFragmentsSnapshot() {
466
+ if (!runtime.fragments) return [];
467
+ return runtime.fragments.getInstances().map((inst) => ({
468
+ instanceId: inst.instanceId,
469
+ name: inst.name,
470
+ input: inst.input ? { ...inst.input } : {},
471
+ dependsOn: inst.dependsOn ? Array.from(inst.dependsOn) : [],
472
+ version: inst.version || 1,
473
+ pagePath: inst.pagePath || '/',
474
+ el: inst.el,
475
+ }));
476
+ }
477
+
478
+ function getNetworkSnapshot() {
479
+ if (!runtime.debug || typeof runtime.debug.getSnapshot !== 'function') return [];
480
+ const snap = runtime.debug.getSnapshot();
481
+ return {
482
+ activeRequests: snap.activeRequests || [],
483
+ causalChains: snap.causalChains || [],
484
+ fragmentTimings: snap.fragmentTimings || [],
485
+ events: snap.events || [],
486
+ };
487
+ }
488
+
489
+ function getStorageSnapshot() {
490
+ const localEntries = [];
491
+ const sessionEntries = [];
492
+ try {
493
+ if (typeof localStorage !== 'undefined') {
494
+ for (let i = 0; i < localStorage.length; i++) {
495
+ const k = localStorage.key(i);
496
+ if (k && k.startsWith('w:')) {
497
+ localEntries.push({ key: k, value: localStorage.getItem(k) });
498
+ }
499
+ }
500
+ }
501
+ if (typeof sessionStorage !== 'undefined') {
502
+ for (let i = 0; i < sessionStorage.length; i++) {
503
+ const k = sessionStorage.key(i);
504
+ if (k && k.startsWith('w:')) {
505
+ sessionEntries.push({ key: k, value: sessionStorage.getItem(k) });
506
+ }
507
+ }
508
+ }
509
+ } catch (_) {}
510
+ return { localEntries, sessionEntries };
511
+ }
512
+
513
+ // --- Renderers ---
514
+ function renderPill() {
515
+ const controllers = getControllersSnapshot();
516
+ const fragments = getFragmentsSnapshot();
517
+ return `
518
+ <div class="ws-pill" id="ws-devtool-pill" title="Toggle Webspresso Client DevTools (Alt+W)">
519
+ <span style="font-weight:700;color:#fbbf24;">⚡ Webspresso</span>
520
+ <span class="ws-pill-badge">${controllers.length} ctrl${controllers.length !== 1 ? 's' : ''}</span>
521
+ <span class="ws-pill-badge">${fragments.length} frag${fragments.length !== 1 ? 's' : ''}</span>
522
+ <span class="ws-dot" title="Dev server connected"></span>
523
+ </div>
524
+ `;
525
+ }
526
+
527
+ function renderControllersTab(controllers) {
528
+ if (controllers.length === 0) {
529
+ return `
530
+ <div class="ws-card" style="text-align:center;padding:32px;color:#a1a1aa;">
531
+ <p style="font-size:14px;margin-bottom:6px;">No active controllers found on this page.</p>
532
+ <p style="font-size:11px;color:#71717a;">Define a controller with <code>defineController(...)</code> and mount with <code>data-controller="..."</code>.</p>
533
+ </div>
534
+ `;
535
+ }
536
+
537
+ if (!selectedControllerId || !controllers.find((c) => c.instanceId === selectedControllerId)) {
538
+ selectedControllerId = controllers[0].instanceId;
539
+ }
540
+ const current = controllers.find((c) => c.instanceId === selectedControllerId) || controllers[0];
541
+
542
+ return `
543
+ <div style="display:grid;grid-template-columns:220px 1fr;gap:14px;height:100%;">
544
+ <!-- Sidebar list -->
545
+ <div style="border-right:1px solid #27272a;padding-right:12px;overflow-y:auto;">
546
+ <div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#71717a;margin-bottom:8px;">Mounted Controllers (${controllers.length})</div>
547
+ ${controllers
548
+ .map(
549
+ (c) => `
550
+ <div
551
+ class="ws-card ws-controller-item"
552
+ data-inst-id="${c.instanceId}"
553
+ style="cursor:pointer;padding:8px 10px;margin-bottom:6px;${
554
+ c.instanceId === current.instanceId ? 'border-color:#f59e0b;background:#27272a;' : ''
555
+ }">
556
+ <div style="font-weight:700;color:#f4f4f5;">${c.name || 'anonymous'}</div>
557
+ <div style="font-size:10px;color:#a1a1aa;">&lt;${c.tagName}&gt; • ${c.instanceId}</div>
558
+ </div>
559
+ `
560
+ )
561
+ .join('')}
562
+ </div>
563
+
564
+ <!-- Details pane -->
565
+ <div style="overflow-y:auto;padding-right:4px;">
566
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
567
+ <div>
568
+ <h3 style="font-size:14px;font-weight:700;color:#fff;">${current.name || 'anonymous'}</h3>
569
+ <p style="font-size:10px;color:#71717a;">Instance: ${current.instanceId} • Connected: ${current.isConnected ? '✅' : '❌'}</p>
570
+ </div>
571
+ <button class="ws-btn ws-btn-sm" id="ws-btn-highlight-ctrl" data-inst-id="${current.instanceId}">
572
+ 🎯 Highlight in DOM
573
+ </button>
574
+ </div>
575
+
576
+ <!-- Reactive State -->
577
+ <div class="ws-card">
578
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
579
+ <strong style="color:#fbbf24;font-size:11px;">REACTIVE STATE</strong>
580
+ <button class="ws-btn ws-btn-sm" id="ws-btn-copy-state" data-inst-id="${current.instanceId}">📋 Copy</button>
581
+ </div>
582
+ <div class="ws-code-block">${formatJsonHtml(current.state)}</div>
583
+ </div>
584
+ </div>
585
+ </div>
586
+ `;
587
+ }
588
+
589
+ function renderFragmentsTab(fragments) {
590
+ if (fragments.length === 0) {
591
+ return `
592
+ <div class="ws-card" style="text-align:center;padding:32px;color:#a1a1aa;">
593
+ <p style="font-size:14px;margin-bottom:6px;">No SSR fragments found in DOM.</p>
594
+ <p style="font-size:11px;color:#71717a;">Mark elements with <code>data-w-fragment="name"</code> to enable targeted server partial refreshes.</p>
595
+ </div>
596
+ `;
597
+ }
598
+
599
+ return `
600
+ <div>
601
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
602
+ <div>
603
+ <strong style="font-size:13px;color:#fff;">Active SSR Fragments (${fragments.length})</strong>
604
+ <p style="font-size:10px;color:#71717a;">Targeted server-rendered DOM segments that can be refreshed independently.</p>
605
+ </div>
606
+ <button class="ws-btn ws-btn-primary ws-btn-sm" id="ws-btn-refresh-all-frags">
607
+ 🔄 Refresh All
608
+ </button>
609
+ </div>
610
+
611
+ ${fragments
612
+ .map(
613
+ (f) => `
614
+ <div class="ws-card" style="margin-bottom:10px;">
615
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
616
+ <div>
617
+ <strong style="color:#38bdf8;font-size:12px;">${f.name}</strong>
618
+ <span style="font-size:10px;color:#71717a;margin-left:6px;">#${f.instanceId}</span>
619
+ </div>
620
+ <div style="display:flex;gap:6px;">
621
+ <button class="ws-btn ws-btn-sm" id="ws-btn-highlight-frag" data-inst-id="${f.instanceId}">
622
+ 🎯 Highlight
623
+ </button>
624
+ <button class="ws-btn ws-btn-primary ws-btn-sm" id="ws-btn-refresh-frag" data-frag-name="${f.name}">
625
+ 🔄 Refresh
626
+ </button>
627
+ </div>
628
+ </div>
629
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:11px;">
630
+ <div>
631
+ <span style="color:#a1a1aa;">Depends On Tags:</span>
632
+ ${
633
+ f.dependsOn.length > 0
634
+ ? f.dependsOn.map((t) => `<span class="ws-badge ws-badge-info" style="margin-left:4px;">${t}</span>`).join('')
635
+ : '<span style="color:#71717a;margin-left:4px;">(none)</span>'
636
+ }
637
+ </div>
638
+ <div>
639
+ <span style="color:#a1a1aa;">Version:</span>
640
+ <span style="color:#f4f4f5;margin-left:4px;">${f.version}</span>
641
+ </div>
642
+ </div>
643
+ ${
644
+ Object.keys(f.input).length > 0
645
+ ? `<div style="margin-top:6px;"><span style="color:#a1a1aa;font-size:10px;">Parameters:</span><div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(
646
+ f.input
647
+ )}</div></div>`
648
+ : ''
649
+ }
650
+ </div>
651
+ `
652
+ )
653
+ .join('')}
654
+ </div>
655
+ `;
656
+ }
657
+
658
+ function renderNetworkTab(network) {
659
+ const { activeRequests, causalChains, fragmentTimings } = network;
660
+ return `
661
+ <div>
662
+ <div style="margin-bottom:12px;">
663
+ <strong style="font-size:13px;color:#fff;">Client Network & Causal Invalidation Chains</strong>
664
+ <p style="font-size:10px;color:#71717a;">Tracks API calls, concurrency policies (latest, drop), and mutation-to-patch timings.</p>
665
+ </div>
666
+
667
+ <div style="margin-bottom:14px;">
668
+ <div style="font-size:11px;font-weight:700;color:#fbbf24;margin-bottom:6px;">IN-FLIGHT & RECENT REQUESTS</div>
669
+ ${
670
+ activeRequests.length === 0
671
+ ? '<div class="ws-card" style="padding:10px;color:#71717a;">No active or tracked requests in ring buffer.</div>'
672
+ : activeRequests
673
+ .map(
674
+ (r) => `
675
+ <div class="ws-card" style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;margin-bottom:6px;">
676
+ <div>
677
+ <span class="ws-badge ws-badge-${r.method === 'GET' ? 'info' : 'warning'}">${r.method}</span>
678
+ <span style="font-weight:600;margin-left:6px;">${r.url}</span>
679
+ </div>
680
+ <div style="font-size:10px;color:#a1a1aa;">
681
+ Policy: <span style="color:#38bdf8;">${r.policy || 'none'}</span> • Status: <span style="color:#34d399;">${r.status}</span>
682
+ </div>
683
+ </div>
684
+ `
685
+ )
686
+ .join('')
687
+ }
688
+ </div>
689
+
690
+ ${
691
+ causalChains.length > 0
692
+ ? `
693
+ <div style="margin-bottom:14px;">
694
+ <div style="font-size:11px;font-weight:700;color:#a78bfa;margin-bottom:6px;">CAUSAL CHAINS (MUTATION ➔ REFRESH ➔ PATCH)</div>
695
+ ${causalChains
696
+ .map(
697
+ (c) => `
698
+ <div class="ws-card" style="padding:10px;margin-bottom:6px;">
699
+ <div style="font-weight:600;color:#f4f4f5;font-size:11px;">Chain: ${c.chainId}</div>
700
+ <div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(c)}</div>
701
+ </div>
702
+ `
703
+ )
704
+ .join('')}
705
+ </div>
706
+ `
707
+ : ''
708
+ }
709
+
710
+ ${
711
+ fragmentTimings.length > 0
712
+ ? `
713
+ <div>
714
+ <div style="font-size:11px;font-weight:700;color:#34d399;margin-bottom:6px;">RECENT FRAGMENT TIMINGS</div>
715
+ <div class="ws-card" style="padding:8px 12px;">
716
+ ${fragmentTimings
717
+ .slice(-5)
718
+ .map(
719
+ (t) => `
720
+ <div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid #27272a;font-size:11px;">
721
+ <span>${t.fragmentName || t.instanceId}</span>
722
+ <span style="color:#fbbf24;">patch: ${t.patchMs}ms ${t.loadMs ? `(load: ${t.loadMs}ms, render: ${t.renderMs}ms)` : ''}</span>
723
+ </div>
724
+ `
725
+ )
726
+ .join('')}
727
+ </div>
728
+ </div>
729
+ `
730
+ : ''
731
+ }
732
+ </div>
733
+ `;
734
+ }
735
+
736
+ function renderStorageTab(storage) {
737
+ const { localEntries, sessionEntries } = storage;
738
+ const totalCount = localEntries.length + sessionEntries.length;
739
+
740
+ return `
741
+ <div>
742
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
743
+ <div>
744
+ <strong style="font-size:13px;color:#fff;">Webspresso Storage Inspector (${totalCount})</strong>
745
+ <p style="font-size:10px;color:#71717a;">Inspects namespaced keys (<code>w:*</code>) in localStorage and sessionStorage.</p>
746
+ </div>
747
+ <button class="ws-btn ws-btn-sm" id="ws-btn-clear-storage" style="color:#fb7185;border-color:#881337;">
748
+ 🗑️ Clear Webspresso Storage
749
+ </button>
750
+ </div>
751
+
752
+ <div style="margin-bottom:14px;">
753
+ <div style="font-size:11px;font-weight:700;color:#38bdf8;margin-bottom:6px;">LOCAL STORAGE (w:*)</div>
754
+ ${
755
+ localEntries.length === 0
756
+ ? '<div class="ws-card" style="padding:8px 12px;color:#71717a;">No Webspresso entries in localStorage.</div>'
757
+ : localEntries
758
+ .map(
759
+ (e) => `
760
+ <div class="ws-card" style="margin-bottom:6px;">
761
+ <div style="font-weight:600;color:#38bdf8;font-size:11px;">${e.key}</div>
762
+ <div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(
763
+ (() => {
764
+ try {
765
+ return JSON.parse(e.value);
766
+ } catch (_) {
767
+ return e.value;
768
+ }
769
+ })()
770
+ )}</div>
771
+ </div>
772
+ `
773
+ )
774
+ .join('')
775
+ }
776
+ </div>
777
+
778
+ <div>
779
+ <div style="font-size:11px;font-weight:700;color:#a78bfa;margin-bottom:6px;">SESSION STORAGE (w:*)</div>
780
+ ${
781
+ sessionEntries.length === 0
782
+ ? '<div class="ws-card" style="padding:8px 12px;color:#71717a;">No Webspresso entries in sessionStorage.</div>'
783
+ : sessionEntries
784
+ .map(
785
+ (e) => `
786
+ <div class="ws-card" style="margin-bottom:6px;">
787
+ <div style="font-weight:600;color:#a78bfa;font-size:11px;">${e.key}</div>
788
+ <div class="ws-code-block" style="margin-top:4px;padding:6px;">${formatJsonHtml(
789
+ (() => {
790
+ try {
791
+ return JSON.parse(e.value);
792
+ } catch (_) {
793
+ return e.value;
794
+ }
795
+ })()
796
+ )}</div>
797
+ </div>
798
+ `
799
+ )
800
+ .join('')
801
+ }
802
+ </div>
803
+ </div>
804
+ `;
805
+ }
806
+
807
+ function renderStudioTab() {
808
+ const features = resolveDevFeatures();
809
+
810
+ const studioCardHtml = features.hasStudio
811
+ ? `
812
+ <div class="ws-card" style="margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;">
813
+ <div>
814
+ <div style="font-weight:700;color:#fbbf24;font-size:13px;">🗄️ Webspresso DB Studio</div>
815
+ <div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Visual database browser, query runner, and table inspector.</div>
816
+ <div style="font-size:9px;color:#71717a;margin-top:4px;">Run in terminal: <code>npx webspresso studio</code> (port 4567)</div>
817
+ </div>
818
+ <a href="${studioUrl}" target="_blank" rel="noopener noreferrer" class="ws-btn ws-btn-primary" id="ws-btn-open-studio" style="text-decoration:none;">
819
+ Open DB Studio ↗
820
+ </a>
821
+ </div>
822
+ `
823
+ : '';
824
+
825
+ const adminCardHtml = features.hasAdmin
826
+ ? `
827
+ <div class="ws-card" style="margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;">
828
+ <div>
829
+ <div style="font-weight:700;color:#38bdf8;font-size:13px;">⚙️ Admin Panel SPA</div>
830
+ <div style="font-size:10px;color:#a1a1aa;margin-top:2px;">Mithril.js model CRUD and staff administration cockpit.</div>
831
+ </div>
832
+ <a href="${features.adminPath}" target="_blank" rel="noopener noreferrer" class="ws-btn" id="ws-btn-open-admin" style="text-decoration:none;">
833
+ Open ${features.adminPath} ↗
834
+ </a>
835
+ </div>
836
+ `
837
+ : '';
838
+
839
+ return `
840
+ <div>
841
+ <div style="margin-bottom:16px;">
842
+ <strong style="font-size:13px;color:#fff;">Developer Tools & Studio Integrations</strong>
843
+ <p style="font-size:10px;color:#71717a;">Direct shortcuts to Webspresso's integrated developer productivity tools.</p>
844
+ </div>
845
+
846
+ ${studioCardHtml}
847
+ ${adminCardHtml}
848
+
849
+ <div class="ws-card" style="display:flex;align-items:center;justify-content:space-between;">
850
+ <div>
851
+ <div style="font-weight:700;color:#34d399;font-size:13px;">⚡ Dev Live Reload Hub</div>
852
+ <div style="font-size:10px;color:#a1a1aa;margin-top:2px;">SSE-based CSS hot reload and server page reload connection.</div>
853
+ </div>
854
+ <button class="ws-btn ws-btn-sm" id="ws-btn-force-reload">
855
+ 🔄 Force Page Reload
856
+ </button>
857
+ </div>
858
+ </div>
859
+ `;
860
+ }
861
+
862
+ function renderDrawer() {
863
+ const controllers = getControllersSnapshot();
864
+ const fragments = getFragmentsSnapshot();
865
+ const network = getNetworkSnapshot();
866
+ const storage = getStorageSnapshot();
867
+
868
+ return `
869
+ <div class="ws-drawer" id="ws-devtool-drawer">
870
+ <!-- Header -->
871
+ <div class="ws-header">
872
+ <div class="ws-title">
873
+ <span>⚡ Webspresso DevTools</span>
874
+ <span class="ws-badge ws-badge-warning">DEV</span>
875
+ </div>
876
+
877
+ <!-- Navigation tabs -->
878
+ <div class="ws-tabs">
879
+ <button class="ws-tab-btn ${activeTab === 'controllers' ? 'active' : ''}" data-tab="controllers">
880
+ 🎮 Controllers (${controllers.length})
881
+ </button>
882
+ <button class="ws-tab-btn ${activeTab === 'fragments' ? 'active' : ''}" data-tab="fragments">
883
+ 🧩 Fragments (${fragments.length})
884
+ </button>
885
+ <button class="ws-tab-btn ${activeTab === 'network' ? 'active' : ''}" data-tab="network">
886
+ 🌐 Network
887
+ </button>
888
+ <button class="ws-tab-btn ${activeTab === 'storage' ? 'active' : ''}" data-tab="storage">
889
+ 💾 Storage
890
+ </button>
891
+ <button class="ws-tab-btn ${activeTab === 'studio' ? 'active' : ''}" data-tab="studio">
892
+ 🛠️ Studio
893
+ </button>
894
+ </div>
895
+
896
+ <!-- Controls -->
897
+ <div style="display:flex;align-items:center;gap:6px;">
898
+ <button class="ws-btn ws-btn-sm" id="ws-btn-refresh-snap" title="Refresh Snapshot">🔄</button>
899
+ <button class="ws-btn ws-btn-sm" id="ws-btn-close" title="Minimize DevTools (Alt+W)">✕</button>
900
+ </div>
901
+ </div>
902
+
903
+ <!-- Body -->
904
+ <div class="ws-content">
905
+ ${
906
+ activeTab === 'controllers'
907
+ ? renderControllersTab(controllers)
908
+ : activeTab === 'fragments'
909
+ ? renderFragmentsTab(fragments)
910
+ : activeTab === 'network'
911
+ ? renderNetworkTab(network)
912
+ : activeTab === 'storage'
913
+ ? renderStorageTab(storage)
914
+ : renderStudioTab()
915
+ }
916
+ </div>
917
+
918
+ <!-- Footer -->
919
+ <div class="ws-footer">
920
+ <span>Shortcuts: <strong>Alt + W</strong> to toggle • Console: <code>window.__WEBSPRESSO_DEVTOOLS__</code></span>
921
+ <span style="color:#a1a1aa;">Webspresso v${PKG_VERSION}</span>
922
+ </div>
923
+ </div>
924
+ `;
925
+ }
926
+
927
+ let lastRenderedHtml = '';
928
+ let wasOpen = false;
929
+
930
+ function findInShadow(id) {
931
+ if (shadow && typeof shadow.getElementById === 'function') {
932
+ return shadow.getElementById(id);
933
+ }
934
+ return shadow && typeof shadow.querySelector === 'function' ? shadow.querySelector(`#${id}`) : null;
935
+ }
936
+
937
+ function updateDom(force = false) {
938
+ const nextHtml = isOpen ? renderDrawer() : renderPill();
939
+ if (!force && nextHtml === lastRenderedHtml && isOpen === wasOpen) {
940
+ return;
941
+ }
942
+
943
+ // Preserve scroll positions inside drawer before updating DOM
944
+ let prevContentScrollTop = 0;
945
+ const prevContentEl =
946
+ isOpen && wasOpen && shadow && typeof shadow.querySelector === 'function'
947
+ ? shadow.querySelector('.ws-content')
948
+ : null;
949
+ if (prevContentEl) {
950
+ prevContentScrollTop = prevContentEl.scrollTop || 0;
951
+ }
952
+
953
+ lastRenderedHtml = nextHtml;
954
+ const openingTransition = isOpen && !wasOpen;
955
+ wasOpen = isOpen;
956
+
957
+ container.innerHTML = nextHtml;
958
+ if (openingTransition) {
959
+ const drawerEl = findInShadow('ws-devtool-drawer');
960
+ if (drawerEl && drawerEl.classList) {
961
+ drawerEl.classList.add('ws-drawer-enter');
962
+ }
963
+ }
964
+ if (prevContentScrollTop > 0 && shadow && typeof shadow.querySelector === 'function') {
965
+ const nextContentEl = shadow.querySelector('.ws-content');
966
+ if (nextContentEl) {
967
+ nextContentEl.scrollTop = prevContentScrollTop;
968
+ }
969
+ }
970
+ bindEvents();
971
+ }
972
+
973
+ function bindEvents() {
974
+ if (!isOpen) {
975
+ const pill = findInShadow('ws-devtool-pill');
976
+ if (pill) {
977
+ pill.addEventListener('click', () => {
978
+ isOpen = true;
979
+ updateDom();
980
+ });
981
+ }
982
+ return;
983
+ }
984
+
985
+ // Close button
986
+ const closeBtn = findInShadow('ws-btn-close');
987
+ if (closeBtn) {
988
+ closeBtn.addEventListener('click', () => {
989
+ isOpen = false;
990
+ updateDom();
991
+ });
992
+ }
993
+
994
+ // Refresh snapshot button
995
+ const refreshSnapBtn = findInShadow('ws-btn-refresh-snap');
996
+ if (refreshSnapBtn) {
997
+ refreshSnapBtn.addEventListener('click', () => {
998
+ updateDom();
999
+ });
1000
+ }
1001
+
1002
+ // Tab buttons
1003
+ const tabBtns = shadow.querySelectorAll('.ws-tab-btn');
1004
+ for (let i = 0; i < tabBtns.length; i++) {
1005
+ tabBtns[i].addEventListener('click', (e) => {
1006
+ const tab = e.currentTarget.getAttribute('data-tab');
1007
+ if (tab) {
1008
+ activeTab = tab;
1009
+ updateDom();
1010
+ }
1011
+ });
1012
+ }
1013
+
1014
+ // Controller selection in sidebar
1015
+ const ctrlItems = shadow.querySelectorAll('.ws-controller-item');
1016
+ for (let i = 0; i < ctrlItems.length; i++) {
1017
+ ctrlItems[i].addEventListener('click', (e) => {
1018
+ selectedControllerId = e.currentTarget.getAttribute('data-inst-id');
1019
+ updateDom();
1020
+ });
1021
+ }
1022
+
1023
+ // Highlight controller in DOM
1024
+ const hlCtrlBtn = findInShadow('ws-btn-highlight-ctrl');
1025
+ if (hlCtrlBtn) {
1026
+ hlCtrlBtn.addEventListener('click', (e) => {
1027
+ const instId = e.currentTarget.getAttribute('data-inst-id');
1028
+ const instances = runtime.controllers ? runtime.controllers.getActiveInstances() : [];
1029
+ const target = instances.find((i) => i.instanceId === instId);
1030
+ if (target && target.el) {
1031
+ highlightElement(target.el);
1032
+ }
1033
+ });
1034
+ }
1035
+
1036
+ // Copy state
1037
+ const copyStateBtn = findInShadow('ws-btn-copy-state');
1038
+ if (copyStateBtn) {
1039
+ copyStateBtn.addEventListener('click', (e) => {
1040
+ const instId = e.currentTarget.getAttribute('data-inst-id');
1041
+ const instances = runtime.controllers ? runtime.controllers.getActiveInstances() : [];
1042
+ const target = instances.find((i) => i.instanceId === instId);
1043
+ if (target && target.state && typeof navigator !== 'undefined' && navigator.clipboard) {
1044
+ navigator.clipboard.writeText(JSON.stringify(target.state, null, 2));
1045
+ copyStateBtn.textContent = '✅ Copied!';
1046
+ setTimeout(() => {
1047
+ try {
1048
+ copyStateBtn.textContent = '📋 Copy';
1049
+ } catch (_) {}
1050
+ }, 1500);
1051
+ }
1052
+ });
1053
+ }
1054
+
1055
+ // Highlight fragment in DOM
1056
+ const hlFragBtns = shadow.querySelectorAll('#ws-btn-highlight-frag');
1057
+ for (let i = 0; i < hlFragBtns.length; i++) {
1058
+ hlFragBtns[i].addEventListener('click', (e) => {
1059
+ const instId = e.currentTarget.getAttribute('data-inst-id');
1060
+ const frag = runtime.fragments ? runtime.fragments.getInstance(instId) : null;
1061
+ if (frag && frag.el) {
1062
+ highlightElement(frag.el);
1063
+ }
1064
+ });
1065
+ }
1066
+
1067
+ // Refresh fragment on demand
1068
+ const refreshFragBtns = shadow.querySelectorAll('#ws-btn-refresh-frag');
1069
+ for (let i = 0; i < refreshFragBtns.length; i++) {
1070
+ refreshFragBtns[i].addEventListener('click', async (e) => {
1071
+ const name = e.currentTarget.getAttribute('data-frag-name');
1072
+ if (name && runtime.fragments) {
1073
+ const btn = e.currentTarget;
1074
+ const origText = btn.textContent;
1075
+ btn.textContent = '⏳ ...';
1076
+ try {
1077
+ await runtime.fragments.refresh(name);
1078
+ } finally {
1079
+ setTimeout(() => {
1080
+ btn.textContent = origText;
1081
+ updateDom();
1082
+ }, 300);
1083
+ }
1084
+ }
1085
+ });
1086
+ }
1087
+
1088
+ // Refresh all fragments
1089
+ const refreshAllBtn = findInShadow('ws-btn-refresh-all-frags');
1090
+ if (refreshAllBtn) {
1091
+ refreshAllBtn.addEventListener('click', async () => {
1092
+ if (!runtime.fragments) return;
1093
+ const origText = refreshAllBtn.textContent;
1094
+ refreshAllBtn.textContent = '⏳ Refreshing...';
1095
+ try {
1096
+ const frags = runtime.fragments.getInstances();
1097
+ for (const f of frags) {
1098
+ await runtime.fragments.refresh(f.name);
1099
+ }
1100
+ } finally {
1101
+ setTimeout(() => {
1102
+ refreshAllBtn.textContent = origText;
1103
+ updateDom();
1104
+ }, 300);
1105
+ }
1106
+ });
1107
+ }
1108
+
1109
+ // Clear Webspresso storage
1110
+ const clearStorageBtn = findInShadow('ws-btn-clear-storage');
1111
+ if (clearStorageBtn) {
1112
+ clearStorageBtn.addEventListener('click', () => {
1113
+ defaultStorage.local.clear();
1114
+ defaultStorage.session.clear();
1115
+ clearStorageBtn.textContent = '✅ Cleared!';
1116
+ setTimeout(() => {
1117
+ updateDom();
1118
+ }, 800);
1119
+ });
1120
+ }
1121
+
1122
+ // Force page reload
1123
+ const forceReloadBtn = findInShadow('ws-btn-force-reload');
1124
+ if (forceReloadBtn) {
1125
+ forceReloadBtn.addEventListener('click', () => {
1126
+ if (typeof window !== 'undefined' && window.location) {
1127
+ window.location.reload();
1128
+ }
1129
+ });
1130
+ }
1131
+ }
1132
+
1133
+ // Hotkey listener: Alt+W toggles the DevTool dock
1134
+ function onKeyDown(e) {
1135
+ if (e.altKey && (e.key === 'w' || e.key === 'W' || e.code === 'KeyW')) {
1136
+ e.preventDefault();
1137
+ isOpen = !isOpen;
1138
+ updateDom();
1139
+ }
1140
+ }
1141
+
1142
+ document.addEventListener('keydown', onKeyDown);
1143
+
1144
+ // Periodic polling to keep snapshots live
1145
+ const pollIntervalMs = isNumber(options.pollInterval) ? options.pollInterval : 2000;
1146
+ let pollTimer = null;
1147
+ if (pollIntervalMs > 0) {
1148
+ pollTimer = setInterval(() => {
1149
+ // If open, re-render to reflect live state changes
1150
+ if (isOpen) {
1151
+ updateDom();
1152
+ }
1153
+ }, pollIntervalMs);
1154
+ }
1155
+
1156
+ // Mount into DOM
1157
+ const targetBody = document.body || document.documentElement;
1158
+ targetBody.appendChild(host);
1159
+ updateDom();
1160
+
1161
+ // Public handle & window console export
1162
+ const devtoolApi = {
1163
+ open() {
1164
+ isOpen = true;
1165
+ updateDom();
1166
+ },
1167
+ close() {
1168
+ isOpen = false;
1169
+ updateDom();
1170
+ },
1171
+ toggle() {
1172
+ isOpen = !isOpen;
1173
+ updateDom();
1174
+ },
1175
+ isOpen() {
1176
+ return isOpen;
1177
+ },
1178
+ setMeta(nextMeta = {}) {
1179
+ if (nextMeta && typeof nextMeta === 'object') {
1180
+ metaOverrides = { ...metaOverrides, ...nextMeta };
1181
+ updateDom();
1182
+ }
1183
+ },
1184
+ getSnapshot() {
1185
+ return {
1186
+ controllers: getControllersSnapshot(),
1187
+ fragments: getFragmentsSnapshot(),
1188
+ network: getNetworkSnapshot(),
1189
+ storage: getStorageSnapshot(),
1190
+ };
1191
+ },
1192
+ getState(controllerName) {
1193
+ const controllers = getControllersSnapshot();
1194
+ const match = controllers.find((c) => c.name === controllerName || c.instanceId === controllerName);
1195
+ return match ? match.state : null;
1196
+ },
1197
+ refresh(fragmentName) {
1198
+ return runtime.fragments ? runtime.fragments.refresh(fragmentName) : Promise.resolve();
1199
+ },
1200
+ highlight(controllerOrFragmentName) {
1201
+ const controllers = getControllersSnapshot();
1202
+ const ctrl = controllers.find((c) => c.name === controllerOrFragmentName || c.instanceId === controllerOrFragmentName);
1203
+ if (ctrl && ctrl.el) {
1204
+ return highlightElement(ctrl.el);
1205
+ }
1206
+ const fragments = getFragmentsSnapshot();
1207
+ const frag = fragments.find((f) => f.name === controllerOrFragmentName || f.instanceId === controllerOrFragmentName);
1208
+ if (frag && frag.el) {
1209
+ return highlightElement(frag.el);
1210
+ }
1211
+ return false;
1212
+ },
1213
+ destroy() {
1214
+ document.removeEventListener('keydown', onKeyDown);
1215
+ if (pollTimer) clearInterval(pollTimer);
1216
+ try {
1217
+ host.remove();
1218
+ } catch (_) {}
1219
+ if (typeof window !== 'undefined' && window.__WEBSPRESSO_DEVTOOLS__ === devtoolApi) {
1220
+ delete window.__WEBSPRESSO_DEVTOOLS__;
1221
+ }
1222
+ },
1223
+ };
1224
+
1225
+ host.__webspressoDevtoolInstance = devtoolApi;
1226
+
1227
+ if (typeof window !== 'undefined') {
1228
+ window.__WEBSPRESSO_DEVTOOLS__ = devtoolApi;
1229
+ }
1230
+
1231
+ return devtoolApi;
1232
+ }
1233
+
1234
+ module.exports = {
1235
+ mountDevtool,
1236
+ highlightElement,
1237
+ setupReduxDevToolsBridge,
1238
+ formatJsonHtml,
1239
+ };