webspresso 0.0.108 → 0.0.110

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