slicejs-web-framework 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +97 -97
  3. package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
  4. package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +470 -470
  5. package/Slice/Components/Structural/Controller/Controller.js +1406 -1406
  6. package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -52
  7. package/Slice/Components/Structural/Debugger/Debugger.html +72 -72
  8. package/Slice/Components/Structural/Debugger/Debugger.js +1505 -1505
  9. package/Slice/Components/Structural/EventManager/EventManager.js +651 -651
  10. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +985 -985
  11. package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -296
  12. package/Slice/Components/Structural/Logger/Log.js +10 -10
  13. package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +662 -662
  14. package/Slice/Components/Structural/Logger/Logger.js +225 -225
  15. package/Slice/Components/Structural/Router/Router.js +820 -810
  16. package/Slice/Components/Structural/Router/applyRouteMetadata.js +39 -0
  17. package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
  18. package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +175 -175
  19. package/Slice/Slice.js +711 -710
  20. package/api/framework/devDepsOptimizer.js +335 -335
  21. package/api/framework/express.js +3 -3
  22. package/api/framework/precompressedStatic.js +102 -93
  23. package/api/framework/publicEnvResolver.js +123 -123
  24. package/api/framework/securityMiddleware.js +197 -197
  25. package/api/framework/seo.js +387 -0
  26. package/api/framework/server.js +777 -410
  27. package/api/index.js +14 -4
  28. package/package.json +6 -2
  29. package/sliceConfig.schema.json +122 -122
  30. package/src/App/index.html +69 -69
  31. package/src/App/index.js +20 -20
  32. package/src/App/style.css +11 -11
  33. package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -9
  34. package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -8
  35. package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -12
  36. package/src/Components/AppComponents/AppShell/AppShell.css +10 -10
  37. package/src/Components/AppComponents/AppShell/AppShell.html +4 -4
  38. package/src/Components/AppComponents/AppShell/AppShell.js +36 -36
  39. package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -20
  40. package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -10
  41. package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -19
  42. package/src/Components/components.js +8 -8
  43. package/src/public/Styles/sliceStyles.css +34 -34
  44. package/src/public/Themes/Dark.css +42 -42
  45. package/src/public/Themes/Light.css +31 -31
  46. package/src/public/Themes/Slice.css +47 -47
  47. package/src/public/manifest.json +15 -15
  48. package/src/public/robots.txt +4 -4
  49. package/src/public/sitemap.xml +8 -8
  50. package/src/routes.js +9 -9
  51. package/src/sliceConfig.json +72 -72
  52. package/types/index.d.ts +219 -219
  53. package/.opencode/opencode.json +0 -15
  54. package/Slice/tests/build-bundled-component-without-category.test.js +0 -103
  55. package/Slice/tests/build-js-only-visual-components.test.js +0 -144
  56. package/Slice/tests/build-singleton.test.js +0 -244
  57. package/Slice/tests/bundle-v2-runtime-contract.test.js +0 -747
  58. package/Slice/tests/context-patch-use.test.js +0 -95
  59. package/Slice/tests/destroy-cascade.test.js +0 -215
  60. package/Slice/tests/event-emitters.test.js +0 -79
  61. package/Slice/tests/event-registry.test.js +0 -141
  62. package/Slice/tests/fixtures/real-runtime-loader.mjs +0 -21
  63. package/Slice/tests/leak-orphans.test.js +0 -131
  64. package/Slice/tests/props-allowed-values-validation.test.js +0 -119
  65. package/Slice/tests/public-env-runtime-accessors.test.js +0 -44
  66. package/Slice/tests/public-env-typed-accessors.test.js +0 -66
  67. package/Slice/tests/router-loading-finally.test.js +0 -68
  68. package/api/tests/dev-deps-optimizer.test.js +0 -137
  69. package/api/tests/dev-server-external.test.js +0 -56
  70. package/api/tests/precompressed-static.test.js +0 -119
  71. package/api/tests/public-env-resolver.test.js +0 -193
  72. package/tests/router-build-failure.test.js +0 -125
  73. package/tests/router-emit-ordering.test.js +0 -63
@@ -1,985 +1,985 @@
1
- /**
2
- * EventManager debug panel.
3
- */
4
- export default class EventManagerDebugger extends HTMLElement {
5
- constructor() {
6
- super();
7
- this.isOpen = false;
8
- this.filterText = '';
9
- this.activeTab = 'subscribers';
10
- this.refreshInterval = null;
11
- this._autoRefreshTimer = null;
12
- this._lastHistoryLength = 0;
13
- }
14
-
15
- /**
16
- * Initialize panel UI.
17
- * @returns {Promise<void>}
18
- */
19
- async init() {
20
- this.innerHTML = this.renderTemplate();
21
- slice.stylesManager.registerComponentStyles('EventManagerDebugger', this.renderStyles());
22
- this.cacheElements();
23
- this.bindEvents();
24
- this.makeDraggable();
25
- this._loadStaticGraph();
26
- this.renderList();
27
- }
28
-
29
- /**
30
- * Best-effort load of the generated static event graph (documentation layer).
31
- * Produced by `slice types generate` at /slice-events.generated.js. Absent =>
32
- * the Registry tab simply shows no static emitters/listeners (no error).
33
- */
34
- async _loadStaticGraph() {
35
- if (slice.events?.graph) return;
36
- try {
37
- const module = await import(/* @vite-ignore */ '/slice-events.generated.js');
38
- if (module?.default) slice.events.loadGraph(module.default);
39
- if (this.isOpen && this.activeTab === 'registry') this.renderRegistry();
40
- } catch {
41
- /* manifest not generated — documentation layer stays empty, runtime tracing still works */
42
- }
43
- }
44
-
45
- /**
46
- * Toggle panel visibility.
47
- * @returns {void}
48
- */
49
- toggle() {
50
- this.isOpen = !this.isOpen;
51
- this.container.classList.toggle('active', this.isOpen);
52
- if (this.isOpen) {
53
- this.renderList();
54
- }
55
- }
56
-
57
- /**
58
- * Show panel.
59
- * @returns {void}
60
- */
61
- open() {
62
- this.isOpen = true;
63
- this.container.classList.add('active');
64
- this.activeTab = 'subscribers';
65
- if (slice.events) {
66
- slice.events.startRecording();
67
- }
68
- this._startAutoRefresh();
69
- this.renderList();
70
- }
71
-
72
- /**
73
- * Hide panel.
74
- * @returns {void}
75
- */
76
- close() {
77
- this.isOpen = false;
78
- this.container.classList.remove('active');
79
- this._stopAutoRefresh();
80
- if (slice.events) {
81
- slice.events.stopRecording();
82
- }
83
- }
84
-
85
- _startAutoRefresh() {
86
- this._stopAutoRefresh();
87
- this._autoRefreshTimer = setInterval(() => {
88
- if (!this.isOpen) return;
89
- this._renderActive();
90
- }, 1500);
91
- }
92
-
93
- _stopAutoRefresh() {
94
- if (this._autoRefreshTimer) {
95
- clearInterval(this._autoRefreshTimer);
96
- this._autoRefreshTimer = null;
97
- }
98
- }
99
-
100
- cacheElements() {
101
- this.container = this.querySelector('#events-debugger');
102
- this.header = this.querySelector('.events-header');
103
- this.list = this.querySelector('#events-list');
104
- this.filterInput = this.querySelector('#events-filter');
105
- this.countLabel = this.querySelector('#events-count');
106
- this.refreshButton = this.querySelector('#events-refresh');
107
- this.closeButton = this.querySelector('#events-close');
108
- this.tabSubs = this.querySelector('#tab-subscribers');
109
- this.tabRegistry = this.querySelector('#tab-registry');
110
- this.tabHistory = this.querySelector('#tab-history');
111
- }
112
-
113
- bindEvents() {
114
- this.refreshButton.addEventListener('click', () => this._renderActive());
115
- this.closeButton.addEventListener('click', () => this.close());
116
- this.filterInput.addEventListener('input', (event) => {
117
- this.filterText = event.target.value.trim().toLowerCase();
118
- this._renderActive();
119
- });
120
- this.tabSubs.addEventListener('click', () => this._selectTab('subscribers'));
121
- this.tabRegistry.addEventListener('click', () => this._selectTab('registry'));
122
- this.tabHistory.addEventListener('click', () => this._selectTab('history'));
123
- }
124
-
125
- _selectTab(tab) {
126
- this.activeTab = tab;
127
- this.tabSubs.classList.toggle('active', tab === 'subscribers');
128
- this.tabRegistry.classList.toggle('active', tab === 'registry');
129
- this.tabHistory.classList.toggle('active', tab === 'history');
130
- const placeholders = { subscribers: 'filter events…', registry: 'filter registry…', history: 'filter history…' };
131
- this.filterInput.placeholder = placeholders[tab] || 'filter…';
132
- this._renderActive();
133
- }
134
-
135
- _renderActive() {
136
- if (this.activeTab === 'history') this.renderHistory();
137
- else if (this.activeTab === 'registry') this.renderRegistry();
138
- else this.renderList();
139
- }
140
-
141
- makeDraggable() {
142
- if (!this.header || !this.container) return;
143
-
144
- let offset = { x: 0, y: 0 };
145
- let isDragging = false;
146
-
147
- this.header.style.cursor = 'grab';
148
-
149
- this.header.addEventListener('mousedown', (event) => {
150
- if (event.target.closest('.btn')) return;
151
- isDragging = true;
152
- offset.x = event.clientX - this.container.getBoundingClientRect().left;
153
- offset.y = event.clientY - this.container.getBoundingClientRect().top;
154
- this.header.style.cursor = 'grabbing';
155
- });
156
-
157
- document.addEventListener('mousemove', (event) => {
158
- if (!isDragging) return;
159
- const rect = this.container.getBoundingClientRect();
160
- const maxX = window.innerWidth - rect.width;
161
- const maxY = window.innerHeight - rect.height;
162
- const x = Math.min(Math.max(event.clientX - offset.x, 0), maxX);
163
- const y = Math.min(Math.max(event.clientY - offset.y, 0), maxY);
164
- this.container.style.left = `${x}px`;
165
- this.container.style.top = `${y}px`;
166
- this.container.style.right = 'auto';
167
- this.container.style.bottom = 'auto';
168
- });
169
-
170
- document.addEventListener('mouseup', () => {
171
- if (!isDragging) return;
172
- isDragging = false;
173
- this.header.style.cursor = 'grab';
174
- });
175
- }
176
-
177
- renderList() {
178
- if (!slice?.events?.subscriptions) {
179
- this.list.textContent = 'EventManager not available.';
180
- this.countLabel.textContent = '0';
181
- return;
182
- }
183
-
184
- const items = [];
185
- slice.events.subscriptions.forEach((subs, eventName) => {
186
- const emitCount = slice.events.emitCounts?.get(eventName) || 0;
187
- const entries = Array.from(subs.entries()).map(([id, sub]) => {
188
- const componentSliceId = sub.componentSliceId || null;
189
- const component = componentSliceId ? slice.controller.getComponent(componentSliceId) : null;
190
- const componentName = component?.constructor?.name || null;
191
- return {
192
- id,
193
- once: sub.once,
194
- componentSliceId,
195
- componentName
196
- };
197
- });
198
-
199
- if (this.filterText && !eventName.toLowerCase().includes(this.filterText)) {
200
- return;
201
- }
202
-
203
- items.push({ eventName, count: subs.size, emitCount, entries, emitters: this._emittersFor(eventName) });
204
- });
205
-
206
- items.sort((a, b) => a.eventName.localeCompare(b.eventName));
207
-
208
- this.countLabel.textContent = String(items.length);
209
- this.list.innerHTML = items.length
210
- ? items.map((item) => {
211
- const details = item.entries.map((entry) => {
212
- const label = entry.componentName
213
- ? `${entry.componentName} (${entry.componentSliceId})`
214
- : entry.componentSliceId || 'Global';
215
- const onceBadge = entry.once ? '<span class="badge">once</span>' : '';
216
- return `
217
- <div class="subscriber-row">
218
- <div class="subscriber-name">${this.escapeHtml(label)}</div>
219
- <div class="subscriber-meta">${entry.id}${onceBadge}</div>
220
- </div>
221
- `;
222
- }).join('');
223
-
224
- const emittersHtml = item.emitters.length
225
- ? item.emitters.map((emitter) => `
226
- <div class="subscriber-row emitter-row">
227
- <div class="subscriber-name">${this.escapeHtml(this._emitterLabel(emitter))}</div>
228
- <div class="subscriber-meta">×${emitter.count}</div>
229
- </div>
230
- `).join('')
231
- : '<div class="empty">No emitters recorded</div>';
232
-
233
- return `
234
- <details class="event-row">
235
- <summary>
236
- <div class="event-name">${this.escapeHtml(item.eventName)}</div>
237
- <div class="event-metrics">
238
- <span class="emit-count" title="Emits this session">⚡${item.emitCount}</span>
239
- <span class="event-count">${item.count}</span>
240
- </div>
241
- </summary>
242
- <div class="trace-section">
243
- <div class="trace-label">emitted by</div>
244
- ${emittersHtml}
245
- </div>
246
- <div class="trace-section">
247
- <div class="trace-label">listeners</div>
248
- ${details || '<div class="empty">No subscribers</div>'}
249
- </div>
250
- </details>
251
- `;
252
- }).join('')
253
- : '<div class="empty">No events</div>';
254
- }
255
-
256
- /**
257
- * Emitters recorded for an event (only while the debugger is open).
258
- * @param {string} eventName
259
- * @returns {Array<{ sliceId?: string, name?: string, source?: string, count: number }>}
260
- */
261
- _emittersFor(eventName) {
262
- const map = slice?.events?.emitters;
263
- if (!(map instanceof Map) || !map.has(eventName)) return [];
264
- return Array.from(map.get(eventName).values()).sort((a, b) => b.count - a.count);
265
- }
266
-
267
- /**
268
- * Human-readable label for an emitter (component name + sliceId, or source location).
269
- */
270
- _emitterLabel(emitter) {
271
- if (!emitter) return 'unknown';
272
- if (emitter.sliceId) return `${emitter.name || 'Component'} (${emitter.sliceId})`;
273
- if (emitter.source) return emitter.source;
274
- return 'unknown';
275
- }
276
-
277
- /**
278
- * Render the declared event catalog (registry) merged with the live state:
279
- * payload shape, current subscriber count, emit count, plus drift badges for
280
- * declared-but-unused and used-but-undeclared events.
281
- */
282
- renderRegistry() {
283
- if (!slice?.events) {
284
- this.list.textContent = 'EventManager not available.';
285
- this.countLabel.textContent = '0';
286
- return;
287
- }
288
-
289
- const registry = slice.events.registry instanceof Map ? slice.events.registry : new Map();
290
- const undeclared = slice.events.undeclared instanceof Set ? slice.events.undeclared : new Set();
291
-
292
- if (registry.size === 0 && undeclared.size === 0) {
293
- this.countLabel.textContent = '0';
294
- this.list.innerHTML =
295
- '<div class="empty">No events registered.<br/>Declare them with <code>slice.events.register({ ... })</code>.</div>';
296
- return;
297
- }
298
-
299
- const matchesFilter = (name) => !this.filterText || name.toLowerCase().includes(this.filterText);
300
-
301
- // Bucket declared events by namespace (segment before the first ':').
302
- const groups = new Map();
303
- registry.forEach((definition, eventName) => {
304
- if (!matchesFilter(eventName)) return;
305
- const ns = slice.events.namespaceOf?.(eventName) || '(no namespace)';
306
- if (!groups.has(ns)) groups.set(ns, []);
307
- groups.get(ns).push({
308
- eventName,
309
- description: definition?.description || '',
310
- payload: definition?.payload ?? null,
311
- subscriberCount: slice.events.subscriberCount?.(eventName) || 0,
312
- emitCount: slice.events.emitCounts?.get(eventName) || 0
313
- });
314
- });
315
-
316
- const sortedNamespaces = Array.from(groups.keys()).sort((a, b) => a.localeCompare(b));
317
- const declaredCount = Array.from(groups.values()).reduce((sum, list) => sum + list.length, 0);
318
- const drift = Array.from(undeclared).filter(matchesFilter).sort();
319
-
320
- this.countLabel.textContent = String(declaredCount + drift.length);
321
-
322
- const siteRows = (sites) => sites.map((s) => {
323
- const label = s.component ? `${s.component} — ${s.file}:${s.line}` : `${s.file}:${s.line}`;
324
- return `<div class="graph-site">${this.escapeHtml(label)}</div>`;
325
- }).join('');
326
-
327
- const eventRowHtml = (item) => {
328
- const payloadStr = this.formatPayload(item.payload);
329
- const idle = item.subscriberCount === 0
330
- ? '<span class="badge badge-idle" title="No active listeners">idle</span>'
331
- : '';
332
- const desc = item.description
333
- ? `<div class="registry-desc">${this.escapeHtml(item.description)}</div>`
334
- : '';
335
-
336
- // Static documentation layer (from the generated manifest).
337
- const emitters = slice.events.staticEmittersOf?.(item.eventName) || [];
338
- const listeners = slice.events.staticListenersOf?.(item.eventName) || [];
339
- const graphHtml = (emitters.length || listeners.length)
340
- ? `
341
- <div class="graph-block">
342
- <div class="graph-label">emitters <span class="graph-tag">static</span></div>
343
- ${emitters.length ? siteRows(emitters) : '<div class="empty">none found in code</div>'}
344
- <div class="graph-label">listeners <span class="graph-tag">static</span></div>
345
- ${listeners.length ? siteRows(listeners) : '<div class="empty">none found in code</div>'}
346
- </div>`
347
- : '';
348
-
349
- return `
350
- <details class="event-row">
351
- <summary>
352
- <div class="event-name">${this.escapeHtml(item.eventName)}</div>
353
- <div class="event-metrics">
354
- ${idle}
355
- <span class="emit-count" title="Emits this session">⚡${item.emitCount}</span>
356
- <span class="event-count" title="Active listeners">${item.subscriberCount}</span>
357
- </div>
358
- </summary>
359
- <div class="subscriber-list">
360
- ${desc}
361
- <div class="registry-payload"><span class="payload-key">payload</span> ${this.escapeHtml(payloadStr)}</div>
362
- ${graphHtml}
363
- </div>
364
- </details>
365
- `;
366
- };
367
-
368
- const declaredHtml = sortedNamespaces.map((ns) => {
369
- const list = groups.get(ns).sort((a, b) => a.eventName.localeCompare(b.eventName));
370
- const rows = list.map(eventRowHtml).join('');
371
- return `
372
- <details class="ns-group" open>
373
- <summary class="ns-summary">
374
- <span class="ns-name">${this.escapeHtml(ns)}</span>
375
- <span class="ns-count">${list.length}</span>
376
- </summary>
377
- <div class="ns-events">${rows}</div>
378
- </details>
379
- `;
380
- }).join('');
381
-
382
- const driftHtml = drift.length
383
- ? `<div class="registry-drift-title">Used but not declared (${drift.length})</div>` +
384
- drift.map((name) => `
385
- <div class="history-row drift-row">
386
- <div class="history-event">${this.escapeHtml(name)}</div>
387
- <div class="history-time"><span class="badge badge-drift">undeclared</span></div>
388
- </div>
389
- `).join('')
390
- : '';
391
-
392
- // Footer: dynamic emit/subscribe sites (computed names the static scan can't resolve).
393
- const dynamic = slice.events.graph?.dynamic;
394
- const dynamicCount = (dynamic?.emitters?.length || 0) + (dynamic?.listeners?.length || 0);
395
- const dynamicHtml = dynamicCount
396
- ? `<div class="registry-drift-title">Dynamic sites (computed names, ${dynamicCount}) — not statically resolvable</div>`
397
- : '';
398
-
399
- this.list.innerHTML =
400
- (declaredHtml || (drift.length ? '' : '<div class="empty">No matching events</div>')) + driftHtml + dynamicHtml;
401
- }
402
-
403
- /**
404
- * Render a payload mini-schema for display.
405
- * @param {object|null} payload
406
- * @returns {string}
407
- */
408
- formatPayload(payload) {
409
- if (payload === null || payload === undefined) return 'void';
410
- if (typeof payload === 'string') return payload;
411
- if (typeof payload === 'object') {
412
- const parts = Object.entries(payload).map(([key, value]) => {
413
- const type = typeof value === 'string'
414
- ? value
415
- : (value && typeof value === 'object' && value.type) || 'any';
416
- return `${key}: ${type}`;
417
- });
418
- return `{ ${parts.join(', ')} }`;
419
- }
420
- return String(payload);
421
- }
422
-
423
- renderHistory() {
424
- if (!slice?.events?.emitHistory) {
425
- this.list.textContent = 'EventManager not available.';
426
- this.countLabel.textContent = '0';
427
- return;
428
- }
429
-
430
- const history = slice.events.emitHistory;
431
- const filtered = this.filterText
432
- ? history.filter(e => e.eventName.toLowerCase().includes(this.filterText))
433
- : history;
434
-
435
- this.countLabel.textContent = String(filtered.length);
436
- if (!filtered.length) {
437
- this.list.innerHTML = '<div class="empty">No emits recorded yet</div>';
438
- return;
439
- }
440
-
441
- const now = Date.now();
442
- this.list.innerHTML = [...filtered].reverse().map((entry) => {
443
- const diff = now - entry.timestamp;
444
- const timeStr = diff < 1000 ? 'now'
445
- : diff < 60000 ? `${Math.floor(diff / 1000)}s ago`
446
- : `${Math.floor(diff / 60000)}m ago`;
447
- const from = entry.emitter
448
- ? `<div class="history-from" title="Emitter">↑ ${this.escapeHtml(this._emitterLabel(entry.emitter))}</div>`
449
- : '';
450
- return `
451
- <div class="history-row">
452
- <div class="history-main">
453
- <div class="history-event">${this.escapeHtml(entry.eventName)}</div>
454
- ${from}
455
- </div>
456
- <div class="history-time">${timeStr}</div>
457
- </div>
458
- `;
459
- }).join('');
460
- }
461
-
462
- escapeHtml(value) {
463
- return String(value)
464
- .replace(/&/g, '&amp;')
465
- .replace(/</g, '&lt;')
466
- .replace(/>/g, '&gt;')
467
- .replace(/"/g, '&quot;');
468
- }
469
-
470
- renderTemplate() {
471
- return `
472
- <div id="events-debugger">
473
- <div class="events-header">
474
- <div class="brand">
475
- <span class="status-dot"></span>
476
- <span class="glyph">◇</span>
477
- <span class="title">EVENTS</span>
478
- </div>
479
- <div class="actions">
480
- <button id="events-refresh" class="btn" title="Refresh" aria-label="Refresh">⟳</button>
481
- <button id="events-close" class="btn" title="Close" aria-label="Close">✕</button>
482
- </div>
483
- </div>
484
- <div class="events-tabs">
485
- <button id="tab-subscribers" class="tab-btn active">Subscribers</button>
486
- <button id="tab-registry" class="tab-btn">Registry</button>
487
- <button id="tab-history" class="tab-btn">History</button>
488
- </div>
489
- <div class="events-toolbar">
490
- <input id="events-filter" type="text" placeholder="filter events…" autocomplete="off" spellcheck="false" />
491
- <div class="count"><span id="events-count">0</span></div>
492
- </div>
493
- <div class="events-list" id="events-list"></div>
494
- </div>
495
- `;
496
- }
497
-
498
- renderStyles() {
499
- return `
500
- /* Slice Instruments — events console. All selectors scoped to the
501
- <slice-eventmanager-debugger> tag so nothing clashes with app styles.
502
- Every --si-* token reads the matching framework theme variable from
503
- :root, falling back to the original hardcoded value if absent. */
504
- slice-eventmanager-debugger {
505
- --si-accent: var(--primary-color, #6ee7ff);
506
- --si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
507
- --si-surface: var(--primary-background-color, rgba(17, 19, 28, 0.86));
508
- --si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
509
- --si-raised-2: var(--tertiary-background-color, rgba(255, 255, 255, 0.06));
510
- --si-border: var(--medium-color, rgba(255, 255, 255, 0.09));
511
- --si-text: var(--font-primary-color, #e8eaf2);
512
- --si-dim: var(--font-secondary-color, #888fa6);
513
- --si-danger: var(--danger-color, #ff6b6b);
514
- --si-success: var(--success-color, #46d39a);
515
- --si-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace;
516
- }
517
-
518
- slice-eventmanager-debugger #events-debugger {
519
- position: fixed;
520
- bottom: 20px;
521
- right: 20px;
522
- width: min(380px, calc(100vw - 40px));
523
- max-height: 64vh;
524
- background: var(--si-surface);
525
- border: 1px solid var(--si-border);
526
- border-radius: 14px;
527
- box-shadow:
528
- 0 24px 60px -12px rgba(0, 0, 0, 0.55),
529
- 0 0 0 1px rgba(0, 0, 0, 0.2),
530
- 0 0 38px -18px rgba(var(--si-accent-rgb), 0.55);
531
- -webkit-backdrop-filter: blur(22px) saturate(1.3);
532
- backdrop-filter: blur(22px) saturate(1.3);
533
- display: none;
534
- flex-direction: column;
535
- z-index: 10001;
536
- overflow: hidden;
537
- color: var(--si-text);
538
- font-family: var(--si-mono);
539
- }
540
-
541
- slice-eventmanager-debugger #events-debugger.active {
542
- display: flex;
543
- animation: si-events-in 0.26s cubic-bezier(0.16, 1, 0.3, 1);
544
- }
545
-
546
- @keyframes si-events-in {
547
- from { opacity: 0; transform: translateY(10px) scale(0.985); }
548
- to { opacity: 1; transform: translateY(0) scale(1); }
549
- }
550
-
551
- slice-eventmanager-debugger #events-debugger * { box-sizing: border-box; }
552
-
553
- slice-eventmanager-debugger #events-debugger::before {
554
- content: '';
555
- position: absolute;
556
- left: 0; top: 0; bottom: 0;
557
- width: 2px;
558
- background: linear-gradient(180deg, var(--si-accent), transparent 70%);
559
- opacity: 0.85;
560
- pointer-events: none;
561
- }
562
-
563
- slice-eventmanager-debugger .events-header {
564
- display: flex;
565
- justify-content: space-between;
566
- align-items: center;
567
- padding: 12px 14px;
568
- background:
569
- radial-gradient(120% 140% at 0% 0%, rgba(var(--si-accent-rgb), 0.10), transparent 60%),
570
- var(--si-raised);
571
- border-bottom: 1px solid var(--si-border);
572
- user-select: none;
573
- }
574
-
575
- slice-eventmanager-debugger .brand { display: flex; align-items: center; gap: 9px; }
576
-
577
- slice-eventmanager-debugger .status-dot {
578
- width: 7px; height: 7px;
579
- border-radius: 50%;
580
- background: var(--si-accent);
581
- animation: si-pulse-ev 2.4s ease-out infinite;
582
- }
583
-
584
- @keyframes si-pulse-ev {
585
- 0% { box-shadow: 0 0 0 0 rgba(var(--si-accent-rgb), 0.55); }
586
- 70% { box-shadow: 0 0 0 7px rgba(var(--si-accent-rgb), 0); }
587
- 100% { box-shadow: 0 0 0 0 rgba(var(--si-accent-rgb), 0); }
588
- }
589
-
590
- slice-eventmanager-debugger .glyph { color: var(--si-accent); font-size: 12px; opacity: 0.9; }
591
-
592
- slice-eventmanager-debugger .title {
593
- font-weight: 600;
594
- font-size: 11px;
595
- letter-spacing: 0.18em;
596
- color: var(--si-text);
597
- }
598
-
599
- slice-eventmanager-debugger .actions { display: flex; gap: 6px; }
600
-
601
- slice-eventmanager-debugger .btn {
602
- width: 26px; height: 26px;
603
- display: flex; align-items: center; justify-content: center;
604
- border-radius: 7px;
605
- border: 1px solid var(--si-border);
606
- background: var(--si-raised);
607
- color: var(--si-dim);
608
- cursor: pointer;
609
- font-size: 13px;
610
- line-height: 1;
611
- transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
612
- }
613
- slice-eventmanager-debugger .btn:hover {
614
- color: var(--si-text);
615
- background: var(--si-raised-2);
616
- border-color: rgba(var(--si-accent-rgb), 0.5);
617
- }
618
- slice-eventmanager-debugger .btn:active { transform: scale(0.92); }
619
- slice-eventmanager-debugger #events-refresh:hover { color: var(--si-accent); }
620
-
621
- slice-eventmanager-debugger .events-tabs {
622
- display: flex;
623
- gap: 0;
624
- padding: 0 12px;
625
- border-bottom: 1px solid var(--si-border);
626
- background: var(--si-raised);
627
- }
628
- slice-eventmanager-debugger .tab-btn {
629
- all: unset;
630
- cursor: pointer;
631
- font-size: 10px;
632
- font-weight: 600;
633
- letter-spacing: 0.06em;
634
- text-transform: uppercase;
635
- padding: 7px 12px;
636
- color: var(--si-dim);
637
- border-bottom: 2px solid transparent;
638
- transition: color 0.15s ease, border-color 0.15s ease;
639
- }
640
- slice-eventmanager-debugger .tab-btn:hover { color: var(--si-text); }
641
- slice-eventmanager-debugger .tab-btn.active {
642
- color: var(--si-accent);
643
- border-bottom-color: var(--si-accent);
644
- }
645
-
646
- slice-eventmanager-debugger .events-toolbar {
647
- display: flex;
648
- gap: 10px;
649
- align-items: center;
650
- padding: 10px 12px;
651
- border-bottom: 1px solid var(--si-border);
652
- }
653
-
654
- slice-eventmanager-debugger .events-toolbar input {
655
- flex: 1;
656
- min-width: 0;
657
- padding: 7px 10px 7px 30px;
658
- border-radius: 8px;
659
- border: 1px solid var(--si-border);
660
- background:
661
- var(--si-raised) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%23888fa6' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat 10px center;
662
- color: var(--si-text);
663
- font-family: var(--si-mono);
664
- font-size: 12px;
665
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
666
- }
667
- slice-eventmanager-debugger .events-toolbar input::placeholder { color: var(--si-dim); }
668
- slice-eventmanager-debugger .events-toolbar input:focus {
669
- outline: none;
670
- border-color: rgba(var(--si-accent-rgb), 0.6);
671
- box-shadow: 0 0 0 3px rgba(var(--si-accent-rgb), 0.12);
672
- }
673
-
674
- slice-eventmanager-debugger .events-toolbar .count { font-size: 11px; color: var(--si-dim); min-width: 22px; text-align: center; }
675
- slice-eventmanager-debugger .events-toolbar .count span { color: var(--si-accent); font-weight: 600; }
676
-
677
- slice-eventmanager-debugger .events-list {
678
- padding: 10px 12px 12px;
679
- overflow: auto;
680
- display: flex;
681
- flex-direction: column;
682
- gap: 7px;
683
- }
684
- slice-eventmanager-debugger .events-list::-webkit-scrollbar { width: 8px; }
685
- slice-eventmanager-debugger .events-list::-webkit-scrollbar-thumb {
686
- background: var(--si-raised-2);
687
- border-radius: 8px;
688
- border: 2px solid transparent;
689
- background-clip: padding-box;
690
- }
691
- slice-eventmanager-debugger .events-list::-webkit-scrollbar-thumb:hover { background: rgba(var(--si-accent-rgb), 0.4); background-clip: padding-box; }
692
-
693
- slice-eventmanager-debugger .event-row {
694
- display: block;
695
- padding: 9px 11px;
696
- background: var(--si-raised);
697
- border-radius: 9px;
698
- border: 1px solid var(--si-border);
699
- border-left: 2px solid transparent;
700
- transition: border-color 0.18s ease, background 0.18s ease;
701
- }
702
- slice-eventmanager-debugger .event-row:hover { background: var(--si-raised-2); border-left-color: var(--si-accent); }
703
-
704
- slice-eventmanager-debugger .event-row summary {
705
- display: flex;
706
- align-items: center;
707
- justify-content: space-between;
708
- gap: 10px;
709
- cursor: pointer;
710
- list-style: none;
711
- }
712
- slice-eventmanager-debugger .event-row summary::-webkit-details-marker { display: none; }
713
- slice-eventmanager-debugger .event-row summary::after {
714
- content: '›';
715
- margin-left: auto;
716
- color: var(--si-dim);
717
- font-size: 15px;
718
- line-height: 1;
719
- transition: transform 0.2s ease, color 0.2s ease;
720
- }
721
- slice-eventmanager-debugger .event-row[open] summary::after { transform: rotate(90deg); color: var(--si-accent); }
722
-
723
- slice-eventmanager-debugger .event-name {
724
- font-family: var(--si-mono);
725
- font-size: 12px;
726
- color: var(--si-text);
727
- overflow: hidden;
728
- text-overflow: ellipsis;
729
- white-space: nowrap;
730
- }
731
-
732
- slice-eventmanager-debugger .event-metrics {
733
- display: flex;
734
- align-items: center;
735
- gap: 6px;
736
- }
737
- slice-eventmanager-debugger .emit-count {
738
- font-weight: 600;
739
- font-size: 10px;
740
- color: var(--si-success);
741
- background: rgba(70, 211, 154, 0.12);
742
- border: 1px solid rgba(70, 211, 154, 0.25);
743
- padding: 1px 6px;
744
- border-radius: 999px;
745
- white-space: nowrap;
746
- }
747
- slice-eventmanager-debugger .event-count {
748
- font-weight: 600;
749
- font-size: 11px;
750
- color: var(--si-accent);
751
- background: rgba(var(--si-accent-rgb), 0.12);
752
- border: 1px solid rgba(var(--si-accent-rgb), 0.25);
753
- padding: 1px 8px;
754
- border-radius: 999px;
755
- min-width: 22px;
756
- text-align: center;
757
- }
758
- slice-eventmanager-debugger .history-row {
759
- display: flex;
760
- justify-content: space-between;
761
- align-items: center;
762
- gap: 10px;
763
- padding: 6px 11px;
764
- background: var(--si-raised);
765
- border-radius: 7px;
766
- border: 1px solid var(--si-border);
767
- border-left: 2px solid transparent;
768
- transition: border-color 0.15s ease;
769
- }
770
- slice-eventmanager-debugger .history-row:hover { border-left-color: var(--si-accent); }
771
- slice-eventmanager-debugger .history-event {
772
- font-family: var(--si-mono);
773
- font-size: 11.5px;
774
- color: var(--si-text);
775
- overflow: hidden;
776
- text-overflow: ellipsis;
777
- white-space: nowrap;
778
- }
779
- slice-eventmanager-debugger .history-time {
780
- font-size: 10px;
781
- color: var(--si-dim);
782
- white-space: nowrap;
783
- flex-shrink: 0;
784
- }
785
-
786
- slice-eventmanager-debugger .subscriber-list {
787
- margin-top: 9px;
788
- padding-top: 9px;
789
- border-top: 1px dashed var(--si-border);
790
- display: flex;
791
- flex-direction: column;
792
- gap: 6px;
793
- }
794
-
795
- slice-eventmanager-debugger .subscriber-row {
796
- display: flex;
797
- justify-content: space-between;
798
- align-items: center;
799
- gap: 10px;
800
- padding: 6px 9px;
801
- border-radius: 7px;
802
- background: rgba(0, 0, 0, 0.22);
803
- border: 1px solid var(--si-border);
804
- }
805
-
806
- slice-eventmanager-debugger .subscriber-name {
807
- font-size: 11.5px;
808
- color: var(--si-text);
809
- overflow: hidden;
810
- text-overflow: ellipsis;
811
- white-space: nowrap;
812
- }
813
-
814
- slice-eventmanager-debugger .subscriber-meta {
815
- font-size: 10.5px;
816
- color: var(--si-dim);
817
- display: flex;
818
- align-items: center;
819
- gap: 6px;
820
- white-space: nowrap;
821
- }
822
-
823
- slice-eventmanager-debugger .badge {
824
- padding: 1px 6px;
825
- border-radius: 999px;
826
- background: rgba(var(--si-accent-rgb), 0.16);
827
- color: var(--si-accent);
828
- border: 1px solid rgba(var(--si-accent-rgb), 0.3);
829
- font-size: 9px;
830
- letter-spacing: 0.06em;
831
- text-transform: uppercase;
832
- }
833
-
834
- slice-eventmanager-debugger .empty {
835
- color: var(--si-dim);
836
- font-size: 11px;
837
- letter-spacing: 0.04em;
838
- text-align: center;
839
- padding: 22px 0;
840
- }
841
- slice-eventmanager-debugger .empty code {
842
- font-family: var(--si-mono);
843
- color: var(--si-accent);
844
- background: rgba(var(--si-accent-rgb), 0.1);
845
- padding: 1px 5px;
846
- border-radius: 5px;
847
- }
848
-
849
- slice-eventmanager-debugger .registry-desc {
850
- font-size: 11px;
851
- color: var(--si-dim);
852
- padding: 4px 2px 2px;
853
- line-height: 1.4;
854
- }
855
- slice-eventmanager-debugger .registry-payload {
856
- font-family: var(--si-mono);
857
- font-size: 11px;
858
- color: var(--si-text);
859
- padding: 6px 9px;
860
- border-radius: 7px;
861
- background: rgba(0, 0, 0, 0.22);
862
- border: 1px solid var(--si-border);
863
- word-break: break-word;
864
- }
865
- slice-eventmanager-debugger .payload-key {
866
- color: var(--si-dim);
867
- margin-right: 6px;
868
- }
869
- slice-eventmanager-debugger .badge-idle {
870
- background: rgba(136, 143, 166, 0.14);
871
- color: var(--si-dim);
872
- border: 1px solid var(--si-border);
873
- }
874
- slice-eventmanager-debugger .badge-drift {
875
- background: rgba(255, 107, 107, 0.14);
876
- color: var(--si-danger);
877
- border: 1px solid rgba(255, 107, 107, 0.3);
878
- }
879
- slice-eventmanager-debugger .registry-drift-title {
880
- margin-top: 4px;
881
- font-size: 9.5px;
882
- letter-spacing: 0.1em;
883
- text-transform: uppercase;
884
- color: var(--si-danger);
885
- opacity: 0.85;
886
- }
887
- slice-eventmanager-debugger .drift-row { border-left-color: rgba(255, 107, 107, 0.4); }
888
-
889
- slice-eventmanager-debugger .ns-group { display: block; }
890
- slice-eventmanager-debugger .ns-group + .ns-group { margin-top: 4px; }
891
- slice-eventmanager-debugger .ns-summary {
892
- display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
893
- padding: 4px 2px 6px; user-select: none;
894
- }
895
- slice-eventmanager-debugger .ns-summary::-webkit-details-marker { display: none; }
896
- slice-eventmanager-debugger .ns-summary::before {
897
- content: '▾'; color: var(--si-dim); font-size: 10px; transition: transform 0.2s ease;
898
- }
899
- slice-eventmanager-debugger .ns-group:not([open]) .ns-summary::before { transform: rotate(-90deg); }
900
- slice-eventmanager-debugger .ns-name {
901
- font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--si-accent);
902
- }
903
- slice-eventmanager-debugger .ns-count {
904
- font-size: 9.5px; color: var(--si-dim);
905
- background: var(--si-raised-2); border: 1px solid var(--si-border);
906
- border-radius: 999px; padding: 0 6px;
907
- }
908
- slice-eventmanager-debugger .ns-events { display: flex; flex-direction: column; gap: 7px; padding-left: 4px; }
909
-
910
- slice-eventmanager-debugger .trace-section {
911
- margin-top: 9px;
912
- padding-top: 9px;
913
- border-top: 1px dashed var(--si-border);
914
- display: flex;
915
- flex-direction: column;
916
- gap: 6px;
917
- }
918
- slice-eventmanager-debugger .trace-label {
919
- font-size: 9px;
920
- font-weight: 700;
921
- letter-spacing: 0.1em;
922
- text-transform: uppercase;
923
- color: var(--si-dim);
924
- }
925
- slice-eventmanager-debugger .emitter-row { border-left: 2px solid var(--si-success); }
926
- slice-eventmanager-debugger .emitter-row .subscriber-name { color: var(--si-text); }
927
-
928
- slice-eventmanager-debugger .history-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
929
- slice-eventmanager-debugger .history-from {
930
- font-size: 10px;
931
- color: var(--si-success);
932
- overflow: hidden;
933
- text-overflow: ellipsis;
934
- white-space: nowrap;
935
- }
936
-
937
- slice-eventmanager-debugger .graph-block {
938
- margin-top: 8px;
939
- padding-top: 8px;
940
- border-top: 1px dashed var(--si-border);
941
- display: flex;
942
- flex-direction: column;
943
- gap: 4px;
944
- }
945
- slice-eventmanager-debugger .graph-label {
946
- font-size: 9px;
947
- font-weight: 700;
948
- letter-spacing: 0.08em;
949
- text-transform: uppercase;
950
- color: var(--si-dim);
951
- margin-top: 4px;
952
- display: flex;
953
- align-items: center;
954
- gap: 6px;
955
- }
956
- slice-eventmanager-debugger .graph-tag {
957
- font-size: 8px;
958
- font-weight: 600;
959
- letter-spacing: 0.04em;
960
- color: var(--si-accent);
961
- background: rgba(var(--si-accent-rgb), 0.12);
962
- border: 1px solid rgba(var(--si-accent-rgb), 0.25);
963
- border-radius: 999px;
964
- padding: 0 5px;
965
- }
966
- slice-eventmanager-debugger .graph-site {
967
- font-family: var(--si-mono);
968
- font-size: 10.5px;
969
- color: var(--si-text);
970
- padding: 3px 8px;
971
- border-radius: 6px;
972
- background: rgba(0, 0, 0, 0.2);
973
- border: 1px solid var(--si-border);
974
- word-break: break-word;
975
- }
976
-
977
- @media (prefers-reduced-motion: reduce) {
978
- slice-eventmanager-debugger #events-debugger.active { animation: none; }
979
- slice-eventmanager-debugger .status-dot { animation: none; }
980
- }
981
- `;
982
- }
983
- }
984
-
985
- customElements.define('slice-eventmanager-debugger', EventManagerDebugger);
1
+ /**
2
+ * EventManager debug panel.
3
+ */
4
+ export default class EventManagerDebugger extends HTMLElement {
5
+ constructor() {
6
+ super();
7
+ this.isOpen = false;
8
+ this.filterText = '';
9
+ this.activeTab = 'subscribers';
10
+ this.refreshInterval = null;
11
+ this._autoRefreshTimer = null;
12
+ this._lastHistoryLength = 0;
13
+ }
14
+
15
+ /**
16
+ * Initialize panel UI.
17
+ * @returns {Promise<void>}
18
+ */
19
+ async init() {
20
+ this.innerHTML = this.renderTemplate();
21
+ slice.stylesManager.registerComponentStyles('EventManagerDebugger', this.renderStyles());
22
+ this.cacheElements();
23
+ this.bindEvents();
24
+ this.makeDraggable();
25
+ this._loadStaticGraph();
26
+ this.renderList();
27
+ }
28
+
29
+ /**
30
+ * Best-effort load of the generated static event graph (documentation layer).
31
+ * Produced by `slice types generate` at /slice-events.generated.js. Absent =>
32
+ * the Registry tab simply shows no static emitters/listeners (no error).
33
+ */
34
+ async _loadStaticGraph() {
35
+ if (slice.events?.graph) return;
36
+ try {
37
+ const module = await import(/* @vite-ignore */ '/slice-events.generated.js');
38
+ if (module?.default) slice.events.loadGraph(module.default);
39
+ if (this.isOpen && this.activeTab === 'registry') this.renderRegistry();
40
+ } catch {
41
+ /* manifest not generated — documentation layer stays empty, runtime tracing still works */
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Toggle panel visibility.
47
+ * @returns {void}
48
+ */
49
+ toggle() {
50
+ this.isOpen = !this.isOpen;
51
+ this.container.classList.toggle('active', this.isOpen);
52
+ if (this.isOpen) {
53
+ this.renderList();
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Show panel.
59
+ * @returns {void}
60
+ */
61
+ open() {
62
+ this.isOpen = true;
63
+ this.container.classList.add('active');
64
+ this.activeTab = 'subscribers';
65
+ if (slice.events) {
66
+ slice.events.startRecording();
67
+ }
68
+ this._startAutoRefresh();
69
+ this.renderList();
70
+ }
71
+
72
+ /**
73
+ * Hide panel.
74
+ * @returns {void}
75
+ */
76
+ close() {
77
+ this.isOpen = false;
78
+ this.container.classList.remove('active');
79
+ this._stopAutoRefresh();
80
+ if (slice.events) {
81
+ slice.events.stopRecording();
82
+ }
83
+ }
84
+
85
+ _startAutoRefresh() {
86
+ this._stopAutoRefresh();
87
+ this._autoRefreshTimer = setInterval(() => {
88
+ if (!this.isOpen) return;
89
+ this._renderActive();
90
+ }, 1500);
91
+ }
92
+
93
+ _stopAutoRefresh() {
94
+ if (this._autoRefreshTimer) {
95
+ clearInterval(this._autoRefreshTimer);
96
+ this._autoRefreshTimer = null;
97
+ }
98
+ }
99
+
100
+ cacheElements() {
101
+ this.container = this.querySelector('#events-debugger');
102
+ this.header = this.querySelector('.events-header');
103
+ this.list = this.querySelector('#events-list');
104
+ this.filterInput = this.querySelector('#events-filter');
105
+ this.countLabel = this.querySelector('#events-count');
106
+ this.refreshButton = this.querySelector('#events-refresh');
107
+ this.closeButton = this.querySelector('#events-close');
108
+ this.tabSubs = this.querySelector('#tab-subscribers');
109
+ this.tabRegistry = this.querySelector('#tab-registry');
110
+ this.tabHistory = this.querySelector('#tab-history');
111
+ }
112
+
113
+ bindEvents() {
114
+ this.refreshButton.addEventListener('click', () => this._renderActive());
115
+ this.closeButton.addEventListener('click', () => this.close());
116
+ this.filterInput.addEventListener('input', (event) => {
117
+ this.filterText = event.target.value.trim().toLowerCase();
118
+ this._renderActive();
119
+ });
120
+ this.tabSubs.addEventListener('click', () => this._selectTab('subscribers'));
121
+ this.tabRegistry.addEventListener('click', () => this._selectTab('registry'));
122
+ this.tabHistory.addEventListener('click', () => this._selectTab('history'));
123
+ }
124
+
125
+ _selectTab(tab) {
126
+ this.activeTab = tab;
127
+ this.tabSubs.classList.toggle('active', tab === 'subscribers');
128
+ this.tabRegistry.classList.toggle('active', tab === 'registry');
129
+ this.tabHistory.classList.toggle('active', tab === 'history');
130
+ const placeholders = { subscribers: 'filter events…', registry: 'filter registry…', history: 'filter history…' };
131
+ this.filterInput.placeholder = placeholders[tab] || 'filter…';
132
+ this._renderActive();
133
+ }
134
+
135
+ _renderActive() {
136
+ if (this.activeTab === 'history') this.renderHistory();
137
+ else if (this.activeTab === 'registry') this.renderRegistry();
138
+ else this.renderList();
139
+ }
140
+
141
+ makeDraggable() {
142
+ if (!this.header || !this.container) return;
143
+
144
+ let offset = { x: 0, y: 0 };
145
+ let isDragging = false;
146
+
147
+ this.header.style.cursor = 'grab';
148
+
149
+ this.header.addEventListener('mousedown', (event) => {
150
+ if (event.target.closest('.btn')) return;
151
+ isDragging = true;
152
+ offset.x = event.clientX - this.container.getBoundingClientRect().left;
153
+ offset.y = event.clientY - this.container.getBoundingClientRect().top;
154
+ this.header.style.cursor = 'grabbing';
155
+ });
156
+
157
+ document.addEventListener('mousemove', (event) => {
158
+ if (!isDragging) return;
159
+ const rect = this.container.getBoundingClientRect();
160
+ const maxX = window.innerWidth - rect.width;
161
+ const maxY = window.innerHeight - rect.height;
162
+ const x = Math.min(Math.max(event.clientX - offset.x, 0), maxX);
163
+ const y = Math.min(Math.max(event.clientY - offset.y, 0), maxY);
164
+ this.container.style.left = `${x}px`;
165
+ this.container.style.top = `${y}px`;
166
+ this.container.style.right = 'auto';
167
+ this.container.style.bottom = 'auto';
168
+ });
169
+
170
+ document.addEventListener('mouseup', () => {
171
+ if (!isDragging) return;
172
+ isDragging = false;
173
+ this.header.style.cursor = 'grab';
174
+ });
175
+ }
176
+
177
+ renderList() {
178
+ if (!slice?.events?.subscriptions) {
179
+ this.list.textContent = 'EventManager not available.';
180
+ this.countLabel.textContent = '0';
181
+ return;
182
+ }
183
+
184
+ const items = [];
185
+ slice.events.subscriptions.forEach((subs, eventName) => {
186
+ const emitCount = slice.events.emitCounts?.get(eventName) || 0;
187
+ const entries = Array.from(subs.entries()).map(([id, sub]) => {
188
+ const componentSliceId = sub.componentSliceId || null;
189
+ const component = componentSliceId ? slice.controller.getComponent(componentSliceId) : null;
190
+ const componentName = component?.constructor?.name || null;
191
+ return {
192
+ id,
193
+ once: sub.once,
194
+ componentSliceId,
195
+ componentName
196
+ };
197
+ });
198
+
199
+ if (this.filterText && !eventName.toLowerCase().includes(this.filterText)) {
200
+ return;
201
+ }
202
+
203
+ items.push({ eventName, count: subs.size, emitCount, entries, emitters: this._emittersFor(eventName) });
204
+ });
205
+
206
+ items.sort((a, b) => a.eventName.localeCompare(b.eventName));
207
+
208
+ this.countLabel.textContent = String(items.length);
209
+ this.list.innerHTML = items.length
210
+ ? items.map((item) => {
211
+ const details = item.entries.map((entry) => {
212
+ const label = entry.componentName
213
+ ? `${entry.componentName} (${entry.componentSliceId})`
214
+ : entry.componentSliceId || 'Global';
215
+ const onceBadge = entry.once ? '<span class="badge">once</span>' : '';
216
+ return `
217
+ <div class="subscriber-row">
218
+ <div class="subscriber-name">${this.escapeHtml(label)}</div>
219
+ <div class="subscriber-meta">${entry.id}${onceBadge}</div>
220
+ </div>
221
+ `;
222
+ }).join('');
223
+
224
+ const emittersHtml = item.emitters.length
225
+ ? item.emitters.map((emitter) => `
226
+ <div class="subscriber-row emitter-row">
227
+ <div class="subscriber-name">${this.escapeHtml(this._emitterLabel(emitter))}</div>
228
+ <div class="subscriber-meta">×${emitter.count}</div>
229
+ </div>
230
+ `).join('')
231
+ : '<div class="empty">No emitters recorded</div>';
232
+
233
+ return `
234
+ <details class="event-row">
235
+ <summary>
236
+ <div class="event-name">${this.escapeHtml(item.eventName)}</div>
237
+ <div class="event-metrics">
238
+ <span class="emit-count" title="Emits this session">⚡${item.emitCount}</span>
239
+ <span class="event-count">${item.count}</span>
240
+ </div>
241
+ </summary>
242
+ <div class="trace-section">
243
+ <div class="trace-label">emitted by</div>
244
+ ${emittersHtml}
245
+ </div>
246
+ <div class="trace-section">
247
+ <div class="trace-label">listeners</div>
248
+ ${details || '<div class="empty">No subscribers</div>'}
249
+ </div>
250
+ </details>
251
+ `;
252
+ }).join('')
253
+ : '<div class="empty">No events</div>';
254
+ }
255
+
256
+ /**
257
+ * Emitters recorded for an event (only while the debugger is open).
258
+ * @param {string} eventName
259
+ * @returns {Array<{ sliceId?: string, name?: string, source?: string, count: number }>}
260
+ */
261
+ _emittersFor(eventName) {
262
+ const map = slice?.events?.emitters;
263
+ if (!(map instanceof Map) || !map.has(eventName)) return [];
264
+ return Array.from(map.get(eventName).values()).sort((a, b) => b.count - a.count);
265
+ }
266
+
267
+ /**
268
+ * Human-readable label for an emitter (component name + sliceId, or source location).
269
+ */
270
+ _emitterLabel(emitter) {
271
+ if (!emitter) return 'unknown';
272
+ if (emitter.sliceId) return `${emitter.name || 'Component'} (${emitter.sliceId})`;
273
+ if (emitter.source) return emitter.source;
274
+ return 'unknown';
275
+ }
276
+
277
+ /**
278
+ * Render the declared event catalog (registry) merged with the live state:
279
+ * payload shape, current subscriber count, emit count, plus drift badges for
280
+ * declared-but-unused and used-but-undeclared events.
281
+ */
282
+ renderRegistry() {
283
+ if (!slice?.events) {
284
+ this.list.textContent = 'EventManager not available.';
285
+ this.countLabel.textContent = '0';
286
+ return;
287
+ }
288
+
289
+ const registry = slice.events.registry instanceof Map ? slice.events.registry : new Map();
290
+ const undeclared = slice.events.undeclared instanceof Set ? slice.events.undeclared : new Set();
291
+
292
+ if (registry.size === 0 && undeclared.size === 0) {
293
+ this.countLabel.textContent = '0';
294
+ this.list.innerHTML =
295
+ '<div class="empty">No events registered.<br/>Declare them with <code>slice.events.register({ ... })</code>.</div>';
296
+ return;
297
+ }
298
+
299
+ const matchesFilter = (name) => !this.filterText || name.toLowerCase().includes(this.filterText);
300
+
301
+ // Bucket declared events by namespace (segment before the first ':').
302
+ const groups = new Map();
303
+ registry.forEach((definition, eventName) => {
304
+ if (!matchesFilter(eventName)) return;
305
+ const ns = slice.events.namespaceOf?.(eventName) || '(no namespace)';
306
+ if (!groups.has(ns)) groups.set(ns, []);
307
+ groups.get(ns).push({
308
+ eventName,
309
+ description: definition?.description || '',
310
+ payload: definition?.payload ?? null,
311
+ subscriberCount: slice.events.subscriberCount?.(eventName) || 0,
312
+ emitCount: slice.events.emitCounts?.get(eventName) || 0
313
+ });
314
+ });
315
+
316
+ const sortedNamespaces = Array.from(groups.keys()).sort((a, b) => a.localeCompare(b));
317
+ const declaredCount = Array.from(groups.values()).reduce((sum, list) => sum + list.length, 0);
318
+ const drift = Array.from(undeclared).filter(matchesFilter).sort();
319
+
320
+ this.countLabel.textContent = String(declaredCount + drift.length);
321
+
322
+ const siteRows = (sites) => sites.map((s) => {
323
+ const label = s.component ? `${s.component} — ${s.file}:${s.line}` : `${s.file}:${s.line}`;
324
+ return `<div class="graph-site">${this.escapeHtml(label)}</div>`;
325
+ }).join('');
326
+
327
+ const eventRowHtml = (item) => {
328
+ const payloadStr = this.formatPayload(item.payload);
329
+ const idle = item.subscriberCount === 0
330
+ ? '<span class="badge badge-idle" title="No active listeners">idle</span>'
331
+ : '';
332
+ const desc = item.description
333
+ ? `<div class="registry-desc">${this.escapeHtml(item.description)}</div>`
334
+ : '';
335
+
336
+ // Static documentation layer (from the generated manifest).
337
+ const emitters = slice.events.staticEmittersOf?.(item.eventName) || [];
338
+ const listeners = slice.events.staticListenersOf?.(item.eventName) || [];
339
+ const graphHtml = (emitters.length || listeners.length)
340
+ ? `
341
+ <div class="graph-block">
342
+ <div class="graph-label">emitters <span class="graph-tag">static</span></div>
343
+ ${emitters.length ? siteRows(emitters) : '<div class="empty">none found in code</div>'}
344
+ <div class="graph-label">listeners <span class="graph-tag">static</span></div>
345
+ ${listeners.length ? siteRows(listeners) : '<div class="empty">none found in code</div>'}
346
+ </div>`
347
+ : '';
348
+
349
+ return `
350
+ <details class="event-row">
351
+ <summary>
352
+ <div class="event-name">${this.escapeHtml(item.eventName)}</div>
353
+ <div class="event-metrics">
354
+ ${idle}
355
+ <span class="emit-count" title="Emits this session">⚡${item.emitCount}</span>
356
+ <span class="event-count" title="Active listeners">${item.subscriberCount}</span>
357
+ </div>
358
+ </summary>
359
+ <div class="subscriber-list">
360
+ ${desc}
361
+ <div class="registry-payload"><span class="payload-key">payload</span> ${this.escapeHtml(payloadStr)}</div>
362
+ ${graphHtml}
363
+ </div>
364
+ </details>
365
+ `;
366
+ };
367
+
368
+ const declaredHtml = sortedNamespaces.map((ns) => {
369
+ const list = groups.get(ns).sort((a, b) => a.eventName.localeCompare(b.eventName));
370
+ const rows = list.map(eventRowHtml).join('');
371
+ return `
372
+ <details class="ns-group" open>
373
+ <summary class="ns-summary">
374
+ <span class="ns-name">${this.escapeHtml(ns)}</span>
375
+ <span class="ns-count">${list.length}</span>
376
+ </summary>
377
+ <div class="ns-events">${rows}</div>
378
+ </details>
379
+ `;
380
+ }).join('');
381
+
382
+ const driftHtml = drift.length
383
+ ? `<div class="registry-drift-title">Used but not declared (${drift.length})</div>` +
384
+ drift.map((name) => `
385
+ <div class="history-row drift-row">
386
+ <div class="history-event">${this.escapeHtml(name)}</div>
387
+ <div class="history-time"><span class="badge badge-drift">undeclared</span></div>
388
+ </div>
389
+ `).join('')
390
+ : '';
391
+
392
+ // Footer: dynamic emit/subscribe sites (computed names the static scan can't resolve).
393
+ const dynamic = slice.events.graph?.dynamic;
394
+ const dynamicCount = (dynamic?.emitters?.length || 0) + (dynamic?.listeners?.length || 0);
395
+ const dynamicHtml = dynamicCount
396
+ ? `<div class="registry-drift-title">Dynamic sites (computed names, ${dynamicCount}) — not statically resolvable</div>`
397
+ : '';
398
+
399
+ this.list.innerHTML =
400
+ (declaredHtml || (drift.length ? '' : '<div class="empty">No matching events</div>')) + driftHtml + dynamicHtml;
401
+ }
402
+
403
+ /**
404
+ * Render a payload mini-schema for display.
405
+ * @param {object|null} payload
406
+ * @returns {string}
407
+ */
408
+ formatPayload(payload) {
409
+ if (payload === null || payload === undefined) return 'void';
410
+ if (typeof payload === 'string') return payload;
411
+ if (typeof payload === 'object') {
412
+ const parts = Object.entries(payload).map(([key, value]) => {
413
+ const type = typeof value === 'string'
414
+ ? value
415
+ : (value && typeof value === 'object' && value.type) || 'any';
416
+ return `${key}: ${type}`;
417
+ });
418
+ return `{ ${parts.join(', ')} }`;
419
+ }
420
+ return String(payload);
421
+ }
422
+
423
+ renderHistory() {
424
+ if (!slice?.events?.emitHistory) {
425
+ this.list.textContent = 'EventManager not available.';
426
+ this.countLabel.textContent = '0';
427
+ return;
428
+ }
429
+
430
+ const history = slice.events.emitHistory;
431
+ const filtered = this.filterText
432
+ ? history.filter(e => e.eventName.toLowerCase().includes(this.filterText))
433
+ : history;
434
+
435
+ this.countLabel.textContent = String(filtered.length);
436
+ if (!filtered.length) {
437
+ this.list.innerHTML = '<div class="empty">No emits recorded yet</div>';
438
+ return;
439
+ }
440
+
441
+ const now = Date.now();
442
+ this.list.innerHTML = [...filtered].reverse().map((entry) => {
443
+ const diff = now - entry.timestamp;
444
+ const timeStr = diff < 1000 ? 'now'
445
+ : diff < 60000 ? `${Math.floor(diff / 1000)}s ago`
446
+ : `${Math.floor(diff / 60000)}m ago`;
447
+ const from = entry.emitter
448
+ ? `<div class="history-from" title="Emitter">↑ ${this.escapeHtml(this._emitterLabel(entry.emitter))}</div>`
449
+ : '';
450
+ return `
451
+ <div class="history-row">
452
+ <div class="history-main">
453
+ <div class="history-event">${this.escapeHtml(entry.eventName)}</div>
454
+ ${from}
455
+ </div>
456
+ <div class="history-time">${timeStr}</div>
457
+ </div>
458
+ `;
459
+ }).join('');
460
+ }
461
+
462
+ escapeHtml(value) {
463
+ return String(value)
464
+ .replace(/&/g, '&amp;')
465
+ .replace(/</g, '&lt;')
466
+ .replace(/>/g, '&gt;')
467
+ .replace(/"/g, '&quot;');
468
+ }
469
+
470
+ renderTemplate() {
471
+ return `
472
+ <div id="events-debugger">
473
+ <div class="events-header">
474
+ <div class="brand">
475
+ <span class="status-dot"></span>
476
+ <span class="glyph">◇</span>
477
+ <span class="title">EVENTS</span>
478
+ </div>
479
+ <div class="actions">
480
+ <button id="events-refresh" class="btn" title="Refresh" aria-label="Refresh">⟳</button>
481
+ <button id="events-close" class="btn" title="Close" aria-label="Close">✕</button>
482
+ </div>
483
+ </div>
484
+ <div class="events-tabs">
485
+ <button id="tab-subscribers" class="tab-btn active">Subscribers</button>
486
+ <button id="tab-registry" class="tab-btn">Registry</button>
487
+ <button id="tab-history" class="tab-btn">History</button>
488
+ </div>
489
+ <div class="events-toolbar">
490
+ <input id="events-filter" type="text" placeholder="filter events…" autocomplete="off" spellcheck="false" />
491
+ <div class="count"><span id="events-count">0</span></div>
492
+ </div>
493
+ <div class="events-list" id="events-list"></div>
494
+ </div>
495
+ `;
496
+ }
497
+
498
+ renderStyles() {
499
+ return `
500
+ /* Slice Instruments — events console. All selectors scoped to the
501
+ <slice-eventmanager-debugger> tag so nothing clashes with app styles.
502
+ Every --si-* token reads the matching framework theme variable from
503
+ :root, falling back to the original hardcoded value if absent. */
504
+ slice-eventmanager-debugger {
505
+ --si-accent: var(--primary-color, #6ee7ff);
506
+ --si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
507
+ --si-surface: var(--primary-background-color, rgba(17, 19, 28, 0.86));
508
+ --si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
509
+ --si-raised-2: var(--tertiary-background-color, rgba(255, 255, 255, 0.06));
510
+ --si-border: var(--medium-color, rgba(255, 255, 255, 0.09));
511
+ --si-text: var(--font-primary-color, #e8eaf2);
512
+ --si-dim: var(--font-secondary-color, #888fa6);
513
+ --si-danger: var(--danger-color, #ff6b6b);
514
+ --si-success: var(--success-color, #46d39a);
515
+ --si-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace;
516
+ }
517
+
518
+ slice-eventmanager-debugger #events-debugger {
519
+ position: fixed;
520
+ bottom: 20px;
521
+ right: 20px;
522
+ width: min(380px, calc(100vw - 40px));
523
+ max-height: 64vh;
524
+ background: var(--si-surface);
525
+ border: 1px solid var(--si-border);
526
+ border-radius: 14px;
527
+ box-shadow:
528
+ 0 24px 60px -12px rgba(0, 0, 0, 0.55),
529
+ 0 0 0 1px rgba(0, 0, 0, 0.2),
530
+ 0 0 38px -18px rgba(var(--si-accent-rgb), 0.55);
531
+ -webkit-backdrop-filter: blur(22px) saturate(1.3);
532
+ backdrop-filter: blur(22px) saturate(1.3);
533
+ display: none;
534
+ flex-direction: column;
535
+ z-index: 10001;
536
+ overflow: hidden;
537
+ color: var(--si-text);
538
+ font-family: var(--si-mono);
539
+ }
540
+
541
+ slice-eventmanager-debugger #events-debugger.active {
542
+ display: flex;
543
+ animation: si-events-in 0.26s cubic-bezier(0.16, 1, 0.3, 1);
544
+ }
545
+
546
+ @keyframes si-events-in {
547
+ from { opacity: 0; transform: translateY(10px) scale(0.985); }
548
+ to { opacity: 1; transform: translateY(0) scale(1); }
549
+ }
550
+
551
+ slice-eventmanager-debugger #events-debugger * { box-sizing: border-box; }
552
+
553
+ slice-eventmanager-debugger #events-debugger::before {
554
+ content: '';
555
+ position: absolute;
556
+ left: 0; top: 0; bottom: 0;
557
+ width: 2px;
558
+ background: linear-gradient(180deg, var(--si-accent), transparent 70%);
559
+ opacity: 0.85;
560
+ pointer-events: none;
561
+ }
562
+
563
+ slice-eventmanager-debugger .events-header {
564
+ display: flex;
565
+ justify-content: space-between;
566
+ align-items: center;
567
+ padding: 12px 14px;
568
+ background:
569
+ radial-gradient(120% 140% at 0% 0%, rgba(var(--si-accent-rgb), 0.10), transparent 60%),
570
+ var(--si-raised);
571
+ border-bottom: 1px solid var(--si-border);
572
+ user-select: none;
573
+ }
574
+
575
+ slice-eventmanager-debugger .brand { display: flex; align-items: center; gap: 9px; }
576
+
577
+ slice-eventmanager-debugger .status-dot {
578
+ width: 7px; height: 7px;
579
+ border-radius: 50%;
580
+ background: var(--si-accent);
581
+ animation: si-pulse-ev 2.4s ease-out infinite;
582
+ }
583
+
584
+ @keyframes si-pulse-ev {
585
+ 0% { box-shadow: 0 0 0 0 rgba(var(--si-accent-rgb), 0.55); }
586
+ 70% { box-shadow: 0 0 0 7px rgba(var(--si-accent-rgb), 0); }
587
+ 100% { box-shadow: 0 0 0 0 rgba(var(--si-accent-rgb), 0); }
588
+ }
589
+
590
+ slice-eventmanager-debugger .glyph { color: var(--si-accent); font-size: 12px; opacity: 0.9; }
591
+
592
+ slice-eventmanager-debugger .title {
593
+ font-weight: 600;
594
+ font-size: 11px;
595
+ letter-spacing: 0.18em;
596
+ color: var(--si-text);
597
+ }
598
+
599
+ slice-eventmanager-debugger .actions { display: flex; gap: 6px; }
600
+
601
+ slice-eventmanager-debugger .btn {
602
+ width: 26px; height: 26px;
603
+ display: flex; align-items: center; justify-content: center;
604
+ border-radius: 7px;
605
+ border: 1px solid var(--si-border);
606
+ background: var(--si-raised);
607
+ color: var(--si-dim);
608
+ cursor: pointer;
609
+ font-size: 13px;
610
+ line-height: 1;
611
+ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
612
+ }
613
+ slice-eventmanager-debugger .btn:hover {
614
+ color: var(--si-text);
615
+ background: var(--si-raised-2);
616
+ border-color: rgba(var(--si-accent-rgb), 0.5);
617
+ }
618
+ slice-eventmanager-debugger .btn:active { transform: scale(0.92); }
619
+ slice-eventmanager-debugger #events-refresh:hover { color: var(--si-accent); }
620
+
621
+ slice-eventmanager-debugger .events-tabs {
622
+ display: flex;
623
+ gap: 0;
624
+ padding: 0 12px;
625
+ border-bottom: 1px solid var(--si-border);
626
+ background: var(--si-raised);
627
+ }
628
+ slice-eventmanager-debugger .tab-btn {
629
+ all: unset;
630
+ cursor: pointer;
631
+ font-size: 10px;
632
+ font-weight: 600;
633
+ letter-spacing: 0.06em;
634
+ text-transform: uppercase;
635
+ padding: 7px 12px;
636
+ color: var(--si-dim);
637
+ border-bottom: 2px solid transparent;
638
+ transition: color 0.15s ease, border-color 0.15s ease;
639
+ }
640
+ slice-eventmanager-debugger .tab-btn:hover { color: var(--si-text); }
641
+ slice-eventmanager-debugger .tab-btn.active {
642
+ color: var(--si-accent);
643
+ border-bottom-color: var(--si-accent);
644
+ }
645
+
646
+ slice-eventmanager-debugger .events-toolbar {
647
+ display: flex;
648
+ gap: 10px;
649
+ align-items: center;
650
+ padding: 10px 12px;
651
+ border-bottom: 1px solid var(--si-border);
652
+ }
653
+
654
+ slice-eventmanager-debugger .events-toolbar input {
655
+ flex: 1;
656
+ min-width: 0;
657
+ padding: 7px 10px 7px 30px;
658
+ border-radius: 8px;
659
+ border: 1px solid var(--si-border);
660
+ background:
661
+ var(--si-raised) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%23888fa6' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat 10px center;
662
+ color: var(--si-text);
663
+ font-family: var(--si-mono);
664
+ font-size: 12px;
665
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
666
+ }
667
+ slice-eventmanager-debugger .events-toolbar input::placeholder { color: var(--si-dim); }
668
+ slice-eventmanager-debugger .events-toolbar input:focus {
669
+ outline: none;
670
+ border-color: rgba(var(--si-accent-rgb), 0.6);
671
+ box-shadow: 0 0 0 3px rgba(var(--si-accent-rgb), 0.12);
672
+ }
673
+
674
+ slice-eventmanager-debugger .events-toolbar .count { font-size: 11px; color: var(--si-dim); min-width: 22px; text-align: center; }
675
+ slice-eventmanager-debugger .events-toolbar .count span { color: var(--si-accent); font-weight: 600; }
676
+
677
+ slice-eventmanager-debugger .events-list {
678
+ padding: 10px 12px 12px;
679
+ overflow: auto;
680
+ display: flex;
681
+ flex-direction: column;
682
+ gap: 7px;
683
+ }
684
+ slice-eventmanager-debugger .events-list::-webkit-scrollbar { width: 8px; }
685
+ slice-eventmanager-debugger .events-list::-webkit-scrollbar-thumb {
686
+ background: var(--si-raised-2);
687
+ border-radius: 8px;
688
+ border: 2px solid transparent;
689
+ background-clip: padding-box;
690
+ }
691
+ slice-eventmanager-debugger .events-list::-webkit-scrollbar-thumb:hover { background: rgba(var(--si-accent-rgb), 0.4); background-clip: padding-box; }
692
+
693
+ slice-eventmanager-debugger .event-row {
694
+ display: block;
695
+ padding: 9px 11px;
696
+ background: var(--si-raised);
697
+ border-radius: 9px;
698
+ border: 1px solid var(--si-border);
699
+ border-left: 2px solid transparent;
700
+ transition: border-color 0.18s ease, background 0.18s ease;
701
+ }
702
+ slice-eventmanager-debugger .event-row:hover { background: var(--si-raised-2); border-left-color: var(--si-accent); }
703
+
704
+ slice-eventmanager-debugger .event-row summary {
705
+ display: flex;
706
+ align-items: center;
707
+ justify-content: space-between;
708
+ gap: 10px;
709
+ cursor: pointer;
710
+ list-style: none;
711
+ }
712
+ slice-eventmanager-debugger .event-row summary::-webkit-details-marker { display: none; }
713
+ slice-eventmanager-debugger .event-row summary::after {
714
+ content: '›';
715
+ margin-left: auto;
716
+ color: var(--si-dim);
717
+ font-size: 15px;
718
+ line-height: 1;
719
+ transition: transform 0.2s ease, color 0.2s ease;
720
+ }
721
+ slice-eventmanager-debugger .event-row[open] summary::after { transform: rotate(90deg); color: var(--si-accent); }
722
+
723
+ slice-eventmanager-debugger .event-name {
724
+ font-family: var(--si-mono);
725
+ font-size: 12px;
726
+ color: var(--si-text);
727
+ overflow: hidden;
728
+ text-overflow: ellipsis;
729
+ white-space: nowrap;
730
+ }
731
+
732
+ slice-eventmanager-debugger .event-metrics {
733
+ display: flex;
734
+ align-items: center;
735
+ gap: 6px;
736
+ }
737
+ slice-eventmanager-debugger .emit-count {
738
+ font-weight: 600;
739
+ font-size: 10px;
740
+ color: var(--si-success);
741
+ background: rgba(70, 211, 154, 0.12);
742
+ border: 1px solid rgba(70, 211, 154, 0.25);
743
+ padding: 1px 6px;
744
+ border-radius: 999px;
745
+ white-space: nowrap;
746
+ }
747
+ slice-eventmanager-debugger .event-count {
748
+ font-weight: 600;
749
+ font-size: 11px;
750
+ color: var(--si-accent);
751
+ background: rgba(var(--si-accent-rgb), 0.12);
752
+ border: 1px solid rgba(var(--si-accent-rgb), 0.25);
753
+ padding: 1px 8px;
754
+ border-radius: 999px;
755
+ min-width: 22px;
756
+ text-align: center;
757
+ }
758
+ slice-eventmanager-debugger .history-row {
759
+ display: flex;
760
+ justify-content: space-between;
761
+ align-items: center;
762
+ gap: 10px;
763
+ padding: 6px 11px;
764
+ background: var(--si-raised);
765
+ border-radius: 7px;
766
+ border: 1px solid var(--si-border);
767
+ border-left: 2px solid transparent;
768
+ transition: border-color 0.15s ease;
769
+ }
770
+ slice-eventmanager-debugger .history-row:hover { border-left-color: var(--si-accent); }
771
+ slice-eventmanager-debugger .history-event {
772
+ font-family: var(--si-mono);
773
+ font-size: 11.5px;
774
+ color: var(--si-text);
775
+ overflow: hidden;
776
+ text-overflow: ellipsis;
777
+ white-space: nowrap;
778
+ }
779
+ slice-eventmanager-debugger .history-time {
780
+ font-size: 10px;
781
+ color: var(--si-dim);
782
+ white-space: nowrap;
783
+ flex-shrink: 0;
784
+ }
785
+
786
+ slice-eventmanager-debugger .subscriber-list {
787
+ margin-top: 9px;
788
+ padding-top: 9px;
789
+ border-top: 1px dashed var(--si-border);
790
+ display: flex;
791
+ flex-direction: column;
792
+ gap: 6px;
793
+ }
794
+
795
+ slice-eventmanager-debugger .subscriber-row {
796
+ display: flex;
797
+ justify-content: space-between;
798
+ align-items: center;
799
+ gap: 10px;
800
+ padding: 6px 9px;
801
+ border-radius: 7px;
802
+ background: rgba(0, 0, 0, 0.22);
803
+ border: 1px solid var(--si-border);
804
+ }
805
+
806
+ slice-eventmanager-debugger .subscriber-name {
807
+ font-size: 11.5px;
808
+ color: var(--si-text);
809
+ overflow: hidden;
810
+ text-overflow: ellipsis;
811
+ white-space: nowrap;
812
+ }
813
+
814
+ slice-eventmanager-debugger .subscriber-meta {
815
+ font-size: 10.5px;
816
+ color: var(--si-dim);
817
+ display: flex;
818
+ align-items: center;
819
+ gap: 6px;
820
+ white-space: nowrap;
821
+ }
822
+
823
+ slice-eventmanager-debugger .badge {
824
+ padding: 1px 6px;
825
+ border-radius: 999px;
826
+ background: rgba(var(--si-accent-rgb), 0.16);
827
+ color: var(--si-accent);
828
+ border: 1px solid rgba(var(--si-accent-rgb), 0.3);
829
+ font-size: 9px;
830
+ letter-spacing: 0.06em;
831
+ text-transform: uppercase;
832
+ }
833
+
834
+ slice-eventmanager-debugger .empty {
835
+ color: var(--si-dim);
836
+ font-size: 11px;
837
+ letter-spacing: 0.04em;
838
+ text-align: center;
839
+ padding: 22px 0;
840
+ }
841
+ slice-eventmanager-debugger .empty code {
842
+ font-family: var(--si-mono);
843
+ color: var(--si-accent);
844
+ background: rgba(var(--si-accent-rgb), 0.1);
845
+ padding: 1px 5px;
846
+ border-radius: 5px;
847
+ }
848
+
849
+ slice-eventmanager-debugger .registry-desc {
850
+ font-size: 11px;
851
+ color: var(--si-dim);
852
+ padding: 4px 2px 2px;
853
+ line-height: 1.4;
854
+ }
855
+ slice-eventmanager-debugger .registry-payload {
856
+ font-family: var(--si-mono);
857
+ font-size: 11px;
858
+ color: var(--si-text);
859
+ padding: 6px 9px;
860
+ border-radius: 7px;
861
+ background: rgba(0, 0, 0, 0.22);
862
+ border: 1px solid var(--si-border);
863
+ word-break: break-word;
864
+ }
865
+ slice-eventmanager-debugger .payload-key {
866
+ color: var(--si-dim);
867
+ margin-right: 6px;
868
+ }
869
+ slice-eventmanager-debugger .badge-idle {
870
+ background: rgba(136, 143, 166, 0.14);
871
+ color: var(--si-dim);
872
+ border: 1px solid var(--si-border);
873
+ }
874
+ slice-eventmanager-debugger .badge-drift {
875
+ background: rgba(255, 107, 107, 0.14);
876
+ color: var(--si-danger);
877
+ border: 1px solid rgba(255, 107, 107, 0.3);
878
+ }
879
+ slice-eventmanager-debugger .registry-drift-title {
880
+ margin-top: 4px;
881
+ font-size: 9.5px;
882
+ letter-spacing: 0.1em;
883
+ text-transform: uppercase;
884
+ color: var(--si-danger);
885
+ opacity: 0.85;
886
+ }
887
+ slice-eventmanager-debugger .drift-row { border-left-color: rgba(255, 107, 107, 0.4); }
888
+
889
+ slice-eventmanager-debugger .ns-group { display: block; }
890
+ slice-eventmanager-debugger .ns-group + .ns-group { margin-top: 4px; }
891
+ slice-eventmanager-debugger .ns-summary {
892
+ display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
893
+ padding: 4px 2px 6px; user-select: none;
894
+ }
895
+ slice-eventmanager-debugger .ns-summary::-webkit-details-marker { display: none; }
896
+ slice-eventmanager-debugger .ns-summary::before {
897
+ content: '▾'; color: var(--si-dim); font-size: 10px; transition: transform 0.2s ease;
898
+ }
899
+ slice-eventmanager-debugger .ns-group:not([open]) .ns-summary::before { transform: rotate(-90deg); }
900
+ slice-eventmanager-debugger .ns-name {
901
+ font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--si-accent);
902
+ }
903
+ slice-eventmanager-debugger .ns-count {
904
+ font-size: 9.5px; color: var(--si-dim);
905
+ background: var(--si-raised-2); border: 1px solid var(--si-border);
906
+ border-radius: 999px; padding: 0 6px;
907
+ }
908
+ slice-eventmanager-debugger .ns-events { display: flex; flex-direction: column; gap: 7px; padding-left: 4px; }
909
+
910
+ slice-eventmanager-debugger .trace-section {
911
+ margin-top: 9px;
912
+ padding-top: 9px;
913
+ border-top: 1px dashed var(--si-border);
914
+ display: flex;
915
+ flex-direction: column;
916
+ gap: 6px;
917
+ }
918
+ slice-eventmanager-debugger .trace-label {
919
+ font-size: 9px;
920
+ font-weight: 700;
921
+ letter-spacing: 0.1em;
922
+ text-transform: uppercase;
923
+ color: var(--si-dim);
924
+ }
925
+ slice-eventmanager-debugger .emitter-row { border-left: 2px solid var(--si-success); }
926
+ slice-eventmanager-debugger .emitter-row .subscriber-name { color: var(--si-text); }
927
+
928
+ slice-eventmanager-debugger .history-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
929
+ slice-eventmanager-debugger .history-from {
930
+ font-size: 10px;
931
+ color: var(--si-success);
932
+ overflow: hidden;
933
+ text-overflow: ellipsis;
934
+ white-space: nowrap;
935
+ }
936
+
937
+ slice-eventmanager-debugger .graph-block {
938
+ margin-top: 8px;
939
+ padding-top: 8px;
940
+ border-top: 1px dashed var(--si-border);
941
+ display: flex;
942
+ flex-direction: column;
943
+ gap: 4px;
944
+ }
945
+ slice-eventmanager-debugger .graph-label {
946
+ font-size: 9px;
947
+ font-weight: 700;
948
+ letter-spacing: 0.08em;
949
+ text-transform: uppercase;
950
+ color: var(--si-dim);
951
+ margin-top: 4px;
952
+ display: flex;
953
+ align-items: center;
954
+ gap: 6px;
955
+ }
956
+ slice-eventmanager-debugger .graph-tag {
957
+ font-size: 8px;
958
+ font-weight: 600;
959
+ letter-spacing: 0.04em;
960
+ color: var(--si-accent);
961
+ background: rgba(var(--si-accent-rgb), 0.12);
962
+ border: 1px solid rgba(var(--si-accent-rgb), 0.25);
963
+ border-radius: 999px;
964
+ padding: 0 5px;
965
+ }
966
+ slice-eventmanager-debugger .graph-site {
967
+ font-family: var(--si-mono);
968
+ font-size: 10.5px;
969
+ color: var(--si-text);
970
+ padding: 3px 8px;
971
+ border-radius: 6px;
972
+ background: rgba(0, 0, 0, 0.2);
973
+ border: 1px solid var(--si-border);
974
+ word-break: break-word;
975
+ }
976
+
977
+ @media (prefers-reduced-motion: reduce) {
978
+ slice-eventmanager-debugger #events-debugger.active { animation: none; }
979
+ slice-eventmanager-debugger .status-dot { animation: none; }
980
+ }
981
+ `;
982
+ }
983
+ }
984
+
985
+ customElements.define('slice-eventmanager-debugger', EventManagerDebugger);